@classytic/arc 2.21.0 → 2.23.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 (115) hide show
  1. package/dist/{BaseController-BnLYgE9A.mjs → BaseController-BQ8KNvWC.mjs} +3 -2
  2. package/dist/{ResourceRegistry-7RnrEWNP.mjs → ResourceRegistry-y9Hnypus.mjs} +1 -0
  3. package/dist/{actionPermissions-ze70xXJi.mjs → actionPermissions-lszoJle1.mjs} +1 -1
  4. package/dist/applyPermissionResult-Tw4iXtlf.mjs +264 -0
  5. package/dist/audit/index.d.mts +2 -2
  6. package/dist/auth/index.d.mts +30 -2
  7. package/dist/auth/index.mjs +6 -3
  8. package/dist/cli/commands/describe.d.mts +1 -1
  9. package/dist/cli/commands/describe.mjs +1 -1
  10. package/dist/cli/commands/docs.mjs +2 -2
  11. package/dist/cli/commands/init.mjs +1 -1
  12. package/dist/cli/commands/introspect.mjs +1 -1
  13. package/dist/cli/index.mjs +1 -1
  14. package/dist/core/index.d.mts +2 -2
  15. package/dist/core/index.mjs +3 -3
  16. package/dist/{core-DZ6Xe1jR.mjs → core-DQ6rDTyH.mjs} +126 -68
  17. package/dist/{createActionRouter-BWMDW-dL.mjs → createActionRouter-BuOv-m4Y.mjs} +10 -6
  18. package/dist/{createAggregationRouter-l5mNdYrZ.mjs → createAggregationRouter-CgJv_BU_.mjs} +3 -3
  19. package/dist/{createApp-Bi61XxqU.mjs → createApp-CjOeWJ9_.mjs} +252 -57
  20. package/dist/docs/index.d.mts +1 -1
  21. package/dist/docs/index.mjs +1 -1
  22. package/dist/encryption/index.mjs +9 -5
  23. package/dist/{errorHandler-Dxf6P_kV.mjs → errorHandler-Bq2OeEQL.mjs} +34 -10
  24. package/dist/{errorHandler-B0e-bq-O.d.mts → errorHandler-C6yv4ZTW.d.mts} +13 -0
  25. package/dist/events/index.mjs +5 -2
  26. package/dist/factory/index.d.mts +46 -9
  27. package/dist/factory/index.mjs +64 -13
  28. package/dist/{fastify-augmentation-DafigGQp.d.mts → fastify-augmentation-g_f9omSc.d.mts} +2 -2
  29. package/dist/fields-D3lhgzhD.d.mts +121 -0
  30. package/dist/health-ScyjIVo5.mjs +202 -0
  31. package/dist/hooks/index.d.mts +1 -1
  32. package/dist/idempotency/index.d.mts +7 -0
  33. package/dist/idempotency/index.mjs +7 -17
  34. package/dist/{index-Dtrj6XZk.d.mts → index-BBsvQt7U.d.mts} +1310 -1226
  35. package/dist/{types-DpxoNX5v.d.mts → index-Ba10fdKp.d.mts} +196 -66
  36. package/dist/{index-B5N-NaHs.d.mts → index-CC5ruDGh.d.mts} +3 -3
  37. package/dist/{index-B5PoNbfu.d.mts → index-wMoWBLkn.d.mts} +68 -2
  38. package/dist/index.d.mts +6 -5
  39. package/dist/index.mjs +6 -5
  40. package/dist/{init-0YhHkXGk.mjs → init-CjO4p_q1.mjs} +123 -4
  41. package/dist/integrations/event-gateway.mjs +1 -1
  42. package/dist/integrations/index.d.mts +1 -1
  43. package/dist/integrations/mcp/index.d.mts +67 -4
  44. package/dist/integrations/mcp/index.mjs +1 -1
  45. package/dist/integrations/mcp/testing.d.mts +1 -1
  46. package/dist/integrations/mcp/testing.mjs +1 -1
  47. package/dist/integrations/webhooks.d.mts +131 -7
  48. package/dist/integrations/webhooks.mjs +169 -10
  49. package/dist/interface-n5hpKg1d.mjs +16 -0
  50. package/dist/jsonBody-BlzAoGCe.mjs +43 -0
  51. package/dist/{loadResourcesFromEntry-OMCvIEE7.mjs → loadResourcesFromEntry-kvGT6EcO.mjs} +1 -1
  52. package/dist/logger/index.d.mts +26 -1
  53. package/dist/logger/index.mjs +39 -1
  54. package/dist/{mcpPlugin-B9XjjNVG.mjs → mcpPlugin-BtzIL_kM.mjs} +312 -39
  55. package/dist/middleware/index.d.mts +1 -1
  56. package/dist/{module-WeuU5jfd.d.mts → module-B4u10iOJ.d.mts} +26 -1
  57. package/dist/{openapi-Kyod_di6.mjs → openapi-jSDx-VDv.mjs} +3 -3
  58. package/dist/permissions/index.d.mts +4 -3
  59. package/dist/permissions/index.mjs +3 -2
  60. package/dist/{permissions-BJMuKMl7.mjs → permissions-tbxFHxjZ.mjs} +77 -264
  61. package/dist/pipeline/index.d.mts +1 -1
  62. package/dist/plugins/index.d.mts +24 -7
  63. package/dist/plugins/index.mjs +14 -282
  64. package/dist/plugins/response-cache.mjs +1 -0
  65. package/dist/plugins/tracing-entry.mjs +7 -4
  66. package/dist/presets/filesUpload.d.mts +2 -2
  67. package/dist/presets/filesUpload.mjs +1 -1
  68. package/dist/presets/index.d.mts +1 -1
  69. package/dist/presets/index.mjs +1 -1
  70. package/dist/presets/multiTenant.d.mts +1 -1
  71. package/dist/presets/search.d.mts +2 -2
  72. package/dist/presets/search.mjs +1 -1
  73. package/dist/{presets-gw0ciIUD.mjs → presets-BOe4Ugkl.mjs} +1 -1
  74. package/dist/rateLimitKey-BoWRH3e7.mjs +14 -0
  75. package/dist/realtime-I8OrfnMR.mjs +375 -0
  76. package/dist/{registerRoute-oGAl0oA8.mjs → registerRoute-CWO6mdYe.mjs} +38 -18
  77. package/dist/registry/index.d.mts +2 -2
  78. package/dist/registry/index.mjs +1 -1
  79. package/dist/requestId-5BhTVzBv.mjs +113 -0
  80. package/dist/schemaConverter-DJvodiHj.mjs +171 -0
  81. package/dist/{schemaIR-ajetw3Fv.mjs → schemaIR-Ca7ySNPx.mjs} +2 -2
  82. package/dist/scim/index.mjs +6 -7
  83. package/dist/scope/index.mjs +2 -76
  84. package/dist/scope-LONQ3mfZ.mjs +37 -0
  85. package/dist/simpleEqualityMatcher-Cg05HPsn.mjs +68 -0
  86. package/dist/{sse-CfUjwjDk.mjs → sse-0u4P4-OI.mjs} +7 -30
  87. package/dist/sseStream-BQ7Qaynp.mjs +57 -0
  88. package/dist/testing/index.d.mts +83 -3
  89. package/dist/testing/index.mjs +70 -3
  90. package/dist/types/index.d.mts +2 -2
  91. package/dist/{types-DRj-Ut4n.d.mts → types-5OuqeTDX.d.mts} +37 -2
  92. package/dist/{fields-BGqrAa_u.d.mts → types-Csxga7d3.d.mts} +1 -121
  93. package/dist/usage/index.d.mts +142 -0
  94. package/dist/usage/index.mjs +0 -0
  95. package/dist/utils/index.d.mts +101 -19
  96. package/dist/utils/index.mjs +5 -3
  97. package/dist/{utils-CO8a3B_1.mjs → utils-CZyeRXTh.mjs} +80 -82
  98. package/dist/{versioning-DQ4mCvg0.d.mts → versioning-D6PvCwpq.d.mts} +43 -9
  99. package/llms.txt +39 -0
  100. package/package.json +513 -447
  101. package/skills/arc/SKILL.md +14 -8
  102. package/skills/arc/references/api-reference.md +34 -0
  103. package/skills/arc/references/auth.md +21 -4
  104. package/skills/arc/references/mcp.md +1 -1
  105. package/skills/arc/references/production.md +734 -654
  106. package/skills/arc-code-review/SKILL.md +1 -1
  107. package/skills/arc-code-review/references/anti-patterns.md +71 -18
  108. package/skills/arc-code-review/references/arc-cheatsheet.md +15 -2
  109. package/skills/arc-code-review/references/migration-recipes.md +7 -7
  110. package/skills/arc-code-review/references/mongokit-migration.md +4 -3
  111. package/skills/arc-module-publishing/SKILL.md +2 -1
  112. package/skills/arc-module-publishing/references/authoring-modules.md +55 -0
  113. package/skills/arc-module-publishing/references/package-manifest.md +4 -2
  114. package/dist/preloadResources-CHw3_oA_.mjs +0 -45
  115. package/dist/schemaConverter-U0MnO9vd.mjs +0 -110
@@ -2,9 +2,10 @@ import { h as SYSTEM_FIELDS, o as DEFAULT_TENANT_FIELD } from "./constants-DNre0
2
2
  import { arcLog } from "./logger/index.mjs";
3
3
  import { d as createDomainError, f as createError, i as NotFoundError, r as ForbiddenError } from "./errors-DkO8Nk6-.mjs";
4
4
  import { b as isElevated, c as getOrgId, n as PUBLIC_SCOPE, x as isMember } from "./types-bHblZ9YN.mjs";
5
- import { F as simpleEqualityMatcher, _ as ArcQueryParser, r as scheduleBackground, t as getUserId } from "./utils-CO8a3B_1.mjs";
6
- import { I as applyFieldWritePermissions, R as resolveEffectiveRoles } from "./permissions-BJMuKMl7.mjs";
5
+ import { t as simpleEqualityMatcher } from "./simpleEqualityMatcher-Cg05HPsn.mjs";
6
+ import { a as applyFieldWritePermissions, s as resolveEffectiveRoles } from "./applyPermissionResult-Tw4iXtlf.mjs";
7
7
  import { t as getUserRoles } from "./types-XTApa8b6.mjs";
8
+ import { _ as ArcQueryParser, r as scheduleBackground, t as getUserId } from "./utils-CZyeRXTh.mjs";
8
9
  import { t as buildQueryKey } from "./keys-Cd316u_y.mjs";
9
10
  //#region src/core/AccessControl.ts
10
11
  const log = arcLog("access-control");
@@ -71,6 +71,7 @@ var ResourceRegistry = class {
71
71
  disabledRoutes: resource.disabledRoutes ? [...resource.disabledRoutes] : void 0,
72
72
  openApiSchemas: options.openApiSchemas,
73
73
  fieldPermissions: extractFieldPermissions(resource.fields),
74
+ fields: resource.fields && Object.keys(resource.fields).length > 0 ? resource.fields : void 0,
74
75
  pipelineSteps: extractPipelineSteps(resource.pipe),
75
76
  rateLimit: resource.rateLimit,
76
77
  audit: resource.audit,
@@ -1,5 +1,5 @@
1
1
  import { b as isElevated, c as getOrgId, g as getUserId, n as PUBLIC_SCOPE, x as isMember } from "./types-bHblZ9YN.mjs";
2
- import { F as applyFieldReadPermissions, R as resolveEffectiveRoles } from "./permissions-BJMuKMl7.mjs";
2
+ import { i as applyFieldReadPermissions, s as resolveEffectiveRoles } from "./applyPermissionResult-Tw4iXtlf.mjs";
3
3
  import { t as getUserRoles } from "./types-XTApa8b6.mjs";
4
4
  import { t as executePipeline } from "./pipe-BOLmGIxa.mjs";
5
5
  import { isPaginatedResult, toCanonicalList } from "@classytic/repo-core/pagination";
@@ -0,0 +1,264 @@
1
+ //#region src/permissions/fields.ts
2
+ /**
3
+ * Field-Level Permissions
4
+ *
5
+ * Control field visibility and writability per role.
6
+ * Integrated into the response path (read) and sanitization path (write).
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { fields, defineResource } from '@classytic/arc';
11
+ *
12
+ * const userResource = defineResource({
13
+ * name: 'user',
14
+ * adapter: userAdapter,
15
+ * fields: {
16
+ * salary: fields.visibleTo(['admin', 'hr']),
17
+ * internalNotes: fields.writableBy(['admin']),
18
+ * email: fields.redactFor(['viewer']),
19
+ * password: fields.hidden(),
20
+ * },
21
+ * });
22
+ * ```
23
+ */
24
+ /** Type guard for Mongoose-like documents with toObject() */
25
+ function isMongooseDoc(obj) {
26
+ return !!obj && typeof obj === "object" && "toObject" in obj && typeof obj.toObject === "function";
27
+ }
28
+ const fields = {
29
+ /**
30
+ * Field is never included in responses. Not writable via API.
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * fields: { password: fields.hidden() }
35
+ * ```
36
+ */
37
+ hidden() {
38
+ return { _type: "hidden" };
39
+ },
40
+ /**
41
+ * Field is only visible to users with specified roles.
42
+ * Other users don't see the field at all.
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * fields: { salary: fields.visibleTo(['admin', 'hr']) }
47
+ * ```
48
+ */
49
+ visibleTo(roles) {
50
+ return {
51
+ _type: "visibleTo",
52
+ roles
53
+ };
54
+ },
55
+ /**
56
+ * Field is only writable by users with specified roles.
57
+ * All users can still read the field. Users without the role
58
+ * have the field silently stripped from write operations.
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * fields: { role: fields.writableBy(['admin']) }
63
+ * ```
64
+ */
65
+ writableBy(roles) {
66
+ return {
67
+ _type: "writableBy",
68
+ roles
69
+ };
70
+ },
71
+ /**
72
+ * Field is redacted (replaced with a placeholder) for specified roles.
73
+ * Other users see the real value.
74
+ *
75
+ * @param roles - Roles that see the redacted value
76
+ * @param redactValue - Replacement value (default: '***')
77
+ *
78
+ * @example
79
+ * ```typescript
80
+ * fields: {
81
+ * email: fields.redactFor(['viewer']),
82
+ * ssn: fields.redactFor(['basic'], '***-**-****'),
83
+ * }
84
+ * ```
85
+ */
86
+ redactFor(roles, redactValue = "***") {
87
+ return {
88
+ _type: "redactFor",
89
+ roles,
90
+ redactValue
91
+ };
92
+ }
93
+ };
94
+ /**
95
+ * Apply field-level READ permissions to a response object.
96
+ * Strips hidden fields, enforces visibility, and applies redaction.
97
+ *
98
+ * @param data - The response object (mutated in place for performance)
99
+ * @param fieldPermissions - Field permission map from resource config
100
+ * @param userRoles - Current user's roles (empty array for unauthenticated)
101
+ * @returns The filtered object
102
+ */
103
+ function applyFieldReadPermissions(data, fieldPermissions, userRoles) {
104
+ if (!data || typeof data !== "object") return data;
105
+ const result = { ...isMongooseDoc(data) ? data.toObject() : data };
106
+ for (const [field, perm] of Object.entries(fieldPermissions)) switch (perm._type) {
107
+ case "hidden":
108
+ delete result[field];
109
+ break;
110
+ case "visibleTo":
111
+ if (!perm.roles?.some((r) => userRoles.includes(r))) delete result[field];
112
+ break;
113
+ case "redactFor":
114
+ if (perm.roles?.some((r) => userRoles.includes(r))) result[field] = perm.redactValue ?? "***";
115
+ break;
116
+ case "writableBy": break;
117
+ }
118
+ return result;
119
+ }
120
+ /**
121
+ * Apply field-level WRITE permissions to request body.
122
+ *
123
+ * Returns both the filtered body and the list of denied fields. Callers are
124
+ * expected to reject the request when `deniedFields.length > 0` — silently
125
+ * stripping fields hides misconfigurations and real attacks. See
126
+ * `BodySanitizer` for the default policy.
127
+ *
128
+ * @param body - The request body (returns a new filtered copy)
129
+ * @param fieldPermissions - Field permission map from resource config
130
+ * @param userRoles - Current user's roles
131
+ */
132
+ function applyFieldWritePermissions(body, fieldPermissions, userRoles) {
133
+ const result = { ...body };
134
+ const deniedFields = [];
135
+ for (const [field, perm] of Object.entries(fieldPermissions)) switch (perm._type) {
136
+ case "hidden":
137
+ if (field in result) {
138
+ deniedFields.push(field);
139
+ delete result[field];
140
+ }
141
+ break;
142
+ case "writableBy":
143
+ if (field in result && !perm.roles?.some((r) => userRoles.includes(r))) {
144
+ deniedFields.push(field);
145
+ delete result[field];
146
+ }
147
+ break;
148
+ }
149
+ return {
150
+ body: result,
151
+ deniedFields
152
+ };
153
+ }
154
+ /**
155
+ * Resolve effective roles by merging global user roles with org-level roles.
156
+ *
157
+ * Global roles come from `req.user.role` (normalized via getUserRoles()).
158
+ * Org roles come from `req.context.orgRoles` (set by BA adapter's org bridge).
159
+ *
160
+ * When no org context exists, returns global roles only — backward compatible.
161
+ */
162
+ function resolveEffectiveRoles(userRoles, orgRoles) {
163
+ if (orgRoles.length === 0) return [...userRoles];
164
+ if (userRoles.length === 0) return [...orgRoles];
165
+ return [.../* @__PURE__ */ new Set([...userRoles, ...orgRoles])];
166
+ }
167
+ //#endregion
168
+ //#region src/permissions/applyPermissionResult.ts
169
+ /**
170
+ * Normalize a permission check return value (`boolean | PermissionResult`)
171
+ * into a concrete `PermissionResult`. This is the only place in Arc that
172
+ * promotes booleans to results — keeps the type narrowing honest everywhere.
173
+ */
174
+ function normalizePermissionResult(result) {
175
+ if (typeof result === "boolean") return { granted: result };
176
+ return result;
177
+ }
178
+ /**
179
+ * Apply a granted `PermissionResult` to a Fastify request — merges row-level
180
+ * filters into `_policyFilters` and conditionally installs the scope.
181
+ *
182
+ * **Scope install rule:** only writes `scope` when the current request scope
183
+ * is absent or `public`. This prevents downgrading an already-authenticated
184
+ * request (e.g. Better Auth set `member`, then a permission check returns a
185
+ * narrower `service` scope — the original `member` wins because it came from
186
+ * a more authoritative source).
187
+ *
188
+ * Safe to call with a non-granted result — it simply no-ops. Callers should
189
+ * still check `result.granted` and send an error response before reaching here,
190
+ * but this function tolerates the misuse defensively.
191
+ */
192
+ function applyPermissionResult(result, request) {
193
+ if (!result.granted) return;
194
+ if (result.filters) request._policyFilters = {
195
+ ...request._policyFilters ?? {},
196
+ ...result.filters
197
+ };
198
+ if (result.scope) {
199
+ const current = request.scope;
200
+ if (!current || current.kind === "public") request.scope = result.scope;
201
+ }
202
+ }
203
+ /**
204
+ * Max length of a `PermissionResult.reason` string before we fall back to the
205
+ * generic default message. Upstream checks can return arbitrary strings; we
206
+ * clamp to prevent accidental leakage of internal diagnostics or oversized
207
+ * payloads via the 4xx response body.
208
+ */
209
+ const MAX_DENIAL_REASON_LENGTH = 100;
210
+ /**
211
+ * End-to-end evaluator: runs the permission check, catches throws, normalizes
212
+ * the result, sends a 401/403 response on denial, and applies side-effects on
213
+ * grant. Returns `true` if the caller should continue, `false` if a response
214
+ * has been sent and the caller should return.
215
+ *
216
+ * This is the single source of truth for the 5-step sequence shared by the
217
+ * CRUD router, action router, and MCP tool handlers:
218
+ *
219
+ * 1. `try { await check(ctx) } catch { reply 403 }`
220
+ * 2. `normalizePermissionResult(result)`
221
+ * 3. If denied → 401 (no user) or 403 (user) with clamped reason
222
+ * 4. If granted → `applyPermissionResult` (filters + scope)
223
+ * 5. Return true/false so the caller knows whether to keep going
224
+ *
225
+ * Context construction, pre-check auth gating, and success-path handler
226
+ * invocation stay at the callsite — those are genuinely different per router
227
+ * and don't belong here.
228
+ *
229
+ * @returns `true` if authorized (caller continues), `false` if a response was sent
230
+ */
231
+ async function evaluateAndApplyPermission(check, context, request, reply, opts) {
232
+ let result;
233
+ try {
234
+ result = await check(context);
235
+ } catch (err) {
236
+ request.log?.warn?.({
237
+ err,
238
+ resource: context.resource,
239
+ action: context.action
240
+ }, "Permission check threw");
241
+ reply.code(403).send({
242
+ code: "arc.forbidden",
243
+ message: "Permission denied",
244
+ status: 403
245
+ });
246
+ return false;
247
+ }
248
+ const permResult = normalizePermissionResult(result);
249
+ if (!permResult.granted) {
250
+ const defaultMsg = opts?.defaultDenialMessage?.(context.user) ?? (context.user ? "Permission denied" : "Authentication required");
251
+ const reason = permResult.reason && permResult.reason.length <= MAX_DENIAL_REASON_LENGTH ? permResult.reason : defaultMsg;
252
+ const status = context.user ? 403 : 401;
253
+ reply.code(status).send({
254
+ code: context.user ? "arc.forbidden" : "arc.unauthorized",
255
+ message: reason,
256
+ status
257
+ });
258
+ return false;
259
+ }
260
+ applyPermissionResult(permResult, request);
261
+ return true;
262
+ }
263
+ //#endregion
264
+ export { applyFieldWritePermissions as a, applyFieldReadPermissions as i, evaluateAndApplyPermission as n, fields as o, normalizePermissionResult as r, resolveEffectiveRoles as s, applyPermissionResult as t };
@@ -1,5 +1,5 @@
1
- import "../index-Dtrj6XZk.mjs";
2
- import { d as UserBase } from "../fields-BGqrAa_u.mjs";
1
+ import "../index-BBsvQt7U.mjs";
2
+ import { i as UserBase } from "../types-Csxga7d3.mjs";
3
3
  import { FastifyPluginAsync } from "fastify";
4
4
  import { RepositoryLike } from "@classytic/repo-core/adapter";
5
5
  //#region src/audit/stores/interface.d.ts
@@ -1,5 +1,5 @@
1
- import { Ht as AuthPluginOptions, Vt as AuthHelpers } from "../index-Dtrj6XZk.mjs";
2
- import { c as PermissionCheck } from "../fields-BGqrAa_u.mjs";
1
+ import { Ht as AuthHelpers, Ut as AuthPluginOptions } from "../index-BBsvQt7U.mjs";
2
+ import { t as PermissionCheck } from "../types-Csxga7d3.mjs";
3
3
  import { t as ExternalOpenApiPaths } from "../externalPaths-BE6wFpYv.mjs";
4
4
  import { a as SessionManagerOptions, c as createSessionManager, i as SessionData, n as MemorySessionStoreOptions, o as SessionManagerResult, r as SessionCookieOptions, s as SessionStore, t as MemorySessionStore } from "../sessionManager-BHOM3N_0.mjs";
5
5
  import { FastifyPluginAsync, FastifyReply as FastifyReply$1, FastifyRequest } from "fastify";
@@ -84,6 +84,34 @@ interface BetterAuthAdapterOptions {
84
84
  * When true, includes the actual error message for debugging.
85
85
  */
86
86
  exposeAuthErrors?: boolean;
87
+ /**
88
+ * Per-route rate limit for the Better Auth catch-all (`/api/auth/*`),
89
+ * applied via `@fastify/rate-limit`'s route config (requires the
90
+ * limiter to be registered, which createApp does by default).
91
+ *
92
+ * Credential endpoints deserve a tighter bucket than the global limit
93
+ * (brute-force / credential-stuffing surface), BUT the same catch-all
94
+ * also serves Better Auth's chatty `get-session` heartbeat — so arc
95
+ * ships NO default here; a forced strict limit (the classic "5/min on
96
+ * /login") would 429 every SPA that polls sessions. Pick a ceiling that
97
+ * covers your session-poll cadence, or key the bucket smarter:
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * createBetterAuthAdapter({
102
+ * auth,
103
+ * // simple ceiling over the whole auth surface
104
+ * rateLimit: { max: 60, timeWindow: '1 minute' },
105
+ * });
106
+ *
107
+ * // OR keep the global limiter but exempt the heartbeat:
108
+ * createApp({ rateLimit: { max: 100, skipPaths: ['/api/auth/get-session'] } });
109
+ * ```
110
+ */
111
+ rateLimit?: {
112
+ max: number;
113
+ timeWindow: string | number;
114
+ } | false;
87
115
  }
88
116
  interface BetterAuthAdapterResult {
89
117
  /** Fastify plugin that registers catch-all auth routes */
@@ -1,7 +1,7 @@
1
1
  import { d as createDomainError, l as UnauthorizedError, p as isArcError, t as ArcError } from "../errors-DkO8Nk6-.mjs";
2
- import { h as requireOrgRole, m as requireOrgMembership, v as requireTeamMembership } from "../permissions-BJMuKMl7.mjs";
3
2
  import { n as normalizeRoles, t as getUserRoles } from "../types-XTApa8b6.mjs";
4
- import { a as toJsonSchema } from "../schemaConverter-U0MnO9vd.mjs";
3
+ import { _ as requireOrgMembership, v as requireOrgRole, x as requireTeamMembership } from "../permissions-tbxFHxjZ.mjs";
4
+ import { o as toJsonSchema } from "../schemaConverter-DJvodiHj.mjs";
5
5
  import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
6
6
  import fp from "fastify-plugin";
7
7
  //#region src/auth/authPlugin.ts
@@ -605,7 +605,8 @@ function createBetterAuthAdapter(options) {
605
605
  if (openapiOpt === false) extractedOpenApi = void 0;
606
606
  else if (typeof openapiOpt === "object") extractedOpenApi = openapiOpt;
607
607
  const betterAuthPlugin = async (fastify) => {
608
- fastify.all(`${normalizedBase}/*`, async (request, reply) => {
608
+ const routeOpts = options.rateLimit && typeof options.rateLimit === "object" ? { config: { rateLimit: options.rateLimit } } : {};
609
+ fastify.all(`${normalizedBase}/*`, routeOpts, async (request, reply) => {
609
610
  try {
610
611
  const fetchRequest = toFetchRequest(request);
611
612
  await sendFetchResponse(await auth.handler(fetchRequest), reply);
@@ -619,6 +620,8 @@ function createBetterAuthAdapter(options) {
619
620
  });
620
621
  if (!fastify.hasDecorator("authenticate")) fastify.decorate("authenticate", authenticate);
621
622
  if (!fastify.hasDecorator("optionalAuthenticate")) fastify.decorate("optionalAuthenticate", optionalAuthenticate);
623
+ if (!fastify.hasRequestDecorator("user")) fastify.decorateRequest("user", void 0);
624
+ if (!fastify.hasRequestDecorator("session")) fastify.decorateRequest("session", void 0);
622
625
  if (!extractedOpenApi && openapiOpt !== false && auth.api && typeof auth.api === "object") {
623
626
  const { extractBetterAuthOpenApi } = await import("../betterAuthOpenApi-CV1lxpRX.mjs");
624
627
  extractedOpenApi = extractBetterAuthOpenApi(auth.api, {
@@ -1,4 +1,4 @@
1
- import { V as ResourceDefinition, ht as RouteSchemaOptions, it as RateLimitConfig } from "../../index-Dtrj6XZk.mjs";
1
+ import { Nt as RateLimitConfig, V as ResourceDefinition, et as RouteSchemaOptions } from "../../index-BBsvQt7U.mjs";
2
2
  //#region src/cli/commands/describe.d.ts
3
3
  interface DescribedResource {
4
4
  name: string;
@@ -1,5 +1,5 @@
1
1
  import { t as CRUD_OPERATIONS } from "../../constants-DNre0wRg.mjs";
2
- import { n as stringifyMeasureMap } from "../../ResourceRegistry-7RnrEWNP.mjs";
2
+ import { n as stringifyMeasureMap } from "../../ResourceRegistry-y9Hnypus.mjs";
3
3
  import { resolve } from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
5
  //#region src/cli/commands/describe.ts
@@ -1,5 +1,5 @@
1
- import { t as buildOpenApiSpec } from "../../openapi-Kyod_di6.mjs";
2
- import { t as loadResourcesFromEntry } from "../../loadResourcesFromEntry-OMCvIEE7.mjs";
1
+ import { t as buildOpenApiSpec } from "../../openapi-jSDx-VDv.mjs";
2
+ import { t as loadResourcesFromEntry } from "../../loadResourcesFromEntry-kvGT6EcO.mjs";
3
3
  import { dirname, resolve } from "node:path";
4
4
  import { mkdirSync, writeFileSync } from "node:fs";
5
5
  //#region src/cli/commands/docs.ts
@@ -1,2 +1,2 @@
1
- import { t as init } from "../../init-0YhHkXGk.mjs";
1
+ import { t as init } from "../../init-CjO4p_q1.mjs";
2
2
  export { init };
@@ -1,4 +1,4 @@
1
- import { t as loadResourcesFromEntry } from "../../loadResourcesFromEntry-OMCvIEE7.mjs";
1
+ import { t as loadResourcesFromEntry } from "../../loadResourcesFromEntry-kvGT6EcO.mjs";
2
2
  //#region src/cli/commands/introspect.ts
3
3
  function describePermission(value) {
4
4
  if (!value) return "none";
@@ -2,6 +2,6 @@ import { describe } from "./commands/describe.mjs";
2
2
  import { exportDocs } from "./commands/docs.mjs";
3
3
  import { doctor } from "./commands/doctor.mjs";
4
4
  import { t as generate } from "../generate-usRSdQmP.mjs";
5
- import { t as init } from "../init-0YhHkXGk.mjs";
5
+ import { t as init } from "../init-CjO4p_q1.mjs";
6
6
  import { introspect } from "./commands/introspect.mjs";
7
7
  export { describe, doctor, exportDocs, generate, init, introspect };
@@ -1,3 +1,3 @@
1
- import { At as AggregationMaterializedResult, B as defineResource, Bn as BodySanitizerConfig, Ct as AggMeasureInput, Dt as AggregationDateRangeRequirement, Et as AggregationConfig, Fn as BaseControllerOptions, In as ControllerConfigurableOptions, Ln as ControllerConstructionOptions, Mt as AggregationsMap, Ot as AggregationIndexHint, Rn as ListResult, Tt as AggregationCacheConfig, V as ResourceDefinition, an as SlugExt, cn as BulkMixin, dn as QueryResolverConfig, en as BaseController, fn as AccessControl, in as TreeMixin, jt as AggregationRateLimit, kt as AggregationMaterializedContext, ln as BaseCrudController, nn as SoftDeleteMixin, on as SlugMixin, pn as AccessControlConfig, rn as TreeExt, sn as BulkExt, tn as SoftDeleteExt, un as QueryResolver, wt as AggMeasureShorthand, zn as BodySanitizer } from "../index-Dtrj6XZk.mjs";
2
- import { A as HOOK_OPERATIONS, B as SYSTEM_FIELDS, C as CrudOperation, D as DEFAULT_SORT, E as DEFAULT_MAX_LIMIT, F as MAX_REGEX_LENGTH, I as MAX_SEARCH_LENGTH, L as MUTATION_OPERATIONS, M as HookOperation, N as HookPhase, O as DEFAULT_TENANT_FIELD, P as MAX_FILTER_DEPTH, R as MutationOperation, S as CRUD_OPERATIONS, T as DEFAULT_LIMIT, _ as DefineActionConfig, a as createRequestContext, b as createPermissionMiddleware, c as sendControllerResponse, d as getEntityQuery, f as defineResourceVariants, g as mergeResourceConfig, h as SeamedResourceConfig, i as createFastifyHandler, j as HOOK_PHASES, k as DEFAULT_UPDATE_METHOD, l as getEntityId, m as ResourceSeams, n as isFieldReadable, o as getControllerContext, p as AdapterLike, r as createCrudHandlers, s as getControllerScope, t as collectReadBlockedFields, u as getEntityIdField, v as defineAction, w as DEFAULT_ID_FIELD, x as defineAggregation, y as createCrudRouter, z as RESERVED_QUERY_PARAMS } from "../index-B5N-NaHs.mjs";
1
+ import { B as defineResource, Bn as BodySanitizer, In as BaseControllerOptions, Ln as ControllerConfigurableOptions, Rn as ControllerConstructionOptions, V as ResourceDefinition, Vn as BodySanitizerConfig, an as TreeMixin, cn as BulkExt, ct as AggMeasureShorthand, dn as QueryResolver, dt as AggregationDateRangeRequirement, fn as QueryResolverConfig, ft as AggregationIndexHint, gt as AggregationsMap, ht as AggregationRateLimit, in as TreeExt, ln as BulkMixin, lt as AggregationCacheConfig, mn as AccessControlConfig, mt as AggregationMaterializedResult, nn as SoftDeleteExt, on as SlugExt, pn as AccessControl, pt as AggregationMaterializedContext, rn as SoftDeleteMixin, sn as SlugMixin, st as AggMeasureInput, tn as BaseController, un as BaseCrudController, ut as AggregationConfig, zn as ListResult } from "../index-BBsvQt7U.mjs";
2
+ import { A as HOOK_OPERATIONS, B as SYSTEM_FIELDS, C as CrudOperation, D as DEFAULT_SORT, E as DEFAULT_MAX_LIMIT, F as MAX_REGEX_LENGTH, I as MAX_SEARCH_LENGTH, L as MUTATION_OPERATIONS, M as HookOperation, N as HookPhase, O as DEFAULT_TENANT_FIELD, P as MAX_FILTER_DEPTH, R as MutationOperation, S as CRUD_OPERATIONS, T as DEFAULT_LIMIT, _ as DefineActionConfig, a as createRequestContext, b as createPermissionMiddleware, c as sendControllerResponse, d as getEntityQuery, f as defineResourceVariants, g as mergeResourceConfig, h as SeamedResourceConfig, i as createFastifyHandler, j as HOOK_PHASES, k as DEFAULT_UPDATE_METHOD, l as getEntityId, m as ResourceSeams, n as isFieldReadable, o as getControllerContext, p as AdapterLike, r as createCrudHandlers, s as getControllerScope, t as collectReadBlockedFields, u as getEntityIdField, v as defineAction, w as DEFAULT_ID_FIELD, x as defineAggregation, y as createCrudRouter, z as RESERVED_QUERY_PARAMS } from "../index-CC5ruDGh.mjs";
3
3
  export { AccessControl, type AccessControlConfig, type AdapterLike, type AggMeasureInput, type AggMeasureShorthand, type AggregationCacheConfig, type AggregationConfig, type AggregationDateRangeRequirement, type AggregationIndexHint, type AggregationMaterializedContext, type AggregationMaterializedResult, type AggregationRateLimit, type AggregationsMap, BaseController, type BaseControllerOptions, BaseCrudController, BodySanitizer, type BodySanitizerConfig, type BulkExt, BulkMixin, CRUD_OPERATIONS, type ControllerConfigurableOptions, type ControllerConstructionOptions, CrudOperation, DEFAULT_ID_FIELD, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, DEFAULT_SORT, DEFAULT_TENANT_FIELD, DEFAULT_UPDATE_METHOD, type DefineActionConfig, HOOK_OPERATIONS, HOOK_PHASES, HookOperation, HookPhase, type ListResult, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, MutationOperation, QueryResolver, type QueryResolverConfig, RESERVED_QUERY_PARAMS, ResourceDefinition, type ResourceSeams, SYSTEM_FIELDS, type SeamedResourceConfig, type SlugExt, SlugMixin, type SoftDeleteExt, SoftDeleteMixin, type TreeExt, TreeMixin, collectReadBlockedFields, createCrudHandlers, createCrudRouter, createFastifyHandler, createPermissionMiddleware, createRequestContext, defineAction, defineAggregation, defineResource, defineResourceVariants, getControllerContext, getControllerScope, getEntityId, getEntityIdField, getEntityQuery, isFieldReadable, mergeResourceConfig, sendControllerResponse };
@@ -1,5 +1,5 @@
1
1
  import { a as DEFAULT_SORT, c as HOOK_OPERATIONS, d as MAX_REGEX_LENGTH, f as MAX_SEARCH_LENGTH, h as SYSTEM_FIELDS, i as DEFAULT_MAX_LIMIT, l as HOOK_PHASES, m as RESERVED_QUERY_PARAMS, n as DEFAULT_ID_FIELD, o as DEFAULT_TENANT_FIELD, p as MUTATION_OPERATIONS, r as DEFAULT_LIMIT, s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS, u as MAX_FILTER_DEPTH } from "../constants-DNre0wRg.mjs";
2
- import { a as BulkMixin, c as collectReadBlockedFields, f as AccessControl, i as SlugMixin, l as isFieldReadable, n as TreeMixin, o as BaseCrudController, r as SoftDeleteMixin, s as QueryResolver, t as BaseController, u as BodySanitizer } from "../BaseController-BnLYgE9A.mjs";
3
- import { a as mergeResourceConfig, c as defineAction, d as defineAggregation, i as defineResourceVariants, l as createCrudRouter, n as getEntityIdField, o as defineResource, r as getEntityQuery, s as ResourceDefinition, t as getEntityId, u as createPermissionMiddleware } from "../core-DZ6Xe1jR.mjs";
4
- import { a as createCrudHandlers, c as getControllerContext, l as getControllerScope, o as createFastifyHandler, s as createRequestContext, u as sendControllerResponse } from "../actionPermissions-ze70xXJi.mjs";
2
+ import { a as BulkMixin, c as collectReadBlockedFields, f as AccessControl, i as SlugMixin, l as isFieldReadable, n as TreeMixin, o as BaseCrudController, r as SoftDeleteMixin, s as QueryResolver, t as BaseController, u as BodySanitizer } from "../BaseController-BQ8KNvWC.mjs";
3
+ import { a as defineResource, c as defineAction, d as defineAggregation, i as defineResourceVariants, l as createCrudRouter, n as getEntityIdField, o as ResourceDefinition, r as getEntityQuery, s as mergeResourceConfig, t as getEntityId, u as createPermissionMiddleware } from "../core-DQ6rDTyH.mjs";
4
+ import { a as createCrudHandlers, c as getControllerContext, l as getControllerScope, o as createFastifyHandler, s as createRequestContext, u as sendControllerResponse } from "../actionPermissions-lszoJle1.mjs";
5
5
  export { AccessControl, BaseController, BaseCrudController, BodySanitizer, BulkMixin, CRUD_OPERATIONS, DEFAULT_ID_FIELD, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, DEFAULT_SORT, DEFAULT_TENANT_FIELD, DEFAULT_UPDATE_METHOD, HOOK_OPERATIONS, HOOK_PHASES, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, QueryResolver, RESERVED_QUERY_PARAMS, ResourceDefinition, SYSTEM_FIELDS, SlugMixin, SoftDeleteMixin, TreeMixin, collectReadBlockedFields, createCrudHandlers, createCrudRouter, createFastifyHandler, createPermissionMiddleware, createRequestContext, defineAction, defineAggregation, defineResource, defineResourceVariants, getControllerContext, getControllerScope, getEntityId, getEntityIdField, getEntityQuery, isFieldReadable, mergeResourceConfig, sendControllerResponse };