@classytic/arc 2.31.0 → 2.33.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/README.md +1 -1
- package/bin/arc.js +58 -17
- package/dist/{BaseController-De7GqHck.mjs → BaseController-CaQC5EXX.mjs} +298 -35
- package/dist/{EventTransport-2uVV34WU.d.mts → EventTransport-BEVP50ka.d.mts} +16 -0
- package/dist/{EventTransport-9mlTgqzE.mjs → EventTransport-tq4CEB-q.mjs} +39 -2
- package/dist/{HookSystem-BcmPyq5I.mjs → HookSystem-DqXkMXkB.mjs} +2 -2
- package/dist/{ResourceRegistry-qq0mn2s9.mjs → ResourceRegistry-DoALUlOO.mjs} +18 -15
- package/dist/{actionPermissions-BE5NwaQk.mjs → actionPermissions-CUeCmpUd.mjs} +1 -1
- package/dist/audit/index.d.mts +2 -2
- package/dist/audit/index.mjs +2 -2
- package/dist/auth/audit.mjs +1 -1
- package/dist/auth/index.d.mts +2 -2
- package/dist/auth/index.mjs +44 -11
- package/dist/auth/redis-session.mjs +1 -1
- package/dist/{authorizationDecision-ChvUQGG1.mjs → authorizationDecision-B3mP_QTy.mjs} +2 -2
- package/dist/{types-B7XeLEgK.d.mts → base-Csjc2oF-.d.mts} +74 -74
- package/dist/{buildHandler-CDrCJzmB.mjs → buildHandler-DLE9xX-V.mjs} +6 -5
- package/dist/cache/index.mjs +4 -4
- package/dist/{caching-BFHA6Scf.mjs → caching-BcGmvnHh.mjs} +2 -2
- package/dist/cleanup/index.d.mts +52 -18
- package/dist/cleanup/index.mjs +281 -32
- package/dist/cli/commands/describe.d.mts +1 -1
- package/dist/cli/commands/describe.mjs +4 -6
- 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-D3Dv4LAV.mjs → constants-BxCoxmiN.mjs} +1 -1
- package/dist/context/index.mjs +3 -3
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +7 -7
- package/dist/{core-BGpIxINN.mjs → core-dxhcHhUD.mjs} +3 -3
- package/dist/{createActionRouter-CDXEdasl.mjs → createActionRouter-DVpoA0do.mjs} +14 -11
- package/dist/{createAggregationRouter-KrQDzx-k.mjs → createAggregationRouter-B_B48JZ-.mjs} +10 -8
- package/dist/{createApp-Df89BEni.mjs → createApp-CLGegqiG.mjs} +287 -62
- package/dist/{defineAggregation-DbPPS8RI.mjs → defineAggregation-Nv-DwfOf.mjs} +1 -1
- package/dist/{defineResource-Bv6EtN1x.mjs → defineResource-DR9vsUYy.mjs} +318 -35
- package/dist/discovery/index.mjs +1 -1
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +3 -3
- package/dist/{elevation-D2l1mh4g.mjs → elevation-B0tlY8y1.mjs} +4 -4
- package/dist/encryption/index.mjs +5 -4
- package/dist/{errorHandler-C5NeD6bT.mjs → errorHandler-C7xBqHsI.mjs} +42 -5
- package/dist/{errorHandler-CpYUJmPA.d.mts → errorHandler-DI5AN2b5.d.mts} +14 -3
- package/dist/{errors-Cj42AntO.mjs → errors-Cd_yy7aW.mjs} +1 -1
- package/dist/{eventPlugin-GF5nnBIq.d.mts → eventPlugin-DSNEZWfV.d.mts} +29 -1
- package/dist/{eventPlugin-CkffEihm.mjs → eventPlugin-Dar8qRaj.mjs} +68 -11
- package/dist/events/index.d.mts +18 -297
- package/dist/events/index.mjs +7 -758
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis-stream-entry.mjs +14 -10
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/events/transports/redis.mjs +1 -1
- package/dist/factory/index.d.mts +20 -5
- package/dist/factory/index.mjs +131 -8
- package/dist/fastify-augmentation-DVsTAy6X.d.mts +179 -0
- package/dist/{fields-CB5Cuzk9.mjs → fields-CojeUkS4.mjs} +6 -11
- package/dist/{generate-EcA6ui4i.mjs → generate-DiB0nHPR.mjs} +2 -2
- package/dist/{headers-3zvE0rM3.mjs → headers-CveNO9gR.mjs} +2 -2
- package/dist/{health-DjZYjM7H.mjs → health-CasG2A28.mjs} +1 -1
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +2 -2
- package/dist/idempotency/index.d.mts +45 -1
- package/dist/idempotency/index.mjs +95 -11
- package/dist/idempotency/redis.mjs +1 -1
- package/dist/{index-CEJNZFtX.d.mts → index-B-DvUzsr.d.mts} +82 -3
- package/dist/{index-CpzxqePp.d.mts → index-BcR2bFZQ.d.mts} +707 -373
- package/dist/{index-Bc2ox8_U.d.mts → index-Brin0ODU.d.mts} +4 -4
- package/dist/{index-CFThn1K9.d.mts → index-C5NnMEKF.d.mts} +70 -4
- package/dist/{index-C-481Zf2.d.mts → index-ugr1tQlW.d.mts} +17 -1
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +11 -11
- package/dist/{init-BEiNZBsJ.mjs → init-mdJ-v4Xp.mjs} +24 -16
- package/dist/integrations/event-gateway.d.mts +1 -1
- package/dist/integrations/event-gateway.mjs +2 -2
- package/dist/integrations/index.d.mts +2 -2
- package/dist/integrations/index.mjs +1 -1
- package/dist/integrations/jobs/index.d.mts +1 -1
- package/dist/integrations/jobs/index.mjs +100 -98
- package/dist/integrations/mcp/index.d.mts +12 -3
- package/dist/integrations/mcp/index.mjs +2 -2
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +2 -2
- package/dist/integrations/streamline.mjs +3 -3
- package/dist/integrations/webhooks.d.mts +124 -1
- package/dist/integrations/webhooks.mjs +180 -2
- package/dist/integrations/websocket-pushref-redis.mjs +1 -1
- package/dist/integrations/websocket-redis.mjs +1 -1
- package/dist/integrations/websocket.mjs +38 -35
- package/dist/{interface-DYzM0q-u.mjs → interface-DznJrm9R.mjs} +1 -1
- package/dist/{jsonBody-BYbLDi5D.mjs → jsonBody-CfGH-GxO.mjs} +2 -2
- package/dist/{keys-C0_pa1tY.mjs → keys-WeKvY1dE.mjs} +1 -1
- package/dist/{loadResources-yawi9tAE.mjs → loadResources-Bj-aSsXi.mjs} +4 -3
- package/dist/{loadResourcesFromEntry-BAjYZonx.mjs → loadResourcesFromEntry-DwaxNcE2.mjs} +2 -2
- package/dist/logger/index.mjs +1 -1
- package/dist/{mcpPlugin-B_es0hiF.mjs → mcpPlugin-aJg5TqHi.mjs} +76 -36
- package/dist/{memory-DR6wpa9F.mjs → memory-qYSB7rMQ.mjs} +5 -5
- package/dist/{metrics-CUdw_wLB.mjs → metrics-D7uchLjN.mjs} +2 -2
- package/dist/middleware/index.d.mts +1 -1
- package/dist/middleware/index.mjs +3 -3
- package/dist/migrations/index.mjs +3 -3
- package/dist/{module-0pFJA6YW.mjs → module-CwKsgZXp.mjs} +1 -1
- package/dist/{multipartBody-wOgj_fnj.mjs → multipartBody-4w-S3-PU.mjs} +3 -3
- package/dist/{openapi-B2L4js1v.mjs → openapi-BN5cQaO2.mjs} +6 -6
- package/dist/outbox-admin/index.d.mts +85 -0
- package/dist/outbox-admin/index.mjs +115 -0
- package/dist/outbox-module-CXB79bvt.mjs +839 -0
- package/dist/outbox-module-D-VFq37T.d.mts +368 -0
- package/dist/permissions/index.d.mts +3 -3
- package/dist/permissions/index.mjs +6 -6
- package/dist/{permissions-L--QDAir.mjs → permissions-BAfwgBxc.mjs} +120 -8
- package/dist/{pipe-DRDNsI2T.mjs → pipe-BzzoEhHj.mjs} +3 -5
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/pipeline/index.mjs +2 -2
- package/dist/{pipeline-UPQFatit.mjs → pipeline-q69DZzgG.mjs} +17 -15
- package/dist/plugins/index.d.mts +6 -6
- package/dist/plugins/index.mjs +16 -16
- package/dist/plugins/response-cache.mjs +2 -2
- package/dist/plugins/tracing-entry.mjs +2 -2
- package/dist/{pluralize-O0FZgESl.mjs → pluralize-B7xf8qZF.mjs} +1 -1
- package/dist/{preloadResources-Dz6BjzM6.mjs → preloadResources-BkyqeTDz.mjs} +1 -1
- package/dist/{preloadResources-CyoJMSiO.d.mts → preloadResources-g07ldtzh.d.mts} +30 -7
- package/dist/presets/filesUpload.d.mts +2 -2
- package/dist/presets/filesUpload.mjs +6 -6
- package/dist/presets/index.d.mts +2 -2
- package/dist/presets/index.mjs +2 -2
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +3 -3
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +2 -2
- package/dist/{presets-C4-g4_4P.mjs → presets-D-h0uWtP.mjs} +6 -5
- package/dist/projection-BKM0Ui-s.mjs +42 -0
- package/dist/{queryCachePlugin-BZr8v46E.mjs → queryCachePlugin-DhsZ0ZIc.mjs} +50 -8
- package/dist/{rateLimit-CMPY5Qms.mjs → rateLimit-D4-ASwsI.mjs} +2 -2
- package/dist/{rateLimitKey-Dn5JNmkj.mjs → rateLimitKey-XngqVNEV.mjs} +1 -1
- package/dist/{realtime-C5NoxWFY.mjs → realtime-CTkv3O59.mjs} +10 -10
- package/dist/{redis-stream-qN6A-RFv.d.mts → redis-stream-mwLWlfHG.d.mts} +1 -1
- package/dist/{registerRoute-CBN84fc7.mjs → registerRoute-DNnd3vkz.mjs} +10 -8
- package/dist/registry/index.d.mts +3 -3
- package/dist/registry/index.mjs +3 -3
- package/dist/{registry-4213U4zb.mjs → registry-CLxCR5LF.mjs} +2 -2
- package/dist/{renewingLease-Dll8mGgc.mjs → renewingLease-BnWtbA2U.mjs} +1 -1
- package/dist/{replyHelpers-DIiZKGUt.mjs → replyHelpers-BSBVCwL3.mjs} +2 -2
- package/dist/{repositoryFilter-f9eAfq_l.mjs → repositoryFilter-DKny7PDn.mjs} +1 -1
- package/dist/{requestContext-2TxXOJ7T.mjs → requestContext-uYE35JxI.mjs} +1 -1
- package/dist/{rolldown-runtime-CUsjNX9R.mjs → rolldown-runtime-DNg-KNCk.mjs} +1 -1
- package/dist/runtimeCapabilities-Dm88DaTW.mjs +49 -0
- package/dist/{schedules-DdtlICTf.mjs → schedules-CAcZgia0.mjs} +3 -3
- package/dist/{schemaConverter-D--wdJM-.mjs → schemaConverter-XH1RZRyu.mjs} +1 -1
- package/dist/{schemaIR-Bm2enbnh.mjs → schemaIR-CDvclAew.mjs} +2 -2
- package/dist/schemas/index.mjs +1 -1
- package/dist/scim/index.mjs +2 -2
- package/dist/scope/index.mjs +5 -5
- package/dist/{scope-DHx5Fous.mjs → scope-BaWcqBvr.mjs} +4 -4
- package/dist/{simpleEqualityMatcher-DjQJ7FKA.mjs → simpleEqualityMatcher-Se8VrsaP.mjs} +1 -1
- package/dist/{sse-CUTo9y0X.mjs → sse-Cf0jUHsh.mjs} +7 -6
- package/dist/{sseStream-B_5XcVVd.mjs → sseStream-Bvpi-UXQ.mjs} +2 -2
- package/dist/{store-helpers-D1MP_gRG.mjs → store-helpers-B88p99Z2.mjs} +1 -1
- package/dist/{streaming-PSZwi8JV.mjs → streaming-DsT1B5zJ.mjs} +1 -1
- package/dist/{subscribe-helpers-C0Tb2aUp.mjs → subscribe-helpers-CLIOHsOK.mjs} +2 -2
- package/dist/sync/changelog.mjs +1 -1
- package/dist/testing/cleanupStoreContract.d.mts +1 -1
- package/dist/testing/cleanupStoreContract.mjs +12 -6
- package/dist/testing/index.d.mts +52 -13
- package/dist/testing/index.mjs +110 -40
- package/dist/testing/outboxStoreContract.d.mts +11 -2
- package/dist/testing/outboxStoreContract.mjs +110 -21
- package/dist/testing/storageContract.mjs +11 -6
- package/dist/{transactionContext-dAfBVUrk.mjs → transactionContext-BYtWnwAS.mjs} +1 -1
- package/dist/{typeGuards-B0ryPo3I.mjs → typeGuards-jjKJDN13.mjs} +1 -1
- package/dist/types/index.d.mts +3 -3
- package/dist/types/index.mjs +1 -1
- package/dist/types/storage.mjs +1 -1
- package/dist/{types-CBgvkp8O.d.mts → types-BcTumAz_.d.mts} +128 -18
- package/dist/{types-CwcUQ610.d.mts → types-BfC83KsP.d.mts} +15 -1
- package/dist/{types-Cys0ko9Z.mjs → types-CoqIRrkY.mjs} +4 -4
- package/dist/{types-C4QE8qnm.d.mts → types-DVLddu2q.d.mts} +144 -1
- package/dist/usage/index.d.mts +1 -1
- package/dist/usage/index.mjs +0 -0
- package/dist/{userHelpers-ChYJ6CdK.mjs → userHelpers-CWF5pbZD.mjs} +1 -1
- package/dist/utils/index.d.mts +57 -5
- package/dist/utils/index.mjs +12 -11
- package/dist/{utils-vL8BrDE2.mjs → utils-h1V_sT-t.mjs} +38 -7
- package/dist/{validate-_KbifpHF.mjs → validate-CdQlz52F.mjs} +2 -2
- package/dist/{versioning-D_rhHfZm.d.mts → versioning-DEs-_hFi.d.mts} +1 -1
- package/dist/{versioning-CHDTor3P.mjs → versioning-rZbyLVD-.mjs} +4 -4
- package/package.json +31 -20
- package/skills/arc/SKILL.md +96 -4
- package/skills/arc/references/api-reference.md +19 -0
- package/skills/arc/references/production.md +2 -2
- package/dist/fastify-augmentation-zh0_AYHa.d.mts +0 -85
- package/dist/projection-BHw6xlOQ.mjs +0 -18
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
/*! @classytic/arc v2.
|
|
2
|
-
import { s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS } from "./constants-
|
|
1
|
+
/*! @classytic/arc v2.34.0 | MIT | https://classytic.com */
|
|
2
|
+
import { p as MUTATION_OPERATIONS, s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS } from "./constants-BxCoxmiN.mjs";
|
|
3
3
|
import { arcLog } from "./logger/index.mjs";
|
|
4
|
-
import { r as ForbiddenError } from "./errors-
|
|
5
|
-
import { n as getUserRoles } from "./userHelpers-
|
|
6
|
-
import { b as isElevated, n as PUBLIC_SCOPE, x as isMember } from "./types-
|
|
7
|
-
import { d as DEFAULT_FIELD_WRITE_DENIAL_POLICY, t as BaseController } from "./BaseController-
|
|
8
|
-
import { a as buildRouteHooks, c as buildAuthMiddleware, i as selectPluginMw, n as buildRouteConfig, o as resolveRoutePreHandlers, r as resolveRouterPluginMw, s as routeHookOptions, t as buildRateLimitConfig, u as buildArcDecorator } from "./rateLimit-
|
|
9
|
-
import { i as resolveEffectiveRoles, n as applyFieldWritePermissions } from "./fields-
|
|
10
|
-
import { r as buildCrudPermissionMw, t as tryRegisterRoute } from "./registerRoute-
|
|
11
|
-
import { c as getDefaultCrudSchemas } from "./utils-
|
|
12
|
-
import { n as convertRouteSchema, r as ensureSchemaConverter, t as convertOpenApiSchemas } from "./schemaConverter-
|
|
13
|
-
import { i as pipeUIMessageStreamToReply, r as isReadableStream } from "./streaming-
|
|
14
|
-
import { t as hasEvents } from "./typeGuards-
|
|
15
|
-
import { a as createFastifyHandler, i as createCrudHandlers, n as buildPipelineHandler, o as createRequestContext, r as resolvePipelineSteps } from "./pipeline-
|
|
16
|
-
import {
|
|
17
|
-
import { r as getAvailablePresets, t as applyPresets } from "./presets-
|
|
18
|
-
import { t as pluralize } from "./pluralize-
|
|
19
|
-
import { t as resolveActionPermission } from "./actionPermissions-
|
|
4
|
+
import { r as ForbiddenError } from "./errors-Cd_yy7aW.mjs";
|
|
5
|
+
import { n as getUserRoles } from "./userHelpers-CWF5pbZD.mjs";
|
|
6
|
+
import { b as isElevated, n as PUBLIC_SCOPE, x as isMember } from "./types-CoqIRrkY.mjs";
|
|
7
|
+
import { d as isWriteVerbCapable, p as DEFAULT_FIELD_WRITE_DENIAL_POLICY, t as BaseController, u as detectOverriddenWriteSlots } from "./BaseController-CaQC5EXX.mjs";
|
|
8
|
+
import { a as buildRouteHooks, c as buildAuthMiddleware, i as selectPluginMw, n as buildRouteConfig, o as resolveRoutePreHandlers, r as resolveRouterPluginMw, s as routeHookOptions, t as buildRateLimitConfig, u as buildArcDecorator } from "./rateLimit-D4-ASwsI.mjs";
|
|
9
|
+
import { i as resolveEffectiveRoles, n as applyFieldWritePermissions } from "./fields-CojeUkS4.mjs";
|
|
10
|
+
import { r as buildCrudPermissionMw, t as tryRegisterRoute } from "./registerRoute-DNnd3vkz.mjs";
|
|
11
|
+
import { c as getDefaultCrudSchemas } from "./utils-h1V_sT-t.mjs";
|
|
12
|
+
import { n as convertRouteSchema, r as ensureSchemaConverter, t as convertOpenApiSchemas } from "./schemaConverter-XH1RZRyu.mjs";
|
|
13
|
+
import { i as pipeUIMessageStreamToReply, r as isReadableStream } from "./streaming-DsT1B5zJ.mjs";
|
|
14
|
+
import { t as hasEvents } from "./typeGuards-jjKJDN13.mjs";
|
|
15
|
+
import { a as createFastifyHandler, i as createCrudHandlers, n as buildPipelineHandler, o as createRequestContext, r as resolvePipelineSteps } from "./pipeline-q69DZzgG.mjs";
|
|
16
|
+
import { B as requireAuth } from "./permissions-BAfwgBxc.mjs";
|
|
17
|
+
import { r as getAvailablePresets, t as applyPresets } from "./presets-D-h0uWtP.mjs";
|
|
18
|
+
import { t as pluralize } from "./pluralize-B7xf8qZF.mjs";
|
|
19
|
+
import { t as resolveActionPermission } from "./actionPermissions-CUeCmpUd.mjs";
|
|
20
|
+
import { isReadOnlyRepo } from "@classytic/repo-core/repository";
|
|
20
21
|
//#region src/core/middlewares/fieldWrite.ts
|
|
21
22
|
/**
|
|
22
23
|
* Build a preHandler that enforces field-write permissions on the request body.
|
|
@@ -42,7 +43,10 @@ function buildFieldWritePreHandler(fieldPermissions, policy = DEFAULT_FIELD_WRIT
|
|
|
42
43
|
if (!body || typeof body !== "object" || Array.isArray(body)) return;
|
|
43
44
|
const scope = request.scope ?? PUBLIC_SCOPE;
|
|
44
45
|
if (isElevated(scope)) return;
|
|
45
|
-
const
|
|
46
|
+
const globalRoles = getUserRoles(request.user);
|
|
47
|
+
const orgRoles = isMember(scope) ? scope.orgRoles : [];
|
|
48
|
+
const effectiveRoles = resolveEffectiveRoles(globalRoles, orgRoles);
|
|
49
|
+
const { body: filtered, deniedFields } = applyFieldWritePermissions(body, fieldPermissions, effectiveRoles);
|
|
46
50
|
if (deniedFields.length > 0 && policy === "reject") throw new ForbiddenError(`Not permitted to write field${deniedFields.length === 1 ? "" : "s"}: ${deniedFields.join(", ")}`);
|
|
47
51
|
request.body = filtered;
|
|
48
52
|
};
|
|
@@ -58,6 +62,18 @@ function methodCarriesBody(method) {
|
|
|
58
62
|
//#endregion
|
|
59
63
|
//#region src/core/createCrudRouter.ts
|
|
60
64
|
/**
|
|
65
|
+
* Fold a route's host-supplied `config` together with arc's derived one into the
|
|
66
|
+
* SINGLE `config` key Fastify reads. Returns arc's object untouched when the host
|
|
67
|
+
* supplied none, so the route options stay literally what was declared.
|
|
68
|
+
*/
|
|
69
|
+
function mergeRouteConfig(hostConfig, arcConfig) {
|
|
70
|
+
if (!hostConfig) return arcConfig;
|
|
71
|
+
return { config: {
|
|
72
|
+
...hostConfig,
|
|
73
|
+
...arcConfig.config ?? {}
|
|
74
|
+
} };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
61
77
|
* Mount custom routes (from presets or user-defined `routes`) on Fastify.
|
|
62
78
|
* `wrapHandler` is derived inline from whether `rawHandler` was set.
|
|
63
79
|
*/
|
|
@@ -114,8 +130,9 @@ function createCustomRoutes(fastify, routes, controller, options) {
|
|
|
114
130
|
const fieldWriteMw = buildFieldWritePreHandler(fieldPermissions, onFieldWriteDenied);
|
|
115
131
|
if (fieldWriteMw) customPreHandlers.push(fieldWriteMw);
|
|
116
132
|
}
|
|
133
|
+
const preAuthHandlers = route.preAuth ?? [];
|
|
117
134
|
const hooks = buildRouteHooks({
|
|
118
|
-
preAuth:
|
|
135
|
+
preAuth: preAuthHandlers,
|
|
119
136
|
arcDecorator,
|
|
120
137
|
authMw: buildAuthMiddleware(fastify, route.permissions),
|
|
121
138
|
permissionMw: buildCrudPermissionMw(route.permissions, resourceName, opName),
|
|
@@ -140,7 +157,25 @@ function createCustomRoutes(fastify, routes, controller, options) {
|
|
|
140
157
|
}
|
|
141
158
|
return result;
|
|
142
159
|
} : handler,
|
|
143
|
-
|
|
160
|
+
/**
|
|
161
|
+
* ONE `config` object, merging the host's `route.config` with arc's own
|
|
162
|
+
* derived keys (per-route rate limit, cors, `arcExtensions`).
|
|
163
|
+
*
|
|
164
|
+
* It has to be merged HERE because Fastify does not merge for us: it
|
|
165
|
+
* builds the context from a single `opts.config`
|
|
166
|
+
* (`{ ...opts.config, url, method }` — lib/route.js), so two spreads
|
|
167
|
+
* writing `config` means the later one silently replaces the earlier.
|
|
168
|
+
* Emitting them separately dropped `route.config` on every resource
|
|
169
|
+
* that declared extensions, a rate limit or per-route cors — which is
|
|
170
|
+
* the same silent-absence bug `config` was added to fix, just narrowed
|
|
171
|
+
* to the resources most likely to have it.
|
|
172
|
+
*
|
|
173
|
+
* Arc's derived keys win on collision: they come from the declared
|
|
174
|
+
* `route.rateLimit` / `route.cors` / `extensions` fields, so letting a
|
|
175
|
+
* hand-written `config.rateLimit` override them would give one setting
|
|
176
|
+
* two sources that can disagree.
|
|
177
|
+
*/
|
|
178
|
+
...mergeRouteConfig(route.config, buildRouteConfig(route.rateLimit !== void 0 ? buildRateLimitConfig(route.rateLimit) : rateLimitConfig, extensions, route.cors))
|
|
144
179
|
}, {
|
|
145
180
|
resourceName,
|
|
146
181
|
op: opName
|
|
@@ -287,7 +322,8 @@ function buildCrudHandlers(ctrl, pipeline, resourceName) {
|
|
|
287
322
|
for (const op of CRUD_OPERATIONS) {
|
|
288
323
|
const steps = resolvePipelineSteps(pipeline, op);
|
|
289
324
|
if (steps.length === 0) continue;
|
|
290
|
-
|
|
325
|
+
const method = ctrl[op].bind(ctrl);
|
|
326
|
+
wrapped[op] = buildPipelineHandler(method, steps, op, resourceName);
|
|
291
327
|
}
|
|
292
328
|
return wrapped;
|
|
293
329
|
}
|
|
@@ -300,6 +336,77 @@ function createPermissionMiddleware(permission, resourceName, action) {
|
|
|
300
336
|
return buildCrudPermissionMw(permission, resourceName, action);
|
|
301
337
|
}
|
|
302
338
|
//#endregion
|
|
339
|
+
//#region src/core/defineResource/writeVerbs.ts
|
|
340
|
+
/**
|
|
341
|
+
* Phase 4b — write-verb registration policy.
|
|
342
|
+
*
|
|
343
|
+
* A declared command that never executes is the defect `writes` exists to
|
|
344
|
+
* close, one layer up: the resource READS as though the kernel's guarded verb
|
|
345
|
+
* owns the slot while generic CRUD serves the route. So reachability is
|
|
346
|
+
* enforced at REGISTRATION, fail-loud (same posture as `threadQueryParser`):
|
|
347
|
+
*
|
|
348
|
+
* 1. the controller must DISPATCH `_writes` — proven by a prototype
|
|
349
|
+
* capability mark, not by a duck-typed `configure()`, which shows an
|
|
350
|
+
* options channel exists and not that anything reads `writes` from it;
|
|
351
|
+
* 2. no declared verb's slot may be OVERRIDDEN — route dispatch calls the
|
|
352
|
+
* override, so the verb behind it is dead code. Holds even for overrides
|
|
353
|
+
* that delegate to `super`: unprovable statically, so the combination is
|
|
354
|
+
* forbidden. Wrap behaviour with `hooks` instead.
|
|
355
|
+
*
|
|
356
|
+
* Config-SHAPE validation (unknown keys, non-function entries, disabled slots)
|
|
357
|
+
* needs no controller and lives in `validateResourceConfig`.
|
|
358
|
+
*/
|
|
359
|
+
/**
|
|
360
|
+
* The write slots, as a runtime list. `MUTATION_OPERATIONS` is the canonical
|
|
361
|
+
* constant; `satisfies` pins it to the `WriteVerbKey` type so the two cannot
|
|
362
|
+
* drift without a compile error.
|
|
363
|
+
*/
|
|
364
|
+
const WRITE_VERBS = MUTATION_OPERATIONS;
|
|
365
|
+
/** The verbs a resource actually declared (function-valued entries only). */
|
|
366
|
+
function declaredWriteVerbs(writes) {
|
|
367
|
+
if (!writes) return [];
|
|
368
|
+
return WRITE_VERBS.filter((op) => typeof writes[op] === "function");
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Boot-fatal reachability check for declared write verbs on a HOST-SUPPLIED
|
|
372
|
+
* controller. Auto-built controllers never call this — they are capable and
|
|
373
|
+
* un-overridden by construction.
|
|
374
|
+
*
|
|
375
|
+
* Throws before any option threading, so a rejected resource has no partial
|
|
376
|
+
* side effects to reason about.
|
|
377
|
+
*/
|
|
378
|
+
function enforceWriteVerbReachability(controller, resolvedConfig) {
|
|
379
|
+
const declared = declaredWriteVerbs(resolvedConfig.writes);
|
|
380
|
+
if (declared.length === 0) return;
|
|
381
|
+
if (typeof controller !== "object" || controller === null || !isWriteVerbCapable(controller)) throw new Error(`Resource "${resolvedConfig.name}" declares write verb(s) [${declared.join(", ")}] but its controller is not built on arc's write pipeline, so the verbs would be silently dropped and generic CRUD would serve those routes — the exact bypass \`writes\` exists to close. Extend \`BaseController\` / \`BaseCrudController\` (their write methods dispatch declared verbs), or omit \`controller\` and let arc auto-build one, or move the commands into the controller itself and remove \`writes\`.`);
|
|
382
|
+
const collisions = detectOverriddenWriteSlots(controller, declared);
|
|
383
|
+
if (collisions.length > 0) throw new Error(`Resource "${resolvedConfig.name}" declares write verb(s) [${collisions.join(", ")}] AND overrides the controller method(s) for the same slot(s). Route dispatch calls the override, so the declared verb can never execute — it is dead code that reads as if it guards the route. Keep exactly one owner per slot: remove the override and let the verb own persistence (wrap request-level behaviour with \`hooks\` — before/around/after run around the verb), or remove the \`writes\` entry and let the override own the whole pipeline.`);
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Warn when a controller OVERRIDES a write method while the resource declares
|
|
387
|
+
* field rules only arc's pipeline enforces.
|
|
388
|
+
*
|
|
389
|
+
* Measured: a resource with eight `systemManaged` fields overrode `update` to
|
|
390
|
+
* reach its kernel's guarded verb; `PATCH` then wrote `status: "posted"` and a
|
|
391
|
+
* forged `number` straight through, answering 200. The override was added to
|
|
392
|
+
* close one hole and opened a wider one.
|
|
393
|
+
*
|
|
394
|
+
* Scoped so it stays worth reading: write-verb-capable controllers only,
|
|
395
|
+
* BODY-BEARING ops only (`delete` carries no body for the sanitizer), and only
|
|
396
|
+
* when protection is actually declared. A slot WITH a verb never reaches here —
|
|
397
|
+
* `enforceWriteVerbReachability` already threw.
|
|
398
|
+
*/
|
|
399
|
+
function warnOnWriteMethodOverride(controller, resolvedConfig) {
|
|
400
|
+
if (typeof controller !== "object" || controller === null || !isWriteVerbCapable(controller)) return;
|
|
401
|
+
const fieldRules = resolvedConfig.schemaOptions?.fieldRules;
|
|
402
|
+
const protectedFields = fieldRules ? Object.keys(fieldRules) : [];
|
|
403
|
+
const hasFieldPermissions = resolvedConfig.fields !== void 0 && Object.keys(resolvedConfig.fields).length > 0;
|
|
404
|
+
if (protectedFields.length === 0 && !hasFieldPermissions) return;
|
|
405
|
+
const overridden = detectOverriddenWriteSlots(controller, ["create", "update"]);
|
|
406
|
+
if (overridden.length === 0) return;
|
|
407
|
+
arcLog("defineResource").warn(`Resource "${resolvedConfig.name}" overrides controller method(s) [${overridden.join(", ")}] AND declares field protection (${protectedFields.length > 0 ? `fieldRules: ${protectedFields.join(", ")}` : "fields"}). An override replaces arc's entire write pipeline — body sanitization against those field rules, tenant injection, createdBy/updatedBy stamping and the before/around/after hooks all stop running for that op, so the declared protection is NOT enforced. If the override exists to reach a domain command, declare it as a write verb instead (\`writes: { ${overridden[0]}: (…) => engine.<verb>(…) } \`) — arc keeps the pipeline and calls your command in place of the repository. If the override genuinely needs to replace the pipeline, call \`this.bodySanitizer.sanitize(body, op, req, ctx)\` inside it.`);
|
|
408
|
+
}
|
|
409
|
+
//#endregion
|
|
303
410
|
//#region src/core/defineResource/controller.ts
|
|
304
411
|
/**
|
|
305
412
|
* Phase 4 — pick (or auto-create) the resource's controller.
|
|
@@ -319,17 +426,33 @@ function createPermissionMiddleware(permission, resourceName, action) {
|
|
|
319
426
|
* lists the dropped options, and points at the canonical fix. All
|
|
320
427
|
* warns honour `ARC_SUPPRESS_WARNINGS=1` via `arcLog()`.
|
|
321
428
|
*/
|
|
429
|
+
/** CRUD routes refused over a read-only repository. */
|
|
430
|
+
const READ_ONLY_REFUSED_ROUTES = [
|
|
431
|
+
"create",
|
|
432
|
+
"update",
|
|
433
|
+
"delete"
|
|
434
|
+
];
|
|
322
435
|
/**
|
|
323
436
|
* Resolve the controller for the resource. See module docstring for
|
|
324
437
|
* branch semantics.
|
|
325
438
|
*/
|
|
326
439
|
function resolveOrAutoCreateController(resolvedConfig, adapter, repository, hasCrudRoutes) {
|
|
327
440
|
const userController = resolvedConfig.controller;
|
|
441
|
+
if (resolvedConfig.transactional === true) {
|
|
442
|
+
if (typeof repository?.withTransaction !== "function") throw new Error(`Resource "${resolvedConfig.name}" declares \`transactional: true\` but its repository has no \`withTransaction\` (capability \`transactions: false\` — D1, standalone Mongo). Wire a transactional kit, or remove the flag; arc will not silently drop a requested transaction envelope.`);
|
|
443
|
+
}
|
|
444
|
+
if (repository && isReadOnlyRepo(repository)) {
|
|
445
|
+
const disabled = new Set(resolvedConfig.disabledRoutes ?? []);
|
|
446
|
+
const enabledWrites = READ_ONLY_REFUSED_ROUTES.filter((op) => !resolvedConfig.disableDefaultRoutes && !disabled.has(op));
|
|
447
|
+
if (enabledWrites.length > 0) throw new Error(`Resource "${resolvedConfig.name}" mounts write routes (${enabledWrites.join(", ")}) over a READ-ONLY repository — its rows are owned by another component that enforces invariants generic CRUD would bypass (Better Auth hashes passwords, revokes sessions, and cascades org membership on every write it performs). Disable the write routes (\`disabledRoutes: ['create', 'update', 'delete']\`), or if raw writes are genuinely required, build the repository with the kit's explicit \`unsafeWritable: true\` opt-in.`);
|
|
448
|
+
}
|
|
328
449
|
if (userController) {
|
|
329
450
|
threadQueryParser(userController, resolvedConfig);
|
|
451
|
+
enforceWriteVerbReachability(userController, resolvedConfig);
|
|
330
452
|
threadConfigureLifecycle(userController, resolvedConfig, adapter);
|
|
331
453
|
warnOnDroppedAuthorOptions(resolvedConfig);
|
|
332
454
|
warnOnDroppedPresetOptions(resolvedConfig);
|
|
455
|
+
warnOnWriteMethodOverride(userController, resolvedConfig);
|
|
333
456
|
return userController;
|
|
334
457
|
}
|
|
335
458
|
if (!hasCrudRoutes || !repository) return void 0;
|
|
@@ -365,7 +488,16 @@ function threadConfigureLifecycle(controller, resolvedConfig, adapter) {
|
|
|
365
488
|
if (resolvedConfig.defaultSort !== void 0 && wasDeclared("defaultSort")) opts.defaultSort = resolvedConfig.defaultSort;
|
|
366
489
|
if (resolvedConfig.cache !== void 0 && wasDeclared("cache")) opts.cache = resolvedConfig.cache;
|
|
367
490
|
if (resolvedConfig.onFieldWriteDenied !== void 0 && wasDeclared("onFieldWriteDenied")) opts.onFieldWriteDenied = resolvedConfig.onFieldWriteDenied;
|
|
491
|
+
if (resolvedConfig.onImmutableWrite !== void 0 && wasDeclared("onImmutableWrite")) opts.onImmutableWrite = resolvedConfig.onImmutableWrite;
|
|
368
492
|
if (resolvedConfig.queryParser !== void 0 && wasDeclared("queryParser")) opts.queryParser = resolvedConfig.queryParser;
|
|
493
|
+
/**
|
|
494
|
+
* NOT gated on `_declaredKeys`: `writes` is never inferred or injected by a
|
|
495
|
+
* preset, so its presence IS the declaration. Gating it would mean a
|
|
496
|
+
* resource that declares write verbs alongside its own controller silently
|
|
497
|
+
* kept generic CRUD — the exact bypass the seam exists to close.
|
|
498
|
+
*/
|
|
499
|
+
if (resolvedConfig.writes !== void 0) opts.writes = resolvedConfig.writes;
|
|
500
|
+
if (resolvedConfig.transactional !== void 0) opts.transactional = resolvedConfig.transactional;
|
|
369
501
|
if (adapter?.matchesFilter !== void 0) opts.matchesFilter = adapter.matchesFilter;
|
|
370
502
|
if (resolvedConfig._controllerOptions) opts.presetFields = {
|
|
371
503
|
slugField: resolvedConfig._controllerOptions.slugField,
|
|
@@ -415,6 +547,7 @@ function warnOnDroppedAuthorOptions(resolvedConfig) {
|
|
|
415
547
|
if (resolvedConfig.defaultSort !== void 0 && isDeclared("defaultSort")) dropped.push("defaultSort");
|
|
416
548
|
if (resolvedConfig.cache !== void 0 && isDeclared("cache")) dropped.push("cache");
|
|
417
549
|
if (resolvedConfig.onFieldWriteDenied !== void 0 && isDeclared("onFieldWriteDenied")) dropped.push("onFieldWriteDenied");
|
|
550
|
+
if (resolvedConfig.onImmutableWrite !== void 0 && isDeclared("onImmutableWrite")) dropped.push("onImmutableWrite");
|
|
418
551
|
if (dropped.length === 0) return;
|
|
419
552
|
arcLog("defineResource").warn(`Resource "${resolvedConfig.name}" declares a custom controller AND resource-level option(s) [${dropped.join(", ")}]. Arc only threads these when it auto-builds the controller — when you pass your own, they are dropped silently and the controller falls back to its own defaults (e.g. tenantField → 'organizationId'). Either implement \`configure(opts)\` on the controller (arc 2.15+ canonical) or forward them via \`super(repo, { ... })\`. Same root cause as the \`queryParser\` warn above.`);
|
|
420
553
|
}
|
|
@@ -459,12 +592,34 @@ function buildBaseController(resolvedConfig, adapter, repository) {
|
|
|
459
592
|
matchesFilter: adapter?.matchesFilter,
|
|
460
593
|
cache: resolvedConfig.cache,
|
|
461
594
|
onFieldWriteDenied: resolvedConfig.onFieldWriteDenied,
|
|
595
|
+
onImmutableWrite: resolvedConfig.onImmutableWrite,
|
|
596
|
+
/**
|
|
597
|
+
* Domain commands for the write slots. Threaded here rather than
|
|
598
|
+
* subclassing: the pipeline stays arc's and the command stays the
|
|
599
|
+
* kernel's, so neither has to be re-implemented to reach the other.
|
|
600
|
+
*/
|
|
601
|
+
writes: resolvedConfig.writes,
|
|
602
|
+
transactional: resolvedConfig.transactional === true,
|
|
462
603
|
presetFields: resolvedConfig._controllerOptions ? {
|
|
463
604
|
slugField: resolvedConfig._controllerOptions.slugField,
|
|
464
605
|
parentField: resolvedConfig._controllerOptions.parentField
|
|
465
606
|
} : void 0
|
|
466
607
|
});
|
|
467
608
|
}
|
|
609
|
+
/**
|
|
610
|
+
* The DEFERRED half of the `transactional: true` gate — run at resource
|
|
611
|
+
* registration, after `beforeBoot()` has had its chance to connect.
|
|
612
|
+
*
|
|
613
|
+
* Fail-closed: anything other than a confirmed `true` refuses the boot. A
|
|
614
|
+
* repository with NO capability descriptor (hand-written, custom kit) passes
|
|
615
|
+
* — it never promised one, and the define-time presence check already covered
|
|
616
|
+
* the only signal it offers.
|
|
617
|
+
*/
|
|
618
|
+
function assertTransactionCapability(repository, resourceName) {
|
|
619
|
+
const capabilities = repository?.capabilities;
|
|
620
|
+
if (!capabilities || capabilities.transactions === true) return;
|
|
621
|
+
throw new Error(`Resource "${resourceName}" declares \`transactional: true\` but its repository reports \`capabilities.transactions: ${String(capabilities.transactions)}\` — this deployment cannot run transactions (standalone MongoDB, D1, or a topology arc could not confirm). \`withTransaction\` exists but every call would fail at BEGIN. Point at a replica set / transactional backend, or remove the flag. Checked at REGISTRATION, so a connection opened in \`beforeBoot()\` is already live by now.`);
|
|
622
|
+
}
|
|
468
623
|
//#endregion
|
|
469
624
|
//#region src/core/defineResource/hooks.ts
|
|
470
625
|
/**
|
|
@@ -965,7 +1120,7 @@ function collectUngatedCrudDiagnostics(config, strict = false) {
|
|
|
965
1120
|
code: "crud-public-by-omission",
|
|
966
1121
|
message: `[Arc] Resource '${config.name}' mounts ${ungated.length} CRUD route(s) with no permission gate: ${ungated.join(", ")}.\n` + (severity === "error" ? "strictPermissions is on and an ungated WRITE is fatal. State intent explicitly:\n" : "An omitted permission mounts the route WITHOUT auth. State intent explicitly:\n") + ` permissions: permissions.fullPublic() // intentionally public
|
|
967
1122
|
permissions: permissions.authenticated() // require auth on every op
|
|
968
|
-
permissions: { ${ungated[0]}: requireAuth(), ... } // per-operation`
|
|
1123
|
+
permissions: { ${ungated[0]}: requireAuth(), ... } // per-operation` + (severity === "warn" ? "\nRecommended in production: ARC_STRICT_PERMISSIONS=true (or `strictPermissions: true` on the resource) makes an ungated WRITE fail the boot instead of shipping. This is the arc v3 default." : "")
|
|
969
1124
|
}];
|
|
970
1125
|
}
|
|
971
1126
|
//#endregion
|
|
@@ -1209,6 +1364,7 @@ function buildResourcePlugin(resource) {
|
|
|
1209
1364
|
const sharedRoot = fastify.server;
|
|
1210
1365
|
const isFirstMount = !resource._sharedStateRegisteredOn.has(sharedRoot);
|
|
1211
1366
|
if (isFirstMount) resource._sharedStateRegisteredOn.add(sharedRoot);
|
|
1367
|
+
if (isFirstMount && resource._deferredChecks?.length) for (const check of resource._deferredChecks) check();
|
|
1212
1368
|
if (isFirstMount && resource._diagnostics?.length) for (const diagnostic of resource._diagnostics) {
|
|
1213
1369
|
const level = diagnostic.severity === "info" ? "info" : diagnostic.severity === "error" ? "error" : "warn";
|
|
1214
1370
|
fastify.log?.[level]?.(diagnostic.message);
|
|
@@ -1226,11 +1382,13 @@ function buildResourcePlugin(resource) {
|
|
|
1226
1382
|
handler: hook.handler,
|
|
1227
1383
|
priority: hook.priority
|
|
1228
1384
|
});
|
|
1229
|
-
if (isFirstMount && resource.cache?.invalidateOn)
|
|
1230
|
-
pattern,
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1385
|
+
if (isFirstMount && resource.cache?.invalidateOn) {
|
|
1386
|
+
if (fastify.registerCacheInvalidationRule) for (const [pattern, tags] of Object.entries(resource.cache.invalidateOn)) fastify.registerCacheInvalidationRule({
|
|
1387
|
+
pattern,
|
|
1388
|
+
tags
|
|
1389
|
+
});
|
|
1390
|
+
else fastify.log?.warn?.(`[Arc] Resource '${resource.name}' declares \`cache.invalidateOn\` but \`queryCachePlugin\` is not registered. Cache-invalidation rules are silently ignored. Either register the plugin (\`await fastify.register(queryCachePlugin, ...)\`) or remove the \`cache.invalidateOn\` config to clean up the resource definition.`);
|
|
1391
|
+
}
|
|
1234
1392
|
if (isFirstMount && resource.audit) {
|
|
1235
1393
|
if (!(fastify.hasDecorator("audit") && typeof fastify.audit?.create === "function" && fastify.audit?._noop !== true)) fastify.log?.warn?.(`[Arc] Resource '${resource.name}' declares \`audit: ${JSON.stringify(resource.audit)}\` but \`auditPlugin\` is not registered (or is registered with a noop logger). Audit events for this resource will NOT be recorded. Either register \`auditPlugin\` (\`await fastify.register(auditPlugin, { store: ... })\`) or remove the \`audit\` flag from the resource definition.`);
|
|
1236
1394
|
}
|
|
@@ -1271,7 +1429,7 @@ function buildResourcePlugin(resource) {
|
|
|
1271
1429
|
extensions: resource.extensions
|
|
1272
1430
|
});
|
|
1273
1431
|
if (resource.actions && Object.keys(resource.actions).length > 0) {
|
|
1274
|
-
const { createActionRouter } = await import("./createActionRouter-
|
|
1432
|
+
const { createActionRouter } = await import("./createActionRouter-DVpoA0do.mjs").then((n) => n.n);
|
|
1275
1433
|
createActionRouter(typedInstance, {
|
|
1276
1434
|
...normalizeActionsToRouterConfig(resource.actions, resource.actionPermissions, resource.tag, resource.permissions, resource.name, typedInstance.log),
|
|
1277
1435
|
resourceName: resource.name,
|
|
@@ -1286,7 +1444,7 @@ function buildResourcePlugin(resource) {
|
|
|
1286
1444
|
});
|
|
1287
1445
|
}
|
|
1288
1446
|
if (resource.aggregations && Object.keys(resource.aggregations).length > 0) {
|
|
1289
|
-
const [{ createAggregationRouter }, { adapterSupportsAggregate, ArcAggregationConfigError }] = await Promise.all([import("./createAggregationRouter-
|
|
1447
|
+
const [{ createAggregationRouter }, { adapterSupportsAggregate, ArcAggregationConfigError }] = await Promise.all([import("./createAggregationRouter-B_B48JZ-.mjs"), import("./validate-CdQlz52F.mjs").then((n) => n.r)]);
|
|
1290
1448
|
const repoForAgg = resource.controller?.repository ?? resource.repository;
|
|
1291
1449
|
if (!adapterSupportsAggregate(repoForAgg)) {
|
|
1292
1450
|
const undispatchable = Object.entries(resource.aggregations).filter(([, cfg]) => !cfg.materialized).map(([name]) => name);
|
|
@@ -1399,6 +1557,22 @@ var ResourceDefinition = class {
|
|
|
1399
1557
|
*/
|
|
1400
1558
|
_diagnostics;
|
|
1401
1559
|
/**
|
|
1560
|
+
* Boot-time assertions that CANNOT be settled at define time because
|
|
1561
|
+
* they read state a connection establishes.
|
|
1562
|
+
*
|
|
1563
|
+
* `defineResource()` frequently runs at module-import time — before
|
|
1564
|
+
* `beforeBoot()` opens the database. A repository's capability
|
|
1565
|
+
* descriptor is therefore not yet authoritative then: mongokit
|
|
1566
|
+
* deliberately reports `transactions: false` for an unresolved topology
|
|
1567
|
+
* (fail-closed), so asserting at define time would reject a correctly
|
|
1568
|
+
* configured app for the crime of importing its resources early.
|
|
1569
|
+
*
|
|
1570
|
+
* `buildResourcePlugin` runs these on first mount — lifecycle slot 5,
|
|
1571
|
+
* after `beforeBoot` (0) and module resolution (0.5) — where the same
|
|
1572
|
+
* fail-closed rule finally means what it says.
|
|
1573
|
+
*/
|
|
1574
|
+
_deferredChecks;
|
|
1575
|
+
/**
|
|
1402
1576
|
* Public view of {@link _diagnostics} for programmatic consumption.
|
|
1403
1577
|
*
|
|
1404
1578
|
* Returns the collected define-time diagnostics for this resource.
|
|
@@ -1486,10 +1660,12 @@ var ResourceDefinition = class {
|
|
|
1486
1660
|
_validateControllerMethods() {
|
|
1487
1661
|
const errors = [];
|
|
1488
1662
|
const enabledCrudRoutes = CRUD_OPERATIONS.filter((route) => !this.disabledRoutes.includes(route));
|
|
1489
|
-
if (!this.disableDefaultRoutes && enabledCrudRoutes.length > 0)
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1663
|
+
if (!this.disableDefaultRoutes && enabledCrudRoutes.length > 0) {
|
|
1664
|
+
if (!this.controller) errors.push("Controller is required when CRUD routes are enabled");
|
|
1665
|
+
else {
|
|
1666
|
+
const ctrl = this.controller;
|
|
1667
|
+
for (const method of enabledCrudRoutes) if (typeof ctrl[method] !== "function") errors.push(`CRUD method '${method}' not found on controller`);
|
|
1668
|
+
}
|
|
1493
1669
|
}
|
|
1494
1670
|
for (const route of this.routes) {
|
|
1495
1671
|
if (typeof route.handler !== "string") continue;
|
|
@@ -1542,6 +1718,7 @@ var ResourceDefinition = class {
|
|
|
1542
1718
|
summary: r.summary,
|
|
1543
1719
|
description: r.description,
|
|
1544
1720
|
permissions: r.permissions,
|
|
1721
|
+
capability: r.capability,
|
|
1545
1722
|
raw: r.rawHandler !== void 0,
|
|
1546
1723
|
schema: r.schema
|
|
1547
1724
|
})),
|
|
@@ -1784,6 +1961,41 @@ function validateResourceConfig(config, options = {}) {
|
|
|
1784
1961
|
message: "Resource has no adapter and no routes",
|
|
1785
1962
|
suggestion: "Provide either adapter for CRUD or routes for custom logic"
|
|
1786
1963
|
});
|
|
1964
|
+
if (config.writes) {
|
|
1965
|
+
const writeVerbSlots = MUTATION_OPERATIONS;
|
|
1966
|
+
const entries = Object.entries(config.writes);
|
|
1967
|
+
const unknownKeys = entries.map(([key]) => key).filter((key) => !writeVerbSlots.includes(key));
|
|
1968
|
+
if (unknownKeys.length > 0) errors.push({
|
|
1969
|
+
field: "writes",
|
|
1970
|
+
message: `Unknown write verb slot(s) [${unknownKeys.join(", ")}] — only [${writeVerbSlots.join(", ")}] exist. An unknown slot is silently never called, so the command it names guards nothing`,
|
|
1971
|
+
suggestion: "Rename to a real slot (create/update/delete), or move the behaviour into a custom route / action"
|
|
1972
|
+
});
|
|
1973
|
+
const nonFunction = entries.filter(([key, value]) => writeVerbSlots.includes(key) && typeof value !== "function");
|
|
1974
|
+
for (const [key, value] of nonFunction) errors.push({
|
|
1975
|
+
field: `writes.${key}`,
|
|
1976
|
+
message: `Write verb "${key}" is declared but is ${value === void 0 ? "undefined" : `a ${typeof value}`}, not a function — generic repository CRUD would silently serve that slot instead of the command the config claims guards it`,
|
|
1977
|
+
suggestion: "Pass the command function itself (e.g. `update: (id, data, ctx) => engine.updateDraft(id, data)`), or delete the entry"
|
|
1978
|
+
});
|
|
1979
|
+
const declared = writeVerbSlots.filter((op) => typeof config.writes?.[op] === "function");
|
|
1980
|
+
if (entries.length === 0) warnings.push({
|
|
1981
|
+
field: "writes",
|
|
1982
|
+
message: "`writes` is declared but contains no verb functions",
|
|
1983
|
+
suggestion: "Remove the empty `writes` block, or declare create/update/delete"
|
|
1984
|
+
});
|
|
1985
|
+
if (declared.length > 0 && config.disableDefaultRoutes) errors.push({
|
|
1986
|
+
field: "writes",
|
|
1987
|
+
message: `Resource declares write verb(s) [${declared.join(", ")}] but \`disableDefaultRoutes: true\` means no CRUD slot will call them`,
|
|
1988
|
+
suggestion: "Drop `disableDefaultRoutes` so the slots mount, or move the command into a custom route / action and remove `writes`"
|
|
1989
|
+
});
|
|
1990
|
+
else if (declared.length > 0) {
|
|
1991
|
+
const notMounted = declared.filter((op) => disabledRoutes.has(op));
|
|
1992
|
+
if (notMounted.length > 0) errors.push({
|
|
1993
|
+
field: "writes",
|
|
1994
|
+
message: `Write verb(s) [${notMounted.join(", ")}] are declared but those CRUD slots are disabled, so the verbs can never run`,
|
|
1995
|
+
suggestion: `Remove [${notMounted.join(", ")}] from \`disabledRoutes\`, or delete the matching \`writes\` entr(ies) — a command nothing calls is decoration`
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1787
1999
|
if (config.controller && !options.skipControllerCheck && !config.disableDefaultRoutes) {
|
|
1788
2000
|
const ctrl = config.controller;
|
|
1789
2001
|
const requiredMethods = CRUD_OPERATIONS;
|
|
@@ -2065,7 +2277,12 @@ function validateDefineResourceConfig(config) {
|
|
|
2065
2277
|
validatePermissionsShape(config);
|
|
2066
2278
|
validateCustomRoutePermissions(config);
|
|
2067
2279
|
validateActionsShape(config);
|
|
2068
|
-
|
|
2280
|
+
validateRouteCapabilities(config);
|
|
2281
|
+
return [
|
|
2282
|
+
...collectRedundantFieldRuleDiagnostics(config),
|
|
2283
|
+
...collectReservedFilterNameDiagnostics(config),
|
|
2284
|
+
...collectPaginationCapDiagnostics(config)
|
|
2285
|
+
];
|
|
2069
2286
|
}
|
|
2070
2287
|
/**
|
|
2071
2288
|
* Query-string param names arc's `QueryParser` reserves for
|
|
@@ -2102,6 +2319,37 @@ function collectReservedFilterNameDiagnostics(config) {
|
|
|
2102
2319
|
message: `[Arc] Resource '${config.name}': filterable field(s) [${collisions.join(", ")}] collide with reserved query params (page, limit, cursor, after, sort, search, select, populate). The query parser consumes these names for pagination/projection, so filtering by them via the query string — and via the MCP list tool — will NOT work. Rename the field(s), or drop them from \`filterableFields\` if the collision is intended.`
|
|
2103
2320
|
}];
|
|
2104
2321
|
}
|
|
2322
|
+
/**
|
|
2323
|
+
* Warn when the parser's page cap exceeds the repository's.
|
|
2324
|
+
*
|
|
2325
|
+
* Page size is capped in THREE independent places — the query parser, the
|
|
2326
|
+
* repository's pagination engine, and arc — and none of them can see the others.
|
|
2327
|
+
* The lowest silently wins, so a resource that declares 1000 and a repository
|
|
2328
|
+
* left at its default 100 serves 100 rows with a `200` and no signal anywhere.
|
|
2329
|
+
* That is not hypothetical: an account picker read 100 of 696 rows, filtered
|
|
2330
|
+
* that arbitrary slice client-side, and rendered "No accounts found".
|
|
2331
|
+
*
|
|
2332
|
+
* arc is the only layer that holds BOTH, so it is the only one that can notice.
|
|
2333
|
+
* A diagnostic rather than a throw: the mismatch is usually an unconfigured
|
|
2334
|
+
* default rather than a mistake, and a published framework must not refuse to
|
|
2335
|
+
* boot an app that works today — it just works less than the author intended.
|
|
2336
|
+
*
|
|
2337
|
+
* Read structurally, never by importing a kit: `repository._pagination.config`
|
|
2338
|
+
* is mongokit's shape, and arc stays database-agnostic (`check:boundaries`).
|
|
2339
|
+
* Any kit exposing the same shape gets the check for free; one that does not is
|
|
2340
|
+
* simply skipped.
|
|
2341
|
+
*/
|
|
2342
|
+
function collectPaginationCapDiagnostics(config) {
|
|
2343
|
+
const parserCap = config.queryParser?.maxLimit;
|
|
2344
|
+
if (typeof parserCap !== "number") return [];
|
|
2345
|
+
const repoCap = (config.adapter?.repository)?._pagination?.config?.maxLimit;
|
|
2346
|
+
if (typeof repoCap !== "number" || parserCap <= repoCap) return [];
|
|
2347
|
+
return [{
|
|
2348
|
+
severity: "warn",
|
|
2349
|
+
code: "pagination-cap-mismatch",
|
|
2350
|
+
message: `[Arc] Resource '${config.name}': the query parser allows ${parserCap} rows per page but the repository caps at ${repoCap}, so ${repoCap} wins and larger pages are truncated with a 200 and no error. Configure the repository's pagination \`maxLimit\` to ${parserCap} as well, or lower the parser's to match.`
|
|
2351
|
+
}];
|
|
2352
|
+
}
|
|
2105
2353
|
/** Permissions must be `PermissionCheck` functions, not arbitrary values. */
|
|
2106
2354
|
function validatePermissionsShape(config) {
|
|
2107
2355
|
if (!config.permissions) return;
|
|
@@ -2157,6 +2405,37 @@ function collectRedundantFieldRuleDiagnostics(config) {
|
|
|
2157
2405
|
return diagnostics;
|
|
2158
2406
|
}
|
|
2159
2407
|
/**
|
|
2408
|
+
* Route `capability` keys — must not collide with a CRUD slot, an action, an
|
|
2409
|
+
* aggregation, or another route on the same resource.
|
|
2410
|
+
*
|
|
2411
|
+
* At BOOT, not at introspection. `introspectRegistry` also refuses a collision,
|
|
2412
|
+
* but nothing inside arc calls it: it is a host API, and a host calls it from a
|
|
2413
|
+
* permission-matrix endpoint. So the only thing catching a duplicate key was a
|
|
2414
|
+
* 500 on that endpoint at REQUEST time — and because a UI reads its whole
|
|
2415
|
+
* permission map from there, one static config typo took every client's gates
|
|
2416
|
+
* out at once, in production, rather than failing the deploy.
|
|
2417
|
+
*
|
|
2418
|
+
* A duplicate `capability` is decidable from the config alone, so it belongs
|
|
2419
|
+
* with the other boot checks. The introspection-side throw stays as a backstop
|
|
2420
|
+
* for callers that build a registry entry by hand instead of via
|
|
2421
|
+
* `defineResource`.
|
|
2422
|
+
*/
|
|
2423
|
+
function validateRouteCapabilities(config) {
|
|
2424
|
+
const routes = config.routes;
|
|
2425
|
+
if (!routes?.length) return;
|
|
2426
|
+
const taken = /* @__PURE__ */ new Map();
|
|
2427
|
+
for (const op of Object.keys(config.permissions ?? {})) taken.set(op, "CRUD slot");
|
|
2428
|
+
for (const name of Object.keys(config.actions ?? {})) taken.set(`action:${name}`, "action");
|
|
2429
|
+
for (const name of Object.keys(config.aggregations ?? {})) taken.set(`agg:${name}`, "aggregation");
|
|
2430
|
+
for (const route of routes) {
|
|
2431
|
+
const key = route.capability;
|
|
2432
|
+
if (key === void 0) continue;
|
|
2433
|
+
const owner = taken.get(key);
|
|
2434
|
+
if (owner !== void 0) throw new Error(`[Arc] Resource '${config.name}': route ${route.method} ${route.path} declares capability '${key}', which is already published by a ${owner}. Two gates under one key means one answers for the other verb — rename the route's \`capability\`.`);
|
|
2435
|
+
taken.set(key, `route ${route.method} ${route.path}`);
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
/**
|
|
2160
2439
|
* Actions (v2.8) — name must not collide with CRUD ops; handler +
|
|
2161
2440
|
* permissions must have the right shapes. Fail at boot so production
|
|
2162
2441
|
* never ships a misconfigured action endpoint.
|
|
@@ -2202,6 +2481,10 @@ function defineResource(config) {
|
|
|
2202
2481
|
const registryMeta = resolveOpenApiSchemas(narrowedConfig);
|
|
2203
2482
|
if (registryMeta) resource._registryMeta = registryMeta;
|
|
2204
2483
|
}
|
|
2484
|
+
if (resolvedConfig.transactional === true) {
|
|
2485
|
+
const name = resolvedConfig.name;
|
|
2486
|
+
resource._deferredChecks = [() => assertTransactionCapability(repository, name)];
|
|
2487
|
+
}
|
|
2205
2488
|
if (diagnostics.length > 0) resource._diagnostics = diagnostics;
|
|
2206
2489
|
return resource;
|
|
2207
2490
|
}
|
package/dist/discovery/index.mjs
CHANGED
package/dist/docs/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
import { p as RegistryEntry } from "../index-
|
|
2
|
+
import { p as RegistryEntry } from "../index-BcR2bFZQ.mjs";
|
|
3
3
|
import { t as ExternalOpenApiPaths } from "../externalPaths-CmGOxOtD.mjs";
|
|
4
4
|
import { FastifyPluginAsync } from "fastify";
|
|
5
5
|
//#region src/docs/openapi/types.d.ts
|
package/dist/docs/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
/*! @classytic/arc v2.
|
|
2
|
-
import { n as getUserRoles } from "../userHelpers-
|
|
3
|
-
import { n as openApiPlugin, r as openapi_default, t as buildOpenApiSpec } from "../openapi-
|
|
1
|
+
/*! @classytic/arc v2.34.0 | MIT | https://classytic.com */
|
|
2
|
+
import { n as getUserRoles } from "../userHelpers-CWF5pbZD.mjs";
|
|
3
|
+
import { n as openApiPlugin, r as openapi_default, t as buildOpenApiSpec } from "../openapi-BN5cQaO2.mjs";
|
|
4
4
|
import fp from "fastify-plugin";
|
|
5
5
|
//#region src/docs/scalar.ts
|
|
6
6
|
const scalarPlugin = async (fastify, opts = {}) => {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
/*! @classytic/arc v2.
|
|
2
|
-
import { t as __exportAll } from "./rolldown-runtime-
|
|
1
|
+
/*! @classytic/arc v2.34.0 | MIT | https://classytic.com */
|
|
2
|
+
import { t as __exportAll } from "./rolldown-runtime-DNg-KNCk.mjs";
|
|
3
3
|
import { arcLog } from "./logger/index.mjs";
|
|
4
|
-
import { n as requireSingleHeaderValue } from "./headers-
|
|
5
|
-
import { n as getUserRoles } from "./userHelpers-
|
|
4
|
+
import { n as requireSingleHeaderValue } from "./headers-CveNO9gR.mjs";
|
|
5
|
+
import { n as getUserRoles } from "./userHelpers-CWF5pbZD.mjs";
|
|
6
6
|
import fp from "fastify-plugin";
|
|
7
7
|
//#region src/scope/elevation.ts
|
|
8
8
|
var elevation_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
/*! @classytic/arc v2.
|
|
2
|
-
import { t as ArcError } from "../errors-
|
|
3
|
-
import { t as parseJsonBody } from "../jsonBody-
|
|
1
|
+
/*! @classytic/arc v2.34.0 | MIT | https://classytic.com */
|
|
2
|
+
import { t as ArcError } from "../errors-Cd_yy7aW.mjs";
|
|
3
|
+
import { t as parseJsonBody } from "../jsonBody-CfGH-GxO.mjs";
|
|
4
4
|
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
5
5
|
import fp from "fastify-plugin";
|
|
6
6
|
//#region src/encryption/fieldCipher.ts
|
|
@@ -71,7 +71,8 @@ function parseFieldEnvelope(token) {
|
|
|
71
71
|
}
|
|
72
72
|
/** Decrypt a parsed `arc.v1` envelope with the resolved key. Throws on tamper. */
|
|
73
73
|
function decryptField(envelope, key) {
|
|
74
|
-
const
|
|
74
|
+
const secret = toKeyBuffer(key);
|
|
75
|
+
const decipher = createDecipheriv("aes-256-gcm", secret, envelope.iv);
|
|
75
76
|
decipher.setAuthTag(envelope.tag);
|
|
76
77
|
return Buffer.concat([decipher.update(envelope.ciphertext), decipher.final()]).toString("utf8");
|
|
77
78
|
}
|