@thenavidm/slipway 0.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.
- package/CHANGELOG.md +21 -0
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +757 -0
- package/SECURITY.md +33 -0
- package/SKILL.md +136 -0
- package/dist/app.d.ts +210 -0
- package/dist/app.js +364 -0
- package/dist/bin.d.ts +14 -0
- package/dist/bin.js +178 -0
- package/dist/check.d.ts +52 -0
- package/dist/check.js +313 -0
- package/dist/cli/completion.d.ts +5 -0
- package/dist/cli/completion.js +72 -0
- package/dist/cli/context.d.ts +97 -0
- package/dist/cli/context.js +94 -0
- package/dist/cli/data.d.ts +17 -0
- package/dist/cli/data.js +120 -0
- package/dist/cli/flags.d.ts +35 -0
- package/dist/cli/flags.js +208 -0
- package/dist/cli/help.d.ts +15 -0
- package/dist/cli/help.js +213 -0
- package/dist/cli/install.d.ts +10 -0
- package/dist/cli/install.js +65 -0
- package/dist/cli/output.d.ts +22 -0
- package/dist/cli/output.js +159 -0
- package/dist/cli/run.d.ts +10 -0
- package/dist/cli/run.js +343 -0
- package/dist/confirm.d.ts +90 -0
- package/dist/confirm.js +166 -0
- package/dist/data.d.ts +109 -0
- package/dist/data.js +324 -0
- package/dist/docs.d.ts +13 -0
- package/dist/docs.js +66 -0
- package/dist/doctor.d.ts +12 -0
- package/dist/doctor.js +100 -0
- package/dist/entry.d.ts +11 -0
- package/dist/entry.js +34 -0
- package/dist/errors.d.ts +96 -0
- package/dist/errors.js +153 -0
- package/dist/guard.d.ts +46 -0
- package/dist/guard.js +89 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +16 -0
- package/dist/install.d.ts +98 -0
- package/dist/install.js +325 -0
- package/dist/jobs.d.ts +143 -0
- package/dist/jobs.js +247 -0
- package/dist/openapi.d.ts +127 -0
- package/dist/openapi.js +549 -0
- package/dist/pages.d.ts +22 -0
- package/dist/pages.js +61 -0
- package/dist/policy.d.ts +66 -0
- package/dist/policy.js +74 -0
- package/dist/redact.d.ts +18 -0
- package/dist/redact.js +60 -0
- package/dist/result.d.ts +44 -0
- package/dist/result.js +74 -0
- package/dist/rpc.d.ts +69 -0
- package/dist/rpc.js +120 -0
- package/dist/schema.d.ts +99 -0
- package/dist/schema.js +192 -0
- package/dist/search.d.ts +18 -0
- package/dist/search.js +119 -0
- package/dist/serve.d.ts +30 -0
- package/dist/serve.js +149 -0
- package/dist/server.d.ts +20 -0
- package/dist/server.js +244 -0
- package/dist/sync.d.ts +35 -0
- package/dist/sync.js +119 -0
- package/dist/testing.d.ts +35 -0
- package/dist/testing.js +41 -0
- package/dist/tool.d.ts +194 -0
- package/dist/tool.js +151 -0
- package/dist/util.d.ts +12 -0
- package/dist/util.js +35 -0
- package/package.json +89 -0
package/dist/jobs.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Work that takes longer than a client waits for one call.
|
|
3
|
+
*
|
|
4
|
+
* Clients stop waiting on a tool after about a minute, and some much sooner,
|
|
5
|
+
* so a render, an export or a long sync cannot simply run inside the call.
|
|
6
|
+
* A job tool starts the work and waits a bounded time. A job that finishes in
|
|
7
|
+
* time comes back with its result, as any call would. One that does not comes
|
|
8
|
+
* back as a job the caller checks with a generated `<name>_status` tool, so a
|
|
9
|
+
* model never polls blind and a script never guesses at a status endpoint.
|
|
10
|
+
*
|
|
11
|
+
* Two kinds:
|
|
12
|
+
* - the service runs the job and has its own status endpoint (`id`, `status`
|
|
13
|
+
* and `done` say how to read it), or
|
|
14
|
+
* - the handler itself is slow (`background: true`), and Slipway runs it in
|
|
15
|
+
* this process and keeps its result for an hour.
|
|
16
|
+
*/
|
|
17
|
+
import { randomBytes } from "node:crypto";
|
|
18
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
19
|
+
import { ApiError, CanceledError, NotFoundError, RateLimitError, toSlipwayError } from "./errors.js";
|
|
20
|
+
/** A client stops waiting on a call after about a minute, so no call waits longer than this. */
|
|
21
|
+
export const MAX_WAIT_SECONDS = 55;
|
|
22
|
+
export const DEFAULT_WAIT_SECONDS = 25;
|
|
23
|
+
const DEFAULT_POLL_MS = 2_000;
|
|
24
|
+
const MIN_POLL_MS = 250;
|
|
25
|
+
export function isBackground(job) {
|
|
26
|
+
return job.background === true;
|
|
27
|
+
}
|
|
28
|
+
export function waitSecondsFor(job) {
|
|
29
|
+
const wanted = job.waitSeconds ?? DEFAULT_WAIT_SECONDS;
|
|
30
|
+
return Math.max(0, Math.min(MAX_WAIT_SECONDS, Math.floor(wanted)));
|
|
31
|
+
}
|
|
32
|
+
export function pollMsFor(job) {
|
|
33
|
+
return Math.max(MIN_POLL_MS, job.pollMs ?? DEFAULT_POLL_MS);
|
|
34
|
+
}
|
|
35
|
+
export function readJobId(job, started) {
|
|
36
|
+
let value;
|
|
37
|
+
if (typeof job.id === "function")
|
|
38
|
+
value = job.id(started);
|
|
39
|
+
else {
|
|
40
|
+
value = started;
|
|
41
|
+
for (const part of job.id.split(".").filter(Boolean)) {
|
|
42
|
+
value = value !== null && typeof value === "object" ? value[part] : undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return typeof value === "string" && value ? value : typeof value === "number" ? String(value) : undefined;
|
|
46
|
+
}
|
|
47
|
+
/** The longest delay a Node timer takes. Anything longer fires after 1 ms instead. */
|
|
48
|
+
const MAX_TIMER_MS = 2_147_483_647;
|
|
49
|
+
/**
|
|
50
|
+
* Wait until `finished` settles or `ms` passes, whichever is first. Infinity
|
|
51
|
+
* waits for as long as it takes. Resolves true when it finished. A canceled
|
|
52
|
+
* call stops waiting at once.
|
|
53
|
+
*/
|
|
54
|
+
export async function waitFor(finished, ms, signal) {
|
|
55
|
+
let settled = false;
|
|
56
|
+
const done = finished.then(() => void (settled = true), () => void (settled = true));
|
|
57
|
+
if (ms <= 0)
|
|
58
|
+
return settled;
|
|
59
|
+
let timer;
|
|
60
|
+
let onAbort;
|
|
61
|
+
const stop = new Promise((resolve) => {
|
|
62
|
+
if (Number.isFinite(ms))
|
|
63
|
+
timer = setTimeout(resolve, Math.min(ms, MAX_TIMER_MS));
|
|
64
|
+
if (signal) {
|
|
65
|
+
onAbort = () => resolve();
|
|
66
|
+
if (signal.aborted)
|
|
67
|
+
resolve();
|
|
68
|
+
else
|
|
69
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
try {
|
|
73
|
+
await Promise.race([done, stop]);
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
if (signal && onAbort)
|
|
78
|
+
signal.removeEventListener("abort", onAbort);
|
|
79
|
+
}
|
|
80
|
+
if (!settled && signal?.aborted)
|
|
81
|
+
throw new CanceledError("Stopped waiting for the job.");
|
|
82
|
+
return settled;
|
|
83
|
+
}
|
|
84
|
+
/** Sleep between status checks, cut short by a canceled call. */
|
|
85
|
+
export async function pause(ms, signal) {
|
|
86
|
+
try {
|
|
87
|
+
await sleep(Math.min(ms, MAX_TIMER_MS), undefined, signal ? { signal } : undefined);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
throw new CanceledError("Stopped waiting for the job.");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** A failed service job, reported with the status that says why. */
|
|
94
|
+
export function jobFailed(tool, id, status) {
|
|
95
|
+
return new ApiError(`${tool} job ${id} failed.`, { details: status });
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Background jobs of one process.
|
|
99
|
+
*
|
|
100
|
+
* Bounded both ways: at most 100 run at once, and a finished job is kept for
|
|
101
|
+
* an hour, so a server that runs for weeks never grows without limit. Ids are
|
|
102
|
+
* random, so one caller cannot read another's job by counting.
|
|
103
|
+
*/
|
|
104
|
+
export class JobRegistry {
|
|
105
|
+
jobs = new Map();
|
|
106
|
+
static MAX_RUNNING = 100;
|
|
107
|
+
static KEEP_MS = 60 * 60_000;
|
|
108
|
+
static MAX_KEPT = 500;
|
|
109
|
+
start(tool, work) {
|
|
110
|
+
this.prune();
|
|
111
|
+
const running = [...this.jobs.values()].filter((entry) => entry.state === "running").length;
|
|
112
|
+
if (running >= JobRegistry.MAX_RUNNING) {
|
|
113
|
+
throw new RateLimitError(`${running} background jobs are already running. Wait for some to finish.`);
|
|
114
|
+
}
|
|
115
|
+
const entry = { id: `job_${randomBytes(12).toString("hex")}`, tool, startedAt: Date.now(), state: "running", finished: Promise.resolve() };
|
|
116
|
+
const report = (update) => {
|
|
117
|
+
entry.progress = update;
|
|
118
|
+
entry.listener?.(update);
|
|
119
|
+
};
|
|
120
|
+
entry.finished = (async () => {
|
|
121
|
+
try {
|
|
122
|
+
entry.result = await work(report);
|
|
123
|
+
entry.state = "done";
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
entry.error = toSlipwayError(error);
|
|
127
|
+
entry.state = "failed";
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
entry.finishedAt = Date.now();
|
|
131
|
+
entry.listener = undefined;
|
|
132
|
+
}
|
|
133
|
+
})();
|
|
134
|
+
this.jobs.set(entry.id, entry);
|
|
135
|
+
return entry;
|
|
136
|
+
}
|
|
137
|
+
get(id, tool) {
|
|
138
|
+
this.prune();
|
|
139
|
+
const entry = this.jobs.get(id);
|
|
140
|
+
if (!entry || entry.tool !== tool) {
|
|
141
|
+
throw new NotFoundError(`No ${tool} job ${id} in this server.`, {
|
|
142
|
+
hint: "Background jobs live in the server that started them, for an hour after they finish. Start it again if the server restarted.",
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return entry;
|
|
146
|
+
}
|
|
147
|
+
prune() {
|
|
148
|
+
const now = Date.now();
|
|
149
|
+
for (const [id, entry] of this.jobs) {
|
|
150
|
+
if (entry.finishedAt !== undefined && now - entry.finishedAt > JobRegistry.KEEP_MS)
|
|
151
|
+
this.jobs.delete(id);
|
|
152
|
+
}
|
|
153
|
+
// Oldest finished first, when there are too many to keep.
|
|
154
|
+
if (this.jobs.size > JobRegistry.MAX_KEPT) {
|
|
155
|
+
for (const [id, entry] of this.jobs) {
|
|
156
|
+
if (this.jobs.size <= JobRegistry.MAX_KEPT)
|
|
157
|
+
break;
|
|
158
|
+
if (entry.state !== "running")
|
|
159
|
+
this.jobs.delete(id);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/** A background job as its caller sees it. Throws the job's own error once it failed. */
|
|
165
|
+
export function backgroundResult(entry, check) {
|
|
166
|
+
if (entry.state === "failed")
|
|
167
|
+
throw entry.error;
|
|
168
|
+
const done = entry.state === "done";
|
|
169
|
+
return {
|
|
170
|
+
job_id: entry.id,
|
|
171
|
+
tool: entry.tool,
|
|
172
|
+
done,
|
|
173
|
+
...(done ? { result: entry.result } : {}),
|
|
174
|
+
...(entry.progress ? { progress: entry.progress } : {}),
|
|
175
|
+
started_at: new Date(entry.startedAt).toISOString(),
|
|
176
|
+
...(entry.finishedAt !== undefined ? { finished_at: new Date(entry.finishedAt).toISOString() } : {}),
|
|
177
|
+
...(done ? {} : { check }),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/** Start or check a job, and wait for it as long as this call may. */
|
|
181
|
+
export async function runJob(call) {
|
|
182
|
+
if (isBackground(call.job))
|
|
183
|
+
return runBackground(call);
|
|
184
|
+
return runServiceJob(call, call.job);
|
|
185
|
+
}
|
|
186
|
+
async function runBackground(call) {
|
|
187
|
+
let entry;
|
|
188
|
+
if (call.start) {
|
|
189
|
+
const start = call.start;
|
|
190
|
+
entry = call.registry.start(call.jobTool, (report) => {
|
|
191
|
+
// The job outlives the call that started it, so it gets its own signal.
|
|
192
|
+
const signal = call.timeoutMs ? AbortSignal.timeout(call.timeoutMs) : new AbortController().signal;
|
|
193
|
+
return call.untilAborted(Promise.resolve(start(call.context(signal, report))), signal);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
entry = call.registry.get(call.jobId, call.jobTool);
|
|
198
|
+
}
|
|
199
|
+
if (entry.progress)
|
|
200
|
+
call.progress(entry.progress);
|
|
201
|
+
entry.listener = call.progress;
|
|
202
|
+
try {
|
|
203
|
+
await waitFor(entry.finished, call.waitMs, call.waitSignal);
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
if (entry.listener === call.progress)
|
|
207
|
+
entry.listener = undefined;
|
|
208
|
+
}
|
|
209
|
+
return backgroundResult(entry, call.check(entry.id));
|
|
210
|
+
}
|
|
211
|
+
async function runServiceJob(call, job) {
|
|
212
|
+
const read = (id) => {
|
|
213
|
+
const signal = call.requestSignal();
|
|
214
|
+
return call.untilAborted(Promise.resolve(job.status(id, call.context(signal, call.progress))), signal);
|
|
215
|
+
};
|
|
216
|
+
let id;
|
|
217
|
+
let status;
|
|
218
|
+
if (call.start) {
|
|
219
|
+
const signal = call.requestSignal();
|
|
220
|
+
status = await call.untilAborted(Promise.resolve(call.start(call.context(signal, call.progress))), signal);
|
|
221
|
+
const found = readJobId(job, status);
|
|
222
|
+
if (!found) {
|
|
223
|
+
throw new ApiError(`${call.jobTool} started a job but returned no job id${typeof job.id === "string" ? ` at '${job.id}'` : ""}.`, { details: status });
|
|
224
|
+
}
|
|
225
|
+
id = found;
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
id = call.jobId;
|
|
229
|
+
status = await read(id);
|
|
230
|
+
}
|
|
231
|
+
const deadline = call.waitMs === Number.POSITIVE_INFINITY ? Number.POSITIVE_INFINITY : Date.now() + call.waitMs;
|
|
232
|
+
for (;;) {
|
|
233
|
+
const update = job.progress?.(status);
|
|
234
|
+
if (update)
|
|
235
|
+
call.progress(update);
|
|
236
|
+
if (job.done(status)) {
|
|
237
|
+
if (job.failed?.(status))
|
|
238
|
+
throw jobFailed(call.jobTool, id, status);
|
|
239
|
+
return { job_id: id, tool: call.jobTool, done: true, status };
|
|
240
|
+
}
|
|
241
|
+
const remaining = deadline - Date.now();
|
|
242
|
+
if (remaining <= 0)
|
|
243
|
+
return { job_id: id, tool: call.jobTool, done: false, status, check: call.check(id) };
|
|
244
|
+
await pause(Math.min(pollMsFor(job), remaining), call.waitSignal);
|
|
245
|
+
status = await read(id);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tools from an OpenAPI document.
|
|
3
|
+
*
|
|
4
|
+
* An API that publishes OpenAPI already says what every operation takes. This
|
|
5
|
+
* turns each operation into a tool with that input, the risk its HTTP method
|
|
6
|
+
* implies, and its tags as toolsets, so a large API becomes a server and a CLI
|
|
7
|
+
* without a hand-written tool per endpoint. The tools join the same list as
|
|
8
|
+
* hand-written ones and go through the same guard.
|
|
9
|
+
*
|
|
10
|
+
* A generated tool is only as trustworthy as the document it came from, so a
|
|
11
|
+
* document can be pinned by hash: a changed document refuses to build until
|
|
12
|
+
* someone has looked at what changed and updated the pin.
|
|
13
|
+
*/
|
|
14
|
+
import { type JsonSchema } from "./schema.js";
|
|
15
|
+
import { type Risk, type Tool, type ToolContext } from "./tool.js";
|
|
16
|
+
export type OperationParameter = {
|
|
17
|
+
/** The parameter's name in the API. */
|
|
18
|
+
name: string;
|
|
19
|
+
in: "path" | "query" | "header";
|
|
20
|
+
required: boolean;
|
|
21
|
+
/** The input property that carries it. */
|
|
22
|
+
property: string;
|
|
23
|
+
/** How the value is written, as the document says: form and deepObject in a query, simple in a path or header. */
|
|
24
|
+
style: string;
|
|
25
|
+
explode: boolean;
|
|
26
|
+
};
|
|
27
|
+
export type Operation = {
|
|
28
|
+
operationId: string;
|
|
29
|
+
/** Upper case: GET, POST. */
|
|
30
|
+
method: string;
|
|
31
|
+
/** The path template: /pets/{petId}. */
|
|
32
|
+
path: string;
|
|
33
|
+
summary?: string;
|
|
34
|
+
description?: string;
|
|
35
|
+
tags: string[];
|
|
36
|
+
deprecated: boolean;
|
|
37
|
+
parameters: OperationParameter[];
|
|
38
|
+
/**
|
|
39
|
+
* The request body, and how the input carries it: spread into the input's
|
|
40
|
+
* own properties, or as one `body` property. `fields` maps each input
|
|
41
|
+
* property back to the body field it carries, which differs where a field's
|
|
42
|
+
* own name was taken.
|
|
43
|
+
*/
|
|
44
|
+
body?: {
|
|
45
|
+
contentType: string;
|
|
46
|
+
required: boolean;
|
|
47
|
+
spread: boolean;
|
|
48
|
+
fields: Record<string, string>;
|
|
49
|
+
};
|
|
50
|
+
/** The tool's input, as JSON Schema 2020-12. */
|
|
51
|
+
input: JsonSchema;
|
|
52
|
+
/** The JSON schema of a successful response, when the document gives one. */
|
|
53
|
+
output?: JsonSchema;
|
|
54
|
+
};
|
|
55
|
+
/** What one call sends, split the way HTTP carries it. */
|
|
56
|
+
export type OperationInput = {
|
|
57
|
+
path: Record<string, unknown>;
|
|
58
|
+
query: Record<string, unknown>;
|
|
59
|
+
headers: Record<string, string>;
|
|
60
|
+
body?: unknown;
|
|
61
|
+
};
|
|
62
|
+
/** Runs one operation. `httpExecutor` covers JSON and form APIs; write your own for anything else. */
|
|
63
|
+
export type Executor<Ctx> = (operation: Operation, input: OperationInput, ctx: ToolContext<Ctx>) => unknown | Promise<unknown>;
|
|
64
|
+
export type FromOpenAPIOptions<Ctx> = {
|
|
65
|
+
execute: Executor<Ctx>;
|
|
66
|
+
/** Only these operations, by operationId, or the ones a test accepts. */
|
|
67
|
+
include?: readonly string[] | ((operation: Operation) => boolean);
|
|
68
|
+
/** Tool names by operationId, where the generated one reads badly. */
|
|
69
|
+
names?: Record<string, string>;
|
|
70
|
+
/** Risk by operationId, where the method misleads: a POST that only searches is a read. */
|
|
71
|
+
risk?: Record<string, Risk>;
|
|
72
|
+
/** Refuse to build from a document whose hash differs. `slipway openapi <file>` prints the hash to pin. */
|
|
73
|
+
pin?: {
|
|
74
|
+
sha256: string;
|
|
75
|
+
};
|
|
76
|
+
/** Declare each operation's documented JSON response as the tool's output. Off by default, since APIs often return more than they document. */
|
|
77
|
+
typedOutput?: boolean;
|
|
78
|
+
/** Put before every tool name, to keep two APIs in one app apart. */
|
|
79
|
+
prefix?: string;
|
|
80
|
+
};
|
|
81
|
+
export type SkippedOperation = {
|
|
82
|
+
method: string;
|
|
83
|
+
path: string;
|
|
84
|
+
operationId?: string;
|
|
85
|
+
reason: string;
|
|
86
|
+
};
|
|
87
|
+
/** The hash to pin a document by: the same for the same content, however its keys were ordered. */
|
|
88
|
+
export declare function openapiHash(document: unknown): string;
|
|
89
|
+
/**
|
|
90
|
+
* An OpenAPI schema as JSON Schema 2020-12: `nullable` becomes a null type,
|
|
91
|
+
* 3.0's boolean exclusive bounds become numbers, `example` becomes
|
|
92
|
+
* `examples`, and keys that only mean something to OpenAPI are dropped.
|
|
93
|
+
* Properties the server sets itself (`readOnly`) leave a request's input.
|
|
94
|
+
*/
|
|
95
|
+
export declare function toJsonSchema(node: unknown, forInput: boolean): unknown;
|
|
96
|
+
/**
|
|
97
|
+
* operationId as a tool name: listPets and pets.list become list_pets and
|
|
98
|
+
* pets_list. A name past the 64 characters clients allow keeps its start and
|
|
99
|
+
* ends in a short hash of the id, so two long ids that begin alike stay
|
|
100
|
+
* two names, and the same id always gets the same one.
|
|
101
|
+
*/
|
|
102
|
+
export declare function toolName(id: string): string;
|
|
103
|
+
/** Every operation in a document, as tools would see it, and the ones that cannot become tools, with why. */
|
|
104
|
+
export declare function readOperations(document: unknown, options?: {
|
|
105
|
+
typedOutput?: boolean;
|
|
106
|
+
}): {
|
|
107
|
+
operations: Operation[];
|
|
108
|
+
skipped: SkippedOperation[];
|
|
109
|
+
};
|
|
110
|
+
/** Split a tool's arguments back into what goes in the path, the query, the headers and the body. */
|
|
111
|
+
export declare function splitInput(operation: Operation, args: Record<string, unknown>): OperationInput;
|
|
112
|
+
/** Build a tool for every operation in an OpenAPI 3 document. Throws on a pinned document that changed. */
|
|
113
|
+
export declare function fromOpenAPI<Ctx>(document: unknown, options: FromOpenAPIOptions<Ctx>): Tool<Ctx>[];
|
|
114
|
+
/** A form body, with nested objects and lists in the bracket style form APIs read: `metadata[plan]=pro`, `items[0][price]=...`. */
|
|
115
|
+
export declare function formEncode(body: unknown): string;
|
|
116
|
+
export type HttpExecutorOptions<Ctx> = {
|
|
117
|
+
/** Where the API lives: https://api.example.com/v1. Plain http only reaches this machine. */
|
|
118
|
+
baseUrl: string | ((ctx: ToolContext<Ctx>) => string);
|
|
119
|
+
/** Headers every call sends, such as the credentials. Register those as secrets on the app, so they are masked. */
|
|
120
|
+
headers?: (ctx: ToolContext<Ctx>) => Record<string, string | undefined> | Promise<Record<string, string | undefined>>;
|
|
121
|
+
fetch?: typeof fetch;
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Calls the API over HTTP. Errors come back as Slipway errors with the API's
|
|
125
|
+
* own message and status, so a model can act on a 404 or a 429 like any other.
|
|
126
|
+
*/
|
|
127
|
+
export declare function httpExecutor<Ctx>(options: HttpExecutorOptions<Ctx>): Executor<Ctx>;
|