@classytic/arc 2.38.0 → 2.41.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-CYaDvmOB.mjs → BaseController-DYJRZ0EJ.mjs} +114 -24
- package/dist/{EventTransport-Cx60_s-6.d.mts → EventTransport-CbUKDzZZ.d.mts} +33 -0
- package/dist/{EventTransport-Gq7JWbWw.mjs → EventTransport-n7qO5Cz3.mjs} +22 -2
- package/dist/{HookSystem-C0ikzqQf.mjs → HookSystem-CxvZBo77.mjs} +2 -2
- package/dist/{ResourceRegistry-DPcX--zk.mjs → ResourceRegistry-Cu7uTJp9.mjs} +2 -2
- package/dist/{actionPermissions-jlFZ1it4.mjs → actionPermissions-DPwPWPOV.mjs} +7 -6
- package/dist/audit/index.d.mts +2 -2
- package/dist/audit/index.mjs +10 -3
- package/dist/auth/audit.mjs +1 -1
- package/dist/auth/index.d.mts +2 -2
- package/dist/auth/index.mjs +12 -6
- package/dist/auth/redis-session.mjs +1 -1
- package/dist/{authorizationDecision-BP8GHtjc.mjs → authorizationDecision-2YY_Kaj1.mjs} +2 -2
- package/dist/{base-C-NL2fTi.d.mts → base-CxrV1LmG.d.mts} +10 -0
- package/dist/{buildHandler-DjXOqIvX.mjs → buildHandler-CUhjGHGZ.mjs} +4 -4
- package/dist/cache/index.d.mts +4 -2
- package/dist/cache/index.mjs +4 -4
- package/dist/{caching-a3ZEcJJs.mjs → caching-DsWDjIxx.mjs} +2 -2
- package/dist/cleanup/index.d.mts +2 -2
- package/dist/cleanup/index.mjs +21 -5
- package/dist/cli/commands/describe.d.mts +1 -1
- package/dist/cli/commands/describe.mjs +3 -3
- package/dist/cli/commands/docs.mjs +3 -3
- package/dist/cli/commands/doctor.mjs +1 -1
- package/dist/cli/commands/generate.mjs +2 -2
- package/dist/cli/commands/init.mjs +2 -2
- package/dist/cli/commands/introspect.mjs +2 -2
- package/dist/cli/index.mjs +3 -3
- package/dist/{constants-BONghE1Y.mjs → constants-BxLgNFF7.mjs} +1 -1
- package/dist/context/index.d.mts +34 -6
- package/dist/context/index.mjs +42 -24
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +7 -7
- package/dist/{core-8She7FgF.mjs → core-iNGqYerO.mjs} +3 -3
- package/dist/{createActionRouter-Bqi6Bhhx.mjs → createActionRouter-BBlbPs3k.mjs} +7 -7
- package/dist/{createAggregationRouter-BsYO7ktd.mjs → createAggregationRouter-Dv5SuK2x.mjs} +7 -7
- package/dist/createMcpServer-CbDWkZ1T.d.mts +48 -0
- package/dist/{defineAggregation-0KBvqEEC.mjs → defineAggregation-UyeurGkp.mjs} +1 -1
- package/dist/{defineResource-BMdyRelW.mjs → defineResource-DzGT-2LP.mjs} +20 -22
- package/dist/discovery/index.mjs +1 -1
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +3 -3
- package/dist/{elevation-C6woysGC.mjs → elevation-BF-vhGxY.mjs} +4 -4
- package/dist/encryption/index.mjs +3 -3
- package/dist/{errorHandler-DI5AN2b5.d.mts → errorHandler--4rSMMvq.d.mts} +7 -1
- package/dist/{errorHandler-BdmIWPba.mjs → errorHandler-CIMkCOWm.mjs} +3 -3
- package/dist/{errors-2ExmOgIJ.mjs → errors-C_YhELhe.mjs} +1 -1
- package/dist/{eventPlugin-DFvHa4Nf.d.mts → eventPlugin-B52abJ8W.d.mts} +1 -1
- package/dist/{eventPlugin-DJwpGMzx.mjs → eventPlugin-CmXaPWLv.mjs} +5 -5
- package/dist/events/index.d.mts +4 -4
- package/dist/events/index.mjs +5 -5
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis-stream-entry.mjs +34 -24
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/events/transports/redis.mjs +11 -8
- package/dist/factory/index.d.mts +5 -5
- package/dist/factory/index.mjs +5 -5
- package/dist/{fastify-augmentation-jYA2oHyL.d.mts → fastify-augmentation-Cq2nHVaW.d.mts} +3 -3
- package/dist/{fields-Cha2QCKH.mjs → fields-BTyPLHK0.mjs} +1 -1
- package/dist/{generate-BUP4Oydg.mjs → generate-D_YmiCNf.mjs} +4 -3
- package/dist/{headers-DeXNifVj.mjs → headers-CNECDE6q.mjs} +2 -2
- package/dist/{health-Bh0N_FLk.mjs → health-CxQzbJmr.mjs} +10 -2
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +2 -2
- package/dist/idempotency/index.d.mts +1 -1
- package/dist/idempotency/index.mjs +4 -4
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/idempotency/redis.mjs +1 -1
- package/dist/{index-BtYoMip1.d.mts → index-BHdZD8zK.d.mts} +10 -2
- package/dist/{index-D2uIUSQk.d.mts → index-CFfLKMH4.d.mts} +2 -2
- package/dist/{index-eyg5WEcy.d.mts → index-CbJiYrfv.d.mts} +1 -1
- package/dist/{index-BRoDtjek.d.mts → index-D4u76eQW.d.mts} +3 -3
- package/dist/{index-B78WZom8.d.mts → index-DHbyPo5j.d.mts} +52 -12
- package/dist/index.d.mts +5 -5
- package/dist/index.mjs +11 -11
- package/dist/{init-Dg_Am2o_.mjs → init-Y2MlSN9n.mjs} +9 -9
- package/dist/integrations/event-gateway.d.mts +1 -1
- package/dist/integrations/event-gateway.mjs +2 -2
- package/dist/integrations/index.d.mts +2 -2
- package/dist/integrations/index.mjs +1 -1
- package/dist/integrations/jobs/index.d.mts +1 -1
- package/dist/integrations/jobs/index.mjs +2 -2
- package/dist/integrations/mcp/index.d.mts +4 -47
- package/dist/integrations/mcp/index.mjs +2 -2
- package/dist/integrations/mcp/testing.d.mts +22 -2
- package/dist/integrations/mcp/testing.mjs +55 -33
- package/dist/integrations/streamline.mjs +3 -3
- package/dist/integrations/webhooks.d.mts +2 -2
- package/dist/integrations/webhooks.mjs +3 -3
- package/dist/integrations/websocket-pushref-redis.d.mts +1 -1
- package/dist/integrations/websocket-pushref-redis.mjs +1 -1
- package/dist/integrations/websocket-redis.mjs +1 -1
- package/dist/integrations/websocket.mjs +24 -6
- package/dist/{interface-DYPVscCU.mjs → interface-BIpgzbZl.mjs} +1 -1
- package/dist/{jsonBody-DVui0uJl.mjs → jsonBody-Cq9KYl2Z.mjs} +2 -2
- package/dist/{keys-BSuxGS-B.mjs → keys-Cmv4cBRa.mjs} +1 -1
- package/dist/{loadResources-CNd1DWnK.mjs → loadResources-CblF7xt2.mjs} +2 -2
- package/dist/{loadResourcesFromEntry-DNj8cpiY.mjs → loadResourcesFromEntry-99fIMC3p.mjs} +2 -2
- package/dist/logger/index.mjs +1 -1
- package/dist/{mcpPlugin-KWsezMx7.mjs → mcpPlugin-xF3Gw-HW.mjs} +75 -31
- package/dist/{memory-BiPVaCPg.mjs → memory-CpDZ9NPw.mjs} +2 -2
- package/dist/{metrics-CsWaIVTa.mjs → metrics-BaKnq8ag.mjs} +2 -2
- package/dist/middleware/index.d.mts +1 -1
- package/dist/middleware/index.mjs +3 -3
- package/dist/migrations/index.mjs +2 -2
- package/dist/{module-BQXHxMEk.mjs → module-BkQEesNQ.mjs} +1 -1
- package/dist/{multipartBody-u6VrS_iH.mjs → multipartBody-C92aJ8am.mjs} +1 -1
- package/dist/{openapi-Dxc8RLy_.mjs → openapi-C3LFRzPq.mjs} +6 -6
- package/dist/outbox-admin/index.d.mts +2 -2
- package/dist/outbox-admin/index.mjs +4 -4
- package/dist/{outbox-module-CQXu_O6o.d.mts → outbox-module-BHdUnuuK.d.mts} +15 -2
- package/dist/{outbox-module-DyIqA5vo.mjs → outbox-module-dsCyLxXH.mjs} +43 -21
- package/dist/permissions/index.d.mts +2 -2
- package/dist/permissions/index.mjs +5 -5
- package/dist/{permissions-H7Zcb82Z.mjs → permissions-DiHXy8Ck.mjs} +12 -6
- package/dist/{pipe-SunhEaZ6.mjs → pipe-CBVmUJ-j.mjs} +2 -2
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/pipeline/index.mjs +2 -2
- package/dist/plugins/index.d.mts +7 -37
- package/dist/plugins/index.mjs +34 -22
- package/dist/plugins/response-cache.mjs +9 -2
- package/dist/plugins/tracing-entry.mjs +2 -2
- package/dist/{pluralize-Da7IT_qJ.mjs → pluralize-2_U6QZQg.mjs} +1 -1
- package/dist/{preloadResources-DlhH81py.mjs → preloadResources-Bbp4QJ8V.mjs} +82 -26
- package/dist/{preloadResources-BbBkNkqK.d.mts → preloadResources-CjS2OfpU.d.mts} +14 -9
- package/dist/presets/filesUpload.d.mts +2 -2
- package/dist/presets/filesUpload.mjs +5 -5
- package/dist/presets/index.d.mts +2 -2
- package/dist/presets/index.mjs +2 -2
- package/dist/presets/multiTenant.d.mts +38 -1
- package/dist/presets/multiTenant.mjs +24 -6
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +2 -2
- package/dist/{presets-CNyfaxGL.mjs → presets-BdlugYTc.mjs} +4 -4
- package/dist/{projection-DSfEoooA.mjs → projection-Uvgp-EEj.mjs} +2 -2
- package/dist/{queryCachePlugin-oTsmVyEQ.mjs → queryCachePlugin-B5F09bYr.mjs} +11 -5
- package/dist/rateLimitKey-CK-EU2L8.mjs +41 -0
- package/dist/{realtime-CPmjjgqd.mjs → realtime-FnnD0n3Z.mjs} +9 -9
- package/dist/{redis-CduhKsJi.d.mts → redis-Bzi1y3F9.d.mts} +10 -2
- package/dist/{redis-stream-eh0eOtjh.d.mts → redis-stream-BPKscsTB.d.mts} +12 -7
- package/dist/{registerRoute-XvcI_3sh.mjs → registerRoute-DRsSHPSM.mjs} +6 -6
- package/dist/registry/index.d.mts +2 -2
- package/dist/registry/index.mjs +3 -3
- package/dist/{registry-UFEJQN8R.mjs → registry-D3y8VEJ1.mjs} +2 -2
- package/dist/{renewingLease-DuRGVOMe.mjs → renewingLease-B3VYh5aU.mjs} +1 -1
- package/dist/{replyHelpers-BHebarf0.mjs → replyHelpers-DPHsdw-5.mjs} +2 -2
- package/dist/{requestContext-DpmntRZy.mjs → requestContext-cSVKnYPk.mjs} +1 -1
- package/dist/{rolldown-runtime-Add2L50I.mjs → rolldown-runtime-DjDuyglE.mjs} +1 -1
- package/dist/{runtimeCapabilities-D793Pxci.mjs → runtimeCapabilities-DSA_dH1_.mjs} +31 -5
- package/dist/{schedules-DthM8Qag.mjs → schedules-BktJb3Y3.mjs} +8 -4
- package/dist/{schemaConverter-Db-MMsH6.mjs → schemaConverter-BLCSmNbr.mjs} +1 -1
- package/dist/{schemaIR-CIXShch7.mjs → schemaIR-K16Bb5BX.mjs} +2 -2
- package/dist/schemas/index.mjs +1 -1
- package/dist/scim/index.mjs +2 -2
- package/dist/scope/index.mjs +5 -5
- package/dist/{scope-D03sk17n.mjs → scope-jSxuBTFM.mjs} +4 -4
- package/dist/{simpleEqualityMatcher-B7qVjQBv.mjs → simpleEqualityMatcher-BiOnyflP.mjs} +1 -1
- package/dist/{sse-D4MYqkic.mjs → sse-CH7Slxc1.mjs} +5 -5
- package/dist/{sseStream-BuMNPR6r.mjs → sseStream-CN-Lh1Tu.mjs} +2 -2
- package/dist/{store-helpers-CIF4RG8s.mjs → store-helpers-9DX9-1j_.mjs} +1 -1
- package/dist/{streaming-DkpW1HwJ.mjs → streaming-dvppnxsr.mjs} +1 -1
- package/dist/{subscribe-helpers-CBZOi6pk.mjs → subscribe-helpers-AVYPgqTl.mjs} +2 -2
- package/dist/sync/changelog.mjs +1 -1
- package/dist/testing/cleanupStoreContract.mjs +1 -1
- package/dist/testing/index.d.mts +5 -5
- package/dist/testing/index.mjs +10 -10
- package/dist/testing/outboxStoreContract.mjs +14 -3
- package/dist/testing/storageContract.mjs +1 -1
- package/dist/{transactionContext-DMmvcfdo.mjs → transactionContext-DCWMI6hV.mjs} +1 -1
- package/dist/{typeGuards-DHTnEs5X.mjs → typeGuards-D7tHFdH6.mjs} +1 -1
- package/dist/types/index.d.mts +2 -2
- package/dist/types/index.mjs +1 -1
- package/dist/types/storage.mjs +1 -1
- package/dist/{types-BqLyyH0o.mjs → types-CDy3-zw1.mjs} +4 -4
- package/dist/{types-LQFwC37d.d.mts → types-CWQn9-IC.d.mts} +46 -1
- package/dist/{types-BRU-Kd3t.d.mts → types-DfVDvkdP.d.mts} +20 -2
- package/dist/usage/index.d.mts +58 -2
- package/dist/usage/index.mjs +0 -0
- package/dist/{userHelpers-CXySQFRg.mjs → userHelpers-DDMOZ_Pm.mjs} +1 -1
- package/dist/utils/index.d.mts +110 -8
- package/dist/utils/index.mjs +12 -12
- package/dist/{utils-BbGG8XNT.mjs → utils-B-zj_b__.mjs} +61 -12
- package/dist/{validate-CidSeUrx.mjs → validate-R0G_JrnK.mjs} +2 -2
- package/dist/{versioning-B843jOYX.d.mts → versioning-D6JIK8Tk.d.mts} +58 -2
- package/dist/{versioning-C2r0eOZ4.mjs → versioning-DuVWcu-F.mjs} +3 -3
- package/dist/workScope-BhsWde4k.mjs +41 -0
- package/package.json +574 -565
- package/skills/arc/SKILL.md +8 -4
- package/skills/arc/references/mcp.md +20 -0
- package/skills/arc/references/production.md +94 -9
- package/skills/arc/references/testing.md +0 -12
- package/skills/arc-code-review/references/anti-patterns.md +1 -1
- package/skills/arc-code-review/references/migration-recipes.md +0 -60
- package/skills/arc-code-review/references/mongokit-migration.md +0 -27
- package/skills/arc-module-publishing/references/package-manifest.md +3 -1
- package/dist/rateLimitKey-CewdR-m6.mjs +0 -15
package/skills/arc/SKILL.md
CHANGED
|
@@ -725,7 +725,7 @@ await app.audit.custom('order', req.params.id, 'refund', { reason }, { user });
|
|
|
725
725
|
|
|
726
726
|
## Usage counters + plan limits (2.22)
|
|
727
727
|
|
|
728
|
-
`@classytic/arc/usage` — `usagePlugin` decorates `fastify.usage` with per-actor, per-UTC-month counters (`record`/`summary`/`period`/`actorOf`; actor chain org → user → client → ip). Auto-tracks `api.requests` (default on) + `api.egress.bytes` (opt-in); recording is fail-safe (a throwing `UsageStore` never fails a request
|
|
728
|
+
`@classytic/arc/usage` — `usagePlugin` decorates `fastify.usage` with per-actor, per-UTC-month counters (`record`/`summary`/`period`/`actorOf`; actor chain org → user → client → ip). Auto-tracks `api.requests` (default on) + `api.egress.bytes` (opt-in); recording is fail-safe (a throwing `UsageStore` never fails a request). Stores: `MemoryUsageStore` (default, single-process) and `RedisUsageStore` (2.41 — `HINCRBY`, the right shape for a hot per-request counter) both from `@classytic/arc/usage`; or a kit's (`@classytic/mongokit/usage`) when counters belong beside domain data. `RedisUsageStore` keeps counters forever by default — `retentionSeconds` is opt-in, because expiring billing history on an unchosen schedule is silent (`summary()` returns `{}` for an expired bucket and a never-written one alike). Pair with `createApp({ rateLimit: { plan: { resolve, limits: { free: { max: 60 }, enterprise: false }, default: 'free' } } })` for per-plan ceilings — `false` = unlimited, unknown/throwing resolvers fall back to `default` then global `max`, and buckets follow the tenant key chain by default. Caveat: the limiter runs before route auth, so `resolve` sees the raw request.
|
|
729
729
|
|
|
730
730
|
## Runtime capability registry (2.33)
|
|
731
731
|
|
|
@@ -737,12 +737,16 @@ import { declareRuntimeCapability } from '@classytic/arc/utils';
|
|
|
737
737
|
declareRuntimeCapability(fastify, {
|
|
738
738
|
subsystem: 'billing.sequence-cache',
|
|
739
739
|
durability: 'memory', // 'memory' | 'shared'
|
|
740
|
-
|
|
741
|
-
|
|
740
|
+
detail: 'invoice numbering cached per process',
|
|
741
|
+
// accepted: true, // ONLY when per-process is intended by design
|
|
742
|
+
// // (logged, never fatal). Must be stated by the
|
|
743
|
+
// // declarant — the audit never infers it.
|
|
742
744
|
});
|
|
743
745
|
```
|
|
744
746
|
|
|
745
|
-
`createApp`'s constructor-time guard only sees what it is passed; state a host wires inside `plugins()` was invisible to it and lived on a checklist. This makes the checklist enforcement.
|
|
747
|
+
`createApp`'s constructor-time guard only sees what it is passed; state a host wires inside `plugins()` was invisible to it and lived on a checklist. This makes the checklist enforcement. Declare from anywhere a registration reaches — `plugins()`, a module phase, or your own encapsulated `fastify.register()`; all land in the one audit (2.41).
|
|
748
|
+
|
|
749
|
+
**Since 2.41 every memory-backed default arc ships declares itself**: `auth.sessions`, `usage.store`, `audit.store`, `cache.query`, `permissions.dynamic-cache` (pass `fastify:` to the matrix — it has no handle of its own), `websocket.adapter`, `websocket.pushref-store`, `webhooks.store`. `http.response-cache` declares `accepted` (per-replica by design). So a distributed app that booted before 2.41 can now fail on a default it was silently running replica-local — the error names each one, and `references/production.md` has the store-by-store fix table.
|
|
746
750
|
|
|
747
751
|
## Adapters
|
|
748
752
|
|
|
@@ -133,6 +133,26 @@ await app.register(mcpPlugin, { resources: catalogResources, prefix: '/mcp/catal
|
|
|
133
133
|
await app.register(mcpPlugin, { resources: orderResources, prefix: '/mcp/orders' });
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
+
## DNS-rebinding protection (`Host` / `Origin`)
|
|
137
|
+
|
|
138
|
+
An MCP endpoint on loopback is reachable from any page the developer visits: if
|
|
139
|
+
an attacker-controlled DNS name resolves to `127.0.0.1`, the browser treats the
|
|
140
|
+
reply as same-origin and can read every tool's output. Validating `Host` breaks
|
|
141
|
+
the chain.
|
|
142
|
+
|
|
143
|
+
`dnsRebindingProtection` **defaults on when `auth: false`** (the local shape the
|
|
144
|
+
attack targets) and **off when auth is configured** (a real deployment's `Host`
|
|
145
|
+
is its own domain — defaulting on would 403 it; credentials are the gate there).
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
mcpPlugin({ resources, auth, dnsRebindingProtection: {} }); // localhost + auth
|
|
149
|
+
mcpPlugin({ resources, dnsRebindingProtection: { allowedHosts: ['mcp.example.com'] } });
|
|
150
|
+
mcpPlugin({ resources, auth: false, dnsRebindingProtection: false }); // deliberately public
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
An absent `Origin` passes — non-browser MCP clients send none. A present but
|
|
154
|
+
unlisted one is refused. `${prefix}/health` is exempt so probes keep working.
|
|
155
|
+
|
|
136
156
|
## Auth — Three Modes
|
|
137
157
|
|
|
138
158
|
Arc doesn't enforce an auth strategy. You choose what fits.
|
|
@@ -46,23 +46,50 @@ await fastify.register(requestIdPlugin, {
|
|
|
46
46
|
|
|
47
47
|
## Graceful Shutdown Plugin
|
|
48
48
|
|
|
49
|
+
Registered by default (`arcPlugins.gracefulShutdown`). Pass an options object to configure it inline from `createApp`.
|
|
50
|
+
|
|
49
51
|
```typescript
|
|
50
52
|
import { gracefulShutdownPlugin } from '@classytic/arc/plugins';
|
|
51
53
|
|
|
52
54
|
await fastify.register(gracefulShutdownPlugin, {
|
|
53
|
-
timeout: 30000,
|
|
55
|
+
timeout: 30000, // hard cap; drainDelayMs counts against it
|
|
56
|
+
drainDelayMs: 10000, // lame-duck window — see below
|
|
54
57
|
signals: ['SIGTERM', 'SIGINT'],
|
|
55
58
|
logEvents: true,
|
|
56
59
|
onShutdown: async () => {
|
|
57
60
|
await mongoose.disconnect();
|
|
58
61
|
await redis.quit();
|
|
59
62
|
},
|
|
63
|
+
onForceExit: () => {}, // tests: stop a timeout from killing the runner
|
|
60
64
|
});
|
|
61
65
|
|
|
62
|
-
// Sequence:
|
|
63
|
-
//
|
|
66
|
+
// Sequence: signal → shutdownState.draining = true (/ready starts 503ing)
|
|
67
|
+
// → wait drainDelayMs (still serving) → fastify.close() (stops accepting,
|
|
68
|
+
// drains in-flight) → onShutdown() → exit naturally
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**`fastify.close()` runs BEFORE `onShutdown`** — so `onShutdown` is where you release what arc doesn't own (DB connections, your own Redis client), and it runs after in-flight requests are done. Don't close anything a request might still need.
|
|
72
|
+
|
|
73
|
+
### `drainDelayMs` — the rolling-deploy 502
|
|
74
|
+
|
|
75
|
+
Default `0`, which is the pre-2.41 behaviour: close the moment SIGTERM arrives. That is the classic rolling-deploy 502 — a load balancer keeps routing to an instance until its readiness probe has failed **for several intervals**, and by then the instance already stopped accepting.
|
|
76
|
+
|
|
77
|
+
`drainDelayMs` is the window between "start failing readiness" and "stop accepting":
|
|
78
|
+
|
|
79
|
+
- `/health/ready` → `503 { status: 'draining' }` immediately on signal
|
|
80
|
+
- `/health/live` → stays `200` (a draining process is alive — that's the point; a 503 here gets the pod **killed**, not drained)
|
|
81
|
+
- everything already routed here is still served for the whole window
|
|
82
|
+
|
|
83
|
+
Set it **above** your LB's deregistration window — typically 5000–15000ms. On Kubernetes, also keep `terminationGracePeriodSeconds` > `drainDelayMs + timeout`, or the kubelet SIGKILLs you mid-drain.
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
createApp({
|
|
87
|
+
arcPlugins: { gracefulShutdown: { drainDelayMs: 10_000, timeout: 30_000 } },
|
|
88
|
+
});
|
|
64
89
|
```
|
|
65
90
|
|
|
91
|
+
**Do not also write `process.on('SIGTERM', () => app.close())`.** The plugin already owns those signals; a second handler closes the server immediately and skips the drain entirely — you get the 502s back while the config says you fixed them.
|
|
92
|
+
|
|
66
93
|
## Audit Plugin
|
|
67
94
|
|
|
68
95
|
Change tracking. Pass a `RepositoryLike` for persistence, or omit for in-memory dev:
|
|
@@ -464,21 +491,79 @@ const app = await createApp({
|
|
|
464
491
|
auth: { type: 'jwt', jwt: { secret: process.env.JWT_SECRET } },
|
|
465
492
|
cors: { origin: process.env.ALLOWED_ORIGINS?.split(',') ?? [], credentials: true },
|
|
466
493
|
rateLimit: { max: 100, timeWindow: '1 minute' },
|
|
467
|
-
arcPlugins: {
|
|
494
|
+
arcPlugins: {
|
|
495
|
+
queryCache: true,
|
|
496
|
+
// Behind a load balancer: fail readiness first, keep serving while it
|
|
497
|
+
// deregisters, then close. No manual process.on('SIGTERM') — the plugin
|
|
498
|
+
// owns the signals, and a second handler skips the drain.
|
|
499
|
+
gracefulShutdown: { drainDelayMs: 10_000 },
|
|
500
|
+
},
|
|
468
501
|
});
|
|
469
|
-
|
|
470
|
-
process.on('SIGTERM', () => app.close());
|
|
471
|
-
process.on('SIGINT', () => app.close());
|
|
472
502
|
```
|
|
473
503
|
|
|
474
504
|
## Distributed Runtime
|
|
475
505
|
|
|
476
|
-
`runtime: 'distributed'` only
|
|
506
|
+
`runtime: 'distributed'` is the "more than one replica" switch. It refuses to boot when something in the app holds state that only exists in **this** process. Two checks, one policy:
|
|
507
|
+
|
|
508
|
+
**1. Constructor-time guard** — what `createApp` can see in its own options, checked before any boot cost:
|
|
477
509
|
|
|
478
510
|
- `stores.events` — always required
|
|
479
511
|
- `stores.cache` — only when `arcPlugins.caching` enabled
|
|
480
512
|
- `stores.queryCache` — only when `arcPlugins.queryCache` enabled
|
|
481
|
-
- `stores.idempotency` —
|
|
513
|
+
- `stores.idempotency` — warn only (per-resource opt-in)
|
|
514
|
+
- `rateLimit` — needs a shared store: `{ redis: client }` or `{ store: CustomStore }`. Both forms count; `rateLimit: false` opts out.
|
|
515
|
+
- `arcPlugins.schedules` — needs `lock` (a `LockAdapter`, for a fleet where any replica may arm schedules) **or** `singleReplica: true` (exactly one replica arms them — the `role: 'scheduler'` deployment pinned at 1). Without one, boot fails: otherwise every replica fires every tick, and duplicate side effects are two invoices, not a stale cache. `singleReplica` is a claim about your deployment — scale that deployment past one replica and it becomes false.
|
|
516
|
+
|
|
517
|
+
**2. Capability audit** (end of boot) — everything wired inside `plugins()`, `bootstrap[]`, or a module phase, which the guard above cannot inspect. Subsystems declare via `declareRuntimeCapability`; an undeclared-shared memory default **fails the boot in one error naming every violator**, so you fix them in one pass rather than one restart each.
|
|
518
|
+
|
|
519
|
+
What declares itself, and what fixes it:
|
|
520
|
+
|
|
521
|
+
| Subsystem | Default | Shared option |
|
|
522
|
+
|---|---|---|
|
|
523
|
+
| `auth.sessions` | `MemorySessionStore` | `RedisSessionStore` (`@classytic/arc/auth/redis`) |
|
|
524
|
+
| `usage.store` | `MemoryUsageStore` | `RedisUsageStore` (`@classytic/arc/usage`) or `@classytic/mongokit/usage` |
|
|
525
|
+
| `audit.store` | `MemoryAuditStore` | `repository: yourRepo` (any kit) |
|
|
526
|
+
| `cache.query` | `MemoryCacheStore` | `RedisCacheStore` (`@classytic/arc/cache`) |
|
|
527
|
+
| `permissions.dynamic-cache` | internal memory | `cacheStore:` — **and pass `fastify:`**, the matrix has no handle of its own so it is invisible to the audit without it |
|
|
528
|
+
| `websocket.adapter` | `LocalWebSocketAdapter` | Redis adapter (`@classytic/arc/integrations/websocket-redis`) |
|
|
529
|
+
| `websocket.pushref-store` | memory | Redis pushRef store |
|
|
530
|
+
| `webhooks.store` | memory | shared store, or durable mode |
|
|
531
|
+
| `http.response-cache` | memory | **nothing — declared `accepted`.** Per-replica by design; correctness comes from short TTLs. Logged, never a failure. |
|
|
532
|
+
|
|
533
|
+
### Circuit breakers count per replica unless you say otherwise
|
|
534
|
+
|
|
535
|
+
`CircuitBreaker` state is in-process. With 50 pods and `failureThreshold: 5`, a dead downstream absorbs **250 real requests** before anything opens — each pod rediscovers the same outage independently. Share the failure window:
|
|
536
|
+
|
|
537
|
+
```typescript
|
|
538
|
+
import { CircuitBreaker, RedisCircuitBreakerState } from '@classytic/arc/utils';
|
|
539
|
+
|
|
540
|
+
const shared = new RedisCircuitBreakerState({ redis }); // fixed window, default 60s
|
|
541
|
+
|
|
542
|
+
const payments = new CircuitBreaker(charge, {
|
|
543
|
+
name: 'stripe.charges', // REQUIRED with sharedState — the key every replica counts under
|
|
544
|
+
failureThreshold: 5,
|
|
545
|
+
sharedState: shared,
|
|
546
|
+
});
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
First replica trips at the threshold; every other trips on its **next** failure. It shares the failure *signal*, not the state machine — each replica still owns its own OPEN/HALF_OPEN probing, which avoids needing consensus over who probes. Nothing touches the network on the success path, the failure path never *waits* on the store, and a store that throws degrades to local counting rather than disabling the breaker.
|
|
550
|
+
|
|
551
|
+
Hosts declare their own too — a replica-local cache you built in `plugins()` belongs in the audit:
|
|
552
|
+
|
|
553
|
+
```typescript
|
|
554
|
+
import { declareRuntimeCapability } from '@classytic/arc/utils';
|
|
555
|
+
|
|
556
|
+
declareRuntimeCapability(fastify, {
|
|
557
|
+
subsystem: 'billing.sequence-cache',
|
|
558
|
+
durability: 'memory', // 'memory' | 'shared'
|
|
559
|
+
detail: 'invoice numbering cached per process',
|
|
560
|
+
// accepted: true, // only when per-process is INTENDED
|
|
561
|
+
});
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
Declare from anywhere a registration reaches — `plugins()`, a module phase, or inside your own encapsulated `fastify.register()`; all land in the one audit. `accepted: true` is for state that is per-process **by design**; it must be stated by the declarant, never inferred.
|
|
565
|
+
|
|
566
|
+
**Migration note:** a distributed app that booted fine before 2.41 can now fail on a default it was silently running replica-local (usage counters and audit trails were the common ones). The boot error names each. That is the bug surfacing, not a new one.
|
|
482
567
|
|
|
483
568
|
## Under-Pressure
|
|
484
569
|
|
|
@@ -198,15 +198,3 @@ runStorageContract(async () => {
|
|
|
198
198
|
3. **Use `ctx.fixtures.clear()` in `afterEach`** — destroyers handle dependency order.
|
|
199
199
|
4. **Test permission denials explicitly** — `expectArc(res).forbidden()` beats status-code assertions.
|
|
200
200
|
5. **Reach for `createHttpTestHarness` first** — 16 tests for one function call.
|
|
201
|
-
|
|
202
|
-
## Migration from pre-2.11 testing APIs
|
|
203
|
-
|
|
204
|
-
| Pre-2.11 | 2.11 |
|
|
205
|
-
|---|---|
|
|
206
|
-
| `TestHarness` / `createTestHarness` | `createHttpTestHarness(resource, ctxFn)` |
|
|
207
|
-
| `TestAppResult` | `TestAppContext` |
|
|
208
|
-
| `testApp.mongoUri` | `ctx.dbUri` |
|
|
209
|
-
| `createJwtAuthProvider` / `createBetterAuthProvider` (as `HttpTestHarness` imports) | `ctx.auth` from `createTestApp` (or direct factory import) |
|
|
210
|
-
| `withTestDb()` | `createTestApp({ db: 'in-memory' })` + `ctx.dbUri` |
|
|
211
|
-
| `TestDatabase` / `TestSeeder` / `TestTransaction` | `createTestFixtures` + kit-level cleanup |
|
|
212
|
-
| `setupBetterAuthOrg` | `setupBetterAuthTestApp` + `helpers.signUpWithOrg` |
|
|
@@ -831,7 +831,7 @@ output_mode: content
|
|
|
831
831
|
|
|
832
832
|
Also: adapter contract types imported from arc — `import type { DataAdapter, AdapterRepositoryInput, AdapterFactory, OpenApiSchemas, SchemaMetadata, FieldMetadata, RelationMetadata, AdapterValidationResult, AdapterSchemaContext } from '@classytic/arc'` (any subpath). And: `mergeFieldRuleConstraints` from `@classytic/arc/adapters`. Any `from '@classytic/arc/adapters'` import — the entire subpath was removed in arc 2.12.
|
|
833
833
|
|
|
834
|
-
**Why critical:**
|
|
834
|
+
**Why critical:** `@classytic/arc/adapters` does not exist. Kit-specific adapters (Mongoose, Drizzle, Prisma) live in their kit; the cross-framework contract lives in `@classytic/repo-core/adapter`. Importing these names from arc fails to resolve — the build breaks at install time. The rule is **strict**: kit-specific things come from the kit, the contract comes from repo-core.
|
|
835
835
|
|
|
836
836
|
**Fix:**
|
|
837
837
|
```typescript
|
|
@@ -4,66 +4,6 @@ Concrete diffs for the most common transformations. Apply in the order listed in
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
## §0. arc 2.x → 3.0 — every kit-specific adapter moves to its kit
|
|
8
|
-
|
|
9
|
-
**Why:** Through arc 2.12, kit-specific adapter factories (`createMongooseAdapter`, `createDrizzleAdapter`, `createPrismaAdapter`) shipped from `@classytic/arc` and arc had a peer dep on `@classytic/mongokit`. This coupled arc's release cadence to every kit's, dragged mongoose into every arc consumer's resolution graph (even Drizzle/Prisma users), and double-published the adapter contract types.
|
|
10
|
-
|
|
11
|
-
In arc 2.12:
|
|
12
|
-
- Adapter contract → `@classytic/repo-core/adapter` (new subpath in repo-core 0.4.0).
|
|
13
|
-
- Mongoose adapter → `@classytic/mongokit/adapter` (3.13.0).
|
|
14
|
-
- Drizzle adapter → `@classytic/sqlitekit/adapter` (0.3.0).
|
|
15
|
-
- Prisma adapter → `@classytic/prismakit/adapter` (0.1.0 — new kit).
|
|
16
|
-
- `mergeFieldRuleConstraints` + `applyNullable` → `@classytic/repo-core/schema`.
|
|
17
|
-
- `@classytic/mongokit`, `@prisma/client`, `mongoose` all dropped from arc's `peerDependencies`. Arc 2.12 has zero kit- or driver-bound peers.
|
|
18
|
-
- The `@classytic/arc/adapters` subpath was removed entirely. The `src/adapters/` directory inside arc is gone.
|
|
19
|
-
- `RepositoryLike` is still re-exported from `@classytic/arc` for convenience.
|
|
20
|
-
- Custom kits implementing `DataAdapter<TDoc>` plug in identically — same contract, no special-casing.
|
|
21
|
-
|
|
22
|
-
### Coordinated versions
|
|
23
|
-
|
|
24
|
-
These are the floors the adapter SPLIT requires — fixed history, safe to rely on:
|
|
25
|
-
|
|
26
|
-
| Package | Min (adapter split) |
|
|
27
|
-
|---|---|
|
|
28
|
-
| `@classytic/arc` | 2.12.0 |
|
|
29
|
-
| `@classytic/repo-core` | 0.4.0 |
|
|
30
|
-
| `@classytic/mongokit` | 3.13.0 |
|
|
31
|
-
| `@classytic/sqlitekit` | 0.3.0 |
|
|
32
|
-
| `@classytic/prismakit` | 0.1.0 (not published to npm — build from source) |
|
|
33
|
-
|
|
34
|
-
A second "current best-practice floor" column used to sit here and is gone: it named
|
|
35
|
-
arc 2.21.0 while arc shipped 2.37, and sqlitekit 0.7.0 against a published 0.9.0. A
|
|
36
|
-
version restated in a skill is wrong within a release and teaches nothing (CLAUDE.md,
|
|
37
|
-
"no rotting metrics") — worse, it reads as researched. **For current floors read
|
|
38
|
-
`peerDependencies` in arc's `package.json` and each kit's own README**; for what a
|
|
39
|
-
given arc version added, [CHANGELOG.md](../../../CHANGELOG.md) is the index.
|
|
40
|
-
|
|
41
|
-
### Import migration table
|
|
42
|
-
|
|
43
|
-
| Old (arc 2.11.x) | New (arc 2.12+) |
|
|
44
|
-
|---|---|
|
|
45
|
-
| `import { createMongooseAdapter } from '@classytic/arc'` | `import { createMongooseAdapter } from '@classytic/mongokit/adapter'` |
|
|
46
|
-
| `import { createMongooseAdapter } from '@classytic/arc/adapters'` | `import { createMongooseAdapter } from '@classytic/mongokit/adapter'` |
|
|
47
|
-
| `import { createDrizzleAdapter } from '@classytic/arc/adapters'` | `import { createDrizzleAdapter } from '@classytic/sqlitekit/adapter'` |
|
|
48
|
-
| `import { createPrismaAdapter } from '@classytic/arc/adapters'` | `import { createPrismaAdapter } from '@classytic/prismakit/adapter'` |
|
|
49
|
-
| `import type { DataAdapter, RepositoryLike, AdapterRepositoryInput } from '@classytic/arc'` | `import type { DataAdapter, RepositoryLike, AdapterRepositoryInput } from '@classytic/repo-core/adapter'` |
|
|
50
|
-
| `import type { ... } from '@classytic/arc/adapters'` (any contract type) | `import type { ... } from '@classytic/repo-core/adapter'` |
|
|
51
|
-
| `import type { InferMongooseDoc } from '@classytic/arc/adapters'` | `import type { InferMongooseDoc } from '@classytic/mongokit/adapter'` |
|
|
52
|
-
| `import { mergeFieldRuleConstraints } from '@classytic/arc/adapters'` | `import { mergeFieldRuleConstraints } from '@classytic/repo-core/schema'` |
|
|
53
|
-
| `MongooseAdapter`, `DrizzleAdapter`, `PrismaAdapter` (classes) | Same path moves: `@classytic/mongokit/adapter` / `@classytic/sqlitekit/adapter` / `@classytic/prismakit/adapter` |
|
|
54
|
-
| Any `from '@classytic/arc/adapters'` import | The subpath was removed in arc 2.12 — re-route to the kit or repo-core per the rows above. |
|
|
55
|
-
|
|
56
|
-
### Mechanical steps
|
|
57
|
-
|
|
58
|
-
1. Bump versions in `package.json` to the matrix above. Drop `mongoose` from explicit deps if you only used it for the adapter — mongokit will pull it as its own peer.
|
|
59
|
-
2. Run a project-wide find/replace using the table above. Detection regex: see anti-patterns.md §32g.
|
|
60
|
-
3. `npx tsc --noEmit` — zero errors.
|
|
61
|
-
4. Smoke test the resource OpenAPI endpoint and one MCP tool — confirm the schema is non-empty (still requires `schemaGenerator`).
|
|
62
|
-
|
|
63
|
-
No runtime behavior change — the symbols are identical, only their package paths moved.
|
|
64
|
-
|
|
65
|
-
---
|
|
66
|
-
|
|
67
7
|
## §1. Manual CRUD module → `defineResource()`
|
|
68
8
|
|
|
69
9
|
**Scope:** typically 5 routes (`GET`, `GET /:id`, `POST`, `PATCH /:id`, `DELETE /:id`) + ~150 LOC of pagination/validation/permission glue per resource.
|
|
@@ -347,33 +347,6 @@ If consumers see `as unknown as RepositoryLike<T>` casts when wiring a repo into
|
|
|
347
347
|
|
|
348
348
|
In the client project, prefer `createMongooseAdapter({ model, repository })` from `@classytic/mongokit/adapter` over manual `RepositoryLike` shaping — it accepts mongokit-native repos with no casts, and since mongokit 3.21 `schemaGenerator` defaults to `buildCrudSchemasFromModel` (`schemaGenerator: false` opts out — see anti-patterns §32f before recommending that).
|
|
349
349
|
|
|
350
|
-
## arc 2.12 / mongokit 3.13.0 — adapter split
|
|
351
|
-
|
|
352
|
-
Through mongokit 3.12 / arc 2.11, `createMongooseAdapter` shipped from `@classytic/arc`. In mongokit 3.13.0 + arc 2.12, the adapter lives in mongokit at `@classytic/mongokit/adapter`. Coordinated minimums:
|
|
353
|
-
|
|
354
|
-
| Package | Min |
|
|
355
|
-
|---|---|
|
|
356
|
-
| `@classytic/arc` | 2.12.0 |
|
|
357
|
-
| `@classytic/mongokit` | 3.13.0 |
|
|
358
|
-
| `@classytic/repo-core` | 0.4.0 |
|
|
359
|
-
|
|
360
|
-
Migration shape:
|
|
361
|
-
|
|
362
|
-
```typescript
|
|
363
|
-
// arc 2.x
|
|
364
|
-
import { createMongooseAdapter } from '@classytic/arc';
|
|
365
|
-
import type { DataAdapter, RepositoryLike, AdapterRepositoryInput } from '@classytic/arc';
|
|
366
|
-
import type { InferMongooseDoc, MongooseAdapterOptions } from '@classytic/arc/adapters';
|
|
367
|
-
|
|
368
|
-
// arc 2.12+
|
|
369
|
-
import { createMongooseAdapter } from '@classytic/mongokit/adapter';
|
|
370
|
-
import type { DataAdapter, RepositoryLike, AdapterRepositoryInput } from '@classytic/repo-core/adapter';
|
|
371
|
-
import type { InferMongooseDoc, MongooseAdapterOptions } from '@classytic/mongokit/adapter';
|
|
372
|
-
```
|
|
373
|
-
|
|
374
|
-
The kit owns mongoose as its peer dep; arc dropped both `@classytic/mongokit` and `mongoose` from its `peerDependencies`. Hosts depend on `@classytic/mongokit` directly, which transitively pulls mongoose. Detection regex for stragglers: see `references/anti-patterns.md` §32g.
|
|
375
|
-
|
|
376
|
-
---
|
|
377
350
|
|
|
378
351
|
## What mongokit does NOT do
|
|
379
352
|
|
|
@@ -46,7 +46,9 @@ optional-peer typing pattern. Values below are the real shapes shipped by
|
|
|
46
46
|
},
|
|
47
47
|
|
|
48
48
|
// Optional peers: declared here AND marked optional below. Host installs only if used.
|
|
49
|
-
//
|
|
49
|
+
// The floor is the major YOUR suite runs — never one below it. A floor you never test
|
|
50
|
+
// is a compatibility claim, not a fact (arc shipped seven of those until 2.40).
|
|
51
|
+
// "peerDependencies": { ..., "zod": ">=4.4.0", "ioredis": ">=6.0.0" },
|
|
50
52
|
// "peerDependenciesMeta": {
|
|
51
53
|
// "zod": { "optional": true },
|
|
52
54
|
// "ioredis": { "optional": true }
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/*! @classytic/arc v2.38.0 | MIT | https://classytic.com */
|
|
2
|
-
//#region src/scope/rateLimitKey.ts
|
|
3
|
-
function createTenantKeyGenerator(opts) {
|
|
4
|
-
if (opts?.strategy) return opts.strategy;
|
|
5
|
-
return (ctx) => {
|
|
6
|
-
const scope = ctx.scope;
|
|
7
|
-
if (!scope || scope.kind === "public") return ctx.ip;
|
|
8
|
-
if (scope.kind === "member") return scope.organizationId;
|
|
9
|
-
if (scope.kind === "service") return scope.organizationId;
|
|
10
|
-
if (scope.kind === "elevated") return scope.organizationId ?? scope.userId ?? ctx.ip;
|
|
11
|
-
return scope.userId ?? ctx.ip;
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
//#endregion
|
|
15
|
-
export { createTenantKeyGenerator as t };
|