@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.
- package/dist/{BaseController-BnLYgE9A.mjs → BaseController-C6FlRWxF.mjs} +1 -1
- package/dist/audit/index.d.mts +2 -2
- package/dist/auth/index.d.mts +30 -2
- package/dist/auth/index.mjs +5 -2
- package/dist/cli/commands/describe.d.mts +1 -1
- package/dist/cli/commands/docs.mjs +1 -1
- package/dist/cli/commands/init.mjs +1 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +2 -2
- package/dist/{core-DZ6Xe1jR.mjs → core-b6fgFgnd.mjs} +123 -66
- package/dist/{createActionRouter-BWMDW-dL.mjs → createActionRouter-h1cEEaEX.mjs} +9 -5
- package/dist/{createAggregationRouter-l5mNdYrZ.mjs → createAggregationRouter-B1C0kvTL.mjs} +3 -3
- package/dist/{createApp-Bi61XxqU.mjs → createApp-Ci7yG5gv.mjs} +242 -56
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +1 -1
- package/dist/encryption/index.mjs +9 -5
- package/dist/{errorHandler-Dxf6P_kV.mjs → errorHandler-BTzXSwIg.mjs} +24 -3
- package/dist/{errorHandler-B0e-bq-O.d.mts → errorHandler-C6yv4ZTW.d.mts} +13 -0
- package/dist/factory/index.d.mts +29 -8
- package/dist/factory/index.mjs +34 -3
- package/dist/{fastify-augmentation-DafigGQp.d.mts → fastify-augmentation-9_fuetB4.d.mts} +2 -2
- package/dist/fields-D3lhgzhD.d.mts +121 -0
- package/dist/health-ScyjIVo5.mjs +202 -0
- package/dist/hooks/index.d.mts +1 -1
- package/dist/idempotency/index.mjs +2 -0
- package/dist/{types-DpxoNX5v.d.mts → index-BEu4wjbL.d.mts} +187 -66
- package/dist/{index-B5N-NaHs.d.mts → index-D8MKYGDa.d.mts} +3 -3
- package/dist/{index-Dtrj6XZk.d.mts → index-DbtgCOCj.d.mts} +1287 -1225
- package/dist/{index-B5PoNbfu.d.mts → index-ogIHvOjo.d.mts} +1 -1
- package/dist/index.d.mts +6 -5
- package/dist/index.mjs +3 -3
- package/dist/{init-0YhHkXGk.mjs → init-DleK3p9f.mjs} +118 -2
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/mcp/index.d.mts +3 -3
- package/dist/integrations/mcp/index.mjs +1 -1
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/webhooks.d.mts +8 -4
- package/dist/integrations/webhooks.mjs +9 -5
- package/dist/jsonBody-BlzAoGCe.mjs +43 -0
- package/dist/logger/index.d.mts +26 -1
- package/dist/logger/index.mjs +39 -1
- package/dist/{mcpPlugin-B9XjjNVG.mjs → mcpPlugin-ByA2tf6D.mjs} +6 -2
- package/dist/middleware/index.d.mts +1 -1
- package/dist/{module-WeuU5jfd.d.mts → module-B4u10iOJ.d.mts} +26 -1
- package/dist/{openapi-Kyod_di6.mjs → openapi-BRpMZD7C.mjs} +2 -2
- package/dist/permissions/index.d.mts +3 -2
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/plugins/index.d.mts +22 -5
- package/dist/plugins/index.mjs +11 -280
- package/dist/plugins/response-cache.mjs +1 -0
- package/dist/plugins/tracing-entry.mjs +7 -4
- package/dist/presets/filesUpload.d.mts +2 -2
- package/dist/presets/index.d.mts +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/search.d.mts +2 -2
- package/dist/rateLimitKey-BoWRH3e7.mjs +14 -0
- package/dist/{registerRoute-oGAl0oA8.mjs → registerRoute-xHc42fwB.mjs} +37 -17
- package/dist/registry/index.d.mts +2 -2
- package/dist/requestId-5BhTVzBv.mjs +113 -0
- package/dist/schemaConverter-Ds5reSGs.mjs +168 -0
- package/dist/{schemaIR-ajetw3Fv.mjs → schemaIR-B-6SPe7g.mjs} +2 -2
- package/dist/scim/index.mjs +6 -7
- package/dist/scope/index.mjs +2 -76
- package/dist/scope-k15RXfcq.mjs +64 -0
- package/dist/testing/index.d.mts +83 -3
- package/dist/testing/index.mjs +70 -3
- package/dist/types/index.d.mts +2 -2
- package/dist/{types-DRj-Ut4n.d.mts → types-Bc42CUyk.d.mts} +1 -1
- package/dist/{fields-BGqrAa_u.d.mts → types-Csxga7d3.d.mts} +1 -121
- package/dist/usage/index.d.mts +142 -0
- package/dist/usage/index.mjs +0 -0
- package/dist/utils/index.d.mts +40 -18
- package/dist/utils/index.mjs +2 -2
- package/dist/{utils-CO8a3B_1.mjs → utils-FhJ88TJE.mjs} +59 -13
- package/dist/{versioning-DQ4mCvg0.d.mts → versioning-DLwklPlG.d.mts} +0 -5
- package/llms.txt +39 -0
- package/package.json +453 -447
- package/skills/arc/SKILL.md +14 -8
- package/skills/arc/references/api-reference.md +34 -0
- package/skills/arc/references/auth.md +21 -4
- package/skills/arc/references/mcp.md +1 -1
- package/skills/arc/references/production.md +44 -2
- package/skills/arc-code-review/SKILL.md +1 -1
- package/skills/arc-code-review/references/anti-patterns.md +71 -18
- package/skills/arc-code-review/references/arc-cheatsheet.md +15 -2
- package/skills/arc-code-review/references/migration-recipes.md +7 -7
- package/skills/arc-code-review/references/mongokit-migration.md +4 -3
- package/skills/arc-module-publishing/SKILL.md +2 -1
- package/skills/arc-module-publishing/references/authoring-modules.md +55 -0
- package/skills/arc-module-publishing/references/package-manifest.md +4 -2
- package/dist/preloadResources-CHw3_oA_.mjs +0 -45
- package/dist/schemaConverter-U0MnO9vd.mjs +0 -110
|
@@ -195,124 +195,4 @@ interface PermissionCheckMeta {
|
|
|
195
195
|
};
|
|
196
196
|
}
|
|
197
197
|
//#endregion
|
|
198
|
-
|
|
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
|
package/dist/utils/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Qt as UserLike, S as QueryParserInterface,
|
|
2
|
-
import { n as ErrorMapper } from "../errorHandler-
|
|
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: [...] }`) —
|
|
603
|
-
*
|
|
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 —
|
|
627
|
-
*
|
|
628
|
-
*
|
|
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
|
-
*
|
|
635
|
-
*
|
|
636
|
-
*
|
|
637
|
-
*
|
|
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: [...] }`) —
|
|
667
|
-
*
|
|
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.
|
|
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
|
|
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.
|
package/dist/utils/index.mjs
CHANGED
|
@@ -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-
|
|
3
|
-
import { a as
|
|
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 —
|
|
1299
|
-
*
|
|
1300
|
-
*
|
|
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
|
-
*
|
|
1307
|
-
*
|
|
1308
|
-
*
|
|
1309
|
-
*
|
|
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 {
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
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.
|