@classytic/arc 2.41.1 → 2.43.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/{EventTransport-CbUKDzZZ.d.mts → EventTransport-CiicKz2Z.d.mts} +1 -1
- package/dist/{EventTransport-DSqD_Xsg.mjs → EventTransport-tzA7CAB4.mjs} +22 -17
- package/dist/{HookSystem-CMF94SEA.mjs → HookSystem-B6UaKRbF.mjs} +2 -2
- package/dist/{QueryCache-DFA-QrU9.d.mts → QueryCache--5uAuYPW.d.mts} +7 -3
- package/dist/{ResourceRegistry-MGpXoxHZ.mjs → ResourceRegistry-CuCARu2B.mjs} +44 -13
- package/dist/actionPermissions-BotX1YOa.mjs +776 -0
- package/dist/audit/index.d.mts +43 -5
- package/dist/audit/index.mjs +48 -26
- package/dist/auth/audit.mjs +1 -1
- package/dist/auth/index.d.mts +43 -13
- package/dist/auth/index.mjs +130 -155
- package/dist/auth/redis-session.mjs +1 -1
- package/dist/{authorizationDecision-DawKGoXb.mjs → authorizationDecision-DQYrkl5f.mjs} +7 -102
- package/dist/{base-CxrV1LmG.d.mts → base-CZyixEQR.d.mts} +27 -5
- package/dist/boundedMap-BULzfXYU.mjs +57 -0
- package/dist/{buildHandler-CvxKOc0m.mjs → buildHandler-CRQ8EcX5.mjs} +4 -4
- package/dist/cache/index.d.mts +22 -34
- package/dist/cache/index.mjs +4 -4
- package/dist/{caching-DbCBxlgw.mjs → caching-Cc2M1DwO.mjs} +3 -3
- package/dist/claimed-B10RTfkM.mjs +8 -0
- package/dist/cleanup/index.d.mts +6 -10
- package/dist/cleanup/index.mjs +27 -73
- package/dist/cli/commands/describe.d.mts +4 -2
- package/dist/cli/commands/describe.mjs +7 -4
- 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-D5Ubn1fe.mjs → constants-Cu7Mx7_U.mjs} +4 -2
- package/dist/context/index.d.mts +2 -5
- package/dist/context/index.mjs +18 -38
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +8 -8
- package/dist/{core-5lazhva9.mjs → core-Cb0LjccA.mjs} +5 -4
- package/dist/{createActionRouter-DNqoaK81.mjs → createActionRouter-xjqonMRO.mjs} +87 -39
- package/dist/{createAggregationRouter-BcbQJf10.mjs → createAggregationRouter-zRYWw_5D.mjs} +11 -8
- package/dist/{preloadResources-f3boenVZ.mjs → createApp-D-953ZAq.mjs} +299 -390
- package/dist/{createMcpServer-CbDWkZ1T.d.mts → createMcpServer-C9cxS8zt.d.mts} +1 -1
- package/dist/{defineAggregation-D_eJjm5A.mjs → defineAggregation-Bm8RjMbq.mjs} +1 -1
- package/dist/{defineResource-DxkMTjRn.mjs → defineResource-DIC7JZGU.mjs} +118 -1024
- package/dist/discovery/index.mjs +1 -1
- package/dist/docs/index.d.mts +10 -4
- package/dist/docs/index.mjs +4 -4
- package/dist/{elevation-BXCfVcNG.d.mts → elevation-BW2J3tNj.d.mts} +1 -2
- package/dist/{elevation-CkvG0LG2.mjs → elevation-DG8SrLN-.mjs} +41 -12
- package/dist/encryption/index.mjs +3 -3
- package/dist/{errorHandler-D1GHKGlV.mjs → errorHandler-BnfDviL1.mjs} +26 -5
- package/dist/{errors-BH7YU_9O.mjs → errors-CtrywMHP.mjs} +58 -23
- package/dist/{errors-BGdVwP25.d.mts → errors-JYc9vwqE.d.mts} +9 -0
- package/dist/{eventPlugin-B52abJ8W.d.mts → eventPlugin-BtmA4-3P.d.mts} +65 -9
- package/dist/{eventPlugin-zZY-unhr.mjs → eventPlugin-Q6BhzHzs.mjs} +82 -21
- package/dist/{subscribe-helpers-DatDkcuG.mjs → eventTypes-Cdhm6ZlV.mjs} +151 -17
- package/dist/events/index.d.mts +33 -8
- package/dist/events/index.mjs +49 -53
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis-stream-entry.mjs +139 -157
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/events/transports/redis.mjs +2 -2
- package/dist/factory/index.d.mts +32 -17
- package/dist/factory/index.mjs +213 -32
- package/dist/{fastify-augmentation-Cq2nHVaW.d.mts → fastify-augmentation-DhUJ-mvo.d.mts} +11 -25
- package/dist/{fields-I_EeRWND.d.mts → fields-BBSpf8pS.d.mts} +1 -1
- package/dist/{fields-BHrkoF5y.mjs → fields-ToPYpMqS.mjs} +1 -1
- package/dist/filter-merge-IpGywvym.mjs +99 -0
- package/dist/{generate-BLRf2m3y.mjs → generate-DRTkLlIl.mjs} +2 -2
- package/dist/{headers-BIXWKrzQ.mjs → headers-BbkIF6vA.mjs} +2 -2
- package/dist/health-D-Ft6JM6.mjs +22 -0
- package/dist/{health-ZRHeDHXH.mjs → health-DqbsG-_w.mjs} +115 -55
- package/dist/hooks/index.d.mts +2 -2
- package/dist/hooks/index.mjs +2 -2
- package/dist/idempotency/index.d.mts +31 -7
- package/dist/idempotency/index.mjs +155 -98
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/idempotency/redis.mjs +48 -11
- package/dist/{index-BHdZD8zK.d.mts → index-Bxriq3so.d.mts} +84 -21
- package/dist/{preloadResources-CjS2OfpU.d.mts → index-C8zVGYsk.d.mts} +37 -38
- package/dist/{index-D4u76eQW.d.mts → index-CRoPtISf.d.mts} +23 -73
- package/dist/{index-CbJiYrfv.d.mts → index-Chgz3d0i.d.mts} +1 -1
- package/dist/index-JdbZAIsm.d.mts +510 -0
- package/dist/{index-DHbyPo5j.d.mts → index-mqaqZNpe.d.mts} +253 -254
- package/dist/index.d.mts +8 -8
- package/dist/index.mjs +11 -11
- package/dist/{init-CpLkjU18.mjs → init-B_oUYDXI.mjs} +1 -1
- package/dist/integrations/event-gateway.d.mts +8 -3
- package/dist/integrations/event-gateway.mjs +4 -4
- package/dist/integrations/index.d.mts +3 -3
- package/dist/integrations/index.mjs +1 -1
- package/dist/integrations/jobs/index.d.mts +1 -1
- package/dist/integrations/jobs/index.mjs +18 -5
- package/dist/integrations/mcp/index.d.mts +72 -59
- package/dist/integrations/mcp/index.mjs +19 -4
- package/dist/integrations/mcp/testing.d.mts +2 -2
- package/dist/integrations/mcp/testing.mjs +2 -2
- package/dist/integrations/streamline.d.mts +78 -136
- package/dist/integrations/streamline.mjs +116 -54
- package/dist/integrations/webhooks.d.mts +60 -120
- package/dist/integrations/webhooks.mjs +36 -80
- package/dist/integrations/websocket-pushref-redis.d.mts +1 -1
- package/dist/integrations/websocket-pushref-redis.mjs +1 -1
- package/dist/integrations/websocket-redis.d.mts +1 -1
- package/dist/integrations/websocket-redis.mjs +1 -1
- package/dist/integrations/websocket.d.mts +3 -3
- package/dist/integrations/websocket.mjs +2 -2
- package/dist/{interface-DEWNOMza.d.mts → interface-C0395yUN.d.mts} +13 -1
- package/dist/{jsonBody-ELR9lfeU.mjs → jsonBody-B4YVGcql.mjs} +2 -2
- package/dist/{keys-cpZCtcIE.mjs → keys-B2KtgMLJ.mjs} +7 -18
- package/dist/{loadResources-BPfWiI5s.mjs → loadResources-Dn4lalw2.mjs} +3 -3
- package/dist/{loadResourcesFromEntry-ixtfL3J2.mjs → loadResourcesFromEntry-3sLYbuse.mjs} +2 -2
- package/dist/logger/index.d.mts +1 -8
- package/dist/logger/index.mjs +3 -153
- package/dist/logger-DXjfIqjV.mjs +160 -0
- package/dist/{mcpPlugin-BX5PujI0.mjs → mcpPlugin-C08HalN5.mjs} +433 -170
- package/dist/{memory-Iotz0cCx.mjs → memory-9Cp7611C.mjs} +36 -34
- package/dist/{metrics-NMsmpRmY.mjs → metrics-CWI11eRu.mjs} +39 -72
- package/dist/middleware/index.d.mts +2 -2
- package/dist/middleware/index.mjs +3 -3
- package/dist/migrations/index.mjs +2 -2
- package/dist/{module-DpLJG2_N.mjs → module-BSvOQC7E.mjs} +8 -8
- package/dist/multiTenant-cY0FkDiA.d.mts +104 -0
- package/dist/{multipartBody-DtRWM6_U.mjs → multipartBody-CXM2a-Dy.mjs} +46 -7
- package/dist/{openapi-CmFf3yq_.mjs → openapi-DT_G4VhY.mjs} +48 -10
- package/dist/outbox-admin/index.d.mts +3 -3
- package/dist/outbox-admin/index.mjs +6 -18
- package/dist/{outbox-module-D2z5WkDY.mjs → outbox-module-Bjgzl6tg.mjs} +203 -33
- package/dist/{outbox-module-BHdUnuuK.d.mts → outbox-module-gZCHoE4H.d.mts} +79 -6
- package/dist/permissions/index.d.mts +5 -4
- package/dist/permissions/index.mjs +8 -6
- package/dist/{permissions-D8TBH44E.mjs → permissions-Cm0uYHvd.mjs} +149 -33
- package/dist/{pipe-FMWNheSO.mjs → pipe-BqeGT6UO.mjs} +2 -2
- package/dist/pipeline/index.d.mts +2 -2
- package/dist/pipeline/index.mjs +2 -2
- package/dist/plan-FVR6LP59.mjs +565 -0
- package/dist/{BaseController-CYEwjyjN.mjs → plugin-BccET1tL.mjs} +1256 -345
- package/dist/plugins/index.d.mts +14 -11
- package/dist/plugins/index.mjs +44 -22
- package/dist/plugins/response-cache.mjs +8 -14
- package/dist/plugins/tracing-entry.mjs +2 -2
- package/dist/{pluralize-CXWZIzTv.mjs → pluralize-CVIOcgMF.mjs} +1 -1
- package/dist/presets/filesUpload.d.mts +2 -2
- package/dist/presets/filesUpload.mjs +5 -5
- package/dist/presets/index.d.mts +26 -31
- package/dist/presets/index.mjs +4 -4
- package/dist/presets/multiTenant.d.mts +2 -107
- package/dist/presets/multiTenant.mjs +22 -162
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +2 -2
- package/dist/{presets-ZJSPgmT2.mjs → presets-Cpvj6fGO.mjs} +99 -107
- package/dist/{projection-BzIgzoKZ.mjs → projection-Ce4aQhQk.mjs} +2 -2
- package/dist/{pushref-registry-Dov9l4qE.d.mts → pushref-registry-UxCkoA4C.d.mts} +1 -1
- package/dist/{queryCachePlugin-BzqxconG.d.mts → queryCachePlugin-C2lZjTxk.d.mts} +1 -1
- package/dist/{queryCachePlugin-DZHPOm9p.mjs → queryCachePlugin-CFWNH8aT.mjs} +12 -14
- package/dist/{rateLimitKey-BvOvcBDi.mjs → rateLimitKey-C4O-kPm0.mjs} +1 -1
- package/dist/{realtime-8tIQWZz4.mjs → realtime-DmPE9S54.mjs} +25 -21
- package/dist/redis-stream-BMk-bpQS.d.mts +219 -0
- package/dist/{redis-Bzi1y3F9.d.mts → redis-vLvtcGy_.d.mts} +13 -24
- package/dist/redisShapes-CQksY67q.d.mts +55 -0
- package/dist/registry/index.d.mts +9 -4
- package/dist/registry/index.mjs +4 -4
- package/dist/{registry-DNkgGQcK.mjs → registry-Bcz9G1bh.mjs} +10 -8
- package/dist/{renewingLease-yF8BdW4b.mjs → renewingLease-qODa7KJQ.mjs} +1 -1
- package/dist/{replyHelpers-CNdvMfM4.mjs → replyHelpers-CG_VtVgS.mjs} +2 -2
- package/dist/{requestContext-cDkAUr-P.mjs → requestContext-Dsu9P1bV.mjs} +1 -1
- package/dist/requirement-CbiLdQ1n.d.mts +22 -0
- package/dist/requirement-KDdoRcOP.mjs +35 -0
- package/dist/resourceRateLimit-Wf6d6FL9.d.mts +30 -0
- package/dist/resourceRateLimit-dSjVr1pk.mjs +180 -0
- package/dist/{rolldown-runtime-eysyzFAF.mjs → rolldown-runtime-D1w9Y1VJ.mjs} +1 -1
- package/dist/routeValidator-D36SW8qt.mjs +70 -0
- package/dist/{runtimeCapabilities-CkhS6jn6.mjs → runtimeCapabilities-0tccjRlj.mjs} +1 -1
- package/dist/{schedules-j6uXXAeJ.mjs → schedules-C-OpmHBq.mjs} +46 -37
- package/dist/{schemaConverter-BVA7cFRG.mjs → schemaConverter-BORek3O-.mjs} +2 -2
- package/dist/{schemaIR-CTvPDoPB.mjs → schemaIR-C7Vbp71I.mjs} +2 -2
- package/dist/schemas/index.d.mts +10 -0
- package/dist/schemas/index.mjs +8 -3
- package/dist/scim/index.mjs +2 -2
- package/dist/scope/index.d.mts +2 -2
- package/dist/scope/index.mjs +6 -6
- package/dist/{scope-BUi1xV1I.mjs → scope-DQthe88b.mjs} +4 -4
- package/dist/shutdown-MqA81z_P.mjs +59 -0
- package/dist/{simpleEqualityMatcher-C9bzq2gO.mjs → simpleEqualityMatcher-auW8QjXN.mjs} +1 -1
- package/dist/{sse-BeqVKQKu.mjs → sse-CYzQBVFe.mjs} +11 -15
- package/dist/{sseStream-Ppzo-WUH.mjs → sseStream-B5_Afiqn.mjs} +2 -2
- package/dist/{store-helpers-dW71XVWL.mjs → store-helpers-BOzk-CuX.mjs} +1 -1
- package/dist/{streaming-IcRFjWQU.mjs → streaming-DjTQhZrN.mjs} +1 -1
- package/dist/subscriberIdentity-CW39uA8G.mjs +28 -0
- package/dist/tenancy--ed9eH_z.mjs +43 -0
- package/dist/tenantPreparation-BKon6Yox.mjs +67 -0
- package/dist/testing/cleanupStoreContract.mjs +1 -1
- package/dist/testing/index.d.mts +193 -175
- package/dist/testing/index.mjs +286 -212
- package/dist/testing/outboxStoreContract.mjs +137 -2
- package/dist/testing/requiredPorts.d.mts +17 -0
- package/dist/testing/requiredPorts.mjs +65 -0
- package/dist/testing/storageContract.mjs +1 -1
- package/dist/timing-qczZ0YC-.mjs +46 -0
- package/dist/{transactionContext-CPbdlnVw.mjs → transactionContext-DjFkrNG6.mjs} +1 -1
- package/dist/{typeGuards-BLjPgVBm.mjs → typeGuards-CySF12G-.mjs} +1 -1
- package/dist/types/index.d.mts +4 -4
- package/dist/types/index.mjs +1 -1
- package/dist/types/storage.mjs +1 -1
- package/dist/{types-CWQn9-IC.d.mts → types-B4Aq4Sdj.d.mts} +22 -9
- package/dist/{types-DfVDvkdP.d.mts → types-BJ-2XqVF.d.mts} +189 -204
- package/dist/{types-CgenB1gv.mjs → types-Bk9NOgOC.mjs} +4 -4
- package/dist/usage/index.d.mts +2 -3
- package/dist/usage/index.mjs +5 -5
- package/dist/{userHelpers-ChRvkuSZ.mjs → userHelpers-aklawRaK.mjs} +1 -1
- package/dist/utils/index.d.mts +84 -223
- package/dist/utils/index.mjs +13 -12
- package/dist/{utils-CxcctrAh.mjs → utils-ec1oTo1n.mjs} +315 -404
- package/dist/{validate-DrYWnOzI.mjs → validate-9yzHC1e2.mjs} +6 -14
- package/dist/{versioning-D6JIK8Tk.d.mts → versioning-Bsb51gGT.d.mts} +28 -13
- package/dist/{versioning-DKkT4hNJ.mjs → versioning-fhhWHV74.mjs} +4 -4
- package/dist/{websocket-BOXVggMf.d.mts → websocket-Cs4awVyD.d.mts} +1 -1
- package/dist/{workScope-CxKqoIFn.mjs → workScope-zpPyge4t.mjs} +2 -2
- package/package.json +16 -12
- package/skills/arc/SKILL.md +13 -4
- package/skills/arc/references/auth.md +8 -5
- package/skills/arc/references/integrations.md +1 -1
- package/skills/arc/references/mcp.md +8 -6
- package/skills/arc/references/multi-tenancy.md +19 -14
- package/skills/arc/references/production.md +27 -10
- package/dist/actionPermissions-BoPGQ5-4.mjs +0 -350
- package/dist/index-CFfLKMH4.d.mts +0 -290
- package/dist/interface-YzIbF7Ii.mjs +0 -17
- package/dist/redis-stream-BPKscsTB.d.mts +0 -260
- package/dist/registerRoute-Bsnk6NiZ.mjs +0 -364
- package/dist/sync/changelog.d.mts +0 -3
- package/dist/sync/changelog.mjs +0 -3
|
@@ -1,15 +1,22 @@
|
|
|
1
|
-
/*! @classytic/arc v2.
|
|
2
|
-
import { t as __exportAll } from "./rolldown-runtime-
|
|
3
|
-
import {
|
|
4
|
-
import { n as PUBLIC_SCOPE } from "./types-
|
|
5
|
-
import { r as initRuntimeCapabilityRegistry, t as auditRuntimeCapabilities } from "./runtimeCapabilities-
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { r as
|
|
10
|
-
import {
|
|
1
|
+
/*! @classytic/arc v2.43.0 | MIT | https://classytic.com */
|
|
2
|
+
import { t as __exportAll } from "./rolldown-runtime-D1w9Y1VJ.mjs";
|
|
3
|
+
import { a as enterArcLoggerScope, i as runWithArcLogger, n as configureArcLogger, r as createPinoWriter, t as arcLog } from "./logger-DXjfIqjV.mjs";
|
|
4
|
+
import { n as PUBLIC_SCOPE } from "./types-Bk9NOgOC.mjs";
|
|
5
|
+
import { r as initRuntimeCapabilityRegistry, t as auditRuntimeCapabilities } from "./runtimeCapabilities-0tccjRlj.mjs";
|
|
6
|
+
import { a as runShutdown, t as initShutdownRegistry } from "./shutdown-MqA81z_P.mjs";
|
|
7
|
+
import { n as isScopeAwareKeyGenerator, t as createTenantKeyGenerator } from "./rateLimitKey-C4O-kPm0.mjs";
|
|
8
|
+
import { i as resolveResourceRateLimiter, n as markRateLimitingDisabled } from "./resourceRateLimit-dSjVr1pk.mjs";
|
|
9
|
+
import { r as markArcRootValidator, t as arcAjvCustomOptions } from "./routeValidator-D36SW8qt.mjs";
|
|
10
|
+
import { c as createRequestIdGenerator, r as mergeHealthChecks } from "./health-DqbsG-_w.mjs";
|
|
11
|
+
import { t as parseJsonBody } from "./jsonBody-B4YVGcql.mjs";
|
|
12
|
+
import { a as portsForModule, b as verifyEventDurability, f as resolveModuleArm, g as crudEventsOf, h as buildModuleGraph, m as analyzeModuleGraph, n as composeModules, x as verifyModuleGraph } from "./plan-FVR6LP59.mjs";
|
|
13
|
+
import { c as wrapWithAttribution, f as defineEvent, l as wrapWithBoundary, r as CRUD_EVENT_SCHEMA } from "./eventTypes-Cdhm6ZlV.mjs";
|
|
14
|
+
import { r as withSubscriberId } from "./subscriberIdentity-CW39uA8G.mjs";
|
|
15
|
+
import { r as getOptionalModuleExports } from "./module-BSvOQC7E.mjs";
|
|
16
|
+
import { isProductionEnv, readEnv } from "@classytic/primitives/environment";
|
|
11
17
|
import Fastify from "fastify";
|
|
12
18
|
import qs from "qs";
|
|
19
|
+
import { createEventRouter } from "@classytic/primitives/events";
|
|
13
20
|
//#region src/factory/security/pluginLoader.ts
|
|
14
21
|
const PLUGIN_REGISTRY = {
|
|
15
22
|
cors: {
|
|
@@ -205,47 +212,6 @@ async function registerAssetRoots(fastify, roots) {
|
|
|
205
212
|
}
|
|
206
213
|
}
|
|
207
214
|
//#endregion
|
|
208
|
-
//#region src/factory/module/resolve.ts
|
|
209
|
-
/**
|
|
210
|
-
* Resolve any module-input form to a concrete `ArcModule`. A thunk is invoked
|
|
211
|
-
* (this is where a dynamic `import()` fires); a promise (or plain module) is
|
|
212
|
-
* awaited. Called once per module at the start of the bootstrap phase.
|
|
213
|
-
*/
|
|
214
|
-
async function resolveModule(input) {
|
|
215
|
-
const resolved = typeof input === "function" ? await input() : await input;
|
|
216
|
-
if (!resolved || typeof resolved !== "object" || typeof resolved.name !== "string") throw new Error("[arc] a `modules` entry resolved to something that is not an ArcModule (expected an object with a string `name`). Check dynamic-import thunks return `createXModule(deps)` (or the module object), not the namespace.");
|
|
217
|
-
if (resolved.name.trim().length === 0) throw new Error("[arc] a `modules` entry has an empty (or whitespace-only) `name`. A module's name is its graph key, its `arc.modules` export key, its `dependsOn` target, and its label in every boot diagnostic — give it a stable, non-empty identifier.");
|
|
218
|
-
return resolved;
|
|
219
|
-
}
|
|
220
|
-
/** Resolve a `ModuleContribution` to a concrete array (array passthrough or factory call). */
|
|
221
|
-
async function resolveContribution(contribution, fastify) {
|
|
222
|
-
if (!contribution) return [];
|
|
223
|
-
return typeof contribution === "function" ? await contribution(fastify) : contribution;
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* `resolveContribution` with module attribution — the form every arm collector
|
|
227
|
-
* should use.
|
|
228
|
-
*
|
|
229
|
-
* A contribution factory closes over booted engines, so it throws for exactly
|
|
230
|
-
* the reasons a bootstrap does ("connection unavailable", "engine not ready").
|
|
231
|
-
* Bare, that message names neither the module nor the arm, which in a graph of
|
|
232
|
-
* twenty modules turns a one-line fix into a bisect. Every other module
|
|
233
|
-
* boundary (`plugins`, `bootstrap`, `resources`, `afterResources`) already
|
|
234
|
-
* wraps with the owning module's name; this keeps the arms consistent with
|
|
235
|
-
* them. The original error is preserved via `{ cause }`.
|
|
236
|
-
*/
|
|
237
|
-
async function resolveModuleArm(module, arm, contribution, fastify) {
|
|
238
|
-
let resolved;
|
|
239
|
-
try {
|
|
240
|
-
resolved = await resolveContribution(contribution, fastify);
|
|
241
|
-
} catch (err) {
|
|
242
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
243
|
-
throw new Error(`[arc] module "${module.name}" ${arm} factory threw: ${msg}`, { cause: err });
|
|
244
|
-
}
|
|
245
|
-
if (!Array.isArray(resolved)) throw new TypeError(`[arc] module "${module.name}" ${arm} factory must return an array — received ${resolved === null ? "null" : typeof resolved}.`);
|
|
246
|
-
return resolved;
|
|
247
|
-
}
|
|
248
|
-
//#endregion
|
|
249
215
|
//#region src/factory/module/contributions.ts
|
|
250
216
|
/**
|
|
251
217
|
* Collect every module's `healthChecks` in the caller's order. Fails at boot
|
|
@@ -321,7 +287,15 @@ function describeResolvedModule(input) {
|
|
|
321
287
|
}
|
|
322
288
|
//#endregion
|
|
323
289
|
//#region src/factory/module/disposers.ts
|
|
324
|
-
|
|
290
|
+
const NO_PORTS = Object.freeze({});
|
|
291
|
+
/**
|
|
292
|
+
* The one typing boundary for ports: `assertModuleRequirements` has already proven every
|
|
293
|
+
* declared required port present, so the module's subset is read as its declared shape.
|
|
294
|
+
*/
|
|
295
|
+
function asModulePorts(ports) {
|
|
296
|
+
return ports;
|
|
297
|
+
}
|
|
298
|
+
function createDisposerRegistry(fastify, portsOf = () => NO_PORTS) {
|
|
325
299
|
const stacks = /* @__PURE__ */ new Map();
|
|
326
300
|
const drained = /* @__PURE__ */ new Set();
|
|
327
301
|
const contexts = /* @__PURE__ */ new Map();
|
|
@@ -329,25 +303,28 @@ function createDisposerRegistry(fastify) {
|
|
|
329
303
|
contextFor(moduleName) {
|
|
330
304
|
const existing = contexts.get(moduleName);
|
|
331
305
|
if (existing) return existing;
|
|
332
|
-
const ctx = {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
(
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
306
|
+
const ctx = {
|
|
307
|
+
ports: asModulePorts(portsOf(moduleName)),
|
|
308
|
+
defer(disposer) {
|
|
309
|
+
if (typeof disposer !== "function") throw new TypeError(`[arc] module "${moduleName}" called defer() with a ${typeof disposer} — expected a function.`);
|
|
310
|
+
if (drained.has(moduleName)) {
|
|
311
|
+
(async () => {
|
|
312
|
+
try {
|
|
313
|
+
await disposer();
|
|
314
|
+
} catch (err) {
|
|
315
|
+
fastify.log?.error({
|
|
316
|
+
err,
|
|
317
|
+
module: moduleName
|
|
318
|
+
}, `[arc] module "${moduleName}" deferred a disposer after teardown had already run; it was invoked immediately and threw.`);
|
|
319
|
+
}
|
|
320
|
+
})();
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const stack = stacks.get(moduleName);
|
|
324
|
+
if (stack) stack.push(disposer);
|
|
325
|
+
else stacks.set(moduleName, [disposer]);
|
|
346
326
|
}
|
|
347
|
-
|
|
348
|
-
if (stack) stack.push(disposer);
|
|
349
|
-
else stacks.set(moduleName, [disposer]);
|
|
350
|
-
} };
|
|
327
|
+
};
|
|
351
328
|
contexts.set(moduleName, ctx);
|
|
352
329
|
return ctx;
|
|
353
330
|
},
|
|
@@ -372,7 +349,7 @@ function createDisposerRegistry(fastify) {
|
|
|
372
349
|
* Resolves factory contributions (after bootstraps) so a handler can close over
|
|
373
350
|
* booted engines rather than a global getter.
|
|
374
351
|
*/
|
|
375
|
-
async function subscribeModuleEventHandlers(fastify, modules, countsByModule) {
|
|
352
|
+
async function subscribeModuleEventHandlers(fastify, modules, countsByModule, consumedByModule, handlersByModule) {
|
|
376
353
|
const unsubscribes = [];
|
|
377
354
|
const owner = /* @__PURE__ */ new Map();
|
|
378
355
|
try {
|
|
@@ -382,14 +359,24 @@ async function subscribeModuleEventHandlers(fastify, modules, countsByModule) {
|
|
|
382
359
|
if (handlers.length === 0) continue;
|
|
383
360
|
const bus = fastify.events;
|
|
384
361
|
if (!bus) throw new Error(`[arc] module "${m.name}" declares eventHandlers but the event subsystem is unavailable (fastify.events). Enable arcPlugins.events.`);
|
|
385
|
-
for (const def of handlers) {
|
|
362
|
+
for (const [index, def] of handlers.entries()) {
|
|
386
363
|
if (def.name !== void 0) {
|
|
387
364
|
const prior = owner.get(def.name);
|
|
388
365
|
if (prior !== void 0) throw new Error(`[arc] duplicate event-handler name "${def.name}" — declared by module "${prior}" and module "${m.name}". Named event handlers must be unique across the module graph; prefix names with the owning module.`);
|
|
389
366
|
owner.set(def.name, m.name);
|
|
390
367
|
}
|
|
391
|
-
const handler = applyBoundary(def, m.name, fastify);
|
|
392
368
|
const patterns = Array.isArray(def.event) ? def.event : [def.event];
|
|
369
|
+
if (def.durability === "retry" && def.boundary) throw new Error(`[arc] event handler "${def.name ?? patterns.join(",")}" in module "${m.name}" declares durability 'retry' and a boundary — a boundary swallows, so it is 'boundary'.`);
|
|
370
|
+
const label = handlerLabel(def, m.name, patterns, index);
|
|
371
|
+
const identity = def.name ?? [...patterns].sort().join(",");
|
|
372
|
+
if (!def.name && handlers.some((other) => other !== def && !other.name && (Array.isArray(other.event) ? [...other.event] : [other.event]).sort().join(",") === identity)) throw new Error(`[arc] module "${m.name}" has ambiguous unnamed event handlers for "${identity}" — give each handler a stable name.`);
|
|
373
|
+
const handler = withSubscriberId(`${m.name}:${identity}`, applyBoundary(def, m.name, label, fastify));
|
|
374
|
+
handlersByModule?.set(m.name, [...handlersByModule.get(m.name) ?? [], {
|
|
375
|
+
label,
|
|
376
|
+
patterns,
|
|
377
|
+
...def.durability !== void 0 ? { durability: def.durability } : {}
|
|
378
|
+
}]);
|
|
379
|
+
consumedByModule?.set(m.name, [...consumedByModule.get(m.name) ?? [], ...patterns]);
|
|
393
380
|
for (const pattern of patterns) unsubscribes.push(await bus.subscribe(pattern, handler));
|
|
394
381
|
}
|
|
395
382
|
}
|
|
@@ -401,18 +388,24 @@ async function subscribeModuleEventHandlers(fastify, modules, countsByModule) {
|
|
|
401
388
|
}
|
|
402
389
|
}
|
|
403
390
|
/**
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
391
|
+
* The name a subscriber's errors and logs carry: its declared `name`, else
|
|
392
|
+
* `<module>:<patterns>#<index in the module's handlers>` — unique, so two unnamed
|
|
393
|
+
* subscribers of one event stay distinguishable in a dead-lettered row.
|
|
394
|
+
*/
|
|
395
|
+
function handlerLabel(def, moduleName, patterns, index) {
|
|
396
|
+
return def.name ?? `${moduleName}:${patterns.join(",")}#${index}`;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Without a boundary, attribute the handler's errors to `label` and let them reach the transport —
|
|
400
|
+
* that is what makes the durable path work (unacked → redelivered → DLQ), and the label is what
|
|
401
|
+
* names the subscriber in the outbox's `lastError`. With the opt-in boundary, failures are logged
|
|
402
|
+
* under the handler's name (`<module>.<pattern>` when unnamed) and swallowed, so the original error
|
|
403
|
+
* reaches the boundary's `onError` untouched.
|
|
412
404
|
*/
|
|
413
|
-
function applyBoundary(def, moduleName, fastify) {
|
|
414
|
-
|
|
415
|
-
|
|
405
|
+
function applyBoundary(def, moduleName, label, fastify) {
|
|
406
|
+
const boundary = def.boundary ?? def.durability === "boundary";
|
|
407
|
+
if (!boundary) return wrapWithAttribution(def.handler, label);
|
|
408
|
+
const onError = typeof boundary === "object" ? boundary.onError : void 0;
|
|
416
409
|
return wrapWithBoundary(def.handler, {
|
|
417
410
|
logger: fastify.log,
|
|
418
411
|
name: def.name ?? `${moduleName}.${String(def.event)}`,
|
|
@@ -430,98 +423,6 @@ async function unsubscribeModuleEventHandlers(unsubscribes) {
|
|
|
430
423
|
return errors;
|
|
431
424
|
}
|
|
432
425
|
//#endregion
|
|
433
|
-
//#region src/factory/module/order.ts
|
|
434
|
-
/**
|
|
435
|
-
* Order modules for composition by their `dependsOn` edges — a STABLE
|
|
436
|
-
* topological sort. Called once at the start of the bootstrap phase; every
|
|
437
|
-
* subsequent phase (bootstrap, resources, afterResources, and reverse-order
|
|
438
|
-
* onClose) iterates the returned list, so a module's declared dependencies are
|
|
439
|
-
* always composed before it.
|
|
440
|
-
*
|
|
441
|
-
* "Stable" = modules with no edge between them keep their original list order,
|
|
442
|
-
* so declaring `dependsOn` on one module never silently reorders an unrelated
|
|
443
|
-
* one. Backward compatible: a `modules` array with NO `dependsOn` anywhere is
|
|
444
|
-
* returned unchanged.
|
|
445
|
-
*
|
|
446
|
-
* Fail-fast — throws (never reorders past a broken contract) on:
|
|
447
|
-
* - duplicate module names (the name is the graph key)
|
|
448
|
-
* - a `dependsOn` name not present in the composed set
|
|
449
|
-
* - a self-reference (`dependsOn` includes the module's own name)
|
|
450
|
-
* - a dependency cycle (reports the concrete `a → b → … → a` path)
|
|
451
|
-
*/
|
|
452
|
-
function orderModules(modules) {
|
|
453
|
-
const byName = /* @__PURE__ */ new Map();
|
|
454
|
-
for (const m of modules) {
|
|
455
|
-
if (byName.has(m.name)) throw new Error(`[arc] Duplicate module name "${m.name}" — composed twice; check your modules array.`);
|
|
456
|
-
byName.set(m.name, m);
|
|
457
|
-
}
|
|
458
|
-
for (const m of modules) for (const dep of m.dependsOn ?? []) {
|
|
459
|
-
if (dep === m.name) throw new Error(`[arc] module "${m.name}" dependsOn itself — remove the self-reference.`);
|
|
460
|
-
if (!byName.has(dep)) throw new Error(`[arc] module "${m.name}" dependsOn "${dep}", which is not composed. Add the "${dep}" module to createApp({ modules }) (before this one is fine — arc orders them), or drop the dependency. Composed modules: ${[...byName.keys()].join(", ") || "(none)"}.`);
|
|
461
|
-
}
|
|
462
|
-
if (modules.every((m) => !m.dependsOn || m.dependsOn.length === 0)) return [...modules];
|
|
463
|
-
const originalIndex = /* @__PURE__ */ new Map();
|
|
464
|
-
let nextIndex = 0;
|
|
465
|
-
for (const m of modules) originalIndex.set(m.name, nextIndex++);
|
|
466
|
-
const pendingDeps = new Map(modules.map((m) => [m.name, (m.dependsOn ?? []).length]));
|
|
467
|
-
const dependents = /* @__PURE__ */ new Map();
|
|
468
|
-
for (const m of modules) for (const dep of m.dependsOn ?? []) {
|
|
469
|
-
const list = dependents.get(dep);
|
|
470
|
-
if (list) list.push(m.name);
|
|
471
|
-
else dependents.set(dep, [m.name]);
|
|
472
|
-
}
|
|
473
|
-
const ordered = [];
|
|
474
|
-
const remaining = new Set(modules.map((m) => m.name));
|
|
475
|
-
while (remaining.size > 0) {
|
|
476
|
-
let pick;
|
|
477
|
-
let pickIndex = Number.POSITIVE_INFINITY;
|
|
478
|
-
for (const name of remaining) {
|
|
479
|
-
if (pendingDeps.get(name) !== 0) continue;
|
|
480
|
-
const index = originalIndex.get(name) ?? Number.POSITIVE_INFINITY;
|
|
481
|
-
if (index < pickIndex) {
|
|
482
|
-
pick = name;
|
|
483
|
-
pickIndex = index;
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
if (pick === void 0) throw new Error(describeModuleCycle(remaining, byName));
|
|
487
|
-
const picked = byName.get(pick);
|
|
488
|
-
if (!picked) throw new Error(`[arc] internal: ordered module "${pick}" is missing from the index`);
|
|
489
|
-
ordered.push(picked);
|
|
490
|
-
remaining.delete(pick);
|
|
491
|
-
for (const dependent of dependents.get(pick) ?? []) pendingDeps.set(dependent, (pendingDeps.get(dependent) ?? 1) - 1);
|
|
492
|
-
}
|
|
493
|
-
return ordered;
|
|
494
|
-
}
|
|
495
|
-
/** Walk the still-unordered subgraph for one concrete cycle path. */
|
|
496
|
-
function describeModuleCycle(remaining, byName) {
|
|
497
|
-
const stack = [];
|
|
498
|
-
const onStack = /* @__PURE__ */ new Set();
|
|
499
|
-
const done = /* @__PURE__ */ new Set();
|
|
500
|
-
let cycle = null;
|
|
501
|
-
const walk = (name) => {
|
|
502
|
-
if (cycle) return;
|
|
503
|
-
stack.push(name);
|
|
504
|
-
onStack.add(name);
|
|
505
|
-
for (const dep of byName.get(name)?.dependsOn ?? []) {
|
|
506
|
-
if (!remaining.has(dep)) continue;
|
|
507
|
-
if (onStack.has(dep)) {
|
|
508
|
-
cycle = [...stack.slice(stack.indexOf(dep)), dep];
|
|
509
|
-
return;
|
|
510
|
-
}
|
|
511
|
-
if (!done.has(dep)) walk(dep);
|
|
512
|
-
if (cycle) return;
|
|
513
|
-
}
|
|
514
|
-
onStack.delete(name);
|
|
515
|
-
stack.pop();
|
|
516
|
-
done.add(name);
|
|
517
|
-
};
|
|
518
|
-
for (const name of remaining) {
|
|
519
|
-
if (!done.has(name)) walk(name);
|
|
520
|
-
if (cycle) break;
|
|
521
|
-
}
|
|
522
|
-
return `[arc] module dependency cycle: ${cycle ? cycle.join(" → ") : [...remaining].join(", ")}. Modules cannot dependsOn each other circularly — break it with a shared module both point at, or wire the softer direction through an event/port instead of a hard dependsOn.`;
|
|
523
|
-
}
|
|
524
|
-
//#endregion
|
|
525
426
|
//#region src/factory/module/state.ts
|
|
526
427
|
/**
|
|
527
428
|
* The honest read of the state map. `fastify.arc` is optional (see
|
|
@@ -578,6 +479,28 @@ function getModuleState(fastify, name) {
|
|
|
578
479
|
const states = readStates(fastify);
|
|
579
480
|
return states !== void 0 && Object.hasOwn(states, name) ? states[name] : void 0;
|
|
580
481
|
}
|
|
482
|
+
/**
|
|
483
|
+
* The `ResourceDefinition`s module `name` mounted, in its declared order — for building that
|
|
484
|
+
* module's MCP tools or docs after boot. Throws when `name` is not a composed module (a typo would
|
|
485
|
+
* otherwise read as "no resources"), and when called before `registerResources` has published the
|
|
486
|
+
* descriptors. A resource declaring `skipRegistry` is absent, as from every introspection surface.
|
|
487
|
+
*/
|
|
488
|
+
function getModuleResources(fastify, name) {
|
|
489
|
+
const descriptors = fastify.arc?.moduleDescriptors;
|
|
490
|
+
const registry = fastify.arc?.registry;
|
|
491
|
+
if (!descriptors || !registry) throw new Error(`[arc] getModuleResources("${name}"): no composed module graph on this instance yet — call it after boot (afterResources, onReady, or after app.ready()).`);
|
|
492
|
+
const descriptor = descriptors.find((d) => d.name === name);
|
|
493
|
+
if (!descriptor) {
|
|
494
|
+
const known = descriptors.map((d) => d.name).join(", ") || "(none)";
|
|
495
|
+
throw new Error(`[arc] getModuleResources: "${name}" is not a composed module. Composed: ${known}.`);
|
|
496
|
+
}
|
|
497
|
+
const out = [];
|
|
498
|
+
for (const resource of descriptor.resources) {
|
|
499
|
+
const definition = resource.name === void 0 ? void 0 : registry.getDefinition(resource.name);
|
|
500
|
+
if (definition) out.push(definition);
|
|
501
|
+
}
|
|
502
|
+
return out;
|
|
503
|
+
}
|
|
581
504
|
//#endregion
|
|
582
505
|
//#region src/factory/module/teardown.ts
|
|
583
506
|
function createModuleTeardown(fastify, modules, disposers) {
|
|
@@ -851,18 +774,13 @@ async function registerArcCore(fastify, config, trackPlugin) {
|
|
|
851
774
|
await fastify.register(arcCorePlugin, { emitEvents: config.arcPlugins?.emitEvents !== false });
|
|
852
775
|
trackPlugin("arc-core");
|
|
853
776
|
if (config.arcPlugins?.events !== false) {
|
|
854
|
-
const { default: eventPlugin } = await import("./eventPlugin-
|
|
777
|
+
const { default: eventPlugin } = await import("./eventPlugin-Q6BhzHzs.mjs").then((n) => n.r);
|
|
855
778
|
const eventOpts = typeof config.arcPlugins?.events === "object" ? config.arcPlugins.events : {};
|
|
856
779
|
await fastify.register(eventPlugin, {
|
|
857
780
|
/**
|
|
858
|
-
*
|
|
859
|
-
*
|
|
860
|
-
*
|
|
861
|
-
* once, no second knob to keep in sync. The comparison is against the
|
|
862
|
-
* literal, not the resolved default: `runtime` UNSET also means memory
|
|
863
|
-
* stores, but an unstated topology must keep the forgotten-default
|
|
864
|
-
* warn. `eventOpts` spreads after, so a direct
|
|
865
|
-
* `arcPlugins.events.singleProcess` still wins either way.
|
|
781
|
+
* Only an EXPLICIT `runtime: 'memory'` declares single-instance — an
|
|
782
|
+
* UNSET runtime must keep the forgotten-default warn. `eventOpts`
|
|
783
|
+
* spreads after, so `arcPlugins.events.singleProcess` still wins.
|
|
866
784
|
*/
|
|
867
785
|
...config.runtime === "memory" ? { singleProcess: true } : {},
|
|
868
786
|
...eventOpts,
|
|
@@ -911,15 +829,15 @@ async function registerArcPlugins(fastify, config, trackPlugin, modules, moduleH
|
|
|
911
829
|
trackPlugin("arc-graceful-shutdown", shutdownOpts);
|
|
912
830
|
}
|
|
913
831
|
if (config.arcPlugins?.caching) {
|
|
914
|
-
const { default: cachingPlugin } = await import("./caching-
|
|
832
|
+
const { default: cachingPlugin } = await import("./caching-Cc2M1DwO.mjs").then((n) => n.n);
|
|
915
833
|
const opts = config.arcPlugins.caching === true ? {} : config.arcPlugins.caching;
|
|
916
834
|
await fastify.register(cachingPlugin, opts);
|
|
917
835
|
trackPlugin("arc-caching", opts);
|
|
918
836
|
}
|
|
919
837
|
if (config.arcPlugins?.queryCache) {
|
|
920
|
-
const { queryCachePlugin } = await import("./queryCachePlugin-
|
|
838
|
+
const { queryCachePlugin } = await import("./queryCachePlugin-CFWNH8aT.mjs").then((n) => n.n);
|
|
921
839
|
const opts = config.arcPlugins.queryCache === true ? {} : config.arcPlugins.queryCache;
|
|
922
|
-
const store = config.stores?.queryCache ?? new (await (import("./memory-
|
|
840
|
+
const store = config.stores?.queryCache ?? new (await (import("./memory-9Cp7611C.mjs").then((n) => n.n))).MemoryCacheStore();
|
|
923
841
|
await fastify.register(queryCachePlugin, {
|
|
924
842
|
store,
|
|
925
843
|
...opts
|
|
@@ -929,7 +847,7 @@ async function registerArcPlugins(fastify, config, trackPlugin, modules, moduleH
|
|
|
929
847
|
if (config.arcPlugins?.sse) {
|
|
930
848
|
if (config.arcPlugins?.events === false) fastify.log.warn("SSE plugin requires events plugin (arcPlugins.events). SSE disabled.");
|
|
931
849
|
else {
|
|
932
|
-
const { default: ssePlugin } = await import("./sse-
|
|
850
|
+
const { default: ssePlugin } = await import("./sse-CYzQBVFe.mjs").then((n) => n.n);
|
|
933
851
|
const opts = config.arcPlugins.sse === true ? {} : config.arcPlugins.sse;
|
|
934
852
|
await fastify.register(ssePlugin, opts);
|
|
935
853
|
trackPlugin("arc-sse", opts);
|
|
@@ -938,20 +856,20 @@ async function registerArcPlugins(fastify, config, trackPlugin, modules, moduleH
|
|
|
938
856
|
if (config.arcPlugins?.realtime) {
|
|
939
857
|
if (config.arcPlugins?.events === false) fastify.log.warn("Realtime plugin requires events plugin (arcPlugins.events). Realtime disabled.");
|
|
940
858
|
else {
|
|
941
|
-
const { default: realtimePlugin } = await import("./realtime-
|
|
859
|
+
const { default: realtimePlugin } = await import("./realtime-DmPE9S54.mjs").then((n) => n.n);
|
|
942
860
|
const opts = config.arcPlugins.realtime === true ? {} : config.arcPlugins.realtime;
|
|
943
861
|
await fastify.register(realtimePlugin, opts);
|
|
944
862
|
trackPlugin("arc-realtime", opts);
|
|
945
863
|
}
|
|
946
864
|
}
|
|
947
865
|
if (config.arcPlugins?.metrics) {
|
|
948
|
-
const { default: metricsPlugin } = await import("./metrics-
|
|
866
|
+
const { default: metricsPlugin } = await import("./metrics-CWI11eRu.mjs").then((n) => n.n);
|
|
949
867
|
const opts = config.arcPlugins.metrics === true ? {} : config.arcPlugins.metrics;
|
|
950
868
|
await fastify.register(metricsPlugin, opts);
|
|
951
869
|
trackPlugin("arc-metrics", opts);
|
|
952
870
|
}
|
|
953
871
|
if (config.arcPlugins?.versioning) {
|
|
954
|
-
const { default: versioningPlugin } = await import("./versioning-
|
|
872
|
+
const { default: versioningPlugin } = await import("./versioning-fhhWHV74.mjs").then((n) => n.n);
|
|
955
873
|
await fastify.register(versioningPlugin, config.arcPlugins.versioning);
|
|
956
874
|
trackPlugin("arc-versioning", config.arcPlugins.versioning);
|
|
957
875
|
}
|
|
@@ -1028,7 +946,7 @@ async function registerAuth(fastify, config, trackPlugin) {
|
|
|
1028
946
|
*/
|
|
1029
947
|
async function registerElevation(fastify, config, trackPlugin) {
|
|
1030
948
|
if (!config.elevation) return;
|
|
1031
|
-
const { elevationPlugin } = await import("./elevation-
|
|
949
|
+
const { elevationPlugin } = await import("./elevation-DG8SrLN-.mjs").then((n) => n.n);
|
|
1032
950
|
await fastify.register(elevationPlugin, config.elevation);
|
|
1033
951
|
trackPlugin("arc-elevation", config.elevation);
|
|
1034
952
|
fastify.log.debug("Elevation plugin enabled");
|
|
@@ -1043,7 +961,7 @@ async function registerElevation(fastify, config, trackPlugin) {
|
|
|
1043
961
|
*/
|
|
1044
962
|
async function registerErrorHandler(fastify, config, trackPlugin, moduleErrorMappers) {
|
|
1045
963
|
if (config.errorHandler === false) return;
|
|
1046
|
-
const { errorHandlerPlugin } = await import("./errorHandler-
|
|
964
|
+
const { errorHandlerPlugin } = await import("./errorHandler-BnfDviL1.mjs").then((n) => n.r);
|
|
1047
965
|
/**
|
|
1048
966
|
* DISCLOSURE DEFAULTS MERGE — they used to be REPLACED, and that was two bugs.
|
|
1049
967
|
*
|
|
@@ -1128,6 +1046,7 @@ function createOptionalAuthenticate(authenticate) {
|
|
|
1128
1046
|
}
|
|
1129
1047
|
//#endregion
|
|
1130
1048
|
//#region src/factory/registerResources.ts
|
|
1049
|
+
/** Resource registration and the module/app lifecycle for createApp. */
|
|
1131
1050
|
function isResourcesFactory(value) {
|
|
1132
1051
|
return typeof value === "function";
|
|
1133
1052
|
}
|
|
@@ -1150,8 +1069,8 @@ async function registerOne(parent, resource, mountRoutes) {
|
|
|
1150
1069
|
* infra. Every phase below iterates that order; each module phase runs BEFORE
|
|
1151
1070
|
* its app-level counterpart:
|
|
1152
1071
|
*
|
|
1153
|
-
* 1.
|
|
1154
|
-
* deps, cycles
|
|
1072
|
+
* 1. composeModules — validate the graph (dup names, missing/self
|
|
1073
|
+
* deps, cycles, unmet `requires`) BEFORE side effects
|
|
1155
1074
|
* 2. plugins() — app infra (DB, data, webhooks)
|
|
1156
1075
|
* 3. module.plugins — module infra, dependsOn order, before bootstraps
|
|
1157
1076
|
* 4a. module.bootstrap — engine init (return value → arc.modules[name])
|
|
@@ -1170,33 +1089,27 @@ async function registerOne(parent, resource, mountRoutes) {
|
|
|
1170
1089
|
* THEN app onClose, in ONE hook so module
|
|
1171
1090
|
* teardown runs before shared infra closes.
|
|
1172
1091
|
*
|
|
1173
|
-
* Boot is TRANSACTIONAL for module-held resources:
|
|
1174
|
-
*
|
|
1175
|
-
*
|
|
1176
|
-
*
|
|
1177
|
-
* ORIGINAL boot error. Without this, a failure after module A's bootstrap but
|
|
1178
|
-
* before the §7b hook registration leaked A's clients/timers/engines with no
|
|
1179
|
-
* teardown path.
|
|
1180
|
-
*
|
|
1181
|
-
* "Entered", not "completed": a module is eligible for teardown from the
|
|
1182
|
-
* moment it enters `plugins()`/`bootstrap()`, so the callback that allocates a
|
|
1183
|
-
* client and THEN throws — the commonest partial-init shape — gets its closer
|
|
1184
|
-
* called too.
|
|
1092
|
+
* Boot is TRANSACTIONAL for module-held resources: on any failure in phases
|
|
1093
|
+
* 2–6, arc unsubscribes live module event handlers, closes every module that
|
|
1094
|
+
* ENTERED an init phase (see module/teardown.ts), and rethrows the ORIGINAL
|
|
1095
|
+
* boot error.
|
|
1185
1096
|
*/
|
|
1186
|
-
async function registerResources(fastify, config, preResolvedModules) {
|
|
1097
|
+
async function registerResources(fastify, config, preResolvedModules, skippedBridges = []) {
|
|
1098
|
+
if (config.strictResources === void 0) config = {
|
|
1099
|
+
...config,
|
|
1100
|
+
strictResources: true
|
|
1101
|
+
};
|
|
1187
1102
|
if (config.preset === "production") {
|
|
1188
|
-
if (config.strictResources === void 0) config = {
|
|
1189
|
-
...config,
|
|
1190
|
-
strictResources: true
|
|
1191
|
-
};
|
|
1192
1103
|
if (config.strictResourceDir === void 0) config = {
|
|
1193
1104
|
...config,
|
|
1194
1105
|
strictResourceDir: true
|
|
1195
1106
|
};
|
|
1196
1107
|
}
|
|
1197
|
-
|
|
1108
|
+
if (config.strictResources === false) fastify.arc?.registry?.tolerateDuplicateNames();
|
|
1109
|
+
const modules = preResolvedModules ?? (await composeModules(config.modules ?? [], config.ports)).modules;
|
|
1198
1110
|
initModuleStates(fastify, modules.map((m) => m.name));
|
|
1199
|
-
const
|
|
1111
|
+
const portsByModule = new Map(modules.map((m) => [m.name, portsForModule(m, config.ports)]));
|
|
1112
|
+
const disposers = createDisposerRegistry(fastify, (name) => portsByModule.get(name) ?? {});
|
|
1200
1113
|
const teardown = createModuleTeardown(fastify, modules, disposers);
|
|
1201
1114
|
let eventUnsubscribes = [];
|
|
1202
1115
|
try {
|
|
@@ -1267,7 +1180,7 @@ async function registerResources(fastify, config, preResolvedModules) {
|
|
|
1267
1180
|
let discoveryPath;
|
|
1268
1181
|
let discoveryYieldedZero = false;
|
|
1269
1182
|
if (resolvedResources === void 0 && config.resourceDir) {
|
|
1270
|
-
const { loadResources } = await import("./loadResources-
|
|
1183
|
+
const { loadResources } = await import("./loadResources-Dn4lalw2.mjs").then((n) => n.n);
|
|
1271
1184
|
const { resolve, dirname } = await import("node:path");
|
|
1272
1185
|
const { fileURLToPath } = await import("node:url");
|
|
1273
1186
|
const rawDir = config.resourceDir;
|
|
@@ -1276,7 +1189,7 @@ async function registerResources(fastify, config, preResolvedModules) {
|
|
|
1276
1189
|
discoveryPath = dir;
|
|
1277
1190
|
const discovered = await loadResources(dir, { logger: fastify.log });
|
|
1278
1191
|
if (discovered.length === 0) {
|
|
1279
|
-
if (config.strictResourceDir) throw new Error(`[arc] loadResources: resourceDir "${rawDir}" resolved to "${dir}" but yielded 0 resources. Check the path, file naming (*.resource.{ts,js,mts,mjs}), and runtime layout (src/ vs dist/)
|
|
1192
|
+
if (config.strictResourceDir) throw new Error(`[arc] loadResources: resourceDir "${rawDir}" resolved to "${dir}" but yielded 0 resources. Check the path, file naming (*.resource.{ts,js,mts,mjs}), and runtime layout (src/ vs dist/).`);
|
|
1280
1193
|
discoveryYieldedZero = true;
|
|
1281
1194
|
}
|
|
1282
1195
|
resolvedResources = discovered;
|
|
@@ -1336,9 +1249,9 @@ Resources supplied by "${m.name}": ${provided}.`);
|
|
|
1336
1249
|
const seen = /* @__PURE__ */ new Set();
|
|
1337
1250
|
for (const resource of resolvedResources) if (resource.name) {
|
|
1338
1251
|
if (seen.has(resource.name)) {
|
|
1339
|
-
const msg = `Duplicate resource name "${resource.name}" detected. This will cause route conflicts. Check your resources array and loadResources() output. Common cause: stale compiled files in dist/ alongside src
|
|
1252
|
+
const msg = `Duplicate resource name "${resource.name}" detected. This will cause route conflicts. Check your resources array and loadResources() output. Common cause: stale compiled files in dist/ alongside src/.`;
|
|
1340
1253
|
if (config.strictResources) throw new Error(msg);
|
|
1341
|
-
fastify.log.warn(msg);
|
|
1254
|
+
fastify.log.warn(`${msg} Boot continues because strictResources is false.`);
|
|
1342
1255
|
}
|
|
1343
1256
|
seen.add(resource.name);
|
|
1344
1257
|
}
|
|
@@ -1375,11 +1288,8 @@ Resources supplied by "${m.name}": ${provided}.`);
|
|
|
1375
1288
|
const scheduleCounts = /* @__PURE__ */ new Map();
|
|
1376
1289
|
const collectedJobs = await collectModuleScheduledJobs(fastify, modules, scheduleCounts);
|
|
1377
1290
|
/**
|
|
1378
|
-
* ROLE gate
|
|
1379
|
-
*
|
|
1380
|
-
* that a different deployment of the same codebase executes. 'relay' runs
|
|
1381
|
-
* only kind:'relay' arms (the outbox relays); 'api'/'worker' run none;
|
|
1382
|
-
* 'scheduler'/'all' run everything.
|
|
1291
|
+
* ROLE gate: arms this role does not run are skipped and logged, never
|
|
1292
|
+
* fatal — another deployment of the same codebase runs them.
|
|
1383
1293
|
*/
|
|
1384
1294
|
const role = config.role ?? "all";
|
|
1385
1295
|
const scheduledJobs = role === "scheduler" || role === "all" ? collectedJobs : role === "relay" ? collectedJobs.filter((j) => j.kind === "relay") : [];
|
|
@@ -1393,7 +1303,7 @@ Resources supplied by "${m.name}": ${provided}.`);
|
|
|
1393
1303
|
if (fastify.hasDecorator("getScheduleStats")) throw new Error("[arc] modules declare scheduledJobs but schedulesPlugin was already registered manually. Configure their runner through arcPlugins.schedules so Arc can compose one schedule table.");
|
|
1394
1304
|
const frozenJobs = Object.freeze(scheduledJobs.map((job) => Object.freeze({ ...job })));
|
|
1395
1305
|
const scheduleOptions = typeof config.arcPlugins?.schedules === "object" ? config.arcPlugins.schedules : {};
|
|
1396
|
-
const { default: schedulesPlugin } = await import("./schedules-
|
|
1306
|
+
const { default: schedulesPlugin } = await import("./schedules-C-OpmHBq.mjs").then((n) => n.n);
|
|
1397
1307
|
await fastify.register(schedulesPlugin, {
|
|
1398
1308
|
...scheduleOptions,
|
|
1399
1309
|
schedules: frozenJobs
|
|
@@ -1408,7 +1318,64 @@ Resources supplied by "${m.name}": ${provided}.`);
|
|
|
1408
1318
|
}
|
|
1409
1319
|
}
|
|
1410
1320
|
const subscriptionCounts = /* @__PURE__ */ new Map();
|
|
1411
|
-
|
|
1321
|
+
const consumedByModule = /* @__PURE__ */ new Map();
|
|
1322
|
+
const handlersByModule = /* @__PURE__ */ new Map();
|
|
1323
|
+
eventUnsubscribes = await subscribeModuleEventHandlers(fastify, modules, subscriptionCounts, consumedByModule, handlersByModule);
|
|
1324
|
+
const registry = fastify.arc?.registry;
|
|
1325
|
+
const emitsCrud = config.arcPlugins?.emitEvents !== false && registry !== void 0;
|
|
1326
|
+
const derived = /* @__PURE__ */ new Map();
|
|
1327
|
+
const moduleOwned = /* @__PURE__ */ new Set();
|
|
1328
|
+
if (emitsCrud) for (const [moduleName, names] of moduleResourceNames) {
|
|
1329
|
+
const publishes = [];
|
|
1330
|
+
for (const name of names) {
|
|
1331
|
+
moduleOwned.add(name);
|
|
1332
|
+
const entry = registry.get(name);
|
|
1333
|
+
if (entry) publishes.push(...crudEventsOf(entry));
|
|
1334
|
+
}
|
|
1335
|
+
derived.set(moduleName, {
|
|
1336
|
+
publishes,
|
|
1337
|
+
namespaces: [...names]
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
const appCrudEvents = emitsCrud ? registry.getAll().filter((e) => !moduleOwned.has(e.name)).flatMap((e) => crudEventsOf(e)) : [];
|
|
1341
|
+
const eventRegistry = fastify.events?.registry;
|
|
1342
|
+
if (emitsCrud && eventRegistry) for (const entry of registry.getAll()) for (const name of crudEventsOf(entry)) {
|
|
1343
|
+
if (eventRegistry.get(name)) continue;
|
|
1344
|
+
eventRegistry.register(defineEvent({
|
|
1345
|
+
name,
|
|
1346
|
+
description: `arc CRUD event — a ${entry.name} was ${name.slice(entry.name.length + 1)}.`,
|
|
1347
|
+
schema: CRUD_EVENT_SCHEMA
|
|
1348
|
+
}));
|
|
1349
|
+
}
|
|
1350
|
+
const moduleGraph = buildModuleGraph(modules, consumedByModule, skippedBridges, [
|
|
1351
|
+
...config.moduleGraph?.externalEvents ?? [],
|
|
1352
|
+
...ARC_OWNED_EVENTS,
|
|
1353
|
+
...appCrudEvents
|
|
1354
|
+
], derived);
|
|
1355
|
+
for (const bridge of moduleGraph.skippedBridges) fastify.log.info({
|
|
1356
|
+
bridge: bridge.name,
|
|
1357
|
+
missing: bridge.missing
|
|
1358
|
+
}, "[arc] bridge module inactive — a module it joins is not composed");
|
|
1359
|
+
const graphMode = config.moduleGraph?.events ?? (config.preset === "production" ? "warn" : "off");
|
|
1360
|
+
if (graphMode !== "off") {
|
|
1361
|
+
const analysis = analyzeModuleGraph(moduleGraph);
|
|
1362
|
+
const issues = verifyModuleGraph(moduleGraph, analysis);
|
|
1363
|
+
if (issues.length > 0 && graphMode === "strict") throw new Error(`[arc] module graph has ${issues.length} issue(s):\n${issues.map((i) => ` - ${i.message}`).join("\n")}`);
|
|
1364
|
+
for (const issue of issues) fastify.log.warn({ issue }, `[arc] module graph: ${issue.message}`);
|
|
1365
|
+
const outboxModules = new Set(modules.filter((m) => isTransactionalOutbox(getOptionalModuleExports(fastify, m.name))).map((m) => m.name));
|
|
1366
|
+
const durabilityIssues = verifyEventDurability(modules, {
|
|
1367
|
+
outboxModules,
|
|
1368
|
+
handlers: handlersByModule
|
|
1369
|
+
});
|
|
1370
|
+
if (durabilityIssues.length && graphMode === "strict") throw new Error(`[arc] module graph durability has ${durabilityIssues.length} issue(s):\n${durabilityIssues.map((i) => ` - ${i.message}`).join("\n")}`);
|
|
1371
|
+
for (const issue of durabilityIssues) fastify.log.warn({ issue }, `[arc] module graph (${issue.code}): ${issue.message}`);
|
|
1372
|
+
}
|
|
1373
|
+
if (fastify.arc) fastify.arc.moduleGraph = moduleGraph;
|
|
1374
|
+
const events = fastify.events;
|
|
1375
|
+
if (graphMode !== "off" && typeof events?.observe === "function" && fastify.arc) {
|
|
1376
|
+
const undeclared = watchUndeclaredEvents(fastify, moduleGraph, graphMode);
|
|
1377
|
+
fastify.arc.undeclaredEvents = undeclared;
|
|
1378
|
+
}
|
|
1412
1379
|
if (fastify.arc) fastify.arc.moduleDescriptors = Object.freeze(modules.map((m) => describeResolvedModule({
|
|
1413
1380
|
name: m.name,
|
|
1414
1381
|
dependsOn: m.dependsOn,
|
|
@@ -1427,6 +1394,8 @@ Resources supplied by "${m.name}": ${provided}.`);
|
|
|
1427
1394
|
fastify.log.debug("afterResources hook executed");
|
|
1428
1395
|
}
|
|
1429
1396
|
} catch (err) {
|
|
1397
|
+
const shutdownErrors = await runShutdown(fastify);
|
|
1398
|
+
for (const shutdownError of shutdownErrors) fastify.log.error({ err: shutdownError }, "[arc] runtime rollback failed");
|
|
1430
1399
|
const unsubscribeErrors = await unsubscribeModuleEventHandlers(eventUnsubscribes);
|
|
1431
1400
|
for (const unsubscribeError of unsubscribeErrors) fastify.log.error({ err: unsubscribeError }, "[arc] module event-handler rollback after boot failure failed");
|
|
1432
1401
|
const closeErrors = await teardown.rollback();
|
|
@@ -1441,30 +1410,53 @@ Resources supplied by "${m.name}": ${provided}.`);
|
|
|
1441
1410
|
}
|
|
1442
1411
|
const appOnClose = config.onClose;
|
|
1443
1412
|
if (teardown.hasComposedModules() || appOnClose || eventUnsubscribes.length) fastify.addHook("onClose", async () => {
|
|
1413
|
+
const shutdownErrors = await runShutdown(fastify);
|
|
1444
1414
|
const unsubscribeErrors = await unsubscribeModuleEventHandlers(eventUnsubscribes);
|
|
1445
1415
|
for (const err of unsubscribeErrors) fastify.log.error({ err }, "[arc] module event-handler shutdown unsubscribe failed");
|
|
1446
1416
|
const closeErrors = await teardown.closeAll();
|
|
1447
1417
|
for (const err of closeErrors) fastify.log.error({ err }, "[arc] module onClose failed during shutdown");
|
|
1448
1418
|
if (appOnClose) await appOnClose(fastify);
|
|
1449
1419
|
if (closeErrors.length > 0) throw closeErrors[0];
|
|
1420
|
+
if (shutdownErrors.length > 0) throw shutdownErrors[0];
|
|
1450
1421
|
});
|
|
1451
1422
|
}
|
|
1452
1423
|
/**
|
|
1453
|
-
* `plugins()` is CALLED by arc, never
|
|
1454
|
-
*
|
|
1455
|
-
*
|
|
1456
|
-
* invokes their plugin at shutdown with no arguments.
|
|
1457
|
-
*
|
|
1458
|
-
* The `fastify-plugin` case is unambiguous (its marker symbols are only ever
|
|
1459
|
-
* set by `fp()`), so it throws. A bare `(fastify, opts)` function is a strong
|
|
1460
|
-
* but not certain signal — arc's disposer contract calls it with ZERO
|
|
1461
|
-
* arguments, so a 2-parameter disposer is meaningless either way — and warns.
|
|
1424
|
+
* `plugins()` is CALLED by arc, never `register()`ed — a returned Fastify
|
|
1425
|
+
* plugin would be silently treated as a disposer. `fp()` markers are
|
|
1426
|
+
* unambiguous, so that throws; a bare 2+-arg function only warns.
|
|
1462
1427
|
*/
|
|
1463
1428
|
function assertNotAReturnedPlugin(returned, moduleName, fastify) {
|
|
1464
1429
|
const fn = returned;
|
|
1465
1430
|
if (fn[Symbol.for("skip-override")] !== void 0 || fn[Symbol.for("plugin-meta")] !== void 0 || fn[Symbol.for("fastify.display-name")] !== void 0) throw new Error(`[arc] module "${moduleName}" returned a fastify-plugin from plugins(). That slot is a SETUP function arc calls directly — it is never passed to fastify.register(), so the returned value is read as a disposer and your plugin would be invoked at shutdown instead of registered at boot. Register it yourself: \`plugins: async (fastify) => { await fastify.register(myPlugin) }\`.`);
|
|
1466
1431
|
if (returned.length >= 2) fastify.log?.warn?.(`[arc] module "${moduleName}" returned a ${returned.length}-argument function from plugins(). Arc calls disposers with NO arguments, so this looks like a Fastify plugin that was meant to be registered (\`await fastify.register(fn)\`) rather than a teardown callback. If it really is a disposer, declare it with no parameters.`);
|
|
1467
1432
|
}
|
|
1433
|
+
/** Namespaces arc itself publishes — lifecycle (`arc.*`) and its streamline integration (`workflow.*`). */
|
|
1434
|
+
const ARC_OWNED_EVENTS = ["arc.*", "workflow.*"];
|
|
1435
|
+
/**
|
|
1436
|
+
* Record every published event type the graph has no owner for. Logs each name once; never
|
|
1437
|
+
* touches delivery.
|
|
1438
|
+
*/
|
|
1439
|
+
function watchUndeclaredEvents(fastify, graph, mode) {
|
|
1440
|
+
const owned = new Set(graph.modules.flatMap((m) => m.publishes));
|
|
1441
|
+
const external = createEventRouter();
|
|
1442
|
+
for (const pattern of graph.externalEvents) external.add(pattern, true);
|
|
1443
|
+
const undeclared = /* @__PURE__ */ new Set();
|
|
1444
|
+
const stop = fastify.events.observe((event) => {
|
|
1445
|
+
const type = event.type;
|
|
1446
|
+
if (owned.has(type) || undeclared.has(type) || type.startsWith("$")) return;
|
|
1447
|
+
if (external.route(type).length > 0) return;
|
|
1448
|
+
undeclared.add(type);
|
|
1449
|
+
fastify.log[mode === "strict" ? "error" : "warn"]({ event: type }, `[arc] module graph: "${type}" was published but no module declares it — add it to its owner's \`publishes\` (or \`moduleGraph.externalEvents\`).`);
|
|
1450
|
+
});
|
|
1451
|
+
fastify.addHook("onClose", async () => stop());
|
|
1452
|
+
return undeclared;
|
|
1453
|
+
}
|
|
1454
|
+
/** A module export that is an outbox whose `save` enlists the caller's session. */
|
|
1455
|
+
function isTransactionalOutbox(exports) {
|
|
1456
|
+
if (typeof exports !== "object" || exports === null || !("store" in exports)) return false;
|
|
1457
|
+
const store = exports.store;
|
|
1458
|
+
return typeof store === "object" && store !== null && "transactionalSave" in store && store.transactionalSave === true;
|
|
1459
|
+
}
|
|
1468
1460
|
//#endregion
|
|
1469
1461
|
//#region src/factory/security/cors.ts
|
|
1470
1462
|
/**
|
|
@@ -1650,12 +1642,17 @@ async function registerSecurityPlugins(fastify, config) {
|
|
|
1650
1642
|
});
|
|
1651
1643
|
await fastify.register(rateLimit, rateLimitOpts);
|
|
1652
1644
|
const rateLimitRecord = typeof rateLimitOpts === "object" && rateLimitOpts !== null ? rateLimitOpts : void 0;
|
|
1653
|
-
|
|
1645
|
+
const hasStore = rateLimitRecord !== void 0 && (rateLimitRecord.store != null || rateLimitRecord.redis != null);
|
|
1646
|
+
resolveResourceRateLimiter(fastify, hasStore ? "shared" : "memory");
|
|
1647
|
+
if (!hasStore) {
|
|
1654
1648
|
if (config.runtime === "distributed") throw new Error("[Arc] runtime: 'distributed' with rate limiting requires a shared store.\nProvide rateLimit: { redis: ioredisClient } (or { store: CustomStore }) or disable rate limiting: rateLimit: false");
|
|
1655
1649
|
else if (config.preset === "production") fastify.log.warn("Rate limiting is using in-memory store. In multi-instance deployments, each instance tracks limits independently. Configure a Redis store for distributed rate limiting.");
|
|
1656
1650
|
}
|
|
1657
1651
|
fastify.log.debug("Rate limiting enabled");
|
|
1658
|
-
} else
|
|
1652
|
+
} else {
|
|
1653
|
+
fastify.log.warn("Rate limiting disabled");
|
|
1654
|
+
markRateLimitingDisabled(fastify);
|
|
1655
|
+
}
|
|
1659
1656
|
}
|
|
1660
1657
|
/**
|
|
1661
1658
|
* Register performance and utility plugins (Under Pressure, Sensible, Multipart, Raw Body).
|
|
@@ -1721,40 +1718,18 @@ async function registerUtilityPlugins(fastify, config) {
|
|
|
1721
1718
|
//#endregion
|
|
1722
1719
|
//#region src/factory/createApp.ts
|
|
1723
1720
|
/**
|
|
1724
|
-
* ArcFactory - Production-ready Fastify application factory
|
|
1725
|
-
*
|
|
1726
|
-
* Enforces security best practices by making plugins opt-out instead of opt-in.
|
|
1727
|
-
* A developer must explicitly disable security features rather than forget to enable them.
|
|
1721
|
+
* ArcFactory - Production-ready Fastify application factory.
|
|
1728
1722
|
*
|
|
1729
|
-
*
|
|
1730
|
-
*
|
|
1731
|
-
* connection pooling, and full control over your data layer.
|
|
1723
|
+
* Security plugins are opt-out, not opt-in. Arc is database-agnostic: connect
|
|
1724
|
+
* your database separately and pass adapters to `defineResource`.
|
|
1732
1725
|
*
|
|
1733
1726
|
* @example
|
|
1734
|
-
* // 1. Connect your database(s) separately
|
|
1735
|
-
* import mongoose from 'mongoose';
|
|
1736
|
-
* await mongoose.connect(process.env.MONGO_URI);
|
|
1737
|
-
*
|
|
1738
|
-
* // 2. Create Arc app with resources
|
|
1739
1727
|
* const app = await createApp({
|
|
1740
1728
|
* preset: 'production',
|
|
1729
|
+
* runtime: 'memory',
|
|
1741
1730
|
* auth: { type: 'jwt', jwt: { secret: process.env.JWT_SECRET } },
|
|
1742
|
-
* cors: { origin: ['https://example.com'] },
|
|
1743
1731
|
* resources: [productResource, orderResource],
|
|
1744
1732
|
* });
|
|
1745
|
-
*
|
|
1746
|
-
* @example
|
|
1747
|
-
* // Multiple databases example
|
|
1748
|
-
* const primaryDb = await mongoose.connect(process.env.PRIMARY_DB);
|
|
1749
|
-
* const analyticsDb = mongoose.createConnection(process.env.ANALYTICS_DB);
|
|
1750
|
-
*
|
|
1751
|
-
* const orderResource = defineResource({
|
|
1752
|
-
* adapter: createMongooseAdapter({ model: OrderModel, repository: orderRepo }),
|
|
1753
|
-
* });
|
|
1754
|
-
*
|
|
1755
|
-
* const analyticsResource = defineResource({
|
|
1756
|
-
* adapter: createMongooseAdapter({ model: AnalyticsModel, repository: analyticsRepo }),
|
|
1757
|
-
* });
|
|
1758
1733
|
*/
|
|
1759
1734
|
var createApp_exports = /* @__PURE__ */ __exportAll({
|
|
1760
1735
|
ArcFactory: () => ArcFactory,
|
|
@@ -1763,10 +1738,8 @@ var createApp_exports = /* @__PURE__ */ __exportAll({
|
|
|
1763
1738
|
resolveLoggerConfig: () => resolveLoggerConfig
|
|
1764
1739
|
});
|
|
1765
1740
|
/**
|
|
1766
|
-
* The fail-loud switches
|
|
1767
|
-
*
|
|
1768
|
-
* Every one is a SPECIFIC instruction defeated by a GENERAL default, which is
|
|
1769
|
-
* why they are reported together rather than one at a time.
|
|
1741
|
+
* The fail-loud switches — ON by default — and what each one costs when a deployment turns it OFF.
|
|
1742
|
+
* Reported together at production boot, so an opt-out is never silent.
|
|
1770
1743
|
*/
|
|
1771
1744
|
const STRICT_ENV_FLAGS = [
|
|
1772
1745
|
["ARC_STRICT_PERMISSIONS", "An ungated write route warns instead of failing boot."],
|
|
@@ -1775,18 +1748,10 @@ const STRICT_ENV_FLAGS = [
|
|
|
1775
1748
|
];
|
|
1776
1749
|
const MEMORY_STORE_NAMES = /* @__PURE__ */ new Set(["memory", "memory-cache"]);
|
|
1777
1750
|
/**
|
|
1778
|
-
* Default redact paths
|
|
1779
|
-
*
|
|
1780
|
-
*
|
|
1781
|
-
*
|
|
1782
|
-
* outgoing logs may carry either), API-key headers, and any nested
|
|
1783
|
-
* `password` / `token` / `secret` / `apiKey` fields anywhere in the log
|
|
1784
|
-
* tree.
|
|
1785
|
-
*
|
|
1786
|
-
* Hosts that need NO redaction (test fixtures, security audits) opt out
|
|
1787
|
-
* with `logger: { redact: [] }`. Hosts that need MORE redaction supply
|
|
1788
|
-
* their own `redact` and arc steps aside — this default never overrides
|
|
1789
|
-
* an explicit setting.
|
|
1751
|
+
* Default pino redact paths (auth/cookie/API-key headers, nested
|
|
1752
|
+
* password/token/secret fields), applied only when the host supplies no
|
|
1753
|
+
* `logger.redact`. Opt out with `logger: { redact: [] }`; an explicit
|
|
1754
|
+
* `redact` always wins.
|
|
1790
1755
|
*/
|
|
1791
1756
|
const DEFAULT_LOGGER_REDACT_PATHS = [
|
|
1792
1757
|
"req.headers.authorization",
|
|
@@ -1805,16 +1770,8 @@ const DEFAULT_LOGGER_REDACT_PATHS = [
|
|
|
1805
1770
|
"*.apiKey"
|
|
1806
1771
|
];
|
|
1807
1772
|
/**
|
|
1808
|
-
* Resolve the host's `logger` option, layering
|
|
1809
|
-
*
|
|
1810
|
-
* for its `logger` server option.
|
|
1811
|
-
*
|
|
1812
|
-
* Three branches:
|
|
1813
|
-
* - `logger === false` → pass through (no logger).
|
|
1814
|
-
* - `logger === undefined` → enable with default redact paths.
|
|
1815
|
-
* - `logger === true` → enable with default redact paths.
|
|
1816
|
-
* - `logger` is an object → respect explicit `redact` (host wins);
|
|
1817
|
-
* otherwise inject defaults.
|
|
1773
|
+
* Resolve the host's `logger` option for Fastify, layering the default redact
|
|
1774
|
+
* paths unless it is `false` or an object with its own `redact`.
|
|
1818
1775
|
*/
|
|
1819
1776
|
function resolveLoggerConfig(logger) {
|
|
1820
1777
|
if (logger === false) return false;
|
|
@@ -1900,28 +1857,31 @@ Missing:\n${lines}\n\nFix: pass durable adapters under \`stores\`, or use runtim
|
|
|
1900
1857
|
*/
|
|
1901
1858
|
async function createApp(options) {
|
|
1902
1859
|
/**
|
|
1903
|
-
*
|
|
1904
|
-
*
|
|
1905
|
-
|
|
1906
|
-
|
|
1860
|
+
* TOPOLOGY is declared, never guessed: a second replica on the in-memory runtime silently
|
|
1861
|
+
* loses cross-instance events and caches.
|
|
1862
|
+
*/
|
|
1863
|
+
if (options.preset === "production" && options.runtime === void 0) throw new Error("createApp: preset 'production' requires an explicit `runtime` — 'memory' for a single instance, 'distributed' before running a second replica (it requires shared stores and is audited at boot).");
|
|
1864
|
+
/**
|
|
1865
|
+
* A relay/scheduler process declaring HTTP resources is a wiring error —
|
|
1866
|
+
* mounting routes anyway would hide the misdeployment behind a working endpoint.
|
|
1907
1867
|
*/
|
|
1908
1868
|
const declaredRole = options.role ?? "all";
|
|
1909
1869
|
if ((declaredRole === "relay" || declaredRole === "scheduler") && (options.resources !== void 0 || options.resourceDir !== void 0)) throw new Error(`createApp: role '${declaredRole}' declares HTTP resources — a ${declaredRole} process must not serve routes. Remove \`resources\`/\`resourceDir\`, or change the role.`);
|
|
1910
1870
|
/**
|
|
1911
|
-
* This app's own `arcLog` config, entered for the WHOLE boot.
|
|
1912
|
-
*
|
|
1913
|
-
*
|
|
1914
|
-
*
|
|
1915
|
-
* post-construction boot warning to the console instead of this app's
|
|
1916
|
-
* logger. Scoping it per app is what stops two apps in one process from
|
|
1917
|
-
* overwriting each other's writer — see `src/logger/index.ts`.
|
|
1871
|
+
* This app's own `arcLog` config, entered for the WHOLE boot. Held by
|
|
1872
|
+
* reference and mutated in `buildApp` once `fastify.log` exists — a copy
|
|
1873
|
+
* would send post-construction boot warnings to the console. Per-app so two
|
|
1874
|
+
* apps in one process keep separate writers.
|
|
1918
1875
|
*/
|
|
1919
1876
|
const loggerScope = {};
|
|
1920
1877
|
if (options.debug !== void 0 && options.debug !== false) loggerScope.debug = options.debug;
|
|
1921
1878
|
let partial;
|
|
1879
|
+
let disposeBoot;
|
|
1922
1880
|
try {
|
|
1923
1881
|
return await runWithArcLogger(loggerScope, () => buildApp(options, loggerScope, (instance) => {
|
|
1924
1882
|
partial = instance;
|
|
1883
|
+
}, (dispose) => {
|
|
1884
|
+
disposeBoot = dispose;
|
|
1925
1885
|
}));
|
|
1926
1886
|
} catch (err) {
|
|
1927
1887
|
if (partial) try {
|
|
@@ -1929,38 +1889,42 @@ async function createApp(options) {
|
|
|
1929
1889
|
} catch (closeErr) {
|
|
1930
1890
|
partial.log?.error?.({ err: closeErr }, "[arc] cleanup of a partially-booted app failed; original boot error follows");
|
|
1931
1891
|
}
|
|
1892
|
+
if (disposeBoot) try {
|
|
1893
|
+
await disposeBoot();
|
|
1894
|
+
} catch (disposeErr) {
|
|
1895
|
+
arcLog("createApp").error("beforeBoot disposer failed during failed-boot cleanup; original boot error follows", disposeErr);
|
|
1896
|
+
}
|
|
1932
1897
|
throw err;
|
|
1933
1898
|
}
|
|
1934
1899
|
}
|
|
1935
|
-
async function buildApp(options, loggerScope, onInstance) {
|
|
1900
|
+
async function buildApp(options, loggerScope, onInstance, onBootDisposer) {
|
|
1936
1901
|
validateAuthOptions(options);
|
|
1937
1902
|
const deferredWarnings = validateDistributedRuntime(options);
|
|
1938
|
-
|
|
1903
|
+
let disposeBoot;
|
|
1904
|
+
if (options.beforeBoot) {
|
|
1905
|
+
const dispose = await options.beforeBoot();
|
|
1906
|
+
if (typeof dispose === "function") {
|
|
1907
|
+
let once;
|
|
1908
|
+
disposeBoot = () => {
|
|
1909
|
+
once ??= Promise.resolve().then(dispose);
|
|
1910
|
+
return once;
|
|
1911
|
+
};
|
|
1912
|
+
onBootDisposer(disposeBoot);
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1939
1915
|
const config = {
|
|
1940
1916
|
...options.preset ? getPreset(options.preset) : {},
|
|
1941
|
-
...options
|
|
1917
|
+
...options,
|
|
1918
|
+
...options.role === "relay" || options.role === "scheduler" ? { mountRoutes: false } : {}
|
|
1942
1919
|
};
|
|
1943
1920
|
const inheritedTrustProxyDefault = options.preset === "production" && options.trustProxy === void 0;
|
|
1944
1921
|
/**
|
|
1945
|
-
* A NUMERIC `trustProxy`
|
|
1946
|
-
*
|
|
1947
|
-
*
|
|
1948
|
-
* `lib/request.js` is explicit: "Hop-count-only trust cannot validate the
|
|
1949
|
-
* immediate peer. Fail closed so direct clients cannot spoof X-Forwarded-*
|
|
1950
|
-
* values by supplying enough hops." So `trustProxy: 1` now trusts NOTHING.
|
|
1951
|
-
*
|
|
1952
|
-
* That is the right call upstream and the wrong thing to inherit silently:
|
|
1953
|
-
* `request.ip` becomes the proxy's address, which is the exact failure the
|
|
1954
|
-
* production-preset warning below describes — rate-limit keys collapse onto
|
|
1955
|
-
* one bucket and audit rows record the wrong client. The host asked for
|
|
1956
|
-
* proxy trust and got none, with no error.
|
|
1957
|
-
*
|
|
1958
|
-
* Warn rather than refuse: the app still serves, and a hard boot failure on
|
|
1959
|
-
* a transitive `fastify` bump would be a harsher surprise than the one being
|
|
1960
|
-
* reported. The replacement is named so the fix is a copy-paste.
|
|
1922
|
+
* A NUMERIC `trustProxy` trusts NOTHING on Fastify >= 5.12.1 (hop-count
|
|
1923
|
+
* trust fails closed upstream), so the host silently gets the proxy's IP.
|
|
1924
|
+
* Warn rather than refuse — a boot failure on a transitive bump is harsher.
|
|
1961
1925
|
*/
|
|
1962
1926
|
const numericTrustProxy = typeof config.trustProxy === "number";
|
|
1963
|
-
const resolvedModules =
|
|
1927
|
+
const { modules: resolvedModules, skipped: skippedBridges } = await composeModules(config.modules ?? [], config.ports);
|
|
1964
1928
|
const moduleErrorMappers = resolvedModules.flatMap((m) => m.errorMappers ?? []);
|
|
1965
1929
|
const moduleHealthChecks = collectModuleHealthChecks(resolvedModules);
|
|
1966
1930
|
const resolvedLogger = resolveLoggerConfig(config.logger);
|
|
@@ -1983,16 +1947,13 @@ async function buildApp(options, loggerScope, onInstance) {
|
|
|
1983
1947
|
}),
|
|
1984
1948
|
maxParamLength: config.maxParamLength ?? 400
|
|
1985
1949
|
},
|
|
1986
|
-
ajv: { customOptions:
|
|
1987
|
-
coerceTypes: true,
|
|
1988
|
-
useDefaults: true,
|
|
1989
|
-
removeAdditional: false,
|
|
1990
|
-
strictTypes: false,
|
|
1991
|
-
keywords: ["example", ...config.ajv?.keywords ?? []]
|
|
1992
|
-
} }
|
|
1950
|
+
ajv: { customOptions: arcAjvCustomOptions(config.ajv?.keywords) }
|
|
1993
1951
|
});
|
|
1952
|
+
markArcRootValidator(fastify);
|
|
1994
1953
|
onInstance(fastify);
|
|
1954
|
+
if (disposeBoot) fastify.addHook("onClose", disposeBoot);
|
|
1995
1955
|
initRuntimeCapabilityRegistry(fastify);
|
|
1956
|
+
initShutdownRegistry(fastify);
|
|
1996
1957
|
if (resolvedLogger !== false) {
|
|
1997
1958
|
const writer = createPinoWriter(fastify.log);
|
|
1998
1959
|
loggerScope.writer = writer;
|
|
@@ -2006,22 +1967,15 @@ async function buildApp(options, loggerScope, onInstance) {
|
|
|
2006
1967
|
});
|
|
2007
1968
|
for (const warning of deferredWarnings) fastify.log.warn(warning);
|
|
2008
1969
|
if (numericTrustProxy) fastify.log.warn(`[arc] trustProxy: ${String(config.trustProxy)} (a hop COUNT) trusts nothing on Fastify >= 5.12.1 — hop-count trust cannot validate the immediate peer, so it fails closed upstream to stop clients spoofing X-Forwarded-* with padded hops. \`request.ip\` is therefore the PROXY's address: rate-limit keys collapse onto one bucket and audit rows record the wrong client. Name the proxies instead — trustProxy: 'loopback', '10.0.0.0/8', a comma-separated list, or \`true\` if every upstream hop is genuinely trusted.`);
|
|
2009
|
-
if (inheritedTrustProxyDefault) fastify.log.warn("[arc] preset: 'production'
|
|
1970
|
+
if (inheritedTrustProxyDefault) fastify.log.warn("[arc] preset: 'production' defaults to trustProxy: false (fail-closed). If this app runs behind a load balancer or reverse proxy, request.ip is currently the PROXY's address — rate-limit keys collapse onto one bucket and audit logs record the wrong client. Set trustProxy explicitly: the proxies' addresses ('loopback', '10.0.0.0/8', a comma-separated list), a resolver function, or false if clients truly connect directly. A hop count trusts nothing on Fastify >= 5.12.1.");
|
|
2010
1971
|
/**
|
|
2011
|
-
*
|
|
2012
|
-
*
|
|
2013
|
-
* All three are read where the object is CONSTRUCTED — `defineResource` at
|
|
2014
|
-
* define time, `QueryParser` and `BodySanitizer` in their constructors — and a
|
|
2015
|
-
* host imports its resources before it calls `createApp`. So setting
|
|
2016
|
-
* `process.env` here would be too late for everything already built: wired to
|
|
2017
|
-
* read, doing nothing. Reporting cannot fail that way.
|
|
2018
|
-
*
|
|
2019
|
-
* Set them in the process entrypoint, BEFORE the first resource import, with
|
|
2020
|
-
* `??=` so an operator's explicit `false` still wins.
|
|
1972
|
+
* Production names every fail-loud switch turned OFF. They are read before `createApp`, so this
|
|
1973
|
+
* reports rather than sets.
|
|
2021
1974
|
*/
|
|
2022
1975
|
if (config.preset === "production") {
|
|
2023
|
-
const
|
|
2024
|
-
|
|
1976
|
+
const env = readEnv(process.env);
|
|
1977
|
+
const off = STRICT_ENV_FLAGS.filter(([name]) => !env.bool(name, true));
|
|
1978
|
+
if (off.length > 0) fastify.log.warn(`[arc] preset: 'production' with fail-loud turned OFF: ${off.map(([n]) => n).join(", ")}. ${off.map(([, what]) => what).join(" ")} They default ON; unset them to restore enforcement. Per-resource options still override.`);
|
|
2025
1979
|
}
|
|
2026
1980
|
fastify.removeContentTypeParser("application/json");
|
|
2027
1981
|
fastify.addContentTypeParser("application/json", { parseAs: "string" }, (_req, body, done) => {
|
|
@@ -2053,15 +2007,14 @@ async function buildApp(options, loggerScope, onInstance) {
|
|
|
2053
2007
|
await registerAuth(fastify, config, trackPlugin);
|
|
2054
2008
|
await registerElevation(fastify, config, trackPlugin);
|
|
2055
2009
|
await registerErrorHandler(fastify, config, trackPlugin, moduleErrorMappers);
|
|
2056
|
-
await registerResources(fastify, config, resolvedModules);
|
|
2010
|
+
await registerResources(fastify, config, resolvedModules, skippedBridges);
|
|
2057
2011
|
if (config.replyHelpers) {
|
|
2058
|
-
const { replyHelpersPlugin } = await import("./replyHelpers-
|
|
2012
|
+
const { replyHelpersPlugin } = await import("./replyHelpers-CG_VtVgS.mjs").then((n) => n.n);
|
|
2059
2013
|
await fastify.register(replyHelpersPlugin);
|
|
2060
2014
|
}
|
|
2061
2015
|
if (config.serializeBigInt) {
|
|
2062
|
-
const mode = config.serializeBigInt
|
|
2063
|
-
if (
|
|
2064
|
-
else if (mode === "number") arcLog("createApp").warn("serializeBigInt: 'number' loses precision above Number.MAX_SAFE_INTEGER (9007199254740991). Use 'string' instead unless you've audited that no bigint payload field can exceed the safe range.");
|
|
2016
|
+
const mode = config.serializeBigInt;
|
|
2017
|
+
if (mode === "number") arcLog("createApp").warn("serializeBigInt: 'number' loses precision above Number.MAX_SAFE_INTEGER (9007199254740991). Use 'string' instead unless you've audited that no bigint payload field can exceed the safe range.");
|
|
2065
2018
|
fastify.addHook("preSerialization", async (_request, _reply, payload) => {
|
|
2066
2019
|
if (payload === null || payload === void 0) return payload;
|
|
2067
2020
|
try {
|
|
@@ -2107,48 +2060,4 @@ const ArcFactory = {
|
|
|
2107
2060
|
}
|
|
2108
2061
|
};
|
|
2109
2062
|
//#endregion
|
|
2110
|
-
|
|
2111
|
-
/**
|
|
2112
|
-
* Normalize an eager `import.meta.glob` result into a `ResourceLike[]`.
|
|
2113
|
-
*
|
|
2114
|
-
* Accepts either:
|
|
2115
|
-
* - `{ import: 'default' }` form: values are the resource directly
|
|
2116
|
-
* - default form: values are the full module — picks first export with `toPlugin()`
|
|
2117
|
-
*
|
|
2118
|
-
* Throws if any module doesn't yield a valid `ResourceLike`.
|
|
2119
|
-
*/
|
|
2120
|
-
function preloadResources(globResult) {
|
|
2121
|
-
const resources = [];
|
|
2122
|
-
for (const [path, value] of Object.entries(globResult)) {
|
|
2123
|
-
const resource = pickResource(value);
|
|
2124
|
-
if (!resource) throw new Error(`preloadResources: ${path} does not export a valid resource.\n Expected: a default export OR a named export with toPlugin().`);
|
|
2125
|
-
resources.push(resource);
|
|
2126
|
-
}
|
|
2127
|
-
return resources.sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
|
|
2128
|
-
}
|
|
2129
|
-
/**
|
|
2130
|
-
* Normalize a lazy `import.meta.glob` result into a `Promise<ResourceLike[]>`.
|
|
2131
|
-
*
|
|
2132
|
-
* Use this when resources depend on prior bootstrap (e.g., engine init) and
|
|
2133
|
-
* cannot be evaluated at import time of the preload file.
|
|
2134
|
-
*/
|
|
2135
|
-
async function preloadResourcesAsync(globResult) {
|
|
2136
|
-
return (await Promise.all(Object.entries(globResult).map(async ([path, loader]) => {
|
|
2137
|
-
const resource = pickResource(await loader());
|
|
2138
|
-
if (!resource) throw new Error(`preloadResourcesAsync: ${path} does not export a valid resource.\n Expected: a default export OR a named export with toPlugin().`);
|
|
2139
|
-
return resource;
|
|
2140
|
-
}))).sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
|
|
2141
|
-
}
|
|
2142
|
-
function pickResource(value) {
|
|
2143
|
-
if (!value || typeof value !== "object") return void 0;
|
|
2144
|
-
if (typeof value.toPlugin === "function") return value;
|
|
2145
|
-
const mod = value;
|
|
2146
|
-
const candidates = [
|
|
2147
|
-
mod.default,
|
|
2148
|
-
mod.resource,
|
|
2149
|
-
...Object.values(mod)
|
|
2150
|
-
];
|
|
2151
|
-
for (const c of candidates) if (c && typeof c === "object" && typeof c.toPlugin === "function") return c;
|
|
2152
|
-
}
|
|
2153
|
-
//#endregion
|
|
2154
|
-
export { resolveModule as S, describeResolvedModule as _, createApp as a, collectModuleWorkflows as b, edgePreset as c, testingPreset as d, workerPreset as f, subscribeModuleEventHandlers as g, orderModules as h, DEFAULT_LOGGER_REDACT_PATHS as i, getPreset as l, hasModule as m, preloadResourcesAsync as n, createApp_exports as o, getModuleState as p, ArcFactory as r, developmentPreset as s, preloadResources as t, productionPreset as u, collectModuleHealthChecks as v, resolveContribution as x, collectModuleScheduledJobs as y };
|
|
2063
|
+
export { developmentPreset as a, productionPreset as c, getModuleResources as d, getModuleState as f, collectModuleWorkflows as g, asModulePorts as h, createApp_exports as i, testingPreset as l, subscribeModuleEventHandlers as m, DEFAULT_LOGGER_REDACT_PATHS as n, edgePreset as o, hasModule as p, createApp as r, getPreset as s, ArcFactory as t, workerPreset as u };
|