@classytic/arc 2.11.4 → 2.13.1
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 +16 -12
- package/dist/{BaseController-swXruJ2_.mjs → BaseController-DX_T-bDB.mjs} +388 -423
- package/dist/EventTransport-CT_52aWU.d.mts +34 -0
- package/dist/EventTransport-DLWoUMHy.mjs +103 -0
- package/dist/{ResourceRegistry-DkAeAuTX.mjs → ResourceRegistry-CTERg_2x.mjs} +139 -66
- package/dist/audit/index.d.mts +2 -2
- package/dist/audit/index.mjs +1 -1
- package/dist/auth/audit.d.mts +199 -0
- package/dist/auth/audit.mjs +288 -0
- package/dist/auth/index.d.mts +3 -3
- package/dist/auth/index.mjs +117 -191
- package/dist/{betterAuthOpenApi-DwxtK3uG.mjs → betterAuthOpenApi--M_i87dQ.mjs} +1 -1
- package/dist/buildHandler-olo-gt94.mjs +610 -0
- package/dist/cache/index.mjs +3 -3
- package/dist/cli/commands/describe.d.mts +89 -13
- package/dist/cli/commands/describe.mjs +56 -2
- package/dist/cli/commands/docs.mjs +2 -2
- package/dist/cli/commands/generate.mjs +147 -48
- package/dist/cli/commands/init.d.mts +13 -0
- package/dist/cli/commands/init.mjs +130 -87
- package/dist/cli/commands/introspect.mjs +8 -1
- package/dist/context/index.mjs +1 -1
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +5 -5
- package/dist/core-D72ia0EH.mjs +1399 -0
- package/dist/{createActionRouter-CIKOcNA7.mjs → createActionRouter-CEvzKcy8.mjs} +7 -20
- package/dist/createAggregationRouter-CyecOxnO.mjs +114 -0
- package/dist/{createApp-C9bRrqlX.mjs → createApp-XX2-N0Yd.mjs} +28 -22
- package/dist/{defineEvent-D1Ky9M1D.mjs → defineEvent-D5h7EvAx.mjs} +1 -1
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +2 -2
- package/dist/{elevation-DOFoxoDs.mjs → elevation-DgoeTyfX.mjs} +1 -1
- package/dist/errorHandler-Bk-AGhkU.mjs +174 -0
- package/dist/errorHandler-DFr45ZG4.d.mts +45 -0
- package/dist/errors-j4aJm1Wg.mjs +184 -0
- package/dist/{eventPlugin-Cts2-Tfj.mjs → eventPlugin-CaKTYkYM.mjs} +28 -4
- package/dist/{eventPlugin-DDJoNEPL.d.mts → eventPlugin-qXpqTebY.d.mts} +24 -1
- package/dist/events/index.d.mts +6 -6
- package/dist/events/index.mjs +11 -35
- 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 +2 -2
- package/dist/factory/index.mjs +2 -2
- package/dist/{fields-BRjxOAFp.d.mts → fields-COhcH3fk.d.mts} +23 -2
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/idempotency/index.d.mts +1 -1
- package/dist/idempotency/index.mjs +1 -20
- package/dist/idempotency/redis.mjs +1 -1
- package/dist/{index-rHjXmJar.d.mts → index-BTqLEvhu.d.mts} +163 -3
- package/dist/{index-CXXRbnf8.d.mts → index-BtW7qYwa.d.mts} +660 -326
- package/dist/{index-m8mOOlFW.d.mts → index-Ds61mrJE.d.mts} +50 -4
- package/dist/{index-D9t1KNaB.d.mts → index-Dz5IKsrE.d.mts} +360 -219
- package/dist/index.d.mts +6 -7
- package/dist/index.mjs +9 -10
- package/dist/integrations/event-gateway.d.mts +1 -1
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +1 -1
- 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 +60 -11
- package/dist/integrations/streamline.mjs +75 -85
- package/dist/integrations/websocket.mjs +2 -8
- package/dist/middleware/index.d.mts +1 -1
- package/dist/middleware/index.mjs +2 -2
- package/dist/migrations/index.d.mts +23 -3
- package/dist/migrations/index.mjs +0 -7
- package/dist/{multipartBody-CvTR1Un6.mjs → multipartBody-BOvVSVCD.mjs} +11 -8
- package/dist/{openapi-D7G1V7ex.mjs → openapi-CiOMVW1p.mjs} +143 -13
- package/dist/org/index.d.mts +2 -2
- package/dist/org/index.mjs +1 -1
- package/dist/permissions/index.d.mts +3 -3
- package/dist/permissions/index.mjs +3 -3
- package/dist/{permissions-gd_aUWrR.mjs → permissions-ohQyv50e.mjs} +404 -176
- package/dist/{pipe-DVoIheVC.mjs → pipe-Zr0KXjQe.mjs} +1 -1
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/pipeline/index.mjs +1 -1
- package/dist/plugins/index.d.mts +16 -31
- package/dist/plugins/index.mjs +33 -13
- package/dist/plugins/response-cache.mjs +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/presets/filesUpload.d.mts +4 -4
- package/dist/presets/filesUpload.mjs +6 -9
- package/dist/presets/index.d.mts +1 -1
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +2 -2
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +6 -8
- package/dist/{presets-Z7P5w4gF.mjs → presets-BbkjdPeH.mjs} +6 -28
- package/dist/{queryCachePlugin-Bq6bO6vc.mjs → queryCachePlugin-m1XsgAIJ.mjs} +3 -3
- package/dist/{redis-stream-xTGxB2bm.d.mts → redis-stream-D6HzR1Z_.d.mts} +1 -1
- package/dist/registry/index.d.mts +1 -1
- package/dist/registry/index.mjs +2 -2
- package/dist/{replyHelpers-ByllIXXV.mjs → replyHelpers-CK-FNO8E.mjs} +3 -21
- package/dist/{resourceToTools-CxNmI6xF.mjs → resourceToTools-C5coh64w.mjs} +224 -71
- package/dist/{routerShared-BqLRb5l7.mjs → routerShared-D6_fEGHh.mjs} +40 -36
- package/dist/{schemaIR-Dy2p4MxS.mjs → schemaIR-7Vl611Qs.mjs} +1 -1
- package/dist/schemas/index.d.mts +100 -30
- package/dist/schemas/index.mjs +86 -29
- package/dist/scim/index.d.mts +264 -0
- package/dist/scim/index.mjs +963 -0
- package/dist/scope/index.d.mts +3 -3
- package/dist/scope/index.mjs +4 -4
- package/dist/{sse-V7aXc3bW.mjs → sse-Bz-5ZeTt.mjs} +1 -1
- package/dist/{store-helpers-Cp4uKC1U.mjs → store-helpers-BkIN9-vu.mjs} +1 -1
- package/dist/testing/index.d.mts +2 -8
- package/dist/testing/index.mjs +16 -24
- package/dist/types/index.d.mts +4 -4
- package/dist/{types-D7KpfiL1.d.mts → types-BvqwCCSx.d.mts} +73 -25
- package/dist/{types-DDyTPc6y.d.mts → types-CTYvcwHe.d.mts} +195 -1
- package/dist/{types-AOD8fxIw.mjs → types-C_s5moIu.mjs} +117 -1
- package/dist/{types-BQ9TJQNy.d.mts → types-DQHFc8PM.d.mts} +1 -1
- package/dist/utils/index.d.mts +2 -2
- package/dist/utils/index.mjs +5 -5
- package/dist/{utils-CcYTj09l.mjs → utils-_h9B3c57.mjs} +1269 -1334
- package/dist/{versioning-DsglKfM_.d.mts → versioning-DTTvc80y.d.mts} +1 -1
- package/package.json +24 -34
- package/skills/arc/SKILL.md +147 -51
- package/skills/arc/references/agent-auth.md +238 -0
- package/skills/arc/references/api-reference.md +187 -0
- package/skills/arc/references/auth.md +354 -7
- package/skills/arc/references/enterprise-auth.md +94 -0
- package/skills/arc/references/events.md +8 -6
- package/skills/arc/references/mcp.md +2 -2
- package/skills/arc/references/multi-tenancy.md +11 -2
- package/skills/arc/references/production.md +10 -9
- package/skills/arc/references/scim.md +247 -0
- package/skills/arc/references/testing.md +1 -1
- package/skills/arc-code-review/SKILL.md +141 -0
- package/skills/arc-code-review/references/anti-patterns.md +911 -0
- package/skills/arc-code-review/references/arc-cheatsheet.md +380 -0
- package/skills/arc-code-review/references/migration-recipes.md +700 -0
- package/skills/arc-code-review/references/mongokit-migration.md +386 -0
- package/skills/arc-code-review/references/scaffolding.md +230 -0
- package/skills/arc-code-review/references/severity.md +127 -0
- package/dist/EventTransport-BFQjw9pB.mjs +0 -133
- package/dist/EventTransport-CYNUXdCJ.d.mts +0 -293
- package/dist/adapters/index.d.mts +0 -3
- package/dist/adapters/index.mjs +0 -2
- package/dist/adapters-DUUiiimH.mjs +0 -964
- package/dist/auth/mongoose.d.mts +0 -191
- package/dist/auth/mongoose.mjs +0 -73
- package/dist/core-CbcQRIch.mjs +0 -1054
- package/dist/errorHandler-BQm8ZxTK.mjs +0 -173
- package/dist/errorHandler-DEWmGWPz.d.mts +0 -114
- package/dist/errors-D5c-5BJL.mjs +0 -232
- package/dist/index-Rg8axYPz.d.mts +0 -370
- /package/dist/{HookSystem-CGsMd6oK.mjs → HookSystem-Iiebom92.mjs} +0 -0
- /package/dist/{actionPermissions-sUUKDhtP.mjs → actionPermissions-CyUkQu6O.mjs} +0 -0
- /package/dist/{caching-CheW3m-S.mjs → caching-SM8gghN6.mjs} +0 -0
- /package/dist/{constants-BhY1OHoH.mjs → constants-Cxde4rpC.mjs} +0 -0
- /package/dist/{elevation-BQQXZ_VR.d.mts → elevation-BXOWoGCF.d.mts} +0 -0
- /package/dist/{keys-CARyUjiR.mjs → keys-CGcCbNyu.mjs} +0 -0
- /package/dist/{loadResources-CPpkyKfM.mjs → loadResources-DBMQg_Aj.mjs} +0 -0
- /package/dist/{memory-DikHSvWa.mjs → memory-UBydS5ku.mjs} +0 -0
- /package/dist/{metrics-Csh4nsvv.mjs → metrics-Qnvwc-LQ.mjs} +0 -0
- /package/dist/{pluralize-CWP6MB39.mjs → pluralize-DQgqgifU.mjs} +0 -0
- /package/dist/{registry-D63ee7fl.mjs → registry-I-ogLgL9.mjs} +0 -0
- /package/dist/{requestContext-C5XeK3VA.mjs → requestContext-SSaaTgW8.mjs} +0 -0
- /package/dist/{schemaConverter-B0oKLuqI.mjs → schemaConverter-De34B1ZG.mjs} +0 -0
- /package/dist/{typeGuards-CcFZXgU7.mjs → typeGuards-BzkXkvVv.mjs} +0 -0
- /package/dist/{types-DV9WDfeg.mjs → types-D57iXYb8.mjs} +0 -0
- /package/dist/{versioning-CGPjkqAg.mjs → versioning-BUrT5aP4.mjs} +0 -0
|
@@ -1,175 +1,8 @@
|
|
|
1
1
|
import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
|
|
2
|
-
import { _ as
|
|
3
|
-
import { t as getUserRoles } from "./types-
|
|
4
|
-
import { t as MemoryCacheStore } from "./memory-
|
|
2
|
+
import { S as isService, _ as hasOrgAccess, a as getDPoPJkt, b as isMember, d as getScopeContext, f as getScopeContextMap, h as getUserId, m as getTeamId, o as getMandate, p as getServiceScopes, u as getRequestScope, x as isOrgInScope, y as isElevated } from "./types-C_s5moIu.mjs";
|
|
3
|
+
import { t as getUserRoles } from "./types-D57iXYb8.mjs";
|
|
4
|
+
import { t as MemoryCacheStore } from "./memory-UBydS5ku.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
|
-
/**
|
|
35
|
-
* Field is never included in responses. Not writable via API.
|
|
36
|
-
*
|
|
37
|
-
* @example
|
|
38
|
-
* ```typescript
|
|
39
|
-
* fields: { password: fields.hidden() }
|
|
40
|
-
* ```
|
|
41
|
-
*/
|
|
42
|
-
hidden() {
|
|
43
|
-
return { _type: "hidden" };
|
|
44
|
-
},
|
|
45
|
-
/**
|
|
46
|
-
* Field is only visible to users with specified roles.
|
|
47
|
-
* Other users don't see the field at all.
|
|
48
|
-
*
|
|
49
|
-
* @example
|
|
50
|
-
* ```typescript
|
|
51
|
-
* fields: { salary: fields.visibleTo(['admin', 'hr']) }
|
|
52
|
-
* ```
|
|
53
|
-
*/
|
|
54
|
-
visibleTo(roles) {
|
|
55
|
-
return {
|
|
56
|
-
_type: "visibleTo",
|
|
57
|
-
roles
|
|
58
|
-
};
|
|
59
|
-
},
|
|
60
|
-
/**
|
|
61
|
-
* Field is only writable by users with specified roles.
|
|
62
|
-
* All users can still read the field. Users without the role
|
|
63
|
-
* have the field silently stripped from write operations.
|
|
64
|
-
*
|
|
65
|
-
* @example
|
|
66
|
-
* ```typescript
|
|
67
|
-
* fields: { role: fields.writableBy(['admin']) }
|
|
68
|
-
* ```
|
|
69
|
-
*/
|
|
70
|
-
writableBy(roles) {
|
|
71
|
-
return {
|
|
72
|
-
_type: "writableBy",
|
|
73
|
-
roles
|
|
74
|
-
};
|
|
75
|
-
},
|
|
76
|
-
/**
|
|
77
|
-
* Field is redacted (replaced with a placeholder) for specified roles.
|
|
78
|
-
* Other users see the real value.
|
|
79
|
-
*
|
|
80
|
-
* @param roles - Roles that see the redacted value
|
|
81
|
-
* @param redactValue - Replacement value (default: '***')
|
|
82
|
-
*
|
|
83
|
-
* @example
|
|
84
|
-
* ```typescript
|
|
85
|
-
* fields: {
|
|
86
|
-
* email: fields.redactFor(['viewer']),
|
|
87
|
-
* ssn: fields.redactFor(['basic'], '***-**-****'),
|
|
88
|
-
* }
|
|
89
|
-
* ```
|
|
90
|
-
*/
|
|
91
|
-
redactFor(roles, redactValue = "***") {
|
|
92
|
-
return {
|
|
93
|
-
_type: "redactFor",
|
|
94
|
-
roles,
|
|
95
|
-
redactValue
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
/**
|
|
100
|
-
* Apply field-level READ permissions to a response object.
|
|
101
|
-
* Strips hidden fields, enforces visibility, and applies redaction.
|
|
102
|
-
*
|
|
103
|
-
* @param data - The response object (mutated in place for performance)
|
|
104
|
-
* @param fieldPermissions - Field permission map from resource config
|
|
105
|
-
* @param userRoles - Current user's roles (empty array for unauthenticated)
|
|
106
|
-
* @returns The filtered object
|
|
107
|
-
*/
|
|
108
|
-
function applyFieldReadPermissions(data, fieldPermissions, userRoles) {
|
|
109
|
-
if (!data || typeof data !== "object") return data;
|
|
110
|
-
const result = { ...isMongooseDoc(data) ? data.toObject() : data };
|
|
111
|
-
for (const [field, perm] of Object.entries(fieldPermissions)) switch (perm._type) {
|
|
112
|
-
case "hidden":
|
|
113
|
-
delete result[field];
|
|
114
|
-
break;
|
|
115
|
-
case "visibleTo":
|
|
116
|
-
if (!perm.roles?.some((r) => userRoles.includes(r))) delete result[field];
|
|
117
|
-
break;
|
|
118
|
-
case "redactFor":
|
|
119
|
-
if (perm.roles?.some((r) => userRoles.includes(r))) result[field] = perm.redactValue ?? "***";
|
|
120
|
-
break;
|
|
121
|
-
case "writableBy": break;
|
|
122
|
-
}
|
|
123
|
-
return result;
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Apply field-level WRITE permissions to request body.
|
|
127
|
-
*
|
|
128
|
-
* Returns both the filtered body and the list of denied fields. Callers are
|
|
129
|
-
* expected to reject the request when `deniedFields.length > 0` — silently
|
|
130
|
-
* stripping fields hides misconfigurations and real attacks. See
|
|
131
|
-
* `BodySanitizer` for the default policy.
|
|
132
|
-
*
|
|
133
|
-
* @param body - The request body (returns a new filtered copy)
|
|
134
|
-
* @param fieldPermissions - Field permission map from resource config
|
|
135
|
-
* @param userRoles - Current user's roles
|
|
136
|
-
*/
|
|
137
|
-
function applyFieldWritePermissions(body, fieldPermissions, userRoles) {
|
|
138
|
-
const result = { ...body };
|
|
139
|
-
const deniedFields = [];
|
|
140
|
-
for (const [field, perm] of Object.entries(fieldPermissions)) switch (perm._type) {
|
|
141
|
-
case "hidden":
|
|
142
|
-
if (field in result) {
|
|
143
|
-
deniedFields.push(field);
|
|
144
|
-
delete result[field];
|
|
145
|
-
}
|
|
146
|
-
break;
|
|
147
|
-
case "writableBy":
|
|
148
|
-
if (field in result && !perm.roles?.some((r) => userRoles.includes(r))) {
|
|
149
|
-
deniedFields.push(field);
|
|
150
|
-
delete result[field];
|
|
151
|
-
}
|
|
152
|
-
break;
|
|
153
|
-
}
|
|
154
|
-
return {
|
|
155
|
-
body: result,
|
|
156
|
-
deniedFields
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
/**
|
|
160
|
-
* Resolve effective roles by merging global user roles with org-level roles.
|
|
161
|
-
*
|
|
162
|
-
* Global roles come from `req.user.role` (normalized via getUserRoles()).
|
|
163
|
-
* Org roles come from `req.context.orgRoles` (set by BA adapter's org bridge).
|
|
164
|
-
*
|
|
165
|
-
* When no org context exists, returns global roles only — backward compatible.
|
|
166
|
-
*/
|
|
167
|
-
function resolveEffectiveRoles(userRoles, orgRoles) {
|
|
168
|
-
if (orgRoles.length === 0) return [...userRoles];
|
|
169
|
-
if (userRoles.length === 0) return [...orgRoles];
|
|
170
|
-
return [...new Set([...userRoles, ...orgRoles])];
|
|
171
|
-
}
|
|
172
|
-
//#endregion
|
|
173
6
|
//#region src/permissions/applyPermissionResult.ts
|
|
174
7
|
/**
|
|
175
8
|
* Normalize a permission check return value (`boolean | PermissionResult`)
|
|
@@ -244,8 +77,9 @@ async function evaluateAndApplyPermission(check, context, request, reply, opts)
|
|
|
244
77
|
action: context.action
|
|
245
78
|
}, "Permission check threw");
|
|
246
79
|
reply.code(403).send({
|
|
247
|
-
|
|
248
|
-
|
|
80
|
+
code: "arc.forbidden",
|
|
81
|
+
message: "Permission denied",
|
|
82
|
+
status: 403
|
|
249
83
|
});
|
|
250
84
|
return false;
|
|
251
85
|
}
|
|
@@ -253,9 +87,11 @@ async function evaluateAndApplyPermission(check, context, request, reply, opts)
|
|
|
253
87
|
if (!permResult.granted) {
|
|
254
88
|
const defaultMsg = opts?.defaultDenialMessage?.(context.user) ?? (context.user ? "Permission denied" : "Authentication required");
|
|
255
89
|
const reason = permResult.reason && permResult.reason.length <= MAX_DENIAL_REASON_LENGTH ? permResult.reason : defaultMsg;
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
90
|
+
const status = context.user ? 403 : 401;
|
|
91
|
+
reply.code(status).send({
|
|
92
|
+
code: context.user ? "arc.forbidden" : "arc.unauthorized",
|
|
93
|
+
message: reason,
|
|
94
|
+
status
|
|
259
95
|
});
|
|
260
96
|
return false;
|
|
261
97
|
}
|
|
@@ -263,6 +99,231 @@ async function evaluateAndApplyPermission(check, context, request, reply, opts)
|
|
|
263
99
|
return true;
|
|
264
100
|
}
|
|
265
101
|
//#endregion
|
|
102
|
+
//#region src/permissions/agent.ts
|
|
103
|
+
/**
|
|
104
|
+
* Agent-Auth Permission Helpers — DPoP + capability mandates for AI-agent flows
|
|
105
|
+
*
|
|
106
|
+
* Three checks for the 2025 agent-authorization stack (AP2 / Stripe x402 /
|
|
107
|
+
* MCP authorization / RFC 9700 / RFC 9449 / RFC 9728):
|
|
108
|
+
*
|
|
109
|
+
* - `requireDPoP()` — token must be sender-constrained (RFC 9449)
|
|
110
|
+
* - `requireMandate(cap, …)` — capability mandate must authorize this action
|
|
111
|
+
* - `requireAgentScope(opts)` — composite gate: service identity + mandate + DPoP
|
|
112
|
+
*
|
|
113
|
+
* Arc reads `request.scope.mandate` and `request.scope.dpopJkt` populated by
|
|
114
|
+
* your `authenticate` callback. Arc does **not** parse mandate JWTs/VCs or
|
|
115
|
+
* verify DPoP proofs — that's a 1-2 line `jose` call in your authenticator.
|
|
116
|
+
* Arc validates *what's already proved* against the action being attempted.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```typescript
|
|
120
|
+
* import { requireDPoP, requireMandate, requireAgentScope } from '@classytic/arc/permissions';
|
|
121
|
+
*
|
|
122
|
+
* defineResource({
|
|
123
|
+
* name: 'invoice',
|
|
124
|
+
* permissions: {
|
|
125
|
+
* pay: requireAgentScope({
|
|
126
|
+
* capability: 'payment.charge',
|
|
127
|
+
* requireDPoP: true,
|
|
128
|
+
* validateAmount: (ctx, mandate) =>
|
|
129
|
+
* typeof ctx.data?.amount === 'number' && ctx.data.amount <= (mandate.cap ?? 0),
|
|
130
|
+
* audience: (ctx) => `invoice:${ctx.params?.id}`,
|
|
131
|
+
* }),
|
|
132
|
+
* },
|
|
133
|
+
* });
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
/** Default grace window for mandate `expiresAt` — accommodates clock skew. */
|
|
137
|
+
const DEFAULT_TTL_GRACE_MS = 3e4;
|
|
138
|
+
/**
|
|
139
|
+
* Require a sender-constrained credential — the inbound token MUST carry a
|
|
140
|
+
* DPoP proof (RFC 9449) bound to a known key. Arc reads `scope.dpopJkt` (the
|
|
141
|
+
* JWK SHA-256 thumbprint per RFC 7638); your `authenticate` function performs
|
|
142
|
+
* the cryptographic `jose.dpop.verify(...)` and sets the field on success.
|
|
143
|
+
*
|
|
144
|
+
* **Pass behavior:**
|
|
145
|
+
* - `service` scope where `dpopJkt` is set → grant
|
|
146
|
+
* - `elevated` scope → grant (platform admin bypass)
|
|
147
|
+
* - Anything else → deny with a clear reason
|
|
148
|
+
*
|
|
149
|
+
* Use for high-value endpoints where bearer-token replay must be impossible:
|
|
150
|
+
* payment charges, data exports, account-takeover-class admin actions.
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```typescript
|
|
154
|
+
* permissions: { charge: allOf(requireServiceScope('payment.write'), requireDPoP()) }
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
function requireDPoP() {
|
|
158
|
+
const check = (ctx) => {
|
|
159
|
+
const scope = getRequestScope(ctx.request);
|
|
160
|
+
if (isElevated(scope)) return true;
|
|
161
|
+
if (!isService(scope)) return {
|
|
162
|
+
granted: false,
|
|
163
|
+
reason: "DPoP-bound service identity required. Configure your authenticate callback to set scope.dpopJkt after verifying the DPoP proof header (RFC 9449)."
|
|
164
|
+
};
|
|
165
|
+
if (!getDPoPJkt(scope)) return {
|
|
166
|
+
granted: false,
|
|
167
|
+
reason: "Sender-constrained credential required (DPoP). Inbound token is bearer; replay-resistance is mandatory on this endpoint."
|
|
168
|
+
};
|
|
169
|
+
return true;
|
|
170
|
+
};
|
|
171
|
+
check._dpopRequired = true;
|
|
172
|
+
return check;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Require a capability mandate (AP2 / x402 / MCP authorization) that
|
|
176
|
+
* authorizes the action being attempted.
|
|
177
|
+
*
|
|
178
|
+
* The mandate is set on `request.scope.mandate` by your authenticate function
|
|
179
|
+
* after verifying the inbound mandate JWT/VC. This check validates that the
|
|
180
|
+
* presented mandate covers the requested capability, hasn't expired, is bound
|
|
181
|
+
* to the right resource (when `audience` opt is set), and respects the
|
|
182
|
+
* mandate's numeric ceiling (when `validateAmount` opt is set).
|
|
183
|
+
*
|
|
184
|
+
* **Pass behavior:**
|
|
185
|
+
* - `elevated` scope → grant unless `noElevatedBypass: true`
|
|
186
|
+
* - `service` scope with mandate matching `capability`, not expired, and
|
|
187
|
+
* passing `validateAmount` + `audience` checks → grant
|
|
188
|
+
* - Anything else → deny with a precise reason
|
|
189
|
+
*
|
|
190
|
+
* Pair with `requireDPoP()` for replay-resistance, or use the bundled
|
|
191
|
+
* `requireAgentScope(...)` to declare both at once.
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* ```typescript
|
|
195
|
+
* // Single payment charge — amount must fit the mandate's cap
|
|
196
|
+
* permissions: {
|
|
197
|
+
* pay: requireMandate('payment.charge', {
|
|
198
|
+
* validateAmount: (ctx, m) => (ctx.data as { amount: number }).amount <= (m.cap ?? 0),
|
|
199
|
+
* audience: (ctx) => `invoice:${ctx.params?.id}`,
|
|
200
|
+
* }),
|
|
201
|
+
* }
|
|
202
|
+
*
|
|
203
|
+
* // Boolean capability — presence of mandate is the gate
|
|
204
|
+
* permissions: {
|
|
205
|
+
* exportData: requireMandate('data.export'),
|
|
206
|
+
* }
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
209
|
+
function requireMandate(capability, opts = {}) {
|
|
210
|
+
if (!capability || typeof capability !== "string") throw new Error("requireMandate(capability) requires a non-empty capability string (e.g. 'payment.charge')");
|
|
211
|
+
const ttlGrace = opts.ttlGraceMs ?? DEFAULT_TTL_GRACE_MS;
|
|
212
|
+
const noElevatedBypass = opts.noElevatedBypass === true;
|
|
213
|
+
const check = (ctx) => {
|
|
214
|
+
const scope = getRequestScope(ctx.request);
|
|
215
|
+
if (isElevated(scope) && !noElevatedBypass) return true;
|
|
216
|
+
const mandate = getMandate(scope);
|
|
217
|
+
if (!mandate) return {
|
|
218
|
+
granted: false,
|
|
219
|
+
reason: `Capability mandate required (${capability}). Configure your authenticate callback to populate request.scope.mandate after verifying the mandate JWT/VC.`
|
|
220
|
+
};
|
|
221
|
+
if (mandate.capability !== capability) return {
|
|
222
|
+
granted: false,
|
|
223
|
+
reason: `Mandate authorizes "${mandate.capability}", not "${capability}"`
|
|
224
|
+
};
|
|
225
|
+
if (mandate.expiresAt !== void 0 && Date.now() > mandate.expiresAt + ttlGrace) return {
|
|
226
|
+
granted: false,
|
|
227
|
+
reason: `Mandate expired at ${new Date(mandate.expiresAt).toISOString()}`
|
|
228
|
+
};
|
|
229
|
+
if (opts.audience) {
|
|
230
|
+
const required = typeof opts.audience === "function" ? opts.audience(ctx) : opts.audience;
|
|
231
|
+
if (required && mandate.audience && mandate.audience !== required) return {
|
|
232
|
+
granted: false,
|
|
233
|
+
reason: `Mandate is bound to "${mandate.audience}", not "${required}"`
|
|
234
|
+
};
|
|
235
|
+
if (required && !mandate.audience) return {
|
|
236
|
+
granted: false,
|
|
237
|
+
reason: `Mandate must be bound to "${required}" (no audience claim)`
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
if (opts.validateAmount) {
|
|
241
|
+
const result = opts.validateAmount(ctx, mandate);
|
|
242
|
+
if (result !== true) return {
|
|
243
|
+
granted: false,
|
|
244
|
+
reason: typeof result === "string" ? result : `Action exceeds mandate cap ${mandate.cap}${mandate.currency ? ` ${mandate.currency}` : ""}`
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
return true;
|
|
248
|
+
};
|
|
249
|
+
check._mandateCapability = capability;
|
|
250
|
+
return check;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Composite gate for AI-agent / M2M flows on protected resources.
|
|
254
|
+
*
|
|
255
|
+
* Bundles the three things every high-value agent endpoint needs:
|
|
256
|
+
* 1. **Service identity** — `scope.kind === 'service'` with `clientId`
|
|
257
|
+
* 2. **Capability mandate** — narrows what *this request* may do
|
|
258
|
+
* 3. **DPoP binding** — credential cannot be replayed from a different key
|
|
259
|
+
*
|
|
260
|
+
* Use this instead of hand-composing `allOf(requireServiceScope(...),
|
|
261
|
+
* requireMandate(...), requireDPoP())` — fewer ways to misconfigure, one
|
|
262
|
+
* meta-tag downstream tools (audit, MCP, OpenAPI) can read.
|
|
263
|
+
*
|
|
264
|
+
* @example
|
|
265
|
+
* ```typescript
|
|
266
|
+
* import { requireAgentScope } from '@classytic/arc/permissions';
|
|
267
|
+
*
|
|
268
|
+
* defineResource({
|
|
269
|
+
* name: 'invoice',
|
|
270
|
+
* actions: {
|
|
271
|
+
* pay: {
|
|
272
|
+
* handler: payInvoice,
|
|
273
|
+
* permissions: requireAgentScope({
|
|
274
|
+
* capability: 'payment.charge',
|
|
275
|
+
* scopes: ['payment.write'],
|
|
276
|
+
* requireDPoP: true,
|
|
277
|
+
* audience: (ctx) => `invoice:${ctx.params?.id}`,
|
|
278
|
+
* validateAmount: (ctx, m) => (ctx.data as { amount: number }).amount <= (m.cap ?? 0),
|
|
279
|
+
* }),
|
|
280
|
+
* },
|
|
281
|
+
* },
|
|
282
|
+
* });
|
|
283
|
+
* ```
|
|
284
|
+
*/
|
|
285
|
+
function requireAgentScope(opts) {
|
|
286
|
+
const { capability, scopes, requireDPoP: needsDPoP = true, ...mandateOpts } = opts;
|
|
287
|
+
if (!capability) throw new Error("requireAgentScope({ capability }) is required");
|
|
288
|
+
const mandateCheck = requireMandate(capability, mandateOpts);
|
|
289
|
+
const dpopCheck = needsDPoP ? requireDPoP() : null;
|
|
290
|
+
const requiredScopes = scopes && scopes.length > 0 ? [...scopes] : null;
|
|
291
|
+
const check = async (ctx) => {
|
|
292
|
+
const scope = getRequestScope(ctx.request);
|
|
293
|
+
if (isElevated(scope) && !mandateOpts.noElevatedBypass) return true;
|
|
294
|
+
if (!isService(scope)) return {
|
|
295
|
+
granted: false,
|
|
296
|
+
reason: "Service identity required (machine principal). Agent flows must authenticate as a service, not a logged-in user."
|
|
297
|
+
};
|
|
298
|
+
if (requiredScopes) {
|
|
299
|
+
const granted = scope.scopes ?? [];
|
|
300
|
+
if (!requiredScopes.some((s) => granted.includes(s))) return {
|
|
301
|
+
granted: false,
|
|
302
|
+
reason: `Service identity is missing required OAuth scope(s): ${requiredScopes.join(", ")}`
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
const mandateResult = await mandateCheck(ctx);
|
|
306
|
+
if (mandateResult !== true) return typeof mandateResult === "object" ? mandateResult : {
|
|
307
|
+
granted: false,
|
|
308
|
+
reason: "Mandate check failed"
|
|
309
|
+
};
|
|
310
|
+
if (dpopCheck) {
|
|
311
|
+
const dpopResult = await dpopCheck(ctx);
|
|
312
|
+
if (dpopResult !== true) return typeof dpopResult === "object" ? dpopResult : {
|
|
313
|
+
granted: false,
|
|
314
|
+
reason: "DPoP binding required"
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
return true;
|
|
318
|
+
};
|
|
319
|
+
check._agentScope = {
|
|
320
|
+
capability,
|
|
321
|
+
scopes: requiredScopes ?? void 0,
|
|
322
|
+
dpop: needsDPoP
|
|
323
|
+
};
|
|
324
|
+
return check;
|
|
325
|
+
}
|
|
326
|
+
//#endregion
|
|
266
327
|
//#region src/permissions/core.ts
|
|
267
328
|
/**
|
|
268
329
|
* Normalize a `string | [readonly string[]]` rest-args tuple into a single
|
|
@@ -1087,6 +1148,173 @@ function createDynamicPermissionMatrix(config) {
|
|
|
1087
1148
|
};
|
|
1088
1149
|
}
|
|
1089
1150
|
//#endregion
|
|
1151
|
+
//#region src/permissions/fields.ts
|
|
1152
|
+
/**
|
|
1153
|
+
* Field-Level Permissions
|
|
1154
|
+
*
|
|
1155
|
+
* Control field visibility and writability per role.
|
|
1156
|
+
* Integrated into the response path (read) and sanitization path (write).
|
|
1157
|
+
*
|
|
1158
|
+
* @example
|
|
1159
|
+
* ```typescript
|
|
1160
|
+
* import { fields, defineResource } from '@classytic/arc';
|
|
1161
|
+
*
|
|
1162
|
+
* const userResource = defineResource({
|
|
1163
|
+
* name: 'user',
|
|
1164
|
+
* adapter: userAdapter,
|
|
1165
|
+
* fields: {
|
|
1166
|
+
* salary: fields.visibleTo(['admin', 'hr']),
|
|
1167
|
+
* internalNotes: fields.writableBy(['admin']),
|
|
1168
|
+
* email: fields.redactFor(['viewer']),
|
|
1169
|
+
* password: fields.hidden(),
|
|
1170
|
+
* },
|
|
1171
|
+
* });
|
|
1172
|
+
* ```
|
|
1173
|
+
*/
|
|
1174
|
+
/** Type guard for Mongoose-like documents with toObject() */
|
|
1175
|
+
function isMongooseDoc(obj) {
|
|
1176
|
+
return !!obj && typeof obj === "object" && "toObject" in obj && typeof obj.toObject === "function";
|
|
1177
|
+
}
|
|
1178
|
+
const fields = {
|
|
1179
|
+
/**
|
|
1180
|
+
* Field is never included in responses. Not writable via API.
|
|
1181
|
+
*
|
|
1182
|
+
* @example
|
|
1183
|
+
* ```typescript
|
|
1184
|
+
* fields: { password: fields.hidden() }
|
|
1185
|
+
* ```
|
|
1186
|
+
*/
|
|
1187
|
+
hidden() {
|
|
1188
|
+
return { _type: "hidden" };
|
|
1189
|
+
},
|
|
1190
|
+
/**
|
|
1191
|
+
* Field is only visible to users with specified roles.
|
|
1192
|
+
* Other users don't see the field at all.
|
|
1193
|
+
*
|
|
1194
|
+
* @example
|
|
1195
|
+
* ```typescript
|
|
1196
|
+
* fields: { salary: fields.visibleTo(['admin', 'hr']) }
|
|
1197
|
+
* ```
|
|
1198
|
+
*/
|
|
1199
|
+
visibleTo(roles) {
|
|
1200
|
+
return {
|
|
1201
|
+
_type: "visibleTo",
|
|
1202
|
+
roles
|
|
1203
|
+
};
|
|
1204
|
+
},
|
|
1205
|
+
/**
|
|
1206
|
+
* Field is only writable by users with specified roles.
|
|
1207
|
+
* All users can still read the field. Users without the role
|
|
1208
|
+
* have the field silently stripped from write operations.
|
|
1209
|
+
*
|
|
1210
|
+
* @example
|
|
1211
|
+
* ```typescript
|
|
1212
|
+
* fields: { role: fields.writableBy(['admin']) }
|
|
1213
|
+
* ```
|
|
1214
|
+
*/
|
|
1215
|
+
writableBy(roles) {
|
|
1216
|
+
return {
|
|
1217
|
+
_type: "writableBy",
|
|
1218
|
+
roles
|
|
1219
|
+
};
|
|
1220
|
+
},
|
|
1221
|
+
/**
|
|
1222
|
+
* Field is redacted (replaced with a placeholder) for specified roles.
|
|
1223
|
+
* Other users see the real value.
|
|
1224
|
+
*
|
|
1225
|
+
* @param roles - Roles that see the redacted value
|
|
1226
|
+
* @param redactValue - Replacement value (default: '***')
|
|
1227
|
+
*
|
|
1228
|
+
* @example
|
|
1229
|
+
* ```typescript
|
|
1230
|
+
* fields: {
|
|
1231
|
+
* email: fields.redactFor(['viewer']),
|
|
1232
|
+
* ssn: fields.redactFor(['basic'], '***-**-****'),
|
|
1233
|
+
* }
|
|
1234
|
+
* ```
|
|
1235
|
+
*/
|
|
1236
|
+
redactFor(roles, redactValue = "***") {
|
|
1237
|
+
return {
|
|
1238
|
+
_type: "redactFor",
|
|
1239
|
+
roles,
|
|
1240
|
+
redactValue
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
};
|
|
1244
|
+
/**
|
|
1245
|
+
* Apply field-level READ permissions to a response object.
|
|
1246
|
+
* Strips hidden fields, enforces visibility, and applies redaction.
|
|
1247
|
+
*
|
|
1248
|
+
* @param data - The response object (mutated in place for performance)
|
|
1249
|
+
* @param fieldPermissions - Field permission map from resource config
|
|
1250
|
+
* @param userRoles - Current user's roles (empty array for unauthenticated)
|
|
1251
|
+
* @returns The filtered object
|
|
1252
|
+
*/
|
|
1253
|
+
function applyFieldReadPermissions(data, fieldPermissions, userRoles) {
|
|
1254
|
+
if (!data || typeof data !== "object") return data;
|
|
1255
|
+
const result = { ...isMongooseDoc(data) ? data.toObject() : data };
|
|
1256
|
+
for (const [field, perm] of Object.entries(fieldPermissions)) switch (perm._type) {
|
|
1257
|
+
case "hidden":
|
|
1258
|
+
delete result[field];
|
|
1259
|
+
break;
|
|
1260
|
+
case "visibleTo":
|
|
1261
|
+
if (!perm.roles?.some((r) => userRoles.includes(r))) delete result[field];
|
|
1262
|
+
break;
|
|
1263
|
+
case "redactFor":
|
|
1264
|
+
if (perm.roles?.some((r) => userRoles.includes(r))) result[field] = perm.redactValue ?? "***";
|
|
1265
|
+
break;
|
|
1266
|
+
case "writableBy": break;
|
|
1267
|
+
}
|
|
1268
|
+
return result;
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Apply field-level WRITE permissions to request body.
|
|
1272
|
+
*
|
|
1273
|
+
* Returns both the filtered body and the list of denied fields. Callers are
|
|
1274
|
+
* expected to reject the request when `deniedFields.length > 0` — silently
|
|
1275
|
+
* stripping fields hides misconfigurations and real attacks. See
|
|
1276
|
+
* `BodySanitizer` for the default policy.
|
|
1277
|
+
*
|
|
1278
|
+
* @param body - The request body (returns a new filtered copy)
|
|
1279
|
+
* @param fieldPermissions - Field permission map from resource config
|
|
1280
|
+
* @param userRoles - Current user's roles
|
|
1281
|
+
*/
|
|
1282
|
+
function applyFieldWritePermissions(body, fieldPermissions, userRoles) {
|
|
1283
|
+
const result = { ...body };
|
|
1284
|
+
const deniedFields = [];
|
|
1285
|
+
for (const [field, perm] of Object.entries(fieldPermissions)) switch (perm._type) {
|
|
1286
|
+
case "hidden":
|
|
1287
|
+
if (field in result) {
|
|
1288
|
+
deniedFields.push(field);
|
|
1289
|
+
delete result[field];
|
|
1290
|
+
}
|
|
1291
|
+
break;
|
|
1292
|
+
case "writableBy":
|
|
1293
|
+
if (field in result && !perm.roles?.some((r) => userRoles.includes(r))) {
|
|
1294
|
+
deniedFields.push(field);
|
|
1295
|
+
delete result[field];
|
|
1296
|
+
}
|
|
1297
|
+
break;
|
|
1298
|
+
}
|
|
1299
|
+
return {
|
|
1300
|
+
body: result,
|
|
1301
|
+
deniedFields
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* Resolve effective roles by merging global user roles with org-level roles.
|
|
1306
|
+
*
|
|
1307
|
+
* Global roles come from `req.user.role` (normalized via getUserRoles()).
|
|
1308
|
+
* Org roles come from `req.context.orgRoles` (set by BA adapter's org bridge).
|
|
1309
|
+
*
|
|
1310
|
+
* When no org context exists, returns global roles only — backward compatible.
|
|
1311
|
+
*/
|
|
1312
|
+
function resolveEffectiveRoles(userRoles, orgRoles) {
|
|
1313
|
+
if (orgRoles.length === 0) return [...userRoles];
|
|
1314
|
+
if (userRoles.length === 0) return [...orgRoles];
|
|
1315
|
+
return [...new Set([...userRoles, ...orgRoles])];
|
|
1316
|
+
}
|
|
1317
|
+
//#endregion
|
|
1090
1318
|
//#region src/permissions/roleHierarchy.ts
|
|
1091
1319
|
/**
|
|
1092
1320
|
* Create a role hierarchy from a parent → children map.
|
|
@@ -1262,4 +1490,4 @@ function readOnly(overrides) {
|
|
|
1262
1490
|
}, overrides);
|
|
1263
1491
|
}
|
|
1264
1492
|
//#endregion
|
|
1265
|
-
export {
|
|
1493
|
+
export { roles as A, allowPublic as C, requireAuth as D, not as E, applyPermissionResult as F, evaluateAndApplyPermission as I, normalizePermissionResult as L, requireAgentScope as M, requireDPoP as N, requireOwnership as O, requireMandate as P, allOf as S, denyAll as T, requireOrgMembership as _, presets_exports as a, requireServiceScope as b, readOnly as c, applyFieldWritePermissions as d, fields as f, requireOrgInScope as g, createOrgPermissions as h, ownerWithAdminBypass as i, when as j, requireRoles as k, createRoleHierarchy as l, createDynamicPermissionMatrix as m, authenticated as n, publicRead as o, resolveEffectiveRoles as p, fullPublic as r, publicReadAdminWrite as s, adminOnly as t, applyFieldReadPermissions as u, requireOrgRole as v, anyOf as w, requireTeamMembership as x, requireScopeContext as y };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { At as Guard, Ft as PipelineContext, It as PipelineStep, Lt as Transform, Mt as NextFunction, Nt as OperationFilter, Pt as PipelineConfig, _t as IControllerResponse, jt as Interceptor } from "../index-BtW7qYwa.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/pipeline/guard.d.ts
|
|
4
4
|
interface GuardOptions {
|
package/dist/pipeline/index.mjs
CHANGED