@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.
Files changed (166) hide show
  1. package/README.md +16 -12
  2. package/dist/{BaseController-swXruJ2_.mjs → BaseController-DX_T-bDB.mjs} +388 -423
  3. package/dist/EventTransport-CT_52aWU.d.mts +34 -0
  4. package/dist/EventTransport-DLWoUMHy.mjs +103 -0
  5. package/dist/{ResourceRegistry-DkAeAuTX.mjs → ResourceRegistry-CTERg_2x.mjs} +139 -66
  6. package/dist/audit/index.d.mts +2 -2
  7. package/dist/audit/index.mjs +1 -1
  8. package/dist/auth/audit.d.mts +199 -0
  9. package/dist/auth/audit.mjs +288 -0
  10. package/dist/auth/index.d.mts +3 -3
  11. package/dist/auth/index.mjs +117 -191
  12. package/dist/{betterAuthOpenApi-DwxtK3uG.mjs → betterAuthOpenApi--M_i87dQ.mjs} +1 -1
  13. package/dist/buildHandler-olo-gt94.mjs +610 -0
  14. package/dist/cache/index.mjs +3 -3
  15. package/dist/cli/commands/describe.d.mts +89 -13
  16. package/dist/cli/commands/describe.mjs +56 -2
  17. package/dist/cli/commands/docs.mjs +2 -2
  18. package/dist/cli/commands/generate.mjs +147 -48
  19. package/dist/cli/commands/init.d.mts +13 -0
  20. package/dist/cli/commands/init.mjs +130 -87
  21. package/dist/cli/commands/introspect.mjs +8 -1
  22. package/dist/context/index.mjs +1 -1
  23. package/dist/core/index.d.mts +3 -3
  24. package/dist/core/index.mjs +5 -5
  25. package/dist/core-D72ia0EH.mjs +1399 -0
  26. package/dist/{createActionRouter-CIKOcNA7.mjs → createActionRouter-CEvzKcy8.mjs} +7 -20
  27. package/dist/createAggregationRouter-CyecOxnO.mjs +114 -0
  28. package/dist/{createApp-C9bRrqlX.mjs → createApp-XX2-N0Yd.mjs} +28 -22
  29. package/dist/{defineEvent-D1Ky9M1D.mjs → defineEvent-D5h7EvAx.mjs} +1 -1
  30. package/dist/docs/index.d.mts +1 -1
  31. package/dist/docs/index.mjs +2 -2
  32. package/dist/{elevation-DOFoxoDs.mjs → elevation-DgoeTyfX.mjs} +1 -1
  33. package/dist/errorHandler-Bk-AGhkU.mjs +174 -0
  34. package/dist/errorHandler-DFr45ZG4.d.mts +45 -0
  35. package/dist/errors-j4aJm1Wg.mjs +184 -0
  36. package/dist/{eventPlugin-Cts2-Tfj.mjs → eventPlugin-CaKTYkYM.mjs} +28 -4
  37. package/dist/{eventPlugin-DDJoNEPL.d.mts → eventPlugin-qXpqTebY.d.mts} +24 -1
  38. package/dist/events/index.d.mts +6 -6
  39. package/dist/events/index.mjs +11 -35
  40. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  41. package/dist/events/transports/redis.d.mts +1 -1
  42. package/dist/factory/index.d.mts +2 -2
  43. package/dist/factory/index.mjs +2 -2
  44. package/dist/{fields-BRjxOAFp.d.mts → fields-COhcH3fk.d.mts} +23 -2
  45. package/dist/hooks/index.d.mts +1 -1
  46. package/dist/hooks/index.mjs +1 -1
  47. package/dist/idempotency/index.d.mts +1 -1
  48. package/dist/idempotency/index.mjs +1 -20
  49. package/dist/idempotency/redis.mjs +1 -1
  50. package/dist/{index-rHjXmJar.d.mts → index-BTqLEvhu.d.mts} +163 -3
  51. package/dist/{index-CXXRbnf8.d.mts → index-BtW7qYwa.d.mts} +660 -326
  52. package/dist/{index-m8mOOlFW.d.mts → index-Ds61mrJE.d.mts} +50 -4
  53. package/dist/{index-D9t1KNaB.d.mts → index-Dz5IKsrE.d.mts} +360 -219
  54. package/dist/index.d.mts +6 -7
  55. package/dist/index.mjs +9 -10
  56. package/dist/integrations/event-gateway.d.mts +1 -1
  57. package/dist/integrations/event-gateway.mjs +1 -1
  58. package/dist/integrations/index.d.mts +1 -1
  59. package/dist/integrations/mcp/index.d.mts +2 -2
  60. package/dist/integrations/mcp/index.mjs +1 -1
  61. package/dist/integrations/mcp/testing.d.mts +1 -1
  62. package/dist/integrations/mcp/testing.mjs +1 -1
  63. package/dist/integrations/streamline.d.mts +60 -11
  64. package/dist/integrations/streamline.mjs +75 -85
  65. package/dist/integrations/websocket.mjs +2 -8
  66. package/dist/middleware/index.d.mts +1 -1
  67. package/dist/middleware/index.mjs +2 -2
  68. package/dist/migrations/index.d.mts +23 -3
  69. package/dist/migrations/index.mjs +0 -7
  70. package/dist/{multipartBody-CvTR1Un6.mjs → multipartBody-BOvVSVCD.mjs} +11 -8
  71. package/dist/{openapi-D7G1V7ex.mjs → openapi-CiOMVW1p.mjs} +143 -13
  72. package/dist/org/index.d.mts +2 -2
  73. package/dist/org/index.mjs +1 -1
  74. package/dist/permissions/index.d.mts +3 -3
  75. package/dist/permissions/index.mjs +3 -3
  76. package/dist/{permissions-gd_aUWrR.mjs → permissions-ohQyv50e.mjs} +404 -176
  77. package/dist/{pipe-DVoIheVC.mjs → pipe-Zr0KXjQe.mjs} +1 -1
  78. package/dist/pipeline/index.d.mts +1 -1
  79. package/dist/pipeline/index.mjs +1 -1
  80. package/dist/plugins/index.d.mts +16 -31
  81. package/dist/plugins/index.mjs +33 -13
  82. package/dist/plugins/response-cache.mjs +1 -1
  83. package/dist/plugins/tracing-entry.mjs +1 -1
  84. package/dist/presets/filesUpload.d.mts +4 -4
  85. package/dist/presets/filesUpload.mjs +6 -9
  86. package/dist/presets/index.d.mts +1 -1
  87. package/dist/presets/index.mjs +1 -1
  88. package/dist/presets/multiTenant.d.mts +1 -1
  89. package/dist/presets/multiTenant.mjs +2 -2
  90. package/dist/presets/search.d.mts +2 -2
  91. package/dist/presets/search.mjs +6 -8
  92. package/dist/{presets-Z7P5w4gF.mjs → presets-BbkjdPeH.mjs} +6 -28
  93. package/dist/{queryCachePlugin-Bq6bO6vc.mjs → queryCachePlugin-m1XsgAIJ.mjs} +3 -3
  94. package/dist/{redis-stream-xTGxB2bm.d.mts → redis-stream-D6HzR1Z_.d.mts} +1 -1
  95. package/dist/registry/index.d.mts +1 -1
  96. package/dist/registry/index.mjs +2 -2
  97. package/dist/{replyHelpers-ByllIXXV.mjs → replyHelpers-CK-FNO8E.mjs} +3 -21
  98. package/dist/{resourceToTools-CxNmI6xF.mjs → resourceToTools-C5coh64w.mjs} +224 -71
  99. package/dist/{routerShared-BqLRb5l7.mjs → routerShared-D6_fEGHh.mjs} +40 -36
  100. package/dist/{schemaIR-Dy2p4MxS.mjs → schemaIR-7Vl611Qs.mjs} +1 -1
  101. package/dist/schemas/index.d.mts +100 -30
  102. package/dist/schemas/index.mjs +86 -29
  103. package/dist/scim/index.d.mts +264 -0
  104. package/dist/scim/index.mjs +963 -0
  105. package/dist/scope/index.d.mts +3 -3
  106. package/dist/scope/index.mjs +4 -4
  107. package/dist/{sse-V7aXc3bW.mjs → sse-Bz-5ZeTt.mjs} +1 -1
  108. package/dist/{store-helpers-Cp4uKC1U.mjs → store-helpers-BkIN9-vu.mjs} +1 -1
  109. package/dist/testing/index.d.mts +2 -8
  110. package/dist/testing/index.mjs +16 -24
  111. package/dist/types/index.d.mts +4 -4
  112. package/dist/{types-D7KpfiL1.d.mts → types-BvqwCCSx.d.mts} +73 -25
  113. package/dist/{types-DDyTPc6y.d.mts → types-CTYvcwHe.d.mts} +195 -1
  114. package/dist/{types-AOD8fxIw.mjs → types-C_s5moIu.mjs} +117 -1
  115. package/dist/{types-BQ9TJQNy.d.mts → types-DQHFc8PM.d.mts} +1 -1
  116. package/dist/utils/index.d.mts +2 -2
  117. package/dist/utils/index.mjs +5 -5
  118. package/dist/{utils-CcYTj09l.mjs → utils-_h9B3c57.mjs} +1269 -1334
  119. package/dist/{versioning-DsglKfM_.d.mts → versioning-DTTvc80y.d.mts} +1 -1
  120. package/package.json +24 -34
  121. package/skills/arc/SKILL.md +147 -51
  122. package/skills/arc/references/agent-auth.md +238 -0
  123. package/skills/arc/references/api-reference.md +187 -0
  124. package/skills/arc/references/auth.md +354 -7
  125. package/skills/arc/references/enterprise-auth.md +94 -0
  126. package/skills/arc/references/events.md +8 -6
  127. package/skills/arc/references/mcp.md +2 -2
  128. package/skills/arc/references/multi-tenancy.md +11 -2
  129. package/skills/arc/references/production.md +10 -9
  130. package/skills/arc/references/scim.md +247 -0
  131. package/skills/arc/references/testing.md +1 -1
  132. package/skills/arc-code-review/SKILL.md +141 -0
  133. package/skills/arc-code-review/references/anti-patterns.md +911 -0
  134. package/skills/arc-code-review/references/arc-cheatsheet.md +380 -0
  135. package/skills/arc-code-review/references/migration-recipes.md +700 -0
  136. package/skills/arc-code-review/references/mongokit-migration.md +386 -0
  137. package/skills/arc-code-review/references/scaffolding.md +230 -0
  138. package/skills/arc-code-review/references/severity.md +127 -0
  139. package/dist/EventTransport-BFQjw9pB.mjs +0 -133
  140. package/dist/EventTransport-CYNUXdCJ.d.mts +0 -293
  141. package/dist/adapters/index.d.mts +0 -3
  142. package/dist/adapters/index.mjs +0 -2
  143. package/dist/adapters-DUUiiimH.mjs +0 -964
  144. package/dist/auth/mongoose.d.mts +0 -191
  145. package/dist/auth/mongoose.mjs +0 -73
  146. package/dist/core-CbcQRIch.mjs +0 -1054
  147. package/dist/errorHandler-BQm8ZxTK.mjs +0 -173
  148. package/dist/errorHandler-DEWmGWPz.d.mts +0 -114
  149. package/dist/errors-D5c-5BJL.mjs +0 -232
  150. package/dist/index-Rg8axYPz.d.mts +0 -370
  151. /package/dist/{HookSystem-CGsMd6oK.mjs → HookSystem-Iiebom92.mjs} +0 -0
  152. /package/dist/{actionPermissions-sUUKDhtP.mjs → actionPermissions-CyUkQu6O.mjs} +0 -0
  153. /package/dist/{caching-CheW3m-S.mjs → caching-SM8gghN6.mjs} +0 -0
  154. /package/dist/{constants-BhY1OHoH.mjs → constants-Cxde4rpC.mjs} +0 -0
  155. /package/dist/{elevation-BQQXZ_VR.d.mts → elevation-BXOWoGCF.d.mts} +0 -0
  156. /package/dist/{keys-CARyUjiR.mjs → keys-CGcCbNyu.mjs} +0 -0
  157. /package/dist/{loadResources-CPpkyKfM.mjs → loadResources-DBMQg_Aj.mjs} +0 -0
  158. /package/dist/{memory-DikHSvWa.mjs → memory-UBydS5ku.mjs} +0 -0
  159. /package/dist/{metrics-Csh4nsvv.mjs → metrics-Qnvwc-LQ.mjs} +0 -0
  160. /package/dist/{pluralize-CWP6MB39.mjs → pluralize-DQgqgifU.mjs} +0 -0
  161. /package/dist/{registry-D63ee7fl.mjs → registry-I-ogLgL9.mjs} +0 -0
  162. /package/dist/{requestContext-C5XeK3VA.mjs → requestContext-SSaaTgW8.mjs} +0 -0
  163. /package/dist/{schemaConverter-B0oKLuqI.mjs → schemaConverter-De34B1ZG.mjs} +0 -0
  164. /package/dist/{typeGuards-CcFZXgU7.mjs → typeGuards-BzkXkvVv.mjs} +0 -0
  165. /package/dist/{types-DV9WDfeg.mjs → types-D57iXYb8.mjs} +0 -0
  166. /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 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
- import { t as getUserRoles } from "./types-DV9WDfeg.mjs";
4
- import { t as MemoryCacheStore } from "./memory-DikHSvWa.mjs";
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
- success: false,
248
- error: "Permission denied"
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
- reply.code(context.user ? 403 : 401).send({
257
- success: false,
258
- error: reason
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 { 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 };
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 { r as ForbiddenError } from "./errors-D5c-5BJL.mjs";
1
+ import { r as ForbiddenError } from "./errors-j4aJm1Wg.mjs";
2
2
  //#region src/pipeline/pipe.ts
3
3
  /**
4
4
  * Compose pipeline steps into an ordered array.
@@ -1,4 +1,4 @@
1
- import { Ct as OperationFilter, Dt as Transform, Et as PipelineStep, St as NextFunction, Tt as PipelineContext, _t as IControllerResponse, bt as Guard, wt as PipelineConfig, xt as Interceptor } from "../index-CXXRbnf8.mjs";
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 {
@@ -1,4 +1,4 @@
1
- import { n as pipe, t as executePipeline } from "../pipe-DVoIheVC.mjs";
1
+ import { n as pipe, t as executePipeline } from "../pipe-Zr0KXjQe.mjs";
2
2
  //#region src/pipeline/guard.ts
3
3
  /**
4
4
  * Create a named guard.