@classytic/arc 2.7.1 → 2.7.3
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/README.md +14 -2
- package/dist/adapters/index.d.mts +2 -2
- package/dist/audit/index.d.mts +1 -1
- package/dist/audit/index.mjs +1 -1
- package/dist/audit/mongodb.d.mts +1 -1
- package/dist/audit/mongodb.mjs +1 -1
- package/dist/auth/index.d.mts +4 -4
- package/dist/auth/redis-session.d.mts +1 -1
- package/dist/cache/index.d.mts +2 -2
- package/dist/cli/commands/docs.mjs +1 -1
- package/dist/cli/commands/generate.mjs +1 -1
- package/dist/core/index.d.mts +2 -2
- package/dist/{createApp-B_nvKNAQ.mjs → createApp-D7e77m8C.mjs} +18 -7
- package/dist/docs/index.d.mts +2 -2
- package/dist/docs/index.mjs +1 -1
- package/dist/dynamic/index.d.mts +2 -2
- package/dist/{errorHandler-DXUttWEO.mjs → errorHandler-CH8wk1eD.mjs} +16 -1
- package/dist/{errorHandler-COa51ho_.d.mts → errorHandler-pCpEtNd7.d.mts} +46 -2
- package/dist/{eventPlugin-DsaNNXzZ.mjs → eventPlugin-B6U_nCFU.mjs} +3 -2
- package/dist/{eventPlugin-BgLxJkIB.d.mts → eventPlugin-CdvUoUna.d.mts} +1 -1
- package/dist/events/index.d.mts +3 -3
- package/dist/events/index.mjs +1 -1
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/factory/index.d.mts +1 -1
- package/dist/factory/index.mjs +1 -1
- package/dist/hooks/index.d.mts +1 -1
- package/dist/idempotency/index.d.mts +3 -3
- package/dist/idempotency/mongodb.d.mts +1 -1
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/{index-BYpRGXif.d.mts → index-B0extFr4.d.mts} +3 -3
- package/dist/{index-KXM8_JmQ.d.mts → index-BjShrzoj.d.mts} +3 -3
- package/dist/{index-StgFaQKD.d.mts → index-C9eYNjGR.d.mts} +1 -1
- package/dist/index.d.mts +8 -7
- package/dist/index.mjs +1 -1
- package/dist/integrations/event-gateway.d.mts +1 -1
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/mcp/index.d.mts +2 -2
- package/dist/integrations/mcp/index.mjs +8 -5
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/webhooks.d.mts +58 -1
- package/dist/integrations/webhooks.mjs +78 -7
- package/dist/integrations/websocket.d.mts +7 -1
- package/dist/integrations/websocket.mjs +7 -1
- package/dist/{interface-Dwzqt4mn.d.mts → interface-B91alUzq.d.mts} +4 -4
- package/dist/{mongodb-Bq90j-Uj.d.mts → mongodb-B7zupyck.d.mts} +1 -1
- package/dist/{mongodb-DdyYlIXg.d.mts → mongodb-Cgu9F1Nd.d.mts} +1 -1
- package/dist/org/index.d.mts +2 -2
- package/dist/permissions/index.d.mts +3 -3
- package/dist/plugins/index.d.mts +52 -5
- package/dist/plugins/index.mjs +5 -4
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/policies/index.d.mts +1 -1
- package/dist/presets/index.d.mts +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/{queryCachePlugin-Bw8XyJpX.d.mts → queryCachePlugin-Ckl71mkc.d.mts} +1 -1
- package/dist/{redis-CyCntzTO.d.mts → redis-3TQxm2VZ.d.mts} +1 -1
- package/dist/{redis-stream-We_Ucl9-.d.mts → redis-stream-Dag5LFa9.d.mts} +1 -1
- package/dist/registry/index.d.mts +1 -1
- package/dist/replyHelpers-uDUIYh7u.mjs +40 -0
- package/dist/{resourceToTools-CkVSSzKg.mjs → resourceToTools-BJkoQoUP.mjs} +11 -5
- package/dist/rpc/index.d.mts +1 -1
- package/dist/scope/index.d.mts +2 -2
- package/dist/testing/index.d.mts +2 -2
- package/dist/testing/index.mjs +1 -1
- package/dist/types/index.d.mts +4 -4
- package/dist/{types-D0qf0Mf4.d.mts → types-2FlNl0mL.d.mts} +44 -9
- package/dist/{types-DPsC0taJ.d.mts → types-B4BNthET.d.mts} +1 -1
- package/dist/{types-ClmkMDK1.d.mts → types-C5g2oRC7.d.mts} +18 -2
- package/dist/utils/index.d.mts +3 -3
- package/package.json +5 -2
- package/skills/arc/SKILL.md +62 -1
- package/skills/arc/references/integrations.md +32 -7
- package/skills/arc/references/mcp.md +31 -7
- package/skills/arc/references/production.md +69 -0
- /package/dist/{EventTransport-CUpRK_Lg.d.mts → EventTransport-C4VheKeC.d.mts} +0 -0
- /package/dist/{circuitBreaker-DwxrljLB.d.mts → circuitBreaker-BBPDt-J_.d.mts} +0 -0
- /package/dist/{elevation-Dm-HTBCt.d.mts → elevation-D7WK0RXq.d.mts} +0 -0
- /package/dist/{errors-CCSsMpXE.d.mts → errors-BS6lZvWy.d.mts} +0 -0
- /package/dist/{externalPaths-Dg7OLsKo.d.mts → externalPaths-iba7jD3d.d.mts} +0 -0
- /package/dist/{fields-CYuLMJPD.d.mts → fields-D4nMDqnK.d.mts} +0 -0
- /package/dist/{interface-CnluRL4_.d.mts → interface-CG7oRZjX.d.mts} +0 -0
- /package/dist/{interface-B9rHWPxD.d.mts → interface-CSbZdv_3.d.mts} +0 -0
- /package/dist/{mongodb-mlgxkYI3.mjs → mongodb-B7X7P1P8.mjs} +0 -0
- /package/dist/{openapi-C5UhIeWu.mjs → openapi-BBSTVcMm.mjs} +0 -0
- /package/dist/{pluralize-COpOVar8.mjs → pluralize-Dckfq6US.mjs} +0 -0
- /package/dist/{sessionManager-IW4sbIea.d.mts → sessionManager-CEo9jwPI.d.mts} +0 -0
- /package/dist/{sse-Bp3dabF1.mjs → sse-6W0hjVS_.mjs} +0 -0
- /package/dist/{tracing-65B51Dw3.d.mts → tracing-DEqdGkr-.d.mts} +0 -0
- /package/dist/{types-CNEbix8T.d.mts → types--D3vvfdt.d.mts} +0 -0
- /package/dist/{versioning-aUUVziBY.mjs → versioning-CdBbFefk.mjs} +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { _ as Authenticator } from "./interface-
|
|
2
|
-
import { n as ElevationOptions } from "./elevation-
|
|
3
|
-
import { t as ExternalOpenApiPaths } from "./externalPaths-
|
|
4
|
-
import { i as CacheStore } from "./interface-
|
|
5
|
-
import { r as QueryCachePluginOptions } from "./queryCachePlugin-
|
|
6
|
-
import { i as EventTransport } from "./EventTransport-
|
|
7
|
-
import { t as EventPluginOptions } from "./eventPlugin-
|
|
8
|
-
import { c as MetricsOptions, d as SSEOptions, m as CachingOptions, r as VersioningOptions, t as ErrorHandlerOptions } from "./errorHandler-
|
|
9
|
-
import { r as IdempotencyStore } from "./interface-
|
|
1
|
+
import { _ as Authenticator } from "./interface-B91alUzq.mjs";
|
|
2
|
+
import { n as ElevationOptions } from "./elevation-D7WK0RXq.mjs";
|
|
3
|
+
import { t as ExternalOpenApiPaths } from "./externalPaths-iba7jD3d.mjs";
|
|
4
|
+
import { i as CacheStore } from "./interface-CG7oRZjX.mjs";
|
|
5
|
+
import { r as QueryCachePluginOptions } from "./queryCachePlugin-Ckl71mkc.mjs";
|
|
6
|
+
import { i as EventTransport } from "./EventTransport-C4VheKeC.mjs";
|
|
7
|
+
import { t as EventPluginOptions } from "./eventPlugin-CdvUoUna.mjs";
|
|
8
|
+
import { c as MetricsOptions, d as SSEOptions, m as CachingOptions, r as VersioningOptions, t as ErrorHandlerOptions } from "./errorHandler-pCpEtNd7.mjs";
|
|
9
|
+
import { r as IdempotencyStore } from "./interface-CSbZdv_3.mjs";
|
|
10
10
|
import { FastifyInstance, FastifyPluginAsync, FastifyReply, FastifyRequest, FastifyServerOptions } from "fastify";
|
|
11
11
|
|
|
12
12
|
//#region src/factory/loadResources.d.ts
|
|
@@ -598,6 +598,41 @@ interface CreateAppOptions {
|
|
|
598
598
|
ajv?: {
|
|
599
599
|
keywords?: string[];
|
|
600
600
|
};
|
|
601
|
+
/**
|
|
602
|
+
* Enable `reply.ok()`, `reply.fail()`, `reply.paginated()` response helpers.
|
|
603
|
+
*
|
|
604
|
+
* Default: `false` (opt-in).
|
|
605
|
+
*
|
|
606
|
+
* @example
|
|
607
|
+
* ```typescript
|
|
608
|
+
* const app = await createApp({ replyHelpers: true });
|
|
609
|
+
*
|
|
610
|
+
* // Then in any handler:
|
|
611
|
+
* return reply.ok({ name: 'MacBook' }); // → 200 { success: true, data: { ... } }
|
|
612
|
+
* return reply.ok(product, 201); // → 201 { success: true, data: { ... } }
|
|
613
|
+
* return reply.fail('Not found', 404); // → 404 { success: false, error: '...' }
|
|
614
|
+
* return reply.fail(['err1', 'err2'], 422); // → 422 { success: false, errors: [...] }
|
|
615
|
+
* return reply.paginated({ docs, total, page, limit });
|
|
616
|
+
* ```
|
|
617
|
+
*/
|
|
618
|
+
replyHelpers?: boolean;
|
|
619
|
+
/**
|
|
620
|
+
* Auto-convert BigInt values to Number in all JSON responses.
|
|
621
|
+
*
|
|
622
|
+
* When `true`, Arc adds a `preSerialization` hook that converts BigInt values
|
|
623
|
+
* to Number before JSON serialization. Without this, `JSON.stringify` throws
|
|
624
|
+
* on BigInt values (e.g., from financial libraries like fin-io).
|
|
625
|
+
*
|
|
626
|
+
* Default: `false` (opt-in — most apps don't use BigInt).
|
|
627
|
+
*
|
|
628
|
+
* @example
|
|
629
|
+
* ```typescript
|
|
630
|
+
* const app = await createApp({
|
|
631
|
+
* serializeBigInt: true,
|
|
632
|
+
* });
|
|
633
|
+
* ```
|
|
634
|
+
*/
|
|
635
|
+
serializeBigInt?: boolean;
|
|
601
636
|
/**
|
|
602
637
|
* Resources to register automatically.
|
|
603
638
|
* Each resource's `.toPlugin()` is called and registered for you.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Vt as ResourceDefinition } from "./interface-
|
|
1
|
+
import { Vt as ResourceDefinition } from "./interface-B91alUzq.mjs";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
|
|
4
4
|
//#region src/integrations/mcp/types.d.ts
|
|
@@ -220,12 +220,28 @@ interface McpSession {
|
|
|
220
220
|
}
|
|
221
221
|
/** Resolved auth identity for a single MCP request */
|
|
222
222
|
interface McpAuthResult {
|
|
223
|
-
|
|
223
|
+
/**
|
|
224
|
+
* Human user ID. Optional for service/machine principals — when `clientId`
|
|
225
|
+
* is set and `userId` is omitted, the principal is purely machine-identity
|
|
226
|
+
* and `ctx.user` will be `null` (not a synthetic user object).
|
|
227
|
+
*/
|
|
228
|
+
userId?: string;
|
|
224
229
|
organizationId?: string;
|
|
225
230
|
/** User roles (global) — used by guard helpers like requireRole() */
|
|
226
231
|
roles?: string[];
|
|
227
232
|
/** Org-level roles — used by guard helpers */
|
|
228
233
|
orgRoles?: string[];
|
|
234
|
+
/**
|
|
235
|
+
* OAuth client ID — set this to enable `kind: "service"` scope.
|
|
236
|
+
* When present, buildRequestContext produces a service scope instead
|
|
237
|
+
* of member/authenticated, enabling `requireServiceScope()` checks.
|
|
238
|
+
*/
|
|
239
|
+
clientId?: string;
|
|
240
|
+
/**
|
|
241
|
+
* OAuth scopes (e.g. `['read:products', 'write:orders']`).
|
|
242
|
+
* Carried on the `service` RequestScope for fine-grained permission checks.
|
|
243
|
+
*/
|
|
244
|
+
scopes?: readonly string[];
|
|
229
245
|
/** Any extra metadata from the auth resolver */
|
|
230
246
|
[key: string]: unknown;
|
|
231
247
|
}
|
package/dist/utils/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { G as OpenApiSchemas, Q as QueryParserInterface, q as ParsedQuery, u as AnyRecord } from "../interface-
|
|
2
|
-
import { a as NotFoundError, c as RateLimitError, d as ValidationError, i as ForbiddenError, l as ServiceUnavailableError, m as isArcError, n as ConflictError, o as OrgAccessDeniedError, p as createError, r as ErrorDetails, s as OrgRequiredError, t as ArcError, u as UnauthorizedError } from "../errors-
|
|
3
|
-
import { a as CircuitBreakerStats, c as createCircuitBreakerRegistry, i as CircuitBreakerRegistry, n as CircuitBreakerError, o as CircuitState, r as CircuitBreakerOptions, s as createCircuitBreaker, t as CircuitBreaker } from "../circuitBreaker-
|
|
1
|
+
import { G as OpenApiSchemas, Q as QueryParserInterface, q as ParsedQuery, u as AnyRecord } from "../interface-B91alUzq.mjs";
|
|
2
|
+
import { a as NotFoundError, c as RateLimitError, d as ValidationError, i as ForbiddenError, l as ServiceUnavailableError, m as isArcError, n as ConflictError, o as OrgAccessDeniedError, p as createError, r as ErrorDetails, s as OrgRequiredError, t as ArcError, u as UnauthorizedError } from "../errors-BS6lZvWy.mjs";
|
|
3
|
+
import { a as CircuitBreakerStats, c as createCircuitBreakerRegistry, i as CircuitBreakerRegistry, n as CircuitBreakerError, o as CircuitState, r as CircuitBreakerOptions, s as createCircuitBreaker, t as CircuitBreaker } from "../circuitBreaker-BBPDt-J_.mjs";
|
|
4
4
|
import { FastifyInstance } from "fastify";
|
|
5
5
|
|
|
6
6
|
//#region src/utils/compensation.d.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/arc",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.3",
|
|
4
4
|
"description": "Resource-oriented backend framework for Fastify — clean, minimal, powerful, tree-shakable",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -212,13 +212,16 @@
|
|
|
212
212
|
"lint:fix": "biome check --fix src/",
|
|
213
213
|
"lint:all": "biome check src/ tests/",
|
|
214
214
|
"test": "vitest run",
|
|
215
|
+
"test:main": "vitest run",
|
|
216
|
+
"test:perf": "node --expose-gc ./node_modules/vitest/vitest.mjs run --config vitest.perf.config.ts",
|
|
217
|
+
"test:ci": "npm run test:main && npm run test:perf",
|
|
215
218
|
"test:watch": "vitest",
|
|
216
219
|
"test:ui": "vitest --ui",
|
|
217
220
|
"test:coverage": "vitest run --coverage",
|
|
218
221
|
"test:e2e": "vitest run tests/e2e",
|
|
219
222
|
"test:unit": "vitest run tests/core tests/hooks tests/utils tests/plugins",
|
|
220
223
|
"smoke": "node scripts/smoke-test.mjs",
|
|
221
|
-
"prepublishOnly": "npm run typecheck && npm test && npm run build && npm run smoke"
|
|
224
|
+
"prepublishOnly": "npm run typecheck && npm run test:ci && npm run build && npm run smoke"
|
|
222
225
|
},
|
|
223
226
|
"engines": {
|
|
224
227
|
"node": ">=22"
|
package/skills/arc/SKILL.md
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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.
|
|
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 `
|
|
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: '
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|