@classytic/arc 2.18.5 → 2.20.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 -1
- package/bin/arc.js +37 -0
- package/dist/{BaseController-Dy6EQHjM.mjs → BaseController-D-MHZ27_.mjs} +4 -4
- package/dist/{ResourceRegistry-5fKZwhK_.mjs → ResourceRegistry-7RnrEWNP.mjs} +1 -1
- package/dist/{actionPermissions-h7DYDNMg.mjs → actionPermissions-C9RMiIpc.mjs} +2 -1
- package/dist/audit/index.mjs +1 -1
- package/dist/auth/index.d.mts +1 -1
- package/dist/auth/index.mjs +17 -6
- package/dist/{buildHandler-B0qvAguf.mjs → buildHandler-D_2ULWxZ.mjs} +3 -3
- package/dist/cache/index.mjs +2 -2
- package/dist/cli/commands/describe.d.mts +1 -1
- package/dist/cli/commands/describe.mjs +2 -2
- package/dist/cli/commands/docs.mjs +2 -2
- package/dist/cli/commands/generate.mjs +1 -1
- package/dist/cli/commands/init.mjs +1 -1
- package/dist/cli/commands/introspect.mjs +4 -13
- package/dist/cli/index.mjs +2 -2
- package/dist/{constants-gaEeMlz-.mjs → constants-DNre0wRg.mjs} +1 -1
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +4 -4
- package/dist/{core-ZnQBa5rh.mjs → core-CWhyGkiM.mjs} +136 -59
- package/dist/{createActionRouter-CagItpxI.mjs → createActionRouter-HlQso0jO.mjs} +8 -7
- package/dist/{createAggregationRouter-DVKroXhC.mjs → createAggregationRouter-5LzPzgw1.mjs} +5 -4
- package/dist/{createApp--703hQE1.mjs → createApp-D6uwzCKu.mjs} +210 -17
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +1 -1
- package/dist/encryption/index.d.mts +222 -0
- package/dist/encryption/index.mjs +270 -0
- package/dist/events/index.mjs +9 -6
- package/dist/factory/index.d.mts +3 -2
- package/dist/factory/index.mjs +2 -2
- package/dist/{fastify-augmentation-Cxb0ROIw.d.mts → fastify-augmentation-CCQJEDgh.d.mts} +12 -1
- package/dist/{generate-Brans3fW.mjs → generate-usRSdQmP.mjs} +4 -3
- package/dist/hooks/index.d.mts +1 -1
- package/dist/idempotency/index.mjs +1 -1
- package/dist/{index-CRKiiP5o.d.mts → index-CdFkIMEd.d.mts} +5 -0
- package/dist/{index-Dh0_RSTd.d.mts → index-DUa8WHz3.d.mts} +7 -7
- package/dist/{index-CHNQIhcb.d.mts → index-_--GyzcO.d.mts} +111 -2
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +6 -6
- package/dist/{init-Bqpt1Hxf.mjs → init-YH_nslQR.mjs} +87 -5
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/mcp/index.d.mts +2 -2
- package/dist/integrations/mcp/index.mjs +1 -1
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/streamline.d.mts +17 -5
- package/dist/integrations/streamline.mjs +9 -5
- package/dist/{loadResourcesFromEntry-BTlFEx05.mjs → loadResourcesFromEntry-OMCvIEE7.mjs} +1 -1
- package/dist/{mcpPlugin-D1wuRD2c.mjs → mcpPlugin-CuW6L_uD.mjs} +148 -49
- package/dist/middleware/index.d.mts +1 -1
- package/dist/middleware/index.mjs +1 -1
- package/dist/module-JYH9V0I8.d.mts +384 -0
- package/dist/{openapi-xVJibapr.mjs → openapi-Dq6RbEGK.mjs} +3 -3
- package/dist/permissions/index.d.mts +1 -1
- package/dist/permissions/index.mjs +1 -1
- package/dist/{permissions-BPB1xqGf.mjs → permissions-CgbOLJZG.mjs} +10 -2
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/plugins/index.d.mts +10 -5
- package/dist/plugins/index.mjs +3 -3
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/{pluralize-DccFV9R7.mjs → pluralize-BhrgwZb9.mjs} +3 -3
- package/dist/presets/filesUpload.d.mts +1 -1
- package/dist/presets/filesUpload.mjs +1 -1
- package/dist/presets/index.d.mts +16 -2
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +1 -1
- package/dist/presets/search.d.mts +1 -1
- package/dist/presets/search.mjs +3 -2
- package/dist/{presets-BtyFwTO4.mjs → presets-DWsYQdbV.mjs} +1 -1
- package/dist/{queryCachePlugin-emHb0CuX.mjs → queryCachePlugin-DzqLN904.mjs} +2 -2
- package/dist/{registerRoute-CPmdkZGu.mjs → registerRoute-DGSREwPw.mjs} +20 -2
- package/dist/registry/index.d.mts +1 -1
- package/dist/registry/index.mjs +2 -2
- package/dist/{registry-Bm9LjWKs.mjs → registry-vcOCtPij.mjs} +1 -1
- package/dist/{schemaIR-D1MJwqmp.mjs → schemaIR-ajetw3Fv.mjs} +1 -42
- package/dist/scim/index.mjs +2 -2
- package/dist/sync/changelog.d.mts +2 -0
- package/dist/sync/changelog.mjs +2 -0
- package/dist/testing/index.d.mts +2 -2
- package/dist/testing/index.mjs +2 -2
- package/dist/types/index.d.mts +2 -2
- package/dist/{types-CmkknLzI.d.mts → types-BHMKlMBU.d.mts} +53 -195
- package/dist/{types-D60PFwfG.d.mts → types-C9b2HR5W.d.mts} +1 -1
- package/dist/utils/index.d.mts +1 -1
- package/dist/utils/index.mjs +1 -1
- package/dist/{utils-C6QQ9-hd.mjs → utils-ChjYQpjn.mjs} +1400 -1400
- package/dist/{validate-CRcaWzp2.mjs → validate-C2mGgSXg.mjs} +3 -3
- package/package.json +22 -8
- package/skills/arc/SKILL.md +10 -0
- package/skills/arc/references/mcp.md +2 -0
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { p as isArcError, t as ArcError } from "./errors-DkO8Nk6-.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { c as getOrgId, g as getUserId } from "./types-bHblZ9YN.mjs";
|
|
3
|
+
import { t as BaseController } from "./BaseController-D-MHZ27_.mjs";
|
|
4
|
+
import { N as normalizePermissionResult } from "./permissions-CgbOLJZG.mjs";
|
|
5
5
|
import { t as executePipeline } from "./pipe-BOLmGIxa.mjs";
|
|
6
|
-
import { t as
|
|
7
|
-
import {
|
|
8
|
-
import { n as
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
6
|
+
import { i as resolvePipelineSteps, t as resolveActionPermission } from "./actionPermissions-C9RMiIpc.mjs";
|
|
7
|
+
import { n as validateAggregations } from "./validate-C2mGgSXg.mjs";
|
|
8
|
+
import { n as executeAggregation } from "./buildHandler-D_2ULWxZ.mjs";
|
|
9
|
+
import { t as pluralize } from "./pluralize-BhrgwZb9.mjs";
|
|
10
|
+
import { r as shouldRejectAdditionalProperties, t as normalizeSchemaIR } from "./schemaIR-ajetw3Fv.mjs";
|
|
11
11
|
import { createHash, randomUUID } from "node:crypto";
|
|
12
|
+
import { isHttpError, toErrorContract } from "@classytic/repo-core/errors";
|
|
12
13
|
import fp from "fastify-plugin";
|
|
13
14
|
import { z } from "zod";
|
|
14
15
|
//#region src/integrations/mcp/createMcpServer.ts
|
|
@@ -264,7 +265,7 @@ function buildRequestContext(input, auth, operation, policyFilters, scopeOverrid
|
|
|
264
265
|
* them explicitly here rather than asking the parser at runtime — Arc must
|
|
265
266
|
* not import MongoKit internals just to know what an operator looks like.
|
|
266
267
|
*/
|
|
267
|
-
const OPERATOR_SUFFIXES = new Set([
|
|
268
|
+
const OPERATOR_SUFFIXES = /* @__PURE__ */ new Set([
|
|
268
269
|
"eq",
|
|
269
270
|
"ne",
|
|
270
271
|
"gt",
|
|
@@ -671,12 +672,20 @@ function resolveCrudDescription(override, meta) {
|
|
|
671
672
|
* // shape = { name: z.string(), price: z.number() }
|
|
672
673
|
* ```
|
|
673
674
|
*/
|
|
675
|
+
/**
|
|
676
|
+
* Projection (`select`) field — reused by the list shape AND the single-entity
|
|
677
|
+
* `get` shape. The controller honors `select` on `getById` identically to list
|
|
678
|
+
* (both flow through `QueryResolver.resolve`), so exposing it on `get` gives an
|
|
679
|
+
* MCP agent the same field-projection it already has on list.
|
|
680
|
+
*/
|
|
681
|
+
const PROJECTION_FIELD = z.string().optional().describe("Comma-separated field list to project (e.g. 'name,price'). Prefix with '-' to exclude (e.g. '-description').");
|
|
674
682
|
const PAGINATION_SHAPE = {
|
|
675
|
-
page: z.number().int().min(1).optional().describe("Page number (1-based)"),
|
|
683
|
+
page: z.number().int().min(1).optional().describe("Page number (1-based). Ignored when `cursor` is set."),
|
|
676
684
|
limit: z.number().int().min(1).max(100).optional().describe("Items per page (max 100)"),
|
|
685
|
+
cursor: z.string().optional().describe("Opaque keyset cursor from a previous page's `next`. Pass it to fetch the following page; when set, `page` is ignored (cursor/keyset pagination — stable under concurrent inserts)."),
|
|
677
686
|
sort: z.string().optional().describe("Sort field, prefix with - for descending"),
|
|
678
687
|
search: z.string().optional().describe("Full-text search query"),
|
|
679
|
-
select:
|
|
688
|
+
select: PROJECTION_FIELD,
|
|
680
689
|
populate: z.string().optional().describe("Comma-separated relation paths to hydrate (e.g. 'supplier,category'). Follows Mongoose populate syntax when the adapter is MongoKit.")
|
|
681
690
|
};
|
|
682
691
|
/**
|
|
@@ -688,7 +697,7 @@ function fieldRulesToZod(fieldRules, options = {}) {
|
|
|
688
697
|
const { mode = "create", hiddenFields = [], readonlyFields = [], extraHideFields = [] } = options;
|
|
689
698
|
if (mode === "list") return buildListShape(fieldRules, options);
|
|
690
699
|
if (!fieldRules) return {};
|
|
691
|
-
const allHidden = new Set([...hiddenFields, ...extraHideFields]);
|
|
700
|
+
const allHidden = /* @__PURE__ */ new Set([...hiddenFields, ...extraHideFields]);
|
|
692
701
|
const allReadonly = new Set(readonlyFields);
|
|
693
702
|
const shape = {};
|
|
694
703
|
for (const [name, rule] of Object.entries(fieldRules)) {
|
|
@@ -738,7 +747,7 @@ function typeToZod(type) {
|
|
|
738
747
|
}
|
|
739
748
|
}
|
|
740
749
|
/** Operators that apply to numeric/date fields */
|
|
741
|
-
const COMPARISON_OPS = new Set([
|
|
750
|
+
const COMPARISON_OPS = /* @__PURE__ */ new Set([
|
|
742
751
|
"gt",
|
|
743
752
|
"gte",
|
|
744
753
|
"lt",
|
|
@@ -761,7 +770,7 @@ function opDescription(op, fieldName) {
|
|
|
761
770
|
/** Build list/query shape with filterable fields, operators, and pagination */
|
|
762
771
|
function buildListShape(fieldRules, options) {
|
|
763
772
|
const { filterableFields = [], hiddenFields = [], extraHideFields = [], allowedOperators } = options;
|
|
764
|
-
const allHidden = new Set([...hiddenFields, ...extraHideFields]);
|
|
773
|
+
const allHidden = /* @__PURE__ */ new Set([...hiddenFields, ...extraHideFields]);
|
|
765
774
|
const shape = { ...PAGINATION_SHAPE };
|
|
766
775
|
if (!fieldRules) return shape;
|
|
767
776
|
for (const name of filterableFields) {
|
|
@@ -917,6 +926,61 @@ async function forwardResponse(reply, response) {
|
|
|
917
926
|
reply.send(await response.text());
|
|
918
927
|
}
|
|
919
928
|
//#endregion
|
|
929
|
+
//#region src/core/schemaIRZod.ts
|
|
930
|
+
/**
|
|
931
|
+
* Schema IR → Zod adapter — the MCP-facing half of [schemaIR.ts](./schemaIR.ts).
|
|
932
|
+
*
|
|
933
|
+
* Lives in its OWN module (not schemaIR.ts) because it is the only part that
|
|
934
|
+
* needs zod at runtime. schemaIR.ts is statically imported by
|
|
935
|
+
* `createActionRouter`, which every actions-bearing resource loads at boot —
|
|
936
|
+
* a top-level `import { z } from "zod"` there made zod a de-facto REQUIRED
|
|
937
|
+
* dependency for any host using `actions:`, despite zod being an optional
|
|
938
|
+
* peer (boot crash when absent, even with plain-JSON-Schema action bodies
|
|
939
|
+
* and no MCP). Only MCP code imports this module, and the MCP integration
|
|
940
|
+
* already hard-requires zod (the SDK does), so the top-level import is
|
|
941
|
+
* correct HERE and a bug THERE.
|
|
942
|
+
*/
|
|
943
|
+
/**
|
|
944
|
+
* Emit a flat Zod shape from the IR. The MCP SDK wraps the returned record
|
|
945
|
+
* in `z.object()` internally, so we return the bare shape (same contract
|
|
946
|
+
* as `ToolDefinition.inputSchema`).
|
|
947
|
+
*
|
|
948
|
+
* `additionalProperties: false` is honored at the MCP handler layer rather
|
|
949
|
+
* than baked into the Zod shape — the SDK's input validation happens before
|
|
950
|
+
* the handler runs, and flat shapes can't express `.strict()` mode.
|
|
951
|
+
* `shouldRejectAdditionalProperties(ir)` (schemaIR.ts) returns the flag so
|
|
952
|
+
* callers can gate their handler on it.
|
|
953
|
+
*/
|
|
954
|
+
function schemaIRToZodShape(ir) {
|
|
955
|
+
const requiredSet = new Set(ir.required);
|
|
956
|
+
const result = {};
|
|
957
|
+
for (const [name, prop] of Object.entries(ir.properties)) {
|
|
958
|
+
const desc = typeof prop.description === "string" && prop.description.length > 0 ? prop.description : name;
|
|
959
|
+
const base = jsonSchemaPropToZod(prop);
|
|
960
|
+
result[name] = requiredSet.has(name) ? base.describe(desc) : base.optional().describe(desc);
|
|
961
|
+
}
|
|
962
|
+
return result;
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Convert a single JSON Schema property to a Zod type. Understands enum,
|
|
966
|
+
* numeric/integer/boolean/array/object, and falls back to string for
|
|
967
|
+
* unrecognized types (matches MCP's "strings for opaque fields" convention).
|
|
968
|
+
*
|
|
969
|
+
* Internal — use `schemaIRToZodShape` which wires this up with required/optional
|
|
970
|
+
* + description handling.
|
|
971
|
+
*/
|
|
972
|
+
function jsonSchemaPropToZod(schema) {
|
|
973
|
+
if (Array.isArray(schema.enum) && schema.enum.length > 0) return z.enum(schema.enum);
|
|
974
|
+
switch (typeof schema.type === "string" ? schema.type : "string") {
|
|
975
|
+
case "number":
|
|
976
|
+
case "integer": return z.number();
|
|
977
|
+
case "boolean": return z.boolean();
|
|
978
|
+
case "array": return z.array(z.unknown());
|
|
979
|
+
case "object": return z.record(z.string(), z.unknown());
|
|
980
|
+
default: return z.string();
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
//#endregion
|
|
920
984
|
//#region src/integrations/mcp/action-tools.ts
|
|
921
985
|
/**
|
|
922
986
|
* Convert an action's `schema` field into a Zod shape for MCP input.
|
|
@@ -942,7 +1006,7 @@ function convertActionSchemaToZod(raw) {
|
|
|
942
1006
|
*/
|
|
943
1007
|
function createActionToolHandler(actionName, handler, permissions, resourceName, _resourcePermissions, rawSchema, idLess = false) {
|
|
944
1008
|
const ir = rawSchema ? normalizeSchemaIR(rawSchema) : void 0;
|
|
945
|
-
const allowedKeys = (ir ? shouldRejectAdditionalProperties(ir) : false) && ir ? new Set([...idLess ? [] : ["id"], ...Object.keys(ir.properties)]) : void 0;
|
|
1009
|
+
const allowedKeys = (ir ? shouldRejectAdditionalProperties(ir) : false) && ir ? /* @__PURE__ */ new Set([...idLess ? [] : ["id"], ...Object.keys(ir.properties)]) : void 0;
|
|
946
1010
|
return async (input, ctx) => {
|
|
947
1011
|
const session = ctx.session;
|
|
948
1012
|
if (allowedKeys) {
|
|
@@ -1239,7 +1303,10 @@ function buildInputSchema(op, fieldRules, opts) {
|
|
|
1239
1303
|
mode: "list",
|
|
1240
1304
|
...opts
|
|
1241
1305
|
});
|
|
1242
|
-
case "get": return
|
|
1306
|
+
case "get": return {
|
|
1307
|
+
...getIdShape(),
|
|
1308
|
+
select: PROJECTION_FIELD
|
|
1309
|
+
};
|
|
1243
1310
|
case "create":
|
|
1244
1311
|
if (!fieldRules && opts.adapterBodies?.createBody) {
|
|
1245
1312
|
const shape = jsonSchemaToZodShape(opts.adapterBodies.createBody, "create");
|
|
@@ -1424,14 +1491,17 @@ function createCustomRouteHandler(route, controller, hasId, options) {
|
|
|
1424
1491
|
const reqCtx = buildRequestContext(input, session, operationKindForRoute(route.method, hasId), permResult?.filters, permResult?.scope);
|
|
1425
1492
|
if (typeof route.handler === "function") {
|
|
1426
1493
|
const fn = route.handler;
|
|
1427
|
-
if (pipelineSteps.length > 0)
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
return
|
|
1434
|
-
|
|
1494
|
+
if (pipelineSteps.length > 0) {
|
|
1495
|
+
const pipeCtx = {
|
|
1496
|
+
...reqCtx,
|
|
1497
|
+
resource: resourceName,
|
|
1498
|
+
operation: operationName
|
|
1499
|
+
};
|
|
1500
|
+
return toCallToolResult(await executePipeline(pipelineSteps, pipeCtx, async (ctx) => {
|
|
1501
|
+
const raw = await fn(ctx);
|
|
1502
|
+
return raw !== null && typeof raw === "object" && "data" in raw ? raw : { data: raw };
|
|
1503
|
+
}, operationName));
|
|
1504
|
+
}
|
|
1435
1505
|
const out = await fn(reqCtx);
|
|
1436
1506
|
return toCallToolResult(out !== null && typeof out === "object" && "data" in out ? out : { data: out });
|
|
1437
1507
|
}
|
|
@@ -1452,13 +1522,7 @@ function createCustomRouteHandler(route, controller, hasId, options) {
|
|
|
1452
1522
|
};
|
|
1453
1523
|
return toCallToolResult(await method(reqCtx));
|
|
1454
1524
|
} catch (err) {
|
|
1455
|
-
return
|
|
1456
|
-
content: [{
|
|
1457
|
-
type: "text",
|
|
1458
|
-
text: `Error: ${err instanceof Error ? err.message : String(err)}`
|
|
1459
|
-
}],
|
|
1460
|
-
isError: true
|
|
1461
|
-
};
|
|
1525
|
+
return toCallToolError(err instanceof Error ? err : new Error(String(err)));
|
|
1462
1526
|
}
|
|
1463
1527
|
};
|
|
1464
1528
|
}
|
|
@@ -1472,13 +1536,7 @@ function createMcpHandlerPassthrough(mcpHandler) {
|
|
|
1472
1536
|
try {
|
|
1473
1537
|
return await mcpHandler(input);
|
|
1474
1538
|
} catch (err) {
|
|
1475
|
-
return
|
|
1476
|
-
content: [{
|
|
1477
|
-
type: "text",
|
|
1478
|
-
text: `Error: ${err instanceof Error ? err.message : String(err)}`
|
|
1479
|
-
}],
|
|
1480
|
-
isError: true
|
|
1481
|
-
};
|
|
1539
|
+
return toCallToolError(err instanceof Error ? err : new Error(String(err)));
|
|
1482
1540
|
}
|
|
1483
1541
|
};
|
|
1484
1542
|
}
|
|
@@ -1655,14 +1713,18 @@ function resourceToTools(resource, config = {}) {
|
|
|
1655
1713
|
if (resource.aggregations && Object.keys(resource.aggregations).length > 0) {
|
|
1656
1714
|
const repoForAgg = resource.controller?.repository;
|
|
1657
1715
|
const buildOptionsFromSession = (session) => {
|
|
1658
|
-
const
|
|
1716
|
+
const auth = session ?? null;
|
|
1717
|
+
const scope = buildScope(auth);
|
|
1659
1718
|
const out = {};
|
|
1660
|
-
const orgId =
|
|
1719
|
+
const orgId = getOrgId(scope);
|
|
1661
1720
|
if (orgId) out.organizationId = orgId;
|
|
1662
|
-
const userId =
|
|
1721
|
+
const userId = getUserId(scope);
|
|
1663
1722
|
if (userId) out.userId = userId;
|
|
1664
|
-
if (
|
|
1665
|
-
|
|
1723
|
+
if (auth?.userId) out.user = {
|
|
1724
|
+
id: auth.userId,
|
|
1725
|
+
roles: auth.roles ?? [],
|
|
1726
|
+
orgRoles: auth.orgRoles ?? []
|
|
1727
|
+
};
|
|
1666
1728
|
return out;
|
|
1667
1729
|
};
|
|
1668
1730
|
tools.push(...buildAggregationTools({
|
|
@@ -1713,13 +1775,29 @@ function registerSchemaResources(server, resources, overrides) {
|
|
|
1713
1775
|
name: r.name,
|
|
1714
1776
|
displayName: r.displayName,
|
|
1715
1777
|
operations: getOps(r, overrides?.[r.name]?.operations),
|
|
1716
|
-
fields: r.schemaOptions?.fieldRules
|
|
1778
|
+
fields: visibleFieldRules(r.schemaOptions?.fieldRules),
|
|
1717
1779
|
filterableFields: schemaOpts?.filterableFields ?? [],
|
|
1718
1780
|
presets: r._appliedPresets ?? []
|
|
1719
1781
|
}, null, 2)
|
|
1720
1782
|
}] }));
|
|
1721
1783
|
}
|
|
1722
1784
|
}
|
|
1785
|
+
/**
|
|
1786
|
+
* Strip `hidden: true` / `systemManaged: true` entries before serializing —
|
|
1787
|
+
* the schema-discovery resource must not re-expose exactly what
|
|
1788
|
+
* `fieldRulesToZod` strips from tool input schemas (tenant keys, internal
|
|
1789
|
+
* scoring fields, ...). Same visibility rule, one discovery surface.
|
|
1790
|
+
*/
|
|
1791
|
+
function visibleFieldRules(fieldRules) {
|
|
1792
|
+
if (!fieldRules) return {};
|
|
1793
|
+
const out = {};
|
|
1794
|
+
for (const [name, rule] of Object.entries(fieldRules)) {
|
|
1795
|
+
const flags = rule ?? {};
|
|
1796
|
+
if (flags.hidden === true || flags.systemManaged === true) continue;
|
|
1797
|
+
out[name] = rule;
|
|
1798
|
+
}
|
|
1799
|
+
return out;
|
|
1800
|
+
}
|
|
1723
1801
|
function getOps(r, override) {
|
|
1724
1802
|
let ops = [
|
|
1725
1803
|
"list",
|
|
@@ -1901,10 +1979,16 @@ const mcpPluginImpl = async (fastify, options) => {
|
|
|
1901
1979
|
const more = allTools.length > 5 ? ` (+${allTools.length - 5} more)` : "";
|
|
1902
1980
|
fastify.log.warn(`[arc/mcp] MCP server registered at "${prefix}" with auth DISABLED. Anyone who can reach this URL has full access to ${allTools.length} tools across ${enabledResources.length} resources: ${toolList}${more}. Pass \`auth: betterAuthInstance\` or \`auth: async (headers) => ...\` to gate access. Safe only for stdio transports, local development, or explicitly-public read-only APIs.`);
|
|
1903
1981
|
}
|
|
1904
|
-
fastify.get(`${prefix}/health`, async (
|
|
1905
|
-
|
|
1982
|
+
fastify.get(`${prefix}/health`, async (request, reply) => {
|
|
1983
|
+
const liveness = {
|
|
1906
1984
|
status: "ok",
|
|
1907
|
-
mode: stateful ? "stateful" : "stateless"
|
|
1985
|
+
mode: stateful ? "stateful" : "stateless"
|
|
1986
|
+
};
|
|
1987
|
+
if (options.auth) {
|
|
1988
|
+
if (!await resolveMcpAuth(request.headers, options.auth)) return reply.send(liveness);
|
|
1989
|
+
}
|
|
1990
|
+
reply.send({
|
|
1991
|
+
...liveness,
|
|
1908
1992
|
tools: allTools.length,
|
|
1909
1993
|
resources: enabledResources.length,
|
|
1910
1994
|
toolNames: allTools.map((t) => t.name),
|
|
@@ -1991,9 +2075,22 @@ function registerStatelessRoutes(fastify, prefix, options, createServer, Transpo
|
|
|
1991
2075
|
});
|
|
1992
2076
|
}
|
|
1993
2077
|
function registerStatefulRoutes(fastify, prefix, options, cache, createServer, Transport) {
|
|
1994
|
-
/**
|
|
2078
|
+
/**
|
|
2079
|
+
* Check if the requesting principal owns the session. FAIL-CLOSED:
|
|
2080
|
+
*
|
|
2081
|
+
* - `authResult === null` (failed/absent credentials) NEVER owns a
|
|
2082
|
+
* session — pre-2.20 this returned `true`, so an unauthenticated
|
|
2083
|
+
* caller with a leaked `Mcp-Session-Id` could attach to the victim's
|
|
2084
|
+
* SSE stream, terminate the session, and (via the auth-snapshot
|
|
2085
|
+
* refresh) wipe `entry.auth` to null — after which ANY principal
|
|
2086
|
+
* passed ownership and took over the session.
|
|
2087
|
+
* - `entry.auth === null` under configured auth is equally a deny:
|
|
2088
|
+
* POST 401s before creating a session, so an auth-mode session always
|
|
2089
|
+
* has an owner snapshot; a missing one means corrupted state.
|
|
2090
|
+
*/
|
|
1995
2091
|
function isSessionOwner(entry, authResult) {
|
|
1996
|
-
if (!options.auth
|
|
2092
|
+
if (!options.auth) return true;
|
|
2093
|
+
if (!authResult || !entry.auth) return false;
|
|
1997
2094
|
const prev = entry.auth;
|
|
1998
2095
|
return prev.userId === authResult.userId && prev.organizationId === authResult.organizationId && prev.clientId === authResult.clientId;
|
|
1999
2096
|
}
|
|
@@ -2053,6 +2150,7 @@ function registerStatefulRoutes(fastify, prefix, options, cache, createServer, T
|
|
|
2053
2150
|
if (!entry) return reply.code(403).send({ error: "Unauthorized" });
|
|
2054
2151
|
if (options.auth) {
|
|
2055
2152
|
const authResult = await resolveMcpAuth(request.headers, options.auth);
|
|
2153
|
+
if (!authResult) return reply.code(401).send({ error: "Unauthorized" });
|
|
2056
2154
|
if (!isSessionOwner(entry, authResult)) return reply.code(403).send({ error: "Unauthorized" });
|
|
2057
2155
|
entry.auth = authResult;
|
|
2058
2156
|
entry.authRef.current = authResult;
|
|
@@ -2067,6 +2165,7 @@ function registerStatefulRoutes(fastify, prefix, options, cache, createServer, T
|
|
|
2067
2165
|
if (!entry) return reply.code(204).send();
|
|
2068
2166
|
if (options.auth) {
|
|
2069
2167
|
const authResult = await resolveMcpAuth(request.headers, options.auth);
|
|
2168
|
+
if (!authResult) return reply.code(401).send({ error: "Unauthorized" });
|
|
2070
2169
|
if (!isSessionOwner(entry, authResult)) return reply.code(403).send({ error: "Unauthorized" });
|
|
2071
2170
|
entry.auth = authResult;
|
|
2072
2171
|
entry.authRef.current = authResult;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { I as MiddlewareHandler, L as RequestWithExtras, Q as MiddlewareConfig } from "../index-
|
|
1
|
+
import { I as MiddlewareHandler, L as RequestWithExtras, Q as MiddlewareConfig } from "../index-_--GyzcO.mjs";
|
|
2
2
|
import { RouteHandlerMethod } from "fastify";
|
|
3
3
|
|
|
4
4
|
//#region src/middleware/middleware.d.ts
|