@classytic/arc 2.7.1 → 2.7.7

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 (107) hide show
  1. package/README.md +14 -2
  2. package/dist/{HookSystem-D7lfx--K.mjs → HookSystem-BNYKnrXF.mjs} +3 -2
  3. package/dist/adapters/index.d.mts +2 -2
  4. package/dist/audit/index.d.mts +1 -1
  5. package/dist/audit/index.mjs +1 -1
  6. package/dist/audit/mongodb.d.mts +1 -1
  7. package/dist/audit/mongodb.mjs +1 -1
  8. package/dist/auth/index.d.mts +4 -4
  9. package/dist/auth/index.mjs +2 -2
  10. package/dist/auth/redis-session.d.mts +1 -1
  11. package/dist/{betterAuthOpenApi-CCw3YX0g.mjs → betterAuthOpenApi-EkPaMWNM.mjs} +1 -1
  12. package/dist/cache/index.d.mts +2 -2
  13. package/dist/cli/commands/docs.mjs +1 -1
  14. package/dist/cli/commands/generate.mjs +1 -1
  15. package/dist/core/index.d.mts +2 -2
  16. package/dist/core/index.mjs +2 -2
  17. package/dist/{core-BWekSEju.mjs → core-B_zEeA2b.mjs} +1 -1
  18. package/dist/{createApp-B_nvKNAQ.mjs → createApp-D7e77m8C.mjs} +18 -7
  19. package/dist/{defineResource-DZzyl4a4.mjs → defineResource-BW2dMCu9.mjs} +1 -6
  20. package/dist/docs/index.d.mts +2 -2
  21. package/dist/docs/index.mjs +1 -1
  22. package/dist/dynamic/index.d.mts +2 -2
  23. package/dist/dynamic/index.mjs +1 -1
  24. package/dist/{errorHandler-DXUttWEO.mjs → errorHandler-CH8wk1eD.mjs} +16 -1
  25. package/dist/{errorHandler-COa51ho_.d.mts → errorHandler-pCpEtNd7.d.mts} +46 -2
  26. package/dist/{eventPlugin-DsaNNXzZ.mjs → eventPlugin-B6U_nCFU.mjs} +3 -2
  27. package/dist/{eventPlugin-BgLxJkIB.d.mts → eventPlugin-CdvUoUna.d.mts} +1 -1
  28. package/dist/events/index.d.mts +3 -3
  29. package/dist/events/index.mjs +1 -1
  30. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  31. package/dist/events/transports/redis.d.mts +1 -1
  32. package/dist/factory/index.d.mts +1 -1
  33. package/dist/factory/index.mjs +7 -6
  34. package/dist/hooks/index.d.mts +1 -1
  35. package/dist/hooks/index.mjs +1 -1
  36. package/dist/idempotency/index.d.mts +3 -3
  37. package/dist/idempotency/mongodb.d.mts +1 -1
  38. package/dist/idempotency/mongodb.mjs +1 -3
  39. package/dist/idempotency/redis.d.mts +1 -1
  40. package/dist/{index-BYpRGXif.d.mts → index-B0extFr4.d.mts} +3 -3
  41. package/dist/{index-KXM8_JmQ.d.mts → index-BjShrzoj.d.mts} +3 -3
  42. package/dist/{index-StgFaQKD.d.mts → index-C9eYNjGR.d.mts} +1 -1
  43. package/dist/index.d.mts +8 -7
  44. package/dist/index.mjs +3 -3
  45. package/dist/integrations/event-gateway.d.mts +1 -1
  46. package/dist/integrations/event-gateway.mjs +1 -1
  47. package/dist/integrations/index.d.mts +1 -1
  48. package/dist/integrations/mcp/index.d.mts +2 -2
  49. package/dist/integrations/mcp/index.mjs +8 -5
  50. package/dist/integrations/mcp/testing.d.mts +1 -1
  51. package/dist/integrations/mcp/testing.mjs +1 -1
  52. package/dist/integrations/streamline.d.mts +39 -7
  53. package/dist/integrations/streamline.mjs +106 -4
  54. package/dist/integrations/webhooks.d.mts +58 -1
  55. package/dist/integrations/webhooks.mjs +78 -7
  56. package/dist/integrations/websocket.d.mts +7 -1
  57. package/dist/integrations/websocket.mjs +7 -1
  58. package/dist/{interface-Dwzqt4mn.d.mts → interface-B91alUzq.d.mts} +4 -4
  59. package/dist/{mongodb-Bq90j-Uj.d.mts → mongodb-B7zupyck.d.mts} +1 -1
  60. package/dist/{mongodb-DdyYlIXg.d.mts → mongodb-Cgu9F1Nd.d.mts} +1 -1
  61. package/dist/{openapi-C5UhIeWu.mjs → openapi-D7Z7VODz.mjs} +1 -1
  62. package/dist/org/index.d.mts +2 -2
  63. package/dist/permissions/index.d.mts +3 -3
  64. package/dist/plugins/index.d.mts +52 -5
  65. package/dist/plugins/index.mjs +6 -5
  66. package/dist/plugins/tracing-entry.d.mts +1 -1
  67. package/dist/plugins/tracing-entry.mjs +1 -1
  68. package/dist/policies/index.d.mts +1 -1
  69. package/dist/presets/index.d.mts +1 -1
  70. package/dist/presets/multiTenant.d.mts +1 -1
  71. package/dist/{queryCachePlugin-Bw8XyJpX.d.mts → queryCachePlugin-Ckl71mkc.d.mts} +1 -1
  72. package/dist/{redis-CyCntzTO.d.mts → redis-3TQxm2VZ.d.mts} +1 -1
  73. package/dist/{redis-stream-We_Ucl9-.d.mts → redis-stream-Dag5LFa9.d.mts} +1 -1
  74. package/dist/registry/index.d.mts +1 -1
  75. package/dist/replyHelpers-uDUIYh7u.mjs +40 -0
  76. package/dist/{resourceToTools-CkVSSzKg.mjs → resourceToTools-BJkoQoUP.mjs} +11 -5
  77. package/dist/rpc/index.d.mts +1 -1
  78. package/dist/{schemaConverter-0TyONAwM.mjs → schemaConverter-Y5EejTnJ.mjs} +1 -4
  79. package/dist/scope/index.d.mts +2 -2
  80. package/dist/testing/index.d.mts +2 -2
  81. package/dist/testing/index.mjs +1 -1
  82. package/dist/types/index.d.mts +4 -4
  83. package/dist/{types-DPsC0taJ.d.mts → types-B4BNthET.d.mts} +1 -1
  84. package/dist/{types-ClmkMDK1.d.mts → types-C5g2oRC7.d.mts} +18 -2
  85. package/dist/{types-D0qf0Mf4.d.mts → types-CKB47kiu.d.mts} +48 -9
  86. package/dist/utils/index.d.mts +3 -3
  87. package/dist/utils/index.mjs +1 -1
  88. package/package.json +9 -5
  89. package/skills/arc/SKILL.md +62 -1
  90. package/skills/arc/references/integrations.md +32 -7
  91. package/skills/arc/references/mcp.md +31 -7
  92. package/skills/arc/references/production.md +69 -0
  93. /package/dist/{EventTransport-CUpRK_Lg.d.mts → EventTransport-C4VheKeC.d.mts} +0 -0
  94. /package/dist/{circuitBreaker-DwxrljLB.d.mts → circuitBreaker-BBPDt-J_.d.mts} +0 -0
  95. /package/dist/{elevation-Dm-HTBCt.d.mts → elevation-D7WK0RXq.d.mts} +0 -0
  96. /package/dist/{errors-CCSsMpXE.d.mts → errors-BS6lZvWy.d.mts} +0 -0
  97. /package/dist/{externalPaths-Dg7OLsKo.d.mts → externalPaths-iba7jD3d.d.mts} +0 -0
  98. /package/dist/{fields-CYuLMJPD.d.mts → fields-D4nMDqnK.d.mts} +0 -0
  99. /package/dist/{interface-CnluRL4_.d.mts → interface-CG7oRZjX.d.mts} +0 -0
  100. /package/dist/{interface-B9rHWPxD.d.mts → interface-CSbZdv_3.d.mts} +0 -0
  101. /package/dist/{mongodb-mlgxkYI3.mjs → mongodb-B7X7P1P8.mjs} +0 -0
  102. /package/dist/{pluralize-COpOVar8.mjs → pluralize-Dckfq6US.mjs} +0 -0
  103. /package/dist/{sessionManager-IW4sbIea.d.mts → sessionManager-CEo9jwPI.d.mts} +0 -0
  104. /package/dist/{sse-Bp3dabF1.mjs → sse-6W0hjVS_.mjs} +0 -0
  105. /package/dist/{tracing-65B51Dw3.d.mts → tracing-DEqdGkr-.d.mts} +0 -0
  106. /package/dist/{types-CNEbix8T.d.mts → types--D3vvfdt.d.mts} +0 -0
  107. /package/dist/{versioning-aUUVziBY.mjs → versioning-CdBbFefk.mjs} +0 -0
@@ -8,7 +8,7 @@ description: |
8
8
  Triggers: arc, fastify resource, defineResource, createApp, BaseController, arc preset,
9
9
  arc auth, arc events, arc jobs, arc websocket, arc mcp, arc plugin, arc testing, arc cli,
10
10
  arc permissions, arc hooks, arc pipeline, arc factory, arc cache, arc QueryCache.
11
- version: 2.7.1
11
+ version: 2.7.3
12
12
  license: MIT
13
13
  metadata:
14
14
  author: Classytic
@@ -748,14 +748,26 @@ Connect Claude CLI: `claude mcp add --transport http my-api http://localhost:300
748
748
  **Auth** — three modes, user chooses: `false` | `getAuth()` (Better Auth OAuth 2.1) | custom function:
749
749
 
750
750
  ```typescript
751
+ // Human user auth
751
752
  auth: async (headers) => {
752
753
  if (headers['x-api-key'] !== process.env.MCP_KEY) return null;
753
754
  return { userId: 'bot', organizationId: 'org-1', roles: ['admin'] };
754
755
  },
756
+
757
+ // Service account / machine-to-machine (produces kind: "service" scope)
758
+ auth: async (headers) => ({
759
+ clientId: 'ingestion-pipeline',
760
+ organizationId: 'org-1',
761
+ scopes: ['read:products', 'write:events'],
762
+ }),
755
763
  ```
756
764
 
765
+ `auth: false` → `ctx.user` is `null`, `scope.kind` is `"public"`. Permission guards like `!!ctx.user` correctly block anonymous callers.
766
+
757
767
  **Guards** for custom tools: `guard(requireAuth, requireOrg, requireRole('admin'), handler)`
758
768
 
769
+ **Service scope**: When `clientId` is set in auth result, MCP produces `kind: "service"` RequestScope — works with `requireServiceScope()`, `getClientId()`, `getServiceScopes()`. No synthetic userId needed for machine principals.
770
+
759
771
  **Multi-tenancy**: `organizationId` from auth flows into BaseController org-scoping automatically.
760
772
 
761
773
  **Permission filters**: `PermissionResult.filters` from resource permissions flow into MCP tools — same as REST. Define once, works everywhere:
@@ -881,6 +893,55 @@ additionalRoutes: [{ preAuth: [(req) => { req.headers.authorization = `Bearer ${
881
893
  additionalRoutes: [{ streamResponse: true, handler: async (req, reply) => reply.send(stream) }]
882
894
  ```
883
895
 
896
+ ## DX Helpers (v2.7.3)
897
+
898
+ **Reply helpers** — consistent response envelopes (opt-in via `createApp({ replyHelpers: true })`):
899
+
900
+ ```typescript
901
+ return reply.ok({ name: 'MacBook' }); // → 200 { success: true, data: {...} }
902
+ return reply.ok(product, 201); // → 201 { success: true, data: {...} }
903
+ return reply.fail('Not found', 404); // → 404 { success: false, error: '...' }
904
+ return reply.fail(['err1', 'err2'], 422); // → 422 { success: false, errors: [...] }
905
+ return reply.paginated({ docs, total, page, limit });
906
+ return reply.stream(csvReadable, { contentType: 'text/csv', filename: 'export.csv' });
907
+ ```
908
+
909
+ **Error mappers** — class-based domain error → HTTP response (in `errorHandler` options):
910
+
911
+ ```typescript
912
+ const app = await createApp({
913
+ errorHandler: {
914
+ errorMappers: [{
915
+ type: AccountingError,
916
+ toResponse: (err) => ({ status: err.status, code: err.code, message: err.message }),
917
+ }],
918
+ },
919
+ });
920
+ // Handlers just throw — Arc catches and maps automatically
921
+ ```
922
+
923
+ **BigInt serialization** — opt-in via `createApp({ serializeBigInt: true })`. Converts BigInt → Number in all JSON responses.
924
+
925
+ **Multipart body middleware** — opt-in file upload for CRUD routes:
926
+
927
+ ```typescript
928
+ import { multipartBody } from '@classytic/arc/middleware';
929
+
930
+ defineResource({
931
+ name: 'product',
932
+ adapter,
933
+ middlewares: { create: [multipartBody({ allowedMimeTypes: ['image/png', 'image/jpeg'], maxFileSize: 5 * 1024 * 1024 })] },
934
+ hooks: {
935
+ 'before:create': async (data) => {
936
+ if (data._files?.image) { data.imageUrl = await uploadToS3(data._files.image); delete data._files; }
937
+ return data;
938
+ },
939
+ },
940
+ });
941
+ ```
942
+
943
+ `multipartBody()` is a no-op for JSON requests — safe to always add.
944
+
884
945
  ## Subpath Imports
885
946
 
886
947
  ```typescript
@@ -297,7 +297,7 @@ All optional, gracefully degrade:
297
297
  import { webhookPlugin } from '@classytic/arc/integrations/webhooks';
298
298
  ```
299
299
 
300
- Fastify plugin that auto-dispatches Arc events to customer webhook endpoints with HMAC-SHA256 signing, delivery logging, and pluggable persistence.
300
+ Fastify plugin that auto-dispatches Arc events to customer webhook endpoints with HMAC-SHA256 signing, delivery logging, bounded concurrency, and pluggable persistence.
301
301
 
302
302
  ### Setup
303
303
 
@@ -309,6 +309,7 @@ await fastify.register(webhookPlugin, {
309
309
  store: myMongoWebhookStore, // implements WebhookStore { getAll, save, remove }
310
310
  timeout: 5000, // delivery timeout (default: 10000ms)
311
311
  maxLogEntries: 500, // ring buffer cap (default: 1000)
312
+ concurrency: 10, // max parallel deliveries per event (default: 5)
312
313
  });
313
314
  ```
314
315
 
@@ -338,21 +339,45 @@ await app.events.publish('order.created', { orderId: '123' });
338
339
  // Body: { type, payload, meta }
339
340
  ```
340
341
 
341
- ### HMAC Signing
342
+ Deliveries run with bounded concurrency (default: 5) — one slow endpoint won't block the rest. Set `concurrency: 1` for sequential delivery.
342
343
 
343
- Every delivery is signed with the subscription's secret using HMAC-SHA256:
344
+ ### HMAC Signing & Verification
345
+
346
+ **Outbound** — every delivery is signed with the subscription's secret:
344
347
 
345
348
  ```
346
349
  x-webhook-signature: sha256=a1b2c3...
347
350
  ```
348
351
 
349
- Verify on the receiving end:
352
+ **Inbound** verify with `verifySignature()` (timing-safe, never throws):
353
+
354
+ ```typescript
355
+ import { verifySignature } from '@classytic/arc/integrations/webhooks';
356
+
357
+ fastify.post('/webhooks/incoming', async (req, reply) => {
358
+ const sig = req.headers['x-webhook-signature'] as string;
359
+ if (!verifySignature(req.rawBody, secret, sig)) {
360
+ return reply.status(401).send({ error: 'Invalid signature' });
361
+ }
362
+ // handle event via req.headers['x-webhook-event']
363
+ });
364
+ ```
365
+
366
+ Accepts `string | Buffer` body, `string | undefined` signature. Configurable for non-Arc senders:
367
+
350
368
  ```typescript
351
- import { createHmac } from 'node:crypto';
352
- const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex');
353
- if (expected !== req.headers['x-webhook-signature']) throw new Error('Invalid signature');
369
+ // GitHub (same prefix, same algorithm — works with defaults)
370
+ verifySignature(body, secret, req.headers['x-hub-signature-256']);
371
+
372
+ // Custom algorithm / bare hex
373
+ verifySignature(body, secret, req.headers['x-custom-sig'], {
374
+ prefix: '', // bare hex, no prefix
375
+ algorithm: 'sha512', // non-default algorithm
376
+ });
354
377
  ```
355
378
 
379
+ **Note:** `req.rawBody` requires `fastify-raw-body` — JSON re-serialization breaks HMAC since field ordering differs.
380
+
356
381
  ### Delivery Log
357
382
 
358
383
  ```typescript
@@ -139,7 +139,7 @@ Arc doesn't enforce an auth strategy. You choose what fits.
139
139
  await app.register(mcpPlugin, { resources, auth: false });
140
140
  ```
141
141
 
142
- All tools open. Every request gets `{ userId: 'anonymous' }`.
142
+ All tools open. `ctx.user` is `null`, `scope.kind` is `"public"`. Permission guards like `!!ctx.user` correctly block — anonymous callers cannot bypass auth checks.
143
143
 
144
144
  ### 2. Better Auth OAuth 2.1 (production SaaS)
145
145
 
@@ -175,13 +175,27 @@ type McpAuthResolver = (headers: Record<string, string | undefined>) =>
175
175
  Promise<McpAuthResult | null> | McpAuthResult | null;
176
176
  ```
177
177
 
178
- Return `{ userId, organizationId? }` to allow. Return `null` to reject (401).
178
+ Return `McpAuthResult` to allow. Return `null` to reject (401).
179
+
180
+ **`McpAuthResult` fields:**
181
+ - `userId?` — human user ID (optional for machine principals)
182
+ - `organizationId?` — org scope
183
+ - `roles?` / `orgRoles?` — user roles
184
+ - `clientId?` — set this to produce `kind: "service"` scope (machine-to-machine)
185
+ - `scopes?` — OAuth scopes for service accounts
179
186
 
180
187
  ```typescript
181
- // API key
188
+ // Human user — API key
182
189
  auth: async (headers) => {
183
190
  if (headers['x-api-key'] !== process.env.MCP_API_KEY) return null;
184
- return { userId: 'service', organizationId: 'org-123' };
191
+ return { userId: 'alice', organizationId: 'org-123', roles: ['admin'] };
192
+ },
193
+
194
+ // Machine principal — service account (no userId needed)
195
+ auth: async (headers) => {
196
+ const key = headers['x-service-key'];
197
+ if (key !== process.env.SVC_KEY) return null;
198
+ return { clientId: 'ingestion-pipeline', organizationId: 'org-123', scopes: ['write:events'] };
185
199
  },
186
200
 
187
201
  // Gateway-validated JWT (token already verified upstream)
@@ -191,9 +205,6 @@ auth: async (headers) => {
191
205
  return userId ? { userId, organizationId: orgId } : null;
192
206
  },
193
207
 
194
- // Static org (trusted internal network)
195
- auth: async () => ({ userId: 'internal', organizationId: 'org-main' }),
196
-
197
208
  // Bearer token with custom validation
198
209
  auth: async (headers) => {
199
210
  const token = headers['authorization']?.replace('Bearer ', '');
@@ -203,6 +214,19 @@ auth: async (headers) => {
203
214
  },
204
215
  ```
205
216
 
217
+ ### Service Scope (machine-to-machine)
218
+
219
+ When `clientId` is present in the auth result, Arc produces `kind: "service"` RequestScope:
220
+
221
+ ```
222
+ auth resolver returns { clientId: 'pipeline-v2', organizationId: 'org-a', scopes: ['read:all'] }
223
+ → buildRequestContext sets _scope: { kind: 'service', clientId: 'pipeline-v2', organizationId: 'org-a', scopes: ['read:all'] }
224
+ → ctx.user is null (machine principals don't masquerade as users)
225
+ → isService(scope), getClientId(scope), getServiceScopes(scope) all work
226
+ ```
227
+
228
+ When `userId` is present (without `clientId`), Arc produces `kind: "member"` or `kind: "authenticated"` as before.
229
+
206
230
  ### Multi-Tenancy
207
231
 
208
232
  The `organizationId` from auth flows into BaseController's org-scoping automatically:
@@ -267,6 +267,75 @@ import { errorHandlerPlugin } from '@classytic/arc/plugins';
267
267
  // Auto-registered by createApp()
268
268
  ```
269
269
 
270
+ ### Error Mappers (v2.7.3)
271
+
272
+ Class-based domain error → HTTP response mapping. Register once, handlers just throw:
273
+
274
+ ```typescript
275
+ class AccountingError extends Error {
276
+ constructor(message: string, public status: number, public code: string) { super(message); }
277
+ }
278
+
279
+ const app = await createApp({
280
+ errorHandler: {
281
+ errorMappers: [{
282
+ type: AccountingError,
283
+ toResponse: (err) => ({ status: err.status, code: err.code, message: err.message }),
284
+ }],
285
+ },
286
+ });
287
+ // Now handlers just throw — Arc catches and maps automatically
288
+ ```
289
+
290
+ Mappers are checked via `instanceof` before all other error classification. Multiple mappers supported — first match wins.
291
+
292
+ ## Reply Helpers (v2.7.3)
293
+
294
+ Opt-in response envelope decorators: `createApp({ replyHelpers: true })`
295
+
296
+ ```typescript
297
+ return reply.ok({ name: 'MacBook' }); // → 200 { success: true, data: {...} }
298
+ return reply.ok(product, 201); // → 201 { success: true, data: {...} }
299
+ return reply.fail('Not found', 404); // → 404 { success: false, error: '...' }
300
+ return reply.fail(['err1', 'err2'], 422); // → 422 { success: false, errors: [...] }
301
+ return reply.paginated({ docs, total, page, limit });
302
+ ```
303
+
304
+ ### Streaming Responses
305
+
306
+ ```typescript
307
+ // CSV export
308
+ return reply.stream(csvReadableStream, { contentType: 'text/csv', filename: 'export.csv' });
309
+ // PDF download
310
+ return reply.stream(pdfBuffer, { contentType: 'application/pdf', filename: 'report.pdf' });
311
+ // Raw stream (Fastify native — works without reply helpers too)
312
+ return reply.header('content-type', 'text/csv').send(readableStream);
313
+ ```
314
+
315
+ ### BigInt Serialization
316
+
317
+ Opt-in: `createApp({ serializeBigInt: true })` — auto-converts BigInt → Number in all JSON responses.
318
+
319
+ ## Multipart Body Middleware (v2.7.3)
320
+
321
+ Opt-in file upload support for CRUD routes — parses multipart form fields into `req.body`, attaches files to `req.body._files`:
322
+
323
+ ```typescript
324
+ import { multipartBody } from '@classytic/arc/middleware';
325
+
326
+ defineResource({
327
+ middlewares: { create: [multipartBody({ allowedMimeTypes: ['image/png'], maxFileSize: 5_000_000 })] },
328
+ hooks: {
329
+ 'before:create': async (data) => {
330
+ if (data._files?.image) { data.imageUrl = await uploadToS3(data._files.image); delete data._files; }
331
+ return data;
332
+ },
333
+ },
334
+ });
335
+ ```
336
+
337
+ No-op for JSON requests — safe to always add. Options: `maxFileSize`, `maxFiles`, `allowedMimeTypes`, `filesKey`.
338
+
270
339
  ## Migrations
271
340
 
272
341
  ```typescript
File without changes