@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
|
@@ -33,6 +33,13 @@ import fp from "fastify-plugin";
|
|
|
33
33
|
/**
|
|
34
34
|
* Sign a payload with HMAC-SHA256 for outbound webhook delivery.
|
|
35
35
|
*
|
|
36
|
+
* @deprecated LEGACY (pre-2.23) format — signs the BODY ONLY, so a captured
|
|
37
|
+
* request can be replayed indefinitely and the event-id header is not
|
|
38
|
+
* cryptographically bound to the body. Still emitted on the legacy
|
|
39
|
+
* `x-webhook-signature` header for existing receivers; new receivers should
|
|
40
|
+
* verify the v1 contract (`x-arc-webhook-signature`) via {@link verifyWebhook}.
|
|
41
|
+
* Legacy emission is scheduled for removal in v3.
|
|
42
|
+
*
|
|
36
43
|
* @returns `sha256=<hex>` — the format written to `x-webhook-signature`
|
|
37
44
|
*/
|
|
38
45
|
function signPayload(payload, secret) {
|
|
@@ -41,6 +48,120 @@ function signPayload(payload, secret) {
|
|
|
41
48
|
return `sha256=${hmac.digest("hex")}`;
|
|
42
49
|
}
|
|
43
50
|
/**
|
|
51
|
+
* Signed-string layout for the v1 webhook contract:
|
|
52
|
+
*
|
|
53
|
+
* v1.<timestampMs>.<deliveryId>.<body>
|
|
54
|
+
*
|
|
55
|
+
* Binding the timestamp bounds replay to the verifier's tolerance window,
|
|
56
|
+
* and binding the delivery id lets receivers deduplicate redeliveries on an
|
|
57
|
+
* AUTHENTICATED value (the legacy `x-webhook-id` header was unsigned — an
|
|
58
|
+
* attacker replaying a captured request could change it and defeat dedup).
|
|
59
|
+
*
|
|
60
|
+
* Wire headers:
|
|
61
|
+
* - `x-arc-webhook-signature: v1=<hex hmac-sha256>`
|
|
62
|
+
* - `x-arc-webhook-timestamp: <unix epoch ms>`
|
|
63
|
+
* - `x-arc-webhook-delivery: <delivery id>` (arc sends the event's
|
|
64
|
+
* `meta.id` — STABLE across retries of the same event, so dedup on it
|
|
65
|
+
* collapses redeliveries)
|
|
66
|
+
*/
|
|
67
|
+
function signWebhook(body, secret, meta) {
|
|
68
|
+
const hmac = createHmac("sha256", secret);
|
|
69
|
+
hmac.update(`v1.${meta.timestamp}.${meta.deliveryId}.${body}`);
|
|
70
|
+
return `v1=${hmac.digest("hex")}`;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Verify an inbound v1 arc webhook: signature + timestamp tolerance, and
|
|
74
|
+
* return the authenticated delivery id for receiver-side deduplication.
|
|
75
|
+
*
|
|
76
|
+
* ⚠ Pass `req.rawBody` (route opted in via `config: { rawBody: true }`),
|
|
77
|
+
* never the parsed `req.body` — re-serialised JSON won't match the HMAC.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```typescript
|
|
81
|
+
* fastify.post('/hooks/arc', { config: { rawBody: true } }, async (req, reply) => {
|
|
82
|
+
* const result = verifyWebhook({
|
|
83
|
+
* body: req.rawBody,
|
|
84
|
+
* secret: process.env.WEBHOOK_SECRET,
|
|
85
|
+
* signature: req.headers['x-arc-webhook-signature'] as string,
|
|
86
|
+
* timestamp: req.headers['x-arc-webhook-timestamp'] as string,
|
|
87
|
+
* deliveryId: req.headers['x-arc-webhook-delivery'] as string,
|
|
88
|
+
* });
|
|
89
|
+
* if (!result.valid) return reply.status(401).send({ error: result.reason });
|
|
90
|
+
* if (await seen(result.deliveryId)) return reply.send({ ok: true }); // dedup
|
|
91
|
+
* // ... handle, then mark result.deliveryId seen
|
|
92
|
+
* });
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
function verifyWebhook(input) {
|
|
96
|
+
const { body, secret, signature, deliveryId, toleranceMs = 3e5, now = Date.now } = input;
|
|
97
|
+
if (typeof body !== "string" && !Buffer.isBuffer(body)) throw new TypeError("verifyWebhook: body must be a string or Buffer (the exact bytes the sender signed). Add `config: { rawBody: true }` to the route and pass req.rawBody — not req.body.");
|
|
98
|
+
if (!signature || !deliveryId || input.timestamp === void 0 || input.timestamp === "") return {
|
|
99
|
+
valid: false,
|
|
100
|
+
reason: "missing_headers"
|
|
101
|
+
};
|
|
102
|
+
const timestamp = Number(input.timestamp);
|
|
103
|
+
if (!Number.isFinite(timestamp)) return {
|
|
104
|
+
valid: false,
|
|
105
|
+
reason: "bad_timestamp"
|
|
106
|
+
};
|
|
107
|
+
if (Math.abs(now() - timestamp) > toleranceMs) return {
|
|
108
|
+
valid: false,
|
|
109
|
+
reason: "expired"
|
|
110
|
+
};
|
|
111
|
+
const expected = signWebhook(typeof body === "string" ? body : body.toString("utf8"), secret, {
|
|
112
|
+
timestamp,
|
|
113
|
+
deliveryId
|
|
114
|
+
});
|
|
115
|
+
const a = Buffer.from(signature);
|
|
116
|
+
const b = Buffer.from(expected);
|
|
117
|
+
if (a.length !== b.length || !timingSafeEqual(a, b)) return {
|
|
118
|
+
valid: false,
|
|
119
|
+
reason: "bad_signature"
|
|
120
|
+
};
|
|
121
|
+
return {
|
|
122
|
+
valid: true,
|
|
123
|
+
deliveryId,
|
|
124
|
+
timestamp
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Opt-in URL policy for tenant-registered webhooks: requires HTTPS and
|
|
129
|
+
* rejects loopback, link-local, private-network, and cloud-metadata
|
|
130
|
+
* destinations. Pass as `validateUrl: defaultWebhookUrlPolicy`.
|
|
131
|
+
*
|
|
132
|
+
* Scope note: this validates the URL LITERAL at registration time. It does
|
|
133
|
+
* not resolve DNS — rebinding/level-2 SSRF protection belongs at the actual
|
|
134
|
+
* outbound network boundary (egress proxy, network policy), where the real
|
|
135
|
+
* connection target is known. Internal deployments that legitimately POST
|
|
136
|
+
* to private services should write their own narrower policy instead.
|
|
137
|
+
*/
|
|
138
|
+
const BLOCKED_V4 = /^(?:127\.|10\.|192\.168\.|172\.(?:1[6-9]|2\d|3[01])\.|169\.254\.|0\.)/;
|
|
139
|
+
function defaultWebhookUrlPolicy(url) {
|
|
140
|
+
if (url.protocol !== "https:") throw new Error(`Webhook URL must use https (got ${url.protocol}//)`);
|
|
141
|
+
let host = url.hostname.toLowerCase();
|
|
142
|
+
if (host.startsWith("[") && host.endsWith("]")) host = host.slice(1, -1);
|
|
143
|
+
const blockedName = host === "localhost" || host.endsWith(".localhost") || host === "metadata.google.internal";
|
|
144
|
+
const isV6 = host.includes(":");
|
|
145
|
+
const mappedV4 = isV6 ? decodeMappedV4(host) : void 0;
|
|
146
|
+
const blockedV6 = isV6 && (host === "::1" || host === "::" || /^f[cd]/.test(host) || /^fe[89ab]/.test(host) || mappedV4 !== void 0 && BLOCKED_V4.test(mappedV4));
|
|
147
|
+
const blockedV4 = !isV6 && BLOCKED_V4.test(host);
|
|
148
|
+
if (blockedName || blockedV4 || blockedV6) throw new Error(`Webhook URL host '${host}' is not allowed (loopback/private/metadata)`);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Decode an IPv4-mapped IPv6 literal to its dotted-quad form. Accepts both
|
|
152
|
+
* the textual spelling (`::ffff:1.2.3.4`) and the hex-group canonical form
|
|
153
|
+
* WHATWG URL produces (`::ffff:102:304`). Returns undefined for anything else.
|
|
154
|
+
*/
|
|
155
|
+
function decodeMappedV4(host) {
|
|
156
|
+
const dotted = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(host)?.[1];
|
|
157
|
+
if (dotted) return dotted;
|
|
158
|
+
const hex = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(host);
|
|
159
|
+
if (!hex?.[1] || !hex[2]) return void 0;
|
|
160
|
+
const hi = Number.parseInt(hex[1], 16);
|
|
161
|
+
const lo = Number.parseInt(hex[2], 16);
|
|
162
|
+
return `${hi >> 8}.${hi & 255}.${lo >> 8}.${lo & 255}`;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
44
165
|
* Verify an inbound webhook signature using timing-safe comparison.
|
|
45
166
|
*
|
|
46
167
|
* Works with Arc's own `signPayload` format by default (`sha256=<hex>`),
|
|
@@ -48,8 +169,9 @@ function signPayload(payload, secret) {
|
|
|
48
169
|
*
|
|
49
170
|
* ⚠ The `body` argument must be the exact bytes the sender signed. Fastify
|
|
50
171
|
* parses JSON bodies by default and the re-serialised object will NOT match
|
|
51
|
-
* the original signature. Register
|
|
52
|
-
*
|
|
172
|
+
* the original signature. Register `fastify-raw-body` (arc's createApp does
|
|
173
|
+
* this automatically with `global: false`) and opt the route in via
|
|
174
|
+
* `config: { rawBody: true }`, then pass `req.rawBody` here.
|
|
53
175
|
*
|
|
54
176
|
* @param body - Raw request body (string or Buffer — must be the exact bytes the sender signed)
|
|
55
177
|
* @param secret - Shared secret between sender and receiver
|
|
@@ -61,8 +183,11 @@ function signPayload(payload, secret) {
|
|
|
61
183
|
* ```typescript
|
|
62
184
|
* import { verifySignature } from '@classytic/arc/integrations/webhooks';
|
|
63
185
|
*
|
|
64
|
-
* // Arc-to-Arc (default headers + format)
|
|
65
|
-
* fastify
|
|
186
|
+
* // Arc-to-Arc (default headers + format). `config: { rawBody: true }` is
|
|
187
|
+
* // REQUIRED — arc registers fastify-raw-body with `global: false`, so
|
|
188
|
+
* // routes opt in per-route; without it `req.rawBody` is undefined and
|
|
189
|
+
* // verifySignature throws a TypeError.
|
|
190
|
+
* fastify.post('/webhooks/incoming', { config: { rawBody: true } }, async (req, reply) => {
|
|
66
191
|
* const sig = req.headers['x-webhook-signature'] as string;
|
|
67
192
|
* if (!verifySignature(req.rawBody, secret, sig)) {
|
|
68
193
|
* return reply.status(401).send({ error: 'Invalid signature' });
|
|
@@ -82,7 +207,7 @@ function signPayload(payload, secret) {
|
|
|
82
207
|
* ```
|
|
83
208
|
*/
|
|
84
209
|
function verifySignature(body, secret, signature, options) {
|
|
85
|
-
if (typeof body !== "string" && !Buffer.isBuffer(body)) throw new TypeError("verifySignature: body must be a string or Buffer (the exact bytes the sender signed).
|
|
210
|
+
if (typeof body !== "string" && !Buffer.isBuffer(body)) throw new TypeError("verifySignature: body must be a string or Buffer (the exact bytes the sender signed). Add `config: { rawBody: true }` to the route (arc registers fastify-raw-body with global: false) and pass req.rawBody — not req.body.");
|
|
86
211
|
if (!signature) return false;
|
|
87
212
|
const prefix = options?.prefix ?? "sha256=";
|
|
88
213
|
const algorithm = options?.algorithm ?? "sha256";
|
|
@@ -129,9 +254,24 @@ const webhookPlugin = async (fastify, opts = {}) => {
|
|
|
129
254
|
const timeout = opts.timeout ?? 1e4;
|
|
130
255
|
const maxLogEntries = opts.maxLogEntries ?? 1e3;
|
|
131
256
|
const concurrency = opts.concurrency ?? 5;
|
|
257
|
+
if (!Number.isInteger(concurrency) || concurrency < 1) throw new Error(`webhookPlugin: concurrency must be an integer >= 1 (got ${String(opts.concurrency)})`);
|
|
132
258
|
let subscriptions = [];
|
|
133
259
|
const log = [];
|
|
134
260
|
subscriptions = await store.getAll();
|
|
261
|
+
if (opts.validateUrl) {
|
|
262
|
+
const admitted = [];
|
|
263
|
+
for (const sub of subscriptions) try {
|
|
264
|
+
await opts.validateUrl(new URL(sub.url), sub);
|
|
265
|
+
admitted.push(sub);
|
|
266
|
+
} catch (err) {
|
|
267
|
+
fastify.log.warn({
|
|
268
|
+
subscriptionId: sub.id,
|
|
269
|
+
url: sub.url,
|
|
270
|
+
err
|
|
271
|
+
}, "webhookPlugin: persisted subscription failed the URL policy — excluded from dispatch. Fix the URL or unregister the subscription.");
|
|
272
|
+
}
|
|
273
|
+
subscriptions = admitted;
|
|
274
|
+
}
|
|
135
275
|
async function dispatchEvent(event) {
|
|
136
276
|
const matching = subscriptions.filter((s) => matchesPattern(s.events, event.type));
|
|
137
277
|
if (matching.length === 0) return;
|
|
@@ -148,7 +288,12 @@ const webhookPlugin = async (fastify, opts = {}) => {
|
|
|
148
288
|
timestamp: /* @__PURE__ */ new Date()
|
|
149
289
|
};
|
|
150
290
|
try {
|
|
151
|
-
const
|
|
291
|
+
const timestamp = Date.now();
|
|
292
|
+
const v1Signature = signWebhook(body, sub.secret, {
|
|
293
|
+
timestamp,
|
|
294
|
+
deliveryId: event.meta.id
|
|
295
|
+
});
|
|
296
|
+
const legacySignature = signPayload(body, sub.secret);
|
|
152
297
|
const ac = new AbortController();
|
|
153
298
|
const timer = setTimeout(() => ac.abort(), timeout);
|
|
154
299
|
try {
|
|
@@ -156,12 +301,16 @@ const webhookPlugin = async (fastify, opts = {}) => {
|
|
|
156
301
|
method: "POST",
|
|
157
302
|
headers: {
|
|
158
303
|
"content-type": "application/json",
|
|
159
|
-
"x-webhook-signature":
|
|
304
|
+
"x-arc-webhook-signature": v1Signature,
|
|
305
|
+
"x-arc-webhook-timestamp": String(timestamp),
|
|
306
|
+
"x-arc-webhook-delivery": event.meta.id,
|
|
307
|
+
"x-webhook-signature": legacySignature,
|
|
160
308
|
"x-webhook-id": event.meta.id,
|
|
161
309
|
"x-webhook-event": event.type
|
|
162
310
|
},
|
|
163
311
|
body,
|
|
164
|
-
signal: ac.signal
|
|
312
|
+
signal: ac.signal,
|
|
313
|
+
...opts.validateUrl ? { redirect: "manual" } : {}
|
|
165
314
|
});
|
|
166
315
|
record.success = response.ok;
|
|
167
316
|
record.status = response.status;
|
|
@@ -184,6 +333,13 @@ const webhookPlugin = async (fastify, opts = {}) => {
|
|
|
184
333
|
if (fastify.events) unsubscribe = await fastify.events.subscribe("*", dispatchEvent);
|
|
185
334
|
fastify.decorate("webhooks", {
|
|
186
335
|
async register(sub) {
|
|
336
|
+
let parsed;
|
|
337
|
+
try {
|
|
338
|
+
parsed = new URL(sub.url);
|
|
339
|
+
} catch {
|
|
340
|
+
throw new Error(`webhookPlugin: invalid webhook URL '${sub.url}'`);
|
|
341
|
+
}
|
|
342
|
+
if (opts.validateUrl) await opts.validateUrl(parsed, sub);
|
|
187
343
|
await store.save(sub);
|
|
188
344
|
subscriptions = subscriptions.filter((s) => s.id !== sub.id);
|
|
189
345
|
subscriptions.push(sub);
|
|
@@ -193,7 +349,10 @@ const webhookPlugin = async (fastify, opts = {}) => {
|
|
|
193
349
|
subscriptions = subscriptions.filter((s) => s.id !== id);
|
|
194
350
|
},
|
|
195
351
|
list() {
|
|
196
|
-
return
|
|
352
|
+
return subscriptions.map(({ secret: _secret, ...pub }) => pub);
|
|
353
|
+
},
|
|
354
|
+
getSecret(id) {
|
|
355
|
+
return subscriptions.find((s) => s.id === id)?.secret;
|
|
197
356
|
},
|
|
198
357
|
deliveryLog(limit) {
|
|
199
358
|
if (limit) return log.slice(-limit);
|
|
@@ -213,4 +372,4 @@ var webhooks_default = fp(webhookPlugin, {
|
|
|
213
372
|
dependencies: ["arc-events"]
|
|
214
373
|
});
|
|
215
374
|
//#endregion
|
|
216
|
-
export { webhooks_default as default, signPayload, verifySignature, webhookPlugin };
|
|
375
|
+
export { webhooks_default as default, defaultWebhookUrlPolicy, signPayload, signWebhook, verifySignature, verifyWebhook, webhookPlugin };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/idempotency/stores/interface.ts
|
|
2
|
+
/**
|
|
3
|
+
* Helper to create a result object
|
|
4
|
+
*/
|
|
5
|
+
function createIdempotencyResult(statusCode, body, headers, ttlMs) {
|
|
6
|
+
const now = /* @__PURE__ */ new Date();
|
|
7
|
+
return {
|
|
8
|
+
statusCode,
|
|
9
|
+
headers,
|
|
10
|
+
body,
|
|
11
|
+
createdAt: now,
|
|
12
|
+
expiresAt: new Date(now.getTime() + ttlMs)
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
export { createIdempotencyResult as t };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { t as ArcError } from "./errors-DkO8Nk6-.mjs";
|
|
2
|
+
import sjson from "secure-json-parse";
|
|
3
|
+
//#region src/utils/jsonBody.ts
|
|
4
|
+
/**
|
|
5
|
+
* Shared JSON request-body parsing for every arc content-type parser
|
|
6
|
+
* (`application/json`, `application/scim+json`, decrypted `application/jose`).
|
|
7
|
+
*
|
|
8
|
+
* One primitive, one contract:
|
|
9
|
+
* - empty body → `undefined` (DELETE/GET with `Content-Type: application/json`
|
|
10
|
+
* must not crash — the original reason arc replaces Fastify's parser)
|
|
11
|
+
* - prototype-poisoning payloads (`__proto__`, `constructor.prototype`)
|
|
12
|
+
* rejected via `secure-json-parse`, same protection Fastify's stock
|
|
13
|
+
* parser has
|
|
14
|
+
* - ANY parse failure → `ArcError` with `statusCode: 400` /
|
|
15
|
+
* `code: 'arc.bad_request'`. Client-sent bytes can never produce a 500:
|
|
16
|
+
* Fastify forwards parser errors to the error handler as-is, so an
|
|
17
|
+
* undecorated `SyntaxError` would fall through to the
|
|
18
|
+
* `arc.internal_error` fallback. The raw parser message stays on
|
|
19
|
+
* `cause` (logged, never on the wire).
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Parse a JSON request body with prototype-poisoning protection and the
|
|
23
|
+
* arc 400 error contract.
|
|
24
|
+
*
|
|
25
|
+
* @param raw - Body text (string or Buffer). Empty/absent → `undefined`.
|
|
26
|
+
* @param message - Wire-safe error message for parse failures
|
|
27
|
+
* (default: `"Invalid JSON payload"`).
|
|
28
|
+
*/
|
|
29
|
+
function parseJsonBody(raw, message = "Invalid JSON payload") {
|
|
30
|
+
const text = typeof raw === "string" ? raw : raw ? raw.toString("utf8") : "";
|
|
31
|
+
if (text.length === 0) return void 0;
|
|
32
|
+
try {
|
|
33
|
+
return sjson.parse(text);
|
|
34
|
+
} catch (cause) {
|
|
35
|
+
throw new ArcError(message, {
|
|
36
|
+
code: "arc.bad_request",
|
|
37
|
+
statusCode: 400,
|
|
38
|
+
cause
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { parseJsonBody as t };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as ResourceRegistry } from "./ResourceRegistry-
|
|
1
|
+
import { t as ResourceRegistry } from "./ResourceRegistry-y9Hnypus.mjs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
//#region src/cli/utils/loadResourcesFromEntry.ts
|
package/dist/logger/index.d.mts
CHANGED
|
@@ -77,5 +77,30 @@ declare function configureArcLogger(options: ArcLoggerOptions): void;
|
|
|
77
77
|
* ```
|
|
78
78
|
*/
|
|
79
79
|
declare function arcLog(module: string): ArcLogger;
|
|
80
|
+
/**
|
|
81
|
+
* Minimal structural slice of a pino-style logger (`fastify.log`). Each
|
|
82
|
+
* level accepts `(msg)` or `(mergeObject, msg)` — the pino calling
|
|
83
|
+
* convention, which differs from console's variadic `(...args)`.
|
|
84
|
+
*/
|
|
85
|
+
interface PinoLike {
|
|
86
|
+
debug: (obj: unknown, msg?: string) => void;
|
|
87
|
+
info: (obj: unknown, msg?: string) => void;
|
|
88
|
+
warn: (obj: unknown, msg?: string) => void;
|
|
89
|
+
error: (obj: unknown, msg?: string) => void;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Bridge an `ArcLogWriter` onto a pino-style logger (`fastify.log`).
|
|
93
|
+
*
|
|
94
|
+
* arcLog call sites use the console convention — positional args like
|
|
95
|
+
* `log.warn("message", { userId })` — which pino would silently drop
|
|
96
|
+
* (extra args are printf interpolation values there). The bridge folds
|
|
97
|
+
* string args into the message, merges object args into the pino merge
|
|
98
|
+
* object, and maps `Error` instances to `{ err }` so pino's error
|
|
99
|
+
* serializer applies.
|
|
100
|
+
*
|
|
101
|
+
* `createApp` installs this automatically so arc-internal warnings flow
|
|
102
|
+
* through the host's transports, level, and redaction config.
|
|
103
|
+
*/
|
|
104
|
+
declare function createPinoWriter(logger: PinoLike): ArcLogWriter;
|
|
80
105
|
//#endregion
|
|
81
|
-
export { ArcLogWriter, ArcLogger, ArcLoggerOptions, arcLog, configureArcLogger };
|
|
106
|
+
export { ArcLogWriter, ArcLogger, ArcLoggerOptions, PinoLike, arcLog, configureArcLogger, createPinoWriter };
|
package/dist/logger/index.mjs
CHANGED
|
@@ -44,6 +44,44 @@ function arcLog(module) {
|
|
|
44
44
|
}
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Bridge an `ArcLogWriter` onto a pino-style logger (`fastify.log`).
|
|
49
|
+
*
|
|
50
|
+
* arcLog call sites use the console convention — positional args like
|
|
51
|
+
* `log.warn("message", { userId })` — which pino would silently drop
|
|
52
|
+
* (extra args are printf interpolation values there). The bridge folds
|
|
53
|
+
* string args into the message, merges object args into the pino merge
|
|
54
|
+
* object, and maps `Error` instances to `{ err }` so pino's error
|
|
55
|
+
* serializer applies.
|
|
56
|
+
*
|
|
57
|
+
* `createApp` installs this automatically so arc-internal warnings flow
|
|
58
|
+
* through the host's transports, level, and redaction config.
|
|
59
|
+
*/
|
|
60
|
+
function createPinoWriter(logger) {
|
|
61
|
+
const emit = (level) => (...args) => {
|
|
62
|
+
const parts = [];
|
|
63
|
+
let merge;
|
|
64
|
+
for (const arg of args) if (typeof arg === "string") parts.push(arg);
|
|
65
|
+
else if (arg instanceof Error) merge = {
|
|
66
|
+
...merge ?? {},
|
|
67
|
+
err: arg
|
|
68
|
+
};
|
|
69
|
+
else if (arg !== null && typeof arg === "object") merge = {
|
|
70
|
+
...merge ?? {},
|
|
71
|
+
...arg
|
|
72
|
+
};
|
|
73
|
+
else parts.push(String(arg));
|
|
74
|
+
const msg = parts.join(" ");
|
|
75
|
+
if (merge) logger[level](merge, msg);
|
|
76
|
+
else logger[level](msg);
|
|
77
|
+
};
|
|
78
|
+
return {
|
|
79
|
+
debug: emit("debug"),
|
|
80
|
+
info: emit("info"),
|
|
81
|
+
warn: emit("warn"),
|
|
82
|
+
error: emit("error")
|
|
83
|
+
};
|
|
84
|
+
}
|
|
47
85
|
function getWriter() {
|
|
48
86
|
return globalOptions.writer ?? console;
|
|
49
87
|
}
|
|
@@ -68,4 +106,4 @@ function isSuppressed() {
|
|
|
68
106
|
return env === "1" || env === "true";
|
|
69
107
|
}
|
|
70
108
|
//#endregion
|
|
71
|
-
export { arcLog, configureArcLogger };
|
|
109
|
+
export { arcLog, configureArcLogger, createPinoWriter };
|