@classytic/arc 2.8.5 → 2.10.3

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 (155) hide show
  1. package/README.md +50 -38
  2. package/dist/{BaseController-DAGGc5Xn.mjs → BaseController-CbKKIflT.mjs} +193 -143
  3. package/dist/EventTransport-CUw5NNWe.d.mts +293 -0
  4. package/dist/{ResourceRegistry-C6uXlWe3.mjs → ResourceRegistry-BPd6NQDm.mjs} +1 -1
  5. package/dist/adapters/index.d.mts +3 -3
  6. package/dist/adapters/index.mjs +2 -2
  7. package/dist/{adapters-BBqAVvPK.mjs → adapters-BXY4i-hw.mjs} +210 -41
  8. package/dist/audit/index.d.mts +135 -11
  9. package/dist/audit/index.mjs +107 -20
  10. package/dist/auth/index.d.mts +17 -9
  11. package/dist/auth/index.mjs +14 -7
  12. package/dist/auth/redis-session.d.mts +1 -1
  13. package/dist/{betterAuthOpenApi-BuUcUEJq.mjs → betterAuthOpenApi-BBRVhjQN.mjs} +1 -1
  14. package/dist/cache/index.d.mts +17 -15
  15. package/dist/cache/index.mjs +15 -14
  16. package/dist/{caching-IMuYVjTL.mjs → caching-CBpK_SCM.mjs} +8 -3
  17. package/dist/cli/commands/describe.mjs +1 -1
  18. package/dist/cli/commands/docs.mjs +2 -2
  19. package/dist/cli/commands/generate.mjs +1 -1
  20. package/dist/cli/commands/init.mjs +1 -1
  21. package/dist/cli/commands/introspect.mjs +1 -1
  22. package/dist/core/index.d.mts +3 -3
  23. package/dist/core/index.mjs +4 -6
  24. package/dist/{defineResource-tcgySDo1.mjs → core-CcR01lup.mjs} +58 -61
  25. package/dist/{createActionRouter-BORM8f17.mjs → createActionRouter-Bp_5c_2b.mjs} +3 -3
  26. package/dist/{createApp-B1EY8zxa.mjs → createApp-BuvPma24.mjs} +15 -14
  27. package/dist/docs/index.d.mts +2 -2
  28. package/dist/docs/index.mjs +2 -2
  29. package/dist/{elevation-DtFxrG0s.mjs → elevation-C7hgL_aI.mjs} +22 -8
  30. package/dist/{errorHandler-f869_8PQ.mjs → errorHandler-Bb49BvPD.mjs} +59 -7
  31. package/dist/{errorHandler-Bah5JhBd.d.mts → errorHandler-DRQ3EqfL.d.mts} +37 -2
  32. package/dist/{eventPlugin-D9DKB2zM.d.mts → eventPlugin-CxWgpd6K.d.mts} +14 -2
  33. package/dist/{eventPlugin-CDjVTM82.mjs → eventPlugin-DCUjuiQT.mjs} +83 -5
  34. package/dist/events/index.d.mts +150 -36
  35. package/dist/events/index.mjs +355 -101
  36. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  37. package/dist/events/transports/redis.d.mts +1 -1
  38. package/dist/factory/index.d.mts +1 -1
  39. package/dist/factory/index.mjs +2 -2
  40. package/dist/{types-DZi1aYhm.d.mts → fields-Lo1VUDpt.d.mts} +121 -1
  41. package/dist/{fields-ipsbIRPK.mjs → fields-bxkeltzz.mjs} +18 -5
  42. package/dist/{filesUpload-C7r7HIeA.mjs → filesUpload-t21LS-py.mjs} +65 -7
  43. package/dist/hooks/index.d.mts +1 -1
  44. package/dist/hooks/index.mjs +1 -1
  45. package/dist/idempotency/index.d.mts +32 -5
  46. package/dist/idempotency/index.mjs +119 -12
  47. package/dist/idempotency/redis.d.mts +1 -1
  48. package/dist/{index-DtDzOBn8.d.mts → index-8qw4y6ff.d.mts} +4 -135
  49. package/dist/{index-BLXBmWud.d.mts → index-ChIw3776.d.mts} +283 -408
  50. package/dist/{interface-CMRutPfe.d.mts → index-Cl0uoKd5.d.mts} +1758 -2506
  51. package/dist/{index-C1meYuDn.d.mts → index-DStwgFUK.d.mts} +81 -7
  52. package/dist/index.d.mts +7 -8
  53. package/dist/index.mjs +11 -12
  54. package/dist/integrations/event-gateway.d.mts +1 -1
  55. package/dist/integrations/event-gateway.mjs +1 -1
  56. package/dist/integrations/index.d.mts +1 -1
  57. package/dist/integrations/mcp/index.d.mts +26 -8
  58. package/dist/integrations/mcp/index.mjs +96 -17
  59. package/dist/integrations/mcp/testing.d.mts +1 -1
  60. package/dist/integrations/mcp/testing.mjs +1 -1
  61. package/dist/integrations/webhooks.d.mts +5 -0
  62. package/dist/integrations/webhooks.mjs +6 -0
  63. package/dist/interface-D218ikEo.d.mts +77 -0
  64. package/dist/{memory-Cp7_cAko.mjs → memory-B5Amv9A1.mjs} +23 -8
  65. package/dist/{openapi-CbKUJY_m.mjs → openapi-B5F8AddX.mjs} +3 -3
  66. package/dist/org/index.d.mts +2 -2
  67. package/dist/permissions/index.d.mts +3 -4
  68. package/dist/permissions/index.mjs +5 -5
  69. package/dist/{permissions-CH4cNwJi.mjs → permissions-Dk6mshja.mjs} +315 -397
  70. package/dist/plugins/index.d.mts +7 -7
  71. package/dist/plugins/index.mjs +14 -16
  72. package/dist/plugins/response-cache.mjs +2 -2
  73. package/dist/plugins/tracing-entry.d.mts +1 -1
  74. package/dist/plugins/tracing-entry.mjs +1 -1
  75. package/dist/presets/filesUpload.d.mts +27 -5
  76. package/dist/presets/filesUpload.mjs +1 -1
  77. package/dist/presets/index.d.mts +3 -2
  78. package/dist/presets/index.mjs +4 -3
  79. package/dist/presets/multiTenant.d.mts +1 -1
  80. package/dist/presets/multiTenant.mjs +2 -2
  81. package/dist/presets/search.d.mts +178 -0
  82. package/dist/presets/search.mjs +150 -0
  83. package/dist/{presets-C2xgzW6x.mjs → presets-fLJVXdVn.mjs} +1 -1
  84. package/dist/{queryCachePlugin-BJJGBTlu.d.mts → queryCachePlugin-BKbWjgDG.d.mts} +1 -1
  85. package/dist/{queryCachePlugin-BH-fidlv.mjs → queryCachePlugin-DQCEfJis.mjs} +9 -9
  86. package/dist/{queryParser-CgCtsjti.mjs → queryParser-DBqBB6AC.mjs} +1 -1
  87. package/dist/{redis-BM00zaPB.d.mts → redis-DqyeggCa.d.mts} +1 -1
  88. package/dist/{redis-stream-CrsfUmPt.d.mts → redis-stream-CakIQmwR.d.mts} +1 -1
  89. package/dist/registry/index.d.mts +1 -1
  90. package/dist/registry/index.mjs +2 -2
  91. package/dist/{resourceToTools-8s-EsCCe.mjs → resourceToTools-BElv3xPT.mjs} +65 -48
  92. package/dist/{schemaConverter-Y7nCYaLJ.mjs → schemaConverter-BxFDdtXu.mjs} +1 -1
  93. package/dist/scope/index.d.mts +1 -1
  94. package/dist/scope/index.mjs +2 -2
  95. package/dist/{sse-Ad7ypl9e.mjs → sse-yBCgOLGu.mjs} +1 -1
  96. package/dist/store-helpers-ZCSMJJAX.mjs +57 -0
  97. package/dist/testing/index.d.mts +9 -17
  98. package/dist/testing/index.mjs +27 -83
  99. package/dist/testing/storageContract.d.mts +1 -1
  100. package/dist/types/index.d.mts +4 -4
  101. package/dist/types/index.mjs +1 -31
  102. package/dist/types/storage.d.mts +1 -1
  103. package/dist/{types-BsbNMEDR.d.mts → types-Btdda02s.d.mts} +1 -1
  104. package/dist/{types-Ch9pTQbf.d.mts → types-Co8k3NyS.d.mts} +11 -9
  105. package/dist/types-Csi3FLfq.mjs +27 -0
  106. package/dist/utils/index.d.mts +208 -4
  107. package/dist/utils/index.mjs +5 -6
  108. package/dist/{utils-yYT3HDXt.mjs → utils-B2fNOD_i.mjs} +285 -2
  109. package/dist/{versioning-CDugduqI.mjs → versioning-C2U_bLY0.mjs} +3 -5
  110. package/package.json +20 -26
  111. package/skills/arc/SKILL.md +97 -23
  112. package/skills/arc/references/auth.md +94 -0
  113. package/skills/arc/references/events.md +200 -12
  114. package/skills/arc/references/mcp.md +4 -17
  115. package/skills/arc/references/multi-tenancy.md +43 -0
  116. package/skills/arc/references/production.md +34 -60
  117. package/dist/EventTransport-BXja8NOc.d.mts +0 -135
  118. package/dist/audit/mongodb.d.mts +0 -2
  119. package/dist/audit/mongodb.mjs +0 -2
  120. package/dist/circuitBreaker-cmi5XDv5.mjs +0 -284
  121. package/dist/circuitBreaker-dTtG-UyS.d.mts +0 -206
  122. package/dist/core-F0QoWBt2.mjs +0 -34
  123. package/dist/dynamic/index.d.mts +0 -93
  124. package/dist/dynamic/index.mjs +0 -122
  125. package/dist/fields-DpZQa_Q3.d.mts +0 -109
  126. package/dist/idempotency/mongodb.d.mts +0 -2
  127. package/dist/idempotency/mongodb.mjs +0 -123
  128. package/dist/interface-4y979v99.d.mts +0 -54
  129. package/dist/mongodb-BsP-WbhN.d.mts +0 -127
  130. package/dist/mongodb-CTcp0hQZ.d.mts +0 -80
  131. package/dist/mongodb-Utc5k_-0.mjs +0 -90
  132. package/dist/policies/index.d.mts +0 -432
  133. package/dist/policies/index.mjs +0 -318
  134. package/dist/rpc/index.d.mts +0 -90
  135. package/dist/rpc/index.mjs +0 -248
  136. /package/dist/{HookSystem-HprTmvVY.mjs → HookSystem-BNYKnrXF.mjs} +0 -0
  137. /package/dist/{applyPermissionResult-D6GPMsvh.mjs → applyPermissionResult-QhV1Pa-g.mjs} +0 -0
  138. /package/dist/{constants-Cxde4rpC.mjs → constants-BhY1OHoH.mjs} +0 -0
  139. /package/dist/{elevation-B6S5csVA.d.mts → elevation-C5SwtkAn.d.mts} +0 -0
  140. /package/dist/{errors-Ck2h67pm.d.mts → errors-CCSsMpXE.d.mts} +0 -0
  141. /package/dist/{errors-BF2bIOIS.mjs → errors-D5c-5BJL.mjs} +0 -0
  142. /package/dist/{externalPaths-BnkYrNzp.d.mts → externalPaths-BQ8QijNH.d.mts} +0 -0
  143. /package/dist/{interface-DfLGcus7.d.mts → interface-CSbZdv_3.d.mts} +0 -0
  144. /package/dist/{loadResources-PWd0OCpV.mjs → loadResources-BAzJItAJ.mjs} +0 -0
  145. /package/dist/{logger-D1YrIImS.mjs → logger-DLg8-Ueg.mjs} +0 -0
  146. /package/dist/{metrics-B-PU4-Yu.mjs → metrics-DuhiSEZI.mjs} +0 -0
  147. /package/dist/{pluralize-CWP6MB39.mjs → pluralize-A0tWEl1K.mjs} +0 -0
  148. /package/dist/{registry-BiTKT1Dg.mjs → registry-B3lRFBWo.mjs} +0 -0
  149. /package/dist/{replyHelpers-CxkYGT81.mjs → replyHelpers-CXtJDAZ0.mjs} +0 -0
  150. /package/dist/{requestContext-DYvHl113.mjs → requestContext-xHIKedG6.mjs} +0 -0
  151. /package/dist/{sessionManager-DDCmiNIo.d.mts → sessionManager-BkzVU8h2.d.mts} +0 -0
  152. /package/dist/{storage-Dfzt4VTl.d.mts → storage-CVk_SEn2.d.mts} +0 -0
  153. /package/dist/{tracing-DdN2-wHJ.d.mts → tracing-65B51Dw3.d.mts} +0 -0
  154. /package/dist/{typeGuards-CcFZXgU7.mjs → typeGuards-Cj5Rgvlg.mjs} +0 -0
  155. /package/dist/{types-ZUu_h0jp.mjs → types-DV9WDfeg.mjs} +0 -0
@@ -1,209 +1,23 @@
1
1
  import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
2
2
  import { _ as isElevated, b as isService, c as getRequestScope, d as getServiceScopes, f as getTeamId, h as hasOrgAccess, l as getScopeContext, p as getUserId, u as getScopeContextMap, v as isMember, y as isOrgInScope } from "./types-AOD8fxIw.mjs";
3
- import { t as getUserRoles } from "./types-ZUu_h0jp.mjs";
4
- import { t as MemoryCacheStore } from "./memory-Cp7_cAko.mjs";
3
+ import { t as getUserRoles } from "./types-DV9WDfeg.mjs";
4
+ import { t as MemoryCacheStore } from "./memory-B5Amv9A1.mjs";
5
5
  import { randomUUID } from "node:crypto";
6
- //#region src/permissions/roleHierarchy.ts
7
- /**
8
- * Create a role hierarchy from a parent → children map.
9
- *
10
- * Each key is a parent role, each value is the array of roles it inherits.
11
- * Inheritance is transitive: if A → B and B → C, then A expands to [A, B, C].
12
- * Circular references are handled safely (visited set).
13
- */
14
- function createRoleHierarchy(map) {
15
- const cache = /* @__PURE__ */ new Map();
16
- function resolveRole(role, visited) {
17
- if (visited.has(role)) return [];
18
- visited.add(role);
19
- const cached = cache.get(role);
20
- if (cached) return cached;
21
- const children = map[role];
22
- if (!children || children.length === 0) {
23
- cache.set(role, [role]);
24
- return [role];
25
- }
26
- const result = [role];
27
- for (const child of children) result.push(...resolveRole(child, visited));
28
- const deduped = [...new Set(result)];
29
- cache.set(role, deduped);
30
- return deduped;
31
- }
32
- return {
33
- expand(roles) {
34
- if (roles.length === 0) return [];
35
- const all = /* @__PURE__ */ new Set();
36
- for (const role of roles) for (const expanded of resolveRole(role, /* @__PURE__ */ new Set())) all.add(expanded);
37
- return [...all];
38
- },
39
- includes(userRoles, requiredRole) {
40
- if (userRoles.length === 0) return false;
41
- return this.expand(userRoles).includes(requiredRole);
42
- }
43
- };
44
- }
45
- //#endregion
46
- //#region src/permissions/presets.ts
47
- /**
48
- * Permission Presets — Common permission patterns in one call.
49
- *
50
- * Reduces 5 lines of permission declarations to 1.
51
- * Each preset returns a ResourcePermissions object that can be
52
- * spread or overridden per-operation.
53
- *
54
- * @example
55
- * ```typescript
56
- * import { permissions } from '@classytic/arc';
57
- *
58
- * // Public read, authenticated write
59
- * defineResource({ name: 'product', permissions: permissions.publicRead() });
60
- *
61
- * // Override specific operations
62
- * defineResource({
63
- * name: 'product',
64
- * permissions: permissions.publicRead({ delete: requireRoles(['superadmin']) }),
65
- * });
66
- * ```
67
- */
68
- var presets_exports = /* @__PURE__ */ __exportAll({
69
- adminOnly: () => adminOnly,
70
- authenticated: () => authenticated,
71
- fullPublic: () => fullPublic,
72
- ownerWithAdminBypass: () => ownerWithAdminBypass,
73
- publicRead: () => publicRead,
74
- publicReadAdminWrite: () => publicReadAdminWrite,
75
- readOnly: () => readOnly
76
- });
77
- /**
78
- * Merge a base preset with user overrides.
79
- * Overrides replace individual operations — undefined values don't clear them.
80
- */
81
- function withOverrides(base, overrides) {
82
- if (!overrides) return base;
83
- const filtered = Object.fromEntries(Object.entries(overrides).filter(([, v]) => v !== void 0));
84
- return {
85
- ...base,
86
- ...filtered
87
- };
88
- }
89
- /**
90
- * Public read, authenticated write.
91
- * list + get = allowPublic(), create + update + delete = requireAuth()
92
- */
93
- function publicRead(overrides) {
94
- return withOverrides({
95
- list: allowPublic(),
96
- get: allowPublic(),
97
- create: requireAuth(),
98
- update: requireAuth(),
99
- delete: requireAuth()
100
- }, overrides);
101
- }
102
- /**
103
- * Public read, admin write.
104
- * list + get = allowPublic(), create + update + delete = requireRoles(['admin'])
105
- */
106
- function publicReadAdminWrite(roles = ["admin"], overrides) {
107
- return withOverrides({
108
- list: allowPublic(),
109
- get: allowPublic(),
110
- create: requireRoles(roles),
111
- update: requireRoles(roles),
112
- delete: requireRoles(roles)
113
- }, overrides);
114
- }
115
- /**
116
- * All operations require authentication.
117
- */
118
- function authenticated(overrides) {
119
- return withOverrides({
120
- list: requireAuth(),
121
- get: requireAuth(),
122
- create: requireAuth(),
123
- update: requireAuth(),
124
- delete: requireAuth()
125
- }, overrides);
126
- }
127
- /**
128
- * All operations require specific roles.
129
- * @param roles - Required roles (user needs at least one). Default: ['admin']
130
- */
131
- function adminOnly(roles = ["admin"], overrides) {
132
- return withOverrides({
133
- list: requireRoles(roles),
134
- get: requireRoles(roles),
135
- create: requireRoles(roles),
136
- update: requireRoles(roles),
137
- delete: requireRoles(roles)
138
- }, overrides);
139
- }
140
- /**
141
- * Owner-scoped with admin bypass.
142
- * list = auth (scoped to owner), get = auth, create = auth,
143
- * update + delete = ownership check with admin bypass.
144
- *
145
- * @param ownerField - Field containing owner ID (default: 'userId')
146
- * @param bypassRoles - Roles that bypass ownership check (default: ['admin'])
147
- */
148
- function ownerWithAdminBypass(ownerField = "userId", bypassRoles = ["admin"], overrides) {
149
- return withOverrides({
150
- list: requireAuth(),
151
- get: requireAuth(),
152
- create: requireAuth(),
153
- update: anyOf(requireRoles(bypassRoles), requireOwnership(ownerField)),
154
- delete: anyOf(requireRoles(bypassRoles), requireOwnership(ownerField))
155
- }, overrides);
156
- }
157
- /**
158
- * Full public access — no auth required for any operation.
159
- * Use sparingly (dev/testing, truly public APIs).
160
- */
161
- function fullPublic(overrides) {
162
- return withOverrides({
163
- list: allowPublic(),
164
- get: allowPublic(),
165
- create: allowPublic(),
166
- update: allowPublic(),
167
- delete: allowPublic()
168
- }, overrides);
169
- }
170
- /**
171
- * Read-only: list + get authenticated, write operations denied.
172
- * Useful for computed/derived resources.
173
- */
174
- function readOnly(overrides) {
175
- return withOverrides({
176
- list: requireAuth(),
177
- get: requireAuth()
178
- }, overrides);
179
- }
180
- //#endregion
181
- //#region src/permissions/index.ts
6
+ //#region src/permissions/core.ts
182
7
  /**
183
8
  * Normalize a `string | [readonly string[]]` rest-args tuple into a single
184
9
  * `readonly string[]`. Lets a permission helper accept BOTH variadic and
185
- * array call shapes from the same overload signature without each helper
186
- * re-implementing the same ternary.
10
+ * array call shapes from one overload signature.
187
11
  *
188
12
  * Used by `requireOrgRole`, `requireServiceScope`, etc. **Not** used by
189
13
  * `requireRoles` — that helper has a richer overload signature with an
190
14
  * options object and stays on its own normalization path.
191
- *
192
- * @example
193
- * ```typescript
194
- * function requireFoo(...args: string[] | [readonly string[]]) {
195
- * const items = normalizeVariadicOrArray(args);
196
- * // items is always readonly string[]
197
- * }
198
- * requireFoo('a', 'b', 'c');
199
- * requireFoo(['a', 'b', 'c']);
200
- * ```
201
15
  */
202
16
  function normalizeVariadicOrArray(args) {
203
17
  return Array.isArray(args[0]) ? args[0] : args;
204
18
  }
205
19
  /**
206
- * Allow public access (no authentication required)
20
+ * Allow public access (no authentication required).
207
21
  *
208
22
  * @example
209
23
  * ```typescript
@@ -219,7 +33,7 @@ function allowPublic() {
219
33
  return check;
220
34
  }
221
35
  /**
222
- * Require authentication (any authenticated user)
36
+ * Require authentication (any authenticated user).
223
37
  *
224
38
  * @example
225
39
  * ```typescript
@@ -276,27 +90,9 @@ function requireRoles(rolesOrFirst, optionsOrSecond, ...rest) {
276
90
  return check;
277
91
  }
278
92
  /**
279
- * **Alias of `requireRoles()`** — checks both platform roles AND org roles.
280
- *
281
- * Since 2.7.1, `requireRoles()` defaults to `includeOrgRoles: true`, which
282
- * means `roles('admin')` and `requireRoles('admin')` are now functionally
283
- * identical. This helper is preserved for backwards compatibility and for
284
- * call sites that prefer the shorter `roles()` name.
285
- *
286
- * **For new code, prefer `requireRoles()`** — it's the canonical name and
287
- * matches the rest of the `requireXxx()` family (`requireAuth`, `requireOwnership`,
288
- * `requireOrgRole`, etc.).
289
- *
290
- * For platform-only checks: `requireRoles(['admin'], { includeOrgRoles: false })`
291
- * For org-only checks: `requireOrgRole('admin')`
292
- *
293
- * @example
294
- * ```typescript
295
- * // These are identical:
296
- * roles('admin', 'editor')
297
- * requireRoles('admin', 'editor')
298
- * requireRoles(['admin', 'editor'])
299
- * ```
93
+ * Short-form alias of `requireRoles()`. Identical behavior — checks both
94
+ * platform roles AND org roles. Prefer `requireRoles` for new code; this
95
+ * exists for call sites that want a terser name.
300
96
  */
301
97
  function roles(...args) {
302
98
  const roleList = normalizeVariadicOrArray(args);
@@ -319,12 +115,8 @@ function roles(...args) {
319
115
  return check;
320
116
  }
321
117
  /**
322
- * Require resource ownership
323
- *
324
- * Returns filters to scope queries to user's owned resources.
325
- *
326
- * @param ownerField - Field containing owner ID (default: 'userId')
327
- * @param options - Optional bypass roles
118
+ * Require resource ownership. Returns filters to scope queries to the
119
+ * caller's owned resources.
328
120
  *
329
121
  * @example
330
122
  * ```typescript
@@ -354,33 +146,18 @@ function requireOwnership(ownerField = "userId", options) {
354
146
  };
355
147
  }
356
148
  /**
357
- * Combine multiple checks - ALL must pass (AND logic).
149
+ * Combine multiple checks ALL must pass (AND logic).
358
150
  *
359
151
  * Each child runs against the **accumulated** state of previous children:
360
- * - `filters` from earlier children are merged into the next child's
361
- * `_policyFilters` (so e.g. `requireOwnership` sees row-level scoping)
362
- * - `scope` from earlier children is installed on the request before the
363
- * next child runs (so e.g. `requireOrgMembership` after `requireApiKey`
364
- * sees the service scope from the API key check)
152
+ * - `filters` from earlier children merge into the next child's `_policyFilters`
153
+ * - `scope` from earlier children installs on the request before the next child runs
365
154
  *
366
- * The final returned `PermissionResult` carries both the merged `filters` AND
367
- * the merged `scope`, so the outer middleware's `applyPermissionResult` call
368
- * sees the same end-state.
155
+ * The final result carries both merged `filters` and merged `scope`.
369
156
  *
370
157
  * @example
371
158
  * ```typescript
372
- * // CRUD permissions composed across roles + ownership
373
- * permissions: {
374
- * update: allOf(
375
- * requireAuth(),
376
- * requireRoles(['editor']),
377
- * requireOwnership('createdBy')
378
- * ),
379
- * }
380
- *
381
- * // Custom auth + org membership — first check installs the scope,
382
- * // second check reads it.
383
159
  * permissions: {
160
+ * update: allOf(requireAuth(), requireRoles(['editor']), requireOwnership('createdBy')),
384
161
  * list: allOf(requireApiKey(), requireOrgMembership()),
385
162
  * }
386
163
  * ```
@@ -432,15 +209,12 @@ function allOf(...checks) {
432
209
  };
433
210
  }
434
211
  /**
435
- * Combine multiple checks - ANY must pass (OR logic)
212
+ * Combine multiple checks ANY must pass (OR logic).
436
213
  *
437
214
  * @example
438
215
  * ```typescript
439
216
  * permissions: {
440
- * update: anyOf(
441
- * requireRoles(['admin']),
442
- * requireOwnership('createdBy')
443
- * ),
217
+ * update: anyOf(requireRoles(['admin']), requireOwnership('createdBy')),
444
218
  * }
445
219
  * ```
446
220
  */
@@ -460,15 +234,38 @@ function anyOf(...checks) {
460
234
  };
461
235
  }
462
236
  /**
463
- * Deny all access
237
+ * Invert a permission check. Grants when the wrapped check denies, denies
238
+ * when the wrapped check grants. Useful for "block if X" patterns —
239
+ * e.g. `not(requireRoles(['guest']))` to deny guest access.
240
+ *
241
+ * NOTE: filters and scope from the wrapped check are intentionally
242
+ * discarded — an inverted check has no row-level meaning.
464
243
  *
465
244
  * @example
466
245
  * ```typescript
467
246
  * permissions: {
468
- * delete: denyAll('Deletion not allowed'),
247
+ * internalApi: not(requireRoles(['external'])),
248
+ * adminUI: allOf(requireAuth(), not(requireRoles(['readonly']))),
469
249
  * }
470
250
  * ```
471
251
  */
252
+ function not(check, reason = "Access denied") {
253
+ return async (ctx) => {
254
+ const result = await check(ctx);
255
+ return (typeof result === "boolean" ? { granted: result } : result).granted ? {
256
+ granted: false,
257
+ reason
258
+ } : true;
259
+ };
260
+ }
261
+ /**
262
+ * Deny all access.
263
+ *
264
+ * @example
265
+ * ```typescript
266
+ * permissions: { delete: denyAll('Deletion not allowed') }
267
+ * ```
268
+ */
472
269
  function denyAll(reason = "Access denied") {
473
270
  return () => ({
474
271
  granted: false,
@@ -476,7 +273,7 @@ function denyAll(reason = "Access denied") {
476
273
  });
477
274
  }
478
275
  /**
479
- * Dynamic permission based on context
276
+ * Dynamic permission based on a condition function.
480
277
  *
481
278
  * @example
482
279
  * ```typescript
@@ -494,26 +291,28 @@ function when(condition) {
494
291
  };
495
292
  };
496
293
  }
294
+ //#endregion
295
+ //#region src/permissions/scope.ts
497
296
  /**
498
- * Require an org-bound caller. Grants access for any scope kind that
499
- * carries org context: `member` (human user with org membership), `service`
500
- * (API key bound to an org), or `elevated` (platform admin). Denies for
501
- * `public` and `authenticated` scopes (no org context).
297
+ * Permission Scope checks bound to RequestScope.
502
298
  *
503
- * This is the canonical "is the caller acting inside an org" check, and the
504
- * usual partner for `multiTenantPreset` if a route is multi-tenant
505
- * filtered, you almost always want this gate too.
299
+ * All read `request.scope` populated by an auth adapter (Better Auth bridge,
300
+ * JWT custom auth, or an upstream permission check returning
301
+ * `PermissionResult.scope`).
302
+ */
303
+ /**
304
+ * Require an org-bound caller. Grants for `member`, `service`, and
305
+ * `elevated` scopes (anything with org context). Denies `public` and
306
+ * `authenticated` (no org context).
506
307
  *
507
- * Reads `request.scope` set by auth adapters or by upstream permission
508
- * checks via `PermissionResult.scope` (e.g. a custom `requireApiKey()`).
308
+ * Canonical "is the caller acting inside an org" check. Usual partner for
309
+ * `multiTenantPreset` if a route filters by tenant, you almost always
310
+ * want this gate too.
509
311
  *
510
312
  * @example
511
313
  * ```typescript
512
314
  * permissions: {
513
315
  * list: requireOrgMembership(),
514
- * get: requireOrgMembership(),
515
- *
516
- * // Composed with an OAuth-style scope check for API-key callers
517
316
  * create: allOf(requireOrgMembership(), requireServiceScope('jobs:write')),
518
317
  * }
519
318
  * ```
@@ -534,28 +333,15 @@ function requireOrgMembership() {
534
333
  return check;
535
334
  }
536
335
  /**
537
- * Require specific org-level roles.
538
- * Reads `request.scope.orgRoles` (set by auth adapters).
336
+ * Require specific org-level roles. Reads `request.scope.orgRoles`.
539
337
  * Elevated scope always passes (platform admin bypass).
540
338
  *
541
339
  * **Service scopes (API keys) always fail this check** — services don't
542
- * carry user-style org roles, only OAuth-style `scopes` strings. For routes
543
- * that should accept BOTH human admins AND API keys, compose explicitly:
544
- *
545
- * ```typescript
546
- * permissions: {
547
- * create: anyOf(
548
- * requireOrgRole('admin'), // human path
549
- * requireServiceScope('jobs:write'), // machine path
550
- * ),
551
- * }
552
- * ```
553
- *
554
- * This separation is intentional — implicit "API key bypasses role checks"
555
- * is the kind of footgun that ships data breaches. Services must opt into
556
- * specific scopes the same way OAuth clients do.
557
- *
558
- * @param roles - Required org roles (user needs at least one)
340
+ * carry user-style org roles, only OAuth-style `scopes` strings. For
341
+ * routes that should accept BOTH human admins AND API keys, compose with
342
+ * `anyOf(requireOrgRole(...), requireServiceScope(...))`. The implicit
343
+ * "API key bypasses role check" path is intentionally NOT supported —
344
+ * it's the kind of footgun that ships data breaches.
559
345
  *
560
346
  * @example
561
347
  * ```typescript
@@ -593,44 +379,24 @@ function requireOrgRole(...args) {
593
379
  }
594
380
  /**
595
381
  * Require specific OAuth-style scope strings on a service (API key) identity.
596
- *
597
- * Reads `request.scope.scopes` — only populated when the scope kind is
598
- * `service`. Mirrors how OAuth 2.0 / Better Auth's apiKey plugin / API
599
- * gateways express machine permissions: a comma- or array-encoded list of
600
- * scope strings like `'jobs:read'`, `'jobs:write'`, `'memories:*'`.
382
+ * Reads `request.scope.scopes` (only present when `scope.kind === 'service'`).
601
383
  *
602
384
  * **Pass behavior:**
603
- * - `service` scope where `scopes` contains ANY of the required strings → grant
604
- * - `elevated` scope (platform admin) → grant
385
+ * - `service` scope where `scopes` contains ANY required string → grant
386
+ * - `elevated` scope → grant
605
387
  * - Anything else → deny with a clear reason
606
388
  *
607
- * Notably this does **not** grant for `member` scopes — humans go through
608
- * `requireOrgRole`. For routes that should accept both, compose with `anyOf`:
609
- *
610
- * ```typescript
611
- * permissions: {
612
- * create: anyOf(
613
- * requireOrgRole('admin'),
614
- * requireServiceScope('jobs:write'),
615
- * ),
616
- * }
617
- * ```
618
- *
619
- * @param scopes - Required scope strings (caller needs at least one)
389
+ * Does **not** grant for `member` scopes — humans go through `requireOrgRole`.
390
+ * For routes that should accept both, compose with `anyOf`.
620
391
  *
621
392
  * @example
622
393
  * ```typescript
623
- * // Variadic
624
394
  * requireServiceScope('jobs:write')
625
395
  * requireServiceScope('jobs:read', 'jobs:write')
626
- *
627
- * // Array
628
396
  * requireServiceScope(['jobs:read', 'jobs:write'])
629
397
  *
630
- * // Composed with org membership for org-scoped API keys
631
398
  * permissions: {
632
399
  * list: allOf(requireOrgMembership(), requireServiceScope('jobs:read')),
633
- * create: allOf(requireOrgMembership(), requireServiceScope('jobs:write')),
634
400
  * }
635
401
  * ```
636
402
  */
@@ -655,46 +421,26 @@ function requireServiceScope(...args) {
655
421
  return check;
656
422
  }
657
423
  /**
658
- * Require app-defined scope context dimensions (branch, project, department,
659
- * region, workspace, etc.) on the current request.
660
- *
661
- * Reads `request.scope.context` (a `Readonly<Record<string, string>>` slot
662
- * available on `member`, `service`, and `elevated` scope kinds). Arc takes
663
- * no position on what dimensions you use — you set them, you check them.
424
+ * Require app-defined scope context dimensions (branch, project, region,
425
+ * workspace, ) on the request. Arc takes no position on what dimensions
426
+ * you use — your auth function populates `scope.context`, your routes
427
+ * gate on it.
664
428
  *
665
429
  * **Three call shapes:**
666
- *
667
430
  * ```typescript
668
- * // 1. Presence check — key must exist on scope.context
669
- * requireScopeContext('branchId')
670
- *
671
- * // 2. Value match key must equal a specific string
672
- * requireScopeContext('branchId', 'eng-paris')
673
- *
674
- * // 3. Multi-key (object form, AND semantics) — every key must match
675
- * requireScopeContext({ branchId: 'eng-paris', projectId: 'p-123' })
676
- * requireScopeContext({ region: 'eu', branchId: undefined }) // 'undefined' = presence-only for that key
431
+ * requireScopeContext('branchId') // presence only
432
+ * requireScopeContext('branchId', 'eng-paris') // value match
433
+ * requireScopeContext({ branchId: 'eng-paris', projectId: 'p-1' }) // multi-key (AND)
434
+ * requireScopeContext({ region: 'eu', branchId: undefined }) // mixed
677
435
  * ```
678
436
  *
679
- * **Pass behavior:**
680
- * - All required keys present (and matching values when specified) → grant
681
- * - `elevated` scope (platform admin) → grant unconditionally (cross-context bypass)
682
- * - Any required key missing or mismatched → deny with a clear reason
683
- * - Scope kind without context support (`public`, `authenticated`) → deny
684
- *
685
- * Pairs with `multiTenantPreset({ tenantFields: [...] })` for row-level
686
- * filtering on the same dimensions.
437
+ * **Pass behavior:** all required keys present (and matching when
438
+ * specified) grant. `elevated` scope grants unconditionally.
687
439
  *
688
440
  * @example
689
441
  * ```typescript
690
442
  * permissions: {
691
- * // Branch-scoped CRUD — caller must have branchId in their scope context
692
443
  * list: allOf(requireOrgMembership(), requireScopeContext('branchId')),
693
- *
694
- * // Project admin — caller must have BOTH project context AND admin role
695
- * delete: allOf(requireOrgRole('admin'), requireScopeContext('projectId')),
696
- *
697
- * // Region-locked endpoint
698
444
  * euOnly: requireScopeContext('region', 'eu'),
699
445
  * }
700
446
  * ```
@@ -734,34 +480,20 @@ function requireScopeContext(keyOrMap, value) {
734
480
  * Require that the caller's scope grants access to a target organization
735
481
  * — either the current org or one of its ancestors (`scope.ancestorOrgIds`).
736
482
  *
737
- * Designed for parent-child organization hierarchies (holding company
738
- * subsidiary → branch, MSP → managed tenants, white-label parent → child
739
- * accounts) where some routes need to accept "this org OR any org I have
740
- * access to via the chain". Arc takes no position on the source of the
741
- * chain — your auth function loads `ancestorOrgIds` from your own data
742
- * model. There's no automatic inheritance: every route opts in explicitly.
483
+ * For parent-child organization hierarchies (holding subsidiary branch,
484
+ * MSP → tenant, white-label parent → child). Auth function pre-loads
485
+ * `ancestorOrgIds`; routes opt in explicitly. No automatic inheritance.
743
486
  *
744
487
  * **Two call shapes:**
745
- *
746
488
  * ```typescript
747
- * // Static target — rare, used when one route only ever acts on one org
748
- * requireOrgInScope('acme-holding')
749
- *
750
- * // Dynamic target — extracted from request params/body/headers per call
751
- * requireOrgInScope((ctx) => ctx.request.params.orgId)
752
- * requireOrgInScope((ctx) => ctx.request.body?.organizationId)
489
+ * requireOrgInScope('acme-holding') // static
490
+ * requireOrgInScope((ctx) => ctx.request.params.orgId) // dynamic
753
491
  * ```
754
492
  *
755
- * **Pass behavior:**
756
- * - Target equals `scope.organizationId` → grant
757
- * - Target appears in `scope.ancestorOrgIds` → grant
758
- * - `elevated` scope → grant unconditionally (cross-org admin bypass)
759
- * - Target is undefined (extractor returned nothing) → deny with reason
760
- * - Anything else → deny with target name in reason
493
+ * `elevated` scope grants unconditionally (cross-org bypass).
761
494
  *
762
495
  * @example
763
496
  * ```typescript
764
- * // /orgs/:orgId/jobs — caller can act on any org in their hierarchy chain
765
497
  * permissions: {
766
498
  * list: requireOrgInScope((ctx) => ctx.request.params.orgId),
767
499
  * create: allOf(
@@ -791,8 +523,56 @@ function requireOrgInScope(target) {
791
523
  return check;
792
524
  }
793
525
  /**
794
- * Create a scoped permission system for resource-action patterns.
795
- * Maps org roles to fine-grained permissions without external API calls.
526
+ * Require membership in the active team. User must be authenticated, a
527
+ * member of the active org, AND have an active team. Better Auth teams
528
+ * are flat member groups (no team-level roles). Reads `request.scope.teamId`.
529
+ *
530
+ * @example
531
+ * ```typescript
532
+ * permissions: {
533
+ * list: requireTeamMembership(),
534
+ * create: requireTeamMembership(),
535
+ * }
536
+ * ```
537
+ */
538
+ function requireTeamMembership() {
539
+ const check = (ctx) => {
540
+ if (!ctx.user) return {
541
+ granted: false,
542
+ reason: "Authentication required"
543
+ };
544
+ const scope = getRequestScope(ctx.request);
545
+ if (isElevated(scope)) return true;
546
+ if (!isMember(scope)) return {
547
+ granted: false,
548
+ reason: "Organization membership required"
549
+ };
550
+ if (!getTeamId(scope)) return {
551
+ granted: false,
552
+ reason: "No active team"
553
+ };
554
+ return true;
555
+ };
556
+ check._teamPermission = "membership";
557
+ return check;
558
+ }
559
+ //#endregion
560
+ //#region src/permissions/dynamic.ts
561
+ /**
562
+ * Permission Matrices — role × resource × action mapping.
563
+ *
564
+ * Two flavors:
565
+ * - `createOrgPermissions` — static, compile-time-known matrix
566
+ * - `createDynamicPermissionMatrix` — runtime-resolved, with optional
567
+ * cache + cross-node event invalidation
568
+ *
569
+ * Both produce `PermissionCheck` instances that compose with the rest of
570
+ * the permission system.
571
+ */
572
+ /**
573
+ * Create a static role × resource × action permission system. Compile-time
574
+ * matrix — use when role mappings are known at build time and don't change
575
+ * per-deployment.
796
576
  *
797
577
  * @example
798
578
  * ```typescript
@@ -856,24 +636,24 @@ function createOrgPermissions(config) {
856
636
  };
857
637
  }
858
638
  /**
859
- * Create a dynamic role-based permission matrix.
860
- *
861
- * Use this when role/action mappings are managed outside code
862
- * (e.g., admin UI matrix, DB-stored ACLs, remote policy service).
639
+ * Create a dynamic role-based permission matrix. Use when role/action
640
+ * mappings are managed outside code (admin UI, DB-stored ACLs, remote
641
+ * policy service).
863
642
  *
864
643
  * Supports:
865
- * - org role union (any assigned org role can grant)
866
- * - global bypass roles
867
- * - wildcard resource/action (`*`)
868
- * - optional in-memory cache
644
+ * - Org role union (any assigned org role can grant)
645
+ * - Global bypass roles
646
+ * - Wildcard resource/action (`*`)
647
+ * - Optional in-memory or distributed cache
648
+ * - Cross-node invalidation via the event bus
869
649
  */
870
650
  function createDynamicPermissionMatrix(config) {
871
651
  const logger = config.logger ?? console;
872
- const legacyTtlMs = config.cache?.ttlMs ?? 0;
652
+ const configuredTtlSeconds = config.cache?.ttlSeconds ?? 0;
873
653
  const hasExternalStore = !!config.cacheStore;
874
- const cacheTtlMs = legacyTtlMs > 0 ? legacyTtlMs : hasExternalStore ? 3e5 : 0;
875
- const internalStore = !config.cacheStore && cacheTtlMs > 0 ? new MemoryCacheStore({
876
- defaultTtlMs: cacheTtlMs,
654
+ const cacheTtlSeconds = configuredTtlSeconds > 0 ? configuredTtlSeconds : hasExternalStore ? 300 : 0;
655
+ const internalStore = !config.cacheStore && cacheTtlSeconds > 0 ? new MemoryCacheStore({
656
+ defaultTtlSeconds: cacheTtlSeconds,
877
657
  maxEntries: config.cache?.maxEntries ?? 1e3
878
658
  }) : void 0;
879
659
  const cacheStore = config.cacheStore ?? internalStore;
@@ -881,7 +661,6 @@ function createDynamicPermissionMatrix(config) {
881
661
  const nodeId = randomUUID().slice(0, 8);
882
662
  const DEFAULT_EVENT_TYPE = "arc.permissions.invalidated";
883
663
  let eventBridge = null;
884
- /** Clear local cache for an org without publishing events (avoids infinite loops). */
885
664
  async function localInvalidateByOrg(orgId) {
886
665
  if (!cacheStore) return;
887
666
  const prefix = `${orgId}::`;
@@ -922,7 +701,7 @@ function createDynamicPermissionMatrix(config) {
922
701
  }
923
702
  const value = await config.resolveRolePermissions(ctx);
924
703
  try {
925
- await cacheStore.set(cacheKey, value, { ttlMs: cacheTtlMs });
704
+ await cacheStore.set(cacheKey, value, cacheTtlSeconds);
926
705
  trackedKeys.add(cacheKey);
927
706
  const maxTracked = config.cache?.maxEntries ?? 1e4;
928
707
  if (trackedKeys.size > maxTracked) {
@@ -1047,41 +826,180 @@ function createDynamicPermissionMatrix(config) {
1047
826
  }
1048
827
  };
1049
828
  }
829
+ //#endregion
830
+ //#region src/permissions/roleHierarchy.ts
1050
831
  /**
1051
- * Require membership in the active team.
1052
- * User must be authenticated, a member of the active org, AND have an active team.
832
+ * Create a role hierarchy from a parent → children map.
1053
833
  *
1054
- * Better Auth teams are flat member groups (no team-level roles).
1055
- * Reads `request.scope.teamId` set by the Better Auth adapter.
834
+ * Each key is a parent role, each value is the array of roles it inherits.
835
+ * Inheritance is transitive: if A B and B → C, then A expands to [A, B, C].
836
+ * Circular references are handled safely (visited set).
837
+ */
838
+ function createRoleHierarchy(map) {
839
+ const cache = /* @__PURE__ */ new Map();
840
+ function resolveRole(role, visited) {
841
+ if (visited.has(role)) return [];
842
+ visited.add(role);
843
+ const cached = cache.get(role);
844
+ if (cached) return cached;
845
+ const children = map[role];
846
+ if (!children || children.length === 0) {
847
+ cache.set(role, [role]);
848
+ return [role];
849
+ }
850
+ const result = [role];
851
+ for (const child of children) result.push(...resolveRole(child, visited));
852
+ const deduped = [...new Set(result)];
853
+ cache.set(role, deduped);
854
+ return deduped;
855
+ }
856
+ return {
857
+ expand(roles) {
858
+ if (roles.length === 0) return [];
859
+ const all = /* @__PURE__ */ new Set();
860
+ for (const role of roles) for (const expanded of resolveRole(role, /* @__PURE__ */ new Set())) all.add(expanded);
861
+ return [...all];
862
+ },
863
+ includes(userRoles, requiredRole) {
864
+ if (userRoles.length === 0) return false;
865
+ return this.expand(userRoles).includes(requiredRole);
866
+ }
867
+ };
868
+ }
869
+ //#endregion
870
+ //#region src/permissions/presets.ts
871
+ /**
872
+ * Permission Presets — Common permission patterns in one call.
873
+ *
874
+ * Reduces 5 lines of permission declarations to 1.
875
+ * Each preset returns a ResourcePermissions object that can be
876
+ * spread or overridden per-operation.
1056
877
  *
1057
878
  * @example
1058
879
  * ```typescript
1059
- * permissions: {
1060
- * list: requireTeamMembership(),
1061
- * create: requireTeamMembership(),
1062
- * }
880
+ * import { permissions } from '@classytic/arc';
881
+ *
882
+ * // Public read, authenticated write
883
+ * defineResource({ name: 'product', permissions: permissions.publicRead() });
884
+ *
885
+ * // Override specific operations
886
+ * defineResource({
887
+ * name: 'product',
888
+ * permissions: permissions.publicRead({ delete: requireRoles(['superadmin']) }),
889
+ * });
1063
890
  * ```
1064
891
  */
1065
- function requireTeamMembership() {
1066
- const check = (ctx) => {
1067
- if (!ctx.user) return {
1068
- granted: false,
1069
- reason: "Authentication required"
1070
- };
1071
- const scope = getRequestScope(ctx.request);
1072
- if (isElevated(scope)) return true;
1073
- if (!isMember(scope)) return {
1074
- granted: false,
1075
- reason: "Organization membership required"
1076
- };
1077
- if (!getTeamId(scope)) return {
1078
- granted: false,
1079
- reason: "No active team"
1080
- };
1081
- return true;
892
+ var presets_exports = /* @__PURE__ */ __exportAll({
893
+ adminOnly: () => adminOnly,
894
+ authenticated: () => authenticated,
895
+ fullPublic: () => fullPublic,
896
+ ownerWithAdminBypass: () => ownerWithAdminBypass,
897
+ publicRead: () => publicRead,
898
+ publicReadAdminWrite: () => publicReadAdminWrite,
899
+ readOnly: () => readOnly
900
+ });
901
+ /**
902
+ * Merge a base preset with user overrides.
903
+ * Overrides replace individual operations — undefined values don't clear them.
904
+ */
905
+ function withOverrides(base, overrides) {
906
+ if (!overrides) return base;
907
+ const filtered = Object.fromEntries(Object.entries(overrides).filter(([, v]) => v !== void 0));
908
+ return {
909
+ ...base,
910
+ ...filtered
1082
911
  };
1083
- check._teamPermission = "membership";
1084
- return check;
912
+ }
913
+ /**
914
+ * Public read, authenticated write.
915
+ * list + get = allowPublic(), create + update + delete = requireAuth()
916
+ */
917
+ function publicRead(overrides) {
918
+ return withOverrides({
919
+ list: allowPublic(),
920
+ get: allowPublic(),
921
+ create: requireAuth(),
922
+ update: requireAuth(),
923
+ delete: requireAuth()
924
+ }, overrides);
925
+ }
926
+ /**
927
+ * Public read, admin write.
928
+ * list + get = allowPublic(), create + update + delete = requireRoles(['admin'])
929
+ */
930
+ function publicReadAdminWrite(roles = ["admin"], overrides) {
931
+ return withOverrides({
932
+ list: allowPublic(),
933
+ get: allowPublic(),
934
+ create: requireRoles(roles),
935
+ update: requireRoles(roles),
936
+ delete: requireRoles(roles)
937
+ }, overrides);
938
+ }
939
+ /**
940
+ * All operations require authentication.
941
+ */
942
+ function authenticated(overrides) {
943
+ return withOverrides({
944
+ list: requireAuth(),
945
+ get: requireAuth(),
946
+ create: requireAuth(),
947
+ update: requireAuth(),
948
+ delete: requireAuth()
949
+ }, overrides);
950
+ }
951
+ /**
952
+ * All operations require specific roles.
953
+ * @param roles - Required roles (user needs at least one). Default: ['admin']
954
+ */
955
+ function adminOnly(roles = ["admin"], overrides) {
956
+ return withOverrides({
957
+ list: requireRoles(roles),
958
+ get: requireRoles(roles),
959
+ create: requireRoles(roles),
960
+ update: requireRoles(roles),
961
+ delete: requireRoles(roles)
962
+ }, overrides);
963
+ }
964
+ /**
965
+ * Owner-scoped with admin bypass.
966
+ * list = auth (scoped to owner), get = auth, create = auth,
967
+ * update + delete = ownership check with admin bypass.
968
+ *
969
+ * @param ownerField - Field containing owner ID (default: 'userId')
970
+ * @param bypassRoles - Roles that bypass ownership check (default: ['admin'])
971
+ */
972
+ function ownerWithAdminBypass(ownerField = "userId", bypassRoles = ["admin"], overrides) {
973
+ return withOverrides({
974
+ list: requireAuth(),
975
+ get: requireAuth(),
976
+ create: requireAuth(),
977
+ update: anyOf(requireRoles(bypassRoles), requireOwnership(ownerField)),
978
+ delete: anyOf(requireRoles(bypassRoles), requireOwnership(ownerField))
979
+ }, overrides);
980
+ }
981
+ /**
982
+ * Full public access — no auth required for any operation.
983
+ * Use sparingly (dev/testing, truly public APIs).
984
+ */
985
+ function fullPublic(overrides) {
986
+ return withOverrides({
987
+ list: allowPublic(),
988
+ get: allowPublic(),
989
+ create: allowPublic(),
990
+ update: allowPublic(),
991
+ delete: allowPublic()
992
+ }, overrides);
993
+ }
994
+ /**
995
+ * Read-only: list + get authenticated, write operations denied.
996
+ * Useful for computed/derived resources.
997
+ */
998
+ function readOnly(overrides) {
999
+ return withOverrides({
1000
+ list: requireAuth(),
1001
+ get: requireAuth()
1002
+ }, overrides);
1085
1003
  }
1086
1004
  //#endregion
1087
- export { publicRead as C, createRoleHierarchy as E, presets_exports as S, readOnly as T, when as _, createOrgPermissions as a, fullPublic as b, requireOrgInScope as c, requireOwnership as d, requireRoles as f, roles as g, requireTeamMembership as h, createDynamicPermissionMatrix as i, requireOrgMembership as l, requireServiceScope as m, allowPublic as n, denyAll as o, requireScopeContext as p, anyOf as r, requireAuth as s, allOf as t, requireOrgRole as u, adminOnly as v, publicReadAdminWrite as w, ownerWithAdminBypass as x, authenticated as y };
1005
+ export { requireAuth as C, when as D, roles as E, not as S, requireRoles as T, requireTeamMembership as _, presets_exports as a, anyOf as b, readOnly as c, createOrgPermissions as d, requireOrgInScope as f, requireServiceScope as g, requireScopeContext as h, ownerWithAdminBypass as i, createRoleHierarchy as l, requireOrgRole as m, authenticated as n, publicRead as o, requireOrgMembership as p, fullPublic as r, publicReadAdminWrite as s, adminOnly as t, createDynamicPermissionMatrix as u, allOf as v, requireOwnership as w, denyAll as x, allowPublic as y };