youeduc-sdk-messaging 0.0.0 → 0.5.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 (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +218 -0
  3. package/dist/admin.d.ts +61 -0
  4. package/dist/admin.js +143 -0
  5. package/dist/admin.js.map +1 -0
  6. package/dist/config.d.ts +74 -0
  7. package/dist/config.js +128 -0
  8. package/dist/config.js.map +1 -0
  9. package/dist/consumer-client.d.ts +155 -0
  10. package/dist/consumer-client.js +299 -0
  11. package/dist/consumer-client.js.map +1 -0
  12. package/dist/consumer.d.ts +156 -0
  13. package/dist/consumer.js +1228 -0
  14. package/dist/consumer.js.map +1 -0
  15. package/dist/contracts/index.d.ts +16 -0
  16. package/dist/contracts/index.js +42 -0
  17. package/dist/contracts/index.js.map +1 -0
  18. package/dist/contracts/schemas.d.ts +570 -0
  19. package/dist/contracts/schemas.js +902 -0
  20. package/dist/contracts/schemas.js.map +1 -0
  21. package/dist/contracts/usuarios.d.ts +163 -0
  22. package/dist/contracts/usuarios.js +5 -0
  23. package/dist/contracts/usuarios.js.map +1 -0
  24. package/dist/dedup.d.ts +29 -0
  25. package/dist/dedup.js +65 -0
  26. package/dist/dedup.js.map +1 -0
  27. package/dist/dlq.d.ts +161 -0
  28. package/dist/dlq.js +341 -0
  29. package/dist/dlq.js.map +1 -0
  30. package/dist/errors.d.ts +72 -0
  31. package/dist/errors.js +95 -0
  32. package/dist/errors.js.map +1 -0
  33. package/dist/headers.d.ts +92 -0
  34. package/dist/headers.js +144 -0
  35. package/dist/headers.js.map +1 -0
  36. package/dist/index.d.ts +30 -0
  37. package/dist/index.js +32 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/integrations/redis.d.ts +118 -0
  40. package/dist/integrations/redis.js +272 -0
  41. package/dist/integrations/redis.js.map +1 -0
  42. package/dist/integrations/runtime.d.ts +232 -0
  43. package/dist/integrations/runtime.js +554 -0
  44. package/dist/integrations/runtime.js.map +1 -0
  45. package/dist/logger.d.ts +37 -0
  46. package/dist/logger.js +49 -0
  47. package/dist/logger.js.map +1 -0
  48. package/dist/messaging.d.ts +145 -0
  49. package/dist/messaging.js +164 -0
  50. package/dist/messaging.js.map +1 -0
  51. package/dist/naming.d.ts +64 -0
  52. package/dist/naming.js +167 -0
  53. package/dist/naming.js.map +1 -0
  54. package/dist/payload.d.ts +8 -0
  55. package/dist/payload.js +32 -0
  56. package/dist/payload.js.map +1 -0
  57. package/dist/publisher.d.ts +50 -0
  58. package/dist/publisher.js +199 -0
  59. package/dist/publisher.js.map +1 -0
  60. package/dist/retry.d.ts +24 -0
  61. package/dist/retry.js +42 -0
  62. package/dist/retry.js.map +1 -0
  63. package/dist/schema.d.ts +83 -0
  64. package/dist/schema.js +676 -0
  65. package/dist/schema.js.map +1 -0
  66. package/dist/telemetry.d.ts +52 -0
  67. package/dist/telemetry.js +221 -0
  68. package/dist/telemetry.js.map +1 -0
  69. package/dist/transport.d.ts +130 -0
  70. package/dist/transport.js +283 -0
  71. package/dist/transport.js.map +1 -0
  72. package/dist/types.d.ts +148 -0
  73. package/dist/types.js +35 -0
  74. package/dist/types.js.map +1 -0
  75. package/dist/util.d.ts +38 -0
  76. package/dist/util.js +112 -0
  77. package/dist/util.js.map +1 -0
  78. package/dist/w3c.d.ts +16 -0
  79. package/dist/w3c.js +119 -0
  80. package/dist/w3c.js.map +1 -0
  81. package/package.json +74 -1
package/dist/schema.js ADDED
@@ -0,0 +1,676 @@
1
+ /**
2
+ * Loading and validation of JSON Schemas (Draft 2020-12).
3
+ *
4
+ * No error message produced here contains payload *values* (PII): only the JSON pointer of the
5
+ * field, the name of the violated rule and, for `required` / `additionalProperties`, the names
6
+ * of the properties involved. Wording, the selected error and the checked formats follow the
7
+ * Python SDK (`jsonschema` + `best_match`).
8
+ */
9
+ import * as fs from 'node:fs';
10
+ import * as path from 'node:path';
11
+ import Ajv2020Module from 'ajv/dist/2020.js';
12
+ import addFormatsModule from 'ajv-formats';
13
+ import { SchemaNotFoundError, SchemaValidationError } from './errors.js';
14
+ import * as naming from './naming.js';
15
+ import { errorMessage, isPlainObject, pyRepr } from './util.js';
16
+ // ajv / ajv-formats are CommonJS modules exposing the implementation on `module.exports` and
17
+ // on `exports.default`; under NodeNext the typed entry point is `.default`.
18
+ const Ajv2020 = Ajv2020Module.default;
19
+ const addFormats = addFormatsModule.default;
20
+ // ------------------------------------------------------------------------------ loaders
21
+ function keyOf(domain, event, version) {
22
+ return JSON.stringify([domain, event, version]);
23
+ }
24
+ function cloneJson(value) {
25
+ return JSON.parse(JSON.stringify(value));
26
+ }
27
+ /**
28
+ * Schemas in memory, indexed by `"domain/eventSlug/version"` (spec §17). Stores and returns
29
+ * deep copies: caller mutations never leak.
30
+ */
31
+ export class InMemorySchemaLoader {
32
+ #schemas = new Map();
33
+ constructor(schemas) {
34
+ for (const [key, schema] of Object.entries(schemas)) {
35
+ const parts = key.split('/');
36
+ if (parts.length !== 3) {
37
+ throw new TypeError(`schema key must be 'domain/slug/version'; got ${pyRepr(key)}`);
38
+ }
39
+ const [domain, event, version] = parts;
40
+ this.#schemas.set(keyOf(domain, event, version), cloneJson(schema));
41
+ }
42
+ }
43
+ load(domain, event, version) {
44
+ const schema = this.#schemas.get(keyOf(domain, event, version));
45
+ if (schema === undefined)
46
+ throw new SchemaNotFoundError(domain, event, version);
47
+ return cloneJson(schema);
48
+ }
49
+ }
50
+ /**
51
+ * Asks `loaders` in order: the first one that does not throw `SchemaNotFoundError` wins.
52
+ *
53
+ * Typical use: the service's own schemas first, the SDK's bundled contracts after them:
54
+ * `new ChainSchemaLoader(new FileSystemSchemaLoader('schemas'), contracts.schemaLoader())`.
55
+ * Any other error of a loader (I/O, `SchemaValidationError` for a malformed file) propagates at
56
+ * once, without asking the next ones. None found → `SchemaNotFoundError`.
57
+ */
58
+ export class ChainSchemaLoader {
59
+ #loaders;
60
+ constructor(...loaders) {
61
+ if (loaders.length === 0)
62
+ throw new RangeError('ChainSchemaLoader requires at least one loader');
63
+ this.#loaders = Object.freeze([...loaders]);
64
+ }
65
+ get loaders() {
66
+ return this.#loaders;
67
+ }
68
+ load(domain, event, version) {
69
+ for (const loader of this.#loaders) {
70
+ try {
71
+ return loader.load(domain, event, version);
72
+ }
73
+ catch (error) {
74
+ if (!(error instanceof SchemaNotFoundError))
75
+ throw error;
76
+ }
77
+ }
78
+ throw new SchemaNotFoundError(domain, event, version);
79
+ }
80
+ }
81
+ // Errors meaning "the file does not exist". The others (EACCES, EMFILE, EIO...) are environment
82
+ // problems, possibly transient, and propagate unchanged.
83
+ const NOT_FOUND_CODES = new Set(['ENOENT', 'ENOTDIR', 'EISDIR']);
84
+ const FORBIDDEN_COMPONENTS = new Set(['', '.', '..']);
85
+ const nodeFileSystem = {
86
+ readFile: (filePath) => fs.readFileSync(filePath),
87
+ realpath: (filePath) => fs.realpathSync(filePath),
88
+ };
89
+ function isNotFound(error) {
90
+ const code = error?.code;
91
+ return typeof code === 'string' && NOT_FOUND_CODES.has(code);
92
+ }
93
+ /**
94
+ * Reads `root/{domain}/{event}/{version}.json`.
95
+ *
96
+ * Protected against path traversal: empty components, `.`/`..`, separators or NUL are refused,
97
+ * and the final path (after resolving symlinks) must stay inside `root`. Otherwise →
98
+ * `SchemaNotFoundError`. Only a missing file (`ENOENT`, `ENOTDIR`, `EISDIR`) becomes
99
+ * `SchemaNotFoundError`; other I/O errors propagate unchanged. Non-UTF-8, malformed JSON or a
100
+ * non-object → `SchemaValidationError`.
101
+ *
102
+ * Synchronous I/O: meant to be called once per schema (`SchemaRegistry` caches validators).
103
+ */
104
+ export class FileSystemSchemaLoader {
105
+ #root;
106
+ #fs;
107
+ constructor(root, options = {}) {
108
+ this.#fs = options.fileSystem ?? nodeFileSystem;
109
+ const resolved = path.resolve(root);
110
+ let real = resolved;
111
+ try {
112
+ real = this.#fs.realpath(resolved);
113
+ }
114
+ catch {
115
+ // Missing root: keep the lexical path (every load will be "not found").
116
+ }
117
+ this.#root = real;
118
+ }
119
+ get root() {
120
+ return this.#root;
121
+ }
122
+ load(domain, event, version) {
123
+ const filePath = this.#pathFor(domain, event, version);
124
+ const name = path.basename(filePath);
125
+ let raw;
126
+ try {
127
+ raw = this.#fs.readFile(filePath);
128
+ }
129
+ catch (error) {
130
+ if (isNotFound(error))
131
+ throw new SchemaNotFoundError(domain, event, version);
132
+ throw error;
133
+ }
134
+ let text;
135
+ try {
136
+ text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(raw);
137
+ }
138
+ catch {
139
+ throw new SchemaValidationError('', `schema file ${pyRepr(name)} is not valid UTF-8`);
140
+ }
141
+ let data;
142
+ try {
143
+ data = JSON.parse(text);
144
+ }
145
+ catch (error) {
146
+ throw new SchemaValidationError('', `malformed schema JSON in ${pyRepr(name)}: ${errorMessage(error)}`);
147
+ }
148
+ if (!isPlainObject(data)) {
149
+ throw new SchemaValidationError('', `schema in ${pyRepr(name)} must be a JSON object`);
150
+ }
151
+ return data;
152
+ }
153
+ #pathFor(domain, event, version) {
154
+ for (const part of [domain, event, version]) {
155
+ if (!isSafeComponent(part))
156
+ throw new SchemaNotFoundError(domain, event, version);
157
+ }
158
+ const lexical = path.join(this.#root, domain, event, `${version}.json`);
159
+ let candidate;
160
+ try {
161
+ candidate = this.#fs.realpath(lexical);
162
+ }
163
+ catch (error) {
164
+ if (isNotFound(error))
165
+ throw new SchemaNotFoundError(domain, event, version);
166
+ throw error;
167
+ }
168
+ if (candidate !== this.#root && !candidate.startsWith(this.#root + path.sep)) {
169
+ throw new SchemaNotFoundError(domain, event, version);
170
+ }
171
+ return candidate;
172
+ }
173
+ }
174
+ function isSafeComponent(part) {
175
+ return (typeof part === 'string' &&
176
+ !FORBIDDEN_COMPONENTS.has(part) &&
177
+ !part.includes('/') &&
178
+ !part.includes('\\') &&
179
+ !part.includes('\0'));
180
+ }
181
+ // ------------------------------------------------------------------------------ formats
182
+ //
183
+ // Spec §10: `format` is an assertion only for date, date-time, time, email, idn-email, uuid,
184
+ // ipv4 and ipv6 (strings only); every other format (unknown included) is an annotation and is
185
+ // never checked (Ajv with `strict: false` ignores formats it does not know).
186
+ const RFC3339_RE = /^(\d{4})-(0[1-9]|1[0-2])-(\d{2})T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/;
187
+ const DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
188
+ const UUID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
189
+ const DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
190
+ function isLeap(year) {
191
+ return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
192
+ }
193
+ function maxDay(year, month) {
194
+ if (month === 2 && isLeap(year))
195
+ return 29;
196
+ return DAYS_IN_MONTH[month - 1] ?? 0;
197
+ }
198
+ /** RFC 3339 `date-time`, exact match; `t`/`z` accepted in lowercase; no leap second. */
199
+ function isRfc3339DateTime(value) {
200
+ const match = RFC3339_RE.exec(value.replace(/[tz]/g, (c) => c.toUpperCase()));
201
+ if (match === null)
202
+ return false;
203
+ const year = Number(match[1]);
204
+ const month = Number(match[2]);
205
+ const day = Number(match[3]);
206
+ if (year === 0)
207
+ return false;
208
+ return day >= 1 && day <= maxDay(year, month);
209
+ }
210
+ function isTime(value) {
211
+ return isRfc3339DateTime(`1970-01-01T${value}`);
212
+ }
213
+ function isDate(value) {
214
+ const match = DATE_RE.exec(value);
215
+ if (match === null)
216
+ return false;
217
+ const year = Number(match[1]);
218
+ const month = Number(match[2]);
219
+ const day = Number(match[3]);
220
+ return year >= 1 && month >= 1 && month <= 12 && day >= 1 && day <= maxDay(year, month);
221
+ }
222
+ /** Strict `8-4-4-4-12` hexadecimal. */
223
+ function isUuid(value) {
224
+ return UUID_RE.test(value);
225
+ }
226
+ function isEmail(value) {
227
+ return value.includes('@');
228
+ }
229
+ function newAjv() {
230
+ const ajv = new Ajv2020({
231
+ allErrors: true,
232
+ verbose: true,
233
+ strict: false,
234
+ logger: false,
235
+ validateFormats: true,
236
+ unicodeRegExp: true, // ECMA-262 with `u`: `.` matches an astral code point as one
237
+ });
238
+ ajv.addFormat('date-time', { type: 'string', validate: isRfc3339DateTime });
239
+ ajv.addFormat('time', { type: 'string', validate: isTime });
240
+ ajv.addFormat('date', { type: 'string', validate: isDate });
241
+ ajv.addFormat('uuid', { type: 'string', validate: isUuid });
242
+ ajv.addFormat('email', { type: 'string', validate: isEmail });
243
+ ajv.addFormat('idn-email', { type: 'string', validate: isEmail });
244
+ addFormats(ajv, { formats: ['ipv4', 'ipv6'], mode: 'full', keywords: false });
245
+ return ajv;
246
+ }
247
+ // ------------------------------------------------------------------------------ validator
248
+ const ROOT_KEY = 'sdk-messaging-root';
249
+ // Keywords whose value is data (instance), not a subschema: a "$ref" there is literal.
250
+ const DATA_KEYWORDS = new Set(['const', 'enum', 'default', 'examples']);
251
+ const REF_KEYWORDS = ['$ref', '$dynamicRef'];
252
+ // Python's best_match: anyOf/oneOf are "weak" (superseded by same-level errors).
253
+ const WEAK_KEYWORDS = new Set(['anyOf', 'oneOf']);
254
+ // Ajv-only wrapper errors (jsonschema reports only the inner errors).
255
+ const WRAPPER_KEYWORDS = new Set(['if', 'propertyNames']);
256
+ const MISSING_REF_RE = /^can't resolve reference (.*) from id /s;
257
+ function escapePointerToken(token) {
258
+ return token.replaceAll('~', '~0').replaceAll('/', '~1');
259
+ }
260
+ function toPointer(segments) {
261
+ return segments.map((segment) => `/${escapePointerToken(String(segment))}`).join('');
262
+ }
263
+ /** Decodes an Ajv `instancePath`, typing array indexes as numbers (by walking the instance). */
264
+ function decodeInstancePath(instancePath, root) {
265
+ if (instancePath === '')
266
+ return [];
267
+ const segments = [];
268
+ let current = root;
269
+ for (const raw of instancePath.slice(1).split('/')) {
270
+ const token = raw.replaceAll('~1', '/').replaceAll('~0', '~');
271
+ if (Array.isArray(current)) {
272
+ const index = Number(token);
273
+ segments.push(index);
274
+ current = current[index];
275
+ }
276
+ else {
277
+ segments.push(token);
278
+ current =
279
+ isPlainObject(current) || (typeof current === 'object' && current !== null)
280
+ ? current[token]
281
+ : undefined;
282
+ }
283
+ }
284
+ return segments;
285
+ }
286
+ // Keywords whose descend adds an instance path segment: `{keyword}/{name}/false schema`.
287
+ const PATH_DESCEND_KEYWORDS = new Set(['properties', 'patternProperties', 'prefixItems']);
288
+ function reachedByPathDescend(schemaPath) {
289
+ const tokens = schemaPath.split('/');
290
+ return tokens.length >= 3 && PATH_DESCEND_KEYWORDS.has(tokens[tokens.length - 3] ?? '');
291
+ }
292
+ function isJsonType(value, type) {
293
+ switch (type) {
294
+ case 'null':
295
+ return value === null;
296
+ case 'boolean':
297
+ return typeof value === 'boolean';
298
+ case 'integer':
299
+ return typeof value === 'number' && Number.isInteger(value);
300
+ case 'number':
301
+ return typeof value === 'number';
302
+ case 'string':
303
+ return typeof value === 'string';
304
+ case 'array':
305
+ return Array.isArray(value);
306
+ case 'object':
307
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
308
+ default:
309
+ return false;
310
+ }
311
+ }
312
+ /** jsonschema `ValidationError._matches_type()`. */
313
+ function matchesType(candidate) {
314
+ const schema = candidate.parentSchema;
315
+ if (!isPlainObject(schema) || !('type' in schema))
316
+ return false;
317
+ const expected = schema.type;
318
+ if (typeof expected === 'string')
319
+ return isJsonType(candidate.data, expected);
320
+ if (Array.isArray(expected))
321
+ return expected.some((type) => isJsonType(candidate.data, type));
322
+ return false;
323
+ }
324
+ function comparePaths(a, b) {
325
+ const length = Math.min(a.length, b.length);
326
+ for (let i = 0; i < length; i += 1) {
327
+ const x = a[i];
328
+ const y = b[i];
329
+ if (x === y)
330
+ continue;
331
+ if (typeof x === 'number' && typeof y === 'number')
332
+ return x < y ? -1 : 1;
333
+ return String(x) < String(y) ? -1 : 1;
334
+ }
335
+ return a.length - b.length;
336
+ }
337
+ /** Compares jsonschema `relevance` keys: (-len(path), path, not weak, strong, not matches_type). */
338
+ function compareRelevance(a, b) {
339
+ if (a.path.length !== b.path.length)
340
+ return b.path.length - a.path.length;
341
+ const byPath = comparePaths(a.path, b.path);
342
+ if (byPath !== 0)
343
+ return byPath;
344
+ const weakA = WEAK_KEYWORDS.has(a.keyword) ? 0 : 1;
345
+ const weakB = WEAK_KEYWORDS.has(b.keyword) ? 0 : 1;
346
+ if (weakA !== weakB)
347
+ return weakA - weakB;
348
+ const typeA = matchesType(a) ? 0 : 1;
349
+ const typeB = matchesType(b) ? 0 : 1;
350
+ return typeA - typeB;
351
+ }
352
+ /** `max(errors, key=relevance)`; exact ties in the same schema object follow schema order. */
353
+ function pickMostRelevant(candidates) {
354
+ let best;
355
+ for (const candidate of candidates) {
356
+ if (best === undefined) {
357
+ best = candidate;
358
+ continue;
359
+ }
360
+ const order = compareRelevance(candidate, best);
361
+ if (order > 0 ||
362
+ (order === 0 &&
363
+ candidate.parentSchema === best.parentSchema &&
364
+ candidate.keywordIndex < best.keywordIndex)) {
365
+ best = candidate;
366
+ }
367
+ }
368
+ return best;
369
+ }
370
+ /** jsonschema `best_match`: most relevant error, descending into anyOf/oneOf context. */
371
+ function bestMatch(candidates) {
372
+ let best = pickMostRelevant(candidates);
373
+ while (best !== undefined && best.context.length > 0) {
374
+ const sorted = [...best.context].sort(compareRelevance);
375
+ const [first, second] = sorted;
376
+ if (first === undefined)
377
+ break;
378
+ if (second !== undefined && compareRelevance(first, second) === 0)
379
+ return best;
380
+ best = first;
381
+ }
382
+ return best;
383
+ }
384
+ function missingRequired(candidate) {
385
+ const schema = candidate.parentSchema;
386
+ const data = candidate.data;
387
+ if (!isPlainObject(schema) || !Array.isArray(schema.required) || !isPlainObject(data))
388
+ return [];
389
+ return schema.required.filter((name) => typeof name === 'string' && (!Object.hasOwn(data, name) || data[name] === undefined));
390
+ }
391
+ function additionalProperties(candidate) {
392
+ const schema = candidate.parentSchema;
393
+ const data = candidate.data;
394
+ if (!isPlainObject(schema) || !isPlainObject(data))
395
+ return [];
396
+ const properties = isPlainObject(schema.properties) ? schema.properties : {};
397
+ const patterns = isPlainObject(schema.patternProperties) ? Object.keys(schema.patternProperties) : [];
398
+ const regexes = patterns.flatMap((pattern) => {
399
+ try {
400
+ return [new RegExp(pattern, 'u')];
401
+ }
402
+ catch {
403
+ return [];
404
+ }
405
+ });
406
+ return Object.keys(data)
407
+ .filter((name) => !Object.hasOwn(properties, name) && !regexes.some((regex) => regex.test(name)))
408
+ .sort();
409
+ }
410
+ /** Description of the error without payload values (Python `_safe_reason`). */
411
+ function safeReason(candidate) {
412
+ if (candidate.keyword === 'false schema')
413
+ return 'failed schema validation';
414
+ let reason = `failed ${pyRepr(candidate.keyword)} constraint`;
415
+ if (candidate.keyword === 'required') {
416
+ const names = missingRequired(candidate);
417
+ if (names.length > 0)
418
+ reason += `: missing ${names.map(pyRepr).join(', ')}`;
419
+ }
420
+ else if (candidate.keyword === 'additionalProperties') {
421
+ const names = additionalProperties(candidate);
422
+ if (names.length > 0)
423
+ reason += `: unexpected ${names.map(pyRepr).join(', ')}`;
424
+ }
425
+ return reason;
426
+ }
427
+ function iterRefHolders(node, pointer, visit) {
428
+ if (Array.isArray(node)) {
429
+ node.forEach((item, index) => {
430
+ iterRefHolders(item, `${pointer}/${index}`, visit);
431
+ });
432
+ return;
433
+ }
434
+ if (!isPlainObject(node))
435
+ return;
436
+ if (REF_KEYWORDS.some((keyword) => typeof node[keyword] === 'string'))
437
+ visit(pointer, node);
438
+ for (const [keyword, value] of Object.entries(node)) {
439
+ if (!DATA_KEYWORDS.has(keyword))
440
+ iterRefHolders(value, `${pointer}/${uriPointerToken(keyword)}`, visit);
441
+ }
442
+ }
443
+ function uriPointerToken(token) {
444
+ return encodeURIComponent(escapePointerToken(token));
445
+ }
446
+ function indexPointers(node, pointer, index) {
447
+ if (typeof node !== 'object' || node === null)
448
+ return;
449
+ if (!index.has(node))
450
+ index.set(node, pointer);
451
+ if (Array.isArray(node)) {
452
+ node.forEach((item, i) => {
453
+ indexPointers(item, `${pointer}/${i}`, index);
454
+ });
455
+ }
456
+ else {
457
+ for (const [keyword, value] of Object.entries(node)) {
458
+ indexPointers(value, `${pointer}/${uriPointerToken(keyword)}`, index);
459
+ }
460
+ }
461
+ }
462
+ function invalidSchema(reason) {
463
+ return new SchemaValidationError('', `invalid schema${reason}`);
464
+ }
465
+ function unresolvable(error) {
466
+ if (!(error instanceof Error) || error.constructor.name !== 'MissingRefError')
467
+ return null;
468
+ const match = MISSING_REF_RE.exec(error.message);
469
+ const ref = match?.[1] ?? String(error.missingRef ?? '');
470
+ return invalidSchema(`: unresolvable reference ${pyRepr(ref)}`);
471
+ }
472
+ /**
473
+ * Validates payloads with Ajv (Draft 2020-12), including `format`.
474
+ *
475
+ * The schema is checked in the constructor (an invalid schema fails early). `$ref` is never
476
+ * fetched over the network: only local references (`#/$defs/...`, anchors, embedded subschemas
477
+ * with `$id`) resolve, and every `$ref`/`$dynamicRef` is resolved statically at construction;
478
+ * one that does not resolve (remote included) becomes `SchemaValidationError`.
479
+ *
480
+ * The reported error is the one Python's `jsonschema.best_match` would pick (shallowest
481
+ * instance path, lexicographically last sibling, anyOf/oneOf descended into their context).
482
+ */
483
+ export class JsonSchemaValidator {
484
+ #ajv;
485
+ #validate;
486
+ #pointers = new Map();
487
+ #subValidators = new Map();
488
+ constructor(schema) {
489
+ const input = schema;
490
+ if (!isPlainObject(input) && typeof input !== 'boolean') {
491
+ throw invalidSchema(': schema must be a JSON object or boolean');
492
+ }
493
+ // Private tree copy: Ajv keeps references, and error objects are matched by identity.
494
+ const copy = cloneJson(input);
495
+ // Python always validates as Draft 2020-12, whatever "$schema" says.
496
+ if (isPlainObject(copy))
497
+ delete copy.$schema;
498
+ this.#ajv = newAjv();
499
+ if (!this.#ajv.validateSchema(copy)) {
500
+ const first = this.#ajv.errors?.[0];
501
+ const where = first?.instancePath ? ` at ${pyRepr(first.instancePath)}` : '';
502
+ throw invalidSchema(`${where}: ${first?.message ?? 'does not match the 2020-12 meta-schema'}`);
503
+ }
504
+ try {
505
+ this.#ajv.addSchema(copy, ROOT_KEY);
506
+ const validate = this.#ajv.getSchema(ROOT_KEY);
507
+ if (validate === undefined)
508
+ throw invalidSchema(': schema could not be compiled');
509
+ this.#validate = validate;
510
+ // Like Python, every reference must resolve, even in subschemas never reached.
511
+ iterRefHolders(copy, '', (pointer) => {
512
+ this.#ajv.getSchema(`${ROOT_KEY}#${pointer}`);
513
+ });
514
+ }
515
+ catch (error) {
516
+ if (error instanceof SchemaValidationError)
517
+ throw error;
518
+ throw unresolvable(error) ?? invalidSchema(`: ${errorMessage(error)}`);
519
+ }
520
+ indexPointers(copy, '', this.#pointers);
521
+ }
522
+ validate(payload) {
523
+ if (this.#validate(payload))
524
+ return;
525
+ const candidates = this.#structure(this.#validate.errors ?? [], payload, []);
526
+ const best = bestMatch(candidates);
527
+ if (best === undefined)
528
+ throw new SchemaValidationError('', 'failed schema validation');
529
+ // No `cause`: Ajv errors carry the instance (payload values).
530
+ throw new SchemaValidationError(toPointer([...best.base, ...best.path]), safeReason(best));
531
+ }
532
+ /** Turns Ajv's flat error list into jsonschema-like top-level errors (with context). */
533
+ #structure(errors, root, base) {
534
+ const result = [];
535
+ let i = errors.length - 1;
536
+ while (i >= 0) {
537
+ const error = errors[i];
538
+ if (WRAPPER_KEYWORDS.has(error.keyword)) {
539
+ i -= 1;
540
+ continue;
541
+ }
542
+ let context = [];
543
+ let consumed = 0;
544
+ if (error.keyword === 'anyOf' || error.keyword === 'oneOf' || error.keyword === 'contains') {
545
+ const sub = this.#subErrors(error, root, base);
546
+ if (sub !== null && sub.count <= i) {
547
+ consumed = sub.count;
548
+ context = sub.context;
549
+ }
550
+ else {
551
+ // Fallback: drop the preceding errors located under this keyword.
552
+ const prefix = `${error.schemaPath}/`;
553
+ while (consumed < i && errors[i - consumed - 1].schemaPath.startsWith(prefix)) {
554
+ consumed += 1;
555
+ }
556
+ }
557
+ }
558
+ result.push(this.#candidate(error, root, base, context));
559
+ i -= 1 + consumed;
560
+ }
561
+ return result.reverse();
562
+ }
563
+ #candidate(error, root, base, context) {
564
+ const parentSchema = error.parentSchema;
565
+ let errorPath = decodeInstancePath(error.instancePath, root);
566
+ if (error.keyword === 'false schema' && reachedByPathDescend(error.schemaPath)) {
567
+ // jsonschema quirk: the error of a `false` subschema does not get the path segment of
568
+ // the descend that reached it ({"properties": {"x": false}} reports at the parent).
569
+ errorPath = errorPath.slice(0, -1);
570
+ }
571
+ return {
572
+ keyword: error.keyword,
573
+ path: errorPath,
574
+ base,
575
+ parentSchema,
576
+ data: error.data,
577
+ keywordIndex: isPlainObject(parentSchema) ? Object.keys(parentSchema).indexOf(error.keyword) : -1,
578
+ context,
579
+ };
580
+ }
581
+ /**
582
+ * Re-validates the instance of an anyOf/oneOf/contains error against each branch (compiled in
583
+ * the root document, so local refs resolve) to rebuild its context and to know how many of
584
+ * the preceding flat errors belong to it. `null` when the branches cannot be located.
585
+ */
586
+ #subErrors(error, root, base) {
587
+ const keywordValue = error.schema;
588
+ if (typeof keywordValue !== 'object' || keywordValue === null)
589
+ return null;
590
+ const pointer = this.#pointers.get(keywordValue);
591
+ if (pointer === undefined)
592
+ return null;
593
+ const instance = error.data;
594
+ const instanceBase = [...base, ...decodeInstancePath(error.instancePath, root)];
595
+ if (error.keyword === 'contains') {
596
+ const validate = this.#subValidator(pointer);
597
+ if (validate === null || !Array.isArray(instance))
598
+ return null;
599
+ let count = 0;
600
+ for (const item of instance) {
601
+ if (!validate(item))
602
+ count += validate.errors?.length ?? 0;
603
+ }
604
+ return { count, context: [] };
605
+ }
606
+ if (!Array.isArray(keywordValue))
607
+ return null;
608
+ let count = 0;
609
+ const context = [];
610
+ for (let index = 0; index < keywordValue.length; index += 1) {
611
+ const validate = this.#subValidator(`${pointer}/${index}`);
612
+ if (validate === null)
613
+ return null;
614
+ if (validate(instance))
615
+ continue;
616
+ const branchErrors = [...(validate.errors ?? [])];
617
+ count += branchErrors.length;
618
+ context.push(...this.#structure(branchErrors, instance, instanceBase));
619
+ }
620
+ const passing = error.params.passingSchemas;
621
+ // oneOf failing because several branches match: jsonschema reports it without context.
622
+ if (error.keyword === 'oneOf' && Array.isArray(passing))
623
+ return { count, context: [] };
624
+ return { count, context };
625
+ }
626
+ #subValidator(pointer) {
627
+ if (!this.#subValidators.has(pointer)) {
628
+ let validate = null;
629
+ try {
630
+ validate = this.#ajv.getSchema(`${ROOT_KEY}#${pointer}`) ?? null;
631
+ }
632
+ catch {
633
+ validate = null;
634
+ }
635
+ this.#subValidators.set(pointer, validate);
636
+ }
637
+ return this.#subValidators.get(pointer) ?? null;
638
+ }
639
+ }
640
+ // ------------------------------------------------------------------------------ registry
641
+ /**
642
+ * Implements {@link PayloadValidator}: resolves the schema of an event and validates.
643
+ *
644
+ * Validators are cached per `[domain, slug, version]`. Load failures (`SchemaNotFoundError`)
645
+ * and invalid schemas are not cached, so a schema published later is found without restarting.
646
+ */
647
+ export class SchemaRegistry {
648
+ #loader;
649
+ #factory;
650
+ #cache = new Map();
651
+ constructor(loader, validatorFactory = (schema) => new JsonSchemaValidator(schema)) {
652
+ this.#loader = loader;
653
+ this.#factory = validatorFactory;
654
+ }
655
+ validate(request) {
656
+ let slug;
657
+ try {
658
+ slug = naming.schemaSlug(request.eventType);
659
+ }
660
+ catch (error) {
661
+ throw new SchemaValidationError('', errorMessage(error));
662
+ }
663
+ this.#validatorFor(request.domain, slug, request.version).validate(request.payload);
664
+ }
665
+ #validatorFor(domain, slug, version) {
666
+ const key = keyOf(domain, slug, version);
667
+ let validator = this.#cache.get(key);
668
+ if (validator === undefined) {
669
+ const schema = this.#loader.load(domain, slug, version);
670
+ validator = this.#factory(schema);
671
+ this.#cache.set(key, validator);
672
+ }
673
+ return validator;
674
+ }
675
+ }
676
+ //# sourceMappingURL=schema.js.map