@raybase/core 0.0.1 → 0.3.3

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 (435) hide show
  1. package/LICENSE +21 -0
  2. package/dist/.admin-build/AdminApp.css +2 -0
  3. package/dist/.admin-build/AdminApp.js +77 -0
  4. package/dist/.admin-docs-build/AdminDocsApp.css +2 -0
  5. package/dist/.admin-docs-build/AdminDocsApp.js +45 -0
  6. package/dist/actions.d.ts +347 -0
  7. package/dist/actions.d.ts.map +1 -0
  8. package/dist/actions.js +661 -0
  9. package/dist/actions.js.map +1 -0
  10. package/dist/admin-assets/admin.7738f50720a8ed7b.css +2 -0
  11. package/dist/admin-assets/admin.8678cb61db34dc81.js +77 -0
  12. package/dist/admin-boot.d.ts +94 -0
  13. package/dist/admin-boot.d.ts.map +1 -0
  14. package/dist/admin-boot.js +139 -0
  15. package/dist/admin-boot.js.map +1 -0
  16. package/dist/admin-docs-content.d.ts +95 -0
  17. package/dist/admin-docs-content.d.ts.map +1 -0
  18. package/dist/admin-docs-content.js +463 -0
  19. package/dist/admin-docs-content.js.map +1 -0
  20. package/dist/admin-docs-ui.d.ts +11 -0
  21. package/dist/admin-docs-ui.d.ts.map +1 -0
  22. package/dist/admin-docs-ui.js +23 -0
  23. package/dist/admin-docs-ui.js.map +1 -0
  24. package/dist/admin-kit.d.ts +29 -0
  25. package/dist/admin-kit.js +1557 -0
  26. package/dist/admin-role.d.ts +8 -0
  27. package/dist/admin-role.d.ts.map +1 -0
  28. package/dist/admin-role.js +22 -0
  29. package/dist/admin-role.js.map +1 -0
  30. package/dist/admin-ui.d.ts +3 -0
  31. package/dist/admin-ui.d.ts.map +1 -0
  32. package/dist/admin-ui.js +16 -0
  33. package/dist/admin-ui.js.map +1 -0
  34. package/dist/agent-ai-chat.d.ts +127 -0
  35. package/dist/agent-ai-chat.d.ts.map +1 -0
  36. package/dist/agent-ai-chat.js +5975 -0
  37. package/dist/agent-ai-chat.js.map +1 -0
  38. package/dist/agent-chapter-archive.d.ts +214 -0
  39. package/dist/agent-chapter-archive.d.ts.map +1 -0
  40. package/dist/agent-chapter-archive.js +696 -0
  41. package/dist/agent-chapter-archive.js.map +1 -0
  42. package/dist/agent-chapter-search.d.ts +116 -0
  43. package/dist/agent-chapter-search.d.ts.map +1 -0
  44. package/dist/agent-chapter-search.js +605 -0
  45. package/dist/agent-chapter-search.js.map +1 -0
  46. package/dist/agent-chapter-tools.d.ts +45 -0
  47. package/dist/agent-chapter-tools.d.ts.map +1 -0
  48. package/dist/agent-chapter-tools.js +344 -0
  49. package/dist/agent-chapter-tools.js.map +1 -0
  50. package/dist/agent-chapters.d.ts +228 -0
  51. package/dist/agent-chapters.d.ts.map +1 -0
  52. package/dist/agent-chapters.js +737 -0
  53. package/dist/agent-chapters.js.map +1 -0
  54. package/dist/agent-data-parts.d.ts +167 -0
  55. package/dist/agent-data-parts.d.ts.map +1 -0
  56. package/dist/agent-data-parts.js +68 -0
  57. package/dist/agent-data-parts.js.map +1 -0
  58. package/dist/agent-errors.d.ts +41 -0
  59. package/dist/agent-errors.d.ts.map +1 -0
  60. package/dist/agent-errors.js +191 -0
  61. package/dist/agent-errors.js.map +1 -0
  62. package/dist/agent-follow-up-store.d.ts +273 -0
  63. package/dist/agent-follow-up-store.d.ts.map +1 -0
  64. package/dist/agent-follow-up-store.js +703 -0
  65. package/dist/agent-follow-up-store.js.map +1 -0
  66. package/dist/agent-follow-ups.d.ts +314 -0
  67. package/dist/agent-follow-ups.d.ts.map +1 -0
  68. package/dist/agent-follow-ups.js +840 -0
  69. package/dist/agent-follow-ups.js.map +1 -0
  70. package/dist/agent-memory.d.ts +323 -0
  71. package/dist/agent-memory.d.ts.map +1 -0
  72. package/dist/agent-memory.js +1270 -0
  73. package/dist/agent-memory.js.map +1 -0
  74. package/dist/agent-message-parts.d.ts +58 -0
  75. package/dist/agent-message-parts.d.ts.map +1 -0
  76. package/dist/agent-message-parts.js +163 -0
  77. package/dist/agent-message-parts.js.map +1 -0
  78. package/dist/agent-remote-mcp-credentials.d.ts +154 -0
  79. package/dist/agent-remote-mcp-credentials.d.ts.map +1 -0
  80. package/dist/agent-remote-mcp-credentials.js +287 -0
  81. package/dist/agent-remote-mcp-credentials.js.map +1 -0
  82. package/dist/agent-remote-mcp-crypto.d.ts +32 -0
  83. package/dist/agent-remote-mcp-crypto.d.ts.map +1 -0
  84. package/dist/agent-remote-mcp-crypto.js +101 -0
  85. package/dist/agent-remote-mcp-crypto.js.map +1 -0
  86. package/dist/agent-remote-mcp-install.d.ts +299 -0
  87. package/dist/agent-remote-mcp-install.d.ts.map +1 -0
  88. package/dist/agent-remote-mcp-install.js +785 -0
  89. package/dist/agent-remote-mcp-install.js.map +1 -0
  90. package/dist/agent-remote-mcp-oauth.d.ts +73 -0
  91. package/dist/agent-remote-mcp-oauth.d.ts.map +1 -0
  92. package/dist/agent-remote-mcp-oauth.js +344 -0
  93. package/dist/agent-remote-mcp-oauth.js.map +1 -0
  94. package/dist/agent-remote-mcp-transport.d.ts +46 -0
  95. package/dist/agent-remote-mcp-transport.d.ts.map +1 -0
  96. package/dist/agent-remote-mcp-transport.js +225 -0
  97. package/dist/agent-remote-mcp-transport.js.map +1 -0
  98. package/dist/agent-remote-mcp.d.ts +147 -0
  99. package/dist/agent-remote-mcp.d.ts.map +1 -0
  100. package/dist/agent-remote-mcp.js +681 -0
  101. package/dist/agent-remote-mcp.js.map +1 -0
  102. package/dist/agent-room-redaction.d.ts +66 -0
  103. package/dist/agent-room-redaction.d.ts.map +1 -0
  104. package/dist/agent-room-redaction.js +264 -0
  105. package/dist/agent-room-redaction.js.map +1 -0
  106. package/dist/agent-rooms.d.ts +209 -0
  107. package/dist/agent-rooms.d.ts.map +1 -0
  108. package/dist/agent-rooms.js +244 -0
  109. package/dist/agent-rooms.js.map +1 -0
  110. package/dist/agent-runtime.d.ts +541 -0
  111. package/dist/agent-runtime.d.ts.map +1 -0
  112. package/dist/agent-runtime.js +4260 -0
  113. package/dist/agent-runtime.js.map +1 -0
  114. package/dist/agent-skill-archive.d.ts +20 -0
  115. package/dist/agent-skill-archive.d.ts.map +1 -0
  116. package/dist/agent-skill-archive.js +316 -0
  117. package/dist/agent-skill-archive.js.map +1 -0
  118. package/dist/agent-skill-discovery.d.ts +96 -0
  119. package/dist/agent-skill-discovery.d.ts.map +1 -0
  120. package/dist/agent-skill-discovery.js +464 -0
  121. package/dist/agent-skill-discovery.js.map +1 -0
  122. package/dist/agent-skill-format.d.ts +57 -0
  123. package/dist/agent-skill-format.d.ts.map +1 -0
  124. package/dist/agent-skill-format.js +238 -0
  125. package/dist/agent-skill-format.js.map +1 -0
  126. package/dist/agent-skill-import.d.ts +64 -0
  127. package/dist/agent-skill-import.d.ts.map +1 -0
  128. package/dist/agent-skill-import.js +295 -0
  129. package/dist/agent-skill-import.js.map +1 -0
  130. package/dist/agent-skill-publish.d.ts +51 -0
  131. package/dist/agent-skill-publish.d.ts.map +1 -0
  132. package/dist/agent-skill-publish.js +136 -0
  133. package/dist/agent-skill-publish.js.map +1 -0
  134. package/dist/agent-skill-store.d.ts +252 -0
  135. package/dist/agent-skill-store.d.ts.map +1 -0
  136. package/dist/agent-skill-store.js +514 -0
  137. package/dist/agent-skill-store.js.map +1 -0
  138. package/dist/agent-skills.d.ts +85 -0
  139. package/dist/agent-skills.d.ts.map +1 -0
  140. package/dist/agent-skills.js +291 -0
  141. package/dist/agent-skills.js.map +1 -0
  142. package/dist/agent-transcript-pager.d.ts +77 -0
  143. package/dist/agent-transcript-pager.d.ts.map +1 -0
  144. package/dist/agent-transcript-pager.js +210 -0
  145. package/dist/agent-transcript-pager.js.map +1 -0
  146. package/dist/agent-user-data-parts.d.ts +20 -0
  147. package/dist/agent-user-data-parts.d.ts.map +1 -0
  148. package/dist/agent-user-data-parts.js +161 -0
  149. package/dist/agent-user-data-parts.js.map +1 -0
  150. package/dist/agent-user-transcript.d.ts +46 -0
  151. package/dist/agent-user-transcript.d.ts.map +1 -0
  152. package/dist/agent-user-transcript.js +106 -0
  153. package/dist/agent-user-transcript.js.map +1 -0
  154. package/dist/agent-voice-notes.d.ts +15 -0
  155. package/dist/agent-voice-notes.d.ts.map +1 -0
  156. package/dist/agent-voice-notes.js +17 -0
  157. package/dist/agent-voice-notes.js.map +1 -0
  158. package/dist/agent-wakes.d.ts +213 -0
  159. package/dist/agent-wakes.d.ts.map +1 -0
  160. package/dist/agent-wakes.js +534 -0
  161. package/dist/agent-wakes.js.map +1 -0
  162. package/dist/agent.d.ts +1780 -0
  163. package/dist/agent.d.ts.map +1 -0
  164. package/dist/agent.js +3123 -0
  165. package/dist/agent.js.map +1 -0
  166. package/dist/ai.d.ts +583 -0
  167. package/dist/ai.d.ts.map +1 -0
  168. package/dist/ai.js +1858 -0
  169. package/dist/ai.js.map +1 -0
  170. package/dist/app-manifest.d.ts +255 -0
  171. package/dist/app-manifest.d.ts.map +1 -0
  172. package/dist/app-manifest.js +372 -0
  173. package/dist/app-manifest.js.map +1 -0
  174. package/dist/app-users.d.ts +30 -0
  175. package/dist/app-users.d.ts.map +1 -0
  176. package/dist/app-users.js +91 -0
  177. package/dist/app-users.js.map +1 -0
  178. package/dist/audit-archive.d.ts +85 -0
  179. package/dist/audit-archive.d.ts.map +1 -0
  180. package/dist/audit-archive.js +178 -0
  181. package/dist/audit-archive.js.map +1 -0
  182. package/dist/audit.d.ts +54 -0
  183. package/dist/audit.d.ts.map +1 -0
  184. package/dist/audit.js +95 -0
  185. package/dist/audit.js.map +1 -0
  186. package/dist/auth-logger.d.ts +28 -0
  187. package/dist/auth-logger.d.ts.map +1 -0
  188. package/dist/auth-logger.js +53 -0
  189. package/dist/auth-logger.js.map +1 -0
  190. package/dist/auth-password-secret.d.ts +59 -0
  191. package/dist/auth-password-secret.d.ts.map +1 -0
  192. package/dist/auth-password-secret.js +409 -0
  193. package/dist/auth-password-secret.js.map +1 -0
  194. package/dist/auth-schema.d.ts +120 -0
  195. package/dist/auth-schema.d.ts.map +1 -0
  196. package/dist/auth-schema.js +285 -0
  197. package/dist/auth-schema.js.map +1 -0
  198. package/dist/auth.d.ts +321 -0
  199. package/dist/auth.d.ts.map +1 -0
  200. package/dist/auth.js +1562 -0
  201. package/dist/auth.js.map +1 -0
  202. package/dist/backup-archive.d.ts +257 -0
  203. package/dist/backup-archive.d.ts.map +1 -0
  204. package/dist/backup-archive.js +2068 -0
  205. package/dist/backup-archive.js.map +1 -0
  206. package/dist/backup-crypto.d.ts +313 -0
  207. package/dist/backup-crypto.d.ts.map +1 -0
  208. package/dist/backup-crypto.js +693 -0
  209. package/dist/backup-crypto.js.map +1 -0
  210. package/dist/backup-format.d.ts +200 -0
  211. package/dist/backup-format.d.ts.map +1 -0
  212. package/dist/backup-format.js +1247 -0
  213. package/dist/backup-format.js.map +1 -0
  214. package/dist/cli.d.ts +9 -0
  215. package/dist/cli.d.ts.map +1 -0
  216. package/dist/cli.js +15 -0
  217. package/dist/cli.js.map +1 -0
  218. package/dist/clock.d.ts +35 -0
  219. package/dist/clock.d.ts.map +1 -0
  220. package/dist/clock.js +83 -0
  221. package/dist/clock.js.map +1 -0
  222. package/dist/data-client.d.ts +187 -0
  223. package/dist/data-client.d.ts.map +1 -0
  224. package/dist/data-client.js +590 -0
  225. package/dist/data-client.js.map +1 -0
  226. package/dist/data-secrets.d.ts +164 -0
  227. package/dist/data-secrets.d.ts.map +1 -0
  228. package/dist/data-secrets.js +383 -0
  229. package/dist/data-secrets.js.map +1 -0
  230. package/dist/db.d.ts +453 -0
  231. package/dist/db.d.ts.map +1 -0
  232. package/dist/db.js +324 -0
  233. package/dist/db.js.map +1 -0
  234. package/dist/dev-activity.d.ts +48 -0
  235. package/dist/dev-activity.d.ts.map +1 -0
  236. package/dist/dev-activity.js +50 -0
  237. package/dist/dev-activity.js.map +1 -0
  238. package/dist/do.d.ts +429 -0
  239. package/dist/do.d.ts.map +1 -0
  240. package/dist/do.js +4526 -0
  241. package/dist/do.js.map +1 -0
  242. package/dist/email.d.ts +137 -0
  243. package/dist/email.d.ts.map +1 -0
  244. package/dist/email.js +527 -0
  245. package/dist/email.js.map +1 -0
  246. package/dist/generated/admin-assets.d.ts +16 -0
  247. package/dist/generated/admin-assets.d.ts.map +1 -0
  248. package/dist/generated/admin-assets.js +16 -0
  249. package/dist/generated/admin-assets.js.map +1 -0
  250. package/dist/generated/admin-docs-assets.d.ts +3 -0
  251. package/dist/generated/admin-docs-assets.d.ts.map +1 -0
  252. package/dist/generated/admin-docs-assets.js +3 -0
  253. package/dist/generated/admin-docs-assets.js.map +1 -0
  254. package/dist/handoff-consumer.d.ts +79 -0
  255. package/dist/handoff-consumer.d.ts.map +1 -0
  256. package/dist/handoff-consumer.js +300 -0
  257. package/dist/handoff-consumer.js.map +1 -0
  258. package/dist/handoff.d.ts +70 -0
  259. package/dist/handoff.d.ts.map +1 -0
  260. package/dist/handoff.js +228 -0
  261. package/dist/handoff.js.map +1 -0
  262. package/dist/host-auth.d.ts +47 -0
  263. package/dist/host-auth.d.ts.map +1 -0
  264. package/dist/host-auth.js +59 -0
  265. package/dist/host-auth.js.map +1 -0
  266. package/dist/host-user.d.ts +15 -0
  267. package/dist/host-user.d.ts.map +1 -0
  268. package/dist/host-user.js +23 -0
  269. package/dist/host-user.js.map +1 -0
  270. package/dist/html.d.ts +35 -0
  271. package/dist/html.d.ts.map +1 -0
  272. package/dist/html.js +634 -0
  273. package/dist/html.js.map +1 -0
  274. package/dist/http-error.d.ts +13 -0
  275. package/dist/http-error.d.ts.map +1 -0
  276. package/dist/http-error.js +15 -0
  277. package/dist/http-error.js.map +1 -0
  278. package/dist/imports.d.ts +219 -0
  279. package/dist/imports.d.ts.map +1 -0
  280. package/dist/imports.js +571 -0
  281. package/dist/imports.js.map +1 -0
  282. package/dist/index.d.ts +78 -0
  283. package/dist/index.d.ts.map +1 -0
  284. package/dist/index.js +76 -0
  285. package/dist/index.js.map +1 -0
  286. package/dist/inference-tape.d.ts +64 -0
  287. package/dist/inference-tape.d.ts.map +1 -0
  288. package/dist/inference-tape.js +278 -0
  289. package/dist/inference-tape.js.map +1 -0
  290. package/dist/internal.d.ts +15 -0
  291. package/dist/internal.d.ts.map +1 -0
  292. package/dist/internal.js +48 -0
  293. package/dist/internal.js.map +1 -0
  294. package/dist/jobs.d.ts +796 -0
  295. package/dist/jobs.d.ts.map +1 -0
  296. package/dist/jobs.js +538 -0
  297. package/dist/jobs.js.map +1 -0
  298. package/dist/json-schema.d.ts +91 -0
  299. package/dist/json-schema.d.ts.map +1 -0
  300. package/dist/json-schema.js +884 -0
  301. package/dist/json-schema.js.map +1 -0
  302. package/dist/knowledge.d.ts +33 -0
  303. package/dist/knowledge.d.ts.map +1 -0
  304. package/dist/knowledge.js +348 -0
  305. package/dist/knowledge.js.map +1 -0
  306. package/dist/local-dev-chrome.d.ts +29 -0
  307. package/dist/local-dev-chrome.d.ts.map +1 -0
  308. package/dist/local-dev-chrome.js +514 -0
  309. package/dist/local-dev-chrome.js.map +1 -0
  310. package/dist/local-dev-personas.d.ts +15 -0
  311. package/dist/local-dev-personas.d.ts.map +1 -0
  312. package/dist/local-dev-personas.js +46 -0
  313. package/dist/local-dev-personas.js.map +1 -0
  314. package/dist/mcp.d.ts +567 -0
  315. package/dist/mcp.d.ts.map +1 -0
  316. package/dist/mcp.js +2480 -0
  317. package/dist/mcp.js.map +1 -0
  318. package/dist/modules.d.ts +145 -0
  319. package/dist/modules.d.ts.map +1 -0
  320. package/dist/modules.js +164 -0
  321. package/dist/modules.js.map +1 -0
  322. package/dist/notification-client.d.ts +94 -0
  323. package/dist/notification-client.d.ts.map +1 -0
  324. package/dist/notification-client.js +127 -0
  325. package/dist/notification-client.js.map +1 -0
  326. package/dist/notifications.d.ts +447 -0
  327. package/dist/notifications.d.ts.map +1 -0
  328. package/dist/notifications.js +1625 -0
  329. package/dist/notifications.js.map +1 -0
  330. package/dist/operator-data-query.d.ts +117 -0
  331. package/dist/operator-data-query.d.ts.map +1 -0
  332. package/dist/operator-data-query.js +431 -0
  333. package/dist/operator-data-query.js.map +1 -0
  334. package/dist/operator-mcp.d.ts +63 -0
  335. package/dist/operator-mcp.d.ts.map +1 -0
  336. package/dist/operator-mcp.js +2828 -0
  337. package/dist/operator-mcp.js.map +1 -0
  338. package/dist/outbound-https.d.ts +24 -0
  339. package/dist/outbound-https.d.ts.map +1 -0
  340. package/dist/outbound-https.js +172 -0
  341. package/dist/outbound-https.js.map +1 -0
  342. package/dist/pitr.d.ts +60 -0
  343. package/dist/pitr.d.ts.map +1 -0
  344. package/dist/pitr.js +94 -0
  345. package/dist/pitr.js.map +1 -0
  346. package/dist/platform.d.ts +199 -0
  347. package/dist/platform.d.ts.map +1 -0
  348. package/dist/platform.js +196 -0
  349. package/dist/platform.js.map +1 -0
  350. package/dist/policies.d.ts +115 -0
  351. package/dist/policies.d.ts.map +1 -0
  352. package/dist/policies.js +158 -0
  353. package/dist/policies.js.map +1 -0
  354. package/dist/principal.d.ts +143 -0
  355. package/dist/principal.d.ts.map +1 -0
  356. package/dist/principal.js +174 -0
  357. package/dist/principal.js.map +1 -0
  358. package/dist/realtime.d.ts +280 -0
  359. package/dist/realtime.d.ts.map +1 -0
  360. package/dist/realtime.js +578 -0
  361. package/dist/realtime.js.map +1 -0
  362. package/dist/retrieval.d.ts +263 -0
  363. package/dist/retrieval.d.ts.map +1 -0
  364. package/dist/retrieval.js +602 -0
  365. package/dist/retrieval.js.map +1 -0
  366. package/dist/runtime-error-client.d.ts +10 -0
  367. package/dist/runtime-error-client.d.ts.map +1 -0
  368. package/dist/runtime-error-client.js +70 -0
  369. package/dist/runtime-error-client.js.map +1 -0
  370. package/dist/runtime-errors.d.ts +180 -0
  371. package/dist/runtime-errors.d.ts.map +1 -0
  372. package/dist/runtime-errors.js +572 -0
  373. package/dist/runtime-errors.js.map +1 -0
  374. package/dist/schema.d.ts +434 -0
  375. package/dist/schema.d.ts.map +1 -0
  376. package/dist/schema.js +232 -0
  377. package/dist/schema.js.map +1 -0
  378. package/dist/search.d.ts +216 -0
  379. package/dist/search.d.ts.map +1 -0
  380. package/dist/search.js +785 -0
  381. package/dist/search.js.map +1 -0
  382. package/dist/settings.d.ts +191 -0
  383. package/dist/settings.d.ts.map +1 -0
  384. package/dist/settings.js +478 -0
  385. package/dist/settings.js.map +1 -0
  386. package/dist/sql.d.ts +7 -0
  387. package/dist/sql.d.ts.map +1 -0
  388. package/dist/sql.js +30 -0
  389. package/dist/sql.js.map +1 -0
  390. package/dist/storage-client.d.ts +69 -0
  391. package/dist/storage-client.d.ts.map +1 -0
  392. package/dist/storage-client.js +469 -0
  393. package/dist/storage-client.js.map +1 -0
  394. package/dist/storage.d.ts +21 -0
  395. package/dist/storage.d.ts.map +1 -0
  396. package/dist/storage.js +82 -0
  397. package/dist/storage.js.map +1 -0
  398. package/dist/subject-domains.d.ts +15 -0
  399. package/dist/subject-domains.d.ts.map +1 -0
  400. package/dist/subject-domains.js +38 -0
  401. package/dist/subject-domains.js.map +1 -0
  402. package/dist/support.d.ts +132 -0
  403. package/dist/support.d.ts.map +1 -0
  404. package/dist/support.js +483 -0
  405. package/dist/support.js.map +1 -0
  406. package/dist/system-tables.d.ts +12 -0
  407. package/dist/system-tables.d.ts.map +1 -0
  408. package/dist/system-tables.js +225 -0
  409. package/dist/system-tables.js.map +1 -0
  410. package/dist/test.d.ts +57 -0
  411. package/dist/test.d.ts.map +1 -0
  412. package/dist/test.js +454 -0
  413. package/dist/test.js.map +1 -0
  414. package/dist/vite.d.ts +82 -0
  415. package/dist/vite.d.ts.map +1 -0
  416. package/dist/vite.js +82 -0
  417. package/dist/vite.js.map +1 -0
  418. package/dist/webpush-crypto.d.ts +39 -0
  419. package/dist/webpush-crypto.d.ts.map +1 -0
  420. package/dist/webpush-crypto.js +213 -0
  421. package/dist/webpush-crypto.js.map +1 -0
  422. package/dist/webpush-private.d.ts +11 -0
  423. package/dist/webpush-private.d.ts.map +1 -0
  424. package/dist/webpush-private.js +11 -0
  425. package/dist/webpush-private.js.map +1 -0
  426. package/dist/webpush.d.ts +86 -0
  427. package/dist/webpush.d.ts.map +1 -0
  428. package/dist/webpush.js +375 -0
  429. package/dist/webpush.js.map +1 -0
  430. package/dist/worker.d.ts +185 -0
  431. package/dist/worker.d.ts.map +1 -0
  432. package/dist/worker.js +13021 -0
  433. package/dist/worker.js.map +1 -0
  434. package/package.json +161 -8
  435. package/README.md +0 -3
@@ -0,0 +1,2068 @@
1
+ import { sha256Hex } from "./audit-archive.js";
2
+ import { audit, parseStoredAppRoles, raybaseAuthExecutor } from "./auth.js";
3
+ import { assertRaybasePasswordSecretParticipantsReady, createRaybasePasswordWrapParticipants, listRaybasePasswordSecretReadiness, raybasePasswordSecretPurpose } from "./auth-password-secret.js";
4
+ import { createDeploymentBackupKeyProvider, backupProviderLimits, generateBackupDek, RaybaseBackupError, unwrapBackupDek, wrapBackupDek } from "./backup-crypto.js";
5
+ import { BackupLogicalDecoder, backupArchiveMagic, backupBinaryContentType, backupFrameHeaderBytes, backupFrameTagBytes, backupFormatLimits, canonicalBackupJson, createBackupArchiveDecryptor, createBackupArchiveEncryptor, createBackupSha256Digest, decodeBackupArchiveHeader, encodeBackupArchiveHeader, encodeBackupLogicalRecord } from "./backup-format.js";
6
+ import { backupDatabaseProtocolLimits, dataStub, retryConsistentBackupSnapshot } from "./db.js";
7
+ import { pitrUnavailableReason, recoverInterruptedBackupRestore } from "./pitr.js";
8
+ export const backupSqlContentType = "application/sql; charset=utf-8";
9
+ export const backupRootManifestKey = "backups/manifest.json";
10
+ export const coreBackupManifestKey = "backups/core/manifest.json";
11
+ export const coreBackupPrefix = "backups/core/";
12
+ export const backupTemporaryPrefix = "backups/.temporary/";
13
+ export const backupTemporaryRetentionMs = 24 * 60 * 60 * 1000;
14
+ export const backupLegacyUploadMaximumBytes = 16 * 1024 * 1024;
15
+ export const backupMultipartMinimumPartBytes = 5 * 1024 * 1024;
16
+ export const backupMultipartDefaultPartBytes = 8 * 1024 * 1024;
17
+ export const backupMultipartMaximumParts = 10_000;
18
+ export const backupMultipartMaximumPartBytes = 5 * 1024 * 1024 * 1024;
19
+ export const backupMultipartMaximumBufferedPartBytes = 64 * 1024 * 1024;
20
+ export const backupMultipartMaximumObjectBytes = 5 * 1024 * 1024 * 1024 * 1024;
21
+ export const backupArchiveDiscoveryMaximumObjects = 10_000;
22
+ const defaultRetention = 30;
23
+ const backupArchiveDiscoveryMaximumWarnings = 20;
24
+ const backupArchiveDataFrameKind = 1;
25
+ const backupArchiveFinalFrameKind = 2;
26
+ const backupArchiveFinalFrameMaximumBytes = 4096;
27
+ function normalizedFlag(value) {
28
+ const trimmed = value?.trim().toLowerCase();
29
+ return trimmed ? trimmed : null;
30
+ }
31
+ export function backupsEnabled(env) {
32
+ const value = normalizedFlag(env.RAYBASE_BACKUPS_ENABLED);
33
+ if (value && ["false", "0", "no", "off"].includes(value))
34
+ return false;
35
+ if (value)
36
+ return ["true", "1", "yes", "on"].includes(value);
37
+ return Boolean(env.RAYBASE_BACKUPS);
38
+ }
39
+ export function parseBackupRetention(value) {
40
+ if (value === undefined || value.trim() === "")
41
+ return defaultRetention;
42
+ const retention = Number(value);
43
+ if (!Number.isInteger(retention) || retention < 1) {
44
+ throw new Error("RAYBASE_BACKUPS_RETAIN must be a positive integer");
45
+ }
46
+ return retention;
47
+ }
48
+ function backupBytesToHex(value) {
49
+ return Array.from(value, (byte) => byte.toString(16).padStart(2, "0")).join("");
50
+ }
51
+ function retainedDeploymentSecrets(value) {
52
+ return (value ?? "")
53
+ .split(/[\s,]+/)
54
+ .map((secret) => secret.trim())
55
+ .filter(Boolean);
56
+ }
57
+ async function backupProviderSet(env, backups) {
58
+ const currentSecret = env.RAYBASE_BACKUPS_KEY?.trim();
59
+ const deploymentProvider = currentSecret
60
+ ? await createDeploymentBackupKeyProvider({
61
+ currentSecret,
62
+ retainedSecrets: retainedDeploymentSecrets(env.RAYBASE_BACKUPS_KEY_RETAINED)
63
+ })
64
+ : undefined;
65
+ return {
66
+ ...(backups?.keyProvider ? { keyProvider: backups.keyProvider } : {}),
67
+ ...(deploymentProvider ? { deploymentProvider } : {})
68
+ };
69
+ }
70
+ function selectedBackupProvider(providers) {
71
+ return providers.keyProvider ?? providers.deploymentProvider;
72
+ }
73
+ function backupReadinessContext(environment) {
74
+ return {
75
+ archiveId: crypto.randomUUID(),
76
+ product: "core",
77
+ target: "core",
78
+ targetId: "core",
79
+ createdAt: new Date().toISOString(),
80
+ purpose: "readiness",
81
+ signal: new AbortController().signal,
82
+ ...(environment ? { environment } : {})
83
+ };
84
+ }
85
+ async function probeBackupProviderForCreation(env, providers, context) {
86
+ const dek = generateBackupDek();
87
+ const selected = selectedBackupProvider(providers);
88
+ const wrapParticipants = selected
89
+ ? await resolvePasswordParticipantsForWrap(env, selected, context)
90
+ : { participants: Object.freeze([]), release() { } };
91
+ try {
92
+ return await wrapBackupDek({ providers, context, dek, participants: wrapParticipants.participants });
93
+ }
94
+ finally {
95
+ wrapParticipants.release();
96
+ dek.fill(0);
97
+ }
98
+ }
99
+ const participantDirectoryStringPattern = /^[A-Za-z0-9][A-Za-z0-9._:/-]*$/;
100
+ function validatedDirectoryStrings(value, label, allowNull = false) {
101
+ if (value === undefined)
102
+ return [];
103
+ if (!Array.isArray(value) || value.length > 32) {
104
+ throw new RaybaseBackupError("BACKUP_LIMIT_EXCEEDED", `${label} must contain at most 32 values`);
105
+ }
106
+ const values = value.map((item) => {
107
+ if (allowNull && item === null)
108
+ return null;
109
+ if (typeof item !== "string" ||
110
+ !participantDirectoryStringPattern.test(item) ||
111
+ new TextEncoder().encode(item).byteLength > backupProviderLimits.identifierBytes) {
112
+ throw new RaybaseBackupError("BACKUP_PROVIDER_ENVELOPE_INVALID", `${label} contains an invalid value`);
113
+ }
114
+ return item;
115
+ });
116
+ if (new Set(values).size !== values.length) {
117
+ throw new RaybaseBackupError("BACKUP_PROVIDER_ENVELOPE_INVALID", `${label} contains duplicate values`);
118
+ }
119
+ return values;
120
+ }
121
+ function createPasswordParticipantDirectory(env, provider) {
122
+ const lifecycle = provider.passwordSecrets;
123
+ if (!lifecycle) {
124
+ throw new RaybaseBackupError("BACKUP_PROVIDER_UNAVAILABLE", "The backup provider has no password lifecycle");
125
+ }
126
+ const purpose = `${raybasePasswordSecretPurpose}:${provider.id}`;
127
+ const lifecycleVersion = lifecycle.version.trim();
128
+ return Object.freeze({
129
+ async find(request = {}) {
130
+ const limit = request.limit ?? 50;
131
+ if (!Number.isInteger(limit) || limit < 1 || limit > backupProviderLimits.participantDirectoryPageRecords) {
132
+ throw new RaybaseBackupError("BACKUP_LIMIT_EXCEEDED", `Backup participant directory pages support 1-${backupProviderLimits.participantDirectoryPageRecords} records`);
133
+ }
134
+ const afterUserId = request.afterUserId?.trim();
135
+ if (request.afterUserId !== undefined &&
136
+ (!afterUserId || new TextEncoder().encode(afterUserId).byteLength > 256)) {
137
+ throw new RaybaseBackupError("BACKUP_PROVIDER_ENVELOPE_INVALID", "Backup participant directory cursor is invalid");
138
+ }
139
+ const filter = request.filter ?? {};
140
+ const allRoles = validatedDirectoryStrings(filter.appRoles?.all, "Backup participant all-role filter");
141
+ const anyRoles = validatedDirectoryStrings(filter.appRoles?.any, "Backup participant any-role filter");
142
+ const noRoles = validatedDirectoryStrings(filter.appRoles?.none, "Backup participant excluded-role filter");
143
+ const operatorRoles = validatedDirectoryStrings(filter.operatorRoles, "Backup participant operator-role filter", true);
144
+ const lifecycleEligibility = filter.lifecycleEligibility === undefined
145
+ ? []
146
+ : [...filter.lifecycleEligibility];
147
+ if (lifecycleEligibility.length > 2 ||
148
+ new Set(lifecycleEligibility).size !== lifecycleEligibility.length ||
149
+ lifecycleEligibility.some((value) => value !== "ready" && value !== "not-ready")) {
150
+ throw new RaybaseBackupError("BACKUP_PROVIDER_ENVELOPE_INVALID", "Backup participant lifecycle filter is invalid");
151
+ }
152
+ const where = [`COALESCE("banned", 0) = 0`];
153
+ const parameters = [provider.id, lifecycleVersion, purpose];
154
+ if (afterUserId) {
155
+ where.push(`"userId" > ?`);
156
+ parameters.push(afterUserId);
157
+ }
158
+ for (const role of allRoles) {
159
+ if (role === "user")
160
+ continue;
161
+ where.push(`EXISTS (SELECT 1 FROM json_each(COALESCE("appRoles", '["user"]')) WHERE value = ?)`);
162
+ parameters.push(role);
163
+ }
164
+ if (anyRoles.length && !anyRoles.includes("user")) {
165
+ where.push(`EXISTS (SELECT 1 FROM json_each(COALESCE("appRoles", '["user"]')) WHERE value IN (${anyRoles.map(() => "?").join(", ")}))`);
166
+ parameters.push(...anyRoles);
167
+ }
168
+ for (const role of noRoles) {
169
+ if (role === "user") {
170
+ where.push("0 = 1");
171
+ continue;
172
+ }
173
+ where.push(`NOT EXISTS (SELECT 1 FROM json_each(COALESCE("appRoles", '["user"]')) WHERE value = ?)`);
174
+ parameters.push(role);
175
+ }
176
+ if (operatorRoles.length) {
177
+ const nonNull = operatorRoles.filter((value) => value !== null);
178
+ const choices = [];
179
+ if (nonNull.length) {
180
+ choices.push(`"operatorRole" IN (${nonNull.map(() => "?").join(", ")})`);
181
+ parameters.push(...nonNull);
182
+ }
183
+ if (operatorRoles.includes(null))
184
+ choices.push(`"operatorRole" IS NULL`);
185
+ where.push(`(${choices.join(" OR ")})`);
186
+ }
187
+ if (lifecycleEligibility.length) {
188
+ where.push(`"lifecycleEligibility" IN (${lifecycleEligibility.map(() => "?").join(", ")})`);
189
+ parameters.push(...lifecycleEligibility);
190
+ }
191
+ parameters.push(limit);
192
+ const rows = await raybaseAuthExecutor(env).all(`WITH "participantDirectory" AS (
193
+ SELECT u."id" AS "userId", u."appRoles",
194
+ CASE WHEN u."operatorRole" IS NULL OR u."operatorRole" IN ('', 'none')
195
+ THEN NULL ELSE u."operatorRole" END AS "operatorRole",
196
+ u."banned",
197
+ CASE WHEN s."providerId" = ? AND s."providerVersion" = ?
198
+ AND s."lastFailureCode" IS NULL
199
+ AND s."readiness" IN ('ready', 'reset-destructive')
200
+ THEN 'ready' ELSE 'not-ready' END AS "lifecycleEligibility"
201
+ FROM "_auth_users" u
202
+ LEFT JOIN "_auth_password_secret_state" s
203
+ ON s."userId" = u."id" AND s."purpose" = ?
204
+ -- Host users (RaybaseHostAuth) have no password and hold no roles.
205
+ WHERE u."id" NOT LIKE 'hostuser\\_%' ESCAPE '\\'
206
+ )
207
+ SELECT "userId", "appRoles", "operatorRole", "lifecycleEligibility"
208
+ FROM "participantDirectory"
209
+ WHERE ${where.join(" AND ")}
210
+ ORDER BY "userId" ASC
211
+ LIMIT ?`, parameters);
212
+ return Object.freeze(rows.map((row) => Object.freeze({
213
+ userId: row.userId,
214
+ appRoles: Object.freeze(parseStoredAppRoles(row.appRoles)),
215
+ operatorRole: row.operatorRole,
216
+ lifecycleEligibility: row.lifecycleEligibility
217
+ })));
218
+ }
219
+ });
220
+ }
221
+ async function requiredPasswordParticipantUserIds(env, provider, context) {
222
+ const lifecycle = provider.passwordSecrets;
223
+ if (!lifecycle)
224
+ return null;
225
+ if (lifecycle.resolveRequiredParticipants && lifecycle.requiredParticipantUserIds) {
226
+ throw new RaybaseBackupError("BACKUP_PROVIDER_UNAVAILABLE", "The password-coupled backup provider declares conflicting participant resolvers");
227
+ }
228
+ if (!lifecycle.resolveRequiredParticipants && !lifecycle.requiredParticipantUserIds) {
229
+ throw new RaybaseBackupError("BACKUP_THRESHOLD_UNMET", "The password-coupled backup provider does not declare its required participants");
230
+ }
231
+ let raw;
232
+ try {
233
+ raw = lifecycle.resolveRequiredParticipants
234
+ ? await lifecycle.resolveRequiredParticipants(Object.freeze({
235
+ context,
236
+ directory: createPasswordParticipantDirectory(env, provider)
237
+ }))
238
+ : await lifecycle.requiredParticipantUserIds(context);
239
+ }
240
+ catch {
241
+ throw new RaybaseBackupError("BACKUP_PROVIDER_UNAVAILABLE", "The backup key provider could not resolve required password-secret participants");
242
+ }
243
+ if (!Array.isArray(raw) || raw.length < 1 || raw.length > backupProviderLimits.wrappedKeyRecords) {
244
+ throw new RaybaseBackupError("BACKUP_THRESHOLD_UNMET", `Password-coupled backup providers must declare 1-${backupProviderLimits.wrappedKeyRecords} participants`);
245
+ }
246
+ const ids = raw.map((value) => typeof value === "string" ? value.trim() : "");
247
+ if (ids.some((value) => !value || new TextEncoder().encode(value).byteLength > 256) || new Set(ids).size !== ids.length) {
248
+ throw new RaybaseBackupError("BACKUP_THRESHOLD_UNMET", "The password-coupled backup provider returned invalid or duplicate participant ids");
249
+ }
250
+ return ids;
251
+ }
252
+ async function resolvePasswordParticipantsForWrap(env, provider, context) {
253
+ if (!provider.passwordSecrets)
254
+ return { participants: Object.freeze([]), release() { } };
255
+ const ids = await requiredPasswordParticipantUserIds(env, provider, context);
256
+ if (!ids)
257
+ return { participants: Object.freeze([]), release() { } };
258
+ if (provider.mode !== "wrapping") {
259
+ await assertRaybasePasswordSecretParticipantsReady(raybaseAuthExecutor(env), { providerId: provider.id, lifecycle: provider.passwordSecrets }, ids);
260
+ return { participants: Object.freeze([]), release() { } };
261
+ }
262
+ return createRaybasePasswordWrapParticipants(raybaseAuthExecutor(env), { providerId: provider.id, lifecycle: provider.passwordSecrets }, ids);
263
+ }
264
+ async function passwordParticipantReadiness(env, provider, context, assertReady) {
265
+ if (!provider.passwordSecrets)
266
+ return undefined;
267
+ let ids;
268
+ try {
269
+ ids = (await requiredPasswordParticipantUserIds(env, provider, context)) ?? [];
270
+ }
271
+ catch (error) {
272
+ if (assertReady)
273
+ throw error;
274
+ return {
275
+ declared: false,
276
+ readyForNewBackups: false,
277
+ required: null,
278
+ ready: null,
279
+ statuses: {},
280
+ reason: error instanceof RaybaseBackupError
281
+ ? error.message
282
+ : "Password-secret participant readiness could not be determined."
283
+ };
284
+ }
285
+ const registration = { providerId: provider.id, lifecycle: provider.passwordSecrets };
286
+ let readiness;
287
+ try {
288
+ readiness = assertReady
289
+ ? await assertRaybasePasswordSecretParticipantsReady(raybaseAuthExecutor(env), registration, ids)
290
+ : await listRaybasePasswordSecretReadiness(raybaseAuthExecutor(env), registration, ids);
291
+ }
292
+ catch (error) {
293
+ if (assertReady) {
294
+ if (error instanceof RaybaseBackupError)
295
+ throw error;
296
+ throw new RaybaseBackupError("BACKUP_PROVIDER_UNAVAILABLE", "Password-secret participant readiness could not be determined");
297
+ }
298
+ return {
299
+ declared: true,
300
+ readyForNewBackups: false,
301
+ required: ids.length,
302
+ ready: null,
303
+ statuses: {},
304
+ reason: "Password-secret participant readiness could not be determined."
305
+ };
306
+ }
307
+ const byUser = new Map(readiness.map((item) => [item.userId, item]));
308
+ const statuses = {};
309
+ let ready = 0;
310
+ for (const id of ids) {
311
+ const item = byUser.get(id);
312
+ const status = item?.status ?? "missing";
313
+ statuses[status] = (statuses[status] ?? 0) + 1;
314
+ if (item?.readyForNewBackups)
315
+ ready += 1;
316
+ }
317
+ return {
318
+ declared: true,
319
+ readyForNewBackups: ready === ids.length,
320
+ required: ids.length,
321
+ ready,
322
+ statuses,
323
+ ...(ready === ids.length
324
+ ? {}
325
+ : { reason: `Password-secret participants are not ready (${ids.length - ready} unavailable).` })
326
+ };
327
+ }
328
+ export async function readCoreBackupRestoreRuntimeStatus(env) {
329
+ const unavailable = pitrUnavailableReason(env);
330
+ const maintenance = env.RAYBASE_DATA ? await dataStub(env).backupRestoreStatus() : null;
331
+ return {
332
+ pitrAvailable: unavailable === null,
333
+ ...(unavailable ? { pitrUnavailableReason: unavailable } : {}),
334
+ maintenance
335
+ };
336
+ }
337
+ export async function prepareCoreBackupRestore(env, options) {
338
+ const runtime = await readCoreBackupRestoreRuntimeStatus(env);
339
+ if (runtime.maintenance?.active) {
340
+ if (!runtime.pitrAvailable) {
341
+ throw new RaybaseBackupError("BACKUP_RESTORE_MAINTENANCE_REQUIRED", `Interrupted restore maintenance requires production PITR recovery. ${runtime.pitrUnavailableReason ?? "PITR is unavailable."}`);
342
+ }
343
+ const recovered = await recoverInterruptedBackupRestore({ stub: dataStub(env) });
344
+ if (recovered.recoveryRequested) {
345
+ throw new RaybaseBackupError("BACKUP_RESTORE_MAINTENANCE_REQUIRED", "Interrupted restore rollback was requested. Retry after the database Durable Object restarts.");
346
+ }
347
+ }
348
+ if (options.requirePitr && !runtime.pitrAvailable) {
349
+ throw new RaybaseBackupError("BACKUP_RESTORE_MAINTENANCE_REQUIRED", `Encrypted restore requires production PITR recovery support. ${runtime.pitrUnavailableReason ?? "PITR is unavailable."}`);
350
+ }
351
+ }
352
+ /**
353
+ * Consume exact encrypted archive bytes into legal R2 multipart parts while
354
+ * independently hashing the bytes submitted to the upload session. All
355
+ * non-final parts meet R2's 5 MiB floor and uploads are deliberately
356
+ * sequential, bounding retained memory to one configured part plus one input
357
+ * chunk owned by the upstream stream.
358
+ */
359
+ export async function uploadBackupMultipartStream(upload, stream, options = {}) {
360
+ const partBytes = options.partBytes ?? backupMultipartDefaultPartBytes;
361
+ const maximumParts = options.maximumParts ?? backupMultipartMaximumParts;
362
+ const abortUpload = async () => {
363
+ try {
364
+ await upload.abort();
365
+ }
366
+ catch {
367
+ // Preserve the primary configuration, stream, or upload failure.
368
+ }
369
+ };
370
+ try {
371
+ if (!Number.isSafeInteger(partBytes) ||
372
+ partBytes < backupMultipartMinimumPartBytes ||
373
+ partBytes > backupMultipartMaximumPartBytes) {
374
+ throw new RaybaseBackupError("BACKUP_LIMIT_EXCEEDED", `R2 multipart parts must contain ${backupMultipartMinimumPartBytes}-${backupMultipartMaximumPartBytes} bytes`);
375
+ }
376
+ if (partBytes > backupMultipartMaximumBufferedPartBytes) {
377
+ throw new RaybaseBackupError("BACKUP_LIMIT_EXCEEDED", `Core bounds in-memory backup parts to ${backupMultipartMaximumBufferedPartBytes} bytes`);
378
+ }
379
+ if (!Number.isInteger(maximumParts) ||
380
+ maximumParts < 1 ||
381
+ maximumParts > backupMultipartMaximumParts) {
382
+ throw new RaybaseBackupError("BACKUP_LIMIT_EXCEEDED", `Backup multipart uploads support 1-${backupMultipartMaximumParts} parts`);
383
+ }
384
+ }
385
+ catch (error) {
386
+ await abortUpload();
387
+ throw error;
388
+ }
389
+ let digest;
390
+ let reader;
391
+ try {
392
+ digest = (options.digestFactory ?? createBackupSha256Digest)();
393
+ reader = stream.getReader();
394
+ }
395
+ catch (error) {
396
+ await abortUpload();
397
+ throw error;
398
+ }
399
+ const uploadedParts = [];
400
+ let pending;
401
+ try {
402
+ pending = new Uint8Array(partBytes);
403
+ }
404
+ catch (error) {
405
+ reader.releaseLock();
406
+ await digest.abort(error);
407
+ await abortUpload();
408
+ throw error;
409
+ }
410
+ let pendingLength = 0;
411
+ let bytes = 0;
412
+ let completed = false;
413
+ const uploadPart = async (part) => {
414
+ if (uploadedParts.length >= maximumParts) {
415
+ throw new RaybaseBackupError("BACKUP_LIMIT_EXCEEDED", "Backup exceeds the R2 multipart part-count limit");
416
+ }
417
+ if (!Number.isSafeInteger(bytes + part.byteLength)) {
418
+ throw new RaybaseBackupError("BACKUP_LIMIT_EXCEEDED", "Backup exceeds the safe byte-count limit");
419
+ }
420
+ if (bytes + part.byteLength > backupMultipartMaximumObjectBytes) {
421
+ throw new RaybaseBackupError("BACKUP_LIMIT_EXCEEDED", "Backup exceeds the R2 object-size limit");
422
+ }
423
+ await digest.write(part);
424
+ const partNumber = uploadedParts.length + 1;
425
+ const uploaded = await upload.uploadPart(partNumber, part);
426
+ if (!uploaded ||
427
+ uploaded.partNumber !== partNumber ||
428
+ typeof uploaded.etag !== "string" ||
429
+ uploaded.etag.length === 0) {
430
+ throw new RaybaseBackupError("BACKUP_ARCHIVE_CORRUPT", "R2 returned invalid multipart upload evidence");
431
+ }
432
+ uploadedParts.push({ partNumber, etag: uploaded.etag });
433
+ bytes += part.byteLength;
434
+ };
435
+ try {
436
+ while (true) {
437
+ const result = await reader.read();
438
+ if (result.done)
439
+ break;
440
+ const chunk = result.value;
441
+ if (!(chunk instanceof Uint8Array)) {
442
+ throw new RaybaseBackupError("BACKUP_ARCHIVE_CORRUPT", "Backup upload stream must emit Uint8Array chunks");
443
+ }
444
+ let offset = 0;
445
+ while (offset < chunk.byteLength) {
446
+ const take = Math.min(partBytes - pendingLength, chunk.byteLength - offset);
447
+ pending.set(chunk.subarray(offset, offset + take), pendingLength);
448
+ pendingLength += take;
449
+ offset += take;
450
+ if (pendingLength === partBytes) {
451
+ const part = pending;
452
+ try {
453
+ await uploadPart(part);
454
+ }
455
+ finally {
456
+ part.fill(0);
457
+ }
458
+ pending = new Uint8Array(partBytes);
459
+ pendingLength = 0;
460
+ }
461
+ }
462
+ }
463
+ if (pendingLength > 0) {
464
+ const part = pending.slice(0, pendingLength);
465
+ pending.fill(0);
466
+ try {
467
+ await uploadPart(part);
468
+ }
469
+ finally {
470
+ part.fill(0);
471
+ }
472
+ pendingLength = 0;
473
+ }
474
+ if (uploadedParts.length === 0) {
475
+ throw new RaybaseBackupError("BACKUP_ARCHIVE_TRUNCATED", "Encrypted backup archive emitted no bytes");
476
+ }
477
+ const sha256Bytes = await digest.finish();
478
+ const sha256 = backupBytesToHex(sha256Bytes);
479
+ sha256Bytes.fill(0);
480
+ const object = await upload.complete(uploadedParts);
481
+ completed = true;
482
+ return {
483
+ object,
484
+ uploadedParts,
485
+ partCount: uploadedParts.length,
486
+ bytes,
487
+ sha256
488
+ };
489
+ }
490
+ catch (error) {
491
+ pending.fill(0);
492
+ pendingLength = 0;
493
+ try {
494
+ await reader.cancel(error);
495
+ }
496
+ catch {
497
+ // Preserve the primary upload/encryption failure.
498
+ }
499
+ await digest.abort(error);
500
+ await abortUpload();
501
+ throw error;
502
+ }
503
+ finally {
504
+ reader.releaseLock();
505
+ pending.fill(0);
506
+ if (!completed)
507
+ pendingLength = 0;
508
+ }
509
+ }
510
+ export async function uploadBackupArchiveMultipart(bucket, key, stream, options = {}) {
511
+ const upload = await bucket.createMultipartUpload(key, options.r2);
512
+ return uploadBackupMultipartStream(upload, stream, options);
513
+ }
514
+ export function backupTimestamp(date = new Date()) {
515
+ return `${date.toISOString().slice(0, 19).replace(/:/g, "-")}Z`;
516
+ }
517
+ export function backupMetadataComment(metadata) {
518
+ return `-- raybase-backup: ${JSON.stringify(metadata)}`;
519
+ }
520
+ export function prependBackupMetadata(sql, metadata) {
521
+ return `${backupMetadataComment(metadata)}\n${sql}`;
522
+ }
523
+ export function parseBackupMetadata(sql) {
524
+ const firstLine = sql.split(/\r?\n/, 1)[0]?.trim() ?? "";
525
+ const match = firstLine.match(/^--\s*raybase-backup:\s*(\{.*\})$/);
526
+ if (!match)
527
+ return null;
528
+ try {
529
+ const parsed = JSON.parse(match[1]);
530
+ if (parsed.format !== 1)
531
+ return null;
532
+ if (parsed.product !== "core" && parsed.product !== "pro")
533
+ return null;
534
+ if (parsed.target !== "core" && parsed.target !== "control-plane" && parsed.target !== "app-data")
535
+ return null;
536
+ if (typeof parsed.createdAt !== "string" || !parsed.createdAt)
537
+ return null;
538
+ return {
539
+ format: 1,
540
+ product: parsed.product,
541
+ target: parsed.target,
542
+ createdAt: parsed.createdAt,
543
+ ...(typeof parsed.organizationId === "string" ? { organizationId: parsed.organizationId } : {})
544
+ };
545
+ }
546
+ catch {
547
+ return null;
548
+ }
549
+ }
550
+ export function validateCoreBackupSql(sql) {
551
+ const metadata = parseBackupMetadata(sql);
552
+ if (!metadata)
553
+ return;
554
+ if (metadata.product !== "core" || metadata.target !== "core") {
555
+ throw new Error(`Backup SQL targets ${metadata.product}/${metadata.target}; expected core/core`);
556
+ }
557
+ }
558
+ export function validateCoreBackupKey(key) {
559
+ const normalized = key.trim();
560
+ if (!normalized.startsWith(coreBackupPrefix) ||
561
+ (!normalized.endsWith(".sql") && !normalized.endsWith(".rbb")) ||
562
+ normalized.includes("..")) {
563
+ throw new Error("Backup key is outside the Core backup prefix");
564
+ }
565
+ return normalized;
566
+ }
567
+ function manifestObject(value) {
568
+ if (!value || typeof value !== "object" || Array.isArray(value))
569
+ return null;
570
+ return value;
571
+ }
572
+ function normalizeManifestCommon(value) {
573
+ const entry = manifestObject(value);
574
+ if (!entry)
575
+ return null;
576
+ if (typeof entry.key !== "string" ||
577
+ !entry.key ||
578
+ entry.key.includes("..") ||
579
+ typeof entry.date !== "string" ||
580
+ !Number.isFinite(Date.parse(entry.date)) ||
581
+ typeof entry.sha256 !== "string" ||
582
+ !/^[a-f0-9]{64}$/.test(entry.sha256) ||
583
+ typeof entry.bytes !== "number" ||
584
+ !Number.isSafeInteger(entry.bytes) ||
585
+ entry.bytes <= 0 ||
586
+ (entry.target !== "core" && entry.target !== "control-plane" && entry.target !== "app-data") ||
587
+ (entry.product !== "core" && entry.product !== "pro") ||
588
+ (entry.trigger !== "scheduled" && entry.trigger !== "manual")) {
589
+ return null;
590
+ }
591
+ return {
592
+ key: entry.key,
593
+ date: entry.date,
594
+ sha256: entry.sha256,
595
+ bytes: entry.bytes,
596
+ target: entry.target,
597
+ product: entry.product,
598
+ ...(typeof entry.organizationId === "string" ? { organizationId: entry.organizationId } : {}),
599
+ ...(typeof entry.organizationSlug === "string" ? { organizationSlug: entry.organizationSlug } : {}),
600
+ ...(typeof entry.actorId === "string" || entry.actorId === null ? { actorId: entry.actorId } : {}),
601
+ trigger: entry.trigger
602
+ };
603
+ }
604
+ /**
605
+ * Normalize both historical version-1 entries and version-2 entries into the
606
+ * discriminated archive model. Version-1 data gains only explicit routing
607
+ * facts (`archiveFormat`, `encrypted`, and `contentType`), never invented key
608
+ * or encryption metadata.
609
+ */
610
+ export function normalizeBackupManifestEntry(value) {
611
+ const common = normalizeManifestCommon(value);
612
+ const entry = manifestObject(value);
613
+ if (!common || !entry)
614
+ return null;
615
+ if (entry.archiveFormat === 2 || entry.encrypted === true) {
616
+ if (entry.archiveFormat !== 2 ||
617
+ entry.encrypted !== true ||
618
+ entry.contentType !== backupBinaryContentType ||
619
+ typeof entry.archiveId !== "string" ||
620
+ typeof entry.targetId !== "string" ||
621
+ typeof entry.providerId !== "string" ||
622
+ typeof entry.kekKeyId !== "string") {
623
+ return null;
624
+ }
625
+ try {
626
+ const header = decodeBackupArchiveHeader(encodeBackupArchiveHeader({
627
+ format: 2,
628
+ archiveId: entry.archiveId,
629
+ product: common.product,
630
+ target: common.target,
631
+ targetId: entry.targetId,
632
+ createdAt: common.date,
633
+ logical: entry.logical,
634
+ encryption: entry.encryption,
635
+ keyEnvelope: entry.keyEnvelope
636
+ })).header;
637
+ if (header.keyEnvelope.providerId !== entry.providerId ||
638
+ header.keyEnvelope.kekKeyId !== entry.kekKeyId) {
639
+ return null;
640
+ }
641
+ return {
642
+ ...common,
643
+ archiveFormat: 2,
644
+ archiveId: entry.archiveId,
645
+ targetId: entry.targetId,
646
+ encrypted: true,
647
+ contentType: backupBinaryContentType,
648
+ encryption: header.encryption,
649
+ logical: header.logical,
650
+ providerId: entry.providerId,
651
+ kekKeyId: entry.kekKeyId,
652
+ keyEnvelope: header.keyEnvelope
653
+ };
654
+ }
655
+ catch {
656
+ return null;
657
+ }
658
+ }
659
+ if ((entry.archiveFormat !== undefined && entry.archiveFormat !== 1) ||
660
+ (entry.encrypted !== undefined && entry.encrypted !== false) ||
661
+ (entry.contentType !== undefined && entry.contentType !== backupSqlContentType)) {
662
+ return null;
663
+ }
664
+ return {
665
+ ...common,
666
+ archiveFormat: 1,
667
+ encrypted: false,
668
+ contentType: backupSqlContentType
669
+ };
670
+ }
671
+ function equalBytes(left, right) {
672
+ return left.byteLength === right.byteLength && left.every((value, index) => value === right[index]);
673
+ }
674
+ export function assertBackupManifestIdentity(entry, header) {
675
+ if (entry.archiveId !== header.archiveId ||
676
+ entry.product !== header.product ||
677
+ entry.target !== header.target ||
678
+ entry.targetId !== header.targetId ||
679
+ entry.date !== header.createdAt ||
680
+ entry.providerId !== header.keyEnvelope.providerId ||
681
+ entry.kekKeyId !== header.keyEnvelope.kekKeyId ||
682
+ !equalBytes(canonicalBackupJson({
683
+ encryption: entry.encryption,
684
+ logical: entry.logical,
685
+ keyEnvelope: entry.keyEnvelope
686
+ }), canonicalBackupJson({
687
+ encryption: header.encryption,
688
+ logical: header.logical,
689
+ keyEnvelope: header.keyEnvelope
690
+ }))) {
691
+ throw new RaybaseBackupError("BACKUP_ARCHIVE_IDENTITY_MISMATCH", "Backup archive header does not match its manifest entry");
692
+ }
693
+ }
694
+ async function readJsonObject(bucket, key) {
695
+ const object = await bucket.get(key);
696
+ if (!object)
697
+ return { value: null, found: false };
698
+ try {
699
+ return { value: (await object.json()), found: true };
700
+ }
701
+ catch (error) {
702
+ return {
703
+ value: null,
704
+ found: true,
705
+ warning: `Could not parse ${key}: ${error instanceof Error ? error.message : String(error)}`
706
+ };
707
+ }
708
+ }
709
+ async function writeJsonObject(bucket, key, value) {
710
+ await bucket.put(key, `${JSON.stringify(value, null, 2)}\n`, {
711
+ httpMetadata: { contentType: "application/json; charset=utf-8" }
712
+ });
713
+ }
714
+ function emptyTargetManifest(descriptor, updatedAt) {
715
+ return {
716
+ version: 2,
717
+ updatedAt,
718
+ product: descriptor.product,
719
+ target: descriptor.target,
720
+ ...(descriptor.organizationId ? { organizationId: descriptor.organizationId } : {}),
721
+ ...(descriptor.organizationSlug ? { organizationSlug: descriptor.organizationSlug } : {}),
722
+ sourceOfTruth: "archive-objects",
723
+ entries: []
724
+ };
725
+ }
726
+ async function readTargetManifest(bucket, descriptor, now) {
727
+ const read = await readJsonObject(bucket, descriptor.manifestKey);
728
+ if (!read.value) {
729
+ return {
730
+ manifest: emptyTargetManifest(descriptor, now),
731
+ needsRepair: true,
732
+ ...(read.warning ? { warning: read.warning } : {})
733
+ };
734
+ }
735
+ const manifest = manifestObject(read.value);
736
+ const rawEntries = Array.isArray(manifest?.entries) ? manifest.entries : [];
737
+ const entries = rawEntries
738
+ .map(normalizeBackupManifestEntry)
739
+ .filter((entry) => Boolean(entry));
740
+ const structurallyValid = Boolean(manifest &&
741
+ (manifest.version === 1 || manifest.version === 2) &&
742
+ manifest.product === descriptor.product &&
743
+ manifest.target === descriptor.target &&
744
+ (manifest.organizationId ?? "") === (descriptor.organizationId ?? "") &&
745
+ Array.isArray(manifest.entries));
746
+ const invalidEntries = rawEntries.length - entries.length;
747
+ const warning = [
748
+ read.warning,
749
+ !structurallyValid ? `${descriptor.manifestKey} has invalid target-manifest metadata.` : undefined,
750
+ invalidEntries > 0 ? `${descriptor.manifestKey} contains ${invalidEntries} invalid ${invalidEntries === 1 ? "entry" : "entries"}.` : undefined
751
+ ].filter(Boolean).join(" ") || undefined;
752
+ return {
753
+ manifest: {
754
+ version: 2,
755
+ updatedAt: typeof manifest?.updatedAt === "string" && Number.isFinite(Date.parse(manifest.updatedAt)) ? manifest.updatedAt : now,
756
+ product: descriptor.product,
757
+ target: descriptor.target,
758
+ ...(descriptor.organizationId ? { organizationId: descriptor.organizationId } : {}),
759
+ ...(descriptor.organizationSlug ? { organizationSlug: descriptor.organizationSlug } : {}),
760
+ sourceOfTruth: "archive-objects",
761
+ entries
762
+ },
763
+ needsRepair: !read.found || !structurallyValid || invalidEntries > 0 || manifest?.version !== 2 || manifest?.sourceOfTruth !== "archive-objects",
764
+ ...(warning ? { warning } : {})
765
+ };
766
+ }
767
+ function emptyRootManifest(updatedAt) {
768
+ return { version: 2, updatedAt, sourceOfTruth: "archive-objects", lastSuccessfulFullRunAt: null, partialRun: null, targets: [] };
769
+ }
770
+ function normalizeRootTarget(value) {
771
+ const target = manifestObject(value);
772
+ if (!target)
773
+ return null;
774
+ if ((target.product !== "core" && target.product !== "pro") ||
775
+ (target.target !== "core" && target.target !== "control-plane" && target.target !== "app-data") ||
776
+ typeof target.manifestKey !== "string" ||
777
+ !target.manifestKey.startsWith("backups/") ||
778
+ !target.manifestKey.endsWith("/manifest.json") ||
779
+ target.manifestKey.includes("..") ||
780
+ (target.latestBackupAt !== undefined && target.latestBackupAt !== null &&
781
+ (typeof target.latestBackupAt !== "string" || !Number.isFinite(Date.parse(target.latestBackupAt))))) {
782
+ return null;
783
+ }
784
+ return {
785
+ product: target.product,
786
+ target: target.target,
787
+ ...(typeof target.organizationId === "string" ? { organizationId: target.organizationId } : {}),
788
+ ...(typeof target.organizationSlug === "string" ? { organizationSlug: target.organizationSlug } : {}),
789
+ manifestKey: target.manifestKey,
790
+ ...(target.latestBackupAt === null || typeof target.latestBackupAt === "string"
791
+ ? { latestBackupAt: target.latestBackupAt }
792
+ : {})
793
+ };
794
+ }
795
+ function normalizeRootPartialRun(value) {
796
+ if (value === null || value === undefined)
797
+ return null;
798
+ const partial = manifestObject(value);
799
+ if (!partial ||
800
+ typeof partial.runId !== "string" ||
801
+ typeof partial.startedAt !== "string" ||
802
+ !Number.isFinite(Date.parse(partial.startedAt)) ||
803
+ typeof partial.stoppedAt !== "string" ||
804
+ !Number.isFinite(Date.parse(partial.stoppedAt)) ||
805
+ !Number.isSafeInteger(partial.completedTargets) ||
806
+ partial.completedTargets < 0 ||
807
+ !Number.isSafeInteger(partial.remainingTargets) ||
808
+ partial.remainingTargets < 0 ||
809
+ partial.reason !== "wall-clock-budget") {
810
+ return null;
811
+ }
812
+ return {
813
+ runId: partial.runId,
814
+ startedAt: partial.startedAt,
815
+ stoppedAt: partial.stoppedAt,
816
+ completedTargets: partial.completedTargets,
817
+ remainingTargets: partial.remainingTargets,
818
+ reason: "wall-clock-budget"
819
+ };
820
+ }
821
+ async function readRootManifest(bucket, now) {
822
+ const read = await readJsonObject(bucket, backupRootManifestKey);
823
+ if (!read.value) {
824
+ return {
825
+ manifest: emptyRootManifest(now),
826
+ needsRepair: true,
827
+ ...(read.warning ? { warning: read.warning } : {})
828
+ };
829
+ }
830
+ const manifest = manifestObject(read.value);
831
+ const rawTargets = Array.isArray(manifest?.targets) ? manifest.targets : [];
832
+ const targets = rawTargets
833
+ .map(normalizeRootTarget)
834
+ .filter((target) => Boolean(target));
835
+ const partialRun = normalizeRootPartialRun(manifest?.partialRun);
836
+ const structurallyValid = Boolean(manifest &&
837
+ manifest.version === 2 &&
838
+ manifest.sourceOfTruth === "archive-objects" &&
839
+ Array.isArray(manifest.targets) &&
840
+ (manifest.partialRun === null || manifest.partialRun === undefined || partialRun));
841
+ const invalidTargets = rawTargets.length - targets.length;
842
+ const warning = [
843
+ read.warning,
844
+ !structurallyValid ? `${backupRootManifestKey} has invalid root-manifest metadata.` : undefined,
845
+ invalidTargets > 0 ? `${backupRootManifestKey} contains ${invalidTargets} invalid target ${invalidTargets === 1 ? "entry" : "entries"}.` : undefined
846
+ ].filter(Boolean).join(" ") || undefined;
847
+ return {
848
+ manifest: {
849
+ version: 2,
850
+ updatedAt: typeof manifest?.updatedAt === "string" && Number.isFinite(Date.parse(manifest.updatedAt)) ? manifest.updatedAt : now,
851
+ sourceOfTruth: "archive-objects",
852
+ lastSuccessfulFullRunAt: typeof manifest?.lastSuccessfulFullRunAt === "string" && Number.isFinite(Date.parse(manifest.lastSuccessfulFullRunAt))
853
+ ? manifest.lastSuccessfulFullRunAt
854
+ : null,
855
+ partialRun,
856
+ targets
857
+ },
858
+ needsRepair: !read.found || !structurallyValid || invalidTargets > 0,
859
+ ...(warning ? { warning } : {})
860
+ };
861
+ }
862
+ function upsertRootTarget(root, descriptor, latestBackupAt) {
863
+ const targets = root.targets.filter((target) => !(target.product === descriptor.product &&
864
+ target.target === descriptor.target &&
865
+ (target.organizationId ?? "") === (descriptor.organizationId ?? "")));
866
+ targets.push({
867
+ product: descriptor.product,
868
+ target: descriptor.target,
869
+ ...(descriptor.organizationId ? { organizationId: descriptor.organizationId } : {}),
870
+ ...(descriptor.organizationSlug ? { organizationSlug: descriptor.organizationSlug } : {}),
871
+ manifestKey: descriptor.manifestKey,
872
+ latestBackupAt
873
+ });
874
+ targets.sort((left, right) => `${left.target}:${left.organizationId ?? ""}`.localeCompare(`${right.target}:${right.organizationId ?? ""}`));
875
+ return { ...root, targets };
876
+ }
877
+ function verifyBackupObject(entry, object) {
878
+ if (!object)
879
+ throw new Error(`Backup R2 object was not found after upload: ${entry.key}`);
880
+ if (object.httpMetadata?.contentType !== entry.contentType) {
881
+ throw new Error(`Backup R2 object content type mismatch for ${entry.key}`);
882
+ }
883
+ const metadata = object.customMetadata ?? {};
884
+ const expected = {
885
+ "raybase-product": entry.product,
886
+ "raybase-target": entry.target,
887
+ "raybase-backup-date": entry.date,
888
+ "raybase-archive-format": String(entry.archiveFormat),
889
+ "raybase-encrypted": String(entry.encrypted),
890
+ "raybase-trigger": entry.trigger
891
+ };
892
+ if (entry.archiveFormat === 2) {
893
+ expected["raybase-archive-id"] = entry.archiveId;
894
+ expected["raybase-provider-id"] = entry.providerId;
895
+ expected["raybase-kek-key-id"] = entry.kekKeyId;
896
+ }
897
+ if (entry.organizationId)
898
+ expected["raybase-organization-id"] = entry.organizationId;
899
+ for (const [key, value] of Object.entries(expected)) {
900
+ if (metadata[key] !== value)
901
+ throw new Error(`Backup R2 object metadata mismatch for ${key}`);
902
+ }
903
+ if (typeof object.size === "number" && object.size !== entry.bytes) {
904
+ throw new Error(`Backup R2 object size mismatch for ${entry.key}`);
905
+ }
906
+ }
907
+ function backupObjectMetadata(entry) {
908
+ return {
909
+ "raybase-product": entry.product,
910
+ "raybase-target": entry.target,
911
+ "raybase-backup-date": entry.date,
912
+ "raybase-archive-format": String(entry.archiveFormat),
913
+ "raybase-encrypted": String(entry.encrypted),
914
+ "raybase-trigger": entry.trigger,
915
+ ...(entry.archiveFormat === 2
916
+ ? {
917
+ "raybase-archive-id": entry.archiveId,
918
+ "raybase-provider-id": entry.providerId,
919
+ "raybase-kek-key-id": entry.kekKeyId
920
+ }
921
+ : {})
922
+ };
923
+ }
924
+ function sameBytes(left, right) {
925
+ return left.byteLength === right.byteLength && left.every((byte, index) => byte === right[index]);
926
+ }
927
+ function deferredValue() {
928
+ let resolve;
929
+ let reject;
930
+ const promise = new Promise((resolvePromise, rejectPromise) => {
931
+ resolve = resolvePromise;
932
+ reject = rejectPromise;
933
+ });
934
+ return { promise, resolve, reject };
935
+ }
936
+ function snapshotLogicalStream(stub, operationId, consistency) {
937
+ const totals = deferredValue();
938
+ // The encryptor subscribes when it emits its terminal frame. A snapshot can
939
+ // fail earlier, however, so register a rejection observer immediately while
940
+ // preserving the rejected promise for that later await.
941
+ void totals.promise.catch(() => undefined);
942
+ const decoder = new BackupLogicalDecoder();
943
+ const digest = createBackupSha256Digest();
944
+ const mode = consistency?.mode === "write-gated" ? "write-gated" : "optimistic";
945
+ const descriptorPromise = stub.beginBackupSnapshot({ operationId, mode });
946
+ let descriptor = null;
947
+ let cursor = null;
948
+ let buffered = [];
949
+ let recordCount = 0;
950
+ let rowCount = 0;
951
+ let tableCount = 0;
952
+ let completed = false;
953
+ const acceptLogicalBytes = async (chunk) => {
954
+ await digest.write(chunk);
955
+ for (const record of decoder.push(chunk)) {
956
+ recordCount += 1;
957
+ if (record.kind === "row")
958
+ rowCount += 1;
959
+ if (record.kind === "table-end")
960
+ tableCount += 1;
961
+ }
962
+ };
963
+ const cancel = async (reason) => {
964
+ if (completed)
965
+ return;
966
+ completed = true;
967
+ await digest.abort(reason);
968
+ await stub.cancelBackupSnapshot({ operationId }).catch(() => undefined);
969
+ totals.reject(reason);
970
+ };
971
+ return {
972
+ totals: totals.promise,
973
+ stream: new ReadableStream({
974
+ async pull(controller) {
975
+ try {
976
+ descriptor ??= await descriptorPromise;
977
+ cursor ??= descriptor.cursor;
978
+ if (buffered.length > 0) {
979
+ controller.enqueue(buffered.shift());
980
+ if (completed && buffered.length === 0)
981
+ controller.close();
982
+ return;
983
+ }
984
+ if (completed) {
985
+ controller.close();
986
+ return;
987
+ }
988
+ if (descriptor.recommendedMode === "small") {
989
+ const small = await stub.readBackupSnapshotSmall({
990
+ operationId,
991
+ epoch: descriptor.epoch,
992
+ generation: descriptor.generation,
993
+ schemaFingerprint: descriptor.schemaFingerprint,
994
+ maxBytes: backupDatabaseProtocolLimits.smallSnapshotLogicalBytes
995
+ });
996
+ const expectedEnd = await stub.finishBackupSnapshot({
997
+ operationId,
998
+ epoch: descriptor.epoch,
999
+ generation: descriptor.generation,
1000
+ schemaFingerprint: descriptor.schemaFingerprint,
1001
+ recordCount: small.recordCount,
1002
+ rowCount: small.rowCount,
1003
+ tableCount: small.tableCount,
1004
+ plaintextSha256: small.plaintextSha256
1005
+ });
1006
+ const actualEnd = small.records.at(-1);
1007
+ if (!actualEnd || !sameBytes(actualEnd, expectedEnd.record)) {
1008
+ throw new RaybaseBackupError("BACKUP_ARCHIVE_CORRUPT", "Small snapshot logical end is inconsistent");
1009
+ }
1010
+ for (const record of small.records.slice(0, -1))
1011
+ await acceptLogicalBytes(record);
1012
+ const digestBytes = await digest.finish();
1013
+ const plaintextSha256 = backupBytesToHex(digestBytes);
1014
+ digestBytes.fill(0);
1015
+ if (plaintextSha256 !== small.plaintextSha256 ||
1016
+ recordCount !== small.recordCount ||
1017
+ rowCount !== small.rowCount ||
1018
+ tableCount !== small.tableCount) {
1019
+ throw new RaybaseBackupError("BACKUP_ARCHIVE_CORRUPT", "Small snapshot totals are inconsistent");
1020
+ }
1021
+ decoder.push(actualEnd);
1022
+ decoder.finish();
1023
+ buffered = small.records;
1024
+ completed = true;
1025
+ totals.resolve({ recordCount, rowCount, tableCount });
1026
+ controller.enqueue(buffered.shift());
1027
+ return;
1028
+ }
1029
+ const page = await stub.readBackupSnapshotPage({
1030
+ operationId,
1031
+ epoch: descriptor.epoch,
1032
+ generation: descriptor.generation,
1033
+ schemaFingerprint: descriptor.schemaFingerprint,
1034
+ cursor,
1035
+ maxBytes: backupDatabaseProtocolLimits.defaultPageBytes,
1036
+ maxRows: backupDatabaseProtocolLimits.defaultPageRows
1037
+ });
1038
+ cursor = page.nextCursor;
1039
+ for (const chunk of page.chunks)
1040
+ await acceptLogicalBytes(chunk);
1041
+ buffered = page.chunks;
1042
+ if (page.done) {
1043
+ const digestBytes = await digest.finish();
1044
+ const plaintextSha256 = backupBytesToHex(digestBytes);
1045
+ digestBytes.fill(0);
1046
+ const end = await stub.finishBackupSnapshot({
1047
+ operationId,
1048
+ epoch: descriptor.epoch,
1049
+ generation: descriptor.generation,
1050
+ schemaFingerprint: descriptor.schemaFingerprint,
1051
+ recordCount,
1052
+ rowCount,
1053
+ tableCount,
1054
+ plaintextSha256
1055
+ });
1056
+ decoder.push(end.record);
1057
+ decoder.finish();
1058
+ buffered.push(end.record);
1059
+ completed = true;
1060
+ totals.resolve({ recordCount, rowCount, tableCount });
1061
+ }
1062
+ if (buffered.length > 0)
1063
+ controller.enqueue(buffered.shift());
1064
+ if (completed && buffered.length === 0)
1065
+ controller.close();
1066
+ }
1067
+ catch (error) {
1068
+ await cancel(error);
1069
+ controller.error(error);
1070
+ }
1071
+ },
1072
+ async cancel(reason) {
1073
+ await cancel(reason ?? new RaybaseBackupError("BACKUP_ARCHIVE_TRUNCATED", "Backup snapshot stream was cancelled"));
1074
+ }
1075
+ })
1076
+ };
1077
+ }
1078
+ function validateCoreArchiveHeader(header) {
1079
+ if (header.product !== "core" || header.target !== "core" || header.targetId !== "core") {
1080
+ throw new RaybaseBackupError("BACKUP_ARCHIVE_IDENTITY_MISMATCH", `Backup targets ${header.product}/${header.target}/${header.targetId}; expected core/core/core`);
1081
+ }
1082
+ }
1083
+ async function pruneEntries(bucket, entries, retention) {
1084
+ const kept = entries.slice(0, retention);
1085
+ const pruned = entries.slice(retention);
1086
+ for (const entry of pruned) {
1087
+ await bucket.delete(entry.key);
1088
+ }
1089
+ return { kept, pruned };
1090
+ }
1091
+ export async function pruneTemporaryBackupObjects(bucket, options = {}) {
1092
+ const now = options.now ?? new Date();
1093
+ const retentionMs = options.retentionMs ?? backupTemporaryRetentionMs;
1094
+ if (!Number.isFinite(retentionMs) || retentionMs < 0) {
1095
+ throw new Error("Temporary backup retention must be a non-negative number of milliseconds");
1096
+ }
1097
+ const cutoff = now.getTime() - retentionMs;
1098
+ const deleted = [];
1099
+ let cursor;
1100
+ do {
1101
+ const page = await bucket.list({
1102
+ prefix: backupTemporaryPrefix,
1103
+ ...(cursor ? { cursor } : {})
1104
+ });
1105
+ const expired = page.objects
1106
+ .filter((object) => object.uploaded.getTime() <= cutoff)
1107
+ .map((object) => object.key);
1108
+ if (expired.length) {
1109
+ await bucket.delete(expired);
1110
+ deleted.push(...expired);
1111
+ }
1112
+ cursor = page.truncated ? page.cursor : undefined;
1113
+ } while (cursor);
1114
+ return deleted;
1115
+ }
1116
+ function coreDescriptor() {
1117
+ return { product: "core", target: "core", manifestKey: coreBackupManifestKey, prefix: coreBackupPrefix };
1118
+ }
1119
+ function archiveDiscoveryWarning(warnings, warning) {
1120
+ if (warnings.length < backupArchiveDiscoveryMaximumWarnings)
1121
+ warnings.push(warning);
1122
+ }
1123
+ function targetArchiveObjectKey(descriptor, key) {
1124
+ if (!key.startsWith(descriptor.prefix))
1125
+ return false;
1126
+ const basename = key.slice(descriptor.prefix.length);
1127
+ return Boolean(basename) && !basename.includes("/") && (basename.endsWith(".rbb") || basename.endsWith(".sql"));
1128
+ }
1129
+ function expectedEncryptedArchiveKey(descriptor, identity) {
1130
+ return `${descriptor.prefix}${backupTimestamp(new Date(identity.createdAt))}-${identity.archiveId}.rbb`;
1131
+ }
1132
+ function entryMatchesDescriptor(entry, descriptor) {
1133
+ if (entry.product !== descriptor.product ||
1134
+ entry.target !== descriptor.target ||
1135
+ (entry.organizationId ?? "") !== (descriptor.organizationId ?? "") ||
1136
+ !targetArchiveObjectKey(descriptor, entry.key)) {
1137
+ return false;
1138
+ }
1139
+ if (entry.archiveFormat === 1)
1140
+ return entry.key.endsWith(".sql");
1141
+ return entry.targetId === (descriptor.organizationId ?? descriptor.target) &&
1142
+ entry.key === expectedEncryptedArchiveKey(descriptor, { archiveId: entry.archiveId, createdAt: entry.date });
1143
+ }
1144
+ async function listTargetArchiveObjects(bucket, descriptor) {
1145
+ const objects = [];
1146
+ let cursor;
1147
+ do {
1148
+ const page = await bucket.list({
1149
+ prefix: descriptor.prefix,
1150
+ limit: 1_000,
1151
+ ...(cursor ? { cursor } : {})
1152
+ });
1153
+ if (objects.length + page.objects.length > backupArchiveDiscoveryMaximumObjects) {
1154
+ throw new RaybaseBackupError("BACKUP_LIMIT_EXCEEDED", `Backup archive discovery exceeds ${backupArchiveDiscoveryMaximumObjects} objects under ${descriptor.prefix}`);
1155
+ }
1156
+ objects.push(...page.objects);
1157
+ if (page.truncated && (!page.cursor || page.cursor === cursor)) {
1158
+ throw new RaybaseBackupError("BACKUP_ARCHIVE_CORRUPT", "Backup archive discovery returned an invalid cursor");
1159
+ }
1160
+ cursor = page.truncated ? page.cursor : undefined;
1161
+ } while (cursor);
1162
+ return objects.filter((object) => targetArchiveObjectKey(descriptor, object.key));
1163
+ }
1164
+ function createBackupArchiveStructureValidator() {
1165
+ // Discovery must work even when the historical provider/key is unavailable.
1166
+ // Validate the canonical authenticated-header shape, routing metadata, frame
1167
+ // sequence, terminal frame, and exact ciphertext digest here; restore still
1168
+ // performs cryptographic header/frame authentication before any DB write.
1169
+ let headerBuffer = new Uint8Array(backupArchiveMagic.byteLength + 4);
1170
+ let headerLength = 0;
1171
+ let headerRequired = headerBuffer.byteLength;
1172
+ let header = null;
1173
+ const frameHeader = new Uint8Array(backupFrameHeaderBytes);
1174
+ let frameHeaderLength = 0;
1175
+ let frameRemaining = 0;
1176
+ let frameKind = 0;
1177
+ let sequence = 0;
1178
+ let ended = false;
1179
+ const acceptHeaderBytes = (bytes, offset) => {
1180
+ const take = Math.min(bytes.byteLength - offset, headerRequired - headerLength);
1181
+ headerBuffer.set(bytes.subarray(offset, offset + take), headerLength);
1182
+ headerLength += take;
1183
+ if (headerLength !== headerRequired)
1184
+ return take;
1185
+ if (headerRequired === backupArchiveMagic.byteLength + 4) {
1186
+ for (let index = 0; index < backupArchiveMagic.byteLength; index += 1) {
1187
+ if (headerBuffer[index] !== backupArchiveMagic[index]) {
1188
+ throw new RaybaseBackupError("BACKUP_ARCHIVE_VERSION_UNSUPPORTED", "Backup archive magic is invalid");
1189
+ }
1190
+ }
1191
+ const bodyLength = new DataView(headerBuffer.buffer, headerBuffer.byteOffset + backupArchiveMagic.byteLength, 4).getUint32(0, false);
1192
+ if (bodyLength > backupFormatLimits.headerBytes) {
1193
+ throw new RaybaseBackupError("BACKUP_LIMIT_EXCEEDED", "Backup archive header exceeds its byte limit");
1194
+ }
1195
+ const expanded = new Uint8Array(backupArchiveMagic.byteLength + 4 + bodyLength);
1196
+ expanded.set(headerBuffer);
1197
+ headerBuffer.fill(0);
1198
+ headerBuffer = expanded;
1199
+ headerRequired = expanded.byteLength;
1200
+ if (headerLength !== headerRequired)
1201
+ return take;
1202
+ }
1203
+ const decoded = decodeBackupArchiveHeader(headerBuffer);
1204
+ if (decoded.bytesRead !== headerBuffer.byteLength) {
1205
+ throw new RaybaseBackupError("BACKUP_ARCHIVE_CORRUPT", "Backup archive header has trailing bytes");
1206
+ }
1207
+ header = decoded.header;
1208
+ headerBuffer.fill(0);
1209
+ return take;
1210
+ };
1211
+ const acceptFrameHeader = () => {
1212
+ if (!header)
1213
+ throw new RaybaseBackupError("BACKUP_ARCHIVE_CORRUPT", "Backup frame appeared before its header");
1214
+ const view = new DataView(frameHeader.buffer, frameHeader.byteOffset, frameHeader.byteLength);
1215
+ const version = frameHeader[0];
1216
+ const kind = frameHeader[1];
1217
+ const flags = view.getUint16(2, false);
1218
+ const encodedSequence = view.getUint32(4, false);
1219
+ const plaintextBytes = view.getUint32(8, false);
1220
+ if (version !== 1 || (kind !== backupArchiveDataFrameKind && kind !== backupArchiveFinalFrameKind) || flags !== 0) {
1221
+ throw new RaybaseBackupError("BACKUP_FRAME_VERSION_UNSUPPORTED", "Backup frame header is unsupported");
1222
+ }
1223
+ if (encodedSequence !== sequence) {
1224
+ throw new RaybaseBackupError("BACKUP_ARCHIVE_CORRUPT", "Backup frame sequence is missing, duplicated, or reordered");
1225
+ }
1226
+ if (kind === backupArchiveDataFrameKind && sequence >= 0xffff_ffff) {
1227
+ throw new RaybaseBackupError("BACKUP_LIMIT_EXCEEDED", "Backup frame sequence is exhausted before the final frame");
1228
+ }
1229
+ const maximum = kind === backupArchiveFinalFrameKind
1230
+ ? backupArchiveFinalFrameMaximumBytes
1231
+ : header.encryption.framePlaintextBytes;
1232
+ if (plaintextBytes > maximum) {
1233
+ throw new RaybaseBackupError("BACKUP_LIMIT_EXCEEDED", "Backup frame exceeds its plaintext byte limit");
1234
+ }
1235
+ frameKind = kind;
1236
+ frameRemaining = plaintextBytes + backupFrameTagBytes;
1237
+ frameHeader.fill(0);
1238
+ frameHeaderLength = 0;
1239
+ };
1240
+ return {
1241
+ push(bytes) {
1242
+ let offset = 0;
1243
+ while (offset < bytes.byteLength) {
1244
+ if (!header) {
1245
+ offset += acceptHeaderBytes(bytes, offset);
1246
+ continue;
1247
+ }
1248
+ if (frameRemaining > 0) {
1249
+ const take = Math.min(bytes.byteLength - offset, frameRemaining);
1250
+ offset += take;
1251
+ frameRemaining -= take;
1252
+ if (frameRemaining === 0) {
1253
+ if (frameKind === backupArchiveFinalFrameKind)
1254
+ ended = true;
1255
+ sequence += 1;
1256
+ }
1257
+ continue;
1258
+ }
1259
+ if (ended) {
1260
+ throw new RaybaseBackupError("BACKUP_ARCHIVE_CORRUPT", "Backup archive contains bytes after its final frame");
1261
+ }
1262
+ const take = Math.min(bytes.byteLength - offset, backupFrameHeaderBytes - frameHeaderLength);
1263
+ frameHeader.set(bytes.subarray(offset, offset + take), frameHeaderLength);
1264
+ frameHeaderLength += take;
1265
+ offset += take;
1266
+ if (frameHeaderLength === backupFrameHeaderBytes)
1267
+ acceptFrameHeader();
1268
+ }
1269
+ },
1270
+ finish() {
1271
+ if (!header)
1272
+ throw new RaybaseBackupError("BACKUP_ARCHIVE_TRUNCATED", "Backup archive header is truncated");
1273
+ if (frameHeaderLength > 0 || frameRemaining > 0) {
1274
+ throw new RaybaseBackupError("BACKUP_ARCHIVE_TRUNCATED", "Backup archive frame is truncated");
1275
+ }
1276
+ if (!ended)
1277
+ throw new RaybaseBackupError("BACKUP_ARCHIVE_TRUNCATED", "Backup archive final frame is missing");
1278
+ return header;
1279
+ }
1280
+ };
1281
+ }
1282
+ async function reconstructEncryptedBackupEntry(bucket, descriptor, listed, options = {}) {
1283
+ const object = await bucket.get(listed.key);
1284
+ if (!object)
1285
+ throw new Error("Backup object disappeared during manifest reconstruction");
1286
+ if (object.size <= 0 || object.size > backupMultipartMaximumObjectBytes) {
1287
+ throw new RaybaseBackupError("BACKUP_LIMIT_EXCEEDED", "Backup object size is outside the supported encrypted archive bounds");
1288
+ }
1289
+ const digest = createBackupSha256Digest();
1290
+ const structure = createBackupArchiveStructureValidator();
1291
+ const reader = object.body.getReader();
1292
+ let bytes = 0;
1293
+ let sha256;
1294
+ try {
1295
+ while (true) {
1296
+ const result = await reader.read();
1297
+ if (result.done)
1298
+ break;
1299
+ const chunk = result.value;
1300
+ bytes += chunk.byteLength;
1301
+ if (bytes > backupMultipartMaximumObjectBytes) {
1302
+ throw new RaybaseBackupError("BACKUP_LIMIT_EXCEEDED", "Backup object exceeds the supported encrypted archive size");
1303
+ }
1304
+ await digest.write(chunk);
1305
+ structure.push(chunk);
1306
+ }
1307
+ const digestBytes = await digest.finish();
1308
+ sha256 = backupBytesToHex(digestBytes);
1309
+ digestBytes.fill(0);
1310
+ }
1311
+ catch (error) {
1312
+ await digest.abort(error);
1313
+ throw error;
1314
+ }
1315
+ finally {
1316
+ reader.releaseLock();
1317
+ }
1318
+ if (bytes !== object.size || bytes !== listed.size) {
1319
+ throw new RaybaseBackupError("BACKUP_ARCHIVE_CORRUPT", "Backup object size changed during manifest reconstruction");
1320
+ }
1321
+ const header = structure.finish();
1322
+ if (options.expectedIntegrity &&
1323
+ (bytes !== options.expectedIntegrity.bytes || sha256 !== options.expectedIntegrity.sha256)) {
1324
+ throw new RaybaseBackupError("BACKUP_ARCHIVE_CORRUPT", "Backup object ciphertext integrity contradicts its existing manifest entry");
1325
+ }
1326
+ if (header.product !== descriptor.product ||
1327
+ header.target !== descriptor.target ||
1328
+ header.targetId !== (descriptor.organizationId ?? descriptor.target)) {
1329
+ throw new RaybaseBackupError("BACKUP_ARCHIVE_IDENTITY_MISMATCH", `Backup object targets ${header.product}/${header.target}/${header.targetId}, not this manifest target`);
1330
+ }
1331
+ if (listed.key !== expectedEncryptedArchiveKey(descriptor, header)) {
1332
+ throw new RaybaseBackupError("BACKUP_ARCHIVE_IDENTITY_MISMATCH", "Backup object key does not match its archive header");
1333
+ }
1334
+ const trigger = object.customMetadata?.["raybase-trigger"];
1335
+ if (trigger !== "manual" && trigger !== "scheduled") {
1336
+ throw new RaybaseBackupError("BACKUP_ARCHIVE_CORRUPT", "Backup object trigger metadata is invalid");
1337
+ }
1338
+ const entry = {
1339
+ key: listed.key,
1340
+ date: header.createdAt,
1341
+ sha256,
1342
+ bytes,
1343
+ product: header.product,
1344
+ target: header.target,
1345
+ ...(descriptor.organizationId ? { organizationId: descriptor.organizationId } : {}),
1346
+ ...(descriptor.organizationSlug ? { organizationSlug: descriptor.organizationSlug } : {}),
1347
+ trigger,
1348
+ archiveFormat: 2,
1349
+ archiveId: header.archiveId,
1350
+ targetId: header.targetId,
1351
+ encrypted: true,
1352
+ contentType: backupBinaryContentType,
1353
+ encryption: header.encryption,
1354
+ logical: header.logical,
1355
+ providerId: header.keyEnvelope.providerId,
1356
+ kekKeyId: header.keyEnvelope.kekKeyId,
1357
+ keyEnvelope: header.keyEnvelope
1358
+ };
1359
+ verifyBackupObject(entry, object);
1360
+ const recordedProviderAvailable = [options.providers?.keyProvider, options.providers?.deploymentProvider]
1361
+ .some((provider) => provider?.id === header.keyEnvelope.providerId);
1362
+ if (recordedProviderAvailable && options.providers) {
1363
+ const authenticationObject = await bucket.get(listed.key);
1364
+ if (!authenticationObject)
1365
+ throw new Error("Backup object disappeared during authenticated manifest reconstruction");
1366
+ const decryptor = createBackupArchiveDecryptor({
1367
+ providers: options.providers,
1368
+ ...(options.environment ? { environment: options.environment } : {}),
1369
+ expectedCiphertextBytes: bytes,
1370
+ expectedCiphertextSha256: sha256,
1371
+ validateHeader(candidate) {
1372
+ if (!sameBytes(encodeBackupArchiveHeader(candidate), encodeBackupArchiveHeader(header))) {
1373
+ throw new RaybaseBackupError("BACKUP_ARCHIVE_IDENTITY_MISMATCH", "Backup header changed during authenticated manifest reconstruction");
1374
+ }
1375
+ }
1376
+ });
1377
+ const authenticatedReader = authenticationObject.body.pipeThrough(decryptor.transform).getReader();
1378
+ try {
1379
+ while (!(await authenticatedReader.read()).done) {
1380
+ // Authentication is streaming and plaintext is deliberately discarded.
1381
+ }
1382
+ await Promise.all([decryptor.header, decryptor.completion]);
1383
+ }
1384
+ catch (error) {
1385
+ await Promise.allSettled([decryptor.header, decryptor.completion]);
1386
+ if (error instanceof RaybaseBackupError &&
1387
+ (error.code === "BACKUP_PROVIDER_UNAVAILABLE" ||
1388
+ error.code === "BACKUP_KEY_UNAVAILABLE" ||
1389
+ error.code === "BACKUP_THRESHOLD_UNMET")) {
1390
+ // Keep historical archives discoverable when their provider/key/share
1391
+ // material is unavailable. Restore availability reports that state.
1392
+ }
1393
+ else {
1394
+ throw error;
1395
+ }
1396
+ }
1397
+ finally {
1398
+ authenticatedReader.releaseLock();
1399
+ }
1400
+ }
1401
+ return entry;
1402
+ }
1403
+ async function readTargetCatalog(bucket, descriptor, now, options = {}) {
1404
+ const read = await readTargetManifest(bucket, descriptor, now);
1405
+ const manifestEntries = new Map(read.manifest.entries
1406
+ .filter((entry) => entryMatchesDescriptor(entry, descriptor))
1407
+ .map((entry) => [entry.key, entry]));
1408
+ const objects = await listTargetArchiveObjects(bucket, descriptor);
1409
+ const entries = [];
1410
+ const repairEntries = [];
1411
+ const warnings = read.warning ? [read.warning] : [];
1412
+ let ignoredWarnings = 0;
1413
+ for (const object of objects.sort((left, right) => left.key.localeCompare(right.key))) {
1414
+ const existing = manifestEntries.get(object.key);
1415
+ try {
1416
+ if (object.key.endsWith(".sql")) {
1417
+ const legacy = existing;
1418
+ if (!legacy || legacy.archiveFormat !== 1)
1419
+ continue;
1420
+ verifyBackupObject(legacy, await bucket.head(object.key));
1421
+ entries.push(legacy);
1422
+ repairEntries.push(legacy);
1423
+ continue;
1424
+ }
1425
+ const reconstructed = await reconstructEncryptedBackupEntry(bucket, descriptor, object, {
1426
+ ...options,
1427
+ ...(existing?.archiveFormat === 2
1428
+ ? { expectedIntegrity: { bytes: existing.bytes, sha256: existing.sha256 } }
1429
+ : {})
1430
+ });
1431
+ if (existing?.archiveFormat === 2 &&
1432
+ existing.bytes === reconstructed.bytes &&
1433
+ existing.sha256 === reconstructed.sha256) {
1434
+ assertBackupManifestIdentity(existing, decodeBackupArchiveHeader(encodeBackupArchiveHeader({
1435
+ format: 2,
1436
+ archiveId: reconstructed.archiveId,
1437
+ product: reconstructed.product,
1438
+ target: reconstructed.target,
1439
+ targetId: reconstructed.targetId,
1440
+ createdAt: reconstructed.date,
1441
+ logical: reconstructed.logical,
1442
+ encryption: reconstructed.encryption,
1443
+ keyEnvelope: reconstructed.keyEnvelope
1444
+ })).header);
1445
+ const normalized = {
1446
+ ...reconstructed,
1447
+ ...(existing.actorId !== undefined ? { actorId: existing.actorId } : {})
1448
+ };
1449
+ entries.push(normalized);
1450
+ repairEntries.push(normalized);
1451
+ }
1452
+ else {
1453
+ entries.push(reconstructed);
1454
+ repairEntries.push(reconstructed);
1455
+ }
1456
+ }
1457
+ catch (error) {
1458
+ if (existing)
1459
+ repairEntries.push(existing);
1460
+ const warning = `Ignored invalid backup object ${object.key}: ${error instanceof Error ? error.message : String(error)}`;
1461
+ if (warnings.length < backupArchiveDiscoveryMaximumWarnings)
1462
+ archiveDiscoveryWarning(warnings, warning);
1463
+ else
1464
+ ignoredWarnings += 1;
1465
+ }
1466
+ }
1467
+ if (ignoredWarnings > 0)
1468
+ warnings.push(`Ignored ${ignoredWarnings} additional invalid backup objects.`);
1469
+ entries.sort((left, right) => right.date.localeCompare(left.date) || left.key.localeCompare(right.key));
1470
+ repairEntries.sort((left, right) => right.date.localeCompare(left.date) || left.key.localeCompare(right.key));
1471
+ const needsRepair = read.needsRepair || JSON.stringify(repairEntries) !== JSON.stringify(read.manifest.entries);
1472
+ return {
1473
+ manifest: {
1474
+ ...read.manifest,
1475
+ updatedAt: needsRepair ? now : read.manifest.updatedAt,
1476
+ entries: repairEntries
1477
+ },
1478
+ entries,
1479
+ needsRepair,
1480
+ ...(warnings.length ? { warning: warnings.join(" ") } : {})
1481
+ };
1482
+ }
1483
+ async function readAndRepairCoreBackupCatalog(bucket, now, options = {}) {
1484
+ const descriptor = coreDescriptor();
1485
+ const target = await readTargetCatalog(bucket, descriptor, now, options);
1486
+ const warnings = target.warning ? [target.warning] : [];
1487
+ if (target.needsRepair) {
1488
+ try {
1489
+ await writeJsonObject(bucket, descriptor.manifestKey, target.manifest);
1490
+ }
1491
+ catch (error) {
1492
+ warnings.push(`Could not repair ${descriptor.manifestKey}: ${error instanceof Error ? error.message : String(error)}`);
1493
+ }
1494
+ }
1495
+ const rootRead = await readRootManifest(bucket, now);
1496
+ if (rootRead.warning)
1497
+ warnings.push(rootRead.warning);
1498
+ const root = upsertRootTarget(rootRead.manifest, descriptor, target.entries[0]?.date ?? null);
1499
+ const rootNeedsRepair = rootRead.needsRepair || JSON.stringify(root.targets) !== JSON.stringify(rootRead.manifest.targets);
1500
+ const repairedRoot = {
1501
+ ...root,
1502
+ updatedAt: rootNeedsRepair ? now : root.updatedAt
1503
+ };
1504
+ if (rootNeedsRepair) {
1505
+ try {
1506
+ await writeJsonObject(bucket, backupRootManifestKey, repairedRoot);
1507
+ }
1508
+ catch (error) {
1509
+ warnings.push(`Could not repair ${backupRootManifestKey}: ${error instanceof Error ? error.message : String(error)}`);
1510
+ }
1511
+ }
1512
+ return {
1513
+ entries: target.entries,
1514
+ root: repairedRoot,
1515
+ ...(warnings.length ? { warning: warnings.join(" ") } : {})
1516
+ };
1517
+ }
1518
+ export async function listCoreBackupEntries(env, backups, environment) {
1519
+ if (!env.RAYBASE_BACKUPS)
1520
+ throw new Error("RAYBASE_BACKUPS R2 binding is missing");
1521
+ let providers = {};
1522
+ try {
1523
+ providers = await backupProviderSet(env, backups);
1524
+ }
1525
+ catch {
1526
+ // Listing remains available when provider configuration is invalid; such
1527
+ // archives receive structural validation and explicit restore readiness.
1528
+ }
1529
+ const catalog = await readAndRepairCoreBackupCatalog(env.RAYBASE_BACKUPS, new Date().toISOString(), {
1530
+ providers,
1531
+ ...(environment ?? env.RAYBASE_ENV ? { environment: environment ?? env.RAYBASE_ENV } : {})
1532
+ });
1533
+ return { entries: catalog.entries, ...(catalog.warning ? { warning: catalog.warning } : {}) };
1534
+ }
1535
+ export async function readBackupRestoreAvailability(env, entry, backups, runtimeStatus) {
1536
+ const restore = runtimeStatus ?? await readCoreBackupRestoreRuntimeStatus(env);
1537
+ if (restore.maintenance?.active) {
1538
+ return {
1539
+ available: false,
1540
+ reason: `Restore maintenance is active (${restore.maintenance.phase ?? "unknown phase"}).`
1541
+ };
1542
+ }
1543
+ if (entry.archiveFormat === 1) {
1544
+ return {
1545
+ available: entry.bytes <= backupLegacyUploadMaximumBytes,
1546
+ ...(entry.bytes > backupLegacyUploadMaximumBytes
1547
+ ? { reason: `Legacy plaintext restore exceeds the ${backupLegacyUploadMaximumBytes}-byte compatibility limit.` }
1548
+ : {})
1549
+ };
1550
+ }
1551
+ if (!restore.pitrAvailable) {
1552
+ return {
1553
+ available: false,
1554
+ reason: `Encrypted restore requires production PITR recovery support. ${restore.pitrUnavailableReason ?? "PITR is unavailable."}`
1555
+ };
1556
+ }
1557
+ try {
1558
+ const providers = await backupProviderSet(env, backups);
1559
+ const provider = [providers.keyProvider, providers.deploymentProvider]
1560
+ .find((candidate) => candidate?.id === entry.providerId);
1561
+ if (!provider) {
1562
+ return { available: false, reason: `Backup provider ${entry.providerId} is unavailable.` };
1563
+ }
1564
+ const context = {
1565
+ archiveId: entry.archiveId,
1566
+ product: entry.product,
1567
+ target: entry.target,
1568
+ targetId: entry.targetId,
1569
+ createdAt: entry.date,
1570
+ purpose: "readiness",
1571
+ signal: new AbortController().signal,
1572
+ ...(env.RAYBASE_ENV ? { environment: env.RAYBASE_ENV } : {})
1573
+ };
1574
+ if (provider.passwordSecrets) {
1575
+ const participantStatus = await passwordParticipantReadiness(env, provider, context, false);
1576
+ if (participantStatus && !participantStatus.readyForNewBackups) {
1577
+ return {
1578
+ available: false,
1579
+ reason: participantStatus.reason ?? "Password-secret participants are not ready."
1580
+ };
1581
+ }
1582
+ }
1583
+ const dek = await unwrapBackupDek({ providers, context, envelope: entry.keyEnvelope });
1584
+ dek.fill(0);
1585
+ return { available: true };
1586
+ }
1587
+ catch (error) {
1588
+ if (error instanceof RaybaseBackupError && error.code === "BACKUP_KEY_UNAVAILABLE") {
1589
+ return { available: false, reason: `Historical key ${entry.kekKeyId} is unavailable.` };
1590
+ }
1591
+ if (error instanceof RaybaseBackupError) {
1592
+ return { available: false, reason: error.message };
1593
+ }
1594
+ return {
1595
+ available: false,
1596
+ reason: "Backup restore readiness could not be determined."
1597
+ };
1598
+ }
1599
+ }
1600
+ export async function readCoreBackupStatus(env, options = {}) {
1601
+ const retention = parseBackupRetention(env.RAYBASE_BACKUPS_RETAIN);
1602
+ const enabled = backupsEnabled(env);
1603
+ const consistencyPolicy = options.backups?.consistency?.mode === "write-gated" ? "write-gated" : "optimistic";
1604
+ const restore = await readCoreBackupRestoreRuntimeStatus(env);
1605
+ const caveats = [
1606
+ "New backups are envelope-encrypted format-2 archives; SHA-256 covers the stored ciphertext.",
1607
+ "Backups include Durable Object table data and Raybase file metadata.",
1608
+ ...(options.storageConfigured ? ["R2 file bytes are not included."] : []),
1609
+ ...(options.vectorizeConfigured ? ["Vectorize indexes are not included."] : ["Vectorize indexes are not included when configured."]),
1610
+ "Legacy plaintext format-1 SQL is restore-only and keeps its historical memory and statement-size limits.",
1611
+ ...(consistencyPolicy === "write-gated"
1612
+ ? ["The write-gated consistency policy temporarily rejects database mutations while a backup snapshot is read."]
1613
+ : ["Hot databases are retried optimistically and fail closed when a consistent generation cannot be read."]),
1614
+ ...(restore.pitrAvailable
1615
+ ? ["Encrypted restores use a persistent maintenance gate and production PITR undo bookmark for interrupted-restore recovery."]
1616
+ : [`Encrypted restore is unavailable: ${restore.pitrUnavailableReason ?? "production PITR recovery support is unavailable."}`]),
1617
+ ...(restore.maintenance?.active
1618
+ ? [`Restore maintenance is active in ${restore.maintenance.phase ?? "an unknown"} phase; normal database access remains gated until rollback recovery completes.`]
1619
+ : [])
1620
+ ];
1621
+ let encryptionReady = false;
1622
+ let provider = {
1623
+ id: null,
1624
+ mode: null,
1625
+ keyId: null,
1626
+ passwordCoupled: false
1627
+ };
1628
+ let encryptionWarning;
1629
+ let catalogProviders = {};
1630
+ try {
1631
+ const providers = await backupProviderSet(env, options.backups);
1632
+ catalogProviders = providers;
1633
+ const selected = selectedBackupProvider(providers);
1634
+ if (selected) {
1635
+ const context = backupReadinessContext(options.environment ?? env.RAYBASE_ENV);
1636
+ const participants = await passwordParticipantReadiness(env, selected, context, false);
1637
+ const envelope = participants?.readyForNewBackups === false
1638
+ ? null
1639
+ : await probeBackupProviderForCreation(env, providers, context);
1640
+ provider = {
1641
+ id: selected.id,
1642
+ mode: selected.mode,
1643
+ keyId: envelope?.kekKeyId ?? null,
1644
+ passwordCoupled: Boolean(selected.passwordSecrets),
1645
+ ...(selected.passwordSecrets ? { passwordLifecycleVersion: selected.passwordSecrets.version } : {}),
1646
+ ...(participants ? { passwordParticipants: participants } : {})
1647
+ };
1648
+ encryptionReady = participants?.readyForNewBackups ?? true;
1649
+ if (participants && !participants.readyForNewBackups) {
1650
+ encryptionWarning = participants.reason ?? "Password-secret participants are not ready for backup creation.";
1651
+ }
1652
+ if (selected.passwordSecrets) {
1653
+ caveats.push("This app provider couples restore access to password-secret participant readiness; passwordless users may be ineligible, and password reset or loss can permanently destroy access to older protected shares.");
1654
+ }
1655
+ }
1656
+ else {
1657
+ encryptionWarning = "Encrypted backup creation requires an app key provider or RAYBASE_BACKUPS_KEY.";
1658
+ }
1659
+ }
1660
+ catch (error) {
1661
+ encryptionWarning = error instanceof RaybaseBackupError
1662
+ ? error.message
1663
+ : "Backup encryption provider readiness failed.";
1664
+ }
1665
+ if (!env.RAYBASE_BACKUPS) {
1666
+ return {
1667
+ enabled,
1668
+ bindingPresent: false,
1669
+ encryptionReady,
1670
+ archiveFormat: 2,
1671
+ consistencyPolicy,
1672
+ provider,
1673
+ restore,
1674
+ retention,
1675
+ rootManifestKey: backupRootManifestKey,
1676
+ targets: [],
1677
+ latestBackup: null,
1678
+ caveats,
1679
+ ...(encryptionWarning ? { warning: encryptionWarning } : {})
1680
+ };
1681
+ }
1682
+ const now = new Date().toISOString();
1683
+ const catalog = await readAndRepairCoreBackupCatalog(env.RAYBASE_BACKUPS, now, {
1684
+ providers: catalogProviders,
1685
+ ...(options.environment ?? env.RAYBASE_ENV ? { environment: options.environment ?? env.RAYBASE_ENV } : {})
1686
+ });
1687
+ return {
1688
+ enabled,
1689
+ bindingPresent: true,
1690
+ encryptionReady,
1691
+ archiveFormat: 2,
1692
+ consistencyPolicy,
1693
+ provider,
1694
+ restore,
1695
+ retention,
1696
+ rootManifestKey: backupRootManifestKey,
1697
+ targets: catalog.root.targets,
1698
+ latestBackup: catalog.entries[0] ?? null,
1699
+ caveats,
1700
+ ...(catalog.warning || encryptionWarning
1701
+ ? { warning: [catalog.warning, encryptionWarning].filter(Boolean).join(" ") }
1702
+ : {})
1703
+ };
1704
+ }
1705
+ export async function readCoreBackupSqlFromR2(env, key) {
1706
+ if (!env.RAYBASE_BACKUPS)
1707
+ throw new Error("RAYBASE_BACKUPS R2 binding is missing");
1708
+ const normalized = validateCoreBackupKey(key);
1709
+ if (!normalized.endsWith(".sql"))
1710
+ throw new Error("Encrypted backup archives are binary, not SQL text");
1711
+ const object = await env.RAYBASE_BACKUPS.get(normalized);
1712
+ if (!object)
1713
+ throw new Error("Backup object not found");
1714
+ if (object.size > backupLegacyUploadMaximumBytes) {
1715
+ throw new RaybaseBackupError("BACKUP_LIMIT_EXCEEDED", `Legacy plaintext restores are limited to ${backupLegacyUploadMaximumBytes} bytes`);
1716
+ }
1717
+ const sql = new TextDecoder("utf-8", { fatal: true }).decode(await object.arrayBuffer());
1718
+ validateCoreBackupSql(sql);
1719
+ return { key: normalized, sql, object };
1720
+ }
1721
+ export async function readCoreBackupObjectFromR2(env, key, backups, environment) {
1722
+ if (!env.RAYBASE_BACKUPS)
1723
+ throw new Error("RAYBASE_BACKUPS R2 binding is missing");
1724
+ const normalized = validateCoreBackupKey(key);
1725
+ const listed = await listCoreBackupEntries(env, backups, environment);
1726
+ const entry = listed.entries.find((candidate) => candidate.key === normalized);
1727
+ if (!entry)
1728
+ throw new Error("Backup manifest entry not found");
1729
+ const object = await env.RAYBASE_BACKUPS.get(normalized);
1730
+ verifyBackupObject(entry, object);
1731
+ if (!object)
1732
+ throw new Error("Backup object not found");
1733
+ return { key: normalized, entry, object };
1734
+ }
1735
+ async function consumeEncryptedBackup(object, providers, options) {
1736
+ const decryptor = createBackupArchiveDecryptor({
1737
+ providers,
1738
+ ...(options.providerInput ? { providerInput: options.providerInput } : {}),
1739
+ ...(options.environment ? { environment: options.environment } : {}),
1740
+ ...(options.expectedCiphertextBytes !== undefined
1741
+ ? { expectedCiphertextBytes: options.expectedCiphertextBytes }
1742
+ : {}),
1743
+ ...(options.expectedCiphertextSha256
1744
+ ? { expectedCiphertextSha256: options.expectedCiphertextSha256 }
1745
+ : {}),
1746
+ validateHeader(header) {
1747
+ validateCoreArchiveHeader(header);
1748
+ if (options.expectedEntry)
1749
+ assertBackupManifestIdentity(options.expectedEntry, header);
1750
+ if (options.expectedHeader && !sameBytes(encodeBackupArchiveHeader(options.expectedHeader), encodeBackupArchiveHeader(header))) {
1751
+ throw new RaybaseBackupError("BACKUP_ARCHIVE_IDENTITY_MISMATCH", "Backup header changed between restore passes");
1752
+ }
1753
+ }
1754
+ });
1755
+ const decoder = new BackupLogicalDecoder();
1756
+ const logicalDigest = createBackupSha256Digest();
1757
+ const reader = object.body.pipeThrough(decryptor.transform).getReader();
1758
+ let logicalEnd = null;
1759
+ try {
1760
+ while (true) {
1761
+ const result = await reader.read();
1762
+ if (result.done)
1763
+ break;
1764
+ for (const record of decoder.push(result.value)) {
1765
+ if (record.kind === "logical-end") {
1766
+ if (logicalEnd)
1767
+ throw new RaybaseBackupError("BACKUP_ARCHIVE_CORRUPT", "Backup has duplicate logical end records");
1768
+ const digestBytes = await logicalDigest.finish();
1769
+ const digestHex = backupBytesToHex(digestBytes);
1770
+ digestBytes.fill(0);
1771
+ if (digestHex !== record.plaintextSha256) {
1772
+ throw new RaybaseBackupError("BACKUP_ARCHIVE_CORRUPT", "Backup logical plaintext digest does not match");
1773
+ }
1774
+ logicalEnd = record;
1775
+ }
1776
+ else {
1777
+ await logicalDigest.write(encodeBackupLogicalRecord(record));
1778
+ }
1779
+ await options.onRecord?.(record);
1780
+ }
1781
+ }
1782
+ decoder.finish();
1783
+ const header = await decryptor.header;
1784
+ const completion = await decryptor.completion;
1785
+ if (!logicalEnd)
1786
+ throw new RaybaseBackupError("BACKUP_ARCHIVE_TRUNCATED", "Backup logical end is missing");
1787
+ if (completion.logicalTotals.recordCount !== logicalEnd.recordCount ||
1788
+ completion.logicalTotals.rowCount !== logicalEnd.rowCount ||
1789
+ completion.logicalTotals.tableCount !== logicalEnd.tableCount) {
1790
+ throw new RaybaseBackupError("BACKUP_ARCHIVE_CORRUPT", "Authenticated frame totals do not match the logical archive");
1791
+ }
1792
+ if (!sameBytes(encodeBackupArchiveHeader(header), encodeBackupArchiveHeader(completion.header))) {
1793
+ throw new RaybaseBackupError("BACKUP_ARCHIVE_IDENTITY_MISMATCH", "Backup header completion is inconsistent");
1794
+ }
1795
+ return { completion, logicalEnd };
1796
+ }
1797
+ catch (error) {
1798
+ await logicalDigest.abort(error);
1799
+ throw error;
1800
+ }
1801
+ finally {
1802
+ reader.releaseLock();
1803
+ }
1804
+ }
1805
+ export async function restoreCoreEncryptedBackup(env, source, options = {}) {
1806
+ const providers = await backupProviderSet(env, options.backups);
1807
+ if (!selectedBackupProvider(providers)) {
1808
+ throw new RaybaseBackupError("BACKUP_ENCRYPTION_NOT_CONFIGURED", "Encrypted restore requires the provider and historical key named by the archive");
1809
+ }
1810
+ const firstObject = await source.getObject();
1811
+ const verified = await consumeEncryptedBackup(firstObject, providers, {
1812
+ ...(options.providerInput ? { providerInput: options.providerInput } : {}),
1813
+ ...(options.environment ?? env.RAYBASE_ENV ? { environment: options.environment ?? env.RAYBASE_ENV } : {}),
1814
+ ...(source.entry ? { expectedEntry: source.entry } : {}),
1815
+ ...(source.entry ? { expectedCiphertextBytes: source.entry.bytes, expectedCiphertextSha256: source.entry.sha256 } : {})
1816
+ });
1817
+ await prepareCoreBackupRestore(env, { requirePitr: true });
1818
+ const stub = dataStub(env);
1819
+ const operationId = crypto.randomUUID();
1820
+ await stub.beginBackupRestore({
1821
+ operationId,
1822
+ sourceSha256: verified.completion.ciphertextSha256,
1823
+ archiveId: verified.completion.header.archiveId,
1824
+ targetId: verified.completion.header.targetId,
1825
+ expectedRecordCount: verified.logicalEnd.recordCount,
1826
+ expectedRowCount: verified.logicalEnd.rowCount,
1827
+ expectedTableCount: verified.logicalEnd.tableCount,
1828
+ plaintextSha256: verified.logicalEnd.plaintextSha256
1829
+ });
1830
+ let batch = [];
1831
+ let batchBytes = 0;
1832
+ let batchIndex = 0;
1833
+ const flush = async () => {
1834
+ if (!batch.length)
1835
+ return;
1836
+ const encoded = batch.map(encodeBackupLogicalRecord);
1837
+ const joined = new Uint8Array(encoded.reduce((total, record) => total + record.byteLength, 0));
1838
+ let offset = 0;
1839
+ for (const record of encoded) {
1840
+ joined.set(record, offset);
1841
+ offset += record.byteLength;
1842
+ }
1843
+ const batchSha256 = await sha256Hex(joined);
1844
+ joined.fill(0);
1845
+ await stub.applyBackupRestoreBatch({ operationId, batchIndex, batchSha256, records: batch });
1846
+ batch = [];
1847
+ batchBytes = 0;
1848
+ batchIndex += 1;
1849
+ };
1850
+ try {
1851
+ const secondObject = await source.getObject();
1852
+ await consumeEncryptedBackup(secondObject, providers, {
1853
+ ...(options.providerInput ? { providerInput: options.providerInput } : {}),
1854
+ ...(options.environment ?? env.RAYBASE_ENV ? { environment: options.environment ?? env.RAYBASE_ENV } : {}),
1855
+ expectedHeader: verified.completion.header,
1856
+ expectedCiphertextBytes: verified.completion.ciphertextBytes,
1857
+ expectedCiphertextSha256: verified.completion.ciphertextSha256,
1858
+ async onRecord(record) {
1859
+ const encodedBytes = encodeBackupLogicalRecord(record).byteLength;
1860
+ if (batch.length > 0 &&
1861
+ (batch.length >= backupDatabaseProtocolLimits.maximumRestoreBatchRecords ||
1862
+ batchBytes + encodedBytes > backupDatabaseProtocolLimits.maximumRestoreBatchBytes)) {
1863
+ await flush();
1864
+ }
1865
+ batch.push(record);
1866
+ batchBytes += encodedBytes;
1867
+ if (batch.length >= backupDatabaseProtocolLimits.maximumRestoreBatchRecords ||
1868
+ batchBytes >= backupDatabaseProtocolLimits.maximumRestoreBatchBytes) {
1869
+ await flush();
1870
+ }
1871
+ }
1872
+ });
1873
+ await flush();
1874
+ await stub.finishBackupRestore({ operationId });
1875
+ }
1876
+ catch (error) {
1877
+ await stub.failBackupRestore({
1878
+ operationId,
1879
+ error: error instanceof Error ? error.message : String(error)
1880
+ }).catch(() => undefined);
1881
+ try {
1882
+ await recoverInterruptedBackupRestore({ stub });
1883
+ }
1884
+ catch (recoveryError) {
1885
+ throw new RaybaseBackupError("BACKUP_RESTORE_MAINTENANCE_REQUIRED", `Restore failed and automatic PITR rollback could not complete: ${recoveryError instanceof Error ? recoveryError.message : String(recoveryError)}`);
1886
+ }
1887
+ throw error;
1888
+ }
1889
+ return {
1890
+ ok: true,
1891
+ archiveFormat: 2,
1892
+ encrypted: true,
1893
+ archiveId: verified.completion.header.archiveId,
1894
+ sourceSha256: verified.completion.ciphertextSha256,
1895
+ recordCount: verified.logicalEnd.recordCount,
1896
+ rowCount: verified.logicalEnd.rowCount,
1897
+ tableCount: verified.logicalEnd.tableCount
1898
+ };
1899
+ }
1900
+ export async function restoreCoreEncryptedBackupFromR2(env, key, options = {}) {
1901
+ const stored = await readCoreBackupObjectFromR2(env, key, options.backups, options.environment);
1902
+ if (stored.entry.archiveFormat !== 2)
1903
+ throw new Error("Legacy SQL backups use the compatibility restore path");
1904
+ return restoreCoreEncryptedBackup(env, {
1905
+ entry: stored.entry,
1906
+ async getObject() {
1907
+ if (!env.RAYBASE_BACKUPS)
1908
+ throw new Error("RAYBASE_BACKUPS R2 binding is missing");
1909
+ const object = await env.RAYBASE_BACKUPS.get(stored.key);
1910
+ if (!object)
1911
+ throw new Error("Backup object not found");
1912
+ return object;
1913
+ }
1914
+ }, options);
1915
+ }
1916
+ export async function archiveBackupToR2(env, options = {}) {
1917
+ if (!backupsEnabled(env)) {
1918
+ console.info("Raybase backup archive skipped", { reason: "disabled" });
1919
+ return { ok: true, skipped: true, reason: "disabled", archived: false };
1920
+ }
1921
+ if (!env.RAYBASE_BACKUPS) {
1922
+ console.info("Raybase backup archive skipped", { reason: "missing-r2-binding" });
1923
+ return { ok: true, skipped: true, reason: "missing-r2-binding", archived: false };
1924
+ }
1925
+ await pruneTemporaryBackupObjects(env.RAYBASE_BACKUPS, { now: options.now }).catch((error) => {
1926
+ console.warn("Temporary encrypted backup cleanup failed", error);
1927
+ });
1928
+ const providers = await backupProviderSet(env, options.backups);
1929
+ if (!selectedBackupProvider(providers)) {
1930
+ throw new RaybaseBackupError("BACKUP_ENCRYPTION_NOT_CONFIGURED", "Encrypted backups require an app key provider or RAYBASE_BACKUPS_KEY");
1931
+ }
1932
+ const now = options.now ?? new Date();
1933
+ const date = now.toISOString();
1934
+ const trigger = options.trigger ?? "scheduled";
1935
+ const descriptor = coreDescriptor();
1936
+ const maxAttempts = options.backups?.consistency?.mode === "write-gated"
1937
+ ? 1
1938
+ : options.backups?.consistency?.maxAttempts;
1939
+ const entry = await retryConsistentBackupSnapshot(async () => {
1940
+ const archiveId = crypto.randomUUID();
1941
+ const key = `${descriptor.prefix}${backupTimestamp(now)}-${archiveId}.rbb`;
1942
+ const identity = {
1943
+ archiveId,
1944
+ product: "core",
1945
+ target: "core",
1946
+ targetId: "core",
1947
+ createdAt: date
1948
+ };
1949
+ const wrapContext = {
1950
+ ...identity,
1951
+ purpose: "wrap",
1952
+ signal: new AbortController().signal,
1953
+ ...(options.environment ?? env.RAYBASE_ENV ? { environment: options.environment ?? env.RAYBASE_ENV } : {})
1954
+ };
1955
+ const wrapParticipants = await resolvePasswordParticipantsForWrap(env, selectedBackupProvider(providers), wrapContext);
1956
+ let snapshot;
1957
+ let encryptor;
1958
+ try {
1959
+ encryptor = await createBackupArchiveEncryptor({
1960
+ identity,
1961
+ providers,
1962
+ participants: wrapParticipants.participants,
1963
+ logicalTotals: () => {
1964
+ if (!snapshot)
1965
+ throw new RaybaseBackupError("BACKUP_ARCHIVE_CORRUPT", "Backup snapshot did not initialize");
1966
+ return snapshot.totals;
1967
+ },
1968
+ ...(options.environment ?? env.RAYBASE_ENV ? { environment: options.environment ?? env.RAYBASE_ENV } : {})
1969
+ });
1970
+ }
1971
+ finally {
1972
+ wrapParticipants.release();
1973
+ }
1974
+ const provisional = {
1975
+ key,
1976
+ date,
1977
+ sha256: "0".repeat(64),
1978
+ bytes: 0,
1979
+ product: "core",
1980
+ target: "core",
1981
+ actorId: options.actorId ?? null,
1982
+ trigger,
1983
+ archiveFormat: 2,
1984
+ archiveId,
1985
+ targetId: "core",
1986
+ encrypted: true,
1987
+ contentType: backupBinaryContentType,
1988
+ encryption: encryptor.header.encryption,
1989
+ logical: encryptor.header.logical,
1990
+ providerId: encryptor.header.keyEnvelope.providerId,
1991
+ kekKeyId: encryptor.header.keyEnvelope.kekKeyId,
1992
+ keyEnvelope: encryptor.header.keyEnvelope
1993
+ };
1994
+ const upload = await env.RAYBASE_BACKUPS.createMultipartUpload(key, {
1995
+ httpMetadata: { contentType: backupBinaryContentType },
1996
+ customMetadata: backupObjectMetadata(provisional)
1997
+ });
1998
+ snapshot = snapshotLogicalStream(dataStub(env), archiveId, options.backups?.consistency);
1999
+ const ciphertext = snapshot.stream.pipeThrough(encryptor.transform);
2000
+ const [uploaded, completion] = await Promise.all([
2001
+ uploadBackupMultipartStream(upload, ciphertext),
2002
+ encryptor.completion
2003
+ ]);
2004
+ if (uploaded.bytes !== completion.ciphertextBytes ||
2005
+ uploaded.sha256 !== completion.ciphertextSha256) {
2006
+ await env.RAYBASE_BACKUPS.delete(key).catch(() => undefined);
2007
+ throw new RaybaseBackupError("BACKUP_ARCHIVE_CORRUPT", "Uploaded backup bytes do not match encryption completion evidence");
2008
+ }
2009
+ return {
2010
+ ...provisional,
2011
+ bytes: uploaded.bytes,
2012
+ sha256: uploaded.sha256
2013
+ };
2014
+ }, { ...(maxAttempts ? { maxAttempts } : {}) });
2015
+ verifyBackupObject(entry, await env.RAYBASE_BACKUPS.head(entry.key));
2016
+ const catalog = await readTargetCatalog(env.RAYBASE_BACKUPS, descriptor, date, {
2017
+ providers,
2018
+ ...(options.environment ?? env.RAYBASE_ENV ? { environment: options.environment ?? env.RAYBASE_ENV } : {})
2019
+ });
2020
+ const operationalEntries = [entry, ...catalog.entries.filter((existing) => existing.key !== entry.key)]
2021
+ .sort((left, right) => right.date.localeCompare(left.date) || left.key.localeCompare(right.key));
2022
+ const manifestEntries = [entry, ...catalog.manifest.entries.filter((existing) => existing.key !== entry.key)]
2023
+ .sort((left, right) => right.date.localeCompare(left.date) || left.key.localeCompare(right.key));
2024
+ const retention = parseBackupRetention(env.RAYBASE_BACKUPS_RETAIN);
2025
+ await writeJsonObject(env.RAYBASE_BACKUPS, descriptor.manifestKey, {
2026
+ ...catalog.manifest,
2027
+ updatedAt: date,
2028
+ entries: manifestEntries
2029
+ });
2030
+ const rootRead = await readRootManifest(env.RAYBASE_BACKUPS, date);
2031
+ const root = upsertRootTarget(rootRead.manifest, descriptor, entry.date);
2032
+ await writeJsonObject(env.RAYBASE_BACKUPS, backupRootManifestKey, {
2033
+ ...root,
2034
+ updatedAt: date,
2035
+ lastSuccessfulFullRunAt: date,
2036
+ partialRun: null
2037
+ });
2038
+ const pruned = await pruneEntries(env.RAYBASE_BACKUPS, operationalEntries, retention);
2039
+ if (pruned.pruned.length) {
2040
+ const prunedKeys = new Set(pruned.pruned.map((prunedEntry) => prunedEntry.key));
2041
+ await writeJsonObject(env.RAYBASE_BACKUPS, descriptor.manifestKey, {
2042
+ ...catalog.manifest,
2043
+ updatedAt: date,
2044
+ entries: manifestEntries.filter((manifestEntry) => !prunedKeys.has(manifestEntry.key))
2045
+ });
2046
+ }
2047
+ await audit(raybaseAuthExecutor(env), options.actorId ?? null, `database.backup.${trigger}`, "raybase_database", undefined, {
2048
+ key: entry.key,
2049
+ bytes: entry.bytes,
2050
+ sha256: entry.sha256,
2051
+ archiveFormat: entry.archiveFormat,
2052
+ encrypted: entry.encrypted,
2053
+ providerId: entry.providerId,
2054
+ kekKeyId: entry.kekKeyId
2055
+ }).catch((error) => {
2056
+ console.warn("Raybase backup archive audit failed", error);
2057
+ });
2058
+ console.info("Raybase backup archive complete", {
2059
+ key: entry.key,
2060
+ bytes: entry.bytes,
2061
+ sha256: entry.sha256,
2062
+ providerId: entry.providerId,
2063
+ kekKeyId: entry.kekKeyId,
2064
+ trigger: entry.trigger
2065
+ });
2066
+ return { ok: true, skipped: false, archived: true, entry, pruned: pruned.pruned };
2067
+ }
2068
+ //# sourceMappingURL=backup-archive.js.map