@ultimat3/action 29.4.0 → 30.1.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.
@@ -1,226 +0,0 @@
1
- /**
2
- * A value an input schema accepts, derived from that schema's own IR. The policy contract
3
- * test needs its invocation to REACH the policy, and `{}` fails `input:` first for every
4
- * action with a required field — which is how "policy denies an anonymous actor" passed on
5
- * `X_INPUT_INVALID` and proved nothing about authz.
6
- */
7
-
8
- import type { SchemaFormat, SchemaNode, StandardSchemaV1 } from '@ultimat3/schema';
9
- import { requiredKeys, tryIntrospect } from '@ultimat3/schema';
10
-
11
- /** Version 4, variant 1 — the shape `t.uuid` insists on, and no byte of it means anything. */
12
- const SAMPLE_UUID = '00000000-0000-4000-8000-000000000000';
13
-
14
- /** Short, and valid under `t.slug`'s and `t.cursor`'s patterns as well as bare `t.string`. */
15
- const SAMPLE_STRING = 'sample';
16
-
17
- /**
18
- * One entry per `SchemaFormat`, exhaustively — a new format is a type error here rather than a
19
- * silent `'sample'` that fails its own validator and reads as an authz test that cannot pass.
20
- */
21
- const BY_FORMAT: Readonly<Record<SchemaFormat, string>> = {
22
- uuid: SAMPLE_UUID,
23
- email: 'sample@example.test',
24
- uri: 'https://example.test/sample',
25
- 'date-time': '2020-01-01T00:00:00.000Z',
26
- slug: SAMPLE_STRING,
27
- timezone: 'UTC',
28
- locale: 'en',
29
- cursor: SAMPLE_STRING,
30
- };
31
-
32
- function sampleString(node: SchemaNode): string {
33
- if (node.format !== undefined) {
34
- // A format value is already the exact shape its validator wants; padding or truncating it
35
- // to a length bound would break the only thing that makes it valid.
36
- // `Object.hasOwn`, never the read alone: `node.format` comes from a provider's IR, and
37
- // `BY_FORMAT['constructor']` is the `Object` function off the prototype chain rather than
38
- // `undefined` — a function in the payload the policy assertion invokes with, where the type
39
- // says `string`. Same discriminator `naming.ts` uses on its irregular-plural table.
40
- const known = Object.hasOwn(BY_FORMAT, node.format) ? BY_FORMAT[node.format] : undefined;
41
- return known ?? SAMPLE_STRING;
42
- }
43
- const min = node.minLength ?? 0;
44
- const padded = SAMPLE_STRING.length >= min ? SAMPLE_STRING : SAMPLE_STRING.padEnd(min, 'x');
45
- return node.maxLength === undefined ? padded : padded.slice(0, node.maxLength);
46
- }
47
-
48
- /**
49
- * Whether the value this module built for `node` satisfies the node's own `pattern`.
50
- *
51
- * A regex cannot be inverted, so `sampleString` cannot construct a value for an arbitrary one —
52
- * but `pattern` IS in the IR, so whether the value it DID construct is acceptable is knowable
53
- * here, before the sample is ever handed to `invoke`. That is the whole difference between
54
- * "the framework could not build your payload, pass one" and an `X_INPUT_INVALID` surfacing out
55
- * of the action's own parse, which reads as the action being wrong when the action is fine.
56
- *
57
- * An uncompilable pattern is a gap, not a throw: only a foreign provider's IR can produce one
58
- * (`t.string.pattern()` takes a `RegExp`, whose `.source` always recompiles), and a generated
59
- * contract test must not die on the way to reporting what it needs.
60
- */
61
- function satisfiesPattern(node: SchemaNode, value: unknown): boolean {
62
- if (node.pattern === undefined) return true;
63
- if (typeof value !== 'string') return false;
64
- try {
65
- return new RegExp(node.pattern, node.patternFlags ?? '').test(value);
66
- } catch {
67
- return false;
68
- }
69
- }
70
-
71
- function sampleNumber(node: SchemaNode): number {
72
- const low = node.minimum ?? 0;
73
- const value = node.integer === true ? Math.ceil(low) : low;
74
- if (node.maximum === undefined || value <= node.maximum) return value;
75
- return node.integer === true ? Math.floor(node.maximum) : node.maximum;
76
- }
77
-
78
- /**
79
- * `Object.create(null)`, the shape `@ultimat3/schema`'s own object check already builds: on a `{}`
80
- * literal `sample['__proto__'] = value` reaches `Object.prototype`'s SETTER, so a required field
81
- * named `__proto__` never became an own key and REPLACED the sample's prototype instead. The
82
- * payload then failed the very schema it was derived from, and the contract test reported the
83
- * action as drifted. A field can carry that name through any computed key
84
- * (`t.object({ [name]: t.string })`) or a provider whose IR was parsed from JSON.
85
- *
86
- * The read is guarded for the same reason `patternAt` guards its own: `requiredKeys` happens to
87
- * answer own keys only (`Object.entries`), so this is the invariant stated locally rather than
88
- * borrowed from a function two packages away.
89
- */
90
- function sampleObject(node: SchemaNode): Record<string, unknown> {
91
- const properties = node.properties ?? {};
92
- const sample: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
93
- // Required-only is what "minimal" means: an optional key and a defaulted one are both
94
- // absences the schema already accepts, so adding them would only widen what can go wrong.
95
- for (const key of requiredKeys(node)) {
96
- const child = Object.hasOwn(properties, key) ? properties[key] : undefined;
97
- if (child !== undefined) sample[key] = sampleFor(child);
98
- }
99
- return sample;
100
- }
101
-
102
- /**
103
- * The most items a sample will hold. A contract sample is a handful of values; `minItems` past
104
- * this is an input the framework does not invent — `2 ** 32` made `Array.from` throw, and anything
105
- * below it allocated that many samples first. Such an array is a GAP (`sampleGaps`), reported as
106
- * `X_CONTRACT_DRIFT` with the instruction to pass the input, never silently under-sampled.
107
- */
108
- export const SAMPLE_ITEMS_MAX = 1000;
109
-
110
- /**
111
- * The `minItems` an array sample owes, or `undefined` when none can be built: past the ceiling,
112
- * or not a count at all. The builtin `t.array` refuses a bad bound at declaration
113
- * (`X_SCHEMA_BOUNDS_INVALID`), but this IR may come from a swapped provider.
114
- */
115
- function owedItems(node: SchemaNode): number | undefined {
116
- const min = node.minItems;
117
- if (min === undefined) return 0;
118
- return Number.isSafeInteger(min) && min >= 0 && min <= SAMPLE_ITEMS_MAX ? min : undefined;
119
- }
120
-
121
- function sampleFor(node: SchemaNode): unknown {
122
- // A nullable field accepts `null`, and `null` is the smallest thing it accepts.
123
- if (node.nullable === true) return null;
124
- switch (node.kind) {
125
- case 'string':
126
- return sampleString(node);
127
- case 'number':
128
- return sampleNumber(node);
129
- case 'boolean':
130
- return false;
131
- case 'date':
132
- return BY_FORMAT['date-time'];
133
- case 'enum':
134
- return node.values?.[0] ?? SAMPLE_STRING;
135
- case 'literal':
136
- return node.literal ?? null;
137
- // The fewest items the array accepts: `[]` under a `minItems` is a sample its own schema
138
- // refuses. Items repeat the one item sample; a uniqueness rule would be a refinement.
139
- case 'array':
140
- // An unbuildable count samples `[]`, which the schema refuses — `sampleGaps` names why.
141
- return Array.from({ length: owedItems(node) ?? 0 }, () =>
142
- node.items === undefined ? null : sampleFor(node.items),
143
- );
144
- case 'union': {
145
- const first = node.anyOf?.[0];
146
- return first === undefined ? null : sampleFor(first);
147
- }
148
- case 'record':
149
- return {};
150
- case 'money':
151
- return { minor: 0, currency: 'USD' };
152
- case 'object':
153
- return sampleObject(node);
154
- default:
155
- // `unknown`, and any kind a third-party provider emits that this build has never heard
156
- // of. Deliberately not `assertNever`: a swapped schema provider must not turn a
157
- // generated contract test into a crash, and the caller already reports a rejected
158
- // sample as drift with the instruction to pass the input itself.
159
- return null;
160
- }
161
- }
162
-
163
- /**
164
- * Best effort, and honest about it: a schema carrying a constraint the IR does not — a provider's
165
- * own refinement — yields a value the schema rejects. The caller turns that into
166
- * `X_CONTRACT_DRIFT`, because a silently skipped assertion is the vacuous test this function
167
- * exists to end. The one constraint the IR DOES carry is `pattern`, and `sampleGaps` reads it.
168
- */
169
- export function sampleInput(schema: StandardSchemaV1): unknown {
170
- const node = tryIntrospect(schema);
171
- return node === undefined ? {} : sampleFor(node);
172
- }
173
-
174
- /** The root's name in a gap path — a bare `t.string` input has no field to point at. */
175
- const ROOT_PATH = '(the input)';
176
-
177
- /**
178
- * Dotted paths of every sampled field whose own `pattern` the synthesized value cannot satisfy —
179
- * "what this schema needs that the framework cannot invent", in the order a reader fills them in.
180
- *
181
- * Empty is the common case, including for `t.slug` and `t.cursor`, whose patterns `'sample'`
182
- * already matches. It walks the SAMPLED shape, so an optional key — which `sampleObject`
183
- * deliberately omits — owes nothing.
184
- */
185
- export function sampleGaps(schema: StandardSchemaV1): readonly string[] {
186
- const node = tryIntrospect(schema);
187
- return node === undefined ? [] : gapsIn(node, '');
188
- }
189
-
190
- function gapsIn(node: SchemaNode, path: string): string[] {
191
- if (node.nullable === true) return [];
192
- if (node.kind === 'object') {
193
- const properties = node.properties ?? {};
194
- const out: string[] = [];
195
- for (const key of requiredKeys(node)) {
196
- const child = Object.hasOwn(properties, key) ? properties[key] : undefined;
197
- if (child !== undefined) out.push(...gapsIn(child, path === '' ? key : `${path}.${key}`));
198
- }
199
- return out;
200
- }
201
- const unbuildable = node.kind === 'array' && owedItems(node) === undefined;
202
- if (!unbuildable && satisfiesPattern(node, sampleFor(node))) return [];
203
- return [path === '' ? ROOT_PATH : path];
204
- }
205
-
206
- /** `orderRef` -> `orderRef (must match ^ORD-\d{4}$)`, for a cause a reader can act on. */
207
- export function describeSampleGap(schema: StandardSchemaV1, path: string): string {
208
- const node = nodeAt(tryIntrospect(schema), path);
209
- if (node?.kind === 'array' && node.minItems !== undefined && owedItems(node) === undefined) {
210
- return `${path} (needs at least ${node.minItems} items)`;
211
- }
212
- const pattern = node?.pattern;
213
- return pattern === undefined ? path : `${path} (must match ${pattern})`;
214
- }
215
-
216
- function nodeAt(node: SchemaNode | undefined, path: string): SchemaNode | undefined {
217
- if (node === undefined) return undefined;
218
- if (path === '' || path === ROOT_PATH) return node;
219
- const [head, ...rest] = path.split('.');
220
- // `path` is the caller's — `describeSampleGap` is exported — so the same own-property read the
221
- // format table takes: `properties['constructor']` is otherwise the `Object` function.
222
- const properties = node.properties ?? {};
223
- const child =
224
- head === undefined || !Object.hasOwn(properties, head) ? undefined : properties[head];
225
- return child === undefined ? undefined : nodeAt(child, rest.join('.'));
226
- }