@oberik/sdk 0.5.0 → 0.7.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/dist/cjs/index.js +268 -15
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.d.ts +170 -11
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +266 -15
- package/dist/esm/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cjs/index.js
CHANGED
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
*/
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.OberikProject = exports.DEFAULT_CONTROL_PLANE_URL = exports.AgentFramework = exports.AgentStreamError = exports.AgentApiError = exports.AgentCancelledError = exports.DEFAULT_BASE_URL = void 0;
|
|
37
|
+
exports.coerceUiArgs = coerceUiArgs;
|
|
38
|
+
exports.uiSchemaViolations = uiSchemaViolations;
|
|
37
39
|
exports.createProjectClient = createProjectClient;
|
|
38
40
|
exports.createClient = createClient;
|
|
39
41
|
// ============================================================================
|
|
@@ -68,6 +70,103 @@ exports.AgentCancelledError = AgentCancelledError;
|
|
|
68
70
|
// Errors
|
|
69
71
|
// ============================================================================
|
|
70
72
|
/** Does this look like a page rather than an answer? */
|
|
73
|
+
// ---- UI component arguments ----
|
|
74
|
+
//
|
|
75
|
+
// A UI component declares `parameters` exactly as a client tool does, and `render` draws
|
|
76
|
+
// into the customer's own product. Two schema violations of the *same* declared schema
|
|
77
|
+
// reached `render` unchecked: `bars` as a JSON string rather than an array, and array
|
|
78
|
+
// items missing a required `percent` while carrying two fields the schema never mentioned.
|
|
79
|
+
//
|
|
80
|
+
// For a client *tool* you can validate in the handler and answer the model. A UI component
|
|
81
|
+
// paints, so the check has to happen before it draws.
|
|
82
|
+
/** JSON Schema `properties`, whichever of the two shapes the app declared. */
|
|
83
|
+
function uiProps(schema) {
|
|
84
|
+
const s = schema;
|
|
85
|
+
return (s?.properties ?? s?.parameters?.properties ?? {});
|
|
86
|
+
}
|
|
87
|
+
function parseIfJson(value, want) {
|
|
88
|
+
if (typeof value !== "string")
|
|
89
|
+
return value;
|
|
90
|
+
const text = value.trim();
|
|
91
|
+
if (!text)
|
|
92
|
+
return value;
|
|
93
|
+
const opens = want === "array" ? "[" : "{";
|
|
94
|
+
if (!text.startsWith(opens))
|
|
95
|
+
return value;
|
|
96
|
+
try {
|
|
97
|
+
return JSON.parse(text);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Passed through untouched. A cosmetic mismatch must not become a thrown render.
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Make the arguments match the declared types, as far as that is safe.
|
|
106
|
+
*
|
|
107
|
+
* Narrow on purpose — this is the app's own schema and the values go straight back to it.
|
|
108
|
+
* A container declared as `array`/`object` that arrived as its JSON text is parsed, and
|
|
109
|
+
* that parsing recurses into array items, which is where the server's own coercion stops.
|
|
110
|
+
*/
|
|
111
|
+
function coerceUiArgs(schema, args) {
|
|
112
|
+
const props = uiProps(schema);
|
|
113
|
+
if (!Object.keys(props).length)
|
|
114
|
+
return args;
|
|
115
|
+
const out = { ...args };
|
|
116
|
+
for (const [key, spec] of Object.entries(props)) {
|
|
117
|
+
if (!(key in out))
|
|
118
|
+
continue;
|
|
119
|
+
const want = String(spec?.type ?? "");
|
|
120
|
+
if (want === "array" || want === "object")
|
|
121
|
+
out[key] = parseIfJson(out[key], want);
|
|
122
|
+
// Items of an array of objects: the same JSON-text-instead-of-a-container mistake,
|
|
123
|
+
// one level down, which is the level a chart's data actually lives at.
|
|
124
|
+
if (want === "array" && Array.isArray(out[key]) && spec?.items?.type === "object") {
|
|
125
|
+
out[key] = out[key].map((item) => parseIfJson(item, "object"));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
/** How `args` fails `schema`, as sentences — empty when it does not. */
|
|
131
|
+
function uiSchemaViolations(schema, args) {
|
|
132
|
+
const props = uiProps(schema);
|
|
133
|
+
if (!Object.keys(props).length)
|
|
134
|
+
return [];
|
|
135
|
+
const required = (schema?.required ?? []);
|
|
136
|
+
const out = [];
|
|
137
|
+
for (const key of required) {
|
|
138
|
+
if (args[key] === undefined || args[key] === null)
|
|
139
|
+
out.push(`missing required "${key}"`);
|
|
140
|
+
}
|
|
141
|
+
for (const [key, spec] of Object.entries(props)) {
|
|
142
|
+
const value = args[key];
|
|
143
|
+
if (value === undefined || value === null)
|
|
144
|
+
continue;
|
|
145
|
+
const want = String(spec?.type ?? "");
|
|
146
|
+
if (want === "array" && !Array.isArray(value)) {
|
|
147
|
+
out.push(`"${key}" should be an array, got ${typeof value}`);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (want === "array" && Array.isArray(value) && spec?.items) {
|
|
151
|
+
const itemRequired = (spec.items.required ?? []);
|
|
152
|
+
value.forEach((item, i) => {
|
|
153
|
+
if (typeof item !== "object" || item === null)
|
|
154
|
+
return;
|
|
155
|
+
for (const f of itemRequired) {
|
|
156
|
+
if (item[f] === undefined)
|
|
157
|
+
out.push(`"${key}[${i}]" is missing required "${f}"`);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
if (want === "number" && typeof value !== "number") {
|
|
162
|
+
out.push(`"${key}" should be a number, got ${typeof value}`);
|
|
163
|
+
}
|
|
164
|
+
if (want === "string" && typeof value !== "string") {
|
|
165
|
+
out.push(`"${key}" should be a string, got ${typeof value}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
71
170
|
function looksLikeHtml(text) {
|
|
72
171
|
return /^\s*(<!doctype html|<html|<head|<body)/i.test(text) || /<\/html>\s*$/i.test(text);
|
|
73
172
|
}
|
|
@@ -88,9 +187,11 @@ function looksLikeHtml(text) {
|
|
|
88
187
|
function describeDetail(status, detail) {
|
|
89
188
|
if (typeof detail === "string") {
|
|
90
189
|
if (looksLikeHtml(detail)) {
|
|
190
|
+
// Report what arrived, not a guess about what produced it. The API answers with
|
|
191
|
+
// JSON, so an HTML body is worth saying plainly — but naming whatever returned it
|
|
192
|
+
// would be speculation about someone else's infrastructure.
|
|
91
193
|
const title = /<title[^>]*>([^<]{1,160})<\/title>/i.exec(detail)?.[1]?.trim();
|
|
92
|
-
|
|
93
|
-
return `HTTP ${status}${title ? `: ${title}` : ""}${where}`;
|
|
194
|
+
return `HTTP ${status}${title ? `: ${title}` : ""} (the response body was HTML, not JSON)`;
|
|
94
195
|
}
|
|
95
196
|
return detail.slice(0, 2000) || `HTTP ${status}`;
|
|
96
197
|
}
|
|
@@ -288,6 +389,37 @@ async function collectDecisions(pending, handler) {
|
|
|
288
389
|
* A handler that throws is treated as the user declining rather than as a failure:
|
|
289
390
|
* the alternative is leaving the turn paused forever on a question nobody can now
|
|
290
391
|
* answer, and "they'd rather talk about it" is both true and recoverable. */
|
|
392
|
+
/** One signal that aborts when any of its inputs does.
|
|
393
|
+
*
|
|
394
|
+
* `AbortSignal.any` exists on Node 20+ and modern browsers; the manual path is for
|
|
395
|
+
* anything older, because a client that cannot compose signals should lose the timeout,
|
|
396
|
+
* not the request. */
|
|
397
|
+
function combineSignals(...signals) {
|
|
398
|
+
const live = signals.filter(Boolean);
|
|
399
|
+
if (live.length <= 1)
|
|
400
|
+
return live[0];
|
|
401
|
+
const anyOf = AbortSignal.any;
|
|
402
|
+
if (typeof anyOf === "function")
|
|
403
|
+
return anyOf(live);
|
|
404
|
+
const ac = new AbortController();
|
|
405
|
+
for (const s of live) {
|
|
406
|
+
if (s.aborted) {
|
|
407
|
+
ac.abort(s.reason);
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
s.addEventListener("abort", () => ac.abort(s.reason), { once: true });
|
|
411
|
+
}
|
|
412
|
+
return ac.signal;
|
|
413
|
+
}
|
|
414
|
+
/** Anything `UploadInput` allows, as a Blob for `FormData`. */
|
|
415
|
+
function asBlob(input) {
|
|
416
|
+
if (typeof Blob !== "undefined" && input instanceof Blob)
|
|
417
|
+
return input;
|
|
418
|
+
const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
419
|
+
// Copied into a fresh buffer: a Node Buffer is a view onto a pooled allocation, and
|
|
420
|
+
// handing that straight to Blob can carry bytes belonging to something else.
|
|
421
|
+
return new Blob([bytes.slice()]);
|
|
422
|
+
}
|
|
291
423
|
async function collectAnswers(pending, handler) {
|
|
292
424
|
const out = [];
|
|
293
425
|
for (const batch of pending) {
|
|
@@ -899,8 +1031,25 @@ class AgentFramework {
|
|
|
899
1031
|
const component = registry.get(r.name);
|
|
900
1032
|
if (!component)
|
|
901
1033
|
continue;
|
|
1034
|
+
// Coerced and checked against the schema the app declared, because `render` paints
|
|
1035
|
+
// straight into somebody's product and the frame handed to it is the one thing you
|
|
1036
|
+
// would expect to have been checked against the schema you just supplied.
|
|
1037
|
+
//
|
|
1038
|
+
// The server coerces too, but only the top-level properties, and it checks nothing
|
|
1039
|
+
// required — so a declared `bars: array of {label, percent}` arrived as a JSON
|
|
1040
|
+
// *string* in a browser and as `[{label, value, target}]` in Node, and both were
|
|
1041
|
+
// dispatched. The page drew "? 0.0%" four times.
|
|
1042
|
+
const args = coerceUiArgs(component.parameters, r.args ?? {});
|
|
1043
|
+
const problems = uiSchemaViolations(component.parameters, args);
|
|
1044
|
+
if (problems.length) {
|
|
1045
|
+
// Warn, then draw anyway. The agent has already told the user it showed them
|
|
1046
|
+
// something, so silently skipping leaves them looking for a chart that never
|
|
1047
|
+
// appears — worse than a chart with a gap in it. Loud enough to find in a console.
|
|
1048
|
+
console.warn(`[oberik] UI component "${r.name}" was called with arguments that do not match ` +
|
|
1049
|
+
`its declared schema: ${problems.join("; ")}. Rendering anyway.`);
|
|
1050
|
+
}
|
|
902
1051
|
try {
|
|
903
|
-
component.render(
|
|
1052
|
+
component.render(args);
|
|
904
1053
|
}
|
|
905
1054
|
catch (e) {
|
|
906
1055
|
console.warn(`[oberik] UI component "${r.name}" threw while rendering:`, e);
|
|
@@ -938,7 +1087,16 @@ class AgentFramework {
|
|
|
938
1087
|
headers["Content-Type"] = "application/json";
|
|
939
1088
|
body = JSON.stringify(init.body);
|
|
940
1089
|
}
|
|
941
|
-
|
|
1090
|
+
// A timeout the caller can actually set. Node's built-in fetch gives up waiting
|
|
1091
|
+
// for response headers after 300s with `UND_ERR_HEADERS_TIMEOUT`, and there was
|
|
1092
|
+
// no way to raise it short of hand-building an undici dispatcher — so a blocking
|
|
1093
|
+
// turn that ran a long sandbox job simply died as `TypeError: fetch failed` with
|
|
1094
|
+
// nothing naming this SDK. Composed with the caller's own signal rather than
|
|
1095
|
+
// replacing it, so `opts.signal` still cancels.
|
|
1096
|
+
const ms = this.opts.timeoutMs ?? 600_000;
|
|
1097
|
+
const timer = ms > 0 ? AbortSignal.timeout(ms) : undefined;
|
|
1098
|
+
const signal = combineSignals(init.signal, timer);
|
|
1099
|
+
return this._fetch(url.toString(), { method, headers, body, signal });
|
|
942
1100
|
};
|
|
943
1101
|
let res = await send();
|
|
944
1102
|
// Token expired mid-session: get a fresh one and replay the request once, so the
|
|
@@ -1040,8 +1198,15 @@ class AgentFramework {
|
|
|
1040
1198
|
},
|
|
1041
1199
|
/** Blocking chat that AUTO-EXECUTES registered client tools: it sends the
|
|
1042
1200
|
* message, and whenever the agent asks for client tools it runs their
|
|
1043
|
-
* handlers, submits the results, and repeats until the agent is done.
|
|
1044
|
-
|
|
1201
|
+
* handlers, submits the results, and repeats until the agent is done.
|
|
1202
|
+
*
|
|
1203
|
+
* Takes ONE argument, with the handlers inside it — unlike `chat.stream(req,
|
|
1204
|
+
* handlers)`, which takes two. Both forms are accepted here because the asymmetry
|
|
1205
|
+
* is a trap: passing handlers the way `stream` takes them was silently ignored, so
|
|
1206
|
+
* `onApproval` never fired and the turn came back with `approvals` set, which reads
|
|
1207
|
+
* exactly like approvals being broken. TypeScript catches it; the docs' own browser
|
|
1208
|
+
* examples are plain JS, where it does not. */
|
|
1209
|
+
run: (opts = {}, handlers) => this.runWithTools(handlers ? { ...opts, ...handlers } : opts),
|
|
1045
1210
|
/** Streaming chat. When client tools are registered/passed, tool calls are
|
|
1046
1211
|
* auto-executed and the stream resumes, so `done` only resolves when the
|
|
1047
1212
|
* agent finishes (never with `requires_action`). */
|
|
@@ -2148,6 +2313,20 @@ class OberikProject {
|
|
|
2148
2313
|
throw new Error("No fetch available; pass options.fetch");
|
|
2149
2314
|
this._fetch = raw.bind(globalThis);
|
|
2150
2315
|
}
|
|
2316
|
+
/**
|
|
2317
|
+
* Any Project API route, with this project's key — the escape hatch.
|
|
2318
|
+
*
|
|
2319
|
+
* The end-user client has always had `raw()`; this one had a private `request` and
|
|
2320
|
+
* nothing else, so the documented routes with no method here (the webhook secret,
|
|
2321
|
+
* webhook-tool rotation, `webhook-tools/deliveries`) meant hand-rolling `fetch` with
|
|
2322
|
+
* an `X-API-Key` header and re-deriving the base URL. A published SDK should not make
|
|
2323
|
+
* you leave it to use the API it wraps.
|
|
2324
|
+
*
|
|
2325
|
+
* `path` is relative to `/api/projects/<id>` — `raw("GET", "/webhook-secret")`.
|
|
2326
|
+
*/
|
|
2327
|
+
async raw(method, path, body, form) {
|
|
2328
|
+
return this.request(method, path, body, form);
|
|
2329
|
+
}
|
|
2151
2330
|
async request(method, path, body, form) {
|
|
2152
2331
|
const res = await this._fetch(`${this.baseUrl}/api/projects/${this.projectId}${path}`, {
|
|
2153
2332
|
method,
|
|
@@ -2160,7 +2339,9 @@ class OberikProject {
|
|
|
2160
2339
|
const text = await res.text();
|
|
2161
2340
|
const data = text ? safeJson(text) : null;
|
|
2162
2341
|
if (!res.ok) {
|
|
2163
|
-
|
|
2342
|
+
// `detail` on both planes now, so an integrator writes one accessor rather than
|
|
2343
|
+
// discovering that the control plane spelled it differently.
|
|
2344
|
+
throw new AgentApiError(res.status, (data && (data.detail || data.message)) || `HTTP ${res.status}`);
|
|
2164
2345
|
}
|
|
2165
2346
|
return data;
|
|
2166
2347
|
}
|
|
@@ -2185,11 +2366,38 @@ class OberikProject {
|
|
|
2185
2366
|
*/
|
|
2186
2367
|
forUser: (input) => async () => (await this.request("POST", "/token", input)).access_token,
|
|
2187
2368
|
};
|
|
2369
|
+
/**
|
|
2370
|
+
* The project itself — its name, description, and the settings that are properties of
|
|
2371
|
+
* the project rather than permissions on a token.
|
|
2372
|
+
*
|
|
2373
|
+
* These had no SDK path at all. `doneWebhookUrl` is documented in the capability table
|
|
2374
|
+
* but is NOT a capability, so the obvious `capabilities.set({ doneWebhookUrl })` stored
|
|
2375
|
+
* nothing; the only symptom was `turn.stopped` never firing. Renaming a project, or
|
|
2376
|
+
* turning citation markers off, meant dropping to raw HTTP through `raw()`.
|
|
2377
|
+
*/
|
|
2378
|
+
project = {
|
|
2379
|
+
get: () => this.request("GET", ""),
|
|
2380
|
+
/** Merges — send only what you want to change. */
|
|
2381
|
+
set: (fields) => this.request("PATCH", "", fields),
|
|
2382
|
+
/** Whether `[2]` stays in the visible answer. Its own route, not a PATCH field.
|
|
2383
|
+
* `stripped` leaves `claims[].start/end` as the only way to place a footnote. */
|
|
2384
|
+
citations: (markers) => this.request("PUT", "/citations", { markers }),
|
|
2385
|
+
};
|
|
2188
2386
|
/** The capability ceiling: the maximum any token minted here may hold. */
|
|
2189
2387
|
capabilities = {
|
|
2190
2388
|
get: () => this.request("GET", "/capabilities"),
|
|
2191
|
-
/**
|
|
2192
|
-
|
|
2389
|
+
/**
|
|
2390
|
+
* Merges — send only what you want to change.
|
|
2391
|
+
*
|
|
2392
|
+
* Read-only fields that `get()` returns are dropped rather than rejected, so
|
|
2393
|
+
* `set(await get())` — the obvious way to flip one flag — works. Anything else
|
|
2394
|
+
* unrecognised still errors: a capability nobody enforces must not be accepted in
|
|
2395
|
+
* silence, which is the failure this whole check exists for.
|
|
2396
|
+
*/
|
|
2397
|
+
set: (caps) => {
|
|
2398
|
+
const { supportedModalities: _readOnly, ...settable } = caps;
|
|
2399
|
+
return this.request("PATCH", "", { capabilities: settable });
|
|
2400
|
+
},
|
|
2193
2401
|
};
|
|
2194
2402
|
/** Documents owned by the project rather than by any one end-user: the corpus you
|
|
2195
2403
|
* curate and your users only read. */
|
|
@@ -2206,11 +2414,20 @@ class OberikProject {
|
|
|
2206
2414
|
* accepted nor needed — a recipe that worked by luck rather than by expression.
|
|
2207
2415
|
*/
|
|
2208
2416
|
documents = {
|
|
2209
|
-
list
|
|
2417
|
+
/** Same filters as the end-user client's `documents.list`, so the two agree. */
|
|
2418
|
+
list: (query = {}) => {
|
|
2419
|
+
const qs = new URLSearchParams(Object.entries(query).filter(([, v]) => v != null)).toString();
|
|
2420
|
+
return this.request("GET", `/documents${qs ? `?${qs}` : ""}`);
|
|
2421
|
+
},
|
|
2210
2422
|
get: (documentId) => this.request("GET", `/documents/${documentId}`),
|
|
2423
|
+
/** `file` may be a Blob/File, an ArrayBuffer or a Uint8Array — the same shapes the
|
|
2424
|
+
* end-user client takes. It was `Blob | File` only, so the quickstart's own
|
|
2425
|
+
* `await readFile("q3-report.pdf")` (a Buffer) failed deep inside undici as
|
|
2426
|
+
* `parameter 2 is not of type 'Blob'`, with nothing naming Oberik in the stack. */
|
|
2211
2427
|
upload: (file, opts = {}) => {
|
|
2212
2428
|
const form = new FormData();
|
|
2213
|
-
|
|
2429
|
+
const name = opts.filename ?? file.name ?? "upload.bin";
|
|
2430
|
+
form.append("file", asBlob(file), name);
|
|
2214
2431
|
form.append("tags", (opts.tags ?? []).join(","));
|
|
2215
2432
|
return this.request("POST", "/documents", undefined, form);
|
|
2216
2433
|
},
|
|
@@ -2298,9 +2515,19 @@ class OberikProject {
|
|
|
2298
2515
|
/** Procedures you publish as Agent Plugins, and what your end-users have added. */
|
|
2299
2516
|
skills = {
|
|
2300
2517
|
list: () => this.request("GET", "/skills"),
|
|
2301
|
-
|
|
2518
|
+
/**
|
|
2519
|
+
* `zip` takes the same shapes as every other upload here — a Blob/File, an
|
|
2520
|
+
* ArrayBuffer or a Uint8Array — and `filename` goes in the options object.
|
|
2521
|
+
*
|
|
2522
|
+
* It was `Blob | File` with a positional filename, alone among four upload methods.
|
|
2523
|
+
* `await readFile("skill.zip")` (a Buffer) died as `parameter 2 is not of type 'Blob'`
|
|
2524
|
+
* inside undici with nothing naming Oberik in the stack — the identical failure that
|
|
2525
|
+
* was fixed for `documents.upload` and left here — and passing `{ filename }` in the
|
|
2526
|
+
* second slot, as the siblings take it, silently named the file `[object Object]`.
|
|
2527
|
+
*/
|
|
2528
|
+
upload: (zip, opts = {}) => {
|
|
2302
2529
|
const form = new FormData();
|
|
2303
|
-
form.append("file", zip, filename ?? zip.name ?? "skill.zip");
|
|
2530
|
+
form.append("file", asBlob(zip), opts.filename ?? zip.name ?? "skill.zip");
|
|
2304
2531
|
return this.request("POST", "/skills", undefined, form);
|
|
2305
2532
|
},
|
|
2306
2533
|
delete: (pluginId) => this.request("DELETE", `/skills/${pluginId}`),
|
|
@@ -2320,9 +2547,16 @@ class OberikProject {
|
|
|
2320
2547
|
tasks = {
|
|
2321
2548
|
list: () => this.request("GET", "/tasks"),
|
|
2322
2549
|
};
|
|
2323
|
-
/**
|
|
2550
|
+
/**
|
|
2551
|
+
* What the agent has written down: remembered facts and wiki pages.
|
|
2552
|
+
*
|
|
2553
|
+
* `kind` defaults to `"wiki"` — the route's default, and the reason this looked like
|
|
2554
|
+
* it "returns pages only": it does, and there was no argument here to ask for the
|
|
2555
|
+
* memories. Same shape and same type as the end-user client's `knowledge.list`, so the
|
|
2556
|
+
* two do not disagree about what a stored item is.
|
|
2557
|
+
*/
|
|
2324
2558
|
wiki = {
|
|
2325
|
-
list: () => this.request("GET", "
|
|
2559
|
+
list: (opts = {}) => this.request("GET", `/wiki?kind=${opts.kind ?? "wiki"}`),
|
|
2326
2560
|
delete: (itemId) => this.request("DELETE", `/wiki/${itemId}`),
|
|
2327
2561
|
};
|
|
2328
2562
|
/** Live sandboxes, and what to do about one. */
|
|
@@ -2336,14 +2570,33 @@ class OberikProject {
|
|
|
2336
2570
|
};
|
|
2337
2571
|
/** Browser origins allowed to call the data plane with this project's tokens. */
|
|
2338
2572
|
origins = {
|
|
2573
|
+
/** What is allowed right now — the first thing you want when a browser request is
|
|
2574
|
+
* being refused, and previously unanswerable without the dashboard. */
|
|
2575
|
+
get: () => this.request("GET", "/origins"),
|
|
2339
2576
|
set: (origins) => this.request("PUT", "/origins", { origins }),
|
|
2340
2577
|
};
|
|
2341
2578
|
/** Tools the agent calls by URL. The signing secret comes back once, on create. */
|
|
2342
2579
|
webhookTools = {
|
|
2343
2580
|
list: () => this.request("GET", "/webhook-tools"),
|
|
2344
2581
|
create: (tool) => this.request("POST", "/webhook-tools", tool),
|
|
2582
|
+
/** Change a tool WITHOUT rotating its secret.
|
|
2583
|
+
*
|
|
2584
|
+
* There was no update at all, so moving a URL after a tunnel restart meant
|
|
2585
|
+
* delete-and-recreate — which issues a new signing secret, shown once, that your
|
|
2586
|
+
* handler then has to be redeployed with. A URL change is not a credential
|
|
2587
|
+
* rotation. `name` is what the model calls and is not editable. */
|
|
2588
|
+
update: (toolId, patch) => this.request("PATCH", `/webhook-tools/${toolId}`, patch),
|
|
2589
|
+
/** A new signing secret, with the old one still verifying for 24 hours. */
|
|
2590
|
+
rotate: (toolId) => this.request("POST", `/webhook-tools/${toolId}/rotate`, {}),
|
|
2591
|
+
/** Recent calls to your handlers: when, which tool, what came back, how long. */
|
|
2592
|
+
deliveries: () => this.request("GET", "/webhook-tools/deliveries"),
|
|
2345
2593
|
delete: (toolId) => this.request("DELETE", `/webhook-tools/${toolId}`),
|
|
2346
2594
|
};
|
|
2595
|
+
/** The key that signs `turn.stopped` and scheduled-task deliveries to you. */
|
|
2596
|
+
webhookSecret = {
|
|
2597
|
+
get: () => this.request("GET", "/webhook-secret"),
|
|
2598
|
+
rotate: () => this.request("POST", "/webhook-secret/rotate", {}),
|
|
2599
|
+
};
|
|
2347
2600
|
/** Server-side keys. A created one is returned once and never again. */
|
|
2348
2601
|
/**
|
|
2349
2602
|
* Checks on what goes into the model and what comes back.
|