{"_id":"@12-apps/request-scope","_rev":"4-e159cafaf97e4d6dbaf364a91d5dbb8c","name":"@12-apps/request-scope","dist-tags":{"latest":"2.1.0"},"versions":{"1.0.0":{"name":"@12-apps/request-scope","version":"1.0.0","license":"MIT","_id":"@12-apps/request-scope@1.0.0","maintainers":[{"name":"tigredonorte","email":"tigredonorte3@gmail.com"}],"homepage":"https://github.com/12-apps/shared-packages#readme","bugs":{"url":"https://github.com/12-apps/shared-packages/issues"},"dist":{"shasum":"c0490ef08e4bdab488b7324d99d2439fb559e383","tarball":"https://registry.npmjs.org/@12-apps/request-scope/-/request-scope-1.0.0.tgz","fileCount":9,"integrity":"sha512-2MlDepqxxPXd37jf98l1IraVeNhdg6CftAhZyRbtOk5/mj2bvdRjN8aIWgRf8ulnBz/6dAATaLTJiB5usB3BQQ==","signatures":[{"sig":"MEUCIQCK7qz6K2pMKy1RlmAR58RI7e9DiLnTKonsKds122IM5wIgEwb4HHuqVyKj4Y08pV+BpUvXEiZlex0mUYZjWlzZeXw=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":40174},"type":"module","engines":{"node":">=22.0.0"},"exports":{".":"./src/index.ts","./hono":"./src/hono/index.ts","./next-compat":"./src/next-compat/index.ts","./package.json":"./package.json"},"gitHead":"91c16cb08d2c2e1afe31c6a72493de948540bda6","scripts":{"lint":"eslint src --max-warnings 0","test":"node ../../scripts/vitest-with-teardown.mjs run","clean":"rm -rf node_modules coverage","typecheck":"tsc --noEmit","test:watch":"vitest watch","check-types":"tsc --noEmit"},"_npmUser":{"name":"tigredonorte","email":"tigredonorte3@gmail.com"},"repository":{"url":"git+https://github.com/12-apps/shared-packages.git","type":"git","directory":"packages/request-scope"},"_npmVersion":"12.0.2","description":"Generic, portable AMBIENT REQUEST SCOPE for hosts serving web-standard Request/Response. Framework-free core (an AsyncLocalStorage-backed per-request store, a paired cookie read/write codec, queued cookie writes drained onto the outgoing response, and the","directories":{},"_nodeVersion":"24.19.0","publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"hono":"^4.6.0","eslint":"^9.39.1","vitest":"^3.2.4","typescript":"^5.9.2","@12-apps/eslint-config":"^1.20.0","@12-apps/typescript-config":"^1.20.0","eslint-plugin-test-flakiness":"^1.4.0"},"peerDependencies":{"hono":">=4.0.0"},"peerDependenciesMeta":{"hono":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/request-scope_1.0.0_1786794282163_0.41885446827479944","host":"s3://npm-registry-packages-npm-production"}},"2.0.0":{"name":"@12-apps/request-scope","version":"2.0.0","license":"MIT","_id":"@12-apps/request-scope@2.0.0","maintainers":[{"name":"tigredonorte","email":"tigredonorte3@gmail.com"}],"homepage":"https://github.com/12-apps/shared-packages#readme","bugs":{"url":"https://github.com/12-apps/shared-packages/issues"},"dist":{"shasum":"7c24198739df8374254bd0b643ff1e7432f4cef5","tarball":"https://registry.npmjs.org/@12-apps/request-scope/-/request-scope-2.0.0.tgz","fileCount":9,"integrity":"sha512-PHOG/KRI7388aQvVimWrLngBm5suNhmW4Scx659FxV5fr2b0VpRmlghs0nm0ra5HBl9PpCaQ3k7oPUyN8UzvIw==","signatures":[{"sig":"MEQCIAE6MIIKjCjjXK2Fxbb+sQidheJZdMJoHBVnblnDwrj7AiBMJNJAta6ivk0Z4cSxOeVTsEdOU85nBagdKjUe7GspcA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@12-apps%2frequest-scope@2.0.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":40174},"type":"module","engines":{"node":">=22.0.0"},"exports":{".":"./src/index.ts","./hono":"./src/hono/index.ts","./next-compat":"./src/next-compat/index.ts","./package.json":"./package.json"},"gitHead":"ac9c72913da6ac4821ba1907f874901304ef0910","scripts":{"lint":"eslint src --max-warnings 0","test":"node ../../scripts/vitest-with-teardown.mjs run","clean":"rm -rf node_modules coverage","typecheck":"tsc --noEmit","test:watch":"vitest watch","check-types":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:4bc13053-9098-4dc4-b8ec-6112f2d3a1a2"}},"repository":{"url":"git+https://github.com/12-apps/shared-packages.git","type":"git","directory":"packages/request-scope"},"_npmVersion":"12.0.2","description":"Generic, portable AMBIENT REQUEST SCOPE for hosts serving web-standard Request/Response. Framework-free core (an AsyncLocalStorage-backed per-request store, a paired cookie read/write codec, queued cookie writes drained onto the outgoing response, and the","directories":{},"_nodeVersion":"24.19.0","publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"hono":"^4.6.0","eslint":"^9.39.1","vitest":"^3.2.4","typescript":"^5.9.2","@12-apps/eslint-config":"^1.20.0","@12-apps/typescript-config":"^1.20.0","eslint-plugin-test-flakiness":"^1.4.0"},"peerDependencies":{"hono":">=4.0.0"},"peerDependenciesMeta":{"hono":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/request-scope_2.0.0_1786924494034_0.30264399949992327","host":"s3://npm-registry-packages-npm-production"}},"2.0.1":{"name":"@12-apps/request-scope","version":"2.0.1","license":"MIT","_id":"@12-apps/request-scope@2.0.1","maintainers":[{"name":"tigredonorte","email":"tigredonorte3@gmail.com"}],"homepage":"https://github.com/12-apps/shared-packages#readme","bugs":{"url":"https://github.com/12-apps/shared-packages/issues"},"dist":{"shasum":"28113ad83b31f8ff66653fdbcae8c3620ee6900b","tarball":"https://registry.npmjs.org/@12-apps/request-scope/-/request-scope-2.0.1.tgz","fileCount":9,"integrity":"sha512-oYTI9grUZaJ+ZFQfyNzA9JNG8jzoiUFmsW3FbvX47g6D7hjGYMl3SRF4venbnwI+xJfQuJlZffxOZomlSt0kCQ==","signatures":[{"sig":"MEUCIQDRjQTTV0AcBXAYlbf5qn5OGJe+sRO+16AXr4g6xXfZzwIgEGIXpjr8/3UlE4qKFlVZYfjzHn8BtN1Mg8r2E0H4nos=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@12-apps%2frequest-scope@2.0.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":40174},"type":"module","engines":{"node":">=22.0.0"},"exports":{".":"./src/index.ts","./hono":"./src/hono/index.ts","./next-compat":"./src/next-compat/index.ts","./package.json":"./package.json"},"gitHead":"3c4711049bf8351685612372531c3cae1370bbbb","scripts":{"lint":"eslint src --max-warnings 0","test":"node ../../scripts/vitest-with-teardown.mjs run","clean":"rm -rf node_modules coverage","typecheck":"tsc --noEmit","test:watch":"vitest watch","check-types":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:4bc13053-9098-4dc4-b8ec-6112f2d3a1a2"}},"repository":{"url":"git+https://github.com/12-apps/shared-packages.git","type":"git","directory":"packages/request-scope"},"_npmVersion":"12.0.2","description":"Generic, portable AMBIENT REQUEST SCOPE for hosts serving web-standard Request/Response. Framework-free core (an AsyncLocalStorage-backed per-request store, a paired cookie read/write codec, queued cookie writes drained onto the outgoing response, and the","directories":{},"_nodeVersion":"24.19.0","publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"hono":"^4.6.0","eslint":"^9.39.1","vitest":"^3.2.4","typescript":"^5.9.2","@12-apps/eslint-config":"^1.21.1","@12-apps/typescript-config":"^1.20.1","eslint-plugin-test-flakiness":"^1.4.0"},"peerDependencies":{"hono":">=4.0.0"},"peerDependenciesMeta":{"hono":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/request-scope_2.0.1_1787327194088_0.438407391267422","host":"s3://npm-registry-packages-npm-production"}},"2.1.0":{"name":"@12-apps/request-scope","version":"2.1.0","type":"module","sideEffects":false,"description":"Generic, portable AMBIENT REQUEST SCOPE for hosts serving web-standard Request/Response. Framework-free core (an AsyncLocalStorage-backed per-request store, a paired cookie read/write codec, queued cookie writes drained onto the outgoing response, and the","exports":{".":"./src/index.ts","./hono":"./src/hono/index.ts","./next-compat":"./src/next-compat/index.ts","./package.json":"./package.json"},"scripts":{"clean":"rm -rf node_modules coverage","test":"node ../../scripts/vitest-with-teardown.mjs run","test:watch":"vitest watch","lint":"eslint src --max-warnings 0","check-types":"tsc --noEmit","typecheck":"tsc --noEmit"},"peerDependencies":{"hono":">=4.0.0"},"peerDependenciesMeta":{"hono":{"optional":true}},"devDependencies":{"@12-apps/eslint-config":"^1.22.0","@12-apps/typescript-config":"^1.21.0","eslint":"^9.39.1","eslint-plugin-test-flakiness":"^1.4.0","hono":"^4.6.0","typescript":"^5.9.2","vitest":"^3.2.4"},"engines":{"node":">=22.0.0"},"license":"MIT","publishConfig":{"registry":"https://registry.npmjs.org","access":"public"},"repository":{"type":"git","url":"git+https://github.com/12-apps/shared-packages.git","directory":"packages/request-scope"},"gitHead":"4714bd10c901bcc51544843d5d6091cc1509e649","_id":"@12-apps/request-scope@2.1.0","bugs":{"url":"https://github.com/12-apps/shared-packages/issues"},"homepage":"https://github.com/12-apps/shared-packages#readme","_nodeVersion":"24.19.0","_npmVersion":"12.0.2","dist":{"integrity":"sha512-hdbERHmtOZn5knk59xzD2gvUDIJiQZPSVdKnNCWA2MN9Wj0DzJer4zWuwCzz6h/f8SHDw5/w/dfC8vkc9NUsag==","shasum":"9e3f848920a38fb07230cd9ace4ea615b21c281d","tarball":"https://registry.npmjs.org/@12-apps/request-scope/-/request-scope-2.1.0.tgz","fileCount":9,"unpackedSize":40198,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@12-apps%2frequest-scope@2.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIGaVC8yxz5PV4k7TUsomaXh4Knooek12D/eBS6d2p47/AiEAqxN5dehaGZlpztRYwpVHtKyJ/2GXQWLrcHlv68Idl30="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:999fd483-25b7-4f7b-ab64-2edd0447bb07"}},"directories":{},"maintainers":[{"name":"tigredonorte","email":"tigredonorte3@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/request-scope_2.1.0_1787617775625_0.4836041131798625"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-15T11:44:41.893Z","modified":"2026-08-25T00:29:36.076Z","1.0.0":"2026-08-15T11:44:42.318Z","2.0.0":"2026-08-16T23:54:54.186Z","2.0.1":"2026-08-21T15:46:34.248Z","2.1.0":"2026-08-25T00:29:35.763Z"},"bugs":{"url":"https://github.com/12-apps/shared-packages/issues"},"license":"MIT","homepage":"https://github.com/12-apps/shared-packages#readme","repository":{"type":"git","url":"git+https://github.com/12-apps/shared-packages.git","directory":"packages/request-scope"},"description":"Generic, portable AMBIENT REQUEST SCOPE for hosts serving web-standard Request/Response. Framework-free core (an AsyncLocalStorage-backed per-request store, a paired cookie read/write codec, queued cookie writes drained onto the outgoing response, and the","maintainers":[{"name":"tigredonorte","email":"tigredonorte3@gmail.com"}],"readme":"# `@12-apps/request-scope`\n\nThe ambient per-request scope for a host serving web-standard\n`Request`/`Response`: read the incoming headers and cookies from anywhere in the\ncall chain, queue a cookie write from code that has no response in hand, and\nhave both merged onto the answer on the way out.\n\nZero runtime dependencies. `node:async_hooks` and nothing else.\n\n---\n\n## The problem it solves\n\nA web-standard handler takes a `Request` and returns a `Response`. That is a\nclean contract right up to the moment something twelve calls deep needs to know\nwho is calling — a cart keyed by a cookie, a consent gate, an impersonation\nmarker, an incoming bearer. There are two ways out:\n\n1. **Thread the `Request` through every intervening signature.** This turns a\n   transport detail into a parameter of the domain layer, and it is viral: one\n   new cookie reader changes a dozen function signatures and their tests.\n2. **An ambient accessor**, backed by an explicit store.\n\nThis package is the second, and the store is the point. `AsyncLocalStorage` is\neasy to reach for and easy to get subtly wrong — the store identity across a hot\nreload, the write half that has nowhere to write, the redirect whose headers\nrefuse a `Set-Cookie`. Those are the parts worth sharing.\n\n## The three entry points\n\n| Import | For |\n| --- | --- |\n| `@12-apps/request-scope` | the framework-free core — the store, the codec, the response helpers |\n| `@12-apps/request-scope/hono` | the Hono middleware (`hono` is an **optional** peer) |\n| `@12-apps/request-scope/next-compat` | `cookies()` / `headers()` with `next/headers`' shapes, for a host migrating off the App Router |\n\n```ts\nimport { requestScope } from '@12-apps/request-scope/hono';\n\napp.use('*', requestScope());\n```\n\nThat is the whole wiring. Then, anywhere downstream:\n\n```ts\nimport { requireRequestScope, writeCookie } from '@12-apps/request-scope';\n\nconst scope = requireRequestScope();\nconst cartId = scope.values.get('cart');\nwriteCookie(scope, 'cart', nextId, { httpOnly: true, sameSite: 'lax', path: '/' });\n```\n\nA host that dispatches its own route table rather than composing middleware uses\nthe one-call form instead:\n\n```ts\nimport { serveWithRequestScope } from '@12-apps/request-scope';\n\nconst response = await serveWithRequestScope(request, () => handler(request));\n```\n\n---\n\n## Four decisions worth knowing about\n\n### The read and write halves of a cookie ship as one object\n\n`createCookieCodec()` hands back `serialize`, `serializeDeletion` and `parse`\ntogether. The only way a cookie layer can be wrong is by disagreeing with\nitself — a parser that percent-decodes paired with a serializer that does not\nencode round-trips everything it was tested with and mangles the first value\ncontaining a delimiter. Binding the pair to one object removes the call site\nthat could import half of it.\n\n### Encoding is a knob, and its default is the safe direction\n\nRFC 6265 forbids `;`, `,`, whitespace and control characters in a value, so\npercent-encoding is correct and is the default. But a host adopting this package\nalready has cookies sitting in browsers, written by whatever it used before.\n\nThe two formats meet during a rollout, and only one direction survives:\n\n- a **raw** value read by the **decoding** parser is fine — `decodeURIComponent`\n  is the identity on anything with no `%` in it, and a malformed escape falls\n  back to the raw text rather than throwing;\n- an **encoded** value read by a **raw** parser is not, and that reader is the\n  host's old deployed code, which this package cannot reach.\n\nSo `createCookieCodec({ encode: false })` exists for hosts with existing raw\ncookies, and the flag flips once nothing parses those cookies by hand.\n\n### `requireRequestScope()` throws, deliberately\n\nCode reading an optional credential — a bearer header, an impersonation cookie\n— wraps the accessor in a `try` and treats a throw as *\"there is no incoming\nrequest, so there is no credential\"*. Returning `undefined` would collapse that\ninto the same answer as *\"a request with no such cookie\"*, and those need\ndifferent handling: one is a background job, the other is an anonymous visitor.\n\n`currentRequestScope()` is the non-throwing form, for code that legitimately\nruns both inside and outside a request.\n\n### A redirect gets rebuilt rather than forbidden\n\n`Response.redirect()` is specified to return an **immutable** header list, so\nappending a `Set-Cookie` to it throws. Redirecting while clearing a cookie is\ncompletely ordinary (an OAuth callback dropping its state cookie), so rather\nthan ban the built-in — a rule the next handler rediscovers as a 500 —\n`applyResponseCookies` rebuilds the response when its headers refuse the write.\n\n`redirectResponse()` is the other half: a redirect built through the constructor,\nso its headers stay mutable. It defaults to **307**, not the 302\n`Response.redirect()` gives, because 307 preserves the method — a redirect added\nto a non-GET handler later behaves as written instead of silently downgrading\nthe follow-up to a GET and losing the body.\n\n---\n\n## The Hono adapter and `c.res`\n\nWorth surfacing, because it is invisible until it bites. Hono's `c.res` **setter**\nre-merges headers from the previous response, and its `set-cookie` branch reads:\n\n```js\nconst cookies = this.#res.headers.getSetCookie();  // the OLD list\n_res.headers.delete('set-cookie');                 // wipes the NEW list\nfor (const cookie of cookies) _res.headers.append('set-cookie', cookie);\n```\n\nSo merging by assignment would **drop** the cookies this middleware queued —\nprecisely and only when the handler wrote one of its own, which is the case\nwhere both must survive. The adapter appends in place instead, and falls back to\nassignment only for an immutable response, which by construction cannot be\ncarrying a `Set-Cookie` of its own. Both halves are pinned by tests.\n\n## Mount order\n\nMount the middleware **before** anything that reads a cookie or header\nambiently. A route registered above it runs outside the scope and its accessors\nthrow — which is the intended failure: loud, at the first request, rather than a\nsilently absent session.\n\n## Coexisting with another `AsyncLocalStorage`\n\nThis store is kept on `globalThis` so a dev server that re-evaluates the module\ncannot create a second one invisible to closures captured against the first. The\nkey it lives under is configurable, for the same reason `@12-apps/audit` makes\nits actor-store key configurable: a host that already has an in-house\nrequest-scope module, with call sites importing it, needs both modules on one\nstore.\n\n```ts\nimport { declareRequestScopeKey } from '@12-apps/request-scope';\n\ndeclareRequestScopeKey('__myHostRequestStore'); // at wiring time, once\n```\n\nTwo stores that disagree do not fail loudly — the accessors read a store nothing\never entered and throw \"outside a request scope\" from inside a perfectly\nordinary request. Calling this after a store exists is refused rather than\nsilently honoured.\n\nNote that the actor context in `@12-apps/audit` is a **different** scope with a\ndifferent lifetime: it carries *who is acting*, this one carries *what arrived*.\nThey are opened by the same adapter and otherwise have nothing to say to each\nother.\n\n---\n\nSee [`ADOPTING.md`](./ADOPTING.md) for the integration playbook.\n","readmeFilename":"README.md"}