@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.
Files changed (115) hide show
  1. package/dist/{BaseController-BnLYgE9A.mjs → BaseController-BQ8KNvWC.mjs} +3 -2
  2. package/dist/{ResourceRegistry-7RnrEWNP.mjs → ResourceRegistry-y9Hnypus.mjs} +1 -0
  3. package/dist/{actionPermissions-ze70xXJi.mjs → actionPermissions-lszoJle1.mjs} +1 -1
  4. package/dist/applyPermissionResult-Tw4iXtlf.mjs +264 -0
  5. package/dist/audit/index.d.mts +2 -2
  6. package/dist/auth/index.d.mts +30 -2
  7. package/dist/auth/index.mjs +6 -3
  8. package/dist/cli/commands/describe.d.mts +1 -1
  9. package/dist/cli/commands/describe.mjs +1 -1
  10. package/dist/cli/commands/docs.mjs +2 -2
  11. package/dist/cli/commands/init.mjs +1 -1
  12. package/dist/cli/commands/introspect.mjs +1 -1
  13. package/dist/cli/index.mjs +1 -1
  14. package/dist/core/index.d.mts +2 -2
  15. package/dist/core/index.mjs +3 -3
  16. package/dist/{core-DZ6Xe1jR.mjs → core-DQ6rDTyH.mjs} +126 -68
  17. package/dist/{createActionRouter-BWMDW-dL.mjs → createActionRouter-BuOv-m4Y.mjs} +10 -6
  18. package/dist/{createAggregationRouter-l5mNdYrZ.mjs → createAggregationRouter-CgJv_BU_.mjs} +3 -3
  19. package/dist/{createApp-Bi61XxqU.mjs → createApp-CjOeWJ9_.mjs} +252 -57
  20. package/dist/docs/index.d.mts +1 -1
  21. package/dist/docs/index.mjs +1 -1
  22. package/dist/encryption/index.mjs +9 -5
  23. package/dist/{errorHandler-Dxf6P_kV.mjs → errorHandler-Bq2OeEQL.mjs} +34 -10
  24. package/dist/{errorHandler-B0e-bq-O.d.mts → errorHandler-C6yv4ZTW.d.mts} +13 -0
  25. package/dist/events/index.mjs +5 -2
  26. package/dist/factory/index.d.mts +46 -9
  27. package/dist/factory/index.mjs +64 -13
  28. package/dist/{fastify-augmentation-DafigGQp.d.mts → fastify-augmentation-g_f9omSc.d.mts} +2 -2
  29. package/dist/fields-D3lhgzhD.d.mts +121 -0
  30. package/dist/health-ScyjIVo5.mjs +202 -0
  31. package/dist/hooks/index.d.mts +1 -1
  32. package/dist/idempotency/index.d.mts +7 -0
  33. package/dist/idempotency/index.mjs +7 -17
  34. package/dist/{index-Dtrj6XZk.d.mts → index-BBsvQt7U.d.mts} +1310 -1226
  35. package/dist/{types-DpxoNX5v.d.mts → index-Ba10fdKp.d.mts} +196 -66
  36. package/dist/{index-B5N-NaHs.d.mts → index-CC5ruDGh.d.mts} +3 -3
  37. package/dist/{index-B5PoNbfu.d.mts → index-wMoWBLkn.d.mts} +68 -2
  38. package/dist/index.d.mts +6 -5
  39. package/dist/index.mjs +6 -5
  40. package/dist/{init-0YhHkXGk.mjs → init-CjO4p_q1.mjs} +123 -4
  41. package/dist/integrations/event-gateway.mjs +1 -1
  42. package/dist/integrations/index.d.mts +1 -1
  43. package/dist/integrations/mcp/index.d.mts +67 -4
  44. package/dist/integrations/mcp/index.mjs +1 -1
  45. package/dist/integrations/mcp/testing.d.mts +1 -1
  46. package/dist/integrations/mcp/testing.mjs +1 -1
  47. package/dist/integrations/webhooks.d.mts +131 -7
  48. package/dist/integrations/webhooks.mjs +169 -10
  49. package/dist/interface-n5hpKg1d.mjs +16 -0
  50. package/dist/jsonBody-BlzAoGCe.mjs +43 -0
  51. package/dist/{loadResourcesFromEntry-OMCvIEE7.mjs → loadResourcesFromEntry-kvGT6EcO.mjs} +1 -1
  52. package/dist/logger/index.d.mts +26 -1
  53. package/dist/logger/index.mjs +39 -1
  54. package/dist/{mcpPlugin-B9XjjNVG.mjs → mcpPlugin-BtzIL_kM.mjs} +312 -39
  55. package/dist/middleware/index.d.mts +1 -1
  56. package/dist/{module-WeuU5jfd.d.mts → module-B4u10iOJ.d.mts} +26 -1
  57. package/dist/{openapi-Kyod_di6.mjs → openapi-jSDx-VDv.mjs} +3 -3
  58. package/dist/permissions/index.d.mts +4 -3
  59. package/dist/permissions/index.mjs +3 -2
  60. package/dist/{permissions-BJMuKMl7.mjs → permissions-tbxFHxjZ.mjs} +77 -264
  61. package/dist/pipeline/index.d.mts +1 -1
  62. package/dist/plugins/index.d.mts +24 -7
  63. package/dist/plugins/index.mjs +14 -282
  64. package/dist/plugins/response-cache.mjs +1 -0
  65. package/dist/plugins/tracing-entry.mjs +7 -4
  66. package/dist/presets/filesUpload.d.mts +2 -2
  67. package/dist/presets/filesUpload.mjs +1 -1
  68. package/dist/presets/index.d.mts +1 -1
  69. package/dist/presets/index.mjs +1 -1
  70. package/dist/presets/multiTenant.d.mts +1 -1
  71. package/dist/presets/search.d.mts +2 -2
  72. package/dist/presets/search.mjs +1 -1
  73. package/dist/{presets-gw0ciIUD.mjs → presets-BOe4Ugkl.mjs} +1 -1
  74. package/dist/rateLimitKey-BoWRH3e7.mjs +14 -0
  75. package/dist/realtime-I8OrfnMR.mjs +375 -0
  76. package/dist/{registerRoute-oGAl0oA8.mjs → registerRoute-CWO6mdYe.mjs} +38 -18
  77. package/dist/registry/index.d.mts +2 -2
  78. package/dist/registry/index.mjs +1 -1
  79. package/dist/requestId-5BhTVzBv.mjs +113 -0
  80. package/dist/schemaConverter-DJvodiHj.mjs +171 -0
  81. package/dist/{schemaIR-ajetw3Fv.mjs → schemaIR-Ca7ySNPx.mjs} +2 -2
  82. package/dist/scim/index.mjs +6 -7
  83. package/dist/scope/index.mjs +2 -76
  84. package/dist/scope-LONQ3mfZ.mjs +37 -0
  85. package/dist/simpleEqualityMatcher-Cg05HPsn.mjs +68 -0
  86. package/dist/{sse-CfUjwjDk.mjs → sse-0u4P4-OI.mjs} +7 -30
  87. package/dist/sseStream-BQ7Qaynp.mjs +57 -0
  88. package/dist/testing/index.d.mts +83 -3
  89. package/dist/testing/index.mjs +70 -3
  90. package/dist/types/index.d.mts +2 -2
  91. package/dist/{types-DRj-Ut4n.d.mts → types-5OuqeTDX.d.mts} +37 -2
  92. package/dist/{fields-BGqrAa_u.d.mts → types-Csxga7d3.d.mts} +1 -121
  93. package/dist/usage/index.d.mts +142 -0
  94. package/dist/usage/index.mjs +0 -0
  95. package/dist/utils/index.d.mts +101 -19
  96. package/dist/utils/index.mjs +5 -3
  97. package/dist/{utils-CO8a3B_1.mjs → utils-CZyeRXTh.mjs} +80 -82
  98. package/dist/{versioning-DQ4mCvg0.d.mts → versioning-D6PvCwpq.d.mts} +43 -9
  99. package/llms.txt +39 -0
  100. package/package.json +513 -447
  101. package/skills/arc/SKILL.md +14 -8
  102. package/skills/arc/references/api-reference.md +34 -0
  103. package/skills/arc/references/auth.md +21 -4
  104. package/skills/arc/references/mcp.md +1 -1
  105. package/skills/arc/references/production.md +734 -654
  106. package/skills/arc-code-review/SKILL.md +1 -1
  107. package/skills/arc-code-review/references/anti-patterns.md +71 -18
  108. package/skills/arc-code-review/references/arc-cheatsheet.md +15 -2
  109. package/skills/arc-code-review/references/migration-recipes.md +7 -7
  110. package/skills/arc-code-review/references/mongokit-migration.md +4 -3
  111. package/skills/arc-module-publishing/SKILL.md +2 -1
  112. package/skills/arc-module-publishing/references/authoring-modules.md +55 -0
  113. package/skills/arc-module-publishing/references/package-manifest.md +4 -2
  114. package/dist/preloadResources-CHw3_oA_.mjs +0 -45
  115. 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 `@fastify/raw-body` or use `preParsing`
52
- * to capture `req.rawBody`, then pass that here.
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.post('/webhooks/incoming', async (req, reply) => {
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). Register @fastify/raw-body and pass req.rawBody — not req.body.");
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 signature = signPayload(body, sub.secret);
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": 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 [...subscriptions];
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-7RnrEWNP.mjs";
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
@@ -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 };
@@ -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 };