Agents, read CONTRIBUTING.md first.
AGENTS.md must not duplicate information. It should only provide agent-specific guidance and improve progressive disclosure.
Preserve the server-side trust boundary described in CONTRIBUTING.md. The
implementation is organized around these entry points:
netlify/edge-functions/audit-stream.ts:POST /api/audit-stream; runs and persists resumabletrust/manual/externalaudits.netlify/edge-functions/user-publishes-stream.ts: runs an unpersisted user-publish audit atPOST /api/user-publishes-stream.netlify/functions/reports.ts: reads saved reports and trust history and creates daily schedules. It does not compute or write reports.netlify/functions/trust-reruns-background.ts: hourly scheduler for due, all-packagetrustaudits.netlify/functions/audit-jobs-cleanup-background.ts: removes transientaudit_jobsrows older than two hours.
src/AppRouter.svelte owns the /, /report/:id, /orgs/:orgs, and /tracked
client routes. Shared snapshots render through SharedReport.svelte and reuse
components/ResultsView.svelte; /report/:id is immutable, while /orgs/:orgs
keeps its URL and resolves the latest all-package trust snapshot for the
normalized org set. TrackedOrgSets.svelte summarizes enabled daily schedules.
Runtime ownership is explicit in the source tree:
src/client/is browser-only.src/shared/contains contracts and utilities usable in the browser, Deno, and Node.src/audit/is the server-side audit graph shared by Deno edge functions and Node scheduled functions.
Anything in src/shared/ or src/audit/ must use APIs available in every
declared runtime: no node: builtins or browser-only globals. Use Web Crypto,
not node:crypto.
Cross-runtime Netlify helpers live in netlify/_shared/;
netlify/functions/_shared/ is reserved for Node-only function helpers.
Internal modules use the package.json#imports namespaces #client/*,
#shared/*, #audit/*, #server/*, #node/*, and #db/*. Prefer these over
cross-directory relative imports; the namespace makes runtime ownership visible.
Every edge-reachable alias must also have an exact .ts mapping in
import_map.json: Netlify's deploy-time Deno bundler does not resolve the
package.json wildcard mappings.
Netlify edge bundling has stricter rules than Vite or local development:
- Edge-reachable relative imports require explicit
.tsextensions. Node-only serverless files use.jsand must not enter an edge bundle. - Edge-reachable internal aliases,
valibot, andpackumetaare mapped inimport_map.json. Keep internal mappings synchronized with theirpackage.json#importstargets and pin third-party esm.sh builds. @netlify/databaseis Netlify-first-party and deliberately not mapped. Use its native tagged-SQL client; validate returned rows withdb/schema.ts.- Never put tests in
netlify/edge-functions/: Netlify treats every.tsor.jsfile there as an entry point. Put them innetlify/tests/edge-functions/.
Fetch npm directly through src/audit/npmClient.ts:
registry.npmjs.org: packuments and per-version manifests.api.npmjs.org: weekly downloads.npm.antfu.dev: use this fast caching proxy whenever possible.
npmGet retries network failures, 429s, and 5xx responses. It honors a valid
Retry-After; otherwise backoff is 1, 4, 9, and 16 seconds. Exhausted or
unexpected failures enter FailureLog and the UI warns that results may be
incomplete. A 404 is legitimately empty.
A failed per-version manifest fetch during a trust check fails the report. It must not silently classify an unknown package as having no trust signal.
- Trust classification delegates to
packumeta. Do not reimplement it. Preserve its semantics for provenance, trusted publishing, and staged publishing. - Recency, deprecation, and trust apply to the
latestdist-tag. Recency islatest.publishedAt, not the newest timestamp among all versions. - Discovery has no registry fallback. fast-npm-meta requests contain at most 100
names joined with
+; scoped package names retain the literal slash. - Trust checks fetch lightweight per-version manifests. Only
manualand user publish history fetch full packuments because they need version_npmUserdata. trustandmanualshare one discovery pass.manualuses every discovered package for all-package audits and only recency-filtered packages otherwise.externalenumerates the full org independently because dormant packages can still have active maintainers.
Unscoped downloads use the bulk endpoint in batches of 100. Scoped downloads
are sequential with a 500 ms delay; do not parallelize them. A present null
value means zero downloads. Only failed or absent fetches remain unknown (?).
auditDefaults.ts enforces MAX_ORGS (5) and BLOCKED_ORGS (types) at both
the UI and server trust boundary. These are cost and abuse limits.
The Netlify rate limits are part of the abuse boundary: audit-stream and user-publishes-stream
allow 30 requests/minute per IP; reports.ts allows 120. Resumable audits reconnect, so the audit
window must accommodate those extra requests.
Both audit endpoints return text/event-stream, one JSON payload per data:
line:
log: progress text with a monotonicid:.result: the complete report object.done: terminal success.audit-streamincludes the saved report id and URL; a save-only failure may return{ error }after the usable result.error: terminal audit failure.
audit-stream is resumable because Netlify recycles the client connection after
roughly 60 seconds. The first request for a client-generated jobId runs the
audit and records progress, result, and saved report id in audit_jobs; it keeps
running after its client disconnects. Requests with the same jobId and the
last seen from sequence only replay and tail the existing job. Do not replace
this with keepalives or rerun-on-reconnect.
The client implementation is split across:
sseStream.ts: frame reassembly andid:parsing.auditStream.ts: reconnect and resume.userPublishStream.ts: non-resumable stream consumption.
The stream uses fetch and a ReadableStream, not EventSource: requests are
POSTs, and external member lists can exceed URL limits. Request bodies are
validated server-side by AuditRequestSchema and UserPublishRequestSchema in
schemas.ts; validated data is the trust boundary.
Completed /api/audit-stream runs are saved server-side. Report ids are
<orgs>-<yyyy-mm-dd>-<shorthash>; hashing the payload makes same-day saves
idempotent. There is no public report-write endpoint.
Daily schedules are keyed by normalized org sets and can be created only from a
saved all-package trust report with a report_trust_history row. Scheduling is
idempotent: an existing enabled schedule is returned without resetting it. The
privacy and retention contract for saved reports, member input, and transient
jobs lives in CONTRIBUTING.md and must remain true when persistence changes.
Prefer native platform features over custom substitutes: popovers and dialogs,
Clipboard and Web Crypto APIs, matchMedia, semantic live regions,
:focus-visible, and URL-backed state. Use Baseline widely available features
by default and progressive enhancement for newer ones; do not add JS polyfills
for CSS anchor positioning.
Human-facing dates and times go through src/client/dateFormatting.ts and follow
the viewer's locale and timezone. Persistence, APIs, sorting, report ids,
exports, and <time datetime> retain ISO-8601 values. Date-only values are
calendar dates and must not shift across timezones. If visible text omits part
of a timestamp, expose the full localized value in a tooltip.
LogTerminal is a display-only, dependency-free progress log. Its visible log
is the screen-reader live region (role="log", aria-live="polite"). Do not
introduce a shell, terminal emulator, WASM runtime, or a CSP need for
'wasm-unsafe-eval'.
- For automated edits, do not run build, dev, or typecheck unless the user asks; use focused checks and report what was not run.
- Preserve unrelated user changes and existing staging boundaries.
- Comments should explain enduring constraints or non-obvious current behavior. Do not narrate previous implementations, intermediary states, or the edit that introduced the code.