@warlock.js/context 5.30.0 → 5.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ All notable changes to `@warlock.js/context` are documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
6
6
 
7
+ ## 5.31.0 - 2026-10-18
8
+
9
+ ### Changed
10
+
11
+ - Lockstep release; package APIs are unchanged.
12
+
7
13
  ## 5.30.0 - 2026-10-16
8
14
 
9
15
  ### Changed
@@ -81,6 +87,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
81
87
  ### Changed
82
88
 
83
89
  - Lockstep release maintenance and dependency refresh.
90
+
84
91
  ## 5.20.0 - 2026-09-24
85
92
 
86
93
  ### Changed
@@ -162,11 +169,11 @@ changelogs.
162
169
 
163
170
  ### Security
164
171
 
165
- - **`update()` and `set()` now drop `__proto__` / `constructor` / `prototype` keys instead of merging them.** `update()` merged with `Object.assign(store, updates)`, which does not create a `__proto__` property — it invokes the inherited setter and reparents the store. An app that forwards request-shaped data into a context (`tenantContext.update(req.body)`, or `set(key, value)` with a caller-supplied key — both close to patterns the README shows) therefore handed a body of `{"__proto__":{"isAdmin":true}}` a way to pollute `Object.prototype` for the whole process. Because the polluted object is a *shared* context store, that turns an app-level slip into a cross-request, cross-tenant authorization problem: every later lookup of a missing property anywhere in the process resolves through the attacker's object
172
+ - **`update()` and `set()` now drop `__proto__` / `constructor` / `prototype` keys instead of merging them.** `update()` merged with `Object.assign(store, updates)`, which does not create a `__proto__` property — it invokes the inherited setter and reparents the store. An app that forwards request-shaped data into a context (`tenantContext.update(req.body)`, or `set(key, value)` with a caller-supplied key — both close to patterns the README shows) therefore handed a body of `{"__proto__":{"isAdmin":true}}` a way to pollute `Object.prototype` for the whole process. Because the polluted object is a _shared_ context store, that turns an app-level slip into a cross-request, cross-tenant authorization problem: every later lookup of a missing property anywhere in the process resolves through the attacker's object
166
173
 
167
174
  Dangerous keys are dropped rather than throwing — a request that trips this must not be able to take the process down — and the rest of the payload merges as before. Symbol keys and object identity for clean payloads are unchanged (the payload is only copied when a dangerous key is actually present). `run()` / `enter()`, where the caller supplies the whole store rather than merging into a shared one, are untouched
168
175
 
169
- This closes the prototype vector, not the broader one: merging unvalidated input into a context still lets an attacker choose what your context *says*. Validate before you merge
176
+ This closes the prototype vector, not the broader one: merging unvalidated input into a context still lets an attacker choose what your context _says_. Validate before you merge
170
177
 
171
178
  ## 4.12.0
172
179
 
package/README.md CHANGED
@@ -69,16 +69,13 @@ contextManager.register("user", userContext);
69
69
  #### With `run()` - Scoped execution
70
70
 
71
71
  ```typescript
72
- await userContext.run(
73
- { userId: "123", role: "admin", tenantId: "acme" },
74
- async () => {
75
- // Context is available throughout this async scope
76
- const userId = userContext.get("userId"); // '123'
77
- const role = userContext.get("role"); // 'admin'
78
-
79
- await someAsyncOperation(); // Context propagates through async calls
80
- }
81
- );
72
+ await userContext.run({ userId: "123", role: "admin", tenantId: "acme" }, async () => {
73
+ // Context is available throughout this async scope
74
+ const userId = userContext.get("userId"); // '123'
75
+ const role = userContext.get("role"); // 'admin'
76
+
77
+ await someAsyncOperation(); // Context propagates through async calls
78
+ });
82
79
  ```
83
80
 
84
81
  #### With `enter()` - Middleware-style
@@ -291,9 +288,7 @@ app.use((req, res, next) => {
291
288
  traceContext.run(stores.trace, async () => {
292
289
  traceContext.log(`Request started: ${req.path}`);
293
290
  await next();
294
- traceContext.log(
295
- `Request completed in ${Date.now() - stores.trace.startTime}ms`
296
- );
291
+ traceContext.log(`Request completed in ${Date.now() - stores.trace.startTime}ms`);
297
292
  });
298
293
  });
299
294
  ```
package/cjs/index.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["AsyncLocalStorage"],"sources":["../../../../../../../packages/context/src/base-context.ts","../../../../../../../packages/context/src/context-manager.ts"],"sourcesContent":["import { AsyncLocalStorage } from \"async_hooks\";\n\n/**\n * Keys that reach `Object.prototype` instead of the store when assigned.\n *\n * `Object.assign(store, { __proto__: {...} })` does not create a property — it\n * invokes the inherited `__proto__` setter and reparents the store, which for a\n * shared context store means every later property lookup in the process can\n * resolve through attacker-supplied data. `constructor` / `prototype` are the\n * neighbouring rungs of the same ladder.\n */\nconst DANGEROUS_KEYS = [\"__proto__\", \"constructor\", \"prototype\"] as const;\n\n/**\n * Drop prototype-poisoning keys from a merge payload.\n *\n * Contexts are frequently fed request-shaped data (`context.update(req.body)`,\n * `context.set(key, value)` with a caller-supplied key), so the merge path is\n * treated as an untrusted boundary: dangerous keys are dropped rather than\n * throwing, since a request that trips this must not be able to take the\n * process down, and the store simply never carries them.\n *\n * The common case allocates nothing — a copy is made only when a dangerous key\n * is actually present. The copy is built by spread (which defines properties\n * rather than assigning them, so it cannot trigger the setter itself) and\n * preserves symbol keys, which `Object.assign` would have copied.\n */\nfunction withoutDangerousKeys<T>(updates: T): T {\n if (!updates || typeof updates !== \"object\") return updates;\n\n const hasDangerousKey = DANGEROUS_KEYS.some(key =>\n Object.prototype.hasOwnProperty.call(updates, key),\n );\n\n if (!hasDangerousKey) return updates;\n\n const safe: Record<string, any> = { ...(updates as Record<string, any>) };\n\n for (const key of DANGEROUS_KEYS) {\n delete safe[key];\n }\n\n return safe as T;\n}\n\n/**\n * Base class for all AsyncLocalStorage-based contexts\n *\n * Provides a consistent API for managing context across async operations.\n * All framework contexts (request, storage, database) extend this class.\n *\n * @template TStore - The type of data stored in context\n *\n * @example\n * ```typescript\n * interface MyContextStore {\n * userId: string;\n * tenant: string;\n * }\n *\n * class MyContext extends Context<MyContextStore> {}\n * const myContext = new MyContext();\n *\n * // Use it\n * await myContext.run({ userId: '123', tenant: 'acme' }, async () => {\n * const userId = myContext.get('userId'); // '123'\n * });\n * ```\n */\nexport abstract class Context<TStore extends Record<string, any>> {\n protected readonly storage: AsyncLocalStorage<TStore> = new AsyncLocalStorage<TStore>();\n\n /**\n * Run a callback within a new context\n *\n * Creates a new async context with the provided store data.\n * All operations within the callback will have access to this context.\n *\n * @param store - Initial context data\n * @param callback - Async function to execute\n * @returns Result of the callback\n */\n public run<T>(store: TStore, callback: () => Promise<T>): Promise<T> {\n return this.storage.run(store, callback);\n }\n\n /**\n * Enter a new context without a callback\n *\n * Useful for middleware where you want to set context for the rest of the request.\n * Unlike `run()`, this doesn't require a callback.\n *\n * @param store - Context data to set\n */\n public enter(store: TStore): void {\n this.storage.enterWith(store);\n }\n\n /**\n * Update the current context\n *\n * Merges new data into existing context, or enters new context if none exists.\n *\n * Keys named `__proto__`, `constructor`, or `prototype` are dropped: the\n * merge target is a store shared by everything running in this async chain,\n * so letting those through would let one caller's payload change property\n * resolution for the whole process. Passing unvalidated input here is still\n * discouraged — this guard closes the prototype vector, not the \"an attacker\n * chose what your context says\" one.\n *\n * @param updates - Partial context data to merge\n */\n public update(updates: Partial<TStore>): void {\n const safeUpdates = withoutDangerousKeys(updates);\n\n const current = this.storage.getStore();\n\n if (current) {\n Object.assign(current, safeUpdates);\n } else {\n this.enter(safeUpdates as TStore);\n }\n }\n\n /**\n * Get the current context store\n *\n * @returns Current context or undefined if not in context\n */\n public getStore(): TStore | undefined {\n return this.storage.getStore();\n }\n\n /**\n * Get a specific value from context\n *\n * @param key - Key to retrieve\n * @returns Value or undefined\n */\n public get<K extends keyof TStore>(key: K): TStore[K] | undefined {\n return this.storage.getStore()?.[key];\n }\n\n /**\n * Set a specific value in context\n *\n * Goes through `update()`, so a caller-supplied `key` of `__proto__` /\n * `constructor` / `prototype` is dropped rather than reparenting the store.\n *\n * @param key - Key to set\n * @param value - Value to store\n */\n public set<K extends keyof TStore>(key: K, value: TStore[K]): void {\n this.update({ [key]: value } as any);\n }\n\n /**\n * Clear the context\n */\n public clear(): void {\n this.storage.enterWith({} as TStore);\n }\n\n /**\n * Check if currently in a context\n */\n public hasContext(): boolean {\n return this.storage.getStore() !== undefined;\n }\n\n /**\n * Build the initial store for this context\n *\n * Override this method to provide custom initialization logic.\n * Called by ContextManager.buildStores() for each registered context.\n *\n * @param payload - Generic payload (e.g., { request, response } for HTTP contexts)\n * @returns Initial store data\n */\n public abstract buildStore(payload?: Record<string, any>): TStore;\n}\n","import type { Context } from \"./base-context\";\n\n/**\n * Context Manager - Orchestrates multiple contexts together\n *\n * Allows running multiple AsyncLocalStorage contexts in a single operation,\n * making it easy to link request, storage, database, and other contexts.\n *\n * @example\n * ```typescript\n * // Register contexts\n * contextManager\n * .register('request', requestContext)\n * .register('storage', storageDriverContext)\n * .register('database', databaseDataSourceContext);\n *\n * // Run all contexts together\n * await contextManager.runAll({\n * request: { request, response, user },\n * storage: { driver, metadata: { tenantId: '123' } },\n * database: { dataSource: 'primary' },\n * }, async () => {\n * // All contexts active!\n * await handleRequest();\n * });\n * ```\n */\nexport class ContextManager {\n private contexts = new Map<string, Context<any>>();\n\n /**\n * Register a context\n *\n * @param name - Unique context name\n * @param context - Context instance\n * @returns This instance for chaining\n */\n public register(name: string, context: Context<any>): this {\n this.contexts.set(name, context);\n return this;\n }\n\n /**\n * Run all registered contexts together\n *\n * Nests all context.run() calls, ensuring all contexts are active\n * for the duration of the callback.\n *\n * @param stores - Context stores keyed by context name\n * @param callback - Async function to execute\n * @returns Result of the callback\n */\n public async runAll<T>(stores: Record<string, any>, callback: () => Promise<T>): Promise<T> {\n const entries = Array.from(this.contexts.entries());\n\n // Build nested context runners\n const runner = entries.reduceRight((next, [name, context]) => {\n return () => context.run(stores[name] || {}, next);\n }, callback);\n\n return runner();\n }\n\n /**\n * Enter all contexts at once (for middleware)\n *\n * @param stores - Context stores keyed by context name\n */\n public enterAll(stores: Record<string, any>): void {\n for (const [name, context] of this.contexts.entries()) {\n if (stores[name]) {\n context.enter(stores[name]);\n }\n }\n }\n\n /**\n * Clear all contexts\n */\n public clearAll(): void {\n for (const context of this.contexts.values()) {\n context.clear();\n }\n }\n\n /**\n * Get a specific registered context\n *\n * @param name - Context name\n * @returns Context instance or undefined\n */\n public getContext<T extends Context<any>>(name: string): T | undefined {\n return this.contexts.get(name) as T | undefined;\n }\n\n /**\n * Check if a context is registered\n *\n * @param name - Context name\n * @returns True if context is registered\n */\n public hasContext(name: string): boolean {\n return this.contexts.has(name);\n }\n\n /**\n * Build all context stores by calling each context's buildStore() method\n *\n * This is the immutable pattern - returns a new record of stores.\n * Each context defines its own initialization logic.\n *\n * @param payload - Payload passed to each buildStore() (e.g., { request, response })\n * @returns Record of context name -> store data\n *\n * @example\n * ```typescript\n * const httpContextStore = contextManager.buildStores({ request, response });\n * await contextManager.runAll(httpContextStore, async () => { ... });\n * ```\n */\n public buildStores(payload?: Record<string, any>): Record<string, any> {\n const stores: Record<string, any> = {};\n\n for (const [name, context] of this.contexts.entries()) {\n stores[name] = context.buildStore(payload) ?? {};\n }\n\n return stores;\n }\n\n /**\n * Unregister a context\n *\n * @param name - Context name to remove\n * @returns True if context was removed\n */\n public unregister(name: string): boolean {\n return this.contexts.delete(name);\n }\n}\n\n/**\n * Global context manager instance\n *\n * Use this singleton to register and manage all framework contexts.\n */\nexport const contextManager = new ContextManager();\n"],"mappings":";;;;;;;;;;;;;AAWA,MAAM,iBAAiB;CAAC;CAAa;CAAe;AAAW;;;;;;;;;;;;;;;AAgB/D,SAAS,qBAAwB,SAAe;CAC9C,IAAI,CAAC,WAAW,OAAO,YAAY,UAAU,OAAO;CAMpD,IAAI,CAJoB,eAAe,MAAK,QAC1C,OAAO,UAAU,eAAe,KAAK,SAAS,GAAG,CAGhC,GAAG,OAAO;CAE7B,MAAM,OAA4B,EAAE,GAAI,QAAgC;CAExE,KAAK,MAAM,OAAO,gBAChB,OAAO,KAAK;CAGd,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,IAAsB,UAAtB,MAAkE;;iBACR,IAAIA,8BAA0B;;;;;;;;;;;;CAYtF,AAAO,IAAO,OAAe,UAAwC;EACnE,OAAO,KAAK,QAAQ,IAAI,OAAO,QAAQ;CACzC;;;;;;;;;CAUA,AAAO,MAAM,OAAqB;EAChC,KAAK,QAAQ,UAAU,KAAK;CAC9B;;;;;;;;;;;;;;;CAgBA,AAAO,OAAO,SAAgC;EAC5C,MAAM,cAAc,qBAAqB,OAAO;EAEhD,MAAM,UAAU,KAAK,QAAQ,SAAS;EAEtC,IAAI,SACF,OAAO,OAAO,SAAS,WAAW;OAElC,KAAK,MAAM,WAAqB;CAEpC;;;;;;CAOA,AAAO,WAA+B;EACpC,OAAO,KAAK,QAAQ,SAAS;CAC/B;;;;;;;CAQA,AAAO,IAA4B,KAA+B;EAChE,OAAO,KAAK,QAAQ,SAAS,CAAC,GAAG;CACnC;;;;;;;;;;CAWA,AAAO,IAA4B,KAAQ,OAAwB;EACjE,KAAK,OAAO,GAAG,MAAM,MAAM,CAAQ;CACrC;;;;CAKA,AAAO,QAAc;EACnB,KAAK,QAAQ,UAAU,CAAC,CAAW;CACrC;;;;CAKA,AAAO,aAAsB;EAC3B,OAAO,KAAK,QAAQ,SAAS,MAAM;CACrC;AAYF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACzJA,IAAa,iBAAb,MAA4B;;kCACP,IAAI,IAA0B;;;;;;;;;CASjD,AAAO,SAAS,MAAc,SAA6B;EACzD,KAAK,SAAS,IAAI,MAAM,OAAO;EAC/B,OAAO;CACT;;;;;;;;;;;CAYA,MAAa,OAAU,QAA6B,UAAwC;EAQ1F,OAPgB,MAAM,KAAK,KAAK,SAAS,QAAQ,CAG5B,CAAC,CAAC,aAAa,MAAM,CAAC,MAAM,aAAa;GAC5D,aAAa,QAAQ,IAAI,OAAO,SAAS,CAAC,GAAG,IAAI;EACnD,GAAG,QAES,CAAC,CAAC;CAChB;;;;;;CAOA,AAAO,SAAS,QAAmC;EACjD,KAAK,MAAM,CAAC,MAAM,YAAY,KAAK,SAAS,QAAQ,GAClD,IAAI,OAAO,OACT,QAAQ,MAAM,OAAO,KAAK;CAGhC;;;;CAKA,AAAO,WAAiB;EACtB,KAAK,MAAM,WAAW,KAAK,SAAS,OAAO,GACzC,QAAQ,MAAM;CAElB;;;;;;;CAQA,AAAO,WAAmC,MAA6B;EACrE,OAAO,KAAK,SAAS,IAAI,IAAI;CAC/B;;;;;;;CAQA,AAAO,WAAW,MAAuB;EACvC,OAAO,KAAK,SAAS,IAAI,IAAI;CAC/B;;;;;;;;;;;;;;;;CAiBA,AAAO,YAAY,SAAoD;EACrE,MAAM,SAA8B,CAAC;EAErC,KAAK,MAAM,CAAC,MAAM,YAAY,KAAK,SAAS,QAAQ,GAClD,OAAO,QAAQ,QAAQ,WAAW,OAAO,KAAK,CAAC;EAGjD,OAAO;CACT;;;;;;;CAQA,AAAO,WAAW,MAAuB;EACvC,OAAO,KAAK,SAAS,OAAO,IAAI;CAClC;AACF;;;;;;AAOA,MAAa,iBAAiB,IAAI,eAAe"}
1
+ {"version":3,"file":"index.cjs","names":["AsyncLocalStorage"],"sources":["../../../../../../../packages/context/src/base-context.ts","../../../../../../../packages/context/src/context-manager.ts"],"sourcesContent":["import { AsyncLocalStorage } from \"async_hooks\";\n\n/**\n * Keys that reach `Object.prototype` instead of the store when assigned.\n *\n * `Object.assign(store, { __proto__: {...} })` does not create a property — it\n * invokes the inherited `__proto__` setter and reparents the store, which for a\n * shared context store means every later property lookup in the process can\n * resolve through attacker-supplied data. `constructor` / `prototype` are the\n * neighbouring rungs of the same ladder.\n */\nconst DANGEROUS_KEYS = [\"__proto__\", \"constructor\", \"prototype\"] as const;\n\n/**\n * Drop prototype-poisoning keys from a merge payload.\n *\n * Contexts are frequently fed request-shaped data (`context.update(req.body)`,\n * `context.set(key, value)` with a caller-supplied key), so the merge path is\n * treated as an untrusted boundary: dangerous keys are dropped rather than\n * throwing, since a request that trips this must not be able to take the\n * process down, and the store simply never carries them.\n *\n * The common case allocates nothing — a copy is made only when a dangerous key\n * is actually present. The copy is built by spread (which defines properties\n * rather than assigning them, so it cannot trigger the setter itself) and\n * preserves symbol keys, which `Object.assign` would have copied.\n */\nfunction withoutDangerousKeys<T>(updates: T): T {\n if (!updates || typeof updates !== \"object\") return updates;\n\n const hasDangerousKey = DANGEROUS_KEYS.some((key) =>\n Object.prototype.hasOwnProperty.call(updates, key),\n );\n\n if (!hasDangerousKey) return updates;\n\n const safe: Record<string, any> = { ...(updates as Record<string, any>) };\n\n for (const key of DANGEROUS_KEYS) {\n delete safe[key];\n }\n\n return safe as T;\n}\n\n/**\n * Base class for all AsyncLocalStorage-based contexts\n *\n * Provides a consistent API for managing context across async operations.\n * All framework contexts (request, storage, database) extend this class.\n *\n * @template TStore - The type of data stored in context\n *\n * @example\n * ```typescript\n * interface MyContextStore {\n * userId: string;\n * tenant: string;\n * }\n *\n * class MyContext extends Context<MyContextStore> {}\n * const myContext = new MyContext();\n *\n * // Use it\n * await myContext.run({ userId: '123', tenant: 'acme' }, async () => {\n * const userId = myContext.get('userId'); // '123'\n * });\n * ```\n */\nexport abstract class Context<TStore extends Record<string, any>> {\n protected readonly storage: AsyncLocalStorage<TStore> = new AsyncLocalStorage<TStore>();\n\n /**\n * Run a callback within a new context\n *\n * Creates a new async context with the provided store data.\n * All operations within the callback will have access to this context.\n *\n * @param store - Initial context data\n * @param callback - Async function to execute\n * @returns Result of the callback\n */\n public run<T>(store: TStore, callback: () => Promise<T>): Promise<T> {\n return this.storage.run(store, callback);\n }\n\n /**\n * Enter a new context without a callback\n *\n * Useful for middleware where you want to set context for the rest of the request.\n * Unlike `run()`, this doesn't require a callback.\n *\n * @param store - Context data to set\n */\n public enter(store: TStore): void {\n this.storage.enterWith(store);\n }\n\n /**\n * Update the current context\n *\n * Merges new data into existing context, or enters new context if none exists.\n *\n * Keys named `__proto__`, `constructor`, or `prototype` are dropped: the\n * merge target is a store shared by everything running in this async chain,\n * so letting those through would let one caller's payload change property\n * resolution for the whole process. Passing unvalidated input here is still\n * discouraged — this guard closes the prototype vector, not the \"an attacker\n * chose what your context says\" one.\n *\n * @param updates - Partial context data to merge\n */\n public update(updates: Partial<TStore>): void {\n const safeUpdates = withoutDangerousKeys(updates);\n\n const current = this.storage.getStore();\n\n if (current) {\n Object.assign(current, safeUpdates);\n } else {\n this.enter(safeUpdates as TStore);\n }\n }\n\n /**\n * Get the current context store\n *\n * @returns Current context or undefined if not in context\n */\n public getStore(): TStore | undefined {\n return this.storage.getStore();\n }\n\n /**\n * Get a specific value from context\n *\n * @param key - Key to retrieve\n * @returns Value or undefined\n */\n public get<K extends keyof TStore>(key: K): TStore[K] | undefined {\n return this.storage.getStore()?.[key];\n }\n\n /**\n * Set a specific value in context\n *\n * Goes through `update()`, so a caller-supplied `key` of `__proto__` /\n * `constructor` / `prototype` is dropped rather than reparenting the store.\n *\n * @param key - Key to set\n * @param value - Value to store\n */\n public set<K extends keyof TStore>(key: K, value: TStore[K]): void {\n this.update({ [key]: value } as any);\n }\n\n /**\n * Clear the context\n */\n public clear(): void {\n this.storage.enterWith({} as TStore);\n }\n\n /**\n * Check if currently in a context\n */\n public hasContext(): boolean {\n return this.storage.getStore() !== undefined;\n }\n\n /**\n * Build the initial store for this context\n *\n * Override this method to provide custom initialization logic.\n * Called by ContextManager.buildStores() for each registered context.\n *\n * @param payload - Generic payload (e.g., { request, response } for HTTP contexts)\n * @returns Initial store data\n */\n public abstract buildStore(payload?: Record<string, any>): TStore;\n}\n","import type { Context } from \"./base-context\";\n\n/**\n * Context Manager - Orchestrates multiple contexts together\n *\n * Allows running multiple AsyncLocalStorage contexts in a single operation,\n * making it easy to link request, storage, database, and other contexts.\n *\n * @example\n * ```typescript\n * // Register contexts\n * contextManager\n * .register('request', requestContext)\n * .register('storage', storageDriverContext)\n * .register('database', databaseDataSourceContext);\n *\n * // Run all contexts together\n * await contextManager.runAll({\n * request: { request, response, user },\n * storage: { driver, metadata: { tenantId: '123' } },\n * database: { dataSource: 'primary' },\n * }, async () => {\n * // All contexts active!\n * await handleRequest();\n * });\n * ```\n */\nexport class ContextManager {\n private contexts = new Map<string, Context<any>>();\n\n /**\n * Register a context\n *\n * @param name - Unique context name\n * @param context - Context instance\n * @returns This instance for chaining\n */\n public register(name: string, context: Context<any>): this {\n this.contexts.set(name, context);\n return this;\n }\n\n /**\n * Run all registered contexts together\n *\n * Nests all context.run() calls, ensuring all contexts are active\n * for the duration of the callback.\n *\n * @param stores - Context stores keyed by context name\n * @param callback - Async function to execute\n * @returns Result of the callback\n */\n public async runAll<T>(stores: Record<string, any>, callback: () => Promise<T>): Promise<T> {\n const entries = Array.from(this.contexts.entries());\n\n // Build nested context runners\n const runner = entries.reduceRight((next, [name, context]) => {\n return () => context.run(stores[name] || {}, next);\n }, callback);\n\n return runner();\n }\n\n /**\n * Enter all contexts at once (for middleware)\n *\n * @param stores - Context stores keyed by context name\n */\n public enterAll(stores: Record<string, any>): void {\n for (const [name, context] of this.contexts.entries()) {\n if (stores[name]) {\n context.enter(stores[name]);\n }\n }\n }\n\n /**\n * Clear all contexts\n */\n public clearAll(): void {\n for (const context of this.contexts.values()) {\n context.clear();\n }\n }\n\n /**\n * Get a specific registered context\n *\n * @param name - Context name\n * @returns Context instance or undefined\n */\n public getContext<T extends Context<any>>(name: string): T | undefined {\n return this.contexts.get(name) as T | undefined;\n }\n\n /**\n * Check if a context is registered\n *\n * @param name - Context name\n * @returns True if context is registered\n */\n public hasContext(name: string): boolean {\n return this.contexts.has(name);\n }\n\n /**\n * Build all context stores by calling each context's buildStore() method\n *\n * This is the immutable pattern - returns a new record of stores.\n * Each context defines its own initialization logic.\n *\n * @param payload - Payload passed to each buildStore() (e.g., { request, response })\n * @returns Record of context name -> store data\n *\n * @example\n * ```typescript\n * const httpContextStore = contextManager.buildStores({ request, response });\n * await contextManager.runAll(httpContextStore, async () => { ... });\n * ```\n */\n public buildStores(payload?: Record<string, any>): Record<string, any> {\n const stores: Record<string, any> = {};\n\n for (const [name, context] of this.contexts.entries()) {\n stores[name] = context.buildStore(payload) ?? {};\n }\n\n return stores;\n }\n\n /**\n * Unregister a context\n *\n * @param name - Context name to remove\n * @returns True if context was removed\n */\n public unregister(name: string): boolean {\n return this.contexts.delete(name);\n }\n}\n\n/**\n * Global context manager instance\n *\n * Use this singleton to register and manage all framework contexts.\n */\nexport const contextManager = new ContextManager();\n"],"mappings":";;;;;;;;;;;;;AAWA,MAAM,iBAAiB;CAAC;CAAa;CAAe;AAAW;;;;;;;;;;;;;;;AAgB/D,SAAS,qBAAwB,SAAe;CAC9C,IAAI,CAAC,WAAW,OAAO,YAAY,UAAU,OAAO;CAMpD,IAAI,CAJoB,eAAe,MAAM,QAC3C,OAAO,UAAU,eAAe,KAAK,SAAS,GAAG,CAGhC,GAAG,OAAO;CAE7B,MAAM,OAA4B,EAAE,GAAI,QAAgC;CAExE,KAAK,MAAM,OAAO,gBAChB,OAAO,KAAK;CAGd,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,IAAsB,UAAtB,MAAkE;;iBACR,IAAIA,8BAA0B;;;;;;;;;;;;CAYtF,AAAO,IAAO,OAAe,UAAwC;EACnE,OAAO,KAAK,QAAQ,IAAI,OAAO,QAAQ;CACzC;;;;;;;;;CAUA,AAAO,MAAM,OAAqB;EAChC,KAAK,QAAQ,UAAU,KAAK;CAC9B;;;;;;;;;;;;;;;CAgBA,AAAO,OAAO,SAAgC;EAC5C,MAAM,cAAc,qBAAqB,OAAO;EAEhD,MAAM,UAAU,KAAK,QAAQ,SAAS;EAEtC,IAAI,SACF,OAAO,OAAO,SAAS,WAAW;OAElC,KAAK,MAAM,WAAqB;CAEpC;;;;;;CAOA,AAAO,WAA+B;EACpC,OAAO,KAAK,QAAQ,SAAS;CAC/B;;;;;;;CAQA,AAAO,IAA4B,KAA+B;EAChE,OAAO,KAAK,QAAQ,SAAS,CAAC,GAAG;CACnC;;;;;;;;;;CAWA,AAAO,IAA4B,KAAQ,OAAwB;EACjE,KAAK,OAAO,GAAG,MAAM,MAAM,CAAQ;CACrC;;;;CAKA,AAAO,QAAc;EACnB,KAAK,QAAQ,UAAU,CAAC,CAAW;CACrC;;;;CAKA,AAAO,aAAsB;EAC3B,OAAO,KAAK,QAAQ,SAAS,MAAM;CACrC;AAYF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACzJA,IAAa,iBAAb,MAA4B;;kCACP,IAAI,IAA0B;;;;;;;;;CASjD,AAAO,SAAS,MAAc,SAA6B;EACzD,KAAK,SAAS,IAAI,MAAM,OAAO;EAC/B,OAAO;CACT;;;;;;;;;;;CAYA,MAAa,OAAU,QAA6B,UAAwC;EAQ1F,OAPgB,MAAM,KAAK,KAAK,SAAS,QAAQ,CAG5B,CAAC,CAAC,aAAa,MAAM,CAAC,MAAM,aAAa;GAC5D,aAAa,QAAQ,IAAI,OAAO,SAAS,CAAC,GAAG,IAAI;EACnD,GAAG,QAES,CAAC,CAAC;CAChB;;;;;;CAOA,AAAO,SAAS,QAAmC;EACjD,KAAK,MAAM,CAAC,MAAM,YAAY,KAAK,SAAS,QAAQ,GAClD,IAAI,OAAO,OACT,QAAQ,MAAM,OAAO,KAAK;CAGhC;;;;CAKA,AAAO,WAAiB;EACtB,KAAK,MAAM,WAAW,KAAK,SAAS,OAAO,GACzC,QAAQ,MAAM;CAElB;;;;;;;CAQA,AAAO,WAAmC,MAA6B;EACrE,OAAO,KAAK,SAAS,IAAI,IAAI;CAC/B;;;;;;;CAQA,AAAO,WAAW,MAAuB;EACvC,OAAO,KAAK,SAAS,IAAI,IAAI;CAC/B;;;;;;;;;;;;;;;;CAiBA,AAAO,YAAY,SAAoD;EACrE,MAAM,SAA8B,CAAC;EAErC,KAAK,MAAM,CAAC,MAAM,YAAY,KAAK,SAAS,QAAQ,GAClD,OAAO,QAAQ,QAAQ,WAAW,OAAO,KAAK,CAAC;EAGjD,OAAO;CACT;;;;;;;CAQA,AAAO,WAAW,MAAuB;EACvC,OAAO,KAAK,SAAS,OAAO,IAAI;CAClC;AACF;;;;;;AAOA,MAAa,iBAAiB,IAAI,eAAe"}
@@ -1 +1 @@
1
- {"version":3,"file":"base-context.mjs","names":[],"sources":["../../../../../../../packages/context/src/base-context.ts"],"sourcesContent":["import { AsyncLocalStorage } from \"async_hooks\";\n\n/**\n * Keys that reach `Object.prototype` instead of the store when assigned.\n *\n * `Object.assign(store, { __proto__: {...} })` does not create a property — it\n * invokes the inherited `__proto__` setter and reparents the store, which for a\n * shared context store means every later property lookup in the process can\n * resolve through attacker-supplied data. `constructor` / `prototype` are the\n * neighbouring rungs of the same ladder.\n */\nconst DANGEROUS_KEYS = [\"__proto__\", \"constructor\", \"prototype\"] as const;\n\n/**\n * Drop prototype-poisoning keys from a merge payload.\n *\n * Contexts are frequently fed request-shaped data (`context.update(req.body)`,\n * `context.set(key, value)` with a caller-supplied key), so the merge path is\n * treated as an untrusted boundary: dangerous keys are dropped rather than\n * throwing, since a request that trips this must not be able to take the\n * process down, and the store simply never carries them.\n *\n * The common case allocates nothing — a copy is made only when a dangerous key\n * is actually present. The copy is built by spread (which defines properties\n * rather than assigning them, so it cannot trigger the setter itself) and\n * preserves symbol keys, which `Object.assign` would have copied.\n */\nfunction withoutDangerousKeys<T>(updates: T): T {\n if (!updates || typeof updates !== \"object\") return updates;\n\n const hasDangerousKey = DANGEROUS_KEYS.some(key =>\n Object.prototype.hasOwnProperty.call(updates, key),\n );\n\n if (!hasDangerousKey) return updates;\n\n const safe: Record<string, any> = { ...(updates as Record<string, any>) };\n\n for (const key of DANGEROUS_KEYS) {\n delete safe[key];\n }\n\n return safe as T;\n}\n\n/**\n * Base class for all AsyncLocalStorage-based contexts\n *\n * Provides a consistent API for managing context across async operations.\n * All framework contexts (request, storage, database) extend this class.\n *\n * @template TStore - The type of data stored in context\n *\n * @example\n * ```typescript\n * interface MyContextStore {\n * userId: string;\n * tenant: string;\n * }\n *\n * class MyContext extends Context<MyContextStore> {}\n * const myContext = new MyContext();\n *\n * // Use it\n * await myContext.run({ userId: '123', tenant: 'acme' }, async () => {\n * const userId = myContext.get('userId'); // '123'\n * });\n * ```\n */\nexport abstract class Context<TStore extends Record<string, any>> {\n protected readonly storage: AsyncLocalStorage<TStore> = new AsyncLocalStorage<TStore>();\n\n /**\n * Run a callback within a new context\n *\n * Creates a new async context with the provided store data.\n * All operations within the callback will have access to this context.\n *\n * @param store - Initial context data\n * @param callback - Async function to execute\n * @returns Result of the callback\n */\n public run<T>(store: TStore, callback: () => Promise<T>): Promise<T> {\n return this.storage.run(store, callback);\n }\n\n /**\n * Enter a new context without a callback\n *\n * Useful for middleware where you want to set context for the rest of the request.\n * Unlike `run()`, this doesn't require a callback.\n *\n * @param store - Context data to set\n */\n public enter(store: TStore): void {\n this.storage.enterWith(store);\n }\n\n /**\n * Update the current context\n *\n * Merges new data into existing context, or enters new context if none exists.\n *\n * Keys named `__proto__`, `constructor`, or `prototype` are dropped: the\n * merge target is a store shared by everything running in this async chain,\n * so letting those through would let one caller's payload change property\n * resolution for the whole process. Passing unvalidated input here is still\n * discouraged — this guard closes the prototype vector, not the \"an attacker\n * chose what your context says\" one.\n *\n * @param updates - Partial context data to merge\n */\n public update(updates: Partial<TStore>): void {\n const safeUpdates = withoutDangerousKeys(updates);\n\n const current = this.storage.getStore();\n\n if (current) {\n Object.assign(current, safeUpdates);\n } else {\n this.enter(safeUpdates as TStore);\n }\n }\n\n /**\n * Get the current context store\n *\n * @returns Current context or undefined if not in context\n */\n public getStore(): TStore | undefined {\n return this.storage.getStore();\n }\n\n /**\n * Get a specific value from context\n *\n * @param key - Key to retrieve\n * @returns Value or undefined\n */\n public get<K extends keyof TStore>(key: K): TStore[K] | undefined {\n return this.storage.getStore()?.[key];\n }\n\n /**\n * Set a specific value in context\n *\n * Goes through `update()`, so a caller-supplied `key` of `__proto__` /\n * `constructor` / `prototype` is dropped rather than reparenting the store.\n *\n * @param key - Key to set\n * @param value - Value to store\n */\n public set<K extends keyof TStore>(key: K, value: TStore[K]): void {\n this.update({ [key]: value } as any);\n }\n\n /**\n * Clear the context\n */\n public clear(): void {\n this.storage.enterWith({} as TStore);\n }\n\n /**\n * Check if currently in a context\n */\n public hasContext(): boolean {\n return this.storage.getStore() !== undefined;\n }\n\n /**\n * Build the initial store for this context\n *\n * Override this method to provide custom initialization logic.\n * Called by ContextManager.buildStores() for each registered context.\n *\n * @param payload - Generic payload (e.g., { request, response } for HTTP contexts)\n * @returns Initial store data\n */\n public abstract buildStore(payload?: Record<string, any>): TStore;\n}\n"],"mappings":";;;;;;;;;;;;AAWA,MAAM,iBAAiB;CAAC;CAAa;CAAe;AAAW;;;;;;;;;;;;;;;AAgB/D,SAAS,qBAAwB,SAAe;CAC9C,IAAI,CAAC,WAAW,OAAO,YAAY,UAAU,OAAO;CAMpD,IAAI,CAJoB,eAAe,MAAK,QAC1C,OAAO,UAAU,eAAe,KAAK,SAAS,GAAG,CAGhC,GAAG,OAAO;CAE7B,MAAM,OAA4B,EAAE,GAAI,QAAgC;CAExE,KAAK,MAAM,OAAO,gBAChB,OAAO,KAAK;CAGd,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,IAAsB,UAAtB,MAAkE;;iBACR,IAAI,kBAA0B;;;;;;;;;;;;CAYtF,AAAO,IAAO,OAAe,UAAwC;EACnE,OAAO,KAAK,QAAQ,IAAI,OAAO,QAAQ;CACzC;;;;;;;;;CAUA,AAAO,MAAM,OAAqB;EAChC,KAAK,QAAQ,UAAU,KAAK;CAC9B;;;;;;;;;;;;;;;CAgBA,AAAO,OAAO,SAAgC;EAC5C,MAAM,cAAc,qBAAqB,OAAO;EAEhD,MAAM,UAAU,KAAK,QAAQ,SAAS;EAEtC,IAAI,SACF,OAAO,OAAO,SAAS,WAAW;OAElC,KAAK,MAAM,WAAqB;CAEpC;;;;;;CAOA,AAAO,WAA+B;EACpC,OAAO,KAAK,QAAQ,SAAS;CAC/B;;;;;;;CAQA,AAAO,IAA4B,KAA+B;EAChE,OAAO,KAAK,QAAQ,SAAS,CAAC,GAAG;CACnC;;;;;;;;;;CAWA,AAAO,IAA4B,KAAQ,OAAwB;EACjE,KAAK,OAAO,GAAG,MAAM,MAAM,CAAQ;CACrC;;;;CAKA,AAAO,QAAc;EACnB,KAAK,QAAQ,UAAU,CAAC,CAAW;CACrC;;;;CAKA,AAAO,aAAsB;EAC3B,OAAO,KAAK,QAAQ,SAAS,MAAM;CACrC;AAYF"}
1
+ {"version":3,"file":"base-context.mjs","names":[],"sources":["../../../../../../../packages/context/src/base-context.ts"],"sourcesContent":["import { AsyncLocalStorage } from \"async_hooks\";\n\n/**\n * Keys that reach `Object.prototype` instead of the store when assigned.\n *\n * `Object.assign(store, { __proto__: {...} })` does not create a property — it\n * invokes the inherited `__proto__` setter and reparents the store, which for a\n * shared context store means every later property lookup in the process can\n * resolve through attacker-supplied data. `constructor` / `prototype` are the\n * neighbouring rungs of the same ladder.\n */\nconst DANGEROUS_KEYS = [\"__proto__\", \"constructor\", \"prototype\"] as const;\n\n/**\n * Drop prototype-poisoning keys from a merge payload.\n *\n * Contexts are frequently fed request-shaped data (`context.update(req.body)`,\n * `context.set(key, value)` with a caller-supplied key), so the merge path is\n * treated as an untrusted boundary: dangerous keys are dropped rather than\n * throwing, since a request that trips this must not be able to take the\n * process down, and the store simply never carries them.\n *\n * The common case allocates nothing — a copy is made only when a dangerous key\n * is actually present. The copy is built by spread (which defines properties\n * rather than assigning them, so it cannot trigger the setter itself) and\n * preserves symbol keys, which `Object.assign` would have copied.\n */\nfunction withoutDangerousKeys<T>(updates: T): T {\n if (!updates || typeof updates !== \"object\") return updates;\n\n const hasDangerousKey = DANGEROUS_KEYS.some((key) =>\n Object.prototype.hasOwnProperty.call(updates, key),\n );\n\n if (!hasDangerousKey) return updates;\n\n const safe: Record<string, any> = { ...(updates as Record<string, any>) };\n\n for (const key of DANGEROUS_KEYS) {\n delete safe[key];\n }\n\n return safe as T;\n}\n\n/**\n * Base class for all AsyncLocalStorage-based contexts\n *\n * Provides a consistent API for managing context across async operations.\n * All framework contexts (request, storage, database) extend this class.\n *\n * @template TStore - The type of data stored in context\n *\n * @example\n * ```typescript\n * interface MyContextStore {\n * userId: string;\n * tenant: string;\n * }\n *\n * class MyContext extends Context<MyContextStore> {}\n * const myContext = new MyContext();\n *\n * // Use it\n * await myContext.run({ userId: '123', tenant: 'acme' }, async () => {\n * const userId = myContext.get('userId'); // '123'\n * });\n * ```\n */\nexport abstract class Context<TStore extends Record<string, any>> {\n protected readonly storage: AsyncLocalStorage<TStore> = new AsyncLocalStorage<TStore>();\n\n /**\n * Run a callback within a new context\n *\n * Creates a new async context with the provided store data.\n * All operations within the callback will have access to this context.\n *\n * @param store - Initial context data\n * @param callback - Async function to execute\n * @returns Result of the callback\n */\n public run<T>(store: TStore, callback: () => Promise<T>): Promise<T> {\n return this.storage.run(store, callback);\n }\n\n /**\n * Enter a new context without a callback\n *\n * Useful for middleware where you want to set context for the rest of the request.\n * Unlike `run()`, this doesn't require a callback.\n *\n * @param store - Context data to set\n */\n public enter(store: TStore): void {\n this.storage.enterWith(store);\n }\n\n /**\n * Update the current context\n *\n * Merges new data into existing context, or enters new context if none exists.\n *\n * Keys named `__proto__`, `constructor`, or `prototype` are dropped: the\n * merge target is a store shared by everything running in this async chain,\n * so letting those through would let one caller's payload change property\n * resolution for the whole process. Passing unvalidated input here is still\n * discouraged — this guard closes the prototype vector, not the \"an attacker\n * chose what your context says\" one.\n *\n * @param updates - Partial context data to merge\n */\n public update(updates: Partial<TStore>): void {\n const safeUpdates = withoutDangerousKeys(updates);\n\n const current = this.storage.getStore();\n\n if (current) {\n Object.assign(current, safeUpdates);\n } else {\n this.enter(safeUpdates as TStore);\n }\n }\n\n /**\n * Get the current context store\n *\n * @returns Current context or undefined if not in context\n */\n public getStore(): TStore | undefined {\n return this.storage.getStore();\n }\n\n /**\n * Get a specific value from context\n *\n * @param key - Key to retrieve\n * @returns Value or undefined\n */\n public get<K extends keyof TStore>(key: K): TStore[K] | undefined {\n return this.storage.getStore()?.[key];\n }\n\n /**\n * Set a specific value in context\n *\n * Goes through `update()`, so a caller-supplied `key` of `__proto__` /\n * `constructor` / `prototype` is dropped rather than reparenting the store.\n *\n * @param key - Key to set\n * @param value - Value to store\n */\n public set<K extends keyof TStore>(key: K, value: TStore[K]): void {\n this.update({ [key]: value } as any);\n }\n\n /**\n * Clear the context\n */\n public clear(): void {\n this.storage.enterWith({} as TStore);\n }\n\n /**\n * Check if currently in a context\n */\n public hasContext(): boolean {\n return this.storage.getStore() !== undefined;\n }\n\n /**\n * Build the initial store for this context\n *\n * Override this method to provide custom initialization logic.\n * Called by ContextManager.buildStores() for each registered context.\n *\n * @param payload - Generic payload (e.g., { request, response } for HTTP contexts)\n * @returns Initial store data\n */\n public abstract buildStore(payload?: Record<string, any>): TStore;\n}\n"],"mappings":";;;;;;;;;;;;AAWA,MAAM,iBAAiB;CAAC;CAAa;CAAe;AAAW;;;;;;;;;;;;;;;AAgB/D,SAAS,qBAAwB,SAAe;CAC9C,IAAI,CAAC,WAAW,OAAO,YAAY,UAAU,OAAO;CAMpD,IAAI,CAJoB,eAAe,MAAM,QAC3C,OAAO,UAAU,eAAe,KAAK,SAAS,GAAG,CAGhC,GAAG,OAAO;CAE7B,MAAM,OAA4B,EAAE,GAAI,QAAgC;CAExE,KAAK,MAAM,OAAO,gBAChB,OAAO,KAAK;CAGd,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,IAAsB,UAAtB,MAAkE;;iBACR,IAAI,kBAA0B;;;;;;;;;;;;CAYtF,AAAO,IAAO,OAAe,UAAwC;EACnE,OAAO,KAAK,QAAQ,IAAI,OAAO,QAAQ;CACzC;;;;;;;;;CAUA,AAAO,MAAM,OAAqB;EAChC,KAAK,QAAQ,UAAU,KAAK;CAC9B;;;;;;;;;;;;;;;CAgBA,AAAO,OAAO,SAAgC;EAC5C,MAAM,cAAc,qBAAqB,OAAO;EAEhD,MAAM,UAAU,KAAK,QAAQ,SAAS;EAEtC,IAAI,SACF,OAAO,OAAO,SAAS,WAAW;OAElC,KAAK,MAAM,WAAqB;CAEpC;;;;;;CAOA,AAAO,WAA+B;EACpC,OAAO,KAAK,QAAQ,SAAS;CAC/B;;;;;;;CAQA,AAAO,IAA4B,KAA+B;EAChE,OAAO,KAAK,QAAQ,SAAS,CAAC,GAAG;CACnC;;;;;;;;;;CAWA,AAAO,IAA4B,KAAQ,OAAwB;EACjE,KAAK,OAAO,GAAG,MAAM,MAAM,CAAQ;CACrC;;;;CAKA,AAAO,QAAc;EACnB,KAAK,QAAQ,UAAU,CAAC,CAAW;CACrC;;;;CAKA,AAAO,aAAsB;EAC3B,OAAO,KAAK,QAAQ,SAAS,MAAM;CACrC;AAYF"}
package/llms-full.txt CHANGED
@@ -56,16 +56,13 @@ export const userContext = new UserContext();
56
56
  ### `run()` — scoped execution
57
57
 
58
58
  ```ts
59
- await userContext.run(
60
- { userId: "123", role: "admin", tenantId: "acme" },
61
- async () => {
62
- // Context is available throughout this async scope and any awaited calls inside it.
63
- const userId = userContext.get("userId"); // "123"
64
- const role = userContext.get("role"); // "admin"
65
-
66
- await someAsyncOperation(); // context propagates through awaits
67
- },
68
- );
59
+ await userContext.run({ userId: "123", role: "admin", tenantId: "acme" }, async () => {
60
+ // Context is available throughout this async scope and any awaited calls inside it.
61
+ const userId = userContext.get("userId"); // "123"
62
+ const role = userContext.get("role"); // "admin"
63
+
64
+ await someAsyncOperation(); // context propagates through awaits
65
+ });
69
66
  ```
70
67
 
71
68
  Use `run()` when you have a clear scope boundary (a request handler, a job, a CLI command). The context is auto-cleaned when the callback returns.
@@ -80,7 +77,7 @@ function authMiddleware(req, res, next) {
80
77
  tenantId: req.headers["x-tenant-id"],
81
78
  });
82
79
 
83
- next(); // context lives for the rest of the request
80
+ next(); // context lives for the rest of the request
84
81
  }
85
82
  ```
86
83
 
@@ -100,9 +97,9 @@ If there's no current store, `update` creates one with the partial (cast to the
100
97
  ## Reading
101
98
 
102
99
  ```ts
103
- const userId = userContext.get("userId"); // TStore[K] | undefined
104
- const store = userContext.getStore(); // TStore | undefined
105
- const inside = userContext.hasContext(); // boolean
100
+ const userId = userContext.get("userId"); // TStore[K] | undefined
101
+ const store = userContext.getStore(); // TStore | undefined
102
+ const inside = userContext.hasContext(); // boolean
106
103
  ```
107
104
 
108
105
  `get` is the daily-use accessor. `getStore` returns the whole record. `hasContext` distinguishes "key absent" from "no context at all" (which matters for safety checks at the framework boundary).
@@ -110,7 +107,7 @@ const inside = userContext.hasContext(); // boolean
110
107
  ## Writing within a context
111
108
 
112
109
  ```ts
113
- userContext.set("role", "admin"); // sugar for update({ role: "admin" })
110
+ userContext.set("role", "admin"); // sugar for update({ role: "admin" })
114
111
  ```
115
112
 
116
113
  Only call `set` inside an active context. Outside one, it enters a new context with just that key set — usually not what you want.
@@ -186,9 +183,7 @@ You can call `context.run(store, fn)` directly when you only have one context. W
186
183
  ```ts
187
184
  // ❌ Without the manager — fragile, easy to forget a layer
188
185
  await requestContext.run(reqStore, async () =>
189
- databaseContext.run(dbStore, async () =>
190
- tenantContext.run(tenantStore, async () => handle()),
191
- ),
186
+ databaseContext.run(dbStore, async () => tenantContext.run(tenantStore, async () => handle())),
192
187
  );
193
188
 
194
189
  // ✅ With the manager — one call, deterministic order
@@ -250,11 +245,11 @@ function contextMiddleware(req, res, next) {
250
245
  ## Lookup + introspection
251
246
 
252
247
  ```ts
253
- contextManager.hasContext("tenant"); // boolean
248
+ contextManager.hasContext("tenant"); // boolean
254
249
  const tenant = contextManager.getContext<TenantContext>("tenant");
255
250
  // returns the registered instance or undefined
256
- contextManager.unregister("debug"); // remove a context
257
- contextManager.clearAll(); // clear stores on every context
251
+ contextManager.unregister("debug"); // remove a context
252
+ contextManager.clearAll(); // clear stores on every context
258
253
  ```
259
254
 
260
255
  `getContext<T>` returns the registered instance cast to `T` (the generic is constrained to `Context<any>`, so pass the concrete context class — `getContext<TenantContext>("tenant")`). It's an unchecked cast: an unknown `name` returns `undefined`, and a wrong type argument won't be caught at runtime. Useful in shared utilities that want to read from a context without importing it at the call site.
@@ -265,10 +260,10 @@ contextManager.clearAll(); // clear stores on every co
265
260
 
266
261
  ```ts
267
262
  contextManager
268
- .register("trace", traceContext) // outermost — runs first
263
+ .register("trace", traceContext) // outermost — runs first
269
264
  .register("request", requestContext)
270
265
  .register("database", databaseContext)
271
- .register("tenant", tenantContext); // innermost — runs last, inside all others
266
+ .register("tenant", tenantContext); // innermost — runs last, inside all others
272
267
  ```
273
268
 
274
269
  ## Real-world: multi-tenant request lifecycle
@@ -379,17 +374,18 @@ singleton and run them all together with a single call. Covers
379
374
  `clearAll`, `getContext` / `hasContext`.
380
375
 
381
376
  Load when several contexts apply to the same scope (request + database
382
- + trace + tenant) and you'd otherwise nest `run()` calls by hand.
377
+
378
+ - trace + tenant) and you'd otherwise nest `run()` calls by hand.
383
379
 
384
380
  ## Two operating modes — when to pick which
385
381
 
386
- | Situation | Reach for |
387
- | --- | --- |
388
- | Framework gives you a callback to wrap | `context.run(store, callback)` |
389
- | Framework expects middleware that returns synchronously (Express, etc.) | `context.enter(store)` |
390
- | You have 2+ contexts active for the same request | `contextManager.runAll(stores, callback)` |
391
- | Middleware-style with 2+ contexts | `contextManager.enterAll(stores)` then `clearAll()` at end |
392
- | You only have one context and the framework cooperates | Single `context.run(...)`. Skip the manager. |
382
+ | Situation | Reach for |
383
+ | ----------------------------------------------------------------------- | ---------------------------------------------------------- |
384
+ | Framework gives you a callback to wrap | `context.run(store, callback)` |
385
+ | Framework expects middleware that returns synchronously (Express, etc.) | `context.enter(store)` |
386
+ | You have 2+ contexts active for the same request | `contextManager.runAll(stores, callback)` |
387
+ | Middleware-style with 2+ contexts | `contextManager.enterAll(stores)` then `clearAll()` at end |
388
+ | You only have one context and the framework cooperates | Single `context.run(...)`. Skip the manager. |
393
389
 
394
390
  ## What this package deliberately doesn't do
395
391
 
package/package.json CHANGED
@@ -25,7 +25,7 @@
25
25
  "engines": {
26
26
  "node": ">=18.0.0"
27
27
  },
28
- "version": "5.30.0",
28
+ "version": "5.31.0",
29
29
  "main": "./cjs/index.cjs",
30
30
  "module": "./esm/index.mjs",
31
31
  "types": "./esm/index.d.mts",
@@ -48,16 +48,13 @@ export const userContext = new UserContext();
48
48
  ### `run()` — scoped execution
49
49
 
50
50
  ```ts
51
- await userContext.run(
52
- { userId: "123", role: "admin", tenantId: "acme" },
53
- async () => {
54
- // Context is available throughout this async scope and any awaited calls inside it.
55
- const userId = userContext.get("userId"); // "123"
56
- const role = userContext.get("role"); // "admin"
57
-
58
- await someAsyncOperation(); // context propagates through awaits
59
- },
60
- );
51
+ await userContext.run({ userId: "123", role: "admin", tenantId: "acme" }, async () => {
52
+ // Context is available throughout this async scope and any awaited calls inside it.
53
+ const userId = userContext.get("userId"); // "123"
54
+ const role = userContext.get("role"); // "admin"
55
+
56
+ await someAsyncOperation(); // context propagates through awaits
57
+ });
61
58
  ```
62
59
 
63
60
  Use `run()` when you have a clear scope boundary (a request handler, a job, a CLI command). The context is auto-cleaned when the callback returns.
@@ -72,7 +69,7 @@ function authMiddleware(req, res, next) {
72
69
  tenantId: req.headers["x-tenant-id"],
73
70
  });
74
71
 
75
- next(); // context lives for the rest of the request
72
+ next(); // context lives for the rest of the request
76
73
  }
77
74
  ```
78
75
 
@@ -92,9 +89,9 @@ If there's no current store, `update` creates one with the partial (cast to the
92
89
  ## Reading
93
90
 
94
91
  ```ts
95
- const userId = userContext.get("userId"); // TStore[K] | undefined
96
- const store = userContext.getStore(); // TStore | undefined
97
- const inside = userContext.hasContext(); // boolean
92
+ const userId = userContext.get("userId"); // TStore[K] | undefined
93
+ const store = userContext.getStore(); // TStore | undefined
94
+ const inside = userContext.hasContext(); // boolean
98
95
  ```
99
96
 
100
97
  `get` is the daily-use accessor. `getStore` returns the whole record. `hasContext` distinguishes "key absent" from "no context at all" (which matters for safety checks at the framework boundary).
@@ -102,7 +99,7 @@ const inside = userContext.hasContext(); // boolean
102
99
  ## Writing within a context
103
100
 
104
101
  ```ts
105
- userContext.set("role", "admin"); // sugar for update({ role: "admin" })
102
+ userContext.set("role", "admin"); // sugar for update({ role: "admin" })
106
103
  ```
107
104
 
108
105
  Only call `set` inside an active context. Outside one, it enters a new context with just that key set — usually not what you want.
package/skills/index.md CHANGED
@@ -1,17 +1,20 @@
1
1
  ---
2
- description: "Typed request-scoped context on Node AsyncLocalStorage, so data (user, tenant, trace id, transaction) flows through awaits without parameter threading. Exports `Context`, `contextManager`, `ContextManager`. Use for: \"share the current user or tenant across async calls\", \"request-scoped storage\", \"AsyncLocalStorage wrapper\", \"run several contexts together\", \"avoid passing data through every function\". Not this package: cross-request state → @warlock.js/cache or a database; HTTP request/response objects → @warlock.js/core."
2
+ description: 'Typed request-scoped context on Node AsyncLocalStorage, so data (user, tenant, trace id, transaction) flows through awaits without parameter threading. Exports `Context`, `contextManager`, `ContextManager`. Use for: "share the current user or tenant across async calls", "request-scoped storage", "AsyncLocalStorage wrapper", "run several contexts together", "avoid passing data through every function". Not this package: cross-request state → @warlock.js/cache or a database; HTTP request/response objects → @warlock.js/core.'
3
3
  ---
4
+
4
5
  # @warlock.js/context
5
6
 
6
7
  A thin typed wrapper over `AsyncLocalStorage`. Each `Context<TStore>` subclass declares its store shape and how `buildStore` creates it; the store lives for the scope and propagates through every `await` inside. `contextManager` runs several contexts together. Zero dependencies; the framework uses it internally for request, user and tenant.
7
8
 
8
9
  ## The 80% path
10
+
9
11
  1. Orient with `overview.md`.
10
12
  2. Subclass `Context`, implement `buildStore`, read with `get` / `set` anywhere down the call tree: `define-context.md`.
11
13
  3. Wrap work with `run(store, callback)`, or `enter(store)` for middleware that cannot wrap a callback: `define-context.md`.
12
14
  4. With two or more active contexts, register them on `contextManager` and use `runAll` / `enterAll`: `orchestrate-contexts.md`.
13
15
 
14
16
  ## Conventions and pitfalls
17
+
15
18
  - Outside a `run`/`enter` scope there is no store; guard with `hasContext` when code may run unscoped.
16
19
  - `enter` leaks until cleared; pair `enterAll` with `clearAll` at the end of the request.
17
20
  - Worker threads do not share the store; serialize data across and re-enter.
@@ -14,9 +14,7 @@ You can call `context.run(store, fn)` directly when you only have one context. W
14
14
  ```ts
15
15
  // ❌ Without the manager — fragile, easy to forget a layer
16
16
  await requestContext.run(reqStore, async () =>
17
- databaseContext.run(dbStore, async () =>
18
- tenantContext.run(tenantStore, async () => handle()),
19
- ),
17
+ databaseContext.run(dbStore, async () => tenantContext.run(tenantStore, async () => handle())),
20
18
  );
21
19
 
22
20
  // ✅ With the manager — one call, deterministic order
@@ -78,11 +76,11 @@ function contextMiddleware(req, res, next) {
78
76
  ## Lookup + introspection
79
77
 
80
78
  ```ts
81
- contextManager.hasContext("tenant"); // boolean
79
+ contextManager.hasContext("tenant"); // boolean
82
80
  const tenant = contextManager.getContext<TenantContext>("tenant");
83
81
  // returns the registered instance or undefined
84
- contextManager.unregister("debug"); // remove a context
85
- contextManager.clearAll(); // clear stores on every context
82
+ contextManager.unregister("debug"); // remove a context
83
+ contextManager.clearAll(); // clear stores on every context
86
84
  ```
87
85
 
88
86
  `getContext<T>` returns the registered instance cast to `T` (the generic is constrained to `Context<any>`, so pass the concrete context class — `getContext<TenantContext>("tenant")`). It's an unchecked cast: an unknown `name` returns `undefined`, and a wrong type argument won't be caught at runtime. Useful in shared utilities that want to read from a context without importing it at the call site.
@@ -93,10 +91,10 @@ contextManager.clearAll(); // clear stores on every co
93
91
 
94
92
  ```ts
95
93
  contextManager
96
- .register("trace", traceContext) // outermost — runs first
94
+ .register("trace", traceContext) // outermost — runs first
97
95
  .register("request", requestContext)
98
96
  .register("database", databaseContext)
99
- .register("tenant", tenantContext); // innermost — runs last, inside all others
97
+ .register("tenant", tenantContext); // innermost — runs last, inside all others
100
98
  ```
101
99
 
102
100
  ## Real-world: multi-tenant request lifecycle
@@ -42,17 +42,18 @@ singleton and run them all together with a single call. Covers
42
42
  `clearAll`, `getContext` / `hasContext`.
43
43
 
44
44
  Load when several contexts apply to the same scope (request + database
45
- + trace + tenant) and you'd otherwise nest `run()` calls by hand.
45
+
46
+ - trace + tenant) and you'd otherwise nest `run()` calls by hand.
46
47
 
47
48
  ## Two operating modes — when to pick which
48
49
 
49
- | Situation | Reach for |
50
- | --- | --- |
51
- | Framework gives you a callback to wrap | `context.run(store, callback)` |
52
- | Framework expects middleware that returns synchronously (Express, etc.) | `context.enter(store)` |
53
- | You have 2+ contexts active for the same request | `contextManager.runAll(stores, callback)` |
54
- | Middleware-style with 2+ contexts | `contextManager.enterAll(stores)` then `clearAll()` at end |
55
- | You only have one context and the framework cooperates | Single `context.run(...)`. Skip the manager. |
50
+ | Situation | Reach for |
51
+ | ----------------------------------------------------------------------- | ---------------------------------------------------------- |
52
+ | Framework gives you a callback to wrap | `context.run(store, callback)` |
53
+ | Framework expects middleware that returns synchronously (Express, etc.) | `context.enter(store)` |
54
+ | You have 2+ contexts active for the same request | `contextManager.runAll(stores, callback)` |
55
+ | Middleware-style with 2+ contexts | `contextManager.enterAll(stores)` then `clearAll()` at end |
56
+ | You only have one context and the framework cooperates | Single `context.run(...)`. Skip the manager. |
56
57
 
57
58
  ## What this package deliberately doesn't do
58
59