@classytic/arc 2.23.0 → 2.28.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.
Files changed (147) hide show
  1. package/README.md +2 -0
  2. package/dist/{BaseController-BQ8KNvWC.mjs → BaseController-B65zbz9m.mjs} +94 -23
  3. package/dist/QueryCache-DOMT_iq7.d.mts +72 -0
  4. package/dist/actionPermissions-CAphcmUG.mjs +24 -0
  5. package/dist/{applyPermissionResult-Tw4iXtlf.mjs → applyPermissionResult-BnyVE9eD.mjs} +10 -0
  6. package/dist/audit/index.d.mts +32 -3
  7. package/dist/audit/index.mjs +7 -4
  8. package/dist/auth/index.d.mts +2 -2
  9. package/dist/auth/index.mjs +263 -259
  10. package/dist/{buildHandler-D_2ULWxZ.mjs → buildHandler-Dtwd8fdz.mjs} +1 -1
  11. package/dist/cache/index.d.mts +41 -3
  12. package/dist/cache/index.mjs +33 -4
  13. package/dist/cleanup/index.d.mts +313 -0
  14. package/dist/cleanup/index.mjs +1267 -0
  15. package/dist/cli/commands/describe.d.mts +1 -1
  16. package/dist/cli/commands/docs.mjs +1 -1
  17. package/dist/cli/commands/generate.mjs +1 -1
  18. package/dist/cli/commands/init.mjs +1 -1
  19. package/dist/cli/index.mjs +2 -2
  20. package/dist/context/index.mjs +2 -64
  21. package/dist/core/index.d.mts +3 -3
  22. package/dist/core/index.mjs +5 -4
  23. package/dist/core-BGa8TXJR.mjs +221 -0
  24. package/dist/{createActionRouter-BuOv-m4Y.mjs → createActionRouter-C-N7z2EL.mjs} +3 -2
  25. package/dist/{createAggregationRouter-CgJv_BU_.mjs → createAggregationRouter-wzwFa_dW.mjs} +3 -3
  26. package/dist/{createApp-CjOeWJ9_.mjs → createApp-Du5G54TM.mjs} +187 -96
  27. package/dist/{core-DQ6rDTyH.mjs → defineResource-2zvVVpxg.mjs} +297 -230
  28. package/dist/docs/index.d.mts +1 -1
  29. package/dist/docs/index.mjs +2 -2
  30. package/dist/{elevation-COpYKt2h.mjs → elevation-CIz1GjGm.mjs} +4 -3
  31. package/dist/{eventPlugin-A6EbthXR.d.mts → eventPlugin-Bg3cP2_u.d.mts} +38 -3
  32. package/dist/{eventPlugin-DJG2D7sy.mjs → eventPlugin-DrQ_FesK.mjs} +22 -4
  33. package/dist/events/index.d.mts +83 -314
  34. package/dist/events/index.mjs +183 -95
  35. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  36. package/dist/events/transports/redis-stream-entry.mjs +24 -2
  37. package/dist/factory/index.d.mts +5 -5
  38. package/dist/factory/index.mjs +30 -8
  39. package/dist/{fastify-augmentation-g_f9omSc.d.mts → fastify-augmentation-OzYGIiU1.d.mts} +26 -2
  40. package/dist/{generate-usRSdQmP.mjs → generate-D0wZupBg.mjs} +1 -1
  41. package/dist/headers-BvlQSDMB.mjs +45 -0
  42. package/dist/health-CB_m4S32.mjs +377 -0
  43. package/dist/hooks/index.d.mts +1 -1
  44. package/dist/idempotency/index.mjs +27 -3
  45. package/dist/{index-wMoWBLkn.d.mts → index-BMnbzvtO.d.mts} +3 -3
  46. package/dist/{integrations/jobs.d.mts → index-CG4yqMDB.d.mts} +82 -2
  47. package/dist/index-CZY4EGga.d.mts +107 -0
  48. package/dist/{index-CC5ruDGh.d.mts → index-CjVg-Epg.d.mts} +37 -5
  49. package/dist/{index-BBsvQt7U.d.mts → index-DPRH0Cme.d.mts} +36 -106
  50. package/dist/index.d.mts +6 -6
  51. package/dist/index.mjs +7 -6
  52. package/dist/{init-CjO4p_q1.mjs → init-tlRSt667.mjs} +206 -208
  53. package/dist/integrations/event-gateway.d.mts +1 -1
  54. package/dist/integrations/event-gateway.mjs +1 -1
  55. package/dist/integrations/index.d.mts +3 -3
  56. package/dist/integrations/jobs/index.d.mts +2 -0
  57. package/dist/integrations/{jobs.mjs → jobs/index.mjs} +110 -67
  58. package/dist/integrations/mcp/index.d.mts +4 -4
  59. package/dist/integrations/mcp/index.mjs +2 -2
  60. package/dist/integrations/mcp/testing.d.mts +1 -1
  61. package/dist/integrations/mcp/testing.mjs +1 -1
  62. package/dist/integrations/webhooks.d.mts +37 -30
  63. package/dist/integrations/webhooks.mjs +51 -62
  64. package/dist/integrations/websocket-redis.d.mts +1 -1
  65. package/dist/integrations/websocket.d.mts +1 -1
  66. package/dist/integrations/websocket.mjs +41 -14
  67. package/dist/{interface-C1_WLTMK.d.mts → interface-BGZCzOGL.d.mts} +24 -31
  68. package/dist/{mcpPlugin-BtzIL_kM.mjs → mcpPlugin-Cs6nRGCt.mjs} +12 -10
  69. package/dist/{memory-CLXA2VeJ.mjs → memory-CmnzXE5Z.mjs} +29 -0
  70. package/dist/middleware/index.d.mts +17 -1
  71. package/dist/middleware/index.mjs +1 -1
  72. package/dist/migrations/index.d.mts +94 -44
  73. package/dist/migrations/index.mjs +196 -44
  74. package/dist/module-_-M7C5ES.mjs +29 -0
  75. package/dist/{multipartBody-CHEQSZ6G.mjs → multipartBody-D0eBvkeq.mjs} +17 -0
  76. package/dist/{openapi-jSDx-VDv.mjs → openapi-BLV4OMXb.mjs} +3 -3
  77. package/dist/permissions/index.d.mts +2 -2
  78. package/dist/permissions/index.mjs +3 -3
  79. package/dist/{permissions-tbxFHxjZ.mjs → permissions-BrtiBKGs.mjs} +229 -229
  80. package/dist/pipeline/index.d.mts +1 -1
  81. package/dist/{actionPermissions-lszoJle1.mjs → pipeline-BGRnV-To.mjs} +4 -27
  82. package/dist/plugins/index.d.mts +6 -77
  83. package/dist/plugins/index.mjs +11 -95
  84. package/dist/plugins/response-cache.mjs +1 -0
  85. package/dist/plugins/tracing-entry.mjs +1 -1
  86. package/dist/{index-Ba10fdKp.d.mts → preloadResources-CM4uUaFv.d.mts} +51 -50
  87. package/dist/preloadResources-DSY5HGpS.mjs +45 -0
  88. package/dist/presets/filesUpload.d.mts +3 -3
  89. package/dist/presets/filesUpload.mjs +3 -3
  90. package/dist/presets/index.d.mts +2 -1
  91. package/dist/presets/index.mjs +1 -1
  92. package/dist/presets/multiTenant.d.mts +1 -1
  93. package/dist/presets/multiTenant.mjs +1 -1
  94. package/dist/presets/search.d.mts +2 -2
  95. package/dist/presets/search.mjs +1 -1
  96. package/dist/{presets-BOe4Ugkl.mjs → presets-DwFDVKP0.mjs} +2 -2
  97. package/dist/{queryCachePlugin-DzqLN904.mjs → queryCachePlugin-DSGuSVLW.mjs} +39 -8
  98. package/dist/{queryCachePlugin-CfYWq-Ak.d.mts → queryCachePlugin-br4otiqF.d.mts} +2 -2
  99. package/dist/{registerRoute-CWO6mdYe.mjs → rateLimit-DrED-C4v.mjs} +2 -109
  100. package/dist/{realtime-I8OrfnMR.mjs → realtime-DEuDu9io.mjs} +3 -3
  101. package/dist/{redis-stream-iVR2l7aS.d.mts → redis-stream-uIaauNpZ.d.mts} +22 -0
  102. package/dist/registerRoute-DTp18Ah_.mjs +108 -0
  103. package/dist/registry/index.d.mts +2 -2
  104. package/dist/registry/index.mjs +1 -1
  105. package/dist/{registry-vcOCtPij.mjs → registry-9km_w1aP.mjs} +15 -0
  106. package/dist/renewingLease-3cuQH7h8.mjs +44 -0
  107. package/dist/{requestContext-BZKOr13H.mjs → requestContext-MrlJwECf.mjs} +2 -2
  108. package/dist/schedules-CjOd2MG9.mjs +113 -0
  109. package/dist/schemas/index.d.mts +67 -67
  110. package/dist/schemas/index.mjs +2 -3
  111. package/dist/scim/index.mjs +1 -2
  112. package/dist/scope/index.d.mts +2 -2
  113. package/dist/scope/index.mjs +4 -4
  114. package/dist/{scope-LONQ3mfZ.mjs → scope-DfaPntpo.mjs} +4 -5
  115. package/dist/{sse-0u4P4-OI.mjs → sse-BQ1w0dfl.mjs} +1 -1
  116. package/dist/testing/cleanupStoreContract.d.mts +10 -0
  117. package/dist/testing/cleanupStoreContract.mjs +375 -0
  118. package/dist/testing/index.d.mts +5 -3
  119. package/dist/testing/index.mjs +4 -2
  120. package/dist/testing/outboxStoreContract.d.mts +13 -0
  121. package/dist/testing/outboxStoreContract.mjs +247 -0
  122. package/dist/transactionContext-PNdoyLLm.mjs +64 -0
  123. package/dist/types/index.d.mts +4 -4
  124. package/dist/{types-bHblZ9YN.mjs → types-CSyCxl2V.mjs} +38 -7
  125. package/dist/types-C_ZZRcD6.d.mts +450 -0
  126. package/dist/{module-B4u10iOJ.d.mts → types-Cu6vVJmh.d.mts} +196 -60
  127. package/dist/{types-DH0GupI7.d.mts → types-DjC-KUCz.d.mts} +118 -20
  128. package/dist/{types-Csxga7d3.d.mts → types-Dmw-6Qeo.d.mts} +96 -4
  129. package/dist/{types-5OuqeTDX.d.mts → types-QFGc3Cjv.d.mts} +1 -1
  130. package/dist/usage/index.d.mts +3 -52
  131. package/dist/usage/index.mjs +0 -0
  132. package/dist/userHelpers-Cg_y3fBz.mjs +29 -0
  133. package/dist/utils/index.d.mts +58 -85
  134. package/dist/utils/index.mjs +4 -2
  135. package/dist/{utils-CZyeRXTh.mjs → utils-taUVD4p3.mjs} +47 -337
  136. package/dist/{validate-C2mGgSXg.mjs → validate-Bk44F2yT.mjs} +1 -1
  137. package/dist/{versioning-D6PvCwpq.d.mts → versioning-DXgY5BUm.d.mts} +11 -29
  138. package/dist/{versioning-Dk-71iIz.mjs → versioning-gKWyryT3.mjs} +8 -1
  139. package/dist/{websocket-uuWqM6UY.d.mts → websocket-_Pd0CgR0.d.mts} +15 -1
  140. package/package.json +541 -513
  141. package/skills/arc/references/api-reference.md +3 -1
  142. package/skills/arc/references/integrations.md +20 -11
  143. package/dist/QueryCache-Z1B38koh.d.mts +0 -40
  144. package/dist/betterAuthOpenApi-CV1lxpRX.mjs +0 -2
  145. package/dist/health-ScyjIVo5.mjs +0 -202
  146. package/dist/requestId-5BhTVzBv.mjs +0 -113
  147. package/dist/types-XTApa8b6.mjs +0 -24
@@ -1,233 +1,8 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
2
- import { C as isService, S as isOrgInScope, a as getDPoPJkt, b as isElevated, d as getScopeContext, f as getScopeContextMap, g as getUserId, m as getTeamId, o as getMandate, p as getServiceScopes, u as getRequestScope, v as hasOrgAccess, x as isMember } from "./types-bHblZ9YN.mjs";
3
- import { t as getUserRoles } from "./types-XTApa8b6.mjs";
4
- import { t as MemoryCacheStore } from "./memory-CLXA2VeJ.mjs";
2
+ import { n as getUserRoles } from "./userHelpers-Cg_y3fBz.mjs";
3
+ import { C as isService, S as isOrgInScope, a as getDPoPJkt, b as isElevated, d as getScopeContext, f as getScopeContextMap, g as getUserId, m as getTeamId, o as getMandate, p as getServiceScopes, u as getRequestScope, v as hasOrgAccess, x as isMember } from "./types-CSyCxl2V.mjs";
4
+ import { t as MemoryCacheStore } from "./memory-CmnzXE5Z.mjs";
5
5
  import { randomUUID } from "node:crypto";
6
- //#region src/permissions/agent.ts
7
- /**
8
- * Agent-Auth Permission Helpers — DPoP + capability mandates for AI-agent flows
9
- *
10
- * Three checks for the 2025 agent-authorization stack (AP2 / Stripe x402 /
11
- * MCP authorization / RFC 9700 / RFC 9449 / RFC 9728):
12
- *
13
- * - `requireDPoP()` — token must be sender-constrained (RFC 9449)
14
- * - `requireMandate(cap, …)` — capability mandate must authorize this action
15
- * - `requireAgentScope(opts)` — composite gate: service identity + mandate + DPoP
16
- *
17
- * Arc reads `request.scope.mandate` and `request.scope.dpopJkt` populated by
18
- * your `authenticate` callback. Arc does **not** parse mandate JWTs/VCs or
19
- * verify DPoP proofs — that's a 1-2 line `jose` call in your authenticator.
20
- * Arc validates *what's already proved* against the action being attempted.
21
- *
22
- * @example
23
- * ```typescript
24
- * import { requireDPoP, requireMandate, requireAgentScope } from '@classytic/arc/permissions';
25
- *
26
- * defineResource({
27
- * name: 'invoice',
28
- * permissions: {
29
- * pay: requireAgentScope({
30
- * capability: 'payment.charge',
31
- * requireDPoP: true,
32
- * validateAmount: (ctx, mandate) =>
33
- * typeof ctx.data?.amount === 'number' && ctx.data.amount <= (mandate.cap ?? 0),
34
- * audience: (ctx) => `invoice:${ctx.params?.id}`,
35
- * }),
36
- * },
37
- * });
38
- * ```
39
- */
40
- /** Default grace window for mandate `expiresAt` — accommodates clock skew. */
41
- const DEFAULT_TTL_GRACE_MS = 3e4;
42
- /**
43
- * Require a sender-constrained credential — the inbound token MUST carry a
44
- * DPoP proof (RFC 9449) bound to a known key. Arc reads `scope.dpopJkt` (the
45
- * JWK SHA-256 thumbprint per RFC 7638); your `authenticate` function performs
46
- * the cryptographic `jose.dpop.verify(...)` and sets the field on success.
47
- *
48
- * **Pass behavior:**
49
- * - `service` scope where `dpopJkt` is set → grant
50
- * - `elevated` scope → grant (platform admin bypass)
51
- * - Anything else → deny with a clear reason
52
- *
53
- * Use for high-value endpoints where bearer-token replay must be impossible:
54
- * payment charges, data exports, account-takeover-class admin actions.
55
- *
56
- * @example
57
- * ```typescript
58
- * permissions: { charge: allOf(requireServiceScope('payment.write'), requireDPoP()) }
59
- * ```
60
- */
61
- function requireDPoP() {
62
- const check = (ctx) => {
63
- const scope = getRequestScope(ctx.request);
64
- if (isElevated(scope)) return true;
65
- if (!isService(scope)) return {
66
- granted: false,
67
- reason: "DPoP-bound service identity required. Configure your authenticate callback to set scope.dpopJkt after verifying the DPoP proof header (RFC 9449)."
68
- };
69
- if (!getDPoPJkt(scope)) return {
70
- granted: false,
71
- reason: "Sender-constrained credential required (DPoP). Inbound token is bearer; replay-resistance is mandatory on this endpoint."
72
- };
73
- return true;
74
- };
75
- check._dpopRequired = true;
76
- return check;
77
- }
78
- /**
79
- * Require a capability mandate (AP2 / x402 / MCP authorization) that
80
- * authorizes the action being attempted.
81
- *
82
- * The mandate is set on `request.scope.mandate` by your authenticate function
83
- * after verifying the inbound mandate JWT/VC. This check validates that the
84
- * presented mandate covers the requested capability, hasn't expired, is bound
85
- * to the right resource (when `audience` opt is set), and respects the
86
- * mandate's numeric ceiling (when `validateAmount` opt is set).
87
- *
88
- * **Pass behavior:**
89
- * - `elevated` scope → grant unless `noElevatedBypass: true`
90
- * - `service` scope with mandate matching `capability`, not expired, and
91
- * passing `validateAmount` + `audience` checks → grant
92
- * - Anything else → deny with a precise reason
93
- *
94
- * Pair with `requireDPoP()` for replay-resistance, or use the bundled
95
- * `requireAgentScope(...)` to declare both at once.
96
- *
97
- * @example
98
- * ```typescript
99
- * // Single payment charge — amount must fit the mandate's cap
100
- * permissions: {
101
- * pay: requireMandate('payment.charge', {
102
- * validateAmount: (ctx, m) => (ctx.data as { amount: number }).amount <= (m.cap ?? 0),
103
- * audience: (ctx) => `invoice:${ctx.params?.id}`,
104
- * }),
105
- * }
106
- *
107
- * // Boolean capability — presence of mandate is the gate
108
- * permissions: {
109
- * exportData: requireMandate('data.export'),
110
- * }
111
- * ```
112
- */
113
- function requireMandate(capability, opts = {}) {
114
- if (!capability || typeof capability !== "string") throw new Error("requireMandate(capability) requires a non-empty capability string (e.g. 'payment.charge')");
115
- const ttlGrace = opts.ttlGraceMs ?? DEFAULT_TTL_GRACE_MS;
116
- const noElevatedBypass = opts.noElevatedBypass === true;
117
- const check = (ctx) => {
118
- const scope = getRequestScope(ctx.request);
119
- if (isElevated(scope) && !noElevatedBypass) return true;
120
- const mandate = getMandate(scope);
121
- if (!mandate) return {
122
- granted: false,
123
- reason: `Capability mandate required (${capability}). Configure your authenticate callback to populate request.scope.mandate after verifying the mandate JWT/VC.`
124
- };
125
- if (mandate.capability !== capability) return {
126
- granted: false,
127
- reason: `Mandate authorizes "${mandate.capability}", not "${capability}"`
128
- };
129
- if (mandate.expiresAt !== void 0 && Date.now() > mandate.expiresAt + ttlGrace) return {
130
- granted: false,
131
- reason: `Mandate expired at ${new Date(mandate.expiresAt).toISOString()}`
132
- };
133
- if (opts.audience) {
134
- const required = typeof opts.audience === "function" ? opts.audience(ctx) : opts.audience;
135
- if (required && mandate.audience && mandate.audience !== required) return {
136
- granted: false,
137
- reason: `Mandate is bound to "${mandate.audience}", not "${required}"`
138
- };
139
- if (required && !mandate.audience) return {
140
- granted: false,
141
- reason: `Mandate must be bound to "${required}" (no audience claim)`
142
- };
143
- }
144
- if (opts.validateAmount) {
145
- const result = opts.validateAmount(ctx, mandate);
146
- if (result !== true) return {
147
- granted: false,
148
- reason: typeof result === "string" ? result : `Action exceeds mandate cap ${mandate.cap}${mandate.currency ? ` ${mandate.currency}` : ""}`
149
- };
150
- }
151
- return true;
152
- };
153
- check._mandateCapability = capability;
154
- return check;
155
- }
156
- /**
157
- * Composite gate for AI-agent / M2M flows on protected resources.
158
- *
159
- * Bundles the three things every high-value agent endpoint needs:
160
- * 1. **Service identity** — `scope.kind === 'service'` with `clientId`
161
- * 2. **Capability mandate** — narrows what *this request* may do
162
- * 3. **DPoP binding** — credential cannot be replayed from a different key
163
- *
164
- * Use this instead of hand-composing `allOf(requireServiceScope(...),
165
- * requireMandate(...), requireDPoP())` — fewer ways to misconfigure, one
166
- * meta-tag downstream tools (audit, MCP, OpenAPI) can read.
167
- *
168
- * @example
169
- * ```typescript
170
- * import { requireAgentScope } from '@classytic/arc/permissions';
171
- *
172
- * defineResource({
173
- * name: 'invoice',
174
- * actions: {
175
- * pay: {
176
- * handler: payInvoice,
177
- * permissions: requireAgentScope({
178
- * capability: 'payment.charge',
179
- * scopes: ['payment.write'],
180
- * requireDPoP: true,
181
- * audience: (ctx) => `invoice:${ctx.params?.id}`,
182
- * validateAmount: (ctx, m) => (ctx.data as { amount: number }).amount <= (m.cap ?? 0),
183
- * }),
184
- * },
185
- * },
186
- * });
187
- * ```
188
- */
189
- function requireAgentScope(opts) {
190
- const { capability, scopes, requireDPoP: needsDPoP = true, ...mandateOpts } = opts;
191
- if (!capability) throw new Error("requireAgentScope({ capability }) is required");
192
- const mandateCheck = requireMandate(capability, mandateOpts);
193
- const dpopCheck = needsDPoP ? requireDPoP() : null;
194
- const requiredScopes = scopes && scopes.length > 0 ? [...scopes] : null;
195
- const check = async (ctx) => {
196
- const scope = getRequestScope(ctx.request);
197
- if (isElevated(scope) && !mandateOpts.noElevatedBypass) return true;
198
- if (!isService(scope)) return {
199
- granted: false,
200
- reason: "Service identity required (machine principal). Agent flows must authenticate as a service, not a logged-in user."
201
- };
202
- if (requiredScopes) {
203
- const granted = scope.scopes ?? [];
204
- if (!requiredScopes.some((s) => granted.includes(s))) return {
205
- granted: false,
206
- reason: `Service identity is missing required OAuth scope(s): ${requiredScopes.join(", ")}`
207
- };
208
- }
209
- const mandateResult = await mandateCheck(ctx);
210
- if (mandateResult !== true) return typeof mandateResult === "object" ? mandateResult : {
211
- granted: false,
212
- reason: "Mandate check failed"
213
- };
214
- if (dpopCheck) {
215
- const dpopResult = await dpopCheck(ctx);
216
- if (dpopResult !== true) return typeof dpopResult === "object" ? dpopResult : {
217
- granted: false,
218
- reason: "DPoP binding required"
219
- };
220
- }
221
- return true;
222
- };
223
- check._agentScope = {
224
- capability,
225
- scopes: requiredScopes ?? void 0,
226
- dpop: needsDPoP
227
- };
228
- return check;
229
- }
230
- //#endregion
231
6
  //#region src/permissions/core.ts
232
7
  /**
233
8
  * Normalize a `string | [readonly string[]]` rest-args tuple into a single
@@ -517,6 +292,231 @@ function when(condition) {
517
292
  };
518
293
  }
519
294
  //#endregion
295
+ //#region src/permissions/agent.ts
296
+ /**
297
+ * Agent-Auth Permission Helpers — DPoP + capability mandates for AI-agent flows
298
+ *
299
+ * Three checks for the 2025 agent-authorization stack (AP2 / Stripe x402 /
300
+ * MCP authorization / RFC 9700 / RFC 9449 / RFC 9728):
301
+ *
302
+ * - `requireDPoP()` — token must be sender-constrained (RFC 9449)
303
+ * - `requireMandate(cap, …)` — capability mandate must authorize this action
304
+ * - `requireAgentScope(opts)` — composite gate: service identity + mandate + DPoP
305
+ *
306
+ * Arc reads `request.scope.mandate` and `request.scope.dpopJkt` populated by
307
+ * your `authenticate` callback. Arc does **not** parse mandate JWTs/VCs or
308
+ * verify DPoP proofs — that's a 1-2 line `jose` call in your authenticator.
309
+ * Arc validates *what's already proved* against the action being attempted.
310
+ *
311
+ * @example
312
+ * ```typescript
313
+ * import { requireDPoP, requireMandate, requireAgentScope } from '@classytic/arc/permissions';
314
+ *
315
+ * defineResource({
316
+ * name: 'invoice',
317
+ * permissions: {
318
+ * pay: requireAgentScope({
319
+ * capability: 'payment.charge',
320
+ * requireDPoP: true,
321
+ * validateAmount: (ctx, mandate) =>
322
+ * typeof ctx.data?.amount === 'number' && ctx.data.amount <= (mandate.cap ?? 0),
323
+ * audience: (ctx) => `invoice:${ctx.params?.id}`,
324
+ * }),
325
+ * },
326
+ * });
327
+ * ```
328
+ */
329
+ /** Default grace window for mandate `expiresAt` — accommodates clock skew. */
330
+ const DEFAULT_TTL_GRACE_MS = 3e4;
331
+ /**
332
+ * Require a sender-constrained credential — the inbound token MUST carry a
333
+ * DPoP proof (RFC 9449) bound to a known key. Arc reads `scope.dpopJkt` (the
334
+ * JWK SHA-256 thumbprint per RFC 7638); your `authenticate` function performs
335
+ * the cryptographic `jose.dpop.verify(...)` and sets the field on success.
336
+ *
337
+ * **Pass behavior:**
338
+ * - `service` scope where `dpopJkt` is set → grant
339
+ * - `elevated` scope → grant (platform admin bypass)
340
+ * - Anything else → deny with a clear reason
341
+ *
342
+ * Use for high-value endpoints where bearer-token replay must be impossible:
343
+ * payment charges, data exports, account-takeover-class admin actions.
344
+ *
345
+ * @example
346
+ * ```typescript
347
+ * permissions: { charge: allOf(requireServiceScope('payment.write'), requireDPoP()) }
348
+ * ```
349
+ */
350
+ function requireDPoP() {
351
+ const check = (ctx) => {
352
+ const scope = getRequestScope(ctx.request);
353
+ if (isElevated(scope)) return true;
354
+ if (!isService(scope)) return {
355
+ granted: false,
356
+ reason: "DPoP-bound service identity required. Configure your authenticate callback to set scope.dpopJkt after verifying the DPoP proof header (RFC 9449)."
357
+ };
358
+ if (!getDPoPJkt(scope)) return {
359
+ granted: false,
360
+ reason: "Sender-constrained credential required (DPoP). Inbound token is bearer; replay-resistance is mandatory on this endpoint."
361
+ };
362
+ return true;
363
+ };
364
+ check._dpopRequired = true;
365
+ return check;
366
+ }
367
+ /**
368
+ * Require a capability mandate (AP2 / x402 / MCP authorization) that
369
+ * authorizes the action being attempted.
370
+ *
371
+ * The mandate is set on `request.scope.mandate` by your authenticate function
372
+ * after verifying the inbound mandate JWT/VC. This check validates that the
373
+ * presented mandate covers the requested capability, hasn't expired, is bound
374
+ * to the right resource (when `audience` opt is set), and respects the
375
+ * mandate's numeric ceiling (when `validateAmount` opt is set).
376
+ *
377
+ * **Pass behavior:**
378
+ * - `elevated` scope → grant unless `noElevatedBypass: true`
379
+ * - `service` scope with mandate matching `capability`, not expired, and
380
+ * passing `validateAmount` + `audience` checks → grant
381
+ * - Anything else → deny with a precise reason
382
+ *
383
+ * Pair with `requireDPoP()` for replay-resistance, or use the bundled
384
+ * `requireAgentScope(...)` to declare both at once.
385
+ *
386
+ * @example
387
+ * ```typescript
388
+ * // Single payment charge — amount must fit the mandate's cap
389
+ * permissions: {
390
+ * pay: requireMandate('payment.charge', {
391
+ * validateAmount: (ctx, m) => (ctx.data as { amount: number }).amount <= (m.cap ?? 0),
392
+ * audience: (ctx) => `invoice:${ctx.params?.id}`,
393
+ * }),
394
+ * }
395
+ *
396
+ * // Boolean capability — presence of mandate is the gate
397
+ * permissions: {
398
+ * exportData: requireMandate('data.export'),
399
+ * }
400
+ * ```
401
+ */
402
+ function requireMandate(capability, opts = {}) {
403
+ if (!capability || typeof capability !== "string") throw new Error("requireMandate(capability) requires a non-empty capability string (e.g. 'payment.charge')");
404
+ const ttlGrace = opts.ttlGraceMs ?? DEFAULT_TTL_GRACE_MS;
405
+ const noElevatedBypass = opts.noElevatedBypass === true;
406
+ const check = (ctx) => {
407
+ const scope = getRequestScope(ctx.request);
408
+ if (isElevated(scope) && !noElevatedBypass) return true;
409
+ const mandate = getMandate(scope);
410
+ if (!mandate) return {
411
+ granted: false,
412
+ reason: `Capability mandate required (${capability}). Configure your authenticate callback to populate request.scope.mandate after verifying the mandate JWT/VC.`
413
+ };
414
+ if (mandate.capability !== capability) return {
415
+ granted: false,
416
+ reason: `Mandate authorizes "${mandate.capability}", not "${capability}"`
417
+ };
418
+ if (mandate.expiresAt !== void 0 && Date.now() > mandate.expiresAt + ttlGrace) return {
419
+ granted: false,
420
+ reason: `Mandate expired at ${new Date(mandate.expiresAt).toISOString()}`
421
+ };
422
+ if (opts.audience) {
423
+ const required = typeof opts.audience === "function" ? opts.audience(ctx) : opts.audience;
424
+ if (required && mandate.audience && mandate.audience !== required) return {
425
+ granted: false,
426
+ reason: `Mandate is bound to "${mandate.audience}", not "${required}"`
427
+ };
428
+ if (required && !mandate.audience) return {
429
+ granted: false,
430
+ reason: `Mandate must be bound to "${required}" (no audience claim)`
431
+ };
432
+ }
433
+ if (opts.validateAmount) {
434
+ const result = opts.validateAmount(ctx, mandate);
435
+ if (result !== true) return {
436
+ granted: false,
437
+ reason: typeof result === "string" ? result : `Action exceeds mandate cap ${mandate.cap}${mandate.currency ? ` ${mandate.currency}` : ""}`
438
+ };
439
+ }
440
+ return true;
441
+ };
442
+ check._mandateCapability = capability;
443
+ return check;
444
+ }
445
+ /**
446
+ * Composite gate for AI-agent / M2M flows on protected resources.
447
+ *
448
+ * Bundles the three things every high-value agent endpoint needs:
449
+ * 1. **Service identity** — `scope.kind === 'service'` with `clientId`
450
+ * 2. **Capability mandate** — narrows what *this request* may do
451
+ * 3. **DPoP binding** — credential cannot be replayed from a different key
452
+ *
453
+ * Use this instead of hand-composing `allOf(requireServiceScope(...),
454
+ * requireMandate(...), requireDPoP())` — fewer ways to misconfigure, one
455
+ * meta-tag downstream tools (audit, MCP, OpenAPI) can read.
456
+ *
457
+ * @example
458
+ * ```typescript
459
+ * import { requireAgentScope } from '@classytic/arc/permissions';
460
+ *
461
+ * defineResource({
462
+ * name: 'invoice',
463
+ * actions: {
464
+ * pay: {
465
+ * handler: payInvoice,
466
+ * permissions: requireAgentScope({
467
+ * capability: 'payment.charge',
468
+ * scopes: ['payment.write'],
469
+ * requireDPoP: true,
470
+ * audience: (ctx) => `invoice:${ctx.params?.id}`,
471
+ * validateAmount: (ctx, m) => (ctx.data as { amount: number }).amount <= (m.cap ?? 0),
472
+ * }),
473
+ * },
474
+ * },
475
+ * });
476
+ * ```
477
+ */
478
+ function requireAgentScope(opts) {
479
+ const { capability, scopes, requireDPoP: needsDPoP = true, ...mandateOpts } = opts;
480
+ if (!capability) throw new Error("requireAgentScope({ capability }) is required");
481
+ const mandateCheck = requireMandate(capability, mandateOpts);
482
+ const dpopCheck = needsDPoP ? requireDPoP() : null;
483
+ const requiredScopes = scopes && scopes.length > 0 ? [...scopes] : null;
484
+ const check = async (ctx) => {
485
+ const scope = getRequestScope(ctx.request);
486
+ if (isElevated(scope) && !mandateOpts.noElevatedBypass) return true;
487
+ if (!isService(scope)) return {
488
+ granted: false,
489
+ reason: "Service identity required (machine principal). Agent flows must authenticate as a service, not a logged-in user."
490
+ };
491
+ if (requiredScopes) {
492
+ const granted = scope.scopes ?? [];
493
+ if (!requiredScopes.some((s) => granted.includes(s))) return {
494
+ granted: false,
495
+ reason: `Service identity is missing required OAuth scope(s): ${requiredScopes.join(", ")}`
496
+ };
497
+ }
498
+ const mandateResult = await mandateCheck(ctx);
499
+ if (mandateResult !== true) return typeof mandateResult === "object" ? mandateResult : {
500
+ granted: false,
501
+ reason: "Mandate check failed"
502
+ };
503
+ if (dpopCheck) {
504
+ const dpopResult = await dpopCheck(ctx);
505
+ if (dpopResult !== true) return typeof dpopResult === "object" ? dpopResult : {
506
+ granted: false,
507
+ reason: "DPoP binding required"
508
+ };
509
+ }
510
+ return true;
511
+ };
512
+ check._agentScope = {
513
+ capability,
514
+ scopes: requiredScopes ?? void 0,
515
+ dpop: needsDPoP
516
+ };
517
+ return check;
518
+ }
519
+ //#endregion
520
520
  //#region src/permissions/scope.ts
521
521
  /**
522
522
  * Permission Scope — checks bound to RequestScope.
@@ -1343,4 +1343,4 @@ function readOnly(overrides) {
1343
1343
  }, overrides);
1344
1344
  }
1345
1345
  //#endregion
1346
- export { roles as A, allowPublic as C, requireAuth as D, not as E, requireAgentScope as M, requireDPoP as N, requireOwnership as O, requireMandate as P, allOf as S, denyAll as T, requireOrgMembership as _, permissionMatrix as a, requireServiceScope as b, publicReadAdminWrite as c, GRANT_MODES as d, modeSatisfies as f, requireOrgInScope as g, createOrgPermissions as h, ownerWithAdminBypass as i, when as j, requireRoles as k, readOnly as l, createDynamicPermissionMatrix as m, authenticated as n, presets_exports as o, requireGrant as p, fullPublic as r, publicRead as s, adminOnly as t, createRoleHierarchy as u, requireOrgRole as v, anyOf as w, requireTeamMembership as x, requireScopeContext as y };
1346
+ export { requireAuth as A, requireDPoP as C, anyOf as D, allowPublic as E, requireRoles as M, roles as N, denyAll as O, when as P, requireAgentScope as S, allOf as T, requireOrgMembership as _, permissionMatrix as a, requireServiceScope as b, publicReadAdminWrite as c, GRANT_MODES as d, modeSatisfies as f, requireOrgInScope as g, createOrgPermissions as h, ownerWithAdminBypass as i, requireOwnership as j, not as k, readOnly as l, createDynamicPermissionMatrix as m, authenticated as n, presets_exports as o, requireGrant as p, fullPublic as r, publicRead as s, adminOnly as t, createRoleHierarchy as u, requireOrgRole as v, requireMandate as w, requireTeamMembership as x, requireScopeContext as y };
@@ -1,4 +1,4 @@
1
- import { Ct as PipelineStep, Rt as IControllerResponse, St as PipelineContext, _t as Guard, bt as OperationFilter, vt as Interceptor, wt as Transform, xt as PipelineConfig, yt as NextFunction } from "../index-BBsvQt7U.mjs";
1
+ import { Ct as PipelineStep, Rt as IControllerResponse, St as PipelineContext, _t as Guard, bt as OperationFilter, vt as Interceptor, wt as Transform, xt as PipelineConfig, yt as NextFunction } from "../index-DPRH0Cme.mjs";
2
2
  //#region src/pipeline/guard.d.ts
3
3
  interface GuardOptions {
4
4
  operations?: OperationFilter;
@@ -1,6 +1,6 @@
1
- import { b as isElevated, c as getOrgId, g as getUserId, n as PUBLIC_SCOPE, x as isMember } from "./types-bHblZ9YN.mjs";
2
- import { i as applyFieldReadPermissions, s as resolveEffectiveRoles } from "./applyPermissionResult-Tw4iXtlf.mjs";
3
- import { t as getUserRoles } from "./types-XTApa8b6.mjs";
1
+ import { n as getUserRoles } from "./userHelpers-Cg_y3fBz.mjs";
2
+ import { b as isElevated, c as getOrgId, g as getUserId, n as PUBLIC_SCOPE, x as isMember } from "./types-CSyCxl2V.mjs";
3
+ import { i as applyFieldReadPermissions, s as resolveEffectiveRoles } from "./applyPermissionResult-BnyVE9eD.mjs";
4
4
  import { t as executePipeline } from "./pipe-BOLmGIxa.mjs";
5
5
  import { isPaginatedResult, toCanonicalList } from "@classytic/repo-core/pagination";
6
6
  //#region src/scope/projection.ts
@@ -334,27 +334,4 @@ function buildActionPipelineHandler(handler, steps, operation, resourceName) {
334
334
  };
335
335
  }
336
336
  //#endregion
337
- //#region src/core/actionPermissions.ts
338
- /**
339
- * Return the effective `PermissionCheck` for a single action, or `undefined`
340
- * when the resource declares no gate at any level.
341
- *
342
- * Callers decide what "no gate" means:
343
- * - HTTP: boot-time throw in `normalizeActionsToRouterConfig`.
344
- * - MCP: tool-generation throw in `resourceToTools` (mirrors HTTP — the
345
- * two surfaces fail closed identically so MCP can't expose an
346
- * unauthenticated mutating tool when the HTTP plugin lifecycle hasn't
347
- * run).
348
- * - OpenAPI: docs advertise the endpoint as unauthenticated.
349
- */
350
- function resolveActionPermission(input) {
351
- const { action, resourcePermissions, resourceActionPermissions, globalAuth } = input;
352
- const explicit = typeof action !== "function" && action.permissions ? action.permissions : void 0;
353
- if (explicit) return explicit;
354
- if (resourceActionPermissions) return resourceActionPermissions;
355
- if (globalAuth) return globalAuth;
356
- const updateFallback = resourcePermissions?.update;
357
- if (updateFallback) return updateFallback;
358
- }
359
- //#endregion
360
- export { createCrudHandlers as a, getControllerContext as c, buildRequestScopeProjection as d, resolvePipelineSteps as i, getControllerScope as l, buildActionPipelineHandler as n, createFastifyHandler as o, buildPipelineHandler as r, createRequestContext as s, resolveActionPermission as t, sendControllerResponse as u };
337
+ export { createFastifyHandler as a, getControllerScope as c, createCrudHandlers as i, sendControllerResponse as l, buildPipelineHandler as n, createRequestContext as o, resolvePipelineSteps as r, getControllerContext as s, buildActionPipelineHandler as t, buildRequestScopeProjection as u };
@@ -1,7 +1,9 @@
1
- import { At as RouteDefinition, J as PresetHook, Jt as AnyRecord, Q as MiddlewareConfig, et as RouteSchemaOptions } from "../index-BBsvQt7U.mjs";
1
+ import { At as RouteDefinition, J as PresetHook, Q as MiddlewareConfig, et as RouteSchemaOptions } from "../index-DPRH0Cme.mjs";
2
+ import { c as AnyRecord } from "../types-Dmw-6Qeo.mjs";
2
3
  import { i as errorHandlerPlugin, n as ErrorMapper, r as defaultIsDuplicateKeyError, t as ErrorHandlerOptions } from "../errorHandler-C6yv4ZTW.mjs";
3
- import { a as arcCorePlugin, i as _default, n as ArcCorePluginOptions, r as PluginMeta, t as ArcCore } from "../fastify-augmentation-g_f9omSc.mjs";
4
- import { C as healthPlugin, S as _default$3, _ as CachingRule, a as MetricsCollector, b as HealthCheck, c as metricsPlugin, d as _default$5, f as realtimePlugin, g as CachingOptions, h as ssePlugin, i as MetricEntry, l as ChangeOperation, m as _default$8, n as _default$9, o as MetricsOptions, p as SSEOptions, r as versioningPlugin, s as _default$4, t as VersioningOptions, u as RealtimeOptions, v as _default$1, x as HealthOptions, y as cachingPlugin } from "../versioning-D6PvCwpq.mjs";
4
+ import { _ as _default$3, d as ScheduleStats, f as SchedulesPluginOptions, g as HealthOptions, h as HealthCheck, m as schedulesPlugin, p as _default$7, u as ScheduleDefinition, v as healthPlugin } from "../types-Cu6vVJmh.mjs";
5
+ import { a as arcCorePlugin, i as _default, n as ArcCorePluginOptions, r as PluginMeta, t as ArcCore } from "../fastify-augmentation-OzYGIiU1.mjs";
6
+ import { _ as CachingRule, a as MetricsCollector, c as metricsPlugin, d as _default$5, f as realtimePlugin, g as CachingOptions, h as ssePlugin, i as MetricEntry, l as ChangeOperation, m as _default$8, n as _default$9, o as MetricsOptions, p as SSEOptions, r as versioningPlugin, s as _default$4, t as VersioningOptions, u as RealtimeOptions, v as _default$1, y as cachingPlugin } from "../versioning-DXgY5BUm.mjs";
5
7
  import { t as TracingOptions } from "../tracing-CsGKQJGg.mjs";
6
8
  import { PaginatedResult } from "@classytic/repo-core/pagination";
7
9
  import { FastifyInstance, FastifyPluginAsync } from "fastify";
@@ -165,77 +167,4 @@ declare function createRequestIdGenerator(opts?: Pick<RequestIdOptions, "header"
165
167
  declare const requestIdPlugin: FastifyPluginAsync<RequestIdOptions>;
166
168
  declare const _default$6: FastifyPluginAsync<RequestIdOptions>;
167
169
  //#endregion
168
- //#region src/plugins/schedules.d.ts
169
- /**
170
- * Structural view of the ecosystem lock contract — matches
171
- * `LockAdapter` from `@classytic/repo-core/lock` (and every kit
172
- * implementation) WITHOUT importing the subpath, so arc's emitted
173
- * declarations carry no repo-core@>=0.8 subpath reference and the
174
- * peer floor stays honest. Any conforming adapter assigns.
175
- */
176
- interface ScheduleLockLike {
177
- tryAcquire(name: string, holderId: string, leaseMs: number): Promise<boolean> | boolean;
178
- release(name: string, holderId: string): Promise<boolean> | boolean;
179
- }
180
- interface ScheduleDefinition {
181
- /** Unique name — also the lock key (`arc:schedule:<name>`). */
182
- name: string;
183
- /** Interval in milliseconds between the END of one run and the next tick. */
184
- every: number;
185
- /** The work. Receives the Fastify instance; errors are logged, never fatal. */
186
- handler: (fastify: FastifyInstance) => void | Promise<void>;
187
- /** Fire once immediately when the app becomes ready (default: false). */
188
- runOnStart?: boolean;
189
- /**
190
- * Random extra delay in `[0, jitterMs)` added to the FIRST tick only.
191
- * Spreads boot-aligned jobs so multiple same-cadence schedules don't all
192
- * fire at exactly `boot + every` after every restart (and, with a lock,
193
- * de-synchronizes replicas competing for the same lease). Ignored when
194
- * `runOnStart` is true.
195
- */
196
- jitterMs?: number;
197
- /**
198
- * Lease duration when a `lock` is configured. Default: `every * 0.9`
199
- * (capped at 5 minutes) — long enough that other replicas skip the same
200
- * tick, short enough that a crashed leader fails over within one cycle.
201
- */
202
- leaseMs?: number;
203
- /** Per-schedule kill switch (default: true). */
204
- enabled?: boolean;
205
- }
206
- interface SchedulesPluginOptions {
207
- schedules: ScheduleDefinition[];
208
- /**
209
- * Ecosystem lock adapter for multi-replica deployments. Omit for
210
- * single-instance apps — every tick runs locally.
211
- */
212
- lock?: ScheduleLockLike;
213
- /**
214
- * Identity of this replica for lease ownership. Default: a random id per
215
- * process (repo-core's `getInstanceId()` is a good explicit value).
216
- */
217
- holderId?: string;
218
- /**
219
- * Master switch — `false` registers a typed no-op (config-gated
220
- * environments), same pattern as `idempotencyPlugin`.
221
- */
222
- enabled?: boolean;
223
- }
224
- /** Per-schedule runtime counters, readable via `fastify.getScheduleStats()`. */
225
- interface ScheduleStats {
226
- name: string;
227
- runs: number;
228
- failures: number;
229
- skippedByLock: number;
230
- lastRunAt: string | null;
231
- lastError: string | null;
232
- }
233
- declare module "fastify" {
234
- interface FastifyInstance {
235
- getScheduleStats?: () => ScheduleStats[];
236
- }
237
- }
238
- declare const schedulesPlugin: FastifyPluginAsync<SchedulesPluginOptions>;
239
- declare const _default$7: FastifyPluginAsync<SchedulesPluginOptions>;
240
- //#endregion
241
- export { type ArcCore, type ArcCorePluginOptions, type ArcPlugin, type CachingOptions, type CachingRule, type ChangeOperation, type CreatePluginDefinition, type ErrorHandlerOptions, type ErrorMapper, type GracefulShutdownOptions, type HealthCheck, type HealthOptions, type MetricEntry, type MetricsCollector, type MetricsOptions, type PluginMeta, type PluginResourceResult, type RealtimeOptions, type RequestIdOptions, type SSEOptions, type ScheduleDefinition, type ScheduleLockLike, type ScheduleStats, type SchedulesPluginOptions, type TracingOptions, type VersioningOptions, _default as arcCorePlugin, arcCorePlugin as arcCorePluginFn, _default$1 as cachingPlugin, cachingPlugin as cachingPluginFn, createPlugin, createRequestIdGenerator, defaultIsDuplicateKeyError, errorHandlerPlugin, errorHandlerPlugin as errorHandlerPluginFn, _default$2 as gracefulShutdownPlugin, gracefulShutdownPlugin as gracefulShutdownPluginFn, _default$3 as healthPlugin, healthPlugin as healthPluginFn, _default$4 as metricsPlugin, metricsPlugin as metricsPluginFn, _default$5 as realtimePlugin, realtimePlugin as realtimePluginFn, replyHelpersPlugin, _default$6 as requestIdPlugin, requestIdPlugin as requestIdPluginFn, sanitizeRequestId, _default$7 as schedulesPlugin, schedulesPlugin as schedulesPluginFn, _default$8 as ssePlugin, ssePlugin as ssePluginFn, _default$9 as versioningPlugin, versioningPlugin as versioningPluginFn };
170
+ export { type ArcCore, type ArcCorePluginOptions, type ArcPlugin, type CachingOptions, type CachingRule, type ChangeOperation, type CreatePluginDefinition, type ErrorHandlerOptions, type ErrorMapper, type GracefulShutdownOptions, type HealthCheck, type HealthOptions, type MetricEntry, type MetricsCollector, type MetricsOptions, type PluginMeta, type PluginResourceResult, type RealtimeOptions, type RequestIdOptions, type SSEOptions, type ScheduleDefinition, type ScheduleStats, type SchedulesPluginOptions, type TracingOptions, type VersioningOptions, _default as arcCorePlugin, arcCorePlugin as arcCorePluginFn, _default$1 as cachingPlugin, cachingPlugin as cachingPluginFn, createPlugin, createRequestIdGenerator, defaultIsDuplicateKeyError, errorHandlerPlugin, errorHandlerPlugin as errorHandlerPluginFn, _default$2 as gracefulShutdownPlugin, gracefulShutdownPlugin as gracefulShutdownPluginFn, _default$3 as healthPlugin, healthPlugin as healthPluginFn, _default$4 as metricsPlugin, metricsPlugin as metricsPluginFn, _default$5 as realtimePlugin, realtimePlugin as realtimePluginFn, replyHelpersPlugin, _default$6 as requestIdPlugin, requestIdPlugin as requestIdPluginFn, sanitizeRequestId, _default$7 as schedulesPlugin, schedulesPlugin as schedulesPluginFn, _default$8 as ssePlugin, ssePlugin as ssePluginFn, _default$9 as versioningPlugin, versioningPlugin as versioningPluginFn };