@warlock.js/context 4.15.0 → 5.0.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,22 @@ 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.0.0 - 2026-08-25
8
+
9
+ ### Changed
10
+
11
+ - This package is unchanged in 5.0.0; its version moved only because the Warlock family releases in lockstep.
12
+
13
+ ## 4.16.0 - 2026-08-18
14
+
15
+ ### Security
16
+
17
+ - **`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
18
+
19
+ 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
20
+
21
+ 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
22
+
7
23
  ## 4.12.0
8
24
 
9
25
  ### Changed
package/cjs/index.cjs CHANGED
@@ -3,6 +3,41 @@ let async_hooks = require("async_hooks");
3
3
 
4
4
  //#region ../context/src/base-context.ts
5
5
  /**
6
+ * Keys that reach `Object.prototype` instead of the store when assigned.
7
+ *
8
+ * `Object.assign(store, { __proto__: {...} })` does not create a property — it
9
+ * invokes the inherited `__proto__` setter and reparents the store, which for a
10
+ * shared context store means every later property lookup in the process can
11
+ * resolve through attacker-supplied data. `constructor` / `prototype` are the
12
+ * neighbouring rungs of the same ladder.
13
+ */
14
+ const DANGEROUS_KEYS = [
15
+ "__proto__",
16
+ "constructor",
17
+ "prototype"
18
+ ];
19
+ /**
20
+ * Drop prototype-poisoning keys from a merge payload.
21
+ *
22
+ * Contexts are frequently fed request-shaped data (`context.update(req.body)`,
23
+ * `context.set(key, value)` with a caller-supplied key), so the merge path is
24
+ * treated as an untrusted boundary: dangerous keys are dropped rather than
25
+ * throwing, since a request that trips this must not be able to take the
26
+ * process down, and the store simply never carries them.
27
+ *
28
+ * The common case allocates nothing — a copy is made only when a dangerous key
29
+ * is actually present. The copy is built by spread (which defines properties
30
+ * rather than assigning them, so it cannot trigger the setter itself) and
31
+ * preserves symbol keys, which `Object.assign` would have copied.
32
+ */
33
+ function withoutDangerousKeys(updates) {
34
+ if (!updates || typeof updates !== "object") return updates;
35
+ if (!DANGEROUS_KEYS.some((key) => Object.prototype.hasOwnProperty.call(updates, key))) return updates;
36
+ const safe = { ...updates };
37
+ for (const key of DANGEROUS_KEYS) delete safe[key];
38
+ return safe;
39
+ }
40
+ /**
6
41
  * Base class for all AsyncLocalStorage-based contexts
7
42
  *
8
43
  * Provides a consistent API for managing context across async operations.
@@ -59,12 +94,20 @@ var Context = class {
59
94
  *
60
95
  * Merges new data into existing context, or enters new context if none exists.
61
96
  *
97
+ * Keys named `__proto__`, `constructor`, or `prototype` are dropped: the
98
+ * merge target is a store shared by everything running in this async chain,
99
+ * so letting those through would let one caller's payload change property
100
+ * resolution for the whole process. Passing unvalidated input here is still
101
+ * discouraged — this guard closes the prototype vector, not the "an attacker
102
+ * chose what your context says" one.
103
+ *
62
104
  * @param updates - Partial context data to merge
63
105
  */
64
106
  update(updates) {
107
+ const safeUpdates = withoutDangerousKeys(updates);
65
108
  const current = this.storage.getStore();
66
- if (current) Object.assign(current, updates);
67
- else this.enter(updates);
109
+ if (current) Object.assign(current, safeUpdates);
110
+ else this.enter(safeUpdates);
68
111
  }
69
112
  /**
70
113
  * Get the current context store
@@ -86,6 +129,9 @@ var Context = class {
86
129
  /**
87
130
  * Set a specific value in context
88
131
  *
132
+ * Goes through `update()`, so a caller-supplied `key` of `__proto__` /
133
+ * `constructor` / `prototype` is dropped rather than reparenting the store.
134
+ *
89
135
  * @param key - Key to set
90
136
  * @param value - Value to store
91
137
  */
package/cjs/index.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["AsyncLocalStorage"],"sources":["../../../../../../context/src/base-context.ts","../../../../../../context/src/context-manager.ts"],"sourcesContent":["import { AsyncLocalStorage } from \"async_hooks\";\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 * @param updates - Partial context data to merge\n */\n public update(updates: Partial<TStore>): void {\n const current = this.storage.getStore();\n\n if (current) {\n Object.assign(current, updates);\n } else {\n this.enter(updates 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 * @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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;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;;;;;;;;CASA,AAAO,OAAO,SAAgC;EAC5C,MAAM,UAAU,KAAK,QAAQ,SAAS;EAEtC,IAAI,SACF,OAAO,OAAO,SAAS,OAAO;OAE9B,KAAK,MAAM,OAAiB;CAEhC;;;;;;CAOA,AAAO,WAA+B;EACpC,OAAO,KAAK,QAAQ,SAAS;CAC/B;;;;;;;CAQA,AAAO,IAA4B,KAA+B;EAChE,OAAO,KAAK,QAAQ,SAAS,CAAC,GAAG;CACnC;;;;;;;CAQA,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AClGA,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":["../../../../../../context/src/base-context.ts","../../../../../../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"}
@@ -52,6 +52,13 @@ declare abstract class Context<TStore extends Record<string, any>> {
52
52
  *
53
53
  * Merges new data into existing context, or enters new context if none exists.
54
54
  *
55
+ * Keys named `__proto__`, `constructor`, or `prototype` are dropped: the
56
+ * merge target is a store shared by everything running in this async chain,
57
+ * so letting those through would let one caller's payload change property
58
+ * resolution for the whole process. Passing unvalidated input here is still
59
+ * discouraged — this guard closes the prototype vector, not the "an attacker
60
+ * chose what your context says" one.
61
+ *
55
62
  * @param updates - Partial context data to merge
56
63
  */
57
64
  update(updates: Partial<TStore>): void;
@@ -71,6 +78,9 @@ declare abstract class Context<TStore extends Record<string, any>> {
71
78
  /**
72
79
  * Set a specific value in context
73
80
  *
81
+ * Goes through `update()`, so a caller-supplied `key` of `__proto__` /
82
+ * `constructor` / `prototype` is dropped rather than reparenting the store.
83
+ *
74
84
  * @param key - Key to set
75
85
  * @param value - Value to store
76
86
  */
@@ -1 +1 @@
1
- {"version":3,"file":"base-context.d.mts","names":[],"sources":["../../../../../../context/src/base-context.ts"],"mappings":";;;;;AA0BA;;;;;;;;;;;;;;;;;;;;;;uBAAsB,OAAA,gBAAuB,MAAA;EAAA,mBACxB,OAAA,EAAS,iBAAA,CAAkB,MAAA;EAiGa;;;;;;;;;;EArFpD,GAAA,IAAO,KAAA,EAAO,MAAA,EAAQ,QAAA,QAAgB,OAAA,CAAQ,CAAA,IAAK,OAAA,CAAQ,CAAA;EAApD;;;;;;;;EAYP,KAAA,CAAM,KAAA,EAAO,MAAA;EAWb;;;;;;;EAAA,MAAA,CAAO,OAAA,EAAS,OAAA,CAAQ,MAAA;EAyBJ;;;;;EAVpB,QAAA,IAAY,MAAA;EAoBR;;;;;;EAVJ,GAAA,iBAAoB,MAAA,EAAQ,GAAA,EAAK,CAAA,GAAI,MAAA,CAAO,CAAA;EAiB5C;;;;;;EAPA,GAAA,iBAAoB,MAAA,EAAQ,GAAA,EAAK,CAAA,EAAG,KAAA,EAAO,MAAA,CAAO,CAAA;EA2BQ;;;EApB1D,KAAA;;;;EAOA,UAAA;;;;;;;;;;WAaS,UAAA,CAAW,OAAA,GAAU,MAAA,gBAAsB,MAAA;AAAA"}
1
+ {"version":3,"file":"base-context.d.mts","names":[],"sources":["../../../../../../context/src/base-context.ts"],"mappings":";;;;;AAqEA;;;;;;;;;;;;;;;;;;;;;;uBAAsB,OAAA,gBAAuB,MAAA;EAAA,mBACxB,OAAA,EAAS,iBAAA,CAAkB,MAAA;EA6Ga;;;;;;;;;;EAjGpD,GAAA,IAAO,KAAA,EAAO,MAAA,EAAQ,QAAA,QAAgB,OAAA,CAAQ,CAAA,IAAK,OAAA,CAAQ,CAAA;EAApD;;;;;;;;EAYP,KAAA,CAAM,KAAA,EAAO,MAAA;EAkBb;;;;;;;;;;;;;;EAAA,MAAA,CAAO,OAAA,EAAS,OAAA,CAAQ,MAAA;EAwCJ;;;;;EAvBpB,QAAA,IAAY,MAAA;EA8BZ;;;;;;EApBA,GAAA,iBAAoB,MAAA,EAAQ,GAAA,EAAK,CAAA,GAAI,MAAA,CAAO,CAAA;EAwCc;;;;;;;;;EA3B1D,GAAA,iBAAoB,MAAA,EAAQ,GAAA,EAAK,CAAA,EAAG,KAAA,EAAO,MAAA,CAAO,CAAA;;;;EAOlD,KAAA;;;;EAOA,UAAA;;;;;;;;;;WAaS,UAAA,CAAW,OAAA,GAAU,MAAA,gBAAsB,MAAA;AAAA"}
@@ -2,6 +2,41 @@ import { AsyncLocalStorage } from "async_hooks";
2
2
 
3
3
  //#region ../context/src/base-context.ts
4
4
  /**
5
+ * Keys that reach `Object.prototype` instead of the store when assigned.
6
+ *
7
+ * `Object.assign(store, { __proto__: {...} })` does not create a property — it
8
+ * invokes the inherited `__proto__` setter and reparents the store, which for a
9
+ * shared context store means every later property lookup in the process can
10
+ * resolve through attacker-supplied data. `constructor` / `prototype` are the
11
+ * neighbouring rungs of the same ladder.
12
+ */
13
+ const DANGEROUS_KEYS = [
14
+ "__proto__",
15
+ "constructor",
16
+ "prototype"
17
+ ];
18
+ /**
19
+ * Drop prototype-poisoning keys from a merge payload.
20
+ *
21
+ * Contexts are frequently fed request-shaped data (`context.update(req.body)`,
22
+ * `context.set(key, value)` with a caller-supplied key), so the merge path is
23
+ * treated as an untrusted boundary: dangerous keys are dropped rather than
24
+ * throwing, since a request that trips this must not be able to take the
25
+ * process down, and the store simply never carries them.
26
+ *
27
+ * The common case allocates nothing — a copy is made only when a dangerous key
28
+ * is actually present. The copy is built by spread (which defines properties
29
+ * rather than assigning them, so it cannot trigger the setter itself) and
30
+ * preserves symbol keys, which `Object.assign` would have copied.
31
+ */
32
+ function withoutDangerousKeys(updates) {
33
+ if (!updates || typeof updates !== "object") return updates;
34
+ if (!DANGEROUS_KEYS.some((key) => Object.prototype.hasOwnProperty.call(updates, key))) return updates;
35
+ const safe = { ...updates };
36
+ for (const key of DANGEROUS_KEYS) delete safe[key];
37
+ return safe;
38
+ }
39
+ /**
5
40
  * Base class for all AsyncLocalStorage-based contexts
6
41
  *
7
42
  * Provides a consistent API for managing context across async operations.
@@ -58,12 +93,20 @@ var Context = class {
58
93
  *
59
94
  * Merges new data into existing context, or enters new context if none exists.
60
95
  *
96
+ * Keys named `__proto__`, `constructor`, or `prototype` are dropped: the
97
+ * merge target is a store shared by everything running in this async chain,
98
+ * so letting those through would let one caller's payload change property
99
+ * resolution for the whole process. Passing unvalidated input here is still
100
+ * discouraged — this guard closes the prototype vector, not the "an attacker
101
+ * chose what your context says" one.
102
+ *
61
103
  * @param updates - Partial context data to merge
62
104
  */
63
105
  update(updates) {
106
+ const safeUpdates = withoutDangerousKeys(updates);
64
107
  const current = this.storage.getStore();
65
- if (current) Object.assign(current, updates);
66
- else this.enter(updates);
108
+ if (current) Object.assign(current, safeUpdates);
109
+ else this.enter(safeUpdates);
67
110
  }
68
111
  /**
69
112
  * Get the current context store
@@ -85,6 +128,9 @@ var Context = class {
85
128
  /**
86
129
  * Set a specific value in context
87
130
  *
131
+ * Goes through `update()`, so a caller-supplied `key` of `__proto__` /
132
+ * `constructor` / `prototype` is dropped rather than reparenting the store.
133
+ *
88
134
  * @param key - Key to set
89
135
  * @param value - Value to store
90
136
  */
@@ -1 +1 @@
1
- {"version":3,"file":"base-context.mjs","names":[],"sources":["../../../../../../context/src/base-context.ts"],"sourcesContent":["import { AsyncLocalStorage } from \"async_hooks\";\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 * @param updates - Partial context data to merge\n */\n public update(updates: Partial<TStore>): void {\n const current = this.storage.getStore();\n\n if (current) {\n Object.assign(current, updates);\n } else {\n this.enter(updates 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 * @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":";;;;;;;;;;;;;;;;;;;;;;;;;;;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;;;;;;;;CASA,AAAO,OAAO,SAAgC;EAC5C,MAAM,UAAU,KAAK,QAAQ,SAAS;EAEtC,IAAI,SACF,OAAO,OAAO,SAAS,OAAO;OAE9B,KAAK,MAAM,OAAiB;CAEhC;;;;;;CAOA,AAAO,WAA+B;EACpC,OAAO,KAAK,QAAQ,SAAS;CAC/B;;;;;;;CAQA,AAAO,IAA4B,KAA+B;EAChE,OAAO,KAAK,QAAQ,SAAS,CAAC,GAAG;CACnC;;;;;;;CAQA,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":["../../../../../../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"}
package/llms-full.txt CHANGED
@@ -95,6 +95,8 @@ userContext.update({ role: "admin" });
95
95
 
96
96
  If there's no current store, `update` creates one with the partial (cast to the full type). Use for incremental enrichment as the request flows through layers.
97
97
 
98
+ `update()` (and `set()`, which calls it) silently drops `__proto__` / `constructor` / `prototype` keys instead of merging them — merging with `Object.assign` would otherwise let a caller-shaped payload (e.g. `userContext.update(req.body)`) pollute `Object.prototype` for the whole process, since the context store is shared. The rest of the payload still merges normally; this closes only the prototype-pollution vector, not general trust of merged input — validate before you merge caller-supplied data.
99
+
98
100
  ## Reading
99
101
 
100
102
  ```ts
package/package.json CHANGED
@@ -24,7 +24,7 @@
24
24
  "engines": {
25
25
  "node": ">=18.0.0"
26
26
  },
27
- "version": "4.15.0",
27
+ "version": "5.0.0",
28
28
  "main": "./cjs/index.cjs",
29
29
  "module": "./esm/index.mjs",
30
30
  "types": "./esm/index.d.mts",
@@ -87,6 +87,8 @@ userContext.update({ role: "admin" });
87
87
 
88
88
  If there's no current store, `update` creates one with the partial (cast to the full type). Use for incremental enrichment as the request flows through layers.
89
89
 
90
+ `update()` (and `set()`, which calls it) silently drops `__proto__` / `constructor` / `prototype` keys instead of merging them — merging with `Object.assign` would otherwise let a caller-shaped payload (e.g. `userContext.update(req.body)`) pollute `Object.prototype` for the whole process, since the context store is shared. The rest of the payload still merges normally; this closes only the prototype-pollution vector, not general trust of merged input — validate before you merge caller-supplied data.
91
+
90
92
  ## Reading
91
93
 
92
94
  ```ts