pi-roundtable-web 0.25.0 → 0.26.1
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 +54 -0
- package/README.md +165 -12
- package/dist/account-zk4efb87.css +102 -0
- package/dist/chunk-account-ghq105t4.js +1 -0
- package/dist/chunk-action-menu-me0g9qsc.js +1 -0
- package/dist/{chunk-agent-0pm9p3nn.js → chunk-agent-rdv7ekrt.js} +1 -1
- package/dist/chunk-arranged-rooms-81rqhx9x.js +1 -0
- package/dist/chunk-character-creator-bd426yjw.js +1 -0
- package/dist/chunk-codeeditor-ajhgd33t.js +2 -0
- package/dist/chunk-confirm-p3mts8mx.js +1 -0
- package/dist/{chunk-connectors-anhghy3k.js → chunk-connectors-bkv3xn83.js} +1 -1
- package/dist/chunk-conversations-0e0vbxwr.js +1 -0
- package/dist/{chunk-drag-q4fx9b22.js → chunk-drag-peyb8t0k.js} +1 -1
- package/dist/{chunk-entry-he1embgp.js → chunk-entry-mv7q8br0.js} +1 -1
- package/dist/chunk-folder-list-era3tksh.js +1 -0
- package/dist/chunk-folds-hvws0bd5.js +1 -0
- package/dist/chunk-gate-hc7dd0fx.js +1 -0
- package/dist/chunk-group-scene-dkbpmbh5.js +1 -0
- package/dist/chunk-group-table-fqmxgdfw.js +1 -0
- package/dist/chunk-host-icons-4xde9ntk.js +1 -0
- package/dist/chunk-host-picture-014yzk20.js +1 -0
- package/dist/chunk-host-state-t56e5sdy.js +1 -0
- package/dist/chunk-host-ui-p1zy9nmp.js +1 -0
- package/dist/chunk-index-0fmmvgee.js +2 -0
- package/dist/chunk-index-0q15ysrb.js +1 -0
- package/dist/chunk-index-0xh24y25.js +2 -0
- package/dist/chunk-index-2yebd9tb.js +426 -0
- package/dist/chunk-index-4t0r3t3b.js +2 -0
- package/dist/chunk-index-5yzfrf7b.js +138 -0
- package/dist/{chunk-index-d5abvq7v.js → chunk-index-6twb7ccd.js} +2 -2
- package/dist/chunk-index-73gh7kgr.js +920 -0
- package/dist/chunk-index-7xffskfa.js +8 -0
- package/dist/chunk-index-aqw7cy5j.js +1 -0
- package/dist/chunk-index-b6ca02e8.js +2 -0
- package/dist/chunk-index-b8nvhcx5.js +2 -0
- package/dist/{chunk-index-bvfqjh77.js → chunk-index-cvbdf2ca.js} +2 -2
- package/dist/chunk-index-czdketze.js +2 -0
- package/dist/{chunk-index-9jqcskb4.js → chunk-index-dt9775ka.js} +5 -5
- package/dist/chunk-index-eg55mqqe.js +1 -0
- package/dist/chunk-index-gr6e0hse.js +2 -0
- package/dist/chunk-index-gyary7np.js +13 -0
- package/dist/chunk-index-j214tczc.js +1 -0
- package/dist/chunk-index-jd6632ef.js +2 -0
- package/dist/chunk-index-jw7dtshr.js +2 -0
- package/dist/{chunk-index-krtk6nn2.js → chunk-index-k7t14rwa.js} +2 -2
- package/dist/chunk-index-kwcx62wb.js +4 -0
- package/dist/chunk-index-m7fvw92f.js +2 -0
- package/dist/chunk-index-mmg788a1.js +2 -0
- package/dist/chunk-index-mq0dacwr.js +2 -0
- package/dist/chunk-index-p53ak25v.js +2 -0
- package/dist/{chunk-index-27qca4mr.js → chunk-index-pk1yxkv4.js} +2 -2
- package/dist/{chunk-index-9b1kt7qw.css → chunk-index-rbcz9jd4.css} +1 -1
- package/dist/{chunk-index-s681nny9.js → chunk-index-rmxbdmn2.js} +2 -2
- package/dist/chunk-index-rnxj3z8k.js +2 -0
- package/dist/chunk-index-snzzfhk3.js +2 -0
- package/dist/chunk-index-v3bmr0nw.js +2 -0
- package/dist/chunk-index-vcyzwtk2.js +2 -0
- package/dist/{chunk-index-zfd00hwb.js → chunk-index-w2j3njb6.js} +2 -2
- package/dist/chunk-index-w31c4yjv.js +2 -0
- package/dist/chunk-index-wrm8w165.js +2 -0
- package/dist/chunk-index-xh0mg9cw.js +2 -0
- package/dist/chunk-index-ybtvvx85.js +2 -0
- package/dist/chunk-index-yzsgsvnp.js +2 -0
- package/dist/{chunk-index-18gxy79n.js → chunk-index-z9x4cnzf.js} +2 -2
- package/dist/chunk-index-zrvg23jd.js +2 -0
- package/dist/chunk-index-zynq0b0w.js +50 -0
- package/dist/chunk-lang-css-88v3hp3d.js +1 -0
- package/dist/{chunk-lang-dockerfile-d5mzxs60.js → chunk-lang-dockerfile-k728ewsf.js} +1 -1
- package/dist/{chunk-lang-go-830fmhba.js → chunk-lang-go-wjwgay4v.js} +3 -3
- package/dist/chunk-lang-html-07d47es4.js +1 -0
- package/dist/chunk-lang-javascript-fmhhgzwd.js +1 -0
- package/dist/{chunk-lang-json-5eaarz7j.js → chunk-lang-json-7gd89mvs.js} +1 -1
- package/dist/chunk-lang-markdown-b6qbe8bt.js +6 -0
- package/dist/{chunk-lang-python-fjw84w10.js → chunk-lang-python-3v56y31b.js} +2 -2
- package/dist/{chunk-lang-rust-5rbp2zdk.js → chunk-lang-rust-kh0z74d1.js} +1 -1
- package/dist/{chunk-lang-shell-vzagpym3.js → chunk-lang-shell-9zv0zvbx.js} +1 -1
- package/dist/{chunk-lang-sql-0c2w9c2y.js → chunk-lang-sql-yw6bf3t7.js} +2 -2
- package/dist/{chunk-lang-toml-yr9fv6eb.js → chunk-lang-toml-tg1r96nq.js} +1 -1
- package/dist/chunk-lang-typescript-etjjvr2g.js +1 -0
- package/dist/{chunk-lang-xml-eze08p42.js → chunk-lang-xml-ev1mndd4.js} +1 -1
- package/dist/{chunk-lang-yaml-c4b6ft3y.js → chunk-lang-yaml-fz5qpb3r.js} +1 -1
- package/dist/chunk-layout-actions-48ecgtkg.js +1 -0
- package/dist/{chunk-menu-items-jrwxycr1.js → chunk-menu-items-1epy3h0t.js} +2 -2
- package/dist/chunk-mode-toggle-fxmfxsa4.js +1 -0
- package/dist/chunk-note-row-kd0fy7ee.js +75 -0
- package/dist/chunk-notes-yb8f057c.js +1 -0
- package/dist/chunk-notice-bell-wr4f1rph.js +1 -0
- package/dist/chunk-notices-x07da5bb.js +1 -0
- package/dist/chunk-onboarding-csrkbh9z.js +1 -0
- package/dist/chunk-overview-avc2d30t.js +1 -0
- package/dist/chunk-palette-9nhp95bz.js +1 -0
- package/dist/chunk-placement-items-q694dpgk.js +1 -0
- package/dist/chunk-profile-6qemf3m7.js +1 -0
- package/dist/chunk-prompt-history-7enzp2p3.js +1 -0
- package/dist/chunk-prompts-nv6mrn0q.js +1 -0
- package/dist/chunk-room-confirms-zz7n61y7.js +1 -0
- package/dist/chunk-room-menu-rr6nzap1.js +1 -0
- package/dist/chunk-rooms-menu-button-zbtwj47d.js +1 -0
- package/dist/chunk-schedules-rw57nr0m.js +1 -0
- package/dist/{chunk-seat-menu-jv4s7hmj.js → chunk-seat-menu-ys5v9abz.js} +1 -1
- package/dist/chunk-sheet-hosts-jhvaacst.js +1 -0
- package/dist/chunk-skill-editor-sz8an8qr.js +8 -0
- package/dist/chunk-skills-tnr1h16h.js +1 -0
- package/dist/chunk-switcher-list-17enhrse.js +1 -0
- package/dist/chunk-tab-dot-r6brdbmw.js +1 -0
- package/dist/chunk-table-r12cja6p.js +1 -0
- package/dist/chunk-table-seats-g3t392zz.js +1 -0
- package/dist/{chunk-table2d-4sje1fr0.js → chunk-table2d-p3ezsdcj.js} +1 -1
- package/dist/{chunk-table3d-h64skazp.js → chunk-table3d-4x2n2b00.js} +23 -23
- package/dist/chunk-thread-actions-qc6ezkha.js +1 -0
- package/dist/chunk-transcript-n2s6dher.js +1 -0
- package/dist/chunk-where-f3scdydc.js +1 -0
- package/dist/chunk-workspace-xs023bxm.js +1 -0
- package/dist/chunk-zh-tw-efvmk3fd.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 +356 -4
- package/src/app/app.ts +201 -32
- package/src/app/desk.ts +47 -22
- package/src/app/files.ts +73 -7
- package/src/app/presence.ts +2 -2
- package/src/app/protocol.ts +63 -1
- package/src/app/room-log.ts +153 -14
- package/src/app/rooms.ts +39 -0
- package/src/app/routes.ts +168 -28
- package/src/app/socket.ts +349 -81
- package/src/app/sockets.ts +108 -20
- package/src/app/surface.ts +72 -36
- package/src/app/threads.ts +31 -2
- package/src/app/uploads.ts +33 -7
- package/src/auth/authenticator.ts +77 -8
- package/src/auth/login-page.ts +4 -2
- package/src/auth/next.ts +52 -0
- package/src/auth/oidc-pages.ts +62 -0
- package/src/auth/oidc.ts +649 -0
- package/src/auth/sessions.ts +193 -0
- package/src/authz.ts +173 -0
- package/src/console-api.ts +122 -25
- package/src/console-server.ts +102 -12
- package/src/gate.ts +90 -0
- package/src/host/access.ts +161 -0
- package/src/host/cards.ts +112 -0
- package/src/host/context.ts +188 -0
- package/src/host/forms.ts +240 -0
- package/src/host/frames.ts +349 -0
- package/src/host/host-app.ts +178 -0
- package/src/host/host-context.ts +49 -0
- package/src/host/host-routes.ts +396 -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 +429 -0
- package/src/host/stored-json.ts +13 -0
- package/src/host/views.ts +106 -0
- package/src/i18n/zh-tw.ts +139 -0
- package/src/index.ts +15 -1
- 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 +534 -34
- package/src/page-presentation.ts +47 -2
- 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/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/pwa.ts +32 -4
- package/src/visitors.ts +94 -6
- package/src/web-app.ts +718 -0
- package/src/web-plugin.ts +279 -26
- package/dist/chunk-action-menu-qe839g4b.js +0 -1
- package/dist/chunk-arranged-rooms-enpfswmg.js +0 -1
- package/dist/chunk-character-creator-6yf29xwx.js +0 -1
- package/dist/chunk-codeeditor-gdbdg7rp.js +0 -2
- package/dist/chunk-confirm-xrw7d732.js +0 -1
- package/dist/chunk-conversations-ry228aes.js +0 -1
- package/dist/chunk-folder-list-5cp8stna.js +0 -1
- package/dist/chunk-folds-hff104tk.js +0 -1
- package/dist/chunk-group-scene-h228jdat.js +0 -1
- package/dist/chunk-index-0p5h2kdz.js +0 -2
- package/dist/chunk-index-0pt08yd9.js +0 -1
- package/dist/chunk-index-12bw9yj8.js +0 -2
- package/dist/chunk-index-2ad17eh4.js +0 -2
- package/dist/chunk-index-31wh5twj.js +0 -52
- package/dist/chunk-index-4dy94pbz.js +0 -2
- package/dist/chunk-index-4ecjtdy8.js +0 -13
- package/dist/chunk-index-4v1b7ks9.js +0 -2
- package/dist/chunk-index-5d8yjj4m.js +0 -2
- package/dist/chunk-index-89ajmxh7.js +0 -2
- package/dist/chunk-index-b9ny4623.js +0 -2
- package/dist/chunk-index-e1dr6g10.js +0 -2
- package/dist/chunk-index-gpkzhq1g.js +0 -2
- package/dist/chunk-index-hk3xyy20.js +0 -2
- package/dist/chunk-index-jm23vn03.js +0 -4
- package/dist/chunk-index-k7gh0jvz.js +0 -2
- package/dist/chunk-index-ky42ev0b.js +0 -2
- package/dist/chunk-index-pa2yyqh0.js +0 -2
- package/dist/chunk-index-pcvt668q.js +0 -2
- package/dist/chunk-index-pqw27eye.js +0 -1
- package/dist/chunk-index-wmbmmrd4.js +0 -302
- package/dist/chunk-index-ymbjrsp6.js +0 -2
- package/dist/chunk-lang-css-t85r8rz0.js +0 -1
- package/dist/chunk-lang-html-n6jparge.js +0 -1
- package/dist/chunk-lang-javascript-xj00cp97.js +0 -1
- package/dist/chunk-lang-markdown-zxx8eke8.js +0 -6
- package/dist/chunk-lang-typescript-8f6yywt3.js +0 -1
- package/dist/chunk-layout-actions-bv0chj41.js +0 -1
- package/dist/chunk-notes-9b7pgqfe.js +0 -1
- package/dist/chunk-overview-8yw71esz.js +0 -1
- package/dist/chunk-palette-frms7axe.js +0 -1
- package/dist/chunk-placement-items-fvfy9m0y.js +0 -1
- package/dist/chunk-prompt-history-yrqehads.js +0 -1
- package/dist/chunk-prompts-g6609tqx.js +0 -1
- package/dist/chunk-room-confirms-0a2y0925.js +0 -1
- package/dist/chunk-room-menu-g11q97br.js +0 -1
- package/dist/chunk-rooms-menu-button-h9qge48n.js +0 -1
- package/dist/chunk-schedules-ezmacrbj.js +0 -1
- package/dist/chunk-sheet-hosts-b0azyfhb.js +0 -1
- package/dist/chunk-skill-editor-h6k6whwg.js +0 -8
- package/dist/chunk-skills-gqcfhq56.js +0 -1
- package/dist/chunk-switcher-list-mcpamqnj.js +0 -1
- package/dist/chunk-table-s8cde1xz.js +0 -1
- package/dist/chunk-table-seats-wqkjn8bc.js +0 -1
- package/dist/chunk-transcript-c59c6mpm.js +0 -1
- package/dist/chunk-workspace-0cyspkk4.js +0 -1
- package/dist/chunk-zh-tw-nc93jm10.js +0 -1
package/src/options.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { join, resolve } from "node:path";
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import type {
|
|
3
|
+
ChannelKey,
|
|
4
|
+
ConversationRecord,
|
|
5
|
+
RoundtablePlugin,
|
|
6
|
+
Tier,
|
|
7
|
+
} from "pi-roundtable";
|
|
8
|
+
import { PluginError, TIERS } from "pi-roundtable";
|
|
9
|
+
import { OWNER_ONLY_PANES, PANES, type PaneName } from "./api-types.ts";
|
|
5
10
|
import {
|
|
6
11
|
DEFAULT_UPLOAD_MAX_BYTES,
|
|
7
12
|
UPLOAD_FRAMING_BYTES,
|
|
@@ -70,7 +75,56 @@ export type WebAuthOptions =
|
|
|
70
75
|
/** A request verifier, such as `cloudflareAccess(...)`; the same as the `verifier` option. */
|
|
71
76
|
mode: "verifier";
|
|
72
77
|
verifier: RequestVerifier;
|
|
73
|
-
}
|
|
78
|
+
}
|
|
79
|
+
| OidcOptions;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Sign-in with an OpenID Connect provider (authorization code with PKCE). The id_token is checked
|
|
83
|
+
* once, at the callback; the app then keeps its own server-side session, which slides: it ends
|
|
84
|
+
* after `sessionDays` without a visit. Who the person is, and the tier they hold, come from
|
|
85
|
+
* `IDENTITY` for every request. The origin must be https.
|
|
86
|
+
*/
|
|
87
|
+
export interface OidcOptions {
|
|
88
|
+
mode: "oidc";
|
|
89
|
+
/** The exact `iss` of the id_token. */
|
|
90
|
+
issuer: string;
|
|
91
|
+
/** Default `${issuer}/.well-known/openid-configuration`; read at start, which fails if it cannot be. */
|
|
92
|
+
discoveryUrl?: string;
|
|
93
|
+
clientId: string;
|
|
94
|
+
/** The environment variable that holds the client secret; set = a confidential client, left out = a public client with PKCE. */
|
|
95
|
+
clientSecretEnv?: string;
|
|
96
|
+
/** Default `openid`, `profile`. `offline_access` is refused: the app keeps no refresh token. */
|
|
97
|
+
scopes?: readonly string[];
|
|
98
|
+
/** The claim that names the person; default `sub` (Entra: `oid`). */
|
|
99
|
+
subjectClaim?: string;
|
|
100
|
+
/** The claim for their name; default `name`, then `preferred_username`, then the subject. */
|
|
101
|
+
nameClaim?: string;
|
|
102
|
+
/** The claim that carries app roles, each becoming `<surface>:role:<name>`; default `roles`; `false` reads none. */
|
|
103
|
+
rolesClaim?: string | false;
|
|
104
|
+
/** Decides at sign-in whether this token is acceptable at all. */
|
|
105
|
+
check?: (claims: Readonly<Record<string, unknown>>) => boolean;
|
|
106
|
+
/** How many days without a visit end a session, 1 to 365; default 30. */
|
|
107
|
+
sessionDays?: number;
|
|
108
|
+
/** Whether logging out also signs out at the provider (without `id_token_hint`); default false. */
|
|
109
|
+
endSession?: boolean;
|
|
110
|
+
/**
|
|
111
|
+
* Asks the provider again after this many hours: the next page load goes through a silent
|
|
112
|
+
* `prompt=none` round trip, which refreshes the stored facts and roles; when the provider wants
|
|
113
|
+
* the person, they sign in as usual. Until then an API or socket request of that session is
|
|
114
|
+
* answered 401. Default: never.
|
|
115
|
+
*/
|
|
116
|
+
reauthHours?: number;
|
|
117
|
+
/** The id_token claims to keep with the session (replaced at each sign-in and re-verify, never logged); default none. */
|
|
118
|
+
storeClaims?: readonly string[];
|
|
119
|
+
/**
|
|
120
|
+
* Called with the person's principal id when a request or an open socket proves they hold a
|
|
121
|
+
* live session, at most once per session every five minutes. For a host that counts who is
|
|
122
|
+
* present, such as one that keeps a linked chat account only while its web person is active: a
|
|
123
|
+
* person who only has the app open, calling nothing of the host's, still counts. It is not
|
|
124
|
+
* awaited, and an error it throws is logged and never fails the request.
|
|
125
|
+
*/
|
|
126
|
+
onSessionVerified?: (principalId: string) => void | Promise<void>;
|
|
127
|
+
}
|
|
74
128
|
|
|
75
129
|
/** `auth` checked and filled in. */
|
|
76
130
|
export type ResolvedAuth =
|
|
@@ -81,7 +135,36 @@ export type ResolvedAuth =
|
|
|
81
135
|
sessionDays: number;
|
|
82
136
|
principal?: string;
|
|
83
137
|
}
|
|
84
|
-
| { mode: "verifier"; verifier: RequestVerifier }
|
|
138
|
+
| { mode: "verifier"; verifier: RequestVerifier }
|
|
139
|
+
| ResolvedOidc;
|
|
140
|
+
|
|
141
|
+
export interface ResolvedOidc {
|
|
142
|
+
mode: "oidc";
|
|
143
|
+
issuer: string;
|
|
144
|
+
discoveryUrl: string;
|
|
145
|
+
clientId: string;
|
|
146
|
+
clientSecretEnv: string | undefined;
|
|
147
|
+
scopes: readonly string[];
|
|
148
|
+
subjectClaim: string;
|
|
149
|
+
nameClaim: string | undefined;
|
|
150
|
+
rolesClaim: string | false;
|
|
151
|
+
check: ((claims: Readonly<Record<string, unknown>>) => boolean) | undefined;
|
|
152
|
+
sessionDays: number;
|
|
153
|
+
endSession: boolean;
|
|
154
|
+
reauthHours: number | undefined;
|
|
155
|
+
storeClaims: readonly string[];
|
|
156
|
+
onSessionVerified:
|
|
157
|
+
| ((principalId: string) => void | Promise<void>)
|
|
158
|
+
| undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The panes each tier is served, when the list differs by tier. */
|
|
162
|
+
export interface TierPanes {
|
|
163
|
+
owner: readonly PaneName[];
|
|
164
|
+
/** Default: the member's list. */
|
|
165
|
+
admin?: readonly PaneName[];
|
|
166
|
+
member: readonly PaneName[];
|
|
167
|
+
}
|
|
85
168
|
|
|
86
169
|
export interface WebConsoleOptions {
|
|
87
170
|
/**
|
|
@@ -123,6 +206,14 @@ export interface WebConsoleOptions {
|
|
|
123
206
|
listener?: string;
|
|
124
207
|
/** Hash routes by default; path routes retain links such as /console/skills. */
|
|
125
208
|
routing?: "hash" | "path";
|
|
209
|
+
/**
|
|
210
|
+
* Who the app admits: `owners` (default, as before 0.26) lets in only people who hold the owner
|
|
211
|
+
* role; `access` lets in everyone `IDENTITY` admits on the app's surface, owners, admins and
|
|
212
|
+
* members, and provisions a new principal at a first contact where `access.provisioning` says so
|
|
213
|
+
* for the surface. `access` needs a credential that names a person (`oidc`, or a verifier that
|
|
214
|
+
* reports an actor), not `none` or `password`.
|
|
215
|
+
*/
|
|
216
|
+
admission?: "owners" | "access";
|
|
126
217
|
/**
|
|
127
218
|
* The panes to serve, in the order the page lists them; default `rooms`, `table`, `workspace`,
|
|
128
219
|
* `overview`, `schedules`, `conversations`, `notes`. `rooms` is the owner app: the same agent
|
|
@@ -132,9 +223,14 @@ export interface WebConsoleOptions {
|
|
|
132
223
|
* `workspace` out when there is no directory; an explicit list that names it must have one.
|
|
133
224
|
* An explicit list is kept as written, so a host that listed the panes before 0.23 adds
|
|
134
225
|
* `rooms`, `table` and `schedules` by hand, and one that listed them before 0.24 adds
|
|
135
|
-
* `workspace`.
|
|
226
|
+
* `workspace`. With `admission: "access"` the default also has `profile`, and a member is
|
|
227
|
+
* served `rooms` (the host's rooms only), `conversations`, `notes`, `schedules` and `profile`
|
|
228
|
+
* of those the owners have; give `{ owner, admin?, member }` to choose per tier. A member is
|
|
229
|
+
* never served `overview`, `table`, `workspace`, `skills` or `connectors`, and naming one for
|
|
230
|
+
* them is an error. `notes` in the default list is dropped, with a warning, when the host keeps
|
|
231
|
+
* no memory; naming it with no memory is an error.
|
|
136
232
|
*/
|
|
137
|
-
panes?: readonly PaneName[];
|
|
233
|
+
panes?: readonly PaneName[] | TierPanes;
|
|
138
234
|
/**
|
|
139
235
|
* What the `workspace` pane shows. The agents' working directory (`agents`) and their scratch
|
|
140
236
|
* directory (`scratch`, when the host has one) are shown without being named, from
|
|
@@ -205,17 +301,63 @@ export interface WebConsoleOptions {
|
|
|
205
301
|
* until this is set.
|
|
206
302
|
*/
|
|
207
303
|
subject?: string;
|
|
304
|
+
/** The tiers whose people may subscribe and receive push; default `["owner"]`. */
|
|
305
|
+
tiers?: readonly Tier[];
|
|
306
|
+
/**
|
|
307
|
+
* Where a push is signed and sent: `internal` (default), by this process with its own VAPID
|
|
308
|
+
* key; or `external`, by a sender process the operator runs with the key, so this process
|
|
309
|
+
* never holds it. `publicKey` is the key browsers subscribe against; `subscribePath` is where
|
|
310
|
+
* the sender serves the subscription routes, when it does.
|
|
311
|
+
*/
|
|
312
|
+
sender?:
|
|
313
|
+
| "internal"
|
|
314
|
+
| { external: { publicKey: string; subscribePath?: string } };
|
|
315
|
+
/**
|
|
316
|
+
* What leaves the app for a push: `full` (a title and a preview) or `none` (the kind, a tag and
|
|
317
|
+
* a count only). Default `full` for an internal sender and `none` for an external one. A host
|
|
318
|
+
* room never previews.
|
|
319
|
+
*/
|
|
320
|
+
content?: "full" | "none";
|
|
321
|
+
/**
|
|
322
|
+
* The names of the transports besides `web-push` that the sender carries, such as `slack`. A
|
|
323
|
+
* notice's `via` may name only these and `web-push`; naming any other is a ConfigError when
|
|
324
|
+
* the notice is raised. The app never loads a transport: the sender process does.
|
|
325
|
+
*/
|
|
326
|
+
transports?: readonly string[];
|
|
327
|
+
};
|
|
328
|
+
/**
|
|
329
|
+
* What a person owes before the app opens. `links` names providers whose accounts they must
|
|
330
|
+
* link (`required` gates the app, `nudge` only asks); each needs a `LinkProver` registered
|
|
331
|
+
* through `WEB_APP`. `push` asks them to turn push on and receive a test; `pushWaiver` lets
|
|
332
|
+
* them waive a required push step.
|
|
333
|
+
*/
|
|
334
|
+
onboarding?: {
|
|
335
|
+
links?: Readonly<Record<string, "required" | "nudge">>;
|
|
336
|
+
push?: "required" | "nudge" | "off";
|
|
337
|
+
pushWaiver?: boolean;
|
|
208
338
|
};
|
|
339
|
+
/** Leave out the yolo and long-running switches: no mode is shown or set, and `PUT api/rooms/<key>/mode` answers 404. */
|
|
340
|
+
modes?: false;
|
|
341
|
+
/** The files of the app's agent rooms; `keepDays` is how long stored files are kept, default 30. */
|
|
342
|
+
uploads?: { keepDays?: number };
|
|
209
343
|
/** The page's title and heading; default `Roundtable`. */
|
|
210
344
|
title?: string;
|
|
345
|
+
/**
|
|
346
|
+
* The page's description for search results and link previews (`description` and
|
|
347
|
+
* `og:description`); default a line derived from the title.
|
|
348
|
+
*/
|
|
349
|
+
description?: string;
|
|
211
350
|
/** Where the install icons come from; default the ones this package ships. */
|
|
212
351
|
icons?: { dir: string };
|
|
213
352
|
/** How the page looks: `modern` (default) or `camelot`. The page's owner can switch it in the app. */
|
|
214
353
|
appearance?: { look?: "modern" | "camelot" };
|
|
215
354
|
/** Text a relayed message begins with, taken off the owner's words; default the remote MCP note. */
|
|
216
355
|
relayNotes?: readonly string[];
|
|
217
|
-
/**
|
|
218
|
-
|
|
356
|
+
/**
|
|
357
|
+
* Conversations the console neither lists nor reads. `record` is what the host's registry
|
|
358
|
+
* holds of the conversation, if anything, so a host can decide by its kind or owner.
|
|
359
|
+
*/
|
|
360
|
+
exclude?: (key: ChannelKey, record?: ConversationRecord) => boolean;
|
|
219
361
|
/** The built page's directory; default the `dist/` this package ships. Set it only in tests. */
|
|
220
362
|
assetDir?: string;
|
|
221
363
|
}
|
|
@@ -230,7 +372,11 @@ export interface ResolvedOptions {
|
|
|
230
372
|
mount: string;
|
|
231
373
|
listener: string;
|
|
232
374
|
routing: "hash" | "path";
|
|
375
|
+
admission: "owners" | "access";
|
|
376
|
+
/** The owners' panes: what `panes` named, or the default. */
|
|
233
377
|
panes: PaneName[];
|
|
378
|
+
/** The panes each tier is served. Admins and members are served none unless `admission` is `access`. */
|
|
379
|
+
tierPanes: Readonly<Record<Tier, readonly PaneName[]>>;
|
|
234
380
|
/** Whether `panes` was written out, which makes a missing directory for the workspace pane an error. */
|
|
235
381
|
panesGiven: boolean;
|
|
236
382
|
/** The `workspace` option checked and filled in. */
|
|
@@ -250,12 +396,30 @@ export interface ResolvedOptions {
|
|
|
250
396
|
/** The VAPID contact; undefined = push is off. */
|
|
251
397
|
subject: string | undefined;
|
|
252
398
|
fetch: typeof fetch | undefined;
|
|
399
|
+
tiers: readonly Tier[];
|
|
400
|
+
sender:
|
|
401
|
+
| "internal"
|
|
402
|
+
| { external: { publicKey: string; subscribePath?: string } };
|
|
403
|
+
content: "full" | "none";
|
|
404
|
+
/** `web-push` and the host's own transports. */
|
|
405
|
+
transports: readonly string[];
|
|
253
406
|
};
|
|
407
|
+
onboarding: {
|
|
408
|
+
links: Readonly<Record<string, "required" | "nudge">>;
|
|
409
|
+
push: "required" | "nudge" | "off";
|
|
410
|
+
pushWaiver: boolean;
|
|
411
|
+
};
|
|
412
|
+
/** Whether the yolo and long-running switches are served. */
|
|
413
|
+
modes: boolean;
|
|
414
|
+
uploadKeepDays: number;
|
|
254
415
|
title: string;
|
|
416
|
+
description: string | undefined;
|
|
255
417
|
iconsDir: string | undefined;
|
|
256
418
|
look: "modern" | "camelot";
|
|
257
419
|
relayNotes: readonly string[];
|
|
258
|
-
exclude:
|
|
420
|
+
exclude:
|
|
421
|
+
| ((key: ChannelKey, record?: ConversationRecord) => boolean)
|
|
422
|
+
| undefined;
|
|
259
423
|
assetDir: string | undefined;
|
|
260
424
|
features: WebConsoleOptions["features"];
|
|
261
425
|
presentation: ConsolePresentation | undefined;
|
|
@@ -302,31 +466,110 @@ function checkOrigin(origin: string): string {
|
|
|
302
466
|
return url.origin;
|
|
303
467
|
}
|
|
304
468
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
469
|
+
const DEFAULT_OWNER_PANES: readonly PaneName[] = [
|
|
470
|
+
"rooms",
|
|
471
|
+
"table",
|
|
472
|
+
"workspace",
|
|
473
|
+
"overview",
|
|
474
|
+
"schedules",
|
|
475
|
+
"conversations",
|
|
476
|
+
"notes",
|
|
477
|
+
];
|
|
478
|
+
|
|
479
|
+
/** The panes a member is served by default, of those the owners have. */
|
|
480
|
+
const DEFAULT_MEMBER_PANES: readonly PaneName[] = [
|
|
481
|
+
"rooms",
|
|
482
|
+
"conversations",
|
|
483
|
+
"notes",
|
|
484
|
+
"schedules",
|
|
485
|
+
"profile",
|
|
486
|
+
];
|
|
487
|
+
|
|
488
|
+
function checkPaneList(panes: readonly PaneName[], label: string): PaneName[] {
|
|
489
|
+
if (!Array.isArray(panes)) fail(`${label} must be a list of panes`);
|
|
490
|
+
if (panes.length === 0) fail(`${label} is empty; serve at least one pane`);
|
|
317
491
|
for (const pane of panes)
|
|
318
492
|
if (!PANES.includes(pane))
|
|
319
493
|
fail(
|
|
320
|
-
`unknown pane ${JSON.stringify(pane)}; the panes are ${PANES.join(", ")}`,
|
|
494
|
+
`unknown pane ${JSON.stringify(pane)} in ${label}; the panes are ${PANES.join(", ")}`,
|
|
321
495
|
);
|
|
322
|
-
if (new Set(panes).size !== panes.length) fail(
|
|
323
|
-
|
|
496
|
+
if (new Set(panes).size !== panes.length) fail(`${label} lists a pane twice`);
|
|
497
|
+
return [...panes];
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** The panes of the owners, as before: what the other panes need of them is checked here. */
|
|
501
|
+
function checkOwnerPanes(panes: readonly PaneName[]): PaneName[] {
|
|
502
|
+
const list = checkPaneList(panes, "panes");
|
|
503
|
+
if (list.includes("schedules") && !list.includes("rooms"))
|
|
324
504
|
fail("the schedules pane needs the rooms pane, which serves its API");
|
|
325
|
-
if (
|
|
505
|
+
if (list.includes("table") && !list.includes("rooms"))
|
|
326
506
|
fail("the table pane needs the rooms pane, whose agents it seats");
|
|
327
|
-
if (
|
|
507
|
+
if (list.includes("workspace") && !list.includes("rooms"))
|
|
328
508
|
fail("the workspace pane needs the rooms pane, which serves its API");
|
|
329
|
-
return
|
|
509
|
+
return list;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
interface CheckedPanes {
|
|
513
|
+
owner: PaneName[];
|
|
514
|
+
admin: PaneName[];
|
|
515
|
+
member: PaneName[];
|
|
516
|
+
given: boolean;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function checkPanes(
|
|
520
|
+
panes: WebConsoleOptions["panes"],
|
|
521
|
+
admission: "owners" | "access",
|
|
522
|
+
): CheckedPanes {
|
|
523
|
+
const tiered =
|
|
524
|
+
panes !== undefined && !Array.isArray(panes)
|
|
525
|
+
? (panes as TierPanes)
|
|
526
|
+
: undefined;
|
|
527
|
+
if (tiered && (typeof tiered !== "object" || tiered === null))
|
|
528
|
+
fail("panes must be a list, or { owner, admin?, member }");
|
|
529
|
+
const owner = checkOwnerPanes(
|
|
530
|
+
tiered
|
|
531
|
+
? tiered.owner
|
|
532
|
+
: ((panes as readonly PaneName[] | undefined) ?? [
|
|
533
|
+
...DEFAULT_OWNER_PANES,
|
|
534
|
+
...(admission === "access" ? (["profile"] as const) : []),
|
|
535
|
+
]),
|
|
536
|
+
);
|
|
537
|
+
if (!tiered || admission !== "access") {
|
|
538
|
+
if (tiered && admission === "owners")
|
|
539
|
+
fail(
|
|
540
|
+
'panes names panes per tier, but only admission "access" admits anyone but owners',
|
|
541
|
+
);
|
|
542
|
+
const member =
|
|
543
|
+
admission === "access"
|
|
544
|
+
? DEFAULT_MEMBER_PANES.filter((pane) => owner.includes(pane))
|
|
545
|
+
: [];
|
|
546
|
+
return { owner, admin: member, member, given: panes !== undefined };
|
|
547
|
+
}
|
|
548
|
+
const member = checkPaneList(tiered.member, "panes.member");
|
|
549
|
+
const admin = tiered.admin
|
|
550
|
+
? checkPaneList(tiered.admin, "panes.admin")
|
|
551
|
+
: [...member];
|
|
552
|
+
for (const [label, list] of [
|
|
553
|
+
["panes.member", member],
|
|
554
|
+
["panes.admin", admin],
|
|
555
|
+
] as const) {
|
|
556
|
+
for (const pane of list) {
|
|
557
|
+
if ((OWNER_ONLY_PANES as readonly string[]).includes(pane))
|
|
558
|
+
fail(
|
|
559
|
+
`${label} names the ${pane} pane, which only owners are served: it shows the whole host`,
|
|
560
|
+
);
|
|
561
|
+
if (!owner.includes(pane))
|
|
562
|
+
fail(
|
|
563
|
+
`${label} names the ${pane} pane, which the owners' panes do not have; the pane's API is served for the owners' list`,
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
// A member's table seats the host rooms they may list, which the rooms pane brings with its socket.
|
|
567
|
+
if (list.includes("table") && !list.includes("rooms"))
|
|
568
|
+
fail(
|
|
569
|
+
`${label} names the table pane without the rooms pane, whose host rooms it seats`,
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
return { owner, admin, member, given: true };
|
|
330
573
|
}
|
|
331
574
|
|
|
332
575
|
/**
|
|
@@ -443,13 +686,150 @@ function checkAuth(options: WebConsoleOptions): ResolvedAuth {
|
|
|
443
686
|
);
|
|
444
687
|
return { mode, verifier: given as RequestVerifier };
|
|
445
688
|
}
|
|
689
|
+
case "oidc":
|
|
690
|
+
return checkOidc(auth as OidcOptions);
|
|
446
691
|
default:
|
|
447
692
|
fail(
|
|
448
|
-
`auth.mode ${JSON.stringify(mode)} is unknown; the modes are none, password and
|
|
693
|
+
`auth.mode ${JSON.stringify(mode)} is unknown; the modes are none, password, verifier and oidc`,
|
|
449
694
|
);
|
|
450
695
|
}
|
|
451
696
|
}
|
|
452
697
|
|
|
698
|
+
const isHttpsUrl = (value: unknown): value is string =>
|
|
699
|
+
typeof value === "string" && URL.parse(value)?.protocol === "https:";
|
|
700
|
+
|
|
701
|
+
/** A provider on this machine, which a test or a local identity provider may use over plain http. */
|
|
702
|
+
const isLoopbackHttp = (value: unknown): boolean => {
|
|
703
|
+
const url = typeof value === "string" ? URL.parse(value) : null;
|
|
704
|
+
return (
|
|
705
|
+
url?.protocol === "http:" &&
|
|
706
|
+
["127.0.0.1", "[::1]", "localhost"].includes(url.hostname)
|
|
707
|
+
);
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
const providerUrl = (value: unknown): value is string =>
|
|
711
|
+
isHttpsUrl(value) || isLoopbackHttp(value);
|
|
712
|
+
|
|
713
|
+
function checkOidc(auth: OidcOptions): ResolvedOidc {
|
|
714
|
+
const known = new Set([
|
|
715
|
+
"mode",
|
|
716
|
+
"issuer",
|
|
717
|
+
"discoveryUrl",
|
|
718
|
+
"clientId",
|
|
719
|
+
"clientSecretEnv",
|
|
720
|
+
"scopes",
|
|
721
|
+
"subjectClaim",
|
|
722
|
+
"nameClaim",
|
|
723
|
+
"rolesClaim",
|
|
724
|
+
"check",
|
|
725
|
+
"sessionDays",
|
|
726
|
+
"endSession",
|
|
727
|
+
"reauthHours",
|
|
728
|
+
"storeClaims",
|
|
729
|
+
"onSessionVerified",
|
|
730
|
+
]);
|
|
731
|
+
for (const key of Object.keys(auth))
|
|
732
|
+
if (!known.has(key)) fail(`auth.${key} is not an oidc option`);
|
|
733
|
+
if (!providerUrl(auth.issuer))
|
|
734
|
+
fail("auth.issuer must be the provider's https URL, exactly its `iss`");
|
|
735
|
+
const { issuer } = auth;
|
|
736
|
+
const discoveryUrl =
|
|
737
|
+
auth.discoveryUrl ??
|
|
738
|
+
`${issuer.replace(/\/+$/, "")}/.well-known/openid-configuration`;
|
|
739
|
+
if (!providerUrl(discoveryUrl))
|
|
740
|
+
fail("auth.discoveryUrl must be an https URL");
|
|
741
|
+
if (typeof auth.clientId !== "string" || !auth.clientId.trim())
|
|
742
|
+
fail("auth.clientId is empty");
|
|
743
|
+
if (
|
|
744
|
+
auth.clientSecretEnv !== undefined &&
|
|
745
|
+
(typeof auth.clientSecretEnv !== "string" || !auth.clientSecretEnv.trim())
|
|
746
|
+
)
|
|
747
|
+
fail(
|
|
748
|
+
"auth.clientSecretEnv must name an environment variable; leave it out for a public client",
|
|
749
|
+
);
|
|
750
|
+
const scopes = auth.scopes ?? ["openid", "profile"];
|
|
751
|
+
if (
|
|
752
|
+
!Array.isArray(scopes) ||
|
|
753
|
+
scopes.length === 0 ||
|
|
754
|
+
!scopes.every(
|
|
755
|
+
(scope) =>
|
|
756
|
+
typeof scope === "string" && /^[\x21\x23-\x5b\x5d-\x7e]+$/.test(scope),
|
|
757
|
+
)
|
|
758
|
+
)
|
|
759
|
+
fail("auth.scopes must be a list of scope names");
|
|
760
|
+
if (scopes.includes("offline_access"))
|
|
761
|
+
fail(
|
|
762
|
+
"auth.scopes has offline_access, but the app keeps no refresh token: its own session is what lasts",
|
|
763
|
+
);
|
|
764
|
+
if (!scopes.includes("openid")) fail("auth.scopes must include openid");
|
|
765
|
+
const claim = (name: string, value: unknown) => {
|
|
766
|
+
if (value !== undefined && (typeof value !== "string" || !value.trim()))
|
|
767
|
+
fail(`auth.${name} must be the name of a claim`);
|
|
768
|
+
};
|
|
769
|
+
claim("subjectClaim", auth.subjectClaim);
|
|
770
|
+
claim("nameClaim", auth.nameClaim);
|
|
771
|
+
if (
|
|
772
|
+
auth.rolesClaim !== undefined &&
|
|
773
|
+
auth.rolesClaim !== false &&
|
|
774
|
+
(typeof auth.rolesClaim !== "string" || !auth.rolesClaim.trim())
|
|
775
|
+
)
|
|
776
|
+
fail("auth.rolesClaim must be the name of a claim, or false");
|
|
777
|
+
if (auth.check !== undefined && typeof auth.check !== "function")
|
|
778
|
+
fail("auth.check must be a function");
|
|
779
|
+
const sessionDays = auth.sessionDays ?? DEFAULT_SESSION_DAYS;
|
|
780
|
+
if (
|
|
781
|
+
typeof sessionDays !== "number" ||
|
|
782
|
+
!Number.isInteger(sessionDays) ||
|
|
783
|
+
sessionDays < 1 ||
|
|
784
|
+
sessionDays > 365
|
|
785
|
+
)
|
|
786
|
+
fail("auth.sessionDays must be a whole number of days from 1 to 365");
|
|
787
|
+
if (auth.endSession !== undefined && typeof auth.endSession !== "boolean")
|
|
788
|
+
fail("auth.endSession must be true or false");
|
|
789
|
+
if (
|
|
790
|
+
auth.reauthHours !== undefined &&
|
|
791
|
+
(typeof auth.reauthHours !== "number" ||
|
|
792
|
+
!(auth.reauthHours > 0) ||
|
|
793
|
+
auth.reauthHours > sessionDays * 24)
|
|
794
|
+
)
|
|
795
|
+
fail(
|
|
796
|
+
"auth.reauthHours must be a number of hours above 0 and within the session's own days",
|
|
797
|
+
);
|
|
798
|
+
const storeClaims = auth.storeClaims ?? [];
|
|
799
|
+
if (
|
|
800
|
+
!Array.isArray(storeClaims) ||
|
|
801
|
+
!storeClaims.every((name) => typeof name === "string" && name.trim())
|
|
802
|
+
)
|
|
803
|
+
fail("auth.storeClaims must be a list of claim names");
|
|
804
|
+
if (
|
|
805
|
+
auth.onSessionVerified !== undefined &&
|
|
806
|
+
typeof auth.onSessionVerified !== "function"
|
|
807
|
+
)
|
|
808
|
+
fail("auth.onSessionVerified must be a function");
|
|
809
|
+
return {
|
|
810
|
+
mode: "oidc",
|
|
811
|
+
issuer,
|
|
812
|
+
discoveryUrl,
|
|
813
|
+
clientId: auth.clientId.trim(),
|
|
814
|
+
clientSecretEnv: auth.clientSecretEnv?.trim(),
|
|
815
|
+
scopes: [...scopes],
|
|
816
|
+
subjectClaim: auth.subjectClaim?.trim() ?? "sub",
|
|
817
|
+
nameClaim: auth.nameClaim?.trim(),
|
|
818
|
+
rolesClaim:
|
|
819
|
+
auth.rolesClaim === undefined
|
|
820
|
+
? "roles"
|
|
821
|
+
: typeof auth.rolesClaim === "string"
|
|
822
|
+
? auth.rolesClaim.trim()
|
|
823
|
+
: false,
|
|
824
|
+
check: auth.check,
|
|
825
|
+
sessionDays,
|
|
826
|
+
endSession: auth.endSession === true,
|
|
827
|
+
reauthHours: auth.reauthHours,
|
|
828
|
+
storeClaims: [...storeClaims],
|
|
829
|
+
onSessionVerified: auth.onSessionVerified,
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
|
|
453
833
|
function checkSurface(surface: string | undefined): string {
|
|
454
834
|
if (surface === undefined) return DEFAULT_SURFACE;
|
|
455
835
|
if (typeof surface !== "string" || !/^[a-z][a-z0-9-]*$/.test(surface))
|
|
@@ -516,15 +896,108 @@ function checkPush(
|
|
|
516
896
|
fail(
|
|
517
897
|
"push.subject must be a mailto: address or an https: URL, such as mailto:ops@example.com",
|
|
518
898
|
);
|
|
899
|
+
const tiers = push?.tiers ?? ["owner"];
|
|
900
|
+
if (
|
|
901
|
+
!Array.isArray(tiers) ||
|
|
902
|
+
tiers.length === 0 ||
|
|
903
|
+
!tiers.every((tier) => TIERS.includes(tier))
|
|
904
|
+
)
|
|
905
|
+
fail(`push.tiers must be a list of ${TIERS.join(", ")}`);
|
|
906
|
+
const sender = push?.sender ?? "internal";
|
|
907
|
+
if (sender !== "internal") {
|
|
908
|
+
const external = (sender as { external?: unknown } | null)?.external as
|
|
909
|
+
| { publicKey?: unknown; subscribePath?: unknown }
|
|
910
|
+
| undefined;
|
|
911
|
+
if (
|
|
912
|
+
typeof external !== "object" ||
|
|
913
|
+
external === null ||
|
|
914
|
+
typeof external.publicKey !== "string" ||
|
|
915
|
+
!/^[A-Za-z0-9_-]{80,100}$/.test(external.publicKey) ||
|
|
916
|
+
(external.subscribePath !== undefined &&
|
|
917
|
+
(typeof external.subscribePath !== "string" ||
|
|
918
|
+
!external.subscribePath.startsWith("/")))
|
|
919
|
+
)
|
|
920
|
+
fail(
|
|
921
|
+
'push.sender must be "internal" or { external: { publicKey, subscribePath? } } with the VAPID public key (base64url) and, when the sender serves the subscription routes, their path',
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
const content = push?.content ?? (sender === "internal" ? "full" : "none");
|
|
925
|
+
if (content !== "full" && content !== "none")
|
|
926
|
+
fail('push.content must be "full" or "none"');
|
|
927
|
+
const extra = push?.transports ?? [];
|
|
928
|
+
if (
|
|
929
|
+
!Array.isArray(extra) ||
|
|
930
|
+
!extra.every(
|
|
931
|
+
(name) => typeof name === "string" && /^[a-z][a-z0-9-]{0,39}$/.test(name),
|
|
932
|
+
)
|
|
933
|
+
)
|
|
934
|
+
fail("push.transports must be a list of transport names such as slack");
|
|
519
935
|
return {
|
|
520
936
|
preview: push?.preview ?? true,
|
|
521
937
|
quietHours,
|
|
522
938
|
endpointHosts: [...hosts],
|
|
523
939
|
subject: given ?? (origin.startsWith("https:") ? origin : undefined),
|
|
524
940
|
fetch: pushFetchOf(options),
|
|
941
|
+
tiers: [...new Set(tiers)],
|
|
942
|
+
sender:
|
|
943
|
+
sender === "internal"
|
|
944
|
+
? "internal"
|
|
945
|
+
: {
|
|
946
|
+
external: {
|
|
947
|
+
publicKey: (sender as { external: { publicKey: string } })
|
|
948
|
+
.external.publicKey,
|
|
949
|
+
...((sender as { external: { subscribePath?: string } }).external
|
|
950
|
+
.subscribePath
|
|
951
|
+
? {
|
|
952
|
+
subscribePath: (
|
|
953
|
+
sender as { external: { subscribePath: string } }
|
|
954
|
+
).external.subscribePath,
|
|
955
|
+
}
|
|
956
|
+
: {}),
|
|
957
|
+
},
|
|
958
|
+
},
|
|
959
|
+
content,
|
|
960
|
+
transports: [
|
|
961
|
+
"web-push",
|
|
962
|
+
...new Set(extra.filter((name) => name !== "web-push")),
|
|
963
|
+
],
|
|
525
964
|
};
|
|
526
965
|
}
|
|
527
966
|
|
|
967
|
+
function checkOnboarding(
|
|
968
|
+
options: WebConsoleOptions,
|
|
969
|
+
): ResolvedOptions["onboarding"] {
|
|
970
|
+
const given = options.onboarding;
|
|
971
|
+
if (given === undefined) return { links: {}, push: "off", pushWaiver: false };
|
|
972
|
+
if (typeof given !== "object" || given === null)
|
|
973
|
+
fail("onboarding must be an object such as { links: { line: 'nudge' } }");
|
|
974
|
+
const links = given.links ?? {};
|
|
975
|
+
if (typeof links !== "object" || links === null || Array.isArray(links))
|
|
976
|
+
fail("onboarding.links must map a provider to required or nudge");
|
|
977
|
+
for (const [provider, step] of Object.entries(links)) {
|
|
978
|
+
if (!provider.trim())
|
|
979
|
+
fail("onboarding.links names a provider with an empty name");
|
|
980
|
+
if (step !== "required" && step !== "nudge")
|
|
981
|
+
fail(
|
|
982
|
+
`onboarding.links.${provider} must be required or nudge, not ${JSON.stringify(step)}`,
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
const push = given.push ?? "off";
|
|
986
|
+
if (push !== "required" && push !== "nudge" && push !== "off")
|
|
987
|
+
fail("onboarding.push must be required, nudge or off");
|
|
988
|
+
if (given.pushWaiver !== undefined && typeof given.pushWaiver !== "boolean")
|
|
989
|
+
fail("onboarding.pushWaiver must be true or false");
|
|
990
|
+
return { links: { ...links }, push, pushWaiver: given.pushWaiver === true };
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function checkUploads(options: WebConsoleOptions): number {
|
|
994
|
+
const days = options.uploads?.keepDays;
|
|
995
|
+
if (days === undefined) return 30;
|
|
996
|
+
if (!Number.isInteger(days) || days < 1 || days > 3650)
|
|
997
|
+
fail("uploads.keepDays must be a whole number of days from 1 to 3650");
|
|
998
|
+
return days;
|
|
999
|
+
}
|
|
1000
|
+
|
|
528
1001
|
/** The largest `uploadMaxBytes`: the limit is also the listener's body limit. */
|
|
529
1002
|
const MAX_UPLOAD_MAX_BYTES = 4 * 1024 ** 3;
|
|
530
1003
|
|
|
@@ -558,7 +1031,22 @@ export function resolveOptions(options: WebConsoleOptions): ResolvedOptions {
|
|
|
558
1031
|
fail("presentation.locale is not a valid locale");
|
|
559
1032
|
}
|
|
560
1033
|
}
|
|
561
|
-
const
|
|
1034
|
+
const admission = options.admission ?? "owners";
|
|
1035
|
+
if (admission !== "owners" && admission !== "access")
|
|
1036
|
+
fail('admission must be "owners" or "access"');
|
|
1037
|
+
if (
|
|
1038
|
+
admission === "access" &&
|
|
1039
|
+
(auth.mode === "none" || auth.mode === "password")
|
|
1040
|
+
)
|
|
1041
|
+
fail(
|
|
1042
|
+
`admission "access" needs a credential that names a person, but auth ${auth.mode} speaks for one shared actor; use auth oidc or a verifier that reports who signed in`,
|
|
1043
|
+
);
|
|
1044
|
+
const origin = checkOrigin(options.origin);
|
|
1045
|
+
if (auth.mode === "oidc" && !origin.startsWith("https:"))
|
|
1046
|
+
fail("auth oidc needs an https origin: the session cookie is Secure");
|
|
1047
|
+
if (options.modes !== undefined && options.modes !== false)
|
|
1048
|
+
fail("modes can only be false, to leave the mode switches out");
|
|
1049
|
+
const panes = checkPanes(options.panes, admission);
|
|
562
1050
|
const ownerId = options.ownerId?.trim();
|
|
563
1051
|
if (options.ownerId !== undefined && !ownerId)
|
|
564
1052
|
fail(
|
|
@@ -569,23 +1057,35 @@ export function resolveOptions(options: WebConsoleOptions): ResolvedOptions {
|
|
|
569
1057
|
fail("appearance.look must be modern or camelot");
|
|
570
1058
|
const title = options.title?.trim() ?? "Roundtable";
|
|
571
1059
|
if (!title) fail("title is empty");
|
|
1060
|
+
const description = options.description?.trim();
|
|
1061
|
+
if (
|
|
1062
|
+
options.description !== undefined &&
|
|
1063
|
+
(typeof options.description !== "string" || !description)
|
|
1064
|
+
)
|
|
1065
|
+
fail("description is empty. Leave it out for the default");
|
|
572
1066
|
return {
|
|
573
1067
|
auth,
|
|
574
|
-
origin
|
|
1068
|
+
origin,
|
|
575
1069
|
ownerId,
|
|
576
1070
|
dataDir: options.dataDir ?? "data",
|
|
577
1071
|
sessionsDir: join(options.dataDir ?? "data", "sessions"),
|
|
578
1072
|
mount: checkMount(options.mountPath ?? "/console"),
|
|
579
1073
|
listener: options.listener ?? "public",
|
|
580
1074
|
routing: options.routing ?? "hash",
|
|
581
|
-
|
|
582
|
-
|
|
1075
|
+
admission,
|
|
1076
|
+
panes: panes.owner,
|
|
1077
|
+
tierPanes: { owner: panes.owner, admin: panes.admin, member: panes.member },
|
|
1078
|
+
panesGiven: panes.given,
|
|
583
1079
|
workspace: checkWorkspace(options, options.dataDir ?? "data"),
|
|
584
1080
|
surface: checkSurface(options.surface),
|
|
585
1081
|
cardGraceSeconds: checkGrace(options.cardGraceSeconds),
|
|
586
1082
|
uploadMaxBytes: checkUploadMax(options.uploadMaxBytes),
|
|
587
|
-
push: checkPush(options,
|
|
1083
|
+
push: checkPush(options, origin),
|
|
1084
|
+
onboarding: checkOnboarding(options),
|
|
1085
|
+
modes: options.modes !== false,
|
|
1086
|
+
uploadKeepDays: checkUploads(options),
|
|
588
1087
|
title,
|
|
1088
|
+
description,
|
|
589
1089
|
iconsDir: options.icons?.dir,
|
|
590
1090
|
look,
|
|
591
1091
|
relayNotes: options.relayNotes ?? [DEFAULT_RELAY_NOTE],
|