@classytic/arc 2.21.0 → 2.23.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/dist/{BaseController-BnLYgE9A.mjs → BaseController-BQ8KNvWC.mjs} +3 -2
- package/dist/{ResourceRegistry-7RnrEWNP.mjs → ResourceRegistry-y9Hnypus.mjs} +1 -0
- package/dist/{actionPermissions-ze70xXJi.mjs → actionPermissions-lszoJle1.mjs} +1 -1
- package/dist/applyPermissionResult-Tw4iXtlf.mjs +264 -0
- package/dist/audit/index.d.mts +2 -2
- package/dist/auth/index.d.mts +30 -2
- package/dist/auth/index.mjs +6 -3
- package/dist/cli/commands/describe.d.mts +1 -1
- package/dist/cli/commands/describe.mjs +1 -1
- package/dist/cli/commands/docs.mjs +2 -2
- package/dist/cli/commands/init.mjs +1 -1
- package/dist/cli/commands/introspect.mjs +1 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +3 -3
- package/dist/{core-DZ6Xe1jR.mjs → core-DQ6rDTyH.mjs} +126 -68
- package/dist/{createActionRouter-BWMDW-dL.mjs → createActionRouter-BuOv-m4Y.mjs} +10 -6
- package/dist/{createAggregationRouter-l5mNdYrZ.mjs → createAggregationRouter-CgJv_BU_.mjs} +3 -3
- package/dist/{createApp-Bi61XxqU.mjs → createApp-CjOeWJ9_.mjs} +252 -57
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +1 -1
- package/dist/encryption/index.mjs +9 -5
- package/dist/{errorHandler-Dxf6P_kV.mjs → errorHandler-Bq2OeEQL.mjs} +34 -10
- package/dist/{errorHandler-B0e-bq-O.d.mts → errorHandler-C6yv4ZTW.d.mts} +13 -0
- package/dist/events/index.mjs +5 -2
- package/dist/factory/index.d.mts +46 -9
- package/dist/factory/index.mjs +64 -13
- package/dist/{fastify-augmentation-DafigGQp.d.mts → fastify-augmentation-g_f9omSc.d.mts} +2 -2
- package/dist/fields-D3lhgzhD.d.mts +121 -0
- package/dist/health-ScyjIVo5.mjs +202 -0
- package/dist/hooks/index.d.mts +1 -1
- package/dist/idempotency/index.d.mts +7 -0
- package/dist/idempotency/index.mjs +7 -17
- package/dist/{index-Dtrj6XZk.d.mts → index-BBsvQt7U.d.mts} +1310 -1226
- package/dist/{types-DpxoNX5v.d.mts → index-Ba10fdKp.d.mts} +196 -66
- package/dist/{index-B5N-NaHs.d.mts → index-CC5ruDGh.d.mts} +3 -3
- package/dist/{index-B5PoNbfu.d.mts → index-wMoWBLkn.d.mts} +68 -2
- package/dist/index.d.mts +6 -5
- package/dist/index.mjs +6 -5
- package/dist/{init-0YhHkXGk.mjs → init-CjO4p_q1.mjs} +123 -4
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/mcp/index.d.mts +67 -4
- 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/webhooks.d.mts +131 -7
- package/dist/integrations/webhooks.mjs +169 -10
- package/dist/interface-n5hpKg1d.mjs +16 -0
- package/dist/jsonBody-BlzAoGCe.mjs +43 -0
- package/dist/{loadResourcesFromEntry-OMCvIEE7.mjs → loadResourcesFromEntry-kvGT6EcO.mjs} +1 -1
- package/dist/logger/index.d.mts +26 -1
- package/dist/logger/index.mjs +39 -1
- package/dist/{mcpPlugin-B9XjjNVG.mjs → mcpPlugin-BtzIL_kM.mjs} +312 -39
- package/dist/middleware/index.d.mts +1 -1
- package/dist/{module-WeuU5jfd.d.mts → module-B4u10iOJ.d.mts} +26 -1
- package/dist/{openapi-Kyod_di6.mjs → openapi-jSDx-VDv.mjs} +3 -3
- package/dist/permissions/index.d.mts +4 -3
- package/dist/permissions/index.mjs +3 -2
- package/dist/{permissions-BJMuKMl7.mjs → permissions-tbxFHxjZ.mjs} +77 -264
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/plugins/index.d.mts +24 -7
- package/dist/plugins/index.mjs +14 -282
- package/dist/plugins/response-cache.mjs +1 -0
- package/dist/plugins/tracing-entry.mjs +7 -4
- package/dist/presets/filesUpload.d.mts +2 -2
- package/dist/presets/filesUpload.mjs +1 -1
- 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/search.d.mts +2 -2
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-gw0ciIUD.mjs → presets-BOe4Ugkl.mjs} +1 -1
- package/dist/rateLimitKey-BoWRH3e7.mjs +14 -0
- package/dist/realtime-I8OrfnMR.mjs +375 -0
- package/dist/{registerRoute-oGAl0oA8.mjs → registerRoute-CWO6mdYe.mjs} +38 -18
- package/dist/registry/index.d.mts +2 -2
- package/dist/registry/index.mjs +1 -1
- package/dist/requestId-5BhTVzBv.mjs +113 -0
- package/dist/schemaConverter-DJvodiHj.mjs +171 -0
- package/dist/{schemaIR-ajetw3Fv.mjs → schemaIR-Ca7ySNPx.mjs} +2 -2
- package/dist/scim/index.mjs +6 -7
- package/dist/scope/index.mjs +2 -76
- package/dist/scope-LONQ3mfZ.mjs +37 -0
- package/dist/simpleEqualityMatcher-Cg05HPsn.mjs +68 -0
- package/dist/{sse-CfUjwjDk.mjs → sse-0u4P4-OI.mjs} +7 -30
- package/dist/sseStream-BQ7Qaynp.mjs +57 -0
- package/dist/testing/index.d.mts +83 -3
- package/dist/testing/index.mjs +70 -3
- package/dist/types/index.d.mts +2 -2
- package/dist/{types-DRj-Ut4n.d.mts → types-5OuqeTDX.d.mts} +37 -2
- package/dist/{fields-BGqrAa_u.d.mts → types-Csxga7d3.d.mts} +1 -121
- package/dist/usage/index.d.mts +142 -0
- package/dist/usage/index.mjs +0 -0
- package/dist/utils/index.d.mts +101 -19
- package/dist/utils/index.mjs +5 -3
- package/dist/{utils-CO8a3B_1.mjs → utils-CZyeRXTh.mjs} +80 -82
- package/dist/{versioning-DQ4mCvg0.d.mts → versioning-D6PvCwpq.d.mts} +43 -9
- package/llms.txt +39 -0
- package/package.json +513 -447
- package/skills/arc/SKILL.md +14 -8
- package/skills/arc/references/api-reference.md +34 -0
- package/skills/arc/references/auth.md +21 -4
- package/skills/arc/references/mcp.md +1 -1
- package/skills/arc/references/production.md +734 -654
- package/skills/arc-code-review/SKILL.md +1 -1
- package/skills/arc-code-review/references/anti-patterns.md +71 -18
- package/skills/arc-code-review/references/arc-cheatsheet.md +15 -2
- package/skills/arc-code-review/references/migration-recipes.md +7 -7
- package/skills/arc-code-review/references/mongokit-migration.md +4 -3
- package/skills/arc-module-publishing/SKILL.md +2 -1
- package/skills/arc-module-publishing/references/authoring-modules.md +55 -0
- package/skills/arc-module-publishing/references/package-manifest.md +4 -2
- package/dist/preloadResources-CHw3_oA_.mjs +0 -45
- package/dist/schemaConverter-U0MnO9vd.mjs +0 -110
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
|
|
1
2
|
import * as fs from "node:fs/promises";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import { dirname, join } from "node:path";
|
|
@@ -76,6 +77,60 @@ export function createAdapter${ts ? "<TDoc = unknown>" : ""}(
|
|
|
76
77
|
`;
|
|
77
78
|
}
|
|
78
79
|
//#endregion
|
|
80
|
+
//#region src/cli/commands/init/templates/agents.ts
|
|
81
|
+
function agentsGuideTemplate(config) {
|
|
82
|
+
return `# CLAUDE.md — agent guide for this app
|
|
83
|
+
|
|
84
|
+
> This file doubles as AGENTS.md — copy or symlink if your tool reads that name.
|
|
85
|
+
|
|
86
|
+
**This is an \`@classytic/arc\` application.** Resources are DECLARED, not hand-rolled:
|
|
87
|
+
one \`defineResource()\` = REST + validation + auth + permissions + events + caching +
|
|
88
|
+
OpenAPI + MCP tools. Data layer: ${config.adapter === "mongokit" ? "@classytic/mongokit (Mongoose)" : "a custom DataAdapter kit"}.
|
|
89
|
+
|
|
90
|
+
## The one rule
|
|
91
|
+
|
|
92
|
+
**Hand-rolling is a bug.** Before writing ANY route, validation, pagination, auth check,
|
|
93
|
+
cron, counter, or audit code by hand, prove arc lacks it — check the table below, then
|
|
94
|
+
\`node_modules/@classytic/arc/skills/arc/SKILL.md\` (full framework guide + references).
|
|
95
|
+
|
|
96
|
+
## About to write X? Arc already has Y
|
|
97
|
+
|
|
98
|
+
| If you're about to write… | Use instead |
|
|
99
|
+
|---|---|
|
|
100
|
+
| \`fastify.get/post/...\` CRUD routes for an entity | \`defineResource({ name, adapter, permissions })\` |
|
|
101
|
+
| \`if (req.user.role !== 'admin') return 403\` | \`permissions: { update: requireRoles(['admin']) }\` |
|
|
102
|
+
| Reading \`req.user._id\` / org id directly | \`getUserId(scope)\` / \`requireOrgId(scope)\` from \`@classytic/arc/scope\` |
|
|
103
|
+
| Hand-written \`schema: { body }\` / JSON Schema | \`schemaOptions.fieldRules\` (or \`customSchemas\` for wire≠model) |
|
|
104
|
+
| toJSON transforms hiding \`password\`/internals | \`fieldRules: { password: { hidden: true } }\` |
|
|
105
|
+
| Manual \`req.query\` filter/sort/pagination parsing | the resource's \`queryParser\` (allow-listed fields) |
|
|
106
|
+
| Soft delete / slug lookup / tree / bulk / file upload | \`presets: ['softDelete', 'slugLookup', ...]\` |
|
|
107
|
+
| \`setInterval\` sweeps + hand-rolled leader lock | \`schedulesPlugin\` (\`@classytic/arc/plugins\`) |
|
|
108
|
+
| A background/queue worker entrypoint | \`createWorker(sameOptions)\` (\`@classytic/arc/factory\`) |
|
|
109
|
+
| Usage counters / quotas / plan rate limits | \`usagePlugin\` + \`requireQuota\` + \`rateLimit.plan\` |
|
|
110
|
+
| Audit log rows / "who changed what" endpoints | \`audit: true\` + \`history: true\` on the resource |
|
|
111
|
+
| Manual idempotency keys / dedupe | \`idempotencyPlugin\` |
|
|
112
|
+
| Event emitters / webhooks plumbing | \`eventPlugin\` + resource \`events:\` (+ outbox for guarantees) |
|
|
113
|
+
| Custom MCP/AI tool handlers | generated automatically from every resource |
|
|
114
|
+
|
|
115
|
+
## Load-bearing conventions
|
|
116
|
+
|
|
117
|
+
- \`request.user\` is \`undefined\` on public routes — always guard.
|
|
118
|
+
- Never read \`scope.organizationId\` directly — use \`@classytic/arc/scope\` accessors.
|
|
119
|
+
- The DB connection belongs in \`createApp({ beforeBoot })\` — not before \`createApp\`.
|
|
120
|
+
- Custom endpoints: \`routes:\`/\`actions:\` on the resource (they inherit auth/permissions/
|
|
121
|
+
OpenAPI/MCP) — a bare \`fastify.get()\` gets none of that.
|
|
122
|
+
|
|
123
|
+
## Commands
|
|
124
|
+
|
|
125
|
+
\`\`\`bash
|
|
126
|
+
npm run dev # watch mode
|
|
127
|
+
npm test # vitest
|
|
128
|
+
npx arc generate resource <name> # scaffold a resource (model/repo/resource files)
|
|
129
|
+
npx arc describe # list registered resources + routes
|
|
130
|
+
\`\`\`
|
|
131
|
+
`;
|
|
132
|
+
}
|
|
133
|
+
//#endregion
|
|
79
134
|
//#region src/cli/commands/init/templates/app.ts
|
|
80
135
|
function indexTemplate(config) {
|
|
81
136
|
const ts = config.typescript;
|
|
@@ -442,8 +497,8 @@ function betterAuthSetupTemplate(config) {
|
|
|
442
497
|
import { mongodbAdapter } from '@better-auth/mongo-adapter';` : "";
|
|
443
498
|
const stubsImport = useStubs ? `import { registerBetterAuthStubs } from '@classytic/mongokit/better-auth';` : "";
|
|
444
499
|
const pluginImports = [
|
|
445
|
-
useOrg ? `import { organization } from 'better-auth/plugins';` : "",
|
|
446
|
-
useBearer ? `import { bearer } from 'better-auth/plugins';` : "",
|
|
500
|
+
useOrg ? `import { organization } from 'better-auth/plugins/organization';` : "",
|
|
501
|
+
useBearer ? `import { bearer } from 'better-auth/plugins/bearer';` : "",
|
|
447
502
|
useApiKey ? `import { apiKey } from '@better-auth/api-key';` : ""
|
|
448
503
|
].filter(Boolean).join("\n");
|
|
449
504
|
const dbAdapter = useMongo ? config.typescript ? `database: mongodbAdapter(mongoose.connection.getClient().db() as unknown as never),` : `database: mongodbAdapter(mongoose.connection.getClient().db()),` : `// Configure your database adapter here
|
|
@@ -985,6 +1040,11 @@ ${config.adapter === "mongokit" ? ` await mongoose.connection.collection('use
|
|
|
985
1040
|
* `installDependencies` (runs the package manager's `install` against
|
|
986
1041
|
* the declared ranges). One source — no drift.
|
|
987
1042
|
*/
|
|
1043
|
+
var dependency_plan_exports = /* @__PURE__ */ __exportAll({
|
|
1044
|
+
SCAFFOLD_DEP_VERSIONS: () => SCAFFOLD_DEP_VERSIONS,
|
|
1045
|
+
primeLatestScaffoldVersions: () => primeLatestScaffoldVersions,
|
|
1046
|
+
resolveScaffoldDependencies: () => resolveScaffoldDependencies
|
|
1047
|
+
});
|
|
988
1048
|
const SCAFFOLD_DEP_VERSIONS = {
|
|
989
1049
|
core: {
|
|
990
1050
|
"@classytic/arc": "^2.20.0",
|
|
@@ -1014,10 +1074,10 @@ const SCAFFOLD_DEP_VERSIONS = {
|
|
|
1014
1074
|
},
|
|
1015
1075
|
devCommon: {
|
|
1016
1076
|
"@biomejs/biome": "^2.4.15",
|
|
1017
|
-
"mongodb-memory-server": "^11.1.0",
|
|
1018
1077
|
"pino-pretty": "^13.1.3",
|
|
1019
1078
|
vitest: "^4.1.7"
|
|
1020
1079
|
},
|
|
1080
|
+
devMongokit: { "mongodb-memory-server": "^11.1.0" },
|
|
1021
1081
|
devTypescript: {
|
|
1022
1082
|
"@types/node": "^22.10.0",
|
|
1023
1083
|
tsx: "^4.22.3",
|
|
@@ -1069,6 +1129,51 @@ function floorLessThan(a, b) {
|
|
|
1069
1129
|
* floor>` when the static pin's floor is BELOW the declared peer floor
|
|
1070
1130
|
* (never lowered — static pins may legitimately be ahead of the floor).
|
|
1071
1131
|
*/
|
|
1132
|
+
const latestCache = /* @__PURE__ */ new Map();
|
|
1133
|
+
function allScaffoldPackageNames() {
|
|
1134
|
+
const names = /* @__PURE__ */ new Set();
|
|
1135
|
+
for (const group of Object.values(SCAFFOLD_DEP_VERSIONS)) for (const name of Object.keys(group)) names.add(name);
|
|
1136
|
+
return [...names];
|
|
1137
|
+
}
|
|
1138
|
+
async function primeLatestScaffoldVersions(options) {
|
|
1139
|
+
const timeoutMs = options?.timeoutMs ?? 3500;
|
|
1140
|
+
const registry = (options?.registry ?? process.env.npm_config_registry ?? "https://registry.npmjs.org").replace(/\/$/, "");
|
|
1141
|
+
const names = allScaffoldPackageNames();
|
|
1142
|
+
const results = await Promise.allSettled(names.map(async (name) => {
|
|
1143
|
+
const res = await fetch(`${registry}/${name}/latest`, {
|
|
1144
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
1145
|
+
headers: { accept: "application/json" }
|
|
1146
|
+
});
|
|
1147
|
+
if (!res.ok) throw new Error(String(res.status));
|
|
1148
|
+
const body = await res.json();
|
|
1149
|
+
if (typeof body.version === "string" && /^\d+\.\d+\.\d+$/.test(body.version)) latestCache.set(name, body.version);
|
|
1150
|
+
}));
|
|
1151
|
+
return {
|
|
1152
|
+
resolved: latestCache.size,
|
|
1153
|
+
total: results.length
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
function parseBareVersion(v) {
|
|
1157
|
+
const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(v);
|
|
1158
|
+
return m ? [
|
|
1159
|
+
Number(m[1]),
|
|
1160
|
+
Number(m[2]),
|
|
1161
|
+
Number(m[3])
|
|
1162
|
+
] : void 0;
|
|
1163
|
+
}
|
|
1164
|
+
function applyLatestOverlay(dependencies) {
|
|
1165
|
+
if (latestCache.size === 0) return;
|
|
1166
|
+
for (const [name, currentRange] of Object.entries(dependencies)) {
|
|
1167
|
+
const latest = latestCache.get(name);
|
|
1168
|
+
if (!latest || latest.includes("-")) continue;
|
|
1169
|
+
const latestV = parseBareVersion(latest);
|
|
1170
|
+
const currentFloor = parseFloor(currentRange);
|
|
1171
|
+
if (!latestV || !currentFloor) continue;
|
|
1172
|
+
if (!floorLessThan(currentFloor, latestV)) continue;
|
|
1173
|
+
if (latestV[0] !== currentFloor[0] && !name.startsWith("@classytic/")) continue;
|
|
1174
|
+
dependencies[name] = `^${latest}`;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1072
1177
|
function overlayLiveArcVersions(dependencies) {
|
|
1073
1178
|
const own = readOwnPackageJson();
|
|
1074
1179
|
if (!own) return;
|
|
@@ -1100,8 +1205,13 @@ function resolveScaffoldDependencies(config) {
|
|
|
1100
1205
|
Object.assign(dependencies, SCAFFOLD_DEP_VERSIONS.authJwt);
|
|
1101
1206
|
if (config.typescript) Object.assign(devDependencies, SCAFFOLD_DEP_VERSIONS.typesJwt);
|
|
1102
1207
|
}
|
|
1103
|
-
if (config.adapter === "mongokit")
|
|
1208
|
+
if (config.adapter === "mongokit") {
|
|
1209
|
+
Object.assign(dependencies, SCAFFOLD_DEP_VERSIONS.adapterMongokit);
|
|
1210
|
+
Object.assign(devDependencies, SCAFFOLD_DEP_VERSIONS.devMongokit);
|
|
1211
|
+
}
|
|
1104
1212
|
if (config.typescript) Object.assign(devDependencies, SCAFFOLD_DEP_VERSIONS.devTypescript);
|
|
1213
|
+
applyLatestOverlay(dependencies);
|
|
1214
|
+
applyLatestOverlay(devDependencies);
|
|
1105
1215
|
overlayLiveArcVersions(dependencies);
|
|
1106
1216
|
return {
|
|
1107
1217
|
dependencies: sortByKey(dependencies),
|
|
@@ -3030,6 +3140,7 @@ async function createProjectStructure(projectPath, config) {
|
|
|
3030
3140
|
files["docker-compose.yml"] = dockerComposeTemplate(config);
|
|
3031
3141
|
}
|
|
3032
3142
|
if (config.edge) files["wrangler.toml"] = wranglerTemplate(config);
|
|
3143
|
+
files["CLAUDE.md"] = agentsGuideTemplate(config);
|
|
3033
3144
|
files[".arcrc"] = `${JSON.stringify({
|
|
3034
3145
|
adapter: config.adapter,
|
|
3035
3146
|
auth: config.auth,
|
|
@@ -3329,6 +3440,14 @@ async function init(options = {}) {
|
|
|
3329
3440
|
}
|
|
3330
3441
|
const packageManager = detectPackageManager();
|
|
3331
3442
|
console.log(`Using package manager: ${packageManager}\n`);
|
|
3443
|
+
if (!process.env.VITEST && process.env.ARC_INIT_OFFLINE !== "1") {
|
|
3444
|
+
const { primeLatestScaffoldVersions } = await Promise.resolve().then(() => dependency_plan_exports);
|
|
3445
|
+
const { resolved, total } = await primeLatestScaffoldVersions().catch(() => ({
|
|
3446
|
+
resolved: 0,
|
|
3447
|
+
total: 0
|
|
3448
|
+
}));
|
|
3449
|
+
console.log(resolved > 0 ? `Resolved ${resolved}/${total} dependency versions from npm (latest stable)\n` : "Registry unreachable — using pinned fallback versions\n");
|
|
3450
|
+
}
|
|
3332
3451
|
await createProjectStructure(projectPath, config);
|
|
3333
3452
|
if (!options.skipInstall) {
|
|
3334
3453
|
console.log("\n📥 Installing dependencies...\n");
|
|
@@ -4,7 +4,7 @@ const eventGatewayPluginImpl = async (fastify, opts = {}) => {
|
|
|
4
4
|
const { auth = true, orgScoped = false, roomPolicy, maxMessageBytes, maxSubscriptionsPerClient, authenticate } = opts;
|
|
5
5
|
if (auth && !authenticate && !fastify.hasDecorator("authenticate")) throw new Error("[arc-event-gateway] auth is true but fastify.authenticate is not registered. Register an auth plugin first, provide a custom authenticate function, or set auth: false.");
|
|
6
6
|
if (opts.sse !== false) {
|
|
7
|
-
const { default: ssePlugin } = await import("../sse-
|
|
7
|
+
const { default: ssePlugin } = await import("../sse-0u4P4-OI.mjs").then((n) => n.r);
|
|
8
8
|
await fastify.register(ssePlugin, {
|
|
9
9
|
path: opts.sse?.path ?? "/events/stream",
|
|
10
10
|
requireAuth: auth,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { c as WebSocketClient, l as WebSocketMessage, u as WebSocketPluginOptions } from "../websocket-uuWqM6UY.mjs";
|
|
2
2
|
import { EventGatewayOptions } from "./event-gateway.mjs";
|
|
3
3
|
import { JobDefinition, JobDispatchOptions, JobDispatcher, JobMeta, JobsPluginOptions, QueueStats } from "./jobs.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { _ as ToolDefinition, c as McpAuthResult, d as McpResourceConfig, f as PromptDefinition, g as ToolContext, h as ToolAnnotations, n as CallToolResult, o as CrudOperation, r as CreateMcpServerConfig, t as BetterAuthHandler, u as McpPluginOptions } from "../types-5OuqeTDX.mjs";
|
|
5
5
|
import { StreamlinePluginOptions, WorkflowLike, WorkflowRunLike } from "./streamline.mjs";
|
|
6
6
|
import { WebhookDeliveryRecord, WebhookManager, WebhookPluginOptions, WebhookStore, WebhookSubscription } from "./webhooks.mjs";
|
|
7
7
|
export type { BetterAuthHandler, CallToolResult, CreateMcpServerConfig, CrudOperation, EventGatewayOptions, JobDefinition, JobDispatchOptions, JobDispatcher, JobMeta, JobsPluginOptions, McpAuthResult, McpPluginOptions, McpResourceConfig, PromptDefinition, QueueStats, StreamlinePluginOptions, ToolAnnotations, ToolContext, ToolDefinition, WebSocketClient, WebSocketMessage, WebSocketPluginOptions, WebhookDeliveryRecord, WebhookManager, WebhookPluginOptions, WebhookStore, WebhookSubscription, WorkflowLike, WorkflowRunLike };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { V as ResourceDefinition,
|
|
1
|
+
import { Rt as IControllerResponse, V as ResourceDefinition, Vt as ServerAccessor, zt as IRequestContext } from "../../index-BBsvQt7U.mjs";
|
|
2
2
|
import { i as RequestScope } from "../../types-DH0GupI7.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { t as PermissionCheck } from "../../types-Csxga7d3.mjs";
|
|
4
|
+
import { n as FieldPermissionMap } from "../../fields-D3lhgzhD.mjs";
|
|
5
|
+
import { _ as ToolDefinition, a as CrudDescriptionOverride, c as McpAuthResult, d as McpResourceConfig, f as PromptDefinition, g as ToolContext, h as ToolAnnotations, i as CrudDescriptionMeta, l as McpExecutionWiring, m as SessionEntry, n as CallToolResult, o as CrudOperation, p as PromptResult, r as CreateMcpServerConfig, s as McpAuthResolver, t as BetterAuthHandler, u as McpPluginOptions } from "../../types-5OuqeTDX.mjs";
|
|
5
6
|
import { ErrorContract } from "@classytic/repo-core/errors";
|
|
6
7
|
import { FastifyPluginAsync } from "fastify";
|
|
7
8
|
import { z } from "zod";
|
|
@@ -299,6 +300,34 @@ declare function customGuard(check: (ctx: ToolContext) => boolean | Promise<bool
|
|
|
299
300
|
//#endregion
|
|
300
301
|
//#region src/integrations/mcp/buildRequestContext.d.ts
|
|
301
302
|
type McpOperation = "list" | "get" | "create" | "update" | "delete" | "action";
|
|
303
|
+
/**
|
|
304
|
+
* Optional context extras — the pieces of `IRequestContext` that HTTP's
|
|
305
|
+
* `createRequestContext` assembles from Fastify decorators and the MCP
|
|
306
|
+
* synthetic path historically DROPPED. Without them, controller code that
|
|
307
|
+
* reads `metadata.arc.hooks` (the whole hook system — including the
|
|
308
|
+
* arcCorePlugin after-hook that publishes `<resource>.<op>d` events) or
|
|
309
|
+
* `ctx.server.events` silently no-ops on MCP calls: agents could mutate
|
|
310
|
+
* records without hooks firing, events publishing, realtime feeds updating,
|
|
311
|
+
* or webhooks dispatching. Thread these through `invokeController` /
|
|
312
|
+
* `resourceToTools({ wiring })` to restore full HTTP ↔ MCP parity.
|
|
313
|
+
*/
|
|
314
|
+
interface McpContextExtras {
|
|
315
|
+
/**
|
|
316
|
+
* Resource wiring — becomes `metadata.arc`, the same frozen shape
|
|
317
|
+
* `buildArcDecorator` stamps on HTTP requests (`ArcRouteMeta`).
|
|
318
|
+
*/
|
|
319
|
+
arc?: {
|
|
320
|
+
readonly resourceName: string;
|
|
321
|
+
readonly schemaOptions?: unknown;
|
|
322
|
+
readonly permissions?: unknown;
|
|
323
|
+
readonly hooks?: unknown;
|
|
324
|
+
readonly events?: unknown;
|
|
325
|
+
readonly fields?: unknown;
|
|
326
|
+
readonly idField?: string;
|
|
327
|
+
};
|
|
328
|
+
/** Server accessor — `ctx.server.events/audit/log`, mirroring HTTP. */
|
|
329
|
+
server?: ServerAccessor;
|
|
330
|
+
}
|
|
302
331
|
/**
|
|
303
332
|
* Build an IRequestContext from MCP tool input and session auth.
|
|
304
333
|
*
|
|
@@ -317,7 +346,7 @@ type McpOperation = "list" | "get" | "create" | "update" | "delete" | "action";
|
|
|
317
346
|
* the session-derived scope is `public` (i.e. MCP called with `auth: false`).
|
|
318
347
|
* An authenticated session scope is never overwritten.
|
|
319
348
|
*/
|
|
320
|
-
declare function buildRequestContext(input: Record<string, unknown>, auth: McpAuthResult | null, operation: McpOperation, policyFilters?: Record<string, unknown>, scopeOverride?: RequestScope): IRequestContext;
|
|
349
|
+
declare function buildRequestContext(input: Record<string, unknown>, auth: McpAuthResult | null, operation: McpOperation, policyFilters?: Record<string, unknown>, scopeOverride?: RequestScope, extras?: McpContextExtras): IRequestContext;
|
|
321
350
|
//#endregion
|
|
322
351
|
//#region src/integrations/mcp/invokeController.d.ts
|
|
323
352
|
/**
|
|
@@ -349,6 +378,33 @@ interface InvokeControllerOptions {
|
|
|
349
378
|
* action handler reached through a `string` route handler).
|
|
350
379
|
*/
|
|
351
380
|
readonly methodName?: string;
|
|
381
|
+
/**
|
|
382
|
+
* Resource field-permission map (`ResourceConfig.fields`). When present,
|
|
383
|
+
* field-level READ permissions are applied to the response payload before
|
|
384
|
+
* serialization — the same masking the HTTP wire and realtime frames get.
|
|
385
|
+
* Omit only for tools whose output carries no resource documents.
|
|
386
|
+
*/
|
|
387
|
+
readonly fields?: FieldPermissionMap;
|
|
388
|
+
/**
|
|
389
|
+
* App-level execution wiring (hooks system, event bus, audit, logger,
|
|
390
|
+
* idempotency store). When present, the synthetic request context carries
|
|
391
|
+
* `metadata.arc` + `server` exactly like HTTP's `createRequestContext` —
|
|
392
|
+
* so resource hooks run, CRUD events publish, and `ctx.server.*` works.
|
|
393
|
+
* `mcpPlugin` supplies this automatically; omit for dispatch-only calls.
|
|
394
|
+
*/
|
|
395
|
+
readonly wiring?: McpExecutionWiring;
|
|
396
|
+
/** Resource `schemaOptions` — rides on `metadata.arc` like HTTP. */
|
|
397
|
+
readonly schemaOptions?: unknown;
|
|
398
|
+
/** Resource `idField` — rides on `metadata.arc` for `getEntityIdField()`. */
|
|
399
|
+
readonly idField?: string;
|
|
400
|
+
/**
|
|
401
|
+
* Caller-supplied idempotency key (mutations only). Requires
|
|
402
|
+
* `wiring.idempotencyStore`; the executor then runs the same
|
|
403
|
+
* check → lock → execute → record → replay protocol the HTTP
|
|
404
|
+
* idempotency plugin runs, keyed by tool + input + caller identity.
|
|
405
|
+
* A concurrent call with the same key gets a 409 `arc.idempotency_conflict`.
|
|
406
|
+
*/
|
|
407
|
+
readonly idempotencyKey?: string;
|
|
352
408
|
}
|
|
353
409
|
/**
|
|
354
410
|
* Invoke a controller method through the MCP synthetic-context path.
|
|
@@ -549,6 +605,13 @@ interface ResourceToToolsConfig extends McpResourceConfig {
|
|
|
549
605
|
toolNamePrefix?: string;
|
|
550
606
|
/** Per-operation tool name overrides: `{ get: 'get_job_by_id' }` */
|
|
551
607
|
names?: Partial<Record<CrudOperation, string>>;
|
|
608
|
+
/**
|
|
609
|
+
* App-level execution wiring (hooks, events, audit, log, idempotency
|
|
610
|
+
* store). `mcpPlugin` supplies this automatically from its Fastify
|
|
611
|
+
* instance; standalone callers pass their own or omit it for
|
|
612
|
+
* dispatch-only tools (no hooks/events/idempotency).
|
|
613
|
+
*/
|
|
614
|
+
wiring?: McpExecutionWiring;
|
|
552
615
|
}
|
|
553
616
|
/**
|
|
554
617
|
* Convert a ResourceDefinition into MCP ToolDefinitions.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as defaultCrudDescription, c as mcpHandlerAdapter, d as toCallToolResult, f as toCallToolSuccess, h as resolveToolCollisions, 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-
|
|
1
|
+
import { a as defaultCrudDescription, c as mcpHandlerAdapter, d as toCallToolResult, f as toCallToolSuccess, h as resolveToolCollisions, 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-BtzIL_kM.mjs";
|
|
2
2
|
//#region src/integrations/mcp/defineTool.ts
|
|
3
3
|
/**
|
|
4
4
|
* Define a type-safe MCP tool.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as McpAuthResult,
|
|
1
|
+
import { c as McpAuthResult, u as McpPluginOptions } from "../../types-5OuqeTDX.mjs";
|
|
2
2
|
//#region src/integrations/mcp/testing.d.ts
|
|
3
3
|
interface TestMcpClientOptions {
|
|
4
4
|
/** MCP plugin options (resources, overrides, etc.) — same as mcpPlugin config */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { m as createMcpServer, r as resourceToTools, t as filterResourcesForMcp } from "../../mcpPlugin-
|
|
1
|
+
import { m as createMcpServer, r as resourceToTools, t as filterResourcesForMcp } from "../../mcpPlugin-BtzIL_kM.mjs";
|
|
2
2
|
//#region src/integrations/mcp/testing.ts
|
|
3
3
|
/**
|
|
4
4
|
* @classytic/arc/mcp/testing — MCP Test Utilities
|
|
@@ -36,13 +36,51 @@ interface WebhookPluginOptions {
|
|
|
36
36
|
timeout?: number;
|
|
37
37
|
/** Max delivery log entries kept in memory (default: 1000) */
|
|
38
38
|
maxLogEntries?: number;
|
|
39
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Max concurrent deliveries per event (default: 5). Set to 1 for
|
|
41
|
+
* sequential. Must be >= 1 — the plugin throws at registration otherwise
|
|
42
|
+
* (a zero would make the delivery loop spin forever without progress).
|
|
43
|
+
*/
|
|
40
44
|
concurrency?: number;
|
|
45
|
+
/**
|
|
46
|
+
* URL policy hook — the SSRF seam for tenant-registered URLs. Throw to
|
|
47
|
+
* reject. Pass {@link defaultWebhookUrlPolicy} to require HTTPS and block
|
|
48
|
+
* localhost / link-local / private-network / cloud-metadata destinations,
|
|
49
|
+
* or supply your own (allowlists, per-tenant rules).
|
|
50
|
+
*
|
|
51
|
+
* Enforcement points when set:
|
|
52
|
+
* - `register()` — new subscriptions are validated BEFORE saving.
|
|
53
|
+
* - Plugin init — every PERSISTED subscription is re-validated; failures
|
|
54
|
+
* are excluded from dispatch and logged (they stay in the store for
|
|
55
|
+
* the host to fix), so pre-policy rows and out-of-band store writes
|
|
56
|
+
* can't bypass the policy.
|
|
57
|
+
* - Delivery — `fetch` runs with `redirect: "manual"`, so an allowed
|
|
58
|
+
* public URL can't 302 into a blocked private/metadata target; a 3xx
|
|
59
|
+
* records as a failed delivery.
|
|
60
|
+
*
|
|
61
|
+
* Arc deliberately ships no DEFAULT policy — deployments differ (internal
|
|
62
|
+
* webhooks to private services are legitimate), and DNS-rebinding
|
|
63
|
+
* protection belongs at the actual outbound network boundary, not in the
|
|
64
|
+
* framework.
|
|
65
|
+
*/
|
|
66
|
+
validateUrl?: (url: URL, subscription: WebhookSubscription) => void | Promise<void>;
|
|
41
67
|
}
|
|
68
|
+
/** `WebhookSubscription` as returned by `list()` — secret withheld. */
|
|
69
|
+
type WebhookSubscriptionPublic = Omit<WebhookSubscription, "secret">;
|
|
42
70
|
interface WebhookManager {
|
|
43
71
|
register(sub: WebhookSubscription): Promise<void> | void;
|
|
44
72
|
unregister(id: string): Promise<void> | void;
|
|
45
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Enumerate subscriptions WITHOUT their secrets — safe to surface in
|
|
75
|
+
* admin UIs / API responses. Use {@link WebhookManager.getSecret} for
|
|
76
|
+
* the privileged retrieval path.
|
|
77
|
+
*/
|
|
78
|
+
list(): WebhookSubscriptionPublic[];
|
|
79
|
+
/**
|
|
80
|
+
* Privileged secret retrieval — separate from `list()` so enumerating
|
|
81
|
+
* subscriptions never leaks signing material into logs or UIs.
|
|
82
|
+
*/
|
|
83
|
+
getSecret(id: string): string | undefined;
|
|
46
84
|
deliveryLog(limit?: number): WebhookDeliveryRecord[];
|
|
47
85
|
}
|
|
48
86
|
declare module "fastify" {
|
|
@@ -53,9 +91,91 @@ declare module "fastify" {
|
|
|
53
91
|
/**
|
|
54
92
|
* Sign a payload with HMAC-SHA256 for outbound webhook delivery.
|
|
55
93
|
*
|
|
94
|
+
* @deprecated LEGACY (pre-2.23) format — signs the BODY ONLY, so a captured
|
|
95
|
+
* request can be replayed indefinitely and the event-id header is not
|
|
96
|
+
* cryptographically bound to the body. Still emitted on the legacy
|
|
97
|
+
* `x-webhook-signature` header for existing receivers; new receivers should
|
|
98
|
+
* verify the v1 contract (`x-arc-webhook-signature`) via {@link verifyWebhook}.
|
|
99
|
+
* Legacy emission is scheduled for removal in v3.
|
|
100
|
+
*
|
|
56
101
|
* @returns `sha256=<hex>` — the format written to `x-webhook-signature`
|
|
57
102
|
*/
|
|
58
103
|
declare function signPayload(payload: string, secret: string): string;
|
|
104
|
+
/**
|
|
105
|
+
* Signed-string layout for the v1 webhook contract:
|
|
106
|
+
*
|
|
107
|
+
* v1.<timestampMs>.<deliveryId>.<body>
|
|
108
|
+
*
|
|
109
|
+
* Binding the timestamp bounds replay to the verifier's tolerance window,
|
|
110
|
+
* and binding the delivery id lets receivers deduplicate redeliveries on an
|
|
111
|
+
* AUTHENTICATED value (the legacy `x-webhook-id` header was unsigned — an
|
|
112
|
+
* attacker replaying a captured request could change it and defeat dedup).
|
|
113
|
+
*
|
|
114
|
+
* Wire headers:
|
|
115
|
+
* - `x-arc-webhook-signature: v1=<hex hmac-sha256>`
|
|
116
|
+
* - `x-arc-webhook-timestamp: <unix epoch ms>`
|
|
117
|
+
* - `x-arc-webhook-delivery: <delivery id>` (arc sends the event's
|
|
118
|
+
* `meta.id` — STABLE across retries of the same event, so dedup on it
|
|
119
|
+
* collapses redeliveries)
|
|
120
|
+
*/
|
|
121
|
+
declare function signWebhook(body: string, secret: string, meta: {
|
|
122
|
+
timestamp: number;
|
|
123
|
+
deliveryId: string;
|
|
124
|
+
}): string;
|
|
125
|
+
interface VerifyWebhookInput {
|
|
126
|
+
/** Raw request body — the exact bytes the sender signed (`req.rawBody`). */
|
|
127
|
+
body: string | Buffer;
|
|
128
|
+
/** Shared secret. */
|
|
129
|
+
secret: string;
|
|
130
|
+
/** `x-arc-webhook-signature` header value (`v1=<hex>`). */
|
|
131
|
+
signature: string | undefined;
|
|
132
|
+
/** `x-arc-webhook-timestamp` header value (unix epoch ms). */
|
|
133
|
+
timestamp: string | number | undefined;
|
|
134
|
+
/** `x-arc-webhook-delivery` header value. */
|
|
135
|
+
deliveryId: string | undefined;
|
|
136
|
+
/**
|
|
137
|
+
* Max allowed clock skew between sender timestamp and now, in ms.
|
|
138
|
+
* @default 300_000 (5 minutes)
|
|
139
|
+
*/
|
|
140
|
+
toleranceMs?: number;
|
|
141
|
+
/** Injectable clock for tests. @default Date.now */
|
|
142
|
+
now?: () => number;
|
|
143
|
+
}
|
|
144
|
+
type VerifyWebhookResult = {
|
|
145
|
+
valid: true;
|
|
146
|
+
/** The AUTHENTICATED delivery id — dedup redeliveries on this. */
|
|
147
|
+
deliveryId: string;
|
|
148
|
+
/** Sender timestamp (unix epoch ms), authenticated. */
|
|
149
|
+
timestamp: number;
|
|
150
|
+
} | {
|
|
151
|
+
valid: false;
|
|
152
|
+
reason: "missing_headers" | "bad_timestamp" | "expired" | "bad_signature";
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* Verify an inbound v1 arc webhook: signature + timestamp tolerance, and
|
|
156
|
+
* return the authenticated delivery id for receiver-side deduplication.
|
|
157
|
+
*
|
|
158
|
+
* ⚠ Pass `req.rawBody` (route opted in via `config: { rawBody: true }`),
|
|
159
|
+
* never the parsed `req.body` — re-serialised JSON won't match the HMAC.
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```typescript
|
|
163
|
+
* fastify.post('/hooks/arc', { config: { rawBody: true } }, async (req, reply) => {
|
|
164
|
+
* const result = verifyWebhook({
|
|
165
|
+
* body: req.rawBody,
|
|
166
|
+
* secret: process.env.WEBHOOK_SECRET,
|
|
167
|
+
* signature: req.headers['x-arc-webhook-signature'] as string,
|
|
168
|
+
* timestamp: req.headers['x-arc-webhook-timestamp'] as string,
|
|
169
|
+
* deliveryId: req.headers['x-arc-webhook-delivery'] as string,
|
|
170
|
+
* });
|
|
171
|
+
* if (!result.valid) return reply.status(401).send({ error: result.reason });
|
|
172
|
+
* if (await seen(result.deliveryId)) return reply.send({ ok: true }); // dedup
|
|
173
|
+
* // ... handle, then mark result.deliveryId seen
|
|
174
|
+
* });
|
|
175
|
+
* ```
|
|
176
|
+
*/
|
|
177
|
+
declare function verifyWebhook(input: VerifyWebhookInput): VerifyWebhookResult;
|
|
178
|
+
declare function defaultWebhookUrlPolicy(url: URL): void;
|
|
59
179
|
/** Options for `verifySignature` — customize for non-Arc webhook senders */
|
|
60
180
|
interface VerifySignatureOptions {
|
|
61
181
|
/**
|
|
@@ -77,8 +197,9 @@ interface VerifySignatureOptions {
|
|
|
77
197
|
*
|
|
78
198
|
* ⚠ The `body` argument must be the exact bytes the sender signed. Fastify
|
|
79
199
|
* parses JSON bodies by default and the re-serialised object will NOT match
|
|
80
|
-
* the original signature. Register
|
|
81
|
-
*
|
|
200
|
+
* the original signature. Register `fastify-raw-body` (arc's createApp does
|
|
201
|
+
* this automatically with `global: false`) and opt the route in via
|
|
202
|
+
* `config: { rawBody: true }`, then pass `req.rawBody` here.
|
|
82
203
|
*
|
|
83
204
|
* @param body - Raw request body (string or Buffer — must be the exact bytes the sender signed)
|
|
84
205
|
* @param secret - Shared secret between sender and receiver
|
|
@@ -90,8 +211,11 @@ interface VerifySignatureOptions {
|
|
|
90
211
|
* ```typescript
|
|
91
212
|
* import { verifySignature } from '@classytic/arc/integrations/webhooks';
|
|
92
213
|
*
|
|
93
|
-
* // Arc-to-Arc (default headers + format)
|
|
94
|
-
* fastify
|
|
214
|
+
* // Arc-to-Arc (default headers + format). `config: { rawBody: true }` is
|
|
215
|
+
* // REQUIRED — arc registers fastify-raw-body with `global: false`, so
|
|
216
|
+
* // routes opt in per-route; without it `req.rawBody` is undefined and
|
|
217
|
+
* // verifySignature throws a TypeError.
|
|
218
|
+
* fastify.post('/webhooks/incoming', { config: { rawBody: true } }, async (req, reply) => {
|
|
95
219
|
* const sig = req.headers['x-webhook-signature'] as string;
|
|
96
220
|
* if (!verifySignature(req.rawBody, secret, sig)) {
|
|
97
221
|
* return reply.status(401).send({ error: 'Invalid signature' });
|
|
@@ -114,4 +238,4 @@ declare function verifySignature(body: string | Buffer, secret: string, signatur
|
|
|
114
238
|
declare const webhookPlugin: FastifyPluginAsync<WebhookPluginOptions>;
|
|
115
239
|
declare const _default: FastifyPluginAsync<WebhookPluginOptions>;
|
|
116
240
|
//#endregion
|
|
117
|
-
export { VerifySignatureOptions, WebhookDeliveryRecord, WebhookManager, WebhookPluginOptions, WebhookStore, WebhookSubscription, _default as default, signPayload, verifySignature, webhookPlugin };
|
|
241
|
+
export { VerifySignatureOptions, VerifyWebhookInput, VerifyWebhookResult, WebhookDeliveryRecord, WebhookManager, WebhookPluginOptions, WebhookStore, WebhookSubscription, WebhookSubscriptionPublic, _default as default, defaultWebhookUrlPolicy, signPayload, signWebhook, verifySignature, verifyWebhook, webhookPlugin };
|