@mastra/core
Version:
358 lines (357 loc) • 14 kB
JavaScript
//#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
export { RequestContext as a, MASTRA_VERSIONS_KEY as i, MASTRA_RESOURCE_ID_KEY as n, mergeVersionOverrides as o, MASTRA_THREAD_ID_KEY as r, MASTRA_AUTH_TOKEN_KEY as t };
//# sourceMappingURL=request-context-p_Tq-4EM.js.map