{"_id":"@bounded-sh/observe","_rev":"6-18c570a9026f709ee7ca7b0014c37f6f","name":"@bounded-sh/observe","dist-tags":{"latest":"0.1.2"},"versions":{"0.1.0":{"name":"@bounded-sh/observe","version":"0.1.0","license":"MIT","_id":"@bounded-sh/observe@0.1.0","maintainers":[{"name":"amitpoofdotnew","email":"amit@poof.new"}],"dist":{"shasum":"65bb40027748298085e319ed8f8a220fdc1c0b4c","tarball":"https://registry.npmjs.org/@bounded-sh/observe/-/observe-0.1.0.tgz","fileCount":38,"integrity":"sha512-KTegUZOdg5axnafYA7Pg5fV5zir3d2Vw6BRUAHmM1vuHfSFUoFPuK6VK5yXnJlDjHc3X7TJ4tnFab41V0TbmYw==","signatures":[{"sig":"MEUCIAQXE+vo4VPoSCLEjeZG+RiP4jYTLV+z8xNa6IGt+arBAiEAtYLndogycs1Ndc0wvn3f4DzxfJRp8ICnAuzaczdE3fA=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":207902},"main":"dist/index.js","types":"dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.js"},"./register":{"types":"./dist/register.d.ts","import":"./dist/register.js","require":"./dist/register.js"}},"gitHead":"7d023bca029ad4ff580f525da303bdc192c0da6c","private":false,"scripts":{"test":"vitest run","build":"tsc -p tsconfig.json","test:live":"node scripts/live-test.mjs","typecheck":"tsc -p tsconfig.json --noEmit"},"_npmUser":{"name":"amitpoofdotnew","email":"amit@poof.new"},"_npmVersion":"10.9.2","description":"Bounded observe shim for Node: intercepts fetch + http/https egress, attributes actors, reports metadata-only events to the Bounded observe ingest. Never breaks the app.","directories":{},"_nodeVersion":"22.14.0","dependencies":{},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^3.2.0","typescript":"^5.8.0","@types/node":"^22.0.0"},"_npmOperationalInternal":{"tmp":"tmp/observe_0.1.0_1783389946796_0.8801192779974596","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@bounded-sh/observe","version":"0.1.1","license":"MIT","_id":"@bounded-sh/observe@0.1.1","maintainers":[{"name":"amitpoofdotnew","email":"amit@poof.new"}],"dist":{"shasum":"95f91ca4795064a41116f7616600a75caa343401","tarball":"https://registry.npmjs.org/@bounded-sh/observe/-/observe-0.1.1.tgz","fileCount":38,"integrity":"sha512-oYsWuU0wZVAZlyJEL9/em6nnSSKx5OHOORczP9Si9s7cqUoxBYdMTLWg1Rg2crtmif6YLSxX+VAmFS9lwrXk2A==","signatures":[{"sig":"MEQCICpY4qGasoXKFKbHYSvlvqKaAJWsdT6l/KsJJdoAkWj4AiAS8kr8yx5LLnaFqI8gR8GcBOXP4IJBGH9pSPaomjikAQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":209798},"main":"dist/index.js","types":"dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.js"},"./register":{"types":"./dist/register.d.ts","import":"./dist/register.js","require":"./dist/register.js"}},"gitHead":"b14550d361f9f632ef417e982c829c7ca29f47b3","private":false,"scripts":{"test":"vitest run","build":"tsc -p tsconfig.json","test:live":"node scripts/live-test.mjs","typecheck":"tsc -p tsconfig.json --noEmit"},"_npmUser":{"name":"amitpoofdotnew","email":"amit@poof.new"},"_npmVersion":"10.9.2","description":"Bounded observe shim for Node: intercepts fetch + http/https egress, attributes actors, reports metadata-only events to the Bounded observe ingest. Never breaks the app.","directories":{},"_nodeVersion":"22.14.0","dependencies":{},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^3.2.0","typescript":"^5.8.0","@types/node":"^22.0.0"},"_npmOperationalInternal":{"tmp":"tmp/observe_0.1.1_1783421275307_0.8885885021456557","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@bounded-sh/observe","version":"0.1.2","license":"MIT","_id":"@bounded-sh/observe@0.1.2","maintainers":[{"name":"amitpoofdotnew","email":"amit@poof.new"},{"name":"bilalpoof","email":"bilal@poof.new"}],"dist":{"shasum":"81f3c459c5b2d4660a4aa3be29f29ab446b10ac9","tarball":"https://registry.npmjs.org/@bounded-sh/observe/-/observe-0.1.2.tgz","fileCount":40,"integrity":"sha512-BX6Pz2vd2iyws82KaC/u2ffsITMHrUw3XaJnhTNuJTI8yl5WYuXcxFs8z3fdjXPVVl+YPtyY0Eg4wxbQjq0h6Q==","signatures":[{"sig":"MEUCIGxFttb9uPWO0CEYiVQZmNVl61Q0Ak4TlhZgDXb5sZ7wAiEAxzaMoUkxEWOLN6qvgMCkraBLQcTBo7EWpWeMtBlLntQ=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":220246},"main":"dist/index.js","types":"dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.js"},"./edge":{"types":"./dist/edge.d.ts","import":"./dist/edge.js","require":"./dist/edge.js"},"./register":{"types":"./dist/register.d.ts","import":"./dist/register.js","require":"./dist/register.js"}},"gitHead":"a86212780107a908af298efffc3cff75ffa26cab","private":false,"scripts":{"test":"vitest run","build":"tsc -p tsconfig.json","test:live":"node scripts/live-test.mjs","typecheck":"tsc -p tsconfig.json --noEmit"},"_npmUser":{"name":"bilalpoof","email":"bilal@poof.new"},"_npmVersion":"10.9.8","description":"Bounded observe shim for Node: intercepts fetch + http/https egress, attributes actors, reports metadata-only events to the Bounded observe ingest. Never breaks the app.","directories":{},"_nodeVersion":"22.23.0","dependencies":{},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^3.2.0","typescript":"^5.8.0","@types/node":"^22.0.0"},"_npmOperationalInternal":{"tmp":"tmp/observe_0.1.2_1783539625251_0.7369323086325557","host":"s3://npm-registry-packages-npm-production"}}},"time":{"created":"2026-07-07T02:05:46.582Z","modified":"2026-07-15T17:29:58.722Z","0.1.0":"2026-07-07T02:05:46.942Z","0.1.1":"2026-07-07T10:47:55.427Z","0.1.2":"2026-07-08T19:40:25.440Z"},"license":"MIT","description":"Bounded observe shim for Node: intercepts fetch + http/https egress, attributes actors, reports metadata-only events to the Bounded observe ingest. Never breaks the app.","maintainers":[{"email":"amit@poof.new","name":"amitpoofdotnew"},{"email":"bilal@poof.new","name":"bilalpoof"},{"email":"prpatel05@gmail.com","name":"prpatel05"},{"email":"athar@poof.new","name":"athar-poof"}],"readme":"# @bounded-sh/observe — Bounded observe shim for Node\n\nCP1 origin for the observe/enforce project (`SPEC-OBSERVE-ENFORCE-CUSTODY.md`\n§3.1a). Intercepts your service's **egress** — `globalThis.fetch` (undici) and\n`http`/`https.request` — and reports metadata-only event batches to the\nBounded observe ingest. Dependency-free at runtime. **Never breaks the app**:\nevery shim code path is wrapped; original behavior is always preserved;\nobserve is always fail-open (Bounded down ⇒ nothing changes for your app, T9).\n\n## Install\n\n```bash\nnpm install @bounded-sh/observe\n```\n\n```js\nconst { init, middleware, runAs } = require(\"@bounded-sh/observe\");\n\ninit({\n  ingestBase: \"https://observe-ingest.buildwithtarobase.workers.dev\",\n  token: process.env.BOUNDED_SENSOR_TOKEN, // obs1.<keyId>.<sig>\n  org: \"your-org\",\n});\n```\n\nThat's the whole install: call `init()` once, as early as possible (before\nother modules grab references to `fetch`). ESM: `import { init } from\n\"@bounded-sh/observe\"`.\n\n## Actor context — who did it\n\nAttribution rides the **report only**; nothing is ever added to your outbound\nrequests (any `X-Bounded-*` header is actively **stripped from egress** and\nused purely as attribution fallback).\n\n```js\n// Per-request (Express or Hono): maps your session to an actor for the whole\n// request's async chain.\napp.use(middleware());                       // default: req.session.user / req.user\napp.use(middleware((req) => ({ actor: req.session.user.id, kind: \"human\" })));\n\n// Explicit scope (bots, jobs, agents):\nawait runAs({ actor: \"agent:support-1\", kind: \"agent\", onBehalfOf: customerId }, async () => {\n  await stripe.refunds.create({ charge, amount }); // reported as agent:support-1\n});\n```\n\nActor ids should be **opaque internal ids, never emails** (L9 — the ingest\nscrubber redacts email-shaped values as suspected PII). Unattributed calls are\nstill captured (they roll up under the `unattributed` pseudo-actor).\n\n## What's captured vs. NOT (PII posture)\n\nCaptured (metadata only, per event): destination host, **templated** path\n(UUIDs / numeric ids / hashes / vendor ids become `{id}` before anything\nleaves your process), method, status, duration, byte counts, actor context.\n\nManifest-recognized routes (built-in v0 registry: Stripe\nrefunds/charges/payment_intents; OpenAI + Anthropic spend) additionally\nrecognize **safe fields only**: amounts in cents, opaque ids (`ch_…`,\n`cus_…`), model names, token counts. As of **envelope v2** these ride the wire\nunder `rec.fields` (a bounded, one-level object of primitive SAFE values) —\n`rec{rail, action, registryVersion, fields}` — and drive deterministic action\nstories on the dashboard. They also remain available locally via the `onEvent`\nhook. Server-side, ingest re-checks `rec.fields` KEYS against the L2 denylist\nand scrubs the VALUES (L4), so a leaked email/PAN never lands raw.\n\nNOT captured, ever:\n- request/response **bodies** (unrecognized routes: top-level field\n  **names/types only**, on the first sighting of a shape);\n- query-string **values** (names only, on shape samples);\n- headers (only scanned to strip `X-Bounded-*`);\n- prompts/completions/messages of LLM calls (not in any safe-field list);\n- **PII-named fields — hard denylist (L2)**: `email`, `card*`, `*_name`,\n  `password`, `*token` (auth-shaped; token *counts* are fine), `address`,\n  `ssn`, `dob`, … are compiled into the shim and re-checked at extraction\n  time. Even a malicious/buggy manifest listing `customer_email` cannot\n  capture it, and even the *names* of PII-shaped fields are dropped from\n  shape samples.\n\nFail-safe: no/invalid manifest ⇒ metadata-only mode. Server side, ingest\nre-filters (L3) and value-scrubs (L4) independently — the shim never sends\n`org`/`sensor` (server-stamped from your sensor key), `scrubbed`, or any\nunknown field.\n\nHonest counts: if the shim ever drops events (bounded memory, backpressure),\nthe drop count is reported on the next successful event (`dropped`) and\nsurfaces downstream as completeness flags.\n\n## Event classes (SPEC §3.2)\n\n| class   | when                                     | notes |\n|---------|------------------------------------------|-------|\n| action  | recognized route, 2xx                    | `rec{rail, action, registryVersion, fields}` (v2 `rec.fields` = SAFE values) |\n| error   | recognized route, non-2xx / network fail | `rec{…, fields}`; status 0 = never completed |\n| shape   | unrecognized route, first sighting       | deduped in-memory, rate-capped, names/types only |\n| counter | manifest `counters` matchers + hot unrecognized GETs (> 60 calls/min, exact from call #1) | one event per (route, minute): exact count, summed dur/bytes, class-representative status |\n\n## Kill switch\n\n`BOUNDED_OBSERVE_DISABLED=1`\n- at process start: nothing is patched at all;\n- at runtime: observation stops within one flush tick (≤2s). Clearing the env\n  resumes it.\n\n## Reporting behavior\n\nAsync batches (≤500 events or 2s), fire-and-forget with a 5s send timeout,\nsingle in-flight send, one retry for transient failures, bounded queue\n(default 5000 events) with drop-and-count on overflow. `shutdown()` performs\na final drain (call it on SIGTERM to flush the tail). Hot-path overhead is\nmicroseconds (measured in `test/perf.test.ts`; U2 target <1ms p99).\n\n## Supported environments\n\n- **Node 18 / 20 / 22 server apps** (fetch + http/https interception).\n- **Vercel / Next.js Node runtime**: supported (call `init()` in\n  `instrumentation.ts` or the server entry).\n- **Edge runtimes** (Cloudflare Workers, Vercel Edge, Deno Deploy): the root\n  entry cannot load there (it needs `node:async_hooks`, `node:crypto`, and\n  http/https patching) — use the **`@bounded-sh/observe/edge`** subpath\n  instead (below).\n\n## Edge emitter (`@bounded-sh/observe/edge`)\n\nFor code that already sits at a chokepoint (a Worker proxy, an API gateway)\nand wants to REPORT events rather than intercept egress. No queue, no timers,\nno patching — one fire-and-forget envelope-v2 POST per call:\n\n```ts\nimport { emitEvent } from \"@bounded-sh/observe/edge\";\n\nemitEvent(\n  { ingestUrl: env.BOUNDED_INGEST_URL, sensorToken: env.BOUNDED_SENSOR_TOKEN },\n  {\n    class: \"action\",\n    actor: { id: tenantId, kind: \"service\", grade: \"attested\" },\n    dest: { host: \"api.anthropic.com\", pathTemplate: \"/v1/messages\", method: \"POST\" },\n    status: 200, dur_ms: 0, bytes: { i: 0, o: 0 },\n    rec: { rail: \"llm-gateway\", action: \"acme.tenant.aiRun\", registryVersion: \"acme-proxy\",\n           fields: { actualCents, \"usage.input_tokens\": inTok, \"usage.output_tokens\": outTok } },\n  },\n  ctx, // optional Workers ExecutionContext — the POST rides ctx.waitUntil\n);\n```\n\nPosture: NO-OP unless both `ingestUrl` and `sensorToken` are present (delete\nthe secret = kill switch); `postEvent` never rejects; the POST is bounded by\n`timeoutMs` (default 2000 ms); `org`/`sensor` are never sent — the ingest\nstamps both server-authoritatively from the sensor key.\n\nNotes: `http/https` path records duration to response *headers* and takes\n`bytes.o` from `Content-Length` (0 when chunked); response-field recognition\n(LLM token counts) is fetch-path only.\n\n## Config quick reference\n\n```ts\ninit({\n  ingestBase, token,            // required\n  org,                          // manifest polling scope\n  sessionMapper,                // default mapper for middleware()\n  aliasHosts,                   // {\"127.0.0.1:4242\": \"api.stripe.com\"} — dev/demo\n  flushIntervalMs: 2000, sendTimeoutMs: 5000, maxQueueEvents: 5000,\n  counterPromoteThreshold: 60,  // unrecognized-GET calls/min -> counter class\n  shapesPerMinute: 30,          // new-shape rate cap\n  manifest,                     // override (tests); invalid -> built-in (fail-safe)\n  disableManifestPoll, manifestPollMs,\n  debug, onEvent,               // debug hook: (wireEvent, recognizedFields)\n});\n```\n\n## Manifest (living capture policy, §3.1g)\n\nThe shim polls `<ingestBase>/v1/manifest?org=…` every 60s (T1: capture policy\nchanges reach every interceptor without re-install). The endpoint doesn't\nexist yet — 404/invalid keeps the built-in manifest. Signed-manifest\nverification is a marked TODO in `src/manifest.ts`; until it lands, the\nbuilt-in registry is the trust anchor.\n\n## Development\n\n- `npm run build` · `npm test` (76 tests: interception, templating,\n  recognizers, counters, ALS context, header stripping, kill switch, L1/L2\n  PII behavior, envelope contract vs the real `observe-shared` validator,\n  perf, and an integration run of the sample app in\n  `~/bounded/observe-sample-app`).\n- `npm run test:live` — LIVE prod verification: mints a dev sensor key\n  (needs `/tmp/observe-admin-secret.txt` or `$OBSERVE_ADMIN_SECRET`), runs\n  the sample app against prod ingest, asserts exact rollup deltas via the\n  consumer's `/internal/rollup/acme-demo`.\n- Wire contract: `src/envelope.ts` mirrors the versioned envelope (v2) from\n  `packages/cdk/cloudflare/observe-shared`; `test/envelope-contract.test.ts`\n  fails on any drift (types + constants + runtime validation), including that\n  every emitted v2 event (with `rec.fields`) passes the real ingest validator\n  with ZERO stripped fields.\n","readmeFilename":"README.md"}