pi-roundtable-web 0.25.0 → 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 +15 -0
- package/README.md +145 -8
- 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-0pm9p3nn.js → chunk-agent-b017vswv.js} +1 -1
- 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-anhghy3k.js → chunk-connectors-hrm06nx8.js} +1 -1
- package/dist/chunk-conversations-e26rrf01.js +1 -0
- package/dist/{chunk-drag-q4fx9b22.js → chunk-drag-v41gx2mc.js} +1 -1
- package/dist/{chunk-entry-he1embgp.js → chunk-entry-jxxb3ath.js} +1 -1
- package/dist/{chunk-folder-list-5cp8stna.js → chunk-folder-list-b97xk12b.js} +1 -1
- 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-bvfqjh77.js → chunk-index-014rjv9k.js} +2 -2
- package/dist/{chunk-index-d5abvq7v.js → chunk-index-2d0kg096.js} +2 -2
- package/dist/chunk-index-2qd81wbx.js +2 -0
- package/dist/{chunk-index-9b1kt7qw.css → chunk-index-2stynxw6.css} +1 -1
- package/dist/{chunk-index-gpkzhq1g.js → chunk-index-459drsft.js} +2 -2
- package/dist/chunk-index-4mfj596y.js +2 -0
- package/dist/{chunk-index-18gxy79n.js → chunk-index-66nskrdj.js} +2 -2
- 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-s681nny9.js → chunk-index-km40bhsz.js} +2 -2
- 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-zfd00hwb.js → chunk-index-nqfkwkfp.js} +2 -2
- package/dist/chunk-index-nwqk447w.js +2 -0
- package/dist/{chunk-index-wmbmmrd4.js → chunk-index-pcfhpm8v.js} +4 -4
- package/dist/chunk-index-q8h8xzbm.js +2 -0
- package/dist/chunk-index-r5vszgfs.js +2 -0
- package/dist/{chunk-index-27qca4mr.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-krtk6nn2.js → chunk-index-wnjp6wv0.js} +2 -2
- package/dist/chunk-index-xgkd15n3.js +2 -0
- package/dist/{chunk-index-9jqcskb4.js → chunk-index-y8rnq8zf.js} +5 -5
- package/dist/chunk-index-z3k18atc.js +2 -0
- package/dist/chunk-lang-css-6bepqpnx.js +1 -0
- package/dist/{chunk-lang-dockerfile-d5mzxs60.js → chunk-lang-dockerfile-hrav55t7.js} +1 -1
- package/dist/{chunk-lang-go-830fmhba.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-5eaarz7j.js → chunk-lang-json-hsemyd0r.js} +1 -1
- package/dist/chunk-lang-markdown-zkwb4c9n.js +6 -0
- package/dist/{chunk-lang-python-fjw84w10.js → chunk-lang-python-tewx7006.js} +2 -2
- package/dist/{chunk-lang-rust-5rbp2zdk.js → chunk-lang-rust-0ydx84s1.js} +1 -1
- package/dist/{chunk-lang-shell-vzagpym3.js → chunk-lang-shell-gq1tmdz4.js} +1 -1
- package/dist/{chunk-lang-sql-0c2w9c2y.js → chunk-lang-sql-xt1qyycg.js} +2 -2
- package/dist/{chunk-lang-toml-yr9fv6eb.js → chunk-lang-toml-0hamc5fc.js} +1 -1
- package/dist/chunk-lang-typescript-82xf7z2e.js +1 -0
- package/dist/{chunk-lang-xml-eze08p42.js → chunk-lang-xml-wf6h5pt2.js} +1 -1
- package/dist/{chunk-lang-yaml-c4b6ft3y.js → chunk-lang-yaml-7vk521h1.js} +1 -1
- package/dist/chunk-layout-actions-zh7bezk7.js +1 -0
- package/dist/{chunk-menu-items-jrwxycr1.js → chunk-menu-items-1n9qargb.js} +2 -2
- 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-jv4s7hmj.js → chunk-seat-menu-d3npdtx1.js} +1 -1
- 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-4sje1fr0.js → chunk-table2d-n26b7gd0.js} +1 -1
- package/dist/{chunk-table3d-h64skazp.js → chunk-table3d-8aysyf56.js} +12 -12
- 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 +301 -4
- package/src/app/app.ts +196 -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 +141 -14
- package/src/app/routes.ts +124 -26
- package/src/app/socket.ts +309 -77
- package/src/app/sockets.ts +108 -20
- package/src/app/surface.ts +72 -36
- 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 +132 -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/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 +268 -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-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-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-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,48 @@ 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
|
+
}
|
|
74
120
|
|
|
75
121
|
/** `auth` checked and filled in. */
|
|
76
122
|
export type ResolvedAuth =
|
|
@@ -81,7 +127,33 @@ export type ResolvedAuth =
|
|
|
81
127
|
sessionDays: number;
|
|
82
128
|
principal?: string;
|
|
83
129
|
}
|
|
84
|
-
| { mode: "verifier"; verifier: RequestVerifier }
|
|
130
|
+
| { mode: "verifier"; verifier: RequestVerifier }
|
|
131
|
+
| ResolvedOidc;
|
|
132
|
+
|
|
133
|
+
export interface ResolvedOidc {
|
|
134
|
+
mode: "oidc";
|
|
135
|
+
issuer: string;
|
|
136
|
+
discoveryUrl: string;
|
|
137
|
+
clientId: string;
|
|
138
|
+
clientSecretEnv: string | undefined;
|
|
139
|
+
scopes: readonly string[];
|
|
140
|
+
subjectClaim: string;
|
|
141
|
+
nameClaim: string | undefined;
|
|
142
|
+
rolesClaim: string | false;
|
|
143
|
+
check: ((claims: Readonly<Record<string, unknown>>) => boolean) | undefined;
|
|
144
|
+
sessionDays: number;
|
|
145
|
+
endSession: boolean;
|
|
146
|
+
reauthHours: number | undefined;
|
|
147
|
+
storeClaims: readonly string[];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The panes each tier is served, when the list differs by tier. */
|
|
151
|
+
export interface TierPanes {
|
|
152
|
+
owner: readonly PaneName[];
|
|
153
|
+
/** Default: the member's list. */
|
|
154
|
+
admin?: readonly PaneName[];
|
|
155
|
+
member: readonly PaneName[];
|
|
156
|
+
}
|
|
85
157
|
|
|
86
158
|
export interface WebConsoleOptions {
|
|
87
159
|
/**
|
|
@@ -123,6 +195,14 @@ export interface WebConsoleOptions {
|
|
|
123
195
|
listener?: string;
|
|
124
196
|
/** Hash routes by default; path routes retain links such as /console/skills. */
|
|
125
197
|
routing?: "hash" | "path";
|
|
198
|
+
/**
|
|
199
|
+
* Who the app admits: `owners` (default, as before 0.26) lets in only people who hold the owner
|
|
200
|
+
* role; `access` lets in everyone `IDENTITY` admits on the app's surface, owners, admins and
|
|
201
|
+
* members, and provisions a new principal at a first contact where `access.provisioning` says so
|
|
202
|
+
* for the surface. `access` needs a credential that names a person (`oidc`, or a verifier that
|
|
203
|
+
* reports an actor), not `none` or `password`.
|
|
204
|
+
*/
|
|
205
|
+
admission?: "owners" | "access";
|
|
126
206
|
/**
|
|
127
207
|
* The panes to serve, in the order the page lists them; default `rooms`, `table`, `workspace`,
|
|
128
208
|
* `overview`, `schedules`, `conversations`, `notes`. `rooms` is the owner app: the same agent
|
|
@@ -132,9 +212,14 @@ export interface WebConsoleOptions {
|
|
|
132
212
|
* `workspace` out when there is no directory; an explicit list that names it must have one.
|
|
133
213
|
* An explicit list is kept as written, so a host that listed the panes before 0.23 adds
|
|
134
214
|
* `rooms`, `table` and `schedules` by hand, and one that listed them before 0.24 adds
|
|
135
|
-
* `workspace`.
|
|
215
|
+
* `workspace`. With `admission: "access"` the default also has `profile`, and a member is
|
|
216
|
+
* served `rooms` (the host's rooms only), `conversations`, `notes`, `schedules` and `profile`
|
|
217
|
+
* of those the owners have; give `{ owner, admin?, member }` to choose per tier. A member is
|
|
218
|
+
* never served `overview`, `table`, `workspace`, `skills` or `connectors`, and naming one for
|
|
219
|
+
* them is an error. `notes` in the default list is dropped, with a warning, when the host keeps
|
|
220
|
+
* no memory; naming it with no memory is an error.
|
|
136
221
|
*/
|
|
137
|
-
panes?: readonly PaneName[];
|
|
222
|
+
panes?: readonly PaneName[] | TierPanes;
|
|
138
223
|
/**
|
|
139
224
|
* What the `workspace` pane shows. The agents' working directory (`agents`) and their scratch
|
|
140
225
|
* directory (`scratch`, when the host has one) are shown without being named, from
|
|
@@ -205,7 +290,45 @@ export interface WebConsoleOptions {
|
|
|
205
290
|
* until this is set.
|
|
206
291
|
*/
|
|
207
292
|
subject?: string;
|
|
293
|
+
/** The tiers whose people may subscribe and receive push; default `["owner"]`. */
|
|
294
|
+
tiers?: readonly Tier[];
|
|
295
|
+
/**
|
|
296
|
+
* Where a push is signed and sent: `internal` (default), by this process with its own VAPID
|
|
297
|
+
* key; or `external`, by a sender process the operator runs with the key, so this process
|
|
298
|
+
* never holds it. `publicKey` is the key browsers subscribe against; `subscribePath` is where
|
|
299
|
+
* the sender serves the subscription routes, when it does.
|
|
300
|
+
*/
|
|
301
|
+
sender?:
|
|
302
|
+
| "internal"
|
|
303
|
+
| { external: { publicKey: string; subscribePath?: string } };
|
|
304
|
+
/**
|
|
305
|
+
* What leaves the app for a push: `full` (a title and a preview) or `none` (the kind, a tag and
|
|
306
|
+
* a count only). Default `full` for an internal sender and `none` for an external one. A host
|
|
307
|
+
* room never previews.
|
|
308
|
+
*/
|
|
309
|
+
content?: "full" | "none";
|
|
310
|
+
/**
|
|
311
|
+
* The names of the transports besides `web-push` that the sender carries, such as `slack`. A
|
|
312
|
+
* notice's `via` may name only these and `web-push`; naming any other is a ConfigError when
|
|
313
|
+
* the notice is raised. The app never loads a transport: the sender process does.
|
|
314
|
+
*/
|
|
315
|
+
transports?: readonly string[];
|
|
316
|
+
};
|
|
317
|
+
/**
|
|
318
|
+
* What a person owes before the app opens. `links` names providers whose accounts they must
|
|
319
|
+
* link (`required` gates the app, `nudge` only asks); each needs a `LinkProver` registered
|
|
320
|
+
* through `WEB_APP`. `push` asks them to turn push on and receive a test; `pushWaiver` lets
|
|
321
|
+
* them waive a required push step.
|
|
322
|
+
*/
|
|
323
|
+
onboarding?: {
|
|
324
|
+
links?: Readonly<Record<string, "required" | "nudge">>;
|
|
325
|
+
push?: "required" | "nudge" | "off";
|
|
326
|
+
pushWaiver?: boolean;
|
|
208
327
|
};
|
|
328
|
+
/** Leave out the yolo and long-running switches: no mode is shown or set, and `PUT api/rooms/<key>/mode` answers 404. */
|
|
329
|
+
modes?: false;
|
|
330
|
+
/** The files of the app's agent rooms; `keepDays` is how long stored files are kept, default 30. */
|
|
331
|
+
uploads?: { keepDays?: number };
|
|
209
332
|
/** The page's title and heading; default `Roundtable`. */
|
|
210
333
|
title?: string;
|
|
211
334
|
/** Where the install icons come from; default the ones this package ships. */
|
|
@@ -214,8 +337,11 @@ export interface WebConsoleOptions {
|
|
|
214
337
|
appearance?: { look?: "modern" | "camelot" };
|
|
215
338
|
/** Text a relayed message begins with, taken off the owner's words; default the remote MCP note. */
|
|
216
339
|
relayNotes?: readonly string[];
|
|
217
|
-
/**
|
|
218
|
-
|
|
340
|
+
/**
|
|
341
|
+
* Conversations the console neither lists nor reads. `record` is what the host's registry
|
|
342
|
+
* holds of the conversation, if anything, so a host can decide by its kind or owner.
|
|
343
|
+
*/
|
|
344
|
+
exclude?: (key: ChannelKey, record?: ConversationRecord) => boolean;
|
|
219
345
|
/** The built page's directory; default the `dist/` this package ships. Set it only in tests. */
|
|
220
346
|
assetDir?: string;
|
|
221
347
|
}
|
|
@@ -230,7 +356,11 @@ export interface ResolvedOptions {
|
|
|
230
356
|
mount: string;
|
|
231
357
|
listener: string;
|
|
232
358
|
routing: "hash" | "path";
|
|
359
|
+
admission: "owners" | "access";
|
|
360
|
+
/** The owners' panes: what `panes` named, or the default. */
|
|
233
361
|
panes: PaneName[];
|
|
362
|
+
/** The panes each tier is served. Admins and members are served none unless `admission` is `access`. */
|
|
363
|
+
tierPanes: Readonly<Record<Tier, readonly PaneName[]>>;
|
|
234
364
|
/** Whether `panes` was written out, which makes a missing directory for the workspace pane an error. */
|
|
235
365
|
panesGiven: boolean;
|
|
236
366
|
/** The `workspace` option checked and filled in. */
|
|
@@ -250,12 +380,29 @@ export interface ResolvedOptions {
|
|
|
250
380
|
/** The VAPID contact; undefined = push is off. */
|
|
251
381
|
subject: string | undefined;
|
|
252
382
|
fetch: typeof fetch | undefined;
|
|
383
|
+
tiers: readonly Tier[];
|
|
384
|
+
sender:
|
|
385
|
+
| "internal"
|
|
386
|
+
| { external: { publicKey: string; subscribePath?: string } };
|
|
387
|
+
content: "full" | "none";
|
|
388
|
+
/** `web-push` and the host's own transports. */
|
|
389
|
+
transports: readonly string[];
|
|
253
390
|
};
|
|
391
|
+
onboarding: {
|
|
392
|
+
links: Readonly<Record<string, "required" | "nudge">>;
|
|
393
|
+
push: "required" | "nudge" | "off";
|
|
394
|
+
pushWaiver: boolean;
|
|
395
|
+
};
|
|
396
|
+
/** Whether the yolo and long-running switches are served. */
|
|
397
|
+
modes: boolean;
|
|
398
|
+
uploadKeepDays: number;
|
|
254
399
|
title: string;
|
|
255
400
|
iconsDir: string | undefined;
|
|
256
401
|
look: "modern" | "camelot";
|
|
257
402
|
relayNotes: readonly string[];
|
|
258
|
-
exclude:
|
|
403
|
+
exclude:
|
|
404
|
+
| ((key: ChannelKey, record?: ConversationRecord) => boolean)
|
|
405
|
+
| undefined;
|
|
259
406
|
assetDir: string | undefined;
|
|
260
407
|
features: WebConsoleOptions["features"];
|
|
261
408
|
presentation: ConsolePresentation | undefined;
|
|
@@ -302,31 +449,105 @@ function checkOrigin(origin: string): string {
|
|
|
302
449
|
return url.origin;
|
|
303
450
|
}
|
|
304
451
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
452
|
+
const DEFAULT_OWNER_PANES: readonly PaneName[] = [
|
|
453
|
+
"rooms",
|
|
454
|
+
"table",
|
|
455
|
+
"workspace",
|
|
456
|
+
"overview",
|
|
457
|
+
"schedules",
|
|
458
|
+
"conversations",
|
|
459
|
+
"notes",
|
|
460
|
+
];
|
|
461
|
+
|
|
462
|
+
/** The panes a member is served by default, of those the owners have. */
|
|
463
|
+
const DEFAULT_MEMBER_PANES: readonly PaneName[] = [
|
|
464
|
+
"rooms",
|
|
465
|
+
"conversations",
|
|
466
|
+
"notes",
|
|
467
|
+
"schedules",
|
|
468
|
+
"profile",
|
|
469
|
+
];
|
|
470
|
+
|
|
471
|
+
function checkPaneList(panes: readonly PaneName[], label: string): PaneName[] {
|
|
472
|
+
if (!Array.isArray(panes)) fail(`${label} must be a list of panes`);
|
|
473
|
+
if (panes.length === 0) fail(`${label} is empty; serve at least one pane`);
|
|
317
474
|
for (const pane of panes)
|
|
318
475
|
if (!PANES.includes(pane))
|
|
319
476
|
fail(
|
|
320
|
-
`unknown pane ${JSON.stringify(pane)}; the panes are ${PANES.join(", ")}`,
|
|
477
|
+
`unknown pane ${JSON.stringify(pane)} in ${label}; the panes are ${PANES.join(", ")}`,
|
|
321
478
|
);
|
|
322
|
-
if (new Set(panes).size !== panes.length) fail(
|
|
323
|
-
|
|
479
|
+
if (new Set(panes).size !== panes.length) fail(`${label} lists a pane twice`);
|
|
480
|
+
return [...panes];
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** The panes of the owners, as before: what the other panes need of them is checked here. */
|
|
484
|
+
function checkOwnerPanes(panes: readonly PaneName[]): PaneName[] {
|
|
485
|
+
const list = checkPaneList(panes, "panes");
|
|
486
|
+
if (list.includes("schedules") && !list.includes("rooms"))
|
|
324
487
|
fail("the schedules pane needs the rooms pane, which serves its API");
|
|
325
|
-
if (
|
|
488
|
+
if (list.includes("table") && !list.includes("rooms"))
|
|
326
489
|
fail("the table pane needs the rooms pane, whose agents it seats");
|
|
327
|
-
if (
|
|
490
|
+
if (list.includes("workspace") && !list.includes("rooms"))
|
|
328
491
|
fail("the workspace pane needs the rooms pane, which serves its API");
|
|
329
|
-
return
|
|
492
|
+
return list;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
interface CheckedPanes {
|
|
496
|
+
owner: PaneName[];
|
|
497
|
+
admin: PaneName[];
|
|
498
|
+
member: PaneName[];
|
|
499
|
+
given: boolean;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function checkPanes(
|
|
503
|
+
panes: WebConsoleOptions["panes"],
|
|
504
|
+
admission: "owners" | "access",
|
|
505
|
+
): CheckedPanes {
|
|
506
|
+
const tiered =
|
|
507
|
+
panes !== undefined && !Array.isArray(panes)
|
|
508
|
+
? (panes as TierPanes)
|
|
509
|
+
: undefined;
|
|
510
|
+
if (tiered && (typeof tiered !== "object" || tiered === null))
|
|
511
|
+
fail("panes must be a list, or { owner, admin?, member }");
|
|
512
|
+
const owner = checkOwnerPanes(
|
|
513
|
+
tiered
|
|
514
|
+
? tiered.owner
|
|
515
|
+
: ((panes as readonly PaneName[] | undefined) ?? [
|
|
516
|
+
...DEFAULT_OWNER_PANES,
|
|
517
|
+
...(admission === "access" ? (["profile"] as const) : []),
|
|
518
|
+
]),
|
|
519
|
+
);
|
|
520
|
+
if (!tiered || admission !== "access") {
|
|
521
|
+
if (tiered && admission === "owners")
|
|
522
|
+
fail(
|
|
523
|
+
'panes names panes per tier, but only admission "access" admits anyone but owners',
|
|
524
|
+
);
|
|
525
|
+
const member =
|
|
526
|
+
admission === "access"
|
|
527
|
+
? DEFAULT_MEMBER_PANES.filter((pane) => owner.includes(pane))
|
|
528
|
+
: [];
|
|
529
|
+
return { owner, admin: member, member, given: panes !== undefined };
|
|
530
|
+
}
|
|
531
|
+
const member = checkPaneList(tiered.member, "panes.member");
|
|
532
|
+
const admin = tiered.admin
|
|
533
|
+
? checkPaneList(tiered.admin, "panes.admin")
|
|
534
|
+
: [...member];
|
|
535
|
+
for (const [label, list] of [
|
|
536
|
+
["panes.member", member],
|
|
537
|
+
["panes.admin", admin],
|
|
538
|
+
] as const) {
|
|
539
|
+
for (const pane of list) {
|
|
540
|
+
if ((OWNER_ONLY_PANES as readonly string[]).includes(pane))
|
|
541
|
+
fail(
|
|
542
|
+
`${label} names the ${pane} pane, which only owners are served: it shows the whole host`,
|
|
543
|
+
);
|
|
544
|
+
if (!owner.includes(pane))
|
|
545
|
+
fail(
|
|
546
|
+
`${label} names the ${pane} pane, which the owners' panes do not have; the pane's API is served for the owners' list`,
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return { owner, admin, member, given: true };
|
|
330
551
|
}
|
|
331
552
|
|
|
332
553
|
/**
|
|
@@ -443,13 +664,143 @@ function checkAuth(options: WebConsoleOptions): ResolvedAuth {
|
|
|
443
664
|
);
|
|
444
665
|
return { mode, verifier: given as RequestVerifier };
|
|
445
666
|
}
|
|
667
|
+
case "oidc":
|
|
668
|
+
return checkOidc(auth as OidcOptions);
|
|
446
669
|
default:
|
|
447
670
|
fail(
|
|
448
|
-
`auth.mode ${JSON.stringify(mode)} is unknown; the modes are none, password and
|
|
671
|
+
`auth.mode ${JSON.stringify(mode)} is unknown; the modes are none, password, verifier and oidc`,
|
|
449
672
|
);
|
|
450
673
|
}
|
|
451
674
|
}
|
|
452
675
|
|
|
676
|
+
const isHttpsUrl = (value: unknown): value is string =>
|
|
677
|
+
typeof value === "string" && URL.parse(value)?.protocol === "https:";
|
|
678
|
+
|
|
679
|
+
/** A provider on this machine, which a test or a local identity provider may use over plain http. */
|
|
680
|
+
const isLoopbackHttp = (value: unknown): boolean => {
|
|
681
|
+
const url = typeof value === "string" ? URL.parse(value) : null;
|
|
682
|
+
return (
|
|
683
|
+
url?.protocol === "http:" &&
|
|
684
|
+
["127.0.0.1", "[::1]", "localhost"].includes(url.hostname)
|
|
685
|
+
);
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
const providerUrl = (value: unknown): value is string =>
|
|
689
|
+
isHttpsUrl(value) || isLoopbackHttp(value);
|
|
690
|
+
|
|
691
|
+
function checkOidc(auth: OidcOptions): ResolvedOidc {
|
|
692
|
+
const known = new Set([
|
|
693
|
+
"mode",
|
|
694
|
+
"issuer",
|
|
695
|
+
"discoveryUrl",
|
|
696
|
+
"clientId",
|
|
697
|
+
"clientSecretEnv",
|
|
698
|
+
"scopes",
|
|
699
|
+
"subjectClaim",
|
|
700
|
+
"nameClaim",
|
|
701
|
+
"rolesClaim",
|
|
702
|
+
"check",
|
|
703
|
+
"sessionDays",
|
|
704
|
+
"endSession",
|
|
705
|
+
"reauthHours",
|
|
706
|
+
"storeClaims",
|
|
707
|
+
]);
|
|
708
|
+
for (const key of Object.keys(auth))
|
|
709
|
+
if (!known.has(key)) fail(`auth.${key} is not an oidc option`);
|
|
710
|
+
if (!providerUrl(auth.issuer))
|
|
711
|
+
fail("auth.issuer must be the provider's https URL, exactly its `iss`");
|
|
712
|
+
const { issuer } = auth;
|
|
713
|
+
const discoveryUrl =
|
|
714
|
+
auth.discoveryUrl ??
|
|
715
|
+
`${issuer.replace(/\/+$/, "")}/.well-known/openid-configuration`;
|
|
716
|
+
if (!providerUrl(discoveryUrl))
|
|
717
|
+
fail("auth.discoveryUrl must be an https URL");
|
|
718
|
+
if (typeof auth.clientId !== "string" || !auth.clientId.trim())
|
|
719
|
+
fail("auth.clientId is empty");
|
|
720
|
+
if (
|
|
721
|
+
auth.clientSecretEnv !== undefined &&
|
|
722
|
+
(typeof auth.clientSecretEnv !== "string" || !auth.clientSecretEnv.trim())
|
|
723
|
+
)
|
|
724
|
+
fail(
|
|
725
|
+
"auth.clientSecretEnv must name an environment variable; leave it out for a public client",
|
|
726
|
+
);
|
|
727
|
+
const scopes = auth.scopes ?? ["openid", "profile"];
|
|
728
|
+
if (
|
|
729
|
+
!Array.isArray(scopes) ||
|
|
730
|
+
scopes.length === 0 ||
|
|
731
|
+
!scopes.every(
|
|
732
|
+
(scope) =>
|
|
733
|
+
typeof scope === "string" && /^[\x21\x23-\x5b\x5d-\x7e]+$/.test(scope),
|
|
734
|
+
)
|
|
735
|
+
)
|
|
736
|
+
fail("auth.scopes must be a list of scope names");
|
|
737
|
+
if (scopes.includes("offline_access"))
|
|
738
|
+
fail(
|
|
739
|
+
"auth.scopes has offline_access, but the app keeps no refresh token: its own session is what lasts",
|
|
740
|
+
);
|
|
741
|
+
if (!scopes.includes("openid")) fail("auth.scopes must include openid");
|
|
742
|
+
const claim = (name: string, value: unknown) => {
|
|
743
|
+
if (value !== undefined && (typeof value !== "string" || !value.trim()))
|
|
744
|
+
fail(`auth.${name} must be the name of a claim`);
|
|
745
|
+
};
|
|
746
|
+
claim("subjectClaim", auth.subjectClaim);
|
|
747
|
+
claim("nameClaim", auth.nameClaim);
|
|
748
|
+
if (
|
|
749
|
+
auth.rolesClaim !== undefined &&
|
|
750
|
+
auth.rolesClaim !== false &&
|
|
751
|
+
(typeof auth.rolesClaim !== "string" || !auth.rolesClaim.trim())
|
|
752
|
+
)
|
|
753
|
+
fail("auth.rolesClaim must be the name of a claim, or false");
|
|
754
|
+
if (auth.check !== undefined && typeof auth.check !== "function")
|
|
755
|
+
fail("auth.check must be a function");
|
|
756
|
+
const sessionDays = auth.sessionDays ?? DEFAULT_SESSION_DAYS;
|
|
757
|
+
if (
|
|
758
|
+
typeof sessionDays !== "number" ||
|
|
759
|
+
!Number.isInteger(sessionDays) ||
|
|
760
|
+
sessionDays < 1 ||
|
|
761
|
+
sessionDays > 365
|
|
762
|
+
)
|
|
763
|
+
fail("auth.sessionDays must be a whole number of days from 1 to 365");
|
|
764
|
+
if (auth.endSession !== undefined && typeof auth.endSession !== "boolean")
|
|
765
|
+
fail("auth.endSession must be true or false");
|
|
766
|
+
if (
|
|
767
|
+
auth.reauthHours !== undefined &&
|
|
768
|
+
(typeof auth.reauthHours !== "number" ||
|
|
769
|
+
!(auth.reauthHours > 0) ||
|
|
770
|
+
auth.reauthHours > sessionDays * 24)
|
|
771
|
+
)
|
|
772
|
+
fail(
|
|
773
|
+
"auth.reauthHours must be a number of hours above 0 and within the session's own days",
|
|
774
|
+
);
|
|
775
|
+
const storeClaims = auth.storeClaims ?? [];
|
|
776
|
+
if (
|
|
777
|
+
!Array.isArray(storeClaims) ||
|
|
778
|
+
!storeClaims.every((name) => typeof name === "string" && name.trim())
|
|
779
|
+
)
|
|
780
|
+
fail("auth.storeClaims must be a list of claim names");
|
|
781
|
+
return {
|
|
782
|
+
mode: "oidc",
|
|
783
|
+
issuer,
|
|
784
|
+
discoveryUrl,
|
|
785
|
+
clientId: auth.clientId.trim(),
|
|
786
|
+
clientSecretEnv: auth.clientSecretEnv?.trim(),
|
|
787
|
+
scopes: [...scopes],
|
|
788
|
+
subjectClaim: auth.subjectClaim?.trim() ?? "sub",
|
|
789
|
+
nameClaim: auth.nameClaim?.trim(),
|
|
790
|
+
rolesClaim:
|
|
791
|
+
auth.rolesClaim === undefined
|
|
792
|
+
? "roles"
|
|
793
|
+
: typeof auth.rolesClaim === "string"
|
|
794
|
+
? auth.rolesClaim.trim()
|
|
795
|
+
: false,
|
|
796
|
+
check: auth.check,
|
|
797
|
+
sessionDays,
|
|
798
|
+
endSession: auth.endSession === true,
|
|
799
|
+
reauthHours: auth.reauthHours,
|
|
800
|
+
storeClaims: [...storeClaims],
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
|
|
453
804
|
function checkSurface(surface: string | undefined): string {
|
|
454
805
|
if (surface === undefined) return DEFAULT_SURFACE;
|
|
455
806
|
if (typeof surface !== "string" || !/^[a-z][a-z0-9-]*$/.test(surface))
|
|
@@ -516,15 +867,108 @@ function checkPush(
|
|
|
516
867
|
fail(
|
|
517
868
|
"push.subject must be a mailto: address or an https: URL, such as mailto:ops@example.com",
|
|
518
869
|
);
|
|
870
|
+
const tiers = push?.tiers ?? ["owner"];
|
|
871
|
+
if (
|
|
872
|
+
!Array.isArray(tiers) ||
|
|
873
|
+
tiers.length === 0 ||
|
|
874
|
+
!tiers.every((tier) => TIERS.includes(tier))
|
|
875
|
+
)
|
|
876
|
+
fail(`push.tiers must be a list of ${TIERS.join(", ")}`);
|
|
877
|
+
const sender = push?.sender ?? "internal";
|
|
878
|
+
if (sender !== "internal") {
|
|
879
|
+
const external = (sender as { external?: unknown } | null)?.external as
|
|
880
|
+
| { publicKey?: unknown; subscribePath?: unknown }
|
|
881
|
+
| undefined;
|
|
882
|
+
if (
|
|
883
|
+
typeof external !== "object" ||
|
|
884
|
+
external === null ||
|
|
885
|
+
typeof external.publicKey !== "string" ||
|
|
886
|
+
!/^[A-Za-z0-9_-]{80,100}$/.test(external.publicKey) ||
|
|
887
|
+
(external.subscribePath !== undefined &&
|
|
888
|
+
(typeof external.subscribePath !== "string" ||
|
|
889
|
+
!external.subscribePath.startsWith("/")))
|
|
890
|
+
)
|
|
891
|
+
fail(
|
|
892
|
+
'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',
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
const content = push?.content ?? (sender === "internal" ? "full" : "none");
|
|
896
|
+
if (content !== "full" && content !== "none")
|
|
897
|
+
fail('push.content must be "full" or "none"');
|
|
898
|
+
const extra = push?.transports ?? [];
|
|
899
|
+
if (
|
|
900
|
+
!Array.isArray(extra) ||
|
|
901
|
+
!extra.every(
|
|
902
|
+
(name) => typeof name === "string" && /^[a-z][a-z0-9-]{0,39}$/.test(name),
|
|
903
|
+
)
|
|
904
|
+
)
|
|
905
|
+
fail("push.transports must be a list of transport names such as slack");
|
|
519
906
|
return {
|
|
520
907
|
preview: push?.preview ?? true,
|
|
521
908
|
quietHours,
|
|
522
909
|
endpointHosts: [...hosts],
|
|
523
910
|
subject: given ?? (origin.startsWith("https:") ? origin : undefined),
|
|
524
911
|
fetch: pushFetchOf(options),
|
|
912
|
+
tiers: [...new Set(tiers)],
|
|
913
|
+
sender:
|
|
914
|
+
sender === "internal"
|
|
915
|
+
? "internal"
|
|
916
|
+
: {
|
|
917
|
+
external: {
|
|
918
|
+
publicKey: (sender as { external: { publicKey: string } })
|
|
919
|
+
.external.publicKey,
|
|
920
|
+
...((sender as { external: { subscribePath?: string } }).external
|
|
921
|
+
.subscribePath
|
|
922
|
+
? {
|
|
923
|
+
subscribePath: (
|
|
924
|
+
sender as { external: { subscribePath: string } }
|
|
925
|
+
).external.subscribePath,
|
|
926
|
+
}
|
|
927
|
+
: {}),
|
|
928
|
+
},
|
|
929
|
+
},
|
|
930
|
+
content,
|
|
931
|
+
transports: [
|
|
932
|
+
"web-push",
|
|
933
|
+
...new Set(extra.filter((name) => name !== "web-push")),
|
|
934
|
+
],
|
|
525
935
|
};
|
|
526
936
|
}
|
|
527
937
|
|
|
938
|
+
function checkOnboarding(
|
|
939
|
+
options: WebConsoleOptions,
|
|
940
|
+
): ResolvedOptions["onboarding"] {
|
|
941
|
+
const given = options.onboarding;
|
|
942
|
+
if (given === undefined) return { links: {}, push: "off", pushWaiver: false };
|
|
943
|
+
if (typeof given !== "object" || given === null)
|
|
944
|
+
fail("onboarding must be an object such as { links: { line: 'nudge' } }");
|
|
945
|
+
const links = given.links ?? {};
|
|
946
|
+
if (typeof links !== "object" || links === null || Array.isArray(links))
|
|
947
|
+
fail("onboarding.links must map a provider to required or nudge");
|
|
948
|
+
for (const [provider, step] of Object.entries(links)) {
|
|
949
|
+
if (!provider.trim())
|
|
950
|
+
fail("onboarding.links names a provider with an empty name");
|
|
951
|
+
if (step !== "required" && step !== "nudge")
|
|
952
|
+
fail(
|
|
953
|
+
`onboarding.links.${provider} must be required or nudge, not ${JSON.stringify(step)}`,
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
const push = given.push ?? "off";
|
|
957
|
+
if (push !== "required" && push !== "nudge" && push !== "off")
|
|
958
|
+
fail("onboarding.push must be required, nudge or off");
|
|
959
|
+
if (given.pushWaiver !== undefined && typeof given.pushWaiver !== "boolean")
|
|
960
|
+
fail("onboarding.pushWaiver must be true or false");
|
|
961
|
+
return { links: { ...links }, push, pushWaiver: given.pushWaiver === true };
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function checkUploads(options: WebConsoleOptions): number {
|
|
965
|
+
const days = options.uploads?.keepDays;
|
|
966
|
+
if (days === undefined) return 30;
|
|
967
|
+
if (!Number.isInteger(days) || days < 1 || days > 3650)
|
|
968
|
+
fail("uploads.keepDays must be a whole number of days from 1 to 3650");
|
|
969
|
+
return days;
|
|
970
|
+
}
|
|
971
|
+
|
|
528
972
|
/** The largest `uploadMaxBytes`: the limit is also the listener's body limit. */
|
|
529
973
|
const MAX_UPLOAD_MAX_BYTES = 4 * 1024 ** 3;
|
|
530
974
|
|
|
@@ -558,7 +1002,22 @@ export function resolveOptions(options: WebConsoleOptions): ResolvedOptions {
|
|
|
558
1002
|
fail("presentation.locale is not a valid locale");
|
|
559
1003
|
}
|
|
560
1004
|
}
|
|
561
|
-
const
|
|
1005
|
+
const admission = options.admission ?? "owners";
|
|
1006
|
+
if (admission !== "owners" && admission !== "access")
|
|
1007
|
+
fail('admission must be "owners" or "access"');
|
|
1008
|
+
if (
|
|
1009
|
+
admission === "access" &&
|
|
1010
|
+
(auth.mode === "none" || auth.mode === "password")
|
|
1011
|
+
)
|
|
1012
|
+
fail(
|
|
1013
|
+
`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`,
|
|
1014
|
+
);
|
|
1015
|
+
const origin = checkOrigin(options.origin);
|
|
1016
|
+
if (auth.mode === "oidc" && !origin.startsWith("https:"))
|
|
1017
|
+
fail("auth oidc needs an https origin: the session cookie is Secure");
|
|
1018
|
+
if (options.modes !== undefined && options.modes !== false)
|
|
1019
|
+
fail("modes can only be false, to leave the mode switches out");
|
|
1020
|
+
const panes = checkPanes(options.panes, admission);
|
|
562
1021
|
const ownerId = options.ownerId?.trim();
|
|
563
1022
|
if (options.ownerId !== undefined && !ownerId)
|
|
564
1023
|
fail(
|
|
@@ -571,20 +1030,25 @@ export function resolveOptions(options: WebConsoleOptions): ResolvedOptions {
|
|
|
571
1030
|
if (!title) fail("title is empty");
|
|
572
1031
|
return {
|
|
573
1032
|
auth,
|
|
574
|
-
origin
|
|
1033
|
+
origin,
|
|
575
1034
|
ownerId,
|
|
576
1035
|
dataDir: options.dataDir ?? "data",
|
|
577
1036
|
sessionsDir: join(options.dataDir ?? "data", "sessions"),
|
|
578
1037
|
mount: checkMount(options.mountPath ?? "/console"),
|
|
579
1038
|
listener: options.listener ?? "public",
|
|
580
1039
|
routing: options.routing ?? "hash",
|
|
581
|
-
|
|
582
|
-
|
|
1040
|
+
admission,
|
|
1041
|
+
panes: panes.owner,
|
|
1042
|
+
tierPanes: { owner: panes.owner, admin: panes.admin, member: panes.member },
|
|
1043
|
+
panesGiven: panes.given,
|
|
583
1044
|
workspace: checkWorkspace(options, options.dataDir ?? "data"),
|
|
584
1045
|
surface: checkSurface(options.surface),
|
|
585
1046
|
cardGraceSeconds: checkGrace(options.cardGraceSeconds),
|
|
586
1047
|
uploadMaxBytes: checkUploadMax(options.uploadMaxBytes),
|
|
587
|
-
push: checkPush(options,
|
|
1048
|
+
push: checkPush(options, origin),
|
|
1049
|
+
onboarding: checkOnboarding(options),
|
|
1050
|
+
modes: options.modes !== false,
|
|
1051
|
+
uploadKeepDays: checkUploads(options),
|
|
588
1052
|
title,
|
|
589
1053
|
iconsDir: options.icons?.dir,
|
|
590
1054
|
look,
|