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
@@ -4,8 +4,10 @@ import type { ResolvedAuth } from "../options.ts";
4
4
  import type { Verdict } from "../verifier.ts";
5
5
  import type { Identified, Visitor } from "../visitors.ts";
6
6
  import { checkNoneListener } from "./none.ts";
7
+ import type { OidcLogin } from "./oidc.ts";
7
8
  import { checkPassword, PasswordCredential } from "./password.ts";
8
9
  import { loadSessionKey } from "./session-key.ts";
10
+ import type { WebSessions } from "./sessions.ts";
9
11
 
10
12
  /** The plugin identity `none` and `password` speak through; `RoundtablePlugin.identities` links it to the owner. */
11
13
  export const WEB_CONSOLE_IDENTITY = "token:web-console";
@@ -22,6 +24,14 @@ export interface Authenticated {
22
24
  visitor: Visitor;
23
25
  actor?: ActorFacts;
24
26
  expiresAt?: Date;
27
+ /** The session the credential is, when it is one: what an open socket asks about again. */
28
+ session?: string;
29
+ }
30
+
31
+ /** What a socket holds of the credential it came in on, to ask again whether it still stands. */
32
+ export interface Held {
33
+ actor: ActorFacts | undefined;
34
+ session?: string | undefined;
25
35
  }
26
36
 
27
37
  /** A request that may not, with the status that says so; `login` when signing in would help. */
@@ -38,6 +48,12 @@ export interface Unauthenticated {
38
48
  */
39
49
  export interface Authenticator {
40
50
  authenticate(request: Request): Promise<Authenticated | Unauthenticated>;
51
+ /**
52
+ * Asks again about a credential already admitted, as an open socket does for every frame and on
53
+ * a timer: does the session still stand, and who is the person now, at which tier. It reads the
54
+ * same `IDENTITY` as a request does, so a person disabled or demoted since is seen at once.
55
+ */
56
+ recheck(held: Held): Promise<{ visitor: Visitor } | Unauthenticated>;
41
57
  /**
42
58
  * Answers the paths below the mount that need no session, such as the login page; undefined
43
59
  * for any other, and absent for a mode with none.
@@ -60,6 +76,11 @@ export interface AuthenticatorOptions {
60
76
  dataDir: string;
61
77
  /** The listener the console is served on and its address, for the check of `none`. */
62
78
  listener: { id: string; address: Readonly<ListenerAddress> | undefined };
79
+ /** The sign-in flow and the session store, for `oidc`. */
80
+ oidc?: {
81
+ login: Pick<OidcLogin, "open" | "credential" | "verified">;
82
+ sessions: Pick<WebSessions, "alive">;
83
+ };
63
84
  /** Where the password is read from; default `process.env`. */
64
85
  env?: Readonly<Record<string, string | undefined>>;
65
86
  /** How long each failed login waits; default 1000 ms. Tests set it lower. */
@@ -68,8 +89,15 @@ export interface AuthenticatorOptions {
68
89
  now?: () => number;
69
90
  }
70
91
 
92
+ /** A credential that admits: who it vouches for, and the session it is when it is one. */
93
+ export type Admitted = Extract<Verdict, { admitted: true }> & {
94
+ session?: string;
95
+ };
96
+
71
97
  /** What a credential says about a request, before the person is identified. */
72
- type Credential = (request: Request) => Promise<Verdict | Unauthenticated>;
98
+ export type Credential = (
99
+ request: Request,
100
+ ) => Promise<Verdict | Admitted | Unauthenticated>;
73
101
 
74
102
  const READ_METHODS = new Set(["GET", "HEAD"]);
75
103
 
@@ -78,6 +106,16 @@ const denied = (refusal: string): Unauthenticated => ({
78
106
  status: 403,
79
107
  });
80
108
 
109
+ /** A refusal in the shape identification answers with. */
110
+ const refused = (reason: string): Identified => ({ refusal: reason });
111
+
112
+ /** A session that is gone, which signing in again would fix. */
113
+ const signedOut = (reason: string): Unauthenticated => ({
114
+ refusal: reason,
115
+ status: 401,
116
+ login: true,
117
+ });
118
+
81
119
  /** Admits as the verifier says; one that throws or rejects admits no one. */
82
120
  function verifierCredential(
83
121
  verifier: (request: Request) => Verdict | Promise<Verdict>,
@@ -112,6 +150,11 @@ export function createAuthenticator(
112
150
  logger,
113
151
  );
114
152
  credential = async () => ({ admitted: true, actor: WEB_CONSOLE_ACTOR });
153
+ } else if (auth.mode === "oidc") {
154
+ if (!options.oidc) throw new Error("the oidc sign-in is not set up");
155
+ credential = options.oidc.login.credential;
156
+ open = (request, rest) =>
157
+ options.oidc?.login.open(request, rest) ?? Promise.resolve(undefined);
115
158
  } else {
116
159
  const password = new PasswordCredential({
117
160
  password: checkPassword(options.env ?? process.env, auth.passwordEnv),
@@ -138,30 +181,56 @@ export function createAuthenticator(
138
181
  };
139
182
  open = (request, rest) => password.open(request, rest);
140
183
  }
184
+ const identify = async (
185
+ actor: ActorFacts | undefined,
186
+ ): Promise<Identified> => {
187
+ try {
188
+ return await options.identify(actor);
189
+ } catch {
190
+ // A lookup of who they are that fails admits no one.
191
+ return refused("identity failed");
192
+ }
193
+ };
141
194
  return {
142
195
  ...(open ? { open } : {}),
143
196
  async authenticate(request) {
144
197
  const checked = await credential(request);
145
198
  if ("refusal" in checked) return checked;
146
199
  if (!checked.admitted) return denied(String(checked.reason));
147
- let identified: Identified;
148
- try {
149
- identified = await options.identify(checked.actor);
150
- } catch {
151
- // A lookup of who they are that fails admits no one.
152
- return denied("identity failed");
153
- }
200
+ const identified = await identify(checked.actor);
154
201
  if ("refusal" in identified) return denied(String(identified.refusal));
155
202
  if (
156
203
  !READ_METHODS.has(request.method) &&
157
204
  request.headers.get("origin") !== options.origin
158
205
  )
159
206
  return denied("foreign origin");
207
+ const { session } = checked as Admitted;
208
+ if (session)
209
+ options.oidc?.login.verified(session, identified.visitor.principal.id);
160
210
  return {
161
211
  visitor: identified.visitor,
162
212
  ...(checked.actor ? { actor: checked.actor } : {}),
163
213
  ...(checked.expiresAt ? { expiresAt: checked.expiresAt } : {}),
214
+ ...(session ? { session } : {}),
164
215
  };
165
216
  },
217
+ async recheck(held) {
218
+ if (held.session) {
219
+ try {
220
+ if (!(await options.oidc?.sessions.alive(held.session)))
221
+ return signedOut("session ended");
222
+ } catch {
223
+ return denied("session lookup failed");
224
+ }
225
+ }
226
+ const identified = await identify(held.actor);
227
+ if ("refusal" in identified) return denied(String(identified.refusal));
228
+ if (held.session)
229
+ options.oidc?.login.verified(
230
+ held.session,
231
+ identified.visitor.principal.id,
232
+ );
233
+ return { visitor: identified.visitor };
234
+ },
166
235
  };
167
236
  }
@@ -1,9 +1,10 @@
1
1
  import { type ConsolePresentation, message } from "../features.ts";
2
2
  import { escapeHtml } from "../http.ts";
3
+ import { iconLinks } from "../page-presentation.ts";
3
4
 
4
- /** The login page's CSP: no script at all, one inline style, and the form posts only to this origin. */
5
+ /** The login page's CSP: no script at all, one inline style, its own icon, and the form posts only to this origin. */
5
6
  export const LOGIN_CSP =
6
- "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'";
7
+ "default-src 'none'; style-src 'unsafe-inline'; img-src 'self'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'";
7
8
 
8
9
  /** The login page, as one server-rendered document with no script; `note` is a line above the field. */
9
10
  export function loginPage(
@@ -19,6 +20,7 @@ export function loginPage(
19
20
  <meta charset="utf-8">
20
21
  <meta name="viewport" content="width=device-width, initial-scale=1">
21
22
  <meta name="robots" content="noindex">
23
+ ${iconLinks(mount, false)}
22
24
  <title>${escapeHtml(title)}</title>
23
25
  <style>
24
26
  body{font:16px/1.5 system-ui,sans-serif;margin:0;min-height:100vh;display:grid;place-items:center;background:#f6f6f4;color:#1c1c1a}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Where a sign-in goes back to. A page link (from the host's own dashboard, a notification, or a
3
+ * page the server turned away) names it as `?next=`, and only these shapes are taken: anything
4
+ * else is dropped, so the callback never redirects to a place a link chose.
5
+ *
6
+ * - `profile`, `notices`, `settings`: a page of the app;
7
+ * - `room:<key>`: a room of this app's surface (`<surface>:<kind>.<id>`);
8
+ * - `p:<id>`: a host page, `[a-z0-9-]{1,40}`.
9
+ */
10
+ export function allowedNext(
11
+ value: string | null | undefined,
12
+ surface: string,
13
+ ): string | undefined {
14
+ if (!value) return undefined;
15
+ if (value === "profile" || value === "notices" || value === "settings")
16
+ return value;
17
+ const room = /^room:(.+)$/.exec(value)?.[1];
18
+ if (room !== undefined)
19
+ return new RegExp(`^${surface}:[A-Za-z0-9._~-]{1,160}$`).test(room)
20
+ ? value
21
+ : undefined;
22
+ const page = /^p:(.+)$/.exec(value)?.[1];
23
+ return page !== undefined && /^[a-z0-9-]{1,40}$/.test(page)
24
+ ? value
25
+ : undefined;
26
+ }
27
+
28
+ /** The app route a `next` names, as the page's router writes it: `room/<key>`, `p/<id>` or the page's name. */
29
+ export function routeOfNext(next: string): string {
30
+ if (next.startsWith("room:"))
31
+ return `room/${encodeURIComponent(next.slice("room:".length))}`;
32
+ if (next.startsWith("p:")) return `p/${next.slice("p:".length)}`;
33
+ return next;
34
+ }
35
+
36
+ /**
37
+ * The `next` a page path stands for, for a server that turns away a page navigation (path
38
+ * routing only: a hash route never reaches the server). Undefined for a path that names none.
39
+ */
40
+ export function nextOfPath(rest: string, surface: string): string | undefined {
41
+ const [first, second, ...more] = rest.split("/").filter(Boolean);
42
+ if (more.length > 0 || first === undefined) return undefined;
43
+ try {
44
+ if (second === undefined) return allowedNext(first, surface);
45
+ const key = decodeURIComponent(second);
46
+ if (first === "room") return allowedNext(`room:${key}`, surface);
47
+ if (first === "p") return allowedNext(`p:${key}`, surface);
48
+ } catch {
49
+ return undefined;
50
+ }
51
+ return undefined;
52
+ }
@@ -0,0 +1,62 @@
1
+ import { type ConsolePresentation, message } from "../features.ts";
2
+ import { escapeHtml } from "../http.ts";
3
+ import { iconLinks } from "../page-presentation.ts";
4
+ import { LOGIN_CSP } from "./login-page.ts";
5
+
6
+ /** What a page of the sign-in flow tells, and the link it offers. */
7
+ type Notice = "signed-out" | "failed" | "not-allowed";
8
+
9
+ /**
10
+ * The static pages of the sign-in flow: logged out, a sign-in that did not complete, and a person
11
+ * the app does not admit. They carry no script and no detail of why: the reason goes to the log.
12
+ */
13
+ export function oidcPage(
14
+ notice: Notice,
15
+ mount: string,
16
+ title: string,
17
+ presentation: ConsolePresentation | undefined,
18
+ status: number,
19
+ ): Response {
20
+ const say = (source: string) => escapeHtml(message(presentation, source));
21
+ const text =
22
+ notice === "signed-out"
23
+ ? say("You are signed out.")
24
+ : notice === "not-allowed"
25
+ ? say("This account is not allowed to use this app.")
26
+ : say("Sign-in did not complete.");
27
+ const link = notice === "signed-out" ? say("Sign in") : say("Try again");
28
+ const html = `<!doctype html>
29
+ <html lang="${escapeHtml(presentation?.locale ?? "en")}">
30
+ <head>
31
+ <meta charset="utf-8">
32
+ <meta name="viewport" content="width=device-width, initial-scale=1">
33
+ <meta name="robots" content="noindex">
34
+ ${iconLinks(mount, false)}
35
+ <title>${escapeHtml(title)}</title>
36
+ <style>
37
+ body{font:16px/1.5 system-ui,sans-serif;margin:0;min-height:100vh;display:grid;place-items:center;background:#f6f6f4;color:#1c1c1a}
38
+ @media(prefers-color-scheme:dark){body{background:#151514;color:#ecece8}}
39
+ main{width:min(22rem,calc(100vw - 2rem));display:grid;gap:1rem}
40
+ h1{font-size:1.25rem;margin:0}
41
+ p{margin:0}
42
+ a{font:inherit;padding:.6rem .75rem;border-radius:.5rem;background:#2d5be3;color:#fff;text-decoration:none;text-align:center}
43
+ </style>
44
+ </head>
45
+ <body>
46
+ <main>
47
+ <h1>${escapeHtml(title)}</h1>
48
+ <p role="${notice === "signed-out" ? "status" : "alert"}">${text}</p>
49
+ <a href="${escapeHtml(mount)}/login">${link}</a>
50
+ </main>
51
+ </body>
52
+ </html>
53
+ `;
54
+ return new Response(html, {
55
+ status,
56
+ headers: {
57
+ "content-type": "text/html;charset=utf-8",
58
+ "cache-control": "no-store",
59
+ "content-security-policy": LOGIN_CSP,
60
+ },
61
+ });
62
+ }