pi-roundtable-web 0.24.2 → 0.26.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 +48 -0
- package/README.md +195 -16
- package/dist/account-zk4efb87.css +102 -0
- package/dist/chunk-account-hw49gd0d.js +1 -0
- package/dist/chunk-action-menu-rm5cyqkj.js +1 -0
- package/dist/chunk-agent-b017vswv.js +1 -0
- package/dist/chunk-arranged-rooms-q5v3dm1r.js +1 -0
- package/dist/chunk-character-creator-5dxcvd16.js +1 -0
- package/dist/chunk-codeeditor-pyt09fwg.js +2 -0
- package/dist/chunk-confirm-521yqmta.js +1 -0
- package/dist/chunk-connectors-hrm06nx8.js +1 -0
- package/dist/chunk-conversations-e26rrf01.js +1 -0
- package/dist/chunk-drag-v41gx2mc.js +1 -0
- package/dist/chunk-entry-jxxb3ath.js +1 -0
- package/dist/chunk-folder-list-b97xk12b.js +1 -0
- package/dist/chunk-folds-b8q7eags.js +1 -0
- package/dist/chunk-gate-8k61stsc.js +1 -0
- package/dist/chunk-group-scene-dx6wt19m.js +1 -0
- package/dist/chunk-group-table-e4jr8r3a.js +1 -0
- package/dist/chunk-host-state-g7e7c0k5.js +1 -0
- package/dist/chunk-host-ui-x7k6n07v.js +939 -0
- package/dist/chunk-index-014rjv9k.js +2 -0
- package/dist/chunk-index-2d0kg096.js +2 -0
- package/dist/chunk-index-2qd81wbx.js +2 -0
- package/dist/chunk-index-2stynxw6.css +1 -0
- package/dist/chunk-index-459drsft.js +2 -0
- package/dist/chunk-index-4mfj596y.js +2 -0
- package/dist/chunk-index-66nskrdj.js +47 -0
- package/dist/chunk-index-awx6nnta.js +2 -0
- package/dist/chunk-index-bbsytycb.js +2 -0
- package/dist/chunk-index-c07p8k0x.js +4 -0
- package/dist/chunk-index-cq1e9xdz.js +2 -0
- package/dist/chunk-index-d1prgfbp.js +2 -0
- package/dist/chunk-index-dhpt8qr9.js +2 -0
- package/dist/chunk-index-e5sd30xy.js +2 -0
- package/dist/chunk-index-g351921r.js +1 -0
- package/dist/chunk-index-hby0ap91.js +2 -0
- package/dist/chunk-index-hssjbr1k.js +2 -0
- package/dist/chunk-index-k2qq1gdj.js +2 -0
- package/dist/chunk-index-ke9ynm0n.js +2 -0
- package/dist/chunk-index-km40bhsz.js +2 -0
- package/dist/chunk-index-kpscvg85.js +2 -0
- package/dist/chunk-index-m0d100mg.js +2 -0
- package/dist/chunk-index-mb72snk9.js +2 -0
- package/dist/chunk-index-mjqqver5.js +4 -0
- package/dist/chunk-index-nqfkwkfp.js +2 -0
- package/dist/chunk-index-nwqk447w.js +2 -0
- package/dist/chunk-index-pcfhpm8v.js +302 -0
- package/dist/chunk-index-q8h8xzbm.js +2 -0
- package/dist/chunk-index-r5vszgfs.js +2 -0
- package/dist/{chunk-index-mpm90318.js → chunk-index-rhtvcbsq.js} +2 -2
- package/dist/chunk-index-rjv7vrwd.js +13 -0
- package/dist/chunk-index-v23m38p6.js +50 -0
- package/dist/chunk-index-va0g7d88.js +1 -0
- package/dist/{chunk-index-6t68gbex.js → chunk-index-wnjp6wv0.js} +2 -2
- package/dist/chunk-index-xgkd15n3.js +2 -0
- package/dist/chunk-index-y8rnq8zf.js +14 -0
- package/dist/chunk-index-z3k18atc.js +2 -0
- package/dist/chunk-lang-css-6bepqpnx.js +1 -0
- package/dist/{chunk-lang-dockerfile-ey0nvhhm.js → chunk-lang-dockerfile-hrav55t7.js} +1 -1
- package/dist/{chunk-lang-go-403qsw9f.js → chunk-lang-go-x9ahv6e5.js} +3 -3
- package/dist/chunk-lang-html-8jc72zt4.js +1 -0
- package/dist/chunk-lang-javascript-2p2qtbnf.js +1 -0
- package/dist/chunk-lang-json-hsemyd0r.js +1 -0
- package/dist/chunk-lang-markdown-zkwb4c9n.js +6 -0
- package/dist/{chunk-lang-python-9jk5a49y.js → chunk-lang-python-tewx7006.js} +2 -2
- package/dist/chunk-lang-rust-0ydx84s1.js +1 -0
- package/dist/{chunk-lang-shell-cgq6q0kj.js → chunk-lang-shell-gq1tmdz4.js} +1 -1
- package/dist/chunk-lang-sql-xt1qyycg.js +2 -0
- package/dist/{chunk-lang-toml-xh45nm68.js → chunk-lang-toml-0hamc5fc.js} +1 -1
- package/dist/chunk-lang-typescript-82xf7z2e.js +1 -0
- package/dist/{chunk-lang-xml-1mh879j9.js → chunk-lang-xml-wf6h5pt2.js} +1 -1
- package/dist/{chunk-lang-yaml-jac98srt.js → chunk-lang-yaml-7vk521h1.js} +1 -1
- package/dist/chunk-layout-actions-zh7bezk7.js +1 -0
- package/dist/chunk-menu-items-1n9qargb.js +2 -0
- package/dist/chunk-notes-t15bh04m.js +1 -0
- package/dist/chunk-notice-bell-w3y0h0sr.js +1 -0
- package/dist/chunk-notices-fa3g7qze.js +1 -0
- package/dist/chunk-onboarding-34v3nr17.js +1 -0
- package/dist/chunk-overview-km5vq375.js +1 -0
- package/dist/chunk-palette-a8txdgd0.js +1 -0
- package/dist/chunk-placement-items-wgaxsbvs.js +1 -0
- package/dist/chunk-profile-ypxdbsk1.js +1 -0
- package/dist/chunk-prompt-history-wm9dsy6s.js +1 -0
- package/dist/chunk-prompts-7mhhq7zs.js +1 -0
- package/dist/chunk-room-confirms-t11a1gsw.js +1 -0
- package/dist/chunk-room-menu-2tm2xtfx.js +1 -0
- package/dist/chunk-rooms-menu-button-wxnsxjwc.js +1 -0
- package/dist/chunk-schedules-2ft6xxyw.js +1 -0
- package/dist/chunk-seat-menu-d3npdtx1.js +1 -0
- package/dist/chunk-sheet-hosts-vngvp7wv.js +1 -0
- package/dist/chunk-skill-editor-65n2hsq4.js +8 -0
- package/dist/chunk-skills-shkdwes8.js +1 -0
- package/dist/chunk-switcher-list-g6hhmr1t.js +1 -0
- package/dist/chunk-table-1b6bsq2y.js +1 -0
- package/dist/chunk-table-seats-vy1qhdks.js +1 -0
- package/dist/chunk-table2d-n26b7gd0.js +1 -0
- package/dist/chunk-table3d-8aysyf56.js +4220 -0
- package/dist/chunk-transcript-355faceq.js +1 -0
- package/dist/chunk-where-j5qdt8a4.js +1 -0
- package/dist/chunk-workspace-vrbs6zx1.js +1 -0
- package/dist/chunk-zh-tw-zeg600bk.js +1 -0
- package/dist/index.html +1 -1
- package/dist/sw.js +1 -1
- package/package.json +5 -4
- package/src/api-types.ts +515 -6
- package/src/app/api-http.ts +66 -0
- package/src/app/app.ts +214 -33
- package/src/app/card-log.ts +6 -0
- package/src/app/desk.ts +47 -22
- package/src/app/files.ts +73 -7
- package/src/app/layout-routes.ts +239 -0
- package/src/app/layout.ts +133 -0
- package/src/app/presence.ts +2 -2
- package/src/app/prompt-routes.ts +167 -0
- package/src/app/protocol.ts +63 -1
- package/src/app/room-log.ts +148 -14
- package/src/app/rooms.ts +23 -0
- package/src/app/routes.ts +278 -156
- package/src/app/skill-doc-routes.ts +122 -0
- package/src/app/socket.ts +309 -77
- package/src/app/sockets.ts +108 -20
- package/src/app/surface.ts +72 -36
- package/src/app/team-routes.ts +457 -0
- package/src/app/threads.ts +14 -0
- package/src/app/uploads.ts +33 -7
- package/src/auth/authenticator.ts +71 -8
- package/src/auth/next.ts +52 -0
- package/src/auth/oidc-pages.ts +60 -0
- package/src/auth/oidc.ts +611 -0
- package/src/auth/sessions.ts +193 -0
- package/src/authz.ts +172 -0
- package/src/console-api.ts +122 -25
- package/src/console-server.ts +70 -6
- package/src/gate.ts +90 -0
- package/src/host/access.ts +161 -0
- package/src/host/cards.ts +99 -0
- package/src/host/context.ts +141 -0
- package/src/host/forms.ts +237 -0
- package/src/host/frames.ts +342 -0
- package/src/host/host-app.ts +176 -0
- package/src/host/host-context.ts +49 -0
- package/src/host/host-routes.ts +363 -0
- package/src/host/pages.ts +266 -0
- package/src/host/reads.ts +155 -0
- package/src/host/retention.ts +41 -0
- package/src/host/room-views.ts +137 -0
- package/src/host/rooms-service.ts +414 -0
- package/src/host/stored-json.ts +13 -0
- package/src/host/views.ts +103 -0
- package/src/i18n/zh-tw.ts +309 -10
- package/src/layout-order.ts +210 -0
- package/src/notices/api.ts +43 -0
- package/src/notices/mount.ts +292 -0
- package/src/notices/service.ts +294 -0
- package/src/notices/store.ts +298 -0
- package/src/notices/urls.ts +38 -0
- package/src/onboarding/api.ts +32 -0
- package/src/onboarding/service.ts +149 -0
- package/src/onboarding/steps.ts +75 -0
- package/src/onboarding/waivers.ts +40 -0
- package/src/options.ts +498 -34
- package/src/profile/api.ts +175 -0
- package/src/profile/links.ts +162 -0
- package/src/push/api.ts +189 -52
- package/src/push/dispatch.ts +156 -0
- package/src/push/drain.ts +139 -0
- package/src/push/mutes.ts +17 -0
- package/src/push/outbox.ts +218 -0
- package/src/push/prefs.ts +118 -0
- package/src/push/sender.ts +7 -2
- package/src/push/triggers.ts +56 -5
- package/src/push/vapid.ts +19 -0
- package/src/push/web-push-transport.ts +106 -0
- package/src/push-sender/index.ts +14 -0
- package/src/push-sender/run.ts +118 -0
- package/src/push-sender/session.ts +54 -0
- package/src/visitors.ts +94 -6
- package/src/web-app.ts +674 -0
- package/src/web-plugin.ts +293 -26
- package/dist/chunk-action-menu-7v5fce2v.js +0 -1
- package/dist/chunk-agent-2rw4ydw2.js +0 -1
- package/dist/chunk-agent-sheet-2q24dkkf.js +0 -3
- package/dist/chunk-character-creator-8q6ympxj.js +0 -1
- package/dist/chunk-codeeditor-fgmq298t.js +0 -2
- package/dist/chunk-connectors-mr53r7fh.js +0 -1
- package/dist/chunk-conversations-4vy2z6w9.js +0 -1
- package/dist/chunk-entry-jg1eem82.js +0 -1
- package/dist/chunk-group-scene-na95b1te.js +0 -1
- package/dist/chunk-index-11rjg8a8.js +0 -1
- package/dist/chunk-index-1y50d6sj.js +0 -2
- package/dist/chunk-index-5y2r0eaj.js +0 -47
- package/dist/chunk-index-7fphxxzd.js +0 -14
- package/dist/chunk-index-80f1zkp6.css +0 -1
- package/dist/chunk-index-87h45frg.js +0 -2
- package/dist/chunk-index-bnwmssyn.js +0 -2
- package/dist/chunk-index-ckgbszwt.js +0 -51
- package/dist/chunk-index-fmjjekhd.js +0 -2
- package/dist/chunk-index-t1m65yn6.js +0 -13
- package/dist/chunk-index-t711t17m.js +0 -2
- package/dist/chunk-index-txv7qvyy.js +0 -2
- package/dist/chunk-index-wr3dg24d.js +0 -2
- package/dist/chunk-index-yx683d5n.js +0 -2
- package/dist/chunk-lang-css-fkdg3x8x.js +0 -1
- package/dist/chunk-lang-html-v8h9g977.js +0 -1
- package/dist/chunk-lang-javascript-ykb2vhaa.js +0 -1
- package/dist/chunk-lang-json-2rze3rpy.js +0 -1
- package/dist/chunk-lang-markdown-6w2htnfc.js +0 -6
- package/dist/chunk-lang-rust-24fggcqx.js +0 -1
- package/dist/chunk-lang-sql-gjywb9ke.js +0 -2
- package/dist/chunk-lang-typescript-qh6rebyb.js +0 -1
- package/dist/chunk-menu-items-zjpyj51b.js +0 -2
- package/dist/chunk-notes-sjba7kw3.js +0 -1
- package/dist/chunk-overview-26r77kb6.js +0 -1
- package/dist/chunk-room-confirms-851184ep.js +0 -1
- package/dist/chunk-room-menu-8y9c026y.js +0 -1
- package/dist/chunk-skills-56s4hdbn.js +0 -1
- package/dist/chunk-table-qykpvp32.js +0 -1
- package/dist/chunk-table2d-k60ressj.js +0 -1
- package/dist/chunk-table3d-bv2ajk39.js +0 -4220
- package/dist/chunk-transcript-vekz1mh6.js +0 -1
- package/dist/chunk-workspace-hevefd0d.js +0 -1
- package/dist/chunk-zh-tw-5r2wjre0.js +0 -1
package/src/api-types.ts
CHANGED
|
@@ -16,9 +16,22 @@ export const PANES = [
|
|
|
16
16
|
"schedules",
|
|
17
17
|
"table",
|
|
18
18
|
"workspace",
|
|
19
|
+
"profile",
|
|
19
20
|
] as const;
|
|
20
21
|
export type PaneName = (typeof PANES)[number];
|
|
21
22
|
|
|
23
|
+
/** The panes only an owner is served: they show the whole host, which a member's view never does. */
|
|
24
|
+
export const OWNER_ONLY_PANES = [
|
|
25
|
+
"overview",
|
|
26
|
+
"skills",
|
|
27
|
+
"connectors",
|
|
28
|
+
"table",
|
|
29
|
+
"workspace",
|
|
30
|
+
] as const satisfies readonly PaneName[];
|
|
31
|
+
|
|
32
|
+
/** A person's tier in the app, as `IDENTITY` gives it. */
|
|
33
|
+
export type AppTier = "owner" | "admin" | "member";
|
|
34
|
+
|
|
22
35
|
/** What the page needs before it can draw anything. */
|
|
23
36
|
export interface ConfigView {
|
|
24
37
|
title: string;
|
|
@@ -41,7 +54,50 @@ export interface ConfigView {
|
|
|
41
54
|
surface: string;
|
|
42
55
|
/** The WebSocket subprotocol to offer. */
|
|
43
56
|
protocol: string;
|
|
57
|
+
/** False when the host leaves the yolo and long-running switches out (`modes: false`). */
|
|
58
|
+
modes?: false;
|
|
44
59
|
};
|
|
60
|
+
/** The visitor's tier. Sent only where the host admits more than its owners (`admission: "access"`). */
|
|
61
|
+
tier?: AppTier;
|
|
62
|
+
/** The steps still undone before the app opens; present only while the gate is closed. */
|
|
63
|
+
gate?: GateStepView[];
|
|
64
|
+
/** The host's pages that are not rooms, as far as this visitor may see them. */
|
|
65
|
+
pages?: PageLinkView[];
|
|
66
|
+
/** The kinds of host room this visitor may list. */
|
|
67
|
+
kinds?: KindLinkView[];
|
|
68
|
+
/** Web Push for this visitor's tier: who may subscribe, and (for an external sender) where. */
|
|
69
|
+
push?: { tiers: AppTier[]; key?: string; subscribe?: string };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** A step of onboarding the visitor still owes. */
|
|
73
|
+
export interface GateStepView {
|
|
74
|
+
id: string;
|
|
75
|
+
label: string;
|
|
76
|
+
help?: string;
|
|
77
|
+
/**
|
|
78
|
+
* Set on the steps the app makes itself, so the page words them in its own language: `link`
|
|
79
|
+
* for `link:<provider>` (with the account's label in `label`), `push` for the notifications step.
|
|
80
|
+
*/
|
|
81
|
+
builtin?: "link" | "push";
|
|
82
|
+
/** A required step gates the app; a nudge is shown and never gates. */
|
|
83
|
+
required: boolean;
|
|
84
|
+
/** The page where the person completes it. */
|
|
85
|
+
open: { page: string };
|
|
86
|
+
/** The person may waive it; the label and the confirmation are what they are shown. */
|
|
87
|
+
waiver?: { label: string; confirm: string };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** A host page that is not a room, in the places the shell lists it. */
|
|
91
|
+
export interface PageLinkView {
|
|
92
|
+
id: string;
|
|
93
|
+
label: string;
|
|
94
|
+
place: "main" | "settings" | "hidden";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** A kind of host room, listed by its label. */
|
|
98
|
+
export interface KindLinkView {
|
|
99
|
+
kind: string;
|
|
100
|
+
label: string;
|
|
45
101
|
}
|
|
46
102
|
|
|
47
103
|
export type ChannelName =
|
|
@@ -300,6 +356,96 @@ export interface RoomMessage {
|
|
|
300
356
|
/** ISO time. */
|
|
301
357
|
at: string;
|
|
302
358
|
live?: string;
|
|
359
|
+
/**
|
|
360
|
+
* Host rooms: what kind of author wrote it (`external` text is untrusted and shown as plain
|
|
361
|
+
* text) and the label the host gives that kind, such as "Customer". Absent on the app's own.
|
|
362
|
+
*/
|
|
363
|
+
authorKind?: HostAuthorKind;
|
|
364
|
+
authorLabel?: string;
|
|
365
|
+
/** Host rooms: the author's picture, never an outside URL. */
|
|
366
|
+
picture?: PictureRef;
|
|
367
|
+
/** The text was changed after it was stored. */
|
|
368
|
+
edited?: true;
|
|
369
|
+
/** Files of the message that were deleted by retention. */
|
|
370
|
+
expiredFiles?: number;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** The kinds of author a host room's messages come from. */
|
|
374
|
+
export type HostAuthorKind = "external" | "member" | "assistant" | "notice";
|
|
375
|
+
|
|
376
|
+
/** A picture the page may draw: a stored file, a person's picture, or initials; never an outside URL. */
|
|
377
|
+
export type PictureRef =
|
|
378
|
+
| { file: string }
|
|
379
|
+
| { principal: string }
|
|
380
|
+
| { initials: string; color: string };
|
|
381
|
+
|
|
382
|
+
/** A button a host card or header offers, and the form it opens first. */
|
|
383
|
+
export interface HostActionView {
|
|
384
|
+
id: string;
|
|
385
|
+
label: string;
|
|
386
|
+
style?: "primary" | "secondary" | "danger";
|
|
387
|
+
confirm?: string;
|
|
388
|
+
form?: FormSpecView;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export interface FormSpecView {
|
|
392
|
+
title: string;
|
|
393
|
+
submit: string;
|
|
394
|
+
fields: FormFieldView[];
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export type FormFieldView =
|
|
398
|
+
| {
|
|
399
|
+
name: string;
|
|
400
|
+
label: string;
|
|
401
|
+
type: "text" | "textarea" | "url";
|
|
402
|
+
required?: boolean;
|
|
403
|
+
max?: number;
|
|
404
|
+
initial?: string;
|
|
405
|
+
hint?: string;
|
|
406
|
+
}
|
|
407
|
+
| {
|
|
408
|
+
name: string;
|
|
409
|
+
label: string;
|
|
410
|
+
type: "select" | "multiselect";
|
|
411
|
+
required?: boolean;
|
|
412
|
+
options: { value: string; label: string }[];
|
|
413
|
+
/** The value or values chosen when the form opens; a `select` takes one. */
|
|
414
|
+
initial?: string | readonly string[];
|
|
415
|
+
/** The page offers a filter box over the options, for a long list. */
|
|
416
|
+
search?: boolean;
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
/** A card a host puts in its room; its buttons call the host's `act`. */
|
|
420
|
+
export interface HostCardView {
|
|
421
|
+
id: string;
|
|
422
|
+
room: string;
|
|
423
|
+
title: string;
|
|
424
|
+
body: string;
|
|
425
|
+
tone?: "note" | "ok" | "warn" | "risk";
|
|
426
|
+
state?: "open" | "working" | "done" | "superseded" | "expired";
|
|
427
|
+
links?: { label: string; url: string }[];
|
|
428
|
+
/** Only the buttons this viewer may press. */
|
|
429
|
+
buttons?: HostActionView[];
|
|
430
|
+
/** ISO times. */
|
|
431
|
+
at: string;
|
|
432
|
+
updatedAt: string;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** A person-addressed notice, as the bell shows it. */
|
|
436
|
+
export interface NoticeView {
|
|
437
|
+
tag: string;
|
|
438
|
+
title: string;
|
|
439
|
+
body?: string;
|
|
440
|
+
open?:
|
|
441
|
+
| { room: string; message?: number }
|
|
442
|
+
| { page: string; query?: Record<string, string> };
|
|
443
|
+
urgency: "normal" | "high";
|
|
444
|
+
ackable: boolean;
|
|
445
|
+
/** ISO time. */
|
|
446
|
+
raisedAt: string;
|
|
447
|
+
/** This person has handled it. */
|
|
448
|
+
acked?: true;
|
|
303
449
|
}
|
|
304
450
|
|
|
305
451
|
/** One entry of the room list. */
|
|
@@ -310,8 +456,10 @@ export interface RoomView {
|
|
|
310
456
|
/** The agent's or group's name; a thread's id. */
|
|
311
457
|
name: string;
|
|
312
458
|
displayName: string;
|
|
313
|
-
/** The coordinator's
|
|
459
|
+
/** The room where the page opens: the coordinator's, else the first active agent's. */
|
|
314
460
|
home: boolean;
|
|
461
|
+
/** The coordinator's room: the team's entry, which cannot be archived. */
|
|
462
|
+
coordinator?: true;
|
|
315
463
|
/** The agent's picture, a path under the mount, for notification icons; absent for groups and threads. */
|
|
316
464
|
avatar?: string;
|
|
317
465
|
/** An agent room's character; a group page looks its members' up in the agent rooms. */
|
|
@@ -337,9 +485,40 @@ export interface RoomView {
|
|
|
337
485
|
stoppable: boolean;
|
|
338
486
|
/** The newest stored message, if any. */
|
|
339
487
|
last?: RoomMessage;
|
|
488
|
+
/** Messages past this person's read cursor; absent when none. */
|
|
489
|
+
unread?: number;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** Where one person has read a room up to, with the name the page shows for them. */
|
|
493
|
+
export interface ReadMarkView {
|
|
494
|
+
principal: string;
|
|
495
|
+
name: string;
|
|
496
|
+
/** The id of the newest message they have read. */
|
|
497
|
+
upTo: number;
|
|
498
|
+
/** ISO time. */
|
|
499
|
+
at: string;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** An owner-made folder of rooms, with its rooms in the owner's order. */
|
|
503
|
+
export interface FolderView {
|
|
504
|
+
/** `folder.<uuid>`. */
|
|
505
|
+
id: string;
|
|
506
|
+
name: string;
|
|
507
|
+
/** Room keys in order. */
|
|
508
|
+
rooms: string[];
|
|
340
509
|
}
|
|
341
510
|
|
|
342
|
-
|
|
511
|
+
/**
|
|
512
|
+
* The owners' shared arrangement of the room list: pinned rooms in their order, then the folders
|
|
513
|
+
* in theirs. A room is pinned, in one folder, or in neither; the rest are listed by latest activity.
|
|
514
|
+
*/
|
|
515
|
+
export interface LayoutView {
|
|
516
|
+
/** The pinned rooms' keys, in order. */
|
|
517
|
+
pins: string[];
|
|
518
|
+
folders: FolderView[];
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
export interface RoomsView extends LayoutView {
|
|
343
522
|
rooms: RoomView[];
|
|
344
523
|
}
|
|
345
524
|
|
|
@@ -354,18 +533,30 @@ export interface MessagesView {
|
|
|
354
533
|
* first; the page places each among the messages by `frame.at`.
|
|
355
534
|
*/
|
|
356
535
|
cards: ClosedCard[];
|
|
536
|
+
/** The other readers' cursors: how far each has read the room (the viewer's own is `cursor`). */
|
|
537
|
+
reads?: ReadMarkView[];
|
|
538
|
+
/** The viewer's own read cursor before they opened the room; absent when they never read it. */
|
|
539
|
+
cursor?: number;
|
|
357
540
|
}
|
|
358
541
|
|
|
359
542
|
/** Who the page is signed in as. */
|
|
360
543
|
export interface SessionView {
|
|
361
544
|
principal: PrincipalName;
|
|
362
545
|
surface: string;
|
|
363
|
-
/** How the console signs the person in; the page offers Log out under `password`. */
|
|
364
|
-
auth: "none" | "password" | "verifier";
|
|
546
|
+
/** How the console signs the person in; the page offers Log out under `password` and `oidc`. */
|
|
547
|
+
auth: "none" | "password" | "verifier" | "oidc";
|
|
548
|
+
/** The visitor's tier. Sent only where the host admits more than its owners (`admission: "access"`). */
|
|
549
|
+
tier?: AppTier;
|
|
550
|
+
/** The steps still undone before the app opens; present only while the gate is closed. */
|
|
551
|
+
gate?: GateStepView[];
|
|
365
552
|
/** When the credential lapses, if it does. */
|
|
366
553
|
expiresAt?: string;
|
|
367
554
|
/** Web Push is available: the app serves the rooms and a key for it. */
|
|
368
555
|
push?: true;
|
|
556
|
+
/** The host has prompt documents the owner edits (`api/prompts`). */
|
|
557
|
+
prompts?: true;
|
|
558
|
+
/** The host has the skills addon, so an agent-written skill's SKILL.md can be edited and made here (`api/skill-docs`). */
|
|
559
|
+
skillEdit?: true;
|
|
369
560
|
/** When an agent's post and a notice are not pushed (a card still is); absent = at any hour. */
|
|
370
561
|
quietHours?: {
|
|
371
562
|
/** `HH:MM`, where the window begins. */
|
|
@@ -394,6 +585,85 @@ export interface PushDevicesView {
|
|
|
394
585
|
devices: PushDeviceView[];
|
|
395
586
|
}
|
|
396
587
|
|
|
588
|
+
/** A test push was queued; the device's receipt for `tag` completes the push step. */
|
|
589
|
+
export interface PushTestView {
|
|
590
|
+
tag: string;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/** The person's own push preferences. */
|
|
594
|
+
export interface PushPrefsView {
|
|
595
|
+
/** No push for a normal notice from `start` to `end` on the clock of `timeZone`; absent = none set. */
|
|
596
|
+
quiet?: { start: string; end: string; timeZone: string };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/** The person's notices that are not resolved, newest first, and the count the app icon shows. */
|
|
600
|
+
export interface NoticesView {
|
|
601
|
+
notices: NoticeView[];
|
|
602
|
+
badge: number;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/** One account the person may link, as the profile page shows it. */
|
|
606
|
+
export interface LinkView {
|
|
607
|
+
provider: string;
|
|
608
|
+
label: string;
|
|
609
|
+
/** What linking it is for this deployment: `required` gates the app, `nudge` asks; absent = optional. */
|
|
610
|
+
step?: "required" | "nudge";
|
|
611
|
+
/** Providers to link first, and those of them not linked yet (`issue` is refused until none is left). */
|
|
612
|
+
requires: string[];
|
|
613
|
+
blockedBy: string[];
|
|
614
|
+
linked?: {
|
|
615
|
+
displayName?: string;
|
|
616
|
+
/** ISO time. */
|
|
617
|
+
linkedAt: string;
|
|
618
|
+
/** The configuration or the CLI made the link, so it cannot be removed here. */
|
|
619
|
+
locked?: true;
|
|
620
|
+
};
|
|
621
|
+
pending?: {
|
|
622
|
+
request: string;
|
|
623
|
+
/** ISO time. */
|
|
624
|
+
expiresAt: string;
|
|
625
|
+
redeemedBy?: { displayName: string; picture?: PictureRef };
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
export interface LinksView {
|
|
630
|
+
links: LinkView[];
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/** The code a person puts into the other service to prove an account. */
|
|
634
|
+
export interface LinkIssuedView {
|
|
635
|
+
code: string;
|
|
636
|
+
request: string;
|
|
637
|
+
/** ISO time. */
|
|
638
|
+
expiresAt: string;
|
|
639
|
+
instructions: string;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** How the person is shown, and which of their accounts they may pick to show. */
|
|
643
|
+
export interface ProfileView {
|
|
644
|
+
name: string;
|
|
645
|
+
/** The account the name comes from; absent when it is the person's own name. */
|
|
646
|
+
source?: string;
|
|
647
|
+
/** A picture is served at `api/people/<principal>/picture`. */
|
|
648
|
+
picture: boolean;
|
|
649
|
+
principal: string;
|
|
650
|
+
tier?: AppTier;
|
|
651
|
+
/** The accounts the person linked that can name them; the page shows these as the choice. */
|
|
652
|
+
sources: { provider: string; label: string }[];
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/** A step of onboarding as the page of steps shows it. */
|
|
656
|
+
export interface OnboardingStepView extends GateStepView {
|
|
657
|
+
done: boolean;
|
|
658
|
+
waived: boolean;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
export interface OnboardingView {
|
|
662
|
+
steps: OnboardingStepView[];
|
|
663
|
+
/** Push on the person's devices: how many are subscribed and when one last reported back. */
|
|
664
|
+
push: { devices: number; lastReceiptAt?: string; waived: boolean };
|
|
665
|
+
}
|
|
666
|
+
|
|
397
667
|
/** A schedule, labelled by the room it belongs to or else its channel. */
|
|
398
668
|
export interface ScheduleView {
|
|
399
669
|
id: number;
|
|
@@ -445,6 +715,193 @@ export interface AgentPatch {
|
|
|
445
715
|
thinking?: string;
|
|
446
716
|
}
|
|
447
717
|
|
|
718
|
+
/** One change of a prompt, as `GET api/agents/<name>/prompt-history` and `GET api/prompts/<id>/history` list them, newest first. */
|
|
719
|
+
export interface PromptVersionView {
|
|
720
|
+
id: number;
|
|
721
|
+
/** `set` put new text in force; `revert` took a document back to its file version. */
|
|
722
|
+
action: "set" | "revert";
|
|
723
|
+
/** The text in force after the change; for a revert, the file version it went back to. */
|
|
724
|
+
text: string;
|
|
725
|
+
/** The text the change replaced. */
|
|
726
|
+
previous: string;
|
|
727
|
+
/** Who made it. */
|
|
728
|
+
by: string;
|
|
729
|
+
/** ISO time. */
|
|
730
|
+
at: string;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
export interface PromptHistoryView {
|
|
734
|
+
versions: PromptVersionView[];
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/** A prompt document in the list of `GET api/prompts`: the owner-editable texts the host registered. */
|
|
738
|
+
export interface PromptSummary {
|
|
739
|
+
id: string;
|
|
740
|
+
label: string;
|
|
741
|
+
description?: string;
|
|
742
|
+
/** The owner's text is in force and is not the file version. */
|
|
743
|
+
differs: boolean;
|
|
744
|
+
/** Who set it and when (ISO), while an override is in force. */
|
|
745
|
+
override?: { by: string; at: string };
|
|
746
|
+
/** Why the file version cannot be read; the document cannot be changed until it can, and the others are unaffected. */
|
|
747
|
+
broken?: string;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
export interface PromptsView {
|
|
751
|
+
documents: PromptSummary[];
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/** `GET api/prompts/<id>`: the document with its text in force and its file version. */
|
|
755
|
+
export interface PromptView extends PromptSummary {
|
|
756
|
+
/** The text in force. */
|
|
757
|
+
text: string;
|
|
758
|
+
/** The file version, which `revert` returns to. */
|
|
759
|
+
fileText: string;
|
|
760
|
+
/** The most characters a save may have. */
|
|
761
|
+
maxChars: number;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/** The body of `PUT api/prompts/<id>`: the new text, and the text it was edited from. */
|
|
765
|
+
export interface PromptSave {
|
|
766
|
+
text: string;
|
|
767
|
+
base: string;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/** The body of `POST api/prompts/<id>/revert`: the text in force that the owner saw when they asked. */
|
|
771
|
+
export interface PromptRevert {
|
|
772
|
+
base: string;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/** A 409: the document is not as the writer saw it. */
|
|
776
|
+
export interface PromptConflict extends ApiError {
|
|
777
|
+
conflict: "changed";
|
|
778
|
+
current: PromptView;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/** A skill's SKILL.md as `GET api/skill-docs/<name>` answers: the whole file, with the hash a save names. */
|
|
782
|
+
export interface SkillDocView {
|
|
783
|
+
name: string;
|
|
784
|
+
source: SkillView["source"];
|
|
785
|
+
text: string;
|
|
786
|
+
hash: string;
|
|
787
|
+
/** An agent-written skill; the others are read-only here, for the reason their source gives. */
|
|
788
|
+
writable: boolean;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/** The body of `PUT api/skill-docs/<name>`: the whole file, and the hash it was edited from. */
|
|
792
|
+
export interface SkillDocSave {
|
|
793
|
+
text: string;
|
|
794
|
+
baseHash: string;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/** The body of `POST api/skill-docs`, which makes a skill from a whole SKILL.md. */
|
|
798
|
+
export interface SkillDocCreate {
|
|
799
|
+
text: string;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/** A 409: the skill is not as the writer saw it. */
|
|
803
|
+
export interface SkillConflict extends ApiError {
|
|
804
|
+
conflict: "changed";
|
|
805
|
+
current: SkillDocView;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
export interface SkillVersionView {
|
|
809
|
+
id: number;
|
|
810
|
+
text: string;
|
|
811
|
+
hash: string;
|
|
812
|
+
by: string;
|
|
813
|
+
/** ISO time. */
|
|
814
|
+
at: string;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
export interface SkillHistoryView {
|
|
818
|
+
versions: SkillVersionView[];
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* The body of `POST api/agents`, which makes an agent: the fields of the `agent_create` tool, with
|
|
823
|
+
* its model and thinking level. `name` is permanent (lowercase letters, digits and dashes, 1 to 32).
|
|
824
|
+
* `task` is its opening task; without it the agent only exists. The 201 answer is its profile.
|
|
825
|
+
*/
|
|
826
|
+
export interface AgentCreate {
|
|
827
|
+
name: string;
|
|
828
|
+
displayName: string;
|
|
829
|
+
prompt: string;
|
|
830
|
+
model?: string;
|
|
831
|
+
thinking?: string;
|
|
832
|
+
task?: string;
|
|
833
|
+
/** Fields of its look; any left out come from the look its role suggests. */
|
|
834
|
+
character?: CharacterInput;
|
|
835
|
+
/** Skills it carries, as the Skills pane names them. */
|
|
836
|
+
skills?: string[];
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/** A skill as the agent sheet offers it: the host's catalog, and whether this agent carries it. */
|
|
840
|
+
export interface SkillChoiceView {
|
|
841
|
+
name: string;
|
|
842
|
+
description?: string;
|
|
843
|
+
source: "builtin" | "linked" | "written" | "package";
|
|
844
|
+
/** Built-in skills are carried by every agent and cannot be taken off. */
|
|
845
|
+
builtin: boolean;
|
|
846
|
+
carried: boolean;
|
|
847
|
+
/** Why its file cannot be read, for a skill that is listed but broken. */
|
|
848
|
+
missing?: string;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/** `GET api/agents/<name>/skills` and the answer of `PUT`: every skill the host has. */
|
|
852
|
+
export interface AgentSkillsView {
|
|
853
|
+
skills: SkillChoiceView[];
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/** The body of `PUT api/agents/<name>/skills`: the same fields as the `agent_skills` tool. */
|
|
857
|
+
export interface AgentSkillsChange {
|
|
858
|
+
add?: string[];
|
|
859
|
+
remove?: string[];
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/** A group as the app edits it. */
|
|
863
|
+
export interface GroupProfileView {
|
|
864
|
+
name: string;
|
|
865
|
+
displayName: string;
|
|
866
|
+
/** Its members, by agent name, in speaking-tie order. */
|
|
867
|
+
members: string[];
|
|
868
|
+
/** The member who answers when nobody else fits. */
|
|
869
|
+
host: string;
|
|
870
|
+
room: string;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/** The body of `POST api/groups`; the host is the first member unless given. The 201 answer is the group. */
|
|
874
|
+
export interface GroupCreate {
|
|
875
|
+
name: string;
|
|
876
|
+
displayName: string;
|
|
877
|
+
members: string[];
|
|
878
|
+
host?: string;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/** The body of `PATCH api/groups/<name>`; `members` is the whole new list. */
|
|
882
|
+
export interface GroupPatch {
|
|
883
|
+
displayName?: string;
|
|
884
|
+
members?: string[];
|
|
885
|
+
host?: string;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/** An archived agent or group, kept with its history until it is restored or deleted. */
|
|
889
|
+
export interface ArchivedEntry {
|
|
890
|
+
kind: "agent" | "group";
|
|
891
|
+
name: string;
|
|
892
|
+
displayName: string;
|
|
893
|
+
/** An agent's picture, a path under the mount. */
|
|
894
|
+
avatar?: string;
|
|
895
|
+
character?: CharacterSpec;
|
|
896
|
+
/** A group's members that are still on the team. */
|
|
897
|
+
members?: string[];
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/** `GET api/archive`. */
|
|
901
|
+
export interface ArchiveView {
|
|
902
|
+
entries: ArchivedEntry[];
|
|
903
|
+
}
|
|
904
|
+
|
|
448
905
|
/** The body of `PUT api/agents/<name>/character`: at most 4 KiB. */
|
|
449
906
|
export interface CharacterRequest {
|
|
450
907
|
character: CharacterInput;
|
|
@@ -587,7 +1044,12 @@ export type RefusalCode =
|
|
|
587
1044
|
| "unknown_upload"
|
|
588
1045
|
| "empty"
|
|
589
1046
|
| "too_long"
|
|
590
|
-
| "not_accepted"
|
|
1047
|
+
| "not_accepted"
|
|
1048
|
+
/** A required onboarding step stands undone, so the frame is not taken yet. */
|
|
1049
|
+
| "gated";
|
|
1050
|
+
|
|
1051
|
+
/** The values of a form a host action opened. */
|
|
1052
|
+
export type FormInput = Readonly<Record<string, string | readonly string[]>>;
|
|
591
1053
|
|
|
592
1054
|
/** What the page sends: one JSON object per WebSocket text message. */
|
|
593
1055
|
export type ClientFrame =
|
|
@@ -604,8 +1066,27 @@ export type ClientFrame =
|
|
|
604
1066
|
text: string;
|
|
605
1067
|
/** Files uploaded with `POST api/uploads`, by the `id` each returned. */
|
|
606
1068
|
uploads?: string[];
|
|
1069
|
+
/** Host rooms: a `note` only records, a `turn` is also delivered to the host's claim. Default `turn`; an agent room takes only `turn`. */
|
|
1070
|
+
mode?: "note" | "turn";
|
|
607
1071
|
}
|
|
608
1072
|
| { type: "stop"; room: string }
|
|
1073
|
+
/** Host rooms: follow a room's frames. At most 200 at once; the viewer must be able to read it. */
|
|
1074
|
+
| { type: "subscribe"; room: string }
|
|
1075
|
+
| { type: "unsubscribe"; room: string }
|
|
1076
|
+
/** A host room's or page's action: a card's button, a header action, a form. */
|
|
1077
|
+
| {
|
|
1078
|
+
type: "act";
|
|
1079
|
+
id: string;
|
|
1080
|
+
room?: string;
|
|
1081
|
+
page?: string;
|
|
1082
|
+
action: string;
|
|
1083
|
+
card?: string;
|
|
1084
|
+
input?: FormInput;
|
|
1085
|
+
}
|
|
1086
|
+
/** The person handled a notice. */
|
|
1087
|
+
| { type: "ack"; tag: string }
|
|
1088
|
+
/** The person has read a room up to this message (a cursor never moves back). */
|
|
1089
|
+
| { type: "read"; room: string; upTo: number }
|
|
609
1090
|
| { type: "approval"; prompt: string; approved: boolean }
|
|
610
1091
|
| { type: "answer"; prompt: string; answer: PromptAnswer }
|
|
611
1092
|
| {
|
|
@@ -627,6 +1108,8 @@ export type ServerFrame =
|
|
|
627
1108
|
name: string;
|
|
628
1109
|
/** The most files one message carries and the most bytes of one. */
|
|
629
1110
|
uploads: { perMessage: number; maxBytes: number };
|
|
1111
|
+
/** The required steps the person still owes, when there are any: the socket then carries only the gate and their notices. */
|
|
1112
|
+
gate?: GateStepView[];
|
|
630
1113
|
}
|
|
631
1114
|
/** The message `id` was taken into `room`; `message` is how it was stored. */
|
|
632
1115
|
| { type: "accepted"; id: string; room: string; message: RoomMessage }
|
|
@@ -670,7 +1153,33 @@ export type ServerFrame =
|
|
|
670
1153
|
/** Yolo mode approved a held call by itself. */
|
|
671
1154
|
| { type: "auto_approved"; room: string; tool: string; action: string }
|
|
672
1155
|
/** The team or the room list may differ: refetch. */
|
|
673
|
-
| { type: "changed" }
|
|
1156
|
+
| { type: "changed" }
|
|
1157
|
+
/** A host room's message, edit or removal, and its cards; sent to the sockets subscribed to the room. */
|
|
1158
|
+
| { type: "host_message"; room: string; message: RoomMessage }
|
|
1159
|
+
| { type: "host_edited"; room: string; message: RoomMessage }
|
|
1160
|
+
| { type: "host_removed"; room: string; id: number }
|
|
1161
|
+
| { type: "host_card"; room: string; card: HostCardView }
|
|
1162
|
+
/** A host room's header or access may differ: refetch it. */
|
|
1163
|
+
| { type: "room_changed"; room: string }
|
|
1164
|
+
/** The list of a kind of host room may differ: refetch it. */
|
|
1165
|
+
| { type: "list_changed"; kind: string }
|
|
1166
|
+
| { type: "page_changed"; page: string }
|
|
1167
|
+
| { type: "notice"; notice: NoticeView }
|
|
1168
|
+
| { type: "notice_resolved"; tag: string }
|
|
1169
|
+
/** The count the app icon shows: notices not handled. */
|
|
1170
|
+
| { type: "badge"; count: number }
|
|
1171
|
+
/** The steps still undone; empty when the gate opened. */
|
|
1172
|
+
| { type: "gate"; steps: GateStepView[] }
|
|
1173
|
+
/** Someone read a room up to a message; `name` and `at` let the page show them without asking. */
|
|
1174
|
+
| {
|
|
1175
|
+
type: "read";
|
|
1176
|
+
room: string;
|
|
1177
|
+
principal: string;
|
|
1178
|
+
name: string;
|
|
1179
|
+
upTo: number;
|
|
1180
|
+
/** ISO time. */
|
|
1181
|
+
at: string;
|
|
1182
|
+
};
|
|
674
1183
|
|
|
675
1184
|
/** A turn's live view, as the core reports it. */
|
|
676
1185
|
export type AppProgress =
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { readLimited } from "../request-body.ts";
|
|
2
|
+
|
|
3
|
+
/** The largest JSON body a write reads: 64 KiB. */
|
|
4
|
+
export const MAX_BODY_BYTES = 64 * 1024;
|
|
5
|
+
|
|
6
|
+
export class HttpError extends Error {
|
|
7
|
+
constructor(
|
|
8
|
+
readonly status: number,
|
|
9
|
+
message: string,
|
|
10
|
+
) {
|
|
11
|
+
super(message);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const bad = (message: string) => new HttpError(400, message);
|
|
16
|
+
export const notFound = () =>
|
|
17
|
+
new HttpError(404, "There is no such room, agent or file.");
|
|
18
|
+
|
|
19
|
+
export async function bodyOf(
|
|
20
|
+
request: Request,
|
|
21
|
+
limit = MAX_BODY_BYTES,
|
|
22
|
+
): Promise<Record<string, unknown>> {
|
|
23
|
+
const raw = await readLimited(request, limit);
|
|
24
|
+
if (!raw) throw new HttpError(413, "The body is too large.");
|
|
25
|
+
let value: unknown;
|
|
26
|
+
try {
|
|
27
|
+
value = JSON.parse(raw.toString("utf8"));
|
|
28
|
+
} catch {
|
|
29
|
+
throw bad("The body is not JSON.");
|
|
30
|
+
}
|
|
31
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
32
|
+
throw bad("The body is not an object.");
|
|
33
|
+
return value as Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const optionalText = (
|
|
37
|
+
body: Record<string, unknown>,
|
|
38
|
+
key: string,
|
|
39
|
+
): string | undefined => {
|
|
40
|
+
const value = body[key];
|
|
41
|
+
if (value === undefined) return undefined;
|
|
42
|
+
if (typeof value !== "string") throw bad(`${key} must be text.`);
|
|
43
|
+
return value;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const requiredText = (
|
|
47
|
+
body: Record<string, unknown>,
|
|
48
|
+
key: string,
|
|
49
|
+
): string => {
|
|
50
|
+
const value = body[key];
|
|
51
|
+
if (typeof value !== "string" || value.trim() === "")
|
|
52
|
+
throw bad(`${key} must be text.`);
|
|
53
|
+
return value;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** A list of text, or undefined when the body does not name it. */
|
|
57
|
+
export const optionalList = (
|
|
58
|
+
body: Record<string, unknown>,
|
|
59
|
+
key: string,
|
|
60
|
+
): string[] | undefined => {
|
|
61
|
+
const value = body[key];
|
|
62
|
+
if (value === undefined) return undefined;
|
|
63
|
+
if (!Array.isArray(value) || value.some((one) => typeof one !== "string"))
|
|
64
|
+
throw bad(`${key} must be a list of text.`);
|
|
65
|
+
return value as string[];
|
|
66
|
+
};
|