openmausbot 0.1.90 → 0.1.92

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 (177) hide show
  1. package/dist/assets/DesktopViewer-Do6lBa4C.js +5 -0
  2. package/dist/assets/{abnfDiagram-O67JEVCF-DUyXd0kL.js → abnfDiagram-O67JEVCF-Cd3X7Hxc.js} +1 -1
  3. package/dist/assets/{arc-q73qM-At.js → arc-DIYc7Ymx.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-D33O3Wur.js → architectureDiagram-NJMV4G6O-DBmRGVyP.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-DLsJeoXC.js → blockDiagram-BEXU5L5S-CPFePSEE.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-DnaljZnu.js → c4Diagram-YGBWAQC7-DD_DO1qy.js} +1 -1
  7. package/dist/assets/channel-Dtk5Mtic.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-Da0h2i79.js → chunk-2Q5K7J3B-BNEeNSjX.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-CfzHi54f.js → chunk-3FUC2YCW-BbinLaqH.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-LjE4d9Jf.js → chunk-5VM5RSS4-BBgYrgDR.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-DY-UGmvc.js → chunk-GTNCS2PH-ezXj-kA-.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-BqeGhyhP.js → chunk-GWA4HPMP-9FxZ3diV.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-BYa9EBAg.js → chunk-JWPE2WC7-CO-40JpI.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-vTrDtWyb.js → chunk-NETBCI7D-BEhjslYE.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-ic5tXadM.js → chunk-WEXAMYUT-CNeq4n7_.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-DvRCY-c6.js → chunk-XXDRQBXY-OsCY7KSi.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-DVbmgz_L.js → classDiagram-v2-NBCMYWYE-CloRUEtk.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-vdUs2n57.js → cose-bilkent-JH36ORCC-D1BGZhp4.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-BvLGlV3U.js → cynefin-OW5HDTMX-CUf4T2CN.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-C4GcJQDt.js → cynefinDiagram-VND7K2PF-DPEZrFGA.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-BMSaHpt_.js → dagre-6A5THRUB-bDe6GIv5.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-D5b9UBhC.js → diagram-22UHCM2B-CdcUQz55.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-CIKLlB7Y.js → diagram-3UASUU5V-BZpqGg7c.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-BzXb-7k7.js → diagram-ATOU4E4O-DvGM6ugp.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-HsTvJ7uT.js → diagram-CDSNMT55-DSTmMm0s.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-Db9ySNbQ.js → diagram-MLGK6HIB-15cucnMn.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-D6BKbpqY.js → diagram-MPIPVDR6-tMbuaE6n.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-Bq067L0t.js → ebnfDiagram-ZINNZB2B-BYxYzjHI.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-DhaD7fNJ.js → elk-276RUBZZ-v6bj852W.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-fwIhfz36.js → erDiagram-OPXOYQCR-5lBBlnd2.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-DygIyu0V.js → flowDiagram-KWPJA3E3-CZyl1zIj.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-DPYfSO96.js → ganttDiagram-FUAMR5RP-BiAI-ltZ.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-DhkHeCle.js → gitGraphDiagram-X574FWY7-BAZISkj3.js} +1 -1
  34. package/dist/assets/index-Cdmjrl28.css +1 -0
  35. package/dist/assets/{index-G1PHKGyh.js → index-DGBaEXrM.js} +1 -1
  36. package/dist/assets/index-DR7YPMkW.js +597 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-DstBLxME.js → infoDiagram-VRGFBTTK-B6ooOy-Z.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-CrIsGgch.js → ishikawaDiagram-OU5B5YK6-BZ9ksi-B.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-CYypGDbB.js → journeyDiagram-ZHPQQLJL-DWqlzIEe.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-DGjPt8E_.js → kanban-definition-PNTS6WVX-JX6piDwL.js} +1 -1
  41. package/dist/assets/{linear-Dwc3Hrru.js → linear-CmOWzX0P.js} +1 -1
  42. package/dist/assets/{mermaid.core-Dc4OePBL.js → mermaid.core-CF1zwoQ-.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-DV-vepj-.js → mindmap-definition-NLK3R4M7-CGh2K4fW.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-lx5Kj-n5.js → pegDiagram-GJSIUBJH-CDwYN0s7.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-Bq7Vmm15.js → pieDiagram-5QR66LMP-1rfjJocB.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-DeqeqOBj.js → quadrantDiagram-O4NWA36T-tKPvPiok.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-CXwRQzSC.js → railroadDiagram-XR7U4H2S-BHLm5vXR.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-8tAeoB88.js → requirementDiagram-PLB6GJNP-CB45STp7.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-BG5zjGWU.js → sankeyDiagram-IPEJSGJF-55QA_huC.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-DKF3iLcw.js → sequenceDiagram-PO4LG4MO-BckFmQHn.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-B-y1qUgT.js → sizeCapture-INFHLROL-BFCOJF11.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-MkzPgxAc.js → stateDiagram-v2-GCMORJYK-CZvmiVVN.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-DnbaOEwH.js → swimlanes-2SLR337P-BpDUCEKy.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-M0GYW9Y3.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-Bb4xG_Bt.js → timeline-definition-EJHVYXUP-BpxHq6IK.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-JgkR9pFf.js → usecaseDiagram-POWQR4AR-Cj66vm02.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-CIPIrAYe.js → vennDiagram-UO4OBE2U-C9nRkWE_.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-BjCLuUvJ.js → wardleyDiagram-VNRHLVJA-D-me_nWj.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-DKgiaYxT.js → xychartDiagram-PMCCYNJV-Dc_fL4mk.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist/novnc-NOTICE.txt +118 -0
  62. package/dist-server/cloud-home-start.js +128 -15
  63. package/dist-server/container-mcp.js +277 -45
  64. package/dist-server/drivers/agents-proxy.js +16 -4
  65. package/dist-server/enterprise/server/index.js +5 -1
  66. package/dist-server/index.js +46703 -39955
  67. package/dist-server/local-computer-proxy.js +128 -7
  68. package/dist-server/mcp-gate.js +11 -5
  69. package/dist-server/model-catalog/models-dev.snapshot.json +7299 -0
  70. package/dist-server/openmausbot.js +4099 -1217
  71. package/dist-server/pair-cli.js +4099 -1217
  72. package/dist-server/server/boat.js +67 -22
  73. package/dist-server/server/bot-overview.js +7 -4
  74. package/dist-server/server/browser-engine.js +10 -1
  75. package/dist-server/server/browser-live.js +46 -2
  76. package/dist-server/server/browser-runtime.js +74 -8
  77. package/dist-server/server/browser-tool-shape.js +6 -4
  78. package/dist-server/server/chief-of-staff.js +1 -1
  79. package/dist-server/server/cloud-home-start.js +136 -26
  80. package/dist-server/server/cloud-home.js +64 -5
  81. package/dist-server/server/cloud-lending.js +167 -0
  82. package/dist-server/server/cloud-move-http.js +268 -0
  83. package/dist-server/server/cloud-move.js +418 -0
  84. package/dist-server/server/cloud-owner.js +196 -0
  85. package/dist-server/server/cloud-secrets-boot.js +6 -0
  86. package/dist-server/server/cloud-secrets.js +65 -0
  87. package/dist-server/server/config.js +215 -17
  88. package/dist-server/server/container-computer.js +3 -2
  89. package/dist-server/server/decider/index.js +238 -0
  90. package/dist-server/server/decider/jev.js +183 -0
  91. package/dist-server/server/decider/log.js +62 -0
  92. package/dist-server/server/decider/relay.js +52 -0
  93. package/dist-server/server/decider/room-routing.js +88 -0
  94. package/dist-server/server/decider/types.js +5 -0
  95. package/dist-server/server/desktop-viewer-targets.js +26 -0
  96. package/dist-server/server/drivers/acp/core.js +167 -21
  97. package/dist-server/server/drivers/acp/opencode-go.js +614 -85
  98. package/dist-server/server/drivers/agents-catalog.js +22 -6
  99. package/dist-server/server/drivers/antigravity-acp.js +69 -11
  100. package/dist-server/server/drivers/antigravity-temp.js +360 -0
  101. package/dist-server/server/drivers/antigravity.js +21 -0
  102. package/dist-server/server/drivers/boatagent.js +20 -12
  103. package/dist-server/server/drivers/chatgpt-plan-auth.js +481 -0
  104. package/dist-server/server/drivers/claude.js +313 -34
  105. package/dist-server/server/drivers/codex.js +228 -18
  106. package/dist-server/server/drivers/grok.js +2 -2
  107. package/dist-server/server/drivers/mistral.js +4 -3
  108. package/dist-server/server/drivers/openai-chat-protocol.js +4 -3
  109. package/dist-server/server/drivers/openai-chat.js +41 -3
  110. package/dist-server/server/drivers/openai-compat.js +73 -23
  111. package/dist-server/server/engine-install.js +46 -12
  112. package/dist-server/server/env-path.js +15 -3
  113. package/dist-server/server/group-goal-run.js +6 -4
  114. package/dist-server/server/harness/registry.js +2 -2
  115. package/dist-server/server/harness-capabilities.js +2 -0
  116. package/dist-server/server/included-services.js +70 -0
  117. package/dist-server/server/index.js +1753 -263
  118. package/dist-server/server/lending-memory.js +479 -0
  119. package/dist-server/server/local-vm-idle.js +24 -6
  120. package/dist-server/server/managed-desktop.js +3 -0
  121. package/dist-server/server/mcp-bridge.js +5 -3
  122. package/dist-server/server/mcp-http.js +6 -3
  123. package/dist-server/server/mcp-oauth-discovery.js +133 -0
  124. package/dist-server/server/mcp-oauth-store.js +70 -0
  125. package/dist-server/server/mcp-oauth.js +550 -0
  126. package/dist-server/server/mcp-probe.js +14 -1
  127. package/dist-server/server/mcp-registry.js +43 -0
  128. package/dist-server/server/memory-upkeep.js +94 -84
  129. package/dist-server/server/model-catalog/catalog.js +374 -0
  130. package/dist-server/server/model-catalog/presets.js +176 -0
  131. package/dist-server/server/model-catalog/trim.js +219 -0
  132. package/dist-server/server/model-context-window.js +4 -4
  133. package/dist-server/server/package-export.js +17 -10
  134. package/dist-server/server/package-import.js +6 -3
  135. package/dist-server/server/package-parts.js +3 -1
  136. package/dist-server/server/provider-key-check.js +4 -2
  137. package/dist-server/server/recent-work.js +1 -1
  138. package/dist-server/server/request-auth.js +25 -0
  139. package/dist-server/server/routes/antigravity-leftovers.js +45 -0
  140. package/dist-server/server/routes/bot-memory.js +31 -6
  141. package/dist-server/server/routes/decider.js +19 -0
  142. package/dist-server/server/routes/desktop-viewer.js +194 -0
  143. package/dist-server/server/routine-requests.js +1 -1
  144. package/dist-server/server/routines.js +1 -1
  145. package/dist-server/server/sessions.js +56 -9
  146. package/dist-server/server/shared-computers.js +116 -30
  147. package/dist-server/server/skills.js +25 -10
  148. package/dist-server/server/store.js +79 -124
  149. package/dist-server/server/surface.js +13 -0
  150. package/dist-server/server/system-prompt.js +11 -0
  151. package/dist-server/server/team-backup.js +6 -2
  152. package/dist-server/server/team-manifest.js +11 -3
  153. package/dist-server/server/thread-events.js +2 -0
  154. package/dist-server/server/thread-starters.js +33 -0
  155. package/dist-server/server/thread-title.js +46 -0
  156. package/dist-server/server/tts/elevenlabs.js +11 -6
  157. package/dist-server/server/tts/fish.js +4 -5
  158. package/dist-server/server/tts/index.js +20 -11
  159. package/dist-server/server/vps-computer.js +79 -12
  160. package/dist-server/server/webhooks.js +18 -1
  161. package/dist-server/server/workspace-backup-policy.js +25 -3
  162. package/dist-server/server/workspace-backup.js +28 -8
  163. package/dist-server/server/workspace.js +65 -15
  164. package/dist-server/shared/cloud-home.js +4 -0
  165. package/dist-server/shared/package-format.js +30 -4
  166. package/dist-server/shared/team-backup.js +20 -3
  167. package/dist-server/shared/workspace-backup-client.js +1 -0
  168. package/dist-server/src/lib/citations.js +177 -0
  169. package/dist-server/src/lib/composer-attachments.js +7 -1
  170. package/dist-server/vps-container-mcp.js +278 -45
  171. package/dist-server/workspace-backup.worker.js +184 -38
  172. package/enterprise/server/index.js +5 -1
  173. package/package.json +1 -1
  174. package/dist/assets/channel-BCuyHjFV.js +0 -1
  175. package/dist/assets/index-CRvo-DS2.css +0 -1
  176. package/dist/assets/index-CWYGlA63.js +0 -588
  177. package/dist/assets/swimlanesDiagram-TC7HE7FX-BZQ07DGE.js +0 -8
@@ -0,0 +1,418 @@
1
+ // Move to Cloud (docs/cloud-pro.md): a person's one-time copy of their local
2
+ // workspace onto their OMB Cloud home. The desktop exports the ordinary
3
+ // encrypted workspace backup (workspace-backup.ts) and uploads it here in
4
+ // parts; this machine then previews and restores it like any other backup,
5
+ // so everything that backup policy keeps out (credentials, sign-ins, pairing
6
+ // and sessions, runtime state) stays where it is on both sides.
7
+ //
8
+ // What this file adds: the workspace's size and contents, a resumable upload
9
+ // slot, the space check for the machine's fixed-size volume, a backup of the
10
+ // Cloud's own workspace taken before it is replaced ("Restore previous Cloud"
11
+ // swaps it back), and the tidying that keeps that backup the only copy left
12
+ // behind. Everything lives under `.backups`, which no snapshot includes and
13
+ // no restore replaces.
14
+ import { createHash, randomBytes } from "node:crypto";
15
+ import { createReadStream, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statfsSync, writeFileSync } from "node:fs";
16
+ import { open } from "node:fs/promises";
17
+ import { join } from "node:path";
18
+ import { DatabaseSync } from "node:sqlite";
19
+ import { writeFileAtomic } from "./atomic.js";
20
+ import { MAX_WORKSPACE_BACKUP_FILES, MAX_WORKSPACE_BACKUP_UPLOAD_BYTES, omittedFromWorkspaceBackup, removeWorkspaceBackupJob, stageWorkspaceBackup, } from "./workspace-backup.js";
21
+ /** The part size the desktop sends; a part may be at most CLOUD_MOVE_MAX_PART_BYTES. */
22
+ export const CLOUD_MOVE_PART_BYTES = 16 * 1024 ** 2;
23
+ export const CLOUD_MOVE_MAX_PART_BYTES = 64 * 1024 ** 2;
24
+ export const CLOUD_MOVE_MAX_BYTES = MAX_WORKSPACE_BACKUP_UPLOAD_BYTES;
25
+ export const CLOUD_MOVE_MAX_FILES = MAX_WORKSPACE_BACKUP_FILES;
26
+ /** Headroom kept free on the volume after a move's own files. */
27
+ export const CLOUD_MOVE_SPACE_MARGIN = 256 * 1024 ** 2;
28
+ // Encrypted header plus tag: anything smaller cannot be a backup.
29
+ const MIN_BYTES = 60;
30
+ const SHA256 = /^[a-f0-9]{64}$/;
31
+ // An upload nobody has touched for a day is abandoned, not resumable.
32
+ const UPLOAD_STALE_MS = 24 * 3600_000;
33
+ const fail = (message, status, extra = {}) => Object.assign(new Error(message), { status, ...extra });
34
+ const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
35
+ function rosterLength(path) {
36
+ try {
37
+ const value = JSON.parse(readFileSync(path, "utf8"));
38
+ return Array.isArray(value) ? value.length : 0;
39
+ }
40
+ catch {
41
+ return 0;
42
+ }
43
+ }
44
+ /** What the person would call their work: bots, rooms, and the conversations
45
+ * they took part in (a starter bot's greeting is not a chat). */
46
+ export function workspaceContents(dataDir) {
47
+ let chats = 0;
48
+ const database = join(dataDir, "messages.db");
49
+ if (existsSync(database)) {
50
+ const db = new DatabaseSync(database, { timeout: 5_000 });
51
+ try {
52
+ const columns = db.prepare("SELECT name FROM pragma_table_info('messages')").all().map((row) => String(row.name));
53
+ if (columns.length) {
54
+ const who = columns.includes("role") ? "role" : "json_extract(json, '$.role')";
55
+ chats = Number(db.prepare(`SELECT COUNT(DISTINCT thread_id) AS chats FROM messages WHERE ${who} = 'user'`).get()?.chats ?? 0);
56
+ }
57
+ }
58
+ finally {
59
+ db.close();
60
+ }
61
+ }
62
+ return { bots: rosterLength(join(dataDir, "bots.json")), rooms: rosterLength(join(dataDir, "groups.json")), chats };
63
+ }
64
+ /** A fresh machine: its one starter bot at most, no rooms, nobody has chatted. */
65
+ export function isEmptyWorkspace(contents) {
66
+ return contents.bots <= 1 && contents.rooms === 0 && contents.chats === 0;
67
+ }
68
+ /** Bytes and files a backup of this workspace would carry, by the export's
69
+ * own rules. An estimate: live files can change before the export. */
70
+ export function workspaceMoveSize(dataDir) {
71
+ let bytes = 0, files = 0;
72
+ const walk = (directory, prefix) => {
73
+ for (const name of readdirSync(directory)) {
74
+ const path = prefix ? `${prefix}/${name}` : name;
75
+ if (omittedFromWorkspaceBackup(path))
76
+ continue;
77
+ const stat = lstatSync(join(directory, name));
78
+ if (stat.isSymbolicLink())
79
+ continue;
80
+ if (stat.isDirectory())
81
+ walk(join(directory, name), path);
82
+ else if (stat.isFile()) {
83
+ bytes += stat.size;
84
+ files++;
85
+ }
86
+ if (files > CLOUD_MOVE_MAX_FILES)
87
+ return;
88
+ }
89
+ };
90
+ if (existsSync(dataDir))
91
+ walk(dataDir, "");
92
+ return { bytes, files };
93
+ }
94
+ export function freeVolumeBytes(path) {
95
+ const disk = statfsSync(path, { bigint: true });
96
+ const free = disk.bavail * disk.bsize;
97
+ return free > BigInt(Number.MAX_SAFE_INTEGER) ? Number.MAX_SAFE_INTEGER : Number(free);
98
+ }
99
+ /** Peak extra space a move of `upload` bytes needs: the upload, its decrypted
100
+ * copy and its staged files exist together while it is checked; the restore
101
+ * then installs from the staged copy. Replacing a workspace first backs it up
102
+ * (a snapshot and its archive, briefly both). */
103
+ export function moveSpaceNeeded(upload, current, replacing) {
104
+ return 3 * upload + (replacing ? 2 * current : 0) + CLOUD_MOVE_SPACE_MARGIN;
105
+ }
106
+ function privateFolder(path) {
107
+ mkdirSync(path, { recursive: true, mode: 0o700 });
108
+ const stat = lstatSync(path);
109
+ if (!stat.isDirectory() || stat.isSymbolicLink())
110
+ throw fail("Backup storage must be a real folder, not a link.", 500);
111
+ return path;
112
+ }
113
+ const backupsRoot = (dataDir) => privateFolder(join(dataDir, ".backups"));
114
+ // Reads never create the folder; beginUpload does.
115
+ const uploadFolder = (dataDir) => join(dataDir, ".backups", "cloud-move");
116
+ const uploadMetaPath = (dataDir) => join(uploadFolder(dataDir), "upload.json");
117
+ const uploadPartPath = (dataDir) => join(uploadFolder(dataDir), "upload.part");
118
+ function readUploadMeta(dataDir) {
119
+ try {
120
+ const value = JSON.parse(readFileSync(uploadMetaPath(dataDir), "utf8"));
121
+ if (record(value) && typeof value.sha256 === "string" && SHA256.test(value.sha256) && Number.isSafeInteger(value.bytes) && Number.isSafeInteger(value.updatedAt)) {
122
+ return value;
123
+ }
124
+ }
125
+ catch { /* none, or unreadable: a fresh upload replaces it */ }
126
+ return null;
127
+ }
128
+ function receivedBytes(dataDir) {
129
+ try {
130
+ const stat = lstatSync(uploadPartPath(dataDir));
131
+ return stat.isFile() ? stat.size : 0;
132
+ }
133
+ catch {
134
+ return 0;
135
+ }
136
+ }
137
+ export function uploadStatus(dataDir) {
138
+ const meta = readUploadMeta(dataDir);
139
+ return meta ? { sha256: meta.sha256, bytes: meta.bytes, received: Math.min(receivedBytes(dataDir), meta.bytes) } : null;
140
+ }
141
+ export function discardUpload(dataDir) {
142
+ rmSync(uploadPartPath(dataDir), { force: true });
143
+ rmSync(uploadMetaPath(dataDir), { force: true });
144
+ }
145
+ /** Check a declared upload: its size, file count and SHA-256. */
146
+ export function validUploadDeclaration(input) {
147
+ if (typeof input.sha256 !== "string" || !SHA256.test(input.sha256))
148
+ throw fail("This is not a workspace backup.", 400);
149
+ if (!Number.isSafeInteger(input.bytes) || input.bytes < MIN_BYTES)
150
+ throw fail("This is not a workspace backup.", 400);
151
+ if (input.bytes > CLOUD_MOVE_MAX_BYTES)
152
+ throw fail("This workspace is larger than the 10 GB a move can carry.", 413);
153
+ if (input.files !== undefined && (!Number.isSafeInteger(input.files) || input.files < 0 || input.files > CLOUD_MOVE_MAX_FILES)) {
154
+ throw fail("This workspace has more than the 100,000 files a move can carry.", 413);
155
+ }
156
+ return { sha256: input.sha256, bytes: input.bytes };
157
+ }
158
+ /** Start an upload, or continue the same file's upload where it stopped. */
159
+ export function beginUpload(dataDir, declared, now = Date.now()) {
160
+ const existing = readUploadMeta(dataDir);
161
+ if (existing && existing.sha256 === declared.sha256 && existing.bytes === declared.bytes && existing.updatedAt + UPLOAD_STALE_MS > now && receivedBytes(dataDir) <= declared.bytes) {
162
+ writeFileAtomic(uploadMetaPath(dataDir), JSON.stringify({ ...existing, updatedAt: now }), { mode: 0o600 });
163
+ return { ...declared, received: receivedBytes(dataDir) };
164
+ }
165
+ discardUpload(dataDir);
166
+ backupsRoot(dataDir);
167
+ privateFolder(uploadFolder(dataDir));
168
+ writeFileSync(uploadPartPath(dataDir), "", { mode: 0o600, flag: "wx" });
169
+ writeFileAtomic(uploadMetaPath(dataDir), JSON.stringify({ ...declared, updatedAt: now }), { mode: 0o600 });
170
+ return { ...declared, received: 0 };
171
+ }
172
+ /** Store one part at `offset`. A part already stored is accepted again
173
+ * without writing; any other offset than the next one answers where the
174
+ * upload stands (409, `received`). A failed part is cut back off. */
175
+ export async function writeUploadPart(dataDir, sha256, offset, length, body, now = Date.now()) {
176
+ const meta = readUploadMeta(dataDir);
177
+ if (!meta || meta.sha256 !== sha256)
178
+ throw fail("This upload is not in progress on your Cloud. Start the move again.", 404);
179
+ if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(length) || length <= 0 || length > CLOUD_MOVE_MAX_PART_BYTES) {
180
+ throw fail("Invalid upload part.", 400);
181
+ }
182
+ if (offset + length > meta.bytes)
183
+ throw fail("The upload is larger than the file it declared.", 413);
184
+ const received = receivedBytes(dataDir);
185
+ const drain = async () => { for await (const _chunk of body) { /* already stored */ } };
186
+ if (offset + length <= received) {
187
+ await drain();
188
+ return received;
189
+ }
190
+ if (offset !== received) {
191
+ await drain();
192
+ throw fail("The upload continues from another point.", 409, { received });
193
+ }
194
+ const handle = await open(uploadPartPath(dataDir), "r+");
195
+ let written = 0;
196
+ try {
197
+ for await (const chunk of body) {
198
+ if (written + chunk.length > length)
199
+ throw fail("The upload part is larger than it declared.", 413);
200
+ let done = 0;
201
+ while (done < chunk.length)
202
+ done += (await handle.write(chunk, done, chunk.length - done, offset + written + done)).bytesWritten;
203
+ written += chunk.length;
204
+ }
205
+ if (written !== length)
206
+ throw fail("The upload part was incomplete.", 400);
207
+ await handle.sync();
208
+ }
209
+ catch (error) {
210
+ await handle.truncate(received).catch(() => { });
211
+ throw error;
212
+ }
213
+ finally {
214
+ await handle.close();
215
+ }
216
+ writeFileAtomic(uploadMetaPath(dataDir), JSON.stringify({ ...meta, updatedAt: now }), { mode: 0o600 });
217
+ return received + length;
218
+ }
219
+ /** The complete upload's path, once its size and SHA-256 match what was declared. */
220
+ export async function completedUpload(dataDir, sha256) {
221
+ const status = uploadStatus(dataDir);
222
+ if (!status || status.sha256 !== sha256)
223
+ throw fail("This upload is not in progress on your Cloud. Start the move again.", 404);
224
+ if (status.received !== status.bytes)
225
+ throw fail("The upload is not complete yet.", 409, { received: status.received });
226
+ const hash = createHash("sha256");
227
+ for await (const chunk of createReadStream(uploadPartPath(dataDir), { highWaterMark: 1024 * 1024 }))
228
+ hash.update(chunk);
229
+ if (hash.digest("hex") !== sha256) {
230
+ discardUpload(dataDir);
231
+ throw fail("The uploaded file was damaged on the way. Start the move again.", 400);
232
+ }
233
+ return uploadPartPath(dataDir);
234
+ }
235
+ const previousFolder = (dataDir) => join(dataDir, ".backups", "cloud-previous");
236
+ const nextPreviousFolder = (dataDir) => join(dataDir, ".backups", "cloud-previous.next");
237
+ const moveRestoresPath = (dataDir) => join(dataDir, ".backups", "cloud-move-restores.json");
238
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
239
+ function readPrevious(folder) {
240
+ try {
241
+ const value = JSON.parse(readFileSync(join(folder, "previous.json"), "utf8"));
242
+ if (!record(value) || typeof value.password !== "string" || typeof value.createdAt !== "string" || !record(value.contents))
243
+ return null;
244
+ if (!lstatSync(join(folder, "workspace.ombbackup")).isFile())
245
+ return null;
246
+ return value;
247
+ }
248
+ catch {
249
+ return null;
250
+ }
251
+ }
252
+ /** The previous Cloud that can be put back, never its password. A fresh
253
+ * Cloud's (its starter bot, nothing else) is kept but not offered. */
254
+ export function previousCloud(dataDir) {
255
+ const previous = readPrevious(previousFolder(dataDir));
256
+ if (!previous || isEmptyWorkspace(previous.contents))
257
+ return null;
258
+ return { createdAt: previous.createdAt, ...previous.contents, bytes: lstatSync(join(previousFolder(dataDir), "workspace.ombbackup")).size };
259
+ }
260
+ /** Back up this workspace as the next previous Cloud, for the restore
261
+ * `restoreId`. `create` is createWorkspaceBackup, run by the caller inside
262
+ * the maintenance gate. It becomes the previous Cloud only once startup has
263
+ * installed that restore. */
264
+ export async function prepareNextPreviousCloud(dataDir, restoreId, create) {
265
+ const password = randomBytes(32).toString("base64url");
266
+ const contents = workspaceContents(dataDir);
267
+ const created = await create(password);
268
+ const next = nextPreviousFolder(dataDir);
269
+ try {
270
+ backupsRoot(dataDir);
271
+ rmSync(next, { recursive: true, force: true });
272
+ privateFolder(next);
273
+ renameSync(created.path, join(next, "workspace.ombbackup"));
274
+ const meta = { password, createdAt: new Date().toISOString(), contents, forRestore: restoreId };
275
+ writeFileAtomic(join(next, "previous.json"), JSON.stringify(meta), { mode: 0o600 });
276
+ return { createdAt: meta.createdAt, ...contents };
277
+ }
278
+ catch (error) {
279
+ rmSync(next, { recursive: true, force: true });
280
+ throw error;
281
+ }
282
+ finally {
283
+ try {
284
+ removeWorkspaceBackupJob(dataDir, created.id);
285
+ }
286
+ catch { /* The export job expires with the others. */ }
287
+ }
288
+ }
289
+ /** Drop a next previous Cloud whose restore did not commit. */
290
+ export function discardNextPreviousCloud(dataDir) {
291
+ rmSync(nextPreviousFolder(dataDir), { recursive: true, force: true });
292
+ }
293
+ /** Stage the previous Cloud for an ordinary restore; its archive stays. */
294
+ export async function stagePreviousCloud(dataDir, appVersion) {
295
+ const previous = readPrevious(previousFolder(dataDir));
296
+ if (!previous || isEmptyWorkspace(previous.contents))
297
+ throw fail("There is no previous Cloud to restore.", 404);
298
+ const archive = join(previousFolder(dataDir), "workspace.ombbackup");
299
+ const staged = await stageWorkspaceBackup(dataDir, archive, { password: previous.password, currentAppVersion: appVersion });
300
+ return { ...staged, bytes: lstatSync(archive).size };
301
+ }
302
+ /** The archive size of the previous Cloud (0 without one), for space checks. */
303
+ export function previousCloudArchiveBytes(dataDir) {
304
+ try {
305
+ return lstatSync(join(previousFolder(dataDir), "workspace.ombbackup")).size;
306
+ }
307
+ catch {
308
+ return 0;
309
+ }
310
+ }
311
+ function readMoveRestores(dataDir) {
312
+ try {
313
+ const value = JSON.parse(readFileSync(moveRestoresPath(dataDir), "utf8"));
314
+ return Array.isArray(value) ? value.filter((id) => typeof id === "string" && UUID.test(id)) : [];
315
+ }
316
+ catch {
317
+ return [];
318
+ }
319
+ }
320
+ function writeMoveRestores(dataDir, ids) {
321
+ backupsRoot(dataDir);
322
+ if (ids.length)
323
+ writeFileAtomic(moveRestoresPath(dataDir), JSON.stringify([...new Set(ids)]), { mode: 0o600 });
324
+ else
325
+ rmSync(moveRestoresPath(dataDir), { force: true });
326
+ }
327
+ /** Remember a restore made by a move or a swap back, before it commits, so
328
+ * its safety copy and staged files are tidied once it is installed. */
329
+ export function noteMoveRestore(dataDir, restoreId) {
330
+ writeMoveRestores(dataDir, [...readMoveRestores(dataDir), restoreId]);
331
+ }
332
+ export function forgetMoveRestore(dataDir, restoreId) {
333
+ writeMoveRestores(dataDir, readMoveRestores(dataDir).filter((id) => id !== restoreId));
334
+ }
335
+ /** Delete a staged job, and its safety copy, by id: guarded paths only. */
336
+ export function removeMoveFiles(dataDir, id) {
337
+ if (!UUID.test(id))
338
+ return;
339
+ for (const name of [id, `safety-${id}`]) {
340
+ const path = join(dataDir, ".backups", name);
341
+ try {
342
+ const stat = lstatSync(path);
343
+ if (stat.isDirectory() && !stat.isSymbolicLink())
344
+ rmSync(path, { recursive: true, force: true });
345
+ }
346
+ catch { /* already gone */ }
347
+ }
348
+ }
349
+ function pendingRestoreId(dataDir) {
350
+ try {
351
+ const value = JSON.parse(readFileSync(join(dataDir, ".backups", "pending-restore.json"), "utf8"));
352
+ return record(value) && typeof value.id === "string" ? value.id : null;
353
+ }
354
+ catch {
355
+ return null;
356
+ }
357
+ }
358
+ /** At a Cloud home's startup, after any pending restore was applied:
359
+ * - a next previous Cloud becomes the previous Cloud once its restore is
360
+ * installed, and is dropped otherwise;
361
+ * - moves' and swaps' restores, once settled, keep no safety copy or staged
362
+ * files (the previous Cloud is the one undo point);
363
+ * - an upload nobody has touched for a day is deleted.
364
+ * Never touches a restore still waiting to be applied. */
365
+ export function tidyCloudMoveStorage(dataDir, restored, now = Date.now()) {
366
+ if (!existsSync(join(dataDir, ".backups")))
367
+ return;
368
+ const pending = pendingRestoreId(dataDir);
369
+ const next = readPrevious(nextPreviousFolder(dataDir));
370
+ if (next && next.forRestore !== pending) {
371
+ if (restored.restored && restored.id === next.forRestore) {
372
+ rmSync(previousFolder(dataDir), { recursive: true, force: true });
373
+ renameSync(nextPreviousFolder(dataDir), previousFolder(dataDir));
374
+ }
375
+ else
376
+ discardNextPreviousCloud(dataDir);
377
+ }
378
+ else if (!next && existsSync(nextPreviousFolder(dataDir)) && !pending)
379
+ discardNextPreviousCloud(dataDir);
380
+ const settled = readMoveRestores(dataDir).filter((id) => id !== pending);
381
+ for (const id of settled)
382
+ removeMoveFiles(dataDir, id);
383
+ writeMoveRestores(dataDir, readMoveRestores(dataDir).filter((id) => id === pending));
384
+ if (restored.id && settled.includes(restored.id)) {
385
+ // The receipt must not point at a safety copy that is gone.
386
+ delete restored.safetyCopyPath;
387
+ try {
388
+ const receipt = join(dataDir, ".backups", "last-restore.json");
389
+ const value = JSON.parse(readFileSync(receipt, "utf8"));
390
+ if (record(value) && value.id === restored.id && "safetyCopyPath" in value) {
391
+ delete value.safetyCopyPath;
392
+ writeFileAtomic(receipt, JSON.stringify(value), { mode: 0o600 });
393
+ }
394
+ }
395
+ catch { /* no receipt */ }
396
+ }
397
+ const upload = readUploadMeta(dataDir);
398
+ if ((upload && upload.updatedAt + UPLOAD_STALE_MS <= now) || (!upload && existsSync(uploadPartPath(dataDir))))
399
+ discardUpload(dataDir);
400
+ }
401
+ /** Bytes under `.backups`: what backups, safety copies and moves hold on the volume. */
402
+ export function backupsBytes(dataDir) {
403
+ let bytes = 0;
404
+ const walk = (directory) => {
405
+ for (const name of readdirSync(directory)) {
406
+ const stat = lstatSync(join(directory, name));
407
+ if (stat.isDirectory() && !stat.isSymbolicLink())
408
+ walk(join(directory, name));
409
+ else if (stat.isFile())
410
+ bytes += stat.size;
411
+ }
412
+ };
413
+ try {
414
+ walk(join(dataDir, ".backups"));
415
+ }
416
+ catch { /* none yet */ }
417
+ return bytes;
418
+ }
@@ -0,0 +1,196 @@
1
+ // An OMB Cloud home is personal (docs/cloud-pro.md): only the owner's own
2
+ // devices connect, each with an admin session the Admin's signed pairing
3
+ // gave it. No pairing, sign-in or session without admin scope is minted or
4
+ // redeemed there (server/sessions.ts requireAdmin), and at every boot any
5
+ // that is stored is revoked: what it opened or wrote is nobody's from then
6
+ // on, and its key is recorded first, so no crash can ever make it the
7
+ // owner's.
8
+ //
9
+ // Before, every device carried its own key (v0.1.91), so a device the owner
10
+ // unpaired made the owner's conversations read as someone else's. Once, at
11
+ // the first boot of a personal Cloud home, and again after a restore, the
12
+ // keys named until then are settled, in two tiers:
13
+ // - adopted: every key that opened a conversation, wrote a line or answered
14
+ // a card, except a revoked one. Its conversations are the owner's for who
15
+ // opened them, their approval level and their folder (so the owner's rooms
16
+ // and conversations keep Auto or Full, and their project folder);
17
+ // - proven: only a key with proof it was the owner's, an admin session
18
+ // still paired, or one that answered a card (only the owner's devices
19
+ // could, on a Cloud home). Only those feed lending, memory, recall and the
20
+ // recent-work brief (cloud-lending.ts ownerOnlyConversation): a guest the
21
+ // owner unpaired before is indistinguishable from an owner's old device,
22
+ // so its lines never count as the owner's there.
23
+ // Routines fail closed: a routine's writer is the owner only with proof (the
24
+ // owner's key as its writer, the owner's fingerprint, #2023, a proposal the
25
+ // owner approved on its card, or a restore the owner started whose results
26
+ // conversation is theirs); every other routine is nobody's, and runs
27
+ // confined. What a revoked session opened loses its working folder (its next
28
+ // turn gets a private one), and its routines are paused.
29
+ import { lstatSync, readFileSync } from "node:fs";
30
+ import { z } from "zod";
31
+ import { writeFileAtomic } from "./atomic.js";
32
+ export const CLOUD_PERSONAL_REFUSAL = "Cloud Pro is personal: only your own devices can connect.";
33
+ const KEY = /^p_[\w-]{22}$/;
34
+ const keys = z.array(z.string().regex(KEY)).max(100_000);
35
+ const recordFile = z.object({
36
+ version: z.literal(2),
37
+ machineId: z.string().max(128),
38
+ /** Revoked here, ever: never anyone's but nobody's. */
39
+ revokedKeys: keys,
40
+ adoptedKeys: keys,
41
+ provenKeys: keys,
42
+ /** The earlier keys are settled (and routines named). */
43
+ settled: z.boolean(),
44
+ /** A restore applied here whose data is not settled yet. */
45
+ pendingRestore: z.string().max(128).optional(),
46
+ /** The last restore whose data is settled (a restore applied at a start
47
+ * that ended before recording it is found this way at the next). */
48
+ settledRestore: z.string().max(128).optional(),
49
+ }).strict();
50
+ function readRecord(options) {
51
+ const fresh = { version: 2, machineId: options.machineId, revokedKeys: [], adoptedKeys: [], provenKeys: [], settled: false, fresh: true };
52
+ let stat;
53
+ try {
54
+ stat = lstatSync(options.file, { throwIfNoEntry: false });
55
+ }
56
+ catch {
57
+ stat = null;
58
+ }
59
+ if (stat === undefined)
60
+ return fresh;
61
+ try {
62
+ if (!stat || !stat.isFile() || stat.size > 16_000_000)
63
+ throw new Error("not a file");
64
+ const record = recordFile.parse(JSON.parse(readFileSync(options.file, "utf8")));
65
+ // Another machine's (never carried by a backup): nothing in it is ours.
66
+ if (record.machineId !== options.machineId) {
67
+ options.log(`cloud home: ${options.file} is another machine's; settling this one's own`);
68
+ return fresh;
69
+ }
70
+ return record;
71
+ }
72
+ catch {
73
+ // Settled before, and unreadable now: nothing earlier is the owner's, and
74
+ // running the settlement again could make a revoked key theirs.
75
+ options.log(`cloud home: ${options.file} is unreadable, so nothing from before is treated as the owner's`);
76
+ return { ...fresh, settled: true };
77
+ }
78
+ }
79
+ function save(options, record) {
80
+ try {
81
+ writeFileAtomic(options.file, JSON.stringify(record), { mode: 0o600 });
82
+ return true;
83
+ }
84
+ catch (error) {
85
+ options.log(`cloud home: could not record who is the owner (${error instanceof Error ? error.message : String(error)}); trying again at the next start`);
86
+ return false;
87
+ }
88
+ }
89
+ /** A step that never stops the server from starting: logged when it fails
90
+ * (`failed` is then called), and done again at the next start (every step
91
+ * can be). */
92
+ function attempt(options, what, step, fallback, failed) {
93
+ try {
94
+ return step();
95
+ }
96
+ catch (error) {
97
+ options.log(`cloud home: could not ${what} (${error instanceof Error ? error.message : String(error)})`);
98
+ failed?.();
99
+ return fallback;
100
+ }
101
+ }
102
+ /** The settled keys, never one revoked since. */
103
+ function settledSets(record) {
104
+ const revoked = new Set(record.revokedKeys);
105
+ return { adopted: new Set(record.adoptedKeys.filter((key) => !revoked.has(key))), proven: new Set(record.provenKeys.filter((key) => !revoked.has(key))) };
106
+ }
107
+ /** Revoke every session that is not one of the owner's devices, and settle
108
+ * the earlier keys (once, and after a restore). Every step is recorded
109
+ * before it is taken, so a crash anywhere, or a failed save, only ever
110
+ * leaves a key less trusted, and the next start carries on. */
111
+ export function settleCloudOwnership(options) {
112
+ const { fresh, ...read } = readRecord(options);
113
+ let record = read;
114
+ const others = options.sessions.list().filter((session) => !session.scopes.includes("admin"));
115
+ const revoked = new Set([...record.revokedKeys, ...others.map((session) => options.personKey(session))]);
116
+ // A restore to settle: one applied at this start, or a later one than the
117
+ // last settled (its start ended before it was recorded). At the very first
118
+ // start, only one applied now: what came before settles the strict way.
119
+ const restore = options.restoredNow ??
120
+ (!fresh && options.lastRestore && options.lastRestore !== record.settledRestore && options.lastRestore !== record.pendingRestore ? options.lastRestore : undefined);
121
+ record = { ...record, revokedKeys: [...revoked], ...(restore ? { pendingRestore: restore } : {}),
122
+ ...(fresh && !options.restoredNow && options.lastRestore ? { settledRestore: options.lastRestore } : {}) };
123
+ // 1. The keys about to be revoked are recorded first.
124
+ if (!save(options, record))
125
+ return settledSets(record);
126
+ // 2. Their sessions go.
127
+ let count = 0;
128
+ for (const session of others)
129
+ if (attempt(options, "revoke a session", () => options.sessions.revoke(session.id), false))
130
+ count += 1;
131
+ options.log(`cloud home: revoked ${count} session${count === 1 ? "" : "s"} that ${count === 1 ? "was" : "were"} not the owner's own device${count === 1 ? "" : "s"}`);
132
+ // 3. What any revoked key opened or wrote is nobody's: its conversations
133
+ // lose their working folder (the owner's project, often), its routines stop.
134
+ const orphans = attempt(options, "mark conversations as nobody's", () => options.starters.reassign((person) => revoked.has(person), options.nobodyKey), []);
135
+ if (orphans.length)
136
+ attempt(options, "unpin their folders", () => options.unpin(orphans), undefined);
137
+ const stopped = attempt(options, "mark routines as nobody's", () => options.writers.reassign((person) => revoked.has(person), options.nobodyKey), []);
138
+ if (stopped.length)
139
+ attempt(options, "pause their routines", () => options.routines.pause(stopped), undefined);
140
+ if (fresh) {
141
+ options.log("cloud home: only your own devices can connect now. Review Settings → Remote access → Paired devices, and sign out any device that isn't yours.");
142
+ }
143
+ const restoring = record.pendingRestore !== undefined;
144
+ if (record.settled && !restoring)
145
+ return settledSets(record);
146
+ // 4. The earlier keys, recorded before anything is rewritten.
147
+ const eligible = (person) => KEY.test(person) && person !== options.ownerKey && person !== options.nobodyKey && !revoked.has(person);
148
+ // Settled only once every step below is done; else the next start does it again.
149
+ let complete = true;
150
+ const incomplete = () => { complete = false; };
151
+ const lines = attempt(options, "read who wrote in each conversation", () => options.lines(), { senders: [], answerers: [], approvals: [] }, incomplete);
152
+ const admins = options.sessions.list().filter((session) => session.scopes.includes("admin")).map((session) => options.personKey(session));
153
+ const proven = new Set([...record.provenKeys, ...admins, ...lines.answerers].filter(eligible));
154
+ const adopted = new Set([...record.adoptedKeys, ...proven, ...options.starters.people(), ...lines.senders].filter(eligible));
155
+ record = { ...record, adoptedKeys: [...adopted], provenKeys: [...proven] };
156
+ if (!save(options, record))
157
+ return settledSets(record);
158
+ // 5. Conversations the owner provably opened name the owner; routines are
159
+ // the owner's only with proof, else nobody's.
160
+ const moved = attempt(options, "name the owner on their conversations", () => options.starters.reassign((person) => proven.has(person), options.ownerKey), [], incomplete).length;
161
+ const ownersKey = (person) => person !== undefined && (person === options.ownerKey || proven.has(person));
162
+ // A card the owner allowed is proof of what it showed them: only while the
163
+ // routine still runs exactly that (its instructions, bot, schedule, where
164
+ // it runs and no attachment: anyone could change a routine on v0.1.91
165
+ // without being recorded).
166
+ const approved = new Set();
167
+ for (const [id, person, shape] of lines.approvals) {
168
+ if (ownersKey(person) && options.routines.shape(id) === shape)
169
+ approved.add(id);
170
+ }
171
+ let owners = 0, nobodys = 0;
172
+ attempt(options, "name each routine's writer", () => {
173
+ for (const id of options.routines.ids()) {
174
+ const writer = options.routines.writer(id);
175
+ // A restore is proof only for a routine reporting into a conversation
176
+ // that names nobody yet, or the owner (a backup from before can hold a
177
+ // guest's routine and its conversation).
178
+ const opener = options.routines.resultsOpener(id);
179
+ const owner = writer === options.ownerKey || options.routines.fingerprinted(id) || approved.has(id) ||
180
+ (restoring && writer !== options.nobodyKey && !(writer !== undefined && revoked.has(writer)) && (opener === undefined || ownersKey(opener)));
181
+ if (writer !== (owner ? options.ownerKey : options.nobodyKey))
182
+ options.routines.name(id, owner ? options.ownerKey : options.nobodyKey);
183
+ if (owner)
184
+ owners += 1;
185
+ else
186
+ nobodys += 1;
187
+ }
188
+ }, undefined, incomplete);
189
+ if (!complete)
190
+ return settledSets(record);
191
+ const { pendingRestore: done, ...rest } = record;
192
+ record = { ...rest, settled: true, ...(done ? { settledRestore: done } : {}) };
193
+ save(options, record);
194
+ options.log(`cloud home: settled what came before${restoring ? " (a restore)" : ""}: ${adopted.size} earlier device key${adopted.size === 1 ? "" : "s"} (${proven.size} proven), ${moved} conversation${moved === 1 ? "" : "s"} named the owner's, ${owners} routine${owners === 1 ? "" : "s"} the owner's and ${nobodys} nobody's`);
195
+ return settledSets(record);
196
+ }
@@ -0,0 +1,6 @@
1
+ // The server's first import (server/index.ts): on an OMB Cloud home, the
2
+ // secrets the launcher hands over its pipe are read, and the pipe closed,
3
+ // before any other module is loaded, so no process the server starts can
4
+ // inherit it (cloud-secrets.ts, cloud-home-start.ts).
5
+ import { takeCloudSecrets } from "./cloud-secrets.js";
6
+ export const BOOT_CLOUD_SECRETS = takeCloudSecrets();