@classytic/arc 2.21.0 → 2.22.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 (94) hide show
  1. package/dist/{BaseController-BnLYgE9A.mjs → BaseController-C6FlRWxF.mjs} +1 -1
  2. package/dist/audit/index.d.mts +2 -2
  3. package/dist/auth/index.d.mts +30 -2
  4. package/dist/auth/index.mjs +5 -2
  5. package/dist/cli/commands/describe.d.mts +1 -1
  6. package/dist/cli/commands/docs.mjs +1 -1
  7. package/dist/cli/commands/init.mjs +1 -1
  8. package/dist/cli/index.mjs +1 -1
  9. package/dist/core/index.d.mts +2 -2
  10. package/dist/core/index.mjs +2 -2
  11. package/dist/{core-DZ6Xe1jR.mjs → core-b6fgFgnd.mjs} +123 -66
  12. package/dist/{createActionRouter-BWMDW-dL.mjs → createActionRouter-h1cEEaEX.mjs} +9 -5
  13. package/dist/{createAggregationRouter-l5mNdYrZ.mjs → createAggregationRouter-B1C0kvTL.mjs} +3 -3
  14. package/dist/{createApp-Bi61XxqU.mjs → createApp-Ci7yG5gv.mjs} +242 -56
  15. package/dist/docs/index.d.mts +1 -1
  16. package/dist/docs/index.mjs +1 -1
  17. package/dist/encryption/index.mjs +9 -5
  18. package/dist/{errorHandler-Dxf6P_kV.mjs → errorHandler-BTzXSwIg.mjs} +24 -3
  19. package/dist/{errorHandler-B0e-bq-O.d.mts → errorHandler-C6yv4ZTW.d.mts} +13 -0
  20. package/dist/factory/index.d.mts +29 -8
  21. package/dist/factory/index.mjs +34 -3
  22. package/dist/{fastify-augmentation-DafigGQp.d.mts → fastify-augmentation-9_fuetB4.d.mts} +2 -2
  23. package/dist/fields-D3lhgzhD.d.mts +121 -0
  24. package/dist/health-ScyjIVo5.mjs +202 -0
  25. package/dist/hooks/index.d.mts +1 -1
  26. package/dist/idempotency/index.mjs +2 -0
  27. package/dist/{types-DpxoNX5v.d.mts → index-BEu4wjbL.d.mts} +187 -66
  28. package/dist/{index-B5N-NaHs.d.mts → index-D8MKYGDa.d.mts} +3 -3
  29. package/dist/{index-Dtrj6XZk.d.mts → index-DbtgCOCj.d.mts} +1287 -1225
  30. package/dist/{index-B5PoNbfu.d.mts → index-ogIHvOjo.d.mts} +1 -1
  31. package/dist/index.d.mts +6 -5
  32. package/dist/index.mjs +3 -3
  33. package/dist/{init-0YhHkXGk.mjs → init-DleK3p9f.mjs} +118 -2
  34. package/dist/integrations/index.d.mts +1 -1
  35. package/dist/integrations/mcp/index.d.mts +3 -3
  36. package/dist/integrations/mcp/index.mjs +1 -1
  37. package/dist/integrations/mcp/testing.d.mts +1 -1
  38. package/dist/integrations/mcp/testing.mjs +1 -1
  39. package/dist/integrations/webhooks.d.mts +8 -4
  40. package/dist/integrations/webhooks.mjs +9 -5
  41. package/dist/jsonBody-BlzAoGCe.mjs +43 -0
  42. package/dist/logger/index.d.mts +26 -1
  43. package/dist/logger/index.mjs +39 -1
  44. package/dist/{mcpPlugin-B9XjjNVG.mjs → mcpPlugin-ByA2tf6D.mjs} +6 -2
  45. package/dist/middleware/index.d.mts +1 -1
  46. package/dist/{module-WeuU5jfd.d.mts → module-B4u10iOJ.d.mts} +26 -1
  47. package/dist/{openapi-Kyod_di6.mjs → openapi-BRpMZD7C.mjs} +2 -2
  48. package/dist/permissions/index.d.mts +3 -2
  49. package/dist/pipeline/index.d.mts +1 -1
  50. package/dist/plugins/index.d.mts +22 -5
  51. package/dist/plugins/index.mjs +11 -280
  52. package/dist/plugins/response-cache.mjs +1 -0
  53. package/dist/plugins/tracing-entry.mjs +7 -4
  54. package/dist/presets/filesUpload.d.mts +2 -2
  55. package/dist/presets/index.d.mts +1 -1
  56. package/dist/presets/multiTenant.d.mts +1 -1
  57. package/dist/presets/search.d.mts +2 -2
  58. package/dist/rateLimitKey-BoWRH3e7.mjs +14 -0
  59. package/dist/{registerRoute-oGAl0oA8.mjs → registerRoute-xHc42fwB.mjs} +37 -17
  60. package/dist/registry/index.d.mts +2 -2
  61. package/dist/requestId-5BhTVzBv.mjs +113 -0
  62. package/dist/schemaConverter-Ds5reSGs.mjs +168 -0
  63. package/dist/{schemaIR-ajetw3Fv.mjs → schemaIR-B-6SPe7g.mjs} +2 -2
  64. package/dist/scim/index.mjs +6 -7
  65. package/dist/scope/index.mjs +2 -76
  66. package/dist/scope-k15RXfcq.mjs +64 -0
  67. package/dist/testing/index.d.mts +83 -3
  68. package/dist/testing/index.mjs +70 -3
  69. package/dist/types/index.d.mts +2 -2
  70. package/dist/{types-DRj-Ut4n.d.mts → types-Bc42CUyk.d.mts} +1 -1
  71. package/dist/{fields-BGqrAa_u.d.mts → types-Csxga7d3.d.mts} +1 -121
  72. package/dist/usage/index.d.mts +142 -0
  73. package/dist/usage/index.mjs +0 -0
  74. package/dist/utils/index.d.mts +40 -18
  75. package/dist/utils/index.mjs +2 -2
  76. package/dist/{utils-CO8a3B_1.mjs → utils-FhJ88TJE.mjs} +59 -13
  77. package/dist/{versioning-DQ4mCvg0.d.mts → versioning-DLwklPlG.d.mts} +0 -5
  78. package/llms.txt +39 -0
  79. package/package.json +453 -447
  80. package/skills/arc/SKILL.md +14 -8
  81. package/skills/arc/references/api-reference.md +34 -0
  82. package/skills/arc/references/auth.md +21 -4
  83. package/skills/arc/references/mcp.md +1 -1
  84. package/skills/arc/references/production.md +44 -2
  85. package/skills/arc-code-review/SKILL.md +1 -1
  86. package/skills/arc-code-review/references/anti-patterns.md +71 -18
  87. package/skills/arc-code-review/references/arc-cheatsheet.md +15 -2
  88. package/skills/arc-code-review/references/migration-recipes.md +7 -7
  89. package/skills/arc-code-review/references/mongokit-migration.md +4 -3
  90. package/skills/arc-module-publishing/SKILL.md +2 -1
  91. package/skills/arc-module-publishing/references/authoring-modules.md +55 -0
  92. package/skills/arc-module-publishing/references/package-manifest.md +4 -2
  93. package/dist/preloadResources-CHw3_oA_.mjs +0 -45
  94. package/dist/schemaConverter-U0MnO9vd.mjs +0 -110
@@ -195,124 +195,4 @@ interface PermissionCheckMeta {
195
195
  };
196
196
  }
197
197
  //#endregion
198
- //#region src/permissions/fields.d.ts
199
- /**
200
- * Field-Level Permissions
201
- *
202
- * Control field visibility and writability per role.
203
- * Integrated into the response path (read) and sanitization path (write).
204
- *
205
- * @example
206
- * ```typescript
207
- * import { fields, defineResource } from '@classytic/arc';
208
- *
209
- * const userResource = defineResource({
210
- * name: 'user',
211
- * adapter: userAdapter,
212
- * fields: {
213
- * salary: fields.visibleTo(['admin', 'hr']),
214
- * internalNotes: fields.writableBy(['admin']),
215
- * email: fields.redactFor(['viewer']),
216
- * password: fields.hidden(),
217
- * },
218
- * });
219
- * ```
220
- */
221
- type FieldPermissionType = "hidden" | "visibleTo" | "writableBy" | "redactFor";
222
- interface FieldPermission {
223
- readonly _type: FieldPermissionType;
224
- readonly roles?: readonly string[];
225
- readonly redactValue?: unknown;
226
- }
227
- type FieldPermissionMap = Record<string, FieldPermission>;
228
- declare const fields: {
229
- /**
230
- * Field is never included in responses. Not writable via API.
231
- *
232
- * @example
233
- * ```typescript
234
- * fields: { password: fields.hidden() }
235
- * ```
236
- */
237
- hidden(): FieldPermission;
238
- /**
239
- * Field is only visible to users with specified roles.
240
- * Other users don't see the field at all.
241
- *
242
- * @example
243
- * ```typescript
244
- * fields: { salary: fields.visibleTo(['admin', 'hr']) }
245
- * ```
246
- */
247
- visibleTo(roles: readonly string[]): FieldPermission;
248
- /**
249
- * Field is only writable by users with specified roles.
250
- * All users can still read the field. Users without the role
251
- * have the field silently stripped from write operations.
252
- *
253
- * @example
254
- * ```typescript
255
- * fields: { role: fields.writableBy(['admin']) }
256
- * ```
257
- */
258
- writableBy(roles: readonly string[]): FieldPermission;
259
- /**
260
- * Field is redacted (replaced with a placeholder) for specified roles.
261
- * Other users see the real value.
262
- *
263
- * @param roles - Roles that see the redacted value
264
- * @param redactValue - Replacement value (default: '***')
265
- *
266
- * @example
267
- * ```typescript
268
- * fields: {
269
- * email: fields.redactFor(['viewer']),
270
- * ssn: fields.redactFor(['basic'], '***-**-****'),
271
- * }
272
- * ```
273
- */
274
- redactFor(roles: readonly string[], redactValue?: unknown): FieldPermission;
275
- };
276
- /**
277
- * Apply field-level READ permissions to a response object.
278
- * Strips hidden fields, enforces visibility, and applies redaction.
279
- *
280
- * @param data - The response object (mutated in place for performance)
281
- * @param fieldPermissions - Field permission map from resource config
282
- * @param userRoles - Current user's roles (empty array for unauthenticated)
283
- * @returns The filtered object
284
- */
285
- declare function applyFieldReadPermissions<T extends Record<string, unknown>>(data: T, fieldPermissions: FieldPermissionMap, userRoles: readonly string[]): T;
286
- /**
287
- * Result of applying write permissions — includes both the filtered body
288
- * and the list of fields that were stripped so callers can decide whether
289
- * to reject the request (secure default) or silently strip (legacy).
290
- */
291
- interface FieldWritePermissionResult<T extends Record<string, unknown>> {
292
- readonly body: T;
293
- readonly deniedFields: readonly string[];
294
- }
295
- /**
296
- * Apply field-level WRITE permissions to request body.
297
- *
298
- * Returns both the filtered body and the list of denied fields. Callers are
299
- * expected to reject the request when `deniedFields.length > 0` — silently
300
- * stripping fields hides misconfigurations and real attacks. See
301
- * `BodySanitizer` for the default policy.
302
- *
303
- * @param body - The request body (returns a new filtered copy)
304
- * @param fieldPermissions - Field permission map from resource config
305
- * @param userRoles - Current user's roles
306
- */
307
- declare function applyFieldWritePermissions<T extends Record<string, unknown>>(body: T, fieldPermissions: FieldPermissionMap, userRoles: readonly string[]): FieldWritePermissionResult<T>;
308
- /**
309
- * Resolve effective roles by merging global user roles with org-level roles.
310
- *
311
- * Global roles come from `req.user.role` (normalized via getUserRoles()).
312
- * Org roles come from `req.context.orgRoles` (set by BA adapter's org bridge).
313
- *
314
- * When no org context exists, returns global roles only — backward compatible.
315
- */
316
- declare function resolveEffectiveRoles(userRoles: readonly string[], orgRoles: readonly string[]): string[];
317
- //#endregion
318
- export { applyFieldWritePermissions as a, PermissionCheck as c, UserBase as d, getUserRoles as f, applyFieldReadPermissions as i, PermissionContext as l, FieldPermissionMap as n, fields as o, normalizeRoles as p, FieldPermissionType as r, resolveEffectiveRoles as s, FieldPermission as t, PermissionResult as u };
198
+ export { getUserRoles as a, UserBase as i, PermissionContext as n, normalizeRoles as o, PermissionResult as r, PermissionCheck as t };
@@ -0,0 +1,142 @@
1
+ import { i as RequestScope } from "../types-DH0GupI7.mjs";
2
+ import { n as PermissionContext, t as PermissionCheck } from "../types-Csxga7d3.mjs";
3
+ import { FastifyPluginAsync, FastifyRequest } from "fastify";
4
+ //#region src/usage/requireQuota.d.ts
5
+ interface QuotaOptions {
6
+ /** Counter to enforce — `ai.tokens`, `export.runs`, `api.requests`, ... */
7
+ kind: string;
8
+ /**
9
+ * Ceiling for the current period. A number, `false` (unlimited — lets
10
+ * plan tables express "enterprise: false" uniformly), or a sync/async
11
+ * resolver receiving the permission context (plan-aware quotas).
12
+ */
13
+ limit: number | false | ((ctx: PermissionContext) => number | false | Promise<number | false>);
14
+ /**
15
+ * Actor key override. Default: the meter's own chain
16
+ * (org → user → client → `ip:<addr>`), so quota buckets are the same
17
+ * buckets usage writes and rate limits read.
18
+ */
19
+ actorOf?: (ctx: PermissionContext) => string | undefined;
20
+ /**
21
+ * Counter-store failure policy. Default `'allow'` (fail-open — see
22
+ * module header). `'deny'` throws the same 429 shape with
23
+ * `code: 'quota.unavailable'` and status 503.
24
+ */
25
+ onStoreError?: "allow" | "deny";
26
+ /** Memoize summary reads per (actor, kind) for this many ms. Default 0. */
27
+ cacheTtlMs?: number;
28
+ }
29
+ declare function requireQuota(options: QuotaOptions): PermissionCheck;
30
+ //#endregion
31
+ //#region src/usage/stores/interface.d.ts
32
+ /**
33
+ * Usage store contract — period-bucketed counters per actor.
34
+ *
35
+ * CANONICAL HOME: `@classytic/repo-core/usage` (>=0.10) owns this
36
+ * contract for the ecosystem — kits implement it there without ever
37
+ * depending on arc (`@classytic/mongokit/usage`, ...). This file is a
38
+ * deliberate STRUCTURAL MIRROR of that shape — same trick as
39
+ * `ScheduleLockLike` vs repo-core's `LockAdapter` — so arc's emitted
40
+ * declarations carry no repo-core@>=0.10 subpath reference and the
41
+ * peer floor stays honest. Any conforming adapter assigns directly.
42
+ *
43
+ * The deliberately tiny surface (one increment, one read) is what makes
44
+ * every backend trivial: memory (dev/tests), Redis (`HINCRBY`), or any kit
45
+ * (Mongo `$inc` upsert) implements it in a handful of lines. Aggregation
46
+ * happens IN the store — arc never buffers counters in process memory
47
+ * beyond the built-in memory store, so multi-replica deployments stay
48
+ * correct as long as the store is shared.
49
+ *
50
+ * This is the HORIZONTAL usage layer (per-actor, per-period platform
51
+ * accounting for quotas/plans/billing). Itemized vertical records — e.g.
52
+ * `@classytic/arc-ai/usage`'s per-run AI token/cost tracking — keep their
53
+ * own richer stores and can sink into this one (`kind: 'ai.tokens'`).
54
+ */
55
+ /** One counter cell: (actor, period, kind). */
56
+ interface UsageBucket {
57
+ /** Who consumed — org / user / client id, or an IP-derived fallback. */
58
+ actor: string;
59
+ /** Aggregation period, `YYYY-MM` (see `usagePeriod()`). */
60
+ period: string;
61
+ /** Namespaced counter, dot-separated: `api.requests`, `ai.tokens`, `storage.bytes`. */
62
+ kind: string;
63
+ }
64
+ interface UsageStore {
65
+ /** Store name for diagnostics (e.g. 'memory', 'redis', 'mongo'). */
66
+ readonly name: string;
67
+ /** Add `amount` to the bucket's counter, creating it at 0 first. MUST be atomic per bucket. */
68
+ increment(bucket: UsageBucket, amount: number): Promise<void> | void;
69
+ /** Every counter for an actor in a period: `{ 'api.requests': 40231, ... }`. */
70
+ summary(actor: string, period: string): Promise<Record<string, number>>;
71
+ /** Optional cleanup hook (connections, timers). */
72
+ close?(): Promise<void>;
73
+ }
74
+ /**
75
+ * Canonical period key for a date — calendar month, UTC: `2026-07`.
76
+ * Monthly is the billing-native granularity; stores that want finer
77
+ * windows can shard internally without changing the contract.
78
+ */
79
+ declare function usagePeriod(date?: Date): string;
80
+ //#endregion
81
+ //#region src/usage/stores/memory.d.ts
82
+ declare class MemoryUsageStore implements UsageStore {
83
+ #private;
84
+ readonly name = "memory";
85
+ increment(bucket: UsageBucket, amount: number): void;
86
+ summary(actor: string, period: string): Promise<Record<string, number>>;
87
+ /** Test convenience — drop everything. */
88
+ clear(): void;
89
+ }
90
+ //#endregion
91
+ //#region src/usage/usagePlugin.d.ts
92
+ interface UsageTrackOptions {
93
+ /** Count every completed request as `api.requests` (default: true). */
94
+ requests?: boolean;
95
+ /** Sum `content-length` response bytes as `api.egress.bytes` (default: false). */
96
+ egress?: boolean;
97
+ }
98
+ interface UsagePluginOptions {
99
+ /** Master switch — `false` registers a typed no-op (config-gated environments). */
100
+ enabled?: boolean;
101
+ /** Counter backend. Default: in-memory (per-process — use a shared store in multi-replica). */
102
+ store?: UsageStore;
103
+ /** Automatic per-request accounting. Default: `{ requests: true, egress: false }`. */
104
+ track?: UsageTrackOptions;
105
+ /**
106
+ * Derive the actor key from a request. Default: org → user → client →
107
+ * `ip:<addr>` (same chain as `createTenantKeyGenerator`).
108
+ */
109
+ actorOf?: (request: FastifyRequest) => string | undefined;
110
+ /**
111
+ * Paths exempt from automatic tracking. Exact or trailing-`*` prefix.
112
+ * Default: `['/_health*', '/_metrics*']`.
113
+ */
114
+ ignorePaths?: string[];
115
+ }
116
+ /** The `fastify.usage` surface. */
117
+ interface UsageMeter {
118
+ /**
119
+ * Add to a counter. `actor` may be a raw key or a `RequestScope`
120
+ * (derived via the org → user → client chain). Fire-and-forget safe:
121
+ * awaiting is optional, errors never throw into request flow.
122
+ */
123
+ record(actor: string | RequestScope | undefined, kind: string, amount?: number): Promise<void>;
124
+ /** All counters for an actor. Period defaults to the current month. */
125
+ summary(actor: string, period?: string): Promise<Record<string, number>>;
126
+ /** Canonical period key (`YYYY-MM`) for a date. */
127
+ period(date?: Date): string;
128
+ /** The actor key automatic tracking would use for this request. */
129
+ actorOf(request: FastifyRequest): string;
130
+ /** The active store (diagnostics). */
131
+ readonly store: UsageStore;
132
+ }
133
+ declare module "fastify" {
134
+ interface FastifyInstance {
135
+ /** Per-actor usage counters — registered by `usagePlugin`. */
136
+ usage?: UsageMeter;
137
+ }
138
+ }
139
+ declare const usagePluginFn: FastifyPluginAsync<UsagePluginOptions>;
140
+ declare const _default: FastifyPluginAsync<UsagePluginOptions>;
141
+ //#endregion
142
+ export { MemoryUsageStore, type QuotaOptions, type UsageBucket, type UsageMeter, type UsagePluginOptions, type UsageStore, type UsageTrackOptions, _default as default, requireQuota, usagePeriod, usagePluginFn as usagePlugin };
Binary file
@@ -1,5 +1,5 @@
1
- import { Qt as UserLike, S as QueryParserInterface, b as ParsedQuery, et as OpenApiSchemas, qt as AnyRecord, st as ResourceConfig } from "../index-Dtrj6XZk.mjs";
2
- import { n as ErrorMapper } from "../errorHandler-B0e-bq-O.mjs";
1
+ import { $ as OpenApiSchemas, Qt as UserLike, S as QueryParserInterface, U as ResourceConfig, b as ParsedQuery, qt as AnyRecord } from "../index-DbtgCOCj.mjs";
2
+ import { n as ErrorMapper } from "../errorHandler-C6yv4ZTW.mjs";
3
3
  import { a as NotFoundError, c as RateLimitError, d as ValidationError, f as createDomainError, i as ForbiddenError, l as ServiceUnavailableError, m as isArcError, n as ConflictError, o as OrgAccessDeniedError, p as createError, r as ErrorOptions, s as OrgRequiredError, t as ArcError, u as UnauthorizedError } from "../errors-DHcHVAgk.mjs";
4
4
  import { errorContractSchema, errorDetailSchema } from "@classytic/repo-core/errors";
5
5
  import { FastifyInstance, FastifyReply, FastifyRequest, RouteHandlerMethod } from "fastify";
@@ -599,9 +599,8 @@ declare function createQueryParser(options?: ArcQueryParserOptions): ArcQueryPar
599
599
  interface JsonSchema {
600
600
  /**
601
601
  * Optional because JSON Schema allows top-level combinator-only schemas
602
- * (`{ oneOf: [...] }`, `{ anyOf: [...] }`, `{ allOf: [...] }`) — see
603
- * `listResponse()`, which emits a `oneOf` of the four canonical list
604
- * shapes with no top-level `type`.
602
+ * (`{ oneOf: [...] }`, `{ anyOf: [...] }`, `{ allOf: [...] }`) — hosts
603
+ * may pass such schemas anywhere a `JsonSchema` is accepted.
605
604
  */
606
605
  type?: string | string[];
607
606
  properties?: Record<string, JsonSchema | AnyRecord>;
@@ -623,18 +622,25 @@ interface JsonSchema {
623
622
  */
624
623
  declare const paginationSchema: JsonSchema;
625
624
  /**
626
- * List response schema — full union of every wire shape `toCanonicalList`
627
- * can emit. Hosts who know their endpoint only ever produces one variant
628
- * can pin to a narrower helper:
625
+ * List response schema — accepts every wire shape `toCanonicalList` can
626
+ * emit. Hosts who know their endpoint only ever produces one variant can
627
+ * pin to a narrower helper (sharper OpenAPI docs, same serializer speed):
629
628
  * - `offsetListResponse(item)` — `{ method: 'offset', data, page, limit, total, pages, hasNext, hasPrev }`
630
629
  * - `keysetListResponse(item)` — `{ method: 'keyset', data, limit, hasMore, next: string|null }`
631
630
  * - `aggregateListResponse(item)` — `{ method: 'aggregate', ...offset fields }`
632
631
  * - `bareListResponse(item)` — `{ data }`
633
632
  *
634
- * The default `listResponse(item)` is the union (`oneOf`) of all four so
635
- * Fastify validation accepts any canonical kit shape — pre-2.13 this only
636
- * modelled offset and rejected keyset/aggregate/bare lists at the
637
- * response-schema gate.
633
+ * SHAPE: one merged permissive object, NOT a `oneOf` union (2.22). The
634
+ * pre-2.22 union was semantically identical on the wire — every variant is
635
+ * `additionalProperties: true` and the property definitions never conflict
636
+ * (`next` only exists on keyset, the offset counters only on offset/
637
+ * aggregate; fast-json-stringify skips undefined optionals) — but
638
+ * fast-json-stringify implements top-level `oneOf` by running full AJV
639
+ * validation PER RESPONSE to pick the branch, on the hottest route of
640
+ * every arc app. The merge keeps the exact bytes (pinned by
641
+ * `tests/utils/list-response-serialization.test.ts`) and drops the
642
+ * per-response branch validation. Only `data` is required — the one field
643
+ * all four variants share.
638
644
  */
639
645
  declare function listResponse(itemSchema: JsonSchema): JsonSchema;
640
646
  /** Offset variant — `{ method: 'offset', data, page, limit, total, pages, hasNext, hasPrev }`. */
@@ -663,9 +669,8 @@ declare const responses: {
663
669
  schema: {
664
670
  /**
665
671
  * Optional because JSON Schema allows top-level combinator-only schemas
666
- * (`{ oneOf: [...] }`, `{ anyOf: [...] }`, `{ allOf: [...] }`) — see
667
- * `listResponse()`, which emits a `oneOf` of the four canonical list
668
- * shapes with no top-level `type`.
672
+ * (`{ oneOf: [...] }`, `{ anyOf: [...] }`, `{ allOf: [...] }`) — hosts
673
+ * may pass such schemas anywhere a `JsonSchema` is accepted.
669
674
  */
670
675
  type?: string | string[];
671
676
  properties?: Record<string, JsonSchema | AnyRecord>;
@@ -1103,6 +1108,20 @@ declare function isJsonSchema(input: unknown): input is Record<string, unknown>;
1103
1108
  * Check if an object is a Zod schema (has `_zod` marker from Zod v4).
1104
1109
  */
1105
1110
  declare function isZodSchema(input: unknown): boolean;
1111
+ /**
1112
+ * How `toJsonSchema` reacts when a Zod schema CANNOT be converted (zod not
1113
+ * installed/loaded, zod v3, or `z.toJSONSchema()` threw):
1114
+ *
1115
+ * - `'throw'` — fail fast with an actionable error. Used by
1116
+ * `convertRouteSchema` (VALIDATION path): a schema that silently degrades
1117
+ * to `{ type: 'object' }` is validation switched off — the worst outcome
1118
+ * for a wire contract, worse than a boot failure.
1119
+ * - `'warn'` (default) — log loudly, degrade to a permissive `{ type:
1120
+ * 'object' }`. Used by doc-generation paths (OpenAPI extraction of
1121
+ * Better Auth endpoints etc.), where the schema is third-party and a doc
1122
+ * gap must not take the app down.
1123
+ */
1124
+ type UnconvertibleMode = "warn" | "throw";
1106
1125
  /**
1107
1126
  * Convert any schema input to JSON Schema.
1108
1127
  *
@@ -1110,13 +1129,16 @@ declare function isZodSchema(input: unknown): boolean;
1110
1129
  * 1. `null`/`undefined` → `undefined`
1111
1130
  * 2. Already JSON Schema → pass through as-is (zero overhead)
1112
1131
  * 3. Zod v4 schema → `z.toJSONSchema(schema, { target })`
1113
- * 4. Unrecognized object → return as-is (treat as opaque schema)
1132
+ * 4. Zod v3 schema → unconvertible (actionable "upgrade to v4" error/warn)
1133
+ * 5. Unrecognized object → return as-is (treat as opaque schema)
1114
1134
  *
1115
- * @param input Schema (Zod, plain JSON Schema, or opaque object)
1135
+ * @param input Schema (Zod, plain JSON Schema, or opaque object)
1116
1136
  * @param target Output target — defaults to `draft-7` for Fastify compatibility.
1117
1137
  * Pass `openapi-3.0`/`openapi-3.1` for OpenAPI document generation.
1138
+ * @param mode Failure policy for unconvertible Zod schemas — see
1139
+ * {@link UnconvertibleMode}. Defaults to `'warn'`.
1118
1140
  */
1119
- declare function toJsonSchema(input: unknown, target?: JsonSchemaTarget): Record<string, unknown> | undefined;
1141
+ declare function toJsonSchema(input: unknown, target?: JsonSchemaTarget, mode?: UnconvertibleMode): Record<string, unknown> | undefined;
1120
1142
  /**
1121
1143
  * Convert all schema fields in an OpenApiSchemas object.
1122
1144
  * JSON Schema values pass through unchanged. Only Zod schemas are converted.
@@ -1,6 +1,6 @@
1
1
  import { a as OrgAccessDeniedError, c as ServiceUnavailableError, d as createDomainError, f as createError, i as NotFoundError, l as UnauthorizedError, n as ConflictError, o as OrgRequiredError, p as isArcError, r as ForbiddenError, s as RateLimitError, t as ArcError, u as ValidationError } from "../errors-DkO8Nk6-.mjs";
2
- import { A as createCircuitBreaker, C as defineErrorMapper, D as CircuitBreakerError, E as CircuitBreaker, F as simpleEqualityMatcher, M as assertValidConfig, N as formatValidationErrors, O as CircuitBreakerRegistry, P as validateResourceConfig, S as defineGuard, T as withCompensation, _ as ArcQueryParser, a as bareListResponse, b as MONGO_DIALECT, c as errorDetailSchema, d as keysetListResponse, f as listResponse, g as responses, h as queryParams, i as aggregateListResponse, j as createCircuitBreakerRegistry, k as CircuitState, l as getDefaultCrudSchemas, m as paginationSchema, n as createStateMachine, o as deleteResponse, p as offsetListResponse, r as scheduleBackground, s as errorContractSchema, t as getUserId, u as getListQueryParams, v as createQueryParser, w as defineCompensation, x as NEUTRAL_DIALECT, y as handleRaw } from "../utils-CO8a3B_1.mjs";
3
- import { a as toJsonSchema, i as isZodSchema, n as convertRouteSchema, r as isJsonSchema, t as convertOpenApiSchemas } from "../schemaConverter-U0MnO9vd.mjs";
2
+ import { A as createCircuitBreaker, C as defineErrorMapper, D as CircuitBreakerError, E as CircuitBreaker, F as simpleEqualityMatcher, M as assertValidConfig, N as formatValidationErrors, O as CircuitBreakerRegistry, P as validateResourceConfig, S as defineGuard, T as withCompensation, _ as ArcQueryParser, a as bareListResponse, b as MONGO_DIALECT, c as errorDetailSchema, d as keysetListResponse, f as listResponse, g as responses, h as queryParams, i as aggregateListResponse, j as createCircuitBreakerRegistry, k as CircuitState, l as getDefaultCrudSchemas, m as paginationSchema, n as createStateMachine, o as deleteResponse, p as offsetListResponse, r as scheduleBackground, s as errorContractSchema, t as getUserId, u as getListQueryParams, v as createQueryParser, w as defineCompensation, x as NEUTRAL_DIALECT, y as handleRaw } from "../utils-FhJ88TJE.mjs";
3
+ import { a as isZodSchema, i as isJsonSchema, n as convertRouteSchema, o as toJsonSchema, t as convertOpenApiSchemas } from "../schemaConverter-Ds5reSGs.mjs";
4
4
  import { a as promoteStreamTokenToHeader, i as pipeUIMessageStreamToReply, n as forwardedStreamHeaders, r as isReadableStream, t as UI_MESSAGE_STREAM_HEADERS } from "../streaming-BQcW6_Kq.mjs";
5
5
  import { t as hasEvents } from "../typeGuards-C9KXUly6.mjs";
6
6
  export { ArcError, ArcQueryParser, CircuitBreaker, CircuitBreakerError, CircuitBreakerRegistry, CircuitState, ConflictError, ForbiddenError, MONGO_DIALECT, NEUTRAL_DIALECT, NotFoundError, OrgAccessDeniedError, OrgRequiredError, RateLimitError, ServiceUnavailableError, UI_MESSAGE_STREAM_HEADERS, UnauthorizedError, ValidationError, aggregateListResponse, assertValidConfig, bareListResponse, convertOpenApiSchemas, convertRouteSchema, createCircuitBreaker, createCircuitBreakerRegistry, createDomainError, createError, createQueryParser, createStateMachine, defineCompensation, defineErrorMapper, defineGuard, deleteResponse, errorContractSchema, errorDetailSchema, formatValidationErrors, forwardedStreamHeaders, getDefaultCrudSchemas, getListQueryParams, getUserId, handleRaw, hasEvents, isArcError, isJsonSchema, isReadableStream, isZodSchema, keysetListResponse, listResponse, offsetListResponse, paginationSchema, pipeUIMessageStreamToReply, promoteStreamTokenToHeader, queryParams, responses, scheduleBackground, simpleEqualityMatcher, toJsonSchema, validateResourceConfig, withCompensation };
@@ -1295,26 +1295,72 @@ const paginationSchema = {
1295
1295
  ]
1296
1296
  };
1297
1297
  /**
1298
- * List response schema — full union of every wire shape `toCanonicalList`
1299
- * can emit. Hosts who know their endpoint only ever produces one variant
1300
- * can pin to a narrower helper:
1298
+ * List response schema — accepts every wire shape `toCanonicalList` can
1299
+ * emit. Hosts who know their endpoint only ever produces one variant can
1300
+ * pin to a narrower helper (sharper OpenAPI docs, same serializer speed):
1301
1301
  * - `offsetListResponse(item)` — `{ method: 'offset', data, page, limit, total, pages, hasNext, hasPrev }`
1302
1302
  * - `keysetListResponse(item)` — `{ method: 'keyset', data, limit, hasMore, next: string|null }`
1303
1303
  * - `aggregateListResponse(item)` — `{ method: 'aggregate', ...offset fields }`
1304
1304
  * - `bareListResponse(item)` — `{ data }`
1305
1305
  *
1306
- * The default `listResponse(item)` is the union (`oneOf`) of all four so
1307
- * Fastify validation accepts any canonical kit shape — pre-2.13 this only
1308
- * modelled offset and rejected keyset/aggregate/bare lists at the
1309
- * response-schema gate.
1306
+ * SHAPE: one merged permissive object, NOT a `oneOf` union (2.22). The
1307
+ * pre-2.22 union was semantically identical on the wire — every variant is
1308
+ * `additionalProperties: true` and the property definitions never conflict
1309
+ * (`next` only exists on keyset, the offset counters only on offset/
1310
+ * aggregate; fast-json-stringify skips undefined optionals) — but
1311
+ * fast-json-stringify implements top-level `oneOf` by running full AJV
1312
+ * validation PER RESPONSE to pick the branch, on the hottest route of
1313
+ * every arc app. The merge keeps the exact bytes (pinned by
1314
+ * `tests/utils/list-response-serialization.test.ts`) and drops the
1315
+ * per-response branch validation. Only `data` is required — the one field
1316
+ * all four variants share.
1310
1317
  */
1311
1318
  function listResponse(itemSchema) {
1312
- return { oneOf: [
1313
- offsetListResponse(itemSchema),
1314
- keysetListResponse(itemSchema),
1315
- aggregateListResponse(itemSchema),
1316
- bareListResponse(itemSchema)
1317
- ] };
1319
+ return {
1320
+ type: "object",
1321
+ properties: {
1322
+ method: {
1323
+ type: "string",
1324
+ description: "Pagination method discriminant — 'offset' | 'keyset' | 'aggregate'; absent on bare lists.",
1325
+ example: "offset"
1326
+ },
1327
+ data: {
1328
+ type: "array",
1329
+ items: itemSchema
1330
+ },
1331
+ page: {
1332
+ type: "integer",
1333
+ example: 1
1334
+ },
1335
+ limit: {
1336
+ type: "integer",
1337
+ example: 20
1338
+ },
1339
+ total: {
1340
+ type: "integer",
1341
+ example: 100
1342
+ },
1343
+ pages: {
1344
+ type: "integer",
1345
+ example: 5
1346
+ },
1347
+ hasNext: {
1348
+ type: "boolean",
1349
+ example: false
1350
+ },
1351
+ hasPrev: {
1352
+ type: "boolean",
1353
+ example: false
1354
+ },
1355
+ hasMore: { type: "boolean" },
1356
+ next: {
1357
+ type: ["string", "null"],
1358
+ description: "Cursor token for the next page, or null."
1359
+ }
1360
+ },
1361
+ required: ["data"],
1362
+ additionalProperties: true
1363
+ };
1318
1364
  }
1319
1365
  /** Offset variant — `{ method: 'offset', data, page, limit, total, pages, hasNext, hasPrev }`. */
1320
1366
  function offsetListResponse(itemSchema) {
@@ -1,11 +1,6 @@
1
1
  import { n as DomainEvent } from "./EventTransport-QmY7A258.mjs";
2
2
  import { FastifyPluginAsync, FastifyRequest } from "fastify";
3
3
  //#region src/plugins/health.d.ts
4
- declare module "fastify" {
5
- interface FastifyRequest {
6
- _startTime?: number;
7
- }
8
- }
9
4
  interface HealthCheck {
10
5
  /** Name of the dependency */
11
6
  name: string;
package/llms.txt ADDED
@@ -0,0 +1,39 @@
1
+ # @classytic/arc
2
+
3
+ > Resource-oriented backend framework for Fastify. One `defineResource()` call =
4
+ > REST + validation + auth + permissions + events + caching + OpenAPI + MCP tools.
5
+ > DB-agnostic via @classytic/repo-core adapter kits (mongokit, sqlitekit, prismakit).
6
+
7
+ IMPORTANT for coding agents: hand-rolling is a bug in arc apps. Before writing any
8
+ route, validation, pagination, auth check, cron, queue worker, counter, or audit
9
+ code, check whether arc provides it — it almost certainly does:
10
+
11
+ - CRUD routes → defineResource({ name, adapter, permissions })
12
+ - Role/ownership checks → @classytic/arc/permissions (requireRoles, requireOwnership, permissionMatrix, requireQuota)
13
+ - Identity/tenant access → @classytic/arc/scope (getUserId, requireOrgId — never read scope fields directly)
14
+ - Body/response schemas → schemaOptions.fieldRules (customSchemas when wire ≠ model)
15
+ - Query filter/sort/page → the resource queryParser (allow-listed) + keyset cursors
16
+ - Soft delete/slug/tree/bulk/files/search → presets: [...]
17
+ - Recurring jobs → schedulesPlugin (@classytic/arc/plugins; LockAdapter leader safety)
18
+ - Durable queues → @classytic/arc/integrations/jobs (BullMQ/Redis) or @classytic/streamline (Mongo)
19
+ - Headless worker process→ createWorker(sameOptions) (@classytic/arc/factory)
20
+ - Usage counters/quotas → @classytic/arc/usage (usagePlugin, requireQuota) + rateLimit.plan
21
+ - Audit + record history → audit: true, history: true on the resource
22
+ - Idempotency → @classytic/arc/idempotency
23
+ - Events/outbox/webhooks → @classytic/arc/events, integrations/webhooks
24
+ - AI/MCP tools → generated from every resource (incl. _count/_exists/_distinct list verbs)
25
+ - App boot → createApp({ beforeBoot: connectDatabase, ... }) — DB connect goes IN beforeBoot
26
+
27
+ ## Docs (inside this package)
28
+
29
+ - skills/arc/SKILL.md — full framework guide (boot, defineResource, presets, auth, testing)
30
+ - skills/arc/references/api-reference.md — every subpath import
31
+ - skills/arc-code-review/references/arc-cheatsheet.md — hand-rolled-code → arc-feature map
32
+ - skills/arc-code-review/SKILL.md — audit workflow for existing codebases
33
+
34
+ ## Conventions that bite
35
+
36
+ - request.user is undefined on public routes — guard it.
37
+ - Custom endpoints go in the resource's routes:/actions: (inherit auth/OpenAPI/MCP);
38
+ bare fastify.get() bypasses everything.
39
+ - mongokit >=3.21 defaults schemaGenerator; schemaGenerator: false kills OpenAPI+MCP bodies.