@classytic/arc 2.32.0 → 2.34.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-Dk34dxhI.mjs → BaseController-CbkuhNI2.mjs} +329 -35
- package/dist/{EventTransport-2uVV34WU.d.mts → EventTransport-BEVP50ka.d.mts} +16 -0
- package/dist/{EventTransport-BV10D0oG.mjs → EventTransport-tq4CEB-q.mjs} +39 -2
- package/dist/{HookSystem-vimw5D7Z.mjs → HookSystem-DqXkMXkB.mjs} +2 -2
- package/dist/{ResourceRegistry-BVr7PgA0.mjs → ResourceRegistry-DoALUlOO.mjs} +3 -2
- package/dist/{actionPermissions-Dti7ebyv.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 +39 -7
- package/dist/auth/redis-session.mjs +1 -1
- package/dist/{authorizationDecision-__T3EJDA.mjs → authorizationDecision-CMMLWmuZ.mjs} +6 -2
- package/dist/{types-B7XeLEgK.d.mts → base-Csjc2oF-.d.mts} +74 -74
- package/dist/{buildHandler-Dn7rzUJo.mjs → buildHandler-BrdYrG_1.mjs} +4 -4
- package/dist/cache/index.mjs +4 -4
- package/dist/{caching-DFJqHyFs.mjs → caching-BcGmvnHh.mjs} +2 -2
- package/dist/cleanup/index.d.mts +17 -5
- package/dist/cleanup/index.mjs +240 -7
- package/dist/cli/commands/describe.d.mts +1 -1
- package/dist/cli/commands/describe.mjs +3 -3
- package/dist/cli/commands/docs.mjs +3 -3
- package/dist/cli/commands/doctor.mjs +1 -1
- package/dist/cli/commands/generate.mjs +2 -2
- package/dist/cli/commands/init.mjs +2 -2
- package/dist/cli/commands/introspect.mjs +2 -2
- package/dist/cli/index.mjs +3 -3
- package/dist/{constants-J99LGTJI.mjs → constants-BxCoxmiN.mjs} +1 -1
- package/dist/context/index.mjs +3 -3
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +9 -8
- package/dist/{core-BwwmNSp7.mjs → core-BYKa8wHu.mjs} +3 -3
- package/dist/{createActionRouter-DkJZWo0m.mjs → createActionRouter-D6T0jWhS.mjs} +8 -8
- package/dist/{createAggregationRouter-B1exOVD8.mjs → createAggregationRouter-QLV2PrWP.mjs} +7 -7
- package/dist/{createApp-Bv0m-Q6_.mjs → createApp-zR-DEesr.mjs} +128 -24
- package/dist/{defineAggregation-gX4No1gt.mjs → defineAggregation-Nv-DwfOf.mjs} +1 -1
- package/dist/{defineResource-XFPPUu2c.mjs → defineResource-ZhuLonLm.mjs} +297 -23
- 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-DNXXxgjl.mjs → elevation-B0tlY8y1.mjs} +4 -4
- package/dist/encryption/index.mjs +3 -3
- package/dist/{errorHandler-B2AZUJUj.mjs → errorHandler-C7xBqHsI.mjs} +42 -5
- package/dist/{errorHandler-CpYUJmPA.d.mts → errorHandler-DI5AN2b5.d.mts} +14 -3
- package/dist/{errors-Bh1KK5ps.mjs → errors-Cd_yy7aW.mjs} +1 -1
- package/dist/{eventPlugin-GF5nnBIq.d.mts → eventPlugin-DSNEZWfV.d.mts} +29 -1
- package/dist/{eventPlugin-Dk_TyXeE.mjs → eventPlugin-Dar8qRaj.mjs} +65 -9
- package/dist/events/index.d.mts +5 -351
- package/dist/events/index.mjs +5 -780
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis-stream-entry.mjs +1 -1
- 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 +7 -7
- package/dist/{fastify-augmentation-BppG5bMT.d.mts → fastify-augmentation-DVsTAy6X.d.mts} +3 -3
- package/dist/{fields-DEi65pGj.mjs → fields-CojeUkS4.mjs} +1 -1
- package/dist/{generate-CRSG0jTv.mjs → generate-DiB0nHPR.mjs} +2 -2
- package/dist/{headers-BXjQYbeC.mjs → headers-CveNO9gR.mjs} +2 -2
- package/dist/{health-5XnTa2uo.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 +90 -9
- package/dist/idempotency/redis.mjs +1 -1
- package/dist/{index-zq6VrjaB.d.mts → index-B-DvUzsr.d.mts} +12 -2
- package/dist/{index-CpzxqePp.d.mts → index-BcR2bFZQ.d.mts} +707 -373
- package/dist/{index-ZWhLTp_s.d.mts → index-C5NnMEKF.d.mts} +1 -1
- package/dist/{index-DXlHwvuE.d.mts → index-CFAJEWCX.d.mts} +18 -5
- package/dist/{index-C-481Zf2.d.mts → index-ugr1tQlW.d.mts} +17 -1
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +13 -12
- package/dist/{init-BbfP8aky.mjs → init-mdJ-v4Xp.mjs} +18 -12
- 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 +99 -97
- 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 +1 -1
- package/dist/{interface-BP4IJCBO.mjs → interface-DznJrm9R.mjs} +1 -1
- package/dist/{jsonBody-DDSR2HFl.mjs → jsonBody-CfGH-GxO.mjs} +2 -2
- package/dist/{keys-B-HBTGXl.mjs → keys-WeKvY1dE.mjs} +1 -1
- package/dist/{loadResources-DisEAmkm.mjs → loadResources-Bj-aSsXi.mjs} +2 -2
- package/dist/{loadResourcesFromEntry-D1MJp13f.mjs → loadResourcesFromEntry-DwaxNcE2.mjs} +2 -2
- package/dist/logger/index.mjs +1 -1
- package/dist/{mcpPlugin-CpfVpKHE.mjs → mcpPlugin-DX3PHD7K.mjs} +60 -21
- package/dist/{memory-DsuWnj4T.mjs → memory-qYSB7rMQ.mjs} +2 -2
- package/dist/{metrics-DdQL8syd.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 +2 -2
- package/dist/{module-Br81am6m.mjs → module-CwKsgZXp.mjs} +1 -1
- package/dist/{multipartBody-BnkBLN7v.mjs → multipartBody-4w-S3-PU.mjs} +1 -1
- package/dist/{openapi-DIRzvxC0.mjs → openapi-CkV5mQ53.mjs} +6 -6
- package/dist/outbox-admin/index.d.mts +2 -2
- package/dist/outbox-admin/index.mjs +4 -4
- package/dist/outbox-module-C1Bs_FXF.d.mts +382 -0
- package/dist/outbox-module-D1QfHqO4.mjs +846 -0
- package/dist/permissions/index.d.mts +2 -2
- package/dist/permissions/index.mjs +5 -5
- package/dist/{permissions-Dl_m0NCe.mjs → permissions-CP47jdi7.mjs} +25 -6
- package/dist/{pipe-ELHCP3zj.mjs → pipe-BzzoEhHj.mjs} +2 -2
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/pipeline/index.mjs +2 -2
- package/dist/{pipeline-CPA-jx0M.mjs → pipeline-q69DZzgG.mjs} +6 -6
- 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-C2ZkKbsn.mjs → pluralize-B7xf8qZF.mjs} +1 -1
- package/dist/{preloadResources-CDs87fTw.mjs → preloadResources-BkyqeTDz.mjs} +1 -1
- package/dist/{preloadResources-T1hmlTEZ.d.mts → preloadResources-g07ldtzh.d.mts} +30 -7
- package/dist/presets/filesUpload.d.mts +2 -2
- package/dist/presets/filesUpload.mjs +5 -5
- package/dist/presets/index.d.mts +2 -2
- package/dist/presets/index.mjs +2 -2
- package/dist/presets/multiTenant.d.mts +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-Dntd_pUO.mjs → presets-BAPijK_A.mjs} +4 -4
- package/dist/projection-BKM0Ui-s.mjs +42 -0
- package/dist/{queryCachePlugin-Dhps-Ldt.mjs → queryCachePlugin-DhsZ0ZIc.mjs} +49 -7
- package/dist/{rateLimit-C7mmAGs3.mjs → rateLimit-D4-ASwsI.mjs} +2 -2
- package/dist/{rateLimitKey-CccEFBfk.mjs → rateLimitKey-XngqVNEV.mjs} +1 -1
- package/dist/{realtime-DVvVZZFz.mjs → realtime-G1TC6Yzc.mjs} +9 -9
- package/dist/{redis-stream-qN6A-RFv.d.mts → redis-stream-mwLWlfHG.d.mts} +1 -1
- package/dist/{registerRoute-B_QrjYZS.mjs → registerRoute-Ba8v0PCa.mjs} +4 -4
- package/dist/registry/index.d.mts +3 -3
- package/dist/registry/index.mjs +3 -3
- package/dist/{registry-DqUmzA9W.mjs → registry-CLxCR5LF.mjs} +2 -2
- package/dist/{renewingLease-BpP9wwj7.mjs → renewingLease-BnWtbA2U.mjs} +1 -1
- package/dist/{replyHelpers-DS-P0b32.mjs → replyHelpers-BSBVCwL3.mjs} +2 -2
- package/dist/repositoryFilter-BANpz6BX.mjs +78 -0
- package/dist/{requestContext-B6vQF-sy.mjs → requestContext-uYE35JxI.mjs} +1 -1
- package/dist/{rolldown-runtime-BhflRrBs.mjs → rolldown-runtime-DNg-KNCk.mjs} +1 -1
- package/dist/runtimeCapabilities-Dm88DaTW.mjs +49 -0
- package/dist/{schedules-C2Q3uv2d.mjs → schedules-CAcZgia0.mjs} +3 -3
- package/dist/{schemaConverter-Crjvczi_.mjs → schemaConverter-XH1RZRyu.mjs} +1 -1
- package/dist/{schemaIR-CnyKK_1j.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-afz_Ieek.mjs → scope-BaWcqBvr.mjs} +4 -4
- package/dist/{simpleEqualityMatcher-C4KiMWUa.mjs → simpleEqualityMatcher-Se8VrsaP.mjs} +1 -1
- package/dist/{sse-B9P4Y-UI.mjs → sse-Cf0jUHsh.mjs} +5 -5
- package/dist/{sseStream-B3zb_d1h.mjs → sseStream-Bvpi-UXQ.mjs} +2 -2
- package/dist/{store-helpers-DmEMHlPQ.mjs → store-helpers-B88p99Z2.mjs} +1 -1
- package/dist/{streaming-DPZdQmDz.mjs → streaming-DsT1B5zJ.mjs} +1 -1
- package/dist/{subscribe-helpers-sp5xTDVA.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 +3 -1
- package/dist/testing/index.d.mts +24 -12
- package/dist/testing/index.mjs +24 -12
- package/dist/testing/outboxStoreContract.mjs +1 -1
- package/dist/testing/storageContract.mjs +1 -1
- package/dist/{transactionContext-BvLKvOIL.mjs → transactionContext-BYtWnwAS.mjs} +1 -1
- package/dist/{typeGuards-B_5-hKjJ.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-CT5vu6M0.d.mts → types-BcTumAz_.d.mts} +41 -7
- package/dist/{types-CwcUQ610.d.mts → types-BfC83KsP.d.mts} +15 -1
- package/dist/{types-DXYX7dIR.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-BNsOXzOd.mjs → userHelpers-CWF5pbZD.mjs} +1 -1
- package/dist/utils/index.d.mts +57 -5
- package/dist/utils/index.mjs +12 -11
- package/dist/{utils-BtOha_Sp.mjs → utils-h1V_sT-t.mjs} +38 -7
- package/dist/{validate-PMCPR9El.mjs → validate-CdQlz52F.mjs} +2 -2
- package/dist/{versioning-D_rhHfZm.d.mts → versioning-DEs-_hFi.d.mts} +1 -1
- package/dist/{versioning-CKv2jz_U.mjs → versioning-rZbyLVD-.mjs} +3 -3
- package/package.json +17 -16
- 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/projection-CjHhM5W1.mjs +0 -18
- package/dist/repositoryFilter-DotlxG3L.mjs +0 -45
|
@@ -1,18 +1,436 @@
|
|
|
1
1
|
|
|
2
|
-
import { a as QueryCacheConfig } from "./QueryCache-DFA-QrU9.mjs";
|
|
3
2
|
import { P as ArcHeaders, i as RequestScope } from "./types-ChuShpCP.mjs";
|
|
4
|
-
import {
|
|
3
|
+
import { d as UserBase, l as PermissionCheck, t as AnyRecord } from "./base-Csjc2oF-.mjs";
|
|
4
|
+
import { a as QueryCacheConfig } from "./QueryCache-DFA-QrU9.mjs";
|
|
5
5
|
import "./elevation-BXCfVcNG.mjs";
|
|
6
6
|
import { n as FieldPermissionMap } from "./fields-I_EeRWND.mjs";
|
|
7
7
|
import { n as EncryptionDirective } from "./types-ByHn9rEO.mjs";
|
|
8
|
-
import { n as DomainEvent } from "./EventTransport-
|
|
8
|
+
import { n as DomainEvent } from "./EventTransport-BEVP50ka.mjs";
|
|
9
9
|
import { Filter } from "@classytic/repo-core/filter";
|
|
10
10
|
import { KeysetPaginationResult, OffsetPaginationResult } from "@classytic/repo-core/pagination";
|
|
11
|
+
import { AggDateBucket, AggMeasure, AggTopN, QueryOptions, StandardRepo, TenantPurgeStrategy, TransactionHandle } from "@classytic/repo-core/repository";
|
|
11
12
|
import { FastifyInstance, FastifyPluginAsync, FastifyReply, FastifyRequest, RouteHandlerMethod, RouteHandlerMethod as RouteHandlerMethod$1 } from "fastify";
|
|
12
13
|
import { DataAdapter, RepositoryLike } from "@classytic/repo-core/adapter";
|
|
13
|
-
import { AggDateBucket, AggMeasure, AggTopN, QueryOptions, StandardRepo, TenantPurgeStrategy } from "@classytic/repo-core/repository";
|
|
14
14
|
import { LookupSpec } from "@classytic/repo-core/lookup";
|
|
15
15
|
import { FieldRule, SchemaBuilderOptions } from "@classytic/repo-core/schema";
|
|
16
|
+
//#region src/types/handlers.d.ts
|
|
17
|
+
/**
|
|
18
|
+
* Minimal server accessor — exposes safe, read-only server decorators.
|
|
19
|
+
* Allows controller handlers to publish events, log, and audit
|
|
20
|
+
* without switching to `rawHandler`.
|
|
21
|
+
*/
|
|
22
|
+
interface ServerAccessor {
|
|
23
|
+
/** Event bus — publish domain events from any handler */
|
|
24
|
+
events?: {
|
|
25
|
+
publish: <T>(type: string, payload: T, meta?: Partial<Record<string, unknown>>) => Promise<void>;
|
|
26
|
+
};
|
|
27
|
+
/** Audit logger — log custom audit entries */
|
|
28
|
+
audit?: {
|
|
29
|
+
create: (resource: string, documentId: string, data: Record<string, unknown>, context?: Record<string, unknown>) => Promise<void>;
|
|
30
|
+
update: (resource: string, documentId: string, before: Record<string, unknown>, after: Record<string, unknown>, context?: Record<string, unknown>) => Promise<void>;
|
|
31
|
+
delete: (resource: string, documentId: string, data: Record<string, unknown>, context?: Record<string, unknown>) => Promise<void>;
|
|
32
|
+
custom: (resource: string, documentId: string, action: string, data?: Record<string, unknown>, context?: Record<string, unknown>) => Promise<void>;
|
|
33
|
+
};
|
|
34
|
+
/** Logger — structured logging */
|
|
35
|
+
log?: {
|
|
36
|
+
info: (...args: unknown[]) => void;
|
|
37
|
+
warn: (...args: unknown[]) => void;
|
|
38
|
+
error: (...args: unknown[]) => void;
|
|
39
|
+
debug: (...args: unknown[]) => void;
|
|
40
|
+
};
|
|
41
|
+
/** QueryCache — stale-while-revalidate data cache */
|
|
42
|
+
queryCache?: {
|
|
43
|
+
get: <T>(key: string) => Promise<{
|
|
44
|
+
data: T;
|
|
45
|
+
status: "fresh" | "stale" | "miss";
|
|
46
|
+
}>;
|
|
47
|
+
set: <T>(key: string, data: T, config: {
|
|
48
|
+
staleTime?: number;
|
|
49
|
+
gcTime?: number;
|
|
50
|
+
tags?: string[];
|
|
51
|
+
}) => Promise<void>;
|
|
52
|
+
getResourceVersion: (resource: string) => Promise<number>;
|
|
53
|
+
bumpResourceVersion: (resource: string) => Promise<void>;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Request context passed to controller handlers.
|
|
58
|
+
*
|
|
59
|
+
* **Generic parameters** (all default to safe permissive types so existing code keeps working):
|
|
60
|
+
* - `TBody` — request body shape (default: `unknown`)
|
|
61
|
+
* - `TParams` — route param shape (default: `Record<string, string>`)
|
|
62
|
+
* - `TQuery` — query string shape (default: `Record<string, unknown>`)
|
|
63
|
+
* - `TUser` — authenticated user shape (default: `UserBase`)
|
|
64
|
+
* - `TMetadata` — internal metadata shape (default: `Record<string, unknown>`;
|
|
65
|
+
* override with `ArcInternalMetadata` or your own augmentation when you
|
|
66
|
+
* need typed access to `_scope`, `_policyFilters`, custom hook context, etc.)
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```typescript
|
|
70
|
+
* // Untyped (default) — req.body is `unknown`, must be narrowed
|
|
71
|
+
* async create(req: IRequestContext) {
|
|
72
|
+
* const data = req.body as Partial<Product>;
|
|
73
|
+
* return { success: true, data: await productRepo.create(data) };
|
|
74
|
+
* }
|
|
75
|
+
*
|
|
76
|
+
* // Typed body — req.body is `CreateProductInput`, narrowing not needed
|
|
77
|
+
* async create(req: IRequestContext<CreateProductInput>) {
|
|
78
|
+
* return { success: true, data: await productRepo.create(req.body) };
|
|
79
|
+
* }
|
|
80
|
+
*
|
|
81
|
+
* // Fully typed — body, route params, query, and metadata
|
|
82
|
+
* async update(
|
|
83
|
+
* req: IRequestContext<
|
|
84
|
+
* Partial<Product>,
|
|
85
|
+
* { id: string },
|
|
86
|
+
* { fields?: string },
|
|
87
|
+
* ArcInternalMetadata
|
|
88
|
+
* >,
|
|
89
|
+
* ) {
|
|
90
|
+
* const fields = req.query.fields?.split(',');
|
|
91
|
+
* const orgId = req.metadata?._scope ? getOrgId(req.metadata._scope) : undefined;
|
|
92
|
+
* return { success: true, data: await productRepo.update(req.params.id, req.body) };
|
|
93
|
+
* }
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
/**
|
|
97
|
+
* First-class projection of `request._scope` for controller handlers.
|
|
98
|
+
*
|
|
99
|
+
* Lifts the two fields most hosts need directly onto `req`, so a controller
|
|
100
|
+
* override never digs through `req.metadata._scope` and calls
|
|
101
|
+
* `getOrgId(scope)` / `getUserId(user)` by hand:
|
|
102
|
+
*
|
|
103
|
+
* ```ts
|
|
104
|
+
* async create(req: IRequestContext) {
|
|
105
|
+
* const flowCtx = { organizationId: req.scope?.organizationId, actorId: req.scope?.userId };
|
|
106
|
+
* }
|
|
107
|
+
* ```
|
|
108
|
+
*
|
|
109
|
+
* Full scope shape (discriminated union of `member` / `service` / `elevated` / `public`)
|
|
110
|
+
* still lives on `req.metadata._scope` for code that needs to branch on
|
|
111
|
+
* `scope.kind` — this projection just surfaces the two keys every
|
|
112
|
+
* tenant-scoped resource reaches for.
|
|
113
|
+
*/
|
|
114
|
+
interface RequestScopeProjection$1 {
|
|
115
|
+
/** Tenant the caller is scoped to (org member, service key bound to an org, or elevated admin's target org). */
|
|
116
|
+
organizationId?: string;
|
|
117
|
+
/** Caller's user id when authenticated — undefined for public / service-only scopes. */
|
|
118
|
+
userId?: string;
|
|
119
|
+
/** Org-level roles (e.g. `['admin', 'warehouse-manager']`) — separate from global `user.roles`. */
|
|
120
|
+
orgRoles?: string[];
|
|
121
|
+
}
|
|
122
|
+
interface IRequestContext<TBody = unknown, TParams extends Record<string, string> = Record<string, string>, TQuery extends Record<string, unknown> = Record<string, unknown>, TUser extends UserBase = UserBase, TMetadata extends Record<string, unknown> = Record<string, unknown>> {
|
|
123
|
+
/** Route parameters (e.g., { id: '123' }) */
|
|
124
|
+
params: TParams;
|
|
125
|
+
/** Query string parameters */
|
|
126
|
+
query: TQuery;
|
|
127
|
+
/** Request body */
|
|
128
|
+
body: TBody;
|
|
129
|
+
/** Authenticated user or null */
|
|
130
|
+
user: TUser | null;
|
|
131
|
+
/**
|
|
132
|
+
* Request headers — `ArcHeaders` (`string | string[] | undefined` per
|
|
133
|
+
* name), the HONEST type: on the HTTP path this is Fastify's headers
|
|
134
|
+
* object passed through, and repeated headers arrive as `string[]` at
|
|
135
|
+
* runtime (2.24 widening — the previous `Record<string, string |
|
|
136
|
+
* undefined>` lied about that). Read via `getHeader(ctx.headers, name)`
|
|
137
|
+
* from `@classytic/arc/utils` for array-safe, duplicate-deterministic
|
|
138
|
+
* access, or `requireSingleHeaderValue` for security-sensitive headers.
|
|
139
|
+
*/
|
|
140
|
+
headers: ArcHeaders;
|
|
141
|
+
/** Organization ID (for multi-tenant apps) */
|
|
142
|
+
organizationId?: string;
|
|
143
|
+
/** Team ID (for team-scoped resources) */
|
|
144
|
+
teamId?: string;
|
|
145
|
+
/**
|
|
146
|
+
* First-class tenant/user scope projection — lifted from `metadata._scope`
|
|
147
|
+
* so controller overrides don't have to dig through Arc internals.
|
|
148
|
+
* See {@link RequestScopeProjection}. `undefined` for routes that run
|
|
149
|
+
* without auth / scope resolution.
|
|
150
|
+
*/
|
|
151
|
+
scope?: RequestScopeProjection$1;
|
|
152
|
+
/**
|
|
153
|
+
* Organization/auth context from middleware.
|
|
154
|
+
* Contains orgRoles, orgScope, organizationId, and any custom fields
|
|
155
|
+
* set by the auth adapter or org-scope plugin.
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* ```typescript
|
|
159
|
+
* async create(req: IRequestContext) {
|
|
160
|
+
* const roles = req.context?.orgRoles ?? [];
|
|
161
|
+
* if (roles.includes('manager')) { ... }
|
|
162
|
+
* }
|
|
163
|
+
* ```
|
|
164
|
+
*/
|
|
165
|
+
context?: RequestContext;
|
|
166
|
+
/**
|
|
167
|
+
* Internal metadata (includes context + Arc internals like `_policyFilters`,
|
|
168
|
+
* `_scope`, `log`). Type as `ArcInternalMetadata` for typed access to Arc's
|
|
169
|
+
* built-in fields, or supply your own interface to layer custom fields.
|
|
170
|
+
*/
|
|
171
|
+
metadata?: TMetadata;
|
|
172
|
+
/**
|
|
173
|
+
* Fastify server accessor — publish events, log, and audit
|
|
174
|
+
* from any handler without switching to `rawHandler`.
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* ```typescript
|
|
178
|
+
* async reschedule(req: IRequestContext) {
|
|
179
|
+
* const result = await repo.reschedule(req.params.id, req.body);
|
|
180
|
+
* await req.server?.events?.publish('interview.rescheduled', { data: result });
|
|
181
|
+
* return { data: result };
|
|
182
|
+
* }
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
185
|
+
server?: ServerAccessor;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Controller response shape — the success-path return from any handler.
|
|
189
|
+
*
|
|
190
|
+
* Errors throw `ArcError` (or any `HttpError`-shaped class); the global
|
|
191
|
+
* error handler catches them and emits an `ErrorContract`. There is no
|
|
192
|
+
* `success` discriminator on the response — HTTP status is the wire
|
|
193
|
+
* discriminator (2xx = data, 4xx/5xx = ErrorContract).
|
|
194
|
+
*/
|
|
195
|
+
interface IControllerResponse<T = unknown> {
|
|
196
|
+
/** Response payload — emitted directly to the wire (no envelope wrap). */
|
|
197
|
+
data: T;
|
|
198
|
+
/** HTTP status code. Defaults to 200. */
|
|
199
|
+
status?: number;
|
|
200
|
+
/** Custom response headers (e.g. X-Total-Count, Link, ETag). */
|
|
201
|
+
headers?: Record<string, string>;
|
|
202
|
+
/** Top-level metadata merged into list-shaped responses (e.g. `{ took }`). */
|
|
203
|
+
meta?: Record<string, unknown>;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Controller handler — Arc's standard pattern.
|
|
207
|
+
*
|
|
208
|
+
* Receives a request context object, returns IControllerResponse.
|
|
209
|
+
* Use with `handler` in routes.
|
|
210
|
+
*
|
|
211
|
+
* **Generic parameters:**
|
|
212
|
+
* - `TResponse` — shape of `IControllerResponse.data` (default: `unknown`)
|
|
213
|
+
* - `TBody` — shape of `req.body` (default: `unknown`)
|
|
214
|
+
* - `TParams` — shape of `req.params` (default: `Record<string, string>`)
|
|
215
|
+
* - `TQuery` — shape of `req.query` (default: `Record<string, unknown>`)
|
|
216
|
+
*
|
|
217
|
+
* Backward-compatible: `ControllerHandler<Product>` still works (only the
|
|
218
|
+
* response data is typed); add more generics as needed when you want
|
|
219
|
+
* type-safe access to the request body, params, or query string.
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* ```typescript
|
|
223
|
+
* // Untyped req — body is unknown, must be narrowed
|
|
224
|
+
* const createProduct: ControllerHandler<Product> = async (req) => {
|
|
225
|
+
* const product = await productRepo.create(req.body as Partial<Product>);
|
|
226
|
+
* return { data: product, status: 201 };
|
|
227
|
+
* };
|
|
228
|
+
*
|
|
229
|
+
* // Fully typed — body, params, query, and response all inferred
|
|
230
|
+
* const updateProduct: ControllerHandler<
|
|
231
|
+
* Product,
|
|
232
|
+
* Partial<Product>,
|
|
233
|
+
* { id: string },
|
|
234
|
+
* { upsert?: string }
|
|
235
|
+
* > = async (req) => {
|
|
236
|
+
* const upsert = req.query.upsert === "true";
|
|
237
|
+
* const product = await productRepo.update(req.params.id, req.body, { upsert });
|
|
238
|
+
* return { data: product };
|
|
239
|
+
* };
|
|
240
|
+
*
|
|
241
|
+
* routes: [{
|
|
242
|
+
* method: 'POST',
|
|
243
|
+
* path: '/products',
|
|
244
|
+
* handler: createProduct,
|
|
245
|
+
* permissions: requireAuth(),
|
|
246
|
+
* }]
|
|
247
|
+
* ```
|
|
248
|
+
*/
|
|
249
|
+
type ControllerHandler<TResponse = unknown, TBody = unknown, TParams extends Record<string, string> = Record<string, string>, TQuery extends Record<string, unknown> = Record<string, unknown>> = (req: IRequestContext<TBody, TParams, TQuery>) => Promise<IControllerResponse<TResponse>>;
|
|
250
|
+
/**
|
|
251
|
+
* Fastify native handler
|
|
252
|
+
*
|
|
253
|
+
* Standard Fastify request/reply pattern.
|
|
254
|
+
* Use with `rawHandler` in routes.
|
|
255
|
+
*
|
|
256
|
+
* @example
|
|
257
|
+
* ```typescript
|
|
258
|
+
* const downloadFile: FastifyHandler = async (request, reply) => {
|
|
259
|
+
* const file = await getFile(request.params.id);
|
|
260
|
+
* reply.header('Content-Type', file.mimeType);
|
|
261
|
+
* return reply.send(file.buffer);
|
|
262
|
+
* };
|
|
263
|
+
*
|
|
264
|
+
* routes: [{
|
|
265
|
+
* method: 'GET',
|
|
266
|
+
* path: '/files/:id/download',
|
|
267
|
+
* rawHandler: downloadFile,
|
|
268
|
+
* permissions: requireAuth(),
|
|
269
|
+
* }]
|
|
270
|
+
* ```
|
|
271
|
+
*/
|
|
272
|
+
type RawRouteHandler = {
|
|
273
|
+
/**
|
|
274
|
+
* Fastify-native route handler — the type behind `RouteDefinition.rawHandler`.
|
|
275
|
+
*
|
|
276
|
+
* Declared with METHOD syntax and read back through an indexed access, which
|
|
277
|
+
* buys two properties a plain arrow alias can't have at once:
|
|
278
|
+
*
|
|
279
|
+
* - **Bivariant parameters.** `strictFunctionTypes` exempts method
|
|
280
|
+
* declarations, so a handler annotated `FastifyRequest<{ Body: CreateBody }>`
|
|
281
|
+
* assigns without a cast. Fastify's own `RouteHandlerMethod` is an arrow
|
|
282
|
+
* alias, so it rejects those — hosts used to write `as any`.
|
|
283
|
+
* - **One call signature.** TypeScript contextually types parameters only
|
|
284
|
+
* when a single constituent is callable, so inline
|
|
285
|
+
* `rawHandler: async (request, reply) => …` infers instead of reporting
|
|
286
|
+
* TS7006.
|
|
287
|
+
*
|
|
288
|
+
* The runtime always passes the real `(request, reply)` pair.
|
|
289
|
+
*/
|
|
290
|
+
handler(this: FastifyInstance, request: FastifyRequest, reply: FastifyReply): unknown;
|
|
291
|
+
}["handler"];
|
|
292
|
+
type FastifyHandler<RouteGeneric extends Record<string, unknown> = Record<string, unknown>> = (request: FastifyRequest<RouteGeneric>, reply: FastifyReply) => Promise<unknown> | unknown;
|
|
293
|
+
/**
|
|
294
|
+
* Union type for route handlers
|
|
295
|
+
*/
|
|
296
|
+
type RouteHandler = ControllerHandler | FastifyHandler;
|
|
297
|
+
/**
|
|
298
|
+
* Controller interface for CRUD operations (strict).
|
|
299
|
+
*
|
|
300
|
+
* `list`'s return type aligns with repo-core's `MinimalRepo.getAll()`
|
|
301
|
+
* contract — the kit MAY return an offset envelope, a keyset envelope,
|
|
302
|
+
* or a raw array. Arc's `BaseController` forwards the kit's response
|
|
303
|
+
* verbatim; consumers narrow on shape
|
|
304
|
+
* (`Array.isArray(data)` → bare array, presence of `total` → offset,
|
|
305
|
+
* presence of `nextCursor` → keyset).
|
|
306
|
+
*/
|
|
307
|
+
interface IController<TDoc = unknown> {
|
|
308
|
+
list(req: IRequestContext): Promise<IControllerResponse<unknown>>;
|
|
309
|
+
get(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
|
|
310
|
+
create(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
|
|
311
|
+
update(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
|
|
312
|
+
delete(req: IRequestContext): Promise<IControllerResponse<{
|
|
313
|
+
message: string;
|
|
314
|
+
}>>;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Flexible controller interface — accepts controllers with any handler style,
|
|
318
|
+
* including class instances with extra methods / private fields.
|
|
319
|
+
*
|
|
320
|
+
* Deliberately NO index signature: one would make real class instances fail
|
|
321
|
+
* structural assignment (`new ScrapController()` would need an
|
|
322
|
+
* `as unknown as ControllerLike` cast, because class instances don't have
|
|
323
|
+
* one). Arc only invokes the CRUD methods at runtime, so the rest of the
|
|
324
|
+
* shape is the caller's concern.
|
|
325
|
+
*
|
|
326
|
+
* The five CRUD slots stay optional so partial controllers (e.g. read-only)
|
|
327
|
+
* assign too. Additional domain methods on the controller are allowed by
|
|
328
|
+
* construction (they're just not part of this contract).
|
|
329
|
+
*/
|
|
330
|
+
interface ControllerLike {
|
|
331
|
+
list?: unknown;
|
|
332
|
+
get?: unknown;
|
|
333
|
+
create?: unknown;
|
|
334
|
+
update?: unknown;
|
|
335
|
+
delete?: unknown;
|
|
336
|
+
}
|
|
337
|
+
//#endregion
|
|
338
|
+
//#region src/types/resource/writes.d.ts
|
|
339
|
+
/**
|
|
340
|
+
* What every write verb receives besides its data.
|
|
341
|
+
*
|
|
342
|
+
* `create` gets exactly this shape; `update` / `delete` get the
|
|
343
|
+
* {@link MutationWriteContext} extension, which adds the loaded target.
|
|
344
|
+
*/
|
|
345
|
+
interface WriteContext<TDoc = AnyRecord> {
|
|
346
|
+
/**
|
|
347
|
+
* The arc request context. Resolve identity from `ctx.req.scope` via
|
|
348
|
+
* `getUserId` / `getOrgId` — never from headers, and never from `req.user`.
|
|
349
|
+
*/
|
|
350
|
+
req: IRequestContext;
|
|
351
|
+
/**
|
|
352
|
+
* The repository arc would have called. Passed so a verb can COMPOSE with
|
|
353
|
+
* the default rather than replace it wholesale (validate, then delegate).
|
|
354
|
+
* Typed as the repo-core contract; a verb owning a richer kit repository
|
|
355
|
+
* usually closes over it directly instead of downcasting this one.
|
|
356
|
+
*/
|
|
357
|
+
repository: RepositoryLike<TDoc>;
|
|
358
|
+
/**
|
|
359
|
+
* Present ONLY under `transactional: true`: repo-core's canonical
|
|
360
|
+
* `TransactionHandle` for the transaction this write runs in — imported,
|
|
361
|
+
* never restated, so a field added to the contract reaches write verbs
|
|
362
|
+
* instead of being silently dropped at this boundary.
|
|
363
|
+
* `uow.session` is the raw driver handle — the join point for work OUTSIDE
|
|
364
|
+
* the repository, canonically an outbox row:
|
|
365
|
+
* `outbox.store(event, { session: ctx.uow.session })` commits the event
|
|
366
|
+
* atomically with the business write. Connection-bound backends (SQLite)
|
|
367
|
+
* provide an empty handle; their tx-bound `ctx.repository` is the only
|
|
368
|
+
* join point.
|
|
369
|
+
*/
|
|
370
|
+
uow?: TransactionHandle;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Context for `update` and `delete` verbs — operations with a mutable target.
|
|
374
|
+
*
|
|
375
|
+
* Both fields are REQUIRED, not optional: arc's `loadMutableTarget` preflight
|
|
376
|
+
* (access control, tenant scope, ownership) THROWS before the verb when the
|
|
377
|
+
* target cannot be produced, so by the time a verb runs the target exists.
|
|
378
|
+
* Typing them optional would push a defensive null-check into every command
|
|
379
|
+
* for a case that cannot occur.
|
|
380
|
+
*/
|
|
381
|
+
interface MutationWriteContext<TDoc = AnyRecord> extends WriteContext<TDoc> {
|
|
382
|
+
/**
|
|
383
|
+
* The REPOSITORY PRIMARY KEY — identical to the verb's own `id` argument.
|
|
384
|
+
*
|
|
385
|
+
* This is what `repository.update(id, …)` / `.delete(id, …)` would have
|
|
386
|
+
* received, which repo-core types as "update by primary key". It is NOT
|
|
387
|
+
* necessarily the value in the URL: a resource declaring `idField: 'slug'`
|
|
388
|
+
* routes on the slug while its repository keys off `_id`, and arc translates
|
|
389
|
+
* between them (`resolveMutationRepoId`) before either the repository or a
|
|
390
|
+
* verb is called. Handing a verb the raw route param would give a domain
|
|
391
|
+
* command a value its own repository cannot resolve.
|
|
392
|
+
*
|
|
393
|
+
* The route param is still reachable — `ctx.req.params.id`.
|
|
394
|
+
*/
|
|
395
|
+
id: string;
|
|
396
|
+
/**
|
|
397
|
+
* The document as arc loaded it to run its permission and tenant checks.
|
|
398
|
+
* Handed over rather than re-fetched by the verb on purpose: a second read
|
|
399
|
+
* can observe a different document than the one the request was authorised
|
|
400
|
+
* against.
|
|
401
|
+
*/
|
|
402
|
+
existing: TDoc;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Per-slot write commands. Every field is optional and independent: declare
|
|
406
|
+
* `update` alone and `create` / `delete` keep calling the repository.
|
|
407
|
+
*
|
|
408
|
+
* **Return contract, and it differs from the repository's on purpose.**
|
|
409
|
+
* A repository signals "not found" by returning `null`; a domain command
|
|
410
|
+
* signals failure by THROWING a typed error. So when a verb is declared:
|
|
411
|
+
*
|
|
412
|
+
* - `create` — must return the created document. A nullish return is a
|
|
413
|
+
* CONTRACT VIOLATION and arc throws (a 201 carrying `undefined` data and
|
|
414
|
+
* after-hooks fed a non-document would be worse than the loud failure).
|
|
415
|
+
* - `update` — returning the updated document is preferred (it becomes the
|
|
416
|
+
* response directly). A `void` return is also success — arc issues ONE
|
|
417
|
+
* repository re-read to answer the request, so a void-returning command
|
|
418
|
+
* costs one extra read per update. It is never translated into a 404.
|
|
419
|
+
* - `delete` — `undefined` is success. `deleteDraft()` returns `void`, and
|
|
420
|
+
* treating that as a miss would answer 404 for a delete that happened.
|
|
421
|
+
*/
|
|
422
|
+
interface ResourceWrites<TDoc = AnyRecord> {
|
|
423
|
+
create?(data: Partial<TDoc>, ctx: WriteContext<TDoc>): Promise<TDoc>;
|
|
424
|
+
update?(id: string, data: Partial<TDoc>, ctx: MutationWriteContext<TDoc>): Promise<TDoc | null | undefined>;
|
|
425
|
+
delete?(id: string, ctx: MutationWriteContext<TDoc>): Promise<unknown>;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* The three slots a write verb may be declared for. Kept in lockstep with the
|
|
429
|
+
* runtime list (`MUTATION_OPERATIONS` in `constants.ts`) by a `satisfies`
|
|
430
|
+
* check at its use sites — drift is a compile error, not a runtime surprise.
|
|
431
|
+
*/
|
|
432
|
+
type WriteVerbKey = "create" | "update" | "delete";
|
|
433
|
+
//#endregion
|
|
16
434
|
//#region src/core/BodySanitizer.d.ts
|
|
17
435
|
/**
|
|
18
436
|
* Policy for handling fields the caller lacks write permission for.
|
|
@@ -31,10 +449,25 @@ interface BodySanitizerConfig {
|
|
|
31
449
|
* Default: `'reject'` — surface the misconfiguration as a 403.
|
|
32
450
|
*/
|
|
33
451
|
onFieldWriteDenied?: FieldWriteDenialPolicy;
|
|
452
|
+
/**
|
|
453
|
+
* What to do when an update body carries an `immutable` /
|
|
454
|
+
* `immutableAfterCreate` field. Default: `'strip'` — today's behaviour.
|
|
455
|
+
*
|
|
456
|
+
* `'reject'` throws a 403 instead of returning 200 with the field silently
|
|
457
|
+
* unchanged (observed live: `PATCH {"type":"rent"}` → 200, `"type":"purchase"`).
|
|
458
|
+
*
|
|
459
|
+
* NOT folded into {@link onFieldWriteDenied}, which defaults to `'reject'`:
|
|
460
|
+
* a full-object PATCH echoing an immutable field back UNCHANGED is legitimate,
|
|
461
|
+
* and the sanitizer has no stored document to distinguish it from a real
|
|
462
|
+
* change. Sharing that default would break those callers. Opt in per resource,
|
|
463
|
+
* or fleet-wide via `ARC_STRICT_IMMUTABLE_WRITES`.
|
|
464
|
+
*/
|
|
465
|
+
onImmutableWrite?: FieldWriteDenialPolicy;
|
|
34
466
|
}
|
|
35
467
|
declare class BodySanitizer {
|
|
36
468
|
private schemaOptions;
|
|
37
469
|
private onFieldWriteDenied;
|
|
470
|
+
private onImmutableWrite;
|
|
38
471
|
constructor(config: BodySanitizerConfig);
|
|
39
472
|
/**
|
|
40
473
|
* Strip readonly and system-managed fields from request body.
|
|
@@ -182,6 +615,25 @@ interface ControllerConfigurableOptions {
|
|
|
182
615
|
* - `'strip'`: legacy silent-drop.
|
|
183
616
|
*/
|
|
184
617
|
onFieldWriteDenied?: FieldWriteDenialPolicy;
|
|
618
|
+
/** Immutable-write policy — see `onImmutableWrite` on the resource config. */
|
|
619
|
+
onImmutableWrite?: FieldWriteDenialPolicy;
|
|
620
|
+
/**
|
|
621
|
+
* Domain commands bound to the create / update / delete slots. When a slot
|
|
622
|
+
* declares one, the controller runs its full pipeline and calls the verb
|
|
623
|
+
* instead of `repository.<op>` — see `ResourceWrites`.
|
|
624
|
+
*
|
|
625
|
+
* Configurable rather than construction-only so a HOST-SUPPLIED controller
|
|
626
|
+
* receives it through `configure()` too. A custom controller that keeps
|
|
627
|
+
* `BaseCrudController`'s write methods is the common case, and it must not
|
|
628
|
+
* silently lose the resource's verbs just because it was hand-constructed.
|
|
629
|
+
*/
|
|
630
|
+
writes?: ResourceWrites;
|
|
631
|
+
/**
|
|
632
|
+
* Transactional write envelope — see `ResourceConfig.transactional`.
|
|
633
|
+
* Configurable so a host-supplied controller receives it through
|
|
634
|
+
* `configure()` like `writes`.
|
|
635
|
+
*/
|
|
636
|
+
transactional?: boolean;
|
|
185
637
|
}
|
|
186
638
|
/**
|
|
187
639
|
* Full constructor options — union of construction-only + configurable.
|
|
@@ -197,13 +649,17 @@ interface BaseControllerOptions extends ControllerConstructionOptions, Controlle
|
|
|
197
649
|
* still available through the underlying `RequestScope` for advanced
|
|
198
650
|
* branching on `scope.kind`.
|
|
199
651
|
*/
|
|
200
|
-
interface RequestScopeProjection
|
|
652
|
+
interface RequestScopeProjection {
|
|
201
653
|
/** Tenant the caller is scoped to (member, pinned elevated admin, service key bound to an org). */
|
|
202
654
|
organizationId?: string;
|
|
203
655
|
/** Caller's user id when authenticated — undefined for public / service-only scopes. */
|
|
204
656
|
userId?: string;
|
|
205
657
|
/** Org-level roles (e.g. `['admin', 'warehouse-manager']`) — separate from global `user.roles`. */
|
|
206
658
|
orgRoles?: string[];
|
|
659
|
+
/** Active team for member scopes. */
|
|
660
|
+
teamId?: string;
|
|
661
|
+
/** Host-defined project / branch / region / workspace dimensions. */
|
|
662
|
+
context?: Readonly<Record<string, string>>;
|
|
207
663
|
}
|
|
208
664
|
//#endregion
|
|
209
665
|
//#region src/hooks/HookSystem.d.ts
|
|
@@ -229,7 +685,7 @@ interface HookContext<T = AnyRecord> {
|
|
|
229
685
|
* routes. Branch on `scope.kind` via `ctx.context._scope` when you need the
|
|
230
686
|
* full discriminated union.
|
|
231
687
|
*/
|
|
232
|
-
scope?: RequestScopeProjection
|
|
688
|
+
scope?: RequestScopeProjection;
|
|
233
689
|
}
|
|
234
690
|
type HookHandler<T = AnyRecord> = (ctx: HookContext<T>) => void | Promise<void> | T | Promise<T>;
|
|
235
691
|
/**
|
|
@@ -588,9 +1044,12 @@ interface QueryResolverConfig {
|
|
|
588
1044
|
declare class QueryResolver {
|
|
589
1045
|
private queryParser;
|
|
590
1046
|
private maxLimit;
|
|
1047
|
+
/** Set only when the HOST passed one — it outranks any parser's cap, at any time. */
|
|
1048
|
+
private readonly explicitMaxLimit;
|
|
591
1049
|
private defaultLimit;
|
|
592
1050
|
/** `undefined` means "no default sort" (caller passed `false`). */
|
|
593
1051
|
private defaultSort;
|
|
1052
|
+
private readonly explicitDefaultSort;
|
|
594
1053
|
private schemaOptions;
|
|
595
1054
|
private tenantField;
|
|
596
1055
|
constructor(config?: QueryResolverConfig);
|
|
@@ -601,6 +1060,22 @@ declare class QueryResolver {
|
|
|
601
1060
|
* truth — pairs with `BaseCrudController.setQueryParser()`.
|
|
602
1061
|
*/
|
|
603
1062
|
setParser(parser: QueryParserInterface): void;
|
|
1063
|
+
/**
|
|
1064
|
+
* The default sort, but only when the parser permits the field.
|
|
1065
|
+
*
|
|
1066
|
+
* A resource declaring `allowedSortFields` has stated exactly what may be
|
|
1067
|
+
* sorted on. Arc then applied its OWN default of `-createdAt` on top, and
|
|
1068
|
+
* mongokit 3.31 REJECTS a sort outside the allowlist rather than dropping it —
|
|
1069
|
+
* so every list call on such a resource answered 400, `Blocked sort field not
|
|
1070
|
+
* in allowlist: createdAt`. REST and MCP alike; the endpoint was unusable, and
|
|
1071
|
+
* nothing the caller sent could avoid it.
|
|
1072
|
+
*
|
|
1073
|
+
* A general default must never outrank a specific declaration, so the
|
|
1074
|
+
* FRAMEWORK default withdraws. A HOST-declared `defaultSort` is left alone —
|
|
1075
|
+
* that contradiction is its author's to resolve, and `defineResource` reports
|
|
1076
|
+
* it as a boot diagnostic instead of arc silently picking a winner.
|
|
1077
|
+
*/
|
|
1078
|
+
private permittedDefaultSort;
|
|
604
1079
|
/**
|
|
605
1080
|
* Resolve a request into parsed query options -- ONE parse per request.
|
|
606
1081
|
* Combines what was previously _buildContext + _parseQueryOptions + _applyFilters.
|
|
@@ -690,6 +1165,16 @@ declare class BaseCrudController<TDoc = AnyRecord, TRepository extends Repositor
|
|
|
690
1165
|
*/
|
|
691
1166
|
protected _defaultSort?: string | false;
|
|
692
1167
|
protected _onFieldWriteDenied?: FieldWriteDenialPolicy;
|
|
1168
|
+
protected _onImmutableWrite?: FieldWriteDenialPolicy;
|
|
1169
|
+
/**
|
|
1170
|
+
* Domain commands bound to the write slots (`ResourceWrites`). Consulted at
|
|
1171
|
+
* the PERSISTENCE step only — everything upstream of it (sanitizer, tenant
|
|
1172
|
+
* injection, actor stamp, hook sandwich) runs exactly as it does without
|
|
1173
|
+
* one, which is the entire point of the seam.
|
|
1174
|
+
*/
|
|
1175
|
+
protected _writes?: ResourceWrites;
|
|
1176
|
+
/** Transactional write envelope — see `ResourceConfig.transactional`. */
|
|
1177
|
+
protected _transactional: boolean;
|
|
693
1178
|
constructor(repository: TRepository, options?: BaseControllerOptions);
|
|
694
1179
|
/**
|
|
695
1180
|
* Swap the controller's query parser. Mutates the existing `QueryResolver`
|
|
@@ -823,6 +1308,35 @@ declare class BaseCrudController<TDoc = AnyRecord, TRepository extends Repositor
|
|
|
823
1308
|
meta?: Record<string, unknown>;
|
|
824
1309
|
pipeProcessedData?: boolean;
|
|
825
1310
|
}, executor: (processed: TInput) => Promise<TResult>): Promise<TResult>;
|
|
1311
|
+
/**
|
|
1312
|
+
* Run a write's PERSISTENCE step, transactionally when the resource asked.
|
|
1313
|
+
*
|
|
1314
|
+
* Plain mode hands `fn` the live repository. Transactional mode runs it
|
|
1315
|
+
* through repo-core's `retryingTransaction`: the callback receives the
|
|
1316
|
+
* TX-BOUND repository (so declared verbs and repository calls join the same
|
|
1317
|
+
* transaction), transient conflicts re-run `fn` with jittered bounded
|
|
1318
|
+
* backoff, everything else — including `VersionConflictError` — surfaces
|
|
1319
|
+
* once. Hooks stay OUTSIDE: before/around ran already and must not re-run;
|
|
1320
|
+
* after-hooks run post-commit, which they already do by running after this.
|
|
1321
|
+
*/
|
|
1322
|
+
protected runWritePersistence<T>(fn: (repo: TRepository, uow?: TransactionHandle) => Promise<T>): Promise<T>;
|
|
1323
|
+
/** Assemble the `WriteContext` a `create` verb receives. */
|
|
1324
|
+
protected writeContext(req: IRequestContext, repo?: TRepository, uow?: TransactionHandle): WriteContext;
|
|
1325
|
+
/**
|
|
1326
|
+
* Assemble the `MutationWriteContext` an `update` / `delete` verb receives.
|
|
1327
|
+
*
|
|
1328
|
+
* `existing` is the document arc ALREADY loaded to run its permission and
|
|
1329
|
+
* tenant checks — `loadMutableTarget` throws before any verb runs when it
|
|
1330
|
+
* cannot produce one, which is why the context types it required. Handing
|
|
1331
|
+
* it over rather than letting the verb re-fetch is deliberate: a second
|
|
1332
|
+
* read can observe a different document than the one the request was
|
|
1333
|
+
* authorised against.
|
|
1334
|
+
*
|
|
1335
|
+
* `id` is the REPOSITORY PRIMARY KEY (`repoId`), never the raw route param —
|
|
1336
|
+
* see the note on `MutationWriteContext.id`. Callers pass `resolveRepoId`'s
|
|
1337
|
+
* output.
|
|
1338
|
+
*/
|
|
1339
|
+
protected mutationWriteContext(req: IRequestContext, id: string, existing: unknown, repo?: TRepository, uow?: TransactionHandle): MutationWriteContext;
|
|
826
1340
|
/**
|
|
827
1341
|
* Run `executeAfter` for the given op + data. No-op when hooks aren't
|
|
828
1342
|
* wired or `resourceName` isn't set. Caller passes the data shape it
|
|
@@ -1186,396 +1700,74 @@ interface AuthPluginOptions {
|
|
|
1186
1700
|
/**
|
|
1187
1701
|
* Custom auth failure handler. Customize the 401 response when
|
|
1188
1702
|
* authentication fails.
|
|
1189
|
-
*/
|
|
1190
|
-
onFailure?: (request: FastifyRequest, reply: FastifyReply, error?: Error) => void | Promise<void>;
|
|
1191
|
-
/**
|
|
1192
|
-
* Expose detailed auth error messages in 401 responses. When `false`
|
|
1193
|
-
* (default), returns generic "Authentication required". Decoupled from
|
|
1194
|
-
* log level — set explicitly per environment.
|
|
1195
|
-
*/
|
|
1196
|
-
exposeAuthErrors?: boolean;
|
|
1197
|
-
/** Property name to store user on request (default: 'user') */
|
|
1198
|
-
userProperty?: string;
|
|
1199
|
-
/**
|
|
1200
|
-
* Custom token extractor for the built-in JWT auth path. Defaults to
|
|
1201
|
-
* extracting Bearer token from Authorization header. Use when tokens
|
|
1202
|
-
* are in HttpOnly cookies, custom headers, or query params.
|
|
1203
|
-
*
|
|
1204
|
-
* @example
|
|
1205
|
-
* ```typescript
|
|
1206
|
-
* tokenExtractor: (request) => request.cookies?.['auth-token'] ?? null,
|
|
1207
|
-
* ```
|
|
1208
|
-
*/
|
|
1209
|
-
tokenExtractor?: (request: FastifyRequest) => string | null;
|
|
1210
|
-
/**
|
|
1211
|
-
* Token revocation check — called after JWT verification succeeds.
|
|
1212
|
-
* Return `true` to reject the token (revoked), `false` to allow.
|
|
1213
|
-
*
|
|
1214
|
-
* **Fail-closed**: if the check throws, the token is treated as revoked.
|
|
1215
|
-
*
|
|
1216
|
-
* @example
|
|
1217
|
-
* ```typescript
|
|
1218
|
-
* isRevoked: async (decoded) => {
|
|
1219
|
-
* return await redis.sismember('revoked-tokens', decoded.jti ?? decoded.id);
|
|
1220
|
-
* },
|
|
1221
|
-
* ```
|
|
1222
|
-
*/
|
|
1223
|
-
isRevoked?: (decoded: Record<string, unknown>) => boolean | Promise<boolean>;
|
|
1224
|
-
/**
|
|
1225
|
-
* Enforce strict JWT `type` claim validation (default: `true`).
|
|
1226
|
-
*
|
|
1227
|
-
* When enabled, `authenticate` requires `decoded.type === "access"`.
|
|
1228
|
-
* Tokens with a missing or unexpected `type` claim are rejected —
|
|
1229
|
-
* defence in depth for apps that reuse the JWT secret to sign other
|
|
1230
|
-
* token kinds (invite links, one-time verification codes).
|
|
1231
|
-
*
|
|
1232
|
-
* Arc's own `issueTokens` always sets `type: "access"` or
|
|
1233
|
-
* `type: "refresh"`, so this default is safe for Arc-generated tokens.
|
|
1234
|
-
*
|
|
1235
|
-
* Set to `false` ONLY when you must accept tokens signed without a
|
|
1236
|
-
* `type` claim (e.g. a legacy issuer you don't control). In that mode
|
|
1237
|
-
* Arc still rejects tokens explicitly marked `type: "refresh"`.
|
|
1238
|
-
*/
|
|
1239
|
-
strictTokenType?: boolean;
|
|
1240
|
-
}
|
|
1241
|
-
//#endregion
|
|
1242
|
-
//#region src/core/middlewares/rateLimit.d.ts
|
|
1243
|
-
/**
|
|
1244
|
-
* Per-route CORS override, read by `@fastify/cors` off `routeOptions.config.cors`.
|
|
1245
|
-
*
|
|
1246
|
-
* The app-wide policy cannot serve both an API and public assets: an API wants
|
|
1247
|
-
* `credentials: true` with a pinned origin list, while a public asset wants
|
|
1248
|
-
* `origin: "*"` — and `*` with credentials is forbidden by the CORS spec (arc
|
|
1249
|
-
* throws at boot on that combination). Per-route is the granularity that
|
|
1250
|
-
* resolves it. `false` disables CORS for the route entirely.
|
|
1251
|
-
*
|
|
1252
|
-
* Arc does not interpret the value; `@fastify/cors` owns the semantics. Typed
|
|
1253
|
-
* loosely on purpose so a host is never blocked by arc lagging the plugin's
|
|
1254
|
-
* option surface.
|
|
1255
|
-
*/
|
|
1256
|
-
type RouteCorsConfig = Record<string, unknown> | false;
|
|
1257
|
-
//#endregion
|
|
1258
|
-
//#region src/types/handlers.d.ts
|
|
1259
|
-
/**
|
|
1260
|
-
* Minimal server accessor — exposes safe, read-only server decorators.
|
|
1261
|
-
* Allows controller handlers to publish events, log, and audit
|
|
1262
|
-
* without switching to `rawHandler`.
|
|
1263
|
-
*/
|
|
1264
|
-
interface ServerAccessor {
|
|
1265
|
-
/** Event bus — publish domain events from any handler */
|
|
1266
|
-
events?: {
|
|
1267
|
-
publish: <T>(type: string, payload: T, meta?: Partial<Record<string, unknown>>) => Promise<void>;
|
|
1268
|
-
};
|
|
1269
|
-
/** Audit logger — log custom audit entries */
|
|
1270
|
-
audit?: {
|
|
1271
|
-
create: (resource: string, documentId: string, data: Record<string, unknown>, context?: Record<string, unknown>) => Promise<void>;
|
|
1272
|
-
update: (resource: string, documentId: string, before: Record<string, unknown>, after: Record<string, unknown>, context?: Record<string, unknown>) => Promise<void>;
|
|
1273
|
-
delete: (resource: string, documentId: string, data: Record<string, unknown>, context?: Record<string, unknown>) => Promise<void>;
|
|
1274
|
-
custom: (resource: string, documentId: string, action: string, data?: Record<string, unknown>, context?: Record<string, unknown>) => Promise<void>;
|
|
1275
|
-
};
|
|
1276
|
-
/** Logger — structured logging */
|
|
1277
|
-
log?: {
|
|
1278
|
-
info: (...args: unknown[]) => void;
|
|
1279
|
-
warn: (...args: unknown[]) => void;
|
|
1280
|
-
error: (...args: unknown[]) => void;
|
|
1281
|
-
debug: (...args: unknown[]) => void;
|
|
1282
|
-
};
|
|
1283
|
-
/** QueryCache — stale-while-revalidate data cache */
|
|
1284
|
-
queryCache?: {
|
|
1285
|
-
get: <T>(key: string) => Promise<{
|
|
1286
|
-
data: T;
|
|
1287
|
-
status: "fresh" | "stale" | "miss";
|
|
1288
|
-
}>;
|
|
1289
|
-
set: <T>(key: string, data: T, config: {
|
|
1290
|
-
staleTime?: number;
|
|
1291
|
-
gcTime?: number;
|
|
1292
|
-
tags?: string[];
|
|
1293
|
-
}) => Promise<void>;
|
|
1294
|
-
getResourceVersion: (resource: string) => Promise<number>;
|
|
1295
|
-
bumpResourceVersion: (resource: string) => Promise<void>;
|
|
1296
|
-
};
|
|
1297
|
-
}
|
|
1298
|
-
/**
|
|
1299
|
-
* Request context passed to controller handlers.
|
|
1300
|
-
*
|
|
1301
|
-
* **Generic parameters** (all default to safe permissive types so existing code keeps working):
|
|
1302
|
-
* - `TBody` — request body shape (default: `unknown`)
|
|
1303
|
-
* - `TParams` — route param shape (default: `Record<string, string>`)
|
|
1304
|
-
* - `TQuery` — query string shape (default: `Record<string, unknown>`)
|
|
1305
|
-
* - `TUser` — authenticated user shape (default: `UserBase`)
|
|
1306
|
-
* - `TMetadata` — internal metadata shape (default: `Record<string, unknown>`;
|
|
1307
|
-
* override with `ArcInternalMetadata` or your own augmentation when you
|
|
1308
|
-
* need typed access to `_scope`, `_policyFilters`, custom hook context, etc.)
|
|
1309
|
-
*
|
|
1310
|
-
* @example
|
|
1311
|
-
* ```typescript
|
|
1312
|
-
* // Untyped (default) — req.body is `unknown`, must be narrowed
|
|
1313
|
-
* async create(req: IRequestContext) {
|
|
1314
|
-
* const data = req.body as Partial<Product>;
|
|
1315
|
-
* return { success: true, data: await productRepo.create(data) };
|
|
1316
|
-
* }
|
|
1317
|
-
*
|
|
1318
|
-
* // Typed body — req.body is `CreateProductInput`, narrowing not needed
|
|
1319
|
-
* async create(req: IRequestContext<CreateProductInput>) {
|
|
1320
|
-
* return { success: true, data: await productRepo.create(req.body) };
|
|
1321
|
-
* }
|
|
1322
|
-
*
|
|
1323
|
-
* // Fully typed — body, route params, query, and metadata
|
|
1324
|
-
* async update(
|
|
1325
|
-
* req: IRequestContext<
|
|
1326
|
-
* Partial<Product>,
|
|
1327
|
-
* { id: string },
|
|
1328
|
-
* { fields?: string },
|
|
1329
|
-
* ArcInternalMetadata
|
|
1330
|
-
* >,
|
|
1331
|
-
* ) {
|
|
1332
|
-
* const fields = req.query.fields?.split(',');
|
|
1333
|
-
* const orgId = req.metadata?._scope ? getOrgId(req.metadata._scope) : undefined;
|
|
1334
|
-
* return { success: true, data: await productRepo.update(req.params.id, req.body) };
|
|
1335
|
-
* }
|
|
1336
|
-
* ```
|
|
1337
|
-
*/
|
|
1338
|
-
/**
|
|
1339
|
-
* First-class projection of `request._scope` for controller handlers.
|
|
1340
|
-
*
|
|
1341
|
-
* Lifts the two fields most hosts need directly onto `req`, so a controller
|
|
1342
|
-
* override never digs through `req.metadata._scope` and calls
|
|
1343
|
-
* `getOrgId(scope)` / `getUserId(user)` by hand:
|
|
1344
|
-
*
|
|
1345
|
-
* ```ts
|
|
1346
|
-
* async create(req: IRequestContext) {
|
|
1347
|
-
* const flowCtx = { organizationId: req.scope?.organizationId, actorId: req.scope?.userId };
|
|
1348
|
-
* }
|
|
1349
|
-
* ```
|
|
1350
|
-
*
|
|
1351
|
-
* Full scope shape (discriminated union of `member` / `service` / `elevated` / `public`)
|
|
1352
|
-
* still lives on `req.metadata._scope` for code that needs to branch on
|
|
1353
|
-
* `scope.kind` — this projection just surfaces the two keys every
|
|
1354
|
-
* tenant-scoped resource reaches for.
|
|
1355
|
-
*/
|
|
1356
|
-
interface RequestScopeProjection {
|
|
1357
|
-
/** Tenant the caller is scoped to (org member, service key bound to an org, or elevated admin's target org). */
|
|
1358
|
-
organizationId?: string;
|
|
1359
|
-
/** Caller's user id when authenticated — undefined for public / service-only scopes. */
|
|
1360
|
-
userId?: string;
|
|
1361
|
-
/** Org-level roles (e.g. `['admin', 'warehouse-manager']`) — separate from global `user.roles`. */
|
|
1362
|
-
orgRoles?: string[];
|
|
1363
|
-
}
|
|
1364
|
-
interface IRequestContext<TBody = unknown, TParams extends Record<string, string> = Record<string, string>, TQuery extends Record<string, unknown> = Record<string, unknown>, TUser extends UserBase = UserBase, TMetadata extends Record<string, unknown> = Record<string, unknown>> {
|
|
1365
|
-
/** Route parameters (e.g., { id: '123' }) */
|
|
1366
|
-
params: TParams;
|
|
1367
|
-
/** Query string parameters */
|
|
1368
|
-
query: TQuery;
|
|
1369
|
-
/** Request body */
|
|
1370
|
-
body: TBody;
|
|
1371
|
-
/** Authenticated user or null */
|
|
1372
|
-
user: TUser | null;
|
|
1373
|
-
/**
|
|
1374
|
-
* Request headers — `ArcHeaders` (`string | string[] | undefined` per
|
|
1375
|
-
* name), the HONEST type: on the HTTP path this is Fastify's headers
|
|
1376
|
-
* object passed through, and repeated headers arrive as `string[]` at
|
|
1377
|
-
* runtime (2.24 widening — the previous `Record<string, string |
|
|
1378
|
-
* undefined>` lied about that). Read via `getHeader(ctx.headers, name)`
|
|
1379
|
-
* from `@classytic/arc/utils` for array-safe, duplicate-deterministic
|
|
1380
|
-
* access, or `requireSingleHeaderValue` for security-sensitive headers.
|
|
1381
|
-
*/
|
|
1382
|
-
headers: ArcHeaders;
|
|
1383
|
-
/** Organization ID (for multi-tenant apps) */
|
|
1384
|
-
organizationId?: string;
|
|
1385
|
-
/** Team ID (for team-scoped resources) */
|
|
1386
|
-
teamId?: string;
|
|
1703
|
+
*/
|
|
1704
|
+
onFailure?: (request: FastifyRequest, reply: FastifyReply, error?: Error) => void | Promise<void>;
|
|
1387
1705
|
/**
|
|
1388
|
-
*
|
|
1389
|
-
*
|
|
1390
|
-
*
|
|
1391
|
-
* without auth / scope resolution.
|
|
1706
|
+
* Expose detailed auth error messages in 401 responses. When `false`
|
|
1707
|
+
* (default), returns generic "Authentication required". Decoupled from
|
|
1708
|
+
* log level — set explicitly per environment.
|
|
1392
1709
|
*/
|
|
1393
|
-
|
|
1710
|
+
exposeAuthErrors?: boolean;
|
|
1711
|
+
/** Property name to store user on request (default: 'user') */
|
|
1712
|
+
userProperty?: string;
|
|
1394
1713
|
/**
|
|
1395
|
-
*
|
|
1396
|
-
*
|
|
1397
|
-
*
|
|
1714
|
+
* Custom token extractor for the built-in JWT auth path. Defaults to
|
|
1715
|
+
* extracting Bearer token from Authorization header. Use when tokens
|
|
1716
|
+
* are in HttpOnly cookies, custom headers, or query params.
|
|
1398
1717
|
*
|
|
1399
1718
|
* @example
|
|
1400
1719
|
* ```typescript
|
|
1401
|
-
*
|
|
1402
|
-
* const roles = req.context?.orgRoles ?? [];
|
|
1403
|
-
* if (roles.includes('manager')) { ... }
|
|
1404
|
-
* }
|
|
1720
|
+
* tokenExtractor: (request) => request.cookies?.['auth-token'] ?? null,
|
|
1405
1721
|
* ```
|
|
1406
1722
|
*/
|
|
1407
|
-
|
|
1408
|
-
/**
|
|
1409
|
-
* Internal metadata (includes context + Arc internals like `_policyFilters`,
|
|
1410
|
-
* `_scope`, `log`). Type as `ArcInternalMetadata` for typed access to Arc's
|
|
1411
|
-
* built-in fields, or supply your own interface to layer custom fields.
|
|
1412
|
-
*/
|
|
1413
|
-
metadata?: TMetadata;
|
|
1723
|
+
tokenExtractor?: (request: FastifyRequest) => string | null;
|
|
1414
1724
|
/**
|
|
1415
|
-
*
|
|
1416
|
-
*
|
|
1725
|
+
* Token revocation check — called after JWT verification succeeds.
|
|
1726
|
+
* Return `true` to reject the token (revoked), `false` to allow.
|
|
1727
|
+
*
|
|
1728
|
+
* **Fail-closed**: if the check throws, the token is treated as revoked.
|
|
1417
1729
|
*
|
|
1418
1730
|
* @example
|
|
1419
1731
|
* ```typescript
|
|
1420
|
-
* async
|
|
1421
|
-
*
|
|
1422
|
-
*
|
|
1423
|
-
* return { data: result };
|
|
1424
|
-
* }
|
|
1732
|
+
* isRevoked: async (decoded) => {
|
|
1733
|
+
* return await redis.sismember('revoked-tokens', decoded.jti ?? decoded.id);
|
|
1734
|
+
* },
|
|
1425
1735
|
* ```
|
|
1426
1736
|
*/
|
|
1427
|
-
|
|
1428
|
-
}
|
|
1429
|
-
/**
|
|
1430
|
-
* Controller response shape — the success-path return from any handler.
|
|
1431
|
-
*
|
|
1432
|
-
* Errors throw `ArcError` (or any `HttpError`-shaped class); the global
|
|
1433
|
-
* error handler catches them and emits an `ErrorContract`. There is no
|
|
1434
|
-
* `success` discriminator on the response — HTTP status is the wire
|
|
1435
|
-
* discriminator (2xx = data, 4xx/5xx = ErrorContract).
|
|
1436
|
-
*/
|
|
1437
|
-
interface IControllerResponse<T = unknown> {
|
|
1438
|
-
/** Response payload — emitted directly to the wire (no envelope wrap). */
|
|
1439
|
-
data: T;
|
|
1440
|
-
/** HTTP status code. Defaults to 200. */
|
|
1441
|
-
status?: number;
|
|
1442
|
-
/** Custom response headers (e.g. X-Total-Count, Link, ETag). */
|
|
1443
|
-
headers?: Record<string, string>;
|
|
1444
|
-
/** Top-level metadata merged into list-shaped responses (e.g. `{ took }`). */
|
|
1445
|
-
meta?: Record<string, unknown>;
|
|
1446
|
-
}
|
|
1447
|
-
/**
|
|
1448
|
-
* Controller handler — Arc's standard pattern.
|
|
1449
|
-
*
|
|
1450
|
-
* Receives a request context object, returns IControllerResponse.
|
|
1451
|
-
* Use with `handler` in routes.
|
|
1452
|
-
*
|
|
1453
|
-
* **Generic parameters:**
|
|
1454
|
-
* - `TResponse` — shape of `IControllerResponse.data` (default: `unknown`)
|
|
1455
|
-
* - `TBody` — shape of `req.body` (default: `unknown`)
|
|
1456
|
-
* - `TParams` — shape of `req.params` (default: `Record<string, string>`)
|
|
1457
|
-
* - `TQuery` — shape of `req.query` (default: `Record<string, unknown>`)
|
|
1458
|
-
*
|
|
1459
|
-
* Backward-compatible: `ControllerHandler<Product>` still works (only the
|
|
1460
|
-
* response data is typed); add more generics as needed when you want
|
|
1461
|
-
* type-safe access to the request body, params, or query string.
|
|
1462
|
-
*
|
|
1463
|
-
* @example
|
|
1464
|
-
* ```typescript
|
|
1465
|
-
* // Untyped req — body is unknown, must be narrowed
|
|
1466
|
-
* const createProduct: ControllerHandler<Product> = async (req) => {
|
|
1467
|
-
* const product = await productRepo.create(req.body as Partial<Product>);
|
|
1468
|
-
* return { data: product, status: 201 };
|
|
1469
|
-
* };
|
|
1470
|
-
*
|
|
1471
|
-
* // Fully typed — body, params, query, and response all inferred
|
|
1472
|
-
* const updateProduct: ControllerHandler<
|
|
1473
|
-
* Product,
|
|
1474
|
-
* Partial<Product>,
|
|
1475
|
-
* { id: string },
|
|
1476
|
-
* { upsert?: string }
|
|
1477
|
-
* > = async (req) => {
|
|
1478
|
-
* const upsert = req.query.upsert === "true";
|
|
1479
|
-
* const product = await productRepo.update(req.params.id, req.body, { upsert });
|
|
1480
|
-
* return { data: product };
|
|
1481
|
-
* };
|
|
1482
|
-
*
|
|
1483
|
-
* routes: [{
|
|
1484
|
-
* method: 'POST',
|
|
1485
|
-
* path: '/products',
|
|
1486
|
-
* handler: createProduct,
|
|
1487
|
-
* permissions: requireAuth(),
|
|
1488
|
-
* }]
|
|
1489
|
-
* ```
|
|
1490
|
-
*/
|
|
1491
|
-
type ControllerHandler<TResponse = unknown, TBody = unknown, TParams extends Record<string, string> = Record<string, string>, TQuery extends Record<string, unknown> = Record<string, unknown>> = (req: IRequestContext<TBody, TParams, TQuery>) => Promise<IControllerResponse<TResponse>>;
|
|
1492
|
-
/**
|
|
1493
|
-
* Fastify native handler
|
|
1494
|
-
*
|
|
1495
|
-
* Standard Fastify request/reply pattern.
|
|
1496
|
-
* Use with `rawHandler` in routes.
|
|
1497
|
-
*
|
|
1498
|
-
* @example
|
|
1499
|
-
* ```typescript
|
|
1500
|
-
* const downloadFile: FastifyHandler = async (request, reply) => {
|
|
1501
|
-
* const file = await getFile(request.params.id);
|
|
1502
|
-
* reply.header('Content-Type', file.mimeType);
|
|
1503
|
-
* return reply.send(file.buffer);
|
|
1504
|
-
* };
|
|
1505
|
-
*
|
|
1506
|
-
* routes: [{
|
|
1507
|
-
* method: 'GET',
|
|
1508
|
-
* path: '/files/:id/download',
|
|
1509
|
-
* rawHandler: downloadFile,
|
|
1510
|
-
* permissions: requireAuth(),
|
|
1511
|
-
* }]
|
|
1512
|
-
* ```
|
|
1513
|
-
*/
|
|
1514
|
-
type RawRouteHandler = {
|
|
1737
|
+
isRevoked?: (decoded: Record<string, unknown>) => boolean | Promise<boolean>;
|
|
1515
1738
|
/**
|
|
1516
|
-
*
|
|
1739
|
+
* Enforce strict JWT `type` claim validation (default: `true`).
|
|
1517
1740
|
*
|
|
1518
|
-
*
|
|
1519
|
-
*
|
|
1741
|
+
* When enabled, `authenticate` requires `decoded.type === "access"`.
|
|
1742
|
+
* Tokens with a missing or unexpected `type` claim are rejected —
|
|
1743
|
+
* defence in depth for apps that reuse the JWT secret to sign other
|
|
1744
|
+
* token kinds (invite links, one-time verification codes).
|
|
1520
1745
|
*
|
|
1521
|
-
*
|
|
1522
|
-
*
|
|
1523
|
-
* assigns without a cast. Fastify's own `RouteHandlerMethod` is an arrow
|
|
1524
|
-
* alias, so it rejects those — hosts used to write `as any`.
|
|
1525
|
-
* - **One call signature.** TypeScript contextually types parameters only
|
|
1526
|
-
* when a single constituent is callable, so inline
|
|
1527
|
-
* `rawHandler: async (request, reply) => …` infers instead of reporting
|
|
1528
|
-
* TS7006.
|
|
1746
|
+
* Arc's own `issueTokens` always sets `type: "access"` or
|
|
1747
|
+
* `type: "refresh"`, so this default is safe for Arc-generated tokens.
|
|
1529
1748
|
*
|
|
1530
|
-
*
|
|
1749
|
+
* Set to `false` ONLY when you must accept tokens signed without a
|
|
1750
|
+
* `type` claim (e.g. a legacy issuer you don't control). In that mode
|
|
1751
|
+
* Arc still rejects tokens explicitly marked `type: "refresh"`.
|
|
1531
1752
|
*/
|
|
1532
|
-
|
|
1533
|
-
}["handler"];
|
|
1534
|
-
type FastifyHandler<RouteGeneric extends Record<string, unknown> = Record<string, unknown>> = (request: FastifyRequest<RouteGeneric>, reply: FastifyReply) => Promise<unknown> | unknown;
|
|
1535
|
-
/**
|
|
1536
|
-
* Union type for route handlers
|
|
1537
|
-
*/
|
|
1538
|
-
type RouteHandler = ControllerHandler | FastifyHandler;
|
|
1539
|
-
/**
|
|
1540
|
-
* Controller interface for CRUD operations (strict).
|
|
1541
|
-
*
|
|
1542
|
-
* `list`'s return type aligns with repo-core's `MinimalRepo.getAll()`
|
|
1543
|
-
* contract — the kit MAY return an offset envelope, a keyset envelope,
|
|
1544
|
-
* or a raw array. Arc's `BaseController` forwards the kit's response
|
|
1545
|
-
* verbatim; consumers narrow on shape
|
|
1546
|
-
* (`Array.isArray(data)` → bare array, presence of `total` → offset,
|
|
1547
|
-
* presence of `nextCursor` → keyset).
|
|
1548
|
-
*/
|
|
1549
|
-
interface IController<TDoc = unknown> {
|
|
1550
|
-
list(req: IRequestContext): Promise<IControllerResponse<unknown>>;
|
|
1551
|
-
get(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
|
|
1552
|
-
create(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
|
|
1553
|
-
update(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
|
|
1554
|
-
delete(req: IRequestContext): Promise<IControllerResponse<{
|
|
1555
|
-
message: string;
|
|
1556
|
-
}>>;
|
|
1753
|
+
strictTokenType?: boolean;
|
|
1557
1754
|
}
|
|
1755
|
+
//#endregion
|
|
1756
|
+
//#region src/core/middlewares/rateLimit.d.ts
|
|
1558
1757
|
/**
|
|
1559
|
-
*
|
|
1560
|
-
* including class instances with extra methods / private fields.
|
|
1758
|
+
* Per-route CORS override, read by `@fastify/cors` off `routeOptions.config.cors`.
|
|
1561
1759
|
*
|
|
1562
|
-
*
|
|
1563
|
-
*
|
|
1564
|
-
* `
|
|
1565
|
-
*
|
|
1566
|
-
*
|
|
1760
|
+
* The app-wide policy cannot serve both an API and public assets: an API wants
|
|
1761
|
+
* `credentials: true` with a pinned origin list, while a public asset wants
|
|
1762
|
+
* `origin: "*"` — and `*` with credentials is forbidden by the CORS spec (arc
|
|
1763
|
+
* throws at boot on that combination). Per-route is the granularity that
|
|
1764
|
+
* resolves it. `false` disables CORS for the route entirely.
|
|
1567
1765
|
*
|
|
1568
|
-
*
|
|
1569
|
-
*
|
|
1570
|
-
*
|
|
1766
|
+
* Arc does not interpret the value; `@fastify/cors` owns the semantics. Typed
|
|
1767
|
+
* loosely on purpose so a host is never blocked by arc lagging the plugin's
|
|
1768
|
+
* option surface.
|
|
1571
1769
|
*/
|
|
1572
|
-
|
|
1573
|
-
list?: unknown;
|
|
1574
|
-
get?: unknown;
|
|
1575
|
-
create?: unknown;
|
|
1576
|
-
update?: unknown;
|
|
1577
|
-
delete?: unknown;
|
|
1578
|
-
}
|
|
1770
|
+
type RouteCorsConfig = Record<string, unknown> | false;
|
|
1579
1771
|
//#endregion
|
|
1580
1772
|
//#region src/types/resource/rate-limit.d.ts
|
|
1581
1773
|
/**
|
|
@@ -1672,6 +1864,41 @@ interface RouteDefinition {
|
|
|
1672
1864
|
readonly controllerMethod?: (controller: unknown) => ControllerHandler;
|
|
1673
1865
|
/** Permission check — REQUIRED */
|
|
1674
1866
|
readonly permissions: PermissionCheck;
|
|
1867
|
+
/**
|
|
1868
|
+
* PUBLISH this route's gate in the permission matrix, under this key.
|
|
1869
|
+
*
|
|
1870
|
+
* ## Why a route needs to opt in, when CRUD and actions do not
|
|
1871
|
+
*
|
|
1872
|
+
* `introspectRegistry` walks `permissions`, `actions` and `aggregations` —
|
|
1873
|
+
* each of which has a stable NAME to key by. A route has only a method and a
|
|
1874
|
+
* path, and a path is not an identity: `/recompute` becoming
|
|
1875
|
+
* `/classification/recompute` would silently break every client gating on it.
|
|
1876
|
+
* So routes were skipped, and the consequence was not a missing feature but an
|
|
1877
|
+
* invisible one — a resource whose gates live entirely on `routes[]` publishes
|
|
1878
|
+
* NOTHING, is absent from the matrix, and every `can()` against it returns
|
|
1879
|
+
* `false`. A UI that gates on it hides the feature from everyone including
|
|
1880
|
+
* admins, which reads as "my permissions are broken" rather than "this key was
|
|
1881
|
+
* never published".
|
|
1882
|
+
*
|
|
1883
|
+
* Declaring `capability` gives the gate the stable name it was missing. The key
|
|
1884
|
+
* is BARE, sitting alongside `list` / `get` / `create` — a consumer asks
|
|
1885
|
+
* `can('sku-classification', 'recompute')` and does not need to know whether
|
|
1886
|
+
* the verb is implemented as a route, an action or a CRUD slot.
|
|
1887
|
+
*
|
|
1888
|
+
* Omit it for routes nothing needs to gate a UI on (a health check, a public
|
|
1889
|
+
* manifest). The route is still ENFORCED either way — this only controls
|
|
1890
|
+
* whether clients can SEE the decision in advance.
|
|
1891
|
+
*
|
|
1892
|
+
* Collides with a CRUD slot, an `action:`, an `agg:` or another route on the
|
|
1893
|
+
* same resource ⇒ `defineResource()` THROWS at boot. A silent overwrite would
|
|
1894
|
+
* let one gate answer for a different verb, and `introspectRegistry` refuses
|
|
1895
|
+
* the same collision as a backstop for hand-built registry entries.
|
|
1896
|
+
*
|
|
1897
|
+
* @example
|
|
1898
|
+
* { method: 'POST', path: '/recompute', capability: 'recompute',
|
|
1899
|
+
* permissions: requireRoles(['admin']) }
|
|
1900
|
+
*/
|
|
1901
|
+
readonly capability?: string;
|
|
1675
1902
|
/**
|
|
1676
1903
|
* Fastify-native handler — receives `(request, reply)` and owns the response.
|
|
1677
1904
|
* Bypasses arc's pipeline entirely: no `IControllerResponse` shaping, no
|
|
@@ -1811,6 +2038,35 @@ interface RouteDefinition {
|
|
|
1811
2038
|
response?: Record<number | string, unknown>;
|
|
1812
2039
|
[key: string]: unknown;
|
|
1813
2040
|
};
|
|
2041
|
+
/**
|
|
2042
|
+
* Fastify route CONFIG — `reply.routeOptions.config` at request time.
|
|
2043
|
+
*
|
|
2044
|
+
* Fastify's own first-class route option (Reference/Routes.md § Config), passed
|
|
2045
|
+
* straight through. Arc adds no semantics: it is the documented seam every Fastify
|
|
2046
|
+
* plugin uses to take per-route options, and arc forwarding it is what makes those
|
|
2047
|
+
* plugins reachable from a `defineResource` route at all.
|
|
2048
|
+
*
|
|
2049
|
+
* ## Why this is not optional polish
|
|
2050
|
+
*
|
|
2051
|
+
* Arc REGISTERS `fastify-raw-body` with `global: false` (see `registerSecurity`),
|
|
2052
|
+
* which means a route opts in with `config: { rawBody: true }`. Without this field
|
|
2053
|
+
* that opt-in had nowhere to live, so the plugin arc itself installs was
|
|
2054
|
+
* unreachable from any resource — and a route that wrote it got no error, just a
|
|
2055
|
+
* silently absent `request.rawBody`.
|
|
2056
|
+
*
|
|
2057
|
+
* The cost of that was found end-to-end, not by review: every HMAC-signed webhook
|
|
2058
|
+
* verified its signature against a body it never received, so a real provider
|
|
2059
|
+
* callback 401'd in production while every unit test passed.
|
|
2060
|
+
*
|
|
2061
|
+
* @example
|
|
2062
|
+
* // Signature verification needs the exact transmitted bytes.
|
|
2063
|
+
* { method: 'POST', path: '/webhooks', config: { rawBody: true }, rawHandler }
|
|
2064
|
+
*
|
|
2065
|
+
* @example
|
|
2066
|
+
* // Anything a plugin reads per-route — rate-limit overrides, feature flags.
|
|
2067
|
+
* { method: 'GET', path: '/report', config: { rateLimit: { max: 5 } } }
|
|
2068
|
+
*/
|
|
2069
|
+
readonly config?: Record<string, unknown>;
|
|
1814
2070
|
/**
|
|
1815
2071
|
* MCP tool generation:
|
|
1816
2072
|
* - omitted/true: auto-generate (non-raw routes only)
|
|
@@ -2843,6 +3099,44 @@ interface ResourceConfig<TDoc = AnyRecord> {
|
|
|
2843
3099
|
adapter?: DataAdapter<TDoc>;
|
|
2844
3100
|
/** Controller instance — accepts any object with CRUD methods. */
|
|
2845
3101
|
controller?: IController<TDoc> | ControllerLike;
|
|
3102
|
+
/**
|
|
3103
|
+
* Bind a CRUD write slot to a DOMAIN COMMAND, keeping arc's request
|
|
3104
|
+
* pipeline. Arc still sanitizes against the resource's field rules, injects
|
|
3105
|
+
* the tenant field, stamps the actor and runs the hook sandwich — then calls
|
|
3106
|
+
* your verb where it would have called `repository.create/update/delete`.
|
|
3107
|
+
*
|
|
3108
|
+
* Reach for this whenever the kernel behind the resource owns a GUARDED
|
|
3109
|
+
* write (a verb that refuses once a document is posted / closed / locked).
|
|
3110
|
+
* Overriding the controller method instead silently drops the pipeline —
|
|
3111
|
+
* see `ResourceWrites` for the two-way trap this closes.
|
|
3112
|
+
*
|
|
3113
|
+
* @example
|
|
3114
|
+
* ```typescript
|
|
3115
|
+
* writes: {
|
|
3116
|
+
* create: (data, ctx) => engine.invoices.createDraft(data, resolveCtx(ctx.req)),
|
|
3117
|
+
* update: (id, data, ctx) => engine.invoices.updateDraft(id, data, resolveCtx(ctx.req)),
|
|
3118
|
+
* delete: (id, ctx) => engine.invoices.deleteDraft(id, resolveCtx(ctx.req)),
|
|
3119
|
+
* }
|
|
3120
|
+
* ```
|
|
3121
|
+
*/
|
|
3122
|
+
writes?: ResourceWrites<TDoc>;
|
|
3123
|
+
/**
|
|
3124
|
+
* Run every CRUD write (and its declared verb) inside a repository
|
|
3125
|
+
* transaction with TRANSIENT-conflict retry (repo-core
|
|
3126
|
+
* `retryingTransaction`): begin → persistence/verb → commit, re-run on the
|
|
3127
|
+
* kit-classified conflicts only, jittered bounded backoff.
|
|
3128
|
+
*
|
|
3129
|
+
* Contract: before/around hooks run ONCE (outside the retry); the
|
|
3130
|
+
* persistence step may re-run, which is safe by construction when side
|
|
3131
|
+
* effects go through the tx-bound repository (they roll back) or an outbox
|
|
3132
|
+
* row; after-hooks run once, post-commit. A `VersionConflictError`
|
|
3133
|
+
* (`ifVersion` CAS) is NOT transient and surfaces as 409.
|
|
3134
|
+
*
|
|
3135
|
+
* Boot-fatal when the adapter's repository has no `withTransaction`
|
|
3136
|
+
* (capability `transactions: false`) — a requested transactional envelope
|
|
3137
|
+
* that silently degrades is the defect, not a mode.
|
|
3138
|
+
*/
|
|
3139
|
+
transactional?: boolean;
|
|
2846
3140
|
queryParser?: unknown;
|
|
2847
3141
|
permissions?: ResourcePermissions;
|
|
2848
3142
|
schemaOptions?: RouteSchemaOptions;
|
|
@@ -2888,6 +3182,18 @@ interface ResourceConfig<TDoc = AnyRecord> {
|
|
|
2888
3182
|
* pre-2.9 code that relied on the permissive default.
|
|
2889
3183
|
*/
|
|
2890
3184
|
onFieldWriteDenied?: "reject" | "strip";
|
|
3185
|
+
/**
|
|
3186
|
+
* What to do when an UPDATE body carries an `immutable` /
|
|
3187
|
+
* `immutableAfterCreate` field. Default `'strip'`.
|
|
3188
|
+
*
|
|
3189
|
+
* `'reject'` answers 403 instead of 200-with-the-field-unchanged. Kept
|
|
3190
|
+
* separate from `onFieldWriteDenied` (which defaults to `'reject'`) because a
|
|
3191
|
+
* full-object PATCH echoing an immutable field back UNCHANGED is legitimate,
|
|
3192
|
+
* and the sanitizer has no stored document to tell that from a real change.
|
|
3193
|
+
* Hosts sending partial patches should turn it on; `ARC_STRICT_IMMUTABLE_WRITES`
|
|
3194
|
+
* sets it fleet-wide.
|
|
3195
|
+
*/
|
|
3196
|
+
onImmutableWrite?: "reject" | "strip";
|
|
2891
3197
|
middlewares?: MiddlewareConfig;
|
|
2892
3198
|
/**
|
|
2893
3199
|
* PreHandler guards auto-applied to **every** route on this resource
|
|
@@ -3444,6 +3750,22 @@ declare class ResourceDefinition<TDoc = AnyRecord> {
|
|
|
3444
3750
|
* snapshot so callers can't accidentally mutate the diagnostic list.
|
|
3445
3751
|
*/
|
|
3446
3752
|
_diagnostics?: ResourceDiagnostic[];
|
|
3753
|
+
/**
|
|
3754
|
+
* Boot-time assertions that CANNOT be settled at define time because
|
|
3755
|
+
* they read state a connection establishes.
|
|
3756
|
+
*
|
|
3757
|
+
* `defineResource()` frequently runs at module-import time — before
|
|
3758
|
+
* `beforeBoot()` opens the database. A repository's capability
|
|
3759
|
+
* descriptor is therefore not yet authoritative then: mongokit
|
|
3760
|
+
* deliberately reports `transactions: false` for an unresolved topology
|
|
3761
|
+
* (fail-closed), so asserting at define time would reject a correctly
|
|
3762
|
+
* configured app for the crime of importing its resources early.
|
|
3763
|
+
*
|
|
3764
|
+
* `buildResourcePlugin` runs these on first mount — lifecycle slot 5,
|
|
3765
|
+
* after `beforeBoot` (0) and module resolution (0.5) — where the same
|
|
3766
|
+
* fail-closed rule finally means what it says.
|
|
3767
|
+
*/
|
|
3768
|
+
_deferredChecks?: Array<() => void>;
|
|
3447
3769
|
/**
|
|
3448
3770
|
* Public view of {@link _diagnostics} for programmatic consumption.
|
|
3449
3771
|
*
|
|
@@ -4016,6 +4338,13 @@ interface QueryParserInterface {
|
|
|
4016
4338
|
* is not explicitly configured.
|
|
4017
4339
|
*/
|
|
4018
4340
|
allowedFilterFields?: readonly string[];
|
|
4341
|
+
/**
|
|
4342
|
+
* Optional: the parser's own page-size cap.
|
|
4343
|
+
*
|
|
4344
|
+
* A parser that declares one has ANSWERED the question, and `QueryResolver` must
|
|
4345
|
+
* defer to it rather than applying its own default on top — see the note there.
|
|
4346
|
+
*/
|
|
4347
|
+
maxLimit?: number;
|
|
4019
4348
|
/**
|
|
4020
4349
|
* Optional: Allowed filter operators whitelist. Used by MCP to enrich
|
|
4021
4350
|
* list-tool descriptions. Values are human-readable keys: 'eq', 'ne',
|
|
@@ -4062,6 +4391,11 @@ interface ResourceMetadata {
|
|
|
4062
4391
|
summary?: string;
|
|
4063
4392
|
description?: string;
|
|
4064
4393
|
permissions?: PermissionCheck;
|
|
4394
|
+
/**
|
|
4395
|
+
* Matrix key this route's gate is published under. Absent ⇒ enforced but not
|
|
4396
|
+
* advertised — see `capability` on `RouteDefinition`.
|
|
4397
|
+
*/
|
|
4398
|
+
capability?: string;
|
|
4065
4399
|
raw?: boolean;
|
|
4066
4400
|
schema?: unknown;
|
|
4067
4401
|
}>;
|
|
@@ -4295,4 +4629,4 @@ interface ValidateOptions {
|
|
|
4295
4629
|
strict?: boolean;
|
|
4296
4630
|
}
|
|
4297
4631
|
//#endregion
|
|
4298
|
-
export { OpenApiSchemas as $,
|
|
4632
|
+
export { OpenApiSchemas as $, AccessControlConfig as $t, RequestIdOptions as A, WriteVerbKey as An, RouteDefinition as At, defineResource as B, BaseController as Bt, RequestContext as C, ControllerConstructionOptions as Cn, PipelineStep as Ct, HealthCheck as D, MutationWriteContext as Dn, ActionEntry as Dt, GracefulShutdownOptions as E, BodySanitizerConfig as En, ActionDefinition as Et, FastifyWithDecorators as F, IControllerResponse as Fn, AuthPluginOptions as Ft, OnTenantDeleteConfig as G, SlugExt as Gt, CrudController as H, SoftDeleteMixin as Ht, MiddlewareHandler as I, IRequestContext as In, Authenticator as It, PresetHook as J, BulkMixin as Jt, ResolvedTenantPurge as K, SlugMixin as Kt, RequestWithExtras as L, RawRouteHandler as Ln, AuthenticatorContext as Lt, EventsDecorator as M, ControllerLike as Mn, RouteMethod as Mt, FastifyRequestExtras as N, FastifyHandler as Nn, RateLimitConfig as Nt, HealthOptions as O, ResourceWrites as On, ActionHandlerFn as Ot, FastifyWithAuth as P, IController as Pn, AuthHelpers as Pt, MiddlewareConfig as Q, AccessControl as Qt, RegisterOptions as R, RouteHandler as Rn, JwtContext as Rt, QueryParserInterface as S, ControllerConfigurableOptions as Sn, PipelineContext as St, CrudRouterOptions as T, BodySanitizer as Tn, ResourceCacheConfig as Tt, ResourceConfig as U, TreeExt as Ut, ResourceDefinition as V, SoftDeleteExt as Vt, ResourcePermissions as W, TreeMixin as Wt, CrudRouteKey as X, QueryResolver as Xt, PresetResult as Y, BaseCrudController as Yt, CrudSchemas as Z, QueryResolverConfig as Zt, ControllerQueryOptions as _, ArcDeleteResult as _n, Guard as _t, InferAdapterDoc as a, HookRegistration as an, ResourceExtensions as at, ParsedQuery as b, ArcUpdateResult as bn, OperationFilter as bt, TypedController as c, afterCreate as cn, AggMeasureShorthand as ct, PaginationResult as d, beforeCreate as dn, AggregationDateRangeRequirement as dt, DefineHookOptions as en, RouteSchemaOptions as et, IntrospectionData as f, beforeDelete as fn, AggregationIndexHint as ft, ArcInternalMetadata as g, ArcCreateResult as gn, AggregationsMap as gt, ResourceMetadata as h, defineHook as hn, AggregationRateLimit as ht, ValidationResult as i, HookPhase as in, ResourceHooks as it, ArcDecorator as j, ControllerHandler as jn, RouteMcpConfig as jt, IntrospectionPluginOptions as k, WriteContext as kn, ActionsMap as kt, TypedRepository as l, afterDelete as ln, AggregationCacheConfig as lt, RegistryStats as m, createHookSystem as mn, AggregationMaterializedResult as mt, ConfigError as n, HookHandler as nn, FieldRule$1 as nt, InferDocType as o, HookSystem as on, EventDefinition as ot, RegistryEntry as p, beforeUpdate as pn, AggregationMaterializedContext as pt, PresetFunction as q, BulkExt as qt, ValidateOptions as r, HookOperation as rn, ResourceHookContext as rt, InferResourceDoc as s, HookSystemOptions as sn, AggMeasureInput as st, RouteHandlerMethod$1 as t, HookContext as tn, ArcFieldRule as tt, TypedResourceConfig as u, afterUpdate as un, AggregationConfig as ut, LookupOption as v, ArcGetResult as vn, Interceptor as vt, ServiceContext as w, ListResult as wn, Transform as wt, PopulateOption as x, BaseControllerOptions as xn, PipelineConfig as xt, OwnershipCheck as y, ArcListResult as yn, NextFunction as yt, ResourceRegistry as z, ServerAccessor as zn, TokenPair as zt };
|