@mastra/factory 0.24.0-alpha.11 → 0.24.0-alpha.13
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/README.md +4 -4
- package/dist/environment/build-runner.js +2 -1
- package/dist/environment/build-runner.js.map +1 -1
- package/dist/environment/build-workflow.js +1 -1
- package/dist/environment/build-workflow.js.map +1 -1
- package/dist/environment/build.js +16 -14
- package/dist/environment/build.js.map +1 -1
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +25 -1
- package/dist/factory.js.map +1 -1
- package/dist/integrations/github/app-identity.js.map +1 -1
- package/dist/integrations/github/integration.js +1 -1
- package/dist/integrations/github/integration.js.map +1 -1
- package/dist/integrations/github/reconcile-worker.js +1 -1
- package/dist/integrations/github/reconcile-worker.js.map +1 -1
- package/dist/integrations/github/routes.d.ts.map +1 -1
- package/dist/integrations/github/routes.js +6 -3
- package/dist/integrations/github/routes.js.map +1 -1
- package/dist/integrations/github/rules.js +3 -2
- package/dist/integrations/github/rules.js.map +1 -1
- package/dist/integrations/github/sandbox.js +2 -2
- package/dist/integrations/github/sandbox.js.map +1 -1
- package/dist/integrations/gitlab/api.js.map +1 -1
- package/dist/integrations/gitlab/integration.js.map +1 -1
- package/dist/integrations/gitlab/reconciliation-config.js +2 -1
- package/dist/integrations/gitlab/reconciliation-config.js.map +1 -1
- package/dist/integrations/gitlab/rules.js.map +1 -1
- package/dist/integrations/incidentio/api.js.map +1 -1
- package/dist/integrations/incidentio/integration.js.map +1 -1
- package/dist/integrations/incidentio/rules.js.map +1 -1
- package/dist/integrations/issue-reconcile-worker.js +1 -1
- package/dist/integrations/issue-reconcile-worker.js.map +1 -1
- package/dist/integrations/issue-reconciler.js +1 -1
- package/dist/integrations/issue-reconciler.js.map +1 -1
- package/dist/integrations/jira/api.js.map +1 -1
- package/dist/integrations/jira/integration.js.map +1 -1
- package/dist/integrations/jira/rules.js.map +1 -1
- package/dist/integrations/linear/integration.js.map +1 -1
- package/dist/integrations/linear/rules.js.map +1 -1
- package/dist/integrations/platform/api-client.js.map +1 -1
- package/dist/integrations/platform/github/event-worker.js +1 -1
- package/dist/integrations/platform/github/event-worker.js.map +1 -1
- package/dist/integrations/platform/github/integration.js +2 -2
- package/dist/integrations/platform/github/integration.js.map +1 -1
- package/dist/integrations/platform/gitlab/event-worker.js.map +1 -1
- package/dist/integrations/platform/gitlab/integration.js.map +1 -1
- package/dist/integrations/platform/incidentio/integration.js +2 -1
- package/dist/integrations/platform/incidentio/integration.js.map +1 -1
- package/dist/integrations/platform/jira/integration.js.map +1 -1
- package/dist/integrations/platform/linear/event-worker.js +1 -1
- package/dist/integrations/platform/linear/event-worker.js.map +1 -1
- package/dist/integrations/platform/linear/integration.js.map +1 -1
- package/dist/integrations/slack/connect-route.js +1 -1
- package/dist/integrations/slack/connect-route.js.map +1 -1
- package/dist/integrations/slack/feed-publisher.js +1 -1
- package/dist/integrations/slack/feed-publisher.js.map +1 -1
- package/dist/integrations/slack/integration.js +1 -1
- package/dist/integrations/slack/integration.js.map +1 -1
- package/dist/integrations/slack/slack.js +2 -1
- package/dist/integrations/slack/slack.js.map +1 -1
- package/dist/integrations/workos/integration.js.map +1 -1
- package/dist/packages/_internals/workspace/dist/index.js +18 -1
- package/dist/packages/_internals/workspace/dist/index.js.map +1 -1
- package/dist/routes/attention-activity.js.map +1 -1
- package/dist/routes/attention-parked.js.map +1 -1
- package/dist/routes/attention-providers.js.map +1 -1
- package/dist/routes/automation-runs.js.map +1 -1
- package/dist/routes/contracts.js +1 -1
- package/dist/routes/contracts.js.map +1 -1
- package/dist/routes/custom-provider-source.js.map +1 -1
- package/dist/routes/fs.d.ts +31 -23
- package/dist/routes/fs.d.ts.map +1 -1
- package/dist/routes/fs.js +218 -64
- package/dist/routes/fs.js.map +1 -1
- package/dist/routes/intake.js +2 -1
- package/dist/routes/intake.js.map +1 -1
- package/dist/routes/knowledge.js +1 -1
- package/dist/routes/knowledge.js.map +1 -1
- package/dist/routes/oauth.js +3 -2
- package/dist/routes/oauth.js.map +1 -1
- package/dist/routes/projects.js +32 -30
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/session-access.d.ts +55 -0
- package/dist/routes/session-access.d.ts.map +1 -0
- package/dist/routes/session-access.js +165 -0
- package/dist/routes/session-access.js.map +1 -0
- package/dist/routes/skills.js.map +1 -1
- package/dist/routes/source-control-sessions.d.ts +5 -0
- package/dist/routes/source-control-sessions.d.ts.map +1 -1
- package/dist/routes/source-control-sessions.js +87 -10
- package/dist/routes/source-control-sessions.js.map +1 -1
- package/dist/routes/surface.d.ts +5 -0
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +10 -4
- package/dist/routes/surface.js.map +1 -1
- package/dist/routes/tenant-credentials.js.map +1 -1
- package/dist/routes/work-items.js.map +1 -1
- package/dist/rules/binding-context.js +4 -2
- package/dist/rules/binding-context.js.map +1 -1
- package/dist/rules/dispatcher.js +4 -4
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/start-coordinator.js.map +1 -1
- package/dist/rules/tools.js +2 -2
- package/dist/rules/tools.js.map +1 -1
- package/dist/rules/transition-service.js.map +1 -1
- package/dist/sandbox/callback-factory-sandbox.js.map +1 -1
- package/dist/sandbox/local-factory-sandbox.js.map +1 -1
- package/dist/sandbox/session-retirement.js.map +1 -1
- package/dist/session/live-sessions.js.map +1 -1
- package/dist/session/memory-settings-hydration.js +2 -1
- package/dist/session/memory-settings-hydration.js.map +1 -1
- package/dist/session/resource-migration.d.ts +18 -0
- package/dist/session/resource-migration.d.ts.map +1 -1
- package/dist/session/resource-migration.js +24 -1
- package/dist/session/resource-migration.js.map +1 -1
- package/dist/session/session-threads.d.ts +9 -0
- package/dist/session/session-threads.d.ts.map +1 -1
- package/dist/session/session-threads.js +24 -4
- package/dist/session/session-threads.js.map +1 -1
- package/dist/spa-static.js.map +1 -1
- package/dist/state-signing.js +1 -1
- package/dist/state-signing.js.map +1 -1
- package/dist/storage/domains/audit/base.js.map +1 -1
- package/dist/storage/domains/audit/domain.js.map +1 -1
- package/dist/storage/domains/channel-identity/base.js.map +1 -1
- package/dist/storage/domains/comments/base.js.map +1 -1
- package/dist/storage/domains/comments/domain.js.map +1 -1
- package/dist/storage/domains/comments/feed-context.js.map +1 -1
- package/dist/storage/domains/credentials/base.js.map +1 -1
- package/dist/storage/domains/custom-providers/base.js.map +1 -1
- package/dist/storage/domains/intake/base.js.map +1 -1
- package/dist/storage/domains/integrations/base.js.map +1 -1
- package/dist/storage/domains/memory-settings/base.js.map +1 -1
- package/dist/storage/domains/model-defaults/base.js.map +1 -1
- package/dist/storage/domains/projects/base.js.map +1 -1
- package/dist/storage/domains/queue-health/base.js.map +1 -1
- package/dist/storage/domains/source-control/base.d.ts +15 -0
- package/dist/storage/domains/source-control/base.d.ts.map +1 -1
- package/dist/storage/domains/source-control/base.js +29 -0
- package/dist/storage/domains/source-control/base.js.map +1 -1
- package/dist/storage/domains/source-control/inmemory.d.ts +5 -1
- package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
- package/dist/storage/domains/source-control/inmemory.js +7 -0
- package/dist/storage/domains/source-control/inmemory.js.map +1 -1
- package/dist/storage/domains/work-items/base.js +2 -1
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/dist/storage/domains/work-items/board-version.js.map +1 -1
- package/dist/storage/domains/work-items/metrics.js.map +1 -1
- package/dist/tools/messaging-tools.d.ts.map +1 -1
- package/dist/tools/messaging-tools.js +201 -73
- package/dist/tools/messaging-tools.js.map +1 -1
- package/dist/tools/run-watch.d.ts +2 -0
- package/dist/tools/run-watch.d.ts.map +1 -1
- package/dist/tools/run-watch.js +42 -2
- package/dist/tools/run-watch.js.map +1 -1
- package/dist/tools/session-tools.d.ts.map +1 -1
- package/dist/tools/session-tools.js +25 -10
- package/dist/tools/session-tools.js.map +1 -1
- package/dist/tools/sessions-state-processor.js +2 -2
- package/dist/tools/sessions-state-processor.js.map +1 -1
- package/dist/workspace.d.ts +5 -2
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +26 -13
- package/dist/workspace.js.map +1 -1
- package/package.json +9 -9
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"live-sessions.js","names":[
|
|
1
|
+
{"version":3,"file":"live-sessions.js","names":[],"sources":["../../src/session/live-sessions.ts"],"sourcesContent":["import type { AgentControllerEvent } from '@mastra/core/agent-controller';\n\n/** The slice of a live session this registry needs: its id, its run, its parked tools, and who it works for. */\nexport interface LiveSession {\n readonly identity: { getId(): string };\n readonly run: { isRunning(): boolean };\n readonly state: { get(): Readonly<{ factoryOrgId?: string; factoryProjectId?: string }> };\n readonly displayState: { get(): { pendingSuspensions: ReadonlyMap<string, { toolName: string }> } };\n subscribe(listener: (event: AgentControllerEvent) => void): () => void;\n}\n\ninterface SessionNotifier {\n onSessionCreated(listener: (session: LiveSession) => void): () => void;\n onSessionDeleted(listener: (session: LiveSession) => void): () => void;\n}\n\n/** The tool a session has waited on longest, and since when (epoch ms). */\nexport interface ParkedRun {\n toolName: string;\n suspendedAt: number;\n}\n\ninterface TrackedSession {\n session: LiveSession;\n seenAt: number;\n suspendedAt: Map<string, number>;\n unsubscribe: () => void;\n}\n\n/**\n * The sessions this process has materialized, by session id.\n *\n * Membership comes from the controller's notifications rather than\n * `getSession`, which hands back the pending creation — awaiting it\n * from a request blocks for as long as the session takes to materialize its\n * sandbox, minutes on a cold clone. Notifications only fire once a session\n * exists, so a read never waits.\n *\n * Whether a session is *running* or *parked* is read from the session itself,\n * never tracked here: no missed run event can leave the answer stuck. Only the\n * moment each suspension arrived is kept, since the session does not date them.\n */\nexport class LiveSessions {\n readonly #byId = new Map<string, TrackedSession>();\n readonly #parkedListeners = new Set<(session: LiveSession) => void>();\n /** A park stamp doubles as the receipt key, so two suspensions in one millisecond must not share one. */\n #lastParkStamp = 0;\n\n constructor(controller: SessionNotifier) {\n controller.onSessionCreated(session => this.#track(session));\n controller.onSessionDeleted(session => {\n const id = session.identity.getId();\n const wasParked = this.parked(id) !== undefined;\n this.#byId.get(id)?.unsubscribe();\n this.#byId.delete(id);\n if (wasParked) for (const listener of this.#parkedListeners) listener(session);\n });\n }\n\n #nextParkStamp(): number {\n this.#lastParkStamp = Math.max(Date.now(), this.#lastParkStamp + 1);\n return this.#lastParkStamp;\n }\n\n #track(session: LiveSession): void {\n const suspendedAt = new Map<string, number>();\n const unsubscribe = session.subscribe(event => {\n if (event.type === 'tool_suspended') suspendedAt.set(event.toolCallId, this.#nextParkStamp());\n else if (event.type === 'tool_suspension_cancelled') suspendedAt.delete(event.toolCallId);\n else if (event.type === 'agent_end' && event.reason !== 'suspended') suspendedAt.clear();\n else return;\n for (const listener of this.#parkedListeners) listener(session);\n });\n this.#byId.set(session.identity.getId(), { session, seenAt: Date.now(), suspendedAt, unsubscribe });\n }\n\n /** Whether the session has an agent run in flight right now. */\n isRunning(sessionId: string): boolean {\n return this.#byId.get(sessionId)?.session.run.isRunning() ?? false;\n }\n\n /** The tool the session is parked on, oldest first, or nothing when no answer is owed. */\n parked(sessionId: string): ParkedRun | undefined {\n const tracked = this.#byId.get(sessionId);\n if (!tracked) return undefined;\n let oldest: ParkedRun | undefined;\n for (const [toolCallId, { toolName }] of tracked.session.displayState.get().pendingSuspensions) {\n const suspendedAt = tracked.suspendedAt.get(toolCallId) ?? tracked.seenAt;\n if (!oldest || suspendedAt < oldest.suspendedAt) oldest = { toolName, suspendedAt };\n }\n return oldest;\n }\n\n /** Every parked session of a project, so the inbox reads no card when nothing waits. */\n parkedIn(factoryProjectId: string): Array<{ sessionId: string; run: ParkedRun }> {\n const parked: Array<{ sessionId: string; run: ParkedRun }> = [];\n for (const [sessionId, tracked] of this.#byId) {\n if (tracked.session.state.get().factoryProjectId !== factoryProjectId) continue;\n const run = this.parked(sessionId);\n if (run) parked.push({ sessionId, run });\n }\n return parked;\n }\n\n /** Fires when a session parks, is answered, or finishes a turn: whoever shows parked runs re-reads. */\n onParkedChanged(listener: (session: LiveSession) => void): () => void {\n this.#parkedListeners.add(listener);\n return () => this.#parkedListeners.delete(listener);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;AA0CA,IAAa,eAAb,MAA0B;CACxB,wBAAiB,IAAI,IAA4B;CACjD,mCAA4B,IAAI,IAAoC;;CAEpE,iBAAiB;CAEjB,YAAY,YAA6B;EACvC,WAAW,kBAAiB,YAAW,KAAK,OAAO,OAAO,CAAC;EAC3D,WAAW,kBAAiB,YAAW;GACrC,MAAM,KAAK,QAAQ,SAAS,MAAM;GAClC,MAAM,YAAY,KAAK,OAAO,EAAE,MAAM,KAAA;GACtC,KAAK,MAAM,IAAI,EAAE,CAAC,EAAE,YAAY;GAChC,KAAK,MAAM,OAAO,EAAE;GACpB,IAAI,WAAW,KAAK,MAAM,YAAY,KAAK,kBAAkB,SAAS,OAAO;EAC/E,CAAC;CACH;CAEA,iBAAyB;EACvB,KAAK,iBAAiB,KAAK,IAAI,KAAK,IAAI,GAAG,KAAK,iBAAiB,CAAC;EAClE,OAAO,KAAK;CACd;CAEA,OAAO,SAA4B;EACjC,MAAM,8BAAc,IAAI,IAAoB;EAC5C,MAAM,cAAc,QAAQ,WAAU,UAAS;GAC7C,IAAI,MAAM,SAAS,kBAAkB,YAAY,IAAI,MAAM,YAAY,KAAK,eAAe,CAAC;QACvF,IAAI,MAAM,SAAS,6BAA6B,YAAY,OAAO,MAAM,UAAU;QACnF,IAAI,MAAM,SAAS,eAAe,MAAM,WAAW,aAAa,YAAY,MAAM;QAClF;GACL,KAAK,MAAM,YAAY,KAAK,kBAAkB,SAAS,OAAO;EAChE,CAAC;EACD,KAAK,MAAM,IAAI,QAAQ,SAAS,MAAM,GAAG;GAAE;GAAS,QAAQ,KAAK,IAAI;GAAG;GAAa;EAAY,CAAC;CACpG;;CAGA,UAAU,WAA4B;EACpC,OAAO,KAAK,MAAM,IAAI,SAAS,CAAC,EAAE,QAAQ,IAAI,UAAU,KAAK;CAC/D;;CAGA,OAAO,WAA0C;EAC/C,MAAM,UAAU,KAAK,MAAM,IAAI,SAAS;EACxC,IAAI,CAAC,SAAS,OAAO,KAAA;EACrB,IAAI;EACJ,KAAK,MAAM,CAAC,YAAY,EAAE,eAAe,QAAQ,QAAQ,aAAa,IAAI,CAAC,CAAC,oBAAoB;GAC9F,MAAM,cAAc,QAAQ,YAAY,IAAI,UAAU,KAAK,QAAQ;GACnE,IAAI,CAAC,UAAU,cAAc,OAAO,aAAa,SAAS;IAAE;IAAU;GAAY;EACpF;EACA,OAAO;CACT;;CAGA,SAAS,kBAAwE;EAC/E,MAAM,SAAuD,CAAC;EAC9D,KAAK,MAAM,CAAC,WAAW,YAAY,KAAK,OAAO;GAC7C,IAAI,QAAQ,QAAQ,MAAM,IAAI,CAAC,CAAC,qBAAqB,kBAAkB;GACvE,MAAM,MAAM,KAAK,OAAO,SAAS;GACjC,IAAI,KAAK,OAAO,KAAK;IAAE;IAAW;GAAI,CAAC;EACzC;EACA,OAAO;CACT;;CAGA,gBAAgB,UAAsD;EACpE,KAAK,iBAAiB,IAAI,QAAQ;EAClC,aAAa,KAAK,iBAAiB,OAAO,QAAQ;CACpD;AACF"}
|
|
@@ -31,7 +31,8 @@ async function hydrateSessionMemorySettings(session, { sourceControl }) {
|
|
|
31
31
|
const state = session.state.get() ?? {};
|
|
32
32
|
if (Boolean(state.factoryProjectId) && hasResolvedOrg(state.factoryOrgId)) return;
|
|
33
33
|
try {
|
|
34
|
-
|
|
34
|
+
const record = await sourceControl.sessions.getBySessionId(session.identity.getId());
|
|
35
|
+
await seedSessionOrg(session, record?.orgId);
|
|
35
36
|
} catch (error) {
|
|
36
37
|
console.warn("[Factory session hydration] Unable to resolve the session organization.", error);
|
|
37
38
|
if (!session.state.get()?.factoryOrgId) await seedSessionOrg(session, void 0);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-settings-hydration.js","names":[],"sources":["../../src/session/memory-settings-hydration.ts"],"sourcesContent":["import type { MastraCodeState } from '@mastra/code-sdk/schema';\n\nimport type { MemorySettingsRecord } from '../storage/domains/memory-settings/base.js';\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\nimport { hasResolvedOrg, seedSessionOrg } from './org-seed.js';\n\n/** Default thresholds mirror the TUI `/om` fallbacks. */\nexport const DEFAULT_OBSERVATION_THRESHOLD = 30_000;\nexport const DEFAULT_REFLECTION_THRESHOLD = 40_000;\n\n/**\n * Layer a channel sender's own row over the project's shared row. The sender\n * wins only for knobs they saved; an unsaved (`null`) knob keeps the project's\n * value rather than resetting to Auto or the built-in default, because the\n * project's choice may be the only credentialed one.\n */\nexport function layerPersonalMemorySettings(\n project: MemorySettingsRecord | null,\n personal: MemorySettingsRecord | null,\n): MemorySettingsRecord | null {\n if (!personal) return project;\n if (!project) return personal;\n return {\n ...project,\n observerModelId: personal.observerModelId ?? project.observerModelId,\n reflectorModelId: personal.reflectorModelId ?? project.reflectorModelId,\n observationThreshold: personal.observationThreshold ?? project.observationThreshold,\n reflectionThreshold: personal.reflectionThreshold ?? project.reflectionThreshold,\n observeAttachments: personal.observeAttachments ?? project.observeAttachments,\n };\n}\n\nexport interface MemorySettingsHydrationSession {\n readonly identity: { getId(): string };\n state: {\n get: () => MastraCodeState | undefined;\n set: (updates: Partial<MastraCodeState>) => Promise<void> | void;\n };\n}\n\nexport interface MemorySettingsHydrationDependencies {\n /** GitHub-integration source-control rows — the only creator of web user sessions today. */\n sourceControl: {\n sessions: Pick<SourceControlStorageHandle['sessions'], 'getBySessionId'>;\n };\n}\n\n/**\n * Seed a freshly created controller session's tenant org from its source-control\n * row. Memory settings remain DB-authoritative and are loaded into request\n * context for each invocation instead of being copied into mutable session\n * state.\n */\nexport async function hydrateSessionMemorySettings(\n session: MemorySettingsHydrationSession,\n { sourceControl }: MemorySettingsHydrationDependencies,\n): Promise<void> {\n const state: Partial<MastraCodeState> = session.state.get() ?? {};\n const isFactoryRun = Boolean(state.factoryProjectId);\n if (isFactoryRun && hasResolvedOrg(state.factoryOrgId)) return;\n\n try {\n const record = await sourceControl.sessions.getBySessionId(session.identity.getId());\n await seedSessionOrg(session, record?.orgId);\n } catch (error) {\n console.warn('[Factory session hydration] Unable to resolve the session organization.', error);\n if (!session.state.get()?.factoryOrgId) await seedSessionOrg(session, undefined);\n }\n}\n"],"mappings":";;;AAOA,MAAa,gCAAgC;AAC7C,MAAa,+BAA+B;;;;;;;AAQ5C,SAAgB,4BACd,SACA,UAC6B;CAC7B,IAAI,CAAC,UAAU,OAAO;CACtB,IAAI,CAAC,SAAS,OAAO;CACrB,OAAO;EACL,GAAG;EACH,iBAAiB,SAAS,mBAAmB,QAAQ;EACrD,kBAAkB,SAAS,oBAAoB,QAAQ;EACvD,sBAAsB,SAAS,wBAAwB,QAAQ;EAC/D,qBAAqB,SAAS,uBAAuB,QAAQ;EAC7D,oBAAoB,SAAS,sBAAsB,QAAQ;CAC7D;AACF;;;;;;;AAuBA,eAAsB,6BACpB,SACA,EAAE,iBACa;CACf,MAAM,QAAkC,QAAQ,MAAM,IAAI,KAAK,CAAC;CAEhE,IADqB,QAAQ,MAAM,gBACpB,KAAK,eAAe,MAAM,YAAY,GAAG;CAExD,IAAI;
|
|
1
|
+
{"version":3,"file":"memory-settings-hydration.js","names":[],"sources":["../../src/session/memory-settings-hydration.ts"],"sourcesContent":["import type { MastraCodeState } from '@mastra/code-sdk/schema';\n\nimport type { MemorySettingsRecord } from '../storage/domains/memory-settings/base.js';\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\nimport { hasResolvedOrg, seedSessionOrg } from './org-seed.js';\n\n/** Default thresholds mirror the TUI `/om` fallbacks. */\nexport const DEFAULT_OBSERVATION_THRESHOLD = 30_000;\nexport const DEFAULT_REFLECTION_THRESHOLD = 40_000;\n\n/**\n * Layer a channel sender's own row over the project's shared row. The sender\n * wins only for knobs they saved; an unsaved (`null`) knob keeps the project's\n * value rather than resetting to Auto or the built-in default, because the\n * project's choice may be the only credentialed one.\n */\nexport function layerPersonalMemorySettings(\n project: MemorySettingsRecord | null,\n personal: MemorySettingsRecord | null,\n): MemorySettingsRecord | null {\n if (!personal) return project;\n if (!project) return personal;\n return {\n ...project,\n observerModelId: personal.observerModelId ?? project.observerModelId,\n reflectorModelId: personal.reflectorModelId ?? project.reflectorModelId,\n observationThreshold: personal.observationThreshold ?? project.observationThreshold,\n reflectionThreshold: personal.reflectionThreshold ?? project.reflectionThreshold,\n observeAttachments: personal.observeAttachments ?? project.observeAttachments,\n };\n}\n\nexport interface MemorySettingsHydrationSession {\n readonly identity: { getId(): string };\n state: {\n get: () => MastraCodeState | undefined;\n set: (updates: Partial<MastraCodeState>) => Promise<void> | void;\n };\n}\n\nexport interface MemorySettingsHydrationDependencies {\n /** GitHub-integration source-control rows — the only creator of web user sessions today. */\n sourceControl: {\n sessions: Pick<SourceControlStorageHandle['sessions'], 'getBySessionId'>;\n };\n}\n\n/**\n * Seed a freshly created controller session's tenant org from its source-control\n * row. Memory settings remain DB-authoritative and are loaded into request\n * context for each invocation instead of being copied into mutable session\n * state.\n */\nexport async function hydrateSessionMemorySettings(\n session: MemorySettingsHydrationSession,\n { sourceControl }: MemorySettingsHydrationDependencies,\n): Promise<void> {\n const state: Partial<MastraCodeState> = session.state.get() ?? {};\n const isFactoryRun = Boolean(state.factoryProjectId);\n if (isFactoryRun && hasResolvedOrg(state.factoryOrgId)) return;\n\n try {\n const record = await sourceControl.sessions.getBySessionId(session.identity.getId());\n await seedSessionOrg(session, record?.orgId);\n } catch (error) {\n console.warn('[Factory session hydration] Unable to resolve the session organization.', error);\n if (!session.state.get()?.factoryOrgId) await seedSessionOrg(session, undefined);\n }\n}\n"],"mappings":";;;AAOA,MAAa,gCAAgC;AAC7C,MAAa,+BAA+B;;;;;;;AAQ5C,SAAgB,4BACd,SACA,UAC6B;CAC7B,IAAI,CAAC,UAAU,OAAO;CACtB,IAAI,CAAC,SAAS,OAAO;CACrB,OAAO;EACL,GAAG;EACH,iBAAiB,SAAS,mBAAmB,QAAQ;EACrD,kBAAkB,SAAS,oBAAoB,QAAQ;EACvD,sBAAsB,SAAS,wBAAwB,QAAQ;EAC/D,qBAAqB,SAAS,uBAAuB,QAAQ;EAC7D,oBAAoB,SAAS,sBAAsB,QAAQ;CAC7D;AACF;;;;;;;AAuBA,eAAsB,6BACpB,SACA,EAAE,iBACa;CACf,MAAM,QAAkC,QAAQ,MAAM,IAAI,KAAK,CAAC;CAEhE,IADqB,QAAQ,MAAM,gBACpB,KAAK,eAAe,MAAM,YAAY,GAAG;CAExD,IAAI;EACF,MAAM,SAAS,MAAM,cAAc,SAAS,eAAe,QAAQ,SAAS,MAAM,CAAC;EACnF,MAAM,eAAe,SAAS,QAAQ,KAAK;CAC7C,SAAS,OAAO;EACd,QAAQ,KAAK,2EAA2E,KAAK;EAC7F,IAAI,CAAC,QAAQ,MAAM,IAAI,CAAC,EAAE,cAAc,MAAM,eAAe,SAAS,KAAA,CAAS;CACjF;AACF"}
|
|
@@ -84,4 +84,22 @@ export declare function sessionMemoryResource(session: SourceControlSession, bou
|
|
|
84
84
|
* sessions merge into one resource, and their records cannot be merged.
|
|
85
85
|
*/
|
|
86
86
|
export declare function migrateSessionResources(deps: SessionResourceMigrationDeps): Promise<SessionResourceMigrationReport>;
|
|
87
|
+
export interface SessionVisibilityBackfillDeps {
|
|
88
|
+
sessions: {
|
|
89
|
+
listSessionsAwaitingVisibilityCheck(): Promise<SourceControlSession[]>;
|
|
90
|
+
markVisibilityChecked(sessionId: string, options: {
|
|
91
|
+
makePrivate: boolean;
|
|
92
|
+
}): Promise<void>;
|
|
93
|
+
};
|
|
94
|
+
workItems: Pick<SessionResourceMigrationDeps['workItems'], 'listRunBoundItems'>;
|
|
95
|
+
memory: Pick<MemoryStorage, 'getThreadById'>;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Mark shared user sessions private when their conversation lives on their owner's resource.
|
|
99
|
+
* The resource already keeps teammates out of such a session; this makes lists agree, so they
|
|
100
|
+
* stop seeing a session they can't open. Sessions shared onto their Factory, runs, Slack threads
|
|
101
|
+
* and side sessions are left alone. Each session is checked once: new sessions start checked,
|
|
102
|
+
* and a session that throws stays unchecked so the next start retries it.
|
|
103
|
+
*/
|
|
104
|
+
export declare function privatizeOwnerResourceSessions(deps: SessionVisibilityBackfillDeps): Promise<number>;
|
|
87
105
|
//# sourceMappingURL=resource-migration.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resource-migration.d.ts","sourceRoot":"","sources":["../../src/session/resource-migration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAG1D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACtF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uCAAuC,CAAC;AAazE,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,EAAE;QACR,qCAAqC,IAAI,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAAC;QACzE,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;KACzD,CAAC;IACF,SAAS,EAAE;QACT,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,gBAAgB,GAAG,UAAU,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC;QAC3G,yBAAyB,CAAC,KAAK,EAAE;YAAE,SAAS,EAAE,MAAM,CAAC;YAAC,UAAU,EAAE,MAAM,CAAA;SAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;KAC9F,CAAC;IACF,gGAAgG;IAChG,YAAY,CAAC,EAAE;QACb,2BAA2B,CAAC,KAAK,EAAE;YAAE,cAAc,EAAE,MAAM,CAAC;YAAC,YAAY,EAAE,MAAM,CAAA;SAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;KACvG,CAAC;IACF,MAAM,EAAE,IAAI,CAAC,aAAa,EAAE,aAAa,GAAG,aAAa,GAAG,wBAAwB,GAAG,6BAA6B,CAAC,CAAC;IACtH;;;OAGG;IACH,OAAO,EAAE;QAAE,sBAAsB,CAAC,KAAK,EAAE;YAAE,QAAQ,EAAE,MAAM,CAAC;YAAC,UAAU,EAAE,MAAM,CAAA;SAAE,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAA;KAAE,CAAC;CAClH;AAED,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,+DAA+D;IAC/D,gBAAgB,EAAE,MAAM,CAAC;IACzB,gFAAgF;IAChF,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,gEAAgE;IAChE,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB;;;OAGG;IACH,oBAAoB,EAAE,MAAM,EAAE,CAAC;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,gGAAgG;IAChG,wBAAwB,EAAE,MAAM,EAAE,CAAC;CACpC;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,oBAAoB,EAC7B,UAAU,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,gBAAgB,GAAG,UAAU,CAAC,GAAG,IAAI,CAAC,EAAE,GACtE;IAAE,KAAK,EAAE,SAAS,GAAG,MAAM,GAAG,UAAU,CAAC;IAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAYvE;AAMD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,4BAA4B,GACjC,OAAO,CAAC,8BAA8B,CAAC,CAwGzC"}
|
|
1
|
+
{"version":3,"file":"resource-migration.d.ts","sourceRoot":"","sources":["../../src/session/resource-migration.ts"],"names":[],"mappings":"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"}
|
|
@@ -144,6 +144,29 @@ async function migrateSessionResources(deps) {
|
|
|
144
144
|
if (report.threads + report.runBindings + report.subscriptions + report.skipped.length + report.failed.length + report.subscriptionsPending.length > 0) console.info(`[factory] session resource migration: ${report.threads} threads, ${report.runBindings} run bindings, ${report.subscriptions} subscriptions moved; ${report.skipped.length} Factory sessions without a Factory skipped; ${report.failed.length} sessions failed; ${report.subscriptionsPending.length} sessions wait for integrations to move their subscriptions`);
|
|
145
145
|
return report;
|
|
146
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Mark shared user sessions private when their conversation lives on their owner's resource.
|
|
149
|
+
* The resource already keeps teammates out of such a session; this makes lists agree, so they
|
|
150
|
+
* stop seeing a session they can't open. Sessions shared onto their Factory, runs, Slack threads
|
|
151
|
+
* and side sessions are left alone. Each session is checked once: new sessions start checked,
|
|
152
|
+
* and a session that throws stays unchecked so the next start retries it.
|
|
153
|
+
*/
|
|
154
|
+
async function privatizeOwnerResourceSessions(deps) {
|
|
155
|
+
let privatized = 0;
|
|
156
|
+
for (const session of await deps.sessions.listSessionsAwaitingVisibilityCheck()) try {
|
|
157
|
+
let makePrivate = false;
|
|
158
|
+
if (session.visibility !== "private") {
|
|
159
|
+
const { owner } = sessionMemoryResource(session, await deps.workItems.listRunBoundItems(session.sessionId));
|
|
160
|
+
makePrivate = (owner === "user" ? await deps.memory.getThreadById({ threadId: session.sessionId }) : null)?.resourceId === session.userId;
|
|
161
|
+
}
|
|
162
|
+
await deps.sessions.markVisibilityChecked(session.sessionId, { makePrivate });
|
|
163
|
+
if (makePrivate) privatized += 1;
|
|
164
|
+
} catch (error) {
|
|
165
|
+
console.warn(`[factory] session visibility check: session ${session.sessionId} not checked: ${error instanceof Error ? error.message : error}`);
|
|
166
|
+
}
|
|
167
|
+
if (privatized > 0) console.info(`[factory] session visibility check: ${privatized} sessions on their owner's memory made private`);
|
|
168
|
+
return privatized;
|
|
169
|
+
}
|
|
147
170
|
async function moveThread(deps, thread, resourceId) {
|
|
148
171
|
await deps.threads.updateThreadResourceId({
|
|
149
172
|
threadId: thread.id,
|
|
@@ -158,6 +181,6 @@ async function moveThread(deps, thread, resourceId) {
|
|
|
158
181
|
});
|
|
159
182
|
}
|
|
160
183
|
//#endregion
|
|
161
|
-
export { migrateSessionResources, sessionMemoryResource };
|
|
184
|
+
export { migrateSessionResources, privatizeOwnerResourceSessions, sessionMemoryResource };
|
|
162
185
|
|
|
163
186
|
//# sourceMappingURL=resource-migration.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resource-migration.js","names":[],"sources":["../../src/session/resource-migration.ts"],"sourcesContent":["import type { StorageThreadType } from '@mastra/core/memory';\nimport type { MemoryStorage } from '@mastra/core/storage';\n\nimport { externallyAuthoredWorkItem } from '../rules/types.js';\nimport type { SourceControlSession } from '../storage/domains/source-control/base.js';\nimport type { WorkItemRow } from '../storage/domains/work-items/base.js';\nimport { sessionStampCounts } from './session-threads.js';\n\n/** Slack-started sessions live on a `slack/<thread>` branch (see `threadBranch` in the Slack integration). */\nconst SLACK_SESSION_BRANCH_PREFIX = 'slack/';\n\n/**\n * Set on a thread while it moves and turned off once the move, vectors included, is done. A\n * crash in between leaves the thread moved in storage but no longer listed under its\n * session id, so the next start finds it by this flag and its session stamp and finishes the move.\n */\nconst MOVE_PENDING_KEY = 'factoryResourceMovePending';\n\nexport interface SessionResourceMigrationDeps {\n sessions: {\n listSessionsAwaitingResourceMigration(): Promise<SourceControlSession[]>;\n markResourcesMigrated(sessionId: string): Promise<void>;\n };\n workItems: {\n listRunBoundItems(sessionId: string): Promise<(Pick<WorkItemRow, 'externalSource' | 'metadata'> | null)[]>;\n moveRunBindingsToResource(input: { sessionId: string; resourceId: string }): Promise<number>;\n };\n /** Absent when the integrations domain is not ready; subscriptions then stay where they are. */\n integrations?: {\n moveSubscriptionsToResource(input: { fromResourceId: string; toResourceId: string }): Promise<number>;\n };\n memory: Pick<MemoryStorage, 'listThreads' | 'patchThread' | 'getObservationalMemory' | 'supportsObservationalMemory'>;\n /**\n * Moves a thread with everything keyed by its resource, such as observation vectors. Pass the\n * sessions' `Memory`; the bare memory store only moves the thread and its messages.\n */\n threads: { updateThreadResourceId(input: { threadId: string; resourceId: string }): Promise<StorageThreadType> };\n}\n\nexport interface SessionResourceMigrationReport {\n sessions: number;\n factorySessions: number;\n userSessions: number;\n /** Runs on untrusted content; they keep their own resource. */\n isolatedSessions: number;\n /** Factory-owned sessions with no Factory to move them onto; left untouched. */\n skipped: string[];\n /** Sessions whose move threw; the next startup retries them. */\n failed: string[];\n /**\n * Moved sessions whose subscriptions couldn't move because the integrations\n * domain wasn't ready; left unmarked so a later startup moves them.\n */\n subscriptionsPending: string[];\n threads: number;\n runBindings: number;\n subscriptions: number;\n /** Moved sessions whose resource-scoped observational memory stays under their old resource. */\n resourceObservationsLeft: string[];\n}\n\n/**\n * The memory resource a session belongs to: its Factory for runs, side\n * sessions and Slack channel threads, its owner for sessions a person opened,\n * private side sessions and Slack DMs. A run on untrusted content (a pull request, or a card\n * written outside the write-access circle) keeps its own resource, like a new\n * run would; so does a run whose work item no longer loads, since nothing then\n * shows its content was trusted. `null` when a Factory-owned session has no\n * Factory recorded — guessing would merge it into the wrong resource.\n */\nexport function sessionMemoryResource(\n session: SourceControlSession,\n boundItems: (Pick<WorkItemRow, 'externalSource' | 'metadata'> | null)[],\n): { owner: 'factory' | 'user' | 'isolated'; resourceId: string | null } {\n if (boundItems.some(item => item === null || untrustedWorkItem(item))) {\n return { owner: 'isolated', resourceId: session.sessionId };\n }\n // A Slack DM or private side session is its owner's, so it belongs with their own sessions.\n const sharedWithOrg =\n session.visibility !== 'private' &&\n (session.originSessionId !== null || session.branch.startsWith(SLACK_SESSION_BRANCH_PREFIX));\n const factoryOwned = boundItems.length > 0 || sharedWithOrg;\n return factoryOwned\n ? { owner: 'factory', resourceId: session.factoryProjectId }\n : { owner: 'user', resourceId: session.userId };\n}\n\nfunction untrustedWorkItem(item: Pick<WorkItemRow, 'externalSource' | 'metadata'>): boolean {\n return item.externalSource?.type === 'pull-request' || externallyAuthoredWorkItem(item);\n}\n\n/**\n * Move sessions stored under the legacy `resourceId = sessionId` onto their\n * Factory or user resource. Threads keep their id (`threadId === sessionId`)\n * and get `factorySessionId` so per-session listings still find them.\n *\n * Threads move through the sessions' `Memory`, so their observation vectors\n * follow them and recall search finds their past observations under the new\n * resource. Every write only touches values still equal to the session id, so\n * a rerun is a no-op and a crash leaves a state the next run resumes from. A\n * session is marked once moved, so later starts only read sessions that are not\n * (new sessions are created marked).\n * Nothing is deleted. Thread-scoped observational memory is keyed by thread id\n * and keeps working. Resource-scoped observational memory written under\n * `resource:<sessionId>` stays under that key and is no longer read: several\n * sessions merge into one resource, and their records cannot be merged.\n */\nexport async function migrateSessionResources(\n deps: SessionResourceMigrationDeps,\n): Promise<SessionResourceMigrationReport> {\n const report: SessionResourceMigrationReport = {\n sessions: 0,\n factorySessions: 0,\n userSessions: 0,\n isolatedSessions: 0,\n skipped: [],\n failed: [],\n subscriptionsPending: [],\n threads: 0,\n runBindings: 0,\n subscriptions: 0,\n resourceObservationsLeft: [],\n };\n const sessions = await deps.sessions.listSessionsAwaitingResourceMigration();\n\n for (const session of sessions) {\n report.sessions += 1;\n const { sessionId } = session;\n try {\n const { owner, resourceId } = sessionMemoryResource(session, await deps.workItems.listRunBoundItems(sessionId));\n if (!resourceId) {\n report.skipped.push(sessionId);\n continue;\n }\n if (owner === 'factory') report.factorySessions += 1;\n else if (owner === 'isolated') report.isolatedSessions += 1;\n else report.userSessions += 1;\n if (resourceId === sessionId) {\n await deps.sessions.markResourcesMigrated(sessionId);\n continue;\n }\n\n // Finish moves a crash interrupted after storage moved the thread.\n const { threads: interrupted } = await deps.memory.listThreads({\n filter: { resourceId, metadata: { factorySessionId: sessionId, [MOVE_PENDING_KEY]: true } },\n perPage: false,\n });\n for (const thread of interrupted) {\n if (thread.resourceId === sessionId) continue; // listed under the session id below\n await moveThread(deps, thread, thread.resourceId);\n report.threads += 1;\n }\n const { threads } = await deps.memory.listThreads({ filter: { resourceId: sessionId }, perPage: false });\n // A private session's stamps don't count under its Factory, so only its own\n // thread moves there; its other threads stay listed under its id.\n const stampsCount = sessionStampCounts(session, resourceId);\n for (const thread of threads) {\n if (!stampsCount && thread.id !== sessionId) continue;\n // Tag before moving: once moved, the thread is no longer found under the session id.\n const metadata = { ...thread.metadata, factorySessionId: sessionId, [MOVE_PENDING_KEY]: true };\n await deps.memory.patchThread({ id: thread.id, metadata });\n await moveThread(deps, { ...thread, metadata }, resourceId);\n report.threads += 1;\n }\n report.runBindings += await deps.workItems.moveRunBindingsToResource({ sessionId, resourceId });\n if (deps.integrations) {\n report.subscriptions += await deps.integrations.moveSubscriptionsToResource({\n fromResourceId: sessionId,\n toResourceId: resourceId,\n });\n }\n // Reported once, on the run that moves the session's threads.\n if (\n threads.length > 0 &&\n deps.memory.supportsObservationalMemory &&\n (await deps.memory.getObservationalMemory(null, sessionId))\n ) {\n report.resourceObservationsLeft.push(sessionId);\n console.warn(\n `[factory] session resource migration: session ${sessionId} has resource-scoped observational memory; ` +\n `it stays under resource ${sessionId} and is not carried into ${resourceId}`,\n );\n }\n if (!deps.integrations) {\n report.subscriptionsPending.push(sessionId);\n continue;\n }\n await deps.sessions.markResourcesMigrated(sessionId);\n } catch (error) {\n report.failed.push(sessionId);\n console.warn(\n `[factory] session resource migration: session ${sessionId} not moved: ${error instanceof Error ? error.message : error}`,\n );\n }\n }\n\n if (\n report.threads +\n report.runBindings +\n report.subscriptions +\n report.skipped.length +\n report.failed.length +\n report.subscriptionsPending.length >\n 0\n ) {\n console.info(\n `[factory] session resource migration: ${report.threads} threads, ${report.runBindings} run bindings, ` +\n `${report.subscriptions} subscriptions moved; ${report.skipped.length} Factory sessions without a Factory skipped; ` +\n `${report.failed.length} sessions failed; ${report.subscriptionsPending.length} sessions wait for integrations ` +\n `to move their subscriptions`,\n );\n }\n return report;\n}\n\nasync function moveThread(\n deps: SessionResourceMigrationDeps,\n thread: StorageThreadType,\n resourceId: string,\n): Promise<void> {\n await deps.threads.updateThreadResourceId({ threadId: thread.id, resourceId });\n // Stores merge thread metadata, so the flag is switched off rather than removed.\n await deps.memory.patchThread({ id: thread.id, metadata: { ...thread.metadata, [MOVE_PENDING_KEY]: false } });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"resource-migration.js","names":[],"sources":["../../src/session/resource-migration.ts"],"sourcesContent":["import type { StorageThreadType } from '@mastra/core/memory';\nimport type { MemoryStorage } from '@mastra/core/storage';\n\nimport { externallyAuthoredWorkItem } from '../rules/types.js';\nimport type { SourceControlSession } from '../storage/domains/source-control/base.js';\nimport type { WorkItemRow } from '../storage/domains/work-items/base.js';\nimport { sessionStampCounts } from './session-threads.js';\n\n/** Slack-started sessions live on a `slack/<thread>` branch (see `threadBranch` in the Slack integration). */\nconst SLACK_SESSION_BRANCH_PREFIX = 'slack/';\n\n/**\n * Set on a thread while it moves and turned off once the move, vectors included, is done. A\n * crash in between leaves the thread moved in storage but no longer listed under its\n * session id, so the next start finds it by this flag and its session stamp and finishes the move.\n */\nconst MOVE_PENDING_KEY = 'factoryResourceMovePending';\n\nexport interface SessionResourceMigrationDeps {\n sessions: {\n listSessionsAwaitingResourceMigration(): Promise<SourceControlSession[]>;\n markResourcesMigrated(sessionId: string): Promise<void>;\n };\n workItems: {\n listRunBoundItems(sessionId: string): Promise<(Pick<WorkItemRow, 'externalSource' | 'metadata'> | null)[]>;\n moveRunBindingsToResource(input: { sessionId: string; resourceId: string }): Promise<number>;\n };\n /** Absent when the integrations domain is not ready; subscriptions then stay where they are. */\n integrations?: {\n moveSubscriptionsToResource(input: { fromResourceId: string; toResourceId: string }): Promise<number>;\n };\n memory: Pick<MemoryStorage, 'listThreads' | 'patchThread' | 'getObservationalMemory' | 'supportsObservationalMemory'>;\n /**\n * Moves a thread with everything keyed by its resource, such as observation vectors. Pass the\n * sessions' `Memory`; the bare memory store only moves the thread and its messages.\n */\n threads: { updateThreadResourceId(input: { threadId: string; resourceId: string }): Promise<StorageThreadType> };\n}\n\nexport interface SessionResourceMigrationReport {\n sessions: number;\n factorySessions: number;\n userSessions: number;\n /** Runs on untrusted content; they keep their own resource. */\n isolatedSessions: number;\n /** Factory-owned sessions with no Factory to move them onto; left untouched. */\n skipped: string[];\n /** Sessions whose move threw; the next startup retries them. */\n failed: string[];\n /**\n * Moved sessions whose subscriptions couldn't move because the integrations\n * domain wasn't ready; left unmarked so a later startup moves them.\n */\n subscriptionsPending: string[];\n threads: number;\n runBindings: number;\n subscriptions: number;\n /** Moved sessions whose resource-scoped observational memory stays under their old resource. */\n resourceObservationsLeft: string[];\n}\n\n/**\n * The memory resource a session belongs to: its Factory for runs, side\n * sessions and Slack channel threads, its owner for sessions a person opened,\n * private side sessions and Slack DMs. A run on untrusted content (a pull request, or a card\n * written outside the write-access circle) keeps its own resource, like a new\n * run would; so does a run whose work item no longer loads, since nothing then\n * shows its content was trusted. `null` when a Factory-owned session has no\n * Factory recorded — guessing would merge it into the wrong resource.\n */\nexport function sessionMemoryResource(\n session: SourceControlSession,\n boundItems: (Pick<WorkItemRow, 'externalSource' | 'metadata'> | null)[],\n): { owner: 'factory' | 'user' | 'isolated'; resourceId: string | null } {\n if (boundItems.some(item => item === null || untrustedWorkItem(item))) {\n return { owner: 'isolated', resourceId: session.sessionId };\n }\n // A Slack DM or private side session is its owner's, so it belongs with their own sessions.\n const sharedWithOrg =\n session.visibility !== 'private' &&\n (session.originSessionId !== null || session.branch.startsWith(SLACK_SESSION_BRANCH_PREFIX));\n const factoryOwned = boundItems.length > 0 || sharedWithOrg;\n return factoryOwned\n ? { owner: 'factory', resourceId: session.factoryProjectId }\n : { owner: 'user', resourceId: session.userId };\n}\n\nfunction untrustedWorkItem(item: Pick<WorkItemRow, 'externalSource' | 'metadata'>): boolean {\n return item.externalSource?.type === 'pull-request' || externallyAuthoredWorkItem(item);\n}\n\n/**\n * Move sessions stored under the legacy `resourceId = sessionId` onto their\n * Factory or user resource. Threads keep their id (`threadId === sessionId`)\n * and get `factorySessionId` so per-session listings still find them.\n *\n * Threads move through the sessions' `Memory`, so their observation vectors\n * follow them and recall search finds their past observations under the new\n * resource. Every write only touches values still equal to the session id, so\n * a rerun is a no-op and a crash leaves a state the next run resumes from. A\n * session is marked once moved, so later starts only read sessions that are not\n * (new sessions are created marked).\n * Nothing is deleted. Thread-scoped observational memory is keyed by thread id\n * and keeps working. Resource-scoped observational memory written under\n * `resource:<sessionId>` stays under that key and is no longer read: several\n * sessions merge into one resource, and their records cannot be merged.\n */\nexport async function migrateSessionResources(\n deps: SessionResourceMigrationDeps,\n): Promise<SessionResourceMigrationReport> {\n const report: SessionResourceMigrationReport = {\n sessions: 0,\n factorySessions: 0,\n userSessions: 0,\n isolatedSessions: 0,\n skipped: [],\n failed: [],\n subscriptionsPending: [],\n threads: 0,\n runBindings: 0,\n subscriptions: 0,\n resourceObservationsLeft: [],\n };\n const sessions = await deps.sessions.listSessionsAwaitingResourceMigration();\n\n for (const session of sessions) {\n report.sessions += 1;\n const { sessionId } = session;\n try {\n const { owner, resourceId } = sessionMemoryResource(session, await deps.workItems.listRunBoundItems(sessionId));\n if (!resourceId) {\n report.skipped.push(sessionId);\n continue;\n }\n if (owner === 'factory') report.factorySessions += 1;\n else if (owner === 'isolated') report.isolatedSessions += 1;\n else report.userSessions += 1;\n if (resourceId === sessionId) {\n await deps.sessions.markResourcesMigrated(sessionId);\n continue;\n }\n\n // Finish moves a crash interrupted after storage moved the thread.\n const { threads: interrupted } = await deps.memory.listThreads({\n filter: { resourceId, metadata: { factorySessionId: sessionId, [MOVE_PENDING_KEY]: true } },\n perPage: false,\n });\n for (const thread of interrupted) {\n if (thread.resourceId === sessionId) continue; // listed under the session id below\n await moveThread(deps, thread, thread.resourceId);\n report.threads += 1;\n }\n const { threads } = await deps.memory.listThreads({ filter: { resourceId: sessionId }, perPage: false });\n // A private session's stamps don't count under its Factory, so only its own\n // thread moves there; its other threads stay listed under its id.\n const stampsCount = sessionStampCounts(session, resourceId);\n for (const thread of threads) {\n if (!stampsCount && thread.id !== sessionId) continue;\n // Tag before moving: once moved, the thread is no longer found under the session id.\n const metadata = { ...thread.metadata, factorySessionId: sessionId, [MOVE_PENDING_KEY]: true };\n await deps.memory.patchThread({ id: thread.id, metadata });\n await moveThread(deps, { ...thread, metadata }, resourceId);\n report.threads += 1;\n }\n report.runBindings += await deps.workItems.moveRunBindingsToResource({ sessionId, resourceId });\n if (deps.integrations) {\n report.subscriptions += await deps.integrations.moveSubscriptionsToResource({\n fromResourceId: sessionId,\n toResourceId: resourceId,\n });\n }\n // Reported once, on the run that moves the session's threads.\n if (\n threads.length > 0 &&\n deps.memory.supportsObservationalMemory &&\n (await deps.memory.getObservationalMemory(null, sessionId))\n ) {\n report.resourceObservationsLeft.push(sessionId);\n console.warn(\n `[factory] session resource migration: session ${sessionId} has resource-scoped observational memory; ` +\n `it stays under resource ${sessionId} and is not carried into ${resourceId}`,\n );\n }\n if (!deps.integrations) {\n report.subscriptionsPending.push(sessionId);\n continue;\n }\n await deps.sessions.markResourcesMigrated(sessionId);\n } catch (error) {\n report.failed.push(sessionId);\n console.warn(\n `[factory] session resource migration: session ${sessionId} not moved: ${error instanceof Error ? error.message : error}`,\n );\n }\n }\n\n if (\n report.threads +\n report.runBindings +\n report.subscriptions +\n report.skipped.length +\n report.failed.length +\n report.subscriptionsPending.length >\n 0\n ) {\n console.info(\n `[factory] session resource migration: ${report.threads} threads, ${report.runBindings} run bindings, ` +\n `${report.subscriptions} subscriptions moved; ${report.skipped.length} Factory sessions without a Factory skipped; ` +\n `${report.failed.length} sessions failed; ${report.subscriptionsPending.length} sessions wait for integrations ` +\n `to move their subscriptions`,\n );\n }\n return report;\n}\n\nexport interface SessionVisibilityBackfillDeps {\n sessions: {\n listSessionsAwaitingVisibilityCheck(): Promise<SourceControlSession[]>;\n markVisibilityChecked(sessionId: string, options: { makePrivate: boolean }): Promise<void>;\n };\n workItems: Pick<SessionResourceMigrationDeps['workItems'], 'listRunBoundItems'>;\n memory: Pick<MemoryStorage, 'getThreadById'>;\n}\n\n/**\n * Mark shared user sessions private when their conversation lives on their owner's resource.\n * The resource already keeps teammates out of such a session; this makes lists agree, so they\n * stop seeing a session they can't open. Sessions shared onto their Factory, runs, Slack threads\n * and side sessions are left alone. Each session is checked once: new sessions start checked,\n * and a session that throws stays unchecked so the next start retries it.\n */\nexport async function privatizeOwnerResourceSessions(deps: SessionVisibilityBackfillDeps): Promise<number> {\n let privatized = 0;\n for (const session of await deps.sessions.listSessionsAwaitingVisibilityCheck()) {\n try {\n let makePrivate = false;\n if (session.visibility !== 'private') {\n const { owner } = sessionMemoryResource(session, await deps.workItems.listRunBoundItems(session.sessionId));\n const thread = owner === 'user' ? await deps.memory.getThreadById({ threadId: session.sessionId }) : null;\n makePrivate = thread?.resourceId === session.userId;\n }\n await deps.sessions.markVisibilityChecked(session.sessionId, { makePrivate });\n if (makePrivate) privatized += 1;\n } catch (error) {\n console.warn(\n `[factory] session visibility check: session ${session.sessionId} not checked: ${error instanceof Error ? error.message : error}`,\n );\n }\n }\n if (privatized > 0) {\n console.info(`[factory] session visibility check: ${privatized} sessions on their owner's memory made private`);\n }\n return privatized;\n}\n\nasync function moveThread(\n deps: SessionResourceMigrationDeps,\n thread: StorageThreadType,\n resourceId: string,\n): Promise<void> {\n await deps.threads.updateThreadResourceId({ threadId: thread.id, resourceId });\n // Stores merge thread metadata, so the flag is switched off rather than removed.\n await deps.memory.patchThread({ id: thread.id, metadata: { ...thread.metadata, [MOVE_PENDING_KEY]: false } });\n}\n"],"mappings":"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"}
|
|
@@ -28,6 +28,15 @@ export declare function isRunBound(session: FactorySessionOwnership, runs: Sessi
|
|
|
28
28
|
export declare function memoryResourceOf(session: FactorySessionOwnership, own: {
|
|
29
29
|
resourceId?: string | null;
|
|
30
30
|
} | null | undefined, runBound: boolean): string;
|
|
31
|
+
/** The resource a person's session belongs on for a visibility: its Factory's once shared, its owner's while private. */
|
|
32
|
+
export declare function userSessionResourceFor(session: Pick<FactorySessionOwnership, 'userId' | 'factoryProjectId'>, visibility: 'org' | 'private'): string;
|
|
33
|
+
/**
|
|
34
|
+
* The threads to move when a person's session changes visibility and so moves
|
|
35
|
+
* onto `resourceId`: its own thread and the threads stamped with its id that
|
|
36
|
+
* sit on its owner's or Factory's resource. Threads under the session's own id
|
|
37
|
+
* (from before resources were shared) stay where they are.
|
|
38
|
+
*/
|
|
39
|
+
export declare function sessionThreadsToMove(controller: Pick<AgentController<any>, 'queryThreads' | 'queryThreadById'>, session: FactorySessionOwnership, resourceId: string): Promise<string[]>;
|
|
31
40
|
/**
|
|
32
41
|
* Whether a `factorySessionId` stamp naming this session counts on a thread
|
|
33
42
|
* under `resourceId`. Any org member can write a stamped thread under the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-threads.d.ts","sourceRoot":"","sources":["../../src/session/session-threads.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAE5F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAE9E,6EAA6E;AAC7E,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED,qFAAqF;AACrF,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,gBAAgB,EAAE,yBAAyB,CAAC,CAAC;AAEnF,KAAK,YAAY,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,EAAE,iBAAiB,CAAC,CAAC;AAElE;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,CAC3C,UAAU,EAAE,YAAY,GAAG,SAAS,EACpC,OAAO,EAAE,uBAAuB,EAChC,IAAI,EAAE,kBAAkB,GAAG,SAAS,GACnC,OAAO,CAAC,MAAM,CAAC,CAMjB;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE,uBAAuB,EAAE,IAAI,EAAE,kBAAkB,GAAG,SAAS,oBAOtG;AAED,iGAAiG;AACjG,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,uBAAuB,EAChC,GAAG,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,EACtD,QAAQ,EAAE,OAAO,
|
|
1
|
+
{"version":3,"file":"session-threads.d.ts","sourceRoot":"","sources":["../../src/session/session-threads.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAE5F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAE9E,6EAA6E;AAC7E,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED,qFAAqF;AACrF,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,gBAAgB,EAAE,yBAAyB,CAAC,CAAC;AAEnF,KAAK,YAAY,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,EAAE,iBAAiB,CAAC,CAAC;AAElE;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,CAC3C,UAAU,EAAE,YAAY,GAAG,SAAS,EACpC,OAAO,EAAE,uBAAuB,EAChC,IAAI,EAAE,kBAAkB,GAAG,SAAS,GACnC,OAAO,CAAC,MAAM,CAAC,CAMjB;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE,uBAAuB,EAAE,IAAI,EAAE,kBAAkB,GAAG,SAAS,oBAOtG;AAED,iGAAiG;AACjG,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,uBAAuB,EAChC,GAAG,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,EACtD,QAAQ,EAAE,OAAO,UA4BlB;AAED,yHAAyH;AACzH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,IAAI,CAAC,uBAAuB,EAAE,QAAQ,GAAG,kBAAkB,CAAC,EACrE,UAAU,EAAE,KAAK,GAAG,SAAS,GAC5B,MAAM,CAER;AAED;;;;;GAKG;AACH,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,EAAE,cAAc,GAAG,iBAAiB,CAAC,EAC1E,OAAO,EAAE,uBAAuB,EAChC,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,EAAE,CAAC,CAUnB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,uBAAuB,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAEnH;AAED;;;;;;;;GAQG;AACH,wBAAsB,yBAAyB,CAC7C,UAAU,EAAE,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,EAAE,cAAc,GAAG,iBAAiB,CAAC,EAC1E,OAAO,EAAE,uBAAuB,EAChC,IAAI,EAAE,kBAAkB,GAAG,SAAS,GACnC,OAAO,CAAC,qBAAqB,EAAE,CAAC,CAgBlC"}
|
|
@@ -24,13 +24,33 @@ async function isRunBound(session, runs) {
|
|
|
24
24
|
function memoryResourceOf(session, own, runBound) {
|
|
25
25
|
const factoryProjectId = session.factoryProjectId ?? void 0;
|
|
26
26
|
const shared = (session.branch?.startsWith("slack/") ?? false) || session.originSessionId ? session.visibility === "private" ? session.userId : factoryProjectId : void 0;
|
|
27
|
-
|
|
27
|
+
if (shared) {
|
|
28
|
+
if (own?.resourceId && [session.sessionId, shared].includes(own.resourceId)) return own.resourceId;
|
|
29
|
+
return shared;
|
|
30
|
+
}
|
|
31
|
+
const allowed = [
|
|
28
32
|
session.sessionId,
|
|
29
33
|
session.userId,
|
|
30
|
-
...runBound && factoryProjectId ? [factoryProjectId] : []
|
|
34
|
+
...(runBound || session.visibility === "org") && factoryProjectId ? [factoryProjectId] : []
|
|
31
35
|
];
|
|
32
36
|
if (own?.resourceId && allowed.includes(own.resourceId)) return own.resourceId;
|
|
33
|
-
return
|
|
37
|
+
return userSessionResourceFor(session, session.visibility === "org" ? "org" : "private");
|
|
38
|
+
}
|
|
39
|
+
/** The resource a person's session belongs on for a visibility: its Factory's once shared, its owner's while private. */
|
|
40
|
+
function userSessionResourceFor(session, visibility) {
|
|
41
|
+
return visibility === "org" && session.factoryProjectId ? session.factoryProjectId : session.userId;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The threads to move when a person's session changes visibility and so moves
|
|
45
|
+
* onto `resourceId`: its own thread and the threads stamped with its id that
|
|
46
|
+
* sit on its owner's or Factory's resource. Threads under the session's own id
|
|
47
|
+
* (from before resources were shared) stay where they are.
|
|
48
|
+
*/
|
|
49
|
+
async function sessionThreadsToMove(controller, session, resourceId) {
|
|
50
|
+
const movable = /* @__PURE__ */ new Set([session.userId, ...session.factoryProjectId ? [session.factoryProjectId] : []]);
|
|
51
|
+
const [own, stamped] = await Promise.all([controller.queryThreadById({ threadId: session.sessionId }), controller.queryThreads({ metadata: { factorySessionId: session.sessionId } })]);
|
|
52
|
+
const threads = [...own ? [own] : [], ...stamped].filter((thread) => thread.resourceId !== resourceId && movable.has(thread.resourceId));
|
|
53
|
+
return [...new Set(threads.map((thread) => thread.id))];
|
|
34
54
|
}
|
|
35
55
|
/**
|
|
36
56
|
* Whether a `factorySessionId` stamp naming this session counts on a thread
|
|
@@ -65,6 +85,6 @@ async function listFactorySessionThreads(controller, session, runs) {
|
|
|
65
85
|
return [...threads.values()];
|
|
66
86
|
}
|
|
67
87
|
//#endregion
|
|
68
|
-
export { isRunBound, listFactorySessionThreads, memoryResourceOf, sessionMemoryResourceId, sessionStampCounts };
|
|
88
|
+
export { isRunBound, listFactorySessionThreads, memoryResourceOf, sessionMemoryResourceId, sessionStampCounts, sessionThreadsToMove, userSessionResourceFor };
|
|
69
89
|
|
|
70
90
|
//# sourceMappingURL=session-threads.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-threads.js","names":[],"sources":["../../src/session/session-threads.ts"],"sourcesContent":["import type { AgentController, AgentControllerThread } from '@mastra/core/agent-controller';\n\nimport type { WorkItemsStorage } from '../storage/domains/work-items/base.js';\n\n/** The Factory session row fields that say which memory resource it uses. */\nexport interface FactorySessionOwnership {\n sessionId: string;\n userId: string;\n orgId?: string;\n factoryProjectId?: string | null;\n branch?: string;\n visibility?: string;\n originSessionId?: string | null;\n}\n\n/** Run bindings: the server-side record that a session runs work for its Factory. */\nexport type SessionRunBindings = Pick<WorkItemsStorage, 'hasRunBindingForSession'>;\n\ntype ThreadLookup = Pick<AgentController<any>, 'queryThreadById'>;\n\n/**\n * The one memory resource a Factory session runs under. A Slack session uses\n * its Factory's, or its sender's for a DM; a side session uses its Factory's,\n * or its owner's when private; any other session uses its owner's. A session's own thread can say otherwise\n * only within what the server recorded for it: its own id (created before\n * resources were shared, or a run kept on its own resource), and the Factory's\n * only for a session with a run binding. A client can create a thread under the\n * session's id, so the thread alone never makes a session the Factory's.\n */\nexport async function sessionMemoryResourceId(\n controller: ThreadLookup | undefined,\n session: FactorySessionOwnership,\n runs: SessionRunBindings | undefined,\n): Promise<string> {\n const [own, runBound] = await Promise.all([\n controller?.queryThreadById({ threadId: session.sessionId }),\n isRunBound(session, runs),\n ]);\n return memoryResourceOf(session, own, runBound);\n}\n\nexport async function isRunBound(session: FactorySessionOwnership, runs: SessionRunBindings | undefined) {\n if (!runs || !session.orgId || !session.factoryProjectId) return false;\n return runs.hasRunBindingForSession({\n orgId: session.orgId,\n factoryProjectId: session.factoryProjectId,\n sessionId: session.sessionId,\n });\n}\n\n/** {@link sessionMemoryResourceId} for a caller that already loaded the session's own thread. */\nexport function memoryResourceOf(\n session: FactorySessionOwnership,\n own: { resourceId?: string | null } | null | undefined,\n runBound: boolean,\n) {\n const factoryProjectId = session.factoryProjectId ?? undefined;\n const slack = session.branch?.startsWith('slack/') ?? false;\n // A private Slack DM or side session stays with its owner.\n const shared =\n slack || session.originSessionId\n ? session.visibility === 'private'\n ? session.userId\n : factoryProjectId\n : undefined;\n
|
|
1
|
+
{"version":3,"file":"session-threads.js","names":[],"sources":["../../src/session/session-threads.ts"],"sourcesContent":["import type { AgentController, AgentControllerThread } from '@mastra/core/agent-controller';\n\nimport type { WorkItemsStorage } from '../storage/domains/work-items/base.js';\n\n/** The Factory session row fields that say which memory resource it uses. */\nexport interface FactorySessionOwnership {\n sessionId: string;\n userId: string;\n orgId?: string;\n factoryProjectId?: string | null;\n branch?: string;\n visibility?: string;\n originSessionId?: string | null;\n}\n\n/** Run bindings: the server-side record that a session runs work for its Factory. */\nexport type SessionRunBindings = Pick<WorkItemsStorage, 'hasRunBindingForSession'>;\n\ntype ThreadLookup = Pick<AgentController<any>, 'queryThreadById'>;\n\n/**\n * The one memory resource a Factory session runs under. A Slack session uses\n * its Factory's, or its sender's for a DM; a side session uses its Factory's,\n * or its owner's when private; any other session uses its owner's. A session's own thread can say otherwise\n * only within what the server recorded for it: its own id (created before\n * resources were shared, or a run kept on its own resource), and the Factory's\n * only for a session with a run binding. A client can create a thread under the\n * session's id, so the thread alone never makes a session the Factory's.\n */\nexport async function sessionMemoryResourceId(\n controller: ThreadLookup | undefined,\n session: FactorySessionOwnership,\n runs: SessionRunBindings | undefined,\n): Promise<string> {\n const [own, runBound] = await Promise.all([\n controller?.queryThreadById({ threadId: session.sessionId }),\n isRunBound(session, runs),\n ]);\n return memoryResourceOf(session, own, runBound);\n}\n\nexport async function isRunBound(session: FactorySessionOwnership, runs: SessionRunBindings | undefined) {\n if (!runs || !session.orgId || !session.factoryProjectId) return false;\n return runs.hasRunBindingForSession({\n orgId: session.orgId,\n factoryProjectId: session.factoryProjectId,\n sessionId: session.sessionId,\n });\n}\n\n/** {@link sessionMemoryResourceId} for a caller that already loaded the session's own thread. */\nexport function memoryResourceOf(\n session: FactorySessionOwnership,\n own: { resourceId?: string | null } | null | undefined,\n runBound: boolean,\n) {\n const factoryProjectId = session.factoryProjectId ?? undefined;\n const slack = session.branch?.startsWith('slack/') ?? false;\n // A private Slack DM or side session stays with its owner.\n const shared =\n slack || session.originSessionId\n ? session.visibility === 'private'\n ? session.userId\n : factoryProjectId\n : undefined;\n if (shared) {\n if (own?.resourceId && [session.sessionId, shared].includes(own.resourceId)) return own.resourceId;\n return shared;\n }\n // A session a person started runs on their resource while private and its\n // Factory's once its owner shares it, which moves its threads there. Its own\n // thread says where it is; a client cannot make a private session the\n // Factory's by writing that thread, since the Factory is only allowed for a\n // shared or run-bound session.\n const allowed = [\n session.sessionId,\n session.userId,\n ...((runBound || session.visibility === 'org') && factoryProjectId ? [factoryProjectId] : []),\n ];\n if (own?.resourceId && allowed.includes(own.resourceId)) return own.resourceId;\n // No thread yet: where a session with this visibility starts.\n return userSessionResourceFor(session, session.visibility === 'org' ? 'org' : 'private');\n}\n\n/** The resource a person's session belongs on for a visibility: its Factory's once shared, its owner's while private. */\nexport function userSessionResourceFor(\n session: Pick<FactorySessionOwnership, 'userId' | 'factoryProjectId'>,\n visibility: 'org' | 'private',\n): string {\n return visibility === 'org' && session.factoryProjectId ? session.factoryProjectId : session.userId;\n}\n\n/**\n * The threads to move when a person's session changes visibility and so moves\n * onto `resourceId`: its own thread and the threads stamped with its id that\n * sit on its owner's or Factory's resource. Threads under the session's own id\n * (from before resources were shared) stay where they are.\n */\nexport async function sessionThreadsToMove(\n controller: Pick<AgentController<any>, 'queryThreads' | 'queryThreadById'>,\n session: FactorySessionOwnership,\n resourceId: string,\n): Promise<string[]> {\n const movable = new Set([session.userId, ...(session.factoryProjectId ? [session.factoryProjectId] : [])]);\n const [own, stamped] = await Promise.all([\n controller.queryThreadById({ threadId: session.sessionId }),\n controller.queryThreads({ metadata: { factorySessionId: session.sessionId } }),\n ]);\n const threads = [...(own ? [own] : []), ...stamped].filter(\n thread => thread.resourceId !== resourceId && movable.has(thread.resourceId),\n );\n return [...new Set(threads.map(thread => thread.id))];\n}\n\n/**\n * Whether a `factorySessionId` stamp naming this session counts on a thread\n * under `resourceId`. Any org member can write a stamped thread under the\n * Factory's resource, so a private session's stamp never counts there: only\n * its own thread (`thread.id === sessionId`) does.\n */\nexport function sessionStampCounts(session: FactorySessionOwnership, resourceId: string | null | undefined): boolean {\n return !(session.visibility === 'private' && !!session.factoryProjectId && resourceId === session.factoryProjectId);\n}\n\n/**\n * The threads of one Factory session. Sessions share their Factory's or user's\n * resource, so listing by resource would return every session's threads. A\n * session's own thread carries its id, threads it opens are stamped with\n * `factorySessionId`, and a session created before resources were shared keeps\n * its threads under its own id as the resource. Thread ids and stamps are\n * client-writable, so the own and stamped threads only count under the\n * session's memory resource.\n */\nexport async function listFactorySessionThreads(\n controller: Pick<AgentController<any>, 'queryThreads' | 'queryThreadById'>,\n session: FactorySessionOwnership,\n runs: SessionRunBindings | undefined,\n): Promise<AgentControllerThread[]> {\n const { sessionId } = session;\n const [own, runBound, stamped, legacy] = await Promise.all([\n controller.queryThreadById({ threadId: sessionId }),\n isRunBound(session, runs),\n controller.queryThreads({ metadata: { factorySessionId: sessionId } }),\n controller.queryThreads({ resourceId: sessionId }),\n ]);\n const resourceId = memoryResourceOf(session, own, runBound);\n const threads = new Map<string, AgentControllerThread>();\n const owned = [\n ...(own?.resourceId === resourceId ? [own] : []),\n ...(sessionStampCounts(session, resourceId) ? stamped.filter(thread => thread.resourceId === resourceId) : []),\n ];\n for (const thread of [...owned, ...legacy]) threads.set(thread.id, thread);\n return [...threads.values()];\n}\n"],"mappings":"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"}
|
package/dist/spa-static.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spa-static.js","names":[],"sources":["../src/spa-static.ts"],"sourcesContent":["/**\n * Same-origin SPA serving for production.\n *\n * In dev the SPA is served by Vite (:5173) which proxies API paths to the\n * server (:4111) — this module is effectively idle there. In production the\n * built SPA (vite output) is served by the API server itself at `/`, so the\n * app is a single origin and no CORS / separate static host is required.\n *\n * The middleware is mounted via the Mastra entry's `server.middleware`, which\n * the deployer applies before routes: real file hits and SPA navigations are\n * answered here, everything else falls through to the API routes.\n */\n\nimport { existsSync } from 'node:fs';\nimport { readFile, stat } from 'node:fs/promises';\nimport { dirname, extname, join, normalize, resolve, sep } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nimport type { Context } from 'hono';\n\nconst MIME: Record<string, string> = {\n '.html': 'text/html; charset=utf-8',\n '.js': 'text/javascript; charset=utf-8',\n '.mjs': 'text/javascript; charset=utf-8',\n '.css': 'text/css; charset=utf-8',\n '.json': 'application/json',\n '.webmanifest': 'application/manifest+json',\n '.map': 'application/json',\n '.svg': 'image/svg+xml',\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.gif': 'image/gif',\n '.webp': 'image/webp',\n '.ico': 'image/x-icon',\n '.txt': 'text/plain; charset=utf-8',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n '.ttf': 'font/ttf',\n '.wasm': 'application/wasm',\n};\n\n/** Server-owned path prefixes the SPA middleware must never answer for. */\nconst SERVER_PREFIXES = ['/api', '/web', '/auth', '/connect'];\n\n/**\n * Locate the built SPA (a dir containing `index.html`). Checked in order:\n * 1. `MASTRACODE_UI_DIST` — explicit override for custom layouts.\n * 2. `factory/` under cwd — both `mastra dev` (cwd is the Mastra public dir)\n * and `mastra start` (cwd is `.mastra/output`) expose assets this way.\n * 3. `factory/` next to the bundled server module.\n * 4. Source-layout fallbacks under `MASTRA_PROJECT_ROOT` or cwd.\n * Returns `undefined` when no build is found (e.g. plain `mastra dev` without\n * a prior vite build), in which case the middleware is simply not mounted.\n */\nexport function resolveUiDistDir(): string | undefined {\n const projectRoot = process.env.MASTRA_PROJECT_ROOT?.trim();\n const candidates = [\n process.env.MASTRACODE_UI_DIST,\n resolve(process.cwd(), 'factory'),\n join(dirname(fileURLToPath(import.meta.url)), 'factory'),\n projectRoot && resolve(projectRoot, 'src/mastra/public/factory'),\n resolve(process.cwd(), 'src/mastra/public/factory'),\n ];\n for (const candidate of candidates) {\n if (candidate && existsSync(join(candidate, 'index.html'))) return resolve(candidate);\n }\n return undefined;\n}\n\nasync function isFile(path: string): Promise<boolean> {\n try {\n return (await stat(path)).isFile();\n } catch {\n return false;\n }\n}\n\nasync function serveFile(c: Context, filePath: string, immutable: boolean): Promise<Response> {\n const data = await readFile(filePath);\n c.header('Content-Type', MIME[extname(filePath).toLowerCase()] ?? 'application/octet-stream');\n // Vite emits content-hashed filenames under assets/ — cache those forever;\n // index.html (and anything unhashed) must revalidate so deploys roll out.\n c.header('Cache-Control', immutable ? 'public, max-age=31536000, immutable' : 'no-cache');\n return c.body(new Uint8Array(data));\n}\n\n/**\n * Hono middleware serving the built SPA from `uiDist`:\n * - exact file hits (js/css/assets) are served directly,\n * - GET navigations to non-server paths fall back to `index.html` (SPA\n * client-side routing),\n * - `/api`, `/web`, `/auth` and non-GET requests always pass through.\n */\nexport function createSpaStaticMiddleware(uiDist: string) {\n return async (c: Context, next: () => Promise<void>): Promise<Response | void> => {\n if (c.req.method !== 'GET' && c.req.method !== 'HEAD') return next();\n const path = c.req.path;\n if (SERVER_PREFIXES.some(prefix => path === prefix || path.startsWith(`${prefix}/`))) return next();\n\n // Resolve the request path inside uiDist, rejecting traversal escapes.\n const relative = normalize(decodeURIComponent(path)).replace(/^[/\\\\]+/, '');\n const filePath = resolve(uiDist, relative);\n // Use a trailing separator so a sibling like \"ui.key\" can't slip past the\n // prefix check (same pattern as fs-routes.ts isWithinRoot).\n const uiDistPrefix = uiDist.endsWith(sep) ? uiDist : uiDist + sep;\n if (filePath.startsWith(uiDistPrefix) && relative !== '' && (await isFile(filePath))) {\n return serveFile(c, filePath, relative.startsWith('assets/'));\n }\n\n // SPA fallback: serve index.html for root and html navigations.\n const accept = c.req.header('Accept') ?? '';\n if (path === '/' || accept.includes('text/html')) {\n return serveFile(c, join(uiDist, 'index.html'), false);\n }\n\n return next();\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAoBA,MAAM,OAA+B;CACnC,SAAS;CACT,OAAO;CACP,QAAQ;CACR,QAAQ;CACR,SAAS;CACT,gBAAgB;CAChB,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,SAAS;CACT,QAAQ;CACR,SAAS;CACT,QAAQ;CACR,QAAQ;CACR,SAAS;CACT,UAAU;CACV,QAAQ;CACR,SAAS;AACX;;AAGA,MAAM,kBAAkB;CAAC;CAAQ;CAAQ;CAAS;AAAU;;;;;;;;;;;AAY5D,SAAgB,mBAAuC;CACrD,MAAM,cAAc,QAAQ,IAAI,qBAAqB,KAAK;CAC1D,MAAM,aAAa;EACjB,QAAQ,IAAI;EACZ,QAAQ,QAAQ,IAAI,GAAG,SAAS;EAChC,KAAK,QAAQ,cAAc,
|
|
1
|
+
{"version":3,"file":"spa-static.js","names":[],"sources":["../src/spa-static.ts"],"sourcesContent":["/**\n * Same-origin SPA serving for production.\n *\n * In dev the SPA is served by Vite (:5173) which proxies API paths to the\n * server (:4111) — this module is effectively idle there. In production the\n * built SPA (vite output) is served by the API server itself at `/`, so the\n * app is a single origin and no CORS / separate static host is required.\n *\n * The middleware is mounted via the Mastra entry's `server.middleware`, which\n * the deployer applies before routes: real file hits and SPA navigations are\n * answered here, everything else falls through to the API routes.\n */\n\nimport { existsSync } from 'node:fs';\nimport { readFile, stat } from 'node:fs/promises';\nimport { dirname, extname, join, normalize, resolve, sep } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nimport type { Context } from 'hono';\n\nconst MIME: Record<string, string> = {\n '.html': 'text/html; charset=utf-8',\n '.js': 'text/javascript; charset=utf-8',\n '.mjs': 'text/javascript; charset=utf-8',\n '.css': 'text/css; charset=utf-8',\n '.json': 'application/json',\n '.webmanifest': 'application/manifest+json',\n '.map': 'application/json',\n '.svg': 'image/svg+xml',\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.gif': 'image/gif',\n '.webp': 'image/webp',\n '.ico': 'image/x-icon',\n '.txt': 'text/plain; charset=utf-8',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n '.ttf': 'font/ttf',\n '.wasm': 'application/wasm',\n};\n\n/** Server-owned path prefixes the SPA middleware must never answer for. */\nconst SERVER_PREFIXES = ['/api', '/web', '/auth', '/connect'];\n\n/**\n * Locate the built SPA (a dir containing `index.html`). Checked in order:\n * 1. `MASTRACODE_UI_DIST` — explicit override for custom layouts.\n * 2. `factory/` under cwd — both `mastra dev` (cwd is the Mastra public dir)\n * and `mastra start` (cwd is `.mastra/output`) expose assets this way.\n * 3. `factory/` next to the bundled server module.\n * 4. Source-layout fallbacks under `MASTRA_PROJECT_ROOT` or cwd.\n * Returns `undefined` when no build is found (e.g. plain `mastra dev` without\n * a prior vite build), in which case the middleware is simply not mounted.\n */\nexport function resolveUiDistDir(): string | undefined {\n const projectRoot = process.env.MASTRA_PROJECT_ROOT?.trim();\n const candidates = [\n process.env.MASTRACODE_UI_DIST,\n resolve(process.cwd(), 'factory'),\n join(dirname(fileURLToPath(import.meta.url)), 'factory'),\n projectRoot && resolve(projectRoot, 'src/mastra/public/factory'),\n resolve(process.cwd(), 'src/mastra/public/factory'),\n ];\n for (const candidate of candidates) {\n if (candidate && existsSync(join(candidate, 'index.html'))) return resolve(candidate);\n }\n return undefined;\n}\n\nasync function isFile(path: string): Promise<boolean> {\n try {\n return (await stat(path)).isFile();\n } catch {\n return false;\n }\n}\n\nasync function serveFile(c: Context, filePath: string, immutable: boolean): Promise<Response> {\n const data = await readFile(filePath);\n c.header('Content-Type', MIME[extname(filePath).toLowerCase()] ?? 'application/octet-stream');\n // Vite emits content-hashed filenames under assets/ — cache those forever;\n // index.html (and anything unhashed) must revalidate so deploys roll out.\n c.header('Cache-Control', immutable ? 'public, max-age=31536000, immutable' : 'no-cache');\n return c.body(new Uint8Array(data));\n}\n\n/**\n * Hono middleware serving the built SPA from `uiDist`:\n * - exact file hits (js/css/assets) are served directly,\n * - GET navigations to non-server paths fall back to `index.html` (SPA\n * client-side routing),\n * - `/api`, `/web`, `/auth` and non-GET requests always pass through.\n */\nexport function createSpaStaticMiddleware(uiDist: string) {\n return async (c: Context, next: () => Promise<void>): Promise<Response | void> => {\n if (c.req.method !== 'GET' && c.req.method !== 'HEAD') return next();\n const path = c.req.path;\n if (SERVER_PREFIXES.some(prefix => path === prefix || path.startsWith(`${prefix}/`))) return next();\n\n // Resolve the request path inside uiDist, rejecting traversal escapes.\n const relative = normalize(decodeURIComponent(path)).replace(/^[/\\\\]+/, '');\n const filePath = resolve(uiDist, relative);\n // Use a trailing separator so a sibling like \"ui.key\" can't slip past the\n // prefix check (same pattern as fs-routes.ts isWithinRoot).\n const uiDistPrefix = uiDist.endsWith(sep) ? uiDist : uiDist + sep;\n if (filePath.startsWith(uiDistPrefix) && relative !== '' && (await isFile(filePath))) {\n return serveFile(c, filePath, relative.startsWith('assets/'));\n }\n\n // SPA fallback: serve index.html for root and html navigations.\n const accept = c.req.header('Accept') ?? '';\n if (path === '/' || accept.includes('text/html')) {\n return serveFile(c, join(uiDist, 'index.html'), false);\n }\n\n return next();\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAoBA,MAAM,OAA+B;CACnC,SAAS;CACT,OAAO;CACP,QAAQ;CACR,QAAQ;CACR,SAAS;CACT,gBAAgB;CAChB,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,SAAS;CACT,QAAQ;CACR,SAAS;CACT,QAAQ;CACR,QAAQ;CACR,SAAS;CACT,UAAU;CACV,QAAQ;CACR,SAAS;AACX;;AAGA,MAAM,kBAAkB;CAAC;CAAQ;CAAQ;CAAS;AAAU;;;;;;;;;;;AAY5D,SAAgB,mBAAuC;CACrD,MAAM,cAAc,QAAQ,IAAI,qBAAqB,KAAK;CAC1D,MAAM,aAAa;EACjB,QAAQ,IAAI;EACZ,QAAQ,QAAQ,IAAI,GAAG,SAAS;EAChC,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC,GAAG,SAAS;EACvD,eAAe,QAAQ,aAAa,2BAA2B;EAC/D,QAAQ,QAAQ,IAAI,GAAG,2BAA2B;CACpD;CACA,KAAK,MAAM,aAAa,YACtB,IAAI,aAAa,WAAW,KAAK,WAAW,YAAY,CAAC,GAAG,OAAO,QAAQ,SAAS;AAGxF;AAEA,eAAe,OAAO,MAAgC;CACpD,IAAI;EACF,QAAQ,MAAM,KAAK,IAAI,EAAA,CAAG,OAAO;CACnC,QAAQ;EACN,OAAO;CACT;AACF;AAEA,eAAe,UAAU,GAAY,UAAkB,WAAuC;CAC5F,MAAM,OAAO,MAAM,SAAS,QAAQ;CACpC,EAAE,OAAO,gBAAgB,KAAK,QAAQ,QAAQ,CAAC,CAAC,YAAY,MAAM,0BAA0B;CAG5F,EAAE,OAAO,iBAAiB,YAAY,wCAAwC,UAAU;CACxF,OAAO,EAAE,KAAK,IAAI,WAAW,IAAI,CAAC;AACpC;;;;;;;;AASA,SAAgB,0BAA0B,QAAgB;CACxD,OAAO,OAAO,GAAY,SAAwD;EAChF,IAAI,EAAE,IAAI,WAAW,SAAS,EAAE,IAAI,WAAW,QAAQ,OAAO,KAAK;EACnE,MAAM,OAAO,EAAE,IAAI;EACnB,IAAI,gBAAgB,MAAK,WAAU,SAAS,UAAU,KAAK,WAAW,GAAG,OAAO,EAAE,CAAC,GAAG,OAAO,KAAK;EAGlG,MAAM,WAAW,UAAU,mBAAmB,IAAI,CAAC,CAAC,CAAC,QAAQ,WAAW,EAAE;EAC1E,MAAM,WAAW,QAAQ,QAAQ,QAAQ;EAGzC,MAAM,eAAe,OAAO,SAAS,GAAG,IAAI,SAAS,SAAS;EAC9D,IAAI,SAAS,WAAW,YAAY,KAAK,aAAa,MAAO,MAAM,OAAO,QAAQ,GAChF,OAAO,UAAU,GAAG,UAAU,SAAS,WAAW,SAAS,CAAC;EAI9D,MAAM,SAAS,EAAE,IAAI,OAAO,QAAQ,KAAK;EACzC,IAAI,SAAS,OAAO,OAAO,SAAS,WAAW,GAC7C,OAAO,UAAU,GAAG,KAAK,QAAQ,YAAY,GAAG,KAAK;EAGvD,OAAO,KAAK;CACd;AACF"}
|
package/dist/state-signing.js
CHANGED
|
@@ -25,7 +25,7 @@ import { createHmac, timingSafeEqual } from "crypto";
|
|
|
25
25
|
* so in-flight OAuth states survive a deploy.
|
|
26
26
|
*/
|
|
27
27
|
/** Signed `state` values expire after this window to bound the CSRF token. */
|
|
28
|
-
const STATE_MAX_AGE_MS =
|
|
28
|
+
const STATE_MAX_AGE_MS = 6e5;
|
|
29
29
|
/**
|
|
30
30
|
* Create a state signer. With a `secret`, the signer is deployment-stable
|
|
31
31
|
* (`stable: true`); without one it falls back to a per-process random secret
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state-signing.js","names":[],"sources":["../src/state-signing.ts"],"sourcesContent":["/**\n * Shared OAuth/install `state` signing for web integrations.\n *\n * The GitHub, Linear, and Slack OAuth/OIDC flows each round-trip a signed `state`\n * value\n * through the third party to bind the callback to the `(orgId, userId)` tenant\n * that initiated it (CSRF protection + tenant routing). The signer is a system\n * facility: `MastraFactory` creates ONE signer at boot and hands it to every\n * registered integration through `IntegrationContext` (see\n * `./factory-integration.ts`), so all integrations sign and verify with the\n * same secret.\n *\n * Secret resolution happens in the factory, not here: explicit\n * `config.stateSecret` → the GitHub integration's webhook secret → a\n * per-process random secret. A random secret is NOT stable across replicas —\n * a `state` signed by one replica cannot be verified by another — which is\n * what the `stable` flag reports. The factory fails loud at boot when a\n * registered integration requires a stable signer but only a random one is\n * available.\n *\n * The wire format (base64url JSON payload + `.` + HMAC-SHA256 base64url\n * signature) is unchanged from the previous `github/config.ts` implementation\n * so in-flight OAuth states survive a deploy.\n */\n\nimport { createHmac, timingSafeEqual } from 'node:crypto';\n\n/** Verified tenant and optional Factory context carried by a signed `state`. */\nexport interface StateTenant {\n orgId: string;\n userId: string;\n /** Factory that initiated the integration flow, when the caller supplied one. */\n factoryProjectId?: string;\n /**\n * Per-`state` random value. A signed `state` stays valid for its whole\n * lifetime, so a flow that must not run twice off one `state` (account\n * binding, for instance) can key single-use bookkeeping on this.\n */\n nonce: string;\n}\n\n/** Signs and verifies OAuth `state` values bound to a tenant and optional Factory. */\nexport interface StateSigner {\n /** Build a signed `state` bound to the tenant and optional initiating Factory. */\n sign(orgId: string, userId: string, context?: { factoryProjectId?: string }): string;\n /** Verify a signed `state`; returns the bound tenant, or `null` if invalid. */\n verify(state: string | undefined): StateTenant | null;\n /**\n * True when the signer was built from an explicit deployment-stable secret.\n * False means a per-process random secret: fine for single-process/local\n * dev, broken for multi-replica deploys (see module docs).\n */\n readonly stable: boolean;\n}\n\ninterface StatePayload {\n orgId: string;\n userId: string;\n factoryProjectId?: string;\n nonce: string;\n issuedAt: number;\n}\n\n/** Signed `state` values expire after this window to bound the CSRF token. */\nconst STATE_MAX_AGE_MS = 10 * 60 * 1000;\n\n/**\n * Create a state signer. With a `secret`, the signer is deployment-stable\n * (`stable: true`); without one it falls back to a per-process random secret\n * (`stable: false`).\n */\nexport function createStateSigner(secret?: string): StateSigner {\n const stable = typeof secret === 'string' && secret.length > 0;\n const key = stable ? secret : Buffer.from(globalThis.crypto.getRandomValues(new Uint8Array(32))).toString('hex');\n return {\n stable,\n sign(orgId: string, userId: string, context?: { factoryProjectId?: string }): string {\n const payload: StatePayload = {\n orgId,\n userId,\n ...(context?.factoryProjectId ? { factoryProjectId: context.factoryProjectId } : {}),\n nonce: Buffer.from(globalThis.crypto.getRandomValues(new Uint8Array(8))).toString('hex'),\n issuedAt: Date.now(),\n };\n const body = Buffer.from(JSON.stringify(payload), 'utf8').toString('base64url');\n const sig = createHmac('sha256', key).update(body).digest('base64url');\n return `${body}.${sig}`;\n },\n verify(state: string | undefined): StateTenant | null {\n if (!state) return null;\n const dot = state.lastIndexOf('.');\n if (dot <= 0) return null;\n const body = state.slice(0, dot);\n const sig = state.slice(dot + 1);\n const expected = createHmac('sha256', key).update(body).digest('base64url');\n const sigBuf = Buffer.from(sig);\n const expectedBuf = Buffer.from(expected);\n if (sigBuf.length !== expectedBuf.length || !timingSafeEqual(sigBuf, expectedBuf)) {\n return null;\n }\n try {\n const parsed = JSON.parse(Buffer.from(body, 'base64url').toString('utf8')) as StatePayload;\n if (typeof parsed.orgId !== 'string' || typeof parsed.userId !== 'string') return null;\n if (typeof parsed.issuedAt !== 'number' || !Number.isFinite(parsed.issuedAt)) return null;\n if (typeof parsed.nonce !== 'string' || parsed.nonce.length === 0) return null;\n if (\n parsed.factoryProjectId !== undefined &&\n (typeof parsed.factoryProjectId !== 'string' || parsed.factoryProjectId.length === 0)\n ) {\n return null;\n }\n const age = Date.now() - parsed.issuedAt;\n if (age < 0 || age > STATE_MAX_AGE_MS) return null;\n return {\n orgId: parsed.orgId,\n userId: parsed.userId,\n ...(parsed.factoryProjectId ? { factoryProjectId: parsed.factoryProjectId } : {}),\n nonce: parsed.nonce,\n };\n } catch {\n return null;\n }\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAgEA,MAAM,mBAAmB
|
|
1
|
+
{"version":3,"file":"state-signing.js","names":[],"sources":["../src/state-signing.ts"],"sourcesContent":["/**\n * Shared OAuth/install `state` signing for web integrations.\n *\n * The GitHub, Linear, and Slack OAuth/OIDC flows each round-trip a signed `state`\n * value\n * through the third party to bind the callback to the `(orgId, userId)` tenant\n * that initiated it (CSRF protection + tenant routing). The signer is a system\n * facility: `MastraFactory` creates ONE signer at boot and hands it to every\n * registered integration through `IntegrationContext` (see\n * `./factory-integration.ts`), so all integrations sign and verify with the\n * same secret.\n *\n * Secret resolution happens in the factory, not here: explicit\n * `config.stateSecret` → the GitHub integration's webhook secret → a\n * per-process random secret. A random secret is NOT stable across replicas —\n * a `state` signed by one replica cannot be verified by another — which is\n * what the `stable` flag reports. The factory fails loud at boot when a\n * registered integration requires a stable signer but only a random one is\n * available.\n *\n * The wire format (base64url JSON payload + `.` + HMAC-SHA256 base64url\n * signature) is unchanged from the previous `github/config.ts` implementation\n * so in-flight OAuth states survive a deploy.\n */\n\nimport { createHmac, timingSafeEqual } from 'node:crypto';\n\n/** Verified tenant and optional Factory context carried by a signed `state`. */\nexport interface StateTenant {\n orgId: string;\n userId: string;\n /** Factory that initiated the integration flow, when the caller supplied one. */\n factoryProjectId?: string;\n /**\n * Per-`state` random value. A signed `state` stays valid for its whole\n * lifetime, so a flow that must not run twice off one `state` (account\n * binding, for instance) can key single-use bookkeeping on this.\n */\n nonce: string;\n}\n\n/** Signs and verifies OAuth `state` values bound to a tenant and optional Factory. */\nexport interface StateSigner {\n /** Build a signed `state` bound to the tenant and optional initiating Factory. */\n sign(orgId: string, userId: string, context?: { factoryProjectId?: string }): string;\n /** Verify a signed `state`; returns the bound tenant, or `null` if invalid. */\n verify(state: string | undefined): StateTenant | null;\n /**\n * True when the signer was built from an explicit deployment-stable secret.\n * False means a per-process random secret: fine for single-process/local\n * dev, broken for multi-replica deploys (see module docs).\n */\n readonly stable: boolean;\n}\n\ninterface StatePayload {\n orgId: string;\n userId: string;\n factoryProjectId?: string;\n nonce: string;\n issuedAt: number;\n}\n\n/** Signed `state` values expire after this window to bound the CSRF token. */\nconst STATE_MAX_AGE_MS = 10 * 60 * 1000;\n\n/**\n * Create a state signer. With a `secret`, the signer is deployment-stable\n * (`stable: true`); without one it falls back to a per-process random secret\n * (`stable: false`).\n */\nexport function createStateSigner(secret?: string): StateSigner {\n const stable = typeof secret === 'string' && secret.length > 0;\n const key = stable ? secret : Buffer.from(globalThis.crypto.getRandomValues(new Uint8Array(32))).toString('hex');\n return {\n stable,\n sign(orgId: string, userId: string, context?: { factoryProjectId?: string }): string {\n const payload: StatePayload = {\n orgId,\n userId,\n ...(context?.factoryProjectId ? { factoryProjectId: context.factoryProjectId } : {}),\n nonce: Buffer.from(globalThis.crypto.getRandomValues(new Uint8Array(8))).toString('hex'),\n issuedAt: Date.now(),\n };\n const body = Buffer.from(JSON.stringify(payload), 'utf8').toString('base64url');\n const sig = createHmac('sha256', key).update(body).digest('base64url');\n return `${body}.${sig}`;\n },\n verify(state: string | undefined): StateTenant | null {\n if (!state) return null;\n const dot = state.lastIndexOf('.');\n if (dot <= 0) return null;\n const body = state.slice(0, dot);\n const sig = state.slice(dot + 1);\n const expected = createHmac('sha256', key).update(body).digest('base64url');\n const sigBuf = Buffer.from(sig);\n const expectedBuf = Buffer.from(expected);\n if (sigBuf.length !== expectedBuf.length || !timingSafeEqual(sigBuf, expectedBuf)) {\n return null;\n }\n try {\n const parsed = JSON.parse(Buffer.from(body, 'base64url').toString('utf8')) as StatePayload;\n if (typeof parsed.orgId !== 'string' || typeof parsed.userId !== 'string') return null;\n if (typeof parsed.issuedAt !== 'number' || !Number.isFinite(parsed.issuedAt)) return null;\n if (typeof parsed.nonce !== 'string' || parsed.nonce.length === 0) return null;\n if (\n parsed.factoryProjectId !== undefined &&\n (typeof parsed.factoryProjectId !== 'string' || parsed.factoryProjectId.length === 0)\n ) {\n return null;\n }\n const age = Date.now() - parsed.issuedAt;\n if (age < 0 || age > STATE_MAX_AGE_MS) return null;\n return {\n orgId: parsed.orgId,\n userId: parsed.userId,\n ...(parsed.factoryProjectId ? { factoryProjectId: parsed.factoryProjectId } : {}),\n nonce: parsed.nonce,\n };\n } catch {\n return null;\n }\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAgEA,MAAM,mBAAmB;;;;;;AAOzB,SAAgB,kBAAkB,QAA8B;CAC9D,MAAM,SAAS,OAAO,WAAW,YAAY,OAAO,SAAS;CAC7D,MAAM,MAAM,SAAS,SAAS,OAAO,KAAK,WAAW,OAAO,gCAAgB,IAAI,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,KAAK;CAC/G,OAAO;EACL;EACA,KAAK,OAAe,QAAgB,SAAiD;GACnF,MAAM,UAAwB;IAC5B;IACA;IACA,GAAI,SAAS,mBAAmB,EAAE,kBAAkB,QAAQ,iBAAiB,IAAI,CAAC;IAClF,OAAO,OAAO,KAAK,WAAW,OAAO,gCAAgB,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,KAAK;IACvF,UAAU,KAAK,IAAI;GACrB;GACA,MAAM,OAAO,OAAO,KAAK,KAAK,UAAU,OAAO,GAAG,MAAM,CAAC,CAAC,SAAS,WAAW;GAE9E,OAAO,GAAG,KAAK,GADH,WAAW,UAAU,GAAG,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,OAAO,WACtC;EACtB;EACA,OAAO,OAA+C;GACpD,IAAI,CAAC,OAAO,OAAO;GACnB,MAAM,MAAM,MAAM,YAAY,GAAG;GACjC,IAAI,OAAO,GAAG,OAAO;GACrB,MAAM,OAAO,MAAM,MAAM,GAAG,GAAG;GAC/B,MAAM,MAAM,MAAM,MAAM,MAAM,CAAC;GAC/B,MAAM,WAAW,WAAW,UAAU,GAAG,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,OAAO,WAAW;GAC1E,MAAM,SAAS,OAAO,KAAK,GAAG;GAC9B,MAAM,cAAc,OAAO,KAAK,QAAQ;GACxC,IAAI,OAAO,WAAW,YAAY,UAAU,CAAC,gBAAgB,QAAQ,WAAW,GAC9E,OAAO;GAET,IAAI;IACF,MAAM,SAAS,KAAK,MAAM,OAAO,KAAK,MAAM,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC;IACzE,IAAI,OAAO,OAAO,UAAU,YAAY,OAAO,OAAO,WAAW,UAAU,OAAO;IAClF,IAAI,OAAO,OAAO,aAAa,YAAY,CAAC,OAAO,SAAS,OAAO,QAAQ,GAAG,OAAO;IACrF,IAAI,OAAO,OAAO,UAAU,YAAY,OAAO,MAAM,WAAW,GAAG,OAAO;IAC1E,IACE,OAAO,qBAAqB,KAAA,MAC3B,OAAO,OAAO,qBAAqB,YAAY,OAAO,iBAAiB,WAAW,IAEnF,OAAO;IAET,MAAM,MAAM,KAAK,IAAI,IAAI,OAAO;IAChC,IAAI,MAAM,KAAK,MAAM,kBAAkB,OAAO;IAC9C,OAAO;KACL,OAAO,OAAO;KACd,QAAQ,OAAO;KACf,GAAI,OAAO,mBAAmB,EAAE,kBAAkB,OAAO,iBAAiB,IAAI,CAAC;KAC/E,OAAO,OAAO;IAChB;GACF,QAAQ;IACN,OAAO;GACT;EACF;CACF;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"base.js","names":["#db","#insert"],"sources":["../../../../src/storage/domains/audit/base.ts"],"sourcesContent":["/**\n * Factory audit events domain — the append-only \"who did what, when\" trail\n * behind the software factory.\n *\n * Rows are append-only: there is no update/delete API, and the table is the\n * local source of truth even when the WorkOS Audit Logs mirror is unavailable.\n * Tenancy is org-first, like `work_items`: `actor_id` records who acted but\n * never scopes reads.\n *\n * The actions the trail holds are listed in `./actions.ts`.\n *\n * Agent events carry `actor_type = 'agent'` with `actor_id = 'agent:<threadId>'`\n * and `metadata.startedBy = <userId>` chaining accountability back to the human\n * whose message drove the run. Rule-driven events carry `actor_type = 'system'`.\n */\n\nimport { FactoryStorageDomain, UniqueViolationError } from '@mastra/core/storage';\nimport type { CollectionSchema, CollectionWhere, FactoryStorageOps } from '@mastra/core/storage';\n\nimport type { AuditActorType } from './actors.js';\n\n/** What an audit event acted on (WorkOS Audit Logs target shape). */\nexport interface AuditTarget {\n /** Target kind, e.g. 'work_item', 'worktree', 'issue', 'pull_request'. */\n type: string;\n /** Stable identifier of the target (row id, branch name, issue number...). */\n id: string;\n /** Human-readable label (work-item title, branch name...). */\n name?: string;\n}\n\nexport type { AuditActorType } from './actors.js';\n\n/** Display name and avatar of a human actor, stamped at record time because MastraAuthStudio cannot resolve users by id. */\nexport interface AuditActorProfileInput {\n name?: string;\n avatarUrl?: string;\n}\n\nexport const ACTOR_PROFILE_METADATA_KEY = '__actorProfile';\n\nexport function auditActorProfile(\n user: { name?: string; email?: string; avatarUrl?: string } | undefined,\n): AuditActorProfileInput | undefined {\n const name = user?.name?.trim() || user?.email?.trim();\n const avatarUrl = user?.avatarUrl?.trim();\n if (!name && !avatarUrl) return undefined;\n return { ...(name ? { name } : {}), ...(avatarUrl ? { avatarUrl } : {}) };\n}\n\n/** Request context captured alongside the event. */\nexport interface AuditContext {\n /** Client IP (first hop of `x-forwarded-for`) when available. */\n location?: string;\n /** Request `user-agent` header when available. */\n userAgent?: string;\n}\n\n/** One persisted audit event. */\nexport interface AuditEventRow {\n id: string;\n /** Owning WorkOS organization id — the trail is org-wide. */\n orgId: string;\n /** WorkOS user id of whoever performed the action, or `agent:<threadId>`. */\n actorId: string;\n /** Whether a human or an agent (inside a run) performed the action. */\n actorType: AuditActorType;\n /** Dot-namespaced action, e.g. 'factory.work_item.stage_moved'. */\n action: string;\n /** What was acted on. */\n targets: AuditTarget[];\n /** Bounded event summary — never full payloads, never secrets. */\n metadata: Record<string, unknown>;\n /** Factory project the event is scoped to; null for org-level events. */\n factoryProjectId: string | null;\n /** Project-repository link affected by a repository-specific action. */\n projectRepositoryId: string | null;\n /** Request context (`x-forwarded-for` / `user-agent`). */\n context: AuditContext;\n occurredAt: Date;\n}\n\nexport interface RecordAuditEventInput<Action extends string = string> {\n idempotencyKey?: string;\n orgId: string;\n actorId: string;\n /** Who performed the action; defaults to 'human'. */\n actorType?: AuditActorType;\n actorProfile?: AuditActorProfileInput;\n action: Action;\n targets: AuditTarget[];\n metadata?: Record<string, unknown>;\n factoryProjectId?: string;\n projectRepositoryId?: string;\n context?: AuditContext;\n occurredAt?: Date;\n}\n\n/** A fully-normalized event ready to persist (id assigned on insert). */\nexport type AuditEventInsert = Omit<AuditEventRow, 'id'>;\n\nexport interface ListAuditEventsInput {\n orgId: string;\n factoryProjectId?: string;\n /** Restrict to these actions (exact match). */\n actions?: string[];\n actorId?: string;\n /** Opaque cursor from a previous page (`nextCursor`). */\n before?: string;\n limit?: number;\n}\n\nexport interface AuditEventPage {\n events: AuditEventRow[];\n /** Pass back as `before` to fetch the next (older) page; absent at the end. */\n nextCursor?: string;\n}\n\n/** Metadata is a bounded summary — never full payloads, never secrets. */\nconst MAX_METADATA_JSON_LENGTH = 4096;\n\nconst DEFAULT_PAGE_SIZE = 50;\nconst MAX_PAGE_SIZE = 200;\n\n/** Truncate oversized metadata rather than dropping the whole event. */\nexport function boundAuditMetadata(metadata: Record<string, unknown> | undefined): Record<string, unknown> {\n if (!metadata) return {};\n try {\n if (JSON.stringify(metadata).length <= MAX_METADATA_JSON_LENGTH) return metadata;\n } catch {\n return { truncated: true };\n }\n return { truncated: true };\n}\n\n/** Normalize a requested page size to a finite integer in `[1, MAX_PAGE_SIZE]`. */\nexport function clampAuditLimit(limit: number | undefined): number {\n const normalized = typeof limit === 'number' && Number.isFinite(limit) ? Math.trunc(limit) : DEFAULT_PAGE_SIZE;\n return Math.min(Math.max(normalized, 1), MAX_PAGE_SIZE);\n}\n\n/** Encode the `(occurredAt, id)` keyset cursor of a row. */\nexport function encodeAuditCursor(row: AuditEventRow): string {\n return `${row.occurredAt.toISOString()}_${row.id}`;\n}\n\n/** Decode a cursor back into its `(occurredAt, id)` parts, or `undefined`. */\nexport function decodeAuditCursor(cursor: string): { occurredAt: Date; id: string } | undefined {\n const sep = cursor.lastIndexOf('_');\n if (sep <= 0) return undefined;\n const occurredAt = new Date(cursor.slice(0, sep));\n const id = cursor.slice(sep + 1);\n if (Number.isNaN(occurredAt.getTime()) || !id) return undefined;\n return { occurredAt, id };\n}\n\nexport const AUDIT_EVENTS_SCHEMA: CollectionSchema = {\n name: 'audit_events',\n columns: {\n id: { type: 'uuid-pk' },\n org_id: { type: 'text' },\n actor_id: { type: 'text' },\n actor_type: { type: 'text', default: 'human' },\n action: { type: 'text' },\n targets: { type: 'json' },\n metadata: { type: 'json' },\n factory_project_id: { type: 'text', nullable: true },\n project_repository_id: { type: 'text', nullable: true },\n context: { type: 'json' },\n occurred_at: { type: 'timestamp' },\n },\n indexes: [\n { name: 'audit_events_org_occurred_idx', columns: ['org_id', 'occurred_at'] },\n { name: 'audit_events_org_project_occurred_idx', columns: ['org_id', 'factory_project_id', 'occurred_at'] },\n ],\n};\n\n/** Column shape of one `audit_events` row as returned by ops. */\ninterface AuditEventDbRow extends Record<string, unknown> {\n id: string;\n org_id: string;\n actor_id: string;\n actor_type: AuditActorType;\n action: string;\n targets: AuditTarget[];\n metadata: Record<string, unknown>;\n factory_project_id: string | null;\n project_repository_id: string | null;\n context: AuditContext;\n occurred_at: Date;\n}\n\nfunction toRow(db: AuditEventDbRow): AuditEventRow {\n return {\n id: db.id,\n orgId: db.org_id,\n actorId: db.actor_id,\n actorType: db.actor_type,\n action: db.action,\n targets: db.targets,\n metadata: db.metadata,\n factoryProjectId: db.factory_project_id,\n projectRepositoryId: db.project_repository_id,\n context: db.context,\n occurredAt: db.occurred_at,\n };\n}\n\n/**\n * Audit event storage, written once against the generic `FactoryStorageOps`\n * surface. `record()` normalizes inputs (defaults, metadata bounding);\n * `list()` is keyset-paginated on `(occurred_at, id)` newest-first.\n */\nexport class AuditStorage extends FactoryStorageDomain {\n constructor() {\n super('audit');\n }\n\n async init(): Promise<void> {\n await this.ensureCollections([AUDIT_EVENTS_SCHEMA]);\n }\n\n async dangerouslyClearAll(): Promise<void> {\n await this.ops.deleteMany('audit_events', {});\n }\n\n get #db(): FactoryStorageOps {\n return this.ops;\n }\n\n /** Append one audit event. Throws on failure — swallow-on-failure lives in the caller. */\n async record(input: RecordAuditEventInput): Promise<AuditEventRow> {\n if (input.idempotencyKey) return (await this.recordOnce(input)).event;\n return this.#insert(input);\n }\n\n async recordOnce(input: RecordAuditEventInput): Promise<{ event: AuditEventRow; created: boolean }> {\n if (!input.idempotencyKey) throw new Error('An audit idempotency key is required');\n const hash = Buffer.from(\n await globalThis.crypto.subtle.digest(\n 'SHA-256',\n new TextEncoder().encode(\n JSON.stringify([input.orgId, input.factoryProjectId ?? null, input.action, input.idempotencyKey]),\n ),\n ),\n ).toString('hex');\n const id = `${hash.slice(0, 8)}-${hash.slice(8, 12)}-8${hash.slice(13, 16)}-a${hash.slice(17, 20)}-${hash.slice(20, 32)}`;\n try {\n return { event: await this.#insert(input, id), created: true };\n } catch (error) {\n if (!(error instanceof UniqueViolationError)) throw error;\n const existing = await this.#db.findOne<AuditEventDbRow>('audit_events', { id, org_id: input.orgId });\n if (!existing) throw error;\n return { event: toRow(existing), created: false };\n }\n }\n\n async #insert(input: RecordAuditEventInput, id?: string): Promise<AuditEventRow> {\n const inserted = await this.#db.insertOne<AuditEventDbRow>('audit_events', {\n ...(id ? { id } : {}),\n org_id: input.orgId,\n actor_id: input.actorId,\n actor_type: input.actorType ?? 'human',\n action: input.action,\n targets: input.targets,\n metadata: boundAuditMetadata(\n input.actorProfile ? { ...input.metadata, [ACTOR_PROFILE_METADATA_KEY]: input.actorProfile } : input.metadata,\n ),\n factory_project_id: input.factoryProjectId ?? null,\n project_repository_id: input.projectRepositoryId ?? null,\n context: input.context ?? {},\n occurred_at: input.occurredAt ?? new Date(),\n });\n return toRow(inserted);\n }\n\n /** List an org's audit events newest-first with keyset pagination. */\n async list(input: ListAuditEventsInput): Promise<AuditEventPage> {\n const limit = clampAuditLimit(input.limit);\n\n const where: CollectionWhere = { org_id: input.orgId };\n if (input.factoryProjectId) where.factory_project_id = input.factoryProjectId;\n if (input.actions && input.actions.length > 0) where.action = { in: input.actions };\n if (input.actorId) where.actor_id = input.actorId;\n\n const cursor = input.before ? decodeAuditCursor(input.before) : undefined;\n\n const rows = await this.#db.findMany<AuditEventDbRow>('audit_events', where, {\n orderBy: [\n ['occurred_at', 'desc'],\n ['id', 'desc'],\n ],\n limit: limit + 1,\n ...(cursor ? { cursor: { values: [cursor.occurredAt, cursor.id] } } : {}),\n });\n\n const events = rows.slice(0, limit).map(toRow);\n const hasMore = rows.length > limit;\n const last = events[events.length - 1];\n return {\n events,\n ...(hasMore && last ? { nextCursor: encodeAuditCursor(last) } : {}),\n };\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"base.js","names":[],"sources":["../../../../src/storage/domains/audit/base.ts"],"sourcesContent":["/**\n * Factory audit events domain — the append-only \"who did what, when\" trail\n * behind the software factory.\n *\n * Rows are append-only: there is no update/delete API, and the table is the\n * local source of truth even when the WorkOS Audit Logs mirror is unavailable.\n * Tenancy is org-first, like `work_items`: `actor_id` records who acted but\n * never scopes reads.\n *\n * The actions the trail holds are listed in `./actions.ts`.\n *\n * Agent events carry `actor_type = 'agent'` with `actor_id = 'agent:<threadId>'`\n * and `metadata.startedBy = <userId>` chaining accountability back to the human\n * whose message drove the run. Rule-driven events carry `actor_type = 'system'`.\n */\n\nimport { FactoryStorageDomain, UniqueViolationError } from '@mastra/core/storage';\nimport type { CollectionSchema, CollectionWhere, FactoryStorageOps } from '@mastra/core/storage';\n\nimport type { AuditActorType } from './actors.js';\n\n/** What an audit event acted on (WorkOS Audit Logs target shape). */\nexport interface AuditTarget {\n /** Target kind, e.g. 'work_item', 'worktree', 'issue', 'pull_request'. */\n type: string;\n /** Stable identifier of the target (row id, branch name, issue number...). */\n id: string;\n /** Human-readable label (work-item title, branch name...). */\n name?: string;\n}\n\nexport type { AuditActorType } from './actors.js';\n\n/** Display name and avatar of a human actor, stamped at record time because MastraAuthStudio cannot resolve users by id. */\nexport interface AuditActorProfileInput {\n name?: string;\n avatarUrl?: string;\n}\n\nexport const ACTOR_PROFILE_METADATA_KEY = '__actorProfile';\n\nexport function auditActorProfile(\n user: { name?: string; email?: string; avatarUrl?: string } | undefined,\n): AuditActorProfileInput | undefined {\n const name = user?.name?.trim() || user?.email?.trim();\n const avatarUrl = user?.avatarUrl?.trim();\n if (!name && !avatarUrl) return undefined;\n return { ...(name ? { name } : {}), ...(avatarUrl ? { avatarUrl } : {}) };\n}\n\n/** Request context captured alongside the event. */\nexport interface AuditContext {\n /** Client IP (first hop of `x-forwarded-for`) when available. */\n location?: string;\n /** Request `user-agent` header when available. */\n userAgent?: string;\n}\n\n/** One persisted audit event. */\nexport interface AuditEventRow {\n id: string;\n /** Owning WorkOS organization id — the trail is org-wide. */\n orgId: string;\n /** WorkOS user id of whoever performed the action, or `agent:<threadId>`. */\n actorId: string;\n /** Whether a human or an agent (inside a run) performed the action. */\n actorType: AuditActorType;\n /** Dot-namespaced action, e.g. 'factory.work_item.stage_moved'. */\n action: string;\n /** What was acted on. */\n targets: AuditTarget[];\n /** Bounded event summary — never full payloads, never secrets. */\n metadata: Record<string, unknown>;\n /** Factory project the event is scoped to; null for org-level events. */\n factoryProjectId: string | null;\n /** Project-repository link affected by a repository-specific action. */\n projectRepositoryId: string | null;\n /** Request context (`x-forwarded-for` / `user-agent`). */\n context: AuditContext;\n occurredAt: Date;\n}\n\nexport interface RecordAuditEventInput<Action extends string = string> {\n idempotencyKey?: string;\n orgId: string;\n actorId: string;\n /** Who performed the action; defaults to 'human'. */\n actorType?: AuditActorType;\n actorProfile?: AuditActorProfileInput;\n action: Action;\n targets: AuditTarget[];\n metadata?: Record<string, unknown>;\n factoryProjectId?: string;\n projectRepositoryId?: string;\n context?: AuditContext;\n occurredAt?: Date;\n}\n\n/** A fully-normalized event ready to persist (id assigned on insert). */\nexport type AuditEventInsert = Omit<AuditEventRow, 'id'>;\n\nexport interface ListAuditEventsInput {\n orgId: string;\n factoryProjectId?: string;\n /** Restrict to these actions (exact match). */\n actions?: string[];\n actorId?: string;\n /** Opaque cursor from a previous page (`nextCursor`). */\n before?: string;\n limit?: number;\n}\n\nexport interface AuditEventPage {\n events: AuditEventRow[];\n /** Pass back as `before` to fetch the next (older) page; absent at the end. */\n nextCursor?: string;\n}\n\n/** Metadata is a bounded summary — never full payloads, never secrets. */\nconst MAX_METADATA_JSON_LENGTH = 4096;\n\nconst DEFAULT_PAGE_SIZE = 50;\nconst MAX_PAGE_SIZE = 200;\n\n/** Truncate oversized metadata rather than dropping the whole event. */\nexport function boundAuditMetadata(metadata: Record<string, unknown> | undefined): Record<string, unknown> {\n if (!metadata) return {};\n try {\n if (JSON.stringify(metadata).length <= MAX_METADATA_JSON_LENGTH) return metadata;\n } catch {\n return { truncated: true };\n }\n return { truncated: true };\n}\n\n/** Normalize a requested page size to a finite integer in `[1, MAX_PAGE_SIZE]`. */\nexport function clampAuditLimit(limit: number | undefined): number {\n const normalized = typeof limit === 'number' && Number.isFinite(limit) ? Math.trunc(limit) : DEFAULT_PAGE_SIZE;\n return Math.min(Math.max(normalized, 1), MAX_PAGE_SIZE);\n}\n\n/** Encode the `(occurredAt, id)` keyset cursor of a row. */\nexport function encodeAuditCursor(row: AuditEventRow): string {\n return `${row.occurredAt.toISOString()}_${row.id}`;\n}\n\n/** Decode a cursor back into its `(occurredAt, id)` parts, or `undefined`. */\nexport function decodeAuditCursor(cursor: string): { occurredAt: Date; id: string } | undefined {\n const sep = cursor.lastIndexOf('_');\n if (sep <= 0) return undefined;\n const occurredAt = new Date(cursor.slice(0, sep));\n const id = cursor.slice(sep + 1);\n if (Number.isNaN(occurredAt.getTime()) || !id) return undefined;\n return { occurredAt, id };\n}\n\nexport const AUDIT_EVENTS_SCHEMA: CollectionSchema = {\n name: 'audit_events',\n columns: {\n id: { type: 'uuid-pk' },\n org_id: { type: 'text' },\n actor_id: { type: 'text' },\n actor_type: { type: 'text', default: 'human' },\n action: { type: 'text' },\n targets: { type: 'json' },\n metadata: { type: 'json' },\n factory_project_id: { type: 'text', nullable: true },\n project_repository_id: { type: 'text', nullable: true },\n context: { type: 'json' },\n occurred_at: { type: 'timestamp' },\n },\n indexes: [\n { name: 'audit_events_org_occurred_idx', columns: ['org_id', 'occurred_at'] },\n { name: 'audit_events_org_project_occurred_idx', columns: ['org_id', 'factory_project_id', 'occurred_at'] },\n ],\n};\n\n/** Column shape of one `audit_events` row as returned by ops. */\ninterface AuditEventDbRow extends Record<string, unknown> {\n id: string;\n org_id: string;\n actor_id: string;\n actor_type: AuditActorType;\n action: string;\n targets: AuditTarget[];\n metadata: Record<string, unknown>;\n factory_project_id: string | null;\n project_repository_id: string | null;\n context: AuditContext;\n occurred_at: Date;\n}\n\nfunction toRow(db: AuditEventDbRow): AuditEventRow {\n return {\n id: db.id,\n orgId: db.org_id,\n actorId: db.actor_id,\n actorType: db.actor_type,\n action: db.action,\n targets: db.targets,\n metadata: db.metadata,\n factoryProjectId: db.factory_project_id,\n projectRepositoryId: db.project_repository_id,\n context: db.context,\n occurredAt: db.occurred_at,\n };\n}\n\n/**\n * Audit event storage, written once against the generic `FactoryStorageOps`\n * surface. `record()` normalizes inputs (defaults, metadata bounding);\n * `list()` is keyset-paginated on `(occurred_at, id)` newest-first.\n */\nexport class AuditStorage extends FactoryStorageDomain {\n constructor() {\n super('audit');\n }\n\n async init(): Promise<void> {\n await this.ensureCollections([AUDIT_EVENTS_SCHEMA]);\n }\n\n async dangerouslyClearAll(): Promise<void> {\n await this.ops.deleteMany('audit_events', {});\n }\n\n get #db(): FactoryStorageOps {\n return this.ops;\n }\n\n /** Append one audit event. Throws on failure — swallow-on-failure lives in the caller. */\n async record(input: RecordAuditEventInput): Promise<AuditEventRow> {\n if (input.idempotencyKey) return (await this.recordOnce(input)).event;\n return this.#insert(input);\n }\n\n async recordOnce(input: RecordAuditEventInput): Promise<{ event: AuditEventRow; created: boolean }> {\n if (!input.idempotencyKey) throw new Error('An audit idempotency key is required');\n const hash = Buffer.from(\n await globalThis.crypto.subtle.digest(\n 'SHA-256',\n new TextEncoder().encode(\n JSON.stringify([input.orgId, input.factoryProjectId ?? null, input.action, input.idempotencyKey]),\n ),\n ),\n ).toString('hex');\n const id = `${hash.slice(0, 8)}-${hash.slice(8, 12)}-8${hash.slice(13, 16)}-a${hash.slice(17, 20)}-${hash.slice(20, 32)}`;\n try {\n return { event: await this.#insert(input, id), created: true };\n } catch (error) {\n if (!(error instanceof UniqueViolationError)) throw error;\n const existing = await this.#db.findOne<AuditEventDbRow>('audit_events', { id, org_id: input.orgId });\n if (!existing) throw error;\n return { event: toRow(existing), created: false };\n }\n }\n\n async #insert(input: RecordAuditEventInput, id?: string): Promise<AuditEventRow> {\n const inserted = await this.#db.insertOne<AuditEventDbRow>('audit_events', {\n ...(id ? { id } : {}),\n org_id: input.orgId,\n actor_id: input.actorId,\n actor_type: input.actorType ?? 'human',\n action: input.action,\n targets: input.targets,\n metadata: boundAuditMetadata(\n input.actorProfile ? { ...input.metadata, [ACTOR_PROFILE_METADATA_KEY]: input.actorProfile } : input.metadata,\n ),\n factory_project_id: input.factoryProjectId ?? null,\n project_repository_id: input.projectRepositoryId ?? null,\n context: input.context ?? {},\n occurred_at: input.occurredAt ?? new Date(),\n });\n return toRow(inserted);\n }\n\n /** List an org's audit events newest-first with keyset pagination. */\n async list(input: ListAuditEventsInput): Promise<AuditEventPage> {\n const limit = clampAuditLimit(input.limit);\n\n const where: CollectionWhere = { org_id: input.orgId };\n if (input.factoryProjectId) where.factory_project_id = input.factoryProjectId;\n if (input.actions && input.actions.length > 0) where.action = { in: input.actions };\n if (input.actorId) where.actor_id = input.actorId;\n\n const cursor = input.before ? decodeAuditCursor(input.before) : undefined;\n\n const rows = await this.#db.findMany<AuditEventDbRow>('audit_events', where, {\n orderBy: [\n ['occurred_at', 'desc'],\n ['id', 'desc'],\n ],\n limit: limit + 1,\n ...(cursor ? { cursor: { values: [cursor.occurredAt, cursor.id] } } : {}),\n });\n\n const events = rows.slice(0, limit).map(toRow);\n const hasMore = rows.length > limit;\n const last = events[events.length - 1];\n return {\n events,\n ...(hasMore && last ? { nextCursor: encodeAuditCursor(last) } : {}),\n };\n }\n}\n"],"mappings":"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"}
|