UNPKG

@mastra/core

Version:
393 lines (392 loc) 14.7 kB
//#region ../_internal-core/dist/request-context/index.js /** * Reserved key for setting resourceId from middleware. * When set in RequestContext, this takes precedence over client-provided values * for security (prevents attackers from hijacking another user's memory). * * @example * ```typescript * // In your auth middleware: * const requestContext = c.get('requestContext'); * requestContext.set(MASTRA_RESOURCE_ID_KEY, authenticatedUser.id); * ``` */ const MASTRA_RESOURCE_ID_KEY = "mastra__resourceId"; /** * Reserved key for setting threadId from middleware. * When set in RequestContext, this takes precedence over client-provided values * for security (prevents attackers from hijacking another user's memory). * * @example * ```typescript * // In your auth middleware: * const requestContext = c.get('requestContext'); * requestContext.set(MASTRA_THREAD_ID_KEY, threadId); * ``` */ const MASTRA_THREAD_ID_KEY = "mastra__threadId"; /** * Reserved key for storing version overrides on RequestContext. * When set, sub-agent delegation resolves versioned agents from these overrides. * * @example * ```typescript * requestContext.set(MASTRA_VERSIONS_KEY, { * agents: { 'researcher-agent': { versionId: '123' } }, * }); * ``` */ const MASTRA_VERSIONS_KEY = "mastra__versions"; /** * Reserved key for storing the raw auth token from the incoming request. * Used by the editor to forward authentication when connecting to MCP servers * that require the same auth as the Mastra server itself. */ const MASTRA_AUTH_TOKEN_KEY = "mastra__authToken"; function mergeVersionOverrides(base, overrides) { if (!base && !overrides) return void 0; return { ...base, ...overrides, agents: { ...base?.agents, ...overrides?.agents }, ...overrides?.defaultStatus ? { defaultStatus: overrides.defaultStatus } : base?.defaultStatus ? { defaultStatus: base.defaultStatus } : {} }; } /** * Marker thrown by `RequestContext.toJSON()` when it detects cyclic re-entry. * * Cyclic re-entry happens when a stored value transitively references another * `RequestContext` whose `toJSON()` is already on the call stack. `JSON.stringify` * inside `isSerializable` then walks into that context, V8 invokes its * `toJSON()`, which iterates its registry and calls `JSON.stringify` on values * that may walk back through the first context — and so on. Each step is a * fresh `JSON.stringify` call with a fresh internal cycle stack, so V8's * built-in cycle detection never trips and the recursion would pin one CPU * core at 100% indefinitely. * * The fix: throw this marker on reentry. The marker propagates upward through * `isSerializable`'s nested catches (which re-throw it) until it reaches the * outermost `toJSON()`'s `isSerializable` — there it is swallowed and the * offending key is filtered, the same way in-value circular references are * filtered today. */ var CyclicRequestContextToJSONError = class extends Error { constructor(message) { super(message); this.name = "CyclicRequestContextToJSONError"; } }; /** * Tracks `RequestContext` instances whose `toJSON()` is currently on the call * stack. Used to detect cyclic re-entry. Stored as a `WeakSet` so entries are * garbage-collected with their owning context. */ const _toJSONInProgress = /* @__PURE__ */ new WeakSet(); /** * Nesting depth of active `toJSON()` calls. The outermost call (depth === 1 * after entry) catches the cyclic marker error and filters the offending * value; inner calls re-throw so the marker propagates to the outermost. */ let _toJSONDepth = 0; /** * Maximum number of nodes the `isSerializable` probe lets `JSON.stringify` * visit for a single stored value. * * `JSON.stringify` expands shared (non-circular) references once per path, * not once per object: an acyclic graph where every level shares one child * (`{ a: n, b: n }` nested `d` times) holds `d + 1` heap objects but expands * to `2^d` visited nodes. Around `2^26` the output also exceeds V8's string * length cap and stringify throws `RangeError` — but only after doing the * traversal work, which keeps doubling past the cap. Unbounded, a ~30-object * value can block the event loop for minutes and then be silently filtered. * * The budget counts node *visits* (shared references count once per path) * because that is exactly the work any real downstream serialization of the * value would do — a value that fails the budget would also be pathological * to persist. 1M visits keeps the worst-case probe in the tens of * milliseconds while remaining far above any reasonable context value. * * The budget is shared across nested `RequestContext` probes within one * outermost probe (see `_probeBudgetRemaining`), so a shared-reference graph of * nested contexts is bounded too. One caveat remains: `Buffer.prototype.toJSON` * materializes a `{ type, data }` object before the replacer, so Buffers charge * one budget unit per byte rather than the arithmetic typed-array fast path — a * Buffer past the budget is filtered. */ const SERIALIZATION_PROBE_BUDGET = 1e6; /** * The intrinsic `%TypedArray%.prototype.length` getter. Reads the element * count from internal slots, so it cannot be shadowed by an own `length` * property, works across realms, and throws `TypeError` for `DataView` — * which makes it double as the discriminator between typed arrays (intrinsic * indexed elements) and other `ArrayBuffer` views (plain-object semantics). */ const _typedArrayLength = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(Int8Array.prototype), "length").get; /** * Whether a value is a plain object (prototype `Object.prototype` or `null`) * or an array — i.e. structural data safe to hand to the span serializer to * walk. Class instances, functions, `Map`/`Set`, `Date`, etc. are excluded so * their internals aren't walked into traces. */ function isPlainObjectOrArray(value) { try { if (Array.isArray(value)) return true; if (value === null || typeof value !== "object") return false; const proto = Object.getPrototypeOf(value); return proto === Object.prototype || proto === null; } catch { return false; } } /** * Shared state for the serialization probe budget. * * The budget is drawn down by every `isSerializable` probe running within one * outermost probe — including the probes a nested `RequestContext.toJSON()` * runs, which `JSON.stringify` invokes *before* the outer replacer sees the * result. Sharing one budget across them keeps the probe's time bound holding * regardless of nesting; a fresh per-call budget (the earlier approach) let a * shared-reference graph of nested contexts re-run full-budget probes on every * visit and block for seconds. `_probeBudgetActive` marks that an outermost * probe owns the budget so nested probes draw down rather than reset it. */ let _probeBudgetActive = false; let _probeBudgetRemaining = 0; var RequestContext = class { registry = /* @__PURE__ */ new Map(); constructor(iterable) { if (iterable && typeof iterable === "object" && typeof iterable[Symbol.iterator] !== "function") this.registry = new Map(Object.entries(iterable)); else this.registry = new Map(iterable); } /** * set a value with strict typing if `Values` is a Record and the key exists in it. */ set(key, value) { this.registry.set(key, value); } /** * Get a value with its type */ get(key) { return this.registry.get(key); } /** * Check if a key exists in the container */ has(key) { return this.registry.has(key); } /** * Delete a value by key */ delete(key) { return this.registry.delete(key); } /** * Clear all values from the container */ clear() { this.registry.clear(); } /** * Get all keys in the container */ keys() { return this.registry.keys(); } /** * Get all values in the container */ values() { return this.registry.values(); } /** * Get all entries in the container. * Returns a discriminated union of tuples for proper type narrowing when iterating. */ entries() { return this.registry.entries(); } /** * Get the size of the container */ size() { return this.registry.size; } /** * Execute a function for each entry in the container. * The callback receives properly typed key-value pairs. */ forEach(callbackfn) { this.registry.forEach(callbackfn); } /** * Custom JSON serialization method. * Converts the internal Map to a plain object for proper JSON serialization. * Non-serializable values (functions, symbols, RPC proxies, in-value * circular references, and values whose serialization re-enters this * `toJSON` via cross-context back-references) are skipped to prevent * serialization errors when storing to database. * * Reentry safety: if a stored value's `isSerializable` probe re-enters * `toJSON()` on this same instance (through a chain of RequestContexts * holding references to each other), we throw `CyclicRequestContextToJSONError`. * Inner `isSerializable` calls re-throw the marker; the outermost * `isSerializable` swallows it and filters the offending key, the same * way it filters in-value circular references today. */ toJSON() { if (_toJSONInProgress.has(this)) throw new CyclicRequestContextToJSONError("RequestContext.toJSON: detected cyclic re-entry (a stored value transitively references this context)"); _toJSONInProgress.add(this); _toJSONDepth++; try { const result = {}; for (const [key, value] of this.registry.entries()) if (this.isSerializable(value)) result[key] = value; return result; } finally { _toJSONInProgress.delete(this); _toJSONDepth--; } } /** * Check if a value can be safely serialized to JSON. * * The probe is budgeted (see `SERIALIZATION_PROBE_BUDGET`): a value whose * serialization would visit an unbounded number of nodes — an acyclic * graph with layered shared references expands as 2^depth — is treated as * non-serializable and filtered instead of blocking the event loop for * the full expansion. The budget is shared across nested `RequestContext` * probes within one outermost probe (a nested `toJSON()` runs before the * replacer sees its result), so the bound holds even when the graph reaches * nested contexts through many shared paths. * * Re-throws `CyclicRequestContextToJSONError` when called from a nested * `toJSON()` (`_toJSONDepth > 1`), so the marker propagates up to the * outermost `toJSON()`'s `isSerializable`, which then swallows it and * filters the offending key. This is what lets the outermost call return * a clean JSON-safe dict for cross-context cycles. */ isSerializable(value) { if (value === null || value === void 0) return true; if (typeof value === "function") return false; if (typeof value === "symbol") return false; if (typeof value !== "object") return true; const outermostProbe = !_probeBudgetActive; if (outermostProbe) { _probeBudgetActive = true; _probeBudgetRemaining = SERIALIZATION_PROBE_BUDGET; } try { JSON.stringify(value, (_key, probed) => { if (--_probeBudgetRemaining < 0) throw new RangeError("RequestContext.isSerializable: value expands past the serialization probe budget"); if (ArrayBuffer.isView(probed)) { let elementCount; try { elementCount = _typedArrayLength.call(probed); } catch { return probed; } if (typeof probed[0] === "bigint") return probed; _probeBudgetRemaining -= elementCount; if (_probeBudgetRemaining < 0) throw new RangeError("RequestContext.isSerializable: value expands past the serialization probe budget"); const surrogate = Object.create(null); for (const key of Object.keys(probed)) { const index = Number(key); if (!(Number.isInteger(index) && index >= 0 && String(index) === key)) surrogate[key] = probed[key]; } return surrogate; } return probed; }); return true; } catch (e) { if (e instanceof CyclicRequestContextToJSONError && _toJSONDepth > 1) throw e; return false; } finally { if (outermostProbe) _probeBudgetActive = false; } } /** * Custom span serialization. Exposes the registry *entries* (never the * instance's own private fields) so `deepClean` in `@mastra/observability` * doesn't walk the runtime-enumerable `registry` Map — which would * serialize its raw entries (including bearer tokens) into exported spans. * * Per stored value: * - The framework-managed auth token is redacted by key. * - Primitives are returned as-is. * - Plain objects and arrays are returned by reference so the downstream * `deepClean` walks and bounds them — this keeps nested request-context * data visible in traces instead of collapsing it to `[object]`. * - Every other type (class instances, functions, Map/Set, Date, etc.) is * collapsed to `[${typeof value}]` rather than walked, so a class's * internals never reach the trace serializer. * * The plain objects/arrays passed through here MUST still be bounded by a * downstream `deepClean` before export. */ serializeForSpan() { const safe = {}; for (const [key, value] of this.registry.entries()) if (key === "mastra__authToken") safe[key] = "[REDACTED]"; else if (value === null || value === void 0 || typeof value === "string" || typeof value === "number" || typeof value === "boolean") safe[key] = value; else if (isPlainObjectOrArray(value)) safe[key] = value; else safe[key] = `[${typeof value}]`; return safe; } /** * Get all values as a typed object for destructuring. * Returns Record<string, any> when untyped, or the Values type when typed. * * @example * ```typescript * const ctx = new RequestContext<{ userId: string; apiKey: string }>(); * ctx.set('userId', 'user-123'); * ctx.set('apiKey', 'key-456'); * const { userId, apiKey } = ctx.all; * ``` */ get all() { return Object.fromEntries(this.registry); } }; //#endregion Object.defineProperty(exports, "MASTRA_AUTH_TOKEN_KEY", { enumerable: true, get: function() { return MASTRA_AUTH_TOKEN_KEY; } }); Object.defineProperty(exports, "MASTRA_RESOURCE_ID_KEY", { enumerable: true, get: function() { return MASTRA_RESOURCE_ID_KEY; } }); Object.defineProperty(exports, "MASTRA_THREAD_ID_KEY", { enumerable: true, get: function() { return MASTRA_THREAD_ID_KEY; } }); Object.defineProperty(exports, "MASTRA_VERSIONS_KEY", { enumerable: true, get: function() { return MASTRA_VERSIONS_KEY; } }); Object.defineProperty(exports, "RequestContext", { enumerable: true, get: function() { return RequestContext; } }); Object.defineProperty(exports, "mergeVersionOverrides", { enumerable: true, get: function() { return mergeVersionOverrides; } }); //# sourceMappingURL=request-context-ByoZMp-j.cjs.map