@classytic/arc 2.15.3 → 2.16.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/README.md +1 -0
- package/bin/arc.js +12 -0
- package/dist/{BaseController-dx3m2J8V.mjs → BaseController-DlCCTIxJ.mjs} +61 -19
- package/dist/{HookSystem-Iiebom92.mjs → HookSystem-Cmf7-Etp.mjs} +8 -4
- package/dist/{QueryCache-D41bfdBB.d.mts → QueryCache-SvmT_9ti.d.mts} +1 -1
- package/dist/{ResourceRegistry-CTERg_2x.mjs → ResourceRegistry-f48hFk3m.mjs} +52 -9
- package/dist/audit/index.d.mts +1 -1
- package/dist/audit/index.mjs +4 -2
- package/dist/auth/index.d.mts +4 -4
- package/dist/auth/index.mjs +4 -4
- package/dist/auth/redis-session.d.mts +1 -1
- package/dist/{betterAuthOpenApi--M_i87dQ.mjs → betterAuthOpenApi-ClWxaceA.mjs} +10 -6
- package/dist/buildHandler-BZX6zzDM.mjs +300 -0
- package/dist/cache/index.d.mts +3 -3
- package/dist/cache/index.mjs +3 -3
- package/dist/{caching-SM8gghN6.mjs → caching-TeHE8G-v.mjs} +1 -1
- package/dist/cli/commands/describe.d.mts +35 -1
- package/dist/cli/commands/describe.mjs +52 -12
- package/dist/cli/commands/docs.d.mts +1 -4
- package/dist/cli/commands/docs.mjs +4 -16
- package/dist/cli/commands/generate.d.mts +2 -20
- package/dist/cli/commands/generate.mjs +1 -546
- package/dist/cli/commands/init.d.mts +2 -40
- package/dist/cli/commands/init.mjs +1 -3036
- package/dist/cli/commands/introspect.mjs +53 -64
- package/dist/cli/index.d.mts +2 -2
- package/dist/cli/index.mjs +2 -2
- package/dist/{constants-Cxde4rpC.mjs → constants-TrJVIJl0.mjs} +7 -0
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +5 -5
- package/dist/{core-CvmOqEms.mjs → core-DBJ_j6rX.mjs} +222 -44
- package/dist/createActionRouter-DUpN3Dd1.mjs +288 -0
- package/dist/{createAggregationRouter-B0bPDf5b.mjs → createAggregationRouter-Dq-TUCuY.mjs} +3 -2
- package/dist/{createApp-PFegs47-.mjs → createApp-DNccuhyI.mjs} +16 -14
- package/dist/{defineEvent-D5h7EvAx.mjs → defineEvent-DRwY0fYm.mjs} +1 -1
- package/dist/docs/index.d.mts +2 -2
- package/dist/docs/index.mjs +1 -1
- package/dist/{errorHandler-Bk-AGhkU.mjs → errorHandler-DpoXQHZ9.mjs} +17 -14
- package/dist/errors-C1lX_jlm.d.mts +91 -0
- package/dist/{eventPlugin-CaKTYkYM.mjs → eventPlugin-C2cGqtRO.mjs} +1 -1
- package/dist/{eventPlugin-qXpqTebY.d.mts → eventPlugin-CtHC_av1.d.mts} +1 -1
- package/dist/events/index.d.mts +3 -3
- package/dist/events/index.mjs +5 -5
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/factory/index.d.mts +1 -1
- package/dist/factory/index.mjs +2 -2
- package/dist/{fields-COhcH3fk.d.mts → fields-Anj0xdih.d.mts} +1 -1
- package/dist/generate-BWFwgcCM.d.mts +38 -0
- package/dist/generate-CYac-OLv.mjs +654 -0
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/idempotency/index.d.mts +2 -2
- package/dist/idempotency/index.mjs +1 -1
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/{index-BTqLEvhu.d.mts → index-3oIimXQn.d.mts} +12 -12
- package/dist/{index-BstGxcc3.d.mts → index-B-ulKx5P.d.mts} +55 -4
- package/dist/{index-BswOSJCE.d.mts → index-CkW0flkU.d.mts} +355 -16
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +7 -8
- package/dist/init-Dv71MsJr.d.mts +71 -0
- package/dist/init-HDvoO9L5.mjs +3098 -0
- package/dist/integrations/event-gateway.d.mts +2 -2
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +2 -2
- package/dist/integrations/jobs.mjs +3 -3
- package/dist/integrations/mcp/index.d.mts +239 -7
- package/dist/integrations/mcp/index.mjs +2 -528
- package/dist/integrations/mcp/testing.d.mts +2 -2
- package/dist/integrations/mcp/testing.mjs +6 -10
- package/dist/integrations/streamline.d.mts +71 -2
- package/dist/integrations/streamline.mjs +81 -8
- package/dist/integrations/websocket-redis.d.mts +1 -1
- package/dist/integrations/websocket.d.mts +1 -1
- package/dist/integrations/websocket.mjs +1 -0
- package/dist/loadResourcesFromEntry-BLMEI2Xa.mjs +51 -0
- package/dist/{resourceToTools-tFYUNmM0.mjs → mcpPlugin-7vGV51ED.mjs} +1021 -318
- package/dist/{memory-UBydS5ku.mjs → memory-QOLe11D5.mjs} +2 -0
- package/dist/middleware/index.d.mts +1 -1
- package/dist/middleware/index.mjs +1 -1
- package/dist/{openapi-BHXhoX8O.mjs → openapi-34T9yNwd.mjs} +47 -36
- package/dist/permissions/index.d.mts +2 -2
- package/dist/permissions/index.mjs +1 -1
- package/dist/{permissions-ohQyv50e.mjs → permissions-CTxMrreC.mjs} +2 -2
- package/dist/{pipe-Zr0KXjQe.mjs → pipe-DiCyvyPN.mjs} +1 -0
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/pipeline/index.mjs +1 -1
- package/dist/plugins/index.d.mts +5 -5
- package/dist/plugins/index.mjs +10 -10
- package/dist/plugins/response-cache.mjs +5 -5
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/{pluralize-DQgqgifU.mjs → pluralize-B9M8xvy-.mjs} +2 -1
- package/dist/presets/filesUpload.d.mts +4 -4
- package/dist/presets/filesUpload.mjs +2 -2
- package/dist/presets/index.d.mts +1 -1
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +4 -3
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-BbkjdPeH.mjs → presets-C9BE6WaZ.mjs} +2 -2
- package/dist/{queryCachePlugin-m1XsgAIJ.mjs → queryCachePlugin-B4XMSSe7.mjs} +2 -2
- package/dist/{queryCachePlugin-CqMdLI2-.d.mts → queryCachePlugin-Biqzfbi5.d.mts} +2 -2
- package/dist/{redis-DiMkdHEl.d.mts → redis-Cyzrz6SX.d.mts} +1 -1
- package/dist/{redis-stream-D6HzR1Z_.d.mts → redis-stream-DT-YjzrB.d.mts} +1 -1
- package/dist/registry/index.d.mts +319 -2
- package/dist/registry/index.mjs +3 -3
- package/dist/registry-BBE23CDj.mjs +576 -0
- package/dist/{routerShared-DrOa-26E.mjs → routerShared-CZV5aabX.mjs} +3 -3
- package/dist/scope/index.d.mts +3 -3
- package/dist/scope/index.mjs +3 -3
- package/dist/{sse-Bz-5ZeTt.mjs → sse-BY6sTy4P.mjs} +1 -1
- package/dist/testing/index.d.mts +2 -2
- package/dist/testing/index.mjs +16 -7
- package/dist/testing/storageContract.d.mts +1 -1
- package/dist/types/index.d.mts +5 -5
- package/dist/types/storage.d.mts +1 -1
- package/dist/{types-C_s5moIu.mjs → types-Bi0r0vjG.mjs} +53 -1
- package/dist/{types-BQsjgQzS.d.mts → types-BsJMEQ4D.d.mts} +106 -12
- package/dist/{types-DrBaUwyV.d.mts → types-D-fYtKjb.d.mts} +33 -10
- package/dist/{types-CTYvcwHe.d.mts → types-DVfpSfx2.d.mts} +42 -1
- package/dist/utils/index.d.mts +1286 -2
- package/dist/utils/index.mjs +1 -1
- package/dist/{utils-_h9B3c57.mjs → utils-DC5ycPfr.mjs} +89 -40
- package/dist/{buildHandler-CcFOpJLh.mjs → validate-By96rH0r.mjs} +8 -299
- package/dist/{versioning-hmkPcDlX.d.mts → versioning-ZwX9tmbS.d.mts} +1 -1
- package/package.json +22 -29
- package/skills/arc/SKILL.md +299 -689
- package/skills/arc/references/auth.md +19 -7
- package/skills/arc-code-review/SKILL.md +1 -1
- package/skills/arc-code-review/references/arc-cheatsheet.md +100 -322
- package/dist/createActionRouter-S3MLVYot.mjs +0 -220
- package/dist/index-bRjYu21O.d.mts +0 -1320
- package/dist/org/index.d.mts +0 -66
- package/dist/org/index.mjs +0 -486
- package/dist/org/types.d.mts +0 -82
- package/dist/org/types.mjs +0 -1
- package/dist/registry-I-ogLgL9.mjs +0 -46
- /package/dist/{EventTransport-CT_52aWU.d.mts → EventTransport-C-2oAHtw.d.mts} +0 -0
- /package/dist/{EventTransport-DLWoUMHy.mjs → EventTransport-Hxvv5QQz.mjs} +0 -0
- /package/dist/{actionPermissions-CyUkQu6O.mjs → actionPermissions-Bjmvn7Eb.mjs} +0 -0
- /package/dist/{elevation-BXOWoGCF.d.mts → elevation-0YBpa663.d.mts} +0 -0
- /package/dist/{elevation-DgoeTyfX.mjs → elevation-Dci0AYLT.mjs} +0 -0
- /package/dist/{errorHandler-DFr45ZG4.d.mts → errorHandler-mHuyWzZE.d.mts} +0 -0
- /package/dist/{externalPaths-BD5nw6St.d.mts → externalPaths-DFg-2KTp.d.mts} +0 -0
- /package/dist/{interface-beEtJyWM.d.mts → interface-CH0OQudo.d.mts} +0 -0
- /package/dist/{interface-DfLGcus7.d.mts → interface-NwJ_qPlY.d.mts} +0 -0
- /package/dist/{keys-CGcCbNyu.mjs → keys-DopsCuyQ.mjs} +0 -0
- /package/dist/{loadResources-DBMQg_Aj.mjs → loadResources-ChQEj8ih.mjs} +0 -0
- /package/dist/{metrics-Qnvwc-LQ.mjs → metrics-TuOmguhi.mjs} +0 -0
- /package/dist/{replyHelpers-CK-FNO8E.mjs → replyHelpers-C-gD32oF.mjs} +0 -0
- /package/dist/{schemaIR-lYhC2gE5.mjs → schemaIR-Ctc89DSn.mjs} +0 -0
- /package/dist/{sessionManager-C4Le_UB3.d.mts → sessionManager-BqFegc0W.d.mts} +0 -0
- /package/dist/{storage-Dfzt4VTl.d.mts → storage-D2KZJAmn.d.mts} +0 -0
- /package/dist/{store-helpers-BkIN9-vu.mjs → store-helpers-B0sunfZZ.mjs} +0 -0
- /package/dist/{tracing-QJVprktp.d.mts → tracing-Dm8n7Cnn.d.mts} +0 -0
- /package/dist/{versioning-BUrT5aP4.mjs → versioning-B6mimogM.mjs} +0 -0
- /package/dist/{websocket-ChC2rqe1.d.mts → websocket-BkjeGZRn.d.mts} +0 -0
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
-
import fp from "fastify-plugin";
|
|
1
|
+
import { a as defaultCrudDescription, c as mcpHandlerAdapter, d as toCallToolResult, f as toCallToolSuccess, i as fieldRulesToZod, l as permissionDeniedResult, m as createMcpServer, n as mcpPlugin, o as resolveCrudDescription, p as buildRequestContext, r as resourceToTools, s as invokeController, t as filterResourcesForMcp, u as toCallToolError } from "../../mcpPlugin-7vGV51ED.mjs";
|
|
4
2
|
//#region src/integrations/mcp/defineTool.ts
|
|
5
3
|
/**
|
|
6
4
|
* Define a type-safe MCP tool.
|
|
@@ -186,528 +184,4 @@ function definePrompt(name, config) {
|
|
|
186
184
|
};
|
|
187
185
|
}
|
|
188
186
|
//#endregion
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* @classytic/arc — MCP Auth Bridge
|
|
192
|
-
*
|
|
193
|
-
* Resolves MCP session identity from request headers.
|
|
194
|
-
* Supports three modes — the user chooses:
|
|
195
|
-
*
|
|
196
|
-
* 1. `false` — no auth, anonymous access
|
|
197
|
-
* 2. `BetterAuthHandler` — OAuth 2.1 via Better Auth
|
|
198
|
-
* 3. `McpAuthResolver` — custom function (API key, JWT, gateway headers, etc.)
|
|
199
|
-
*/
|
|
200
|
-
/** Distinguish BetterAuthHandler from McpAuthResolver */
|
|
201
|
-
function isBetterAuth(auth) {
|
|
202
|
-
return typeof auth === "object" && auth !== null && "api" in auth && "handler" in auth;
|
|
203
|
-
}
|
|
204
|
-
/**
|
|
205
|
-
* Resolve MCP session identity from request headers.
|
|
206
|
-
*
|
|
207
|
-
* @param headers - HTTP request headers
|
|
208
|
-
* @param auth - false | BetterAuthHandler | McpAuthResolver
|
|
209
|
-
* @param authCache - Optional short-lived cache to avoid redundant auth lookups
|
|
210
|
-
*/
|
|
211
|
-
async function resolveMcpAuth(headers, auth, authCache) {
|
|
212
|
-
if (auth === false) return null;
|
|
213
|
-
const cacheKey = authCache ? extractAuthCacheKey(headers) : null;
|
|
214
|
-
if (cacheKey && authCache) {
|
|
215
|
-
const cached = authCache.get(cacheKey);
|
|
216
|
-
if (cached !== void 0) return cached;
|
|
217
|
-
}
|
|
218
|
-
let result = null;
|
|
219
|
-
if (typeof auth === "function") try {
|
|
220
|
-
result = await auth(headers);
|
|
221
|
-
} catch {
|
|
222
|
-
result = null;
|
|
223
|
-
}
|
|
224
|
-
else if (isBetterAuth(auth)) try {
|
|
225
|
-
const session = await auth.api.getMcpSession({ headers });
|
|
226
|
-
if (!session?.userId) result = null;
|
|
227
|
-
else result = {
|
|
228
|
-
userId: session.userId,
|
|
229
|
-
organizationId: session.activeOrganizationId,
|
|
230
|
-
...session.clientId ? { clientId: session.clientId } : {},
|
|
231
|
-
...session.scopes ? { scopes: session.scopes.split(" ") } : {}
|
|
232
|
-
};
|
|
233
|
-
} catch {
|
|
234
|
-
result = null;
|
|
235
|
-
}
|
|
236
|
-
if (cacheKey && authCache) authCache.set(cacheKey, result);
|
|
237
|
-
return result;
|
|
238
|
-
}
|
|
239
|
-
const DEFAULT_AUTH_CACHE_TTL_MS = 5e3;
|
|
240
|
-
const DEFAULT_AUTH_CACHE_MAX = 500;
|
|
241
|
-
/** Short-lived auth cache to avoid redundant auth resolver calls in stateless mode */
|
|
242
|
-
var McpAuthCache = class {
|
|
243
|
-
cache = /* @__PURE__ */ new Map();
|
|
244
|
-
ttlMs;
|
|
245
|
-
maxEntries;
|
|
246
|
-
constructor(opts) {
|
|
247
|
-
this.ttlMs = opts?.ttlMs ?? DEFAULT_AUTH_CACHE_TTL_MS;
|
|
248
|
-
this.maxEntries = opts?.maxEntries ?? DEFAULT_AUTH_CACHE_MAX;
|
|
249
|
-
}
|
|
250
|
-
get(key) {
|
|
251
|
-
const entry = this.cache.get(key);
|
|
252
|
-
if (!entry) return void 0;
|
|
253
|
-
if (Date.now() > entry.expires) {
|
|
254
|
-
this.cache.delete(key);
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
return entry.result;
|
|
258
|
-
}
|
|
259
|
-
set(key, result) {
|
|
260
|
-
if (this.cache.size >= this.maxEntries) {
|
|
261
|
-
const now = Date.now();
|
|
262
|
-
for (const [k, v] of this.cache) if (now > v.expires) this.cache.delete(k);
|
|
263
|
-
if (this.cache.size >= this.maxEntries) {
|
|
264
|
-
const firstKey = this.cache.keys().next().value;
|
|
265
|
-
if (firstKey) this.cache.delete(firstKey);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
this.cache.set(key, {
|
|
269
|
-
result,
|
|
270
|
-
expires: Date.now() + this.ttlMs
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
};
|
|
274
|
-
/**
|
|
275
|
-
* Extract a cache key from auth-related headers.
|
|
276
|
-
* Uses SHA-256 hash of header values to prevent cache key collisions
|
|
277
|
-
* and avoid storing raw credentials in memory.
|
|
278
|
-
*/
|
|
279
|
-
function extractAuthCacheKey(headers) {
|
|
280
|
-
if (headers.authorization) return `authz:${hashForCache(headers.authorization)}`;
|
|
281
|
-
if (headers["x-api-key"]) return `apikey:${hashForCache(headers["x-api-key"])}`;
|
|
282
|
-
return null;
|
|
283
|
-
}
|
|
284
|
-
function hashForCache(value) {
|
|
285
|
-
return createHash("sha256").update(value).digest("hex").slice(0, 32);
|
|
286
|
-
}
|
|
287
|
-
/**
|
|
288
|
-
* Register OAuth 2.1 discovery endpoints for MCP clients.
|
|
289
|
-
* Only relevant when using Better Auth — custom auth doesn't need these.
|
|
290
|
-
*/
|
|
291
|
-
async function registerOAuthDiscovery(fastify, auth) {
|
|
292
|
-
fastify.get("/.well-known/oauth-authorization-server", async (req, reply) => {
|
|
293
|
-
await forwardResponse(reply, await auth.handler(toWebRequest(req)));
|
|
294
|
-
});
|
|
295
|
-
fastify.get("/.well-known/oauth-protected-resource", async (req, reply) => {
|
|
296
|
-
await forwardResponse(reply, await auth.handler(toWebRequest(req)));
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
function toWebRequest(req) {
|
|
300
|
-
const protocol = req.protocol ?? "http";
|
|
301
|
-
const host = req.hostname ?? "localhost";
|
|
302
|
-
return new Request(`${protocol}://${host}${req.url}`, {
|
|
303
|
-
method: req.method,
|
|
304
|
-
headers: req.headers
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
async function forwardResponse(reply, response) {
|
|
308
|
-
reply.status(response.status);
|
|
309
|
-
response.headers.forEach((value, key) => {
|
|
310
|
-
if (key.toLowerCase() !== "transfer-encoding") reply.header(key, value);
|
|
311
|
-
});
|
|
312
|
-
reply.send(await response.text());
|
|
313
|
-
}
|
|
314
|
-
//#endregion
|
|
315
|
-
//#region src/integrations/mcp/schemaResources.ts
|
|
316
|
-
/**
|
|
317
|
-
* Register MCP Resources for schema discovery.
|
|
318
|
-
*/
|
|
319
|
-
function registerSchemaResources(server, resources, overrides) {
|
|
320
|
-
const srv = server;
|
|
321
|
-
srv.resource("schemas", "arc://schemas", {
|
|
322
|
-
title: "Arc Resource Schemas",
|
|
323
|
-
description: "All available resources",
|
|
324
|
-
mimeType: "application/json"
|
|
325
|
-
}, async () => ({ contents: [{
|
|
326
|
-
uri: "arc://schemas",
|
|
327
|
-
mimeType: "application/json",
|
|
328
|
-
text: JSON.stringify(resources.map((r) => ({
|
|
329
|
-
name: r.name,
|
|
330
|
-
displayName: r.displayName,
|
|
331
|
-
fieldCount: r.schemaOptions?.fieldRules ? Object.keys(r.schemaOptions.fieldRules).length : 0,
|
|
332
|
-
operations: getOps(r, overrides?.[r.name]?.operations),
|
|
333
|
-
presets: r._appliedPresets ?? []
|
|
334
|
-
})), null, 2)
|
|
335
|
-
}] }));
|
|
336
|
-
for (const r of resources) {
|
|
337
|
-
const uri = `arc://schemas/${r.name}`;
|
|
338
|
-
const schemaOpts = r.schemaOptions;
|
|
339
|
-
srv.resource(`schema-${r.name}`, uri, {
|
|
340
|
-
title: `${r.displayName} Schema`,
|
|
341
|
-
description: `Schema for ${r.displayName}`,
|
|
342
|
-
mimeType: "application/json"
|
|
343
|
-
}, async () => ({ contents: [{
|
|
344
|
-
uri,
|
|
345
|
-
mimeType: "application/json",
|
|
346
|
-
text: JSON.stringify({
|
|
347
|
-
name: r.name,
|
|
348
|
-
displayName: r.displayName,
|
|
349
|
-
operations: getOps(r, overrides?.[r.name]?.operations),
|
|
350
|
-
fields: r.schemaOptions?.fieldRules ?? {},
|
|
351
|
-
filterableFields: schemaOpts?.filterableFields ?? [],
|
|
352
|
-
presets: r._appliedPresets ?? []
|
|
353
|
-
}, null, 2)
|
|
354
|
-
}] }));
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
function getOps(r, override) {
|
|
358
|
-
let ops = [
|
|
359
|
-
"list",
|
|
360
|
-
"get",
|
|
361
|
-
"create",
|
|
362
|
-
"update",
|
|
363
|
-
"delete"
|
|
364
|
-
].filter((op) => !r.disabledRoutes?.includes(op));
|
|
365
|
-
if (override) ops = ops.filter((op) => override.includes(op));
|
|
366
|
-
return ops;
|
|
367
|
-
}
|
|
368
|
-
//#endregion
|
|
369
|
-
//#region src/integrations/mcp/sessionCache.ts
|
|
370
|
-
const DEFAULT_TTL_MS = 1800 * 1e3;
|
|
371
|
-
const DEFAULT_MAX_SESSIONS = 1e3;
|
|
372
|
-
var McpSessionCache = class {
|
|
373
|
-
sessions = /* @__PURE__ */ new Map();
|
|
374
|
-
ttlMs;
|
|
375
|
-
maxSessions;
|
|
376
|
-
cleanupTimer = null;
|
|
377
|
-
constructor(opts = {}) {
|
|
378
|
-
this.ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
379
|
-
this.maxSessions = opts.maxSessions ?? DEFAULT_MAX_SESSIONS;
|
|
380
|
-
if (this.ttlMs > 0) {
|
|
381
|
-
this.cleanupTimer = setInterval(() => this.cleanup(), Math.max(this.ttlMs / 2, 5e3));
|
|
382
|
-
if (this.cleanupTimer.unref) this.cleanupTimer.unref();
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
/** Get an existing session by ID */
|
|
386
|
-
get(sessionId) {
|
|
387
|
-
const entry = this.sessions.get(sessionId);
|
|
388
|
-
if (!entry) return void 0;
|
|
389
|
-
if (Date.now() - entry.lastAccessed > this.ttlMs) {
|
|
390
|
-
this.remove(sessionId);
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
return entry;
|
|
394
|
-
}
|
|
395
|
-
/** Store a new session */
|
|
396
|
-
set(sessionId, entry) {
|
|
397
|
-
if (this.sessions.size >= this.maxSessions && !this.sessions.has(sessionId)) this.evictOldest();
|
|
398
|
-
entry.lastAccessed = Date.now();
|
|
399
|
-
this.sessions.set(sessionId, entry);
|
|
400
|
-
}
|
|
401
|
-
/** Refresh the TTL on a session */
|
|
402
|
-
touch(sessionId) {
|
|
403
|
-
const entry = this.sessions.get(sessionId);
|
|
404
|
-
if (entry) entry.lastAccessed = Date.now();
|
|
405
|
-
}
|
|
406
|
-
/** Remove and close a session */
|
|
407
|
-
remove(sessionId) {
|
|
408
|
-
const entry = this.sessions.get(sessionId);
|
|
409
|
-
if (entry) {
|
|
410
|
-
this.closeTransport(entry);
|
|
411
|
-
this.sessions.delete(sessionId);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
/** Remove all expired sessions */
|
|
415
|
-
cleanup() {
|
|
416
|
-
const now = Date.now();
|
|
417
|
-
for (const [id, entry] of this.sessions) if (now - entry.lastAccessed > this.ttlMs) {
|
|
418
|
-
this.closeTransport(entry);
|
|
419
|
-
this.sessions.delete(id);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
/** Close all sessions and stop cleanup timer */
|
|
423
|
-
close() {
|
|
424
|
-
if (this.cleanupTimer) {
|
|
425
|
-
clearInterval(this.cleanupTimer);
|
|
426
|
-
this.cleanupTimer = null;
|
|
427
|
-
}
|
|
428
|
-
for (const [id, entry] of this.sessions) {
|
|
429
|
-
this.closeTransport(entry);
|
|
430
|
-
this.sessions.delete(id);
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
/** Current session count */
|
|
434
|
-
get size() {
|
|
435
|
-
return this.sessions.size;
|
|
436
|
-
}
|
|
437
|
-
/** Evict the oldest (least recently accessed) session */
|
|
438
|
-
evictOldest() {
|
|
439
|
-
let oldestId = null;
|
|
440
|
-
let oldestTime = Infinity;
|
|
441
|
-
for (const [id, entry] of this.sessions) if (entry.lastAccessed < oldestTime) {
|
|
442
|
-
oldestTime = entry.lastAccessed;
|
|
443
|
-
oldestId = id;
|
|
444
|
-
}
|
|
445
|
-
if (oldestId) this.remove(oldestId);
|
|
446
|
-
}
|
|
447
|
-
/** Safely close a transport */
|
|
448
|
-
closeTransport(entry) {
|
|
449
|
-
try {
|
|
450
|
-
const transport = entry.transport;
|
|
451
|
-
if (transport && typeof transport.close === "function") transport.close();
|
|
452
|
-
} catch {}
|
|
453
|
-
}
|
|
454
|
-
};
|
|
455
|
-
//#endregion
|
|
456
|
-
//#region src/integrations/mcp/mcpPlugin.ts
|
|
457
|
-
/**
|
|
458
|
-
* @classytic/arc — MCP Plugin (Level 1)
|
|
459
|
-
*
|
|
460
|
-
* Fastify plugin that auto-generates MCP tools from Arc resources.
|
|
461
|
-
*
|
|
462
|
-
* Two transport modes:
|
|
463
|
-
* - **Stateless** (default) — fresh server per request, no session tracking.
|
|
464
|
-
* Best for production, horizontal scaling, serverless, edge.
|
|
465
|
-
* - **Stateful** — sessions cached with TTL, reused across requests.
|
|
466
|
-
* Use when you need server-initiated notifications or long-lived connections.
|
|
467
|
-
*
|
|
468
|
-
* Auth is NOT enforced — the plugin respects whatever auth mode you choose:
|
|
469
|
-
* - `auth: false` — no auth, anonymous access (dev/testing/stdio)
|
|
470
|
-
* - `auth: betterAuthInstance` — OAuth 2.1 via Better Auth's mcp() plugin
|
|
471
|
-
* - `auth: async (headers) => {...}` — custom function (API key, JWT, gateway, etc.)
|
|
472
|
-
*
|
|
473
|
-
* @example
|
|
474
|
-
* ```typescript
|
|
475
|
-
* // Stateless (default) — production, scalable
|
|
476
|
-
* await app.register(mcpPlugin, { resources, auth: false });
|
|
477
|
-
*
|
|
478
|
-
* // Stateful — when you need session persistence
|
|
479
|
-
* await app.register(mcpPlugin, { resources, stateful: true, sessionTtlMs: 600000 });
|
|
480
|
-
*
|
|
481
|
-
* // Multiple MCP endpoints scoped to different resource groups
|
|
482
|
-
* await app.register(mcpPlugin, { resources: catalogResources, prefix: '/mcp/catalog' });
|
|
483
|
-
* await app.register(mcpPlugin, { resources: orderResources, prefix: '/mcp/orders' });
|
|
484
|
-
* ```
|
|
485
|
-
*/
|
|
486
|
-
const mcpPluginImpl = async (fastify, options) => {
|
|
487
|
-
let StreamableHTTPServerTransport;
|
|
488
|
-
try {
|
|
489
|
-
StreamableHTTPServerTransport = (await import("@modelcontextprotocol/sdk/server/streamableHttp.js")).StreamableHTTPServerTransport;
|
|
490
|
-
} catch {
|
|
491
|
-
throw new Error("@modelcontextprotocol/sdk is required for MCP support. Install it: npm install @modelcontextprotocol/sdk");
|
|
492
|
-
}
|
|
493
|
-
try {
|
|
494
|
-
await import("zod");
|
|
495
|
-
} catch {
|
|
496
|
-
throw new Error("zod is required for MCP tool schemas. Install it: npm install zod");
|
|
497
|
-
}
|
|
498
|
-
let enabledResources;
|
|
499
|
-
if (options.include) {
|
|
500
|
-
const includeSet = new Set(options.include);
|
|
501
|
-
enabledResources = options.resources.filter((r) => includeSet.has(r.name));
|
|
502
|
-
} else {
|
|
503
|
-
const excludeSet = new Set(options.exclude ?? []);
|
|
504
|
-
enabledResources = options.resources.filter((r) => !excludeSet.has(r.name));
|
|
505
|
-
}
|
|
506
|
-
const overrides = options.overrides ?? {};
|
|
507
|
-
const allTools = enabledResources.flatMap((r) => {
|
|
508
|
-
const resOverrides = overrides[r.name] ?? {};
|
|
509
|
-
return resourceToTools(r, {
|
|
510
|
-
...resOverrides,
|
|
511
|
-
toolNamePrefix: resOverrides.toolNamePrefix ?? options.toolNamePrefix
|
|
512
|
-
});
|
|
513
|
-
});
|
|
514
|
-
if (options.extraTools) allTools.push(...options.extraTools);
|
|
515
|
-
fastify.log.info(`mcpPlugin: ${allTools.length} tools from ${enabledResources.length} resources`);
|
|
516
|
-
const overrideOpsMap = {};
|
|
517
|
-
for (const [name, cfg] of Object.entries(overrides)) overrideOpsMap[name] = { operations: cfg.operations };
|
|
518
|
-
const stateful = options.stateful === true;
|
|
519
|
-
const cache = stateful ? new McpSessionCache({
|
|
520
|
-
ttlMs: options.sessionTtlMs,
|
|
521
|
-
maxSessions: options.maxSessions
|
|
522
|
-
}) : null;
|
|
523
|
-
async function createServerInstance(authRef) {
|
|
524
|
-
const server = await createMcpServer({
|
|
525
|
-
name: options.serverName ?? "arc-mcp",
|
|
526
|
-
version: options.serverVersion ?? "1.0.0",
|
|
527
|
-
instructions: options.instructions,
|
|
528
|
-
tools: allTools,
|
|
529
|
-
prompts: options.extraPrompts
|
|
530
|
-
}, authRef);
|
|
531
|
-
registerSchemaResources(server, enabledResources, overrideOpsMap);
|
|
532
|
-
return server;
|
|
533
|
-
}
|
|
534
|
-
if (options.auth && isBetterAuth(options.auth)) await registerOAuthDiscovery(fastify, options.auth);
|
|
535
|
-
const prefix = options.prefix ?? "/mcp";
|
|
536
|
-
fastify.get(`${prefix}/health`, async (_request, reply) => {
|
|
537
|
-
reply.send({
|
|
538
|
-
status: "ok",
|
|
539
|
-
mode: stateful ? "stateful" : "stateless",
|
|
540
|
-
tools: allTools.length,
|
|
541
|
-
resources: enabledResources.length,
|
|
542
|
-
toolNames: allTools.map((t) => t.name),
|
|
543
|
-
sessions: cache?.size ?? null
|
|
544
|
-
});
|
|
545
|
-
});
|
|
546
|
-
if (stateful) registerStatefulRoutes(fastify, prefix, options, cache, createServerInstance, StreamableHTTPServerTransport);
|
|
547
|
-
else {
|
|
548
|
-
const authCache = options.auth && options.authCacheTtlMs !== 0 ? new McpAuthCache({ ttlMs: options.authCacheTtlMs }) : void 0;
|
|
549
|
-
registerStatelessRoutes(fastify, prefix, options, createServerInstance, StreamableHTTPServerTransport, authCache);
|
|
550
|
-
}
|
|
551
|
-
if (cache) fastify.addHook("onClose", async () => cache.close());
|
|
552
|
-
const registration = {
|
|
553
|
-
sessions: cache,
|
|
554
|
-
toolNames: allTools.map((t) => t.name),
|
|
555
|
-
resourceNames: enabledResources.map((r) => r.name),
|
|
556
|
-
stateful
|
|
557
|
-
};
|
|
558
|
-
if (!fastify.hasDecorator("mcp")) {
|
|
559
|
-
const registrations = /* @__PURE__ */ new Map();
|
|
560
|
-
registrations.set(prefix, registration);
|
|
561
|
-
const first = () => registrations.values().next().value;
|
|
562
|
-
const decorator = {
|
|
563
|
-
registrations,
|
|
564
|
-
get(p) {
|
|
565
|
-
return registrations.get(p);
|
|
566
|
-
},
|
|
567
|
-
get sessions() {
|
|
568
|
-
return first()?.sessions ?? null;
|
|
569
|
-
},
|
|
570
|
-
get toolNames() {
|
|
571
|
-
return first()?.toolNames ?? [];
|
|
572
|
-
},
|
|
573
|
-
get resourceNames() {
|
|
574
|
-
return first()?.resourceNames ?? [];
|
|
575
|
-
},
|
|
576
|
-
get stateful() {
|
|
577
|
-
return first()?.stateful ?? false;
|
|
578
|
-
}
|
|
579
|
-
};
|
|
580
|
-
fastify.decorate("mcp", decorator);
|
|
581
|
-
} else {
|
|
582
|
-
const existing = fastify.mcp;
|
|
583
|
-
if (existing) {
|
|
584
|
-
if (existing.registrations.has(prefix)) throw new Error(`mcpPlugin: prefix "${prefix}" is already registered`);
|
|
585
|
-
existing.registrations.set(prefix, registration);
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
};
|
|
589
|
-
function registerStatelessRoutes(fastify, prefix, options, createServer, Transport, authCache) {
|
|
590
|
-
fastify.post(prefix, async (request, reply) => {
|
|
591
|
-
const authResult = await resolveMcpAuth(request.headers, options.auth ?? false, authCache);
|
|
592
|
-
if (!authResult && options.auth) {
|
|
593
|
-
fastify.log.warn({
|
|
594
|
-
msg: "mcpPlugin: auth failed",
|
|
595
|
-
status: 401
|
|
596
|
-
});
|
|
597
|
-
return reply.code(401).send({
|
|
598
|
-
jsonrpc: "2.0",
|
|
599
|
-
error: {
|
|
600
|
-
code: -32e3,
|
|
601
|
-
message: "Unauthorized"
|
|
602
|
-
}
|
|
603
|
-
});
|
|
604
|
-
}
|
|
605
|
-
const authRef = { current: authResult };
|
|
606
|
-
const transport = new Transport({ sessionIdGenerator: void 0 });
|
|
607
|
-
await (await createServer(authRef)).connect(transport);
|
|
608
|
-
await transport.handleRequest(request.raw, reply.raw, request.body);
|
|
609
|
-
});
|
|
610
|
-
fastify.get(prefix, async (_request, reply) => {
|
|
611
|
-
reply.code(405).send({
|
|
612
|
-
jsonrpc: "2.0",
|
|
613
|
-
error: {
|
|
614
|
-
code: -32e3,
|
|
615
|
-
message: "SSE not available in stateless mode. Use stateful: true for server-initiated messages."
|
|
616
|
-
}
|
|
617
|
-
});
|
|
618
|
-
});
|
|
619
|
-
fastify.delete(prefix, async (_request, reply) => {
|
|
620
|
-
reply.code(200).send();
|
|
621
|
-
});
|
|
622
|
-
}
|
|
623
|
-
function registerStatefulRoutes(fastify, prefix, options, cache, createServer, Transport) {
|
|
624
|
-
/** Check if the requesting principal owns the session */
|
|
625
|
-
function isSessionOwner(entry, authResult) {
|
|
626
|
-
if (!options.auth || !entry.auth || !authResult) return true;
|
|
627
|
-
const prev = entry.auth;
|
|
628
|
-
return prev.userId === authResult.userId && prev.organizationId === authResult.organizationId && prev.clientId === authResult.clientId;
|
|
629
|
-
}
|
|
630
|
-
fastify.post(prefix, async (request, reply) => {
|
|
631
|
-
const authResult = await resolveMcpAuth(request.headers, options.auth ?? false);
|
|
632
|
-
if (!authResult && options.auth) {
|
|
633
|
-
fastify.log.warn({
|
|
634
|
-
msg: "mcpPlugin: auth failed",
|
|
635
|
-
status: 401
|
|
636
|
-
});
|
|
637
|
-
return reply.code(401).send({
|
|
638
|
-
jsonrpc: "2.0",
|
|
639
|
-
error: {
|
|
640
|
-
code: -32e3,
|
|
641
|
-
message: "Unauthorized"
|
|
642
|
-
}
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
const sessionId = request.headers["mcp-session-id"];
|
|
646
|
-
if (sessionId) {
|
|
647
|
-
const entry = cache.get(sessionId);
|
|
648
|
-
if (entry) {
|
|
649
|
-
if (!isSessionOwner(entry, authResult)) return reply.code(403).send({
|
|
650
|
-
jsonrpc: "2.0",
|
|
651
|
-
error: {
|
|
652
|
-
code: -32e3,
|
|
653
|
-
message: "Session ownership mismatch"
|
|
654
|
-
}
|
|
655
|
-
});
|
|
656
|
-
cache.touch(sessionId);
|
|
657
|
-
entry.auth = authResult;
|
|
658
|
-
entry.authRef.current = authResult;
|
|
659
|
-
await entry.transport.handleRequest(request.raw, reply.raw, request.body);
|
|
660
|
-
return;
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
const authRef = { current: authResult };
|
|
664
|
-
const transport = new Transport({
|
|
665
|
-
sessionIdGenerator: () => randomUUID(),
|
|
666
|
-
onsessioninitialized: (newSessionId) => {
|
|
667
|
-
cache.set(newSessionId, {
|
|
668
|
-
transport,
|
|
669
|
-
lastAccessed: Date.now(),
|
|
670
|
-
organizationId: authResult?.organizationId ?? "",
|
|
671
|
-
auth: authResult,
|
|
672
|
-
authRef
|
|
673
|
-
});
|
|
674
|
-
}
|
|
675
|
-
});
|
|
676
|
-
await (await createServer(authRef)).connect(transport);
|
|
677
|
-
await transport.handleRequest(request.raw, reply.raw, request.body);
|
|
678
|
-
});
|
|
679
|
-
fastify.get(prefix, async (request, reply) => {
|
|
680
|
-
const sessionId = request.headers["mcp-session-id"];
|
|
681
|
-
if (!sessionId) return reply.code(400).send({ error: "Missing Mcp-Session-Id header" });
|
|
682
|
-
const entry = cache.get(sessionId);
|
|
683
|
-
if (!entry) return reply.code(403).send({ error: "Unauthorized" });
|
|
684
|
-
if (options.auth) {
|
|
685
|
-
const authResult = await resolveMcpAuth(request.headers, options.auth);
|
|
686
|
-
if (!isSessionOwner(entry, authResult)) return reply.code(403).send({ error: "Unauthorized" });
|
|
687
|
-
entry.auth = authResult;
|
|
688
|
-
entry.authRef.current = authResult;
|
|
689
|
-
}
|
|
690
|
-
cache.touch(sessionId);
|
|
691
|
-
await entry.transport.handleRequest(request.raw, reply.raw);
|
|
692
|
-
});
|
|
693
|
-
fastify.delete(prefix, async (request, reply) => {
|
|
694
|
-
const sessionId = request.headers["mcp-session-id"];
|
|
695
|
-
if (!sessionId) return reply.code(400).send({ error: "Missing Mcp-Session-Id header" });
|
|
696
|
-
const entry = cache.get(sessionId);
|
|
697
|
-
if (!entry) return reply.code(204).send();
|
|
698
|
-
if (options.auth) {
|
|
699
|
-
const authResult = await resolveMcpAuth(request.headers, options.auth);
|
|
700
|
-
if (!isSessionOwner(entry, authResult)) return reply.code(403).send({ error: "Unauthorized" });
|
|
701
|
-
entry.auth = authResult;
|
|
702
|
-
entry.authRef.current = authResult;
|
|
703
|
-
}
|
|
704
|
-
cache.remove(sessionId);
|
|
705
|
-
reply.code(204).send();
|
|
706
|
-
});
|
|
707
|
-
}
|
|
708
|
-
const mcpPlugin = fp(mcpPluginImpl, {
|
|
709
|
-
name: "arc-mcp",
|
|
710
|
-
fastify: "5.x"
|
|
711
|
-
});
|
|
712
|
-
//#endregion
|
|
713
|
-
export { bridgeToMcp, buildMcpToolsFromBridges, createMcpServer, customGuard, definePrompt, defineTool, denied, fieldRulesToZod, getOrgId, getUserId, guard, hasOrg, isAuthenticated, isOrg, mcpPlugin, requireAuth, requireOrg, requireOrgId, requireRole, resourceToTools };
|
|
187
|
+
export { bridgeToMcp, buildMcpToolsFromBridges, buildRequestContext, createMcpServer, customGuard, defaultCrudDescription, definePrompt, defineTool, denied, fieldRulesToZod, filterResourcesForMcp, getOrgId, getUserId, guard, hasOrg, invokeController, isAuthenticated, isOrg, mcpHandlerAdapter, mcpPlugin, permissionDeniedResult, requireAuth, requireOrg, requireOrgId, requireRole, resolveCrudDescription, resourceToTools, toCallToolError, toCallToolResult, toCallToolSuccess };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { c as McpAuthResult, l as McpPluginOptions } from "../../types-BsJMEQ4D.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/integrations/mcp/testing.d.ts
|
|
4
4
|
interface TestMcpClientOptions {
|
|
5
5
|
/** MCP plugin options (resources, overrides, etc.) — same as mcpPlugin config */
|
|
6
|
-
pluginOptions?: Pick<McpPluginOptions, "resources" | "overrides" | "include" | "exclude" | "toolNamePrefix" | "extraTools" | "extraPrompts" | "instructions">;
|
|
6
|
+
pluginOptions?: Pick<McpPluginOptions, "resources" | "overrides" | "expose" | "include" | "exclude" | "toolNamePrefix" | "extraTools" | "extraPrompts" | "instructions">;
|
|
7
7
|
/** Auth identity for the test session */
|
|
8
8
|
auth?: McpAuthResult | null;
|
|
9
9
|
/** Server name (default: 'test-mcp') */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { m as createMcpServer, r as resourceToTools, t as filterResourcesForMcp } from "../../mcpPlugin-7vGV51ED.mjs";
|
|
2
2
|
//#region src/integrations/mcp/testing.ts
|
|
3
3
|
/**
|
|
4
4
|
* @classytic/arc/mcp/testing — MCP Test Utilities
|
|
@@ -52,15 +52,11 @@ async function createTestMcpClient(options = {}) {
|
|
|
52
52
|
const auth = options.auth ?? { userId: "test-user" };
|
|
53
53
|
const serverName = options.serverName ?? "test-mcp";
|
|
54
54
|
const overrides = pluginOpts.overrides ?? {};
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
const excludeSet = new Set(pluginOpts.exclude);
|
|
61
|
-
enabledResources = enabledResources.filter((r) => !excludeSet.has(r.name));
|
|
62
|
-
}
|
|
63
|
-
const tools = enabledResources.flatMap((r) => {
|
|
55
|
+
const tools = filterResourcesForMcp(pluginOpts.resources ?? [], {
|
|
56
|
+
expose: pluginOpts.expose,
|
|
57
|
+
include: pluginOpts.include,
|
|
58
|
+
exclude: pluginOpts.exclude
|
|
59
|
+
}).flatMap((r) => {
|
|
64
60
|
const resOverrides = overrides[r.name] ?? {};
|
|
65
61
|
return resourceToTools(r, {
|
|
66
62
|
...resOverrides,
|
|
@@ -50,7 +50,40 @@ interface WorkflowLike {
|
|
|
50
50
|
}; /** Repository — used by the list-runs endpoint to query workflow_runs. */
|
|
51
51
|
repository?: {
|
|
52
52
|
getAll(params: Record<string, unknown>, options?: Record<string, unknown>): Promise<unknown>;
|
|
53
|
+
/**
|
|
54
|
+
* Tenant-scoped lookup by id. Used by the DELETE handler for a
|
|
55
|
+
* defense-in-depth pre-flight: streamline 2.3.3's `wf.get(runId)` /
|
|
56
|
+
* `engine.get` does NOT accept tenant options, so a cross-tenant
|
|
57
|
+
* runId can leak data through the engine path. Going through the
|
|
58
|
+
* repository here means mongokit's tenant-filter plugin scopes the
|
|
59
|
+
* read — cross-tenant requests get a clean 404 and DELETEs only
|
|
60
|
+
* touch rows the caller actually owns.
|
|
61
|
+
*/
|
|
62
|
+
getById?(id: string, options?: Record<string, unknown>): Promise<WorkflowRunLike | null>;
|
|
63
|
+
/**
|
|
64
|
+
* Hard-delete a run by id. Routed through mongokit's inherited
|
|
65
|
+
* `Repository.delete()` so multi-tenant scope + audit/cache plugins
|
|
66
|
+
* fire. Wired into `DELETE /:workflowId/runs/:runId` — operator
|
|
67
|
+
* escape hatch for dead-lettered or stuck rows.
|
|
68
|
+
*/
|
|
69
|
+
delete?(id: string, options?: Record<string, unknown>): Promise<unknown>;
|
|
53
70
|
};
|
|
71
|
+
/**
|
|
72
|
+
* Streamline >= 2.3.2 — explicit deploy-time index sync (TTL on
|
|
73
|
+
* terminal runs + tenant compounds). When the host configured
|
|
74
|
+
* `createContainer({ retention })`, arc's app-level deploy hook
|
|
75
|
+
* should call `await container.syncRetentionIndexes()` after
|
|
76
|
+
* `mongoose.connect`. Optional so older streamline versions
|
|
77
|
+
* (and partial mocks) still satisfy the structural shape.
|
|
78
|
+
*/
|
|
79
|
+
syncRetentionIndexes?: () => Promise<void>;
|
|
80
|
+
/**
|
|
81
|
+
* Streamline >= 2.3.2 — stop background sweepers and release timers.
|
|
82
|
+
* Arc's `onClose` hook below calls this on every workflow's container
|
|
83
|
+
* during graceful shutdown so SIGTERM doesn't leave the stale-run
|
|
84
|
+
* sweeper running. Optional + idempotent.
|
|
85
|
+
*/
|
|
86
|
+
dispose?: () => void;
|
|
54
87
|
};
|
|
55
88
|
}
|
|
56
89
|
interface WorkflowRunLike {
|
|
@@ -67,7 +100,36 @@ interface WorkflowRunLike {
|
|
|
67
100
|
stepLogs?: unknown[];
|
|
68
101
|
createdAt?: Date;
|
|
69
102
|
updatedAt?: Date;
|
|
103
|
+
/**
|
|
104
|
+
* Streamline >= 2.3.3 — pinned definition version (semver) the run
|
|
105
|
+
* started under. Hosts surfacing a "stuck on old version" UI read this
|
|
106
|
+
* to decide whether to nudge a migration. Optional for back-compat
|
|
107
|
+
* with runs created before 2.3.3.
|
|
108
|
+
*/
|
|
109
|
+
definitionVersion?: string;
|
|
110
|
+
/**
|
|
111
|
+
* Streamline >= 2.3.3 — count of stale-recovery / sweeper transitions
|
|
112
|
+
* applied to this run. Sweeper dead-letters once this hits
|
|
113
|
+
* `RetentionOptions.maxStaleRecoveries`; UIs can highlight runs trending
|
|
114
|
+
* toward dead-letter.
|
|
115
|
+
*/
|
|
116
|
+
recoveryAttempts?: number;
|
|
70
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Streamline >= 2.3.3 dead-letter discriminator. The run.status stays
|
|
120
|
+
* `'failed'`; the discrimination is `error.code`:
|
|
121
|
+
* - `'stale_heartbeat'` — sweeper terminated; transient crash signal.
|
|
122
|
+
* - `'dead_lettered'` — exceeded `maxStaleRecoveries`; permanent.
|
|
123
|
+
* - `'VERSION_MISMATCH'` — engine deployed a step graph the run can't
|
|
124
|
+
* resume against; admin must rewind / migrate / cancel.
|
|
125
|
+
*
|
|
126
|
+
* Hosts switch on `error.code` for dashboards / alerting.
|
|
127
|
+
*/
|
|
128
|
+
declare const STREAMLINE_FAILURE_CODES: {
|
|
129
|
+
readonly STALE_HEARTBEAT: "stale_heartbeat";
|
|
130
|
+
readonly DEAD_LETTERED: "dead_lettered";
|
|
131
|
+
readonly VERSION_MISMATCH: "VERSION_MISMATCH";
|
|
132
|
+
};
|
|
71
133
|
interface StreamlinePluginOptions {
|
|
72
134
|
/** Array of workflows created with createWorkflow() */
|
|
73
135
|
workflows: WorkflowLike[];
|
|
@@ -176,7 +238,14 @@ declare const STREAMLINE_BUS_EVENTS: readonly ["step:started", "step:completed",
|
|
|
176
238
|
* the run is still active after them.
|
|
177
239
|
*/
|
|
178
240
|
declare const STREAMLINE_TERMINAL_EVENTS: readonly ["workflow:completed", "workflow:failed", "workflow:cancelled"];
|
|
179
|
-
/**
|
|
241
|
+
/**
|
|
242
|
+
* Pluggable streamline integration for Arc.
|
|
243
|
+
*
|
|
244
|
+
* Wrapped in `fastify-plugin` so Fastify treats `options.prefix` as a
|
|
245
|
+
* plain plugin option (NOT an encapsulation prefix). Without the wrapper,
|
|
246
|
+
* Fastify would prepend `options.prefix` to every route, then the plugin
|
|
247
|
+
* code would prepend it again — the duplicate-prefix bug.
|
|
248
|
+
*/
|
|
180
249
|
declare const streamlinePlugin: FastifyPluginAsync<StreamlinePluginOptions>;
|
|
181
250
|
//#endregion
|
|
182
|
-
export { STREAMLINE_BUS_EVENTS, STREAMLINE_TERMINAL_EVENTS, StreamlinePluginOptions, WorkflowLike, WorkflowRunLike, WorkflowStartOptions, streamlinePlugin };
|
|
251
|
+
export { STREAMLINE_BUS_EVENTS, STREAMLINE_FAILURE_CODES, STREAMLINE_TERMINAL_EVENTS, StreamlinePluginOptions, WorkflowLike, WorkflowRunLike, WorkflowStartOptions, streamlinePlugin };
|