@paneui/core 0.0.8 → 0.0.10
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 +2 -2
- package/dist/client.d.ts +253 -40
- package/dist/client.js +304 -45
- package/dist/icons.d.ts +24 -0
- package/dist/icons.js +99 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +2 -1
- package/dist/register.js +1 -1
- package/dist/schemas.d.ts +29 -8
- package/dist/schemas.js +85 -22
- package/dist/stream.d.ts +25 -6
- package/dist/stream.js +21 -5
- package/dist/types.d.ts +102 -29
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -87,7 +87,7 @@ export class PaneClient {
|
|
|
87
87
|
}
|
|
88
88
|
catch {
|
|
89
89
|
// Body was not JSON (HTML error page, plain-text proxy error, …).
|
|
90
|
-
// Don't discard it —
|
|
90
|
+
// Don't discard it — pane the raw text so callers can diagnose.
|
|
91
91
|
const snippet = text.length > MAX_RESPONSE_SNIPPET_LENGTH
|
|
92
92
|
? text.slice(0, MAX_RESPONSE_SNIPPET_LENGTH) + "…"
|
|
93
93
|
: text;
|
|
@@ -121,34 +121,38 @@ export class PaneClient {
|
|
|
121
121
|
docsUrl: err?.docs_url,
|
|
122
122
|
});
|
|
123
123
|
}
|
|
124
|
-
/** POST /v1/
|
|
125
|
-
async
|
|
126
|
-
const r = await this.call("POST", "/v1/
|
|
124
|
+
/** POST /v1/panes — create a pane. */
|
|
125
|
+
async createPane(req) {
|
|
126
|
+
const r = await this.call("POST", "/v1/panes", {
|
|
127
127
|
template: req.template,
|
|
128
128
|
title: req.title,
|
|
129
|
+
preamble: req.preamble,
|
|
129
130
|
input_data: req.input_data,
|
|
130
131
|
participants: req.participants,
|
|
131
132
|
ttl: req.ttl,
|
|
132
133
|
metadata: req.metadata,
|
|
133
134
|
callback: req.callback,
|
|
135
|
+
context_key: req.context_key,
|
|
136
|
+
icon_emoji: req.icon_emoji,
|
|
137
|
+
icon_attachment_id: req.icon_attachment_id,
|
|
134
138
|
});
|
|
135
139
|
if (!r.ok)
|
|
136
140
|
this.fail(r);
|
|
137
141
|
return this.asObject(r);
|
|
138
142
|
}
|
|
139
|
-
/** GET /v1/
|
|
140
|
-
async
|
|
141
|
-
const r = await this.call("GET", `/v1/
|
|
143
|
+
/** GET /v1/panes/:id — non-blocking pane metadata. */
|
|
144
|
+
async getPane(paneId) {
|
|
145
|
+
const r = await this.call("GET", `/v1/panes/${encodeURIComponent(paneId)}`);
|
|
142
146
|
if (!r.ok)
|
|
143
147
|
this.fail(r);
|
|
144
148
|
return this.asObject(r);
|
|
145
149
|
}
|
|
146
150
|
/**
|
|
147
|
-
* GET /v1/
|
|
151
|
+
* GET /v1/panes/:id/events — fetch the event log.
|
|
148
152
|
* `since` is an opaque cursor; `waitSeconds` enables the relay long-poll
|
|
149
153
|
* (0 = non-blocking, capped at 30 by the relay).
|
|
150
154
|
*/
|
|
151
|
-
async getEvents(
|
|
155
|
+
async getEvents(paneId, opts = {}) {
|
|
152
156
|
const q = new URLSearchParams();
|
|
153
157
|
if (opts.since != null && opts.since !== "")
|
|
154
158
|
q.set("since", opts.since);
|
|
@@ -156,14 +160,175 @@ export class PaneClient {
|
|
|
156
160
|
q.set("wait", String(Math.floor(opts.waitSeconds)));
|
|
157
161
|
}
|
|
158
162
|
const qs = q.toString();
|
|
159
|
-
const r = await this.call("GET", `/v1/
|
|
163
|
+
const r = await this.call("GET", `/v1/panes/${encodeURIComponent(paneId)}/events${qs ? "?" + qs : ""}`);
|
|
160
164
|
if (!r.ok)
|
|
161
165
|
this.fail(r);
|
|
162
166
|
return this.asObject(r);
|
|
163
167
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
168
|
+
// ----- #355: SQL query API --------------------------------------------
|
|
169
|
+
/**
|
|
170
|
+
* POST /v1/query — run a read-only SQL query against the agent's own
|
|
171
|
+
* scoped data (panes, records, events). The relay scopes the result at
|
|
172
|
+
* the view layer; the agent can never see rows whose pane.owner_human_id
|
|
173
|
+
* does not match the caller's scope. Three generic views (panes, records,
|
|
174
|
+
* events) are always exposed; per-collection / per-event-type views are
|
|
175
|
+
* also materialized for any schemas declared on the caller's templates.
|
|
176
|
+
*
|
|
177
|
+
* `opts.paneId` narrows the scope to a single pane — handy when two of
|
|
178
|
+
* the caller's panes declare the same collection with incompatible types
|
|
179
|
+
* and the materializer would otherwise raise view_conflict.
|
|
180
|
+
*
|
|
181
|
+
* `data` is a JSON column — use Postgres-style operators (->>, ->) to
|
|
182
|
+
* project into it. Cap: 10,000 result rows (response.truncated=true
|
|
183
|
+
* signals the cap was hit); statement timeout: 10 seconds.
|
|
184
|
+
*/
|
|
185
|
+
async query(sql, opts = {}) {
|
|
186
|
+
const body = { sql };
|
|
187
|
+
if (opts.paneId !== undefined)
|
|
188
|
+
body.pane_id = opts.paneId;
|
|
189
|
+
const r = await this.call("POST", "/v1/query", body);
|
|
190
|
+
if (!r.ok)
|
|
191
|
+
this.fail(r);
|
|
192
|
+
return this.asObject(r);
|
|
193
|
+
}
|
|
194
|
+
// ----- #297: records CRUD ---------------------------------------------
|
|
195
|
+
/**
|
|
196
|
+
* GET /v1/panes/:id/records/:collection — cursor-paginated list.
|
|
197
|
+
* Includes tombstones (`deleted_at` set) so reconnecting clients can
|
|
198
|
+
* observe deletions.
|
|
199
|
+
*/
|
|
200
|
+
async listRecords(paneId, collection, opts = {}) {
|
|
201
|
+
const q = new URLSearchParams();
|
|
202
|
+
if (opts.since != null)
|
|
203
|
+
q.set("since", String(opts.since));
|
|
204
|
+
if (opts.limit != null)
|
|
205
|
+
q.set("limit", String(opts.limit));
|
|
206
|
+
const qs = q.toString();
|
|
207
|
+
const r = await this.call("GET", `/v1/panes/${encodeURIComponent(paneId)}/records/${encodeURIComponent(collection)}${qs ? "?" + qs : ""}`);
|
|
208
|
+
if (!r.ok)
|
|
209
|
+
this.fail(r);
|
|
210
|
+
return this.asObject(r);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Convenience: walk listRecords until the named recordKey is found OR
|
|
214
|
+
* the collection is exhausted. The relay has no dedicated single-get
|
|
215
|
+
* route today; this client-side scan trades round trips for not adding
|
|
216
|
+
* a route. Fine for typical CLI use; not appropriate for hot paths.
|
|
217
|
+
*/
|
|
218
|
+
async getRecord(paneId, collection, recordKey) {
|
|
219
|
+
let since;
|
|
220
|
+
for (;;) {
|
|
221
|
+
const page = await this.listRecords(paneId, collection, {
|
|
222
|
+
since,
|
|
223
|
+
limit: 200,
|
|
224
|
+
});
|
|
225
|
+
const hit = page.records.find((r) => r.key === recordKey);
|
|
226
|
+
if (hit)
|
|
227
|
+
return hit;
|
|
228
|
+
if (!page.has_more)
|
|
229
|
+
return null;
|
|
230
|
+
since = page.next_since;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* POST /v1/panes/:id/records/:collection — create-or-return-existing.
|
|
235
|
+
* Duplicate `recordKey` returns the existing row with `deduped: true`.
|
|
236
|
+
*/
|
|
237
|
+
async upsertRecord(paneId, collection, body) {
|
|
238
|
+
const r = await this.call("POST", `/v1/panes/${encodeURIComponent(paneId)}/records/${encodeURIComponent(collection)}`, body);
|
|
239
|
+
if (!r.ok)
|
|
240
|
+
this.fail(r);
|
|
241
|
+
const out = this.asObject(r);
|
|
242
|
+
return { record: out.record, deduped: out.deduped ?? false };
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* PATCH /v1/panes/:id/records/:collection/:recordKey — optimistic
|
|
246
|
+
* update. On 409 the relay returns the current row in `details.current`.
|
|
247
|
+
*/
|
|
248
|
+
async updateRecord(paneId, collection, recordKey, body) {
|
|
249
|
+
const r = await this.call("PATCH", `/v1/panes/${encodeURIComponent(paneId)}/records/${encodeURIComponent(collection)}/${encodeURIComponent(recordKey)}`, body);
|
|
250
|
+
if (!r.ok)
|
|
251
|
+
this.fail(r);
|
|
252
|
+
return this.asObject(r);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* DELETE /v1/panes/:id/records/:collection/:recordKey — soft-delete.
|
|
256
|
+
* Optional `if_match` returns 409 + `details.current` on mismatch.
|
|
257
|
+
*/
|
|
258
|
+
async deleteRecord(paneId, collection, recordKey, opts = {}) {
|
|
259
|
+
const body = opts.ifMatch != null ? { if_match: opts.ifMatch } : undefined;
|
|
260
|
+
const r = await this.call("DELETE", `/v1/panes/${encodeURIComponent(paneId)}/records/${encodeURIComponent(collection)}/${encodeURIComponent(recordKey)}`, body);
|
|
261
|
+
if (!r.ok)
|
|
262
|
+
this.fail(r);
|
|
263
|
+
}
|
|
264
|
+
// ----- template-level records CRUD ------------------------------------
|
|
265
|
+
/**
|
|
266
|
+
* GET /v1/templates/:id/template-records/:collection — owner-only list of
|
|
267
|
+
* a template's curated records. Same wire shape as listRecords, separate
|
|
268
|
+
* route so the relay can route owner-vs-page access independently.
|
|
269
|
+
*/
|
|
270
|
+
async listTemplateRecords(templateIdOrSlug, collection, opts = {}) {
|
|
271
|
+
const q = new URLSearchParams();
|
|
272
|
+
if (opts.since != null)
|
|
273
|
+
q.set("since", String(opts.since));
|
|
274
|
+
if (opts.limit != null)
|
|
275
|
+
q.set("limit", String(opts.limit));
|
|
276
|
+
const qs = q.toString();
|
|
277
|
+
const r = await this.call("GET", `/v1/templates/${encodeURIComponent(templateIdOrSlug)}/template-records/${encodeURIComponent(collection)}${qs ? "?" + qs : ""}`);
|
|
278
|
+
if (!r.ok)
|
|
279
|
+
this.fail(r);
|
|
280
|
+
return this.asObject(r);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Client-side scan helper — same shape as `getRecord` but for template
|
|
284
|
+
* records. Walks listTemplateRecords until the key matches.
|
|
285
|
+
*/
|
|
286
|
+
async getTemplateRecord(templateIdOrSlug, collection, recordKey) {
|
|
287
|
+
let since;
|
|
288
|
+
for (;;) {
|
|
289
|
+
const page = await this.listTemplateRecords(templateIdOrSlug, collection, { since, limit: 200 });
|
|
290
|
+
const hit = page.records.find((r) => r.key === recordKey);
|
|
291
|
+
if (hit)
|
|
292
|
+
return hit;
|
|
293
|
+
if (!page.has_more)
|
|
294
|
+
return null;
|
|
295
|
+
since = page.next_since;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* POST /v1/templates/:id/template-records/:collection — owner-only
|
|
300
|
+
* create-or-return-existing.
|
|
301
|
+
*/
|
|
302
|
+
async upsertTemplateRecord(templateIdOrSlug, collection, body) {
|
|
303
|
+
const r = await this.call("POST", `/v1/templates/${encodeURIComponent(templateIdOrSlug)}/template-records/${encodeURIComponent(collection)}`, body);
|
|
304
|
+
if (!r.ok)
|
|
305
|
+
this.fail(r);
|
|
306
|
+
const out = this.asObject(r);
|
|
307
|
+
return { record: out.record, deduped: out.deduped ?? false };
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* PATCH /v1/templates/:id/template-records/:collection/:recordKey —
|
|
311
|
+
* optimistic-locked update.
|
|
312
|
+
*/
|
|
313
|
+
async updateTemplateRecord(templateIdOrSlug, collection, recordKey, body) {
|
|
314
|
+
const r = await this.call("PATCH", `/v1/templates/${encodeURIComponent(templateIdOrSlug)}/template-records/${encodeURIComponent(collection)}/${encodeURIComponent(recordKey)}`, body);
|
|
315
|
+
if (!r.ok)
|
|
316
|
+
this.fail(r);
|
|
317
|
+
return this.asObject(r);
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* DELETE /v1/templates/:id/template-records/:collection/:recordKey —
|
|
321
|
+
* soft-delete.
|
|
322
|
+
*/
|
|
323
|
+
async deleteTemplateRecord(templateIdOrSlug, collection, recordKey, opts = {}) {
|
|
324
|
+
const body = opts.ifMatch != null ? { if_match: opts.ifMatch } : undefined;
|
|
325
|
+
const r = await this.call("DELETE", `/v1/templates/${encodeURIComponent(templateIdOrSlug)}/template-records/${encodeURIComponent(collection)}/${encodeURIComponent(recordKey)}`, body);
|
|
326
|
+
if (!r.ok)
|
|
327
|
+
this.fail(r);
|
|
328
|
+
}
|
|
329
|
+
/** POST /v1/panes/:id/events — append an agent event. */
|
|
330
|
+
async sendEvent(paneId, ev) {
|
|
331
|
+
const r = await this.call("POST", `/v1/panes/${encodeURIComponent(paneId)}/events`, {
|
|
167
332
|
type: ev.type,
|
|
168
333
|
data: ev.data,
|
|
169
334
|
causation_id: ev.causationId,
|
|
@@ -188,6 +353,7 @@ export class PaneClient {
|
|
|
188
353
|
type: req.type,
|
|
189
354
|
event_schema: req.event_schema,
|
|
190
355
|
input_schema: req.input_schema,
|
|
356
|
+
icon_emoji: req.icon_emoji,
|
|
191
357
|
});
|
|
192
358
|
if (!r.ok)
|
|
193
359
|
this.fail(r);
|
|
@@ -219,6 +385,10 @@ export class PaneClient {
|
|
|
219
385
|
slug: metadata.slug,
|
|
220
386
|
description: metadata.description,
|
|
221
387
|
tags: metadata.tags,
|
|
388
|
+
// Forward null explicitly (clears the icon); undefined is dropped by
|
|
389
|
+
// JSON.stringify so an omitted field is a no-op server-side.
|
|
390
|
+
icon_emoji: metadata.icon_emoji,
|
|
391
|
+
icon_attachment_id: metadata.icon_attachment_id,
|
|
222
392
|
});
|
|
223
393
|
if (!r.ok)
|
|
224
394
|
this.fail(r);
|
|
@@ -280,7 +450,7 @@ export class PaneClient {
|
|
|
280
450
|
* POST /v1/agents/claim — bind this agent to a human via a one-shot
|
|
281
451
|
* claim code the human generated in their settings UI. After a
|
|
282
452
|
* successful claim the agent's existing API key continues to work,
|
|
283
|
-
* but the agent (and its
|
|
453
|
+
* but the agent (and its panes/templates) now belong to the
|
|
284
454
|
* claiming human. One-way operation — there is no unclaim in v1.
|
|
285
455
|
*/
|
|
286
456
|
async claimAgent(code) {
|
|
@@ -333,7 +503,7 @@ export class PaneClient {
|
|
|
333
503
|
const r = await this.call("POST", "/v1/feedback", {
|
|
334
504
|
type: req.type,
|
|
335
505
|
message: req.message,
|
|
336
|
-
|
|
506
|
+
pane_id: req.paneId,
|
|
337
507
|
});
|
|
338
508
|
if (!r.ok)
|
|
339
509
|
this.fail(r);
|
|
@@ -356,14 +526,14 @@ export class PaneClient {
|
|
|
356
526
|
return this.asObject(r);
|
|
357
527
|
}
|
|
358
528
|
/**
|
|
359
|
-
* GET /v1/
|
|
529
|
+
* GET /v1/panes — list the calling agent's panes. Default filter is
|
|
360
530
|
* `status=open` (effective status — respects expiresAt). Response items
|
|
361
531
|
* carry NO secrets: no participant token plaintext, no callback URL, no
|
|
362
532
|
* metadata or input_data. Use `participant_id` from the list as the handle
|
|
363
533
|
* for {@link revokeParticipant}; use {@link mintParticipant} to issue a
|
|
364
534
|
* fresh URL when the original was lost.
|
|
365
535
|
*/
|
|
366
|
-
async
|
|
536
|
+
async listPanes(opts = {}) {
|
|
367
537
|
const q = new URLSearchParams();
|
|
368
538
|
if (opts.status !== undefined)
|
|
369
539
|
q.set("status", opts.status);
|
|
@@ -374,69 +544,115 @@ export class PaneClient {
|
|
|
374
544
|
if (opts.template_id !== undefined && opts.template_id !== "")
|
|
375
545
|
q.set("template_id", opts.template_id);
|
|
376
546
|
const qs = q.toString();
|
|
377
|
-
const r = await this.call("GET", `/v1/
|
|
547
|
+
const r = await this.call("GET", `/v1/panes${qs ? "?" + qs : ""}`);
|
|
378
548
|
if (!r.ok)
|
|
379
549
|
this.fail(r);
|
|
380
550
|
return this.asObject(r);
|
|
381
551
|
}
|
|
382
552
|
/**
|
|
383
|
-
* GET /v1/
|
|
384
|
-
*
|
|
553
|
+
* GET /v1/panes/:id/participants — list every participant on one
|
|
554
|
+
* pane (active and revoked). Bounded by MAX_PARTICIPANTS_PER_PANE
|
|
385
555
|
* on the relay, so the full list is returned with no pagination.
|
|
386
556
|
* Use this to find the `participant_id` you need to pass to
|
|
387
557
|
* {@link revokeParticipant}, or to audit revoked rows.
|
|
388
558
|
*/
|
|
389
|
-
async listParticipants(
|
|
390
|
-
const r = await this.call("GET", `/v1/
|
|
559
|
+
async listParticipants(paneId) {
|
|
560
|
+
const r = await this.call("GET", `/v1/panes/${encodeURIComponent(paneId)}/participants`);
|
|
391
561
|
if (!r.ok)
|
|
392
562
|
this.fail(r);
|
|
393
563
|
return this.asObject(r);
|
|
394
564
|
}
|
|
395
565
|
/**
|
|
396
|
-
* POST /v1/
|
|
397
|
-
* existing
|
|
398
|
-
* was dropped: the
|
|
566
|
+
* POST /v1/panes/:id/participants — mint a fresh participant URL for an
|
|
567
|
+
* existing pane. The one-shot recovery primitive when the original URL
|
|
568
|
+
* was dropped: the pane keeps its event log, template pin, and created_at.
|
|
399
569
|
* v1 supports `kind: "human"` only.
|
|
400
570
|
*
|
|
401
571
|
* The plaintext token is returned EXACTLY ONCE in the response — the relay
|
|
402
572
|
* stores only the hash. Save the response (e.g. pipe to a JSONL log) before
|
|
403
573
|
* delivering the URL to the human.
|
|
404
574
|
*/
|
|
405
|
-
async mintParticipant(
|
|
406
|
-
const r = await this.call("POST", `/v1/
|
|
575
|
+
async mintParticipant(paneId, opts = {}) {
|
|
576
|
+
const r = await this.call("POST", `/v1/panes/${encodeURIComponent(paneId)}/participants`, { kind: opts.kind ?? "human" });
|
|
407
577
|
if (!r.ok)
|
|
408
578
|
this.fail(r);
|
|
409
579
|
return this.asObject(r);
|
|
410
580
|
}
|
|
411
581
|
/**
|
|
412
|
-
* DELETE /v1/
|
|
413
|
-
* participant URL. The
|
|
582
|
+
* DELETE /v1/panes/:id/participants/:participant_id — revoke a single
|
|
583
|
+
* participant URL. The pane's other participants (and the agent's own
|
|
414
584
|
* WebSocket) are untouched. Idempotent: revoking an unknown or already-
|
|
415
585
|
* revoked participant returns 204. The agent participant cannot be revoked
|
|
416
|
-
* via this endpoint — use {@link
|
|
586
|
+
* via this endpoint — use {@link deletePane} instead.
|
|
417
587
|
*
|
|
418
588
|
* Existing WebSocket connections held under the revoked token are NOT
|
|
419
589
|
* actively kicked in v1; new HTTP and WS connections are refused.
|
|
420
590
|
*/
|
|
421
|
-
async revokeParticipant(
|
|
422
|
-
const r = await this.call("DELETE", `/v1/
|
|
591
|
+
async revokeParticipant(paneId, participantId) {
|
|
592
|
+
const r = await this.call("DELETE", `/v1/panes/${encodeURIComponent(paneId)}/participants/${encodeURIComponent(participantId)}`);
|
|
593
|
+
if (!r.ok)
|
|
594
|
+
this.fail(r);
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* DELETE /v1/panes/:id — close/delete a pane. Idempotent on the relay
|
|
598
|
+
* side (an already-closed pane still returns 204 with no body).
|
|
599
|
+
*/
|
|
600
|
+
async deletePane(id) {
|
|
601
|
+
const r = await this.call("DELETE", `/v1/panes/${encodeURIComponent(id)}`);
|
|
602
|
+
if (!r.ok)
|
|
603
|
+
this.fail(r);
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* GET /v1/trash — list soft-deleted panes + templates in the caller's
|
|
607
|
+
* agent-scope. The trash UI / `pane trash list` lives off this. (#306)
|
|
608
|
+
*/
|
|
609
|
+
async listTrash() {
|
|
610
|
+
const r = await this.call("GET", "/v1/trash");
|
|
611
|
+
if (!r.ok)
|
|
612
|
+
this.fail(r);
|
|
613
|
+
return r.data;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* POST /v1/trash/panes/:id/restore — un-trash a soft-deleted pane (clear
|
|
617
|
+
* deletedAt + audit row). (#306)
|
|
618
|
+
*/
|
|
619
|
+
async restorePane(id) {
|
|
620
|
+
const r = await this.call("POST", `/v1/trash/panes/${encodeURIComponent(id)}/restore`);
|
|
621
|
+
if (!r.ok)
|
|
622
|
+
this.fail(r);
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* POST /v1/trash/templates/:id/restore — un-trash a soft-deleted template. (#306)
|
|
626
|
+
*/
|
|
627
|
+
async restoreTemplate(id) {
|
|
628
|
+
const r = await this.call("POST", `/v1/trash/templates/${encodeURIComponent(id)}/restore`);
|
|
629
|
+
if (!r.ok)
|
|
630
|
+
this.fail(r);
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* DELETE /v1/trash/panes/:id — permanently hard-delete a trashed pane
|
|
634
|
+
* (bypass retention window). (#306)
|
|
635
|
+
*/
|
|
636
|
+
async permanentDeletePane(id) {
|
|
637
|
+
const r = await this.call("DELETE", `/v1/trash/panes/${encodeURIComponent(id)}`);
|
|
423
638
|
if (!r.ok)
|
|
424
639
|
this.fail(r);
|
|
425
640
|
}
|
|
426
641
|
/**
|
|
427
|
-
* DELETE /v1/
|
|
428
|
-
*
|
|
642
|
+
* DELETE /v1/trash/templates/:id — permanently hard-delete a trashed
|
|
643
|
+
* template. Refused 409 if a live pane still references one of its
|
|
644
|
+
* versions. (#306)
|
|
429
645
|
*/
|
|
430
|
-
async
|
|
431
|
-
const r = await this.call("DELETE", `/v1/
|
|
646
|
+
async permanentDeleteTemplate(id) {
|
|
647
|
+
const r = await this.call("DELETE", `/v1/trash/templates/${encodeURIComponent(id)}`);
|
|
432
648
|
if (!r.ok)
|
|
433
649
|
this.fail(r);
|
|
434
650
|
}
|
|
435
651
|
/**
|
|
436
652
|
* DELETE /v1/templates/:id — remove an template and (server-side) all its
|
|
437
653
|
* versions. Strict cascade: the relay refuses with 409 conflict if any
|
|
438
|
-
*
|
|
439
|
-
*
|
|
654
|
+
* pane in any state still references one of the template's versions —
|
|
655
|
+
* pane that as a typed PaneApiError so the CLI can render a hint
|
|
440
656
|
* instead of swallowing it.
|
|
441
657
|
*/
|
|
442
658
|
async deleteArtifact(idOrSlug) {
|
|
@@ -444,6 +660,49 @@ export class PaneClient {
|
|
|
444
660
|
if (!r.ok)
|
|
445
661
|
this.fail(r);
|
|
446
662
|
}
|
|
663
|
+
/**
|
|
664
|
+
* POST /v1/templates/:id/publish — enter the public catalog. The optional
|
|
665
|
+
* `scopes` list locks the permissions the template will request from
|
|
666
|
+
* installers at this version (see Phase F §4.5 / §8). Passing an empty
|
|
667
|
+
* array clears the stored scopes; omitting `scopes` keeps the existing
|
|
668
|
+
* ones.
|
|
669
|
+
*/
|
|
670
|
+
async publishTemplate(idOrSlug, body = {}) {
|
|
671
|
+
const r = await this.call("POST", `/v1/templates/${encodeURIComponent(idOrSlug)}/publish`, body);
|
|
672
|
+
if (!r.ok)
|
|
673
|
+
this.fail(r);
|
|
674
|
+
return this.asObject(r);
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* POST /v1/templates/:id/unpublish — leave the public catalog. Existing
|
|
678
|
+
* installs are unaffected (humans keep their pinned version), but the
|
|
679
|
+
* template no longer appears in `searchPublicTemplates` results.
|
|
680
|
+
*/
|
|
681
|
+
async unpublishTemplate(idOrSlug) {
|
|
682
|
+
const r = await this.call("POST", `/v1/templates/${encodeURIComponent(idOrSlug)}/unpublish`);
|
|
683
|
+
if (!r.ok)
|
|
684
|
+
this.fail(r);
|
|
685
|
+
return this.asObject(r);
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* GET /v1/templates/catalog?q=... — agent-side public catalog search.
|
|
689
|
+
* Lets an agent discover already-published apps before authoring a
|
|
690
|
+
* duplicate. Sorted by install_count desc, then publish recency.
|
|
691
|
+
*/
|
|
692
|
+
async searchPublicTemplates(query, opts = {}) {
|
|
693
|
+
const params = new URLSearchParams();
|
|
694
|
+
if (query != null && query !== "")
|
|
695
|
+
params.set("q", query);
|
|
696
|
+
if (opts.limit != null)
|
|
697
|
+
params.set("limit", String(opts.limit));
|
|
698
|
+
if (opts.offset != null)
|
|
699
|
+
params.set("offset", String(opts.offset));
|
|
700
|
+
const qs = params.toString();
|
|
701
|
+
const r = await this.call("GET", `/v1/templates/catalog${qs ? "?" + qs : ""}`);
|
|
702
|
+
if (!r.ok)
|
|
703
|
+
this.fail(r);
|
|
704
|
+
return this.asObject(r);
|
|
705
|
+
}
|
|
447
706
|
// ------------------------------------------------------------------------
|
|
448
707
|
// Blobs (v0.1.0). Three-scope binary attachments with multipart upload.
|
|
449
708
|
// See proposal pane#152 for the full design.
|
|
@@ -453,9 +712,9 @@ export class PaneClient {
|
|
|
453
712
|
* in event payloads (the relay's `format: pane-attachment-id` schema vocab
|
|
454
713
|
* validates the id) or in `pane create --input-data`.
|
|
455
714
|
*
|
|
456
|
-
* Scope defaults to "agent" (reusable across the agent's
|
|
457
|
-
* `scope: "
|
|
458
|
-
* `templateId`. The agent must own the referenced
|
|
715
|
+
* Scope defaults to "agent" (reusable across the agent's panes). For
|
|
716
|
+
* `scope: "pane"` pass `paneId`; for `scope: "template"` pass
|
|
717
|
+
* `templateId`. The agent must own the referenced pane / template;
|
|
459
718
|
* cross-tenant attempts return attachment_not_found.
|
|
460
719
|
*
|
|
461
720
|
* MIME is inferred from `mime` if supplied; otherwise the relay sniffs
|
|
@@ -492,8 +751,8 @@ export class PaneClient {
|
|
|
492
751
|
fd.set("file", attachment, opts.filename ?? "attachment");
|
|
493
752
|
if (opts.scope)
|
|
494
753
|
fd.set("scope", opts.scope);
|
|
495
|
-
if (opts.
|
|
496
|
-
fd.set("
|
|
754
|
+
if (opts.paneId)
|
|
755
|
+
fd.set("pane_id", opts.paneId);
|
|
497
756
|
if (opts.templateId)
|
|
498
757
|
fd.set("template_id", opts.templateId);
|
|
499
758
|
if (opts.filename)
|
|
@@ -575,7 +834,7 @@ export class PaneClient {
|
|
|
575
834
|
}
|
|
576
835
|
/**
|
|
577
836
|
* Mint a `/b/<token>` capability URL for `attachmentId`. Default TTL is set by
|
|
578
|
-
* the relay (24h agent,
|
|
837
|
+
* the relay (24h agent, pane-TTL pane, 30d template). `once: true`
|
|
579
838
|
* tokens self-delete on first GET.
|
|
580
839
|
*/
|
|
581
840
|
async mintBlobToken(attachmentId, opts = {}) {
|
|
@@ -637,7 +896,7 @@ export class PaneClient {
|
|
|
637
896
|
size: opts.size,
|
|
638
897
|
sha256: opts.sha256,
|
|
639
898
|
scope: opts.scope,
|
|
640
|
-
|
|
899
|
+
pane_id: opts.paneId,
|
|
641
900
|
template_id: opts.templateId,
|
|
642
901
|
filename: opts.filename,
|
|
643
902
|
});
|
package/dist/icons.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const RASTER_ICON_MIME_ALLOWLIST: readonly ["image/png", "image/jpeg", "image/webp", "image/gif"];
|
|
2
|
+
export type RasterIconMime = (typeof RASTER_ICON_MIME_ALLOWLIST)[number];
|
|
3
|
+
/** True iff `mime` is a raster image type allowed as an icon. */
|
|
4
|
+
export declare function isRasterImageMime(mime: string | null | undefined): boolean;
|
|
5
|
+
export declare const MAX_ICON_EMOJI_BYTES = 64;
|
|
6
|
+
/**
|
|
7
|
+
* Validate an icon emoji. Returns `{ ok: true }` for a single emoji grapheme,
|
|
8
|
+
* or `{ ok: false, error }` with a human-readable reason otherwise.
|
|
9
|
+
*
|
|
10
|
+
* Rules:
|
|
11
|
+
* - non-empty, ≤ MAX_ICON_EMOJI_BYTES bytes
|
|
12
|
+
* - no control characters
|
|
13
|
+
* - exactly ONE grapheme cluster (via Intl.Segmenter)
|
|
14
|
+
* - that grapheme contains at least one Extended_Pictographic code point
|
|
15
|
+
* (so plain ASCII letters/digits and bare symbols are rejected)
|
|
16
|
+
*/
|
|
17
|
+
export declare function validateIconEmoji(raw: string): {
|
|
18
|
+
ok: true;
|
|
19
|
+
} | {
|
|
20
|
+
ok: false;
|
|
21
|
+
error: string;
|
|
22
|
+
};
|
|
23
|
+
/** Convenience boolean form of {@link validateIconEmoji}. */
|
|
24
|
+
export declare function isValidIconEmoji(raw: string): boolean;
|
package/dist/icons.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Icon helpers shared by the relay and CLI — emoji validation and the raster
|
|
2
|
+
// image MIME allowlist for template/pane icons.
|
|
3
|
+
//
|
|
4
|
+
// Icons are deliberately constrained: either ONE emoji grapheme (rendered
|
|
5
|
+
// inline as text) or an uploaded raster image (served from the relay's blob
|
|
6
|
+
// store). No external URLs, no SVG (XSS vector), no multi-character strings.
|
|
7
|
+
// Raster image MIME types accepted as an uploaded icon. SVG is deliberately
|
|
8
|
+
// EXCLUDED — it can carry script and is an XSS vector when rendered in an
|
|
9
|
+
// <img> from a same-origin route. Vector/animated-vector and non-image types
|
|
10
|
+
// are rejected.
|
|
11
|
+
export const RASTER_ICON_MIME_ALLOWLIST = [
|
|
12
|
+
"image/png",
|
|
13
|
+
"image/jpeg",
|
|
14
|
+
"image/webp",
|
|
15
|
+
"image/gif",
|
|
16
|
+
];
|
|
17
|
+
/** True iff `mime` is a raster image type allowed as an icon. */
|
|
18
|
+
export function isRasterImageMime(mime) {
|
|
19
|
+
if (!mime)
|
|
20
|
+
return false;
|
|
21
|
+
// Normalise: drop any `; charset=...` parameter and lowercase.
|
|
22
|
+
const base = mime.split(";")[0].trim().toLowerCase();
|
|
23
|
+
return RASTER_ICON_MIME_ALLOWLIST.includes(base);
|
|
24
|
+
}
|
|
25
|
+
// Upper bound on the raw byte length of an emoji string. A single rendered
|
|
26
|
+
// emoji can be a long ZWJ sequence (e.g. a family/flag with skin-tone
|
|
27
|
+
// modifiers), but 64 bytes is well beyond any real single grapheme and bounds
|
|
28
|
+
// the work the segmenter does.
|
|
29
|
+
export const MAX_ICON_EMOJI_BYTES = 64;
|
|
30
|
+
// Matches a code point with the Extended_Pictographic property — the Unicode
|
|
31
|
+
// property that flags "this is an emoji-ish pictograph". Used to ensure the
|
|
32
|
+
// grapheme actually contains an emoji and isn't a plain letter/digit/symbol.
|
|
33
|
+
const EXTENDED_PICTOGRAPHIC_RE = /\p{Extended_Pictographic}/u;
|
|
34
|
+
// Control characters (C0 + DEL + C1) are never valid in an icon emoji. The
|
|
35
|
+
// control chars in the class are intentional — that's exactly what we reject.
|
|
36
|
+
// eslint-disable-next-line no-control-regex
|
|
37
|
+
const CONTROL_CHAR_RE = /[\u0000-\u001f\u007f-\u009f]/;
|
|
38
|
+
const byteLength = (s) => typeof TextEncoder !== "undefined"
|
|
39
|
+
? new TextEncoder().encode(s).length
|
|
40
|
+
: Buffer.byteLength(s, "utf8");
|
|
41
|
+
/**
|
|
42
|
+
* Validate an icon emoji. Returns `{ ok: true }` for a single emoji grapheme,
|
|
43
|
+
* or `{ ok: false, error }` with a human-readable reason otherwise.
|
|
44
|
+
*
|
|
45
|
+
* Rules:
|
|
46
|
+
* - non-empty, ≤ MAX_ICON_EMOJI_BYTES bytes
|
|
47
|
+
* - no control characters
|
|
48
|
+
* - exactly ONE grapheme cluster (via Intl.Segmenter)
|
|
49
|
+
* - that grapheme contains at least one Extended_Pictographic code point
|
|
50
|
+
* (so plain ASCII letters/digits and bare symbols are rejected)
|
|
51
|
+
*/
|
|
52
|
+
export function validateIconEmoji(raw) {
|
|
53
|
+
if (typeof raw !== "string" || raw.length === 0) {
|
|
54
|
+
return { ok: false, error: "icon_emoji must be a non-empty string" };
|
|
55
|
+
}
|
|
56
|
+
if (byteLength(raw) > MAX_ICON_EMOJI_BYTES) {
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
error: `icon_emoji must be at most ${MAX_ICON_EMOJI_BYTES} bytes`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (CONTROL_CHAR_RE.test(raw)) {
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
error: "icon_emoji must not contain control characters",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// Count grapheme clusters. Intl.Segmenter is available in Node 20+ and all
|
|
69
|
+
// evergreen browsers (our targets); guard defensively anyway.
|
|
70
|
+
let graphemeCount;
|
|
71
|
+
if (typeof Intl !== "undefined" && typeof Intl.Segmenter === "function") {
|
|
72
|
+
const seg = new Intl.Segmenter("en", { granularity: "grapheme" });
|
|
73
|
+
// Iterate the segmenter to count grapheme clusters. `[...iterable].length`
|
|
74
|
+
// materialises the segments, but a single emoji is tiny so it's fine.
|
|
75
|
+
graphemeCount = [...seg.segment(raw)].length;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
// Fallback: count code points (over-counts ZWJ sequences, but the
|
|
79
|
+
// pictographic check below still gates non-emoji input).
|
|
80
|
+
graphemeCount = Array.from(raw).length;
|
|
81
|
+
}
|
|
82
|
+
if (graphemeCount !== 1) {
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
error: "icon_emoji must be exactly one emoji (a single grapheme)",
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (!EXTENDED_PICTOGRAPHIC_RE.test(raw)) {
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
error: "icon_emoji must be an emoji, not a letter, digit, or symbol",
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return { ok: true };
|
|
95
|
+
}
|
|
96
|
+
/** Convenience boolean form of {@link validateIconEmoji}. */
|
|
97
|
+
export function isValidIconEmoji(raw) {
|
|
98
|
+
return validateIconEmoji(raw).ok;
|
|
99
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
export { PaneClient, PaneApiError } from "./client.js";
|
|
2
|
-
export type { ClientOptions, RelayResponse, CreateArtifactRequest, CreateArtifactVersionRequest, PatchArtifactMetadataRequest, AttachmentRef, UploadBlobOptions, PresignBlobOptions, AttachmentTokenMintResponse, ListBlobsOptions, AttachmentTokenAuditEntry, AttachmentTokenListResponse, } from "./client.js";
|
|
2
|
+
export type { ClientOptions, RelayResponse, CreateArtifactRequest, CreateArtifactVersionRequest, PatchArtifactMetadataRequest, AttachmentRef, UploadBlobOptions, PresignBlobOptions, AttachmentTokenMintResponse, ListBlobsOptions, AttachmentTokenAuditEntry, AttachmentTokenListResponse, QueryResponse, } from "./client.js";
|
|
3
3
|
export { openStream } from "./stream.js";
|
|
4
4
|
export type { OpenStreamOptions, StreamHandlers, StreamHandle, } from "./stream.js";
|
|
5
5
|
export { registerAgent } from "./register.js";
|
|
6
6
|
export type { RegisterAgentOptions, RegisterAgentResult } from "./register.js";
|
|
7
|
-
export { artifactSchema, callbackSchema,
|
|
8
|
-
export type {
|
|
7
|
+
export { artifactSchema, callbackSchema, createPaneSchema, artifactTypeSchema, createArtifactSchema, createArtifactVersionSchema, patchArtifactMetadataSchema, feedbackTypeSchema, submitFeedbackSchema, listPanesStatusSchema, listPanesQuerySchema, mintParticipantSchema, upgradePaneSchema, } from "./schemas.js";
|
|
8
|
+
export type { CreatePaneInput, ListPanesStatus, ListPanesQuery, MintParticipantInput, UpgradePaneInput, } from "./schemas.js";
|
|
9
|
+
export { validateIconEmoji, isValidIconEmoji, isRasterImageMime, RASTER_ICON_MIME_ALLOWLIST, MAX_ICON_EMOJI_BYTES, } from "./icons.js";
|
|
10
|
+
export type { RasterIconMime } from "./icons.js";
|
|
9
11
|
export { MAX_EVENT_TYPE_LENGTH, MAX_IDEMPOTENCY_KEY_LENGTH, MAX_RESPONSE_SNIPPET_LENGTH, MAX_FRAME_SNIPPET_LENGTH, } from "./limits.js";
|
|
10
|
-
export type { AuthorKind, PaneEvent, Template, TemplateType, TemplateVersion, TemplateRecord, TemplateSummary, CreateArtifactResponse, KeyInfo, TasteInfo, FeedbackType, FeedbackSubmission, FeedbackRecord, FeedbackPage, Callback,
|
|
12
|
+
export type { AuthorKind, PaneEvent, SerializedRecord, DeletedRecordRef, RecordDeltaMessage, Template, TemplateType, TemplateVersion, TemplateRecord, TemplateSummary, CreateArtifactResponse, KeyInfo, TasteInfo, FeedbackType, FeedbackSubmission, FeedbackRecord, FeedbackPage, Callback, CreatePaneRequest, CreatePaneResponse, PaneState, EventsPage, ParticipantSummary, ParticipantsList, PaneSummary, PanesPage, MintParticipantResponse, TrashedPaneEntry, TrashedTemplateEntry, TrashListResponse, RelayError, } from "./types.js";
|