@mastra/deployer 1.79.0-alpha.1 → 1.79.0-alpha.3
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/dist/_types/@mastra_hono/dist/browser-stream/index.d.ts +54 -2
- package/dist/_types/@mastra_hono/dist/index.d.ts +1 -0
- package/dist/{analyze-CcyVW_jJ.cjs → analyze-CPVTLGuy.cjs} +168 -25
- package/dist/analyze-CPVTLGuy.cjs.map +1 -0
- package/dist/{analyze-BP0cLyWH.js → analyze-Dvzg7NFK.js} +165 -22
- package/dist/analyze-Dvzg7NFK.js.map +1 -0
- package/dist/build/analyze.cjs +1 -1
- package/dist/build/analyze.js +1 -1
- package/dist/build/fs-routing/discover.d.ts.map +1 -1
- package/dist/build/index.cjs +4 -4
- package/dist/build/index.js +4 -4
- package/dist/build/shared/extract-mastra-option.d.ts.map +1 -1
- package/dist/{build-DGJW5-aB.cjs → build-Bdncb817.cjs} +71 -9
- package/dist/build-Bdncb817.cjs.map +1 -0
- package/dist/{build-CNYwIKPG.js → build-C8gia-n5.js} +71 -8
- package/dist/build-C8gia-n5.js.map +1 -0
- package/dist/bundler/find-workspaces.d.ts +52 -0
- package/dist/bundler/find-workspaces.d.ts.map +1 -0
- package/dist/bundler/index.cjs +3 -3
- package/dist/bundler/index.js +3 -3
- package/dist/bundler/workspaceDependencies.d.ts.map +1 -1
- package/dist/{bundlerOptions-BtMp_x-T.cjs → bundlerOptions-BA9vxjZt.cjs} +51 -1
- package/dist/bundlerOptions-BA9vxjZt.cjs.map +1 -0
- package/dist/{bundlerOptions-ID9i-l8I.js → bundlerOptions-DpvO59QF.js} +49 -1
- package/dist/bundlerOptions-DpvO59QF.js.map +1 -0
- package/dist/docs/SKILL.md +1 -1
- package/dist/docs/assets/SOURCE_MAP.json +1 -1
- package/dist/index.cjs +3 -3
- package/dist/index.js +3 -3
- package/dist/server/index.cjs +243 -4
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +244 -5
- package/dist/server/index.js.map +1 -1
- package/dist/services/deps.d.ts +9 -0
- package/dist/services/deps.d.ts.map +1 -1
- package/dist/services/index.cjs +1 -1
- package/dist/services/index.js +1 -1
- package/dist/{services-D_V7afWX.cjs → services-BQNEW5XJ.cjs} +126 -1
- package/dist/services-BQNEW5XJ.cjs.map +1 -0
- package/dist/{services-DXqmEy8F.js → services-PUoAjxMD.js} +115 -2
- package/dist/services-PUoAjxMD.js.map +1 -0
- package/package.json +8 -9
- package/dist/analyze-BP0cLyWH.js.map +0 -1
- package/dist/analyze-CcyVW_jJ.cjs.map +0 -1
- package/dist/build-CNYwIKPG.js.map +0 -1
- package/dist/build-DGJW5-aB.cjs.map +0 -1
- package/dist/bundlerOptions-BtMp_x-T.cjs.map +0 -1
- package/dist/bundlerOptions-ID9i-l8I.js.map +0 -1
- package/dist/services-DXqmEy8F.js.map +0 -1
- package/dist/services-D_V7afWX.cjs.map +0 -1
package/dist/server/index.cjs
CHANGED
|
@@ -23,7 +23,7 @@ util = require_rolldown_runtime.__toESM(util, 1);
|
|
|
23
23
|
let buffer = require("buffer");
|
|
24
24
|
let hono_body_limit = require("hono/body-limit");
|
|
25
25
|
let hono_streaming = require("hono/streaming");
|
|
26
|
-
require("@mastra/core/request-context");
|
|
26
|
+
let _mastra_core_request_context = require("@mastra/core/request-context");
|
|
27
27
|
let _mastra_server_auth = require("@mastra/server/auth");
|
|
28
28
|
let _mastra_server_browser_stream = require("@mastra/server/browser-stream");
|
|
29
29
|
let _mastra_server_a2a_store = require("@mastra/server/a2a/store");
|
|
@@ -3201,6 +3201,191 @@ function propagateClientDisconnect(fetchResponse, res) {
|
|
|
3201
3201
|
headers: fetchResponse.headers
|
|
3202
3202
|
});
|
|
3203
3203
|
}
|
|
3204
|
+
function createAuthMiddleware({ mastra, requiresAuth = true }) {
|
|
3205
|
+
return async (c, next) => {
|
|
3206
|
+
if (!requiresAuth) return next();
|
|
3207
|
+
const authConfig = mastra.getServer()?.auth;
|
|
3208
|
+
if (!authConfig) return next();
|
|
3209
|
+
const requestContext = c.get("requestContext") ?? new _mastra_core_request_context.RequestContext();
|
|
3210
|
+
c.set("requestContext", requestContext);
|
|
3211
|
+
c.set("mastra", c.get("mastra") ?? mastra);
|
|
3212
|
+
const path = c.req.path;
|
|
3213
|
+
const method = c.req.method;
|
|
3214
|
+
const customRouteAuthConfig = new Map(c.get("customRouteAuthConfig") ?? []);
|
|
3215
|
+
if (!(0, _mastra_server_auth.isCustomRoutePublic)(path, method, customRouteAuthConfig)) customRouteAuthConfig.set(`${method}:${path}`, true);
|
|
3216
|
+
const authHeader = c.req.header("Authorization");
|
|
3217
|
+
let token = authHeader ? authHeader.replace("Bearer ", "") : null;
|
|
3218
|
+
if (!token) token = c.req.query("apiKey") || null;
|
|
3219
|
+
const result = await (0, _mastra_server_auth.coreAuthMiddleware)({
|
|
3220
|
+
path,
|
|
3221
|
+
method,
|
|
3222
|
+
getHeader: (name) => c.req.header(name),
|
|
3223
|
+
mastra,
|
|
3224
|
+
authConfig,
|
|
3225
|
+
customRouteAuthConfig,
|
|
3226
|
+
requestContext,
|
|
3227
|
+
rawRequest: c.req.raw,
|
|
3228
|
+
token,
|
|
3229
|
+
buildAuthorizeContext: () => c
|
|
3230
|
+
});
|
|
3231
|
+
for (const [key, value] of Object.entries(result.headers ?? {})) c.header(key, value, key.toLowerCase() === "set-cookie" ? { append: true } : void 0);
|
|
3232
|
+
if (result.action === "next") return next();
|
|
3233
|
+
return c.json(result.body, result.status);
|
|
3234
|
+
};
|
|
3235
|
+
}
|
|
3236
|
+
const TICKET_BYTES = 32;
|
|
3237
|
+
/**
|
|
3238
|
+
* How long an issued ticket stays valid. A ticket is minted immediately before
|
|
3239
|
+
* the socket is opened, so this only has to cover the round trip plus the
|
|
3240
|
+
* handshake — short enough that a ticket leaked from a log is useless by the
|
|
3241
|
+
* time anyone can act on it.
|
|
3242
|
+
*/
|
|
3243
|
+
const UPGRADE_TICKET_TTL_MS = 3e4;
|
|
3244
|
+
/**
|
|
3245
|
+
* Ceiling on tracked per-agent issuance counters, so the limiter can't grow
|
|
3246
|
+
* without bound as agent ids are invented. Counters are evicted oldest first.
|
|
3247
|
+
*/
|
|
3248
|
+
const MAX_TRACKED_AGENTS = 500;
|
|
3249
|
+
/** 256 bits of base64url, so the ticket can't be guessed or enumerated. */
|
|
3250
|
+
function createTicketValue() {
|
|
3251
|
+
const bytes = new Uint8Array(TICKET_BYTES);
|
|
3252
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
3253
|
+
return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
3254
|
+
}
|
|
3255
|
+
/**
|
|
3256
|
+
* Short-lived, single-use credentials for browser WebSocket upgrades.
|
|
3257
|
+
*
|
|
3258
|
+
* A browser cannot set an `Authorization` header on a WebSocket handshake, so a
|
|
3259
|
+
* deployment that authenticates with a header token would otherwise have to put
|
|
3260
|
+
* that reusable token in the handshake URL, where proxies and access logs can
|
|
3261
|
+
* retain it. Instead the client asks an authenticated HTTP route for a ticket,
|
|
3262
|
+
* and the ticket — worthless once spent, and scoped to one agent and thread —
|
|
3263
|
+
* travels in the URL instead.
|
|
3264
|
+
*
|
|
3265
|
+
* In-process, like the `ViewerRegistry` and the browser sessions it serves: a
|
|
3266
|
+
* screencast is bound to the process holding the browser, so a ticket store
|
|
3267
|
+
* shared across replicas would buy nothing. A ticket presented to a different
|
|
3268
|
+
* replica fails for the same reason the stream itself would.
|
|
3269
|
+
*
|
|
3270
|
+
* Issuance is bounded so the mint endpoint can't be turned into memory or CPU
|
|
3271
|
+
* exhaustion: tickets that are never spent live at most one TTL, a full store
|
|
3272
|
+
* rejects instead of growing, and each agent has a request allowance. Expiry is
|
|
3273
|
+
* swept from the front of the insertion-ordered map, so cleanup costs the
|
|
3274
|
+
* number of entries actually expired rather than a scan of every outstanding
|
|
3275
|
+
* ticket per request.
|
|
3276
|
+
*/
|
|
3277
|
+
var UpgradeTicketStore = class {
|
|
3278
|
+
#tickets = /* @__PURE__ */ new Map();
|
|
3279
|
+
#issuance = /* @__PURE__ */ new Map();
|
|
3280
|
+
#now;
|
|
3281
|
+
constructor(now = () => Date.now()) {
|
|
3282
|
+
this.#now = now;
|
|
3283
|
+
}
|
|
3284
|
+
/**
|
|
3285
|
+
* Issue a ticket bound to one agent and thread. Returns `{ ok: false }` when
|
|
3286
|
+
* the agent exceeded its allowance or the store is at capacity.
|
|
3287
|
+
*/
|
|
3288
|
+
issue(agentId, threadId, ttlMs = UPGRADE_TICKET_TTL_MS) {
|
|
3289
|
+
const now = this.#now();
|
|
3290
|
+
this.#prune(now);
|
|
3291
|
+
if (this.#tickets.size >= 500) return {
|
|
3292
|
+
ok: false,
|
|
3293
|
+
reason: "capacity"
|
|
3294
|
+
};
|
|
3295
|
+
const window = this.#issuance.get(agentId);
|
|
3296
|
+
if (!window || now - window.windowStart >= 6e4) this.#trackIssuance(agentId, {
|
|
3297
|
+
windowStart: now,
|
|
3298
|
+
count: 0
|
|
3299
|
+
});
|
|
3300
|
+
else if (window.count >= 30) return {
|
|
3301
|
+
ok: false,
|
|
3302
|
+
reason: "rate-limited"
|
|
3303
|
+
};
|
|
3304
|
+
this.#issuance.get(agentId).count += 1;
|
|
3305
|
+
const ticket = createTicketValue();
|
|
3306
|
+
this.#tickets.set(ticket, {
|
|
3307
|
+
agentId,
|
|
3308
|
+
threadId,
|
|
3309
|
+
expiresAt: now + ttlMs
|
|
3310
|
+
});
|
|
3311
|
+
return {
|
|
3312
|
+
ok: true,
|
|
3313
|
+
ticket,
|
|
3314
|
+
expiresIn: Math.ceil(ttlMs / 1e3)
|
|
3315
|
+
};
|
|
3316
|
+
}
|
|
3317
|
+
/**
|
|
3318
|
+
* Spend a ticket. Returns `true` only when the ticket is unexpired and was
|
|
3319
|
+
* issued for this exact agent and thread, in which case it is consumed and a
|
|
3320
|
+
* second presentation fails.
|
|
3321
|
+
*/
|
|
3322
|
+
consume(ticket, agentId, threadId) {
|
|
3323
|
+
const entry = this.#tickets.get(ticket);
|
|
3324
|
+
if (!entry) return false;
|
|
3325
|
+
if (entry.expiresAt <= this.#now()) return false;
|
|
3326
|
+
if (entry.agentId !== agentId) return false;
|
|
3327
|
+
if ((entry.threadId ?? void 0) !== (threadId ?? void 0)) return false;
|
|
3328
|
+
this.#tickets.delete(ticket);
|
|
3329
|
+
return true;
|
|
3330
|
+
}
|
|
3331
|
+
/**
|
|
3332
|
+
* Drop expired tickets from the front of the insertion-ordered map. Tickets
|
|
3333
|
+
* are inserted in issue order and share a TTL, so the entries after the first
|
|
3334
|
+
* live one are live too and the sweep can stop there.
|
|
3335
|
+
*/
|
|
3336
|
+
#prune(now) {
|
|
3337
|
+
for (const [ticket, entry] of this.#tickets) {
|
|
3338
|
+
if (entry.expiresAt > now) break;
|
|
3339
|
+
this.#tickets.delete(ticket);
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
/** Record an issuance window, evicting the oldest counter when at the cap. */
|
|
3343
|
+
#trackIssuance(agentId, window) {
|
|
3344
|
+
this.#issuance.delete(agentId);
|
|
3345
|
+
this.#issuance.set(agentId, window);
|
|
3346
|
+
if (this.#issuance.size > MAX_TRACKED_AGENTS) {
|
|
3347
|
+
const oldest = this.#issuance.keys().next().value;
|
|
3348
|
+
if (oldest !== void 0) this.#issuance.delete(oldest);
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
};
|
|
3352
|
+
const TICKET_BODY_MAX_BYTES = 4 * 1024;
|
|
3353
|
+
/**
|
|
3354
|
+
* Resolve an `allowedOrigins` value against the request's `Origin` header.
|
|
3355
|
+
*
|
|
3356
|
+
* Mirrors Hono's `cors.origin` semantics so the same config value means the same
|
|
3357
|
+
* thing on the upgrade as it does on the HTTP routes: `'*'` allows everything,
|
|
3358
|
+
* array entries match literally, and a callback is only an approval when it
|
|
3359
|
+
* returns the request's own origin (the value Hono would echo back) or `'*'`.
|
|
3360
|
+
*/
|
|
3361
|
+
async function isOriginAllowed(allowedOrigins, origin, c) {
|
|
3362
|
+
if (Array.isArray(allowedOrigins)) return allowedOrigins.includes(origin);
|
|
3363
|
+
if (typeof allowedOrigins === "function") {
|
|
3364
|
+
const allowed = await allowedOrigins(origin, c);
|
|
3365
|
+
return allowed === origin || allowed === "*";
|
|
3366
|
+
}
|
|
3367
|
+
return allowedOrigins === "*" || allowedOrigins === origin;
|
|
3368
|
+
}
|
|
3369
|
+
/**
|
|
3370
|
+
* Is the `Origin` header the server's own origin?
|
|
3371
|
+
*
|
|
3372
|
+
* A Studio served by this server connects back to the same host, and operators
|
|
3373
|
+
* don't add their own origin to a CORS allowlist because same-origin requests
|
|
3374
|
+
* never needed CORS. Comparing hosts (rather than scheme + host) keeps that
|
|
3375
|
+
* working behind TLS-terminating proxies, where the server sees `http` while the
|
|
3376
|
+
* browser sends `https`.
|
|
3377
|
+
*/
|
|
3378
|
+
function isSameOrigin(origin, requestHost) {
|
|
3379
|
+
if (!requestHost) return false;
|
|
3380
|
+
let originHost;
|
|
3381
|
+
try {
|
|
3382
|
+
originHost = new URL(origin).host;
|
|
3383
|
+
} catch {
|
|
3384
|
+
return false;
|
|
3385
|
+
}
|
|
3386
|
+
const host = (requestHost.split(",")[0] ?? "").trim();
|
|
3387
|
+
return originHost !== "" && originHost.toLowerCase() === host.toLowerCase();
|
|
3388
|
+
}
|
|
3204
3389
|
/**
|
|
3205
3390
|
* Set up WebSocket-based browser stream endpoint for real-time screencast viewing.
|
|
3206
3391
|
*
|
|
@@ -3210,6 +3395,17 @@ function propagateClientDisconnect(fetchResponse, res) {
|
|
|
3210
3395
|
* - Broadcasts frames to all connected viewers
|
|
3211
3396
|
* - Stops screencast when last viewer disconnects
|
|
3212
3397
|
*
|
|
3398
|
+
* All routes are authenticated with the server's auth configuration. Browsers
|
|
3399
|
+
* cannot attach an `Authorization` header to a WebSocket upgrade, so
|
|
3400
|
+
* session-cookie providers authenticate from the upgrade request's cookies.
|
|
3401
|
+
* Clients that authenticate with a header token instead request a short-lived,
|
|
3402
|
+
* single-use ticket from `POST ${apiPrefix}/agents/:agentId/browser/stream-ticket`
|
|
3403
|
+
* and pass it to the upgrade as `?ticket=`, which keeps the reusable credential
|
|
3404
|
+
* out of the URL. Non-browser clients can pass the token as the `apiKey` query
|
|
3405
|
+
* parameter — use a short-lived token, since a URL can be retained in proxy and
|
|
3406
|
+
* access logs, and prefer the `Authorization` header on the HTTP routes where the
|
|
3407
|
+
* client can set one.
|
|
3408
|
+
*
|
|
3213
3409
|
* **Note**: Requires `ws` package to be installed. If not available, returns null
|
|
3214
3410
|
* and logs a warning. Browser streaming will be disabled but everything else works.
|
|
3215
3411
|
*
|
|
@@ -3225,6 +3421,7 @@ function propagateClientDisconnect(fetchResponse, res) {
|
|
|
3225
3421
|
*
|
|
3226
3422
|
* const app = new Hono();
|
|
3227
3423
|
* const browserStream = await setupBrowserStream(app, {
|
|
3424
|
+
* mastra,
|
|
3228
3425
|
* getToolset: (agentId) => browserToolsets.get(agentId),
|
|
3229
3426
|
* });
|
|
3230
3427
|
*
|
|
@@ -3247,7 +3444,24 @@ async function setupBrowserStream(app, config) {
|
|
|
3247
3444
|
const registry = new _mastra_server_browser_stream.ViewerRegistry();
|
|
3248
3445
|
const rawPrefix = config.apiPrefix ?? "/api";
|
|
3249
3446
|
const apiPrefix = (rawPrefix.endsWith("/") ? rawPrefix.slice(0, -1) : rawPrefix) || "/api";
|
|
3250
|
-
|
|
3447
|
+
const authenticate = createAuthMiddleware({ mastra: config.mastra });
|
|
3448
|
+
const upgradeTickets = new UpgradeTicketStore();
|
|
3449
|
+
const authorizeUpgrade = async (c, next) => {
|
|
3450
|
+
const ticket = c.req.query("ticket");
|
|
3451
|
+
if (!ticket) return authenticate(c, next);
|
|
3452
|
+
const agentId = c.req.param("agentId");
|
|
3453
|
+
if (!upgradeTickets.consume(ticket, agentId, c.req.query("threadId"))) return c.json({ error: "Invalid or expired upgrade ticket" }, 401);
|
|
3454
|
+
return next();
|
|
3455
|
+
};
|
|
3456
|
+
const allowedOrigins = config.allowedOrigins;
|
|
3457
|
+
const checkOrigin = async (c, next) => {
|
|
3458
|
+
const origin = c.req.header("origin");
|
|
3459
|
+
if (!allowedOrigins || !origin) return next();
|
|
3460
|
+
if (isSameOrigin(origin, c.req.header("x-forwarded-host") ?? c.req.header("host"))) return next();
|
|
3461
|
+
if (!await isOriginAllowed(allowedOrigins, origin, c)) return c.text("Forbidden", 403);
|
|
3462
|
+
return next();
|
|
3463
|
+
};
|
|
3464
|
+
app.get("/browser/:agentId/stream", checkOrigin, authorizeUpgrade, upgradeWebSocket((c) => {
|
|
3251
3465
|
const agentId = c.req.param("agentId");
|
|
3252
3466
|
const threadId = c.req.query("threadId");
|
|
3253
3467
|
const viewerKey = threadId ? `${agentId}:${threadId}` : agentId;
|
|
@@ -3269,7 +3483,29 @@ async function setupBrowserStream(app, config) {
|
|
|
3269
3483
|
}
|
|
3270
3484
|
};
|
|
3271
3485
|
}));
|
|
3272
|
-
|
|
3486
|
+
const ticketBodyLimit = (0, hono_body_limit.bodyLimit)({
|
|
3487
|
+
maxSize: TICKET_BODY_MAX_BYTES,
|
|
3488
|
+
onError: (c) => c.json({ error: "Request body too large" }, 413)
|
|
3489
|
+
});
|
|
3490
|
+
app.post(`${apiPrefix}/agents/:agentId/browser/stream-ticket`, authenticate, ticketBodyLimit, async (c) => {
|
|
3491
|
+
const agentId = c.req.param("agentId");
|
|
3492
|
+
if (!agentId) return c.json({ error: "Agent ID is required" }, 400);
|
|
3493
|
+
let threadId = c.req.query("threadId");
|
|
3494
|
+
try {
|
|
3495
|
+
const body = await c.req.json();
|
|
3496
|
+
if (!threadId && typeof body?.threadId === "string") threadId = body.threadId;
|
|
3497
|
+
} catch {}
|
|
3498
|
+
const issued = upgradeTickets.issue(agentId, threadId);
|
|
3499
|
+
if (!issued.ok) {
|
|
3500
|
+
const error = issued.reason === "capacity" ? "Too many outstanding browser stream upgrade tickets" : "Too many browser stream upgrade tickets requested";
|
|
3501
|
+
return c.json({ error }, 429);
|
|
3502
|
+
}
|
|
3503
|
+
return c.json({
|
|
3504
|
+
ticket: issued.ticket,
|
|
3505
|
+
expiresIn: issued.expiresIn
|
|
3506
|
+
});
|
|
3507
|
+
});
|
|
3508
|
+
app.get(`${apiPrefix}/agents/:agentId/browser/session`, authenticate, async (c) => {
|
|
3273
3509
|
const agentId = c.req.param("agentId");
|
|
3274
3510
|
if (!agentId) return c.json({ error: "Agent ID is required" }, 400);
|
|
3275
3511
|
const threadId = c.req.query("threadId");
|
|
@@ -3284,7 +3520,7 @@ async function setupBrowserStream(app, config) {
|
|
|
3284
3520
|
screencastAvailable: true
|
|
3285
3521
|
});
|
|
3286
3522
|
});
|
|
3287
|
-
app.post(`${apiPrefix}/agents/:agentId/browser/close`, async (c) => {
|
|
3523
|
+
app.post(`${apiPrefix}/agents/:agentId/browser/close`, authenticate, ticketBodyLimit, async (c) => {
|
|
3288
3524
|
const agentId = c.req.param("agentId");
|
|
3289
3525
|
if (!agentId) return c.json({ error: "Agent ID is required" }, 400);
|
|
3290
3526
|
const toolset = await config.getToolset(agentId);
|
|
@@ -4701,7 +4937,10 @@ async function createHonoServer(mastra, options = { tools: {} }) {
|
|
|
4701
4937
|
honoServerAdapter.registerContextMiddleware();
|
|
4702
4938
|
const serverMiddleware = mastra.getServerMiddleware?.();
|
|
4703
4939
|
if (serverMiddleware && serverMiddleware.length > 0) for (const m of serverMiddleware) app.use(m.path, skipIfFrameworkPublic(m.handler));
|
|
4940
|
+
const browserStreamOrigins = server?.cors && typeof server.cors === "object" && "origin" in server.cors ? server.cors.origin : void 0;
|
|
4704
4941
|
const browserStreamSetup = options.browserStream === false ? null : await setupBrowserStream(app, {
|
|
4942
|
+
mastra,
|
|
4943
|
+
allowedOrigins: browserStreamOrigins,
|
|
4705
4944
|
getToolset: async (agentId) => {
|
|
4706
4945
|
const resolveBrowser = async (agent) => {
|
|
4707
4946
|
if (!agent) return void 0;
|