@kindgi/schema 0.1.7 → 0.1.8

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/README.md CHANGED
@@ -14,14 +14,42 @@ Turn the JSON Schemas that live in `@kindgi/specs/*.schema.json` into runtime-us
14
14
  - `validate<T>(id, data)` — validate data against a schema by `$id`, returning `Result<T, SchemaError>`.
15
15
  - `getSchema(id)` — retrieve the raw parsed schema JSON.
16
16
  - `ids()` — list every registered `$id`.
17
- - **`compileInlineSchema(schema)`** — compile one anonymous JSON Schema (no `$id`, no cross-schema `$ref`) into a `CompiledInlineSchema` whose `validate<T>(data)` returns `Result<T, ValidationErrorLike>`.
18
- - **`compileJsonSchema(schema, options?)`** — compile one JSON Schema into an Ajv `ValidateFunction`, in the dialect it declares in `$schema` (draft-06, draft-07, 2019-09 or 2020-12; 2020-12 when it declares none), with formats checked. `dialects` limits the ones it may declare. `strict` (default `true`) is Ajv's strict mode, a lint for the schemas you write; turn it off for a schema from elsewhere, such as an MCP server's. `useDefaults` fills in each property's `default`. Throws for another dialect, or a schema that doesn't compile.
17
+ - **`compileInlineSchema(schema)`** — compile one anonymous JSON Schema (no `$id`, no cross-schema `$ref`) into a `CompiledInlineSchema` whose `validate<T>(data)` returns `Result<T, ValidationErrorLike>`. Compiled under the process's bounds (see [Bounds](#bounds)); a schema over them is a `compile` error.
18
+ - **`compileJsonSchema(schema, options?)`** — compile one JSON Schema into an Ajv `ValidateFunction`, in the dialect it declares in `$schema` (draft-06, draft-07, 2019-09 or 2020-12; 2020-12 when it declares none), with formats checked. `dialects` limits the ones it may declare. `strict` (default `true`) is Ajv's strict mode, a lint for the schemas you write; turn it off for a schema from elsewhere, such as an MCP server's. `useDefaults` fills in each property's `default`. Compiled under the process's bounds (see [Bounds](#bounds)). Throws for another dialect, a schema over the bounds, or one that doesn't compile.
19
+ - **`holdJsonSchema(schema, options?)`** — `compileJsonSchema` for a caller that keeps the schema. It compiles the schema now and returns a function that gives its validator on each call. It keeps a JSON schema's cache key, not its validator, so the cache stays all that holds it: once the cache drops it, the next call compiles it again, a recompile the budget counts. A schema JSON can't carry is held as compiled.
19
20
  - **`jsonSchemaDialect(schema, allowed?)`** — the dialect a schema declares, as `'draft-06' | 'draft-07' | '2019-09' | '2020-12'`; throws, naming it, for one Kindgi doesn't validate (or one not in `allowed`).
20
21
  - **`versionOf(schema)`** — extract `{ major, semver }` from a schema's `$id` and `$comment`.
21
22
  - **Zod authoring helpers** — `AnySchema` (a JSON Schema object or a Zod v4 schema), `isZodSchema`, `isJsonSchemaObject`, `schemaKindOf`, and `toJSONSchema` / `toJSONSchemaSync` (with `loadZodConverter` / `loadZodConverterSync`), which convert Zod to JSON Schema through Zod's native `z.toJSONSchema()`. `zod` is an optional peer dependency: it is imported only when a Zod schema is passed, and a missing or failed conversion is an `invalid-zod-conversion` error, not a throw.
22
23
  - **`canonicalize(value)`** — deterministic JSON serialization (sorted keys, `undefined` omitted, no whitespace), a subset of RFC 8785 used for signing.
23
24
  - **`SchemaError`** — typed discriminated union covering load / parse / compile / not-found / validation / Zod-conversion errors.
24
25
 
26
+ ## Bounds
27
+
28
+ `compileJsonSchema` and `compileInlineSchema` compile a schema under the process's bounds, from [`@kindgi/bounds`](../bounds/):
29
+
30
+ - A schema nested deeper than `schemaMaxDepth`, or larger than `schemaMaxBytes` as JSON, is refused before it's compiled, with an error naming the limit.
31
+ - A `$ref` is compiled once, where its target is, not copied into each place that uses it (Ajv's `inlineRefs: false`), so the code a schema compiles to grows with its JSON, not with its targets' sizes times their uses.
32
+ - Each schema is compiled once and its validator kept, by the schema's JSON as written and the options it was compiled with. The cache holds at most `schemaCacheEntries` validators and `schemaCacheMaxBytes` of their schemas' JSON, and drops the least recently used first. A schema that didn't compile is kept too, weighing nothing, and throws the same error again.
33
+
34
+ Under `HOSTED_BOUNDS`, whose patterns run on RE2, every check a schema makes of the data runs in time linear in the data (`uniqueItems`: in its size times its depth):
35
+
36
+ - `pattern`, `patternProperties` and `propertyNames` run on RE2, through `compileTenantPattern`. A schema with a pattern RE2 can't run (a lookahead, a backreference) doesn't compile, and the error names the pattern and why.
37
+ - Formats are ajv-formats' `fast` ones. `regex` accepts a pattern whose syntax `compileTenantPattern` accepts, checked without building RE2's program (`tenantPatternSyntaxProblem`). A pattern RE2 refuses only when it builds the program still passes here: one too large (a repeat over 1,000) or with an escape the check doesn't name (`\cA`, `\p{Letter}`). `url` is ajv-formats' own, written without its backtracking. `fast` is looser in places: `date` accepts `2026-02-30`.
38
+ - `uniqueItems` looks for a repeat by a key for each item, not pair by pair. The verdict and the error are Ajv's, down to the pair of items it names. One difference: an object with its own `valueOf` or `toString` key is compared by its keys, where Ajv's compare throws.
39
+
40
+ Under `SELF_HOSTED_BOUNDS`, patterns run on V8, formats are ajv-formats' `full` ones, and `uniqueItems` is Ajv's own.
41
+
42
+ ### Compile time
43
+
44
+ Compiling a large schema holds the event loop, so each tenant's compiles share a budget: `schemaCompileBudgetMs` of compiling in any 60 seconds (2 seconds under `HOSTED_BOUNDS`, none under `SELF_HOSTED_BOUNDS`). A compile is a tenant's when it runs inside `runAsTenant` from `@kindgi/bounds`, and which compiles count is the scope's:
45
+
46
+ - By default, only a schema compiled again: one the tenant compiled before in this process, dropped from the cache since. A tenant's first compile of a schema is free, for its first 4,096 schemas; past them, every compile counts.
47
+ - With `{ compiles: 'all' }`, which a request that publishes schemas uses, every compile counts, the first ones too.
48
+
49
+ Once a tenant has spent its budget, a compile that counts throws `SchemaCompileBudgetError` (its `budgetMs`, and `retryAfterMs` until enough of its compiles leave the window), from `compileInlineSchema` as from `compileJsonSchema`: it isn't the schema's fault, so it isn't returned as a compile error, and it isn't kept. Validators already kept still serve. A compile isn't stopped part way, so a tenant's minute can run over by one compile. Outside a scope, nothing is refused.
50
+
51
+ `createSpecRegistry` and `loadSpecRegistry` compile Kindgi's own schemas, in `@kindgi/specs`, and run their patterns on V8; the formats and `uniqueItems` follow the bounds as above.
52
+
25
53
  ## Example
26
54
 
27
55
  ```ts
@@ -52,4 +80,4 @@ if (validated.kind === 'ok') {
52
80
  - **No Zod on the wire, and no JSON Schema → Zod conversion.** Zod is an authoring convenience: schemas are converted one way, Zod → JSON Schema, and every schema that is registered or exchanged is JSON Schema.
53
81
  - **No schema migration helpers.** Every schema is at its first major version, so there is nothing to migrate between.
54
82
  - **No TypeScript type generation.** Callers define TypeScript types that match schema shapes by hand, or author with Zod and derive them with `z.infer`.
55
- - **No custom Ajv extensions.** Uses stock Ajv + `ajv-formats`, with no custom keywords. Kindgi's own schemas are Draft 2020-12; `compileJsonSchema` compiles a schema from elsewhere with Ajv's class for its declared dialect.
83
+ - **No custom Ajv keywords.** Uses Ajv + `ajv-formats` and the keywords they define; under `HOSTED_BOUNDS`, `uniqueItems` and the `url` and `regex` formats are replaced with linear versions of the same checks (see [Bounds](#bounds)). Kindgi's own schemas are Draft 2020-12; `compileJsonSchema` compiles a schema from elsewhere with Ajv's class for its declared dialect.
@@ -0,0 +1,57 @@
1
+ import { Ajv } from 'ajv';
2
+ import type { Options, ValidateFunction } from 'ajv';
3
+ import { Ajv2019 } from 'ajv/dist/2019.js';
4
+ import { Ajv2020 } from 'ajv/dist/2020.js';
5
+ import type { JsonSchemaDialect } from './dialect.js';
6
+ type AnyAjv = Ajv | Ajv2019 | Ajv2020;
7
+ /** Whose schemas an Ajv compiles, and the keywords they may carry. */
8
+ export interface BoundedAjvOptions {
9
+ /**
10
+ * `tenant` (the default): anyone's, its patterns run as a tenant's do.
11
+ * `kindgi`: Kindgi's own (`@kindgi/specs`), whose patterns are Kindgi's
12
+ * code and run on V8 as the rest of it does; the checks they make of the
13
+ * data (formats, `uniqueItems`) are bounded all the same.
14
+ */
15
+ readonly schemasFrom?: 'tenant' | 'kindgi';
16
+ /** Keywords Kindgi reads off a schema, which validate nothing. */
17
+ readonly annotations?: readonly string[];
18
+ }
19
+ /**
20
+ * An Ajv for `dialect` under the process's bounds, with ajv-formats'
21
+ * formats and the keywords in `annotations`.
22
+ */
23
+ export declare function boundedAjv(dialect: JsonSchemaDialect, options: Options, how?: BoundedAjvOptions): AnyAjv;
24
+ /** What `compileTenantSchema` compiles: the dialect, Ajv's options, and the annotations. */
25
+ export interface TenantSchemaCompile {
26
+ readonly dialect: JsonSchemaDialect;
27
+ readonly options: Options;
28
+ readonly annotations?: readonly string[];
29
+ }
30
+ /**
31
+ * A validator for a tenant's `schema`, compiled once: measured against the
32
+ * bounds first (its depth, then its size as JSON), then taken from the
33
+ * cache or compiled and kept. Throws, as `ajv.compile` does, when it's over
34
+ * a bound or doesn't compile; a schema that didn't compile is kept too, and
35
+ * throws the same error again. Throws `SchemaCompileBudgetError`, and
36
+ * keeps nothing, when the compile counts against a tenant that has spent
37
+ * its budget.
38
+ *
39
+ * The cache key is the schema's JSON as written, keys in their order: the
40
+ * order of `properties` is the order Ajv fills in defaults and reports
41
+ * errors in. A schema holding a value JSON can't carry (a function,
42
+ * `undefined`) is compiled each time.
43
+ */
44
+ export declare function compileTenantSchema(schema: unknown, how: TenantSchemaCompile): ValidateFunction;
45
+ /**
46
+ * A tenant's `schema` compiled now, as `compileTenantSchema` does, for a
47
+ * caller that keeps it: what this keeps is the cache key, not the
48
+ * validator. Each call returns the validator the cache has under the key
49
+ * or, once the cache has dropped it (or the bounds changed), compiles it
50
+ * again as `compileTenantSchema` does, a recompile the tenant's budget
51
+ * counts, and which can throw as it does. So however long a caller keeps
52
+ * it, the cache is all that holds the compiled schema. A schema holding a
53
+ * value JSON can't carry, never cached, is held as compiled.
54
+ */
55
+ export declare function holdTenantSchema(schema: unknown, how: TenantSchemaCompile): () => ValidateFunction;
56
+ export {};
57
+ //# sourceMappingURL=bounded.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bounded.d.ts","sourceRoot":"","sources":["../src/bounded.ts"],"names":[],"mappings":"AAwCA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAC1B,OAAO,KAAK,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,KAAK,CAAC;AAErD,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAI3C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAGtD,KAAK,MAAM,GAAG,GAAG,GAAG,OAAO,GAAG,OAAO,CAAC;AAatC,sEAAsE;AACtE,MAAM,WAAW,iBAAiB;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC3C,kEAAkE;IAClE,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACxB,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,OAAO,EAChB,GAAG,GAAE,iBAAsB,GAC1B,MAAM,CAoCR;AA4CD,4FAA4F;AAC5F,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,mBAAmB,GAAG,gBAAgB,CAE/F;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,OAAO,EACf,GAAG,EAAE,mBAAmB,GACvB,MAAM,gBAAgB,CAWxB"}
@@ -0,0 +1,308 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+ /**
4
+ * The one place `@kindgi/schema` makes an Ajv, under the process's bounds
5
+ * (`@kindgi/bounds`): `compileJsonSchema`, `compileInlineSchema` and
6
+ * `createSpecRegistry` all compile here.
7
+ *
8
+ * With `SELF_HOSTED_BOUNDS`, Ajv is as it always was: patterns on V8,
9
+ * ajv-formats' full formats, Ajv's own `uniqueItems`. On RE2
10
+ * (`HOSTED_BOUNDS`), every check a tenant's schema makes runs in time
11
+ * linear in the data (`uniqueItems`: in its size times its depth):
12
+ * - `pattern`, `patternProperties` and `propertyNames`' patterns run on
13
+ * RE2, through `compileTenantPattern` (Ajv's `code.regExp`); Kindgi's own
14
+ * schemas' patterns stay on V8 (`schemasFrom`);
15
+ * - formats are ajv-formats' `fast` ones; `regex` is a pattern whose
16
+ * syntax `compileTenantPattern` accepts (`tenantPatternSyntaxProblem`,
17
+ * which builds no RE2 program), and `url` is matched without the
18
+ * backtracking its regular expression has (see `linearUrl`);
19
+ * - `uniqueItems` finds a repeat by each item's key, not pair by pair
20
+ * (`./unique-items.ts`).
21
+ *
22
+ * A tenant's schema (`compileTenantSchema`) is also measured before it's
23
+ * compiled, its depth and its size as JSON, and compiled once: the
24
+ * validators are kept by the schema's JSON, the least recently used
25
+ * dropped first (`schemaCacheEntries`, `schemaCacheMaxBytes`). The time
26
+ * its compiles take is charged to the tenant whose work it is
27
+ * (`./compile-budget.ts`).
28
+ */
29
+ import { createHash } from 'node:crypto';
30
+ import { createRequire } from 'node:module';
31
+ import { compileTenantPattern, currentTenant, tenantInputBounds, tenantPatternSyntaxProblem, } from '@kindgi/bounds';
32
+ import { Ajv } from 'ajv';
33
+ import * as addFormatsModule from 'ajv-formats';
34
+ import { Ajv2019 } from 'ajv/dist/2019.js';
35
+ import { Ajv2020 } from 'ajv/dist/2020.js';
36
+ import { CompileBudget, SchemaCompileBudgetError } from './compile-budget.js';
37
+ import { linearUniqueItems } from './unique-items.js';
38
+ const addFormatsRaw = addFormatsModule;
39
+ const addFormats = typeof addFormatsRaw === 'function'
40
+ ? addFormatsRaw
41
+ : addFormatsRaw.default;
42
+ const require = createRequire(import.meta.url);
43
+ /**
44
+ * An Ajv for `dialect` under the process's bounds, with ajv-formats'
45
+ * formats and the keywords in `annotations`.
46
+ */
47
+ export function boundedAjv(dialect, options, how = {}) {
48
+ const onRe2 = tenantInputBounds().regex === 're2';
49
+ const tenants = how.schemasFrom !== 'kindgi';
50
+ const ajvOptions = tenants
51
+ ? {
52
+ ...options,
53
+ // Ajv's default inlines a `$ref` target at every site that uses
54
+ // it, so the code it writes grows as the target's size times its
55
+ // uses while the JSON stays small: 27 KiB of one `$defs` used 400
56
+ // times took 18 s and 2.8 GiB to compile.
57
+ inlineRefs: false,
58
+ ...(onRe2 && { code: { ...options.code, regExp: tenantRegExp } }),
59
+ }
60
+ : options;
61
+ let ajv;
62
+ if (dialect === '2020-12')
63
+ ajv = new Ajv2020(ajvOptions);
64
+ else if (dialect === '2019-09')
65
+ ajv = new Ajv2019(ajvOptions);
66
+ else {
67
+ ajv = new Ajv(ajvOptions);
68
+ if (dialect === 'draft-06') {
69
+ ajv.addMetaSchema(require('ajv/dist/refs/json-schema-draft-06.json'));
70
+ }
71
+ }
72
+ if (onRe2) {
73
+ addFormats(ajv, { mode: 'fast' });
74
+ // A pattern `compileTenantPattern` would accept for its syntax, checked
75
+ // without building RE2's program: data can carry thousands of values.
76
+ ajv.addFormat('regex', (input) => tenantPatternSyntaxProblem(input) === undefined);
77
+ ajv.addFormat('url', linearUrl());
78
+ ajv.removeKeyword('uniqueItems');
79
+ ajv.addKeyword(linearUniqueItems());
80
+ }
81
+ else {
82
+ addFormats(ajv);
83
+ }
84
+ for (const keyword of how.annotations ?? [])
85
+ ajv.addKeyword(keyword);
86
+ return ajv;
87
+ }
88
+ /**
89
+ * Ajv's `code.regExp` on RE2: a schema's pattern compiled by
90
+ * `compileTenantPattern`, and refused, so the schema doesn't compile, when
91
+ * it can't run there. Ajv keeps one compiled pattern per `toString()`.
92
+ */
93
+ const tenantRegExp = Object.assign((source, flags) => {
94
+ const compiled = compileTenantPattern(source, flags);
95
+ if (compiled.kind === 'refused') {
96
+ throw new Error(`The schema's pattern ${JSON.stringify(source)} isn't a regular expression that compiles: ${compiled.reason}.`);
97
+ }
98
+ const { pattern } = compiled;
99
+ const key = JSON.stringify([source, flags]);
100
+ const like = { test: (input) => pattern.test(input), toString: () => key };
101
+ return like;
102
+ },
103
+ // Only Ajv's standalone code reads `code`, and Kindgi never generates it.
104
+ { code: 'tenantRegExp' });
105
+ /**
106
+ * ajv-formats' `url`, with its user-info part `\S+(?::\S*)?@` written
107
+ * `\S+@`: the same strings (`\S` takes the `:` already), without the
108
+ * backtracking that makes it quadratic in a long string with no `@`.
109
+ * Built on first use, so only a process on RE2 depends on its source.
110
+ */
111
+ function linearUrl() {
112
+ if (rewrittenUrl !== undefined)
113
+ return rewrittenUrl;
114
+ const full = require('ajv-formats/dist/formats.js')
115
+ .fullFormats.url;
116
+ const userInfo = '(?:\\S+(?::\\S*)?@)?';
117
+ if (full.source.split(userInfo).length !== 2) {
118
+ throw new Error("ajv-formats' url format isn't the one @kindgi/schema rewrites.");
119
+ }
120
+ rewrittenUrl = new RegExp(full.source.replace(userInfo, '(?:\\S+@)?'), full.flags);
121
+ return rewrittenUrl;
122
+ }
123
+ let rewrittenUrl;
124
+ /**
125
+ * A validator for a tenant's `schema`, compiled once: measured against the
126
+ * bounds first (its depth, then its size as JSON), then taken from the
127
+ * cache or compiled and kept. Throws, as `ajv.compile` does, when it's over
128
+ * a bound or doesn't compile; a schema that didn't compile is kept too, and
129
+ * throws the same error again. Throws `SchemaCompileBudgetError`, and
130
+ * keeps nothing, when the compile counts against a tenant that has spent
131
+ * its budget.
132
+ *
133
+ * The cache key is the schema's JSON as written, keys in their order: the
134
+ * order of `properties` is the order Ajv fills in defaults and reports
135
+ * errors in. A schema holding a value JSON can't carry (a function,
136
+ * `undefined`) is compiled each time.
137
+ */
138
+ export function compileTenantSchema(schema, how) {
139
+ return compileKeyed(schema, how).validate;
140
+ }
141
+ /**
142
+ * A tenant's `schema` compiled now, as `compileTenantSchema` does, for a
143
+ * caller that keeps it: what this keeps is the cache key, not the
144
+ * validator. Each call returns the validator the cache has under the key
145
+ * or, once the cache has dropped it (or the bounds changed), compiles it
146
+ * again as `compileTenantSchema` does, a recompile the tenant's budget
147
+ * counts, and which can throw as it does. So however long a caller keeps
148
+ * it, the cache is all that holds the compiled schema. A schema holding a
149
+ * value JSON can't carry, never cached, is held as compiled.
150
+ */
151
+ export function holdTenantSchema(schema, how) {
152
+ const first = compileKeyed(schema, how);
153
+ if (first.key === undefined)
154
+ return () => first.validate;
155
+ let key = first.key;
156
+ return () => {
157
+ const kept = stateFor(tenantInputBounds()).cache.get(key);
158
+ if (kept?.kind === 'compiled')
159
+ return kept.validate;
160
+ const again = compileKeyed(schema, how);
161
+ if (again.key !== undefined)
162
+ key = again.key;
163
+ return again.validate;
164
+ };
165
+ }
166
+ /** `compileTenantSchema`'s validator, and the key the cache keeps it under (none when it isn't JSON). */
167
+ function compileKeyed(schema, how) {
168
+ const bounds = tenantInputBounds();
169
+ const measured = measure(schema, bounds);
170
+ if (measured.kind === 'over')
171
+ throw new Error(measured.reason);
172
+ const text = jsonText(schema);
173
+ const bytes = Buffer.byteLength(text);
174
+ if (bytes > bounds.schemaMaxBytes) {
175
+ throw new Error(`The schema is ${bytes} bytes as JSON, over the limit of ${bounds.schemaMaxBytes}.`);
176
+ }
177
+ const compile = () => boundedAjv(how.dialect, how.options, { annotations: how.annotations ?? [] }).compile(schema);
178
+ const { cache, budget } = stateFor(bounds);
179
+ const scope = currentTenant();
180
+ if (!measured.json)
181
+ return { validate: budget.run(scope, undefined, compile) };
182
+ const key = createHash('sha256')
183
+ .update(JSON.stringify([how.dialect, how.options, how.annotations ?? []]))
184
+ .update(text)
185
+ .digest('base64');
186
+ const kept = cache.get(key);
187
+ if (kept !== undefined) {
188
+ if (kept.kind === 'refused')
189
+ throw kept.error;
190
+ return { validate: kept.validate, key };
191
+ }
192
+ let validate;
193
+ try {
194
+ validate = budget.run(scope, key, compile);
195
+ }
196
+ catch (error) {
197
+ // A schema that doesn't compile keeps its error, which is small.
198
+ if (!(error instanceof SchemaCompileBudgetError))
199
+ cache.set(key, { kind: 'refused', error }, 0);
200
+ throw error;
201
+ }
202
+ cache.set(key, { kind: 'compiled', validate }, bytes);
203
+ return { validate, key };
204
+ }
205
+ /** `JSON.stringify(schema)`, or '' when it can't write it (a `BigInt`). */
206
+ function jsonText(schema) {
207
+ try {
208
+ return JSON.stringify(schema) ?? '';
209
+ }
210
+ catch {
211
+ return '';
212
+ }
213
+ }
214
+ /**
215
+ * Whether `schema` nests deeper than the bounds allow, and whether it's
216
+ * JSON: only plain objects, arrays, strings, finite numbers, booleans and
217
+ * `null`. Walked without recursion, and stopped once it has seen more
218
+ * values than fit in `schemaMaxBytes` of JSON, so an object graph that
219
+ * shares its parts (or loops) can't make the walk long.
220
+ */
221
+ function measure(schema, bounds) {
222
+ const max = bounds.schemaMaxDepth;
223
+ const stack = [[schema, 0]];
224
+ let json = true;
225
+ let seen = 0;
226
+ while (stack.length > 0) {
227
+ const [value, depth] = stack.pop();
228
+ if (typeof value === 'number')
229
+ json &&= Number.isFinite(value);
230
+ else if (typeof value !== 'object')
231
+ json &&= typeof value === 'string' || typeof value === 'boolean';
232
+ if (typeof value !== 'object' || value === null)
233
+ continue;
234
+ if (depth === max) {
235
+ return {
236
+ kind: 'over',
237
+ reason: `The schema nests objects and arrays more than ${max} deep, the limit.`,
238
+ };
239
+ }
240
+ const proto = Object.getPrototypeOf(value);
241
+ json &&= proto === Object.prototype || proto === Array.prototype;
242
+ for (const inner of Object.values(value)) {
243
+ seen += 1;
244
+ // Each value takes at least two bytes of JSON, with its comma.
245
+ if (seen * 2 > bounds.schemaMaxBytes) {
246
+ return {
247
+ kind: 'over',
248
+ reason: `The schema is more than ${bounds.schemaMaxBytes} bytes as JSON, the limit.`,
249
+ };
250
+ }
251
+ stack.push([inner, depth + 1]);
252
+ }
253
+ }
254
+ return { kind: 'within', json };
255
+ }
256
+ /**
257
+ * A cache that drops the least recently used entries while it holds more
258
+ * than `maxEntries`, or more than `maxBytes` by the weights it was given.
259
+ */
260
+ class LeastRecentlyUsed {
261
+ maxEntries;
262
+ maxBytes;
263
+ entries = new Map();
264
+ bytes = 0;
265
+ constructor(maxEntries, maxBytes) {
266
+ this.maxEntries = maxEntries;
267
+ this.maxBytes = maxBytes;
268
+ }
269
+ get(key) {
270
+ const entry = this.entries.get(key);
271
+ if (entry === undefined)
272
+ return undefined;
273
+ this.entries.delete(key);
274
+ this.entries.set(key, entry);
275
+ return entry.value;
276
+ }
277
+ set(key, value, bytes) {
278
+ this.delete(key);
279
+ this.entries.set(key, { value, bytes });
280
+ this.bytes += bytes;
281
+ while (this.entries.size > this.maxEntries || this.bytes > this.maxBytes) {
282
+ const oldest = this.entries.keys().next();
283
+ if (oldest.done === true)
284
+ break;
285
+ this.delete(oldest.value);
286
+ }
287
+ }
288
+ delete(key) {
289
+ const entry = this.entries.get(key);
290
+ if (entry === undefined)
291
+ return;
292
+ this.entries.delete(key);
293
+ this.bytes -= entry.bytes;
294
+ }
295
+ }
296
+ let state;
297
+ /** The cache and the budget for `bounds`: new ones when the bounds change (a test's override). */
298
+ function stateFor(bounds) {
299
+ if (state?.bounds !== bounds) {
300
+ state = {
301
+ bounds,
302
+ cache: new LeastRecentlyUsed(bounds.schemaCacheEntries, bounds.schemaCacheMaxBytes),
303
+ budget: new CompileBudget(bounds.schemaCompileBudgetMs),
304
+ };
305
+ }
306
+ return state;
307
+ }
308
+ //# sourceMappingURL=bounded.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bounded.js","sourceRoot":"","sources":["../src/bounded.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The time each tenant's schemas spend compiling, in any 60 seconds,
3
+ * against the bounds' `schemaCompileBudgetMs`. Compiling a large schema
4
+ * holds the event loop (a 256 KiB one up to 0.75 s), so a tenant past its
5
+ * budget waits instead of the others.
6
+ *
7
+ * Which compiles count is the tenant's scope's (`runAsTenant`): every one
8
+ * in a request that publishes schemas (`all`), and otherwise only a
9
+ * schema compiled again (`recompiles`), which is what a tenant whose
10
+ * schemas don't fit in the cache makes each call do. A tenant's first
11
+ * compile of a schema in the process is free, up to `FIRST_COMPILES`
12
+ * schemas; past them, every compile counts. A tenant that compiled
13
+ * nothing for an hour is forgotten, as a restart would forget it: its next
14
+ * compiles are first ones again. The window and the refusal are the
15
+ * bounds' budget kept per tenant (`TenantTimeBudget`).
16
+ */
17
+ import { type BudgetClock, TenantBudgetError, type TenantScope } from '@kindgi/bounds';
18
+ /** The schemas a tenant may compile once for free, in a process. */
19
+ export declare const FIRST_COMPILES = 4096;
20
+ /** How long a tenant compiles nothing before it's forgotten. */
21
+ export declare const FORGET_AFTER_MS: number;
22
+ /** A tenant's compile refused: it spent `budgetMs` in the last 60 seconds. Retry after `retryAfterMs`. */
23
+ export declare class SchemaCompileBudgetError extends TenantBudgetError {
24
+ readonly code = "schema-compile-budget-exceeded";
25
+ constructor(budgetMs: number, retryAfterMs: number);
26
+ }
27
+ export declare class CompileBudget {
28
+ private readonly budgetMs;
29
+ private readonly clock;
30
+ private readonly time;
31
+ /** Each tenant's schemas compiled once, by key, up to `FIRST_COMPILES`, and when it last compiled. */
32
+ private readonly compiled;
33
+ private sweptAt;
34
+ constructor(budgetMs: number, clock?: BudgetClock);
35
+ /**
36
+ * Runs `compile`, the compile of the schema `key` (undefined: one that
37
+ * isn't JSON, which always counts), as `scope`'s work. When it counts,
38
+ * it's refused with `SchemaCompileBudgetError` once the tenant has spent
39
+ * its budget, and its time is charged.
40
+ */
41
+ run<T>(scope: TenantScope | undefined, key: string | undefined, compile: () => T): T;
42
+ /** The schemas `tenant` has compiled once, by key; it compiles `now`. */
43
+ private compiledBy;
44
+ /** Once a window: forgets the tenants idle past `FORGET_AFTER_MS`. */
45
+ private forget;
46
+ }
47
+ //# sourceMappingURL=compile-budget.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compile-budget.d.ts","sourceRoot":"","sources":["../src/compile-budget.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAEL,KAAK,WAAW,EAEhB,iBAAiB,EACjB,KAAK,WAAW,EAEjB,MAAM,gBAAgB,CAAC;AAExB,oEAAoE;AACpE,eAAO,MAAM,cAAc,OAAQ,CAAC;AAEpC,gEAAgE;AAChE,eAAO,MAAM,eAAe,QAAc,CAAC;AAE3C,0GAA0G;AAC1G,qBAAa,wBAAyB,SAAQ,iBAAiB;IAC7D,QAAQ,CAAC,IAAI,oCAAoC;gBAErC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM;CASnD;AAED,qBAAa,aAAa;IAOtB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,KAAK;IAPxB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAmB;IACxC,sGAAsG;IACtG,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiE;IAC1F,OAAO,CAAC,OAAO,CAAK;gBAGD,QAAQ,EAAE,MAAM,EAChB,KAAK,GAAE,WAA0B;IASpD;;;;;OAKG;IACH,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,WAAW,GAAG,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,CAAC,GAAG,CAAC;IAkBpF,yEAAyE;IACzE,OAAO,CAAC,UAAU;IAWlB,sEAAsE;IACtE,OAAO,CAAC,MAAM;CAOf"}
@@ -0,0 +1,91 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+ /**
4
+ * The time each tenant's schemas spend compiling, in any 60 seconds,
5
+ * against the bounds' `schemaCompileBudgetMs`. Compiling a large schema
6
+ * holds the event loop (a 256 KiB one up to 0.75 s), so a tenant past its
7
+ * budget waits instead of the others.
8
+ *
9
+ * Which compiles count is the tenant's scope's (`runAsTenant`): every one
10
+ * in a request that publishes schemas (`all`), and otherwise only a
11
+ * schema compiled again (`recompiles`), which is what a tenant whose
12
+ * schemas don't fit in the cache makes each call do. A tenant's first
13
+ * compile of a schema in the process is free, up to `FIRST_COMPILES`
14
+ * schemas; past them, every compile counts. A tenant that compiled
15
+ * nothing for an hour is forgotten, as a restart would forget it: its next
16
+ * compiles are first ones again. The window and the refusal are the
17
+ * bounds' budget kept per tenant (`TenantTimeBudget`).
18
+ */
19
+ import { BUDGET_WINDOW_MS, SYSTEM_CLOCK, TenantBudgetError, TenantTimeBudget, } from '@kindgi/bounds';
20
+ /** The schemas a tenant may compile once for free, in a process. */
21
+ export const FIRST_COMPILES = 4_096;
22
+ /** How long a tenant compiles nothing before it's forgotten. */
23
+ export const FORGET_AFTER_MS = 60 * 60_000;
24
+ /** A tenant's compile refused: it spent `budgetMs` in the last 60 seconds. Retry after `retryAfterMs`. */
25
+ export class SchemaCompileBudgetError extends TenantBudgetError {
26
+ code = 'schema-compile-budget-exceeded';
27
+ constructor(budgetMs, retryAfterMs) {
28
+ super(`Schemas compiled for this tenant took its budget of ${budgetMs} ms in the last 60 seconds: try again in ${Math.max(1, Math.ceil(retryAfterMs / 1000))} s.`, 'schema-compile-time', budgetMs, retryAfterMs);
29
+ this.name = 'SchemaCompileBudgetError';
30
+ }
31
+ }
32
+ export class CompileBudget {
33
+ budgetMs;
34
+ clock;
35
+ time;
36
+ /** Each tenant's schemas compiled once, by key, up to `FIRST_COMPILES`, and when it last compiled. */
37
+ compiled = new Map();
38
+ sweptAt = 0;
39
+ constructor(budgetMs, clock = SYSTEM_CLOCK) {
40
+ this.budgetMs = budgetMs;
41
+ this.clock = clock;
42
+ this.time = new TenantTimeBudget(budgetMs, (budget, retryAfterMs) => new SchemaCompileBudgetError(budget, retryAfterMs), clock);
43
+ }
44
+ /**
45
+ * Runs `compile`, the compile of the schema `key` (undefined: one that
46
+ * isn't JSON, which always counts), as `scope`'s work. When it counts,
47
+ * it's refused with `SchemaCompileBudgetError` once the tenant has spent
48
+ * its budget, and its time is charged.
49
+ */
50
+ run(scope, key, compile) {
51
+ if (scope === undefined || this.budgetMs === Number.POSITIVE_INFINITY)
52
+ return compile();
53
+ const now = this.clock.now();
54
+ this.forget(now);
55
+ const seen = this.compiledBy(scope.tenantId, now);
56
+ const first = key !== undefined && !seen.has(key) && seen.size < FIRST_COMPILES;
57
+ if (first && scope.compiles === 'recompiles') {
58
+ seen.add(key);
59
+ return compile();
60
+ }
61
+ // Noted once it runs: a refused compile leaves the schema a first one.
62
+ const noted = first ? key : undefined;
63
+ return this.time.charge(scope.tenantId, () => {
64
+ if (noted !== undefined)
65
+ seen.add(noted);
66
+ return compile();
67
+ });
68
+ }
69
+ /** The schemas `tenant` has compiled once, by key; it compiles `now`. */
70
+ compiledBy(tenant, now) {
71
+ const kept = this.compiled.get(tenant);
72
+ if (kept !== undefined) {
73
+ kept.at = now;
74
+ return kept.seen;
75
+ }
76
+ const seen = new Set();
77
+ this.compiled.set(tenant, { seen, at: now });
78
+ return seen;
79
+ }
80
+ /** Once a window: forgets the tenants idle past `FORGET_AFTER_MS`. */
81
+ forget(now) {
82
+ if (now - this.sweptAt <= BUDGET_WINDOW_MS)
83
+ return;
84
+ this.sweptAt = now;
85
+ for (const [tenant, kept] of [...this.compiled]) {
86
+ if (now - kept.at >= FORGET_AFTER_MS)
87
+ this.compiled.delete(tenant);
88
+ }
89
+ }
90
+ }
91
+ //# sourceMappingURL=compile-budget.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compile-budget.js","sourceRoot":"","sources":["../src/compile-budget.ts"],"names":[],"mappings":"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"}
package/dist/dialect.d.ts CHANGED
@@ -1,3 +1,19 @@
1
+ /**
2
+ * Compile a JSON Schema in the dialect it declares (`$schema`). Kindgi's
3
+ * own schemas are Draft 2020-12, the default when a schema declares none.
4
+ * A schema from elsewhere (an MCP server: the TypeScript MCP SDK emits
5
+ * draft-07) means its keywords as its dialect defines them: draft-07's
6
+ * array-form `items` is a tuple, 2020-12's is not. So each dialect gets
7
+ * Ajv's class for it, not 2020-12's with another meta-schema added.
8
+ *
9
+ * `strict` is Ajv's strict mode, an authoring lint (unknown keywords and
10
+ * formats, loose tuples): right for the schemas a pack's author writes,
11
+ * wrong for a schema someone else's server sends, which is valid JSON Schema
12
+ * without being written to that standard. A union of types
13
+ * (`type: ['string', 'number']`) is standard JSON Schema, and what Zod 4
14
+ * writes for a union of scalars, so it's allowed in either mode
15
+ * (`ALLOW_UNION_TYPES`).
16
+ */
1
17
  import type { ValidateFunction } from 'ajv';
2
18
  /**
3
19
  * Every Kindgi schema compiler takes a union of types (`type: ['string', 'number']`): standard
@@ -23,9 +39,18 @@ export interface CompileJsonSchemaOptions {
23
39
  readonly useDefaults?: boolean;
24
40
  }
25
41
  /**
26
- * A validator for `schema`, compiled in its declared dialect. Throws when
27
- * the dialect isn't supported (or allowed) or the schema doesn't compile,
28
- * as `ajv.compile` does.
42
+ * A validator for `schema`, compiled in its declared dialect under the
43
+ * process's bounds (`compileTenantSchema`). Throws when the dialect isn't
44
+ * supported (or allowed), the schema is over a bound, or it doesn't
45
+ * compile, as `ajv.compile` does.
29
46
  */
30
47
  export declare function compileJsonSchema(schema: Readonly<Record<string, unknown>>, options?: CompileJsonSchemaOptions): ValidateFunction;
48
+ /**
49
+ * `compileJsonSchema` for a caller that keeps the schema: compiled now,
50
+ * and what to call for its validator each time (`holdTenantSchema`). The
51
+ * cache stays all that holds a JSON schema's validator. A schema JSON
52
+ * can't carry (a key whose value is `undefined`, say) is never cached, so
53
+ * it's held as compiled rather than compiled on every call.
54
+ */
55
+ export declare function holdJsonSchema(schema: Readonly<Record<string, unknown>>, options?: CompileJsonSchemaOptions): () => ValidateFunction;
31
56
  //# sourceMappingURL=dialect.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dialect.d.ts","sourceRoot":"","sources":["../src/dialect.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,KAAK,CAAC;AAe5C;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,OAAO,CAAC;AAEtC,qDAAqD;AACrD,MAAM,MAAM,iBAAiB,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;AAWhF;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACzC,OAAO,GAAE,SAAS,iBAAiB,EAAiB,GACnD,iBAAiB,CAenB;AAOD,MAAM,WAAW,wBAAwB;IACvC,gFAAgF;IAChF,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;IACjD,0FAA0F;IAC1F,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,qFAAqF;IACrF,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;CAChC;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACzC,OAAO,GAAE,wBAA6B,GACrC,gBAAgB,CAmBlB"}
1
+ {"version":3,"file":"dialect.d.ts","sourceRoot":"","sources":["../src/dialect.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,KAAK,CAAC;AAI5C;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,OAAO,CAAC;AAEtC,qDAAqD;AACrD,MAAM,MAAM,iBAAiB,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;AAWhF;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACzC,OAAO,GAAE,SAAS,iBAAiB,EAAiB,GACnD,iBAAiB,CAenB;AAOD,MAAM,WAAW,wBAAwB;IACvC,gFAAgF;IAChF,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;IACjD,0FAA0F;IAC1F,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,qFAAqF;IACrF,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;CAChC;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACzC,OAAO,GAAE,wBAA6B,GACrC,gBAAgB,CAElB;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACzC,OAAO,GAAE,wBAA6B,GACrC,MAAM,gBAAgB,CAExB"}