@mastra/deployer-sandbox 0.0.0 → 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +30 -0
- package/dist/alias.d.ts +10 -0
- package/dist/alias.d.ts.map +1 -0
- package/dist/chunk-CSXC6CZL.cjs +115 -0
- package/dist/chunk-CSXC6CZL.cjs.map +1 -0
- package/dist/chunk-Z636GNUQ.js +101 -0
- package/dist/chunk-Z636GNUQ.js.map +1 -0
- package/dist/client/index.cjs +159 -0
- package/dist/client/index.cjs.map +1 -0
- package/dist/client/index.d.ts +98 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +155 -0
- package/dist/client/index.js.map +1 -0
- package/dist/deployer.d.ts +57 -0
- package/dist/deployer.d.ts.map +1 -0
- package/dist/engine.d.ts +19 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/index.cjs +381 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +372 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.d.ts +7 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/shared.d.ts +63 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/studio/assets/CommitMono-400-Regular-DzkyLZ26.woff2 +0 -0
- package/dist/studio/assets/CommitMono-700-Regular-DmOSN4kd.woff2 +0 -0
- package/dist/studio/assets/MonaSans-VariableFont_wdth-wght-CX-7s9jm.ttf +0 -0
- package/dist/studio/assets/babel-lWBcy2sH.js +16 -0
- package/dist/studio/assets/bash-atvbtKCR.js +1 -0
- package/dist/studio/assets/core-kRNZLmmj.js +12 -0
- package/dist/studio/assets/engine-compile-BkERmzkH.js +137 -0
- package/dist/studio/assets/engine-javascript-CSIo_1eZ.js +1 -0
- package/dist/studio/assets/estree-Dd9JfUIE.js +44 -0
- package/dist/studio/assets/github-dark-DHJKELXO.js +1 -0
- package/dist/studio/assets/github-light-DAi9KRSo.js +1 -0
- package/dist/studio/assets/index-LFLxh8sD.js +2 -0
- package/dist/studio/assets/index-yUiD89B4.js +1 -0
- package/dist/studio/assets/javascript-ySlJ1b_l.js +1 -0
- package/dist/studio/assets/json-BQoSv7ci.js +1 -0
- package/dist/studio/assets/jsx-BAng5TT0.js +1 -0
- package/dist/studio/assets/livekit-client.esm-CKIgC2IJ.js +39 -0
- package/dist/studio/assets/main-DLJrsCT1.js +780 -0
- package/dist/studio/assets/markdown-UIAJJxZW.js +1 -0
- package/dist/studio/assets/preload-helper-PPVm8Dsz.js +1 -0
- package/dist/studio/assets/python-DhUJRlN_.js +1 -0
- package/dist/studio/assets/shell-CjFT_Tl9.js +1 -0
- package/dist/studio/assets/standalone-Dfz2oS5J.js +29 -0
- package/dist/studio/assets/style-C3ow9ANP.css +1 -0
- package/dist/studio/assets/tsx-B6W0miNI.js +1 -0
- package/dist/studio/assets/typescript-Dj6nwHGl.js +1 -0
- package/dist/studio/index.html +100 -0
- package/dist/studio/mastra.svg +17 -0
- package/dist/studio/routes-manifest.json +27 -0
- package/dist/types.d.ts +90 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +18 -18
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { DEFAULT_PORT, waitForHealthy, resolveRemoteDir, killPreviousServer, launchServer, tailServerLog, getInfoSafe } from '../chunk-Z636GNUQ.js';
|
|
2
|
+
import { supportsNetworking } from '@mastra/core/workspace';
|
|
3
|
+
|
|
4
|
+
function assertServerOnly() {
|
|
5
|
+
if (typeof globalThis.window !== "undefined") {
|
|
6
|
+
throw new Error(
|
|
7
|
+
"@mastra/deployer-sandbox/client is server-only: resolving a sandbox requires provider credentials that must never reach the browser. Proxy requests through your own backend instead (see createSandboxHandler / createSandboxProxy)."
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
async function getDeployment(options) {
|
|
12
|
+
assertServerOnly();
|
|
13
|
+
const {
|
|
14
|
+
sandbox,
|
|
15
|
+
port = DEFAULT_PORT,
|
|
16
|
+
wake = false,
|
|
17
|
+
healthCheckPath = "/api",
|
|
18
|
+
healthCheckTimeoutMs = 6e4
|
|
19
|
+
} = options;
|
|
20
|
+
const handle = (url2, status, expiresAt) => ({
|
|
21
|
+
url: url2,
|
|
22
|
+
status,
|
|
23
|
+
expiresAt,
|
|
24
|
+
stop: async () => {
|
|
25
|
+
await sandbox.stop?.();
|
|
26
|
+
},
|
|
27
|
+
destroy: async () => {
|
|
28
|
+
await sandbox.destroy?.();
|
|
29
|
+
},
|
|
30
|
+
// Resolved lazily — reading logs requires a running sandbox anyway.
|
|
31
|
+
logs: async (lines) => tailServerLog(sandbox, await resolveRemoteDir(sandbox, options.remoteDir), lines)
|
|
32
|
+
});
|
|
33
|
+
if (!wake) {
|
|
34
|
+
const url2 = supportsNetworking(sandbox) ? await sandbox.networking.getPortUrl(port) : null;
|
|
35
|
+
if (!url2) {
|
|
36
|
+
return handle(null, "stopped");
|
|
37
|
+
}
|
|
38
|
+
const healthy2 = await waitForHealthy(url2, { path: healthCheckPath, timeoutMs: 3e3, intervalMs: 1e3 });
|
|
39
|
+
return handle(url2, healthy2 ? "running" : "stopped");
|
|
40
|
+
}
|
|
41
|
+
await sandbox.start?.();
|
|
42
|
+
if (!supportsNetworking(sandbox)) {
|
|
43
|
+
throw new Error(`Sandbox provider "${sandbox.provider}" does not support networking (public port URLs).`);
|
|
44
|
+
}
|
|
45
|
+
const url = await sandbox.networking.getPortUrl(port);
|
|
46
|
+
if (!url) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Sandbox provider "${sandbox.provider}" did not expose a public URL for port ${port}. Make sure the port is declared when constructing the sandbox (e.g. \`ports: [${port}]\`).`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
let healthy = await waitForHealthy(url, { path: healthCheckPath, timeoutMs: 3e3, intervalMs: 1e3 });
|
|
52
|
+
if (!healthy) {
|
|
53
|
+
const remoteDir = await resolveRemoteDir(sandbox, options.remoteDir);
|
|
54
|
+
await killPreviousServer(sandbox, remoteDir);
|
|
55
|
+
await launchServer(sandbox, remoteDir);
|
|
56
|
+
healthy = await waitForHealthy(url, { path: healthCheckPath, timeoutMs: healthCheckTimeoutMs, intervalMs: 1e3 });
|
|
57
|
+
}
|
|
58
|
+
if (!healthy) {
|
|
59
|
+
const log = await resolveRemoteDir(sandbox, options.remoteDir).then((dir) => tailServerLog(sandbox, dir)).catch(() => "");
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Woke sandbox but the Mastra server did not become healthy at ${url}${healthCheckPath}.` + (log ? `
|
|
62
|
+
|
|
63
|
+
Server log:
|
|
64
|
+
${log}` : "")
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const info = await getInfoSafe(sandbox);
|
|
68
|
+
return handle(url, "running", info?.timeoutAt);
|
|
69
|
+
}
|
|
70
|
+
function createSandboxHandler(options) {
|
|
71
|
+
assertServerOnly();
|
|
72
|
+
let cachedUrl = null;
|
|
73
|
+
const forward = async (request, baseUrl) => {
|
|
74
|
+
const incoming = new URL(request.url);
|
|
75
|
+
const target = new URL(incoming.pathname + incoming.search, baseUrl);
|
|
76
|
+
const headers = new Headers(request.headers);
|
|
77
|
+
headers.delete("host");
|
|
78
|
+
if (options.secret) {
|
|
79
|
+
headers.set("x-mastra-sandbox-secret", options.secret);
|
|
80
|
+
}
|
|
81
|
+
const response = await fetch(target, {
|
|
82
|
+
method: request.method,
|
|
83
|
+
headers,
|
|
84
|
+
body: request.body,
|
|
85
|
+
// duplex is required by Node's fetch for streamed request bodies
|
|
86
|
+
duplex: "half",
|
|
87
|
+
redirect: "manual"
|
|
88
|
+
});
|
|
89
|
+
const location = response.headers.get("location");
|
|
90
|
+
if (location) {
|
|
91
|
+
const resolved = new URL(location, target);
|
|
92
|
+
if (resolved.origin === new URL(baseUrl).origin) {
|
|
93
|
+
const rewritten = new URL(resolved.pathname + resolved.search + resolved.hash, incoming.origin);
|
|
94
|
+
const responseHeaders = new Headers(response.headers);
|
|
95
|
+
responseHeaders.set("location", rewritten.toString());
|
|
96
|
+
return new Response(response.body, {
|
|
97
|
+
status: response.status,
|
|
98
|
+
statusText: response.statusText,
|
|
99
|
+
headers: responseHeaders
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return response;
|
|
104
|
+
};
|
|
105
|
+
return async (request) => {
|
|
106
|
+
cachedUrl ??= options.resolve();
|
|
107
|
+
try {
|
|
108
|
+
return await forward(request, await cachedUrl);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
cachedUrl = null;
|
|
111
|
+
if (!["GET", "HEAD", "OPTIONS"].includes(request.method)) {
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
cachedUrl = options.resolve();
|
|
115
|
+
try {
|
|
116
|
+
return await forward(request, await cachedUrl);
|
|
117
|
+
} catch (retryError) {
|
|
118
|
+
cachedUrl = null;
|
|
119
|
+
throw retryError;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function createSandboxProxy(options) {
|
|
125
|
+
assertServerOnly();
|
|
126
|
+
return async (request) => {
|
|
127
|
+
const connection = options.edgeConfig ?? process.env.EDGE_CONFIG;
|
|
128
|
+
if (!connection) {
|
|
129
|
+
throw new Error("createSandboxProxy requires an Edge Config connection string (EDGE_CONFIG).");
|
|
130
|
+
}
|
|
131
|
+
const conn = new URL(connection);
|
|
132
|
+
const token = conn.searchParams.get("token");
|
|
133
|
+
const itemUrl = new URL(`${conn.origin}${conn.pathname}/item/${encodeURIComponent(options.key)}`);
|
|
134
|
+
const res = await fetch(itemUrl, { headers: { Authorization: `Bearer ${token}` } });
|
|
135
|
+
if (!res.ok) {
|
|
136
|
+
return void 0;
|
|
137
|
+
}
|
|
138
|
+
const sandboxUrl = await res.json();
|
|
139
|
+
if (typeof sandboxUrl !== "string" || !sandboxUrl) {
|
|
140
|
+
return void 0;
|
|
141
|
+
}
|
|
142
|
+
const incoming = new URL(request.url);
|
|
143
|
+
const target = new URL(incoming.pathname + incoming.search, sandboxUrl);
|
|
144
|
+
const headers = new Headers({ "x-middleware-rewrite": target.toString() });
|
|
145
|
+
if (options.secret) {
|
|
146
|
+
headers.set("x-middleware-request-x-mastra-sandbox-secret", options.secret);
|
|
147
|
+
headers.set("x-middleware-override-headers", "x-mastra-sandbox-secret");
|
|
148
|
+
}
|
|
149
|
+
return new Response(null, { headers });
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export { createSandboxHandler, createSandboxProxy, getDeployment };
|
|
154
|
+
//# sourceMappingURL=index.js.map
|
|
155
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/client/index.ts"],"names":["url","healthy"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Runtime resolver + routing helpers for sandbox deployments.\n *\n * SERVER-ONLY. This module resolves sandbox URLs using provider credentials\n * (e.g. VERCEL_TOKEN) — importing it in the browser would ship those\n * credentials to the client. Use the proxy/handler patterns instead so the\n * browser only ever talks to your own domain.\n */\nimport { supportsNetworking } from '@mastra/core/workspace';\nimport type { WorkspaceSandbox } from '@mastra/core/workspace';\nimport {\n DEFAULT_PORT,\n getInfoSafe,\n killPreviousServer,\n launchServer,\n resolveRemoteDir,\n tailServerLog,\n waitForHealthy,\n} from '../shared';\n\nfunction assertServerOnly(): void {\n if (typeof (globalThis as { window?: unknown }).window !== 'undefined') {\n throw new Error(\n '@mastra/deployer-sandbox/client is server-only: resolving a sandbox requires provider credentials ' +\n 'that must never reach the browser. Proxy requests through your own backend instead ' +\n '(see createSandboxHandler / createSandboxProxy).',\n );\n }\n}\n\nexport type DeploymentStatus = 'running' | 'stopped' | 'unknown';\n\nexport interface GetDeploymentOptions {\n /**\n * The sandbox to resolve. Provider construction is identity — e.g.\n * `new VercelSandbox({ sandboxName: 'my-preview', ports: [4111] })` resolves\n * the same sandbox from any process.\n */\n sandbox: WorkspaceSandbox;\n /** Port the Mastra server listens on. Defaults to 4111. */\n port?: number;\n /**\n * Wake the sandbox when it is not running. Waking starts (resumes) the\n * sandbox and, only if the server is not answering, relaunches it from the\n * recorded launch script — some providers (e.g. E2B) resume processes on\n * wake, others (e.g. Vercel) restore just the filesystem. Defaults to false.\n */\n wake?: boolean;\n /** Directory the app was deployed into. Defaults to `$HOME/mastra-app` resolved inside the sandbox. */\n remoteDir?: string;\n /** Path polled for health. Defaults to `/api`. */\n healthCheckPath?: string;\n /** Max time to wait for health after a wake, in ms. Defaults to 60000. */\n healthCheckTimeoutMs?: number;\n}\n\nexport interface ResolvedDeployment {\n /** Public URL of the Mastra server, or null when it could not be resolved without waking. */\n url: string | null;\n status: DeploymentStatus;\n /** When the sandbox will auto-shutdown (when known). */\n expiresAt?: Date;\n stop(): Promise<void>;\n destroy(): Promise<void>;\n logs(lines?: number): Promise<string>;\n}\n\n/**\n * Resolve (and optionally wake) a sandbox deployment.\n *\n * With `wake: false` (default) the sandbox is not started: the URL is resolved\n * only if the sandbox is already running in this process, otherwise\n * `{ url: null, status: 'stopped' | 'unknown' }` is returned and the caller\n * decides whether to wake.\n *\n * With `wake: true` the sandbox is started (providers resume by identity,\n * e.g. name), the server is relaunched if it is not answering, and the\n * deployment is returned once healthy.\n */\nexport async function getDeployment(options: GetDeploymentOptions): Promise<ResolvedDeployment> {\n assertServerOnly();\n\n const {\n sandbox,\n port = DEFAULT_PORT,\n wake = false,\n healthCheckPath = '/api',\n healthCheckTimeoutMs = 60_000,\n } = options;\n\n const handle = (url: string | null, status: DeploymentStatus, expiresAt?: Date): ResolvedDeployment => ({\n url,\n status,\n expiresAt,\n stop: async () => {\n await sandbox.stop?.();\n },\n destroy: async () => {\n await sandbox.destroy?.();\n },\n // Resolved lazily — reading logs requires a running sandbox anyway.\n logs: async (lines?: number) => tailServerLog(sandbox, await resolveRemoteDir(sandbox, options.remoteDir), lines),\n });\n\n if (!wake) {\n // Resolve without starting the sandbox (starting can resume billing).\n const url = supportsNetworking(sandbox) ? await sandbox.networking.getPortUrl(port) : null;\n if (!url) {\n return handle(null, 'stopped');\n }\n const healthy = await waitForHealthy(url, { path: healthCheckPath, timeoutMs: 3_000, intervalMs: 1_000 });\n return handle(url, healthy ? 'running' : 'stopped');\n }\n\n await sandbox.start?.();\n\n if (!supportsNetworking(sandbox)) {\n throw new Error(`Sandbox provider \"${sandbox.provider}\" does not support networking (public port URLs).`);\n }\n const url = await sandbox.networking.getPortUrl(port);\n if (!url) {\n throw new Error(\n `Sandbox provider \"${sandbox.provider}\" did not expose a public URL for port ${port}. ` +\n `Make sure the port is declared when constructing the sandbox (e.g. \\`ports: [${port}]\\`).`,\n );\n }\n\n // Some providers (e.g. Vercel) restore the filesystem but not processes on\n // resume, while others (e.g. E2B) resume processes too — relaunch the server\n // from the recorded launch script only when it is not answering.\n let healthy = await waitForHealthy(url, { path: healthCheckPath, timeoutMs: 3_000, intervalMs: 1_000 });\n if (!healthy) {\n const remoteDir = await resolveRemoteDir(sandbox, options.remoteDir);\n await killPreviousServer(sandbox, remoteDir);\n await launchServer(sandbox, remoteDir);\n healthy = await waitForHealthy(url, { path: healthCheckPath, timeoutMs: healthCheckTimeoutMs, intervalMs: 1_000 });\n }\n if (!healthy) {\n const log = await resolveRemoteDir(sandbox, options.remoteDir)\n .then(dir => tailServerLog(sandbox, dir))\n .catch(() => '');\n throw new Error(\n `Woke sandbox but the Mastra server did not become healthy at ${url}${healthCheckPath}.` +\n (log ? `\\n\\nServer log:\\n${log}` : ''),\n );\n }\n\n const info = await getInfoSafe(sandbox);\n return handle(url, 'running', info?.timeoutAt);\n}\n\n// =============================================================================\n// Tier 3 helpers\n// =============================================================================\n\nexport interface CreateSandboxHandlerOptions {\n /**\n * Resolve the current sandbox URL. Called once, cached, and re-invoked when\n * a forwarded request fails at the network level (e.g. the sandbox rotated\n * its URL or went to sleep). Typically wraps `getDeployment`:\n *\n * ```typescript\n * createSandboxHandler({\n * resolve: async () => {\n * const dep = await getDeployment({ sandbox, wake: true });\n * return dep.url!;\n * },\n * });\n * ```\n */\n resolve: () => Promise<string>;\n /** Shared secret attached as the `x-mastra-sandbox-secret` header on forwarded requests. */\n secret?: string;\n}\n\n/**\n * Framework-free route-handler proxy (fetch `Request` → `Response`). Mount it\n * as a catch-all API route; the browser only ever sees your own domain, and\n * the sandbox URL (which rotates across resumes) stays server-side.\n */\nexport function createSandboxHandler(options: CreateSandboxHandlerOptions): (request: Request) => Promise<Response> {\n assertServerOnly();\n\n let cachedUrl: Promise<string> | null = null;\n\n const forward = async (request: Request, baseUrl: string): Promise<Response> => {\n const incoming = new URL(request.url);\n const target = new URL(incoming.pathname + incoming.search, baseUrl);\n\n const headers = new Headers(request.headers);\n headers.delete('host');\n if (options.secret) {\n headers.set('x-mastra-sandbox-secret', options.secret);\n }\n\n const response = await fetch(target, {\n method: request.method,\n headers,\n body: request.body,\n // duplex is required by Node's fetch for streamed request bodies\n duplex: 'half',\n redirect: 'manual',\n } as RequestInit);\n\n // Redirects are passed through manually — rewrite sandbox-host Locations\n // onto the incoming origin so the rotating sandbox URL never reaches the\n // browser (and subsequent requests keep flowing through this handler).\n const location = response.headers.get('location');\n if (location) {\n const resolved = new URL(location, target);\n if (resolved.origin === new URL(baseUrl).origin) {\n const rewritten = new URL(resolved.pathname + resolved.search + resolved.hash, incoming.origin);\n const responseHeaders = new Headers(response.headers);\n responseHeaders.set('location', rewritten.toString());\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers: responseHeaders,\n });\n }\n }\n\n return response;\n };\n\n return async (request: Request): Promise<Response> => {\n cachedUrl ??= options.resolve();\n\n try {\n return await forward(request, await cachedUrl);\n } catch (error) {\n // Connection-level failure: the sandbox may have rotated its URL or gone\n // to sleep. Drop the cached URL so the next request re-resolves, and\n // retry once — but only for idempotent, bodyless methods. A failed\n // connection doesn't prove a write never landed, and a streamed body is\n // already consumed.\n cachedUrl = null;\n if (!['GET', 'HEAD', 'OPTIONS'].includes(request.method)) {\n throw error;\n }\n cachedUrl = options.resolve();\n try {\n return await forward(request, await cachedUrl);\n } catch (retryError) {\n cachedUrl = null;\n throw retryError;\n }\n }\n };\n}\n\nexport interface CreateSandboxProxyOptions {\n /** Edge Config item key that holds the current sandbox URL. */\n key: string;\n /**\n * Edge Config connection string (`https://edge-config.vercel.com/ecfg_...?token=...`).\n * Defaults to `process.env.EDGE_CONFIG`.\n */\n edgeConfig?: string;\n /** Shared secret attached as the `x-mastra-sandbox-secret` header on the rewrite. */\n secret?: string;\n}\n\n/**\n * Next.js middleware helper for Tier 3 routing: reads the current sandbox URL\n * from Edge Config (kept fresh by the deployer's `alias` option) and rewrites\n * the request to it. Returns `undefined` when no URL is configured so the\n * request falls through.\n *\n * ```typescript\n * // middleware.ts\n * export const middleware = createSandboxProxy({ key: 'my-preview' });\n * export const config = { matcher: '/api/:path*' };\n * ```\n */\nexport function createSandboxProxy(\n options: CreateSandboxProxyOptions,\n): (request: Request) => Promise<Response | undefined> {\n // Reads (and would transmit) the Edge Config bearer token — same\n // server-only contract as the rest of this module.\n assertServerOnly();\n\n return async (request: Request): Promise<Response | undefined> => {\n const connection = options.edgeConfig ?? process.env.EDGE_CONFIG;\n if (!connection) {\n throw new Error('createSandboxProxy requires an Edge Config connection string (EDGE_CONFIG).');\n }\n\n const conn = new URL(connection);\n const token = conn.searchParams.get('token');\n const itemUrl = new URL(`${conn.origin}${conn.pathname}/item/${encodeURIComponent(options.key)}`);\n\n const res = await fetch(itemUrl, { headers: { Authorization: `Bearer ${token}` } });\n if (!res.ok) {\n return undefined;\n }\n const sandboxUrl: unknown = await res.json();\n if (typeof sandboxUrl !== 'string' || !sandboxUrl) {\n return undefined;\n }\n\n const incoming = new URL(request.url);\n const target = new URL(incoming.pathname + incoming.search, sandboxUrl);\n\n // A Response with the x-middleware-rewrite header is how Next.js\n // middleware expresses a rewrite (NextResponse.rewrite does the same) —\n // this keeps the helper free of a next dependency.\n const headers = new Headers({ 'x-middleware-rewrite': target.toString() });\n if (options.secret) {\n headers.set('x-middleware-request-x-mastra-sandbox-secret', options.secret);\n headers.set('x-middleware-override-headers', 'x-mastra-sandbox-secret');\n }\n return new Response(null, { headers });\n };\n}\n"]}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Config } from '@mastra/core/mastra';
|
|
2
|
+
import type { WorkspaceSandbox } from '@mastra/core/workspace';
|
|
3
|
+
import { Deployer } from '@mastra/deployer';
|
|
4
|
+
import type { SandboxDeployerOptions } from './types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Deploy a full Mastra server into any workspace sandbox that supports
|
|
7
|
+
* networking (Vercel Sandbox, E2B, ...) and get a live public URL.
|
|
8
|
+
*
|
|
9
|
+
* Positioning: ephemeral environments — instant previews, PR/CI smoke deploys,
|
|
10
|
+
* agent-built-app verification. Not production hosting.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import { SandboxDeployer } from '@mastra/deployer-sandbox';
|
|
15
|
+
* import { VercelSandbox } from '@mastra/vercel';
|
|
16
|
+
*
|
|
17
|
+
* export const mastra = new Mastra({
|
|
18
|
+
* deployer: new SandboxDeployer({
|
|
19
|
+
* sandbox: new VercelSandbox({ sandboxName: 'my-preview', timeout: 3_600_000, ports: [4111] }),
|
|
20
|
+
* }),
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare class SandboxDeployer extends Deployer {
|
|
25
|
+
/** Sandbox deploys are push-style: `mastra build` runs `deploy()` after bundling. */
|
|
26
|
+
readonly deployOnBuild = true;
|
|
27
|
+
readonly sandbox: WorkspaceSandbox;
|
|
28
|
+
readonly port: number;
|
|
29
|
+
readonly studio: boolean;
|
|
30
|
+
/** Explicit remote dir, when configured. The engine defaults to `$HOME/mastra-app` inside the sandbox. */
|
|
31
|
+
readonly remoteDir?: string;
|
|
32
|
+
private readonly env;
|
|
33
|
+
private readonly alias?;
|
|
34
|
+
private readonly healthCheckTimeoutMs?;
|
|
35
|
+
constructor(options: SandboxDeployerOptions);
|
|
36
|
+
/**
|
|
37
|
+
* Merge all existing env files instead of only the first one (base behavior).
|
|
38
|
+
* Later files win in `loadEnvVars()`, so order least → most specific: a
|
|
39
|
+
* `.env.local` written by `vercel env pull` shouldn't shadow the `.env` that
|
|
40
|
+
* holds the app's own keys.
|
|
41
|
+
*/
|
|
42
|
+
getEnvFiles(): Promise<string[]>;
|
|
43
|
+
protected getUserBundlerOptions(mastraEntryFile: string, outputDirectory: string): Promise<NonNullable<Config['bundler']>>;
|
|
44
|
+
protected getEntry(): string;
|
|
45
|
+
prepare(outputDirectory: string): Promise<void>;
|
|
46
|
+
bundle(entryFile: string, outputDirectory: string, { toolsPaths, projectRoot }: {
|
|
47
|
+
toolsPaths: (string | string[])[];
|
|
48
|
+
projectRoot: string;
|
|
49
|
+
}): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Deploy the built output into the sandbox and wait for the server to come
|
|
52
|
+
* up on its public URL. Writes `sandbox-deployment.json` into the output
|
|
53
|
+
* directory and updates the Edge Config alias when configured.
|
|
54
|
+
*/
|
|
55
|
+
deploy(outputDirectory: string): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=deployer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deployer.d.ts","sourceRoot":"","sources":["../src/deployer.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAM5C,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC;AAEtD;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,eAAgB,SAAQ,QAAQ;IAC3C,qFAAqF;IACrF,QAAQ,CAAC,aAAa,QAAQ;IAC9B,QAAQ,CAAC,OAAO,EAAE,gBAAgB,CAAC;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,0GAA0G;IAC1G,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAyB;IAC7C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAkC;IACzD,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAC,CAAS;gBAEnC,OAAO,EAAE,sBAAsB;IAY3C;;;;;OAKG;IACY,WAAW,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;cAc/B,qBAAqB,CACnC,eAAe,EAAE,MAAM,EACvB,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;IAU1C,SAAS,CAAC,QAAQ,IAAI,MAAM;IAqBtB,OAAO,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAoB/C,MAAM,CACV,SAAS,EAAE,MAAM,EACjB,eAAe,EAAE,MAAM,EACvB,EAAE,UAAU,EAAE,WAAW,EAAE,EAAE;QAAE,UAAU,EAAE,CAAC,MAAM,GAAG,MAAM,EAAE,CAAC,EAAE,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,GACtF,OAAO,CAAC,IAAI,CAAC;IAIhB;;;;OAIG;IACG,MAAM,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CA8CrD"}
|
package/dist/engine.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { DeployToSandboxOptions, SandboxDeployment } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Deploy a prebuilt Mastra server directory into any workspace sandbox that
|
|
4
|
+
* supports networking. Provider-agnostic: only uses the core WorkspaceSandbox
|
|
5
|
+
* contract (`executeCommand` + `networking`, with `writeFiles` / `processes`
|
|
6
|
+
* as fast paths).
|
|
7
|
+
*/
|
|
8
|
+
export declare function deployToSandbox(options: DeployToSandboxOptions): Promise<SandboxDeployment>;
|
|
9
|
+
/**
|
|
10
|
+
* Build the POSIX launch script. Re-running the script restarts the server —
|
|
11
|
+
* the wake path uses this after a snapshot resume (which restores the
|
|
12
|
+
* filesystem but not processes).
|
|
13
|
+
*/
|
|
14
|
+
export declare function buildLaunchScript(opts: {
|
|
15
|
+
remoteDir: string;
|
|
16
|
+
port: number;
|
|
17
|
+
env: Record<string, string>;
|
|
18
|
+
}): string;
|
|
19
|
+
//# sourceMappingURL=engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,sBAAsB,EAAuB,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAc9F;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA+HjG;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,MAAM,CAyBhH"}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunkCSXC6CZL_cjs = require('./chunk-CSXC6CZL.cjs');
|
|
4
|
+
var promises = require('fs/promises');
|
|
5
|
+
var path = require('path');
|
|
6
|
+
var url = require('url');
|
|
7
|
+
var deployer = require('@mastra/deployer');
|
|
8
|
+
var esm = require('fs-extra/esm');
|
|
9
|
+
var child_process = require('child_process');
|
|
10
|
+
var crypto = require('crypto');
|
|
11
|
+
var fs = require('fs');
|
|
12
|
+
var os = require('os');
|
|
13
|
+
var util = require('util');
|
|
14
|
+
var workspace = require('@mastra/core/workspace');
|
|
15
|
+
|
|
16
|
+
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
17
|
+
// src/alias.ts
|
|
18
|
+
async function updateEdgeConfigAlias(options) {
|
|
19
|
+
const { token, teamId } = options;
|
|
20
|
+
if (!token) {
|
|
21
|
+
throw new Error("Updating the Edge Config alias requires a Vercel API token. Pass `alias.token`.");
|
|
22
|
+
}
|
|
23
|
+
const endpoint = new URL(`https://api.vercel.com/v1/edge-config/${options.edgeConfigId}/items`);
|
|
24
|
+
if (teamId) {
|
|
25
|
+
endpoint.searchParams.set("teamId", teamId);
|
|
26
|
+
}
|
|
27
|
+
const res = await fetch(endpoint, {
|
|
28
|
+
method: "PATCH",
|
|
29
|
+
headers: {
|
|
30
|
+
Authorization: `Bearer ${token}`,
|
|
31
|
+
"Content-Type": "application/json"
|
|
32
|
+
},
|
|
33
|
+
body: JSON.stringify({
|
|
34
|
+
items: [{ operation: "upsert", key: options.key, value: options.url }]
|
|
35
|
+
}),
|
|
36
|
+
// Bounded so a hung Vercel API request can't keep `mastra build` open
|
|
37
|
+
// after the sandbox itself is already deployed.
|
|
38
|
+
signal: AbortSignal.timeout(3e4)
|
|
39
|
+
});
|
|
40
|
+
if (!res.ok) {
|
|
41
|
+
const body = await res.text().catch(() => "");
|
|
42
|
+
throw new Error(`Failed to update Edge Config alias "${options.key}" (${res.status}): ${body}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
var execFileAsync = util.promisify(child_process.execFile);
|
|
46
|
+
var noopLogger = {
|
|
47
|
+
debug: () => {
|
|
48
|
+
},
|
|
49
|
+
info: () => {
|
|
50
|
+
},
|
|
51
|
+
warn: () => {
|
|
52
|
+
},
|
|
53
|
+
error: () => {
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
var UPLOAD_CHUNK_SIZE = 96e3;
|
|
57
|
+
async function deployToSandbox(options) {
|
|
58
|
+
const {
|
|
59
|
+
sandbox,
|
|
60
|
+
dir,
|
|
61
|
+
port = chunkCSXC6CZL_cjs.DEFAULT_PORT,
|
|
62
|
+
env = {},
|
|
63
|
+
studio = false,
|
|
64
|
+
healthCheckPath = "/api",
|
|
65
|
+
healthCheckTimeoutMs = 6e4,
|
|
66
|
+
healthCheckIntervalMs = 1e3,
|
|
67
|
+
installCommand = "npm install --omit=dev",
|
|
68
|
+
logger = noopLogger
|
|
69
|
+
} = options;
|
|
70
|
+
if (!fs.existsSync(path.join(dir, "index.mjs"))) {
|
|
71
|
+
throw new Error(`No index.mjs found in "${dir}" \u2014 did the build succeed?`);
|
|
72
|
+
}
|
|
73
|
+
logger.info(`Starting ${sandbox.provider} sandbox...`);
|
|
74
|
+
await sandbox.start?.();
|
|
75
|
+
if (!workspace.supportsNetworking(sandbox)) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`Sandbox provider "${sandbox.provider}" does not support networking (public port URLs), which is required for sandbox deploys.`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
if (!sandbox.executeCommand) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`Sandbox provider "${sandbox.provider}" does not support executeCommand, which is required for sandbox deploys.`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
const url = await sandbox.networking.getPortUrl(port);
|
|
86
|
+
if (!url) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Sandbox provider "${sandbox.provider}" did not expose a public URL for port ${port}. Make sure the port is declared when constructing the sandbox (e.g. \`ports: [${port}]\`).`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const remoteDir = await chunkCSXC6CZL_cjs.resolveRemoteDir(sandbox, options.remoteDir);
|
|
92
|
+
const mergedEnv = { ...env };
|
|
93
|
+
if (studio && mergedEnv.MASTRA_STUDIO_PATH === void 0) {
|
|
94
|
+
mergedEnv.MASTRA_STUDIO_PATH = `${remoteDir}/studio`;
|
|
95
|
+
}
|
|
96
|
+
logger.info(`Uploading build output from ${dir}...`);
|
|
97
|
+
const tarball = await createTarball(dir);
|
|
98
|
+
logger.debug(`Tarball size: ${(tarball.length / 1024 / 1024).toFixed(2)} MB`);
|
|
99
|
+
const remoteTarball = `${remoteDir}/.deploy.tgz`;
|
|
100
|
+
await chunkCSXC6CZL_cjs.runInSandbox(sandbox, `mkdir -p ${chunkCSXC6CZL_cjs.shellQuote(remoteDir)}`);
|
|
101
|
+
await uploadFile(sandbox, remoteTarball, tarball);
|
|
102
|
+
await chunkCSXC6CZL_cjs.killPreviousServer(sandbox, remoteDir);
|
|
103
|
+
await chunkCSXC6CZL_cjs.runInSandbox(sandbox, `cd ${chunkCSXC6CZL_cjs.shellQuote(remoteDir)} && tar -xzf .deploy.tgz && rm -f .deploy.tgz`, {
|
|
104
|
+
timeout: 12e4
|
|
105
|
+
});
|
|
106
|
+
const installHash = await hashInstallInputs(dir, installCommand);
|
|
107
|
+
const marker = `${remoteDir}/${chunkCSXC6CZL_cjs.INSTALL_MARKER}`;
|
|
108
|
+
const markerCheck = await chunkCSXC6CZL_cjs.runInSandbox(sandbox, `cat ${chunkCSXC6CZL_cjs.shellQuote(marker)} 2>/dev/null || true`, {
|
|
109
|
+
allowFailure: true
|
|
110
|
+
});
|
|
111
|
+
if (installHash && markerCheck.stdout.trim() === installHash) {
|
|
112
|
+
logger.info("Dependencies unchanged \u2014 skipping install.");
|
|
113
|
+
} else {
|
|
114
|
+
logger.info(`Installing dependencies (${installCommand})...`);
|
|
115
|
+
await chunkCSXC6CZL_cjs.runInSandbox(sandbox, `cd ${chunkCSXC6CZL_cjs.shellQuote(remoteDir)} && ${installCommand}`, {
|
|
116
|
+
timeout: 6e5,
|
|
117
|
+
label: `install dependencies (${installCommand})`
|
|
118
|
+
});
|
|
119
|
+
if (installHash) {
|
|
120
|
+
await chunkCSXC6CZL_cjs.runInSandbox(sandbox, `printf '%s' ${chunkCSXC6CZL_cjs.shellQuote(installHash)} > ${chunkCSXC6CZL_cjs.shellQuote(marker)}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const launchScript = buildLaunchScript({ remoteDir, port, env: mergedEnv });
|
|
124
|
+
await uploadFile(sandbox, `${remoteDir}/${chunkCSXC6CZL_cjs.SERVER_SCRIPT}`, Buffer.from(launchScript));
|
|
125
|
+
await chunkCSXC6CZL_cjs.runInSandbox(sandbox, `chmod 700 ${chunkCSXC6CZL_cjs.shellQuote(`${remoteDir}/${chunkCSXC6CZL_cjs.SERVER_SCRIPT}`)}`);
|
|
126
|
+
logger.info("Starting Mastra server...");
|
|
127
|
+
await chunkCSXC6CZL_cjs.launchServer(sandbox, remoteDir);
|
|
128
|
+
const healthy = await chunkCSXC6CZL_cjs.waitForHealthy(url, {
|
|
129
|
+
path: healthCheckPath,
|
|
130
|
+
timeoutMs: healthCheckTimeoutMs,
|
|
131
|
+
intervalMs: healthCheckIntervalMs
|
|
132
|
+
});
|
|
133
|
+
if (!healthy) {
|
|
134
|
+
const log = await chunkCSXC6CZL_cjs.tailServerLog(sandbox, remoteDir).catch(() => "");
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Mastra server did not become healthy at ${url}${healthCheckPath} within ${healthCheckTimeoutMs}ms.` + (log ? `
|
|
137
|
+
|
|
138
|
+
Server log:
|
|
139
|
+
${log}` : "\n\n(no server log output captured)")
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
const info = await chunkCSXC6CZL_cjs.getInfoSafe(sandbox);
|
|
143
|
+
return {
|
|
144
|
+
url,
|
|
145
|
+
sandboxId: info?.id ?? sandbox.id,
|
|
146
|
+
expiresAt: info?.timeoutAt,
|
|
147
|
+
stop: async () => {
|
|
148
|
+
await sandbox.stop?.();
|
|
149
|
+
},
|
|
150
|
+
destroy: async () => {
|
|
151
|
+
await sandbox.destroy?.();
|
|
152
|
+
},
|
|
153
|
+
logs: (lines) => chunkCSXC6CZL_cjs.tailServerLog(sandbox, remoteDir, lines)
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function buildLaunchScript(opts) {
|
|
157
|
+
const lines = ["#!/bin/sh", `cd ${chunkCSXC6CZL_cjs.shellQuote(opts.remoteDir)}`];
|
|
158
|
+
const env = {
|
|
159
|
+
MASTRA_AUTO_DETECT_URL: "true",
|
|
160
|
+
...opts.env,
|
|
161
|
+
PORT: String(opts.port),
|
|
162
|
+
MASTRA_HOST: "0.0.0.0"
|
|
163
|
+
};
|
|
164
|
+
for (const [key, value] of Object.entries(env)) {
|
|
165
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
166
|
+
throw new Error(`Invalid environment variable name: "${key}"`);
|
|
167
|
+
}
|
|
168
|
+
lines.push(`export ${key}=${chunkCSXC6CZL_cjs.shellQuote(value)}`);
|
|
169
|
+
}
|
|
170
|
+
lines.push(`echo $$ > ${chunkCSXC6CZL_cjs.shellQuote(chunkCSXC6CZL_cjs.SERVER_PIDFILE)}`);
|
|
171
|
+
lines.push(`exec node index.mjs >> ${chunkCSXC6CZL_cjs.shellQuote(chunkCSXC6CZL_cjs.SERVER_LOGFILE)} 2>&1`);
|
|
172
|
+
return lines.join("\n") + "\n";
|
|
173
|
+
}
|
|
174
|
+
async function createTarball(dir) {
|
|
175
|
+
const tmp = await promises.mkdtemp(path.join(os.tmpdir(), "mastra-sandbox-"));
|
|
176
|
+
const tarPath = path.join(tmp, "deploy.tgz");
|
|
177
|
+
try {
|
|
178
|
+
await execFileAsync("tar", ["-czf", tarPath, "--exclude=node_modules", "-C", dir, "."]);
|
|
179
|
+
return await promises.readFile(tarPath);
|
|
180
|
+
} finally {
|
|
181
|
+
await promises.rm(tmp, { recursive: true, force: true });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async function uploadFile(sandbox, remotePath, content) {
|
|
185
|
+
if (sandbox.writeFiles) {
|
|
186
|
+
await sandbox.writeFiles([{ path: remotePath, content }]);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const b64 = content.toString("base64");
|
|
190
|
+
const tmpPath = `${remotePath}.b64`;
|
|
191
|
+
await chunkCSXC6CZL_cjs.runInSandbox(sandbox, `rm -f ${chunkCSXC6CZL_cjs.shellQuote(tmpPath)}`);
|
|
192
|
+
for (let i = 0; i < b64.length; i += UPLOAD_CHUNK_SIZE) {
|
|
193
|
+
const chunk = b64.slice(i, i + UPLOAD_CHUNK_SIZE);
|
|
194
|
+
await chunkCSXC6CZL_cjs.runInSandbox(sandbox, `printf '%s' ${chunkCSXC6CZL_cjs.shellQuote(chunk)} >> ${chunkCSXC6CZL_cjs.shellQuote(tmpPath)}`, {
|
|
195
|
+
label: `upload chunk to ${remotePath}`
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
await chunkCSXC6CZL_cjs.runInSandbox(
|
|
199
|
+
sandbox,
|
|
200
|
+
`base64 -d ${chunkCSXC6CZL_cjs.shellQuote(tmpPath)} > ${chunkCSXC6CZL_cjs.shellQuote(remotePath)} && rm -f ${chunkCSXC6CZL_cjs.shellQuote(tmpPath)}`,
|
|
201
|
+
{ label: `decode upload at ${remotePath}` }
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
var LOCKFILES = ["package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock"];
|
|
205
|
+
async function hashInstallInputs(dir, installCommand) {
|
|
206
|
+
const hash = crypto.createHash("sha256");
|
|
207
|
+
try {
|
|
208
|
+
hash.update(await promises.readFile(path.join(dir, "package.json")));
|
|
209
|
+
} catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
for (const lockfile of LOCKFILES) {
|
|
213
|
+
let content;
|
|
214
|
+
try {
|
|
215
|
+
content = await promises.readFile(path.join(dir, lockfile));
|
|
216
|
+
} catch {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
hash.update(lockfile).update(content);
|
|
220
|
+
}
|
|
221
|
+
hash.update(installCommand);
|
|
222
|
+
return hash.digest("hex");
|
|
223
|
+
}
|
|
224
|
+
var MANIFEST_FILENAME = "sandbox-deployment.json";
|
|
225
|
+
async function writeDeploymentManifest(outputDir, manifest) {
|
|
226
|
+
await promises.writeFile(path.join(outputDir, MANIFEST_FILENAME), JSON.stringify(manifest, null, 2));
|
|
227
|
+
}
|
|
228
|
+
async function readDeploymentManifest(outputDir) {
|
|
229
|
+
let raw;
|
|
230
|
+
try {
|
|
231
|
+
raw = await promises.readFile(path.join(outputDir, MANIFEST_FILENAME), "utf-8");
|
|
232
|
+
} catch (error) {
|
|
233
|
+
if (error.code === "ENOENT") {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
return JSON.parse(raw);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/deployer.ts
|
|
242
|
+
var SandboxDeployer = class extends deployer.Deployer {
|
|
243
|
+
/** Sandbox deploys are push-style: `mastra build` runs `deploy()` after bundling. */
|
|
244
|
+
deployOnBuild = true;
|
|
245
|
+
sandbox;
|
|
246
|
+
port;
|
|
247
|
+
studio;
|
|
248
|
+
/** Explicit remote dir, when configured. The engine defaults to `$HOME/mastra-app` inside the sandbox. */
|
|
249
|
+
remoteDir;
|
|
250
|
+
env;
|
|
251
|
+
alias;
|
|
252
|
+
healthCheckTimeoutMs;
|
|
253
|
+
constructor(options) {
|
|
254
|
+
super({ name: "SANDBOX" });
|
|
255
|
+
this.sandbox = options.sandbox;
|
|
256
|
+
this.port = options.port ?? chunkCSXC6CZL_cjs.DEFAULT_PORT;
|
|
257
|
+
this.studio = options.studio ?? true;
|
|
258
|
+
this.remoteDir = options.remoteDir;
|
|
259
|
+
this.env = options.env ?? {};
|
|
260
|
+
this.alias = options.alias;
|
|
261
|
+
this.healthCheckTimeoutMs = options.healthCheckTimeoutMs;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Merge all existing env files instead of only the first one (base behavior).
|
|
265
|
+
* Later files win in `loadEnvVars()`, so order least → most specific: a
|
|
266
|
+
* `.env.local` written by `vercel env pull` shouldn't shadow the `.env` that
|
|
267
|
+
* holds the app's own keys.
|
|
268
|
+
*/
|
|
269
|
+
async getEnvFiles() {
|
|
270
|
+
const candidates = [".env", ".env.production", ".env.local"];
|
|
271
|
+
const existing = [];
|
|
272
|
+
for (const file of candidates) {
|
|
273
|
+
try {
|
|
274
|
+
await promises.access(file);
|
|
275
|
+
existing.push(file);
|
|
276
|
+
} catch {
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return existing;
|
|
280
|
+
}
|
|
281
|
+
async getUserBundlerOptions(mastraEntryFile, outputDirectory) {
|
|
282
|
+
const bundlerOptions = await super.getUserBundlerOptions(mastraEntryFile, outputDirectory);
|
|
283
|
+
return {
|
|
284
|
+
...bundlerOptions,
|
|
285
|
+
externals: true
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
getEntry() {
|
|
289
|
+
return `
|
|
290
|
+
// @ts-expect-error
|
|
291
|
+
import { scoreTracesWorkflow } from '@mastra/core/evals/scoreTraces';
|
|
292
|
+
import { mastra } from '#mastra';
|
|
293
|
+
import { createNodeServer, getToolExports } from '#server';
|
|
294
|
+
import { tools } from '#tools';
|
|
295
|
+
|
|
296
|
+
// @ts-expect-error
|
|
297
|
+
await createNodeServer(mastra, { tools: getToolExports(tools), studio: ${this.studio} });
|
|
298
|
+
|
|
299
|
+
const storage = mastra.getStorage();
|
|
300
|
+
if (storage) {
|
|
301
|
+
if (!storage.disableInit) {
|
|
302
|
+
storage.init();
|
|
303
|
+
}
|
|
304
|
+
mastra.__registerInternalWorkflow(scoreTracesWorkflow);
|
|
305
|
+
}
|
|
306
|
+
`;
|
|
307
|
+
}
|
|
308
|
+
async prepare(outputDirectory) {
|
|
309
|
+
await super.prepare(outputDirectory);
|
|
310
|
+
if (this.studio) {
|
|
311
|
+
const __filename = url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
|
|
312
|
+
const __dirname = path.dirname(__filename);
|
|
313
|
+
const studioSource = path.join(path.dirname(__dirname), "dist", "studio");
|
|
314
|
+
const studioServePath = path.join(outputDirectory, this.outputDir, "studio");
|
|
315
|
+
try {
|
|
316
|
+
await esm.copy(studioSource, studioServePath, { overwrite: true });
|
|
317
|
+
} catch (err) {
|
|
318
|
+
throw new Error(
|
|
319
|
+
`Failed to copy studio assets from "${studioSource}" to "${studioServePath}": ${err instanceof Error ? err.message : err}`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async bundle(entryFile, outputDirectory, { toolsPaths, projectRoot }) {
|
|
325
|
+
return this._bundle(this.getEntry(), entryFile, { outputDirectory, projectRoot }, toolsPaths);
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Deploy the built output into the sandbox and wait for the server to come
|
|
329
|
+
* up on its public URL. Writes `sandbox-deployment.json` into the output
|
|
330
|
+
* directory and updates the Edge Config alias when configured.
|
|
331
|
+
*/
|
|
332
|
+
async deploy(outputDirectory) {
|
|
333
|
+
const dir = path.join(outputDirectory, this.outputDir);
|
|
334
|
+
const envVars = await this.loadEnvVars();
|
|
335
|
+
const env = { ...Object.fromEntries(envVars), ...this.env };
|
|
336
|
+
if (envVars.size > 0) {
|
|
337
|
+
this.logger.warn(
|
|
338
|
+
"Environment variables from your .env file are injected into the remote sandbox. Anyone with access to the sandbox can read them."
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
const deployment = await deployToSandbox({
|
|
342
|
+
sandbox: this.sandbox,
|
|
343
|
+
dir,
|
|
344
|
+
port: this.port,
|
|
345
|
+
env,
|
|
346
|
+
studio: this.studio,
|
|
347
|
+
remoteDir: this.remoteDir,
|
|
348
|
+
healthCheckTimeoutMs: this.healthCheckTimeoutMs,
|
|
349
|
+
logger: this.logger
|
|
350
|
+
});
|
|
351
|
+
await writeDeploymentManifest(dir, {
|
|
352
|
+
provider: this.sandbox.provider,
|
|
353
|
+
sandboxId: deployment.sandboxId,
|
|
354
|
+
url: deployment.url,
|
|
355
|
+
port: this.port,
|
|
356
|
+
deployedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
357
|
+
expiresAt: deployment.expiresAt?.toISOString()
|
|
358
|
+
});
|
|
359
|
+
if (this.alias) {
|
|
360
|
+
await updateEdgeConfigAlias({ ...this.alias, url: deployment.url });
|
|
361
|
+
this.logger.info(`Edge Config alias "${this.alias.key}" now points at ${deployment.url}`);
|
|
362
|
+
}
|
|
363
|
+
this.logger.info(`Mastra server deployed: ${deployment.url}/api`);
|
|
364
|
+
if (this.studio) {
|
|
365
|
+
this.logger.info(`Studio: ${deployment.url}`);
|
|
366
|
+
}
|
|
367
|
+
if (deployment.expiresAt) {
|
|
368
|
+
this.logger.warn(`Sandbox expires at ${deployment.expiresAt.toISOString()} (provider runtime cap).`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
exports.MANIFEST_FILENAME = MANIFEST_FILENAME;
|
|
374
|
+
exports.SandboxDeployer = SandboxDeployer;
|
|
375
|
+
exports.buildLaunchScript = buildLaunchScript;
|
|
376
|
+
exports.deployToSandbox = deployToSandbox;
|
|
377
|
+
exports.readDeploymentManifest = readDeploymentManifest;
|
|
378
|
+
exports.updateEdgeConfigAlias = updateEdgeConfigAlias;
|
|
379
|
+
exports.writeDeploymentManifest = writeDeploymentManifest;
|
|
380
|
+
//# sourceMappingURL=index.cjs.map
|
|
381
|
+
//# sourceMappingURL=index.cjs.map
|