@bowmark/web 1.12.2 → 1.12.3
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 +409 -11
- package/dist/generated/validators.d.ts +2 -0
- package/dist/generated/validators.js +23430 -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
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/** The subset of `fetch` this package uses. Declared structurally so a caller can
|
|
2
|
+
* pass `undici`'s, a test double, or a proxying wrapper without a type assertion. */
|
|
3
|
+
export type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
|
|
4
|
+
/** How a client reaches the api. Every field is optional; the defaults read the
|
|
5
|
+
* environment the same way every other CLI-shaped client does. */
|
|
6
|
+
export interface ClientOptions {
|
|
7
|
+
/** `bmk_…`. Falls back to `BOWMARK_API_KEY`. Absent is legal — an anonymous
|
|
8
|
+
* caller keeps every browserless capability, on a smaller daily budget. */
|
|
9
|
+
apiKey?: string;
|
|
10
|
+
/** Defaults to `BOWMARK_API_URL`, else `https://api.bowmark.ai`. */
|
|
11
|
+
baseUrl?: string;
|
|
12
|
+
/** Defaults to `globalThis.fetch`. */
|
|
13
|
+
fetch?: FetchLike;
|
|
14
|
+
/** Merged into every request. Cannot override `authorization` or
|
|
15
|
+
* `content-type` — a header that silently replaced the key would make a failed
|
|
16
|
+
* call look like a permissions problem. */
|
|
17
|
+
headers?: Record<string, string>;
|
|
18
|
+
/** Aborts every request this client makes. */
|
|
19
|
+
signal?: AbortSignal;
|
|
20
|
+
/** Called with each call's server-side `logs`, and with anything the client
|
|
21
|
+
* itself swallows (a failed close). Logs are otherwise dropped, because the
|
|
22
|
+
* typed return value is the capability's return value and nothing else. */
|
|
23
|
+
onLog?: (line: string) => void;
|
|
24
|
+
}
|
|
25
|
+
/** The wire shape one session call returns. Mirrors `SessionEnvelope` in
|
|
26
|
+
* `apps/api/src/session-core.ts`.
|
|
27
|
+
*
|
|
28
|
+
* `needs_user` is a STATUS, not an error: the call paused for a human login and
|
|
29
|
+
* **the session is still open**. That is the one thing this surface does that
|
|
30
|
+
* `/v1/run` cannot, and it is why it maps to its own error subclass rather than
|
|
31
|
+
* being folded into a generic failure. */
|
|
32
|
+
export interface CallEnvelope<T = unknown> {
|
|
33
|
+
ok: boolean;
|
|
34
|
+
status: "ok" | "error" | "needs_user";
|
|
35
|
+
result?: T;
|
|
36
|
+
logs?: string[];
|
|
37
|
+
error?: string | null;
|
|
38
|
+
/** Machine-branchable failure reason. Absent on success. */
|
|
39
|
+
code?: string;
|
|
40
|
+
ms?: number;
|
|
41
|
+
needs?: AuthNeed[];
|
|
42
|
+
meta?: {
|
|
43
|
+
handoff?: Handoff;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** The wire shape `POST /v1/run` returns. Mirrors `RunEnvelope` in
|
|
47
|
+
* `apps/api/src/run-core.ts`. It carries a fourth status the session surface does
|
|
48
|
+
* not have — `partial`, a fan-out one leg of which died — because a session call IS
|
|
49
|
+
* one leg and cannot be partial. */
|
|
50
|
+
export interface RunEnvelope<T = unknown> {
|
|
51
|
+
ok: boolean;
|
|
52
|
+
status?: "ok" | "error" | "partial" | "needs_user";
|
|
53
|
+
result?: T;
|
|
54
|
+
logs?: string[];
|
|
55
|
+
error?: string | null;
|
|
56
|
+
ms?: number;
|
|
57
|
+
needs?: AuthNeed[];
|
|
58
|
+
incomplete?: {
|
|
59
|
+
summary: string;
|
|
60
|
+
[key: string]: unknown;
|
|
61
|
+
};
|
|
62
|
+
meta?: {
|
|
63
|
+
handoff?: Handoff;
|
|
64
|
+
wwwAuthenticate?: string;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** Which (capability, provider) pair has no live grant. */
|
|
68
|
+
export interface AuthNeed {
|
|
69
|
+
capability: string;
|
|
70
|
+
provider: string;
|
|
71
|
+
providerTitle: string;
|
|
72
|
+
kind: string;
|
|
73
|
+
discoveredBy: "preflight" | "halt";
|
|
74
|
+
}
|
|
75
|
+
/** The relay URL to hand a human. */
|
|
76
|
+
export interface Handoff {
|
|
77
|
+
ref: string;
|
|
78
|
+
url: string;
|
|
79
|
+
expiresAt: string;
|
|
80
|
+
}
|
|
81
|
+
export interface OpenedSession {
|
|
82
|
+
sessionId: string;
|
|
83
|
+
expiresAt: string;
|
|
84
|
+
}
|
|
85
|
+
export interface ClosedSession {
|
|
86
|
+
ok: true;
|
|
87
|
+
calls: number;
|
|
88
|
+
ms: number;
|
|
89
|
+
}
|
|
90
|
+
/** Anything this package throws.
|
|
91
|
+
*
|
|
92
|
+
* `code` is the field to branch on. The api sends prose in `error` written for an
|
|
93
|
+
* agent to read, which is the wrong shape for a `catch` block — both are present and
|
|
94
|
+
* neither replaces the other. */
|
|
95
|
+
export declare class BowmarkError extends Error {
|
|
96
|
+
readonly code: string;
|
|
97
|
+
/** HTTP status, or 0 when the request never reached us. */
|
|
98
|
+
readonly httpStatus: number;
|
|
99
|
+
readonly logs: readonly string[];
|
|
100
|
+
/** The path that failed, as a caller writes it: `bowmark.music.search`. */
|
|
101
|
+
readonly path?: string;
|
|
102
|
+
constructor(message: string, init: {
|
|
103
|
+
code: string;
|
|
104
|
+
httpStatus?: number;
|
|
105
|
+
logs?: readonly string[];
|
|
106
|
+
path?: string;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/** One call paused for a human login. **The session is still open** and the same
|
|
110
|
+
* call can be retried once the person has signed in at `handoff.url`.
|
|
111
|
+
*
|
|
112
|
+
* A subclass rather than a `code`, because the recovery is completely different: a
|
|
113
|
+
* `BowmarkError` means stop or fix the call, and this means show a URL and wait. An
|
|
114
|
+
* agent that reads a failure retries, and retrying a login halt buys the same halt. */
|
|
115
|
+
export declare class BowmarkNeedsUserError extends BowmarkError {
|
|
116
|
+
readonly handoff?: Handoff;
|
|
117
|
+
readonly needs: readonly AuthNeed[];
|
|
118
|
+
constructor(message: string, init: {
|
|
119
|
+
code: string;
|
|
120
|
+
httpStatus?: number;
|
|
121
|
+
logs?: readonly string[];
|
|
122
|
+
path?: string;
|
|
123
|
+
handoff?: Handoff;
|
|
124
|
+
needs?: readonly AuthNeed[];
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/** A `ClientOptions` with every default already applied, so nothing downstream has
|
|
128
|
+
* to remember which fields were optional. */
|
|
129
|
+
export interface ResolvedClient {
|
|
130
|
+
apiKey: string | null;
|
|
131
|
+
baseUrl: string;
|
|
132
|
+
fetch: FetchLike;
|
|
133
|
+
headers: Record<string, string>;
|
|
134
|
+
signal?: AbortSignal;
|
|
135
|
+
onLog?: (line: string) => void;
|
|
136
|
+
}
|
|
137
|
+
export declare function resolveClient(opts?: ClientOptions): ResolvedClient;
|
|
138
|
+
export declare function openSession(client: ResolvedClient): Promise<OpenedSession>;
|
|
139
|
+
/** Dispatch ONE call inside an open session, and unwrap it.
|
|
140
|
+
*
|
|
141
|
+
* Returns the capability's own return value, because that is what the generated
|
|
142
|
+
* types promise. The envelope's other fields are not dropped: `logs` go to `onLog`,
|
|
143
|
+
* and everything that is not `status: "ok"` becomes a throw carrying `code`. */
|
|
144
|
+
export declare function callInSession(client: ResolvedClient, sessionId: string, path: readonly string[], args: readonly unknown[]): Promise<unknown>;
|
|
145
|
+
/** Close a session. Idempotent server-side, so a `finally` that runs twice is fine. */
|
|
146
|
+
export declare function closeSession(client: ResolvedClient, sessionId: string): Promise<ClosedSession | null>;
|
|
147
|
+
/** The string surface. Returns the ENVELOPE rather than throwing, because a script
|
|
148
|
+
* is composite: a `needs_user` or an `error` is a fact about the run that a caller
|
|
149
|
+
* reads alongside `logs` and `result`, not an exception in their control flow. */
|
|
150
|
+
export declare function postRun<T>(client: ResolvedClient, script: string): Promise<RunEnvelope<T>>;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// The transport — `fetch` and nothing else.
|
|
2
|
+
//
|
|
3
|
+
// Three session endpoints plus the string surface. No retries, no backoff, no
|
|
4
|
+
// connection pool: every one of those is a policy a caller can hold better than we
|
|
5
|
+
// can, and a client that silently retries a `addToCart` is the bug the idempotency
|
|
6
|
+
// discussion in `docs/reference/building-capabilities.md` exists to prevent.
|
|
7
|
+
//
|
|
8
|
+
// ZERO RUNTIME DEPENDENCIES, permanently — including on `@bowmark/schema`. The
|
|
9
|
+
// envelope shapes below are hand-restated rather than imported, because importing a
|
|
10
|
+
// workspace package would make the published tarball uninstallable outside this repo.
|
|
11
|
+
// `tests/unit/bowmark-web-envelopes.test.ts` compares the two status vocabularies
|
|
12
|
+
// TEXTUALLY, because nothing typechecks `tests/unit/` — a `satisfies` there reads as
|
|
13
|
+
// a gate and is compiled away by esbuild. Adding a status on either side alone fails
|
|
14
|
+
// that test by name.
|
|
15
|
+
/** Anything this package throws.
|
|
16
|
+
*
|
|
17
|
+
* `code` is the field to branch on. The api sends prose in `error` written for an
|
|
18
|
+
* agent to read, which is the wrong shape for a `catch` block — both are present and
|
|
19
|
+
* neither replaces the other. */
|
|
20
|
+
export class BowmarkError extends Error {
|
|
21
|
+
code;
|
|
22
|
+
/** HTTP status, or 0 when the request never reached us. */
|
|
23
|
+
httpStatus;
|
|
24
|
+
logs;
|
|
25
|
+
/** The path that failed, as a caller writes it: `bowmark.music.search`. */
|
|
26
|
+
path;
|
|
27
|
+
constructor(message, init) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = "BowmarkError";
|
|
30
|
+
this.code = init.code;
|
|
31
|
+
this.httpStatus = init.httpStatus ?? 0;
|
|
32
|
+
this.logs = init.logs ?? [];
|
|
33
|
+
this.path = init.path;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** One call paused for a human login. **The session is still open** and the same
|
|
37
|
+
* call can be retried once the person has signed in at `handoff.url`.
|
|
38
|
+
*
|
|
39
|
+
* A subclass rather than a `code`, because the recovery is completely different: a
|
|
40
|
+
* `BowmarkError` means stop or fix the call, and this means show a URL and wait. An
|
|
41
|
+
* agent that reads a failure retries, and retrying a login halt buys the same halt. */
|
|
42
|
+
export class BowmarkNeedsUserError extends BowmarkError {
|
|
43
|
+
handoff;
|
|
44
|
+
needs;
|
|
45
|
+
constructor(message, init) {
|
|
46
|
+
super(message, init);
|
|
47
|
+
this.name = "BowmarkNeedsUserError";
|
|
48
|
+
this.handoff = init.handoff;
|
|
49
|
+
this.needs = init.needs ?? [];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const DEFAULT_BASE_URL = "https://api.bowmark.ai";
|
|
53
|
+
/** Read one environment variable without depending on `@types/node`.
|
|
54
|
+
*
|
|
55
|
+
* This package compiles with `"types": []` and must run in a browser, a worker and
|
|
56
|
+
* Node alike, so `process` is reached through `globalThis` and its absence is a
|
|
57
|
+
* normal answer rather than a crash. */
|
|
58
|
+
function envVar(name) {
|
|
59
|
+
const proc = globalThis.process;
|
|
60
|
+
return proc?.env?.[name];
|
|
61
|
+
}
|
|
62
|
+
export function resolveClient(opts = {}) {
|
|
63
|
+
const fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
64
|
+
if (!fetchImpl) {
|
|
65
|
+
throw new BowmarkError("no `fetch` available. Pass one as `{ fetch }` — Node 18+ and every browser have a global one.", { code: "no_fetch" });
|
|
66
|
+
}
|
|
67
|
+
const baseUrl = (opts.baseUrl ?? envVar("BOWMARK_API_URL") ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
68
|
+
return {
|
|
69
|
+
apiKey: opts.apiKey ?? envVar("BOWMARK_API_KEY") ?? null,
|
|
70
|
+
baseUrl,
|
|
71
|
+
fetch: fetchImpl,
|
|
72
|
+
headers: { ...(opts.headers ?? {}) },
|
|
73
|
+
signal: opts.signal,
|
|
74
|
+
onLog: opts.onLog,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** POST JSON and parse JSON back.
|
|
78
|
+
*
|
|
79
|
+
* The caller's headers are merged FIRST so `authorization` and `content-type` win —
|
|
80
|
+
* a header that silently replaced the api key would make an unauthenticated call
|
|
81
|
+
* look like a permissions problem on our side. */
|
|
82
|
+
async function postJson(client, path, body) {
|
|
83
|
+
const headers = {
|
|
84
|
+
...client.headers,
|
|
85
|
+
"content-type": "application/json",
|
|
86
|
+
accept: "application/json",
|
|
87
|
+
};
|
|
88
|
+
if (client.apiKey)
|
|
89
|
+
headers.authorization = `Bearer ${client.apiKey}`;
|
|
90
|
+
let response;
|
|
91
|
+
try {
|
|
92
|
+
response = await client.fetch(`${client.baseUrl}${path}`, {
|
|
93
|
+
method: "POST",
|
|
94
|
+
headers,
|
|
95
|
+
body: JSON.stringify(body),
|
|
96
|
+
signal: client.signal,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
// A transport failure is not a capability failure, and conflating the two is
|
|
101
|
+
// how a caller comes to retry a DNS problem against a site.
|
|
102
|
+
throw new BowmarkError(`could not reach ${client.baseUrl}${path}: ${String(err)}`, {
|
|
103
|
+
code: "network_error",
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const text = await response.text();
|
|
107
|
+
let payload;
|
|
108
|
+
try {
|
|
109
|
+
payload = text ? JSON.parse(text) : null;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// A non-JSON body at this point is a proxy, a captive portal or an outage —
|
|
113
|
+
// never us. Quote a bounded slice of it, because "unexpected token <" with no
|
|
114
|
+
// body is the least actionable error in software.
|
|
115
|
+
throw new BowmarkError(`${client.baseUrl}${path} answered ${response.status} with a non-JSON body: ${text.slice(0, 200)}`, { code: "bad_response", httpStatus: response.status });
|
|
116
|
+
}
|
|
117
|
+
return { status: response.status, payload: payload };
|
|
118
|
+
}
|
|
119
|
+
export async function openSession(client) {
|
|
120
|
+
const { status, payload } = await postJson(client, "/v1/session", {});
|
|
121
|
+
if (status !== 200 || !payload?.sessionId) {
|
|
122
|
+
throw new BowmarkError(payload?.error ?? `could not open a session (HTTP ${status})`, {
|
|
123
|
+
code: payload?.code ?? `http_${status}`,
|
|
124
|
+
httpStatus: status,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return { sessionId: payload.sessionId, expiresAt: payload.expiresAt };
|
|
128
|
+
}
|
|
129
|
+
/** Dispatch ONE call inside an open session, and unwrap it.
|
|
130
|
+
*
|
|
131
|
+
* Returns the capability's own return value, because that is what the generated
|
|
132
|
+
* types promise. The envelope's other fields are not dropped: `logs` go to `onLog`,
|
|
133
|
+
* and everything that is not `status: "ok"` becomes a throw carrying `code`. */
|
|
134
|
+
export async function callInSession(client, sessionId, path, args) {
|
|
135
|
+
const label = ["bowmark", ...path].join(".");
|
|
136
|
+
const { status, payload } = await postJson(client, `/v1/session/${encodeURIComponent(sessionId)}/call`, { path, args });
|
|
137
|
+
if (client.onLog)
|
|
138
|
+
for (const line of payload?.logs ?? [])
|
|
139
|
+
client.onLog(line);
|
|
140
|
+
if (payload?.status === "needs_user") {
|
|
141
|
+
throw new BowmarkNeedsUserError(payload.error ??
|
|
142
|
+
`${label} paused for a login. Open the handoff URL, sign in, then call it again — the session is still open.`, {
|
|
143
|
+
code: payload.code ?? "needs_user",
|
|
144
|
+
httpStatus: status,
|
|
145
|
+
logs: payload.logs,
|
|
146
|
+
path: label,
|
|
147
|
+
handoff: payload.meta?.handoff,
|
|
148
|
+
needs: payload.needs,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
if (status !== 200 || !payload || payload.ok !== true) {
|
|
152
|
+
throw new BowmarkError(payload?.error ?? `${label} failed (HTTP ${status})`, {
|
|
153
|
+
code: payload?.code ?? `http_${status}`,
|
|
154
|
+
httpStatus: status,
|
|
155
|
+
logs: payload?.logs,
|
|
156
|
+
path: label,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return payload.result;
|
|
160
|
+
}
|
|
161
|
+
/** Close a session. Idempotent server-side, so a `finally` that runs twice is fine. */
|
|
162
|
+
export async function closeSession(client, sessionId) {
|
|
163
|
+
const { status, payload } = await postJson(client, `/v1/session/${encodeURIComponent(sessionId)}/close`, {});
|
|
164
|
+
if (status !== 200 || payload?.ok !== true)
|
|
165
|
+
return null;
|
|
166
|
+
return { ok: true, calls: payload.calls, ms: payload.ms };
|
|
167
|
+
}
|
|
168
|
+
/** The string surface. Returns the ENVELOPE rather than throwing, because a script
|
|
169
|
+
* is composite: a `needs_user` or an `error` is a fact about the run that a caller
|
|
170
|
+
* reads alongside `logs` and `result`, not an exception in their control flow. */
|
|
171
|
+
export async function postRun(client, script) {
|
|
172
|
+
const { status, payload } = await postJson(client, "/v1/run", { script });
|
|
173
|
+
if (client.onLog)
|
|
174
|
+
for (const line of payload?.logs ?? [])
|
|
175
|
+
client.onLog(line);
|
|
176
|
+
if (!payload) {
|
|
177
|
+
throw new BowmarkError(`/v1/run answered ${status} with an empty body`, {
|
|
178
|
+
code: `http_${status}`,
|
|
179
|
+
httpStatus: status,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return payload;
|
|
183
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/** One node of a declared argument's shape.
|
|
2
|
+
*
|
|
3
|
+
* A closed union, and `any` is the escape hatch that makes the whole thing safe:
|
|
4
|
+
* every construct the compiler does not model lands there and accepts everything. */
|
|
5
|
+
export type Schema =
|
|
6
|
+
/** Accepts anything. What an unmodelled construct compiles to. */
|
|
7
|
+
{
|
|
8
|
+
k: "any";
|
|
9
|
+
} | {
|
|
10
|
+
k: "string";
|
|
11
|
+
} | {
|
|
12
|
+
k: "number";
|
|
13
|
+
} | {
|
|
14
|
+
k: "boolean";
|
|
15
|
+
} | {
|
|
16
|
+
k: "null";
|
|
17
|
+
} | {
|
|
18
|
+
k: "undefined";
|
|
19
|
+
}
|
|
20
|
+
/** `"hot"`, `3`, `true` — a literal type, compared with `===`. */
|
|
21
|
+
| {
|
|
22
|
+
k: "literal";
|
|
23
|
+
v: string | number | boolean;
|
|
24
|
+
} | {
|
|
25
|
+
k: "array";
|
|
26
|
+
of: Schema;
|
|
27
|
+
}
|
|
28
|
+
/** A fixed-length positional list. Extra entries are accepted, for the same
|
|
29
|
+
* reason a surplus argument is: they cost nothing and refusing them can only
|
|
30
|
+
* hurt a caller the compiler already had its chance at. */
|
|
31
|
+
| {
|
|
32
|
+
k: "tuple";
|
|
33
|
+
of: Schema[];
|
|
34
|
+
}
|
|
35
|
+
/** OPEN — an unlisted property is accepted. `index` covers a declared index
|
|
36
|
+
* signature and applies to unlisted keys only. */
|
|
37
|
+
| {
|
|
38
|
+
k: "object";
|
|
39
|
+
props: PropSchema[];
|
|
40
|
+
index?: Schema;
|
|
41
|
+
}
|
|
42
|
+
/** `Record<string, T>` — every value must match, keys are strings on the wire. */
|
|
43
|
+
| {
|
|
44
|
+
k: "record";
|
|
45
|
+
value: Schema;
|
|
46
|
+
}
|
|
47
|
+
/** Accepts if ANY arm accepts. */
|
|
48
|
+
| {
|
|
49
|
+
k: "union";
|
|
50
|
+
of: Schema[];
|
|
51
|
+
}
|
|
52
|
+
/** A named type declared in the same unit's `types` block. Resolved through the
|
|
53
|
+
* unit's `defs`, which is what makes a self-referential type expressible at all. */
|
|
54
|
+
| {
|
|
55
|
+
k: "ref";
|
|
56
|
+
name: string;
|
|
57
|
+
};
|
|
58
|
+
export interface PropSchema {
|
|
59
|
+
name: string;
|
|
60
|
+
schema: Schema;
|
|
61
|
+
optional: boolean;
|
|
62
|
+
}
|
|
63
|
+
/** One declared parameter. `rest` consumes every remaining argument and validates
|
|
64
|
+
* each against `schema` — which is the ELEMENT type, not the array. */
|
|
65
|
+
export interface ParamSchema {
|
|
66
|
+
/** As declared, or `arg0` when the signature destructured and never named it. */
|
|
67
|
+
name: string;
|
|
68
|
+
schema: Schema;
|
|
69
|
+
optional: boolean;
|
|
70
|
+
rest?: boolean;
|
|
71
|
+
}
|
|
72
|
+
/** One unit's validators, plus the type declarations its parameters reference.
|
|
73
|
+
*
|
|
74
|
+
* `defs` is per UNIT rather than per function because a unit's `types` block is
|
|
75
|
+
* shared by every one of its signatures — `music.search` and `music.getTrack` both
|
|
76
|
+
* resolve `Track` against the same block, and the two tiers deliberately do not
|
|
77
|
+
* share an id space, let alone a type namespace. */
|
|
78
|
+
export interface UnitValidators {
|
|
79
|
+
defs: Record<string, Schema>;
|
|
80
|
+
/** By function name. `null` is EXPLICIT and means "this function is real and
|
|
81
|
+
* callable and we hold no shape for it" — the 20 whose declared argument is a
|
|
82
|
+
* bare destructuring pattern. It is not the same as an absent key, and the
|
|
83
|
+
* difference is what makes failing closed on an absent one safe. */
|
|
84
|
+
functions: Record<string, ParamSchema[] | null>;
|
|
85
|
+
}
|
|
86
|
+
export interface ValidatorTable {
|
|
87
|
+
/** The manifest version these were generated from. Reported in the refusal for an
|
|
88
|
+
* unknown path, because "your package predates that function" is the answer
|
|
89
|
+
* roughly every time. */
|
|
90
|
+
version: string;
|
|
91
|
+
/** Keyed by the unit's NAMESPACE — `music`, `providers.soundcloud` — which is the
|
|
92
|
+
* call path with `bowmark.` and the function name removed. */
|
|
93
|
+
units: Record<string, UnitValidators>;
|
|
94
|
+
}
|
|
95
|
+
/** What is wrong and WHERE. Same shape as `guard.ts`'s, so the two guards produce
|
|
96
|
+
* one error format and a caller never has to tell them apart. */
|
|
97
|
+
export interface ShapeProblem {
|
|
98
|
+
/** Dotted/bracketed path from the argument root. Empty at the root. */
|
|
99
|
+
path: string;
|
|
100
|
+
reason: string;
|
|
101
|
+
}
|
|
102
|
+
/** What the table says about one call path.
|
|
103
|
+
*
|
|
104
|
+
* The two negative answers are SEPARATE because they are different facts, and
|
|
105
|
+
* collapsing them would have made this package refuse the largest part of the
|
|
106
|
+
* library. See `assertArgShape` in `guard.ts`. */
|
|
107
|
+
export type Lookup = {
|
|
108
|
+
kind: "checked";
|
|
109
|
+
params: ParamSchema[];
|
|
110
|
+
}
|
|
111
|
+
/** The function exists and declares no readable argument shape. */
|
|
112
|
+
| {
|
|
113
|
+
kind: "unchecked";
|
|
114
|
+
}
|
|
115
|
+
/** The unit is here and declares no such function. The table IS authoritative
|
|
116
|
+
* about a unit it carries, so this is a typo or a stale install. */
|
|
117
|
+
| {
|
|
118
|
+
kind: "unknown-function";
|
|
119
|
+
}
|
|
120
|
+
/** No such unit. The table is NOT authoritative about this — a family MEMBER
|
|
121
|
+
* (`providers.gymshark`) is deliberately absent from every manifest, so an
|
|
122
|
+
* unknown unit is the normal case for most of the library rather than an error. */
|
|
123
|
+
| {
|
|
124
|
+
kind: "unknown-unit";
|
|
125
|
+
};
|
|
126
|
+
export declare function lookupParams(table: ValidatorTable, path: readonly string[]): Lookup;
|
|
127
|
+
/** Check an argument list against a function's declared parameters.
|
|
128
|
+
*
|
|
129
|
+
* Returns the FIRST problem, or null. `args` is the caller's array verbatim; a
|
|
130
|
+
* trailing `undefined` is treated as absent, because that is what
|
|
131
|
+
* `f(a, undefined)` means to a caller passing an optional through. */
|
|
132
|
+
export declare function argsProblem(params: readonly ParamSchema[], args: readonly unknown[], defs: Record<string, Schema>): ShapeProblem | null;
|