@classytic/arc 2.10.3 → 2.11.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/dist/{BaseController-CbKKIflT.mjs → BaseController-JNV08qOT.mjs} +595 -537
- package/dist/{queryCachePlugin-BKbWjgDG.d.mts → QueryCache-DOBNHBE0.d.mts} +2 -32
- package/dist/actionPermissions-C8YYU92K.mjs +22 -0
- package/dist/adapters/index.d.mts +2 -2
- package/dist/adapters/index.mjs +1 -1
- package/dist/{adapters-BXY4i-hw.mjs → adapters-D0tT2Tyo.mjs} +54 -0
- package/dist/audit/index.d.mts +2 -2
- package/dist/audit/index.mjs +15 -17
- package/dist/auth/index.d.mts +4 -4
- package/dist/auth/index.mjs +3 -3
- package/dist/auth/redis-session.d.mts +1 -1
- package/dist/{betterAuthOpenApi-BBRVhjQN.mjs → betterAuthOpenApi-DwxtK3uG.mjs} +1 -1
- package/dist/cache/index.d.mts +3 -2
- package/dist/cache/index.mjs +3 -3
- package/dist/cli/commands/docs.mjs +2 -2
- package/dist/cli/commands/generate.mjs +37 -27
- package/dist/cli/commands/init.mjs +47 -34
- package/dist/cli/commands/introspect.mjs +1 -1
- package/dist/context/index.d.mts +58 -0
- package/dist/context/index.mjs +2 -0
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +4 -3
- package/dist/core-DXdSSFW-.mjs +1037 -0
- package/dist/createActionRouter-BwaSM0No.mjs +166 -0
- package/dist/{createApp-BuvPma24.mjs → createApp-DvNYEhpb.mjs} +118 -36
- package/dist/docs/index.d.mts +2 -2
- package/dist/docs/index.mjs +1 -1
- package/dist/{elevation-C7hgL_aI.mjs → elevation-DOFoxoDs.mjs} +1 -1
- package/dist/errorHandler-Co3lnVmJ.d.mts +114 -0
- package/dist/{eventPlugin-DCUjuiQT.mjs → eventPlugin--5HIkdPU.mjs} +1 -1
- package/dist/{eventPlugin-CxWgpd6K.d.mts → eventPlugin-CUNjYYRY.d.mts} +1 -1
- package/dist/events/index.d.mts +4 -4
- package/dist/events/index.mjs +69 -51
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/factory/index.d.mts +1 -1
- package/dist/factory/index.mjs +2 -2
- package/dist/{fields-Lo1VUDpt.d.mts → fields-C8Y0XLAu.d.mts} +1 -1
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/idempotency/index.d.mts +3 -3
- package/dist/idempotency/index.mjs +38 -27
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/{index-ChIw3776.d.mts → index-BYCqHCVu.d.mts} +4 -4
- package/dist/{index-Cl0uoKd5.d.mts → index-Cm0vUrr_.d.mts} +2100 -1688
- package/dist/{index-DStwgFUK.d.mts → index-DAushRTt.d.mts} +29 -10
- package/dist/index-DsJ1MNfC.d.mts +1179 -0
- package/dist/{index-8qw4y6ff.d.mts → index-t8pLpPFW.d.mts} +13 -10
- package/dist/index.d.mts +7 -251
- package/dist/index.mjs +8 -128
- package/dist/integrations/event-gateway.d.mts +2 -2
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +2 -2
- package/dist/integrations/mcp/index.d.mts +2 -2
- package/dist/integrations/mcp/index.mjs +1 -1
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/streamline.d.mts +46 -5
- package/dist/integrations/streamline.mjs +50 -21
- package/dist/integrations/websocket-redis.d.mts +1 -1
- package/dist/integrations/websocket.d.mts +2 -154
- package/dist/integrations/websocket.mjs +292 -224
- package/dist/{keys-qcD-TVJl.mjs → keys-CARyUjiR.mjs} +2 -0
- package/dist/{loadResources-BAzJItAJ.mjs → loadResources-YNwKHvRA.mjs} +3 -1
- package/dist/logger/index.d.mts +81 -0
- package/dist/{logger-DLg8-Ueg.mjs → logger/index.mjs} +1 -6
- package/dist/middleware/index.d.mts +109 -0
- package/dist/middleware/index.mjs +70 -0
- package/dist/multipartBody-CvTR1Un6.mjs +123 -0
- package/dist/{openapi-B5F8AddX.mjs → openapi-C0L9ar7m.mjs} +9 -7
- package/dist/org/index.d.mts +2 -2
- package/dist/permissions/index.d.mts +2 -2
- package/dist/permissions/index.mjs +1 -3
- package/dist/{permissions-Dk6mshja.mjs → permissions-B4vU9L0Q.mjs} +220 -2
- package/dist/pipe-DVoIheVC.mjs +62 -0
- package/dist/pipeline/index.d.mts +62 -0
- package/dist/pipeline/index.mjs +53 -0
- package/dist/plugins/index.d.mts +25 -5
- package/dist/plugins/index.mjs +10 -10
- package/dist/plugins/response-cache.mjs +1 -1
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +42 -24
- package/dist/presets/filesUpload.d.mts +4 -4
- package/dist/presets/filesUpload.mjs +255 -1
- package/dist/presets/index.d.mts +1 -1
- package/dist/presets/index.mjs +2 -2
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +48 -8
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-fLJVXdVn.mjs → presets-k604Lj99.mjs} +1 -1
- package/dist/queryCachePlugin-BUXBSm4F.d.mts +34 -0
- package/dist/{queryCachePlugin-DQCEfJis.mjs → queryCachePlugin-Bq6bO6vc.mjs} +3 -3
- package/dist/{redis-DqyeggCa.d.mts → redis-Cm1gnRDf.d.mts} +1 -1
- package/dist/{redis-stream-CakIQmwR.d.mts → redis-stream-CM8TXTix.d.mts} +1 -1
- package/dist/registry/index.d.mts +1 -1
- package/dist/registry/index.mjs +2 -2
- package/dist/{requestContext-xHIKedG6.mjs → requestContext-CfRkaxwf.mjs} +1 -1
- package/dist/{resourceToTools-BElv3xPT.mjs → resourceToTools--okX6QBr.mjs} +534 -415
- package/dist/routerShared-DeESFp4a.mjs +515 -0
- package/dist/schemaIR-BlG9bY7v.mjs +137 -0
- package/dist/scope/index.d.mts +2 -2
- package/dist/scope/index.mjs +1 -1
- package/dist/{sse-yBCgOLGu.mjs → sse-V7aXc3bW.mjs} +1 -1
- package/dist/{store-helpers-ZCSMJJAX.mjs → store-helpers-BhrzxvyQ.mjs} +4 -0
- package/dist/testing/index.d.mts +367 -711
- package/dist/testing/index.mjs +646 -1434
- package/dist/testing/storageContract.d.mts +1 -1
- package/dist/{tracing-65B51Dw3.d.mts → tracing-DokiEsuz.d.mts} +9 -4
- package/dist/types/index.d.mts +5 -5
- package/dist/types/index.mjs +1 -3
- package/dist/types/storage.d.mts +1 -1
- package/dist/{types-Co8k3NyS.d.mts → types-CgikqKAj.d.mts} +133 -21
- package/dist/{types-Btdda02s.d.mts → types-D9NqiYIw.d.mts} +1 -1
- package/dist/utils/index.d.mts +2 -898
- package/dist/utils/index.mjs +4 -5
- package/dist/utils-D3Yxnrwr.mjs +1639 -0
- package/dist/versioning-M9lNLhO8.d.mts +117 -0
- package/dist/websocket-CyJ1VIFI.d.mts +186 -0
- package/package.json +26 -8
- package/skills/arc/SKILL.md +124 -39
- package/skills/arc/references/testing.md +212 -183
- package/dist/applyPermissionResult-QhV1Pa-g.mjs +0 -37
- package/dist/core-CcR01lup.mjs +0 -1411
- package/dist/createActionRouter-Bp_5c_2b.mjs +0 -249
- package/dist/errorHandler-DRQ3EqfL.d.mts +0 -218
- package/dist/errors-CCSsMpXE.d.mts +0 -140
- package/dist/fields-bxkeltzz.mjs +0 -126
- package/dist/filesUpload-t21LS-py.mjs +0 -377
- package/dist/queryParser-DBqBB6AC.mjs +0 -352
- package/dist/types-Csi3FLfq.mjs +0 -27
- package/dist/utils-B2fNOD_i.mjs +0 -929
- /package/dist/{EventTransport-CUw5NNWe.d.mts → EventTransport-CfVEGaEl.d.mts} +0 -0
- /package/dist/{HookSystem-BNYKnrXF.mjs → HookSystem-CGsMd6oK.mjs} +0 -0
- /package/dist/{ResourceRegistry-BPd6NQDm.mjs → ResourceRegistry-DkAeAuTX.mjs} +0 -0
- /package/dist/{caching-CBpK_SCM.mjs → caching-CheW3m-S.mjs} +0 -0
- /package/dist/{elevation-C5SwtkAn.d.mts → elevation-s5ykdNHr.d.mts} +0 -0
- /package/dist/{errorHandler-Bb49BvPD.mjs → errorHandler-BQm8ZxTK.mjs} +0 -0
- /package/dist/{externalPaths-BQ8QijNH.d.mts → externalPaths-Bapitwvd.d.mts} +0 -0
- /package/dist/{interface-CSbZdv_3.d.mts → interface-CkkWm5uR.d.mts} +0 -0
- /package/dist/{interface-D218ikEo.d.mts → interface-Da0r7Lna.d.mts} +0 -0
- /package/dist/{memory-B5Amv9A1.mjs → memory-DikHSvWa.mjs} +0 -0
- /package/dist/{metrics-DuhiSEZI.mjs → metrics-Csh4nsvv.mjs} +0 -0
- /package/dist/{pluralize-A0tWEl1K.mjs → pluralize-BneOJkpi.mjs} +0 -0
- /package/dist/{registry-B3lRFBWo.mjs → registry-D63ee7fl.mjs} +0 -0
- /package/dist/{replyHelpers-CXtJDAZ0.mjs → replyHelpers-ByllIXXV.mjs} +0 -0
- /package/dist/{schemaConverter-BxFDdtXu.mjs → schemaConverter-B0oKLuqI.mjs} +0 -0
- /package/dist/{sessionManager-BkzVU8h2.d.mts → sessionManager-D-oNWHz3.d.mts} +0 -0
- /package/dist/{storage-CVk_SEn2.d.mts → storage-BwGQXUpd.d.mts} +0 -0
- /package/dist/{typeGuards-Cj5Rgvlg.mjs → typeGuards-CcFZXgU7.mjs} +0 -0
- /package/dist/{types-BD85MlEK.d.mts → types-tgR4Pt8F.d.mts} +0 -0
- /package/dist/{versioning-C2U_bLY0.mjs → versioning-CGPjkqAg.mjs} +0 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { I as MiddlewareHandler, L as RequestWithExtras, Q as MiddlewareConfig } from "../index-Cm0vUrr_.mjs";
|
|
2
|
+
import { RouteHandlerMethod } from "fastify";
|
|
3
|
+
|
|
4
|
+
//#region src/middleware/middleware.d.ts
|
|
5
|
+
interface NamedMiddleware {
|
|
6
|
+
/** Unique name for debugging/introspection */
|
|
7
|
+
readonly name: string;
|
|
8
|
+
/** Operations this middleware applies to (default: all) */
|
|
9
|
+
readonly operations?: Array<"list" | "get" | "create" | "update" | "delete" | string>;
|
|
10
|
+
/** Priority — lower numbers run first (default: 10) */
|
|
11
|
+
readonly priority: number;
|
|
12
|
+
/** Conditional execution — return true to run, false to skip */
|
|
13
|
+
readonly when?: (request: RequestWithExtras) => boolean | Promise<boolean>;
|
|
14
|
+
/** The middleware handler */
|
|
15
|
+
readonly handler: MiddlewareHandler;
|
|
16
|
+
}
|
|
17
|
+
interface MiddlewareOptions {
|
|
18
|
+
operations?: NamedMiddleware["operations"];
|
|
19
|
+
priority?: number;
|
|
20
|
+
when?: NamedMiddleware["when"];
|
|
21
|
+
handler: MiddlewareHandler;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Create a named middleware with priority and conditions.
|
|
25
|
+
*/
|
|
26
|
+
declare function middleware(name: string, options: MiddlewareOptions): NamedMiddleware;
|
|
27
|
+
/**
|
|
28
|
+
* Sort named middlewares by priority (ascending — lower runs first).
|
|
29
|
+
* Returns a MiddlewareConfig map keyed by operation, ready to pass to `defineResource()`.
|
|
30
|
+
*/
|
|
31
|
+
declare function sortMiddlewares(middlewares: NamedMiddleware[]): MiddlewareConfig;
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/middleware/multipartBody.d.ts
|
|
34
|
+
/** Parsed file from multipart form-data */
|
|
35
|
+
interface ParsedFile {
|
|
36
|
+
/** Original filename */
|
|
37
|
+
filename: string;
|
|
38
|
+
/** MIME type */
|
|
39
|
+
mimetype: string;
|
|
40
|
+
/** File contents as Buffer */
|
|
41
|
+
buffer: Buffer;
|
|
42
|
+
/** File size in bytes */
|
|
43
|
+
size: number;
|
|
44
|
+
/** Form field name */
|
|
45
|
+
fieldname: string;
|
|
46
|
+
}
|
|
47
|
+
interface MultipartBodyOptions {
|
|
48
|
+
/**
|
|
49
|
+
* Maximum file size in bytes (default: 10MB).
|
|
50
|
+
* Files exceeding this are rejected with 413.
|
|
51
|
+
*/
|
|
52
|
+
maxFileSize?: number;
|
|
53
|
+
/**
|
|
54
|
+
* Maximum number of files (default: 5).
|
|
55
|
+
* Extra files are silently ignored.
|
|
56
|
+
*/
|
|
57
|
+
maxFiles?: number;
|
|
58
|
+
/**
|
|
59
|
+
* Allowed MIME types (default: all).
|
|
60
|
+
* Files with disallowed types are rejected with 415.
|
|
61
|
+
*
|
|
62
|
+
* Supports three forms in a single list:
|
|
63
|
+
* - Exact: `image/png`
|
|
64
|
+
* - Subtype wildcard: `image/\*` — any `image/…`
|
|
65
|
+
* - Any: `\*` or `\*\/\*` — equivalent to omitting the option
|
|
66
|
+
*
|
|
67
|
+
* @example ['image/jpeg', 'image/png', 'application/pdf']
|
|
68
|
+
* @example ['image/*', 'application/pdf']
|
|
69
|
+
* @example ['*'] // accept any type explicitly
|
|
70
|
+
*/
|
|
71
|
+
allowedMimeTypes?: string[];
|
|
72
|
+
/**
|
|
73
|
+
* Key on `req.body` where parsed files are attached (default: '_files').
|
|
74
|
+
* Set to a custom key if '_files' conflicts with your schema.
|
|
75
|
+
*
|
|
76
|
+
* Note: this is the **destination** key — it controls where parsed files
|
|
77
|
+
* land on `req.body`, not which form fields are required. To enforce that
|
|
78
|
+
* a specific file field must be present in the request, use `requiredFields`.
|
|
79
|
+
*/
|
|
80
|
+
filesKey?: string;
|
|
81
|
+
/**
|
|
82
|
+
* Multipart form field names that MUST be present in the request.
|
|
83
|
+
* Returns 400 with `{ success: false, error, code: 'MISSING_FILE_FIELDS' }`
|
|
84
|
+
* when any listed field is absent from the uploaded files.
|
|
85
|
+
*
|
|
86
|
+
* Only enforced when the request IS multipart — JSON requests still pass
|
|
87
|
+
* through as no-ops so the same middleware stays safe to add to shared
|
|
88
|
+
* create/update routes that accept both content types.
|
|
89
|
+
*
|
|
90
|
+
* @example ['file'] // single-field upload (OCR, classify)
|
|
91
|
+
* @example ['avatar', 'cover'] // multi-field upload (profile editor)
|
|
92
|
+
*/
|
|
93
|
+
requiredFields?: string[];
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Create a multipart body parsing middleware.
|
|
97
|
+
*
|
|
98
|
+
* When a request has `content-type: multipart/form-data`, this middleware:
|
|
99
|
+
* 1. Reads all parts (fields + files)
|
|
100
|
+
* 2. Sets text fields on `req.body` as a plain object
|
|
101
|
+
* 3. Attaches file buffers to `req.body[filesKey]` (default: `req.body._files`)
|
|
102
|
+
*
|
|
103
|
+
* For non-multipart requests (regular JSON), this is a no-op — the request
|
|
104
|
+
* passes through unchanged. This makes it safe to add to create/update
|
|
105
|
+
* middlewares without breaking JSON clients.
|
|
106
|
+
*/
|
|
107
|
+
declare function multipartBody(options?: MultipartBodyOptions): RouteHandlerMethod;
|
|
108
|
+
//#endregion
|
|
109
|
+
export { type MultipartBodyOptions, type NamedMiddleware, type ParsedFile, middleware, multipartBody, sortMiddlewares };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { t as CRUD_OPERATIONS } from "../constants-BhY1OHoH.mjs";
|
|
2
|
+
import { t as multipartBody } from "../multipartBody-CvTR1Un6.mjs";
|
|
3
|
+
//#region src/middleware/middleware.ts
|
|
4
|
+
/**
|
|
5
|
+
* Named Middleware — Priority-based, conditional middleware execution.
|
|
6
|
+
*
|
|
7
|
+
* Named middleware replaces flat arrays with structured, inspectable middleware
|
|
8
|
+
* that runs in priority order and supports conditional execution.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { middleware } from '@classytic/arc/middleware';
|
|
13
|
+
*
|
|
14
|
+
* const verifyEmail = middleware('verifyEmail', {
|
|
15
|
+
* operations: ['create', 'update'],
|
|
16
|
+
* priority: 5,
|
|
17
|
+
* when: (req) => !req.user?.emailVerified,
|
|
18
|
+
* handler: async (req, reply) => {
|
|
19
|
+
* reply.code(403).send({ error: 'Email verification required' });
|
|
20
|
+
* },
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* const rateLimit = middleware('rateLimit', {
|
|
24
|
+
* priority: 1,
|
|
25
|
+
* handler: async (req, reply) => {
|
|
26
|
+
* // rate limit logic
|
|
27
|
+
* },
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* const productResource = defineResource({
|
|
31
|
+
* name: 'product',
|
|
32
|
+
* adapter,
|
|
33
|
+
* middlewares: sortMiddlewares([verifyEmail, rateLimit]),
|
|
34
|
+
* });
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* Create a named middleware with priority and conditions.
|
|
39
|
+
*/
|
|
40
|
+
function middleware(name, options) {
|
|
41
|
+
return {
|
|
42
|
+
name,
|
|
43
|
+
operations: options.operations,
|
|
44
|
+
priority: options.priority ?? 10,
|
|
45
|
+
when: options.when,
|
|
46
|
+
handler: options.handler
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Sort named middlewares by priority (ascending — lower runs first).
|
|
51
|
+
* Returns a MiddlewareConfig map keyed by operation, ready to pass to `defineResource()`.
|
|
52
|
+
*/
|
|
53
|
+
function sortMiddlewares(middlewares) {
|
|
54
|
+
const sorted = [...middlewares].sort((a, b) => a.priority - b.priority);
|
|
55
|
+
const operations = CRUD_OPERATIONS;
|
|
56
|
+
const result = {};
|
|
57
|
+
for (const op of operations) {
|
|
58
|
+
const applicable = sorted.filter((m) => !m.operations || m.operations.length === 0 || m.operations.includes(op));
|
|
59
|
+
if (applicable.length > 0) result[op] = applicable.map((m) => {
|
|
60
|
+
if (!m.when) return m.handler;
|
|
61
|
+
const wrapped = async (request, reply) => {
|
|
62
|
+
if (await m.when?.(request)) return m.handler(request, reply);
|
|
63
|
+
};
|
|
64
|
+
return wrapped;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
export { middleware, multipartBody, sortMiddlewares };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
//#region src/middleware/multipartBody.ts
|
|
2
|
+
const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
3
|
+
const DEFAULT_MAX_FILES = 5;
|
|
4
|
+
const DEFAULT_FILES_KEY = "_files";
|
|
5
|
+
/**
|
|
6
|
+
* Build a matcher for MIME allow-lists that supports exact (e.g. `image/png`),
|
|
7
|
+
* subtype wildcards (e.g. `image/\*`), and total wildcards (`\*` or `\*\/\*`).
|
|
8
|
+
*
|
|
9
|
+
* Returns `undefined` when no filter is needed — either because the option
|
|
10
|
+
* was omitted or because a total wildcard was present.
|
|
11
|
+
*/
|
|
12
|
+
function buildMimeMatcher(allowed) {
|
|
13
|
+
if (!allowed || allowed.length === 0) return void 0;
|
|
14
|
+
const exact = /* @__PURE__ */ new Set();
|
|
15
|
+
const prefixes = [];
|
|
16
|
+
for (const entry of allowed) {
|
|
17
|
+
const value = entry.trim().toLowerCase();
|
|
18
|
+
if (!value) continue;
|
|
19
|
+
if (value === "*" || value === "*/*") return void 0;
|
|
20
|
+
if (value.endsWith("/*")) prefixes.push(value.slice(0, -1));
|
|
21
|
+
else exact.add(value);
|
|
22
|
+
}
|
|
23
|
+
if (exact.size === 0 && prefixes.length === 0) return void 0;
|
|
24
|
+
return {
|
|
25
|
+
matches(mime) {
|
|
26
|
+
const m = mime.toLowerCase();
|
|
27
|
+
if (exact.has(m)) return true;
|
|
28
|
+
for (const p of prefixes) if (m.startsWith(p)) return true;
|
|
29
|
+
return false;
|
|
30
|
+
},
|
|
31
|
+
describe() {
|
|
32
|
+
return [...exact, ...prefixes.map((p) => `${p}*`)].join(", ");
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Create a multipart body parsing middleware.
|
|
38
|
+
*
|
|
39
|
+
* When a request has `content-type: multipart/form-data`, this middleware:
|
|
40
|
+
* 1. Reads all parts (fields + files)
|
|
41
|
+
* 2. Sets text fields on `req.body` as a plain object
|
|
42
|
+
* 3. Attaches file buffers to `req.body[filesKey]` (default: `req.body._files`)
|
|
43
|
+
*
|
|
44
|
+
* For non-multipart requests (regular JSON), this is a no-op — the request
|
|
45
|
+
* passes through unchanged. This makes it safe to add to create/update
|
|
46
|
+
* middlewares without breaking JSON clients.
|
|
47
|
+
*/
|
|
48
|
+
function multipartBody(options = {}) {
|
|
49
|
+
const maxFileSize = options.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
|
|
50
|
+
const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
|
|
51
|
+
const mimeMatcher = buildMimeMatcher(options.allowedMimeTypes);
|
|
52
|
+
const filesKey = options.filesKey ?? DEFAULT_FILES_KEY;
|
|
53
|
+
const requiredFields = options.requiredFields && options.requiredFields.length > 0 ? options.requiredFields : void 0;
|
|
54
|
+
return async function parseMultipartBody(request, reply) {
|
|
55
|
+
if (!(request.headers["content-type"] ?? "").includes("multipart/form-data")) return;
|
|
56
|
+
if (typeof request.parts !== "function") {
|
|
57
|
+
request.log.warn("multipartBody middleware: @fastify/multipart not registered. Ensure createApp() has multipart enabled (default) or install @fastify/multipart.");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const body = {};
|
|
61
|
+
const files = {};
|
|
62
|
+
let fileCount = 0;
|
|
63
|
+
try {
|
|
64
|
+
const parts = request.parts();
|
|
65
|
+
for await (const part of parts) if (part.type === "file") {
|
|
66
|
+
if (fileCount >= maxFiles) continue;
|
|
67
|
+
if (mimeMatcher && !mimeMatcher.matches(part.mimetype)) return reply.code(415).send({
|
|
68
|
+
success: false,
|
|
69
|
+
error: `File type '${part.mimetype}' not allowed. Accepted: ${mimeMatcher.describe()}`
|
|
70
|
+
});
|
|
71
|
+
const buffer = await part.toBuffer();
|
|
72
|
+
if (buffer.length > maxFileSize) return reply.code(413).send({
|
|
73
|
+
success: false,
|
|
74
|
+
error: `File '${part.filename}' exceeds maximum size of ${Math.round(maxFileSize / 1024 / 1024)}MB`
|
|
75
|
+
});
|
|
76
|
+
files[part.fieldname] = {
|
|
77
|
+
filename: part.filename,
|
|
78
|
+
mimetype: part.mimetype,
|
|
79
|
+
buffer,
|
|
80
|
+
size: buffer.length,
|
|
81
|
+
fieldname: part.fieldname
|
|
82
|
+
};
|
|
83
|
+
fileCount++;
|
|
84
|
+
} else body[part.fieldname] = tryParseValue(part.value);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
request.log.error({ err }, "multipartBody: failed to parse multipart form");
|
|
87
|
+
return reply.code(400).send({
|
|
88
|
+
success: false,
|
|
89
|
+
error: "Failed to parse multipart form data"
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
if (requiredFields) {
|
|
93
|
+
const missing = requiredFields.filter((name) => !(name in files));
|
|
94
|
+
if (missing.length > 0) return reply.code(400).send({
|
|
95
|
+
success: false,
|
|
96
|
+
error: `Missing required file field${missing.length > 1 ? "s" : ""}: ${missing.join(", ")}`,
|
|
97
|
+
code: "MISSING_FILE_FIELDS",
|
|
98
|
+
details: { missing }
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (fileCount > 0) body[filesKey] = files;
|
|
102
|
+
request.body = body;
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Try to parse a form field value as JSON, number, or boolean.
|
|
107
|
+
* Falls back to the raw string if parsing fails.
|
|
108
|
+
*/
|
|
109
|
+
function tryParseValue(value) {
|
|
110
|
+
if (value === "true") return true;
|
|
111
|
+
if (value === "false") return false;
|
|
112
|
+
if (value === "null") return null;
|
|
113
|
+
if (/^-?\d+(\.\d+)?$/.test(value) && value.length < 16) {
|
|
114
|
+
const num = Number(value);
|
|
115
|
+
if (Number.isFinite(num)) return num;
|
|
116
|
+
}
|
|
117
|
+
if (value.startsWith("{") && value.endsWith("}") || value.startsWith("[") && value.endsWith("]")) try {
|
|
118
|
+
return JSON.parse(value);
|
|
119
|
+
} catch {}
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
122
|
+
//#endregion
|
|
123
|
+
export { multipartBody as t };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { t as getUserRoles } from "./types-DV9WDfeg.mjs";
|
|
2
|
-
import { n as convertRouteSchema } from "./schemaConverter-
|
|
3
|
-
import { t as
|
|
2
|
+
import { n as convertRouteSchema } from "./schemaConverter-B0oKLuqI.mjs";
|
|
3
|
+
import { t as resolveActionPermission } from "./actionPermissions-C8YYU92K.mjs";
|
|
4
|
+
import { t as buildActionBodySchema } from "./createActionRouter-BwaSM0No.mjs";
|
|
4
5
|
import fp from "fastify-plugin";
|
|
5
6
|
//#region src/docs/openapi.ts
|
|
6
7
|
const openApiPlugin = async (fastify, opts = {}) => {
|
|
@@ -327,12 +328,13 @@ function generateResourcePaths(resource, apiPrefix = "", additionalSecurity = []
|
|
|
327
328
|
const descStr = a.description ? ` — ${a.description}` : "";
|
|
328
329
|
descLines.push(`- \`${a.name}\`${roleStr}${descStr}`);
|
|
329
330
|
}
|
|
330
|
-
const fallbackPerm = resource.actionPermissions;
|
|
331
|
-
const fallbackRequiresAuth = typeof fallbackPerm === "function" && !fallbackPerm._isPublic;
|
|
332
331
|
const anyAuthRequired = resource.actions.some((a) => {
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
332
|
+
const effective = resolveActionPermission({
|
|
333
|
+
action: { permissions: a.permissions },
|
|
334
|
+
resourcePermissions: resource.permissions,
|
|
335
|
+
resourceActionPermissions: resource.actionPermissions
|
|
336
|
+
});
|
|
337
|
+
return typeof effective === "function" && !effective._isPublic;
|
|
336
338
|
});
|
|
337
339
|
if (!paths[actionPath]) paths[actionPath] = {};
|
|
338
340
|
paths[actionPath].post = createOperation(resource, "action", `Perform action (${actionEnum.join(" / ")})`, {
|
package/dist/org/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { yt as RouteHandler } from "../index-Cm0vUrr_.mjs";
|
|
2
|
+
import { d as UserBase } from "../fields-C8Y0XLAu.mjs";
|
|
3
3
|
import { InvitationAdapter, InvitationDoc, MemberDoc, OrgAdapter, OrgDoc, OrgPermissionStatement, OrgRole, OrganizationPluginOptions } from "./types.mjs";
|
|
4
4
|
import { FastifyPluginAsync, RouteHandlerMethod } from "fastify";
|
|
5
5
|
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as applyFieldWritePermissions, c as PermissionCheck, d as UserBase, f as getUserRoles, i as applyFieldReadPermissions, l as PermissionContext, n as FieldPermissionMap, o as fields, p as normalizeRoles, r as FieldPermissionType, s as resolveEffectiveRoles, t as FieldPermission, u as PermissionResult } from "../fields-
|
|
2
|
-
import { A as requireRoles, C as allOf, D as not, E as denyAll, M as when, N as applyPermissionResult, O as requireAuth, P as normalizePermissionResult, S as createOrgPermissions, T as anyOf, _ as ConnectEventsOptions, a as presets_d_exports, b as PermissionEventBus, c as readOnly, d as requireOrgRole, f as requireScopeContext, g as createRoleHierarchy, h as RoleHierarchy, i as ownerWithAdminBypass, j as roles, k as requireOwnership, l as requireOrgInScope, m as requireTeamMembership, n as authenticated, o as publicRead, p as requireServiceScope, r as fullPublic, s as publicReadAdminWrite, t as adminOnly, u as requireOrgMembership, v as DynamicPermissionMatrix, w as allowPublic, x as createDynamicPermissionMatrix, y as DynamicPermissionMatrixConfig } from "../index-
|
|
1
|
+
import { a as applyFieldWritePermissions, c as PermissionCheck, d as UserBase, f as getUserRoles, i as applyFieldReadPermissions, l as PermissionContext, n as FieldPermissionMap, o as fields, p as normalizeRoles, r as FieldPermissionType, s as resolveEffectiveRoles, t as FieldPermission, u as PermissionResult } from "../fields-C8Y0XLAu.mjs";
|
|
2
|
+
import { A as requireRoles, C as allOf, D as not, E as denyAll, M as when, N as applyPermissionResult, O as requireAuth, P as normalizePermissionResult, S as createOrgPermissions, T as anyOf, _ as ConnectEventsOptions, a as presets_d_exports, b as PermissionEventBus, c as readOnly, d as requireOrgRole, f as requireScopeContext, g as createRoleHierarchy, h as RoleHierarchy, i as ownerWithAdminBypass, j as roles, k as requireOwnership, l as requireOrgInScope, m as requireTeamMembership, n as authenticated, o as publicRead, p as requireServiceScope, r as fullPublic, s as publicReadAdminWrite, t as adminOnly, u as requireOrgMembership, v as DynamicPermissionMatrix, w as allowPublic, x as createDynamicPermissionMatrix, y as DynamicPermissionMatrixConfig } from "../index-BYCqHCVu.mjs";
|
|
3
3
|
export { ConnectEventsOptions, DynamicPermissionMatrix, DynamicPermissionMatrixConfig, FieldPermission, FieldPermissionMap, FieldPermissionType, PermissionCheck, PermissionContext, PermissionEventBus, PermissionResult, RoleHierarchy, UserBase, adminOnly, allOf, allowPublic, anyOf, applyFieldReadPermissions, applyFieldWritePermissions, applyPermissionResult, authenticated, createDynamicPermissionMatrix, createOrgPermissions, createRoleHierarchy, denyAll, fields, fullPublic, getUserRoles, normalizePermissionResult, normalizeRoles, not, ownerWithAdminBypass, presets_d_exports as permissions, publicRead, publicReadAdminWrite, readOnly, requireAuth, requireOrgInScope, requireOrgMembership, requireOrgRole, requireOwnership, requireRoles, requireScopeContext, requireServiceScope, requireTeamMembership, resolveEffectiveRoles, roles, when };
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { A as normalizePermissionResult, C as requireAuth, D as when, E as roles, M as applyFieldWritePermissions, N as fields, O as applyPermissionResult, P as resolveEffectiveRoles, S as not, T as requireRoles, _ as requireTeamMembership, a as presets_exports, b as anyOf, c as readOnly, d as createOrgPermissions, f as requireOrgInScope, g as requireServiceScope, h as requireScopeContext, i as ownerWithAdminBypass, j as applyFieldReadPermissions, l as createRoleHierarchy, m as requireOrgRole, n as authenticated, o as publicRead, p as requireOrgMembership, r as fullPublic, s as publicReadAdminWrite, t as adminOnly, u as createDynamicPermissionMatrix, v as allOf, w as requireOwnership, x as denyAll, y as allowPublic } from "../permissions-B4vU9L0Q.mjs";
|
|
2
2
|
import { n as normalizeRoles, t as getUserRoles } from "../types-DV9WDfeg.mjs";
|
|
3
|
-
import { n as normalizePermissionResult, t as applyPermissionResult } from "../applyPermissionResult-QhV1Pa-g.mjs";
|
|
4
|
-
import { C as requireAuth, D as when, E as roles, S as not, T as requireRoles, _ as requireTeamMembership, a as presets_exports, b as anyOf, c as readOnly, d as createOrgPermissions, f as requireOrgInScope, g as requireServiceScope, h as requireScopeContext, i as ownerWithAdminBypass, l as createRoleHierarchy, m as requireOrgRole, n as authenticated, o as publicRead, p as requireOrgMembership, r as fullPublic, s as publicReadAdminWrite, t as adminOnly, u as createDynamicPermissionMatrix, v as allOf, w as requireOwnership, x as denyAll, y as allowPublic } from "../permissions-Dk6mshja.mjs";
|
|
5
3
|
export { adminOnly, allOf, allowPublic, anyOf, applyFieldReadPermissions, applyFieldWritePermissions, applyPermissionResult, authenticated, createDynamicPermissionMatrix, createOrgPermissions, createRoleHierarchy, denyAll, fields, fullPublic, getUserRoles, normalizePermissionResult, normalizeRoles, not, ownerWithAdminBypass, presets_exports as permissions, publicRead, publicReadAdminWrite, readOnly, requireAuth, requireOrgInScope, requireOrgMembership, requireOrgRole, requireOwnership, requireRoles, requireScopeContext, requireServiceScope, requireTeamMembership, resolveEffectiveRoles, roles, when };
|
|
@@ -1,8 +1,226 @@
|
|
|
1
1
|
import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
|
|
2
2
|
import { _ as isElevated, b as isService, c as getRequestScope, d as getServiceScopes, f as getTeamId, h as hasOrgAccess, l as getScopeContext, p as getUserId, u as getScopeContextMap, v as isMember, y as isOrgInScope } from "./types-AOD8fxIw.mjs";
|
|
3
3
|
import { t as getUserRoles } from "./types-DV9WDfeg.mjs";
|
|
4
|
-
import { t as MemoryCacheStore } from "./memory-
|
|
4
|
+
import { t as MemoryCacheStore } from "./memory-DikHSvWa.mjs";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
|
+
//#region src/permissions/fields.ts
|
|
7
|
+
/**
|
|
8
|
+
* Field-Level Permissions
|
|
9
|
+
*
|
|
10
|
+
* Control field visibility and writability per role.
|
|
11
|
+
* Integrated into the response path (read) and sanitization path (write).
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { fields, defineResource } from '@classytic/arc';
|
|
16
|
+
*
|
|
17
|
+
* const userResource = defineResource({
|
|
18
|
+
* name: 'user',
|
|
19
|
+
* adapter: userAdapter,
|
|
20
|
+
* fields: {
|
|
21
|
+
* salary: fields.visibleTo(['admin', 'hr']),
|
|
22
|
+
* internalNotes: fields.writableBy(['admin']),
|
|
23
|
+
* email: fields.redactFor(['viewer']),
|
|
24
|
+
* password: fields.hidden(),
|
|
25
|
+
* },
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
/** Type guard for Mongoose-like documents with toObject() */
|
|
30
|
+
function isMongooseDoc(obj) {
|
|
31
|
+
return !!obj && typeof obj === "object" && "toObject" in obj && typeof obj.toObject === "function";
|
|
32
|
+
}
|
|
33
|
+
const fields = {
|
|
34
|
+
hidden() {
|
|
35
|
+
return { _type: "hidden" };
|
|
36
|
+
},
|
|
37
|
+
visibleTo(roles) {
|
|
38
|
+
return {
|
|
39
|
+
_type: "visibleTo",
|
|
40
|
+
roles
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
writableBy(roles) {
|
|
44
|
+
return {
|
|
45
|
+
_type: "writableBy",
|
|
46
|
+
roles
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
redactFor(roles, redactValue = "***") {
|
|
50
|
+
return {
|
|
51
|
+
_type: "redactFor",
|
|
52
|
+
roles,
|
|
53
|
+
redactValue
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Apply field-level READ permissions to a response object.
|
|
59
|
+
* Strips hidden fields, enforces visibility, and applies redaction.
|
|
60
|
+
*
|
|
61
|
+
* @param data - The response object (mutated in place for performance)
|
|
62
|
+
* @param fieldPermissions - Field permission map from resource config
|
|
63
|
+
* @param userRoles - Current user's roles (empty array for unauthenticated)
|
|
64
|
+
* @returns The filtered object
|
|
65
|
+
*/
|
|
66
|
+
function applyFieldReadPermissions(data, fieldPermissions, userRoles) {
|
|
67
|
+
if (!data || typeof data !== "object") return data;
|
|
68
|
+
const result = { ...isMongooseDoc(data) ? data.toObject() : data };
|
|
69
|
+
for (const [field, perm] of Object.entries(fieldPermissions)) switch (perm._type) {
|
|
70
|
+
case "hidden":
|
|
71
|
+
delete result[field];
|
|
72
|
+
break;
|
|
73
|
+
case "visibleTo":
|
|
74
|
+
if (!perm.roles?.some((r) => userRoles.includes(r))) delete result[field];
|
|
75
|
+
break;
|
|
76
|
+
case "redactFor":
|
|
77
|
+
if (perm.roles?.some((r) => userRoles.includes(r))) result[field] = perm.redactValue ?? "***";
|
|
78
|
+
break;
|
|
79
|
+
case "writableBy": break;
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Apply field-level WRITE permissions to request body.
|
|
85
|
+
*
|
|
86
|
+
* Returns both the filtered body and the list of denied fields. Callers are
|
|
87
|
+
* expected to reject the request when `deniedFields.length > 0` — silently
|
|
88
|
+
* stripping fields hides misconfigurations and real attacks. See
|
|
89
|
+
* `BodySanitizer` for the default policy.
|
|
90
|
+
*
|
|
91
|
+
* @param body - The request body (returns a new filtered copy)
|
|
92
|
+
* @param fieldPermissions - Field permission map from resource config
|
|
93
|
+
* @param userRoles - Current user's roles
|
|
94
|
+
*/
|
|
95
|
+
function applyFieldWritePermissions(body, fieldPermissions, userRoles) {
|
|
96
|
+
const result = { ...body };
|
|
97
|
+
const deniedFields = [];
|
|
98
|
+
for (const [field, perm] of Object.entries(fieldPermissions)) switch (perm._type) {
|
|
99
|
+
case "hidden":
|
|
100
|
+
if (field in result) {
|
|
101
|
+
deniedFields.push(field);
|
|
102
|
+
delete result[field];
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
105
|
+
case "writableBy":
|
|
106
|
+
if (field in result && !perm.roles?.some((r) => userRoles.includes(r))) {
|
|
107
|
+
deniedFields.push(field);
|
|
108
|
+
delete result[field];
|
|
109
|
+
}
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
body: result,
|
|
114
|
+
deniedFields
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Resolve effective roles by merging global user roles with org-level roles.
|
|
119
|
+
*
|
|
120
|
+
* Global roles come from `req.user.role` (normalized via getUserRoles()).
|
|
121
|
+
* Org roles come from `req.context.orgRoles` (set by BA adapter's org bridge).
|
|
122
|
+
*
|
|
123
|
+
* When no org context exists, returns global roles only — backward compatible.
|
|
124
|
+
*/
|
|
125
|
+
function resolveEffectiveRoles(userRoles, orgRoles) {
|
|
126
|
+
if (orgRoles.length === 0) return [...userRoles];
|
|
127
|
+
if (userRoles.length === 0) return [...orgRoles];
|
|
128
|
+
return [...new Set([...userRoles, ...orgRoles])];
|
|
129
|
+
}
|
|
130
|
+
//#endregion
|
|
131
|
+
//#region src/permissions/applyPermissionResult.ts
|
|
132
|
+
/**
|
|
133
|
+
* Normalize a permission check return value (`boolean | PermissionResult`)
|
|
134
|
+
* into a concrete `PermissionResult`. This is the only place in Arc that
|
|
135
|
+
* promotes booleans to results — keeps the type narrowing honest everywhere.
|
|
136
|
+
*/
|
|
137
|
+
function normalizePermissionResult(result) {
|
|
138
|
+
if (typeof result === "boolean") return { granted: result };
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Apply a granted `PermissionResult` to a Fastify request — merges row-level
|
|
143
|
+
* filters into `_policyFilters` and conditionally installs the scope.
|
|
144
|
+
*
|
|
145
|
+
* **Scope install rule:** only writes `scope` when the current request scope
|
|
146
|
+
* is absent or `public`. This prevents downgrading an already-authenticated
|
|
147
|
+
* request (e.g. Better Auth set `member`, then a permission check returns a
|
|
148
|
+
* narrower `service` scope — the original `member` wins because it came from
|
|
149
|
+
* a more authoritative source).
|
|
150
|
+
*
|
|
151
|
+
* Safe to call with a non-granted result — it simply no-ops. Callers should
|
|
152
|
+
* still check `result.granted` and send an error response before reaching here,
|
|
153
|
+
* but this function tolerates the misuse defensively.
|
|
154
|
+
*/
|
|
155
|
+
function applyPermissionResult(result, request) {
|
|
156
|
+
if (!result.granted) return;
|
|
157
|
+
if (result.filters) request._policyFilters = {
|
|
158
|
+
...request._policyFilters ?? {},
|
|
159
|
+
...result.filters
|
|
160
|
+
};
|
|
161
|
+
if (result.scope) {
|
|
162
|
+
const current = request.scope;
|
|
163
|
+
if (!current || current.kind === "public") request.scope = result.scope;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Max length of a `PermissionResult.reason` string before we fall back to the
|
|
168
|
+
* generic default message. Upstream checks can return arbitrary strings; we
|
|
169
|
+
* clamp to prevent accidental leakage of internal diagnostics or oversized
|
|
170
|
+
* payloads via the 4xx response body.
|
|
171
|
+
*/
|
|
172
|
+
const MAX_DENIAL_REASON_LENGTH = 100;
|
|
173
|
+
/**
|
|
174
|
+
* End-to-end evaluator: runs the permission check, catches throws, normalizes
|
|
175
|
+
* the result, sends a 401/403 response on denial, and applies side-effects on
|
|
176
|
+
* grant. Returns `true` if the caller should continue, `false` if a response
|
|
177
|
+
* has been sent and the caller should return.
|
|
178
|
+
*
|
|
179
|
+
* This is the single source of truth for the 5-step sequence shared by the
|
|
180
|
+
* CRUD router, action router, and MCP tool handlers:
|
|
181
|
+
*
|
|
182
|
+
* 1. `try { await check(ctx) } catch { reply 403 }`
|
|
183
|
+
* 2. `normalizePermissionResult(result)`
|
|
184
|
+
* 3. If denied → 401 (no user) or 403 (user) with clamped reason
|
|
185
|
+
* 4. If granted → `applyPermissionResult` (filters + scope)
|
|
186
|
+
* 5. Return true/false so the caller knows whether to keep going
|
|
187
|
+
*
|
|
188
|
+
* Context construction, pre-check auth gating, and success-path handler
|
|
189
|
+
* invocation stay at the callsite — those are genuinely different per router
|
|
190
|
+
* and don't belong here.
|
|
191
|
+
*
|
|
192
|
+
* @returns `true` if authorized (caller continues), `false` if a response was sent
|
|
193
|
+
*/
|
|
194
|
+
async function evaluateAndApplyPermission(check, context, request, reply, opts) {
|
|
195
|
+
let result;
|
|
196
|
+
try {
|
|
197
|
+
result = await check(context);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
request.log?.warn?.({
|
|
200
|
+
err,
|
|
201
|
+
resource: context.resource,
|
|
202
|
+
action: context.action
|
|
203
|
+
}, "Permission check threw");
|
|
204
|
+
reply.code(403).send({
|
|
205
|
+
success: false,
|
|
206
|
+
error: "Permission denied"
|
|
207
|
+
});
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
const permResult = normalizePermissionResult(result);
|
|
211
|
+
if (!permResult.granted) {
|
|
212
|
+
const defaultMsg = opts?.defaultDenialMessage?.(context.user) ?? (context.user ? "Permission denied" : "Authentication required");
|
|
213
|
+
const reason = permResult.reason && permResult.reason.length <= MAX_DENIAL_REASON_LENGTH ? permResult.reason : defaultMsg;
|
|
214
|
+
reply.code(context.user ? 403 : 401).send({
|
|
215
|
+
success: false,
|
|
216
|
+
error: reason
|
|
217
|
+
});
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
applyPermissionResult(permResult, request);
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
//#endregion
|
|
6
224
|
//#region src/permissions/core.ts
|
|
7
225
|
/**
|
|
8
226
|
* Normalize a `string | [readonly string[]]` rest-args tuple into a single
|
|
@@ -1002,4 +1220,4 @@ function readOnly(overrides) {
|
|
|
1002
1220
|
}, overrides);
|
|
1003
1221
|
}
|
|
1004
1222
|
//#endregion
|
|
1005
|
-
export { requireAuth as C, when as D, roles as E, not as S, requireRoles as T, requireTeamMembership as _, presets_exports as a, anyOf as b, readOnly as c, createOrgPermissions as d, requireOrgInScope as f, requireServiceScope as g, requireScopeContext as h, ownerWithAdminBypass as i, createRoleHierarchy as l, requireOrgRole as m, authenticated as n, publicRead as o, requireOrgMembership as p, fullPublic as r, publicReadAdminWrite as s, adminOnly as t, createDynamicPermissionMatrix as u, allOf as v, requireOwnership as w, denyAll as x, allowPublic as y };
|
|
1223
|
+
export { normalizePermissionResult as A, requireAuth as C, when as D, roles as E, applyFieldWritePermissions as M, fields as N, applyPermissionResult as O, resolveEffectiveRoles as P, not as S, requireRoles as T, requireTeamMembership as _, presets_exports as a, anyOf as b, readOnly as c, createOrgPermissions as d, requireOrgInScope as f, requireServiceScope as g, requireScopeContext as h, ownerWithAdminBypass as i, applyFieldReadPermissions as j, evaluateAndApplyPermission as k, createRoleHierarchy as l, requireOrgRole as m, authenticated as n, publicRead as o, requireOrgMembership as p, fullPublic as r, publicReadAdminWrite as s, adminOnly as t, createDynamicPermissionMatrix as u, allOf as v, requireOwnership as w, denyAll as x, allowPublic as y };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { r as ForbiddenError } from "./errors-D5c-5BJL.mjs";
|
|
2
|
+
//#region src/pipeline/pipe.ts
|
|
3
|
+
/**
|
|
4
|
+
* Compose pipeline steps into an ordered array.
|
|
5
|
+
* Accepts guards, transforms, and interceptors in any order.
|
|
6
|
+
*/
|
|
7
|
+
function pipe(...steps) {
|
|
8
|
+
return steps;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Check if a step applies to the given operation.
|
|
12
|
+
*/
|
|
13
|
+
function appliesTo(step, operation) {
|
|
14
|
+
if (!step.operations || step.operations.length === 0) return true;
|
|
15
|
+
return step.operations.includes(operation);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Execute a pipeline against a request context.
|
|
19
|
+
*
|
|
20
|
+
* This is the core runtime that createCrudRouter uses to execute pipelines.
|
|
21
|
+
* External usage is not needed — this is wired automatically when `pipe` is set.
|
|
22
|
+
*
|
|
23
|
+
* @param steps - Pipeline steps to execute
|
|
24
|
+
* @param ctx - The pipeline context (extends IRequestContext)
|
|
25
|
+
* @param handler - The actual controller method to call
|
|
26
|
+
* @param operation - The CRUD operation name
|
|
27
|
+
* @returns The controller response (possibly modified by interceptors)
|
|
28
|
+
*/
|
|
29
|
+
async function executePipeline(steps, ctx, handler, operation) {
|
|
30
|
+
const guards = [];
|
|
31
|
+
const transforms = [];
|
|
32
|
+
const interceptors = [];
|
|
33
|
+
for (const step of steps) {
|
|
34
|
+
if (!appliesTo(step, operation)) continue;
|
|
35
|
+
switch (step._type) {
|
|
36
|
+
case "guard":
|
|
37
|
+
guards.push(step);
|
|
38
|
+
break;
|
|
39
|
+
case "transform":
|
|
40
|
+
transforms.push(step);
|
|
41
|
+
break;
|
|
42
|
+
case "interceptor":
|
|
43
|
+
interceptors.push(step);
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const g of guards) if (!await g.handler(ctx)) throw new ForbiddenError(`Guard '${g.name}' denied access`);
|
|
48
|
+
let currentCtx = ctx;
|
|
49
|
+
for (const t of transforms) {
|
|
50
|
+
const result = await t.handler(currentCtx);
|
|
51
|
+
if (result) currentCtx = result;
|
|
52
|
+
}
|
|
53
|
+
let chain = () => handler(currentCtx);
|
|
54
|
+
for (let i = interceptors.length - 1; i >= 0; i--) {
|
|
55
|
+
const interceptor = interceptors[i];
|
|
56
|
+
const next = chain;
|
|
57
|
+
chain = () => interceptor.handler(currentCtx, next);
|
|
58
|
+
}
|
|
59
|
+
return chain();
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
export { pipe as n, executePipeline as t };
|