@vertesia/common 1.6.0-dev.20261004.085740Z → 1.6.0-dev.20261008.064737Z

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 (74) hide show
  1. package/lib/api-contract/components.generated.json +566 -403
  2. package/lib/api-contract/engine.d.ts +65 -0
  3. package/lib/api-contract/engine.d.ts.map +1 -0
  4. package/lib/api-contract/engine.js +569 -0
  5. package/lib/api-contract/engine.js.map +1 -0
  6. package/lib/api-contract/index.d.ts +21 -182
  7. package/lib/api-contract/index.d.ts.map +1 -1
  8. package/lib/api-contract/index.js +13 -551
  9. package/lib/api-contract/index.js.map +1 -1
  10. package/lib/api-schemas/account.d.ts +0 -5
  11. package/lib/api-schemas/account.d.ts.map +1 -1
  12. package/lib/api-schemas/account.js +0 -7
  13. package/lib/api-schemas/account.js.map +1 -1
  14. package/lib/api-schemas/audit-trail.d.ts +255 -0
  15. package/lib/api-schemas/audit-trail.d.ts.map +1 -1
  16. package/lib/api-schemas/audit-trail.js +150 -1
  17. package/lib/api-schemas/audit-trail.js.map +1 -1
  18. package/lib/api-schemas/cost-analytics.d.ts +1 -0
  19. package/lib/api-schemas/cost-analytics.d.ts.map +1 -1
  20. package/lib/api-schemas/cost-analytics.js +7 -0
  21. package/lib/api-schemas/cost-analytics.js.map +1 -1
  22. package/lib/api-schemas/index.d.ts +1 -0
  23. package/lib/api-schemas/index.d.ts.map +1 -1
  24. package/lib/api-schemas/index.js +1 -0
  25. package/lib/api-schemas/index.js.map +1 -1
  26. package/lib/api-schemas/registry.d.ts +256 -109
  27. package/lib/api-schemas/registry.d.ts.map +1 -1
  28. package/lib/api-schemas/registry.js +26 -29
  29. package/lib/api-schemas/registry.js.map +1 -1
  30. package/lib/api-schemas/run-budget.d.ts +0 -36
  31. package/lib/api-schemas/run-budget.d.ts.map +1 -1
  32. package/lib/api-schemas/run-budget.js +0 -33
  33. package/lib/api-schemas/run-budget.js.map +1 -1
  34. package/lib/api-schemas/secrets.d.ts +0 -71
  35. package/lib/api-schemas/secrets.d.ts.map +1 -1
  36. package/lib/api-schemas/secrets.js +0 -73
  37. package/lib/api-schemas/secrets.js.map +1 -1
  38. package/lib/audit-trail.d.ts +14 -2
  39. package/lib/audit-trail.d.ts.map +1 -1
  40. package/lib/audit-trail.js +4 -0
  41. package/lib/audit-trail.js.map +1 -1
  42. package/lib/index.d.ts +1 -1
  43. package/lib/index.d.ts.map +1 -1
  44. package/lib/index.js.map +1 -1
  45. package/lib/platform-event.d.ts +0 -20
  46. package/lib/platform-event.d.ts.map +1 -1
  47. package/lib/run-budget.d.ts +0 -4
  48. package/lib/run-budget.d.ts.map +1 -1
  49. package/lib/secrets.d.ts +0 -10
  50. package/lib/secrets.d.ts.map +1 -1
  51. package/lib/user.d.ts +0 -1
  52. package/lib/user.d.ts.map +1 -1
  53. package/lib/vertesia-common.js +1 -1
  54. package/lib/vertesia-common.js.map +1 -1
  55. package/lib/wire-types.generated.d.ts +101 -105
  56. package/lib/wire-types.generated.d.ts.map +1 -1
  57. package/package.json +6 -6
  58. package/src/api-contract/components.generated.json +566 -403
  59. package/src/api-contract/engine.ts +641 -0
  60. package/src/api-contract/index.ts +30 -661
  61. package/src/api-schemas/account.ts +0 -8
  62. package/src/api-schemas/audit-trail.ts +169 -1
  63. package/src/api-schemas/cost-analytics.ts +8 -0
  64. package/src/api-schemas/index.ts +1 -0
  65. package/src/api-schemas/registry.ts +37 -33
  66. package/src/api-schemas/run-budget.ts +0 -37
  67. package/src/api-schemas/secrets.ts +0 -84
  68. package/src/audit-trail.ts +17 -0
  69. package/src/index.ts +0 -4
  70. package/src/platform-event.ts +0 -25
  71. package/src/run-budget.ts +0 -4
  72. package/src/secrets.ts +0 -11
  73. package/src/user.ts +0 -2
  74. package/src/wire-types.generated.ts +102 -103
@@ -0,0 +1,65 @@
1
+ import { type JsonObject } from '../api-schemas/adapter.js';
2
+ import { type ApiParameterLocation, type NormalizedApiParameters, type RawApiParameters } from '../api-schemas/parameters.js';
3
+ export interface ApiValidationIssue {
4
+ /**
5
+ * AJV's `instancePath` for the failing value, with `'/'` substituted for the root.
6
+ *
7
+ * Not quite a JSON Pointer: the pointer for a document root is the empty string, which reads as
8
+ * a missing path in a message. `'/'` is a display sentinel, so treat this as human-facing rather
9
+ * than as something to resolve against the payload.
10
+ */
11
+ path: string;
12
+ /** AJV's own message, e.g. `must NOT have additional properties`. */
13
+ message: string;
14
+ /** Undeclared property names at {@link path}, in the order AJV reported them. */
15
+ undeclared?: string[];
16
+ /**
17
+ * The union candidate this issue is a claim ABOUT, when it is not a fact about the value.
18
+ *
19
+ * Set on every issue produced by expanding a failed untagged `oneOf`/`anyOf` — see
20
+ * {@link collectIssues}. Under such a union each candidate rejects the value for its own
21
+ * reasons, most of which are irrelevant to the shape the value actually meant, so an issue
22
+ * carrying a component must be read as "as a `<component>`, this is wrong" and never as
23
+ * "this is wrong".
24
+ *
25
+ * Absent means the opposite and stronger thing: the issue holds against the component the
26
+ * payload is being validated as, whatever candidate it turns out to be.
27
+ */
28
+ component?: string;
29
+ }
30
+ export type ValidateApiPayloadResult<T> = {
31
+ valid: true;
32
+ data: T;
33
+ }
34
+ /**
35
+ * `errors` is the rendered form, complete and ready to log; `issues` is the same failures with
36
+ * the property names still separable. Shorten from `issues`, never from `errors`.
37
+ */
38
+ | {
39
+ valid: false;
40
+ errors: string[];
41
+ issues: ApiValidationIssue[];
42
+ };
43
+ export type PruneAndValidateResult<T> = {
44
+ valid: true;
45
+ data: T;
46
+ } | {
47
+ valid: false;
48
+ data: unknown;
49
+ errors: string[];
50
+ issues: ApiValidationIssue[];
51
+ };
52
+ /** Bind shared validation policies to an independently owned component registry. */
53
+ export declare function createApiContract<T extends object>(ApiSchemaComponents: Readonly<Record<string, JsonObject>>): {
54
+ bundleCanonicalComponent: (name: Extract<keyof T, string>, components?: Readonly<Record<string, JsonObject>>) => JsonObject;
55
+ apiComponentRef: (name: Extract<keyof T, string>) => string;
56
+ renderApiValidationIssue: (issue: ApiValidationIssue) => string;
57
+ renderApiValidationIssueHead: (issue: ApiValidationIssue) => string;
58
+ validateApiRequest: <N extends Extract<keyof T, string>>(name: N, value: unknown) => ValidateApiPayloadResult<T[N]>;
59
+ validateApiResponse: <N extends Extract<keyof T, string>>(name: N, value: unknown) => ValidateApiPayloadResult<T[N]>;
60
+ pruneApiResponse: <N extends Extract<keyof T, string>>(name: N, value: T[N]) => T[N];
61
+ pruneAndValidateApiResponse: <N extends Extract<keyof T, string>>(name: N, value: unknown) => PruneAndValidateResult<T[N]>;
62
+ findUnprunableApiPaths: (name: Extract<keyof T, string>) => string[];
63
+ normalizeApiParameters: (name: Extract<keyof T, string>, raw: RawApiParameters, location: ApiParameterLocation) => NormalizedApiParameters;
64
+ };
65
+ //# sourceMappingURL=engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/api-contract/engine.ts"],"names":[],"mappings":"AAGA,OAAO,EAAsC,KAAK,UAAU,EAAiB,MAAM,2BAA2B,CAAC;AAC/G,OAAO,EACH,KAAK,oBAAoB,EACzB,KAAK,uBAAuB,EAE5B,KAAK,gBAAgB,EACxB,MAAM,8BAA8B,CAAC;AAItC,MAAM,WAAW,kBAAkB;IAC/B;;;;;;OAMG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,qEAAqE;IACrE,OAAO,EAAE,MAAM,CAAC;IAChB,iFAAiF;IACjF,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,MAAM,wBAAwB,CAAC,CAAC,IAChC;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,CAAC,CAAA;CAAE;AAC1B;;;GAGG;GACD;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,EAAE,kBAAkB,EAAE,CAAA;CAAE,CAAC;AAEvE,MAAM,MAAM,sBAAsB,CAAC,CAAC,IAC9B;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,CAAC,CAAA;CAAE,GACxB;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,EAAE,kBAAkB,EAAE,CAAA;CAAE,CAAC;AAEtF,oFAAoF;AACpF,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,MAAM,EAAE,mBAAmB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;4EAqBzF,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,KACjD,UAAU;yDA+FqC,MAAM;sCAmTf,kBAAkB,KAAG,MAAM;0CAgBvB,kBAAkB,KAAG,MAAM;yBA4B5C,CAAC,yCACnB,CAAC,SACA,OAAO,KACf,wBAAwB,MAAqB;0BAenB,CAAC,yCACpB,CAAC,SACA,OAAO,KACf,wBAAwB,MAAqB;uBA6BtB,CAAC,yCAAiC,CAAC;kCAcxB,CAAC,yCAC5B,CAAC,SACA,OAAO,KACf,sBAAsB,MAAqB;gEAcW,MAAM,EAAE;kEAmBxD,gBAAgB,YACX,oBAAoB,KAC/B,uBAAuB;EAgB7B"}
@@ -0,0 +1,569 @@
1
+ import { Ajv2020 } from 'ajv/dist/2020.js';
2
+ import ajvFormats from 'ajv-formats';
3
+ import { findUnprunablePaths, isPlainObject, pruneToSchema } from '../api-schemas/adapter.js';
4
+ import { normalizeParameters, } from '../api-schemas/parameters.js';
5
+ const addFormats = ajvFormats.default;
6
+ /** Bind shared validation policies to an independently owned component registry. */
7
+ export function createApiContract(ApiSchemaComponents) {
8
+ /**
9
+ * A canonical component as a SELF-CONTAINED JSON Schema, for consumers that compile it directly.
10
+ *
11
+ * `components` defaults to the generated artifact, which is what a server wants. A GENERATOR must
12
+ * pass `buildApiSchemaComponents()` instead: the artifact on disk is the previous run's output, so
13
+ * bundling from it would emit an artifact derived from the last contract rather than the current Zod
14
+ * schemas — and, run in the wrong order, would leave the two generated files describing different
15
+ * shapes. Passing the fresh emission makes each generator independent of the others' ordering.
16
+ *
17
+ * The published component `$ref`s its neighbours through `#/components/schemas/...`, which resolves
18
+ * only inside the OpenAPI document. AJV and the Monaco editor need a document they can compile on
19
+ * its own, so the transitive closure is inlined under `$defs` and the pointers rewritten. Nothing
20
+ * about the shapes changes — this is a re-rooting of the same objects, plus the removal of the
21
+ * OpenAPI-only discriminator keywords described below, which is what lets a generated artifact be
22
+ * compared with the component it came from.
23
+ */
24
+ function bundleCanonicalComponent(name, components = ApiSchemaComponents) {
25
+ const seen = new Set();
26
+ const queue = [name];
27
+ while (queue.length > 0) {
28
+ const current = queue.shift();
29
+ if (seen.has(current))
30
+ continue;
31
+ seen.add(current);
32
+ for (const referenced of collectComponentRefs(components[current])) {
33
+ if (!seen.has(referenced))
34
+ queue.push(referenced);
35
+ }
36
+ }
37
+ seen.delete(name);
38
+ const defs = {};
39
+ for (const dependency of [...seen].sort()) {
40
+ defs[dependency] = toPlainJsonSchema(components[dependency]);
41
+ }
42
+ const root = toPlainJsonSchema(components[name]);
43
+ return seen.size > 0 ? { ...root, $defs: defs } : root;
44
+ }
45
+ const COMPONENT_REF_PREFIX = '#/components/schemas/';
46
+ /**
47
+ * Re-roots the component pointers AND drops the discriminator keywords, which are OpenAPI's, not
48
+ * JSON Schema's.
49
+ *
50
+ * `synthesizeDiscriminator` in the adapter gives a discriminated union a `discriminator` plus a
51
+ * restated `type: 'object'` and `required: [propertyName]` ON THE UNION NODE, so a generated
52
+ * Java/Go client can pick a subtype from the union rather than the branches. That restatement is a
53
+ * codegen hint: the node carries no `properties`, so it says "an object that must have
54
+ * `_option_id`" while declaring nothing that could be one.
55
+ *
56
+ * A JSON Schema validator does not mind — `required` needs no matching `properties`, and each
57
+ * branch requires the same key anyway, so removing all three is validation-neutral. Gemini's
58
+ * function-declaration validator does mind, and rejects the whole tool:
59
+ *
60
+ * schema at properties.intake.properties.extraction.properties.config.properties.model_options
61
+ * requires unspecified property '_option_id'
62
+ *
63
+ * That was a 400 on every agent turn offering `create_or_update_type`, because the intake-policy
64
+ * artifact reaches Vertex inside that tool's input schema. Stripping here rather than at the tool
65
+ * keeps the next component someone bundles from reintroducing it. The OpenAPI document is untouched
66
+ * — `ApiSchemaComponents` still carries the hint, and the generated clients still read it.
67
+ */
68
+ function toPlainJsonSchema(value) {
69
+ const rerooted = rerootComponentRefs(value);
70
+ return stripDiscriminators(rerooted);
71
+ }
72
+ function stripDiscriminators(value) {
73
+ if (Array.isArray(value))
74
+ return value.map(stripDiscriminators);
75
+ if (!value || typeof value !== 'object')
76
+ return value;
77
+ const node = value;
78
+ // `properties` present means the node describes an object in its own right, so its `type` and
79
+ // `required` are the schema's own rather than the restatement — only the OpenAPI keyword goes.
80
+ const restated = node.discriminator !== undefined && node.properties === undefined;
81
+ const out = {};
82
+ for (const [key, item] of Object.entries(node)) {
83
+ if (key === 'discriminator')
84
+ continue;
85
+ if (restated && (key === 'type' || key === 'required'))
86
+ continue;
87
+ out[key] = stripDiscriminators(item);
88
+ }
89
+ return out;
90
+ }
91
+ function collectComponentRefs(value, out = new Set()) {
92
+ if (Array.isArray(value)) {
93
+ for (const item of value)
94
+ collectComponentRefs(item, out);
95
+ return out;
96
+ }
97
+ if (!value || typeof value !== 'object')
98
+ return out;
99
+ for (const [key, item] of Object.entries(value)) {
100
+ if (key === '$ref' && typeof item === 'string' && item.startsWith(COMPONENT_REF_PREFIX)) {
101
+ out.add(item.slice(COMPONENT_REF_PREFIX.length));
102
+ }
103
+ else {
104
+ collectComponentRefs(item, out);
105
+ }
106
+ }
107
+ return out;
108
+ }
109
+ function rerootComponentRefs(value) {
110
+ if (Array.isArray(value))
111
+ return value.map(rerootComponentRefs);
112
+ if (!value || typeof value !== 'object')
113
+ return value;
114
+ const out = {};
115
+ for (const [key, item] of Object.entries(value)) {
116
+ out[key] =
117
+ key === '$ref' && typeof item === 'string' && item.startsWith(COMPONENT_REF_PREFIX)
118
+ ? `#/$defs/${item.slice(COMPONENT_REF_PREFIX.length)}`
119
+ : rerootComponentRefs(item);
120
+ }
121
+ return out;
122
+ }
123
+ /** The `$ref` pointer for a component, in the same spelling the spec publishes. */
124
+ function apiComponentRef(name) {
125
+ return `#/components/schemas/${name}`;
126
+ }
127
+ /**
128
+ * Validators compiled against the published components, through an envelope so the pointer AJV
129
+ * resolves is the exact pointer the spec publishes. `strictSchema: false` tolerates the
130
+ * `components` wrapper while leaving type and tuple checks active.
131
+ *
132
+ * Compiled lazily and cached: AJV compilation is the expensive step, and most components are
133
+ * never validated in a given process.
134
+ */
135
+ const validators = new Map();
136
+ /**
137
+ * One instance for the process, built on first validation.
138
+ *
139
+ * Per-component instances meant re-registering the whole thousand-component envelope for every
140
+ * component ever validated, and AJV walks a schema when it is added. Sharing one instance also lets
141
+ * it reuse the compiled form of a component that two others reference.
142
+ */
143
+ let ajvInstance;
144
+ /**
145
+ * The published components as AJV can consume them: the same schemas, with the two adjustments
146
+ * AJV's discriminator support requires and the OpenAPI document must not have.
147
+ *
148
+ * `discriminator: true` below is what makes AJV validate a discriminated union through the branch
149
+ * its tag names, instead of trying every branch and reporting all of their failures at once. The
150
+ * difference is not cosmetic — a legacy MCP tool collection missing `id` used to report the real
151
+ * error alongside two impossible ones from the branch it was never meant to match:
152
+ *
153
+ * / must have required property 'id' <- the real one
154
+ * / must NOT have additional properties: oauth_app <- vertesia_sdk branch
155
+ * /type must be equal to constant <- vertesia_sdk branch
156
+ * / must match exactly one schema in oneOf
157
+ *
158
+ * AJV rejects `mapping` outright ("discriminator: mapping is not supported"), because it derives
159
+ * the tag-to-branch map from each branch's own `const`/`enum` instead. The OpenAPI document still
160
+ * needs the mapping — a generated Java or Go client reads it to pick a concrete subtype — so both
161
+ * fixups here apply to AJV's copy only, and `ApiSchemaComponents` is published exactly as built.
162
+ *
163
+ * AJV's remaining requirements — the tag required in every branch, carrying `const` or a
164
+ * single-valued `enum`, with the union node typed as an object — are what `synthesizeDiscriminator`
165
+ * in the adapter already checks before it emits a discriminator at all, and what the hand-written
166
+ * `.meta({ discriminator })` declarations restate. `api-discriminators.test.ts` compiles every
167
+ * registered component so a union satisfying neither fails the build rather than the request.
168
+ */
169
+ function toAjvComponents() {
170
+ const schemas = structuredClone(ApiSchemaComponents);
171
+ const resolve = (value) => {
172
+ if (!isPlainObject(value))
173
+ return undefined;
174
+ const ref = value.$ref;
175
+ if (typeof ref !== 'string' || !ref.startsWith(COMPONENT_REF_PREFIX))
176
+ return value;
177
+ return schemas[ref.slice(COMPONENT_REF_PREFIX.length)];
178
+ };
179
+ visitSchemaNodes(schemas, (node) => {
180
+ const discriminator = node.discriminator;
181
+ if (!isPlainObject(discriminator) || !Array.isArray(node.oneOf))
182
+ return;
183
+ // AJV derives the tag-to-branch map from each branch's own literal, so `mapping` is both
184
+ // redundant and rejected.
185
+ delete discriminator.mapping;
186
+ const tag = discriminator.propertyName;
187
+ if (typeof tag !== 'string')
188
+ return;
189
+ for (const member of node.oneOf) {
190
+ const branch = resolve(member);
191
+ const properties = branch && isPlainObject(branch.properties) ? branch.properties : undefined;
192
+ const tagSchema = properties?.[tag];
193
+ if (!isPlainObject(tagSchema) || tagSchema.const !== undefined || tagSchema.enum !== undefined)
194
+ continue;
195
+ // The tag is a `$ref` to a named literal component — `TaskType_ACTIVITY`,
196
+ // `SupportedIntegrations_gladia` — because the literal carries its own `.meta({ id })`.
197
+ // AJV reads `properties/<tag>` looking for `const` or `enum` and does not follow a
198
+ // `$ref` to find one, so it refuses to compile the union at all. Restating the resolved
199
+ // literal ALONGSIDE the `$ref` is validation-neutral: `$ref` has no special precedence
200
+ // in 2020-12, both keywords apply, and they carry the same value by construction.
201
+ const target = resolve(tagSchema);
202
+ if (!target)
203
+ continue;
204
+ if (target.const !== undefined)
205
+ tagSchema.const = target.const;
206
+ else if (Array.isArray(target.enum))
207
+ tagSchema.enum = target.enum;
208
+ }
209
+ });
210
+ return schemas;
211
+ }
212
+ /** Every object node in the component graph, parents before children. */
213
+ function visitSchemaNodes(value, visit) {
214
+ if (Array.isArray(value)) {
215
+ for (const item of value)
216
+ visitSchemaNodes(item, visit);
217
+ return;
218
+ }
219
+ if (!isPlainObject(value))
220
+ return;
221
+ visit(value);
222
+ for (const item of Object.values(value))
223
+ visitSchemaNodes(item, visit);
224
+ }
225
+ /** The `$id` the whole component envelope is registered under, and the base of every `$ref` to it. */
226
+ const AJV_SCHEMA_ID = 'vertesia://openapi';
227
+ function getAjv() {
228
+ if (ajvInstance)
229
+ return ajvInstance;
230
+ // `verbose` carries two things {@link collectIssues} needs and nothing else supplies: the schema
231
+ // object that raised each error, which is the only unambiguous way to tell one union candidate
232
+ // from another, and the value under evaluation, which is what a failed union is re-checked
233
+ // against. Both are populated only when validation fails, and both are references rather than
234
+ // copies.
235
+ // `allowUnionTypes` because a few published components declare `type: [...]` on purpose:
236
+ // `DurationValue` is `['string', 'number']` specifically so the generated Java client gets one
237
+ // writable property instead of the unusable `AnyOfnumber` an `anyOf` produces. AJV's
238
+ // `strictTypes` default of "log" reported that deliberate choice to its logger on every startup.
239
+ // The default logger is `console`, so the notice left on stderr, where log collectors routinely
240
+ // classify it as an error — turning a healthy design decision into recurring server error noise.
241
+ const ajv = new Ajv2020({
242
+ strictSchema: false,
243
+ allErrors: true,
244
+ discriminator: true,
245
+ verbose: true,
246
+ allowUnionTypes: true,
247
+ });
248
+ // Without this, AJV treats `format` as an annotation and ignores it, so a `date-time` property
249
+ // would document a constraint nothing checks — the exact spec/enforcement gap this design is
250
+ // meant to close.
251
+ addFormats(ajv);
252
+ ajv.addSchema({ $id: AJV_SCHEMA_ID, components: { schemas: toAjvComponents() } });
253
+ ajvInstance = ajv;
254
+ return ajv;
255
+ }
256
+ function getValidator(name) {
257
+ const cached = validators.get(name);
258
+ if (cached)
259
+ return cached;
260
+ const validate = getAjv().compile({ $ref: `${AJV_SCHEMA_ID}${apiComponentRef(name)}` });
261
+ validators.set(name, validate);
262
+ return validate;
263
+ }
264
+ function additionalProperty(error) {
265
+ // AJV writes "must NOT have additional properties" and puts the offending key in `params`,
266
+ // so the message alone says a body is wrong without saying which property made it wrong.
267
+ // Every other keyword names its subject already — `required` quotes the missing property,
268
+ // `enum` and `type` describe the value in place — and this is the one a caller most often
269
+ // trips, so it is the one worth spelling out rather than reformatting all of them.
270
+ return error.params?.additionalProperty;
271
+ }
272
+ /**
273
+ * One validation failure, with the undeclared property names kept AS NAMES.
274
+ *
275
+ * The structure exists because the rendered line cannot be taken apart again. Property names are
276
+ * arbitrary JSON strings: `{"customer secret token": 1}` and `{"a, b": 1}` are both valid, so
277
+ * neither a space nor a comma is a reliable separator once the names have been joined. Any consumer
278
+ * that needs to shorten the list — the HTTP boundary does, the log does not — has to do it here,
279
+ * on the array, where a name can be dropped whole.
280
+ */
281
+ /**
282
+ * The failures, with the undeclared properties a SINGLE schema reported at a given path gathered
283
+ * into one issue, and each failed untagged union expanded per candidate.
284
+ *
285
+ * AJV reports `additionalProperties` per property, so a value carrying a whole foreign object — a
286
+ * Mongoose document that reached the response mapper unmapped, say — produces one error per own key
287
+ * and buries every other failure in the payload. Gathering is lossless: every name is kept, in the
288
+ * order AJV found it. It is keyed by the schema that raised the error as well as the path, so two
289
+ * candidates complaining at one path never merge into a single line. `schemaPath` will not do: a
290
+ * `$ref`'d candidate resets it to `#/additionalProperties`, identical for every candidate. The
291
+ * schema object itself is unambiguous, and `verbose: true` supplies it.
292
+ *
293
+ * `discriminator: true` narrows a TAGGED union to the one branch its tag names, so nothing below
294
+ * applies to those. What is left is the unions with no tag: AJV runs the value against every
295
+ * candidate and reports all of their failures, flat and unattributed. Read as facts they are
296
+ * nonsense — for a condition branch `{ to, when }` with a malformed `when`, raw AJV says
297
+ *
298
+ * /nodes/a/branches/0/when must be object <- BranchDefinition
299
+ * /nodes/a/branches/0 must have required property 'id' <- BranchNodeBranchDefinition
300
+ * /nodes/a/branches/0 must NOT have additional properties: to, when <- BranchNodeBranchDefinition
301
+ * /nodes/a/branches/0 must match exactly one schema in oneOf
302
+ *
303
+ * where `to` and `when` are declared on `BranchDefinition` and only wrong for a candidate the value
304
+ * never meant. Nothing in the error list says which claim belongs to which candidate, and the
305
+ * candidates fail at DIFFERENT paths, so no rule based on the path can recover it.
306
+ *
307
+ * So the union is re-validated candidate by candidate, and each candidate's own failures are
308
+ * attributed to it by name. That is exact rather than inferred, at the cost of one extra validation
309
+ * per candidate — paid only on a response that is already failing:
310
+ *
311
+ * /nodes/a/branches/0 must match exactly one schema in oneOf
312
+ * /nodes/a/branches/0/when as BranchDefinition: must be object
313
+ * /nodes/a/branches/0 as BranchNodeBranchDefinition: must have required property 'id'
314
+ * /nodes/a/branches/0 as BranchNodeBranchDefinition: must NOT have additional properties: to, when
315
+ *
316
+ * Expansion goes ONE level deep: a candidate's own errors are collected without expanding any union
317
+ * nested inside it, which bounds the work and keeps the output readable.
318
+ */
319
+ function collectIssues(validate) {
320
+ return toIssues(validate.errors ?? [], true);
321
+ }
322
+ function toIssues(errors, expandUnions) {
323
+ const expansions = new Map();
324
+ if (expandUnions) {
325
+ for (const error of errors) {
326
+ if (error.keyword !== 'oneOf' && error.keyword !== 'anyOf')
327
+ continue;
328
+ const expanded = expandUnion(error);
329
+ if (expanded)
330
+ expansions.set(error.instancePath, expanded);
331
+ }
332
+ }
333
+ /** Whether an expanded union already accounts for this error, making the raw form redundant. */
334
+ const superseded = (error) => {
335
+ for (const base of expansions.keys()) {
336
+ if (error.instancePath === base || error.instancePath.startsWith(`${base}/`))
337
+ return true;
338
+ }
339
+ return false;
340
+ };
341
+ const issues = [];
342
+ // Keyed by the raising schema, then by path: two candidates at one path stay separate, and one
343
+ // schema's repeats at one path merge.
344
+ const gathered = new Map();
345
+ for (const error of errors) {
346
+ const path = error.instancePath || '/';
347
+ const message = error.message ?? 'is invalid';
348
+ const expanded = expansions.get(error.instancePath);
349
+ if (expanded && (error.keyword === 'oneOf' || error.keyword === 'anyOf')) {
350
+ // The union's own line frames the candidates that follow it.
351
+ issues.push({ path, message });
352
+ issues.push(...expanded);
353
+ continue;
354
+ }
355
+ if (superseded(error))
356
+ continue;
357
+ const additional = additionalProperty(error);
358
+ if (additional === undefined) {
359
+ issues.push({ path, message });
360
+ continue;
361
+ }
362
+ let byPath = gathered.get(error.parentSchema);
363
+ if (!byPath) {
364
+ byPath = new Map();
365
+ gathered.set(error.parentSchema, byPath);
366
+ }
367
+ const existing = byPath.get(path);
368
+ if (existing) {
369
+ // One schema reporting the same name twice is not a second fact.
370
+ if (!existing.names.has(additional)) {
371
+ existing.names.add(additional);
372
+ existing.issue.undeclared?.push(additional);
373
+ }
374
+ continue;
375
+ }
376
+ const issue = { path, message, undeclared: [additional] };
377
+ byPath.set(path, { issue, names: new Set([additional]) });
378
+ issues.push(issue);
379
+ }
380
+ return issues;
381
+ }
382
+ /**
383
+ * One failed union, re-stated as what each candidate objected to.
384
+ *
385
+ * `undefined` when the union cannot be attributed exactly — a candidate that is not a `$ref` to a
386
+ * registered component has no name to report it under, and a partly-named expansion would read as
387
+ * though the unnamed candidate had no objection. The raw errors are kept in that case, which is what
388
+ * AJV would have produced anyway.
389
+ */
390
+ function expandUnion(error) {
391
+ const parent = error.parentSchema;
392
+ if (!isPlainObject(parent))
393
+ return undefined;
394
+ const members = Array.isArray(parent.oneOf)
395
+ ? parent.oneOf
396
+ : Array.isArray(parent.anyOf)
397
+ ? parent.anyOf
398
+ : undefined;
399
+ if (!members?.length)
400
+ return undefined;
401
+ const issues = [];
402
+ for (const member of members) {
403
+ const ref = isPlainObject(member) && typeof member.$ref === 'string' ? member.$ref : undefined;
404
+ if (!ref?.startsWith(COMPONENT_REF_PREFIX))
405
+ return undefined;
406
+ const validateMember = getAjv().getSchema(`${AJV_SCHEMA_ID}${ref}`);
407
+ if (!validateMember)
408
+ return undefined;
409
+ // `verbose: true` puts the value under evaluation on the error, so the candidates are run
410
+ // against exactly what the union saw.
411
+ if (validateMember(error.data))
412
+ continue;
413
+ const component = ref.slice(COMPONENT_REF_PREFIX.length);
414
+ for (const issue of toIssues(validateMember.errors ?? [], false)) {
415
+ issues.push({ ...issue, path: joinInstancePath(error.instancePath, issue.path), component });
416
+ }
417
+ }
418
+ return issues.length > 0 ? issues : undefined;
419
+ }
420
+ /** Re-roots a candidate-relative path under the union's own path. */
421
+ function joinInstancePath(base, relative) {
422
+ if (relative === '/')
423
+ return base || '/';
424
+ return `${base}${relative}`;
425
+ }
426
+ /**
427
+ * The complete rendering of one issue — every name, no budget.
428
+ *
429
+ * This is what gets logged. A caller-facing message renders from {@link ApiValidationIssue} itself
430
+ * with a length limit rather than shortening this string; see `reportableErrors` in the enforcer.
431
+ */
432
+ function renderApiValidationIssue(issue) {
433
+ const head = renderApiValidationIssueHead(issue);
434
+ return issue.undeclared?.length ? `${head}: ${issue.undeclared.join(', ')}` : head;
435
+ }
436
+ /**
437
+ * Everything in a rendered issue except the property names: `<path> [as <component>]: <message>`.
438
+ *
439
+ * Exported because it is not the only renderer. A caller that has to bound the length of a message
440
+ * cannot use {@link renderApiValidationIssue} — it emits every name — and has to rebuild the line
441
+ * from {@link ApiValidationIssue} with its own budget for the names. Rebuilding the HEAD too is what
442
+ * lets the two drift: when `component` was added, the bounded renderer at the HTTP boundary went on
443
+ * concatenating path and message and silently dropped it, so callers were told
444
+ * `must NOT have additional properties: to, when` with no sign it was one candidate's claim. There is
445
+ * one definition of the head so that cannot happen again.
446
+ */
447
+ function renderApiValidationIssueHead(issue) {
448
+ return issue.component
449
+ ? `${issue.path} as ${issue.component}: ${issue.message}`
450
+ : `${issue.path} ${issue.message}`;
451
+ }
452
+ /** The failed branch of every validator here, so the two views cannot be built inconsistently. */
453
+ function invalidResult(validate) {
454
+ const issues = collectIssues(validate);
455
+ return { valid: false, errors: issues.map(renderApiValidationIssue), issues };
456
+ }
457
+ /**
458
+ * Validates an untyped request body against the component the endpoint publishes.
459
+ *
460
+ * Non-mutating by design. AJV's removal modes are never used here: the server would then accept a
461
+ * body the published schema declares invalid, silently disagreeing with its own spec — and
462
+ * `removeAdditional: 'all'` additionally empties freeform maps. An undeclared property is reported,
463
+ * not deleted.
464
+ *
465
+ * The component type is handed back only on the `valid` branch, so a body missing required fields
466
+ * cannot be typed as complete. What the caller does with a failure — reject, or log and continue —
467
+ * is the endpoint's policy, not this function's.
468
+ */
469
+ function validateApiRequest(name, value) {
470
+ const validate = getValidator(name);
471
+ if (validate(value)) {
472
+ return { valid: true, data: value };
473
+ }
474
+ return invalidResult(validate);
475
+ }
476
+ /**
477
+ * Validates an already-mapped response against the component it is published as.
478
+ *
479
+ * Separate from {@link pruneAndValidateApiResponse} because a resource mapper has already produced
480
+ * the wire shape: there is nothing to prune, and pruning would only mask a mapper bug. Use this at
481
+ * the response boundary to detect drift between what the mapper builds and what the spec promises.
482
+ */
483
+ function validateApiResponse(name, value) {
484
+ const validate = getValidator(name);
485
+ if (validate(value)) {
486
+ return { valid: true, data: value };
487
+ }
488
+ return invalidResult(validate);
489
+ }
490
+ /**
491
+ * Best-effort normalization of a response payload towards the fields its published component
492
+ * declares. Use this on the response path INSTEAD of an AJV removal mode, which would empty
493
+ * freeform maps such as `Account.feature_flags`.
494
+ *
495
+ * NOT a secret-removal boundary. Ambiguous schema shapes pass values through untouched — see
496
+ * {@link pruneToSchema} and check a component with {@link findUnprunablePaths} before treating its
497
+ * output as closed. Fields that must never ship still need an explicit response mapper.
498
+ *
499
+ * Never throws and never rejects: an undocumented field is server-side drift, and failing the
500
+ * response would punish the caller for it.
501
+ *
502
+ * Takes a value the caller has already typed as the component. Pruning only REMOVES undeclared
503
+ * extras, so a conforming input yields a conforming output and the return type is honest. It does
504
+ * NOT validate, which is why it cannot accept `unknown` — narrowing a payload that is missing
505
+ * required fields would hand TypeScript a complete `Account` that does not exist at runtime. Use
506
+ * {@link pruneAndValidateApiResponse} when the payload's shape is not already established.
507
+ *
508
+ * Request validation stays separate and non-mutating — pruning a request would make the server
509
+ * silently disagree with the permissive schema it publishes.
510
+ */
511
+ function pruneApiResponse(name, value) {
512
+ return pruneToSchema(value, { $ref: apiComponentRef(name) }, ApiSchemaComponents);
513
+ }
514
+ /**
515
+ * Prunes an untyped payload and validates the result against the published component.
516
+ *
517
+ * The only sound entry point for a payload of unknown shape: the component type is handed back
518
+ * exclusively on the `valid` branch, so a document missing required fields cannot be typed as
519
+ * complete. On failure the pruned value is still returned — the caller decides whether to ship it
520
+ * and log, or fail — which keeps the "never turn drift into a 500" property a policy choice rather
521
+ * than something baked in here.
522
+ *
523
+ */
524
+ function pruneAndValidateApiResponse(name, value) {
525
+ const pruned = pruneToSchema(value, { $ref: apiComponentRef(name) }, ApiSchemaComponents);
526
+ const validate = getValidator(name);
527
+ if (validate(pruned)) {
528
+ return { valid: true, data: pruned };
529
+ }
530
+ return { ...invalidResult(validate), data: pruned };
531
+ }
532
+ /**
533
+ * Paths inside a component where pruning cannot narrow. Empty means no STRUCTURAL gaps for
534
+ * schema-conforming input — see {@link findUnprunablePaths} for why that is weaker than "nothing
535
+ * can escape". Anything listed needs an explicit response mapper.
536
+ */
537
+ function findUnprunableApiPaths(name) {
538
+ return findUnprunablePaths({ $ref: apiComponentRef(name) }, ApiSchemaComponents);
539
+ }
540
+ /**
541
+ * Normalizes raw query or header text into the value a component describes.
542
+ *
543
+ * Binds {@link normalizeParameters} to {@link ApiSchemaComponents} — the same objects the OpenAPI
544
+ * document publishes and {@link validateApiRequest} compiles. That is what keeps the published
545
+ * parameter and the enforced one the same declaration: the scanner expands this component's
546
+ * properties into `in: query` parameters, and the coercion below is decided by those same property
547
+ * schemas, so neither can be changed without changing the other.
548
+ *
549
+ * Validate the result with {@link validateApiRequest}, which is non-coercing: everything type-related
550
+ * has already happened here, so a value this could not coerce is reported against the published
551
+ * schema rather than by a second rule.
552
+ */
553
+ function normalizeApiParameters(name, raw, location) {
554
+ return normalizeParameters(name, raw, location, ApiSchemaComponents[name], ApiSchemaComponents);
555
+ }
556
+ return {
557
+ bundleCanonicalComponent,
558
+ apiComponentRef,
559
+ renderApiValidationIssue,
560
+ renderApiValidationIssueHead,
561
+ validateApiRequest,
562
+ validateApiResponse,
563
+ pruneApiResponse,
564
+ pruneAndValidateApiResponse,
565
+ findUnprunableApiPaths,
566
+ normalizeApiParameters,
567
+ };
568
+ }
569
+ //# sourceMappingURL=engine.js.map