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/app/routes.ts CHANGED
@@ -19,11 +19,13 @@ import type {
19
19
  CharacterRefusal,
20
20
  MessagesView,
21
21
  ModelsView,
22
+ RoomOneView,
22
23
  RoomsView,
23
24
  SchedulesView,
24
25
  ScheduleView,
25
26
  SessionView,
26
27
  SkillView,
28
+ ThreadsDismissed,
27
29
  WorkspaceConflict,
28
30
  WorkspaceNew,
29
31
  WorkspaceRefusalBody,
@@ -33,8 +35,14 @@ import type {
33
35
  } from "../api-types.ts";
34
36
  import type { Authenticated } from "../auth/authenticator.ts";
35
37
  import { type ConsolePresentation, message } from "../features.ts";
38
+ import type { Gate } from "../gate.ts";
39
+ import type { HostApp } from "../host/host-app.ts";
40
+ import type { RoomReads } from "../host/reads.ts";
41
+ import { readsOf, withExpiredFiles } from "../host/room-views.ts";
36
42
  import { json } from "../http.ts";
43
+ import type { ResolvedOptions } from "../options.ts";
37
44
  import type { PushMutes } from "../push/mutes.ts";
45
+ import { viewerOf, type WebAppRegistry } from "../web-app.ts";
38
46
  import { bad, bodyOf, HttpError, notFound } from "./api-http.ts";
39
47
  import type { CardLog } from "./card-log.ts";
40
48
  import { type FileDir, fileResponse, isFileId } from "./files.ts";
@@ -42,7 +50,7 @@ import type { LayoutStore } from "./layout.ts";
42
50
  import { LayoutRoutes } from "./layout-routes.ts";
43
51
  import { PromptApi, type PromptApiOptions } from "./prompt-routes.ts";
44
52
  import { PAGE_LIMIT, type RoomLog } from "./room-log.ts";
45
- import type { Rooms } from "./rooms.ts";
53
+ import type { ResolvedRoom, Rooms } from "./rooms.ts";
46
54
  import { SkillDocApi } from "./skill-doc-routes.ts";
47
55
  import type { AppSurface } from "./surface.ts";
48
56
  import { TeamApi } from "./team-routes.ts";
@@ -69,13 +77,16 @@ export interface AppApiOptions {
69
77
  /** The rooms whose push the owners muted. */
70
78
  mutes: Pick<PushMutes, "set" | "setMany">;
71
79
  /** The dispatch threads, which go with the room they hang under. */
72
- threads: Pick<WebThreads, "under" | "removeUnder">;
80
+ threads: Pick<
81
+ WebThreads,
82
+ "under" | "removeUnder" | "dismiss" | "dismissFinished"
83
+ >;
73
84
  /** The owners' shared arrangement of the room list: pins and folders. */
74
85
  layout: LayoutStore;
75
86
  conversations: Pick<ConversationPort, "startFresh">;
76
- identity: Pick<IdentityService, "tierOf">;
87
+ identity: Pick<IdentityService, "principal">;
77
88
  /** Present when the schedules pane is served. */
78
- schedules?: Pick<ScheduleStore, "all" | "remove">;
89
+ schedules?: Pick<ScheduleStore, "all" | "get" | "remove">;
79
90
  directory: Pick<AgentDirectory, "agent" | "agents" | "group" | "groups">;
80
91
  team: Pick<
81
92
  AgentTeam,
@@ -109,12 +120,28 @@ export interface AppApiOptions {
109
120
  /** Present when the host has prompt documents (the prompts plugin): the main prompts the owner edits. */
110
121
  prompts?: PromptApiOptions["prompts"];
111
122
  avatars: Pick<AvatarStudio, "serve">;
123
+ /** The read cursors, for each room's unread count and who has read what. */
124
+ reads?: Pick<RoomReads, "unread" | "cursorOf" | "marks">;
125
+ /** Who may be served a file bound to a host room or a page. */
126
+ host?: Pick<HostApp, "mayRead">;
127
+ /** The name the app shows for a person. */
128
+ nameOf?(principalId: string): Promise<string>;
112
129
  /** Present when the workspace pane is served. */
113
130
  workspace?: WorkspaceFiles;
114
131
  /** The mount, which a room's agent picture hangs under. */
115
132
  mount: string;
116
133
  /** How the console signs the person in, as the session reports it. */
117
134
  authMode: SessionView["auth"];
135
+ /** Who the app admits; the session reports the person's tier only where it admits more than owners. */
136
+ admission?: ResolvedOptions["admission"];
137
+ /** The area handlers the pieces of the app plug in; without it the API is the owners' as in 0.25. */
138
+ registry?: Pick<WebAppRegistry, "areaFor">;
139
+ /** The onboarding gate; without it nobody is gated. */
140
+ gate?: Pick<Gate, "required">;
141
+ /** The tiers whose people may use push; default owners. */
142
+ pushTiers?: ResolvedOptions["push"]["tiers"];
143
+ /** Whether the yolo and long-running switches are served; `PUT rooms/<key>/mode` answers 404 without them. Default true. */
144
+ modeSwitches?: boolean;
118
145
  /** The quiet hours of push, as the settings page shows them. */
119
146
  quietHours?: NonNullable<SessionView["quietHours"]>;
120
147
  /** Called after a change the other pages should refetch. */
@@ -296,7 +323,12 @@ export class AppApi {
296
323
  const parts = partsOf(rest);
297
324
  if (!parts) return undefined;
298
325
  const [first, second] = parts;
326
+ const area =
327
+ first === "api" && second !== undefined
328
+ ? this.#options.registry?.areaFor(second)
329
+ : undefined;
299
330
  const mine =
331
+ area !== undefined ||
300
332
  (first === "avatars" && parts.length === 2) ||
301
333
  (first === "api" &&
302
334
  [
@@ -316,6 +348,11 @@ export class AppApi {
316
348
  ].includes(second ?? ""));
317
349
  if (!mine) return undefined;
318
350
  try {
351
+ if (area)
352
+ return await area.handle(request, parts.slice(2), {
353
+ visitor: admission.visitor,
354
+ viewer: viewerOf(admission.visitor),
355
+ });
319
356
  return await this.#route(request, parts, admission);
320
357
  } catch (error) {
321
358
  if (error instanceof HttpError || error instanceof UploadRefusal)
@@ -338,16 +375,13 @@ export class AppApi {
338
375
  }
339
376
  }
340
377
 
341
- /** The owner who writes: their tier read again, as a speaker for the calls that need one. */
378
+ /** The owner who writes: the tier the request was admitted at, as a speaker for the calls that need one. */
342
379
  async #owner(admission: Authenticated): Promise<Speaker> {
343
- const { principal } = admission.visitor;
344
- const tier = await this.#options.identity.tierOf(principal.id);
380
+ const { principal, tier, speakerId } = admission.visitor;
345
381
  if (tier !== "owner")
346
382
  throw new HttpError(403, "Only an owner may do this.");
347
383
  return {
348
- id: admission.actor
349
- ? `${admission.actor.provider}:${admission.actor.subject}`
350
- : principal.id,
384
+ id: speakerId,
351
385
  name: principal.displayName,
352
386
  tier,
353
387
  principalId: principal.id,
@@ -368,7 +402,8 @@ export class AppApi {
368
402
  new Response("Not found", { status: 404 })
369
403
  );
370
404
  }
371
- if (area === "session" && method === "GET") return this.#session(admission);
405
+ if (area === "session" && method === "GET")
406
+ return await this.#session(admission);
372
407
  if (area === "models" && method === "GET")
373
408
  return json({
374
409
  models: await this.#options.team.usableModels(),
@@ -376,20 +411,21 @@ export class AppApi {
376
411
  if (area === "rooms") return this.#rooms(request, parts, admission);
377
412
  if (area === "folders")
378
413
  return this.#layout.folders(request, parts, admission);
379
- if (area === "uploads" && method === "POST" && !id) {
380
- const owner = await this.#owner(admission);
414
+ // An upload is the sender's own, whoever they are; a file answers by the room it is bound to.
415
+ if (area === "uploads" && method === "POST" && !id)
381
416
  return json(
382
- await this.#options.uploads.receive(request, owner.principalId),
417
+ await this.#options.uploads.receive(
418
+ request,
419
+ admission.visitor.principal.id,
420
+ ),
383
421
  );
384
- }
385
422
  if (area === "uploads" && method === "DELETE" && id && !action) {
386
- const owner = await this.#owner(admission);
387
- if (!this.#options.uploads.discard(id, owner.principalId))
423
+ if (!this.#options.uploads.discard(id, admission.visitor.principal.id))
388
424
  throw notFound();
389
425
  return json({ ok: true });
390
426
  }
391
427
  if (area === "files" && method === "GET" && id && !action)
392
- return this.#file(id);
428
+ return await this.#file(id, admission);
393
429
  if (area === "workspace") return this.#workspace(request, parts, admission);
394
430
  if (area === "schedules") return this.#schedules(method, id, admission);
395
431
  const owner = () => this.#owner(admission);
@@ -410,13 +446,21 @@ export class AppApi {
410
446
  throw new HttpError(404, "There is no such API.");
411
447
  }
412
448
 
413
- #session(admission: Authenticated): Response {
414
- const { principal } = admission.visitor;
449
+ async #session(admission: Authenticated): Promise<Response> {
450
+ const { principal, tier } = admission.visitor;
451
+ const { admission: admits = "owners", gate } = this.#options;
452
+ const pushTiers = this.#options.pushTiers ?? ["owner"];
453
+ const owed =
454
+ admits === "access" && gate
455
+ ? await gate.required(viewerOf(admission.visitor))
456
+ : [];
415
457
  return json({
416
458
  principal: { id: principal.id, name: principal.displayName },
417
459
  surface: this.#options.surface.surface,
418
460
  auth: this.#options.authMode,
419
- push: true,
461
+ ...(admits === "access" ? { tier } : {}),
462
+ ...(owed.length > 0 ? { gate: owed } : {}),
463
+ ...(pushTiers.includes(tier) ? { push: true as const } : {}),
420
464
  ...(this.#options.prompts ? { prompts: true } : {}),
421
465
  ...(this.#options.skills ? { skillEdit: true } : {}),
422
466
  ...(this.#options.quietHours
@@ -436,15 +480,37 @@ export class AppApi {
436
480
  const { method } = request;
437
481
  const [, , key, action] = parts;
438
482
  const { rooms, log, cards, modes, mutes, conversations } = this.#options;
483
+ // The agent rooms are the owners'; to anyone else this API does not exist.
484
+ if (admission.visitor.tier !== "owner") throw notFound();
439
485
  if (!key) {
440
486
  if (method !== "GET") throw new HttpError(405, "Method not allowed.");
487
+ const listed = await rooms.list();
488
+ // What each room holds past this person's cursor; a room they never read starts from what it has now.
489
+ const unread = await this.#options.reads?.unread(
490
+ admission.visitor.principal.id,
491
+ listed.map((one) => one.key),
492
+ new Set(listed.map((one) => one.key)),
493
+ );
441
494
  return json({
442
- rooms: await rooms.list(),
495
+ rooms: listed.map((one) => {
496
+ const count = unread?.get(one.key);
497
+ return count ? { ...one, unread: count } : one;
498
+ }),
443
499
  ...(await this.#layout.view()),
444
500
  } satisfies RoomsView);
445
501
  }
446
502
  const room = await rooms.resolve(key);
447
503
  if (!room) throw notFound();
504
+ if (!action && method === "GET") {
505
+ // A thread dismissed from the list still opens by its address; the page asks for its view.
506
+ const view = await rooms.threadView(room.key);
507
+ if (!view) throw notFound();
508
+ return json({ room: view } satisfies RoomOneView);
509
+ }
510
+ if (action === "dismiss" && method === "POST" && parts.length === 4)
511
+ return this.#dismiss(room, admission);
512
+ if (action === "dismiss-threads" && method === "POST" && parts.length === 4)
513
+ return this.#dismissFinished(room, admission);
448
514
  if (action === "messages" && method === "GET" && parts.length === 4) {
449
515
  const params = new URL(request.url).searchParams;
450
516
  const before = params.get("before");
@@ -461,17 +527,26 @@ export class AppApi {
461
527
  // The cards stand among the messages by the time they opened; a page that has older
462
528
  // messages leaves the cards before its first one to the older page.
463
529
  const first = page.messages[0];
530
+ const { reads, nameOf } = this.#options;
531
+ const self = admission.visitor.principal.id;
532
+ const cursor = await reads?.cursorOf(room.key, self);
464
533
  return json({
465
534
  ...page,
535
+ messages: withExpiredFiles(page.messages, this.#options.files),
466
536
  cards: await cards.during(room.key, {
467
537
  ...(page.more && first ? { from: first.at } : {}),
468
538
  ...(beforeId === undefined ? {} : { before: beforeId }),
469
539
  }),
540
+ ...(reads && nameOf
541
+ ? { reads: await readsOf(reads, room.key, self, nameOf) }
542
+ : {}),
543
+ ...(cursor === undefined ? {} : { cursor }),
470
544
  } satisfies MessagesView);
471
545
  }
472
546
  if (action === "mode" && method === "PUT" && parts.length === 4) {
473
547
  // Only an agent's or group's room has modes; a thread runs no turn of the owner's.
474
- if (room.readOnly) throw notFound();
548
+ if (room.readOnly || this.#options.modeSwitches === false)
549
+ throw notFound();
475
550
  const owner = await this.#owner(admission);
476
551
  const body = await bodyOf(request);
477
552
  const change: ModeChange = {};
@@ -513,10 +588,47 @@ export class AppApi {
513
588
  throw new HttpError(404, "There is no such API.");
514
589
  }
515
590
 
516
- #file(id: string): Response {
591
+ /** Takes one finished thread off the room list for every owner; an open one is refused. */
592
+ async #dismiss(
593
+ room: ResolvedRoom,
594
+ admission: Authenticated,
595
+ ): Promise<Response> {
596
+ if (room.kind !== "thread") throw notFound();
597
+ await this.#owner(admission);
598
+ const outcome = await this.#options.threads.dismiss(room.name);
599
+ if (outcome === "missing") throw notFound();
600
+ if (outcome === "open")
601
+ throw new HttpError(409, "The thread is still open.");
602
+ this.#options.changed();
603
+ return json({ dismissed: 1 } satisfies ThreadsDismissed);
604
+ }
605
+
606
+ /** Takes every finished thread of a room off the room list; the open ones stay. */
607
+ async #dismissFinished(
608
+ room: ResolvedRoom,
609
+ admission: Authenticated,
610
+ ): Promise<Response> {
611
+ if (room.kind === "thread") throw notFound();
612
+ await this.#owner(admission);
613
+ const dismissed = await this.#options.threads.dismissFinished(room.key);
614
+ if (dismissed) this.#options.changed();
615
+ return json({ dismissed } satisfies ThreadsDismissed);
616
+ }
617
+
618
+ async #file(id: string, admission: Authenticated): Promise<Response> {
517
619
  if (!isFileId(id)) throw notFound();
518
- const meta = this.#options.files.meta(id);
519
- const blob = this.#options.files.blob(id);
620
+ const { files, host } = this.#options;
621
+ // A file bound to a host room or page is for the people it is open to, owners too; every other stored file belongs to an agent room, which only owners read.
622
+ const bound = files.binding(id)?.room;
623
+ if (bound === undefined) {
624
+ if (admission.visitor.tier !== "owner") throw notFound();
625
+ } else if (
626
+ !host ||
627
+ !(await host.mayRead(viewerOf(admission.visitor), bound))
628
+ )
629
+ throw notFound();
630
+ const meta = files.meta(id);
631
+ const blob = files.blob(id);
520
632
  if (!meta || !blob) throw notFound();
521
633
  return fileResponse(meta, blob);
522
634
  }
@@ -636,6 +748,18 @@ export class AppApi {
636
748
  throw new HttpError(404, "There is no such API.");
637
749
  }
638
750
 
751
+ /** Whether a schedule is the visitor's own: its creator id names their principal. */
752
+ async #ownSchedule(
753
+ createdById: string,
754
+ principalId: string,
755
+ ): Promise<boolean> {
756
+ if (createdById === principalId) return true;
757
+ // A schedule from 0.8 names its creator by a user id that the backfill made a principal's id.
758
+ return (
759
+ (await this.#options.identity.principal(createdById))?.id === principalId
760
+ );
761
+ }
762
+
639
763
  async #schedules(
640
764
  method: string,
641
765
  id: string | undefined,
@@ -643,11 +767,19 @@ export class AppApi {
643
767
  ): Promise<Response> {
644
768
  const { schedules, rooms } = this.#options;
645
769
  if (!schedules) throw new HttpError(404, "This pane is not served.");
770
+ const { tier, principal } = admission.visitor;
771
+ const owner = tier === "owner";
646
772
  if (method === "GET" && !id) {
647
773
  const all = await schedules.all();
648
774
  const views: ScheduleView[] = [];
649
775
  for (const schedule of all) {
650
- const room = await rooms.resolve(schedule.channel);
776
+ // The owners see every schedule; anyone else sees their own, and no room of the owners.
777
+ if (
778
+ !owner &&
779
+ !(await this.#ownSchedule(schedule.createdById, principal.id))
780
+ )
781
+ continue;
782
+ const room = owner ? await rooms.resolve(schedule.channel) : undefined;
651
783
  views.push({
652
784
  id: schedule.id,
653
785
  ...(room ? { room: room.key } : {}),
@@ -667,9 +799,17 @@ export class AppApi {
667
799
  return json({ schedules: views } satisfies SchedulesView);
668
800
  }
669
801
  if (method === "DELETE" && id) {
670
- await this.#owner(admission);
671
802
  const number = Number(id);
672
803
  if (!Number.isInteger(number)) throw bad("The schedule id is a number.");
804
+ // Another person's schedule is not there to remove, for anyone but the owners.
805
+ if (!owner) {
806
+ const found = await schedules.get(number);
807
+ if (
808
+ !found ||
809
+ !(await this.#ownSchedule(found.createdById, principal.id))
810
+ )
811
+ throw new HttpError(404, "There is no such schedule.");
812
+ }
673
813
  const removed = await schedules.remove(number);
674
814
  if (!removed) throw new HttpError(404, "There is no such schedule.");
675
815
  this.#options.changed();