@hraness/ghostget 0.17.2

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 (500) hide show
  1. package/CHANGELOG.md +458 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1359 -0
  4. package/bunfig.toml +2 -0
  5. package/dist/apple-photos-client.js +704 -0
  6. package/dist/beeper-client.js +4602 -0
  7. package/dist/client.js +1605 -0
  8. package/dist/index-26yq8q16.js +31 -0
  9. package/dist/index-9t13b4k7.js +508 -0
  10. package/dist/index-axwm9b3v.js +5 -0
  11. package/dist/index-gwk7rbyj.js +30 -0
  12. package/dist/index.js +701 -0
  13. package/dist/messaging.js +1130 -0
  14. package/dist/omni-client.js +1406 -0
  15. package/dist/whatsapp-client.js +355 -0
  16. package/docs/imessage-direct-provider.md +106 -0
  17. package/docs/rental-listings.md +94 -0
  18. package/package.json +600 -0
  19. package/skills/ghostget/SKILL.md +251 -0
  20. package/skills/ghostget/agents/openai.yaml +4 -0
  21. package/skills/ghostget/references/adapter-contract.md +181 -0
  22. package/skills/ghostget/references/apple-photos.md +115 -0
  23. package/skills/ghostget/references/article-drafts.md +235 -0
  24. package/skills/ghostget/references/code-owned-provider-scaffold.md +134 -0
  25. package/skills/ghostget/references/cross-posting-video.md +139 -0
  26. package/skills/ghostget/references/cross-posting.md +82 -0
  27. package/skills/ghostget/references/derivation.md +390 -0
  28. package/skills/ghostget/references/hraness-social-profile-stats.json +269 -0
  29. package/skills/ghostget/references/install.md +66 -0
  30. package/skills/ghostget/references/linkedin-adapter.md +442 -0
  31. package/skills/ghostget/references/messaging.md +168 -0
  32. package/skills/ghostget/references/meta-comet-contract.md +153 -0
  33. package/skills/ghostget/references/platform-patterns.md +122 -0
  34. package/skills/ghostget/references/provider-plugins.md +496 -0
  35. package/skills/ghostget/references/reddit-flair.md +78 -0
  36. package/skills/ghostget/references/safety-and-state.md +214 -0
  37. package/skills/ghostget/references/settlement-and-duplicate-cleanup.md +79 -0
  38. package/skills/ghostget/references/social-platform-routing.md +66 -0
  39. package/skills/ghostget/references/social-profile-stats.md +195 -0
  40. package/skills/ghostget/references/social-video-platform-routing.md +89 -0
  41. package/skills/ghostget/references/whatsapp-adapter.md +101 -0
  42. package/skills/ghostget/references/x-adapter.md +332 -0
  43. package/skills/ghostget/references/x-ai-disclosure.md +71 -0
  44. package/src/apple-photos-cli.ts +639 -0
  45. package/src/apple-photos-client-types.ts +168 -0
  46. package/src/apple-photos-client.ts +201 -0
  47. package/src/apple-photos-contact-evidence.ts +838 -0
  48. package/src/apple-photos-local-source.ts +1131 -0
  49. package/src/args.ts +2053 -0
  50. package/src/article-draft-document.ts +417 -0
  51. package/src/article-draft-embeds.ts +123 -0
  52. package/src/article-draft-images.ts +214 -0
  53. package/src/assets/adapter-template/wrench-adapter.json +40 -0
  54. package/src/assets/adapters/beeper/wrench-web-adapter.json +1500 -0
  55. package/src/assets/adapters/beeper/wrench-web-adapter.v1.0.0.json +129 -0
  56. package/src/assets/adapters/beeper/wrench-web-adapter.v1.1.0.json +207 -0
  57. package/src/assets/adapters/beeper/wrench-web-adapter.v2.0.0.json +1463 -0
  58. package/src/assets/adapters/beeper/wrench-web-adapter.v2.1.0.json +1463 -0
  59. package/src/assets/adapters/beeper/wrench-web-adapter.v2.2.0.json +1475 -0
  60. package/src/assets/adapters/beeper/wrench-web-adapter.v2.3.0.json +1488 -0
  61. package/src/assets/adapters/bluesky/wrench-web-adapter.json +684 -0
  62. package/src/assets/adapters/bluesky/wrench-web-adapter.v1.0.0.json +581 -0
  63. package/src/assets/adapters/bluesky/wrench-web-adapter.v1.1.0.json +581 -0
  64. package/src/assets/adapters/bluesky/wrench-web-adapter.v1.2.0.json +581 -0
  65. package/src/assets/adapters/bluesky/wrench-web-adapter.v1.3.0.json +615 -0
  66. package/src/assets/adapters/bluesky/wrench-web-adapter.v1.4.0.json +684 -0
  67. package/src/assets/adapters/bluesky/wrench-web-adapter.v1.5.0.json +684 -0
  68. package/src/assets/adapters/bluesky/wrench-web-adapter.v1.6.0.json +684 -0
  69. package/src/assets/adapters/clasificados/wrench-web-adapter.json +54 -0
  70. package/src/assets/adapters/facebook/wrench-web-adapter.json +750 -0
  71. package/src/assets/adapters/facebook/wrench-web-adapter.v1.0.0.json +706 -0
  72. package/src/assets/adapters/facebook/wrench-web-adapter.v1.1.0.json +703 -0
  73. package/src/assets/adapters/facebook/wrench-web-adapter.v1.2.0.json +750 -0
  74. package/src/assets/adapters/facebook-group/wrench-web-adapter.json +682 -0
  75. package/src/assets/adapters/facebook-group/wrench-web-adapter.v1.0.0.json +583 -0
  76. package/src/assets/adapters/facebook-group/wrench-web-adapter.v1.1.0.json +682 -0
  77. package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.json +437 -0
  78. package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.v1.0.0.json +383 -0
  79. package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.v1.1.0.json +437 -0
  80. package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.v1.1.1.json +437 -0
  81. package/src/assets/adapters/facebook-page/wrench-web-adapter.json +880 -0
  82. package/src/assets/adapters/facebook-page/wrench-web-adapter.v1.0.0.json +740 -0
  83. package/src/assets/adapters/facebook-page/wrench-web-adapter.v1.1.0.json +880 -0
  84. package/src/assets/adapters/github/wrench-web-adapter.json +69 -0
  85. package/src/assets/adapters/github/wrench-web-adapter.v1.0.0.json +42 -0
  86. package/src/assets/adapters/github/wrench-web-adapter.v1.1.0.json +69 -0
  87. package/src/assets/adapters/gmail/wrench-adapter.json +161 -0
  88. package/src/assets/adapters/gmail/wrench-adapter.v1.2.0.json +157 -0
  89. package/src/assets/adapters/hacker-news/wrench-web-adapter.json +315 -0
  90. package/src/assets/adapters/hacker-news/wrench-web-adapter.v1.0.0.json +315 -0
  91. package/src/assets/adapters/imessage/wrench-web-adapter.json +236 -0
  92. package/src/assets/adapters/instagram/wrench-web-adapter.json +719 -0
  93. package/src/assets/adapters/instagram/wrench-web-adapter.v1.0.0.json +627 -0
  94. package/src/assets/adapters/instagram/wrench-web-adapter.v1.1.0.json +609 -0
  95. package/src/assets/adapters/instagram/wrench-web-adapter.v1.2.0.json +640 -0
  96. package/src/assets/adapters/instagram/wrench-web-adapter.v1.3.0.json +671 -0
  97. package/src/assets/adapters/instagram/wrench-web-adapter.v1.4.0.json +710 -0
  98. package/src/assets/adapters/instagram/wrench-web-adapter.v1.5.0.json +710 -0
  99. package/src/assets/adapters/instagram/wrench-web-adapter.v1.6.0.json +719 -0
  100. package/src/assets/adapters/instagram/wrench-web-adapter.v1.7.0.json +719 -0
  101. package/src/assets/adapters/linkedin/wrench-adapter.json +442 -0
  102. package/src/assets/adapters/linkedin/wrench-adapter.v0.4.0.json +124 -0
  103. package/src/assets/adapters/linkedin/wrench-adapter.v1.0.0.json +411 -0
  104. package/src/assets/adapters/linkedin/wrench-web-adapter.json +914 -0
  105. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.0.0.json +604 -0
  106. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.1.0.json +604 -0
  107. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.10.0.json +809 -0
  108. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.11.0.json +809 -0
  109. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.12.0.json +820 -0
  110. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.13.0.json +819 -0
  111. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.14.0.json +819 -0
  112. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.15.0.json +819 -0
  113. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.16.0.json +874 -0
  114. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.17.0.json +874 -0
  115. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.18.0.json +874 -0
  116. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.19.0.json +874 -0
  117. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.2.0.json +731 -0
  118. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.3.0.json +768 -0
  119. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.4.0.json +808 -0
  120. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.5.0.json +808 -0
  121. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.6.0.json +808 -0
  122. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.7.0.json +825 -0
  123. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.8.0.json +792 -0
  124. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.9.0.json +809 -0
  125. package/src/assets/adapters/reddit/wrench-web-adapter.json +855 -0
  126. package/src/assets/adapters/reddit/wrench-web-adapter.v1.0.0.json +577 -0
  127. package/src/assets/adapters/reddit/wrench-web-adapter.v1.1.0.json +604 -0
  128. package/src/assets/adapters/reddit/wrench-web-adapter.v1.10.0.json +712 -0
  129. package/src/assets/adapters/reddit/wrench-web-adapter.v1.11.0.json +712 -0
  130. package/src/assets/adapters/reddit/wrench-web-adapter.v1.12.0.json +712 -0
  131. package/src/assets/adapters/reddit/wrench-web-adapter.v1.2.0.json +653 -0
  132. package/src/assets/adapters/reddit/wrench-web-adapter.v1.3.0.json +712 -0
  133. package/src/assets/adapters/reddit/wrench-web-adapter.v1.4.0.json +713 -0
  134. package/src/assets/adapters/reddit/wrench-web-adapter.v1.5.0.json +712 -0
  135. package/src/assets/adapters/reddit/wrench-web-adapter.v1.6.0.json +713 -0
  136. package/src/assets/adapters/reddit/wrench-web-adapter.v1.7.0.json +713 -0
  137. package/src/assets/adapters/reddit/wrench-web-adapter.v1.8.0.json +713 -0
  138. package/src/assets/adapters/reddit/wrench-web-adapter.v1.9.0.json +713 -0
  139. package/src/assets/adapters/substack/wrench-web-adapter.json +936 -0
  140. package/src/assets/adapters/substack/wrench-web-adapter.v1.0.0.json +817 -0
  141. package/src/assets/adapters/substack/wrench-web-adapter.v1.1.0.json +812 -0
  142. package/src/assets/adapters/substack/wrench-web-adapter.v1.2.0.json +812 -0
  143. package/src/assets/adapters/substack/wrench-web-adapter.v1.3.0.json +902 -0
  144. package/src/assets/adapters/substack/wrench-web-adapter.v1.4.0.json +936 -0
  145. package/src/assets/adapters/substack/wrench-web-adapter.v1.5.0.json +936 -0
  146. package/src/assets/adapters/substack/wrench-web-adapter.v1.6.0.json +936 -0
  147. package/src/assets/adapters/threads/wrench-web-adapter.json +692 -0
  148. package/src/assets/adapters/threads/wrench-web-adapter.v1.0.0.json +629 -0
  149. package/src/assets/adapters/threads/wrench-web-adapter.v1.1.0.json +623 -0
  150. package/src/assets/adapters/threads/wrench-web-adapter.v1.2.0.json +622 -0
  151. package/src/assets/adapters/threads/wrench-web-adapter.v1.3.0.json +622 -0
  152. package/src/assets/adapters/threads/wrench-web-adapter.v1.4.0.json +622 -0
  153. package/src/assets/adapters/threads/wrench-web-adapter.v1.5.0.json +699 -0
  154. package/src/assets/adapters/threads/wrench-web-adapter.v1.6.0.json +693 -0
  155. package/src/assets/adapters/threads/wrench-web-adapter.v1.7.0.json +692 -0
  156. package/src/assets/adapters/tiktok/wrench-web-adapter.json +762 -0
  157. package/src/assets/adapters/tiktok/wrench-web-adapter.v1.0.0.json +681 -0
  158. package/src/assets/adapters/tiktok/wrench-web-adapter.v1.1.0.json +708 -0
  159. package/src/assets/adapters/tiktok/wrench-web-adapter.v1.2.0.json +762 -0
  160. package/src/assets/adapters/tiktok/wrench-web-adapter.v1.3.0.json +762 -0
  161. package/src/assets/adapters/twitch/wrench-web-adapter.json +42 -0
  162. package/src/assets/adapters/twitch/wrench-web-adapter.v1.0.0.json +42 -0
  163. package/src/assets/adapters/whatsapp/wrench-web-adapter.json +400 -0
  164. package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.0.0.json +355 -0
  165. package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.1.0.json +386 -0
  166. package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.2.0.json +386 -0
  167. package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.3.0.json +400 -0
  168. package/src/assets/adapters/x/wrench-adapter.json +749 -0
  169. package/src/assets/adapters/x/wrench-adapter.v1.0.0.json +700 -0
  170. package/src/assets/adapters/x/wrench-adapter.v1.1.0.json +707 -0
  171. package/src/assets/adapters/x/wrench-web-adapter.json +712 -0
  172. package/src/assets/adapters/x/wrench-web-adapter.v1.1.0.json +579 -0
  173. package/src/assets/adapters/x/wrench-web-adapter.v1.10.0.json +685 -0
  174. package/src/assets/adapters/x/wrench-web-adapter.v1.11.0.json +712 -0
  175. package/src/assets/adapters/x/wrench-web-adapter.v1.12.0.json +712 -0
  176. package/src/assets/adapters/x/wrench-web-adapter.v1.13.0.json +712 -0
  177. package/src/assets/adapters/x/wrench-web-adapter.v1.2.0.json +578 -0
  178. package/src/assets/adapters/x/wrench-web-adapter.v1.3.0.json +610 -0
  179. package/src/assets/adapters/x/wrench-web-adapter.v1.4.0.json +617 -0
  180. package/src/assets/adapters/x/wrench-web-adapter.v1.5.0.json +634 -0
  181. package/src/assets/adapters/x/wrench-web-adapter.v1.6.0.json +632 -0
  182. package/src/assets/adapters/x/wrench-web-adapter.v1.7.0.json +649 -0
  183. package/src/assets/adapters/x/wrench-web-adapter.v1.8.0.json +649 -0
  184. package/src/assets/adapters/x/wrench-web-adapter.v1.9.0.json +683 -0
  185. package/src/assets/adapters/youtube/wrench-web-adapter.json +561 -0
  186. package/src/assets/adapters/youtube/wrench-web-adapter.v1.0.0.json +465 -0
  187. package/src/assets/adapters/youtube/wrench-web-adapter.v1.1.0.json +519 -0
  188. package/src/assets/adapters/youtube/wrench-web-adapter.v1.2.0.json +561 -0
  189. package/src/assets/adapters/youtube/wrench-web-adapter.v1.3.0.json +561 -0
  190. package/src/assets/code-owned-provider-template/plugin.test.ts.template +54 -0
  191. package/src/assets/code-owned-provider-template/plugin.ts.template +102 -0
  192. package/src/assets/code-owned-provider-template/runtime.internal.test.ts.template +44 -0
  193. package/src/assets/code-owned-provider-template/runtime.ts.template +59 -0
  194. package/src/assets/code-owned-provider-template/wrench-adapter.json.template +33 -0
  195. package/src/assets/fonts/nebula-sans/LICENSE.txt +96 -0
  196. package/src/assets/fonts/nebula-sans/NebulaSans-Book.woff2 +0 -0
  197. package/src/assets/fonts/nebula-sans/PROVENANCE.md +14 -0
  198. package/src/assets.d.ts +4 -0
  199. package/src/auth.ts +1157 -0
  200. package/src/beeper-client-types.ts +169 -0
  201. package/src/beeper-client.ts +250 -0
  202. package/src/beeper-contact-interactions-cli.ts +180 -0
  203. package/src/beeper-contact-interactions.ts +1612 -0
  204. package/src/beeper-message-bundle-v1.ts +257 -0
  205. package/src/beeper-message-like-me-cli.ts +84 -0
  206. package/src/beeper-message-like-me-export.ts +2274 -0
  207. package/src/beeper-message-like-me-recovery.ts +931 -0
  208. package/src/beeper-message-like-me-source.ts +4034 -0
  209. package/src/browser-admission.ts +928 -0
  210. package/src/browser-snapshots.ts +260 -0
  211. package/src/browser.ts +3873 -0
  212. package/src/canonical-json.ts +60 -0
  213. package/src/catalog-cli.ts +638 -0
  214. package/src/cli.ts +208 -0
  215. package/src/client-types.ts +301 -0
  216. package/src/client.ts +2726 -0
  217. package/src/confirmed-write-failure.ts +44 -0
  218. package/src/confirmed-write-model.ts +72 -0
  219. package/src/confirmed-write-platform.ts +1055 -0
  220. package/src/confirmed-write-program.ts +224 -0
  221. package/src/confirmed-write-runtime.ts +16 -0
  222. package/src/cursor-token.ts +228 -0
  223. package/src/derivation-dnr-readiness.ts +367 -0
  224. package/src/derivation-file-chooser.ts +384 -0
  225. package/src/derivation-network-boundary.ts +835 -0
  226. package/src/derivation-network-guard.ts +782 -0
  227. package/src/derivation-network-proxy-helper.ts +440 -0
  228. package/src/derivation-network-proxy.ts +385 -0
  229. package/src/derive-command-helper.ts +693 -0
  230. package/src/derive-fixtures.ts +343 -0
  231. package/src/derive-review.ts +980 -0
  232. package/src/derive.ts +4754 -0
  233. package/src/fixtures/beeper-message-like-me-v1/accounts.ndjson +2 -0
  234. package/src/fixtures/beeper-message-like-me-v1/conversations.ndjson +1 -0
  235. package/src/fixtures/beeper-message-like-me-v1/manifest.json +1 -0
  236. package/src/fixtures/beeper-message-like-me-v1/messages.ndjson +2 -0
  237. package/src/fixtures/beeper-message-like-me-v1/participants.ndjson +3 -0
  238. package/src/fixtures/beeper-message-like-me-v1/reactions.ndjson +1 -0
  239. package/src/fixtures/beeper-message-like-me-v1/tombstones.ndjson +1 -0
  240. package/src/ghostget.ts +3672 -0
  241. package/src/gmail-capture.ts +1244 -0
  242. package/src/har-internal.ts +1242 -0
  243. package/src/har.ts +743 -0
  244. package/src/index.ts +218 -0
  245. package/src/invocation-read-platform.ts +55 -0
  246. package/src/invocation-read-program.ts +93 -0
  247. package/src/invocation-read-runtime.ts +11 -0
  248. package/src/linked-device-lifecycle-admission.ts +450 -0
  249. package/src/linked-device-lifecycle-journal.ts +1560 -0
  250. package/src/linked-device-lifecycle-runtime.ts +1602 -0
  251. package/src/local-cli-admission.ts +135 -0
  252. package/src/local-cli-contracts.ts +271 -0
  253. package/src/local-cli-execution.ts +98 -0
  254. package/src/local-cli-surface-contract.ts +1510 -0
  255. package/src/local-cli-tool-identity.ts +336 -0
  256. package/src/media/archive.ts +2325 -0
  257. package/src/media/args.ts +303 -0
  258. package/src/media/cli.ts +287 -0
  259. package/src/media/doctor.ts +475 -0
  260. package/src/media/ffmpeg.ts +1330 -0
  261. package/src/media/http-capture.ts +711 -0
  262. package/src/media/http-probe.ts +593 -0
  263. package/src/media/http.ts +387 -0
  264. package/src/media/index.ts +10 -0
  265. package/src/media/local-transcription.ts +613 -0
  266. package/src/media/lock.ts +321 -0
  267. package/src/media/manifest.ts +1712 -0
  268. package/src/media/metadata.ts +906 -0
  269. package/src/media/process.ts +723 -0
  270. package/src/media/revision.ts +166 -0
  271. package/src/media/runtime-closure.ts +1063 -0
  272. package/src/media/source-router.ts +187 -0
  273. package/src/media/transcriber-config.ts +1122 -0
  274. package/src/media/transcript-persistence-model.ts +15 -0
  275. package/src/media/transcript-persistence-platform.ts +89 -0
  276. package/src/media/transcript-persistence-program.ts +39 -0
  277. package/src/media/transcript-persistence-runtime.ts +24 -0
  278. package/src/media/transcript.ts +616 -0
  279. package/src/media/utf8-order.ts +30 -0
  280. package/src/media/whisper-cpp.ts +464 -0
  281. package/src/media/whisper-language.ts +58 -0
  282. package/src/media/yt-dlp.ts +439 -0
  283. package/src/message-like-me-agentic-messaging.ts +882 -0
  284. package/src/messaging-action-store.ts +793 -0
  285. package/src/messaging-runtime.ts +2136 -0
  286. package/src/messaging-store.ts +539 -0
  287. package/src/messaging-types.ts +1663 -0
  288. package/src/messaging.ts +334 -0
  289. package/src/model.ts +2671 -0
  290. package/src/oauth-google.ts +888 -0
  291. package/src/omni-client-types.ts +306 -0
  292. package/src/omni-client.ts +2225 -0
  293. package/src/omni-limits.ts +34 -0
  294. package/src/omni-model.ts +1614 -0
  295. package/src/omni-request.ts +379 -0
  296. package/src/omni-runtime.ts +1632 -0
  297. package/src/operation-deadline.ts +189 -0
  298. package/src/path-helper.ts +1656 -0
  299. package/src/pinned-https.ts +194 -0
  300. package/src/plan-assets.ts +832 -0
  301. package/src/platform-catalog.ts +1832 -0
  302. package/src/plugins/beeper-linked-device/plugin.ts +310 -0
  303. package/src/plugins/bluesky-web/plugin.ts +178 -0
  304. package/src/plugins/clasificados-web/plugin.ts +66 -0
  305. package/src/plugins/github-web/plugin.ts +80 -0
  306. package/src/plugins/gmail-official/plugin.ts +110 -0
  307. package/src/plugins/hacker-news-web/plugin.ts +52 -0
  308. package/src/plugins/imessage-direct/plugin.ts +195 -0
  309. package/src/plugins/imessage-direct/vendor/0001-fix-keep-AppleScript-send-payloads-out-of-child-argv.patch +1430 -0
  310. package/src/plugins/imessage-direct/vendor/0002-feat-rpc-add-exact-chat-lookup.patch +225 -0
  311. package/src/plugins/imessage-direct/vendor/provenance.json +80 -0
  312. package/src/plugins/linkedin-official/plugin.ts +84 -0
  313. package/src/plugins/linkedin-web/plugin.ts +412 -0
  314. package/src/plugins/meta-web/plugin.ts +317 -0
  315. package/src/plugins/reddit-web/flair.ts +492 -0
  316. package/src/plugins/reddit-web/plugin.ts +477 -0
  317. package/src/plugins/substack-web/plugin.ts +124 -0
  318. package/src/plugins/tiktok-web/plugin.ts +67 -0
  319. package/src/plugins/twitch-web/plugin.ts +54 -0
  320. package/src/plugins/whatsapp-linked-device/plugin.ts +105 -0
  321. package/src/plugins/x-official/plugin.ts +64 -0
  322. package/src/plugins/x-web/plugin.ts +488 -0
  323. package/src/plugins/youtube-web/plugin.ts +101 -0
  324. package/src/portable-run-recovery.ts +466 -0
  325. package/src/process-identity.ts +501 -0
  326. package/src/profile-clone-helper.ts +104 -0
  327. package/src/provider-catalog-views.ts +51 -0
  328. package/src/provider-context.ts +69 -0
  329. package/src/provider-contract-definitions-x.ts +45 -0
  330. package/src/provider-contract-definitions.ts +948 -0
  331. package/src/provider-contract-input-gmail.ts +176 -0
  332. package/src/provider-contract-input-linkedin.ts +77 -0
  333. package/src/provider-contract-input-x.ts +304 -0
  334. package/src/provider-contract-planning.ts +23 -0
  335. package/src/provider-contract-semantic-identity.ts +54 -0
  336. package/src/provider-contracts.ts +251 -0
  337. package/src/provider-http.ts +723 -0
  338. package/src/provider-plugin-auth.ts +36 -0
  339. package/src/provider-plugin-builtins.ts +229 -0
  340. package/src/provider-plugin-cleanup-barrier.ts +229 -0
  341. package/src/provider-plugin-cleanup-execution.ts +79 -0
  342. package/src/provider-plugin-cleanup-resource.ts +470 -0
  343. package/src/provider-plugin-contract-identity.ts +707 -0
  344. package/src/provider-plugin-host.ts +1972 -0
  345. package/src/provider-plugin-identifiers.ts +60 -0
  346. package/src/provider-plugin-invocation-lease.ts +889 -0
  347. package/src/provider-plugin-lifecycle-kernel.ts +566 -0
  348. package/src/provider-plugin-lifecycle.ts +1455 -0
  349. package/src/provider-plugin-module-analysis.ts +336 -0
  350. package/src/provider-plugin-package.ts +2530 -0
  351. package/src/provider-plugin-portable-authority.ts +106 -0
  352. package/src/provider-plugin-portable-catalog.ts +94 -0
  353. package/src/provider-plugin-portable-identity.ts +315 -0
  354. package/src/provider-plugin-portable-runtime.ts +2057 -0
  355. package/src/provider-plugin-protocol.ts +1625 -0
  356. package/src/provider-plugin-registry.ts +3342 -0
  357. package/src/provider-plugin-store.ts +2155 -0
  358. package/src/provider-plugin-test-fixture.ts +5 -0
  359. package/src/provider-plugin.ts +4907 -0
  360. package/src/provider-plugins.generated.ts +43 -0
  361. package/src/provider-plugins.ts +8 -0
  362. package/src/provider-subject.ts +41 -0
  363. package/src/provider.ts +315 -0
  364. package/src/providers/beeper-local-runtime.ts +5426 -0
  365. package/src/providers/beeper-local.ts +2739 -0
  366. package/src/providers/beeper-messaging.ts +462 -0
  367. package/src/providers/beeper-omni.ts +1159 -0
  368. package/src/providers/bluesky-web-runtime.ts +2895 -0
  369. package/src/providers/bluesky-web.ts +1614 -0
  370. package/src/providers/clasificados-web-runtime.ts +255 -0
  371. package/src/providers/clasificados-web.ts +503 -0
  372. package/src/providers/contact-projection.ts +301 -0
  373. package/src/providers/github-read-model.ts +94 -0
  374. package/src/providers/github-read-platform.ts +165 -0
  375. package/src/providers/github-read-program.ts +97 -0
  376. package/src/providers/github-web-runtime.ts +320 -0
  377. package/src/providers/github-web.ts +403 -0
  378. package/src/providers/gmail-api.ts +2834 -0
  379. package/src/providers/gmail-omni.ts +830 -0
  380. package/src/providers/gmail.ts +1130 -0
  381. package/src/providers/hacker-news-web-runtime.ts +220 -0
  382. package/src/providers/hacker-news-web.ts +809 -0
  383. package/src/providers/imessage-direct-install.ts +354 -0
  384. package/src/providers/imessage-direct-messaging.ts +532 -0
  385. package/src/providers/imessage-direct-omni.ts +400 -0
  386. package/src/providers/imessage-direct-runtime.ts +1498 -0
  387. package/src/providers/imessage-direct.ts +351 -0
  388. package/src/providers/instagram-video-foundations.ts +913 -0
  389. package/src/providers/instagram-web-profile-browser.ts +600 -0
  390. package/src/providers/iso-bmff.ts +338 -0
  391. package/src/providers/linkedin-company-platform.ts +42 -0
  392. package/src/providers/linkedin-company-program.ts +88 -0
  393. package/src/providers/linkedin-contact-failure.ts +84 -0
  394. package/src/providers/linkedin-contact-platform.ts +45 -0
  395. package/src/providers/linkedin-contact-program.ts +91 -0
  396. package/src/providers/linkedin-profile-activity-failure.ts +76 -0
  397. package/src/providers/linkedin-profile-activity-platform.ts +41 -0
  398. package/src/providers/linkedin-profile-activity-program.ts +67 -0
  399. package/src/providers/linkedin-read-failure.ts +52 -0
  400. package/src/providers/linkedin-self-platform.ts +53 -0
  401. package/src/providers/linkedin-self-program.ts +122 -0
  402. package/src/providers/linkedin-web-article-browser.ts +794 -0
  403. package/src/providers/linkedin-web-bootstrap.ts +146 -0
  404. package/src/providers/linkedin-web-contact.ts +830 -0
  405. package/src/providers/linkedin-web-feed-browser.ts +832 -0
  406. package/src/providers/linkedin-web-feed.ts +864 -0
  407. package/src/providers/linkedin-web-post-browser.ts +653 -0
  408. package/src/providers/linkedin-web-profile-browser.ts +683 -0
  409. package/src/providers/linkedin-web-runtime.ts +2388 -0
  410. package/src/providers/linkedin-web.ts +3305 -0
  411. package/src/providers/linkedin.ts +1885 -0
  412. package/src/providers/meta-bootstrap.ts +1043 -0
  413. package/src/providers/meta-facebook-group.ts +690 -0
  414. package/src/providers/meta-marketplace-relay.ts +747 -0
  415. package/src/providers/meta-omni.ts +373 -0
  416. package/src/providers/meta-relay-bundle.ts +1049 -0
  417. package/src/providers/meta-web-descriptors.ts +1740 -0
  418. package/src/providers/meta-web-runtime.ts +3388 -0
  419. package/src/providers/meta-web.ts +2931 -0
  420. package/src/providers/read-failure.ts +139 -0
  421. package/src/providers/reddit-omni.ts +462 -0
  422. package/src/providers/reddit-web-runtime.ts +1929 -0
  423. package/src/providers/reddit-web.ts +1611 -0
  424. package/src/providers/rental-listings.ts +460 -0
  425. package/src/providers/substack-omni.ts +406 -0
  426. package/src/providers/substack-video-mp4.ts +33 -0
  427. package/src/providers/substack-web-runtime.ts +2812 -0
  428. package/src/providers/substack-web.ts +1502 -0
  429. package/src/providers/tiktok-video-mp4.ts +33 -0
  430. package/src/providers/tiktok-web-runtime.ts +770 -0
  431. package/src/providers/tiktok-web.ts +2786 -0
  432. package/src/providers/twitch-web-runtime.ts +226 -0
  433. package/src/providers/twitch-web.ts +274 -0
  434. package/src/providers/whatsapp-account-identity.ts +55 -0
  435. package/src/providers/whatsapp-contact-projection-helper.ts +874 -0
  436. package/src/providers/whatsapp-contact-projection-protocol.ts +403 -0
  437. package/src/providers/whatsapp-interaction-projection-helper.ts +1452 -0
  438. package/src/providers/whatsapp-interaction-projection-protocol.ts +462 -0
  439. package/src/providers/whatsapp-message-export-projection-protocol.ts +532 -0
  440. package/src/providers/whatsapp-messaging.ts +132 -0
  441. package/src/providers/whatsapp-omni.ts +586 -0
  442. package/src/providers/whatsapp-web-runtime.ts +3108 -0
  443. package/src/providers/whatsapp-web.ts +1037 -0
  444. package/src/providers/x-image-provenance.ts +523 -0
  445. package/src/providers/x-made-with-ai.ts +106 -0
  446. package/src/providers/x-transaction-id.ts +413 -0
  447. package/src/providers/x-web-runtime.ts +3485 -0
  448. package/src/providers/x-web.ts +2332 -0
  449. package/src/providers/x.ts +1889 -0
  450. package/src/providers/youtube-web-runtime.ts +1400 -0
  451. package/src/providers/youtube-web.ts +1221 -0
  452. package/src/read-admission-runtime.ts +91 -0
  453. package/src/read-client.ts +521 -0
  454. package/src/read-effect-platform.ts +7 -0
  455. package/src/read-effect-runtime.ts +14 -0
  456. package/src/read-effect.ts +52 -0
  457. package/src/read-projection-admission.ts +866 -0
  458. package/src/read-projections.ts +3151 -0
  459. package/src/recovery.ts +1492 -0
  460. package/src/reviewed-template.ts +146 -0
  461. package/src/run-journal.ts +1272 -0
  462. package/src/runtime.ts +5116 -0
  463. package/src/scripts/generate-provider-plugin-catalog.ts +121 -0
  464. package/src/scripts/install-whatsapp-protocol.sh +232 -0
  465. package/src/scripts/recover-install-stages.ts +1491 -0
  466. package/src/scripts/resolve-state-home.ts +30 -0
  467. package/src/scripts/scaffold-web-provider.ts +973 -0
  468. package/src/scripts/sync-bundled-adapters.ts +695 -0
  469. package/src/session-secrets.ts +1183 -0
  470. package/src/state-helper.bunfig.toml +2 -0
  471. package/src/state-helper.ts +2914 -0
  472. package/src/storage.ts +3842 -0
  473. package/src/transport-policy.ts +2 -0
  474. package/src/usage.ts +182 -0
  475. package/src/version.ts +2 -0
  476. package/src/web-session-authentication-policy.ts +136 -0
  477. package/src/web-session-cleanup-admission.ts +2344 -0
  478. package/src/web-session-cleanup-join.ts +99 -0
  479. package/src/web-session-client.ts +996 -0
  480. package/src/web-session-contract-definitions.ts +820 -0
  481. package/src/web-session-contract-planning.ts +42 -0
  482. package/src/web-session-contracts.ts +228 -0
  483. package/src/web-session-cookies.ts +500 -0
  484. package/src/web-session-execution.ts +385 -0
  485. package/src/web-session-http.ts +373 -0
  486. package/src/web-session-read-errors.ts +19 -0
  487. package/src/web-session-read-runtime.ts +124 -0
  488. package/src/web-session-recovery.ts +818 -0
  489. package/src/web-session-sites.ts +58 -0
  490. package/src/web-session-template.ts +994 -0
  491. package/src/web-session.ts +93 -0
  492. package/src/whatsapp-client-binding.ts +21 -0
  493. package/src/whatsapp-client-types.ts +75 -0
  494. package/src/whatsapp-client.ts +395 -0
  495. package/src/whatsapp-export-coordinate.ts +17 -0
  496. package/src/whatsapp-message-bundle-v2.ts +195 -0
  497. package/src/whatsapp-message-like-me-cli.ts +195 -0
  498. package/src/whatsapp-message-like-me-export.ts +1072 -0
  499. package/src/whatsapp-message-like-me-source.ts +1012 -0
  500. package/tsconfig.json +67 -0
@@ -0,0 +1,1614 @@
1
+ import { types as nodeTypes } from "node:util";
2
+
3
+ import { canonicalJson, sha256 } from "./canonical-json";
4
+
5
+ export const OMNI_SCHEMA_VERSION = 1 as const;
6
+ export const OMNI_MAX_PAGE_ENTITIES = 1_000;
7
+ export const OMNI_MAX_SOURCE_ENTITIES = 5_000;
8
+ export const OMNI_MAX_SOURCE_PAGES = 256;
9
+ export const OMNI_MAX_COMPLETE_COVERAGE = 256;
10
+ export const OMNI_MAX_NORMALIZATION_FRONTIERS = 512;
11
+ export const OMNI_MAX_TOMBSTONES = 5_000;
12
+ export const OMNI_MAX_SOURCE_BYTES = 12 * 1024 * 1024;
13
+
14
+ /**
15
+ * A provider-local shape diagnostic retained only inside encrypted normalized
16
+ * state. Public boundaries must replace it with a categorical reason because
17
+ * third-party materializers and foreign property names are not trusted text.
18
+ */
19
+ export class OmniMaterializerDriftError extends Error {
20
+ override readonly name = "OmniMaterializerDriftError";
21
+ readonly provider: string;
22
+ readonly path: string;
23
+ readonly diagnostic: string;
24
+
25
+ constructor(
26
+ provider: string,
27
+ path: string,
28
+ diagnostic: string,
29
+ ) {
30
+ super(`${provider} omni ${path} ${diagnostic}`);
31
+ this.provider = provider;
32
+ this.path = path;
33
+ this.diagnostic = diagnostic;
34
+ }
35
+ }
36
+
37
+ export type OmniEntityKindV1 = "conversation" | "message" | "notification";
38
+ export type OmniCompletenessKindV1 =
39
+ | "complete"
40
+ | "page"
41
+ | "unknown"
42
+ | "first-page-only"
43
+ | "bounded-local"
44
+ | "search-window"
45
+ | "truncated";
46
+
47
+ export type OmniJsonValue =
48
+ | null
49
+ | boolean
50
+ | number
51
+ | string
52
+ | readonly OmniJsonValue[]
53
+ | { readonly [key: string]: OmniJsonValue };
54
+
55
+ export type OmniParticipantV1 = {
56
+ readonly providerId: string | null;
57
+ readonly displayName: string | null;
58
+ readonly handle: string | null;
59
+ };
60
+
61
+ export type OmniAttachmentV1 = {
62
+ readonly kind: "audio" | "document" | "image" | "link" | "sticker" | "video" | "unknown";
63
+ readonly mimeType: string | null;
64
+ readonly name: string | null;
65
+ readonly sizeBytes: number | null;
66
+ };
67
+
68
+ type ProviderEntityCommonV1 = {
69
+ readonly providerId: string;
70
+ readonly providerRevision: string | null;
71
+ readonly orderedAt: string | null;
72
+ };
73
+
74
+ export type ProviderConversationV1 = ProviderEntityCommonV1 & {
75
+ readonly kind: "conversation";
76
+ /** Added after the original V1 shape; absence means the provider did not classify it. */
77
+ readonly conversationKind?: "single" | "group" | "unknown";
78
+ readonly detail: "summary" | "full";
79
+ readonly title: string | null;
80
+ readonly summary: string | null;
81
+ readonly participants: readonly OmniParticipantV1[];
82
+ /** Independent provider marker; may be true even when unreadCount is zero. */
83
+ readonly unread: boolean | null;
84
+ readonly unreadCount: number | null;
85
+ readonly archived: boolean | null;
86
+ readonly pending: boolean | null;
87
+ };
88
+
89
+ export type ProviderMessageV1 = ProviderEntityCommonV1 & {
90
+ readonly kind: "message";
91
+ readonly conversationProviderId: string | null;
92
+ readonly sender: OmniParticipantV1 | null;
93
+ readonly recipients: readonly OmniParticipantV1[];
94
+ readonly direction: "incoming" | "outgoing" | "unknown";
95
+ readonly subject: string | null;
96
+ readonly body: string | null;
97
+ /** Present when a materializer can prove whether body is a bounded prefix. */
98
+ readonly bodyTruncated?: boolean;
99
+ readonly unread: boolean | null;
100
+ readonly replyToProviderId: string | null;
101
+ readonly state: "active" | "revoked" | "deleted-for-me" | "revoked-and-deleted-for-me";
102
+ readonly attachments: readonly OmniAttachmentV1[];
103
+ };
104
+
105
+ export type ProviderNotificationV1 = ProviderEntityCommonV1 & {
106
+ readonly kind: "notification";
107
+ readonly actor: OmniParticipantV1 | null;
108
+ readonly subject: string | null;
109
+ readonly body: string | null;
110
+ readonly unread: boolean | null;
111
+ readonly context: string | null;
112
+ };
113
+
114
+ export type ProviderMaterializedEntityV1 =
115
+ | ProviderConversationV1
116
+ | ProviderMessageV1
117
+ | ProviderNotificationV1;
118
+
119
+ export type ProviderMaterializedPageV1 = {
120
+ readonly schemaVersion: 1;
121
+ readonly partition: string;
122
+ readonly completeness: {
123
+ readonly kind: OmniCompletenessKindV1;
124
+ readonly reason: string | null;
125
+ };
126
+ readonly cursor: {
127
+ readonly direction: "forward" | "backward" | "none";
128
+ /** Opaque provider cursor used to request this page, retained only in ciphertext. */
129
+ readonly request: string | null;
130
+ /** Exact validated provider input for the next page, retained only in ciphertext. */
131
+ readonly nextInput: Readonly<Record<string, OmniJsonValue>> | null;
132
+ };
133
+ readonly entities: readonly ProviderMaterializedEntityV1[];
134
+ readonly tombstones: readonly {
135
+ readonly kind: OmniEntityKindV1;
136
+ readonly providerId: string;
137
+ readonly providerRevision: string | null;
138
+ }[];
139
+ };
140
+
141
+ export type OmniSourceIdentityV1 = {
142
+ readonly schemaVersion: 1;
143
+ readonly adapter: {
144
+ readonly id: string;
145
+ readonly version: string;
146
+ readonly hash: string;
147
+ };
148
+ readonly plugin: {
149
+ readonly id: string;
150
+ readonly version: string;
151
+ readonly closureHash: string;
152
+ };
153
+ readonly surfaceId: string;
154
+ readonly auth: {
155
+ readonly id: string;
156
+ readonly kind: string;
157
+ readonly hash: string;
158
+ readonly subject: string;
159
+ };
160
+ };
161
+
162
+ export type OmniPageProvenanceV1 = {
163
+ readonly operation: "messaging.list" | "messaging.read";
164
+ readonly inputHash: string;
165
+ readonly exactQueryKey: string;
166
+ readonly exactDataRevision: string;
167
+ readonly validatedAt: string;
168
+ readonly startedAt: string;
169
+ readonly finishedAt: string;
170
+ readonly runId: string;
171
+ readonly materializerId: string;
172
+ readonly materializerVersion: number;
173
+ };
174
+
175
+ export type OmniEntityV1 = ProviderMaterializedEntityV1 & {
176
+ /** Account-lifetime-scoped opaque Ghostget identity. */
177
+ readonly id: string;
178
+ /** Canonical revision of the normalized semantic bytes. */
179
+ readonly revision: string;
180
+ readonly source: {
181
+ readonly surfaceId: string;
182
+ readonly authId: string;
183
+ readonly providerId: string;
184
+ };
185
+ readonly conversationId: string | null;
186
+ };
187
+
188
+ export type OmniStoredEntityV1 = {
189
+ readonly entity: OmniEntityV1;
190
+ /** Partitions where this identity has been observed since their latest coverage reset. */
191
+ readonly partitions: readonly string[];
192
+ readonly observationOrder: string;
193
+ readonly provenance: OmniPageProvenanceV1;
194
+ };
195
+
196
+ export type OmniStoredPageV1 = {
197
+ readonly key: string;
198
+ readonly partition: string;
199
+ readonly completeness: ProviderMaterializedPageV1["completeness"];
200
+ readonly cursor: ProviderMaterializedPageV1["cursor"];
201
+ readonly orderedEntityIds: readonly string[];
202
+ /** Explicit deletion observations, including their provider revision, are membership. */
203
+ readonly tombstones: readonly {
204
+ readonly id: string;
205
+ readonly kind: OmniEntityKindV1;
206
+ readonly providerId: string;
207
+ readonly providerRevision: string | null;
208
+ }[];
209
+ readonly observationOrder: string;
210
+ readonly provenance: OmniPageProvenanceV1;
211
+ };
212
+
213
+ export type OmniCompleteCoverageV1 = {
214
+ readonly partition: string;
215
+ readonly pageKey: string;
216
+ /** Revision of the complete stored page, including entity and tombstone membership. */
217
+ readonly pageRevision: string;
218
+ readonly observationOrder: string;
219
+ readonly provenance: OmniPageProvenanceV1;
220
+ };
221
+
222
+ export type OmniStoredTombstoneV1 = {
223
+ readonly id: string;
224
+ readonly kind: OmniEntityKindV1;
225
+ readonly providerId: string;
226
+ readonly providerRevision: string | null;
227
+ readonly observationOrder: string;
228
+ readonly provenance: OmniPageProvenanceV1;
229
+ };
230
+
231
+ export type OmniNormalizationStatusV1 =
232
+ | {
233
+ readonly state: "ready";
234
+ readonly lastGoodAt: string;
235
+ readonly exactDataRevision: string;
236
+ }
237
+ | {
238
+ readonly state: "drift";
239
+ readonly failedAt: string;
240
+ readonly exactDataRevision: string;
241
+ readonly code: "materializer-drift" | "capacity-exceeded";
242
+ readonly message: string;
243
+ readonly lastGoodAt: string | null;
244
+ readonly lastGoodExactDataRevision: string | null;
245
+ };
246
+
247
+ export type OmniNormalizationFrontierV1 = {
248
+ readonly exactQueryKey: string;
249
+ readonly status: OmniNormalizationStatusV1;
250
+ };
251
+
252
+ export type OmniSourceStateV1 = {
253
+ readonly schemaVersion: 1;
254
+ readonly source: OmniSourceIdentityV1;
255
+ readonly entities: readonly OmniStoredEntityV1[];
256
+ readonly pages: readonly OmniStoredPageV1[];
257
+ readonly completeCoverage: readonly OmniCompleteCoverageV1[];
258
+ readonly tombstones: readonly OmniStoredTombstoneV1[];
259
+ readonly normalization: readonly OmniNormalizationFrontierV1[];
260
+ };
261
+
262
+ type JsonRecord = Readonly<Record<string, unknown>>;
263
+
264
+ function fail(label: string, message: string): never {
265
+ throw new Error(`${label} ${message}`);
266
+ }
267
+
268
+ function record(value: unknown, label: string): JsonRecord {
269
+ if (
270
+ nodeTypes.isProxy(value)
271
+ || typeof value !== "object"
272
+ || value === null
273
+ || Array.isArray(value)
274
+ || (
275
+ Object.getPrototypeOf(value) !== Object.prototype
276
+ && Object.getPrototypeOf(value) !== null
277
+ )
278
+ ) return fail(label, "must be a plain non-proxy object");
279
+ const descriptors = Object.getOwnPropertyDescriptors(value);
280
+ const keys = Reflect.ownKeys(descriptors);
281
+ if (keys.some((key) => typeof key !== "string")) {
282
+ return fail(label, "must not contain symbol properties");
283
+ }
284
+ for (const key of keys as string[]) {
285
+ const descriptor = descriptors[key];
286
+ if (
287
+ descriptor === undefined
288
+ || !("value" in descriptor)
289
+ || !descriptor.enumerable
290
+ ) return fail(`${label}.${key}`, "must be an enumerable data property");
291
+ }
292
+ return value as JsonRecord;
293
+ }
294
+
295
+ function exactKeys(
296
+ value: JsonRecord,
297
+ keys: readonly string[],
298
+ label: string,
299
+ ): void {
300
+ const actual = Object.keys(value).sort();
301
+ const expected = [...keys].sort();
302
+ if (
303
+ actual.length !== expected.length
304
+ || actual.some((key, index) => key !== expected[index])
305
+ ) return fail(label, `must contain exactly ${expected.join(", ")}`);
306
+ }
307
+
308
+ function array(value: unknown, label: string, maximum: number): readonly unknown[] {
309
+ if (
310
+ nodeTypes.isProxy(value)
311
+ || !Array.isArray(value)
312
+ || Object.getPrototypeOf(value) !== Array.prototype
313
+ || value.length > maximum
314
+ ) return fail(label, `must be a plain array of at most ${maximum} items`);
315
+ const descriptors = Object.getOwnPropertyDescriptors(value);
316
+ const keys = Reflect.ownKeys(descriptors);
317
+ const expected = new Set<PropertyKey>([
318
+ "length",
319
+ ...Array.from({ length: value.length }, (_unused, index) => String(index)),
320
+ ]);
321
+ if (keys.length !== expected.size || keys.some((key) => !expected.has(key))) {
322
+ return fail(label, "must be a dense array without named properties");
323
+ }
324
+ const snapshot: unknown[] = [];
325
+ for (let index = 0; index < value.length; index += 1) {
326
+ const descriptor = descriptors[String(index)];
327
+ if (
328
+ descriptor === undefined
329
+ || !("value" in descriptor)
330
+ || !descriptor.enumerable
331
+ ) return fail(`${label}[${index}]`, "must be an enumerable data property");
332
+ snapshot.push(descriptor.value);
333
+ }
334
+ return Object.freeze(snapshot);
335
+ }
336
+
337
+ function text(
338
+ value: unknown,
339
+ label: string,
340
+ maximum: number,
341
+ allowEmpty = false,
342
+ ): string {
343
+ if (
344
+ typeof value !== "string"
345
+ || Buffer.byteLength(value, "utf8") > maximum
346
+ || (!allowEmpty && value.length === 0)
347
+ || [...value].some((character) => {
348
+ const code = character.codePointAt(0) ?? -1;
349
+ return code === 0
350
+ || code === 8
351
+ || code === 11
352
+ || code === 12
353
+ || (code >= 14 && code <= 31)
354
+ || code === 127;
355
+ })
356
+ ) return fail(label, "must be bounded text");
357
+ return value;
358
+ }
359
+
360
+ function nullableText(
361
+ value: unknown,
362
+ label: string,
363
+ maximum: number,
364
+ allowEmpty = false,
365
+ ): string | null {
366
+ return value === null ? null : text(value, label, maximum, allowEmpty);
367
+ }
368
+
369
+ function integer(value: unknown, label: string, minimum: number, maximum: number): number {
370
+ if (
371
+ typeof value !== "number"
372
+ || !Number.isSafeInteger(value)
373
+ || value < minimum
374
+ || value > maximum
375
+ ) return fail(label, `must be an integer from ${minimum} through ${maximum}`);
376
+ return value;
377
+ }
378
+
379
+ function nullableInteger(
380
+ value: unknown,
381
+ label: string,
382
+ minimum: number,
383
+ maximum: number,
384
+ ): number | null {
385
+ return value === null ? null : integer(value, label, minimum, maximum);
386
+ }
387
+
388
+ function nullableBoolean(value: unknown, label: string): boolean | null {
389
+ if (value === null || typeof value === "boolean") return value;
390
+ return fail(label, "must be boolean or null");
391
+ }
392
+
393
+ function boolean(value: unknown, label: string): boolean {
394
+ if (typeof value === "boolean") return value;
395
+ return fail(label, "must be boolean");
396
+ }
397
+
398
+ function digest(value: unknown, label: string): string {
399
+ const result = text(value, label, 64);
400
+ if (!/^[a-f0-9]{64}$/u.test(result)) return fail(label, "must be a SHA-256 digest");
401
+ return result;
402
+ }
403
+
404
+ function timestamp(value: unknown, label: string): string {
405
+ const result = text(value, label, 64);
406
+ const parsed = new Date(result);
407
+ if (!Number.isFinite(parsed.getTime()) || parsed.toISOString() !== result) {
408
+ return fail(label, "must be a canonical ISO timestamp");
409
+ }
410
+ return result;
411
+ }
412
+
413
+ function nullableTimestamp(value: unknown, label: string): string | null {
414
+ return value === null ? null : timestamp(value, label);
415
+ }
416
+
417
+ function uuid(value: unknown, label: string): string {
418
+ const result = text(value, label, 36);
419
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u.test(result)) {
420
+ return fail(label, "must be a lowercase UUIDv4");
421
+ }
422
+ return result;
423
+ }
424
+
425
+ function oneOf<const T extends string>(
426
+ value: unknown,
427
+ values: readonly T[],
428
+ label: string,
429
+ ): T {
430
+ if (typeof value !== "string" || !values.includes(value as T)) {
431
+ return fail(label, `must be one of ${values.join(", ")}`);
432
+ }
433
+ return value as T;
434
+ }
435
+
436
+ function cloneJson(
437
+ value: unknown,
438
+ label: string,
439
+ depth = 0,
440
+ nodes = { count: 0 },
441
+ ): OmniJsonValue {
442
+ nodes.count += 1;
443
+ if (depth > 16 || nodes.count > 10_000) return fail(label, "exceeds its JSON bound");
444
+ if (value === null || typeof value === "boolean" || typeof value === "string") {
445
+ return typeof value === "string" ? text(value, label, 64 * 1024, true) : value;
446
+ }
447
+ if (typeof value === "number") {
448
+ if (!Number.isFinite(value)) return fail(label, "must contain only finite numbers");
449
+ return value;
450
+ }
451
+ if (Array.isArray(value)) {
452
+ return Object.freeze(array(value, label, 1_000).map((entry, index) =>
453
+ cloneJson(entry, `${label}[${index}]`, depth + 1, nodes)));
454
+ }
455
+ const source = record(value, label);
456
+ const keys = Object.keys(source).sort();
457
+ if (keys.length > 1_000) return fail(label, "has too many properties");
458
+ const result: Record<string, OmniJsonValue> = Object.create(null) as Record<string, OmniJsonValue>;
459
+ for (const key of keys) {
460
+ text(key, `${label} key`, 256);
461
+ result[key] = cloneJson(source[key], `${label}.${key}`, depth + 1, nodes);
462
+ }
463
+ return Object.freeze(result);
464
+ }
465
+
466
+ function jsonRecord(value: unknown, label: string): Readonly<Record<string, OmniJsonValue>> {
467
+ const cloned = cloneJson(value, label);
468
+ return record(cloned, label) as Readonly<Record<string, OmniJsonValue>>;
469
+ }
470
+
471
+ function participant(value: unknown, label: string): OmniParticipantV1 {
472
+ const source = record(value, label);
473
+ exactKeys(source, ["providerId", "displayName", "handle"], label);
474
+ return Object.freeze({
475
+ providerId: nullableText(source.providerId, `${label}.providerId`, 1_024),
476
+ displayName: nullableText(source.displayName, `${label}.displayName`, 2_048, true),
477
+ handle: nullableText(source.handle, `${label}.handle`, 512),
478
+ });
479
+ }
480
+
481
+ function attachment(value: unknown, label: string): OmniAttachmentV1 {
482
+ const source = record(value, label);
483
+ exactKeys(source, ["kind", "mimeType", "name", "sizeBytes"], label);
484
+ return Object.freeze({
485
+ kind: oneOf(source.kind, ["audio", "document", "image", "link", "sticker", "video", "unknown"] as const, `${label}.kind`),
486
+ mimeType: nullableText(source.mimeType, `${label}.mimeType`, 256),
487
+ name: nullableText(source.name, `${label}.name`, 2_048, true),
488
+ sizeBytes: nullableInteger(source.sizeBytes, `${label}.sizeBytes`, 0, Number.MAX_SAFE_INTEGER),
489
+ });
490
+ }
491
+
492
+ function entity(value: unknown, label: string): ProviderMaterializedEntityV1 {
493
+ const source = record(value, label);
494
+ const kind = oneOf(source.kind, ["conversation", "message", "notification"] as const, `${label}.kind`);
495
+ const common = {
496
+ kind,
497
+ providerId: text(source.providerId, `${label}.providerId`, 1_024),
498
+ providerRevision: nullableText(source.providerRevision, `${label}.providerRevision`, 1_024),
499
+ orderedAt: nullableTimestamp(source.orderedAt, `${label}.orderedAt`),
500
+ } as const;
501
+ if (kind === "conversation") {
502
+ const hasConversationKind = Object.hasOwn(source, "conversationKind");
503
+ exactKeys(source, [
504
+ "kind", "providerId", "providerRevision", "orderedAt",
505
+ ...(hasConversationKind ? ["conversationKind"] : []), "detail", "title",
506
+ "summary", "participants", "unread", "unreadCount", "archived", "pending",
507
+ ], label);
508
+ return Object.freeze({
509
+ ...common,
510
+ kind,
511
+ ...(hasConversationKind
512
+ ? {
513
+ conversationKind: oneOf(
514
+ source.conversationKind,
515
+ ["single", "group", "unknown"] as const,
516
+ `${label}.conversationKind`,
517
+ ),
518
+ }
519
+ : {}),
520
+ detail: oneOf(source.detail, ["summary", "full"] as const, `${label}.detail`),
521
+ title: nullableText(source.title, `${label}.title`, 4_096, true),
522
+ summary: nullableText(source.summary, `${label}.summary`, 64 * 1024, true),
523
+ participants: Object.freeze(array(source.participants, `${label}.participants`, 256)
524
+ .map((entry, index) => participant(entry, `${label}.participants[${index}]`))),
525
+ unread: nullableBoolean(source.unread, `${label}.unread`),
526
+ unreadCount: nullableInteger(source.unreadCount, `${label}.unreadCount`, 0, Number.MAX_SAFE_INTEGER),
527
+ archived: nullableBoolean(source.archived, `${label}.archived`),
528
+ pending: nullableBoolean(source.pending, `${label}.pending`),
529
+ });
530
+ }
531
+ if (kind === "message") {
532
+ const hasBodyTruncated = Object.hasOwn(source, "bodyTruncated");
533
+ exactKeys(source, [
534
+ "kind", "providerId", "providerRevision", "orderedAt", "conversationProviderId",
535
+ "sender", "recipients", "direction", "subject", "body", "unread",
536
+ "replyToProviderId", "state", "attachments",
537
+ ...(hasBodyTruncated ? ["bodyTruncated"] : []),
538
+ ], label);
539
+ const body = nullableText(source.body, `${label}.body`, 256 * 1024, true);
540
+ const bodyTruncated = hasBodyTruncated
541
+ ? boolean(source.bodyTruncated, `${label}.bodyTruncated`)
542
+ : undefined;
543
+ if (body === null && bodyTruncated === true) {
544
+ return fail(`${label}.bodyTruncated`, "cannot be true when body is null");
545
+ }
546
+ return Object.freeze({
547
+ ...common,
548
+ kind,
549
+ conversationProviderId: nullableText(source.conversationProviderId, `${label}.conversationProviderId`, 1_024),
550
+ sender: source.sender === null ? null : participant(source.sender, `${label}.sender`),
551
+ recipients: Object.freeze(array(source.recipients, `${label}.recipients`, 256)
552
+ .map((entry, index) => participant(entry, `${label}.recipients[${index}]`))),
553
+ direction: oneOf(source.direction, ["incoming", "outgoing", "unknown"] as const, `${label}.direction`),
554
+ subject: nullableText(source.subject, `${label}.subject`, 8_192, true),
555
+ body,
556
+ ...(bodyTruncated === undefined ? {} : { bodyTruncated }),
557
+ unread: nullableBoolean(source.unread, `${label}.unread`),
558
+ replyToProviderId: nullableText(source.replyToProviderId, `${label}.replyToProviderId`, 1_024),
559
+ state: oneOf(source.state, ["active", "revoked", "deleted-for-me", "revoked-and-deleted-for-me"] as const, `${label}.state`),
560
+ attachments: Object.freeze(array(source.attachments, `${label}.attachments`, 64)
561
+ .map((entry, index) => attachment(entry, `${label}.attachments[${index}]`))),
562
+ });
563
+ }
564
+ exactKeys(source, [
565
+ "kind", "providerId", "providerRevision", "orderedAt", "actor", "subject",
566
+ "body", "unread", "context",
567
+ ], label);
568
+ return Object.freeze({
569
+ ...common,
570
+ kind,
571
+ actor: source.actor === null ? null : participant(source.actor, `${label}.actor`),
572
+ subject: nullableText(source.subject, `${label}.subject`, 8_192, true),
573
+ body: nullableText(source.body, `${label}.body`, 256 * 1024, true),
574
+ unread: nullableBoolean(source.unread, `${label}.unread`),
575
+ context: nullableText(source.context, `${label}.context`, 8_192),
576
+ });
577
+ }
578
+
579
+ function completeness(value: unknown, label: string): ProviderMaterializedPageV1["completeness"] {
580
+ const source = record(value, label);
581
+ exactKeys(source, ["kind", "reason"], label);
582
+ return Object.freeze({
583
+ kind: oneOf(source.kind, ["complete", "page", "unknown", "first-page-only", "bounded-local", "search-window", "truncated"] as const, `${label}.kind`),
584
+ reason: nullableText(source.reason, `${label}.reason`, 2_048),
585
+ });
586
+ }
587
+
588
+ function cursor(value: unknown, label: string): ProviderMaterializedPageV1["cursor"] {
589
+ const source = record(value, label);
590
+ exactKeys(source, ["direction", "request", "nextInput"], label);
591
+ const direction = oneOf(source.direction, ["forward", "backward", "none"] as const, `${label}.direction`);
592
+ const request = nullableText(source.request, `${label}.request`, 8_192);
593
+ const nextInput = source.nextInput === null
594
+ ? null
595
+ : jsonRecord(source.nextInput, `${label}.nextInput`);
596
+ if (direction === "none" && (request !== null || nextInput !== null)) {
597
+ return fail(label, "cannot retain provider cursors when direction is none");
598
+ }
599
+ return Object.freeze({ direction, request, nextInput });
600
+ }
601
+
602
+ export function parseMaterializedPageV1(value: unknown): ProviderMaterializedPageV1 {
603
+ const source = record(value, "omni materialized page");
604
+ exactKeys(source, [
605
+ "schemaVersion", "partition", "completeness", "cursor", "entities",
606
+ "tombstones",
607
+ ], "omni materialized page");
608
+ if (source.schemaVersion !== 1) return fail("omni materialized page.schemaVersion", "must be 1");
609
+ const parsedCursor = cursor(source.cursor, "omni materialized page.cursor");
610
+ const parsedCompleteness = completeness(source.completeness, "omni materialized page.completeness");
611
+ if (
612
+ parsedCompleteness.kind === "complete"
613
+ && (parsedCursor.request !== null || parsedCursor.nextInput !== null)
614
+ ) {
615
+ return fail(
616
+ "omni materialized page",
617
+ "complete partition coverage must begin at the root and cannot name another provider page",
618
+ );
619
+ }
620
+ if (
621
+ parsedCompleteness.kind === "first-page-only"
622
+ && (
623
+ parsedCursor.direction !== "none"
624
+ || parsedCursor.request !== null
625
+ || parsedCursor.nextInput !== null
626
+ )
627
+ ) {
628
+ return fail(
629
+ "omni materialized page",
630
+ "first-page-only coverage cannot declare a replayable provider continuation",
631
+ );
632
+ }
633
+ const entities = array(source.entities, "omni materialized page.entities", OMNI_MAX_PAGE_ENTITIES)
634
+ .map((entry, index) => entity(entry, `omni materialized page.entities[${index}]`));
635
+ const entityKeys = entities.map((entry) => `${entry.kind}\0${entry.providerId}`);
636
+ if (new Set(entityKeys).size !== entityKeys.length) {
637
+ return fail("omni materialized page.entities", "must not repeat a stable entity identity");
638
+ }
639
+ const tombstones = array(source.tombstones, "omni materialized page.tombstones", OMNI_MAX_PAGE_ENTITIES)
640
+ .map((entry, index) => {
641
+ const label = `omni materialized page.tombstones[${index}]`;
642
+ const value = record(entry, label);
643
+ exactKeys(value, ["kind", "providerId", "providerRevision"], label);
644
+ return Object.freeze({
645
+ kind: oneOf(value.kind, ["conversation", "message", "notification"] as const, `${label}.kind`),
646
+ providerId: text(value.providerId, `${label}.providerId`, 1_024),
647
+ providerRevision: nullableText(value.providerRevision, `${label}.providerRevision`, 1_024),
648
+ });
649
+ });
650
+ const tombstoneKeys = tombstones.map((entry) => `${entry.kind}\0${entry.providerId}`);
651
+ if (
652
+ new Set(tombstoneKeys).size !== tombstoneKeys.length
653
+ || tombstoneKeys.some((key) => entityKeys.includes(key))
654
+ ) return fail("omni materialized page.tombstones", "must be unique and cannot contradict present entities");
655
+ return Object.freeze({
656
+ schemaVersion: 1,
657
+ partition: text(source.partition, "omni materialized page.partition", 1_024),
658
+ completeness: parsedCompleteness,
659
+ cursor: parsedCursor,
660
+ entities: Object.freeze(entities),
661
+ tombstones: Object.freeze(tombstones),
662
+ });
663
+ }
664
+
665
+ function sourceIdentity(value: unknown, label: string): OmniSourceIdentityV1 {
666
+ const source = record(value, label);
667
+ exactKeys(source, ["schemaVersion", "adapter", "plugin", "surfaceId", "auth"], label);
668
+ if (source.schemaVersion !== 1) return fail(`${label}.schemaVersion`, "must be 1");
669
+ const adapter = record(source.adapter, `${label}.adapter`);
670
+ exactKeys(adapter, ["id", "version", "hash"], `${label}.adapter`);
671
+ const plugin = record(source.plugin, `${label}.plugin`);
672
+ exactKeys(plugin, ["id", "version", "closureHash"], `${label}.plugin`);
673
+ const auth = record(source.auth, `${label}.auth`);
674
+ exactKeys(auth, ["id", "kind", "hash", "subject"], `${label}.auth`);
675
+ return Object.freeze({
676
+ schemaVersion: 1,
677
+ adapter: Object.freeze({
678
+ id: text(adapter.id, `${label}.adapter.id`, 64),
679
+ version: text(adapter.version, `${label}.adapter.version`, 64),
680
+ hash: digest(adapter.hash, `${label}.adapter.hash`),
681
+ }),
682
+ plugin: Object.freeze({
683
+ id: text(plugin.id, `${label}.plugin.id`, 64),
684
+ version: text(plugin.version, `${label}.plugin.version`, 64),
685
+ closureHash: digest(plugin.closureHash, `${label}.plugin.closureHash`),
686
+ }),
687
+ surfaceId: text(source.surfaceId, `${label}.surfaceId`, 64),
688
+ auth: Object.freeze({
689
+ id: text(auth.id, `${label}.auth.id`, 64),
690
+ kind: text(auth.kind, `${label}.auth.kind`, 64),
691
+ hash: digest(auth.hash, `${label}.auth.hash`),
692
+ subject: text(auth.subject, `${label}.auth.subject`, 512),
693
+ }),
694
+ });
695
+ }
696
+
697
+ function provenance(value: unknown, label: string): OmniPageProvenanceV1 {
698
+ const source = record(value, label);
699
+ exactKeys(source, [
700
+ "operation", "inputHash", "exactQueryKey", "exactDataRevision", "validatedAt",
701
+ "startedAt", "finishedAt", "runId", "materializerId", "materializerVersion",
702
+ ], label);
703
+ const startedAt = timestamp(source.startedAt, `${label}.startedAt`);
704
+ const finishedAt = timestamp(source.finishedAt, `${label}.finishedAt`);
705
+ if (startedAt > finishedAt) return fail(label, "cannot finish before it starts");
706
+ return Object.freeze({
707
+ operation: oneOf(source.operation, ["messaging.list", "messaging.read"] as const, `${label}.operation`),
708
+ inputHash: digest(source.inputHash, `${label}.inputHash`),
709
+ exactQueryKey: digest(source.exactQueryKey, `${label}.exactQueryKey`),
710
+ exactDataRevision: digest(source.exactDataRevision, `${label}.exactDataRevision`),
711
+ validatedAt: timestamp(source.validatedAt, `${label}.validatedAt`),
712
+ startedAt,
713
+ finishedAt,
714
+ runId: uuid(source.runId, `${label}.runId`),
715
+ materializerId: text(source.materializerId, `${label}.materializerId`, 64),
716
+ materializerVersion: integer(source.materializerVersion, `${label}.materializerVersion`, 1, 1_000_000),
717
+ });
718
+ }
719
+
720
+ export function omniObservationOrder(value: OmniPageProvenanceV1): string {
721
+ return `${value.startedAt}\0${value.finishedAt}\0${value.runId}`;
722
+ }
723
+
724
+ function observationOrderText(value: unknown, label: string): string {
725
+ if (typeof value !== "string" || Buffer.byteLength(value, "utf8") > 256) {
726
+ return fail(label, "must be a bounded observation order");
727
+ }
728
+ return value;
729
+ }
730
+
731
+ function normalizedId(
732
+ source: OmniSourceIdentityV1,
733
+ kind: OmniEntityKindV1,
734
+ providerId: string,
735
+ ): string {
736
+ return sha256(canonicalJson({
737
+ schemaVersion: 1,
738
+ authHash: source.auth.hash,
739
+ surfaceId: source.surfaceId,
740
+ kind,
741
+ providerId,
742
+ }));
743
+ }
744
+
745
+ function publicEntity(
746
+ source: OmniSourceIdentityV1,
747
+ value: ProviderMaterializedEntityV1,
748
+ ): OmniEntityV1 {
749
+ const id = normalizedId(source, value.kind, value.providerId);
750
+ const conversationId = value.kind === "message" && value.conversationProviderId !== null
751
+ ? normalizedId(source, "conversation", value.conversationProviderId)
752
+ : null;
753
+ const semantic = Object.freeze({
754
+ ...value,
755
+ id,
756
+ source: Object.freeze({
757
+ surfaceId: source.surfaceId,
758
+ authId: source.auth.id,
759
+ providerId: value.providerId,
760
+ }),
761
+ conversationId,
762
+ });
763
+ return Object.freeze({
764
+ ...semantic,
765
+ revision: sha256(canonicalJson(semantic)),
766
+ });
767
+ }
768
+
769
+ function parsePublicEntity(value: unknown, label: string): OmniEntityV1 {
770
+ const source = record(value, label);
771
+ const kind = oneOf(source.kind, ["conversation", "message", "notification"] as const, `${label}.kind`);
772
+ const hasConversationKind = kind === "conversation"
773
+ && Object.hasOwn(source, "conversationKind");
774
+ const hasBodyTruncated = kind === "message" && Object.hasOwn(source, "bodyTruncated");
775
+ const providerKeys = kind === "conversation"
776
+ ? ["kind", "providerId", "providerRevision", "orderedAt", ...(hasConversationKind ? ["conversationKind"] : []), "detail", "title", "summary", "participants", "unread", "unreadCount", "archived", "pending"]
777
+ : kind === "message"
778
+ ? ["kind", "providerId", "providerRevision", "orderedAt", "conversationProviderId", "sender", "recipients", "direction", "subject", "body", ...(hasBodyTruncated ? ["bodyTruncated"] : []), "unread", "replyToProviderId", "state", "attachments"]
779
+ : ["kind", "providerId", "providerRevision", "orderedAt", "actor", "subject", "body", "unread", "context"];
780
+ const expectedKeys = kind === "conversation"
781
+ ? ["kind", "providerId", "providerRevision", "orderedAt", ...(hasConversationKind ? ["conversationKind"] : []), "detail", "title", "summary", "participants", "unread", "unreadCount", "archived", "pending", "id", "revision", "source", "conversationId"]
782
+ : kind === "message"
783
+ ? ["kind", "providerId", "providerRevision", "orderedAt", "conversationProviderId", "sender", "recipients", "direction", "subject", "body", ...(hasBodyTruncated ? ["bodyTruncated"] : []), "unread", "replyToProviderId", "state", "attachments", "id", "revision", "source", "conversationId"]
784
+ : ["kind", "providerId", "providerRevision", "orderedAt", "actor", "subject", "body", "unread", "context", "id", "revision", "source", "conversationId"];
785
+ exactKeys(source, expectedKeys, label);
786
+ const providerInput = Object.fromEntries(
787
+ providerKeys.map((key) => [key, source[key]]),
788
+ );
789
+ const provider = entity(providerInput, `${label} provider fields`);
790
+ const entitySource = record(source.source, `${label}.source`);
791
+ exactKeys(entitySource, ["surfaceId", "authId", "providerId"], `${label}.source`);
792
+ const semantic = Object.freeze({
793
+ ...provider,
794
+ id: digest(source.id, `${label}.id`),
795
+ source: Object.freeze({
796
+ surfaceId: text(entitySource.surfaceId, `${label}.source.surfaceId`, 64),
797
+ authId: text(entitySource.authId, `${label}.source.authId`, 64),
798
+ providerId: text(entitySource.providerId, `${label}.source.providerId`, 1_024),
799
+ }),
800
+ conversationId: source.conversationId === null
801
+ ? null
802
+ : digest(source.conversationId, `${label}.conversationId`),
803
+ });
804
+ const revision = digest(source.revision, `${label}.revision`);
805
+ if (sha256(canonicalJson(semantic)) !== revision) {
806
+ return fail(`${label}.revision`, "does not authenticate the normalized semantic bytes");
807
+ }
808
+ return Object.freeze({ ...semantic, revision });
809
+ }
810
+
811
+ function lastGoodNormalization(
812
+ source: JsonRecord,
813
+ label: string,
814
+ ): Readonly<{
815
+ lastGoodAt: string | null;
816
+ lastGoodExactDataRevision: string | null;
817
+ }> {
818
+ const lastGoodAt = source.lastGoodAt === null
819
+ ? null
820
+ : timestamp(source.lastGoodAt, `${label}.lastGoodAt`);
821
+ const lastGoodExactDataRevision = source.lastGoodExactDataRevision === null
822
+ ? null
823
+ : digest(
824
+ source.lastGoodExactDataRevision,
825
+ `${label}.lastGoodExactDataRevision`,
826
+ );
827
+ if ((lastGoodAt === null) !== (lastGoodExactDataRevision === null)) {
828
+ return fail(
829
+ label,
830
+ "must retain last-good time and exact data revision together",
831
+ );
832
+ }
833
+ return Object.freeze({ lastGoodAt, lastGoodExactDataRevision });
834
+ }
835
+
836
+ function normalizationStatus(value: unknown, label: string): OmniNormalizationStatusV1 {
837
+ const source = record(value, label);
838
+ const state = oneOf(source.state, ["ready", "drift"] as const, `${label}.state`);
839
+ if (state === "ready") {
840
+ exactKeys(source, ["state", "lastGoodAt", "exactDataRevision"], label);
841
+ return Object.freeze({
842
+ state,
843
+ lastGoodAt: timestamp(source.lastGoodAt, `${label}.lastGoodAt`),
844
+ exactDataRevision: digest(source.exactDataRevision, `${label}.exactDataRevision`),
845
+ });
846
+ }
847
+ exactKeys(source, [
848
+ "state", "failedAt", "exactDataRevision", "code", "message", "lastGoodAt",
849
+ "lastGoodExactDataRevision",
850
+ ], label);
851
+ const lastGood = lastGoodNormalization(source, label);
852
+ return Object.freeze({
853
+ state,
854
+ failedAt: timestamp(source.failedAt, `${label}.failedAt`),
855
+ exactDataRevision: digest(source.exactDataRevision, `${label}.exactDataRevision`),
856
+ code: oneOf(source.code, ["materializer-drift", "capacity-exceeded"] as const, `${label}.code`),
857
+ message: text(source.message, `${label}.message`, 8_192),
858
+ ...lastGood,
859
+ });
860
+ }
861
+
862
+ function normalizationFrontiers(
863
+ value: unknown,
864
+ label: string,
865
+ ): readonly OmniNormalizationFrontierV1[] {
866
+ const frontiers = array(value, label, OMNI_MAX_NORMALIZATION_FRONTIERS)
867
+ .map((entry, index) => {
868
+ const entryLabel = `${label}[${index}]`;
869
+ const frontier = record(entry, entryLabel);
870
+ exactKeys(frontier, ["exactQueryKey", "status"], entryLabel);
871
+ return Object.freeze({
872
+ exactQueryKey: digest(
873
+ frontier.exactQueryKey,
874
+ `${entryLabel}.exactQueryKey`,
875
+ ),
876
+ status: normalizationStatus(frontier.status, `${entryLabel}.status`),
877
+ });
878
+ });
879
+ if (
880
+ new Set(frontiers.map((frontier) => frontier.exactQueryKey)).size
881
+ !== frontiers.length
882
+ ) return fail(label, "must not repeat exact query keys");
883
+ if (frontiers.some((frontier, index) =>
884
+ index > 0
885
+ && frontiers[index - 1]!.exactQueryKey.localeCompare(frontier.exactQueryKey) >= 0)) {
886
+ return fail(label, "must be canonically ordered by exact query key");
887
+ }
888
+ return Object.freeze(frontiers);
889
+ }
890
+
891
+ export function parseOmniSourceStateV1(value: unknown): OmniSourceStateV1 {
892
+ const source = record(value, "omni source state");
893
+ exactKeys(source, [
894
+ "schemaVersion", "source", "entities", "pages", "completeCoverage",
895
+ "tombstones", "normalization",
896
+ ], "omni source state");
897
+ if (source.schemaVersion !== 1) return fail("omni source state.schemaVersion", "must be 1");
898
+ const parsedSource = sourceIdentity(source.source, "omni source state.source");
899
+ const entities = array(source.entities, "omni source state.entities", OMNI_MAX_SOURCE_ENTITIES)
900
+ .map((entry, index) => {
901
+ const label = `omni source state.entities[${index}]`;
902
+ const stored = record(entry, label);
903
+ exactKeys(stored, [
904
+ "entity", "partitions", "observationOrder", "provenance",
905
+ ], label);
906
+ const parsedEntity = parsePublicEntity(stored.entity, `${label}.entity`);
907
+ const parsedProvenance = provenance(stored.provenance, `${label}.provenance`);
908
+ const partitions = array(
909
+ stored.partitions,
910
+ `${label}.partitions`,
911
+ OMNI_MAX_SOURCE_PAGES,
912
+ ).map((partition, partitionIndex) =>
913
+ text(partition, `${label}.partitions[${partitionIndex}]`, 1_024));
914
+ if (
915
+ partitions.length === 0
916
+ || new Set(partitions).size !== partitions.length
917
+ || partitions.some((partition, partitionIndex) =>
918
+ partitionIndex > 0
919
+ && partitions[partitionIndex - 1]!.localeCompare(partition) >= 0)
920
+ ) return fail(label, "must retain unique canonically ordered partitions");
921
+ const order = observationOrderText(stored.observationOrder, `${label}.observationOrder`);
922
+ if (order !== omniObservationOrder(parsedProvenance)) return fail(label, "has inconsistent observation order");
923
+ if (
924
+ parsedEntity.source.surfaceId !== parsedSource.surfaceId
925
+ || parsedEntity.source.authId !== parsedSource.auth.id
926
+ || parsedEntity.id !== normalizedId(parsedSource, parsedEntity.kind, parsedEntity.providerId)
927
+ ) return fail(label, "has inconsistent source identity");
928
+ return Object.freeze({
929
+ entity: parsedEntity,
930
+ partitions: Object.freeze(partitions),
931
+ observationOrder: order,
932
+ provenance: parsedProvenance,
933
+ });
934
+ });
935
+ if (new Set(entities.map((entry) => entry.entity.id)).size !== entities.length) {
936
+ return fail("omni source state.entities", "must not repeat entity identities");
937
+ }
938
+ const pages = array(source.pages, "omni source state.pages", OMNI_MAX_SOURCE_PAGES)
939
+ .map((entry, index) => {
940
+ const label = `omni source state.pages[${index}]`;
941
+ const page = record(entry, label);
942
+ exactKeys(page, [
943
+ "key", "partition", "completeness", "cursor", "orderedEntityIds",
944
+ "tombstones", "observationOrder", "provenance",
945
+ ], label);
946
+ const parsedProvenance = provenance(page.provenance, `${label}.provenance`);
947
+ const order = observationOrderText(page.observationOrder, `${label}.observationOrder`);
948
+ if (order !== omniObservationOrder(parsedProvenance)) return fail(label, "has inconsistent observation order");
949
+ const orderedEntityIds = array(page.orderedEntityIds, `${label}.orderedEntityIds`, OMNI_MAX_PAGE_ENTITIES)
950
+ .map((id, entityIndex) => digest(id, `${label}.orderedEntityIds[${entityIndex}]`));
951
+ if (new Set(orderedEntityIds).size !== orderedEntityIds.length) return fail(label, "repeats page membership");
952
+ const pageTombstones = array(
953
+ page.tombstones,
954
+ `${label}.tombstones`,
955
+ OMNI_MAX_PAGE_ENTITIES,
956
+ ).map((entry, tombstoneIndex) => {
957
+ const tombstoneLabel = `${label}.tombstones[${tombstoneIndex}]`;
958
+ const tombstone = record(entry, tombstoneLabel);
959
+ exactKeys(tombstone, [
960
+ "id", "kind", "providerId", "providerRevision",
961
+ ], tombstoneLabel);
962
+ const kind = oneOf(
963
+ tombstone.kind,
964
+ ["conversation", "message", "notification"] as const,
965
+ `${tombstoneLabel}.kind`,
966
+ );
967
+ const providerId = text(
968
+ tombstone.providerId,
969
+ `${tombstoneLabel}.providerId`,
970
+ 1_024,
971
+ );
972
+ const id = digest(tombstone.id, `${tombstoneLabel}.id`);
973
+ if (id !== normalizedId(parsedSource, kind, providerId)) {
974
+ return fail(tombstoneLabel, "has inconsistent source identity");
975
+ }
976
+ return Object.freeze({
977
+ id,
978
+ kind,
979
+ providerId,
980
+ providerRevision: nullableText(
981
+ tombstone.providerRevision,
982
+ `${tombstoneLabel}.providerRevision`,
983
+ 1_024,
984
+ ),
985
+ });
986
+ });
987
+ const pageTombstoneIds = pageTombstones.map((tombstone) => tombstone.id);
988
+ if (
989
+ new Set(pageTombstoneIds).size !== pageTombstoneIds.length
990
+ || pageTombstoneIds.some((id) => orderedEntityIds.includes(id))
991
+ ) return fail(label, "repeats or contradicts page entity and tombstone membership");
992
+ return Object.freeze({
993
+ key: digest(page.key, `${label}.key`),
994
+ partition: text(page.partition, `${label}.partition`, 1_024),
995
+ completeness: completeness(page.completeness, `${label}.completeness`),
996
+ cursor: cursor(page.cursor, `${label}.cursor`),
997
+ orderedEntityIds: Object.freeze(orderedEntityIds),
998
+ tombstones: Object.freeze(pageTombstones),
999
+ observationOrder: order,
1000
+ provenance: parsedProvenance,
1001
+ });
1002
+ });
1003
+ if (new Set(pages.map((page) => page.key)).size !== pages.length) {
1004
+ return fail("omni source state.pages", "must not repeat page identities");
1005
+ }
1006
+ const completeCoverage = array(
1007
+ source.completeCoverage,
1008
+ "omni source state.completeCoverage",
1009
+ OMNI_MAX_COMPLETE_COVERAGE,
1010
+ ).map((entry, index) => {
1011
+ const label = `omni source state.completeCoverage[${index}]`;
1012
+ const coverage = record(entry, label);
1013
+ exactKeys(coverage, [
1014
+ "partition", "pageKey", "pageRevision", "observationOrder", "provenance",
1015
+ ], label);
1016
+ const parsedProvenance = provenance(coverage.provenance, `${label}.provenance`);
1017
+ const observationOrder = observationOrderText(
1018
+ coverage.observationOrder,
1019
+ `${label}.observationOrder`,
1020
+ );
1021
+ if (observationOrder !== omniObservationOrder(parsedProvenance)) {
1022
+ return fail(label, "has inconsistent observation order");
1023
+ }
1024
+ return Object.freeze({
1025
+ partition: text(coverage.partition, `${label}.partition`, 1_024),
1026
+ pageKey: digest(coverage.pageKey, `${label}.pageKey`),
1027
+ pageRevision: digest(coverage.pageRevision, `${label}.pageRevision`),
1028
+ observationOrder,
1029
+ provenance: parsedProvenance,
1030
+ });
1031
+ });
1032
+ if (
1033
+ new Set(completeCoverage.map((coverage) => coverage.partition)).size
1034
+ !== completeCoverage.length
1035
+ ) return fail("omni source state.completeCoverage", "must not repeat partitions");
1036
+ if (completeCoverage.some((coverage, index) =>
1037
+ index > 0
1038
+ && completeCoverage[index - 1]!.partition.localeCompare(coverage.partition) >= 0)) {
1039
+ return fail(
1040
+ "omni source state.completeCoverage",
1041
+ "must be canonically ordered by partition",
1042
+ );
1043
+ }
1044
+ const tombstones = array(source.tombstones, "omni source state.tombstones", OMNI_MAX_TOMBSTONES)
1045
+ .map((entry, index) => {
1046
+ const label = `omni source state.tombstones[${index}]`;
1047
+ const tombstone = record(entry, label);
1048
+ exactKeys(tombstone, ["id", "kind", "providerId", "providerRevision", "observationOrder", "provenance"], label);
1049
+ const parsedProvenance = provenance(tombstone.provenance, `${label}.provenance`);
1050
+ const kind = oneOf(tombstone.kind, ["conversation", "message", "notification"] as const, `${label}.kind`);
1051
+ const providerId = text(tombstone.providerId, `${label}.providerId`, 1_024);
1052
+ const id = digest(tombstone.id, `${label}.id`);
1053
+ if (id !== normalizedId(parsedSource, kind, providerId)) return fail(label, "has inconsistent source identity");
1054
+ const order = observationOrderText(tombstone.observationOrder, `${label}.observationOrder`);
1055
+ if (order !== omniObservationOrder(parsedProvenance)) return fail(label, "has inconsistent observation order");
1056
+ return Object.freeze({
1057
+ id,
1058
+ kind,
1059
+ providerId,
1060
+ providerRevision: nullableText(tombstone.providerRevision, `${label}.providerRevision`, 1_024),
1061
+ observationOrder: order,
1062
+ provenance: parsedProvenance,
1063
+ });
1064
+ });
1065
+ if (new Set(tombstones.map((entry) => entry.id)).size !== tombstones.length) {
1066
+ return fail("omni source state.tombstones", "must not repeat tombstone identities");
1067
+ }
1068
+ const entitiesById = new Map(entities.map((entry) => [entry.entity.id, entry]));
1069
+ const entityIds = new Set(entitiesById.keys());
1070
+ const tombstoneIds = new Set(tombstones.map((entry) => entry.id));
1071
+ if ([...entityIds].some((id) => tombstoneIds.has(id))) {
1072
+ return fail(
1073
+ "omni source state",
1074
+ "must not retain one identity as both an entity and a tombstone",
1075
+ );
1076
+ }
1077
+ for (const page of pages) {
1078
+ if (page.orderedEntityIds.some((id) => !entityIds.has(id) && !tombstoneIds.has(id))) {
1079
+ return fail(
1080
+ "omni source state.pages",
1081
+ "must not reference an unknown entity identity",
1082
+ );
1083
+ }
1084
+ if (page.orderedEntityIds.some((id) => {
1085
+ const stored = entitiesById.get(id);
1086
+ return stored !== undefined && !stored.partitions.includes(page.partition);
1087
+ })) {
1088
+ return fail(
1089
+ "omni source state.pages",
1090
+ "entity membership must name the page partition",
1091
+ );
1092
+ }
1093
+ if (page.tombstones.some((tombstone) => !tombstoneIds.has(tombstone.id))) {
1094
+ return fail(
1095
+ "omni source state.pages",
1096
+ "must not reference an identity that is not a retained tombstone",
1097
+ );
1098
+ }
1099
+ }
1100
+ const coverageByPartition = new Map(
1101
+ completeCoverage.map((coverage) => [coverage.partition, coverage]),
1102
+ );
1103
+ for (const page of pages) {
1104
+ if (page.completeness.kind !== "complete") continue;
1105
+ const coverage = coverageByPartition.get(page.partition);
1106
+ if (
1107
+ coverage === undefined
1108
+ || coverage.pageKey !== page.key
1109
+ || coverage.observationOrder !== page.observationOrder
1110
+ || coverage.pageRevision !== sha256(canonicalJson(page))
1111
+ ) {
1112
+ return fail(
1113
+ "omni source state.pages",
1114
+ "complete page must match its partition coverage watermark",
1115
+ );
1116
+ }
1117
+ }
1118
+ for (const coverage of completeCoverage) {
1119
+ const retainedSnapshot = pages.find((page) =>
1120
+ page.key === coverage.pageKey
1121
+ && page.observationOrder === coverage.observationOrder);
1122
+ if (
1123
+ retainedSnapshot === undefined
1124
+ || (
1125
+ retainedSnapshot.partition !== coverage.partition
1126
+ || retainedSnapshot.completeness.kind !== "complete"
1127
+ || sha256(canonicalJson(retainedSnapshot)) !== coverage.pageRevision
1128
+ )
1129
+ ) {
1130
+ return fail(
1131
+ "omni source state.completeCoverage",
1132
+ "does not authenticate its retained complete page",
1133
+ );
1134
+ }
1135
+ }
1136
+ const result = Object.freeze({
1137
+ schemaVersion: 1 as const,
1138
+ source: parsedSource,
1139
+ entities: Object.freeze(entities),
1140
+ pages: Object.freeze(pages),
1141
+ completeCoverage: Object.freeze(completeCoverage),
1142
+ tombstones: Object.freeze(tombstones),
1143
+ normalization: normalizationFrontiers(
1144
+ source.normalization,
1145
+ "omni source state.normalization",
1146
+ ),
1147
+ });
1148
+ if (Buffer.byteLength(canonicalJson(result), "utf8") > OMNI_MAX_SOURCE_BYTES) {
1149
+ return fail("omni source state", "exceeds its byte capacity");
1150
+ }
1151
+ return result;
1152
+ }
1153
+
1154
+ export function reduceOmniSourceStateV1(
1155
+ currentValue: unknown,
1156
+ pageValue: unknown,
1157
+ options: {
1158
+ readonly source: OmniSourceIdentityV1;
1159
+ readonly provenance: OmniPageProvenanceV1;
1160
+ },
1161
+ ): OmniSourceStateV1 {
1162
+ const expectedSource = sourceIdentity(options.source, "omni reduction source");
1163
+ const nextProvenance = provenance(options.provenance, "omni reduction provenance");
1164
+ const page = parseMaterializedPageV1(pageValue);
1165
+ const current = currentValue === null ? null : parseOmniSourceStateV1(currentValue);
1166
+ if (current !== null && canonicalJson(current.source) !== canonicalJson(expectedSource)) {
1167
+ return fail("omni reduction", "cannot cross source, auth lifetime, adapter, or implementation identity");
1168
+ }
1169
+ const order = omniObservationOrder(nextProvenance);
1170
+ const entityMap = new Map((current?.entities ?? []).map((entry) => [entry.entity.id, entry]));
1171
+ const tombstoneMap = new Map((current?.tombstones ?? []).map((entry) => [entry.id, entry]));
1172
+ const incomingEntities = page.entities.map((providerEntity) =>
1173
+ publicEntity(expectedSource, providerEntity));
1174
+ const incomingTombstones = page.tombstones.map((providerTombstone) =>
1175
+ Object.freeze({
1176
+ id: normalizedId(
1177
+ expectedSource,
1178
+ providerTombstone.kind,
1179
+ providerTombstone.providerId,
1180
+ ),
1181
+ kind: providerTombstone.kind,
1182
+ providerId: providerTombstone.providerId,
1183
+ providerRevision: providerTombstone.providerRevision,
1184
+ }));
1185
+ const pageKey = sha256(canonicalJson({
1186
+ schemaVersion: 1,
1187
+ partition: page.partition,
1188
+ operation: nextProvenance.operation,
1189
+ inputHash: nextProvenance.inputHash,
1190
+ request: page.cursor.request,
1191
+ }));
1192
+ const nextPage: OmniStoredPageV1 = Object.freeze({
1193
+ key: pageKey,
1194
+ partition: page.partition,
1195
+ completeness: page.completeness,
1196
+ cursor: page.cursor,
1197
+ orderedEntityIds: Object.freeze(incomingEntities.map((entity) => entity.id)),
1198
+ tombstones: Object.freeze(incomingTombstones),
1199
+ observationOrder: order,
1200
+ provenance: nextProvenance,
1201
+ });
1202
+ const nextPageRevision = sha256(canonicalJson(nextPage));
1203
+ const pageMap = new Map((current?.pages ?? []).map((entry) => [entry.key, entry]));
1204
+ const coverageMap = new Map(
1205
+ (current?.completeCoverage ?? []).map((entry) => [entry.partition, entry]),
1206
+ );
1207
+ const normalizationMap = new Map(
1208
+ (current?.normalization ?? []).map((entry) => [entry.exactQueryKey, entry]),
1209
+ );
1210
+ const existingPage = pageMap.get(pageKey);
1211
+ if (
1212
+ existingPage !== undefined
1213
+ && existingPage.observationOrder === order
1214
+ && canonicalJson(existingPage) !== canonicalJson(nextPage)
1215
+ ) {
1216
+ return fail(
1217
+ "omni reduction",
1218
+ "received conflicting membership for one exact page observation",
1219
+ );
1220
+ }
1221
+
1222
+ // Check all entity/deletion collisions before ignoring an out-of-order page.
1223
+ // An old observation can still reveal an impossible later resurrection.
1224
+ for (const nextEntity of incomingEntities) {
1225
+ const tombstone = tombstoneMap.get(nextEntity.id);
1226
+ if (tombstone !== undefined) {
1227
+ if (tombstone.observationOrder === order) {
1228
+ return fail(
1229
+ "omni reduction",
1230
+ "received an entity and tombstone for one exact observation",
1231
+ );
1232
+ }
1233
+ if (tombstone.observationOrder < order) {
1234
+ return fail(
1235
+ "omni reduction",
1236
+ "cannot resurrect a tombstoned entity without an explicit provider resurrection contract",
1237
+ );
1238
+ }
1239
+ }
1240
+ const existing = entityMap.get(nextEntity.id);
1241
+ if (existing !== undefined) {
1242
+ if (
1243
+ existing.entity.providerRevision !== null
1244
+ && existing.entity.providerRevision === nextEntity.providerRevision
1245
+ && existing.entity.revision !== nextEntity.revision
1246
+ ) return fail("omni reduction", "received conflicting bytes for one provider entity revision");
1247
+ if (existing.observationOrder === order) {
1248
+ if (existing.entity.revision !== nextEntity.revision) {
1249
+ return fail("omni reduction", "received conflicting bytes for one exact observation");
1250
+ }
1251
+ }
1252
+ }
1253
+ }
1254
+ for (const nextTombstone of incomingTombstones) {
1255
+ const existingEntity = entityMap.get(nextTombstone.id);
1256
+ if (existingEntity !== undefined && existingEntity.observationOrder >= order) {
1257
+ return fail(
1258
+ "omni reduction",
1259
+ existingEntity.observationOrder === order
1260
+ ? "received an entity and tombstone for one exact observation"
1261
+ : "cannot retain an entity observed after a tombstone without an explicit provider resurrection contract",
1262
+ );
1263
+ }
1264
+ const existingTombstone = tombstoneMap.get(nextTombstone.id);
1265
+ if (
1266
+ existingTombstone !== undefined
1267
+ && existingTombstone.observationOrder === order
1268
+ && (
1269
+ existingTombstone.kind !== nextTombstone.kind
1270
+ || existingTombstone.providerId !== nextTombstone.providerId
1271
+ || existingTombstone.providerRevision !== nextTombstone.providerRevision
1272
+ )
1273
+ ) {
1274
+ return fail(
1275
+ "omni reduction",
1276
+ "received conflicting tombstone bytes for one exact observation",
1277
+ );
1278
+ }
1279
+ }
1280
+
1281
+ const coverage = coverageMap.get(page.partition);
1282
+ if (
1283
+ coverage !== undefined
1284
+ && coverage.pageKey === pageKey
1285
+ && page.completeness.kind !== "complete"
1286
+ ) {
1287
+ return fail(
1288
+ "omni reduction",
1289
+ "cannot weaken a complete root coordinate without a new reviewed coverage contract",
1290
+ );
1291
+ }
1292
+ if (coverage !== undefined && order <= coverage.observationOrder) {
1293
+ if (
1294
+ order === coverage.observationOrder
1295
+ && (
1296
+ page.completeness.kind !== "complete"
1297
+ || coverage.pageKey !== pageKey
1298
+ || coverage.pageRevision !== nextPageRevision
1299
+ )
1300
+ ) {
1301
+ return fail(
1302
+ "omni reduction",
1303
+ "received conflicting partition membership for one complete observation",
1304
+ );
1305
+ }
1306
+ if (current === null) return fail("omni reduction", "lost its coverage state");
1307
+ return current;
1308
+ }
1309
+ if (existingPage !== undefined && existingPage.observationOrder >= order) {
1310
+ if (current === null) return fail("omni reduction", "lost its page state");
1311
+ return current;
1312
+ }
1313
+
1314
+ for (const nextEntity of incomingEntities) {
1315
+ const tombstone = tombstoneMap.get(nextEntity.id);
1316
+ if (tombstone !== undefined) continue;
1317
+ const existing = entityMap.get(nextEntity.id);
1318
+ const selected = existing !== undefined && existing.observationOrder >= order
1319
+ ? existing
1320
+ : Object.freeze({
1321
+ entity: nextEntity,
1322
+ partitions: Object.freeze([page.partition]),
1323
+ observationOrder: order,
1324
+ provenance: nextProvenance,
1325
+ });
1326
+ const partitions = [...new Set([
1327
+ ...(existing?.partitions ?? []),
1328
+ page.partition,
1329
+ ])].sort((left, right) => left.localeCompare(right));
1330
+ entityMap.set(nextEntity.id, Object.freeze({
1331
+ ...selected,
1332
+ partitions: Object.freeze(partitions),
1333
+ }));
1334
+ }
1335
+ for (const nextTombstone of incomingTombstones) {
1336
+ const existingTombstone = tombstoneMap.get(nextTombstone.id);
1337
+ if (
1338
+ existingTombstone !== undefined
1339
+ && existingTombstone.observationOrder >= order
1340
+ ) continue;
1341
+ entityMap.delete(nextTombstone.id);
1342
+ tombstoneMap.set(nextTombstone.id, Object.freeze({
1343
+ ...nextTombstone,
1344
+ observationOrder: order,
1345
+ provenance: nextProvenance,
1346
+ }));
1347
+ }
1348
+
1349
+ if (page.completeness.kind === "complete") {
1350
+ for (const [key, stored] of pageMap) {
1351
+ if (
1352
+ stored.partition === page.partition
1353
+ && stored.observationOrder <= order
1354
+ ) pageMap.delete(key);
1355
+ }
1356
+ coverageMap.set(page.partition, Object.freeze({
1357
+ partition: page.partition,
1358
+ pageKey,
1359
+ pageRevision: nextPageRevision,
1360
+ observationOrder: order,
1361
+ provenance: nextProvenance,
1362
+ }));
1363
+ }
1364
+ pageMap.set(pageKey, nextPage);
1365
+ if (page.completeness.kind === "complete") {
1366
+ const partitionMembership = new Set([...pageMap.values()]
1367
+ .filter((storedPage) => storedPage.partition === page.partition)
1368
+ .flatMap((storedPage) => storedPage.orderedEntityIds));
1369
+ for (const [id, stored] of entityMap) {
1370
+ if (
1371
+ !stored.partitions.includes(page.partition)
1372
+ || partitionMembership.has(id)
1373
+ ) continue;
1374
+ const partitions = stored.partitions.filter((partition) =>
1375
+ partition !== page.partition);
1376
+ if (partitions.length === 0) {
1377
+ entityMap.delete(id);
1378
+ } else {
1379
+ entityMap.set(id, Object.freeze({
1380
+ ...stored,
1381
+ partitions: Object.freeze(partitions),
1382
+ }));
1383
+ }
1384
+ }
1385
+ }
1386
+ normalizationMap.set(nextProvenance.exactQueryKey, Object.freeze({
1387
+ exactQueryKey: nextProvenance.exactQueryKey,
1388
+ status: Object.freeze({
1389
+ state: "ready",
1390
+ lastGoodAt: nextProvenance.validatedAt,
1391
+ exactDataRevision: nextProvenance.exactDataRevision,
1392
+ }),
1393
+ }));
1394
+ const entities = [...entityMap.values()].sort((left, right) =>
1395
+ left.entity.id.localeCompare(right.entity.id));
1396
+ const pages = [...pageMap.values()].sort((left, right) => left.key.localeCompare(right.key));
1397
+ const completeCoverage = [...coverageMap.values()].sort((left, right) =>
1398
+ left.partition.localeCompare(right.partition));
1399
+ const tombstones = [...tombstoneMap.values()].sort((left, right) => left.id.localeCompare(right.id));
1400
+ const normalization = [...normalizationMap.values()].sort((left, right) =>
1401
+ left.exactQueryKey.localeCompare(right.exactQueryKey));
1402
+ if (
1403
+ entities.length > OMNI_MAX_SOURCE_ENTITIES
1404
+ || pages.length > OMNI_MAX_SOURCE_PAGES
1405
+ || completeCoverage.length > OMNI_MAX_COMPLETE_COVERAGE
1406
+ || tombstones.length > OMNI_MAX_TOMBSTONES
1407
+ || normalization.length > OMNI_MAX_NORMALIZATION_FRONTIERS
1408
+ ) return fail(
1409
+ "omni reduction",
1410
+ "exceeds its entity, page, complete-coverage, tombstone, or normalization capacity",
1411
+ );
1412
+ if (pages.length === 0) return fail("omni reduction", "must retain its applied page");
1413
+ return parseOmniSourceStateV1(Object.freeze({
1414
+ schemaVersion: 1,
1415
+ source: expectedSource,
1416
+ entities: Object.freeze(entities),
1417
+ pages: Object.freeze(pages),
1418
+ completeCoverage: Object.freeze(completeCoverage),
1419
+ tombstones: Object.freeze(tombstones),
1420
+ normalization: Object.freeze(normalization),
1421
+ }));
1422
+ }
1423
+
1424
+ function boundedErrorMessage(error: unknown): string {
1425
+ const message = error instanceof Error ? error.message : String(error);
1426
+ const bytes = Buffer.from(message, "utf8");
1427
+ return bytes.byteLength <= 8_192
1428
+ ? message
1429
+ : `${bytes.subarray(0, 8_192).toString("utf8")}…`;
1430
+ }
1431
+
1432
+ export function markOmniSourceDriftV1(
1433
+ currentValue: unknown,
1434
+ options: {
1435
+ readonly source: OmniSourceIdentityV1;
1436
+ readonly exactQueryKey: string;
1437
+ readonly exactDataRevision: string;
1438
+ readonly failedAt: string;
1439
+ readonly error: unknown;
1440
+ readonly code?: "materializer-drift" | "capacity-exceeded";
1441
+ },
1442
+ ): OmniSourceStateV1 {
1443
+ const source = sourceIdentity(options.source, "omni drift source");
1444
+ const current = currentValue === null ? null : parseOmniSourceStateV1(currentValue);
1445
+ if (current !== null && canonicalJson(current.source) !== canonicalJson(source)) {
1446
+ return fail("omni drift", "cannot cross source identity");
1447
+ }
1448
+ const failedAt = timestamp(options.failedAt, "omni drift failure time");
1449
+ const exactQueryKey = digest(
1450
+ options.exactQueryKey,
1451
+ "omni drift exact query key",
1452
+ );
1453
+ const normalizationMap = new Map(
1454
+ (current?.normalization ?? []).map((entry) => [entry.exactQueryKey, entry]),
1455
+ );
1456
+ const prior = normalizationMap.get(exactQueryKey)?.status;
1457
+ normalizationMap.set(exactQueryKey, Object.freeze({
1458
+ exactQueryKey,
1459
+ status: Object.freeze({
1460
+ state: "drift",
1461
+ failedAt,
1462
+ exactDataRevision: digest(
1463
+ options.exactDataRevision,
1464
+ "omni drift exact data revision",
1465
+ ),
1466
+ code: options.code ?? "materializer-drift",
1467
+ message: boundedErrorMessage(options.error),
1468
+ lastGoodAt: prior?.state === "ready"
1469
+ ? prior.lastGoodAt
1470
+ : prior?.lastGoodAt ?? null,
1471
+ lastGoodExactDataRevision: prior?.state === "ready"
1472
+ ? prior.exactDataRevision
1473
+ : prior?.lastGoodExactDataRevision ?? null,
1474
+ }),
1475
+ }));
1476
+ const normalization = [...normalizationMap.values()].sort((left, right) =>
1477
+ left.exactQueryKey.localeCompare(right.exactQueryKey));
1478
+ if (normalization.length > OMNI_MAX_NORMALIZATION_FRONTIERS) {
1479
+ return fail("omni drift", "exceeds its normalization frontier capacity");
1480
+ }
1481
+ return parseOmniSourceStateV1(Object.freeze({
1482
+ schemaVersion: 1,
1483
+ source,
1484
+ entities: current?.entities ?? Object.freeze([]),
1485
+ pages: current?.pages ?? Object.freeze([]),
1486
+ completeCoverage: current?.completeCoverage ?? Object.freeze([]),
1487
+ tombstones: current?.tombstones ?? Object.freeze([]),
1488
+ normalization: Object.freeze(normalization),
1489
+ }));
1490
+ }
1491
+
1492
+ export function compareOmniEntitiesV1(left: OmniEntityV1, right: OmniEntityV1): number {
1493
+ const leftTime = left.orderedAt ?? "";
1494
+ const rightTime = right.orderedAt ?? "";
1495
+ return rightTime.localeCompare(leftTime) || left.id.localeCompare(right.id);
1496
+ }
1497
+
1498
+ export function queryOmniSourceStatesV1(
1499
+ states: readonly OmniSourceStateV1[],
1500
+ options: {
1501
+ readonly kinds?: readonly OmniEntityKindV1[];
1502
+ readonly conversationId?: string;
1503
+ readonly unread?: boolean;
1504
+ } = {},
1505
+ ): readonly OmniEntityV1[] {
1506
+ const kinds = options.kinds === undefined ? null : new Set(options.kinds);
1507
+ const entityById = new Map<string, OmniStoredEntityV1>();
1508
+ const tombstoneById = new Map<string, OmniStoredTombstoneV1>();
1509
+ const providerRevisionBytes = new Map<string, string>();
1510
+ const exactEvents = new Map<
1511
+ string,
1512
+ Readonly<{ kind: "entity" | "tombstone"; revision: string }>
1513
+ >();
1514
+ const observeExactEvent = (
1515
+ id: string,
1516
+ observationOrder: string,
1517
+ event: Readonly<{ kind: "entity" | "tombstone"; revision: string }>,
1518
+ ): void => {
1519
+ const key = `${id}\0${observationOrder}`;
1520
+ const existing = exactEvents.get(key);
1521
+ if (
1522
+ existing !== undefined
1523
+ && (existing.kind !== event.kind || existing.revision !== event.revision)
1524
+ ) {
1525
+ return fail(
1526
+ "omni query",
1527
+ "encountered conflicting entity or tombstone bytes for one exact observation",
1528
+ );
1529
+ }
1530
+ exactEvents.set(key, event);
1531
+ };
1532
+ for (const stateValue of states) {
1533
+ const state = parseOmniSourceStateV1(stateValue);
1534
+ for (const stored of state.entities) {
1535
+ const value = stored.entity;
1536
+ if (value.providerRevision !== null) {
1537
+ const providerRevisionKey = `${value.id}\0${value.providerRevision}`;
1538
+ const existingRevision = providerRevisionBytes.get(providerRevisionKey);
1539
+ if (
1540
+ existingRevision !== undefined
1541
+ && existingRevision !== value.revision
1542
+ ) {
1543
+ return fail(
1544
+ "omni query",
1545
+ "encountered conflicting normalized bytes for one provider revision",
1546
+ );
1547
+ }
1548
+ providerRevisionBytes.set(providerRevisionKey, value.revision);
1549
+ }
1550
+ observeExactEvent(value.id, stored.observationOrder, {
1551
+ kind: "entity",
1552
+ revision: value.revision,
1553
+ });
1554
+ const existing = entityById.get(value.id);
1555
+ if (
1556
+ existing === undefined
1557
+ || existing.observationOrder < stored.observationOrder
1558
+ ) {
1559
+ entityById.set(value.id, stored);
1560
+ }
1561
+ }
1562
+ for (const tombstone of state.tombstones) {
1563
+ const revision = sha256(canonicalJson({
1564
+ kind: tombstone.kind,
1565
+ providerId: tombstone.providerId,
1566
+ providerRevision: tombstone.providerRevision,
1567
+ }));
1568
+ observeExactEvent(tombstone.id, tombstone.observationOrder, {
1569
+ kind: "tombstone",
1570
+ revision,
1571
+ });
1572
+ const existing = tombstoneById.get(tombstone.id);
1573
+ if (
1574
+ existing === undefined
1575
+ || existing.observationOrder < tombstone.observationOrder
1576
+ ) {
1577
+ tombstoneById.set(tombstone.id, tombstone);
1578
+ }
1579
+ }
1580
+ }
1581
+ const winners: OmniEntityV1[] = [];
1582
+ for (const [id, stored] of entityById) {
1583
+ const tombstone = tombstoneById.get(id);
1584
+ if (tombstone !== undefined) {
1585
+ if (stored.observationOrder > tombstone.observationOrder) {
1586
+ return fail(
1587
+ "omni query",
1588
+ "cannot expose an entity observed after a tombstone without an explicit provider resurrection contract",
1589
+ );
1590
+ }
1591
+ if (stored.observationOrder === tombstone.observationOrder) {
1592
+ return fail(
1593
+ "omni query",
1594
+ "encountered an entity and tombstone for one exact observation",
1595
+ );
1596
+ }
1597
+ continue;
1598
+ }
1599
+ const value = stored.entity;
1600
+ if (kinds !== null && !kinds.has(value.kind)) continue;
1601
+ if (
1602
+ options.conversationId !== undefined
1603
+ && value.conversationId !== options.conversationId
1604
+ ) continue;
1605
+ if (options.unread !== undefined) {
1606
+ const unread = value.kind === "conversation"
1607
+ ? value.unread ?? (value.unreadCount === null ? null : value.unreadCount > 0)
1608
+ : value.unread;
1609
+ if (unread !== options.unread) continue;
1610
+ }
1611
+ winners.push(value);
1612
+ }
1613
+ return Object.freeze(winners.sort(compareOmniEntitiesV1));
1614
+ }