@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,838 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { types as nodeTypes } from "node:util";
3
+
4
+ import { canonicalJson, sha256 } from "./canonical-json";
5
+ import type {
6
+ ApplePhotosContactEvidence,
7
+ ApplePhotosContactEvidenceArtifact,
8
+ ApplePhotosContactEvidenceCapture,
9
+ ApplePhotosContactEvidenceCompleteness,
10
+ ApplePhotosContactEvidenceExportReceipt,
11
+ ApplePhotosContactEvidenceExportResult,
12
+ ApplePhotosContactEvidencePrivacy,
13
+ } from "./apple-photos-client-types";
14
+ import { GHOSTGET_VERSION } from "./version";
15
+
16
+ export const APPLE_PHOTOS_CONTACT_EVIDENCE_SCHEMA_VERSION = 1 as const;
17
+ export const APPLE_PHOTOS_CONTACT_EVIDENCE_FORMAT =
18
+ "wrench.apple-photos-contact-evidence" as const;
19
+ export const APPLE_PHOTOS_CONTACT_EVIDENCE_RECEIPT_FORMAT =
20
+ "wrench.apple-photos-contact-evidence-export-receipt" as const;
21
+ export const APPLE_PHOTOS_LOCAL_SOURCE = Object.freeze({
22
+ id: "apple-photos-local" as const,
23
+ version: "1.0.0" as const,
24
+ });
25
+ export const APPLE_PHOTOS_CONTACT_EVIDENCE_LIMITS = Object.freeze({
26
+ captureAttemptsPerDatabase: 1 as const,
27
+ maximumPhotosDatabaseBytes: 4 * 1024 * 1024 * 1024,
28
+ maximumContactsDatabases: 32,
29
+ maximumContactsDatabaseBytes: 2 * 1024 * 1024 * 1024,
30
+ maximumDirectoryEntries: 4_096,
31
+ maximumContactsSourceDirectories: 256,
32
+ maximumPeople: 100_000,
33
+ maximumContacts: 1_000_000,
34
+ maximumWireBytes: 128 * 1024 * 1024,
35
+ });
36
+
37
+ const COMPLETENESS: ApplePhotosContactEvidenceCompleteness = Object.freeze({
38
+ kind: "bounded-local-observation",
39
+ localPhotos: "one-reviewed-library-database-capture",
40
+ localContacts: "ordered-discovered-address-book-database-captures",
41
+ crossDatabaseAtomicity: "not-asserted",
42
+ remoteSync: "not-asserted",
43
+ unmatchedPeople: "excluded",
44
+ reason:
45
+ "Exact matches from bounded independent local database captures; cross-database atomicity and remote synchronization state are not asserted.",
46
+ });
47
+
48
+ const PRIVACY: ApplePhotosContactEvidencePrivacy = Object.freeze({
49
+ names: "excluded-from-returned-json",
50
+ localPaths: "excluded-from-returned-json",
51
+ images: "excluded-from-returned-json",
52
+ media: "excluded-from-returned-json",
53
+ locations: "excluded-from-returned-json",
54
+ rawContactData: "excluded-from-returned-json",
55
+ rawPhotosData: "excluded-from-returned-json",
56
+ faceClusterIdentifiers: "included-biometric-derived-private-metadata",
57
+ faceClusterCounts: "included-biometric-derived-private-metadata",
58
+ faceprintTemplates: "excluded-from-returned-json",
59
+ faceCrops: "excluded-from-returned-json",
60
+ unmatchedPeople: "excluded-from-returned-json",
61
+ });
62
+
63
+ const SCOPE = Object.freeze({
64
+ people: "exact-zpersonuri-zuniqueid-matches-only" as const,
65
+ faces: "detected-face-links-present-in-photos-capture" as const,
66
+ assets: "distinct-zasset-rows-linked-through-detected-faces" as const,
67
+ });
68
+
69
+ type ArtifactInput = Readonly<{
70
+ observedAt: string;
71
+ libraryRealmSha256: string;
72
+ generationSha256: string;
73
+ photosSchemaSha256: string;
74
+ contactsSchemaSha256: string;
75
+ capture: ApplePhotosContactEvidenceCapture;
76
+ evidence: readonly ApplePhotosContactEvidence[];
77
+ }>;
78
+
79
+ type ExportInput = ArtifactInput & Readonly<{
80
+ runId?: string;
81
+ startedAt: string;
82
+ finishedAt: string;
83
+ contactsDatabases: number;
84
+ }>;
85
+
86
+ function fail(message: string): never {
87
+ throw new Error(`Apple Photos contact evidence: ${message}`);
88
+ }
89
+
90
+ function dataRecord(
91
+ value: unknown,
92
+ label: string,
93
+ ): Readonly<Record<string, unknown>> {
94
+ if (
95
+ typeof value !== "object"
96
+ || value === null
97
+ || Array.isArray(value)
98
+ || nodeTypes.isProxy(value)
99
+ || (
100
+ Object.getPrototypeOf(value) !== Object.prototype
101
+ && Object.getPrototypeOf(value) !== null
102
+ )
103
+ ) return fail(`${label} must be a plain, non-proxy object`);
104
+ const descriptors = Object.getOwnPropertyDescriptors(value);
105
+ for (const key of Reflect.ownKeys(descriptors)) {
106
+ if (typeof key !== "string") return fail(`${label} has a symbol field`);
107
+ const descriptor = descriptors[key];
108
+ if (
109
+ descriptor === undefined
110
+ || !descriptor.enumerable
111
+ || !("value" in descriptor)
112
+ ) return fail(`${label} must contain only enumerable data properties`);
113
+ }
114
+ return Object.freeze(Object.fromEntries(
115
+ Object.entries(descriptors).map(([key, descriptor]) => [key, descriptor.value]),
116
+ ));
117
+ }
118
+
119
+ function dataArray(
120
+ value: unknown,
121
+ label: string,
122
+ maximum: number,
123
+ ): readonly unknown[] {
124
+ if (
125
+ !Array.isArray(value)
126
+ || nodeTypes.isProxy(value)
127
+ || Object.getPrototypeOf(value) !== Array.prototype
128
+ ) return fail(`${label} must be an ordinary, non-proxy array`);
129
+ const descriptors = Object.getOwnPropertyDescriptors(value);
130
+ const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
131
+ if (
132
+ lengthDescriptor === undefined
133
+ || !("value" in lengthDescriptor)
134
+ || typeof lengthDescriptor.value !== "number"
135
+ || !Number.isSafeInteger(lengthDescriptor.value)
136
+ || lengthDescriptor.value < 0
137
+ || lengthDescriptor.value > maximum
138
+ ) return fail(`${label} length exceeds its reviewed bound`);
139
+ const length = lengthDescriptor.value;
140
+ const items: unknown[] = [];
141
+ const keys = Reflect.ownKeys(descriptors);
142
+ if (keys.length !== length + 1 || keys.some((key) => typeof key !== "string")) {
143
+ return fail(`${label} must not have holes, symbols, or named fields`);
144
+ }
145
+ for (let index = 0; index < length; index += 1) {
146
+ const descriptor = descriptors[String(index)];
147
+ if (
148
+ descriptor === undefined
149
+ || !descriptor.enumerable
150
+ || !("value" in descriptor)
151
+ ) return fail(`${label} must contain dense enumerable data elements`);
152
+ items.push(descriptor.value);
153
+ }
154
+ if (keys.some((key) =>
155
+ key !== "length"
156
+ && (typeof key !== "string" || !/^(0|[1-9][0-9]*)$/u.test(key)))) {
157
+ return fail(`${label} must not have holes, symbols, or named fields`);
158
+ }
159
+ return Object.freeze(items);
160
+ }
161
+
162
+ function exactKeys(
163
+ record: Readonly<Record<string, unknown>>,
164
+ expected: readonly string[],
165
+ label: string,
166
+ ): void {
167
+ const actual = Object.keys(record).sort();
168
+ const sorted = [...expected].sort();
169
+ if (
170
+ actual.length !== sorted.length
171
+ || actual.some((key, index) => key !== sorted[index])
172
+ ) return fail(`${label} fields drifted`);
173
+ }
174
+
175
+ function exactString<T extends string | number | boolean>(
176
+ value: unknown,
177
+ expected: T,
178
+ label: string,
179
+ ): T {
180
+ if (value !== expected) return fail(`${label} must equal ${expected}`);
181
+ return expected;
182
+ }
183
+
184
+ function digest(value: unknown, label: string): string {
185
+ if (typeof value !== "string" || !/^[a-f0-9]{64}$/u.test(value)) {
186
+ return fail(`${label} must be a lowercase SHA-256 digest`);
187
+ }
188
+ return value;
189
+ }
190
+
191
+ function timestamp(value: unknown, label: string): string {
192
+ if (
193
+ typeof value !== "string"
194
+ || value.length > 32
195
+ || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(value)
196
+ ) return fail(`${label} must be a canonical UTC timestamp`);
197
+ const parsed = new Date(value);
198
+ if (!Number.isFinite(parsed.getTime()) || parsed.toISOString() !== value) {
199
+ return fail(`${label} must be a real canonical UTC timestamp`);
200
+ }
201
+ return value;
202
+ }
203
+
204
+ function identifier(value: unknown, label: string, maximum: number): string {
205
+ if (
206
+ typeof value !== "string"
207
+ || value.length < 1
208
+ || Buffer.byteLength(value, "utf8") > maximum
209
+ || !/^[A-Za-z0-9._:-]+$/u.test(value)
210
+ ) return fail(`${label} must be a bounded provider identifier`);
211
+ return value;
212
+ }
213
+
214
+ function integer(
215
+ value: unknown,
216
+ label: string,
217
+ maximum: number,
218
+ minimum = 0,
219
+ ): number {
220
+ if (
221
+ typeof value !== "number"
222
+ || !Number.isSafeInteger(value)
223
+ || value < minimum
224
+ || value > maximum
225
+ ) return fail(`${label} must be an integer from ${String(minimum)} through ${String(maximum)}`);
226
+ return value;
227
+ }
228
+
229
+ function parseInterval(
230
+ value: unknown,
231
+ label: string,
232
+ ): Readonly<{ startedAt: string; finishedAt: string }> {
233
+ const record = dataRecord(value, label);
234
+ exactKeys(record, ["startedAt", "finishedAt"], label);
235
+ return parseIntervalFields(record, label);
236
+ }
237
+
238
+ function parseIntervalFields(
239
+ record: Readonly<Record<string, unknown>>,
240
+ label: string,
241
+ ): Readonly<{ startedAt: string; finishedAt: string }> {
242
+ const startedAt = timestamp(record.startedAt, `${label}.startedAt`);
243
+ const finishedAt = timestamp(record.finishedAt, `${label}.finishedAt`);
244
+ if (startedAt > finishedAt) return fail(`${label} timestamps are reversed`);
245
+ return Object.freeze({ startedAt, finishedAt });
246
+ }
247
+
248
+ function parseCapture(value: unknown): ApplePhotosContactEvidenceCapture {
249
+ const record = dataRecord(value, "source.capture");
250
+ exactKeys(record, [
251
+ "startedAt",
252
+ "finishedAt",
253
+ "photos",
254
+ "contacts",
255
+ "consistency",
256
+ "crossDatabaseAtomicity",
257
+ ], "source.capture");
258
+ const enclosing = parseIntervalFields(record, "source.capture");
259
+ const photos = parseInterval(record.photos, "source.capture.photos");
260
+ const contactValues = dataArray(
261
+ record.contacts,
262
+ "source.capture.contacts",
263
+ APPLE_PHOTOS_CONTACT_EVIDENCE_LIMITS.maximumContactsDatabases,
264
+ );
265
+ if (contactValues.length < 1) {
266
+ return fail("source.capture.contacts must contain at least one database interval");
267
+ }
268
+ const contacts = Object.freeze(contactValues.map((item, index) => {
269
+ const label = `source.capture.contacts[${String(index)}]`;
270
+ const contact = dataRecord(item, label);
271
+ exactKeys(contact, ["ordinal", "startedAt", "finishedAt"], label);
272
+ const interval = parseIntervalFields(contact, label);
273
+ const ordinal = integer(
274
+ contact.ordinal,
275
+ `${label}.ordinal`,
276
+ APPLE_PHOTOS_CONTACT_EVIDENCE_LIMITS.maximumContactsDatabases - 1,
277
+ );
278
+ if (ordinal !== index) return fail(`${label}.ordinal must equal its array position`);
279
+ return Object.freeze({ ordinal, ...interval });
280
+ }));
281
+ if (
282
+ enclosing.startedAt > photos.startedAt
283
+ || photos.finishedAt > enclosing.finishedAt
284
+ ) return fail("source.capture.photos must be inside its enclosing interval");
285
+ let priorFinishedAt = photos.finishedAt;
286
+ for (const [index, contact] of contacts.entries()) {
287
+ if (
288
+ contact.startedAt < priorFinishedAt
289
+ || contact.finishedAt > enclosing.finishedAt
290
+ ) return fail(`source.capture.contacts[${String(index)}] is outside capture order`);
291
+ priorFinishedAt = contact.finishedAt;
292
+ }
293
+ return Object.freeze({
294
+ ...enclosing,
295
+ photos,
296
+ contacts,
297
+ consistency: exactString(
298
+ record.consistency,
299
+ "independent-read-transactions",
300
+ "source.capture.consistency",
301
+ ),
302
+ crossDatabaseAtomicity: exactString(
303
+ record.crossDatabaseAtomicity,
304
+ "not-asserted",
305
+ "source.capture.crossDatabaseAtomicity",
306
+ ),
307
+ });
308
+ }
309
+
310
+ function parseCompleteness(value: unknown): ApplePhotosContactEvidenceCompleteness {
311
+ const record = dataRecord(value, "completeness");
312
+ exactKeys(record, Object.keys(COMPLETENESS), "completeness");
313
+ for (const [key, expected] of Object.entries(COMPLETENESS)) {
314
+ exactString(record[key], expected, `completeness.${key}`);
315
+ }
316
+ return COMPLETENESS;
317
+ }
318
+
319
+ function parsePrivacy(value: unknown): ApplePhotosContactEvidencePrivacy {
320
+ const record = dataRecord(value, "privacy");
321
+ exactKeys(record, Object.keys(PRIVACY), "privacy");
322
+ for (const [key, expected] of Object.entries(PRIVACY)) {
323
+ exactString(record[key], expected, `privacy.${key}`);
324
+ }
325
+ return PRIVACY;
326
+ }
327
+
328
+ function parseEvidence(
329
+ value: unknown,
330
+ index: number,
331
+ ): ApplePhotosContactEvidence {
332
+ const label = `evidence[${String(index)}]`;
333
+ const record = dataRecord(value, label);
334
+ exactKeys(record, [
335
+ "photosPersonId",
336
+ "appleContactId",
337
+ "linkedFaceCount",
338
+ "linkedAssetCount",
339
+ "firstAssetAt",
340
+ "lastAssetAt",
341
+ ], label);
342
+ const first = record.firstAssetAt === null
343
+ ? null
344
+ : timestamp(record.firstAssetAt, `${label}.firstAssetAt`);
345
+ const last = record.lastAssetAt === null
346
+ ? null
347
+ : timestamp(record.lastAssetAt, `${label}.lastAssetAt`);
348
+ if ((first === null) !== (last === null)) {
349
+ return fail(`${label} asset date bounds must both be null or both be timestamps`);
350
+ }
351
+ if (first !== null && last !== null && first > last) {
352
+ return fail(`${label} asset date bounds are reversed`);
353
+ }
354
+ const linkedFaceCount = integer(
355
+ record.linkedFaceCount,
356
+ `${label}.linkedFaceCount`,
357
+ 10_000_000,
358
+ );
359
+ const linkedAssetCount = integer(
360
+ record.linkedAssetCount,
361
+ `${label}.linkedAssetCount`,
362
+ 10_000_000,
363
+ );
364
+ if (linkedAssetCount > linkedFaceCount) {
365
+ return fail(`${label} cannot link more distinct assets than detected faces`);
366
+ }
367
+ if (linkedAssetCount === 0 && (first !== null || last !== null)) {
368
+ return fail(`${label} with zero linked assets must have null date bounds`);
369
+ }
370
+ return Object.freeze({
371
+ photosPersonId: identifier(record.photosPersonId, `${label}.photosPersonId`, 128),
372
+ appleContactId: identifier(record.appleContactId, `${label}.appleContactId`, 512),
373
+ linkedFaceCount,
374
+ linkedAssetCount,
375
+ firstAssetAt: first,
376
+ lastAssetAt: last,
377
+ });
378
+ }
379
+
380
+ function compareEvidence(
381
+ left: ApplePhotosContactEvidence,
382
+ right: ApplePhotosContactEvidence,
383
+ ): number {
384
+ const contact = Buffer.compare(
385
+ Buffer.from(left.appleContactId, "utf8"),
386
+ Buffer.from(right.appleContactId, "utf8"),
387
+ );
388
+ if (contact !== 0) return contact;
389
+ return Buffer.compare(
390
+ Buffer.from(left.photosPersonId, "utf8"),
391
+ Buffer.from(right.photosPersonId, "utf8"),
392
+ );
393
+ }
394
+
395
+ function artifactWithoutIntegrity(
396
+ input: ArtifactInput,
397
+ ): Omit<ApplePhotosContactEvidenceArtifact, "integrity"> {
398
+ const evidenceInput = dataArray(
399
+ input.evidence,
400
+ "evidence",
401
+ APPLE_PHOTOS_CONTACT_EVIDENCE_LIMITS.maximumPeople,
402
+ );
403
+ const evidence = Object.freeze(evidenceInput.map((item, index) =>
404
+ parseEvidence(item, index)).sort(compareEvidence));
405
+ if (evidence.length > APPLE_PHOTOS_CONTACT_EVIDENCE_LIMITS.maximumPeople) {
406
+ return fail("evidence exceeds the matched-person bound");
407
+ }
408
+ const people = new Set<string>();
409
+ const pairs = new Set<string>();
410
+ const contacts = new Set<string>();
411
+ let linkedFaces = 0;
412
+ let linkedAssets = 0;
413
+ for (const item of evidence) {
414
+ if (people.has(item.photosPersonId)) {
415
+ return fail("one Photos person appears more than once");
416
+ }
417
+ const pair = `${item.photosPersonId}\0${item.appleContactId}`;
418
+ if (pairs.has(pair)) return fail("one Photos/contact pair appears more than once");
419
+ people.add(item.photosPersonId);
420
+ pairs.add(pair);
421
+ contacts.add(item.appleContactId);
422
+ linkedFaces += item.linkedFaceCount;
423
+ linkedAssets += item.linkedAssetCount;
424
+ if (!Number.isSafeInteger(linkedFaces) || !Number.isSafeInteger(linkedAssets)) {
425
+ return fail("aggregate counts exceed the safe integer range");
426
+ }
427
+ }
428
+ const capture = parseCapture(input.capture);
429
+ const observedAt = timestamp(input.observedAt, "observedAt");
430
+ if (observedAt < capture.startedAt || observedAt > capture.finishedAt) {
431
+ return fail("observedAt must be inside the enclosing capture interval");
432
+ }
433
+ const lastContact = capture.contacts[capture.contacts.length - 1]!;
434
+ if (observedAt < lastContact.finishedAt) {
435
+ return fail("observedAt must not precede the final database capture");
436
+ }
437
+ return Object.freeze({
438
+ schemaVersion: APPLE_PHOTOS_CONTACT_EVIDENCE_SCHEMA_VERSION,
439
+ format: APPLE_PHOTOS_CONTACT_EVIDENCE_FORMAT,
440
+ transform: Object.freeze({
441
+ id: "apple-photos-person-contact-evidence" as const,
442
+ version: 1 as const,
443
+ }),
444
+ source: Object.freeze({
445
+ ...APPLE_PHOTOS_LOCAL_SOURCE,
446
+ platform: "darwin" as const,
447
+ libraryRealmSha256: digest(
448
+ input.libraryRealmSha256,
449
+ "source.libraryRealmSha256",
450
+ ),
451
+ generationSha256: digest(input.generationSha256, "source.generationSha256"),
452
+ photosSchemaSha256: digest(input.photosSchemaSha256, "source.photosSchemaSha256"),
453
+ contactsSchemaSha256: digest(input.contactsSchemaSha256, "source.contactsSchemaSha256"),
454
+ capture,
455
+ }),
456
+ observedAt,
457
+ scope: SCOPE,
458
+ completeness: COMPLETENESS,
459
+ privacy: PRIVACY,
460
+ counts: Object.freeze({
461
+ matchedPeople: evidence.length,
462
+ uniqueContacts: contacts.size,
463
+ linkedFaces,
464
+ linkedAssets,
465
+ }),
466
+ evidence,
467
+ });
468
+ }
469
+
470
+ export function createApplePhotosContactEvidenceArtifact(
471
+ input: ArtifactInput,
472
+ ): ApplePhotosContactEvidenceArtifact {
473
+ const base = artifactWithoutIntegrity(input);
474
+ return Object.freeze({
475
+ ...base,
476
+ integrity: Object.freeze({
477
+ algorithm: "sha256" as const,
478
+ artifactSha256: sha256(canonicalJson(base)),
479
+ }),
480
+ });
481
+ }
482
+
483
+ function parseArtifactSource(
484
+ value: unknown,
485
+ ): ApplePhotosContactEvidenceArtifact["source"] {
486
+ const source = dataRecord(value, "source");
487
+ exactKeys(source, [
488
+ "id",
489
+ "version",
490
+ "platform",
491
+ "libraryRealmSha256",
492
+ "generationSha256",
493
+ "photosSchemaSha256",
494
+ "contactsSchemaSha256",
495
+ "capture",
496
+ ], "source");
497
+ return Object.freeze({
498
+ id: exactString(source.id, "apple-photos-local", "source.id"),
499
+ version: exactString(source.version, "1.0.0", "source.version"),
500
+ platform: exactString(source.platform, "darwin", "source.platform"),
501
+ libraryRealmSha256: digest(
502
+ source.libraryRealmSha256,
503
+ "source.libraryRealmSha256",
504
+ ),
505
+ generationSha256: digest(source.generationSha256, "source.generationSha256"),
506
+ photosSchemaSha256: digest(source.photosSchemaSha256, "source.photosSchemaSha256"),
507
+ contactsSchemaSha256: digest(source.contactsSchemaSha256, "source.contactsSchemaSha256"),
508
+ capture: parseCapture(source.capture),
509
+ });
510
+ }
511
+
512
+ export function parseApplePhotosContactEvidenceArtifact(
513
+ value: unknown,
514
+ ): ApplePhotosContactEvidenceArtifact {
515
+ const artifact = dataRecord(value, "artifact");
516
+ exactKeys(artifact, [
517
+ "schemaVersion",
518
+ "format",
519
+ "transform",
520
+ "source",
521
+ "observedAt",
522
+ "scope",
523
+ "completeness",
524
+ "privacy",
525
+ "counts",
526
+ "evidence",
527
+ "integrity",
528
+ ], "artifact");
529
+ exactString(artifact.schemaVersion, 1, "artifact.schemaVersion");
530
+ exactString(
531
+ artifact.format,
532
+ APPLE_PHOTOS_CONTACT_EVIDENCE_FORMAT,
533
+ "artifact.format",
534
+ );
535
+ const transform = dataRecord(artifact.transform, "transform");
536
+ exactKeys(transform, ["id", "version"], "transform");
537
+ exactString(
538
+ transform.id,
539
+ "apple-photos-person-contact-evidence",
540
+ "transform.id",
541
+ );
542
+ exactString(transform.version, 1, "transform.version");
543
+ const scope = dataRecord(artifact.scope, "scope");
544
+ exactKeys(scope, Object.keys(SCOPE), "scope");
545
+ for (const [key, expected] of Object.entries(SCOPE)) {
546
+ exactString(scope[key], expected, `scope.${key}`);
547
+ }
548
+ const evidenceValue = artifact.evidence;
549
+ const evidence = dataArray(
550
+ evidenceValue,
551
+ "evidence",
552
+ APPLE_PHOTOS_CONTACT_EVIDENCE_LIMITS.maximumPeople,
553
+ );
554
+ const source = parseArtifactSource(artifact.source);
555
+ const parsed = createApplePhotosContactEvidenceArtifact({
556
+ observedAt: timestamp(artifact.observedAt, "observedAt"),
557
+ libraryRealmSha256: source.libraryRealmSha256,
558
+ generationSha256: source.generationSha256,
559
+ photosSchemaSha256: source.photosSchemaSha256,
560
+ contactsSchemaSha256: source.contactsSchemaSha256,
561
+ capture: source.capture,
562
+ evidence: evidence.map(parseEvidence),
563
+ });
564
+ const counts = dataRecord(artifact.counts, "counts");
565
+ exactKeys(counts, Object.keys(parsed.counts), "counts");
566
+ for (const [key, expected] of Object.entries(parsed.counts)) {
567
+ if (counts[key] !== expected) return fail(`counts.${key} is inconsistent`);
568
+ }
569
+ parseCompleteness(artifact.completeness);
570
+ parsePrivacy(artifact.privacy);
571
+ const integrity = dataRecord(artifact.integrity, "integrity");
572
+ exactKeys(integrity, ["algorithm", "artifactSha256"], "integrity");
573
+ exactString(integrity.algorithm, "sha256", "integrity.algorithm");
574
+ if (
575
+ digest(integrity.artifactSha256, "integrity.artifactSha256")
576
+ !== parsed.integrity.artifactSha256
577
+ ) return fail("artifact integrity digest does not match its content");
578
+ if (canonicalJson(parsed) !== canonicalJson(value)) {
579
+ return fail("artifact is not in its canonical schema order and representation");
580
+ }
581
+ return parsed;
582
+ }
583
+
584
+ function receiptWithoutIntegrity(
585
+ input: ExportInput,
586
+ output: ApplePhotosContactEvidenceArtifact,
587
+ ): Omit<ApplePhotosContactEvidenceExportReceipt, "integrity"> {
588
+ const runId = input.runId ?? randomUUID();
589
+ if (!/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/u.test(runId)) {
590
+ return fail("runId must be a lowercase UUIDv4");
591
+ }
592
+ const contactsDatabases = integer(
593
+ input.contactsDatabases,
594
+ "counts.contactsDatabases",
595
+ APPLE_PHOTOS_CONTACT_EVIDENCE_LIMITS.maximumContactsDatabases,
596
+ 1,
597
+ );
598
+ const startedAt = timestamp(input.startedAt, "startedAt");
599
+ const finishedAt = timestamp(input.finishedAt, "finishedAt");
600
+ if (startedAt > finishedAt) return fail("receipt timestamps are reversed");
601
+ if (
602
+ startedAt > output.source.capture.startedAt
603
+ || output.source.capture.finishedAt > finishedAt
604
+ ) return fail("the database capture must be inside the receipt interval");
605
+ if (contactsDatabases !== output.source.capture.contacts.length) {
606
+ return fail("counts.contactsDatabases must match the ordered capture intervals");
607
+ }
608
+ return Object.freeze({
609
+ schemaVersion: 1 as const,
610
+ format: APPLE_PHOTOS_CONTACT_EVIDENCE_RECEIPT_FORMAT,
611
+ runId,
612
+ operation: "apple-photos.export-contact-evidence" as const,
613
+ status: "succeeded" as const,
614
+ transport: "local-sqlite-vacuum-capture" as const,
615
+ implementation: Object.freeze({
616
+ producer: Object.freeze({
617
+ package: "@hraness/ghostget" as const,
618
+ version: GHOSTGET_VERSION,
619
+ }),
620
+ source: APPLE_PHOTOS_LOCAL_SOURCE,
621
+ }),
622
+ startedAt,
623
+ finishedAt,
624
+ bounds: Object.freeze({
625
+ captureAttemptsPerDatabase:
626
+ APPLE_PHOTOS_CONTACT_EVIDENCE_LIMITS.captureAttemptsPerDatabase,
627
+ maximumPhotosDatabaseBytes:
628
+ APPLE_PHOTOS_CONTACT_EVIDENCE_LIMITS.maximumPhotosDatabaseBytes,
629
+ maximumContactsDatabases:
630
+ APPLE_PHOTOS_CONTACT_EVIDENCE_LIMITS.maximumContactsDatabases,
631
+ maximumContactsDatabaseBytes:
632
+ APPLE_PHOTOS_CONTACT_EVIDENCE_LIMITS.maximumContactsDatabaseBytes,
633
+ maximumDirectoryEntries:
634
+ APPLE_PHOTOS_CONTACT_EVIDENCE_LIMITS.maximumDirectoryEntries,
635
+ maximumContactsSourceDirectories:
636
+ APPLE_PHOTOS_CONTACT_EVIDENCE_LIMITS.maximumContactsSourceDirectories,
637
+ maximumPeople: APPLE_PHOTOS_CONTACT_EVIDENCE_LIMITS.maximumPeople,
638
+ maximumContacts: APPLE_PHOTOS_CONTACT_EVIDENCE_LIMITS.maximumContacts,
639
+ }),
640
+ source: output.source,
641
+ completeness: output.completeness,
642
+ counts: Object.freeze({
643
+ ...output.counts,
644
+ contactsDatabases,
645
+ }),
646
+ output: Object.freeze({
647
+ schemaVersion: output.schemaVersion,
648
+ format: output.format,
649
+ artifactSha256: output.integrity.artifactSha256,
650
+ }),
651
+ privacy: output.privacy,
652
+ });
653
+ }
654
+
655
+ export function createApplePhotosContactEvidenceExportResult(
656
+ input: ExportInput,
657
+ ): ApplePhotosContactEvidenceExportResult {
658
+ const output = createApplePhotosContactEvidenceArtifact(input);
659
+ const base = receiptWithoutIntegrity(input, output);
660
+ const receipt: ApplePhotosContactEvidenceExportReceipt = Object.freeze({
661
+ ...base,
662
+ integrity: Object.freeze({
663
+ algorithm: "sha256" as const,
664
+ receiptSha256: sha256(canonicalJson(base)),
665
+ }),
666
+ });
667
+ return Object.freeze({ receipt, output });
668
+ }
669
+
670
+ function parseBounds(
671
+ value: unknown,
672
+ ): ApplePhotosContactEvidenceExportReceipt["bounds"] {
673
+ const bounds = dataRecord(value, "receipt.bounds");
674
+ const expected = {
675
+ captureAttemptsPerDatabase:
676
+ APPLE_PHOTOS_CONTACT_EVIDENCE_LIMITS.captureAttemptsPerDatabase,
677
+ maximumPhotosDatabaseBytes:
678
+ APPLE_PHOTOS_CONTACT_EVIDENCE_LIMITS.maximumPhotosDatabaseBytes,
679
+ maximumContactsDatabases:
680
+ APPLE_PHOTOS_CONTACT_EVIDENCE_LIMITS.maximumContactsDatabases,
681
+ maximumContactsDatabaseBytes:
682
+ APPLE_PHOTOS_CONTACT_EVIDENCE_LIMITS.maximumContactsDatabaseBytes,
683
+ maximumDirectoryEntries:
684
+ APPLE_PHOTOS_CONTACT_EVIDENCE_LIMITS.maximumDirectoryEntries,
685
+ maximumContactsSourceDirectories:
686
+ APPLE_PHOTOS_CONTACT_EVIDENCE_LIMITS.maximumContactsSourceDirectories,
687
+ maximumPeople: APPLE_PHOTOS_CONTACT_EVIDENCE_LIMITS.maximumPeople,
688
+ maximumContacts: APPLE_PHOTOS_CONTACT_EVIDENCE_LIMITS.maximumContacts,
689
+ } as const;
690
+ exactKeys(bounds, Object.keys(expected), "receipt.bounds");
691
+ for (const [key, expectedValue] of Object.entries(expected)) {
692
+ if (bounds[key] !== expectedValue) return fail(`receipt.bounds.${key} drifted`);
693
+ }
694
+ return Object.freeze(expected);
695
+ }
696
+
697
+ export function parseApplePhotosContactEvidenceExportResult(
698
+ value: unknown,
699
+ ): ApplePhotosContactEvidenceExportResult {
700
+ const result = dataRecord(value, "result");
701
+ exactKeys(result, ["receipt", "output"], "result");
702
+ const output = parseApplePhotosContactEvidenceArtifact(result.output);
703
+ const receipt = dataRecord(result.receipt, "receipt");
704
+ exactKeys(receipt, [
705
+ "schemaVersion",
706
+ "format",
707
+ "runId",
708
+ "operation",
709
+ "status",
710
+ "transport",
711
+ "implementation",
712
+ "startedAt",
713
+ "finishedAt",
714
+ "bounds",
715
+ "source",
716
+ "completeness",
717
+ "counts",
718
+ "output",
719
+ "privacy",
720
+ "integrity",
721
+ ], "receipt");
722
+ exactString(receipt.schemaVersion, 1, "receipt.schemaVersion");
723
+ exactString(
724
+ receipt.format,
725
+ APPLE_PHOTOS_CONTACT_EVIDENCE_RECEIPT_FORMAT,
726
+ "receipt.format",
727
+ );
728
+ exactString(
729
+ receipt.operation,
730
+ "apple-photos.export-contact-evidence",
731
+ "receipt.operation",
732
+ );
733
+ exactString(receipt.status, "succeeded", "receipt.status");
734
+ exactString(
735
+ receipt.transport,
736
+ "local-sqlite-vacuum-capture",
737
+ "receipt.transport",
738
+ );
739
+ const implementation = dataRecord(receipt.implementation, "receipt.implementation");
740
+ exactKeys(implementation, ["producer", "source"], "receipt.implementation");
741
+ const producer = dataRecord(implementation.producer, "receipt.implementation.producer");
742
+ exactKeys(producer, ["package", "version"], "receipt.implementation.producer");
743
+ exactString(
744
+ producer.package,
745
+ "@hraness/ghostget",
746
+ "receipt.implementation.producer.package",
747
+ );
748
+ exactString(
749
+ producer.version,
750
+ GHOSTGET_VERSION,
751
+ "receipt.implementation.producer.version",
752
+ );
753
+ const implementationSource = dataRecord(
754
+ implementation.source,
755
+ "receipt.implementation.source",
756
+ );
757
+ exactKeys(implementationSource, ["id", "version"], "receipt.implementation.source");
758
+ exactString(
759
+ implementationSource.id,
760
+ APPLE_PHOTOS_LOCAL_SOURCE.id,
761
+ "receipt.implementation.source.id",
762
+ );
763
+ exactString(
764
+ implementationSource.version,
765
+ APPLE_PHOTOS_LOCAL_SOURCE.version,
766
+ "receipt.implementation.source.version",
767
+ );
768
+ parseBounds(receipt.bounds);
769
+ const source = parseArtifactSource(receipt.source);
770
+ if (canonicalJson(source) !== canonicalJson(output.source)) {
771
+ return fail("receipt source does not match the artifact source");
772
+ }
773
+ parseCompleteness(receipt.completeness);
774
+ parsePrivacy(receipt.privacy);
775
+ const counts = dataRecord(receipt.counts, "receipt.counts");
776
+ exactKeys(counts, [...Object.keys(output.counts), "contactsDatabases"], "receipt.counts");
777
+ for (const [key, expected] of Object.entries(output.counts)) {
778
+ if (counts[key] !== expected) return fail(`receipt.counts.${key} is inconsistent`);
779
+ }
780
+ const contactsDatabases = integer(
781
+ counts.contactsDatabases,
782
+ "receipt.counts.contactsDatabases",
783
+ APPLE_PHOTOS_CONTACT_EVIDENCE_LIMITS.maximumContactsDatabases,
784
+ 1,
785
+ );
786
+ const outputReference = dataRecord(receipt.output, "receipt.output");
787
+ exactKeys(
788
+ outputReference,
789
+ ["schemaVersion", "format", "artifactSha256"],
790
+ "receipt.output",
791
+ );
792
+ exactString(outputReference.schemaVersion, 1, "receipt.output.schemaVersion");
793
+ exactString(
794
+ outputReference.format,
795
+ APPLE_PHOTOS_CONTACT_EVIDENCE_FORMAT,
796
+ "receipt.output.format",
797
+ );
798
+ if (
799
+ digest(outputReference.artifactSha256, "receipt.output.artifactSha256")
800
+ !== output.integrity.artifactSha256
801
+ ) return fail("receipt output digest does not match the artifact");
802
+ const integrity = dataRecord(receipt.integrity, "receipt.integrity");
803
+ exactKeys(integrity, ["algorithm", "receiptSha256"], "receipt.integrity");
804
+ exactString(integrity.algorithm, "sha256", "receipt.integrity.algorithm");
805
+ const rebuilt = createApplePhotosContactEvidenceExportResult({
806
+ runId: identifier(receipt.runId, "receipt.runId", 64),
807
+ startedAt: timestamp(receipt.startedAt, "receipt.startedAt"),
808
+ finishedAt: timestamp(receipt.finishedAt, "receipt.finishedAt"),
809
+ contactsDatabases,
810
+ observedAt: output.observedAt,
811
+ libraryRealmSha256: output.source.libraryRealmSha256,
812
+ generationSha256: output.source.generationSha256,
813
+ photosSchemaSha256: output.source.photosSchemaSha256,
814
+ contactsSchemaSha256: output.source.contactsSchemaSha256,
815
+ capture: output.source.capture,
816
+ evidence: output.evidence,
817
+ });
818
+ if (
819
+ digest(integrity.receiptSha256, "receipt.integrity.receiptSha256")
820
+ !== rebuilt.receipt.integrity.receiptSha256
821
+ ) return fail("receipt integrity digest does not match its content");
822
+ if (canonicalJson(rebuilt) !== canonicalJson(value)) {
823
+ return fail("result is not in its canonical schema representation");
824
+ }
825
+ return rebuilt;
826
+ }
827
+
828
+ export function encodeApplePhotosContactEvidenceExportResult(
829
+ value: ApplePhotosContactEvidenceExportResult,
830
+ ): string {
831
+ const parsed = parseApplePhotosContactEvidenceExportResult(value);
832
+ const encoded = `${canonicalJson(parsed)}\n`;
833
+ if (
834
+ Buffer.byteLength(encoded, "utf8")
835
+ > APPLE_PHOTOS_CONTACT_EVIDENCE_LIMITS.maximumWireBytes
836
+ ) return fail("encoded result exceeds the wire byte bound");
837
+ return encoded;
838
+ }