{"_id":"@aurascope-analytics/forwarder-app-hook","_rev":"2-25ed3caa0fdc436bef5188772c4a2e6d","name":"@aurascope-analytics/forwarder-app-hook","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.0":{"name":"@aurascope-analytics/forwarder-app-hook","version":"0.1.0","license":"MIT","_id":"@aurascope-analytics/forwarder-app-hook@0.1.0","maintainers":[{"name":"naaccs","email":"nic@aurascope.co"}],"homepage":"https://a-analytics.xyz","dist":{"shasum":"67314d314f6c680c92d2d9fa47fb04878fd5cdbf","tarball":"https://registry.npmjs.org/@aurascope-analytics/forwarder-app-hook/-/forwarder-app-hook-0.1.0.tgz","fileCount":27,"integrity":"sha512-b1bWUBlG+Vq3KXynb+UhNEFWltRb4dmlktvmbWVMM32M4C0ScVTyeYrihT423mmZoQrTp5CJ3mS38dChtQuoSQ==","signatures":[{"sig":"MEUCIQDHsPsNqCDOs+QkMoAgGZaJFnSBPaZreVNXGhiSVUNAsgIgaLlidzj7M1NcQ0HLnnTwD1sdmsN9Y4QQmoXUtXVky9E=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":47952},"main":"./dist/index.js","type":"module","_from":"file:/home/runner/work/_temp/aurascope-analytics-forwarder-app-hook-0.1.0.tgz","types":"./dist/index.d.ts","engines":{"node":">=22"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./package.json":"./package.json"},"scripts":{"test":"bun test","build":"tsc","prepack":"npm run build","typecheck":"tsc --noEmit"},"_npmUser":{"name":"naaccs","email":"nic@aurascope.co"},"_resolved":"/home/runner/work/_temp/aurascope-analytics-forwarder-app-hook-0.1.0.tgz","_integrity":"sha512-b1bWUBlG+Vq3KXynb+UhNEFWltRb4dmlktvmbWVMM32M4C0ScVTyeYrihT423mmZoQrTp5CJ3mS38dChtQuoSQ==","_npmVersion":"11.18.0","description":"AuraScope Analytics app-hook forwarder — observes requests from inside a Node.js application and ships them off the request path","directories":{},"_nodeVersion":"22.14.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.8.0","@types/node":"^22.0.0"},"_npmOperationalInternal":{"tmp":"tmp/forwarder-app-hook_0.1.0_1785193553416_0.23610752926075662","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@aurascope-analytics/forwarder-app-hook","version":"0.2.0","description":"AuraScope Analytics app-hook forwarder — observes requests from inside a Node.js application and ships them off the request path","homepage":"https://a-analytics.xyz","license":"MIT","type":"module","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./package.json":"./package.json"},"engines":{"node":">=22"},"publishConfig":{"access":"public"},"scripts":{"build":"tsc","typecheck":"tsc --noEmit","test":"bun test","prepack":"npm run build"},"devDependencies":{"@types/node":"^22.0.0","typescript":"^5.8.0"},"_id":"@aurascope-analytics/forwarder-app-hook@0.2.0","_integrity":"sha512-kr1LR29qxqOf7i3vYdxxg6rjqdsACk/uzDQtj/VhawOnitqKuMYNjkFiUZ0I1qm/IK4FGqOlnenf96AJyyKNEA==","_resolved":"/home/runner/work/_temp/aurascope-analytics-forwarder-app-hook-0.2.0.tgz","_from":"file:/home/runner/work/_temp/aurascope-analytics-forwarder-app-hook-0.2.0.tgz","_nodeVersion":"22.14.0","_npmVersion":"11.19.0","dist":{"integrity":"sha512-kr1LR29qxqOf7i3vYdxxg6rjqdsACk/uzDQtj/VhawOnitqKuMYNjkFiUZ0I1qm/IK4FGqOlnenf96AJyyKNEA==","shasum":"ba96a2578f7e111f6211f425cf35397560af63d1","tarball":"https://registry.npmjs.org/@aurascope-analytics/forwarder-app-hook/-/forwarder-app-hook-0.2.0.tgz","fileCount":27,"unpackedSize":60219,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCwsIuadZcbWpPAzKwNzgAhp2/3Gr5HcUetppcnAG5u5QIhAMYkgbgFAvh2As3woImOeCRnBBohQx8Hhg3k2NtMVsgh"}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:7b8c34e2-8b36-4bc1-a4f5-30936aa90e5c"}},"directories":{},"maintainers":[{"name":"naaccs","email":"nic@aurascope.co"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/forwarder-app-hook_0.2.0_1786413560378_0.45432909032560276"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-27T23:05:53.292Z","modified":"2026-08-11T01:59:20.696Z","0.1.0":"2026-07-27T23:05:53.660Z","0.2.0":"2026-08-11T01:59:20.535Z"},"license":"MIT","homepage":"https://a-analytics.xyz","description":"AuraScope Analytics app-hook forwarder — observes requests from inside a Node.js application and ships them off the request path","maintainers":[{"name":"naaccs","email":"nic@aurascope.co"}],"readme":"# AuraScope app-hook forwarder\n\nThe app-hook forwarder is the replayable server-plane sender for request-lifecycle\nhooks in tenant applications. This package is a Node.js reference implementation for\nNext.js middleware and SvelteKit server hooks. Its core has no framework dependency;\nthe framework boundary is a small structural request type.\n\nThe request path is fail open. `observe()` captures evidence into memory and returns\nsynchronously. It performs no file-system access, network access, or asynchronous wait.\nThe flush loop forms durable immutable batches and ships them away from the response\npath.\n\n## Frozen wire contract\n\nEach batch is sent to `POST {AURASCOPE_INGEST_URL}/v1/logs/app-hook` as newline-delimited\nJSON (NDJSON), with at most 500 non-empty lines. It carries `Authorization: Bearer\n<forwarder secret>`, `Idempotency-Key: <persisted batch key>`, and `Content-Type:\napplication/x-ndjson`.\n\nEach line has exactly these fields:\n\n| Field | Type | App-hook meaning |\n| --- | --- | --- |\n| `v` | integer | Always `1` |\n| `ts` | string | Coordinated Universal Time observation timestamp in RFC 3339 form |\n| `site` | string | Tenant site public key (`pk_…`) |\n| `iid` | string | Random process instance identifier, captured once |\n| `n` | integer | Per-process monotonic sequence, starting at 1 |\n| `method` | string or null | Observed request method |\n| `path` | string or null | Full request target; query string is untouched |\n| `status` | integer or null | Always null because middleware is pre-response |\n| `ip_evidence` | string or null | Complete raw configured header value, not a selected hop |\n| `ua` | string or null | Raw user-agent, shipped byte-pristine (never mutated) |\n\nThere is no `event_id` or `forwarder` line field. Ingest derives `event_id`; the route\nsegment supplies the `app-hook` dialect. Ingest also resolves visitor Internet Protocol\n(IP) evidence and applies the server-plane query allowlist. `src/eventId.ts` exists only\nto reproduce ingest's committed golden vectors.\n\n## Configuration\n\n`loadConfigFromEnv()` reads:\n\n- `AURASCOPE_INGEST_URL` — required ingest base URL.\n- `AURASCOPE_SITE_KEY` — required `pk_…` public site key.\n- `AURASCOPE_FORWARDER_SK` — required `sk_…` forwarder secret. It has no default and\n  is never persisted or logged.\n- `AURASCOPE_QUEUE_DIR` — durable queue root. Defaults to\n  `.aurascope/app-hook-queue` under the process working directory.\n- `AURASCOPE_BUFFER_MAX_EVENTS` — optional explicit event-capacity override.\n- `AURASCOPE_WORST_CASE_RATE_PER_SEC` — sizing input; defaults to 10.\n- `AURASCOPE_TARGET_OUTAGE_SEC` — sizing input; defaults to 86,400 seconds.\n- `AURASCOPE_IP_EVIDENCE_HEADER` — raw evidence header; defaults to\n  `x-forwarded-for`.\n- `AURASCOPE_PROBE_MARKER` — optional synthetic-sender self-declaration (ADR 0020).\n  Sent as the `x-aurascope-probe-marker` batch envelope header on every shipped\n  batch — a versioned addition to the frozen v1 contract; event lines (including\n  `ua`) are never mutated by it.\n\nEvery refusal names the variable it is about. The transposed pair — a `pk_` in\n`AURASCOPE_FORWARDER_SK` and an `sk_` in `AURASCOPE_SITE_KEY` — is its own case and names\nboth variables and both prefixes in one message, because reporting only the first costs\nthe reader a second failed boot to discover the second.\n\nUnless explicitly overridden, buffer capacity is computed as:\n\n```text\nbufferMaxEvents = worstCaseRatePerSec × targetOutageSec\n```\n\nSize both inputs from observed tenant peak rate and the outage window the tenant disk\nmust retain. The default is a starting point, not a universal production measurement.\n\n## Next.js installation\n\nThe durable queue requires the Node.js runtime and writable persistent local storage.\nDo not deploy this implementation to an Edge runtime or ephemeral read-only file\nsystem.\n\nThis package publishes to the public npm registry as\n`@aurascope-analytics/forwarder-app-hook` (ADR 0032), so a tenant consumes it as an\nordinary dependency:\n\n```sh\nnpm install @aurascope-analytics/forwarder-app-hook\n```\n\nThe published tarball is `dist/` only — compiled JavaScript plus type declarations,\nbuilt by `prepack` on the way into every `npm pack`/`npm publish`. `npm run build`\n(`tsc`) produces the same output locally and `npm run typecheck` is the same compiler\nwithout emit; neither is a tenant step. The `wordpress/` tree beside this package\nships through a different channel entirely and is excluded from the tarball by the\n`files` whitelist.\n\nThe tenant-facing walkthrough is `docs/public/install/forwarder-app-hook.md`, and\n`scripts/check-app-hook-install.sh` executes the tenant's real sequence on every change\nto this forwarder — pack, install the tarball into a throwaway project, and import it by\nbare specifier on plain Node.\n\nConfigure the environment at runtime, then adapt the framework response:\n\n```ts\nimport type { NextRequest } from \"next/server\";\nimport { NextResponse } from \"next/server\";\nimport { createAuraScopeMiddleware } from \"@aurascope-analytics/forwarder-app-hook\";\n\nexport const config = { runtime: \"nodejs\" };\n\nconst auraScope = createAuraScopeMiddleware();\n\nexport function middleware(request: NextRequest) {\n  auraScope(request);\n  return NextResponse.next();\n}\n```\n\n`src/middleware.example.ts` is the full documentation-as-code example. It is excluded\nfrom this package's TypeScript build because `next` is intentionally not a dependency —\nthe tarball's dependency surface is a tenant-facing fact. It is still compiled: it is\ntype-checked against the real `next` types inside the throwaway project\n`scripts/check-app-hook-install.sh` builds, where framework packages can be installed\nwithout entering the tarball. The created middleware exposes `.forwarder` when\nconfiguration and queue startup succeed, allowing application shutdown code to call\n`await forwarder.stop()`.\n\nA missing or invalid required setting — or a queue directory that cannot be created —\nmakes the factory return a no-op middleware, and logs one line naming the reason before\nit does. Fail open, not fail silent: the refusal messages above are tenant-facing copy\nand this factory is the only path they can take, because both documented wirings call it\nwith no argument. Pass `{ logger }` as the second argument to send that line somewhere\nother than `console`.\n\nPer-REQUEST capture and middleware errors are swallowed, with no log, so AuraScope never\nchanges the tenant response and one malformed URL cannot flood their error output. The\ncounters carry those instead.\n\n## SvelteKit installation\n\nSvelteKit server hooks run during prerender as well as at request time. Guard creation\nwith `building` so the forwarder does not start its queue directory or shipper during a\nbuild. A Fetch API `Request` already satisfies the middleware's accepted shape directly,\nso no adapter object is needed:\n\n```ts\nimport { building } from \"$app/environment\";\nimport type { Handle } from \"@sveltejs/kit\";\nimport { createAuraScopeMiddleware } from \"@aurascope-analytics/forwarder-app-hook\";\n\nconst auraScope = building ? undefined : createAuraScopeMiddleware();\n\nexport const handle: Handle = async ({ event, resolve }) => {\n  auraScope?.(event.request);\n  return resolve(event);\n};\n```\n\n`src/hooks.server.example.ts` is the documentation-as-code version of this wiring. Like\nthe Next.js example, it is excluded from the package's TypeScript build because its\nframework imports are intentionally not dependencies of this package, and like it, the\npackaged-install gate type-checks it against the real `@sveltejs/kit` types.\n\n## Route matching is a measurement boundary\n\nA framework route matcher (Next.js `config.matcher`, SvelteKit route-level early\nreturns) narrows traffic one level above the forwarder: match every path, or accept\nthat unmatched paths are unobserved and their crawler history is never recoverable.\nRaw-first storage only covers traffic the forwarder saw.\n\n## Retention and retries\n\nStaging is cut into batches of no more than 500 lines. A formed batch's bytes and\nidempotency key are immutable and persisted together. A restart recovers pending files\nin ordinal order. `200` acknowledges and removes a batch, including duplicate rows;\n`401`, `429`, `503`, other server failures, timeouts, and network errors retain it.\nThe circuit breaker pauses, retains, and resumes, honoring `Retry-After` and otherwise\nusing capped exponential backoff.\n\nEach retained outcome moves exactly one counter, and the set covers the space: `http401`,\n`http429`, `http503`, `networkErrors` for a send that never got an answer, and\n`httpOther` for every remaining status the shipper retries — a `500` from ingest's own\ndependency failure, a `502` from a proxy in between, a `404` from a mistyped\n`AURASCOPE_INGEST_URL` — plus a `200` whose acknowledgement could not be parsed, which is\n`retry(200)` because an unreadable acknowledgement cannot release a durable batch. Before\n`httpOther` that whole class moved nothing at all, so a permanently wrong endpoint URL and\na healthy quiet site produced identical metrics.\n\nA `400` — or a batch this process finds locally malformed and refuses to send — retains\nthe batch too, and since #378 also opens the breaker and increments `badRequests`. The\nrefusal is permanent for those bytes, so the alternative shapes are both worse: dropping\nis the ack-and-move-on ADR 0018 forbids for a replayable sender, and the pre-#378 shape\nre-offered the same rejected batch on every flush tick forever with no counter moving.\nA wedged queue head is now a state (`breakerState: \"open\"`) and a number rather than an\ninference from a flat `accepted`.\n\nNothing is dropped merely because the breaker is open. At configured capacity\nexhaustion only, fail-open wins: the queue drops the oldest data and increments the\nlocal `dropped` metric. Persisted batches are immutable, so the implementation evicts\nan entire oldest batch; staged events are evicted one oldest event at a time. This\nkeeps the loss window contiguous and preserves the newest resume edge. No drop signal\nis added to the wire.\n\nThese behaviors implement [ADR 0018](../../docs/adr/0018-capacity-exhaustion-and-replay-horizon.md),\nthe durability acknowledgement rule in [ADR 0004](../../docs/adr/0004-ingest-ack-semantics.md),\nand the frozen server-plane contract in [ADR 0011](../../docs/adr/0011-server-plane-wire-contract.md).\n\nProbe traffic is declared on the BATCH ENVELOPE, as the `x-aurascope-probe-marker`\nheader, because the frozen app-hook dialect has no dedicated marker field and mutating\n`ua` would corrupt the evidence it carries. This is sender self-declaration, not\nwrite-time classification, and is an explicit freeze-task input under\n[ADR 0020](../../docs/adr/0020-counting-source-and-self-traffic.md).\n\n## WordPress seam\n\nThe later WordPress host uses this exact ten-field contract, endpoint, immutable batch\nidentity, durable replay, and circuit-breaker semantics. Its host-specific queue and\noff-response-path scheduling are outlined in `wordpress/README.md`.\n\n## Development\n\n```sh\nbunx tsc --noEmit\nbun test\n```\n\nRuntime code uses only Node.js standard-library modules and the Node 22 global `fetch`.\nDevelopment dependencies are limited to TypeScript and Node.js type declarations.\n","readmeFilename":"README.md"}