@classytic/arc 2.22.0 → 2.25.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-C6FlRWxF.mjs → BaseController--M8EMzt0.mjs} +92 -22
- package/dist/QueryCache-DOMT_iq7.d.mts +72 -0
- package/dist/{ResourceRegistry-7RnrEWNP.mjs → ResourceRegistry-y9Hnypus.mjs} +1 -0
- package/dist/actionPermissions-CAphcmUG.mjs +24 -0
- package/dist/applyPermissionResult-Tw4iXtlf.mjs +264 -0
- package/dist/audit/index.d.mts +32 -3
- package/dist/audit/index.mjs +7 -4
- package/dist/auth/index.d.mts +2 -2
- package/dist/auth/index.mjs +264 -260
- package/dist/{buildHandler-D_2ULWxZ.mjs → buildHandler-Dtwd8fdz.mjs} +1 -1
- package/dist/cache/index.d.mts +41 -3
- package/dist/cache/index.mjs +33 -4
- 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/generate.mjs +1 -1
- package/dist/cli/commands/init.mjs +1 -1
- package/dist/cli/commands/introspect.mjs +1 -1
- package/dist/cli/index.mjs +2 -2
- package/dist/context/index.mjs +2 -64
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +4 -4
- package/dist/{core-b6fgFgnd.mjs → core-EUNu0u07.mjs} +298 -12
- package/dist/{createActionRouter-h1cEEaEX.mjs → createActionRouter-B6GvAuAX.mjs} +4 -3
- package/dist/{createAggregationRouter-B1C0kvTL.mjs → createAggregationRouter-wzwFa_dW.mjs} +3 -3
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +2 -2
- package/dist/{elevation-COpYKt2h.mjs → elevation-CIz1GjGm.mjs} +4 -3
- package/dist/{errorHandler-BTzXSwIg.mjs → errorHandler-Bq2OeEQL.mjs} +10 -7
- package/dist/{eventPlugin-A6EbthXR.d.mts → eventPlugin-Bg3cP2_u.d.mts} +38 -3
- package/dist/{eventPlugin-DJG2D7sy.mjs → eventPlugin-DrQ_FesK.mjs} +22 -4
- package/dist/events/index.d.mts +57 -314
- package/dist/events/index.mjs +168 -93
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis-stream-entry.mjs +24 -2
- package/dist/factory/index.d.mts +21 -6
- package/dist/factory/index.mjs +58 -18
- package/dist/{fastify-augmentation-9_fuetB4.d.mts → fastify-augmentation-dPNBVeIj.d.mts} +25 -2
- package/dist/{generate-usRSdQmP.mjs → generate-D0wZupBg.mjs} +1 -1
- package/dist/headers-BvlQSDMB.mjs +45 -0
- package/dist/health-CB_m4S32.mjs +377 -0
- package/dist/hooks/index.d.mts +1 -1
- package/dist/idempotency/index.d.mts +7 -0
- package/dist/idempotency/index.mjs +32 -20
- package/dist/{index-ogIHvOjo.d.mts → index-BMnbzvtO.d.mts} +70 -4
- package/dist/{integrations/jobs.d.mts → index-CG4yqMDB.d.mts} +82 -2
- package/dist/{index-DbtgCOCj.d.mts → index-DPRH0Cme.d.mts} +46 -94
- package/dist/{module-B4u10iOJ.d.mts → index-KUoxjIq1.d.mts} +263 -23
- package/dist/{index-D8MKYGDa.d.mts → index-f4PTrk3B.d.mts} +37 -5
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +6 -5
- package/dist/{init-DleK3p9f.mjs → init-tlRSt667.mjs} +206 -205
- package/dist/integrations/event-gateway.d.mts +1 -1
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +3 -3
- package/dist/integrations/jobs/index.d.mts +2 -0
- package/dist/integrations/{jobs.mjs → jobs/index.mjs} +110 -67
- package/dist/integrations/mcp/index.d.mts +68 -5
- package/dist/integrations/mcp/index.mjs +2 -2
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/webhooks.d.mts +149 -22
- package/dist/integrations/webhooks.mjs +186 -42
- package/dist/integrations/websocket-redis.d.mts +1 -1
- package/dist/integrations/websocket.d.mts +1 -1
- package/dist/integrations/websocket.mjs +41 -14
- package/dist/{interface-C1_WLTMK.d.mts → interface-BGZCzOGL.d.mts} +24 -31
- package/dist/interface-n5hpKg1d.mjs +16 -0
- package/dist/{loadResourcesFromEntry-OMCvIEE7.mjs → loadResourcesFromEntry-kvGT6EcO.mjs} +1 -1
- package/dist/{mcpPlugin-ByA2tf6D.mjs → mcpPlugin-Cb7olw0j.mjs} +316 -45
- package/dist/{memory-CLXA2VeJ.mjs → memory-CmnzXE5Z.mjs} +29 -0
- package/dist/middleware/index.d.mts +17 -1
- package/dist/middleware/index.mjs +1 -1
- package/dist/migrations/index.d.mts +94 -44
- package/dist/migrations/index.mjs +196 -44
- package/dist/{multipartBody-CHEQSZ6G.mjs → multipartBody-D0eBvkeq.mjs} +17 -0
- package/dist/{openapi-BRpMZD7C.mjs → openapi-DrLtog_B.mjs} +4 -4
- package/dist/permissions/index.d.mts +3 -3
- package/dist/permissions/index.mjs +4 -3
- package/dist/{permissions-BJMuKMl7.mjs → permissions-BrtiBKGs.mjs} +305 -492
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/{actionPermissions-ze70xXJi.mjs → pipeline-BN5G05rd.mjs} +4 -27
- package/dist/plugins/index.d.mts +7 -78
- package/dist/plugins/index.mjs +14 -97
- package/dist/plugins/response-cache.mjs +1 -0
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/{index-BEu4wjbL.d.mts → preloadResources-0oe-JSSS.d.mts} +59 -50
- package/dist/{createApp-Ci7yG5gv.mjs → preloadResources-DfyngIN9.mjs} +240 -68
- package/dist/presets/filesUpload.d.mts +3 -3
- package/dist/presets/filesUpload.mjs +3 -3
- package/dist/presets/index.d.mts +2 -1
- 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 +2 -2
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-gw0ciIUD.mjs → presets-DwFDVKP0.mjs} +2 -2
- package/dist/{queryCachePlugin-DzqLN904.mjs → queryCachePlugin-DSGuSVLW.mjs} +39 -8
- package/dist/{queryCachePlugin-CfYWq-Ak.d.mts → queryCachePlugin-br4otiqF.d.mts} +2 -2
- package/dist/{registerRoute-xHc42fwB.mjs → rateLimit-DrED-C4v.mjs} +2 -109
- package/dist/realtime-Cuw_deA-.mjs +375 -0
- package/dist/{redis-stream-iVR2l7aS.d.mts → redis-stream-uIaauNpZ.d.mts} +22 -0
- package/dist/registerRoute-v0Wb6Plt.mjs +108 -0
- package/dist/registry/index.d.mts +2 -2
- package/dist/registry/index.mjs +1 -1
- package/dist/renewingLease-3cuQH7h8.mjs +44 -0
- package/dist/{requestContext-BZKOr13H.mjs → requestContext-MrlJwECf.mjs} +2 -2
- package/dist/schedules-CjOd2MG9.mjs +113 -0
- package/dist/{schemaConverter-Ds5reSGs.mjs → schemaConverter-DJvodiHj.mjs} +4 -1
- package/dist/{schemaIR-B-6SPe7g.mjs → schemaIR-Ca7ySNPx.mjs} +1 -1
- package/dist/schemas/index.d.mts +67 -67
- package/dist/schemas/index.mjs +2 -3
- package/dist/scim/index.mjs +1 -2
- package/dist/scope/index.d.mts +2 -2
- package/dist/scope/index.mjs +4 -4
- package/dist/scope-DfaPntpo.mjs +36 -0
- package/dist/simpleEqualityMatcher-Cg05HPsn.mjs +68 -0
- package/dist/{sse-CfUjwjDk.mjs → sse-BQ1w0dfl.mjs} +8 -31
- package/dist/sseStream-BQ7Qaynp.mjs +57 -0
- package/dist/testing/index.d.mts +4 -3
- package/dist/testing/index.mjs +2 -2
- package/dist/transactionContext-PNdoyLLm.mjs +64 -0
- package/dist/types/index.d.mts +4 -4
- package/dist/{types-bHblZ9YN.mjs → types-CSyCxl2V.mjs} +38 -7
- package/dist/{types-DH0GupI7.d.mts → types-DjC-KUCz.d.mts} +118 -20
- package/dist/{types-Csxga7d3.d.mts → types-Dmw-6Qeo.d.mts} +96 -4
- package/dist/{types-Bc42CUyk.d.mts → types-QFGc3Cjv.d.mts} +37 -2
- package/dist/usage/index.d.mts +3 -52
- package/dist/usage/index.mjs +0 -0
- package/dist/userHelpers-Cg_y3fBz.mjs +29 -0
- package/dist/utils/index.d.mts +90 -57
- package/dist/utils/index.mjs +7 -3
- package/dist/{utils-FhJ88TJE.mjs → utils-taUVD4p3.mjs} +47 -385
- package/dist/{validate-C2mGgSXg.mjs → validate-Bk44F2yT.mjs} +1 -1
- package/dist/{versioning-DLwklPlG.d.mts → versioning-DXgY5BUm.d.mts} +52 -31
- package/dist/{versioning-Dk-71iIz.mjs → versioning-gKWyryT3.mjs} +8 -1
- package/dist/{websocket-uuWqM6UY.d.mts → websocket-_Pd0CgR0.d.mts} +15 -1
- package/package.json +526 -453
- package/skills/arc/references/api-reference.md +3 -1
- package/skills/arc/references/integrations.md +20 -11
- package/skills/arc/references/production.md +734 -696
- package/dist/QueryCache-Z1B38koh.d.mts +0 -40
- package/dist/betterAuthOpenApi-CV1lxpRX.mjs +0 -2
- package/dist/health-ScyjIVo5.mjs +0 -202
- package/dist/requestId-5BhTVzBv.mjs +0 -113
- package/dist/scope-k15RXfcq.mjs +0 -64
- package/dist/types-XTApa8b6.mjs +0 -24
|
@@ -1,45 +1,53 @@
|
|
|
1
1
|
import fp from "fastify-plugin";
|
|
2
|
-
//#region src/integrations/jobs.ts
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
2
|
+
//#region src/integrations/jobs/execution.ts
|
|
3
|
+
async function executeTimedHandler(options) {
|
|
4
|
+
const { run, label, jobId, timeoutMs, cancelGraceMs = 3e4, logger } = options;
|
|
5
|
+
let released = false;
|
|
6
|
+
const releaseSlot = () => {
|
|
7
|
+
if (released) return;
|
|
8
|
+
released = true;
|
|
9
|
+
options.releaseSlot?.();
|
|
10
|
+
};
|
|
11
|
+
const abort = new AbortController();
|
|
12
|
+
const handlerPromise = (async () => run(abort.signal))();
|
|
13
|
+
handlerPromise.then(releaseSlot, releaseSlot);
|
|
14
|
+
if (!timeoutMs) return handlerPromise;
|
|
15
|
+
let timer;
|
|
16
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
17
|
+
timer = setTimeout(() => {
|
|
18
|
+
const err = /* @__PURE__ */ new Error(`Job '${label}' timed out after ${timeoutMs}ms`);
|
|
19
|
+
abort.abort(err);
|
|
20
|
+
const timedOutAt = Date.now();
|
|
21
|
+
const graceTimer = setTimeout(() => {
|
|
22
|
+
logger?.warn({
|
|
23
|
+
job: label,
|
|
24
|
+
jobId,
|
|
25
|
+
graceMs: cancelGraceMs
|
|
26
|
+
}, "timed-out job handler still running after grace period — force-releasing its maxConcurrent slot (bulkhead may now be exceeded); make the handler honor its AbortSignal");
|
|
27
|
+
releaseSlot();
|
|
28
|
+
}, cancelGraceMs);
|
|
29
|
+
graceTimer.unref?.();
|
|
30
|
+
handlerPromise.then(() => logger?.warn({
|
|
31
|
+
job: label,
|
|
32
|
+
jobId,
|
|
33
|
+
afterMs: Date.now() - timedOutAt
|
|
34
|
+
}, "timed-out job handler eventually completed (result discarded)"), (handlerErr) => logger?.warn({
|
|
35
|
+
job: label,
|
|
36
|
+
jobId,
|
|
37
|
+
afterMs: Date.now() - timedOutAt,
|
|
38
|
+
err: handlerErr
|
|
39
|
+
}, "timed-out job handler eventually failed")).finally(() => clearTimeout(graceTimer));
|
|
40
|
+
reject(err);
|
|
41
|
+
}, timeoutMs);
|
|
42
|
+
});
|
|
43
|
+
try {
|
|
44
|
+
return await Promise.race([handlerPromise, timeoutPromise]);
|
|
45
|
+
} finally {
|
|
46
|
+
clearTimeout(timer);
|
|
41
47
|
}
|
|
42
|
-
}
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/integrations/jobs/repeat.ts
|
|
43
51
|
/**
|
|
44
52
|
* Whether an existing schedule matches the currently configured repeat spec.
|
|
45
53
|
*
|
|
@@ -60,11 +68,6 @@ function repeatSpecMatches(record, spec) {
|
|
|
60
68
|
* Remove repeatable schedules left behind by a previous deploy whose repeat
|
|
61
69
|
* spec no longer matches the current `JobDefinition.repeat`.
|
|
62
70
|
*
|
|
63
|
-
* BullMQ keys repeatable schedules by a hash of their repeat options, so
|
|
64
|
-
* re-adding a job with a CHANGED pattern/every/tz/endDate creates a NEW
|
|
65
|
-
* schedule while the old one keeps firing — the job double-fires after every
|
|
66
|
-
* repeat-config change. This reconciles at plugin registration:
|
|
67
|
-
*
|
|
68
71
|
* - Only schedulers whose `name` equals the registered job's name are
|
|
69
72
|
* candidates — foreign/unknown schedulers are never touched.
|
|
70
73
|
* - A scheduler matching the current spec is kept (the subsequent
|
|
@@ -102,6 +105,32 @@ async function removeStaleRepeatSchedulers(queue, jobName, spec, log) {
|
|
|
102
105
|
}, `[arc/jobs] Failed to reconcile repeatable schedules for '${jobName}' — continuing (boot-time hygiene only)`);
|
|
103
106
|
}
|
|
104
107
|
}
|
|
108
|
+
//#endregion
|
|
109
|
+
//#region src/integrations/jobs/plugin.ts
|
|
110
|
+
var Semaphore = class {
|
|
111
|
+
available;
|
|
112
|
+
waiters = [];
|
|
113
|
+
constructor(permits) {
|
|
114
|
+
this.available = permits;
|
|
115
|
+
}
|
|
116
|
+
get isFull() {
|
|
117
|
+
return this.available === 0;
|
|
118
|
+
}
|
|
119
|
+
acquire() {
|
|
120
|
+
if (this.available > 0) {
|
|
121
|
+
this.available--;
|
|
122
|
+
return Promise.resolve();
|
|
123
|
+
}
|
|
124
|
+
return new Promise((resolve) => {
|
|
125
|
+
this.waiters.push(resolve);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
release() {
|
|
129
|
+
const next = this.waiters.shift();
|
|
130
|
+
if (next) next();
|
|
131
|
+
else this.available++;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
105
134
|
const jobsPluginImpl = async (fastify, options) => {
|
|
106
135
|
const { connection, jobs, prefix = "/jobs", bridgeEvents = true, defaults = {} } = options;
|
|
107
136
|
let Queue;
|
|
@@ -158,11 +187,6 @@ const jobsPluginImpl = async (fastify, options) => {
|
|
|
158
187
|
const throttledSet = /* @__PURE__ */ new Set();
|
|
159
188
|
throttledJobs.set(queueName, throttledSet);
|
|
160
189
|
const worker = new Worker(queueName, async (bullJob) => {
|
|
161
|
-
const meta = {
|
|
162
|
-
jobId: bullJob.id ?? "",
|
|
163
|
-
attemptsMade: bullJob.attemptsMade ?? 0,
|
|
164
|
-
timestamp: Date.now()
|
|
165
|
-
};
|
|
166
190
|
if (semaphore) {
|
|
167
191
|
if (semaphore.isFull) throttledSet.add(bullJob.id ?? "");
|
|
168
192
|
try {
|
|
@@ -171,22 +195,23 @@ const jobsPluginImpl = async (fastify, options) => {
|
|
|
171
195
|
throttledSet.delete(bullJob.id ?? "");
|
|
172
196
|
}
|
|
173
197
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
198
|
+
const result = await executeTimedHandler({
|
|
199
|
+
label: queueName,
|
|
200
|
+
...bullJob.id !== void 0 ? { jobId: bullJob.id } : {},
|
|
201
|
+
...jobTimeout !== void 0 ? { timeoutMs: jobTimeout } : {},
|
|
202
|
+
...job.cancelGraceMs !== void 0 ? { cancelGraceMs: job.cancelGraceMs } : {},
|
|
203
|
+
...semaphore ? { releaseSlot: () => semaphore.release() } : {},
|
|
204
|
+
logger: fastify.log,
|
|
205
|
+
run: (signal) => {
|
|
206
|
+
const meta = {
|
|
207
|
+
jobId: bullJob.id ?? "",
|
|
208
|
+
attemptsMade: bullJob.attemptsMade ?? 0,
|
|
209
|
+
timestamp: Date.now(),
|
|
210
|
+
signal
|
|
211
|
+
};
|
|
212
|
+
return job.handler(bullJob.data, meta);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
190
215
|
if (bridgeEvents && fastify.events?.publish) try {
|
|
191
216
|
await fastify.events.publish(`job.${queueName}.completed`, {
|
|
192
217
|
jobId: bullJob.id,
|
|
@@ -366,4 +391,22 @@ const jobsPlugin = fp(jobsPluginImpl, {
|
|
|
366
391
|
fastify: "5.x"
|
|
367
392
|
});
|
|
368
393
|
//#endregion
|
|
369
|
-
|
|
394
|
+
//#region src/integrations/jobs/types.ts
|
|
395
|
+
/**
|
|
396
|
+
* Define a background job with typed data and configuration.
|
|
397
|
+
*
|
|
398
|
+
* @example
|
|
399
|
+
* const processImage = defineJob({
|
|
400
|
+
* name: 'process-image',
|
|
401
|
+
* handler: async (data: { url: string; width: number }) => {
|
|
402
|
+
* return await sharp(data.url).resize(data.width).toBuffer();
|
|
403
|
+
* },
|
|
404
|
+
* retries: 3,
|
|
405
|
+
* timeout: 60000,
|
|
406
|
+
* });
|
|
407
|
+
*/
|
|
408
|
+
function defineJob(definition) {
|
|
409
|
+
return definition;
|
|
410
|
+
}
|
|
411
|
+
//#endregion
|
|
412
|
+
export { defineJob, executeTimedHandler, jobsPlugin };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { Rt as IControllerResponse, V as ResourceDefinition, zt as IRequestContext } from "../../index-
|
|
2
|
-
import { i as RequestScope } from "../../types-
|
|
3
|
-
import { t as PermissionCheck } from "../../types-
|
|
4
|
-
import {
|
|
1
|
+
import { Rt as IControllerResponse, V as ResourceDefinition, Vt as ServerAccessor, zt as IRequestContext } from "../../index-DPRH0Cme.mjs";
|
|
2
|
+
import { i as RequestScope } from "../../types-DjC-KUCz.mjs";
|
|
3
|
+
import { t as PermissionCheck } from "../../types-Dmw-6Qeo.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-QFGc3Cjv.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-Cb7olw0j.mjs";
|
|
2
2
|
//#region src/integrations/mcp/defineTool.ts
|
|
3
3
|
/**
|
|
4
4
|
* Define a type-safe MCP tool.
|
|
@@ -206,7 +206,7 @@ function createMcpAuthFromBetterAuthApiKey(auth, options = {}) {
|
|
|
206
206
|
...userId ? { userId } : {},
|
|
207
207
|
...organizationId ? { organizationId } : {},
|
|
208
208
|
...clientId ? { clientId } : {},
|
|
209
|
-
...scopes
|
|
209
|
+
...scopes?.length ? { scopes } : {}
|
|
210
210
|
};
|
|
211
211
|
};
|
|
212
212
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as McpAuthResult,
|
|
1
|
+
import { c as McpAuthResult, u as McpPluginOptions } from "../../types-QFGc3Cjv.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-Cb7olw0j.mjs";
|
|
2
2
|
//#region src/integrations/mcp/testing.ts
|
|
3
3
|
/**
|
|
4
4
|
* @classytic/arc/mcp/testing — MCP Test Utilities
|
|
@@ -19,6 +19,8 @@ interface WebhookDeliveryRecord {
|
|
|
19
19
|
status?: number;
|
|
20
20
|
error?: string;
|
|
21
21
|
timestamp: Date;
|
|
22
|
+
/** Delivery attempts made (present when `retry` is configured). */
|
|
23
|
+
attempts?: number;
|
|
22
24
|
}
|
|
23
25
|
/** Pluggable persistence — memory for dev, bring your own DB for prod */
|
|
24
26
|
interface WebhookStore {
|
|
@@ -36,13 +38,70 @@ interface WebhookPluginOptions {
|
|
|
36
38
|
timeout?: number;
|
|
37
39
|
/** Max delivery log entries kept in memory (default: 1000) */
|
|
38
40
|
maxLogEntries?: number;
|
|
39
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Max concurrent deliveries per event (default: 5). Set to 1 for
|
|
43
|
+
* sequential. Must be >= 1 — the plugin throws at registration otherwise
|
|
44
|
+
* (a zero would make the delivery loop spin forever without progress).
|
|
45
|
+
*/
|
|
40
46
|
concurrency?: number;
|
|
47
|
+
/**
|
|
48
|
+
* In-process retry for failed deliveries. Default: single attempt.
|
|
49
|
+
* Retries fire on network errors, timeouts, 429 and 5xx — NOT on other
|
|
50
|
+
* 4xx (the receiver rejected the payload; repeating it can't succeed).
|
|
51
|
+
* Each attempt is re-signed with a fresh timestamp so the receiver's
|
|
52
|
+
* replay-tolerance window applies per attempt; `x-arc-webhook-delivery`
|
|
53
|
+
* stays constant, which is exactly what receiver-side dedup keys on.
|
|
54
|
+
*
|
|
55
|
+
* This is BEST-EFFORT: attempts live in this process, so a crash or
|
|
56
|
+
* deploy drops the remaining ones. Guaranteed delivery is a durable-job
|
|
57
|
+
* concern (queue keyed by subscription + delivery id) — pair the events
|
|
58
|
+
* outbox with a worker if you need it.
|
|
59
|
+
*/
|
|
60
|
+
retry?: {
|
|
61
|
+
/** Total attempts including the first. Must be an integer >= 1. */
|
|
62
|
+
attempts: number;
|
|
63
|
+
/** Backoff before retry N, as `backoffMs * 2^(N-1)` ms (default: 1000). */
|
|
64
|
+
backoffMs?: number;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* URL policy hook — the SSRF seam for tenant-registered URLs. Throw to
|
|
68
|
+
* reject. Pass {@link defaultWebhookUrlPolicy} to require HTTPS and block
|
|
69
|
+
* localhost / link-local / private-network / cloud-metadata destinations,
|
|
70
|
+
* or supply your own (allowlists, per-tenant rules).
|
|
71
|
+
*
|
|
72
|
+
* Enforcement points when set:
|
|
73
|
+
* - `register()` — new subscriptions are validated BEFORE saving.
|
|
74
|
+
* - Plugin init — every PERSISTED subscription is re-validated; failures
|
|
75
|
+
* are excluded from dispatch and logged (they stay in the store for
|
|
76
|
+
* the host to fix), so pre-policy rows and out-of-band store writes
|
|
77
|
+
* can't bypass the policy.
|
|
78
|
+
* - Delivery — `fetch` runs with `redirect: "manual"`, so an allowed
|
|
79
|
+
* public URL can't 302 into a blocked private/metadata target; a 3xx
|
|
80
|
+
* records as a failed delivery.
|
|
81
|
+
*
|
|
82
|
+
* Arc deliberately ships no DEFAULT policy — deployments differ (internal
|
|
83
|
+
* webhooks to private services are legitimate), and DNS-rebinding
|
|
84
|
+
* protection belongs at the actual outbound network boundary, not in the
|
|
85
|
+
* framework.
|
|
86
|
+
*/
|
|
87
|
+
validateUrl?: (url: URL, subscription: WebhookSubscription) => void | Promise<void>;
|
|
41
88
|
}
|
|
89
|
+
/** `WebhookSubscription` as returned by `list()` — secret withheld. */
|
|
90
|
+
type WebhookSubscriptionPublic = Omit<WebhookSubscription, "secret">;
|
|
42
91
|
interface WebhookManager {
|
|
43
92
|
register(sub: WebhookSubscription): Promise<void> | void;
|
|
44
93
|
unregister(id: string): Promise<void> | void;
|
|
45
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Enumerate subscriptions WITHOUT their secrets — safe to surface in
|
|
96
|
+
* admin UIs / API responses. Use {@link WebhookManager.getSecret} for
|
|
97
|
+
* the privileged retrieval path.
|
|
98
|
+
*/
|
|
99
|
+
list(): WebhookSubscriptionPublic[];
|
|
100
|
+
/**
|
|
101
|
+
* Privileged secret retrieval — separate from `list()` so enumerating
|
|
102
|
+
* subscriptions never leaks signing material into logs or UIs.
|
|
103
|
+
*/
|
|
104
|
+
getSecret(id: string): string | undefined;
|
|
46
105
|
deliveryLog(limit?: number): WebhookDeliveryRecord[];
|
|
47
106
|
}
|
|
48
107
|
declare module "fastify" {
|
|
@@ -51,11 +110,80 @@ declare module "fastify" {
|
|
|
51
110
|
}
|
|
52
111
|
}
|
|
53
112
|
/**
|
|
54
|
-
*
|
|
113
|
+
* Signed-string layout for the v1 webhook contract:
|
|
114
|
+
*
|
|
115
|
+
* v1.<timestampMs>.<deliveryId>.<body>
|
|
55
116
|
*
|
|
56
|
-
*
|
|
117
|
+
* Binding the timestamp bounds replay to the verifier's tolerance window,
|
|
118
|
+
* and binding the delivery id lets receivers deduplicate redeliveries on an
|
|
119
|
+
* AUTHENTICATED value (the legacy `x-webhook-id` header was unsigned — an
|
|
120
|
+
* attacker replaying a captured request could change it and defeat dedup).
|
|
121
|
+
*
|
|
122
|
+
* Wire headers:
|
|
123
|
+
* - `x-arc-webhook-signature: v1=<hex hmac-sha256>`
|
|
124
|
+
* - `x-arc-webhook-timestamp: <unix epoch ms>`
|
|
125
|
+
* - `x-arc-webhook-delivery: <delivery id>` (arc sends the event's
|
|
126
|
+
* `meta.id` — STABLE across retries of the same event, so dedup on it
|
|
127
|
+
* collapses redeliveries)
|
|
57
128
|
*/
|
|
58
|
-
declare function
|
|
129
|
+
declare function signWebhook(body: string, secret: string, meta: {
|
|
130
|
+
timestamp: number;
|
|
131
|
+
deliveryId: string;
|
|
132
|
+
}): string;
|
|
133
|
+
interface VerifyWebhookInput {
|
|
134
|
+
/** Raw request body — the exact bytes the sender signed (`req.rawBody`). */
|
|
135
|
+
body: string | Buffer;
|
|
136
|
+
/** Shared secret. */
|
|
137
|
+
secret: string;
|
|
138
|
+
/** `x-arc-webhook-signature` header value (`v1=<hex>`). */
|
|
139
|
+
signature: string | undefined;
|
|
140
|
+
/** `x-arc-webhook-timestamp` header value (unix epoch ms). */
|
|
141
|
+
timestamp: string | number | undefined;
|
|
142
|
+
/** `x-arc-webhook-delivery` header value. */
|
|
143
|
+
deliveryId: string | undefined;
|
|
144
|
+
/**
|
|
145
|
+
* Max allowed clock skew between sender timestamp and now, in ms.
|
|
146
|
+
* @default 300_000 (5 minutes)
|
|
147
|
+
*/
|
|
148
|
+
toleranceMs?: number;
|
|
149
|
+
/** Injectable clock for tests. @default Date.now */
|
|
150
|
+
now?: () => number;
|
|
151
|
+
}
|
|
152
|
+
type VerifyWebhookResult = {
|
|
153
|
+
valid: true;
|
|
154
|
+
/** The AUTHENTICATED delivery id — dedup redeliveries on this. */
|
|
155
|
+
deliveryId: string;
|
|
156
|
+
/** Sender timestamp (unix epoch ms), authenticated. */
|
|
157
|
+
timestamp: number;
|
|
158
|
+
} | {
|
|
159
|
+
valid: false;
|
|
160
|
+
reason: "missing_headers" | "bad_timestamp" | "expired" | "bad_signature";
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Verify an inbound v1 arc webhook: signature + timestamp tolerance, and
|
|
164
|
+
* return the authenticated delivery id for receiver-side deduplication.
|
|
165
|
+
*
|
|
166
|
+
* ⚠ Pass `req.rawBody` (route opted in via `config: { rawBody: true }`),
|
|
167
|
+
* never the parsed `req.body` — re-serialised JSON won't match the HMAC.
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* ```typescript
|
|
171
|
+
* fastify.post('/hooks/arc', { config: { rawBody: true } }, async (req, reply) => {
|
|
172
|
+
* const result = verifyWebhook({
|
|
173
|
+
* body: req.rawBody,
|
|
174
|
+
* secret: process.env.WEBHOOK_SECRET,
|
|
175
|
+
* signature: req.headers['x-arc-webhook-signature'] as string,
|
|
176
|
+
* timestamp: req.headers['x-arc-webhook-timestamp'] as string,
|
|
177
|
+
* deliveryId: req.headers['x-arc-webhook-delivery'] as string,
|
|
178
|
+
* });
|
|
179
|
+
* if (!result.valid) return reply.status(401).send({ error: result.reason });
|
|
180
|
+
* if (await seen(result.deliveryId)) return reply.send({ ok: true }); // dedup
|
|
181
|
+
* // ... handle, then mark result.deliveryId seen
|
|
182
|
+
* });
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
185
|
+
declare function verifyWebhook(input: VerifyWebhookInput): VerifyWebhookResult;
|
|
186
|
+
declare function defaultWebhookUrlPolicy(url: URL): void;
|
|
59
187
|
/** Options for `verifySignature` — customize for non-Arc webhook senders */
|
|
60
188
|
interface VerifySignatureOptions {
|
|
61
189
|
/**
|
|
@@ -65,15 +193,18 @@ interface VerifySignatureOptions {
|
|
|
65
193
|
prefix?: string;
|
|
66
194
|
/**
|
|
67
195
|
* HMAC algorithm (default: `'sha256'`).
|
|
68
|
-
* Must match what the sender uses
|
|
196
|
+
* Must match what the sender uses.
|
|
69
197
|
*/
|
|
70
198
|
algorithm?: string;
|
|
71
199
|
}
|
|
72
200
|
/**
|
|
73
|
-
* Verify an
|
|
201
|
+
* Verify an INBOUND third-party webhook signature using timing-safe
|
|
202
|
+
* comparison — for webhooks arc RECEIVES (GitHub, Stripe, ...), not for
|
|
203
|
+
* arc-to-arc delivery. Arc's own outbound contract is v1
|
|
204
|
+
* (`x-arc-webhook-*`) — receivers of arc webhooks use {@link verifyWebhook}.
|
|
74
205
|
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
206
|
+
* Defaults to the common `sha256=<hex>` format; configurable for any HMAC
|
|
207
|
+
* scheme via options.
|
|
77
208
|
*
|
|
78
209
|
* ⚠ The `body` argument must be the exact bytes the sender signed. Fastify
|
|
79
210
|
* parses JSON bodies by default and the re-serialised object will NOT match
|
|
@@ -91,21 +222,17 @@ interface VerifySignatureOptions {
|
|
|
91
222
|
* ```typescript
|
|
92
223
|
* import { verifySignature } from '@classytic/arc/integrations/webhooks';
|
|
93
224
|
*
|
|
94
|
-
* //
|
|
95
|
-
* //
|
|
96
|
-
* //
|
|
97
|
-
* //
|
|
98
|
-
*
|
|
99
|
-
*
|
|
225
|
+
* // `config: { rawBody: true }` is REQUIRED — arc registers
|
|
226
|
+
* // fastify-raw-body with `global: false`, so routes opt in per-route;
|
|
227
|
+
* // without it `req.rawBody` is undefined and verifySignature throws a
|
|
228
|
+
* // TypeError.
|
|
229
|
+
*
|
|
230
|
+
* // GitHub-style sender (sha256=<hex>)
|
|
231
|
+
* fastify.post('/webhooks/github', { config: { rawBody: true } }, async (req, reply) => {
|
|
232
|
+
* const sig = req.headers['x-hub-signature-256'] as string;
|
|
100
233
|
* if (!verifySignature(req.rawBody, secret, sig)) {
|
|
101
234
|
* return reply.status(401).send({ error: 'Invalid signature' });
|
|
102
235
|
* }
|
|
103
|
-
* // handle event via req.headers['x-webhook-event']
|
|
104
|
-
* });
|
|
105
|
-
*
|
|
106
|
-
* // Third-party sender with custom header + bare hex
|
|
107
|
-
* const valid = verifySignature(body, secret, req.headers['x-hub-signature'], {
|
|
108
|
-
* prefix: 'sha256=', // GitHub format
|
|
109
236
|
* });
|
|
110
237
|
*
|
|
111
238
|
* // Stripe-style (bare hex, different header)
|
|
@@ -118,4 +245,4 @@ declare function verifySignature(body: string | Buffer, secret: string, signatur
|
|
|
118
245
|
declare const webhookPlugin: FastifyPluginAsync<WebhookPluginOptions>;
|
|
119
246
|
declare const _default: FastifyPluginAsync<WebhookPluginOptions>;
|
|
120
247
|
//#endregion
|
|
121
|
-
export { VerifySignatureOptions, WebhookDeliveryRecord, WebhookManager, WebhookPluginOptions, WebhookStore, WebhookSubscription, _default as default,
|
|
248
|
+
export { VerifySignatureOptions, VerifyWebhookInput, VerifyWebhookResult, WebhookDeliveryRecord, WebhookManager, WebhookPluginOptions, WebhookStore, WebhookSubscription, WebhookSubscriptionPublic, _default as default, defaultWebhookUrlPolicy, signWebhook, verifySignature, verifyWebhook, webhookPlugin };
|