@mastra/factory 0.1.0-alpha.3 → 0.1.0-alpha.5

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.
Files changed (72) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/capabilities/intake.d.ts +2 -0
  3. package/dist/capabilities/intake.d.ts.map +1 -1
  4. package/dist/capabilities/version-control.d.ts +6 -0
  5. package/dist/capabilities/version-control.d.ts.map +1 -1
  6. package/dist/factory.d.ts.map +1 -1
  7. package/dist/factory.js +912 -242
  8. package/dist/factory.js.map +1 -1
  9. package/dist/index.js +912 -242
  10. package/dist/index.js.map +1 -1
  11. package/dist/integrations/github/integration.d.ts +2 -2
  12. package/dist/integrations/github/integration.js +45 -12
  13. package/dist/integrations/github/integration.js.map +1 -1
  14. package/dist/integrations/github/provenance.js.map +1 -1
  15. package/dist/integrations/github/routes.js +12 -10
  16. package/dist/integrations/github/routes.js.map +1 -1
  17. package/dist/integrations/github/sandbox.d.ts +1 -0
  18. package/dist/integrations/github/sandbox.d.ts.map +1 -1
  19. package/dist/integrations/github/sandbox.js +2 -2
  20. package/dist/integrations/github/sandbox.js.map +1 -1
  21. package/dist/integrations/github/session-subscriptions.d.ts +3 -0
  22. package/dist/integrations/github/session-subscriptions.d.ts.map +1 -1
  23. package/dist/integrations/github/session-subscriptions.js +30 -0
  24. package/dist/integrations/github/session-subscriptions.js.map +1 -1
  25. package/dist/integrations/github/token-refresh.d.ts +6 -0
  26. package/dist/integrations/github/token-refresh.d.ts.map +1 -0
  27. package/dist/integrations/github/token-refresh.js +17 -0
  28. package/dist/integrations/github/token-refresh.js.map +1 -0
  29. package/dist/integrations/platform/api-client.d.ts +2 -0
  30. package/dist/integrations/platform/api-client.d.ts.map +1 -1
  31. package/dist/integrations/platform/api-client.js +7 -0
  32. package/dist/integrations/platform/api-client.js.map +1 -1
  33. package/dist/integrations/platform/github/event-worker.js.map +1 -1
  34. package/dist/integrations/platform/github/integration.d.ts.map +1 -1
  35. package/dist/integrations/platform/github/integration.js +173 -45
  36. package/dist/integrations/platform/github/integration.js.map +1 -1
  37. package/dist/integrations/platform/linear/integration.js +3 -0
  38. package/dist/integrations/platform/linear/integration.js.map +1 -1
  39. package/dist/routes/config.d.ts +16 -11
  40. package/dist/routes/config.d.ts.map +1 -1
  41. package/dist/routes/config.js +222 -109
  42. package/dist/routes/config.js.map +1 -1
  43. package/dist/routes/custom-provider-source.d.ts +52 -0
  44. package/dist/routes/custom-provider-source.d.ts.map +1 -0
  45. package/dist/routes/custom-provider-source.js +107 -0
  46. package/dist/routes/custom-provider-source.js.map +1 -0
  47. package/dist/routes/oauth.js +33 -1
  48. package/dist/routes/oauth.js.map +1 -1
  49. package/dist/routes/surface.d.ts +4 -0
  50. package/dist/routes/surface.d.ts.map +1 -1
  51. package/dist/routes/surface.js +266 -110
  52. package/dist/routes/surface.js.map +1 -1
  53. package/dist/sandbox/fleet.d.ts +3 -0
  54. package/dist/sandbox/fleet.d.ts.map +1 -1
  55. package/dist/sandbox/fleet.js +10 -3
  56. package/dist/sandbox/fleet.js.map +1 -1
  57. package/dist/spa-static.d.ts +4 -5
  58. package/dist/spa-static.d.ts.map +1 -1
  59. package/dist/spa-static.js +5 -2
  60. package/dist/spa-static.js.map +1 -1
  61. package/dist/storage/domains/custom-providers/base.d.ts +57 -0
  62. package/dist/storage/domains/custom-providers/base.d.ts.map +1 -0
  63. package/dist/storage/domains/custom-providers/base.js +150 -0
  64. package/dist/storage/domains/custom-providers/base.js.map +1 -0
  65. package/dist/storage/domains/memory-settings/base.d.ts +62 -0
  66. package/dist/storage/domains/memory-settings/base.d.ts.map +1 -0
  67. package/dist/storage/domains/memory-settings/base.js +111 -0
  68. package/dist/storage/domains/memory-settings/base.js.map +1 -0
  69. package/dist/workspace.d.ts.map +1 -1
  70. package/dist/workspace.js +17 -0
  71. package/dist/workspace.js.map +1 -1
  72. package/package.json +8 -7
@@ -35,7 +35,10 @@ export interface MaterializationSandbox {
35
35
  }>;
36
36
  executeCommand(command: string, args?: string[], options?: {
37
37
  timeout?: number;
38
+ env?: Record<string, string | undefined>;
38
39
  }): Promise<SandboxCommandResult>;
40
+ /** Update an environment variable for future commands in this sandbox. */
41
+ setEnvironmentVariable?(name: string, value: string): void;
39
42
  /** Tear down the underlying VM. Optional: providers without it are no-ops. */
40
43
  stop?(): Promise<void>;
41
44
  }
@@ -1 +1 @@
1
- {"version":3,"file":"fleet.d.ts","sourceRoot":"","sources":["../../src/sandbox/fleet.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"fleet.d.ts","sourceRoot":"","sources":["../../src/sandbox/fleet.ts"],"names":[],"mappings":"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"}
@@ -18,20 +18,27 @@ var SandboxBudgetError = class extends Error {
18
18
  max;
19
19
  code = "sandbox-budget-exceeded";
20
20
  };
21
- function toMaterializationSandbox(sandbox) {
21
+ function toMaterializationSandbox(sandbox, initialEnvironment = {}) {
22
22
  if (typeof sandbox.executeCommand !== "function") {
23
23
  throw new Error(
24
24
  `Sandbox provider '${sandbox.provider}' does not implement executeCommand() \u2014 cannot materialize repos.`
25
25
  );
26
26
  }
27
27
  const lifecycle = sandbox;
28
+ const environment = { ...initialEnvironment };
28
29
  return {
29
30
  id: sandbox.id,
30
31
  start: async () => {
31
32
  await (lifecycle._start ?? sandbox.start)?.call(sandbox);
32
33
  },
33
34
  getInfo: async () => await sandbox.getInfo?.() ?? {},
34
- executeCommand: (command, args, options) => sandbox.executeCommand(command, args, options),
35
+ executeCommand: (command, args, options) => sandbox.executeCommand(command, args, {
36
+ ...options,
37
+ env: { ...environment, ...options?.env }
38
+ }),
39
+ setEnvironmentVariable: (name, value) => {
40
+ environment[name] = value;
41
+ },
35
42
  stop: async () => {
36
43
  await (lifecycle._stop ?? sandbox.stop)?.call(sandbox);
37
44
  }
@@ -171,7 +178,7 @@ var SandboxFleet = class {
171
178
  ...opts.idleTimeoutMinutes !== void 0 ? { idleTimeoutMinutes: opts.idleTimeoutMinutes } : {},
172
179
  ...opts.checkpointName ? { checkpointName: opts.checkpointName } : {}
173
180
  });
174
- return toMaterializationSandbox(clone);
181
+ return toMaterializationSandbox(clone, opts.env);
175
182
  }
176
183
  async ensureSandbox(store, envOrProgress, progressOrOptions, maybeOptions = {}) {
177
184
  const env = typeof envOrProgress === "function" ? void 0 : envOrProgress;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/sandbox/fleet.ts"],"sourcesContent":["/**\n * Project sandbox fleet: provisioning, reattach, teardown, and budgeting.\n *\n * Server-hosted projects never run on the web host itself. Each project gets\n * its own isolated sandbox (a `WorkspaceSandbox`, e.g. a Railway VM) `clone()`d\n * from the machine the factory was configured with. This module owns everything\n * about that fleet — which provider is active, where checkouts live inside a\n * sandbox, the idle window, the per-replica budget, and the\n * provision/reattach/teardown lifecycle — but knows nothing about what runs\n * inside a sandbox (git materialization lives with its feature, e.g. the\n * GitHub integration's `sandbox.ts`).\n *\n * The fleet is constructed once at boot with the machine config (or none, when\n * sandboxes are disabled) and handed to consumers — no global registry.\n * Persistence of the provider's reattach id is delegated to the caller via\n * {@link SandboxBindingStore}, so the fleet stays storage-agnostic. Tests can\n * swap the low-level construction via {@link SandboxFleet.setFactory}.\n */\n\nimport path from 'node:path';\n\nimport type { WorkspaceSandbox } from '@mastra/core/workspace';\n\n/** Minimal command result shape sandbox consumers depend on. */\nexport interface SandboxCommandResult {\n exitCode: number;\n stdout: string;\n stderr: string;\n}\n\n/**\n * Minimal live-sandbox surface fleet consumers need: an id, a way to start it,\n * a way to learn the provider's reattach id, and command execution.\n */\nexport interface MaterializationSandbox {\n readonly id: string;\n start(): Promise<void>;\n getInfo(): Promise<{ metadata?: Record<string, unknown> }>;\n executeCommand(command: string, args?: string[], options?: { timeout?: number }): Promise<SandboxCommandResult>;\n /** Tear down the underlying VM. Optional: providers without it are no-ops. */\n stop?(): Promise<void>;\n}\n\n/** Options for building (or reattaching) one sandbox. */\nexport interface SandboxCreateOptions {\n /** Reattach to this existing provider VM instead of provisioning a new one. */\n providerSandboxId?: string;\n /** Environment variables baked into the sandbox. */\n env?: Record<string, string>;\n /** Provider working directory for this sandbox. */\n workingDirectory?: string;\n /** Idle teardown window (minutes). The provider stops the VM after this idle period. */\n idleTimeoutMinutes?: number;\n /** Provider checkpoint used to seed and preserve this sandbox's filesystem. */\n checkpointName?: string;\n}\n\n/**\n * A coarse-grained step of the sandbox-preparation flow, reported as it happens\n * so the UI can show the user what the server is doing instead of a static\n * \"Preparing…\" toast. `phase` is a stable machine token; `message` is\n * user-facing copy.\n */\nexport interface PrepareProgress {\n phase: 'reattaching' | 'provisioning' | 'preparing-workspace' | 'cloning' | 'pulling' | 'finalizing' | 'done';\n message: string;\n}\n\n/** Callback invoked with each preparation step. Best-effort; never throws. */\nexport type ProgressFn = (event: PrepareProgress) => void;\n\n/** Invoke a progress callback without letting it break the actual work. */\nexport function reportProgress(onProgress: ProgressFn | undefined, event: PrepareProgress): void {\n if (!onProgress) return;\n try {\n onProgress(event);\n } catch {\n // Progress reporting must never break the actual work.\n }\n}\n\n/**\n * Factory that builds a (not-yet-started) sandbox. When `providerSandboxId` is\n * provided the sandbox should reattach to that existing VM instead of\n * provisioning a new one.\n */\nexport type SandboxFactory = (opts: SandboxCreateOptions) => MaterializationSandbox;\n\n/** Raised when provisioning would exceed the per-replica sandbox budget. */\nexport class SandboxBudgetError extends Error {\n readonly code = 'sandbox-budget-exceeded' as const;\n constructor(readonly max: number) {\n super(\n `Sandbox budget exceeded: this server already has ${max} active sandbox(es), ` +\n `the configured per-replica maximum. Close an existing repository's sandbox and try again.`,\n );\n this.name = 'SandboxBudgetError';\n }\n}\n\n/** Optional knobs for provisioning/reattaching one sandbox. */\nexport interface EnsureSandboxOptions {\n /** Provider working directory for this sandbox. */\n workingDirectory?: string;\n}\n\n/**\n * Where a feature persists its sandbox binding. The fleet reads the stored\n * reattach id and writes updates through this seam so it stays agnostic of\n * the owning table (GitHub projects today, anything else tomorrow).\n */\nexport interface SandboxBindingStore {\n /** Stored provider reattach id from a previous provisioning, if any. */\n readonly sandboxId: string | null;\n /** Provider checkpoint used to seed and preserve this sandbox's filesystem. */\n readonly checkpointName?: string;\n /** Persist a freshly provisioned provider id, or clear a stale one with `null`. */\n setSandboxId(id: string | null): Promise<void>;\n /** Clear all stored sandbox state (reattach id + materialization mark) on teardown. */\n clear(): Promise<void>;\n}\n\n/**\n * Adapt a cloned `WorkspaceSandbox` to the minimal surface this module needs.\n * Lifecycle goes through the `_`-prefixed wrappers when present (they add\n * status tracking and concurrency safety on `MastraSandbox` subclasses),\n * falling back to the plain methods for interface-only implementations.\n */\nfunction toMaterializationSandbox(sandbox: WorkspaceSandbox): MaterializationSandbox {\n if (typeof sandbox.executeCommand !== 'function') {\n throw new Error(\n `Sandbox provider '${sandbox.provider}' does not implement executeCommand() — cannot materialize repos.`,\n );\n }\n const lifecycle = sandbox as { _start?(): Promise<void>; _stop?(): Promise<void> };\n return {\n id: sandbox.id,\n start: async () => {\n await (lifecycle._start ?? sandbox.start)?.call(sandbox);\n },\n getInfo: async () => (await sandbox.getInfo?.()) ?? {},\n executeCommand: (command, args, options) => sandbox.executeCommand!(command, args, options),\n stop: async () => {\n await (lifecycle._stop ?? sandbox.stop)?.call(sandbox);\n },\n };\n}\n\n/**\n * The provider's reattach id for a started sandbox. For Railway this is the\n * underlying `railwaySandboxId` in `getInfo().metadata`. Providers without a\n * provider-native id (e.g. local) reattach by construction id, so fall back\n * to the sandbox's own logical id.\n */\nasync function readProviderSandboxId(sandbox: MaterializationSandbox): Promise<string | undefined> {\n const info = await sandbox.getInfo();\n const id = info.metadata?.railwaySandboxId ?? info.metadata?.sandboxId;\n return typeof id === 'string' ? id : sandbox.id;\n}\n\n/** Keep each path piece a single safe segment (no separators or traversal). */\nfunction sanitizeSegment(segment: string): string {\n const cleaned = segment.replace(/[^A-Za-z0-9._-]/g, '-').replace(/^\\.+/, '');\n return cleaned || 'repo';\n}\n\n/** Resolve a workdir under `root`, refusing any path that escapes the configured root. */\nexport function resolveContainedLocalWorkdir(root: string, ...segments: string[]): string {\n const resolvedRoot = path.resolve(root);\n const resolved = path.resolve(resolvedRoot, ...segments);\n if (resolved !== resolvedRoot && resolved.startsWith(`${resolvedRoot}${path.sep}`)) return resolved;\n throw new Error(`Refusing to use local sandbox path outside configured root: ${resolved}`);\n}\n\n/**\n * Factory-resolved sandbox runtime the fleet is constructed with: the machine\n * projects clone their per-project sandboxes from, plus the knobs the factory\n * resolved around it.\n */\nexport interface SandboxFleetConfig {\n /**\n * Template machine (validated by the factory to implement `clone()`).\n * Never started — acts purely as the credential/default holder that\n * per-project sandboxes are cloned from.\n */\n machine: WorkspaceSandbox;\n /** In-sandbox base directory repos check out under (no trailing slash). */\n workdirBase: string;\n /** Per-replica cap on concurrently provisioned sandboxes. 0 = unlimited. */\n maxSandboxes?: number;\n}\n\n/**\n * The sandbox fleet for one deployment. Constructed once at boot — with a\n * config when a sandbox machine was configured, or without one when sandboxes\n * are disabled (every provisioning entry point then throws and\n * {@link enabled} reports `false` so features stay off).\n */\nexport class SandboxFleet {\n readonly #config: SandboxFleetConfig | undefined;\n #factory: SandboxFactory | undefined;\n #liveCount = 0;\n\n constructor(config?: SandboxFleetConfig) {\n this.#config = config;\n }\n\n /**\n * True when a sandbox machine was configured. The factory validates the\n * machine implements `clone()` at boot, so a configured fleet is usable —\n * sandbox-backed projects stay off only when the slot was omitted.\n */\n get enabled(): boolean {\n return this.#config !== undefined;\n }\n\n /**\n * Name of the active sandbox provider — the configured machine's `provider`\n * discriminator (`'railway'`, `'local'`, …), or `'none'` when the fleet was\n * constructed without a config. Diagnostic only; feature gating goes\n * through {@link enabled}.\n */\n get provider(): string {\n return this.#config?.machine.provider ?? 'none';\n }\n\n /**\n * Idle teardown window for provisioned sandboxes, in minutes; defaults to 30.\n * Read back from the machine's own config when it exposes one\n * (Railway's `idleTimeoutMinutes`) — the knob lives on the sandbox, the\n * fleet only needs it to schedule GC and stamp sandbox clones. Advisory:\n * providers without idle GC ignore it, and a re-open detects a torn-down VM\n * and re-provisions cleanly.\n */\n get idleMinutes(): number {\n const machine = this.#config?.machine as { idleTimeoutMinutes?: unknown } | undefined;\n const minutes = machine?.idleTimeoutMinutes;\n return typeof minutes === 'number' && Number.isFinite(minutes) && minutes > 0 ? minutes : 30;\n }\n\n /**\n * Per-replica cap on concurrently *provisioned* sandboxes. 0 means unlimited.\n * This is a lightweight per-process budget to keep a single replica from\n * exhausting provider quota — it is not a global, cross-replica scheduler\n * (that is a deferred follow-up).\n */\n get maxSandboxes(): number {\n return this.#config?.maxSandboxes ?? 0;\n }\n\n /**\n * Count of sandboxes this fleet has freshly provisioned and not yet torn\n * down. Reattaches to existing VMs do not count (they reuse an already-billed\n * sandbox). Used to enforce {@link maxSandboxes}.\n */\n get liveCount(): number {\n return this.#liveCount;\n }\n\n /** For tests: reset the live-sandbox counter to a known state. */\n __resetLiveCount(value = 0): void {\n this.#liveCount = value;\n }\n\n /** Override the sandbox factory (tests). */\n setFactory(factory: SandboxFactory): void {\n this.#factory = factory;\n }\n\n /** Reset to the default machine-cloning factory. */\n resetFactory(): void {\n this.#factory = undefined;\n }\n\n /**\n * Compute the in-sandbox working directory for a repo: a nested\n * `<base>/<owner>/<name>` layout under the factory-resolved checkout base.\n * Nesting keeps same-name repos apart (`acme/api` vs `other/api`) — cloud\n * sandboxes are one-per-project so it's merely tidy there, but local\n * checkouts share one host root where it prevents collisions. Server-side\n * only; never derived from client input.\n */\n computeWorkdir(repoFullName: string): string {\n if (!this.#config) throw new Error('No sandbox configured');\n const [owner, name] = repoFullName.split('/', 2);\n return `${this.#config.workdirBase}/${sanitizeSegment(owner || 'unknown')}/${sanitizeSegment(name || 'repo')}`;\n }\n\n /**\n * Compute the host working directory for a local GitHub session checkout.\n * This is server-derived only: repo pieces are sanitized and the trusted\n * session id is kept as a single path segment under the configured local root.\n */\n computeLocalSessionWorkdir(repoFullName: string, sessionId: string): string {\n if (!this.#config) throw new Error('No sandbox configured');\n if (this.#config.machine.provider !== 'local') {\n throw new Error('Local session workdirs require the local sandbox provider');\n }\n\n const localRoot = (this.#config.machine as { workingDirectory?: unknown }).workingDirectory;\n if (typeof localRoot !== 'string' || localRoot.length === 0) {\n throw new Error('Local sandbox working directory is not configured');\n }\n\n const [owner, name] = repoFullName.split('/', 2);\n return resolveContainedLocalWorkdir(\n localRoot,\n 'github-sessions',\n sanitizeSegment(owner || 'unknown'),\n sanitizeSegment(name || 'repo'),\n sanitizeSegment(sessionId),\n );\n }\n\n /**\n * Build a (not-yet-started) sandbox: the test-provided factory when set,\n * otherwise a per-project clone of the configured machine. The stored id is\n * passed both as the logical `id` (providers that reattach by construction\n * id, e.g. local) and as the provider-native `sandboxId` hint (Railway) so\n * reattach works across the provider matrix.\n */\n #build(opts: SandboxCreateOptions): MaterializationSandbox {\n if (this.#factory) return this.#factory(opts);\n if (!this.#config) throw new Error('No sandbox configured');\n const clone = this.#config.machine.clone!({\n ...(opts.providerSandboxId ? { id: opts.providerSandboxId, sandboxId: opts.providerSandboxId } : {}),\n ...(opts.env ? { env: opts.env } : {}),\n ...(opts.workingDirectory ? { workingDirectory: opts.workingDirectory } : {}),\n ...(opts.idleTimeoutMinutes !== undefined ? { idleTimeoutMinutes: opts.idleTimeoutMinutes } : {}),\n ...(opts.checkpointName ? { checkpointName: opts.checkpointName } : {}),\n });\n return toMaterializationSandbox(clone);\n }\n\n /**\n * Provision a new sandbox (persisting its provider id on first open) or\n * reattach to the stored one. Returns a started, live sandbox.\n */\n async ensureSandbox(store: SandboxBindingStore, onProgress?: ProgressFn): Promise<MaterializationSandbox>;\n async ensureSandbox(\n store: SandboxBindingStore,\n env?: Record<string, string>,\n onProgress?: ProgressFn,\n options?: EnsureSandboxOptions,\n ): Promise<MaterializationSandbox>;\n async ensureSandbox(\n store: SandboxBindingStore,\n envOrProgress?: Record<string, string> | ProgressFn,\n progressOrOptions?: ProgressFn | EnsureSandboxOptions,\n maybeOptions: EnsureSandboxOptions = {},\n ): Promise<MaterializationSandbox> {\n const env = typeof envOrProgress === 'function' ? undefined : envOrProgress;\n const onProgress =\n typeof envOrProgress === 'function' ? envOrProgress : (progressOrOptions as ProgressFn | undefined);\n const options =\n typeof envOrProgress === 'function'\n ? ((progressOrOptions as EnsureSandboxOptions | undefined) ?? {})\n : maybeOptions;\n const idleTimeoutMinutes = this.idleMinutes;\n const checkpointName = store.checkpointName;\n\n // Reattach path: if we have a stored sandbox id, try to reattach. The VM may\n // have been torn down by the provider's idle GC (or otherwise died), in which\n // case `start()` fails. Recover by clearing the stale id and provisioning a\n // fresh sandbox so the next open succeeds instead of being permanently wedged.\n if (store.sandboxId) {\n reportProgress(onProgress, { phase: 'reattaching', message: 'Reconnecting to your sandbox…' });\n const reattached = this.#build({\n providerSandboxId: store.sandboxId,\n idleTimeoutMinutes,\n ...(checkpointName ? { checkpointName } : {}),\n ...(env ? { env } : {}),\n ...(options.workingDirectory ? { workingDirectory: options.workingDirectory } : {}),\n });\n try {\n await reattached.start();\n return reattached;\n } catch {\n await store.setSandboxId(null);\n // fall through to fresh provision below\n }\n }\n\n // Fresh provision: enforce the per-replica budget before spending quota.\n const max = this.maxSandboxes;\n if (max > 0 && this.#liveCount >= max) {\n throw new SandboxBudgetError(max);\n }\n\n reportProgress(onProgress, { phase: 'provisioning', message: 'Provisioning a new sandbox…' });\n const sandbox = this.#build({\n idleTimeoutMinutes,\n ...(checkpointName ? { checkpointName } : {}),\n ...(env ? { env } : {}),\n ...(options.workingDirectory ? { workingDirectory: options.workingDirectory } : {}),\n });\n await sandbox.start();\n this.#liveCount += 1;\n\n const providerSandboxId = await readProviderSandboxId(sandbox);\n if (providerSandboxId) {\n await store.setSandboxId(providerSandboxId);\n }\n\n return sandbox;\n }\n\n /**\n * Tear down a sandbox binding: stop the live VM (best-effort) and clear the\n * persisted state through the binding store so the next open re-provisions\n * cleanly. Decrements the per-replica live-sandbox counter.\n *\n * @param store the binding to tear down\n * @param sandbox an already-reattached live sandbox to stop, when available\n */\n async teardownSandbox(store: SandboxBindingStore, sandbox?: MaterializationSandbox): Promise<void> {\n if (sandbox?.stop) {\n try {\n await sandbox.stop();\n } catch {\n // Best-effort: the VM may already be gone (idle GC). Still clear the binding.\n }\n }\n if (store.sandboxId) {\n if (this.#liveCount > 0) this.#liveCount -= 1;\n await store.clear();\n }\n }\n\n /**\n * Reattach to an already-provisioned sandbox by its provider id and start it.\n * Used by the workspace seam when opening a project that was already\n * materialized (sandbox id + workdir carried on controller state), so no DB\n * round-trip is needed.\n */\n async reattachSandbox(\n providerSandboxId: string,\n options: EnsureSandboxOptions = {},\n ): Promise<MaterializationSandbox> {\n const sandbox = this.#build({\n providerSandboxId,\n idleTimeoutMinutes: this.idleMinutes,\n ...(options.workingDirectory ? { workingDirectory: options.workingDirectory } : {}),\n });\n await sandbox.start();\n return sandbox;\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/sandbox/fleet.ts"],"sourcesContent":["/**\n * Project sandbox fleet: provisioning, reattach, teardown, and budgeting.\n *\n * Server-hosted projects never run on the web host itself. Each project gets\n * its own isolated sandbox (a `WorkspaceSandbox`, e.g. a Railway VM) `clone()`d\n * from the machine the factory was configured with. This module owns everything\n * about that fleet — which provider is active, where checkouts live inside a\n * sandbox, the idle window, the per-replica budget, and the\n * provision/reattach/teardown lifecycle — but knows nothing about what runs\n * inside a sandbox (git materialization lives with its feature, e.g. the\n * GitHub integration's `sandbox.ts`).\n *\n * The fleet is constructed once at boot with the machine config (or none, when\n * sandboxes are disabled) and handed to consumers — no global registry.\n * Persistence of the provider's reattach id is delegated to the caller via\n * {@link SandboxBindingStore}, so the fleet stays storage-agnostic. Tests can\n * swap the low-level construction via {@link SandboxFleet.setFactory}.\n */\n\nimport path from 'node:path';\n\nimport type { WorkspaceSandbox } from '@mastra/core/workspace';\n\n/** Minimal command result shape sandbox consumers depend on. */\nexport interface SandboxCommandResult {\n exitCode: number;\n stdout: string;\n stderr: string;\n}\n\n/**\n * Minimal live-sandbox surface fleet consumers need: an id, a way to start it,\n * a way to learn the provider's reattach id, and command execution.\n */\nexport interface MaterializationSandbox {\n readonly id: string;\n start(): Promise<void>;\n getInfo(): Promise<{ metadata?: Record<string, unknown> }>;\n executeCommand(\n command: string,\n args?: string[],\n options?: { timeout?: number; env?: Record<string, string | undefined> },\n ): Promise<SandboxCommandResult>;\n /** Update an environment variable for future commands in this sandbox. */\n setEnvironmentVariable?(name: string, value: string): void;\n /** Tear down the underlying VM. Optional: providers without it are no-ops. */\n stop?(): Promise<void>;\n}\n\n/** Options for building (or reattaching) one sandbox. */\nexport interface SandboxCreateOptions {\n /** Reattach to this existing provider VM instead of provisioning a new one. */\n providerSandboxId?: string;\n /** Environment variables baked into the sandbox. */\n env?: Record<string, string>;\n /** Provider working directory for this sandbox. */\n workingDirectory?: string;\n /** Idle teardown window (minutes). The provider stops the VM after this idle period. */\n idleTimeoutMinutes?: number;\n /** Provider checkpoint used to seed and preserve this sandbox's filesystem. */\n checkpointName?: string;\n}\n\n/**\n * A coarse-grained step of the sandbox-preparation flow, reported as it happens\n * so the UI can show the user what the server is doing instead of a static\n * \"Preparing…\" toast. `phase` is a stable machine token; `message` is\n * user-facing copy.\n */\nexport interface PrepareProgress {\n phase: 'reattaching' | 'provisioning' | 'preparing-workspace' | 'cloning' | 'pulling' | 'finalizing' | 'done';\n message: string;\n}\n\n/** Callback invoked with each preparation step. Best-effort; never throws. */\nexport type ProgressFn = (event: PrepareProgress) => void;\n\n/** Invoke a progress callback without letting it break the actual work. */\nexport function reportProgress(onProgress: ProgressFn | undefined, event: PrepareProgress): void {\n if (!onProgress) return;\n try {\n onProgress(event);\n } catch {\n // Progress reporting must never break the actual work.\n }\n}\n\n/**\n * Factory that builds a (not-yet-started) sandbox. When `providerSandboxId` is\n * provided the sandbox should reattach to that existing VM instead of\n * provisioning a new one.\n */\nexport type SandboxFactory = (opts: SandboxCreateOptions) => MaterializationSandbox;\n\n/** Raised when provisioning would exceed the per-replica sandbox budget. */\nexport class SandboxBudgetError extends Error {\n readonly code = 'sandbox-budget-exceeded' as const;\n constructor(readonly max: number) {\n super(\n `Sandbox budget exceeded: this server already has ${max} active sandbox(es), ` +\n `the configured per-replica maximum. Close an existing repository's sandbox and try again.`,\n );\n this.name = 'SandboxBudgetError';\n }\n}\n\n/** Optional knobs for provisioning/reattaching one sandbox. */\nexport interface EnsureSandboxOptions {\n /** Provider working directory for this sandbox. */\n workingDirectory?: string;\n}\n\n/**\n * Where a feature persists its sandbox binding. The fleet reads the stored\n * reattach id and writes updates through this seam so it stays agnostic of\n * the owning table (GitHub projects today, anything else tomorrow).\n */\nexport interface SandboxBindingStore {\n /** Stored provider reattach id from a previous provisioning, if any. */\n readonly sandboxId: string | null;\n /** Provider checkpoint used to seed and preserve this sandbox's filesystem. */\n readonly checkpointName?: string;\n /** Persist a freshly provisioned provider id, or clear a stale one with `null`. */\n setSandboxId(id: string | null): Promise<void>;\n /** Clear all stored sandbox state (reattach id + materialization mark) on teardown. */\n clear(): Promise<void>;\n}\n\n/**\n * Adapt a cloned `WorkspaceSandbox` to the minimal surface this module needs.\n * Lifecycle goes through the `_`-prefixed wrappers when present (they add\n * status tracking and concurrency safety on `MastraSandbox` subclasses),\n * falling back to the plain methods for interface-only implementations.\n */\nfunction toMaterializationSandbox(\n sandbox: WorkspaceSandbox,\n initialEnvironment: Record<string, string> = {},\n): MaterializationSandbox {\n if (typeof sandbox.executeCommand !== 'function') {\n throw new Error(\n `Sandbox provider '${sandbox.provider}' does not implement executeCommand() — cannot materialize repos.`,\n );\n }\n const lifecycle = sandbox as { _start?(): Promise<void>; _stop?(): Promise<void> };\n const environment = { ...initialEnvironment };\n return {\n id: sandbox.id,\n start: async () => {\n await (lifecycle._start ?? sandbox.start)?.call(sandbox);\n },\n getInfo: async () => (await sandbox.getInfo?.()) ?? {},\n executeCommand: (command, args, options) =>\n sandbox.executeCommand!(command, args, {\n ...options,\n env: { ...environment, ...options?.env },\n }),\n setEnvironmentVariable: (name, value) => {\n environment[name] = value;\n },\n stop: async () => {\n await (lifecycle._stop ?? sandbox.stop)?.call(sandbox);\n },\n };\n}\n\n/**\n * The provider's reattach id for a started sandbox. For Railway this is the\n * underlying `railwaySandboxId` in `getInfo().metadata`. Providers without a\n * provider-native id (e.g. local) reattach by construction id, so fall back\n * to the sandbox's own logical id.\n */\nasync function readProviderSandboxId(sandbox: MaterializationSandbox): Promise<string | undefined> {\n const info = await sandbox.getInfo();\n const id = info.metadata?.railwaySandboxId ?? info.metadata?.sandboxId;\n return typeof id === 'string' ? id : sandbox.id;\n}\n\n/** Keep each path piece a single safe segment (no separators or traversal). */\nfunction sanitizeSegment(segment: string): string {\n const cleaned = segment.replace(/[^A-Za-z0-9._-]/g, '-').replace(/^\\.+/, '');\n return cleaned || 'repo';\n}\n\n/** Resolve a workdir under `root`, refusing any path that escapes the configured root. */\nexport function resolveContainedLocalWorkdir(root: string, ...segments: string[]): string {\n const resolvedRoot = path.resolve(root);\n const resolved = path.resolve(resolvedRoot, ...segments);\n if (resolved !== resolvedRoot && resolved.startsWith(`${resolvedRoot}${path.sep}`)) return resolved;\n throw new Error(`Refusing to use local sandbox path outside configured root: ${resolved}`);\n}\n\n/**\n * Factory-resolved sandbox runtime the fleet is constructed with: the machine\n * projects clone their per-project sandboxes from, plus the knobs the factory\n * resolved around it.\n */\nexport interface SandboxFleetConfig {\n /**\n * Template machine (validated by the factory to implement `clone()`).\n * Never started — acts purely as the credential/default holder that\n * per-project sandboxes are cloned from.\n */\n machine: WorkspaceSandbox;\n /** In-sandbox base directory repos check out under (no trailing slash). */\n workdirBase: string;\n /** Per-replica cap on concurrently provisioned sandboxes. 0 = unlimited. */\n maxSandboxes?: number;\n}\n\n/**\n * The sandbox fleet for one deployment. Constructed once at boot — with a\n * config when a sandbox machine was configured, or without one when sandboxes\n * are disabled (every provisioning entry point then throws and\n * {@link enabled} reports `false` so features stay off).\n */\nexport class SandboxFleet {\n readonly #config: SandboxFleetConfig | undefined;\n #factory: SandboxFactory | undefined;\n #liveCount = 0;\n\n constructor(config?: SandboxFleetConfig) {\n this.#config = config;\n }\n\n /**\n * True when a sandbox machine was configured. The factory validates the\n * machine implements `clone()` at boot, so a configured fleet is usable —\n * sandbox-backed projects stay off only when the slot was omitted.\n */\n get enabled(): boolean {\n return this.#config !== undefined;\n }\n\n /**\n * Name of the active sandbox provider — the configured machine's `provider`\n * discriminator (`'railway'`, `'local'`, …), or `'none'` when the fleet was\n * constructed without a config. Diagnostic only; feature gating goes\n * through {@link enabled}.\n */\n get provider(): string {\n return this.#config?.machine.provider ?? 'none';\n }\n\n /**\n * Idle teardown window for provisioned sandboxes, in minutes; defaults to 30.\n * Read back from the machine's own config when it exposes one\n * (Railway's `idleTimeoutMinutes`) — the knob lives on the sandbox, the\n * fleet only needs it to schedule GC and stamp sandbox clones. Advisory:\n * providers without idle GC ignore it, and a re-open detects a torn-down VM\n * and re-provisions cleanly.\n */\n get idleMinutes(): number {\n const machine = this.#config?.machine as { idleTimeoutMinutes?: unknown } | undefined;\n const minutes = machine?.idleTimeoutMinutes;\n return typeof minutes === 'number' && Number.isFinite(minutes) && minutes > 0 ? minutes : 30;\n }\n\n /**\n * Per-replica cap on concurrently *provisioned* sandboxes. 0 means unlimited.\n * This is a lightweight per-process budget to keep a single replica from\n * exhausting provider quota — it is not a global, cross-replica scheduler\n * (that is a deferred follow-up).\n */\n get maxSandboxes(): number {\n return this.#config?.maxSandboxes ?? 0;\n }\n\n /**\n * Count of sandboxes this fleet has freshly provisioned and not yet torn\n * down. Reattaches to existing VMs do not count (they reuse an already-billed\n * sandbox). Used to enforce {@link maxSandboxes}.\n */\n get liveCount(): number {\n return this.#liveCount;\n }\n\n /** For tests: reset the live-sandbox counter to a known state. */\n __resetLiveCount(value = 0): void {\n this.#liveCount = value;\n }\n\n /** Override the sandbox factory (tests). */\n setFactory(factory: SandboxFactory): void {\n this.#factory = factory;\n }\n\n /** Reset to the default machine-cloning factory. */\n resetFactory(): void {\n this.#factory = undefined;\n }\n\n /**\n * Compute the in-sandbox working directory for a repo: a nested\n * `<base>/<owner>/<name>` layout under the factory-resolved checkout base.\n * Nesting keeps same-name repos apart (`acme/api` vs `other/api`) — cloud\n * sandboxes are one-per-project so it's merely tidy there, but local\n * checkouts share one host root where it prevents collisions. Server-side\n * only; never derived from client input.\n */\n computeWorkdir(repoFullName: string): string {\n if (!this.#config) throw new Error('No sandbox configured');\n const [owner, name] = repoFullName.split('/', 2);\n return `${this.#config.workdirBase}/${sanitizeSegment(owner || 'unknown')}/${sanitizeSegment(name || 'repo')}`;\n }\n\n /**\n * Compute the host working directory for a local GitHub session checkout.\n * This is server-derived only: repo pieces are sanitized and the trusted\n * session id is kept as a single path segment under the configured local root.\n */\n computeLocalSessionWorkdir(repoFullName: string, sessionId: string): string {\n if (!this.#config) throw new Error('No sandbox configured');\n if (this.#config.machine.provider !== 'local') {\n throw new Error('Local session workdirs require the local sandbox provider');\n }\n\n const localRoot = (this.#config.machine as { workingDirectory?: unknown }).workingDirectory;\n if (typeof localRoot !== 'string' || localRoot.length === 0) {\n throw new Error('Local sandbox working directory is not configured');\n }\n\n const [owner, name] = repoFullName.split('/', 2);\n return resolveContainedLocalWorkdir(\n localRoot,\n 'github-sessions',\n sanitizeSegment(owner || 'unknown'),\n sanitizeSegment(name || 'repo'),\n sanitizeSegment(sessionId),\n );\n }\n\n /**\n * Build a (not-yet-started) sandbox: the test-provided factory when set,\n * otherwise a per-project clone of the configured machine. The stored id is\n * passed both as the logical `id` (providers that reattach by construction\n * id, e.g. local) and as the provider-native `sandboxId` hint (Railway) so\n * reattach works across the provider matrix.\n */\n #build(opts: SandboxCreateOptions): MaterializationSandbox {\n if (this.#factory) return this.#factory(opts);\n if (!this.#config) throw new Error('No sandbox configured');\n const clone = this.#config.machine.clone!({\n ...(opts.providerSandboxId ? { id: opts.providerSandboxId, sandboxId: opts.providerSandboxId } : {}),\n ...(opts.env ? { env: opts.env } : {}),\n ...(opts.workingDirectory ? { workingDirectory: opts.workingDirectory } : {}),\n ...(opts.idleTimeoutMinutes !== undefined ? { idleTimeoutMinutes: opts.idleTimeoutMinutes } : {}),\n ...(opts.checkpointName ? { checkpointName: opts.checkpointName } : {}),\n });\n return toMaterializationSandbox(clone, opts.env);\n }\n\n /**\n * Provision a new sandbox (persisting its provider id on first open) or\n * reattach to the stored one. Returns a started, live sandbox.\n */\n async ensureSandbox(store: SandboxBindingStore, onProgress?: ProgressFn): Promise<MaterializationSandbox>;\n async ensureSandbox(\n store: SandboxBindingStore,\n env?: Record<string, string>,\n onProgress?: ProgressFn,\n options?: EnsureSandboxOptions,\n ): Promise<MaterializationSandbox>;\n async ensureSandbox(\n store: SandboxBindingStore,\n envOrProgress?: Record<string, string> | ProgressFn,\n progressOrOptions?: ProgressFn | EnsureSandboxOptions,\n maybeOptions: EnsureSandboxOptions = {},\n ): Promise<MaterializationSandbox> {\n const env = typeof envOrProgress === 'function' ? undefined : envOrProgress;\n const onProgress =\n typeof envOrProgress === 'function' ? envOrProgress : (progressOrOptions as ProgressFn | undefined);\n const options =\n typeof envOrProgress === 'function'\n ? ((progressOrOptions as EnsureSandboxOptions | undefined) ?? {})\n : maybeOptions;\n const idleTimeoutMinutes = this.idleMinutes;\n const checkpointName = store.checkpointName;\n\n // Reattach path: if we have a stored sandbox id, try to reattach. The VM may\n // have been torn down by the provider's idle GC (or otherwise died), in which\n // case `start()` fails. Recover by clearing the stale id and provisioning a\n // fresh sandbox so the next open succeeds instead of being permanently wedged.\n if (store.sandboxId) {\n reportProgress(onProgress, { phase: 'reattaching', message: 'Reconnecting to your sandbox…' });\n const reattached = this.#build({\n providerSandboxId: store.sandboxId,\n idleTimeoutMinutes,\n ...(checkpointName ? { checkpointName } : {}),\n ...(env ? { env } : {}),\n ...(options.workingDirectory ? { workingDirectory: options.workingDirectory } : {}),\n });\n try {\n await reattached.start();\n return reattached;\n } catch {\n await store.setSandboxId(null);\n // fall through to fresh provision below\n }\n }\n\n // Fresh provision: enforce the per-replica budget before spending quota.\n const max = this.maxSandboxes;\n if (max > 0 && this.#liveCount >= max) {\n throw new SandboxBudgetError(max);\n }\n\n reportProgress(onProgress, { phase: 'provisioning', message: 'Provisioning a new sandbox…' });\n const sandbox = this.#build({\n idleTimeoutMinutes,\n ...(checkpointName ? { checkpointName } : {}),\n ...(env ? { env } : {}),\n ...(options.workingDirectory ? { workingDirectory: options.workingDirectory } : {}),\n });\n await sandbox.start();\n this.#liveCount += 1;\n\n const providerSandboxId = await readProviderSandboxId(sandbox);\n if (providerSandboxId) {\n await store.setSandboxId(providerSandboxId);\n }\n\n return sandbox;\n }\n\n /**\n * Tear down a sandbox binding: stop the live VM (best-effort) and clear the\n * persisted state through the binding store so the next open re-provisions\n * cleanly. Decrements the per-replica live-sandbox counter.\n *\n * @param store the binding to tear down\n * @param sandbox an already-reattached live sandbox to stop, when available\n */\n async teardownSandbox(store: SandboxBindingStore, sandbox?: MaterializationSandbox): Promise<void> {\n if (sandbox?.stop) {\n try {\n await sandbox.stop();\n } catch {\n // Best-effort: the VM may already be gone (idle GC). Still clear the binding.\n }\n }\n if (store.sandboxId) {\n if (this.#liveCount > 0) this.#liveCount -= 1;\n await store.clear();\n }\n }\n\n /**\n * Reattach to an already-provisioned sandbox by its provider id and start it.\n * Used by the workspace seam when opening a project that was already\n * materialized (sandbox id + workdir carried on controller state), so no DB\n * round-trip is needed.\n */\n async reattachSandbox(\n providerSandboxId: string,\n options: EnsureSandboxOptions = {},\n ): Promise<MaterializationSandbox> {\n const sandbox = this.#build({\n providerSandboxId,\n idleTimeoutMinutes: this.idleMinutes,\n ...(options.workingDirectory ? { workingDirectory: options.workingDirectory } : {}),\n });\n await sandbox.start();\n return sandbox;\n }\n}\n"],"mappings":"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","names":[]}
@@ -14,11 +14,10 @@ import type { Context } from 'hono';
14
14
  /**
15
15
  * Locate the built SPA (a dir containing `index.html`). Checked in order:
16
16
  * 1. `MASTRACODE_UI_DIST` — explicit override for custom layouts.
17
- * 2. `ui/` next to the bundled server module vite builds into
18
- * `src/mastra/public/ui` and `mastra build` copies `public/` into
19
- * `.mastra/output/`, so the build output is self-contained.
20
- * 3. `src/mastra/public/ui` under cwd — the vite outDir when running from
21
- * the package root before `mastra build`.
17
+ * 2. `factory/` under cwd both `mastra dev` (cwd is the Mastra public dir)
18
+ * and `mastra start` (cwd is `.mastra/output`) expose assets this way.
19
+ * 3. `factory/` next to the bundled server module.
20
+ * 4. Source-layout fallbacks under `MASTRA_PROJECT_ROOT` or cwd.
22
21
  * Returns `undefined` when no build is found (e.g. plain `mastra dev` without
23
22
  * a prior vite build), in which case the middleware is simply not mounted.
24
23
  */
@@ -1 +1 @@
1
- {"version":3,"file":"spa-static.d.ts","sourceRoot":"","sources":["../src/spa-static.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAOH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AA0BpC;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,SAAS,CAUrD;AAmBD;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,IACxC,GAAG,OAAO,EAAE,MAAM,MAAM,OAAO,CAAC,IAAI,CAAC,KAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAuB/E"}
1
+ {"version":3,"file":"spa-static.d.ts","sourceRoot":"","sources":["../src/spa-static.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAOH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AA0BpC;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,SAAS,CAarD;AAmBD;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,IACxC,GAAG,OAAO,EAAE,MAAM,MAAM,OAAO,CAAC,IAAI,CAAC,KAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAuB/E"}
@@ -25,10 +25,13 @@ var MIME = {
25
25
  };
26
26
  var SERVER_PREFIXES = ["/api", "/web", "/auth"];
27
27
  function resolveUiDistDir() {
28
+ const projectRoot = process.env.MASTRA_PROJECT_ROOT?.trim();
28
29
  const candidates = [
29
30
  process.env.MASTRACODE_UI_DIST,
30
- join(dirname(fileURLToPath(import.meta.url)), "ui"),
31
- resolve(process.cwd(), "src/mastra/public/ui")
31
+ resolve(process.cwd(), "factory"),
32
+ join(dirname(fileURLToPath(import.meta.url)), "factory"),
33
+ projectRoot && resolve(projectRoot, "src/mastra/public/factory"),
34
+ resolve(process.cwd(), "src/mastra/public/factory")
32
35
  ];
33
36
  for (const candidate of candidates) {
34
37
  if (candidate && existsSync(join(candidate, "index.html"))) return resolve(candidate);
@@ -1 +1 @@
1
- {"version":3,"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 '.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'];\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. `ui/` next to the bundled server module vite builds into\n * `src/mastra/public/ui` and `mastra build` copies `public/` into\n * `.mastra/output/`, so the build output is self-contained.\n * 3. `src/mastra/public/ui` under cwd the vite outDir when running from\n * the package root before `mastra build`.\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 candidates = [\n process.env.MASTRACODE_UI_DIST,\n join(dirname(fileURLToPath(import.meta.url)), 'ui'),\n resolve(process.cwd(), 'src/mastra/public/ui'),\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":";AAaA,SAAS,kBAAkB;AAC3B,SAAS,UAAU,YAAY;AAC/B,SAAS,SAAS,SAAS,MAAM,WAAW,SAAS,WAAW;AAChE,SAAS,qBAAqB;AAI9B,IAAM,OAA+B;AAAA,EACnC,SAAS;AAAA,EACT,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,SAAS;AACX;AAGA,IAAM,kBAAkB,CAAC,QAAQ,QAAQ,OAAO;AAazC,SAAS,mBAAuC;AACrD,QAAM,aAAa;AAAA,IACjB,QAAQ,IAAI;AAAA,IACZ,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC,GAAG,IAAI;AAAA,IAClD,QAAQ,QAAQ,IAAI,GAAG,sBAAsB;AAAA,EAC/C;AACA,aAAW,aAAa,YAAY;AAClC,QAAI,aAAa,WAAW,KAAK,WAAW,YAAY,CAAC,EAAG,QAAO,QAAQ,SAAS;AAAA,EACtF;AACA,SAAO;AACT;AAEA,eAAe,OAAO,MAAgC;AACpD,MAAI;AACF,YAAQ,MAAM,KAAK,IAAI,GAAG,OAAO;AAAA,EACnC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAe,UAAU,GAAY,UAAkB,WAAuC;AAC5F,QAAM,OAAO,MAAM,SAAS,QAAQ;AACpC,IAAE,OAAO,gBAAgB,KAAK,QAAQ,QAAQ,EAAE,YAAY,CAAC,KAAK,0BAA0B;AAG5F,IAAE,OAAO,iBAAiB,YAAY,wCAAwC,UAAU;AACxF,SAAO,EAAE,KAAK,IAAI,WAAW,IAAI,CAAC;AACpC;AASO,SAAS,0BAA0B,QAAgB;AACxD,SAAO,OAAO,GAAY,SAAwD;AAChF,QAAI,EAAE,IAAI,WAAW,SAAS,EAAE,IAAI,WAAW,OAAQ,QAAO,KAAK;AACnE,UAAM,OAAO,EAAE,IAAI;AACnB,QAAI,gBAAgB,KAAK,YAAU,SAAS,UAAU,KAAK,WAAW,GAAG,MAAM,GAAG,CAAC,EAAG,QAAO,KAAK;AAGlG,UAAM,WAAW,UAAU,mBAAmB,IAAI,CAAC,EAAE,QAAQ,WAAW,EAAE;AAC1E,UAAM,WAAW,QAAQ,QAAQ,QAAQ;AAGzC,UAAM,eAAe,OAAO,SAAS,GAAG,IAAI,SAAS,SAAS;AAC9D,QAAI,SAAS,WAAW,YAAY,KAAK,aAAa,MAAO,MAAM,OAAO,QAAQ,GAAI;AACpF,aAAO,UAAU,GAAG,UAAU,SAAS,WAAW,SAAS,CAAC;AAAA,IAC9D;AAGA,UAAM,SAAS,EAAE,IAAI,OAAO,QAAQ,KAAK;AACzC,QAAI,SAAS,OAAO,OAAO,SAAS,WAAW,GAAG;AAChD,aAAO,UAAU,GAAG,KAAK,QAAQ,YAAY,GAAG,KAAK;AAAA,IACvD;AAEA,WAAO,KAAK;AAAA,EACd;AACF;","names":[]}
1
+ {"version":3,"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 '.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'];\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":";AAaA,SAAS,kBAAkB;AAC3B,SAAS,UAAU,YAAY;AAC/B,SAAS,SAAS,SAAS,MAAM,WAAW,SAAS,WAAW;AAChE,SAAS,qBAAqB;AAI9B,IAAM,OAA+B;AAAA,EACnC,SAAS;AAAA,EACT,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,SAAS;AACX;AAGA,IAAM,kBAAkB,CAAC,QAAQ,QAAQ,OAAO;AAYzC,SAAS,mBAAuC;AACrD,QAAM,cAAc,QAAQ,IAAI,qBAAqB,KAAK;AAC1D,QAAM,aAAa;AAAA,IACjB,QAAQ,IAAI;AAAA,IACZ,QAAQ,QAAQ,IAAI,GAAG,SAAS;AAAA,IAChC,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC,GAAG,SAAS;AAAA,IACvD,eAAe,QAAQ,aAAa,2BAA2B;AAAA,IAC/D,QAAQ,QAAQ,IAAI,GAAG,2BAA2B;AAAA,EACpD;AACA,aAAW,aAAa,YAAY;AAClC,QAAI,aAAa,WAAW,KAAK,WAAW,YAAY,CAAC,EAAG,QAAO,QAAQ,SAAS;AAAA,EACtF;AACA,SAAO;AACT;AAEA,eAAe,OAAO,MAAgC;AACpD,MAAI;AACF,YAAQ,MAAM,KAAK,IAAI,GAAG,OAAO;AAAA,EACnC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAe,UAAU,GAAY,UAAkB,WAAuC;AAC5F,QAAM,OAAO,MAAM,SAAS,QAAQ;AACpC,IAAE,OAAO,gBAAgB,KAAK,QAAQ,QAAQ,EAAE,YAAY,CAAC,KAAK,0BAA0B;AAG5F,IAAE,OAAO,iBAAiB,YAAY,wCAAwC,UAAU;AACxF,SAAO,EAAE,KAAK,IAAI,WAAW,IAAI,CAAC;AACpC;AASO,SAAS,0BAA0B,QAAgB;AACxD,SAAO,OAAO,GAAY,SAAwD;AAChF,QAAI,EAAE,IAAI,WAAW,SAAS,EAAE,IAAI,WAAW,OAAQ,QAAO,KAAK;AACnE,UAAM,OAAO,EAAE,IAAI;AACnB,QAAI,gBAAgB,KAAK,YAAU,SAAS,UAAU,KAAK,WAAW,GAAG,MAAM,GAAG,CAAC,EAAG,QAAO,KAAK;AAGlG,UAAM,WAAW,UAAU,mBAAmB,IAAI,CAAC,EAAE,QAAQ,WAAW,EAAE;AAC1E,UAAM,WAAW,QAAQ,QAAQ,QAAQ;AAGzC,UAAM,eAAe,OAAO,SAAS,GAAG,IAAI,SAAS,SAAS;AAC9D,QAAI,SAAS,WAAW,YAAY,KAAK,aAAa,MAAO,MAAM,OAAO,QAAQ,GAAI;AACpF,aAAO,UAAU,GAAG,UAAU,SAAS,WAAW,SAAS,CAAC;AAAA,IAC9D;AAGA,UAAM,SAAS,EAAE,IAAI,OAAO,QAAQ,KAAK;AACzC,QAAI,SAAS,OAAO,OAAO,SAAS,WAAW,GAAG;AAChD,aAAO,UAAU,GAAG,KAAK,QAAQ,YAAY,GAAG,KAAK;AAAA,IACvD;AAEA,WAAO,KAAK;AAAA,EACd;AACF;","names":[]}
@@ -0,0 +1,57 @@
1
+ import { FactoryStorageDomain } from '@mastra/core/storage';
2
+ import type { CollectionSchema } from '@mastra/core/storage';
3
+ /**
4
+ * A user-defined OpenAI-compatible provider. The DB-backed counterpart of the
5
+ * local `settings.json` `customProviders` entries: org-scoped rows keyed by the
6
+ * derived provider id (name slug), API key stored alongside so a deployed
7
+ * server never reads the shared settings file.
8
+ */
9
+ export interface CustomProviderRecord {
10
+ id: string;
11
+ orgId: string;
12
+ createdBy: string;
13
+ /** Stable slug derived from the name (`getCustomProviderId`). */
14
+ providerId: string;
15
+ name: string;
16
+ url: string;
17
+ apiKey: string | null;
18
+ models: string[];
19
+ createdAt: Date;
20
+ updatedAt: Date;
21
+ }
22
+ export interface UpsertCustomProviderInput {
23
+ providerId: string;
24
+ name: string;
25
+ url: string;
26
+ apiKey?: string;
27
+ models: string[];
28
+ }
29
+ export declare const CUSTOM_PROVIDERS_SCHEMA: CollectionSchema;
30
+ export declare class CustomProvidersStorage extends FactoryStorageDomain {
31
+ #private;
32
+ constructor();
33
+ init(): Promise<void>;
34
+ dangerouslyClearAll(): Promise<void>;
35
+ /**
36
+ * Create or wholesale-replace a provider by `(orgId, providerId)` — mirrors
37
+ * the settings.json upsert semantics (no key retention: an absent `apiKey`
38
+ * clears the stored key). `previousProviderId` renames the provider: the
39
+ * common path is a single atomic in-place update of the old row; renaming
40
+ * onto an id that already exists overwrites the target first and removes the
41
+ * old row last, so no interleaving or failure can lose the provider.
42
+ */
43
+ upsert({ orgId, userId, input, previousProviderId, }: {
44
+ orgId: string;
45
+ userId: string;
46
+ input: UpsertCustomProviderInput;
47
+ previousProviderId?: string;
48
+ }): Promise<CustomProviderRecord>;
49
+ list({ orgId }: {
50
+ orgId: string;
51
+ }): Promise<CustomProviderRecord[]>;
52
+ delete({ orgId, providerId }: {
53
+ orgId: string;
54
+ providerId: string;
55
+ }): Promise<boolean>;
56
+ }
57
+ //# sourceMappingURL=base.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../../../src/storage/domains/custom-providers/base.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAwB,MAAM,sBAAsB,CAAC;AAClF,OAAO,KAAK,EAAE,gBAAgB,EAAqB,MAAM,sBAAsB,CAAC;AAEhF;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,yBAAyB;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,eAAO,MAAM,uBAAuB,EAAE,gBAerC,CAAC;AA8BF,qBAAa,sBAAuB,SAAQ,oBAAoB;;;IAKxD,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAIrB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ1C;;;;;;;OAOG;IACG,MAAM,CAAC,EACX,KAAK,EACL,MAAM,EACN,KAAK,EACL,kBAAkB,GACnB,EAAE;QACD,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,yBAAyB,CAAC;QACjC,kBAAkB,CAAC,EAAE,MAAM,CAAC;KAC7B,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAkG3B,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,oBAAoB,EAAE,CAAC;IASnE,MAAM,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC;CAG7F"}
@@ -0,0 +1,150 @@
1
+ // src/storage/domains/custom-providers/base.ts
2
+ import { FactoryStorageDomain, UniqueViolationError } from "@mastra/core/storage";
3
+ var CUSTOM_PROVIDERS_SCHEMA = {
4
+ name: "custom_providers",
5
+ columns: {
6
+ id: { type: "uuid-pk" },
7
+ org_id: { type: "text" },
8
+ created_by: { type: "text" },
9
+ provider_id: { type: "text" },
10
+ name: { type: "text" },
11
+ url: { type: "text" },
12
+ api_key: { type: "text", nullable: true },
13
+ models: { type: "json" },
14
+ created_at: { type: "timestamp" },
15
+ updated_at: { type: "timestamp" }
16
+ },
17
+ uniqueIndexes: [{ name: "custom_providers_org_provider_key", columns: ["org_id", "provider_id"] }]
18
+ };
19
+ function toRecord(row) {
20
+ return {
21
+ id: row.id,
22
+ orgId: row.org_id,
23
+ createdBy: row.created_by,
24
+ providerId: row.provider_id,
25
+ name: row.name,
26
+ url: row.url,
27
+ apiKey: row.api_key,
28
+ models: Array.isArray(row.models) ? row.models : [],
29
+ createdAt: row.created_at,
30
+ updatedAt: row.updated_at
31
+ };
32
+ }
33
+ var CustomProvidersStorage = class extends FactoryStorageDomain {
34
+ constructor() {
35
+ super("custom-providers");
36
+ }
37
+ async init() {
38
+ await this.ensureCollections([CUSTOM_PROVIDERS_SCHEMA]);
39
+ }
40
+ async dangerouslyClearAll() {
41
+ await this.ops.deleteMany("custom_providers", {});
42
+ }
43
+ get #db() {
44
+ return this.ops;
45
+ }
46
+ /**
47
+ * Create or wholesale-replace a provider by `(orgId, providerId)` — mirrors
48
+ * the settings.json upsert semantics (no key retention: an absent `apiKey`
49
+ * clears the stored key). `previousProviderId` renames the provider: the
50
+ * common path is a single atomic in-place update of the old row; renaming
51
+ * onto an id that already exists overwrites the target first and removes the
52
+ * old row last, so no interleaving or failure can lose the provider.
53
+ */
54
+ async upsert({
55
+ orgId,
56
+ userId,
57
+ input,
58
+ previousProviderId
59
+ }) {
60
+ const now = /* @__PURE__ */ new Date();
61
+ const renameFrom = previousProviderId && previousProviderId !== input.providerId ? previousProviderId : void 0;
62
+ if (renameFrom) {
63
+ const target = await this.#db.findOne("custom_providers", {
64
+ org_id: orgId,
65
+ provider_id: input.providerId
66
+ });
67
+ if (!target) {
68
+ const renamed = await this.#db.updateAtomic(
69
+ "custom_providers",
70
+ { org_id: orgId, provider_id: renameFrom },
71
+ () => ({
72
+ provider_id: input.providerId,
73
+ name: input.name,
74
+ url: input.url,
75
+ api_key: input.apiKey ?? null,
76
+ models: input.models,
77
+ updated_at: now
78
+ })
79
+ );
80
+ if (renamed) return toRecord(renamed);
81
+ }
82
+ }
83
+ const record = await this.#write({ orgId, userId, input, now });
84
+ if (renameFrom) {
85
+ await this.#db.deleteMany("custom_providers", { org_id: orgId, provider_id: renameFrom });
86
+ }
87
+ return record;
88
+ }
89
+ /**
90
+ * Update-first, then insert-and-catch-unique-violation (see `queue_health`):
91
+ * concurrent creates of the same provider both succeed (last write wins on
92
+ * the single row) instead of one failing on the unique index, and a row
93
+ * deleted mid-flight is recreated rather than reported as a stale success.
94
+ */
95
+ async #write({
96
+ orgId,
97
+ userId,
98
+ input,
99
+ now
100
+ }) {
101
+ const updateExisting = () => this.#db.updateAtomic(
102
+ "custom_providers",
103
+ { org_id: orgId, provider_id: input.providerId },
104
+ () => ({
105
+ name: input.name,
106
+ url: input.url,
107
+ api_key: input.apiKey ?? null,
108
+ models: input.models,
109
+ updated_at: now
110
+ })
111
+ );
112
+ const updated = await updateExisting();
113
+ if (updated) return toRecord(updated);
114
+ try {
115
+ const row = await this.#db.insertOne("custom_providers", {
116
+ org_id: orgId,
117
+ created_by: userId,
118
+ provider_id: input.providerId,
119
+ name: input.name,
120
+ url: input.url,
121
+ api_key: input.apiKey ?? null,
122
+ models: input.models,
123
+ created_at: now,
124
+ updated_at: now
125
+ });
126
+ return toRecord(row);
127
+ } catch (error) {
128
+ if (!(error instanceof UniqueViolationError)) throw error;
129
+ const row = await updateExisting();
130
+ if (!row) throw error;
131
+ return toRecord(row);
132
+ }
133
+ }
134
+ async list({ orgId }) {
135
+ const rows = await this.#db.findMany(
136
+ "custom_providers",
137
+ { org_id: orgId },
138
+ { orderBy: [["name", "asc"]] }
139
+ );
140
+ return rows.map(toRecord);
141
+ }
142
+ async delete({ orgId, providerId }) {
143
+ return await this.#db.deleteMany("custom_providers", { org_id: orgId, provider_id: providerId }) > 0;
144
+ }
145
+ };
146
+ export {
147
+ CUSTOM_PROVIDERS_SCHEMA,
148
+ CustomProvidersStorage
149
+ };
150
+ //# sourceMappingURL=base.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/storage/domains/custom-providers/base.ts"],"sourcesContent":["import { FactoryStorageDomain, UniqueViolationError } from '@mastra/core/storage';\nimport type { CollectionSchema, FactoryStorageOps } from '@mastra/core/storage';\n\n/**\n * A user-defined OpenAI-compatible provider. The DB-backed counterpart of the\n * local `settings.json` `customProviders` entries: org-scoped rows keyed by the\n * derived provider id (name slug), API key stored alongside so a deployed\n * server never reads the shared settings file.\n */\nexport interface CustomProviderRecord {\n id: string;\n orgId: string;\n createdBy: string;\n /** Stable slug derived from the name (`getCustomProviderId`). */\n providerId: string;\n name: string;\n url: string;\n apiKey: string | null;\n models: string[];\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface UpsertCustomProviderInput {\n providerId: string;\n name: string;\n url: string;\n apiKey?: string;\n models: string[];\n}\n\nexport const CUSTOM_PROVIDERS_SCHEMA: CollectionSchema = {\n name: 'custom_providers',\n columns: {\n id: { type: 'uuid-pk' },\n org_id: { type: 'text' },\n created_by: { type: 'text' },\n provider_id: { type: 'text' },\n name: { type: 'text' },\n url: { type: 'text' },\n api_key: { type: 'text', nullable: true },\n models: { type: 'json' },\n created_at: { type: 'timestamp' },\n updated_at: { type: 'timestamp' },\n },\n uniqueIndexes: [{ name: 'custom_providers_org_provider_key', columns: ['org_id', 'provider_id'] }],\n};\n\ninterface CustomProviderDbRow extends Record<string, unknown> {\n id: string;\n org_id: string;\n created_by: string;\n provider_id: string;\n name: string;\n url: string;\n api_key: string | null;\n models: string[];\n created_at: Date;\n updated_at: Date;\n}\n\nfunction toRecord(row: CustomProviderDbRow): CustomProviderRecord {\n return {\n id: row.id,\n orgId: row.org_id,\n createdBy: row.created_by,\n providerId: row.provider_id,\n name: row.name,\n url: row.url,\n apiKey: row.api_key,\n models: Array.isArray(row.models) ? row.models : [],\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n };\n}\n\nexport class CustomProvidersStorage extends FactoryStorageDomain {\n constructor() {\n super('custom-providers');\n }\n\n async init(): Promise<void> {\n await this.ensureCollections([CUSTOM_PROVIDERS_SCHEMA]);\n }\n\n async dangerouslyClearAll(): Promise<void> {\n await this.ops.deleteMany('custom_providers', {});\n }\n\n get #db(): FactoryStorageOps {\n return this.ops;\n }\n\n /**\n * Create or wholesale-replace a provider by `(orgId, providerId)` — mirrors\n * the settings.json upsert semantics (no key retention: an absent `apiKey`\n * clears the stored key). `previousProviderId` renames the provider: the\n * common path is a single atomic in-place update of the old row; renaming\n * onto an id that already exists overwrites the target first and removes the\n * old row last, so no interleaving or failure can lose the provider.\n */\n async upsert({\n orgId,\n userId,\n input,\n previousProviderId,\n }: {\n orgId: string;\n userId: string;\n input: UpsertCustomProviderInput;\n previousProviderId?: string;\n }): Promise<CustomProviderRecord> {\n const now = new Date();\n const renameFrom = previousProviderId && previousProviderId !== input.providerId ? previousProviderId : undefined;\n\n if (renameFrom) {\n const target = await this.#db.findOne<CustomProviderDbRow>('custom_providers', {\n org_id: orgId,\n provider_id: input.providerId,\n });\n if (!target) {\n // In-place rename: one atomic write with no delete/insert gap, so a\n // failure can never drop the provider (and `created_at`/`created_by`\n // are preserved). If a concurrent create grabs the new id between the\n // lookup and this update, the unique index rejects the update and the\n // old row stays intact — the request fails cleanly and a retry\n // converges on the rename-onto-existing path below.\n const renamed = await this.#db.updateAtomic<CustomProviderDbRow>(\n 'custom_providers',\n { org_id: orgId, provider_id: renameFrom },\n () => ({\n provider_id: input.providerId,\n name: input.name,\n url: input.url,\n api_key: input.apiKey ?? null,\n models: input.models,\n updated_at: now,\n }),\n );\n if (renamed) return toRecord(renamed);\n // Old row already gone — fall through to a plain create of the new id.\n }\n // Rename onto an existing id: overwrite the target below, then delete\n // the old row last — a failure in the gap leaves a redundant duplicate\n // (recoverable by re-submitting or deleting) instead of losing data.\n }\n\n const record = await this.#write({ orgId, userId, input, now });\n if (renameFrom) {\n await this.#db.deleteMany('custom_providers', { org_id: orgId, provider_id: renameFrom });\n }\n return record;\n }\n\n /**\n * Update-first, then insert-and-catch-unique-violation (see `queue_health`):\n * concurrent creates of the same provider both succeed (last write wins on\n * the single row) instead of one failing on the unique index, and a row\n * deleted mid-flight is recreated rather than reported as a stale success.\n */\n async #write({\n orgId,\n userId,\n input,\n now,\n }: {\n orgId: string;\n userId: string;\n input: UpsertCustomProviderInput;\n now: Date;\n }): Promise<CustomProviderRecord> {\n const updateExisting = () =>\n this.#db.updateAtomic<CustomProviderDbRow>(\n 'custom_providers',\n { org_id: orgId, provider_id: input.providerId },\n () => ({\n name: input.name,\n url: input.url,\n api_key: input.apiKey ?? null,\n models: input.models,\n updated_at: now,\n }),\n );\n\n const updated = await updateExisting();\n if (updated) return toRecord(updated);\n\n try {\n const row = await this.#db.insertOne<CustomProviderDbRow>('custom_providers', {\n org_id: orgId,\n created_by: userId,\n provider_id: input.providerId,\n name: input.name,\n url: input.url,\n api_key: input.apiKey ?? null,\n models: input.models,\n created_at: now,\n updated_at: now,\n });\n return toRecord(row);\n } catch (error) {\n if (!(error instanceof UniqueViolationError)) throw error;\n // Lost the insert race — apply this write to the winning row.\n const row = await updateExisting();\n if (!row) throw error;\n return toRecord(row);\n }\n }\n\n async list({ orgId }: { orgId: string }): Promise<CustomProviderRecord[]> {\n const rows = await this.#db.findMany<CustomProviderDbRow>(\n 'custom_providers',\n { org_id: orgId },\n { orderBy: [['name', 'asc']] },\n );\n return rows.map(toRecord);\n }\n\n async delete({ orgId, providerId }: { orgId: string; providerId: string }): Promise<boolean> {\n return (await this.#db.deleteMany('custom_providers', { org_id: orgId, provider_id: providerId })) > 0;\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,62 @@
1
+ import { FactoryStorageDomain } from '@mastra/core/storage';
2
+ import type { CollectionSchema } from '@mastra/core/storage';
3
+ /**
4
+ * A user's persisted memory (observational-memory) configuration. The
5
+ * DB-backed tenant counterpart of the local `settings.json` OM fields
6
+ * (`observerModelOverride`, `reflectorModelOverride`, `omObservationThreshold`,
7
+ * `omReflectionThreshold`, `omObserveAttachments`): one row per `(org, user)`,
8
+ * every knob nullable so only explicitly-changed values are stored.
9
+ */
10
+ export interface MemorySettingsRecord {
11
+ orgId: string;
12
+ userId: string;
13
+ observerModelId: string | null;
14
+ reflectorModelId: string | null;
15
+ observationThreshold: number | null;
16
+ reflectionThreshold: number | null;
17
+ observeAttachments: 'auto' | boolean | null;
18
+ createdAt: Date;
19
+ updatedAt: Date;
20
+ }
21
+ /** Partial update — only the provided knobs are written. */
22
+ export interface MemorySettingsPatch {
23
+ observerModelId?: string;
24
+ reflectorModelId?: string;
25
+ observationThreshold?: number;
26
+ reflectionThreshold?: number;
27
+ observeAttachments?: 'auto' | boolean;
28
+ }
29
+ /**
30
+ * Knobs written only when the stored value is still unset (`NULL`). The check
31
+ * happens inside the backend's atomic-update primitive, so a concurrent
32
+ * explicit write to the same knob is never clobbered by a fill.
33
+ */
34
+ export interface MemorySettingsFillIfUnset {
35
+ observerModelId?: string;
36
+ reflectorModelId?: string;
37
+ }
38
+ export declare const MEMORY_SETTINGS_SCHEMA: CollectionSchema;
39
+ export declare class MemorySettingsStorage extends FactoryStorageDomain {
40
+ #private;
41
+ constructor();
42
+ init(): Promise<void>;
43
+ dangerouslyClearAll(): Promise<void>;
44
+ get({ orgId, userId }: {
45
+ orgId: string;
46
+ userId: string;
47
+ }): Promise<MemorySettingsRecord | null>;
48
+ /**
49
+ * Upsert the user's row, writing only the knobs present in `patch`.
50
+ * `fillIfUnset` knobs are written only where the stored value is still
51
+ * `NULL`, decided inside the atomic update so concurrent explicit writes
52
+ * win over fills. Concurrent first writes are resolved via the shared
53
+ * insert-then-catch-unique-violation pattern (see `queue_health`).
54
+ */
55
+ patch({ orgId, userId, patch, fillIfUnset, }: {
56
+ orgId: string;
57
+ userId: string;
58
+ patch: MemorySettingsPatch;
59
+ fillIfUnset?: MemorySettingsFillIfUnset;
60
+ }): Promise<MemorySettingsRecord>;
61
+ }
62
+ //# sourceMappingURL=base.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../../../src/storage/domains/memory-settings/base.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAwB,MAAM,sBAAsB,CAAC;AAClF,OAAO,KAAK,EAAE,gBAAgB,EAAqB,MAAM,sBAAsB,CAAC;AAEhF;;;;;;GAMG;AACH,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,kBAAkB,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;IAC5C,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,4DAA4D;AAC5D,MAAM,WAAW,mBAAmB;IAClC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACvC;AAED;;;;GAIG;AACH,MAAM,WAAW,yBAAyB;IACxC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,eAAO,MAAM,sBAAsB,EAAE,gBAgBpC,CAAC;AAuCF,qBAAa,qBAAsB,SAAQ,oBAAoB;;;IAKvD,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAIrB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC;IAQpC,GAAG,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IAKrG;;;;;;OAMG;IACG,KAAK,CAAC,EACV,KAAK,EACL,MAAM,EACN,KAAK,EACL,WAAW,GACZ,EAAE;QACD,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,mBAAmB,CAAC;QAC3B,WAAW,CAAC,EAAE,yBAAyB,CAAC;KACzC,GAAG,OAAO,CAAC,oBAAoB,CAAC;CAuClC"}