@bowmark/web 1.12.2 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -3
- package/{src → dist}/generated/library.d.ts +3638 -84
- package/dist/generated/validators.d.ts +2 -0
- package/dist/generated/validators.js +26012 -0
- package/dist/guard.d.ts +64 -0
- package/dist/guard.js +174 -0
- package/dist/index.d.ts +29 -0
- package/{src/index.ts → dist/index.js} +7 -33
- package/dist/session.d.ts +46 -0
- package/dist/session.js +153 -0
- package/dist/transport.d.ts +150 -0
- package/dist/transport.js +183 -0
- package/dist/validate.d.ts +132 -0
- package/dist/validate.js +278 -0
- package/package.json +10 -6
- package/src/generated/validators.ts +0 -23104
- package/src/guard.ts +0 -198
- package/src/session.ts +0 -194
- package/src/transport.ts +0 -342
- package/src/validate.ts +0 -371
package/src/validate.ts
DELETED
|
@@ -1,371 +0,0 @@
|
|
|
1
|
-
// The ARGUMENT SHAPE guard — refuse a value the declared signature does not accept,
|
|
2
|
-
// before the request.
|
|
3
|
-
//
|
|
4
|
-
// `guard.ts` asks a structural question about JSON: can this value cross the wire at
|
|
5
|
-
// all. It is the same question for every function and it says nothing about SHAPE, so
|
|
6
|
-
// `bowmark.music.search({ quesry: "x" })` sails through it and comes back as a
|
|
7
|
-
// capability failure, metered, after a round trip. This file asks the other question:
|
|
8
|
-
// does this value match what `bowmark.music.search` declares.
|
|
9
|
-
//
|
|
10
|
-
// ── A SCHEMA plus one interpreter, not 192 generated functions ───────────────
|
|
11
|
-
//
|
|
12
|
-
// The generator emits DATA (`generated/validators.ts`) and this file is the only
|
|
13
|
-
// thing that reads it. 192 bespoke emitted functions would be 192 pieces of code no
|
|
14
|
-
// test ever runs; one interpreter over a fixture table is testable in an afternoon,
|
|
15
|
-
// and the generator's job shrinks to "TypeScript type text in, tree out". Zero
|
|
16
|
-
// runtime dependencies is the product, so there is no validator library on either
|
|
17
|
-
// side of that line.
|
|
18
|
-
//
|
|
19
|
-
// ── EVERY rule here leans toward ACCEPTING ──────────────────────────────────
|
|
20
|
-
//
|
|
21
|
-
// This is the whole design, and it is not timidity. A false REFUSAL is a caller
|
|
22
|
-
// whose correct argument is rejected by their own client, with no way around it and
|
|
23
|
-
// no server to appeal to. A false ACCEPT costs one round trip and lands them in
|
|
24
|
-
// exactly the error they would have got before this file existed. The two failure
|
|
25
|
-
// directions are not comparable, so:
|
|
26
|
-
//
|
|
27
|
-
// - An object is OPEN. An unknown property is accepted, because TypeScript's
|
|
28
|
-
// excess-property check is a compile-time nicety that fires on literals only,
|
|
29
|
-
// and a caller who spread a wider object is doing something legal.
|
|
30
|
-
// - Anything the type compiler could not fully model becomes `any`, which accepts
|
|
31
|
-
// everything. It never guesses a shape from a name.
|
|
32
|
-
// - Arity is checked DOWNWARD only — a missing required argument is refused, a
|
|
33
|
-
// surplus one is not. A surplus argument is what a caller on an older published
|
|
34
|
-
// version has when the library grew a parameter, and JavaScript ignores it.
|
|
35
|
-
//
|
|
36
|
-
// The one place this file is deliberately strict is a string LITERAL union
|
|
37
|
-
// (`sort?: "hot" | "new"`), because a typo'd enum value is the single most common
|
|
38
|
-
// wrong argument and the accepted set makes a legible error message.
|
|
39
|
-
|
|
40
|
-
/** One node of a declared argument's shape.
|
|
41
|
-
*
|
|
42
|
-
* A closed union, and `any` is the escape hatch that makes the whole thing safe:
|
|
43
|
-
* every construct the compiler does not model lands there and accepts everything. */
|
|
44
|
-
export type Schema =
|
|
45
|
-
/** Accepts anything. What an unmodelled construct compiles to. */
|
|
46
|
-
| { k: "any" }
|
|
47
|
-
| { k: "string" }
|
|
48
|
-
| { k: "number" }
|
|
49
|
-
| { k: "boolean" }
|
|
50
|
-
| { k: "null" }
|
|
51
|
-
| { k: "undefined" }
|
|
52
|
-
/** `"hot"`, `3`, `true` — a literal type, compared with `===`. */
|
|
53
|
-
| { k: "literal"; v: string | number | boolean }
|
|
54
|
-
| { k: "array"; of: Schema }
|
|
55
|
-
/** A fixed-length positional list. Extra entries are accepted, for the same
|
|
56
|
-
* reason a surplus argument is: they cost nothing and refusing them can only
|
|
57
|
-
* hurt a caller the compiler already had its chance at. */
|
|
58
|
-
| { k: "tuple"; of: Schema[] }
|
|
59
|
-
/** OPEN — an unlisted property is accepted. `index` covers a declared index
|
|
60
|
-
* signature and applies to unlisted keys only. */
|
|
61
|
-
| { k: "object"; props: PropSchema[]; index?: Schema }
|
|
62
|
-
/** `Record<string, T>` — every value must match, keys are strings on the wire. */
|
|
63
|
-
| { k: "record"; value: Schema }
|
|
64
|
-
/** Accepts if ANY arm accepts. */
|
|
65
|
-
| { k: "union"; of: Schema[] }
|
|
66
|
-
/** A named type declared in the same unit's `types` block. Resolved through the
|
|
67
|
-
* unit's `defs`, which is what makes a self-referential type expressible at all. */
|
|
68
|
-
| { k: "ref"; name: string };
|
|
69
|
-
|
|
70
|
-
export interface PropSchema {
|
|
71
|
-
name: string;
|
|
72
|
-
schema: Schema;
|
|
73
|
-
optional: boolean;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** One declared parameter. `rest` consumes every remaining argument and validates
|
|
77
|
-
* each against `schema` — which is the ELEMENT type, not the array. */
|
|
78
|
-
export interface ParamSchema {
|
|
79
|
-
/** As declared, or `arg0` when the signature destructured and never named it. */
|
|
80
|
-
name: string;
|
|
81
|
-
schema: Schema;
|
|
82
|
-
optional: boolean;
|
|
83
|
-
rest?: boolean;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** One unit's validators, plus the type declarations its parameters reference.
|
|
87
|
-
*
|
|
88
|
-
* `defs` is per UNIT rather than per function because a unit's `types` block is
|
|
89
|
-
* shared by every one of its signatures — `music.search` and `music.getTrack` both
|
|
90
|
-
* resolve `Track` against the same block, and the two tiers deliberately do not
|
|
91
|
-
* share an id space, let alone a type namespace. */
|
|
92
|
-
export interface UnitValidators {
|
|
93
|
-
defs: Record<string, Schema>;
|
|
94
|
-
/** By function name. `null` is EXPLICIT and means "this function is real and
|
|
95
|
-
* callable and we hold no shape for it" — the 20 whose declared argument is a
|
|
96
|
-
* bare destructuring pattern. It is not the same as an absent key, and the
|
|
97
|
-
* difference is what makes failing closed on an absent one safe. */
|
|
98
|
-
functions: Record<string, ParamSchema[] | null>;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export interface ValidatorTable {
|
|
102
|
-
/** The manifest version these were generated from. Reported in the refusal for an
|
|
103
|
-
* unknown path, because "your package predates that function" is the answer
|
|
104
|
-
* roughly every time. */
|
|
105
|
-
version: string;
|
|
106
|
-
/** Keyed by the unit's NAMESPACE — `music`, `providers.soundcloud` — which is the
|
|
107
|
-
* call path with `bowmark.` and the function name removed. */
|
|
108
|
-
units: Record<string, UnitValidators>;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** What is wrong and WHERE. Same shape as `guard.ts`'s, so the two guards produce
|
|
112
|
-
* one error format and a caller never has to tell them apart. */
|
|
113
|
-
export interface ShapeProblem {
|
|
114
|
-
/** Dotted/bracketed path from the argument root. Empty at the root. */
|
|
115
|
-
path: string;
|
|
116
|
-
reason: string;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/** What the table says about one call path.
|
|
120
|
-
*
|
|
121
|
-
* The two negative answers are SEPARATE because they are different facts, and
|
|
122
|
-
* collapsing them would have made this package refuse the largest part of the
|
|
123
|
-
* library. See `assertArgShape` in `guard.ts`. */
|
|
124
|
-
export type Lookup =
|
|
125
|
-
| { kind: "checked"; params: ParamSchema[] }
|
|
126
|
-
/** The function exists and declares no readable argument shape. */
|
|
127
|
-
| { kind: "unchecked" }
|
|
128
|
-
/** The unit is here and declares no such function. The table IS authoritative
|
|
129
|
-
* about a unit it carries, so this is a typo or a stale install. */
|
|
130
|
-
| { kind: "unknown-function" }
|
|
131
|
-
/** No such unit. The table is NOT authoritative about this — a family MEMBER
|
|
132
|
-
* (`providers.gymshark`) is deliberately absent from every manifest, so an
|
|
133
|
-
* unknown unit is the normal case for most of the library rather than an error. */
|
|
134
|
-
| { kind: "unknown-unit" };
|
|
135
|
-
|
|
136
|
-
/** Split `["music", "search"]` / `["providers", "aa", "getFlightStatus"]` into the
|
|
137
|
-
* unit namespace and the function name.
|
|
138
|
-
*
|
|
139
|
-
* The last segment is always the function and everything before it is the
|
|
140
|
-
* namespace, which is true of both tiers by construction: a capability is
|
|
141
|
-
* `<id>.<fn>` and a provider is `providers.<id>.<fn>`. Nothing deeper exists — the
|
|
142
|
-
* runtime's own dispatcher refuses one. */
|
|
143
|
-
function splitPath(path: readonly string[]): { namespace: string; fn: string } | null {
|
|
144
|
-
if (path.length < 2) return null;
|
|
145
|
-
return { namespace: path.slice(0, -1).join("."), fn: path[path.length - 1] as string };
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export function lookupParams(table: ValidatorTable, path: readonly string[]): Lookup {
|
|
149
|
-
const split = splitPath(path);
|
|
150
|
-
if (!split) return { kind: "unknown-unit" };
|
|
151
|
-
const unit = table.units[split.namespace];
|
|
152
|
-
if (!unit) return { kind: "unknown-unit" };
|
|
153
|
-
if (!Object.hasOwn(unit.functions, split.fn)) return { kind: "unknown-function" };
|
|
154
|
-
// `hasOwn` above is what distinguishes an EXPLICIT null from an absent key, and
|
|
155
|
-
// `noUncheckedIndexedAccess` cannot see that it did — hence the widened read.
|
|
156
|
-
const params = unit.functions[split.fn] as ParamSchema[] | null;
|
|
157
|
-
return params === null ? { kind: "unchecked" } : { kind: "checked", params };
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/** Check an argument list against a function's declared parameters.
|
|
161
|
-
*
|
|
162
|
-
* Returns the FIRST problem, or null. `args` is the caller's array verbatim; a
|
|
163
|
-
* trailing `undefined` is treated as absent, because that is what
|
|
164
|
-
* `f(a, undefined)` means to a caller passing an optional through. */
|
|
165
|
-
export function argsProblem(
|
|
166
|
-
params: readonly ParamSchema[],
|
|
167
|
-
args: readonly unknown[],
|
|
168
|
-
defs: Record<string, Schema>,
|
|
169
|
-
): ShapeProblem | null {
|
|
170
|
-
for (let i = 0; i < params.length; i++) {
|
|
171
|
-
const param = params[i] as ParamSchema;
|
|
172
|
-
if (param.rest) {
|
|
173
|
-
for (let j = i; j < args.length; j++) {
|
|
174
|
-
const problem = check(args[j], param.schema, defs, `args[${j}]`);
|
|
175
|
-
if (problem) return problem;
|
|
176
|
-
}
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
179
|
-
const supplied = i < args.length ? args[i] : undefined;
|
|
180
|
-
if (supplied === undefined) {
|
|
181
|
-
// Checked DOWNWARD only. A required parameter nobody passed is a refusal the
|
|
182
|
-
// caller can act on; a surplus one is not, and refusing it would break a
|
|
183
|
-
// caller on a published version older than the parameter.
|
|
184
|
-
if (!param.optional) {
|
|
185
|
-
return {
|
|
186
|
-
path: `args[${i}]`,
|
|
187
|
-
reason: `missing — \`${param.name}\` is required`,
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
const problem = check(supplied, param.schema, defs, `args[${i}]`);
|
|
193
|
-
if (problem) return problem;
|
|
194
|
-
}
|
|
195
|
-
return null;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const MAX_DEPTH = 64;
|
|
199
|
-
|
|
200
|
-
function check(
|
|
201
|
-
value: unknown,
|
|
202
|
-
schema: Schema,
|
|
203
|
-
defs: Record<string, Schema>,
|
|
204
|
-
path: string,
|
|
205
|
-
depth = 0,
|
|
206
|
-
): ShapeProblem | null {
|
|
207
|
-
// A self-referential type plus a value deep enough to exhaust the stack. The wire
|
|
208
|
-
// guard already refused a CIRCULAR value, so anything reaching here is finite and
|
|
209
|
-
// 64 levels is far past any real argument — but a bound that fails OPEN is the
|
|
210
|
-
// rule of this file, so past it we accept rather than refuse.
|
|
211
|
-
if (depth > MAX_DEPTH) return null;
|
|
212
|
-
|
|
213
|
-
switch (schema.k) {
|
|
214
|
-
case "any":
|
|
215
|
-
return null;
|
|
216
|
-
case "ref": {
|
|
217
|
-
const target = defs[schema.name];
|
|
218
|
-
// An unresolvable name accepts. The generator already refuses a signature
|
|
219
|
-
// naming a type its unit never declares (`gate:public-types`'s
|
|
220
|
-
// KNOWN_UNDECLARED), so this is unreachable today and must not be the thing
|
|
221
|
-
// that invents a refusal if it ever is.
|
|
222
|
-
if (!target) return null;
|
|
223
|
-
return check(value, target, defs, path, depth + 1);
|
|
224
|
-
}
|
|
225
|
-
case "string":
|
|
226
|
-
return typeof value === "string" ? null : mismatch(path, value, "a string");
|
|
227
|
-
case "number":
|
|
228
|
-
return typeof value === "number" ? null : mismatch(path, value, "a number");
|
|
229
|
-
case "boolean":
|
|
230
|
-
return typeof value === "boolean" ? null : mismatch(path, value, "a boolean");
|
|
231
|
-
case "null":
|
|
232
|
-
return value === null ? null : mismatch(path, value, "null");
|
|
233
|
-
case "undefined":
|
|
234
|
-
return value === undefined ? null : mismatch(path, value, "undefined");
|
|
235
|
-
case "literal":
|
|
236
|
-
return value === schema.v ? null : mismatch(path, value, JSON.stringify(schema.v));
|
|
237
|
-
case "array": {
|
|
238
|
-
if (!Array.isArray(value)) return mismatch(path, value, "an array");
|
|
239
|
-
for (let i = 0; i < value.length; i++) {
|
|
240
|
-
const problem = check(value[i], schema.of, defs, `${path}[${i}]`, depth + 1);
|
|
241
|
-
if (problem) return problem;
|
|
242
|
-
}
|
|
243
|
-
return null;
|
|
244
|
-
}
|
|
245
|
-
case "tuple": {
|
|
246
|
-
if (!Array.isArray(value)) return mismatch(path, value, "an array");
|
|
247
|
-
for (let i = 0; i < schema.of.length; i++) {
|
|
248
|
-
const problem = check(value[i], schema.of[i] as Schema, defs, `${path}[${i}]`, depth + 1);
|
|
249
|
-
if (problem) return problem;
|
|
250
|
-
}
|
|
251
|
-
return null;
|
|
252
|
-
}
|
|
253
|
-
case "record": {
|
|
254
|
-
if (!isPlainObject(value)) return mismatch(path, value, "an object");
|
|
255
|
-
for (const [key, child] of Object.entries(value)) {
|
|
256
|
-
const problem = check(child, schema.value, defs, `${path}.${key}`, depth + 1);
|
|
257
|
-
if (problem) return problem;
|
|
258
|
-
}
|
|
259
|
-
return null;
|
|
260
|
-
}
|
|
261
|
-
case "object": {
|
|
262
|
-
if (!isPlainObject(value)) return mismatch(path, value, "an object");
|
|
263
|
-
const declared = new Set<string>();
|
|
264
|
-
for (const prop of schema.props) {
|
|
265
|
-
declared.add(prop.name);
|
|
266
|
-
const child = (value as Record<string, unknown>)[prop.name];
|
|
267
|
-
if (child === undefined) {
|
|
268
|
-
if (prop.optional || !Object.hasOwn(value, prop.name)) {
|
|
269
|
-
if (!prop.optional) {
|
|
270
|
-
return { path: `${path}.${prop.name}`, reason: "missing — it is required" };
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
continue;
|
|
274
|
-
}
|
|
275
|
-
const problem = check(child, prop.schema, defs, `${path}.${prop.name}`, depth + 1);
|
|
276
|
-
if (problem) return problem;
|
|
277
|
-
}
|
|
278
|
-
// OPEN on unlisted keys unless the type declared an index signature, which is
|
|
279
|
-
// the only case where the author said something about them.
|
|
280
|
-
if (schema.index) {
|
|
281
|
-
for (const [key, child] of Object.entries(value)) {
|
|
282
|
-
if (declared.has(key)) continue;
|
|
283
|
-
const problem = check(child, schema.index, defs, `${path}.${key}`, depth + 1);
|
|
284
|
-
if (problem) return problem;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
return null;
|
|
288
|
-
}
|
|
289
|
-
case "union": {
|
|
290
|
-
let deepest: ShapeProblem | null = null;
|
|
291
|
-
for (const arm of schema.of) {
|
|
292
|
-
const problem = check(value, arm, defs, path, depth + 1);
|
|
293
|
-
if (!problem) return null;
|
|
294
|
-
// The arm the caller MEANT is almost always the one the value got furthest
|
|
295
|
-
// into: `string | { query: string }` given `{ query: 5 }` fails the string
|
|
296
|
-
// arm at the root and the object arm at `.query`, and only the second says
|
|
297
|
-
// anything useful. Reporting the union itself would name six arms and point
|
|
298
|
-
// at nothing.
|
|
299
|
-
if (!deepest || problem.path.length > deepest.path.length) deepest = problem;
|
|
300
|
-
}
|
|
301
|
-
if (deepest && deepest.path !== path) return deepest;
|
|
302
|
-
return { path, reason: `${describe(value)} — expected ${describeSchema(schema, defs)}` };
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/** A plain object, by the same test the wire guard uses: a class instance, a `Map`
|
|
308
|
-
* and a `Date` are all refused THERE, with a better message, before this runs. */
|
|
309
|
-
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
310
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
311
|
-
const prototype = Object.getPrototypeOf(value) as object | null;
|
|
312
|
-
return prototype === Object.prototype || prototype === null;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function mismatch(path: string, value: unknown, expected: string): ShapeProblem {
|
|
316
|
-
return { path, reason: `${describe(value)} — expected ${expected}` };
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function describe(value: unknown): string {
|
|
320
|
-
if (value === null) return "null";
|
|
321
|
-
if (Array.isArray(value)) return "an array";
|
|
322
|
-
switch (typeof value) {
|
|
323
|
-
case "string":
|
|
324
|
-
return `the string ${JSON.stringify(value.length > 24 ? `${value.slice(0, 24)}…` : value)}`;
|
|
325
|
-
case "number":
|
|
326
|
-
return `the number ${String(value)}`;
|
|
327
|
-
case "boolean":
|
|
328
|
-
return `the boolean ${String(value)}`;
|
|
329
|
-
case "undefined":
|
|
330
|
-
return "undefined";
|
|
331
|
-
case "object":
|
|
332
|
-
return "an object";
|
|
333
|
-
default:
|
|
334
|
-
return `a ${typeof value}`;
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/** One clause naming what a schema accepts. Only ever reached on a union that no
|
|
339
|
-
* arm matched, which is where a caller most needs the accepted set spelled out —
|
|
340
|
-
* a wrong string literal is the common case and `"hot" | "new" | "top"` is the
|
|
341
|
-
* whole answer. */
|
|
342
|
-
function describeSchema(schema: Schema, defs: Record<string, Schema>, depth = 0): string {
|
|
343
|
-
if (depth > 4) return "…";
|
|
344
|
-
switch (schema.k) {
|
|
345
|
-
case "any":
|
|
346
|
-
return "anything";
|
|
347
|
-
case "ref":
|
|
348
|
-
return schema.name;
|
|
349
|
-
case "literal":
|
|
350
|
-
return JSON.stringify(schema.v);
|
|
351
|
-
case "array":
|
|
352
|
-
return `an array of ${describeSchema(schema.of, defs, depth + 1)}`;
|
|
353
|
-
case "tuple":
|
|
354
|
-
return "an array";
|
|
355
|
-
case "record":
|
|
356
|
-
return "an object";
|
|
357
|
-
case "object":
|
|
358
|
-
return schema.props.length > 0
|
|
359
|
-
? `an object with ${
|
|
360
|
-
schema.props
|
|
361
|
-
.filter((p) => !p.optional)
|
|
362
|
-
.map((p) => p.name)
|
|
363
|
-
.join(", ") || "optional properties only"
|
|
364
|
-
}`
|
|
365
|
-
: "an object";
|
|
366
|
-
case "union":
|
|
367
|
-
return schema.of.map((arm) => describeSchema(arm, defs, depth + 1)).join(" | ");
|
|
368
|
-
default:
|
|
369
|
-
return schema.k;
|
|
370
|
-
}
|
|
371
|
-
}
|