{"_id":"@act-spec/runtime-fetch","name":"@act-spec/runtime-fetch","dist-tags":{"rc":"0.2.0-rc.1","latest":"0.2.0-rc.1"},"versions":{"0.2.0-rc.1":{"name":"@act-spec/runtime-fetch","version":"0.2.0-rc.1","description":"PRD-505 generic WHATWG-fetch handler for the ACT (Agent Content Tree) v0.1 reference implementation. Single-function adapter — `createActFetchHandler(opts) => (req: Request) => Promise<Response | null>` — portable across Cloudflare Workers, Deno Deploy, B","license":"Apache-2.0","author":{"name":"Jeremy Forsythe","email":"jeremy@act-spec.org"},"homepage":"https://act-spec.org","bugs":{"url":"https://github.com/act-spec/act/issues"},"type":"module","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"}},"publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/act-spec/act.git","directory":"packages/runtime-fetch"},"dependencies":{"@act-spec/core":"0.2.0-rc.1","@act-spec/runtime-core":"0.2.0-rc.1","@act-spec/validator":"0.2.0-rc.1"},"devDependencies":{"@vitest/coverage-v8":"^2.1.9","tsx":"^4.19.2","typescript":"^5.6.3","vitest":"^2.1.4"},"scripts":{"build":"tsc -p tsconfig.json","test":"vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage","lint":"eslint src","typecheck":"tsc -p tsconfig.json --noEmit","conformance":"tsx conformance.ts"},"_id":"@act-spec/runtime-fetch@0.2.0-rc.1","_integrity":"sha512-P+GLUWncKo+aKwd3FD9VqyDyxKV4glVDtBYLo3YjD9OapKI0JavMWQ3zHTsS1UFP05SkZQ5Qi0nVZKdwF/CygQ==","_resolved":"/private/var/folders/yp/y7frhcvj70jgr6b8g633lghr0000gn/T/d0a02ab1d9cdce69124126eeaf8a14ae/act-spec-runtime-fetch-0.2.0-rc.1.tgz","_from":"file:act-spec-runtime-fetch-0.2.0-rc.1.tgz","_nodeVersion":"22.19.0","_npmVersion":"11.6.0","dist":{"integrity":"sha512-P+GLUWncKo+aKwd3FD9VqyDyxKV4glVDtBYLo3YjD9OapKI0JavMWQ3zHTsS1UFP05SkZQ5Qi0nVZKdwF/CygQ==","shasum":"65df01eb336c12b099480a3450a1080cc46c0056","tarball":"https://registry.npmjs.org/@act-spec/runtime-fetch/-/runtime-fetch-0.2.0-rc.1.tgz","fileCount":45,"unpackedSize":168659,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIF8DWY2hWQNGQ5HwuvfgdGY1/5GS4mXYUKCZd/axgOmkAiBDA9ny5+aPlqr0qaW0p8kQ8r0LIrwm5AqVWL64NBLmZw=="}]},"_npmUser":{"name":"jdforsythe","email":"jdforsythe@gmail.com"},"directories":{},"maintainers":[{"name":"jdforsythe","email":"jdforsythe@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/runtime-fetch_0.2.0-rc.1_1777915331746_0.8262104222828142"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-04T17:22:11.647Z","0.2.0-rc.1":"2026-05-04T17:22:11.892Z","modified":"2026-05-04T17:22:12.132Z"},"maintainers":[{"name":"jdforsythe","email":"jdforsythe@gmail.com"}],"description":"PRD-505 generic WHATWG-fetch handler for the ACT (Agent Content Tree) v0.1 reference implementation. Single-function adapter — `createActFetchHandler(opts) => (req: Request) => Promise<Response | null>` — portable across Cloudflare Workers, Deno Deploy, B","homepage":"https://act-spec.org","repository":{"type":"git","url":"git+https://github.com/act-spec/act.git","directory":"packages/runtime-fetch"},"author":{"name":"Jeremy Forsythe","email":"jeremy@act-spec.org"},"bugs":{"url":"https://github.com/act-spec/act/issues"},"license":"Apache-2.0","readme":"# @act-spec/runtime-fetch\n\nGeneric WHATWG-fetch handler for [ACT (Agent Content Tree)](https://github.com/act-spec/act).\n\nThis package is the leanest possible leaf over [`@act-spec/runtime-core`](../runtime-core). The whole adapter is one closure exposing one function:\n\n```ts\n(request: Request) => Promise<Response | null>\n```\n\nBecause the input is already a WHATWG `Request` and the output is a WHATWG `Response`, this handler runs unchanged on every fetch-native runtime: Cloudflare Workers, Deno Deploy, Bun's `Bun.serve`, Vercel Edge Functions, Hono, Service Workers, and Node.js 20+ (where `Request` / `Response` are global). No Node-only API dependencies; no framework SDK.\n\n## Status\n\nACT v0.1 internal hand-test candidate. Public release lands at v0.2.\n\n## Install\n\nUnpublished in v0.1. Consume via the workspace:\n\n```jsonc\n// package.json\n{ \"dependencies\": { \"@act-spec/runtime-fetch\": \"workspace:*\" } }\n```\n\nThis package has zero peer dependencies. It depends only on `@act-spec/runtime-core` (which depends on `@act-spec/core` + `@act-spec/validator`); the dispatch pipeline, ETag computation, conditional GET, content negotiation, identity / tenant resolution, and discovery `Link` header all live in runtime-core.\n\n## Quick start (Cloudflare Worker)\n\n```ts\nimport { createActFetchHandler } from '@act-spec/runtime-fetch';\n\nconst actHandler = createActFetchHandler({\n  manifest: {\n    act_version: '0.1',\n    site: { name: 'Acme Workspace' },\n    delivery: 'runtime',\n    conformance: { level: 'core' },\n    auth: { schemes: ['bearer'] },\n    index_url: '/act/index.json',\n    node_url_template: '/act/n/{id}',\n  },\n  runtime: {\n    async resolveManifest(req, ctx) { /* ... */ },\n    async resolveIndex(req, ctx) { /* ... */ },\n    async resolveNode(req, ctx, { id }) { /* ... */ },\n  },\n  identityResolver: async (actReq) => {\n    const auth = actReq.headers.get('authorization');\n    if (!auth?.startsWith('Bearer ')) return { kind: 'auth_required', reason: 'missing' };\n    const claims = await verifyJwt(auth.slice(7));\n    return claims ? { kind: 'principal', key: claims.sub } : { kind: 'auth_required', reason: 'invalid' };\n  },\n});\n\nexport default {\n  fetch: async (req: Request): Promise<Response> => {\n    const actResponse = await actHandler(req);\n    if (actResponse) return actResponse;\n    // Fall through to other handlers (HTML pages, app API, etc.)\n    return new Response('Not Found', { status: 404 });\n  },\n};\n```\n\n## Routing\n\nThe manifest is the routing source of truth. The handler reads the manifest's URL templates at construction time and routes incoming requests against them:\n\n| Path | Endpoint | Level |\n|---|---|---|\n| `manifestPath` (default `/.well-known/act.json`) | manifest | Core |\n| `index_url` | index | Core |\n| `node_url_template` (with `{id}` capture) | node — IDs MAY contain `/` | Core |\n| `subtree_url_template` | subtree | Standard / Plus |\n| `index_ndjson_url` | NDJSON-streamed index | Plus |\n| `search_url_template` | search | Plus |\n\nThe `manifestPath` is the only URL not declared inside the manifest itself. Override via `options.manifestPath` for deployments where `/.well-known/` is reserved by another protocol or rewritten by the host.\n\n### Passthrough vs strict mode\n\n- **`passthrough`** (default) — non-matching requests resolve to `null`. The host chains its own router: `actHandler(req) ?? hostHandler(req)`. This is the common pattern in Workers / Deno / Hono / Bun.\n- **`strict`** — non-matching requests resolve to a 404 with the ACT error envelope (byte-identical to the in-band 404). Use when the deployment is ACT-only and the host wants no fall-through.\n\n```ts\nconst handler = createActFetchHandler({\n  /* ... */,\n  mode: 'strict', // 404 instead of null for non-ACT paths\n});\n```\n\n## Hybrid mounts (`basePath`)\n\nSet `basePath` to mount the handler under a sub-path; advertised URLs in the served manifest are prefixed automatically:\n\n```ts\nconst handler = createActFetchHandler({\n  manifest: { /* ... */ },\n  basePath: '/app',\n  runtime: { /* ... */ },\n  identityResolver: /* ... */,\n});\n\n// Effective served URLs:\n//   /app/.well-known/act.json\n//   /app/act/index.json\n//   /app/act/n/{id}\n```\n\nA parent manifest at the root `/.well-known/act.json` (typically served by a sibling static-export build) declares the mount.\n\n## Hono integration\n\n```ts\nimport { Hono } from 'hono';\nimport { createActFetchHandler } from '@act-spec/runtime-fetch';\n\nconst actHandler = createActFetchHandler({ /* ... */ });\n\nconst app = new Hono();\napp.use('*', async (c, next) => {\n  const r = await actHandler(c.req.raw);\n  if (r) return r;\n  await next();\n});\napp.get('/', (c) => c.html('<h1>Acme</h1>'));\n\nexport default app;\n```\n\nThe handler chains naturally — `null` falls through, a `Response` short-circuits.\n\n## Deno Deploy\n\n```ts\nimport { createActFetchHandler } from '@act-spec/runtime-fetch';\n\nconst actHandler = createActFetchHandler({\n  manifest: { /* ... */ },\n  runtime: { /* ... */ },\n  identityResolver: async (actReq) => {\n    const serviceId = actReq.headers.get('x-service-account');\n    if (!serviceId) return { kind: 'anonymous' }; // public access\n    const allowed = (Deno.env.get('ALLOWED_SERVICES') ?? '').split(',');\n    if (!allowed.includes(serviceId)) return { kind: 'auth_required', reason: 'invalid' };\n    return { kind: 'principal', key: serviceId };\n  },\n});\n\nDeno.serve(async (request) => {\n  const actResponse = await actHandler(request);\n  return actResponse ?? new Response('Not Found', { status: 404 });\n});\n```\n\n## Two-principal probe (cross-tenant non-disclosure)\n\nThis package wires the **mandatory** two-principal probe from [`@act-spec/runtime-core/test-utils`](../runtime-core/src/test-utils) against an in-process synthetic resolver. The probe verifies that:\n\n1. Each principal can resolve their own visible nodes (sanity baseline).\n2. Cross-tenant requests (principal A asks for principal B's node, and vice versa) return 404.\n3. The cross-tenant 404 is **byte-equivalent** to an absent-node 404 (status, body, every header — `Content-Type`, `Cache-Control`, `Link`).\n4. The discovery `Link` header is present and identical across both 404 paths (does not leak tenant identity in error cases).\n\nThe probe is in `src/probe.test.ts` and runs as part of `pnpm test`. It is a **CI-mandatory test**; do not skip and do not weaken.\n\n## NDJSON streaming portability\n\nThe handler streams NDJSON via the manual `new ReadableStream({ start(controller) { … } })` form. We deliberately do **not** use `ReadableStream.from(asyncIterable)` because it is not yet uniformly available across v0.1 target runtimes (Node.js < 22, some Bun versions). The manual form runs on every WHATWG-fetch host.\n\nReverse proxies in front of fetch-native deployments (nginx, Caddy) MUST be configured to disable buffering on `/act/index.ndjson` so clients see lines incrementally.\n\n## Web Crypto availability\n\nThe default ETag computer uses `crypto.subtle.digest('SHA-256', ...)` via `@act-spec/runtime-core`. All v0.1 target runtimes (Cloudflare Workers, Deno Deploy, Bun, Vercel Edge, Hono on Workers, Service Workers, Node.js 20+) provide `crypto.subtle`. Hosts deploying on older Node.js (< 18) must use `--experimental-global-webcrypto` or fall back to [`@act-spec/runtime-express`](../runtime-express).\n\n## Conformance\n\n`pnpm -F @act-spec/runtime-fetch conformance` runs `@act-spec/validator` in `validateSite` runtime-walk mode against an in-process Standard handler via a synthetic fetcher (no real HTTP server). Pass criterion: 0 gaps; achieved level == declared level.\n\n## License\n\nApache-2.0. See [LICENSE](../../LICENSE).\n","readmeFilename":"README.md","_rev":"1-b08ff5cc0e08bf2a9e6e807c4d3af01f"}