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.
Files changed (243) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +165 -12
  3. package/dist/account-zk4efb87.css +102 -0
  4. package/dist/chunk-account-ghq105t4.js +1 -0
  5. package/dist/chunk-action-menu-me0g9qsc.js +1 -0
  6. package/dist/{chunk-agent-0pm9p3nn.js → chunk-agent-rdv7ekrt.js} +1 -1
  7. package/dist/chunk-arranged-rooms-81rqhx9x.js +1 -0
  8. package/dist/chunk-character-creator-bd426yjw.js +1 -0
  9. package/dist/chunk-codeeditor-ajhgd33t.js +2 -0
  10. package/dist/chunk-confirm-p3mts8mx.js +1 -0
  11. package/dist/{chunk-connectors-anhghy3k.js → chunk-connectors-bkv3xn83.js} +1 -1
  12. package/dist/chunk-conversations-0e0vbxwr.js +1 -0
  13. package/dist/{chunk-drag-q4fx9b22.js → chunk-drag-peyb8t0k.js} +1 -1
  14. package/dist/{chunk-entry-he1embgp.js → chunk-entry-mv7q8br0.js} +1 -1
  15. package/dist/chunk-folder-list-era3tksh.js +1 -0
  16. package/dist/chunk-folds-hvws0bd5.js +1 -0
  17. package/dist/chunk-gate-hc7dd0fx.js +1 -0
  18. package/dist/chunk-group-scene-dkbpmbh5.js +1 -0
  19. package/dist/chunk-group-table-fqmxgdfw.js +1 -0
  20. package/dist/chunk-host-icons-4xde9ntk.js +1 -0
  21. package/dist/chunk-host-picture-014yzk20.js +1 -0
  22. package/dist/chunk-host-state-t56e5sdy.js +1 -0
  23. package/dist/chunk-host-ui-p1zy9nmp.js +1 -0
  24. package/dist/chunk-index-0fmmvgee.js +2 -0
  25. package/dist/chunk-index-0q15ysrb.js +1 -0
  26. package/dist/chunk-index-0xh24y25.js +2 -0
  27. package/dist/chunk-index-2yebd9tb.js +426 -0
  28. package/dist/chunk-index-4t0r3t3b.js +2 -0
  29. package/dist/chunk-index-5yzfrf7b.js +138 -0
  30. package/dist/{chunk-index-d5abvq7v.js → chunk-index-6twb7ccd.js} +2 -2
  31. package/dist/chunk-index-73gh7kgr.js +920 -0
  32. package/dist/chunk-index-7xffskfa.js +8 -0
  33. package/dist/chunk-index-aqw7cy5j.js +1 -0
  34. package/dist/chunk-index-b6ca02e8.js +2 -0
  35. package/dist/chunk-index-b8nvhcx5.js +2 -0
  36. package/dist/{chunk-index-bvfqjh77.js → chunk-index-cvbdf2ca.js} +2 -2
  37. package/dist/chunk-index-czdketze.js +2 -0
  38. package/dist/{chunk-index-9jqcskb4.js → chunk-index-dt9775ka.js} +5 -5
  39. package/dist/chunk-index-eg55mqqe.js +1 -0
  40. package/dist/chunk-index-gr6e0hse.js +2 -0
  41. package/dist/chunk-index-gyary7np.js +13 -0
  42. package/dist/chunk-index-j214tczc.js +1 -0
  43. package/dist/chunk-index-jd6632ef.js +2 -0
  44. package/dist/chunk-index-jw7dtshr.js +2 -0
  45. package/dist/{chunk-index-krtk6nn2.js → chunk-index-k7t14rwa.js} +2 -2
  46. package/dist/chunk-index-kwcx62wb.js +4 -0
  47. package/dist/chunk-index-m7fvw92f.js +2 -0
  48. package/dist/chunk-index-mmg788a1.js +2 -0
  49. package/dist/chunk-index-mq0dacwr.js +2 -0
  50. package/dist/chunk-index-p53ak25v.js +2 -0
  51. package/dist/{chunk-index-27qca4mr.js → chunk-index-pk1yxkv4.js} +2 -2
  52. package/dist/{chunk-index-9b1kt7qw.css → chunk-index-rbcz9jd4.css} +1 -1
  53. package/dist/{chunk-index-s681nny9.js → chunk-index-rmxbdmn2.js} +2 -2
  54. package/dist/chunk-index-rnxj3z8k.js +2 -0
  55. package/dist/chunk-index-snzzfhk3.js +2 -0
  56. package/dist/chunk-index-v3bmr0nw.js +2 -0
  57. package/dist/chunk-index-vcyzwtk2.js +2 -0
  58. package/dist/{chunk-index-zfd00hwb.js → chunk-index-w2j3njb6.js} +2 -2
  59. package/dist/chunk-index-w31c4yjv.js +2 -0
  60. package/dist/chunk-index-wrm8w165.js +2 -0
  61. package/dist/chunk-index-xh0mg9cw.js +2 -0
  62. package/dist/chunk-index-ybtvvx85.js +2 -0
  63. package/dist/chunk-index-yzsgsvnp.js +2 -0
  64. package/dist/{chunk-index-18gxy79n.js → chunk-index-z9x4cnzf.js} +2 -2
  65. package/dist/chunk-index-zrvg23jd.js +2 -0
  66. package/dist/chunk-index-zynq0b0w.js +50 -0
  67. package/dist/chunk-lang-css-88v3hp3d.js +1 -0
  68. package/dist/{chunk-lang-dockerfile-d5mzxs60.js → chunk-lang-dockerfile-k728ewsf.js} +1 -1
  69. package/dist/{chunk-lang-go-830fmhba.js → chunk-lang-go-wjwgay4v.js} +3 -3
  70. package/dist/chunk-lang-html-07d47es4.js +1 -0
  71. package/dist/chunk-lang-javascript-fmhhgzwd.js +1 -0
  72. package/dist/{chunk-lang-json-5eaarz7j.js → chunk-lang-json-7gd89mvs.js} +1 -1
  73. package/dist/chunk-lang-markdown-b6qbe8bt.js +6 -0
  74. package/dist/{chunk-lang-python-fjw84w10.js → chunk-lang-python-3v56y31b.js} +2 -2
  75. package/dist/{chunk-lang-rust-5rbp2zdk.js → chunk-lang-rust-kh0z74d1.js} +1 -1
  76. package/dist/{chunk-lang-shell-vzagpym3.js → chunk-lang-shell-9zv0zvbx.js} +1 -1
  77. package/dist/{chunk-lang-sql-0c2w9c2y.js → chunk-lang-sql-yw6bf3t7.js} +2 -2
  78. package/dist/{chunk-lang-toml-yr9fv6eb.js → chunk-lang-toml-tg1r96nq.js} +1 -1
  79. package/dist/chunk-lang-typescript-etjjvr2g.js +1 -0
  80. package/dist/{chunk-lang-xml-eze08p42.js → chunk-lang-xml-ev1mndd4.js} +1 -1
  81. package/dist/{chunk-lang-yaml-c4b6ft3y.js → chunk-lang-yaml-fz5qpb3r.js} +1 -1
  82. package/dist/chunk-layout-actions-48ecgtkg.js +1 -0
  83. package/dist/{chunk-menu-items-jrwxycr1.js → chunk-menu-items-1epy3h0t.js} +2 -2
  84. package/dist/chunk-mode-toggle-fxmfxsa4.js +1 -0
  85. package/dist/chunk-note-row-kd0fy7ee.js +75 -0
  86. package/dist/chunk-notes-yb8f057c.js +1 -0
  87. package/dist/chunk-notice-bell-wr4f1rph.js +1 -0
  88. package/dist/chunk-notices-x07da5bb.js +1 -0
  89. package/dist/chunk-onboarding-csrkbh9z.js +1 -0
  90. package/dist/chunk-overview-avc2d30t.js +1 -0
  91. package/dist/chunk-palette-9nhp95bz.js +1 -0
  92. package/dist/chunk-placement-items-q694dpgk.js +1 -0
  93. package/dist/chunk-profile-6qemf3m7.js +1 -0
  94. package/dist/chunk-prompt-history-7enzp2p3.js +1 -0
  95. package/dist/chunk-prompts-nv6mrn0q.js +1 -0
  96. package/dist/chunk-room-confirms-zz7n61y7.js +1 -0
  97. package/dist/chunk-room-menu-rr6nzap1.js +1 -0
  98. package/dist/chunk-rooms-menu-button-zbtwj47d.js +1 -0
  99. package/dist/chunk-schedules-rw57nr0m.js +1 -0
  100. package/dist/{chunk-seat-menu-jv4s7hmj.js → chunk-seat-menu-ys5v9abz.js} +1 -1
  101. package/dist/chunk-sheet-hosts-jhvaacst.js +1 -0
  102. package/dist/chunk-skill-editor-sz8an8qr.js +8 -0
  103. package/dist/chunk-skills-tnr1h16h.js +1 -0
  104. package/dist/chunk-switcher-list-17enhrse.js +1 -0
  105. package/dist/chunk-tab-dot-r6brdbmw.js +1 -0
  106. package/dist/chunk-table-r12cja6p.js +1 -0
  107. package/dist/chunk-table-seats-g3t392zz.js +1 -0
  108. package/dist/{chunk-table2d-4sje1fr0.js → chunk-table2d-p3ezsdcj.js} +1 -1
  109. package/dist/{chunk-table3d-h64skazp.js → chunk-table3d-4x2n2b00.js} +23 -23
  110. package/dist/chunk-thread-actions-qc6ezkha.js +1 -0
  111. package/dist/chunk-transcript-n2s6dher.js +1 -0
  112. package/dist/chunk-where-f3scdydc.js +1 -0
  113. package/dist/chunk-workspace-xs023bxm.js +1 -0
  114. package/dist/chunk-zh-tw-efvmk3fd.js +1 -0
  115. package/dist/index.html +1 -1
  116. package/dist/sw.js +1 -1
  117. package/package.json +5 -4
  118. package/src/api-types.ts +356 -4
  119. package/src/app/app.ts +201 -32
  120. package/src/app/desk.ts +47 -22
  121. package/src/app/files.ts +73 -7
  122. package/src/app/presence.ts +2 -2
  123. package/src/app/protocol.ts +63 -1
  124. package/src/app/room-log.ts +153 -14
  125. package/src/app/rooms.ts +39 -0
  126. package/src/app/routes.ts +168 -28
  127. package/src/app/socket.ts +349 -81
  128. package/src/app/sockets.ts +108 -20
  129. package/src/app/surface.ts +72 -36
  130. package/src/app/threads.ts +31 -2
  131. package/src/app/uploads.ts +33 -7
  132. package/src/auth/authenticator.ts +77 -8
  133. package/src/auth/login-page.ts +4 -2
  134. package/src/auth/next.ts +52 -0
  135. package/src/auth/oidc-pages.ts +62 -0
  136. package/src/auth/oidc.ts +649 -0
  137. package/src/auth/sessions.ts +193 -0
  138. package/src/authz.ts +173 -0
  139. package/src/console-api.ts +122 -25
  140. package/src/console-server.ts +102 -12
  141. package/src/gate.ts +90 -0
  142. package/src/host/access.ts +161 -0
  143. package/src/host/cards.ts +112 -0
  144. package/src/host/context.ts +188 -0
  145. package/src/host/forms.ts +240 -0
  146. package/src/host/frames.ts +349 -0
  147. package/src/host/host-app.ts +178 -0
  148. package/src/host/host-context.ts +49 -0
  149. package/src/host/host-routes.ts +396 -0
  150. package/src/host/pages.ts +266 -0
  151. package/src/host/reads.ts +155 -0
  152. package/src/host/retention.ts +41 -0
  153. package/src/host/room-views.ts +137 -0
  154. package/src/host/rooms-service.ts +429 -0
  155. package/src/host/stored-json.ts +13 -0
  156. package/src/host/views.ts +106 -0
  157. package/src/i18n/zh-tw.ts +139 -0
  158. package/src/index.ts +15 -1
  159. package/src/notices/api.ts +43 -0
  160. package/src/notices/mount.ts +292 -0
  161. package/src/notices/service.ts +294 -0
  162. package/src/notices/store.ts +298 -0
  163. package/src/notices/urls.ts +38 -0
  164. package/src/onboarding/api.ts +32 -0
  165. package/src/onboarding/service.ts +149 -0
  166. package/src/onboarding/steps.ts +75 -0
  167. package/src/onboarding/waivers.ts +40 -0
  168. package/src/options.ts +534 -34
  169. package/src/page-presentation.ts +47 -2
  170. package/src/profile/api.ts +175 -0
  171. package/src/profile/links.ts +162 -0
  172. package/src/push/api.ts +189 -52
  173. package/src/push/dispatch.ts +156 -0
  174. package/src/push/drain.ts +139 -0
  175. package/src/push/outbox.ts +218 -0
  176. package/src/push/prefs.ts +118 -0
  177. package/src/push/sender.ts +7 -2
  178. package/src/push/triggers.ts +56 -5
  179. package/src/push/vapid.ts +19 -0
  180. package/src/push/web-push-transport.ts +106 -0
  181. package/src/push-sender/index.ts +14 -0
  182. package/src/push-sender/run.ts +118 -0
  183. package/src/push-sender/session.ts +54 -0
  184. package/src/pwa.ts +32 -4
  185. package/src/visitors.ts +94 -6
  186. package/src/web-app.ts +718 -0
  187. package/src/web-plugin.ts +279 -26
  188. package/dist/chunk-action-menu-qe839g4b.js +0 -1
  189. package/dist/chunk-arranged-rooms-enpfswmg.js +0 -1
  190. package/dist/chunk-character-creator-6yf29xwx.js +0 -1
  191. package/dist/chunk-codeeditor-gdbdg7rp.js +0 -2
  192. package/dist/chunk-confirm-xrw7d732.js +0 -1
  193. package/dist/chunk-conversations-ry228aes.js +0 -1
  194. package/dist/chunk-folder-list-5cp8stna.js +0 -1
  195. package/dist/chunk-folds-hff104tk.js +0 -1
  196. package/dist/chunk-group-scene-h228jdat.js +0 -1
  197. package/dist/chunk-index-0p5h2kdz.js +0 -2
  198. package/dist/chunk-index-0pt08yd9.js +0 -1
  199. package/dist/chunk-index-12bw9yj8.js +0 -2
  200. package/dist/chunk-index-2ad17eh4.js +0 -2
  201. package/dist/chunk-index-31wh5twj.js +0 -52
  202. package/dist/chunk-index-4dy94pbz.js +0 -2
  203. package/dist/chunk-index-4ecjtdy8.js +0 -13
  204. package/dist/chunk-index-4v1b7ks9.js +0 -2
  205. package/dist/chunk-index-5d8yjj4m.js +0 -2
  206. package/dist/chunk-index-89ajmxh7.js +0 -2
  207. package/dist/chunk-index-b9ny4623.js +0 -2
  208. package/dist/chunk-index-e1dr6g10.js +0 -2
  209. package/dist/chunk-index-gpkzhq1g.js +0 -2
  210. package/dist/chunk-index-hk3xyy20.js +0 -2
  211. package/dist/chunk-index-jm23vn03.js +0 -4
  212. package/dist/chunk-index-k7gh0jvz.js +0 -2
  213. package/dist/chunk-index-ky42ev0b.js +0 -2
  214. package/dist/chunk-index-pa2yyqh0.js +0 -2
  215. package/dist/chunk-index-pcvt668q.js +0 -2
  216. package/dist/chunk-index-pqw27eye.js +0 -1
  217. package/dist/chunk-index-wmbmmrd4.js +0 -302
  218. package/dist/chunk-index-ymbjrsp6.js +0 -2
  219. package/dist/chunk-lang-css-t85r8rz0.js +0 -1
  220. package/dist/chunk-lang-html-n6jparge.js +0 -1
  221. package/dist/chunk-lang-javascript-xj00cp97.js +0 -1
  222. package/dist/chunk-lang-markdown-zxx8eke8.js +0 -6
  223. package/dist/chunk-lang-typescript-8f6yywt3.js +0 -1
  224. package/dist/chunk-layout-actions-bv0chj41.js +0 -1
  225. package/dist/chunk-notes-9b7pgqfe.js +0 -1
  226. package/dist/chunk-overview-8yw71esz.js +0 -1
  227. package/dist/chunk-palette-frms7axe.js +0 -1
  228. package/dist/chunk-placement-items-fvfy9m0y.js +0 -1
  229. package/dist/chunk-prompt-history-yrqehads.js +0 -1
  230. package/dist/chunk-prompts-g6609tqx.js +0 -1
  231. package/dist/chunk-room-confirms-0a2y0925.js +0 -1
  232. package/dist/chunk-room-menu-g11q97br.js +0 -1
  233. package/dist/chunk-rooms-menu-button-h9qge48n.js +0 -1
  234. package/dist/chunk-schedules-ezmacrbj.js +0 -1
  235. package/dist/chunk-sheet-hosts-b0azyfhb.js +0 -1
  236. package/dist/chunk-skill-editor-h6k6whwg.js +0 -8
  237. package/dist/chunk-skills-gqcfhq56.js +0 -1
  238. package/dist/chunk-switcher-list-mcpamqnj.js +0 -1
  239. package/dist/chunk-table-s8cde1xz.js +0 -1
  240. package/dist/chunk-table-seats-wqkjn8bc.js +0 -1
  241. package/dist/chunk-transcript-c59c6mpm.js +0 -1
  242. package/dist/chunk-workspace-0cyspkk4.js +0 -1
  243. 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 { ChannelKey, RoundtablePlugin } from "pi-roundtable";
3
- import { PluginError } from "pi-roundtable";
4
- import { PANES, type PaneName } from "./api-types.ts";
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
- /** Conversations the console neither lists nor reads. */
218
- exclude?: (key: ChannelKey) => boolean;
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: ((key: ChannelKey) => boolean) | undefined;
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
- function checkPanes(panes: readonly PaneName[] | undefined): PaneName[] {
306
- if (panes === undefined)
307
- return [
308
- "rooms",
309
- "table",
310
- "workspace",
311
- "overview",
312
- "schedules",
313
- "conversations",
314
- "notes",
315
- ];
316
- if (panes.length === 0) fail("panes is empty; serve at least one pane");
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("panes lists a pane twice");
323
- if (panes.includes("schedules") && !panes.includes("rooms"))
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 (panes.includes("table") && !panes.includes("rooms"))
505
+ if (list.includes("table") && !list.includes("rooms"))
326
506
  fail("the table pane needs the rooms pane, whose agents it seats");
327
- if (panes.includes("workspace") && !panes.includes("rooms"))
507
+ if (list.includes("workspace") && !list.includes("rooms"))
328
508
  fail("the workspace pane needs the rooms pane, which serves its API");
329
- return [...panes];
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 verifier`,
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 panes = checkPanes(options.panes);
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: checkOrigin(options.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
- panes,
582
- panesGiven: options.panes !== undefined,
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, checkOrigin(options.origin)),
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],