@classytic/arc 2.30.0 → 2.32.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/LICENSE +1 -1
- package/README.md +7 -0
- package/TRADEMARK.md +22 -0
- package/dist/{BaseController-B5DwufbB.mjs → BaseController-Dk34dxhI.mjs} +21 -19
- package/dist/{EventTransport-QmY7A258.d.mts → EventTransport-2uVV34WU.d.mts} +1 -0
- package/dist/{EventTransport-C6YA8xY9.mjs → EventTransport-BV10D0oG.mjs} +1 -0
- package/dist/{HookSystem-WkIougo6.mjs → HookSystem-vimw5D7Z.mjs} +2 -1
- package/dist/{QueryCache-DOMT_iq7.d.mts → QueryCache-DFA-QrU9.d.mts} +2 -1
- package/dist/{ResourceRegistry-y9Hnypus.mjs → ResourceRegistry-BVr7PgA0.mjs} +18 -15
- package/dist/{actionPermissions-CAphcmUG.mjs → actionPermissions-Dti7ebyv.mjs} +1 -0
- package/dist/audit/index.d.mts +3 -2
- package/dist/audit/index.mjs +2 -1
- package/dist/auth/audit.d.mts +1 -0
- package/dist/auth/audit.mjs +1 -0
- package/dist/auth/index.d.mts +5 -4
- package/dist/auth/index.mjs +11 -9
- package/dist/auth/redis-session.d.mts +2 -1
- package/dist/auth/redis-session.mjs +1 -0
- package/dist/{applyPermissionResult-Cf6zUCvb.mjs → authorizationDecision-__T3EJDA.mjs} +19 -10
- package/dist/{buildHandler-BYS3Vkfz.mjs → buildHandler-Dn7rzUJo.mjs} +6 -4
- package/dist/cache/index.d.mts +4 -3
- package/dist/cache/index.mjs +4 -3
- package/dist/{caching-_NJeDd1_.mjs → caching-DFJqHyFs.mjs} +2 -1
- package/dist/cleanup/index.d.mts +39 -16
- package/dist/cleanup/index.mjs +50 -38
- package/dist/cli/commands/describe.d.mts +2 -1
- package/dist/cli/commands/describe.mjs +4 -5
- package/dist/cli/commands/docs.d.mts +1 -0
- package/dist/cli/commands/docs.mjs +3 -2
- package/dist/cli/commands/doctor.d.mts +1 -0
- package/dist/cli/commands/doctor.mjs +1 -0
- package/dist/cli/commands/generate.d.mts +2 -1
- package/dist/cli/commands/generate.mjs +2 -1
- package/dist/cli/commands/init.d.mts +2 -1
- package/dist/cli/commands/init.mjs +2 -1
- package/dist/cli/commands/introspect.d.mts +1 -0
- package/dist/cli/commands/introspect.mjs +2 -1
- package/dist/cli/index.d.mts +3 -2
- package/dist/cli/index.mjs +3 -2
- package/dist/{constants-DNre0wRg.mjs → constants-J99LGTJI.mjs} +1 -0
- package/dist/context/index.d.mts +1 -0
- package/dist/context/index.mjs +3 -2
- package/dist/core/index.d.mts +3 -2
- package/dist/core/index.mjs +7 -6
- package/dist/{core-poalJLvp.mjs → core-BwwmNSp7.mjs} +3 -2
- package/dist/{createActionRouter-BxuywVRB.mjs → createActionRouter-DkJZWo0m.mjs} +14 -10
- package/dist/{createAggregationRouter-BiY-lGdY.mjs → createAggregationRouter-B1exOVD8.mjs} +10 -7
- package/dist/{createApp-CXrKFciH.mjs → createApp-Bv0m-Q6_.mjs} +625 -229
- package/dist/{defineAggregation-ufWjUwml.mjs → defineAggregation-gX4No1gt.mjs} +1 -0
- package/dist/{defineResource-CxWqDCAy.mjs → defineResource-XFPPUu2c.mjs} +84 -50
- package/dist/discovery/index.d.mts +1 -0
- package/dist/discovery/index.mjs +1 -0
- package/dist/docs/index.d.mts +3 -2
- package/dist/docs/index.mjs +3 -2
- package/dist/{elevation-C9s5hFP_.d.mts → elevation-BXCfVcNG.d.mts} +1 -0
- package/dist/{elevation-CIz1GjGm.mjs → elevation-DNXXxgjl.mjs} +4 -3
- package/dist/encryption/index.d.mts +2 -1
- package/dist/encryption/index.mjs +5 -3
- package/dist/{errorHandler-Bq2OeEQL.mjs → errorHandler-B2AZUJUj.mjs} +3 -2
- package/dist/{errorHandler-C6yv4ZTW.d.mts → errorHandler-CpYUJmPA.d.mts} +1 -0
- package/dist/{errors-DHcHVAgk.d.mts → errors-BGdVwP25.d.mts} +1 -0
- package/dist/{errors-DkO8Nk6-.mjs → errors-Bh1KK5ps.mjs} +1 -0
- package/dist/{eventPlugin-D2NNR4py.mjs → eventPlugin-Dk_TyXeE.mjs} +8 -6
- package/dist/{eventPlugin-Bg3cP2_u.d.mts → eventPlugin-GF5nnBIq.d.mts} +2 -1
- package/dist/events/index.d.mts +112 -44
- package/dist/events/index.mjs +177 -141
- package/dist/events/transports/redis-stream-entry.d.mts +2 -1
- package/dist/events/transports/redis-stream-entry.mjs +14 -9
- package/dist/events/transports/redis.d.mts +2 -1
- package/dist/events/transports/redis.mjs +1 -0
- package/dist/{externalPaths-BE6wFpYv.d.mts → externalPaths-CmGOxOtD.d.mts} +1 -0
- package/dist/factory/index.d.mts +9 -5
- package/dist/factory/index.mjs +131 -7
- package/dist/fastify-augmentation-BppG5bMT.d.mts +179 -0
- package/dist/{fields-CQ_7KQWw.mjs → fields-DEi65pGj.mjs} +6 -10
- package/dist/{fields-D3lhgzhD.d.mts → fields-I_EeRWND.d.mts} +1 -0
- package/dist/{generate-D0wZupBg.mjs → generate-CRSG0jTv.mjs} +2 -1
- package/dist/{generate-BnsL9GBm.d.mts → generate-DCGuEbqI.d.mts} +1 -0
- package/dist/{headers-BvlQSDMB.mjs → headers-BXjQYbeC.mjs} +2 -1
- package/dist/{health-CB_m4S32.mjs → health-5XnTa2uo.mjs} +1 -0
- package/dist/hooks/index.d.mts +2 -1
- package/dist/hooks/index.mjs +2 -1
- package/dist/idempotency/index.d.mts +3 -2
- package/dist/idempotency/index.mjs +8 -5
- package/dist/idempotency/redis.d.mts +2 -1
- package/dist/idempotency/redis.mjs +1 -0
- package/dist/{index-CG4yqMDB.d.mts → index-C-481Zf2.d.mts} +74 -18
- package/dist/{index-Q5gzEIWY.d.mts → index-CpzxqePp.d.mts} +107 -32
- package/dist/{index-Cx7bro4R.d.mts → index-DXlHwvuE.d.mts} +5 -4
- package/dist/index-ZWhLTp_s.d.mts +290 -0
- package/dist/{index-Ch16aAah.d.mts → index-zq6VrjaB.d.mts} +122 -15
- package/dist/index.d.mts +9 -8
- package/dist/index.mjs +11 -10
- package/dist/{init-tlRSt667.mjs → init-BbfP8aky.mjs} +25 -27
- package/dist/{init-BPEu8cs-.d.mts → init-Cscs4lab.d.mts} +1 -0
- package/dist/integrations/event-gateway.d.mts +3 -2
- package/dist/integrations/event-gateway.mjs +2 -1
- package/dist/integrations/index.d.mts +4 -3
- package/dist/integrations/index.mjs +1 -0
- package/dist/integrations/jobs/index.d.mts +2 -1
- package/dist/integrations/jobs/index.mjs +109 -33
- package/dist/integrations/mcp/index.d.mts +6 -6
- package/dist/integrations/mcp/index.mjs +2 -1
- package/dist/integrations/mcp/testing.d.mts +3 -2
- package/dist/integrations/mcp/testing.mjs +2 -2
- package/dist/integrations/streamline.d.mts +1 -0
- package/dist/integrations/streamline.mjs +3 -2
- package/dist/integrations/webhooks.d.mts +1 -0
- package/dist/integrations/webhooks.mjs +1 -0
- package/dist/integrations/websocket-pushref-redis.d.mts +2 -1
- package/dist/integrations/websocket-pushref-redis.mjs +1 -0
- package/dist/integrations/websocket-redis.d.mts +2 -1
- package/dist/integrations/websocket-redis.mjs +1 -0
- package/dist/integrations/websocket.d.mts +3 -2
- package/dist/integrations/websocket.mjs +38 -34
- package/dist/{interface-BGZCzOGL.d.mts → interface-BOk3YnTz.d.mts} +1 -0
- package/dist/{interface-n5hpKg1d.mjs → interface-BP4IJCBO.mjs} +1 -0
- package/dist/{interface-Uk8ONJv7.d.mts → interface-DEWNOMza.d.mts} +1 -0
- package/dist/{jsonBody-BlzAoGCe.mjs → jsonBody-DDSR2HFl.mjs} +2 -1
- package/dist/{keys-Cd316u_y.mjs → keys-B-HBTGXl.mjs} +1 -0
- package/dist/{loadResources-DpU8pfjV.mjs → loadResources-DisEAmkm.mjs} +4 -2
- package/dist/{loadResourcesFromEntry-kvGT6EcO.mjs → loadResourcesFromEntry-D1MJp13f.mjs} +2 -1
- package/dist/logger/index.d.mts +1 -0
- package/dist/logger/index.mjs +1 -0
- package/dist/{mcpPlugin-nRzm5g2s.mjs → mcpPlugin-CpfVpKHE.mjs} +38 -38
- package/dist/{memory-CmnzXE5Z.mjs → memory-DsuWnj4T.mjs} +5 -4
- package/dist/{metrics-fdxI5mn_.mjs → metrics-DdQL8syd.mjs} +2 -1
- package/dist/middleware/index.d.mts +2 -1
- package/dist/middleware/index.mjs +3 -2
- package/dist/migrations/index.d.mts +1 -0
- package/dist/migrations/index.mjs +3 -2
- package/dist/module-Br81am6m.mjs +78 -0
- package/dist/{multipartBody-D0eBvkeq.mjs → multipartBody-BnkBLN7v.mjs} +3 -2
- package/dist/{openapi-B8Vkbxbn.mjs → openapi-DIRzvxC0.mjs} +6 -5
- package/dist/outbox-admin/index.d.mts +85 -0
- package/dist/outbox-admin/index.mjs +115 -0
- package/dist/permissions/index.d.mts +5 -4
- package/dist/permissions/index.mjs +6 -5
- package/dist/{permissions-Bfu_8y1U.mjs → permissions-Dl_m0NCe.mjs} +308 -42
- package/dist/{pipe-BOLmGIxa.mjs → pipe-ELHCP3zj.mjs} +3 -4
- package/dist/pipeline/index.d.mts +2 -1
- package/dist/pipeline/index.mjs +2 -1
- package/dist/{pipeline-BJnpkY9i.mjs → pipeline-CPA-jx0M.mjs} +17 -14
- package/dist/plugins/index.d.mts +8 -7
- package/dist/plugins/index.mjs +16 -15
- package/dist/plugins/response-cache.d.mts +1 -0
- package/dist/plugins/response-cache.mjs +2 -1
- package/dist/plugins/tracing-entry.d.mts +2 -1
- package/dist/plugins/tracing-entry.mjs +2 -1
- package/dist/{pluralize-BhrgwZb9.mjs → pluralize-C2ZkKbsn.mjs} +1 -0
- package/dist/{preloadResources-DSY5HGpS.mjs → preloadResources-CDs87fTw.mjs} +1 -0
- package/dist/{preloadResources-DCfa2egz.d.mts → preloadResources-T1hmlTEZ.d.mts} +117 -12
- package/dist/presets/filesUpload.d.mts +6 -5
- package/dist/presets/filesUpload.mjs +10 -12
- package/dist/presets/index.d.mts +3 -2
- package/dist/presets/index.mjs +2 -1
- package/dist/presets/multiTenant.d.mts +2 -1
- package/dist/presets/multiTenant.mjs +3 -2
- package/dist/presets/search.d.mts +3 -2
- package/dist/presets/search.mjs +2 -1
- package/dist/{presets-CJOEXLuk.mjs → presets-Dntd_pUO.mjs} +6 -4
- package/dist/{projection-UBDgfMwu.mjs → projection-CjHhM5W1.mjs} +2 -1
- package/dist/{pushref-registry-BbcnpNTm.d.mts → pushref-registry-Dov9l4qE.d.mts} +1 -0
- package/dist/{queryCachePlugin-br4otiqF.d.mts → queryCachePlugin-BzqxconG.d.mts} +3 -2
- package/dist/{queryCachePlugin-DSGuSVLW.mjs → queryCachePlugin-Dhps-Ldt.mjs} +6 -5
- package/dist/{rateLimit-DrED-C4v.mjs → rateLimit-C7mmAGs3.mjs} +7 -3
- package/dist/{rateLimitKey-BoWRH3e7.mjs → rateLimitKey-CccEFBfk.mjs} +1 -0
- package/dist/{realtime-B9nmIY5V.mjs → realtime-DVvVZZFz.mjs} +20 -9
- package/dist/{redis-UDtLaNj6.d.mts → redis-CduhKsJi.d.mts} +2 -1
- package/dist/{redis-stream-uIaauNpZ.d.mts → redis-stream-qN6A-RFv.d.mts} +2 -1
- package/dist/{registerRoute-JkxIEBJq.mjs → registerRoute-B_QrjYZS.mjs} +10 -7
- package/dist/registry/index.d.mts +3 -2
- package/dist/registry/index.mjs +3 -2
- package/dist/{registry-BYXUUg21.mjs → registry-DqUmzA9W.mjs} +2 -1
- package/dist/{renewingLease-3cuQH7h8.mjs → renewingLease-BpP9wwj7.mjs} +1 -0
- package/dist/{replyHelpers-67tRawQZ.mjs → replyHelpers-DS-P0b32.mjs} +2 -1
- package/dist/{repositoryFilter-Dz8k3S4P.mjs → repositoryFilter-DotlxG3L.mjs} +1 -0
- package/dist/{requestContext-MrlJwECf.mjs → requestContext-B6vQF-sy.mjs} +1 -0
- package/dist/{rolldown-runtime-8H4AJuhK.mjs → rolldown-runtime-BhflRrBs.mjs} +1 -0
- package/dist/{schedules-CjOd2MG9.mjs → schedules-C2Q3uv2d.mjs} +3 -2
- package/dist/{schemaConverter-DJvodiHj.mjs → schemaConverter-Crjvczi_.mjs} +70 -17
- package/dist/{schemaIR-Ca7ySNPx.mjs → schemaIR-CnyKK_1j.mjs} +2 -1
- package/dist/schemas/index.d.mts +1 -0
- package/dist/schemas/index.mjs +1 -0
- package/dist/scim/index.d.mts +1 -0
- package/dist/scim/index.mjs +2 -1
- package/dist/scope/index.d.mts +3 -2
- package/dist/scope/index.mjs +5 -4
- package/dist/{scope-DfaPntpo.mjs → scope-afz_Ieek.mjs} +4 -3
- package/dist/{sessionManager-BHOM3N_0.d.mts → sessionManager-qimlKV0P.d.mts} +1 -0
- package/dist/{simpleEqualityMatcher-Cg05HPsn.mjs → simpleEqualityMatcher-C4KiMWUa.mjs} +1 -0
- package/dist/{sse-BQ1w0dfl.mjs → sse-B9P4Y-UI.mjs} +7 -5
- package/dist/{sseStream-BQ7Qaynp.mjs → sseStream-B3zb_d1h.mjs} +2 -1
- package/dist/{storage-DLUZ04kK.d.mts → storage-Cx9Sz3Ou.d.mts} +1 -0
- package/dist/{store-helpers-DY6xq4sM.mjs → store-helpers-DmEMHlPQ.mjs} +1 -0
- package/dist/{streaming-BQcW6_Kq.mjs → streaming-DPZdQmDz.mjs} +1 -0
- package/dist/{subscribe-helpers-DfzsivM6.mjs → subscribe-helpers-sp5xTDVA.mjs} +2 -1
- package/dist/sync/changelog.d.mts +1 -0
- package/dist/sync/changelog.mjs +1 -0
- package/dist/testing/cleanupStoreContract.d.mts +2 -1
- package/dist/testing/cleanupStoreContract.mjs +10 -5
- package/dist/testing/index.d.mts +166 -12
- package/dist/testing/index.mjs +315 -43
- package/dist/testing/outboxStoreContract.d.mts +12 -2
- package/dist/testing/outboxStoreContract.mjs +110 -20
- package/dist/testing/storageContract.d.mts +2 -1
- package/dist/testing/storageContract.mjs +11 -5
- package/dist/{tracing-CsGKQJGg.d.mts → tracing-Dro5eH6j.d.mts} +1 -0
- package/dist/{transactionContext-PNdoyLLm.mjs → transactionContext-BvLKvOIL.mjs} +1 -0
- package/dist/{typeGuards-C9KXUly6.mjs → typeGuards-B_5-hKjJ.mjs} +1 -0
- package/dist/types/index.d.mts +6 -5
- package/dist/types/index.mjs +1 -0
- package/dist/types/storage.d.mts +2 -1
- package/dist/types/storage.mjs +1 -0
- package/dist/{types-ChvvVNP8.d.mts → types-B7XeLEgK.d.mts} +34 -2
- package/dist/{types-Cc91nXuA.d.mts → types-ByHn9rEO.d.mts} +1 -0
- package/dist/{types-C_ZZRcD6.d.mts → types-C4QE8qnm.d.mts} +1 -0
- package/dist/{types-D_n8L6VY.d.mts → types-CT5vu6M0.d.mts} +132 -17
- package/dist/{types-DjC-KUCz.d.mts → types-ChuShpCP.d.mts} +1 -0
- package/dist/{types-Bmmn-fO0.d.mts → types-CwcUQ610.d.mts} +2 -6
- package/dist/{types-CSyCxl2V.mjs → types-DXYX7dIR.mjs} +4 -3
- package/dist/usage/index.d.mts +3 -2
- package/dist/usage/index.mjs +0 -0
- package/dist/{userHelpers-Cg_y3fBz.mjs → userHelpers-BNsOXzOd.mjs} +1 -0
- package/dist/utils/index.d.mts +16 -7
- package/dist/utils/index.mjs +10 -9
- package/dist/{utils-taUVD4p3.mjs → utils-BtOha_Sp.mjs} +3 -2
- package/dist/{validate-Bk44F2yT.mjs → validate-PMCPR9El.mjs} +2 -1
- package/dist/{versioning-gKWyryT3.mjs → versioning-CKv2jz_U.mjs} +4 -3
- package/dist/{versioning-DXgY5BUm.d.mts → versioning-D_rhHfZm.d.mts} +5 -2
- package/dist/{websocket-_Pd0CgR0.d.mts → websocket-BOXVggMf.d.mts} +2 -1
- package/package.json +557 -539
- package/skills/arc/SKILL.md +22 -8
- package/skills/arc/references/api-reference.md +4 -0
- package/skills/arc/references/auth.md +3 -0
- package/skills/arc/references/integrations.md +33 -0
- package/skills/arc/references/mcp.md +6 -7
- package/skills/arc/references/production.md +24 -25
- package/skills/arc-code-review/references/anti-patterns.md +3 -3
- package/dist/fastify-augmentation-0WhPnUH7.d.mts +0 -76
- package/dist/index-CCzVR-N3.d.mts +0 -107
- package/dist/module-_-M7C5ES.mjs +0 -29
|
@@ -1,12 +1,158 @@
|
|
|
1
|
-
|
|
1
|
+
/*! @classytic/arc v2.32.0 | MIT | https://classytic.com */
|
|
2
|
+
import { t as __exportAll } from "./rolldown-runtime-BhflRrBs.mjs";
|
|
2
3
|
import { arcLog, configureArcLogger, createPinoWriter } from "./logger/index.mjs";
|
|
3
|
-
import { n as PUBLIC_SCOPE } from "./types-
|
|
4
|
-
import { t as createTenantKeyGenerator } from "./rateLimitKey-
|
|
5
|
-
import { i as createRequestIdGenerator, r as mergeHealthChecks } from "./health-
|
|
6
|
-
import { t as parseJsonBody } from "./jsonBody-
|
|
7
|
-
import { r as wrapWithBoundary } from "./subscribe-helpers-
|
|
4
|
+
import { n as PUBLIC_SCOPE } from "./types-DXYX7dIR.mjs";
|
|
5
|
+
import { t as createTenantKeyGenerator } from "./rateLimitKey-CccEFBfk.mjs";
|
|
6
|
+
import { i as createRequestIdGenerator, r as mergeHealthChecks } from "./health-5XnTa2uo.mjs";
|
|
7
|
+
import { t as parseJsonBody } from "./jsonBody-DDSR2HFl.mjs";
|
|
8
|
+
import { r as wrapWithBoundary } from "./subscribe-helpers-sp5xTDVA.mjs";
|
|
8
9
|
import Fastify from "fastify";
|
|
9
10
|
import qs from "qs";
|
|
11
|
+
//#region src/factory/security/pluginLoader.ts
|
|
12
|
+
const PLUGIN_REGISTRY = {
|
|
13
|
+
cors: {
|
|
14
|
+
package: "@fastify/cors",
|
|
15
|
+
loader: () => import("@fastify/cors").then((m) => m.default)
|
|
16
|
+
},
|
|
17
|
+
helmet: {
|
|
18
|
+
package: "@fastify/helmet",
|
|
19
|
+
loader: () => import("@fastify/helmet").then((m) => m.default)
|
|
20
|
+
},
|
|
21
|
+
rateLimit: {
|
|
22
|
+
package: "@fastify/rate-limit",
|
|
23
|
+
loader: () => import("@fastify/rate-limit").then((m) => m.default)
|
|
24
|
+
},
|
|
25
|
+
underPressure: {
|
|
26
|
+
package: "@fastify/under-pressure",
|
|
27
|
+
loader: () => import("@fastify/under-pressure").then((m) => m.default)
|
|
28
|
+
},
|
|
29
|
+
sensible: {
|
|
30
|
+
package: "@fastify/sensible",
|
|
31
|
+
loader: () => import("@fastify/sensible").then((m) => m.default)
|
|
32
|
+
},
|
|
33
|
+
multipart: {
|
|
34
|
+
package: "@fastify/multipart",
|
|
35
|
+
loader: () => import("@fastify/multipart").then((m) => m.default),
|
|
36
|
+
optional: true
|
|
37
|
+
},
|
|
38
|
+
rawBody: {
|
|
39
|
+
package: "fastify-raw-body",
|
|
40
|
+
loader: () => import("fastify-raw-body").then((m) => m.default),
|
|
41
|
+
optional: true
|
|
42
|
+
},
|
|
43
|
+
static: {
|
|
44
|
+
package: "@fastify/static",
|
|
45
|
+
loader: () => import("@fastify/static").then((m) => m.default),
|
|
46
|
+
optional: true
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
async function loadPlugin(name, logger) {
|
|
50
|
+
const entry = PLUGIN_REGISTRY[name];
|
|
51
|
+
if (!entry) throw new Error(`Unknown plugin: ${name}`);
|
|
52
|
+
try {
|
|
53
|
+
return await entry.loader();
|
|
54
|
+
} catch (error) {
|
|
55
|
+
const err = error;
|
|
56
|
+
const isModuleNotFound = err.message.includes("Cannot find module") || err.message.includes("Cannot find package") || err.message.includes("MODULE_NOT_FOUND") || err.message.includes("Could not resolve");
|
|
57
|
+
if (isModuleNotFound && entry.optional) {
|
|
58
|
+
logger?.warn(`Optional plugin '${name}' skipped (${entry.package} not installed)`);
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
if (isModuleNotFound) throw new Error(`Plugin '${name}' requires package '${entry.package}' which is not installed.\nInstall it with: npm install ${entry.package}\nOr disable this plugin by setting ${name}: false in createApp options.`);
|
|
62
|
+
throw new Error(`Failed to load plugin '${name}': ${err.message}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/factory/assets.ts
|
|
67
|
+
/**
|
|
68
|
+
* Cache-Control per preset.
|
|
69
|
+
*
|
|
70
|
+
* - `immutable` — content-addressed output ONLY (a hashed filename that changes
|
|
71
|
+
* when the bytes change). One year + `immutable` so a revalidation is never
|
|
72
|
+
* even attempted.
|
|
73
|
+
* - `revalidate` — the default. The client may cache but MUST check freshness;
|
|
74
|
+
* `@fastify/static`'s `ETag`/`Last-Modified` then makes the common case a 304
|
|
75
|
+
* with no body, which is nearly as cheap as a cache hit and always correct.
|
|
76
|
+
* - `none` — never store. For a path that is authorization-dependent, where a
|
|
77
|
+
* shared cache holding the bytes would leak them to the next caller.
|
|
78
|
+
*/
|
|
79
|
+
const CACHE_CONTROL = {
|
|
80
|
+
immutable: "public, max-age=31536000, immutable",
|
|
81
|
+
revalidate: "public, max-age=0, must-revalidate",
|
|
82
|
+
none: "no-store"
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Resolve one asset root's declaration into the header policy arc applies per
|
|
86
|
+
* file. Pure — no Fastify, no filesystem — so the policy is unit-testable
|
|
87
|
+
* without booting an app.
|
|
88
|
+
*/
|
|
89
|
+
function resolveAssetPolicy(root) {
|
|
90
|
+
const cache = root.cache ?? "revalidate";
|
|
91
|
+
const crossOrigin = root.crossOrigin ?? "same-site";
|
|
92
|
+
const disposition = root.disposition ?? "attachment";
|
|
93
|
+
return {
|
|
94
|
+
cacheControl: CACHE_CONTROL[cache],
|
|
95
|
+
crossOriginResourcePolicy: crossOrigin,
|
|
96
|
+
disposition,
|
|
97
|
+
varyOrigin: crossOrigin !== "same-origin"
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/** Quote a filename for `Content-Disposition` per RFC 6266 / RFC 5987. */
|
|
101
|
+
function contentDisposition(type, filePath) {
|
|
102
|
+
const name = filePath.split(/[\\/]/).pop() ?? "download";
|
|
103
|
+
return `${type}; filename="${name.replace(/[^\x20-\x7e]/g, "_").replace(/["\\]/g, "\\$&")}"; filename*=UTF-8''${encodeURIComponent(name)}`;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Register every declared asset root.
|
|
107
|
+
*
|
|
108
|
+
* Each root becomes its OWN `@fastify/static` registration. That is deliberate:
|
|
109
|
+
* `@fastify/static` scopes `prefix`, `root`, and the send options per
|
|
110
|
+
* registration, so two roots with different cache or cross-origin policy stay
|
|
111
|
+
* genuinely independent instead of sharing one mutable options bag.
|
|
112
|
+
*
|
|
113
|
+
* Registered AFTER the security plugins so the per-prefix CORP override lands on
|
|
114
|
+
* top of helmet's app-wide default — helmet sets its headers in an `onRequest`
|
|
115
|
+
* hook, while `setHeaders` runs at send time, so the asset policy wins.
|
|
116
|
+
*/
|
|
117
|
+
async function registerAssetRoots(fastify, roots) {
|
|
118
|
+
if (roots.length === 0) return;
|
|
119
|
+
const staticPlugin = await loadPlugin("static", fastify.log);
|
|
120
|
+
if (!staticPlugin) throw new Error("[arc] `assets` was configured but '@fastify/static' is not installed. Install it with: npm install @fastify/static — arc delegates ranges, ETags, immutable caching and pre-compressed variants to it rather than reimplementing them.");
|
|
121
|
+
const seen = /* @__PURE__ */ new Map();
|
|
122
|
+
for (const root of roots) {
|
|
123
|
+
const prior = seen.get(root.prefix);
|
|
124
|
+
if (prior !== void 0) throw new Error(`[arc] duplicate asset prefix "${root.prefix}" — declared for roots "${prior}" and "${root.root}". Asset prefixes must be unique; nest them (e.g. "/files/public" and "/files/private") instead of sharing one.`);
|
|
125
|
+
seen.set(root.prefix, String(root.root));
|
|
126
|
+
}
|
|
127
|
+
for (const root of roots) {
|
|
128
|
+
const policy = resolveAssetPolicy(root);
|
|
129
|
+
await fastify.register(staticPlugin, {
|
|
130
|
+
root: root.root,
|
|
131
|
+
prefix: root.prefix,
|
|
132
|
+
decorateReply: root.decorateReply ?? false,
|
|
133
|
+
list: false,
|
|
134
|
+
index: root.index ?? false,
|
|
135
|
+
dotfiles: "deny",
|
|
136
|
+
serveDotFiles: false,
|
|
137
|
+
cacheControl: false,
|
|
138
|
+
etag: root.etag ?? true,
|
|
139
|
+
lastModified: root.lastModified ?? true,
|
|
140
|
+
acceptRanges: root.acceptRanges ?? true,
|
|
141
|
+
...root.preCompressed !== void 0 ? { preCompressed: root.preCompressed } : {},
|
|
142
|
+
...root.allowedPath ? { allowedPath: root.allowedPath } : {},
|
|
143
|
+
setHeaders: (reply, filePath) => {
|
|
144
|
+
reply.header("Cache-Control", policy.cacheControl);
|
|
145
|
+
reply.header("Cross-Origin-Resource-Policy", policy.crossOriginResourcePolicy);
|
|
146
|
+
reply.header("Content-Disposition", contentDisposition(policy.disposition, filePath));
|
|
147
|
+
reply.header("X-Content-Type-Options", "nosniff");
|
|
148
|
+
if (policy.varyOrigin) reply.header("Vary", "Origin, Accept-Encoding");
|
|
149
|
+
root.setHeaders?.(reply, filePath);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
fastify.log.debug(`[arc] asset root "${root.prefix}" → ${String(root.root)} (cache: ${root.cache ?? "revalidate"}, cross-origin: ${policy.crossOriginResourcePolicy})`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
//#endregion
|
|
10
156
|
//#region src/factory/module/resolve.ts
|
|
11
157
|
/**
|
|
12
158
|
* Resolve any module-input form to a concrete `ArcModule`. A thunk is invoked
|
|
@@ -16,6 +162,7 @@ import qs from "qs";
|
|
|
16
162
|
async function resolveModule(input) {
|
|
17
163
|
const resolved = typeof input === "function" ? await input() : await input;
|
|
18
164
|
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.");
|
|
165
|
+
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.");
|
|
19
166
|
return resolved;
|
|
20
167
|
}
|
|
21
168
|
/** Resolve a `ModuleContribution` to a concrete array (array passthrough or factory call). */
|
|
@@ -23,6 +170,29 @@ async function resolveContribution(contribution, fastify) {
|
|
|
23
170
|
if (!contribution) return [];
|
|
24
171
|
return typeof contribution === "function" ? await contribution(fastify) : contribution;
|
|
25
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* `resolveContribution` with module attribution — the form every arm collector
|
|
175
|
+
* should use.
|
|
176
|
+
*
|
|
177
|
+
* A contribution factory closes over booted engines, so it throws for exactly
|
|
178
|
+
* the reasons a bootstrap does ("connection unavailable", "engine not ready").
|
|
179
|
+
* Bare, that message names neither the module nor the arm, which in a graph of
|
|
180
|
+
* twenty modules turns a one-line fix into a bisect. Every other module
|
|
181
|
+
* boundary (`plugins`, `bootstrap`, `resources`, `afterResources`) already
|
|
182
|
+
* wraps with the owning module's name; this keeps the arms consistent with
|
|
183
|
+
* them. The original error is preserved via `{ cause }`.
|
|
184
|
+
*/
|
|
185
|
+
async function resolveModuleArm(module, arm, contribution, fastify) {
|
|
186
|
+
let resolved;
|
|
187
|
+
try {
|
|
188
|
+
resolved = await resolveContribution(contribution, fastify);
|
|
189
|
+
} catch (err) {
|
|
190
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
191
|
+
throw new Error(`[arc] module "${module.name}" ${arm} factory threw: ${msg}`, { cause: err });
|
|
192
|
+
}
|
|
193
|
+
if (!Array.isArray(resolved)) throw new TypeError(`[arc] module "${module.name}" ${arm} factory must return an array — received ${resolved === null ? "null" : typeof resolved}.`);
|
|
194
|
+
return resolved;
|
|
195
|
+
}
|
|
26
196
|
//#endregion
|
|
27
197
|
//#region src/factory/module/contributions.ts
|
|
28
198
|
/**
|
|
@@ -49,7 +219,7 @@ function collectModuleHealthChecks(modules) {
|
|
|
49
219
|
async function collectModuleWorkflows(fastify, modules) {
|
|
50
220
|
const out = [];
|
|
51
221
|
for (const m of modules) {
|
|
52
|
-
const wfs = await
|
|
222
|
+
const wfs = await resolveModuleArm(m, "workflows", m.workflows, fastify);
|
|
53
223
|
out.push(...wfs);
|
|
54
224
|
}
|
|
55
225
|
return out;
|
|
@@ -60,11 +230,12 @@ async function collectModuleWorkflows(fastify, modules) {
|
|
|
60
230
|
* modules. The caller passes the returned definitions to the canonical
|
|
61
231
|
* schedules plugin; factories must therefore be resolved only once.
|
|
62
232
|
*/
|
|
63
|
-
async function collectModuleScheduledJobs(fastify, modules) {
|
|
233
|
+
async function collectModuleScheduledJobs(fastify, modules, countsByModule) {
|
|
64
234
|
const owner = /* @__PURE__ */ new Map();
|
|
65
235
|
const out = [];
|
|
66
236
|
for (const m of modules) {
|
|
67
|
-
const jobs = await
|
|
237
|
+
const jobs = await resolveModuleArm(m, "scheduledJobs", m.scheduledJobs, fastify);
|
|
238
|
+
countsByModule?.set(m.name, jobs.length);
|
|
68
239
|
for (const job of jobs) {
|
|
69
240
|
const prior = owner.get(job.name);
|
|
70
241
|
if (prior !== void 0) throw new Error(`[arc] duplicate scheduled-job name "${job.name}" — declared by module "${prior}" and module "${m.name}". Scheduled-job names must be unique across the module graph.`);
|
|
@@ -75,6 +246,69 @@ async function collectModuleScheduledJobs(fastify, modules) {
|
|
|
75
246
|
return out;
|
|
76
247
|
}
|
|
77
248
|
//#endregion
|
|
249
|
+
//#region src/factory/module/descriptor.ts
|
|
250
|
+
/**
|
|
251
|
+
* Build a descriptor. Called by `registerResources` once per module, after
|
|
252
|
+
* resources are resolved and `owns` is effective.
|
|
253
|
+
*
|
|
254
|
+
* Frozen at every level: a descriptor is a snapshot of a completed decision,
|
|
255
|
+
* and a consumer mutating it would silently disagree with the running app.
|
|
256
|
+
*/
|
|
257
|
+
function describeResolvedModule(input) {
|
|
258
|
+
return Object.freeze({
|
|
259
|
+
name: input.name,
|
|
260
|
+
dependsOn: Object.freeze([...input.dependsOn ?? []]),
|
|
261
|
+
resources: Object.freeze(input.resources.map((r) => Object.freeze({
|
|
262
|
+
...r.name !== void 0 ? { name: r.name } : {},
|
|
263
|
+
...r.prefix !== void 0 ? { prefix: r.prefix } : {},
|
|
264
|
+
skipGlobalPrefix: r.skipGlobalPrefix === true
|
|
265
|
+
}))),
|
|
266
|
+
owns: Object.freeze([...input.owns]),
|
|
267
|
+
lifecycle: Object.freeze({ ...input.lifecycle })
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
//#endregion
|
|
271
|
+
//#region src/factory/module/disposers.ts
|
|
272
|
+
function createDisposerRegistry(fastify) {
|
|
273
|
+
const stacks = /* @__PURE__ */ new Map();
|
|
274
|
+
const drained = /* @__PURE__ */ new Set();
|
|
275
|
+
const contexts = /* @__PURE__ */ new Map();
|
|
276
|
+
return {
|
|
277
|
+
contextFor(moduleName) {
|
|
278
|
+
const existing = contexts.get(moduleName);
|
|
279
|
+
if (existing) return existing;
|
|
280
|
+
const ctx = { defer(disposer) {
|
|
281
|
+
if (typeof disposer !== "function") throw new TypeError(`[arc] module "${moduleName}" called defer() with a ${typeof disposer} — expected a function.`);
|
|
282
|
+
if (drained.has(moduleName)) {
|
|
283
|
+
(async () => {
|
|
284
|
+
try {
|
|
285
|
+
await disposer();
|
|
286
|
+
} catch (err) {
|
|
287
|
+
fastify.log?.error({
|
|
288
|
+
err,
|
|
289
|
+
module: moduleName
|
|
290
|
+
}, `[arc] module "${moduleName}" deferred a disposer after teardown had already run; it was invoked immediately and threw.`);
|
|
291
|
+
}
|
|
292
|
+
})();
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const stack = stacks.get(moduleName);
|
|
296
|
+
if (stack) stack.push(disposer);
|
|
297
|
+
else stacks.set(moduleName, [disposer]);
|
|
298
|
+
} };
|
|
299
|
+
contexts.set(moduleName, ctx);
|
|
300
|
+
return ctx;
|
|
301
|
+
},
|
|
302
|
+
take(moduleName) {
|
|
303
|
+
drained.add(moduleName);
|
|
304
|
+
const stack = stacks.get(moduleName);
|
|
305
|
+
if (!stack?.length) return [];
|
|
306
|
+
stacks.delete(moduleName);
|
|
307
|
+
return stack.reverse();
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
//#endregion
|
|
78
312
|
//#region src/factory/module/lifecycle.ts
|
|
79
313
|
/**
|
|
80
314
|
* Subscribe every module's `eventHandlers` in dependency order (pass the
|
|
@@ -86,12 +320,13 @@ async function collectModuleScheduledJobs(fastify, modules) {
|
|
|
86
320
|
* Resolves factory contributions (after bootstraps) so a handler can close over
|
|
87
321
|
* booted engines rather than a global getter.
|
|
88
322
|
*/
|
|
89
|
-
async function subscribeModuleEventHandlers(fastify, modules) {
|
|
323
|
+
async function subscribeModuleEventHandlers(fastify, modules, countsByModule) {
|
|
90
324
|
const unsubscribes = [];
|
|
91
325
|
const owner = /* @__PURE__ */ new Map();
|
|
92
326
|
try {
|
|
93
327
|
for (const m of modules) {
|
|
94
|
-
const handlers = await
|
|
328
|
+
const handlers = await resolveModuleArm(m, "eventHandlers", m.eventHandlers, fastify);
|
|
329
|
+
countsByModule?.set(m.name, handlers.length);
|
|
95
330
|
if (handlers.length === 0) continue;
|
|
96
331
|
const bus = fastify.events;
|
|
97
332
|
if (!bus) throw new Error(`[arc] module "${m.name}" declares eventHandlers but the event subsystem is unavailable (fastify.events). Enable arcPlugins.events.`);
|
|
@@ -235,6 +470,110 @@ function describeModuleCycle(remaining, byName) {
|
|
|
235
470
|
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.`;
|
|
236
471
|
}
|
|
237
472
|
//#endregion
|
|
473
|
+
//#region src/factory/module/state.ts
|
|
474
|
+
/**
|
|
475
|
+
* The honest read of the state map. `fastify.arc` is optional (see
|
|
476
|
+
* fastify-augmentation.ts) and `moduleStates` is populated by
|
|
477
|
+
* registerResources — both absent means "no module graph composed here"
|
|
478
|
+
* (or a pre-2.31 embedding), so accessors degrade to undefined/false
|
|
479
|
+
* rather than throwing.
|
|
480
|
+
*/
|
|
481
|
+
function readStates(fastify) {
|
|
482
|
+
return fastify.arc?.moduleStates;
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Record every composed module as `resolved`. Runs at the top of
|
|
486
|
+
* registerResources, BEFORE any side-effecting phase — so from the first
|
|
487
|
+
* lifecycle callback onward, `hasModule()` answers for the whole graph.
|
|
488
|
+
* Null-proto map for the same reason as `arc.modules`: a module named
|
|
489
|
+
* "__proto__" must be an own key, not a prototype write.
|
|
490
|
+
*/
|
|
491
|
+
function initModuleStates(fastify, names) {
|
|
492
|
+
const arc = fastify.arc;
|
|
493
|
+
if (!arc) return;
|
|
494
|
+
const states = Object.create(null);
|
|
495
|
+
for (const name of names) states[name] = "resolved";
|
|
496
|
+
arc.moduleStates = states;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Internal transition writer — no-op when no state map exists (see above),
|
|
500
|
+
* and no-op OUT of `failed`, which is terminal: rollback closes a module whose
|
|
501
|
+
* bootstrap threw, and that cleanup succeeding must not erase the fact that
|
|
502
|
+
* initialization failed (see the file header).
|
|
503
|
+
*/
|
|
504
|
+
function setModuleState(fastify, name, state) {
|
|
505
|
+
const states = readStates(fastify);
|
|
506
|
+
if (!states) return;
|
|
507
|
+
if (states[name] === "failed") return;
|
|
508
|
+
states[name] = state;
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Is `name` in the composed module graph? TRUE for resource-only modules and
|
|
512
|
+
* modules that exported nothing — the question `hasModuleExports()` cannot
|
|
513
|
+
* answer. FALSE before registerResources runs (no graph yet) and for
|
|
514
|
+
* uncomposed names.
|
|
515
|
+
*/
|
|
516
|
+
function hasModule(fastify, name) {
|
|
517
|
+
const states = readStates(fastify);
|
|
518
|
+
return states !== void 0 && Object.hasOwn(states, name);
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* The module's current lifecycle state, or `undefined` when the module is not
|
|
522
|
+
* composed (or no graph has been composed on this instance). See the state
|
|
523
|
+
* machine in the file header.
|
|
524
|
+
*/
|
|
525
|
+
function getModuleState(fastify, name) {
|
|
526
|
+
const states = readStates(fastify);
|
|
527
|
+
return states !== void 0 && Object.hasOwn(states, name) ? states[name] : void 0;
|
|
528
|
+
}
|
|
529
|
+
//#endregion
|
|
530
|
+
//#region src/factory/module/teardown.ts
|
|
531
|
+
function createModuleTeardown(fastify, modules, disposers) {
|
|
532
|
+
const initializedNames = /* @__PURE__ */ new Set();
|
|
533
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
534
|
+
async function closeInitialized() {
|
|
535
|
+
const errors = [];
|
|
536
|
+
for (let index = modules.length - 1; index >= 0; index--) {
|
|
537
|
+
const m = modules[index];
|
|
538
|
+
if (!m || !initializedNames.has(m.name) || consumed.has(m.name)) continue;
|
|
539
|
+
consumed.add(m.name);
|
|
540
|
+
const stack = disposers?.take(m.name) ?? [];
|
|
541
|
+
if (!m.onClose && stack.length === 0) {
|
|
542
|
+
setModuleState(fastify, m.name, "closed");
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
setModuleState(fastify, m.name, "closing");
|
|
546
|
+
let teardownFailed = false;
|
|
547
|
+
if (m.onClose) try {
|
|
548
|
+
await m.onClose(fastify);
|
|
549
|
+
} catch (err) {
|
|
550
|
+
teardownFailed = true;
|
|
551
|
+
errors.push(new Error(`[arc] module "${m.name}" onClose() threw during teardown: ${err instanceof Error ? err.message : String(err)}`, { cause: err }));
|
|
552
|
+
}
|
|
553
|
+
for (let d = 0; d < stack.length; d++) {
|
|
554
|
+
const dispose = stack[d];
|
|
555
|
+
if (!dispose) continue;
|
|
556
|
+
try {
|
|
557
|
+
await dispose();
|
|
558
|
+
} catch (err) {
|
|
559
|
+
teardownFailed = true;
|
|
560
|
+
errors.push(new Error(`[arc] module "${m.name}" deferred disposer #${stack.length - d} threw during teardown: ${err instanceof Error ? err.message : String(err)}`, { cause: err }));
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
setModuleState(fastify, m.name, teardownFailed ? "failed" : "closed");
|
|
564
|
+
}
|
|
565
|
+
return errors;
|
|
566
|
+
}
|
|
567
|
+
return {
|
|
568
|
+
markInitialized(module) {
|
|
569
|
+
initializedNames.add(module.name);
|
|
570
|
+
},
|
|
571
|
+
rollback: closeInitialized,
|
|
572
|
+
closeAll: closeInitialized,
|
|
573
|
+
hasComposedModules: () => modules.length > 0
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
//#endregion
|
|
238
577
|
//#region src/factory/presets.ts
|
|
239
578
|
/**
|
|
240
579
|
* Production preset - strict security, performance optimized
|
|
@@ -295,8 +634,8 @@ const productionPreset = {
|
|
|
295
634
|
exposeStatusRoute: true,
|
|
296
635
|
maxEventLoopDelay: 3e3,
|
|
297
636
|
maxEventLoopUtilization: .98,
|
|
298
|
-
maxHeapUsedBytes:
|
|
299
|
-
maxRssBytes:
|
|
637
|
+
maxHeapUsedBytes: 1073741824,
|
|
638
|
+
maxRssBytes: 1073741824
|
|
300
639
|
}
|
|
301
640
|
};
|
|
302
641
|
/**
|
|
@@ -366,7 +705,7 @@ const testingPreset = {
|
|
|
366
705
|
arcPlugins: { gracefulShutdown: false },
|
|
367
706
|
sensible: true,
|
|
368
707
|
multipart: { limits: {
|
|
369
|
-
fileSize:
|
|
708
|
+
fileSize: 1048576,
|
|
370
709
|
files: 5
|
|
371
710
|
} }
|
|
372
711
|
};
|
|
@@ -460,7 +799,7 @@ async function registerArcCore(fastify, config, trackPlugin) {
|
|
|
460
799
|
await fastify.register(arcCorePlugin, { emitEvents: config.arcPlugins?.emitEvents !== false });
|
|
461
800
|
trackPlugin("arc-core");
|
|
462
801
|
if (config.arcPlugins?.events !== false) {
|
|
463
|
-
const { default: eventPlugin } = await import("./eventPlugin-
|
|
802
|
+
const { default: eventPlugin } = await import("./eventPlugin-Dk_TyXeE.mjs").then((n) => n.n);
|
|
464
803
|
const eventOpts = typeof config.arcPlugins?.events === "object" ? config.arcPlugins.events : {};
|
|
465
804
|
await fastify.register(eventPlugin, {
|
|
466
805
|
...eventOpts,
|
|
@@ -508,43 +847,47 @@ async function registerArcPlugins(fastify, config, trackPlugin, modules, moduleH
|
|
|
508
847
|
trackPlugin("arc-graceful-shutdown");
|
|
509
848
|
}
|
|
510
849
|
if (config.arcPlugins?.caching) {
|
|
511
|
-
const { default: cachingPlugin } = await import("./caching-
|
|
850
|
+
const { default: cachingPlugin } = await import("./caching-DFJqHyFs.mjs").then((n) => n.r);
|
|
512
851
|
const opts = config.arcPlugins.caching === true ? {} : config.arcPlugins.caching;
|
|
513
852
|
await fastify.register(cachingPlugin, opts);
|
|
514
853
|
trackPlugin("arc-caching", opts);
|
|
515
854
|
}
|
|
516
855
|
if (config.arcPlugins?.queryCache) {
|
|
517
|
-
const { queryCachePlugin } = await import("./queryCachePlugin-
|
|
856
|
+
const { queryCachePlugin } = await import("./queryCachePlugin-Dhps-Ldt.mjs").then((n) => n.n);
|
|
518
857
|
const opts = config.arcPlugins.queryCache === true ? {} : config.arcPlugins.queryCache;
|
|
519
|
-
const store = config.stores?.queryCache ?? new (await (import("./memory-
|
|
858
|
+
const store = config.stores?.queryCache ?? new (await (import("./memory-DsuWnj4T.mjs").then((n) => n.n))).MemoryCacheStore();
|
|
520
859
|
await fastify.register(queryCachePlugin, {
|
|
521
860
|
store,
|
|
522
861
|
...opts
|
|
523
862
|
});
|
|
524
863
|
trackPlugin("arc-query-cache", opts);
|
|
525
864
|
}
|
|
526
|
-
if (config.arcPlugins?.sse)
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
865
|
+
if (config.arcPlugins?.sse) {
|
|
866
|
+
if (config.arcPlugins?.events === false) fastify.log.warn("SSE plugin requires events plugin (arcPlugins.events). SSE disabled.");
|
|
867
|
+
else {
|
|
868
|
+
const { default: ssePlugin } = await import("./sse-B9P4Y-UI.mjs").then((n) => n.r);
|
|
869
|
+
const opts = config.arcPlugins.sse === true ? {} : config.arcPlugins.sse;
|
|
870
|
+
await fastify.register(ssePlugin, opts);
|
|
871
|
+
trackPlugin("arc-sse", opts);
|
|
872
|
+
}
|
|
532
873
|
}
|
|
533
|
-
if (config.arcPlugins?.realtime)
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
874
|
+
if (config.arcPlugins?.realtime) {
|
|
875
|
+
if (config.arcPlugins?.events === false) fastify.log.warn("Realtime plugin requires events plugin (arcPlugins.events). Realtime disabled.");
|
|
876
|
+
else {
|
|
877
|
+
const { default: realtimePlugin } = await import("./realtime-DVvVZZFz.mjs").then((n) => n.r);
|
|
878
|
+
const opts = config.arcPlugins.realtime === true ? {} : config.arcPlugins.realtime;
|
|
879
|
+
await fastify.register(realtimePlugin, opts);
|
|
880
|
+
trackPlugin("arc-realtime", opts);
|
|
881
|
+
}
|
|
539
882
|
}
|
|
540
883
|
if (config.arcPlugins?.metrics) {
|
|
541
|
-
const { default: metricsPlugin } = await import("./metrics-
|
|
884
|
+
const { default: metricsPlugin } = await import("./metrics-DdQL8syd.mjs").then((n) => n.r);
|
|
542
885
|
const opts = config.arcPlugins.metrics === true ? {} : config.arcPlugins.metrics;
|
|
543
886
|
await fastify.register(metricsPlugin, opts);
|
|
544
887
|
trackPlugin("arc-metrics", opts);
|
|
545
888
|
}
|
|
546
889
|
if (config.arcPlugins?.versioning) {
|
|
547
|
-
const { default: versioningPlugin } = await import("./versioning-
|
|
890
|
+
const { default: versioningPlugin } = await import("./versioning-CKv2jz_U.mjs").then((n) => n.r);
|
|
548
891
|
await fastify.register(versioningPlugin, config.arcPlugins.versioning);
|
|
549
892
|
trackPlugin("arc-versioning", config.arcPlugins.versioning);
|
|
550
893
|
}
|
|
@@ -590,10 +933,12 @@ async function registerAuth(fastify, config, trackPlugin) {
|
|
|
590
933
|
fastify.decorate("authenticate", async (request, reply) => {
|
|
591
934
|
await authenticate(request, reply);
|
|
592
935
|
});
|
|
593
|
-
if (!fastify.hasDecorator("optionalAuthenticate"))
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
936
|
+
if (!fastify.hasDecorator("optionalAuthenticate")) {
|
|
937
|
+
if (optionalAuthenticate) fastify.decorate("optionalAuthenticate", async (request, reply) => {
|
|
938
|
+
await optionalAuthenticate(request, reply);
|
|
939
|
+
});
|
|
940
|
+
else fastify.decorate("optionalAuthenticate", createOptionalAuthenticate(authenticate));
|
|
941
|
+
}
|
|
597
942
|
trackPlugin("auth-authenticator");
|
|
598
943
|
fastify.log.debug("Custom authenticator enabled");
|
|
599
944
|
break;
|
|
@@ -613,7 +958,7 @@ async function registerAuth(fastify, config, trackPlugin) {
|
|
|
613
958
|
*/
|
|
614
959
|
async function registerElevation(fastify, config, trackPlugin) {
|
|
615
960
|
if (!config.elevation) return;
|
|
616
|
-
const { elevationPlugin } = await import("./elevation-
|
|
961
|
+
const { elevationPlugin } = await import("./elevation-DNXXxgjl.mjs").then((n) => n.r);
|
|
617
962
|
await fastify.register(elevationPlugin, config.elevation);
|
|
618
963
|
trackPlugin("arc-elevation", config.elevation);
|
|
619
964
|
fastify.log.debug("Elevation plugin enabled");
|
|
@@ -628,7 +973,7 @@ async function registerElevation(fastify, config, trackPlugin) {
|
|
|
628
973
|
*/
|
|
629
974
|
async function registerErrorHandler(fastify, config, trackPlugin, moduleErrorMappers) {
|
|
630
975
|
if (config.errorHandler === false) return;
|
|
631
|
-
const { errorHandlerPlugin } = await import("./errorHandler-
|
|
976
|
+
const { errorHandlerPlugin } = await import("./errorHandler-B2AZUJUj.mjs").then((n) => n.r);
|
|
632
977
|
let errorOpts = typeof config.errorHandler === "object" ? config.errorHandler : { includeStack: config.preset !== "production" };
|
|
633
978
|
if (moduleErrorMappers && moduleErrorMappers.length > 0) errorOpts = {
|
|
634
979
|
...errorOpts,
|
|
@@ -701,14 +1046,29 @@ async function registerOne(parent, resource, mountRoutes) {
|
|
|
701
1046
|
* 5. resources factory (if any) — resolved AFTER bootstrap, so engine-backed
|
|
702
1047
|
* adapters can `await ensureEngine()` and pass
|
|
703
1048
|
* live models/repos into `defineResource(...)`
|
|
704
|
-
*
|
|
1049
|
+
* 5a. module resources — RESOLVED after the app factory, but PREPENDED
|
|
705
1050
|
* so they REGISTER before app resources (two
|
|
706
|
-
* orderings, both intentional — see §
|
|
1051
|
+
* orderings, both intentional — see §5a)
|
|
1052
|
+
* 5b. owns validation + filter — verify each claim is supplied by its claimant,
|
|
1053
|
+
* THEN drop the app forks it supersedes
|
|
707
1054
|
* 6. module.afterResources → afterResources() — post-registration wiring
|
|
708
1055
|
* 7. onReady — lifecycle hook
|
|
709
1056
|
* 7b. onClose — module.onClose (reverse composition order)
|
|
710
1057
|
* THEN app onClose, in ONE hook so module
|
|
711
1058
|
* teardown runs before shared infra closes.
|
|
1059
|
+
*
|
|
1060
|
+
* Boot is TRANSACTIONAL for module-held resources: phases 2–6 run inside one
|
|
1061
|
+
* try/catch. On any failure, arc unsubscribes already-active module event
|
|
1062
|
+
* handlers, then closes every module that ENTERED an init phase — reverse
|
|
1063
|
+
* composition order, best-effort (see module/teardown.ts) — and rethrows the
|
|
1064
|
+
* ORIGINAL boot error. Without this, a failure after module A's bootstrap but
|
|
1065
|
+
* before the §7b hook registration leaked A's clients/timers/engines with no
|
|
1066
|
+
* teardown path.
|
|
1067
|
+
*
|
|
1068
|
+
* "Entered", not "completed": a module is eligible for teardown from the
|
|
1069
|
+
* moment it enters `plugins()`/`bootstrap()`, so the callback that allocates a
|
|
1070
|
+
* client and THEN throws — the commonest partial-init shape — gets its closer
|
|
1071
|
+
* called too.
|
|
712
1072
|
*/
|
|
713
1073
|
async function registerResources(fastify, config, preResolvedModules) {
|
|
714
1074
|
if (config.preset === "production") {
|
|
@@ -722,160 +1082,225 @@ async function registerResources(fastify, config, preResolvedModules) {
|
|
|
722
1082
|
};
|
|
723
1083
|
}
|
|
724
1084
|
const modules = preResolvedModules ?? orderModules(await Promise.all((config.modules ?? []).map((m) => resolveModule(m))));
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
if (
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
} catch (err) {
|
|
734
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
735
|
-
throw new Error(`[arc] module "${m.name}" plugins() threw: ${msg}`, { cause: err });
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
for (const m of modules) {
|
|
739
|
-
if (!m.bootstrap) continue;
|
|
740
|
-
let exported;
|
|
741
|
-
try {
|
|
742
|
-
exported = await m.bootstrap(fastify);
|
|
743
|
-
} catch (err) {
|
|
744
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
745
|
-
throw new Error(`[arc] module "${m.name}" bootstrap threw: ${msg}`, { cause: err });
|
|
1085
|
+
initModuleStates(fastify, modules.map((m) => m.name));
|
|
1086
|
+
const disposers = createDisposerRegistry(fastify);
|
|
1087
|
+
const teardown = createModuleTeardown(fastify, modules, disposers);
|
|
1088
|
+
let eventUnsubscribes = [];
|
|
1089
|
+
try {
|
|
1090
|
+
if (config.plugins) {
|
|
1091
|
+
await config.plugins(fastify);
|
|
1092
|
+
fastify.log.debug("Custom plugins registered");
|
|
746
1093
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
1094
|
+
for (const m of modules) {
|
|
1095
|
+
if (!m.plugins) continue;
|
|
1096
|
+
teardown.markInitialized(m);
|
|
1097
|
+
try {
|
|
1098
|
+
const returned = await m.plugins(fastify, disposers.contextFor(m.name));
|
|
1099
|
+
if (typeof returned === "function") disposers.contextFor(m.name).defer(returned);
|
|
1100
|
+
} catch (err) {
|
|
1101
|
+
setModuleState(fastify, m.name, "failed");
|
|
1102
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1103
|
+
throw new Error(`[arc] module "${m.name}" plugins() threw: ${msg}`, { cause: err });
|
|
1104
|
+
}
|
|
752
1105
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
1106
|
+
for (const m of modules) {
|
|
1107
|
+
if (!m.bootstrap) {
|
|
1108
|
+
setModuleState(fastify, m.name, "ready");
|
|
1109
|
+
teardown.markInitialized(m);
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
let exported;
|
|
1113
|
+
teardown.markInitialized(m);
|
|
1114
|
+
setModuleState(fastify, m.name, "bootstrapping");
|
|
1115
|
+
try {
|
|
1116
|
+
exported = await m.bootstrap(fastify, disposers.contextFor(m.name));
|
|
1117
|
+
} catch (err) {
|
|
1118
|
+
setModuleState(fastify, m.name, "failed");
|
|
1119
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1120
|
+
throw new Error(`[arc] module "${m.name}" bootstrap threw: ${msg}`, { cause: err });
|
|
1121
|
+
}
|
|
1122
|
+
setModuleState(fastify, m.name, "ready");
|
|
1123
|
+
if (exported !== void 0) {
|
|
1124
|
+
const arc = fastify.arc;
|
|
1125
|
+
if (!arc) throw new Error(`[arc] module "${m.name}" returned a public export but fastify.arc is not decorated — arcCorePlugin must register first.`);
|
|
1126
|
+
if (!arc.modules) arc.modules = Object.create(null);
|
|
1127
|
+
arc.modules[m.name] = exported;
|
|
1128
|
+
}
|
|
767
1129
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
} else resolvedResources = config.resources;
|
|
773
|
-
let discoveryRawDir;
|
|
774
|
-
let discoveryPath;
|
|
775
|
-
let discoveryYieldedZero = false;
|
|
776
|
-
if (resolvedResources === void 0 && config.resourceDir) {
|
|
777
|
-
const { loadResources } = await import("./loadResources-DpU8pfjV.mjs").then((n) => n.n);
|
|
778
|
-
const { resolve, dirname } = await import("node:path");
|
|
779
|
-
const { fileURLToPath } = await import("node:url");
|
|
780
|
-
const rawDir = config.resourceDir;
|
|
781
|
-
const dir = rawDir.startsWith("file://") ? dirname(fileURLToPath(rawDir)) : resolve(rawDir);
|
|
782
|
-
discoveryRawDir = rawDir;
|
|
783
|
-
discoveryPath = dir;
|
|
784
|
-
const discovered = await loadResources(dir, { logger: fastify.log });
|
|
785
|
-
if (discovered.length === 0) {
|
|
786
|
-
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/). Use \`strictResourceDir: true\` to fail boot.`);
|
|
787
|
-
discoveryYieldedZero = true;
|
|
1130
|
+
if (modules.length) fastify.log.debug(`${modules.length} module(s) composed (in dependency order): ${modules.map((m) => m.name).join(" → ")}`);
|
|
1131
|
+
if (config.bootstrap?.length) {
|
|
1132
|
+
for (const init of config.bootstrap) await init(fastify);
|
|
1133
|
+
fastify.log.debug(`${config.bootstrap.length} bootstrap function(s) executed`);
|
|
788
1134
|
}
|
|
789
|
-
resolvedResources
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
1135
|
+
let resolvedResources;
|
|
1136
|
+
if (isResourcesFactory(config.resources)) {
|
|
1137
|
+
try {
|
|
1138
|
+
resolvedResources = await config.resources(fastify);
|
|
1139
|
+
if (!Array.isArray(resolvedResources)) throw new TypeError(`resources factory must return an array — received ${resolvedResources === null ? "null" : typeof resolvedResources}`);
|
|
1140
|
+
} catch (err) {
|
|
1141
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1142
|
+
fastify.log.error(`Resources factory threw during boot: ${msg}`);
|
|
1143
|
+
throw new Error(`[arc] resources factory threw: ${msg}. Check engine bootstrap order (did you forget a bootstrap step?) and that \`defineResource(...)\` calls inside the factory receive fully-booted adapters / repositories.`, { cause: err });
|
|
1144
|
+
}
|
|
1145
|
+
config = {
|
|
1146
|
+
...config,
|
|
1147
|
+
resources: resolvedResources
|
|
1148
|
+
};
|
|
1149
|
+
} else resolvedResources = config.resources;
|
|
1150
|
+
let discoveryRawDir;
|
|
1151
|
+
let discoveryPath;
|
|
1152
|
+
let discoveryYieldedZero = false;
|
|
1153
|
+
if (resolvedResources === void 0 && config.resourceDir) {
|
|
1154
|
+
const { loadResources } = await import("./loadResources-DisEAmkm.mjs").then((n) => n.n);
|
|
1155
|
+
const { resolve, dirname } = await import("node:path");
|
|
1156
|
+
const { fileURLToPath } = await import("node:url");
|
|
1157
|
+
const rawDir = config.resourceDir;
|
|
1158
|
+
const dir = rawDir.startsWith("file://") ? dirname(fileURLToPath(rawDir)) : resolve(rawDir);
|
|
1159
|
+
discoveryRawDir = rawDir;
|
|
1160
|
+
discoveryPath = dir;
|
|
1161
|
+
const discovered = await loadResources(dir, { logger: fastify.log });
|
|
1162
|
+
if (discovered.length === 0) {
|
|
1163
|
+
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/). Use \`strictResourceDir: true\` to fail boot.`);
|
|
1164
|
+
discoveryYieldedZero = true;
|
|
1165
|
+
}
|
|
1166
|
+
resolvedResources = discovered;
|
|
799
1167
|
}
|
|
800
|
-
|
|
801
|
-
|
|
1168
|
+
const moduleResourceNames = /* @__PURE__ */ new Map();
|
|
1169
|
+
const perModuleResources = /* @__PURE__ */ new Map();
|
|
802
1170
|
const moduleResources = [];
|
|
803
1171
|
for (const m of modules) {
|
|
804
1172
|
if (!m.resources) continue;
|
|
1173
|
+
let rs;
|
|
805
1174
|
try {
|
|
806
|
-
|
|
807
|
-
moduleResources.push(...rs);
|
|
1175
|
+
rs = typeof m.resources === "function" ? await m.resources(fastify) : m.resources;
|
|
808
1176
|
} catch (err) {
|
|
809
1177
|
const msg = err instanceof Error ? err.message : String(err);
|
|
810
1178
|
throw new Error(`[arc] module "${m.name}" resources factory threw: ${msg}. Check the module's bootstrap ran and its adapters received booted engines.`, { cause: err });
|
|
811
1179
|
}
|
|
1180
|
+
if (!Array.isArray(rs)) throw new TypeError(`[arc] module "${m.name}" resources factory must return an array — received ${rs === null ? "null" : typeof rs}.`);
|
|
1181
|
+
const names = /* @__PURE__ */ new Set();
|
|
1182
|
+
for (const r of rs) {
|
|
1183
|
+
if (r.name == null) continue;
|
|
1184
|
+
if (names.has(r.name)) throw new Error(`[arc] module "${m.name}" provides two resources named "${r.name}". Resource names must be unique within a module — arc registers one route group per name, so the second would be dropped.`);
|
|
1185
|
+
names.add(r.name);
|
|
1186
|
+
}
|
|
1187
|
+
moduleResourceNames.set(m.name, names);
|
|
1188
|
+
perModuleResources.set(m.name, rs);
|
|
1189
|
+
moduleResources.push(...rs);
|
|
1190
|
+
}
|
|
1191
|
+
const effectiveOwns = /* @__PURE__ */ new Map();
|
|
1192
|
+
for (const m of modules) {
|
|
1193
|
+
const supplied = moduleResourceNames.get(m.name);
|
|
1194
|
+
if (m.owns === "provided") {
|
|
1195
|
+
if (!supplied?.size) throw new Error(`[arc] module "${m.name}" declares \`owns: "provided"\` but supplies no named resources. Either give it resources to own, or drop the \`owns\` entry — an empty claim is almost always a resources factory that returned early.`);
|
|
1196
|
+
effectiveOwns.set(m.name, [...supplied]);
|
|
1197
|
+
continue;
|
|
1198
|
+
}
|
|
1199
|
+
if (!m.owns?.length) continue;
|
|
1200
|
+
const missing = m.owns.filter((name) => !supplied?.has(name));
|
|
1201
|
+
if (missing.length > 0) {
|
|
1202
|
+
const provided = supplied?.size ? [...supplied].join(", ") : "(none)";
|
|
1203
|
+
throw new Error(`[arc] module "${m.name}" declares owns: [${missing.map((n) => `"${n}"`).join(", ")}] but its own \`resources\` do not supply ${missing.length === 1 ? "that name" : "those names"}. \`owns\` DROPS the app-level resource of that name, so an unmet claim removes the route entirely and nothing serves it. Provide it in this module's \`resources\`, drop the \`owns\` entry, or use \`owns: "provided"\` to derive it.
|
|
1204
|
+
Resources supplied by "${m.name}": ${provided}.`);
|
|
1205
|
+
}
|
|
1206
|
+
effectiveOwns.set(m.name, m.owns);
|
|
1207
|
+
}
|
|
1208
|
+
if (modules.length && resolvedResources && resolvedResources.length > 0) {
|
|
1209
|
+
const owned = /* @__PURE__ */ new Set();
|
|
1210
|
+
for (const m of modules) for (const name of effectiveOwns.get(m.name) ?? []) owned.add(name);
|
|
1211
|
+
if (owned.size > 0) {
|
|
1212
|
+
const before = resolvedResources.length;
|
|
1213
|
+
resolvedResources = resolvedResources.filter((r) => !(r.name != null && owned.has(r.name)));
|
|
1214
|
+
const dropped = before - resolvedResources.length;
|
|
1215
|
+
if (dropped > 0) fastify.log.debug(`[arc] ${dropped} app resource(s) superseded by module owns() — the module version registers instead`);
|
|
1216
|
+
}
|
|
812
1217
|
}
|
|
813
1218
|
if (moduleResources.length) resolvedResources = [...moduleResources, ...resolvedResources ?? []];
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
1219
|
+
if (resolvedResources && resolvedResources.length > 0) {
|
|
1220
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1221
|
+
for (const resource of resolvedResources) if (resource.name) {
|
|
1222
|
+
if (seen.has(resource.name)) {
|
|
1223
|
+
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/. Use \`strictResources: true\` to fail boot.`;
|
|
1224
|
+
if (config.strictResources) throw new Error(msg);
|
|
1225
|
+
fastify.log.warn(msg);
|
|
1226
|
+
}
|
|
1227
|
+
seen.add(resource.name);
|
|
822
1228
|
}
|
|
823
|
-
|
|
1229
|
+
const prefixed = [];
|
|
1230
|
+
const root = [];
|
|
1231
|
+
for (const resource of resolvedResources) if (resource.skipGlobalPrefix) root.push(resource);
|
|
1232
|
+
else prefixed.push(resource);
|
|
1233
|
+
for (const resource of root) await registerOne(fastify, resource, config.mountRoutes);
|
|
1234
|
+
if (prefixed.length) {
|
|
1235
|
+
if (config.resourcePrefix) await fastify.register(async (scoped) => {
|
|
1236
|
+
for (const resource of prefixed) await registerOne(scoped, resource, config.mountRoutes);
|
|
1237
|
+
}, { prefix: config.resourcePrefix });
|
|
1238
|
+
else for (const resource of prefixed) await registerOne(fastify, resource, config.mountRoutes);
|
|
1239
|
+
}
|
|
1240
|
+
const names = resolvedResources.map((r) => r.name ?? "?").join(", ");
|
|
1241
|
+
const prefix = config.resourcePrefix ? ` (prefix: ${config.resourcePrefix})` : "";
|
|
1242
|
+
fastify.log.info(`${resolvedResources.length} resource(s) registered${prefix}: ${names}`);
|
|
1243
|
+
} else {
|
|
1244
|
+
const prefix = config.resourcePrefix ? ` (prefix: ${config.resourcePrefix})` : "";
|
|
1245
|
+
const scanned = discoveryPath ? ` — resourceDir "${discoveryRawDir}" resolved to "${discoveryPath}"` : "";
|
|
1246
|
+
const hints = discoveryYieldedZero ? ` but yielded 0 resources. Check the path, file naming (*.resource.{ts,js,mts,mjs}), and runtime layout (src/ vs dist/). Use \`strictResourceDir: true\` to fail boot.` : "";
|
|
1247
|
+
fastify.log.warn(`0 resources registered${prefix}${scanned}${hints}`);
|
|
824
1248
|
}
|
|
825
|
-
const
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
const names = resolvedResources.map((r) => r.name ?? "?").join(", ");
|
|
835
|
-
const prefix = config.resourcePrefix ? ` (prefix: ${config.resourcePrefix})` : "";
|
|
836
|
-
fastify.log.info(`${resolvedResources.length} resource(s) registered${prefix}: ${names}`);
|
|
837
|
-
} else {
|
|
838
|
-
const prefix = config.resourcePrefix ? ` (prefix: ${config.resourcePrefix})` : "";
|
|
839
|
-
const scanned = discoveryPath ? ` — resourceDir "${discoveryRawDir}" resolved to "${discoveryPath}"` : "";
|
|
840
|
-
const hints = discoveryYieldedZero ? ` but yielded 0 resources. Check the path, file naming (*.resource.{ts,js,mts,mjs}), and runtime layout (src/ vs dist/). Use \`strictResourceDir: true\` to fail boot.` : "";
|
|
841
|
-
fastify.log.warn(`0 resources registered${prefix}${scanned}${hints}`);
|
|
842
|
-
}
|
|
843
|
-
for (const m of modules) {
|
|
844
|
-
if (!m.afterResources) continue;
|
|
845
|
-
try {
|
|
846
|
-
await m.afterResources(fastify);
|
|
847
|
-
} catch (err) {
|
|
848
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
849
|
-
throw new Error(`[arc] module "${m.name}" afterResources() threw: ${msg}`, { cause: err });
|
|
1249
|
+
for (const m of modules) {
|
|
1250
|
+
if (!m.afterResources) continue;
|
|
1251
|
+
try {
|
|
1252
|
+
const returned = await m.afterResources(fastify, disposers.contextFor(m.name));
|
|
1253
|
+
if (typeof returned === "function") disposers.contextFor(m.name).defer(returned);
|
|
1254
|
+
} catch (err) {
|
|
1255
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1256
|
+
throw new Error(`[arc] module "${m.name}" afterResources() threw: ${msg}`, { cause: err });
|
|
1257
|
+
}
|
|
850
1258
|
}
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
});
|
|
863
|
-
if (fastify.arc) {
|
|
864
|
-
fastify.arc.scheduledJobs = frozenJobs;
|
|
865
|
-
fastify.arc.plugins.set("arc-schedules", {
|
|
866
|
-
name: "arc-schedules",
|
|
867
|
-
options: { scheduleCount: frozenJobs.length },
|
|
868
|
-
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1259
|
+
const scheduleCounts = /* @__PURE__ */ new Map();
|
|
1260
|
+
const scheduledJobs = await collectModuleScheduledJobs(fastify, modules, scheduleCounts);
|
|
1261
|
+
if (scheduledJobs.length > 0) {
|
|
1262
|
+
if (config.arcPlugins?.schedules === false) throw new Error("[arc] modules declare scheduledJobs but arcPlugins.schedules is false. Enable the scheduler or remove the declarations.");
|
|
1263
|
+
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.");
|
|
1264
|
+
const frozenJobs = Object.freeze(scheduledJobs.map((job) => Object.freeze({ ...job })));
|
|
1265
|
+
const scheduleOptions = typeof config.arcPlugins?.schedules === "object" ? config.arcPlugins.schedules : {};
|
|
1266
|
+
const { default: schedulesPlugin } = await import("./schedules-C2Q3uv2d.mjs").then((n) => n.r);
|
|
1267
|
+
await fastify.register(schedulesPlugin, {
|
|
1268
|
+
...scheduleOptions,
|
|
1269
|
+
schedules: frozenJobs
|
|
869
1270
|
});
|
|
1271
|
+
if (fastify.arc) {
|
|
1272
|
+
fastify.arc.scheduledJobs = frozenJobs;
|
|
1273
|
+
fastify.arc.plugins.set("arc-schedules", {
|
|
1274
|
+
name: "arc-schedules",
|
|
1275
|
+
options: { scheduleCount: frozenJobs.length },
|
|
1276
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
const subscriptionCounts = /* @__PURE__ */ new Map();
|
|
1281
|
+
eventUnsubscribes = await subscribeModuleEventHandlers(fastify, modules, subscriptionCounts);
|
|
1282
|
+
if (fastify.arc) fastify.arc.moduleDescriptors = Object.freeze(modules.map((m) => describeResolvedModule({
|
|
1283
|
+
name: m.name,
|
|
1284
|
+
dependsOn: m.dependsOn,
|
|
1285
|
+
resources: perModuleResources.get(m.name) ?? [],
|
|
1286
|
+
owns: effectiveOwns.get(m.name) ?? [],
|
|
1287
|
+
lifecycle: {
|
|
1288
|
+
hasClose: typeof m.onClose === "function",
|
|
1289
|
+
subscriptions: subscriptionCounts.get(m.name) ?? 0,
|
|
1290
|
+
scheduledJobs: scheduleCounts.get(m.name) ?? 0,
|
|
1291
|
+
healthChecks: m.healthChecks?.length ?? 0,
|
|
1292
|
+
exports: typeof m.bootstrap === "function"
|
|
1293
|
+
}
|
|
1294
|
+
})));
|
|
1295
|
+
if (config.afterResources) {
|
|
1296
|
+
await config.afterResources(fastify);
|
|
1297
|
+
fastify.log.debug("afterResources hook executed");
|
|
870
1298
|
}
|
|
871
|
-
}
|
|
872
|
-
const eventUnsubscribes = await subscribeModuleEventHandlers(fastify, modules);
|
|
873
|
-
if (config.afterResources) try {
|
|
874
|
-
await config.afterResources(fastify);
|
|
875
|
-
fastify.log.debug("afterResources hook executed");
|
|
876
1299
|
} catch (err) {
|
|
877
|
-
const
|
|
878
|
-
for (const
|
|
1300
|
+
const unsubscribeErrors = await unsubscribeModuleEventHandlers(eventUnsubscribes);
|
|
1301
|
+
for (const unsubscribeError of unsubscribeErrors) fastify.log.error({ err: unsubscribeError }, "[arc] module event-handler rollback after boot failure failed");
|
|
1302
|
+
const closeErrors = await teardown.rollback();
|
|
1303
|
+
for (const closeError of closeErrors) fastify.log.error({ err: closeError }, "[arc] module onClose rollback after boot failure failed");
|
|
879
1304
|
throw err;
|
|
880
1305
|
}
|
|
881
1306
|
if (config.onReady) {
|
|
@@ -884,13 +1309,14 @@ async function registerResources(fastify, config, preResolvedModules) {
|
|
|
884
1309
|
await onReady(fastify);
|
|
885
1310
|
});
|
|
886
1311
|
}
|
|
887
|
-
const closers = modules.filter((m) => m.onClose).reverse();
|
|
888
1312
|
const appOnClose = config.onClose;
|
|
889
|
-
if (
|
|
1313
|
+
if (teardown.hasComposedModules() || appOnClose || eventUnsubscribes.length) fastify.addHook("onClose", async () => {
|
|
890
1314
|
const unsubscribeErrors = await unsubscribeModuleEventHandlers(eventUnsubscribes);
|
|
891
1315
|
for (const err of unsubscribeErrors) fastify.log.error({ err }, "[arc] module event-handler shutdown unsubscribe failed");
|
|
892
|
-
|
|
1316
|
+
const closeErrors = await teardown.closeAll();
|
|
1317
|
+
for (const err of closeErrors) fastify.log.error({ err }, "[arc] module onClose failed during shutdown");
|
|
893
1318
|
if (appOnClose) await appOnClose(fastify);
|
|
1319
|
+
if (closeErrors.length > 0) throw closeErrors[0];
|
|
894
1320
|
});
|
|
895
1321
|
}
|
|
896
1322
|
//#endregion
|
|
@@ -956,56 +1382,6 @@ function resolveCorsOptions(config) {
|
|
|
956
1382
|
};
|
|
957
1383
|
}
|
|
958
1384
|
//#endregion
|
|
959
|
-
//#region src/factory/security/pluginLoader.ts
|
|
960
|
-
const PLUGIN_REGISTRY = {
|
|
961
|
-
cors: {
|
|
962
|
-
package: "@fastify/cors",
|
|
963
|
-
loader: () => import("@fastify/cors").then((m) => m.default)
|
|
964
|
-
},
|
|
965
|
-
helmet: {
|
|
966
|
-
package: "@fastify/helmet",
|
|
967
|
-
loader: () => import("@fastify/helmet").then((m) => m.default)
|
|
968
|
-
},
|
|
969
|
-
rateLimit: {
|
|
970
|
-
package: "@fastify/rate-limit",
|
|
971
|
-
loader: () => import("@fastify/rate-limit").then((m) => m.default)
|
|
972
|
-
},
|
|
973
|
-
underPressure: {
|
|
974
|
-
package: "@fastify/under-pressure",
|
|
975
|
-
loader: () => import("@fastify/under-pressure").then((m) => m.default)
|
|
976
|
-
},
|
|
977
|
-
sensible: {
|
|
978
|
-
package: "@fastify/sensible",
|
|
979
|
-
loader: () => import("@fastify/sensible").then((m) => m.default)
|
|
980
|
-
},
|
|
981
|
-
multipart: {
|
|
982
|
-
package: "@fastify/multipart",
|
|
983
|
-
loader: () => import("@fastify/multipart").then((m) => m.default),
|
|
984
|
-
optional: true
|
|
985
|
-
},
|
|
986
|
-
rawBody: {
|
|
987
|
-
package: "fastify-raw-body",
|
|
988
|
-
loader: () => import("fastify-raw-body").then((m) => m.default),
|
|
989
|
-
optional: true
|
|
990
|
-
}
|
|
991
|
-
};
|
|
992
|
-
async function loadPlugin(name, logger) {
|
|
993
|
-
const entry = PLUGIN_REGISTRY[name];
|
|
994
|
-
if (!entry) throw new Error(`Unknown plugin: ${name}`);
|
|
995
|
-
try {
|
|
996
|
-
return await entry.loader();
|
|
997
|
-
} catch (error) {
|
|
998
|
-
const err = error;
|
|
999
|
-
const isModuleNotFound = err.message.includes("Cannot find module") || err.message.includes("Cannot find package") || err.message.includes("MODULE_NOT_FOUND") || err.message.includes("Could not resolve");
|
|
1000
|
-
if (isModuleNotFound && entry.optional) {
|
|
1001
|
-
logger?.warn(`Optional plugin '${name}' skipped (${entry.package} not installed)`);
|
|
1002
|
-
return null;
|
|
1003
|
-
}
|
|
1004
|
-
if (isModuleNotFound) throw new Error(`Plugin '${name}' requires package '${entry.package}' which is not installed.\nInstall it with: npm install ${entry.package}\nOr disable this plugin by setting ${name}: false in createApp options.`);
|
|
1005
|
-
throw new Error(`Failed to load plugin '${name}': ${err.message}`);
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
//#endregion
|
|
1009
1385
|
//#region src/factory/security/rateLimit.ts
|
|
1010
1386
|
/** "Unlimited" as a number — @fastify/rate-limit's `max` fn must return one. */
|
|
1011
1387
|
const UNLIMITED_MAX = Number.MAX_SAFE_INTEGER;
|
|
@@ -1144,7 +1520,7 @@ async function registerUtilityPlugins(fastify, config) {
|
|
|
1144
1520
|
throwFileSizeLimit: true,
|
|
1145
1521
|
...hostMultipart,
|
|
1146
1522
|
limits: {
|
|
1147
|
-
fileSize:
|
|
1523
|
+
fileSize: 10485760,
|
|
1148
1524
|
files: 10,
|
|
1149
1525
|
fields: 100,
|
|
1150
1526
|
parts: 120,
|
|
@@ -1329,8 +1705,26 @@ Missing:\n${lines}\n\nFix: pass durable adapters under \`stores\`, or use runtim
|
|
|
1329
1705
|
* 10. afterResources() — post-registration wiring
|
|
1330
1706
|
* 11. onReady/onClose — lifecycle hooks
|
|
1331
1707
|
* ```
|
|
1708
|
+
*
|
|
1709
|
+
* **Boot is all-or-nothing.** If any phase throws, the partially-built
|
|
1710
|
+
* instance is closed before the error propagates — see the wrapper below.
|
|
1332
1711
|
*/
|
|
1333
1712
|
async function createApp(options) {
|
|
1713
|
+
let partial;
|
|
1714
|
+
try {
|
|
1715
|
+
return await buildApp(options, (instance) => {
|
|
1716
|
+
partial = instance;
|
|
1717
|
+
});
|
|
1718
|
+
} catch (err) {
|
|
1719
|
+
if (partial) try {
|
|
1720
|
+
await partial.close();
|
|
1721
|
+
} catch (closeErr) {
|
|
1722
|
+
partial.log?.error?.({ err: closeErr }, "[arc] cleanup of a partially-booted app failed; original boot error follows");
|
|
1723
|
+
}
|
|
1724
|
+
throw err;
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
async function buildApp(options, onInstance) {
|
|
1334
1728
|
if (options.debug !== void 0 && options.debug !== false) configureArcLogger({ debug: options.debug });
|
|
1335
1729
|
validateAuthOptions(options);
|
|
1336
1730
|
const deferredWarnings = validateDistributedRuntime(options);
|
|
@@ -1371,6 +1765,7 @@ async function createApp(options) {
|
|
|
1371
1765
|
keywords: ["example", ...config.ajv?.keywords ?? []]
|
|
1372
1766
|
} }
|
|
1373
1767
|
});
|
|
1768
|
+
onInstance(fastify);
|
|
1374
1769
|
if (resolvedLogger !== false) configureArcLogger({
|
|
1375
1770
|
...options.debug !== void 0 && options.debug !== false ? { debug: options.debug } : {},
|
|
1376
1771
|
writer: createPinoWriter(fastify.log)
|
|
@@ -1386,6 +1781,7 @@ async function createApp(options) {
|
|
|
1386
1781
|
}
|
|
1387
1782
|
});
|
|
1388
1783
|
await registerSecurityPlugins(fastify, config);
|
|
1784
|
+
if (config.assets?.length) await registerAssetRoots(fastify, config.assets);
|
|
1389
1785
|
await registerUtilityPlugins(fastify, config);
|
|
1390
1786
|
const trackPlugin = (name, opts) => {
|
|
1391
1787
|
const arc = fastify.arc;
|
|
@@ -1408,7 +1804,7 @@ async function createApp(options) {
|
|
|
1408
1804
|
await registerErrorHandler(fastify, config, trackPlugin, moduleErrorMappers);
|
|
1409
1805
|
await registerResources(fastify, config, resolvedModules);
|
|
1410
1806
|
if (config.replyHelpers) {
|
|
1411
|
-
const { replyHelpersPlugin } = await import("./replyHelpers-
|
|
1807
|
+
const { replyHelpersPlugin } = await import("./replyHelpers-DS-P0b32.mjs").then((n) => n.n);
|
|
1412
1808
|
await fastify.register(replyHelpersPlugin);
|
|
1413
1809
|
}
|
|
1414
1810
|
if (config.serializeBigInt) {
|
|
@@ -1459,4 +1855,4 @@ const ArcFactory = {
|
|
|
1459
1855
|
}
|
|
1460
1856
|
};
|
|
1461
1857
|
//#endregion
|
|
1462
|
-
export { edgePreset as a, testingPreset as c,
|
|
1858
|
+
export { collectModuleWorkflows as _, edgePreset as a, testingPreset as c, hasModule as d, orderModules as f, collectModuleScheduledJobs as g, collectModuleHealthChecks as h, developmentPreset as i, workerPreset as l, describeResolvedModule as m, createApp as n, getPreset as o, subscribeModuleEventHandlers as p, createApp_exports as r, productionPreset as s, ArcFactory as t, getModuleState as u, resolveContribution as v, resolveModule as y };
|