@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,1183 @@
1
+ import {
2
+ createHash,
3
+ createCipheriv,
4
+ createDecipheriv,
5
+ randomBytes,
6
+ } from "node:crypto";
7
+ import { join } from "node:path";
8
+
9
+ import { canonicalJson } from "./canonical-json";
10
+ import {
11
+ projectionAuthIdentityHash,
12
+ withReadProjectionAuthAdmission,
13
+ } from "./read-projection-admission";
14
+ import {
15
+ createPrivateJsonIfAbsent,
16
+ ensurePrivateStateDirectory,
17
+ listPrivateStateDirectory,
18
+ readPrivateStateFileIfPresent,
19
+ readRegularFile,
20
+ removePrivateStateFile,
21
+ removePrivateStateFileIfUnchanged,
22
+ ghostgetStateHome,
23
+ writePrivateJson,
24
+ writePrivateJsonIfUnchanged,
25
+ } from "./storage";
26
+
27
+ const SESSION_SECRET_DIRECTORY = "session-secrets";
28
+ const SESSION_SECRET_COORDINATE_DIRECTORY = "coordinates";
29
+ const SESSION_SECRET_KEY = ".session-encryption-key";
30
+ const MAX_KEY_BYTES = 512;
31
+ const MAX_PLAINTEXT_BYTES = 64 * 1024;
32
+ const MAX_ENCRYPTED_BYTES = 128 * 1024;
33
+ const MAX_COORDINATE_BYTES = 4 * 1024;
34
+
35
+ type Environment = Readonly<Record<string, string | undefined>>;
36
+ type JsonRecord = Record<string, unknown>;
37
+
38
+ type SessionKey = {
39
+ readonly id: string;
40
+ readonly value: Buffer;
41
+ };
42
+
43
+ type SessionSecretCoordinateState = {
44
+ readonly schemaVersion: 1;
45
+ readonly namespace: string;
46
+ readonly authId: string;
47
+ readonly authIdentityHash: string;
48
+ readonly generation: string;
49
+ };
50
+
51
+ type SessionSecretCoordinateSnapshot = {
52
+ readonly value: SessionSecretCoordinateState;
53
+ readonly contentSha256: string;
54
+ };
55
+
56
+ type EncryptedSessionSecretV1 = {
57
+ readonly schemaVersion: 1;
58
+ readonly encryption: "aes-256-gcm";
59
+ readonly namespace: string;
60
+ readonly authId: string;
61
+ readonly authHash: string;
62
+ readonly iv: string;
63
+ readonly ciphertext: string;
64
+ readonly tag: string;
65
+ };
66
+
67
+ type EncryptedSessionSecretV2 = {
68
+ readonly schemaVersion: 2;
69
+ readonly encryption: "aes-256-gcm";
70
+ readonly keyId: string;
71
+ readonly namespace: string;
72
+ readonly authId: string;
73
+ readonly authHash: string;
74
+ readonly iv: string;
75
+ readonly ciphertext: string;
76
+ readonly tag: string;
77
+ };
78
+
79
+ type EncryptedSessionSecretV3 = {
80
+ readonly schemaVersion: 3;
81
+ readonly encryption: "aes-256-gcm";
82
+ readonly keyId: string;
83
+ readonly namespace: string;
84
+ readonly authId: string;
85
+ readonly authIdentityHash: string;
86
+ readonly generation: string;
87
+ readonly iv: string;
88
+ readonly ciphertext: string;
89
+ readonly tag: string;
90
+ };
91
+
92
+ type EncryptedSessionSecret =
93
+ | EncryptedSessionSecretV1
94
+ | EncryptedSessionSecretV2
95
+ | EncryptedSessionSecretV3;
96
+
97
+ export type SessionSecretSnapshot = {
98
+ readonly value: unknown;
99
+ /**
100
+ * An opaque compare-and-swap revision. Present files use the SHA-256 of the
101
+ * exact encrypted bytes; absent files use an auth-incarnation- and
102
+ * coordinate-generation-bound digest.
103
+ * Injected stores may still use null for an unversioned absent coordinate.
104
+ */
105
+ readonly contentSha256: string | null;
106
+ };
107
+
108
+ export type SessionSecretWriteResult =
109
+ | {
110
+ readonly written: true;
111
+ readonly contentSha256: string;
112
+ }
113
+ | {
114
+ readonly written: false;
115
+ };
116
+
117
+ function isRecord(value: unknown): value is JsonRecord {
118
+ return typeof value === "object" && value !== null && !Array.isArray(value);
119
+ }
120
+
121
+ function record(value: unknown, label: string): JsonRecord {
122
+ if (!isRecord(value)) {
123
+ throw new Error(`${label} must be an object`);
124
+ }
125
+ return value;
126
+ }
127
+
128
+ function exactKeys(value: JsonRecord, expected: readonly string[], label: string): void {
129
+ if (Object.keys(value).sort().join(",") !== [...expected].sort().join(",")) {
130
+ throw new Error(`${label} has unsupported fields`);
131
+ }
132
+ }
133
+
134
+ function validateCoordinate(namespace: string, authId: string, authHash: string): void {
135
+ if (!/^[a-z][a-z0-9-]{0,47}$/u.test(namespace)) {
136
+ throw new Error("session-secret namespace must be lowercase kebab-case");
137
+ }
138
+ if (!/^[a-z][a-z0-9-]{0,47}$/u.test(authId)) {
139
+ throw new Error("session-secret auth ID must be lowercase kebab-case");
140
+ }
141
+ if (!/^[a-f0-9]{64}$/u.test(authHash)) {
142
+ throw new Error("session-secret auth hash is malformed");
143
+ }
144
+ }
145
+
146
+ function directory(environment: Environment): string {
147
+ return join(ghostgetStateHome(environment), SESSION_SECRET_DIRECTORY);
148
+ }
149
+
150
+ function secretPath(
151
+ namespace: string,
152
+ authId: string,
153
+ environment: Environment,
154
+ ): string {
155
+ return join(directory(environment), `${namespace}--${authId}.json`);
156
+ }
157
+
158
+ function coordinateDirectory(environment: Environment): string {
159
+ return join(directory(environment), SESSION_SECRET_COORDINATE_DIRECTORY);
160
+ }
161
+
162
+ function coordinatePath(
163
+ namespace: string,
164
+ authId: string,
165
+ environment: Environment,
166
+ ): string {
167
+ return join(
168
+ coordinateDirectory(environment),
169
+ `${namespace}--${authId}.json`,
170
+ );
171
+ }
172
+
173
+ function coordinateSnapshot(
174
+ value: SessionSecretCoordinateState,
175
+ ): SessionSecretCoordinateSnapshot {
176
+ return Object.freeze({
177
+ value,
178
+ contentSha256: createHash("sha256")
179
+ .update(`${canonicalJson(value)}\n`, "utf8")
180
+ .digest("hex"),
181
+ });
182
+ }
183
+
184
+ function parseCoordinateState(
185
+ text: string,
186
+ namespace: string,
187
+ authId: string,
188
+ ): SessionSecretCoordinateSnapshot {
189
+ let parsed: unknown;
190
+ try {
191
+ parsed = JSON.parse(text) as unknown;
192
+ } catch {
193
+ throw new Error("session-secret coordinate state is malformed JSON");
194
+ }
195
+ const value = record(parsed, "session-secret coordinate state");
196
+ exactKeys(
197
+ value,
198
+ [
199
+ "schemaVersion",
200
+ "namespace",
201
+ "authId",
202
+ "authIdentityHash",
203
+ "generation",
204
+ ],
205
+ "session-secret coordinate state",
206
+ );
207
+ if (
208
+ value.schemaVersion !== 1
209
+ || value.namespace !== namespace
210
+ || value.authId !== authId
211
+ || typeof value.authIdentityHash !== "string"
212
+ || !/^[a-f0-9]{64}$/u.test(value.authIdentityHash)
213
+ || typeof value.generation !== "string"
214
+ || !/^[a-f0-9]{64}$/u.test(value.generation)
215
+ ) throw new Error("session-secret coordinate state is malformed");
216
+ const normalized: SessionSecretCoordinateState = Object.freeze({
217
+ schemaVersion: 1,
218
+ namespace,
219
+ authId,
220
+ authIdentityHash: value.authIdentityHash,
221
+ generation: value.generation,
222
+ });
223
+ if (text !== `${canonicalJson(normalized)}\n`) {
224
+ throw new Error("session-secret coordinate state is not canonical JSON");
225
+ }
226
+ return coordinateSnapshot(normalized);
227
+ }
228
+
229
+ function readCoordinateState(
230
+ namespace: string,
231
+ authId: string,
232
+ environment: Environment,
233
+ ): SessionSecretCoordinateSnapshot | null {
234
+ ensurePrivateStateDirectory(coordinateDirectory(environment), environment);
235
+ const text = readPrivateStateFileIfPresent(
236
+ coordinatePath(namespace, authId, environment),
237
+ MAX_COORDINATE_BYTES,
238
+ "session-secret coordinate state",
239
+ environment,
240
+ );
241
+ return text === null
242
+ ? null
243
+ : parseCoordinateState(text, namespace, authId);
244
+ }
245
+
246
+ function newCoordinateState(
247
+ namespace: string,
248
+ authId: string,
249
+ authIdentityHash: string,
250
+ previousGeneration: string | null = null,
251
+ ): SessionSecretCoordinateState {
252
+ let generation: string;
253
+ do {
254
+ generation = randomBytes(32).toString("hex");
255
+ } while (generation === previousGeneration);
256
+ return Object.freeze({
257
+ schemaVersion: 1,
258
+ namespace,
259
+ authId,
260
+ authIdentityHash,
261
+ generation,
262
+ });
263
+ }
264
+
265
+ function replaceCoordinateState(
266
+ current: SessionSecretCoordinateSnapshot | null,
267
+ namespace: string,
268
+ authId: string,
269
+ authIdentityHash: string,
270
+ environment: Environment,
271
+ ): SessionSecretCoordinateSnapshot {
272
+ const replacement = newCoordinateState(
273
+ namespace,
274
+ authId,
275
+ authIdentityHash,
276
+ current?.value.generation ?? null,
277
+ );
278
+ const path = coordinatePath(namespace, authId, environment);
279
+ const written = current === null
280
+ ? createPrivateJsonIfAbsent(path, replacement, { environment }).created
281
+ : writePrivateJsonIfUnchanged(path, replacement, {
282
+ expectedCurrentContentSha256: current.contentSha256,
283
+ });
284
+ if (!written) {
285
+ throw new Error("session-secret coordinate generation changed concurrently");
286
+ }
287
+ return coordinateSnapshot(replacement);
288
+ }
289
+
290
+ function ensureCoordinateState(
291
+ namespace: string,
292
+ authId: string,
293
+ authIdentityHash: string,
294
+ environment: Environment,
295
+ ): SessionSecretCoordinateSnapshot {
296
+ const current = readCoordinateState(namespace, authId, environment);
297
+ if (current !== null) return current;
298
+ return replaceCoordinateState(
299
+ null,
300
+ namespace,
301
+ authId,
302
+ authIdentityHash,
303
+ environment,
304
+ );
305
+ }
306
+
307
+ function keyPath(environment: Environment): string {
308
+ return join(ghostgetStateHome(environment), SESSION_SECRET_KEY);
309
+ }
310
+
311
+ function sessionKeyId(key: Uint8Array): string {
312
+ return createHash("sha256")
313
+ .update("io-session-key-id-v1\0", "utf8")
314
+ .update(key)
315
+ .digest("hex");
316
+ }
317
+
318
+ function newSessionKeyRecord(): Readonly<{
319
+ schemaVersion: 2;
320
+ keyId: string;
321
+ key: string;
322
+ }> {
323
+ const key = randomBytes(32);
324
+ return Object.freeze({
325
+ schemaVersion: 2,
326
+ keyId: sessionKeyId(key),
327
+ key: key.toString("hex"),
328
+ });
329
+ }
330
+
331
+ function encryptedStoreHasState(environment: Environment): boolean {
332
+ const sessionDirectory = directory(environment);
333
+ const identity = ensurePrivateStateDirectory(sessionDirectory, environment);
334
+ return listPrivateStateDirectory(
335
+ sessionDirectory,
336
+ environment,
337
+ identity,
338
+ ).some((entry) =>
339
+ entry.name !== SESSION_SECRET_COORDINATE_DIRECTORY
340
+ || entry.kind !== "directory");
341
+ }
342
+
343
+ function parseSessionKey(text: string): SessionKey {
344
+ try {
345
+ const parsed: unknown = JSON.parse(text);
346
+ const value = record(parsed, "session encryption key");
347
+ if (value.schemaVersion === 1) {
348
+ exactKeys(value, ["schemaVersion", "key"], "session encryption key");
349
+ } else if (value.schemaVersion === 2) {
350
+ exactKeys(
351
+ value,
352
+ ["schemaVersion", "keyId", "key"],
353
+ "session encryption key",
354
+ );
355
+ } else {
356
+ throw new Error("unsupported session encryption-key schema");
357
+ }
358
+ if (
359
+ typeof value.key !== "string"
360
+ || !/^[a-f0-9]{64}$/u.test(value.key)
361
+ ) throw new Error("invalid session encryption-key bytes");
362
+ const key = Buffer.from(value.key, "hex");
363
+ const derivedId = sessionKeyId(key);
364
+ if (
365
+ value.schemaVersion === 2
366
+ && (
367
+ typeof value.keyId !== "string"
368
+ || !/^[a-f0-9]{64}$/u.test(value.keyId)
369
+ || value.keyId !== derivedId
370
+ )
371
+ ) throw new Error("invalid session encryption-key identity");
372
+ return Object.freeze({ id: derivedId, value: key });
373
+ } catch {
374
+ throw new Error("session encryption key is malformed");
375
+ }
376
+ }
377
+
378
+ function sessionKey(environment: Environment): SessionKey {
379
+ const path = keyPath(environment);
380
+ try {
381
+ return parseSessionKey(
382
+ readRegularFile(path, MAX_KEY_BYTES, "session encryption key").trim(),
383
+ );
384
+ } catch (error) {
385
+ if (
386
+ error instanceof Error
387
+ && error.message === "session encryption key is malformed"
388
+ ) throw error;
389
+ if (encryptedStoreHasState(environment)) {
390
+ throw new Error(
391
+ "session encryption key is unavailable while encrypted session state exists",
392
+ );
393
+ }
394
+ }
395
+
396
+ createPrivateJsonIfAbsent(path, newSessionKeyRecord(), {
397
+ environment,
398
+ privateParent: true,
399
+ });
400
+ try {
401
+ return parseSessionKey(
402
+ readRegularFile(path, MAX_KEY_BYTES, "session encryption key").trim(),
403
+ );
404
+ } catch {
405
+ throw new Error("session encryption key is unavailable");
406
+ }
407
+ }
408
+
409
+ function additionalData(
410
+ schemaVersion: 1 | 2 | 3,
411
+ keyId: string | null,
412
+ namespace: string,
413
+ authId: string,
414
+ authIdentityHash: string,
415
+ generation: string | null,
416
+ ): Buffer {
417
+ return Buffer.from(
418
+ schemaVersion === 1
419
+ ? `io-session-secret-v1\0${namespace}\0${authId}\0${authIdentityHash}`
420
+ : schemaVersion === 2
421
+ ? `io-session-secret-v2\0${keyId ?? ""}\0${namespace}\0${authId}\0${authIdentityHash}`
422
+ : `io-session-secret-v3\0${keyId ?? ""}\0${namespace}\0${authId}\0${authIdentityHash}\0${generation ?? ""}`,
423
+ "utf8",
424
+ );
425
+ }
426
+
427
+ function boundedBase64(value: unknown, label: string, maximumBytes: number): Buffer {
428
+ if (
429
+ typeof value !== "string"
430
+ || value.length > Math.ceil(maximumBytes / 3) * 4 + 4
431
+ || !/^[A-Za-z0-9+/]*={0,2}$/u.test(value)
432
+ ) throw new Error(`${label} is malformed`);
433
+ const bytes = Buffer.from(value, "base64");
434
+ if (
435
+ bytes.byteLength > maximumBytes
436
+ || bytes.toString("base64") !== value
437
+ ) throw new Error(`${label} is malformed`);
438
+ return bytes;
439
+ }
440
+
441
+ function encryptedSessionSecret(
442
+ namespace: string,
443
+ authId: string,
444
+ authIdentityHash: string,
445
+ generation: string,
446
+ value: unknown,
447
+ environment: Environment = process.env,
448
+ ): EncryptedSessionSecretV3 {
449
+ validateCoordinate(namespace, authId, authIdentityHash);
450
+ if (!/^[a-f0-9]{64}$/u.test(generation)) {
451
+ throw new Error("session-secret coordinate generation is malformed");
452
+ }
453
+ const plaintext = Buffer.from(canonicalJson(value), "utf8");
454
+ if (plaintext.byteLength > MAX_PLAINTEXT_BYTES) {
455
+ throw new Error("session secret exceeded its plaintext byte bound");
456
+ }
457
+ ensurePrivateStateDirectory(directory(environment), environment);
458
+ const iv = randomBytes(12);
459
+ const key = sessionKey(environment);
460
+ assertSessionKeyOwnsEncryptedStore(key, environment);
461
+ const cipher = createCipheriv("aes-256-gcm", key.value, iv);
462
+ cipher.setAAD(additionalData(
463
+ 3,
464
+ key.id,
465
+ namespace,
466
+ authId,
467
+ authIdentityHash,
468
+ generation,
469
+ ));
470
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
471
+ return Object.freeze({
472
+ schemaVersion: 3,
473
+ encryption: "aes-256-gcm",
474
+ keyId: key.id,
475
+ namespace,
476
+ authId,
477
+ authIdentityHash,
478
+ generation,
479
+ iv: iv.toString("base64"),
480
+ ciphertext: ciphertext.toString("base64"),
481
+ tag: cipher.getAuthTag().toString("base64"),
482
+ });
483
+ }
484
+
485
+ function encryptedContentSha256(encrypted: EncryptedSessionSecret): string {
486
+ return createHash("sha256")
487
+ .update(`${canonicalJson(encrypted)}\n`, "utf8")
488
+ .digest("hex");
489
+ }
490
+
491
+ function absentContentSha256(
492
+ namespace: string,
493
+ authId: string,
494
+ authIdentityHash: string,
495
+ generation: string,
496
+ ): string {
497
+ return createHash("sha256")
498
+ .update(
499
+ `io-session-secret-absent-revision-v2\0${namespace}\0${authId}\0${authIdentityHash}\0${generation}`,
500
+ "utf8",
501
+ )
502
+ .digest("hex");
503
+ }
504
+
505
+ export function writeSessionSecret(
506
+ namespace: string,
507
+ authId: string,
508
+ authHash: string,
509
+ value: unknown,
510
+ environment: Environment = process.env,
511
+ ): void {
512
+ validateCoordinate(namespace, authId, authHash);
513
+ withReadProjectionAuthAdmission(authId, environment, () => {
514
+ const authIdentityHash = projectionAuthIdentityHash(
515
+ authId,
516
+ authHash,
517
+ environment,
518
+ );
519
+ const coordinate = replaceCoordinateState(
520
+ readCoordinateState(namespace, authId, environment),
521
+ namespace,
522
+ authId,
523
+ authIdentityHash,
524
+ environment,
525
+ );
526
+ const encrypted = encryptedSessionSecret(
527
+ namespace,
528
+ authId,
529
+ authIdentityHash,
530
+ coordinate.value.generation,
531
+ value,
532
+ environment,
533
+ );
534
+ writePrivateJson(secretPath(namespace, authId, environment), encrypted);
535
+ });
536
+ }
537
+
538
+ /**
539
+ * Atomically publish rotating session state only when the encrypted file still
540
+ * matches the snapshot that produced the new value.
541
+ *
542
+ * Default store snapshots return a durable generation-bound absent revision,
543
+ * so a writer that started before auth rotation or a create/delete cycle
544
+ * cannot create state afterward. Null is never accepted by the durable store.
545
+ */
546
+ export function writeSessionSecretIfUnchanged(
547
+ namespace: string,
548
+ authId: string,
549
+ authHash: string,
550
+ value: unknown,
551
+ expectedContentSha256: string | null,
552
+ environment: Environment = process.env,
553
+ ): SessionSecretWriteResult {
554
+ validateCoordinate(namespace, authId, authHash);
555
+ if (
556
+ expectedContentSha256 !== null
557
+ && !/^[a-f0-9]{64}$/u.test(expectedContentSha256)
558
+ ) throw new Error("expected session-secret content hash is malformed");
559
+ return withReadProjectionAuthAdmission(authId, environment, () => {
560
+ const authIdentityHash = projectionAuthIdentityHash(
561
+ authId,
562
+ authHash,
563
+ environment,
564
+ );
565
+ if (expectedContentSha256 === null) {
566
+ return Object.freeze({ written: false });
567
+ }
568
+ const coordinate = ensureCoordinateState(
569
+ namespace,
570
+ authId,
571
+ authIdentityHash,
572
+ environment,
573
+ );
574
+ let publicationCoordinate = coordinate;
575
+ const path = secretPath(namespace, authId, environment);
576
+ const currentText = readPrivateStateFileIfPresent(
577
+ path,
578
+ MAX_ENCRYPTED_BYTES,
579
+ "encrypted session secret",
580
+ environment,
581
+ );
582
+ if (currentText === null) {
583
+ if (coordinate.value.authIdentityHash !== authIdentityHash) {
584
+ publicationCoordinate = replaceCoordinateState(
585
+ coordinate,
586
+ namespace,
587
+ authId,
588
+ authIdentityHash,
589
+ environment,
590
+ );
591
+ }
592
+ if (
593
+ expectedContentSha256 !== absentContentSha256(
594
+ namespace,
595
+ authId,
596
+ authIdentityHash,
597
+ publicationCoordinate.value.generation,
598
+ )
599
+ ) return Object.freeze({ written: false });
600
+ publicationCoordinate = replaceCoordinateState(
601
+ publicationCoordinate,
602
+ namespace,
603
+ authId,
604
+ authIdentityHash,
605
+ environment,
606
+ );
607
+ } else {
608
+ const currentContentSha256 = createHash("sha256")
609
+ .update(currentText, "utf8")
610
+ .digest("hex");
611
+ if (currentContentSha256 !== expectedContentSha256) {
612
+ return Object.freeze({ written: false });
613
+ }
614
+ const current = parseEncryptedSessionSecret(currentText);
615
+ if (
616
+ current.namespace !== namespace
617
+ || current.authId !== authId
618
+ ) throw new Error("encrypted session secret is malformed");
619
+ parsedPlaintext(decryptSessionSecret(current, sessionKey(environment)));
620
+ if (
621
+ encryptedCoordinateGeneration(current)
622
+ !== coordinate.value.generation
623
+ ) {
624
+ reclaimStaleEncryptedCoordinate(
625
+ coordinate,
626
+ namespace,
627
+ authId,
628
+ currentContentSha256,
629
+ environment,
630
+ );
631
+ return Object.freeze({ written: false });
632
+ }
633
+ if (coordinate.value.authIdentityHash !== authIdentityHash) {
634
+ return Object.freeze({ written: false });
635
+ }
636
+ if (
637
+ encryptedAuthIdentityHash(current) !== authIdentityHash
638
+ ) {
639
+ reclaimStaleEncryptedCoordinate(
640
+ coordinate,
641
+ namespace,
642
+ authId,
643
+ currentContentSha256,
644
+ environment,
645
+ );
646
+ return Object.freeze({ written: false });
647
+ }
648
+ }
649
+ const encrypted = encryptedSessionSecret(
650
+ namespace,
651
+ authId,
652
+ authIdentityHash,
653
+ publicationCoordinate.value.generation,
654
+ value,
655
+ environment,
656
+ );
657
+ let written: boolean;
658
+ if (currentText === null) {
659
+ written = createPrivateJsonIfAbsent(
660
+ path,
661
+ encrypted,
662
+ { environment },
663
+ ).created;
664
+ } else {
665
+ written = writePrivateJsonIfUnchanged(path, encrypted, {
666
+ expectedCurrentContentSha256: expectedContentSha256,
667
+ });
668
+ }
669
+ return written
670
+ ? Object.freeze({
671
+ written: true,
672
+ contentSha256: encryptedContentSha256(encrypted),
673
+ })
674
+ : Object.freeze({ written: false });
675
+ });
676
+ }
677
+
678
+ function parseEncryptedSessionSecret(
679
+ text: string,
680
+ ): EncryptedSessionSecret {
681
+ let parsed: unknown;
682
+ try {
683
+ parsed = JSON.parse(text);
684
+ } catch {
685
+ throw new Error("encrypted session secret is malformed");
686
+ }
687
+ const encrypted = record(parsed, "encrypted session secret");
688
+ if (encrypted.schemaVersion === 1) {
689
+ exactKeys(
690
+ encrypted,
691
+ [
692
+ "schemaVersion",
693
+ "encryption",
694
+ "namespace",
695
+ "authId",
696
+ "authHash",
697
+ "iv",
698
+ "ciphertext",
699
+ "tag",
700
+ ],
701
+ "encrypted session secret",
702
+ );
703
+ } else if (encrypted.schemaVersion === 2) {
704
+ exactKeys(
705
+ encrypted,
706
+ [
707
+ "schemaVersion",
708
+ "encryption",
709
+ "keyId",
710
+ "namespace",
711
+ "authId",
712
+ "authHash",
713
+ "iv",
714
+ "ciphertext",
715
+ "tag",
716
+ ],
717
+ "encrypted session secret",
718
+ );
719
+ } else if (encrypted.schemaVersion === 3) {
720
+ exactKeys(
721
+ encrypted,
722
+ [
723
+ "schemaVersion",
724
+ "encryption",
725
+ "keyId",
726
+ "namespace",
727
+ "authId",
728
+ "authIdentityHash",
729
+ "generation",
730
+ "iv",
731
+ "ciphertext",
732
+ "tag",
733
+ ],
734
+ "encrypted session secret",
735
+ );
736
+ } else {
737
+ throw new Error("encrypted session secret is malformed");
738
+ }
739
+ if (
740
+ encrypted.encryption !== "aes-256-gcm"
741
+ || typeof encrypted.namespace !== "string"
742
+ || !/^[a-z][a-z0-9-]{0,47}$/u.test(encrypted.namespace)
743
+ || typeof encrypted.authId !== "string"
744
+ || !/^[a-z][a-z0-9-]{0,47}$/u.test(encrypted.authId)
745
+ || typeof encrypted.iv !== "string"
746
+ || typeof encrypted.ciphertext !== "string"
747
+ || typeof encrypted.tag !== "string"
748
+ || (
749
+ (encrypted.schemaVersion === 2 || encrypted.schemaVersion === 3)
750
+ && (
751
+ typeof encrypted.keyId !== "string"
752
+ || !/^[a-f0-9]{64}$/u.test(encrypted.keyId)
753
+ )
754
+ )
755
+ ) throw new Error("encrypted session secret is malformed");
756
+ if (
757
+ encrypted.schemaVersion === 3
758
+ && (
759
+ typeof encrypted.authIdentityHash !== "string"
760
+ || !/^[a-f0-9]{64}$/u.test(encrypted.authIdentityHash)
761
+ || typeof encrypted.generation !== "string"
762
+ || !/^[a-f0-9]{64}$/u.test(encrypted.generation)
763
+ )
764
+ ) throw new Error("encrypted session secret is malformed");
765
+ if (
766
+ (encrypted.schemaVersion === 1 || encrypted.schemaVersion === 2)
767
+ && (
768
+ typeof encrypted.authHash !== "string"
769
+ || !/^[a-f0-9]{64}$/u.test(encrypted.authHash)
770
+ )
771
+ ) throw new Error("encrypted session secret is malformed");
772
+ if (encrypted.schemaVersion === 1) {
773
+ const authHash = encrypted.authHash;
774
+ if (typeof authHash !== "string") {
775
+ throw new Error("encrypted session secret is malformed");
776
+ }
777
+ return {
778
+ schemaVersion: 1,
779
+ encryption: "aes-256-gcm",
780
+ namespace: encrypted.namespace,
781
+ authId: encrypted.authId,
782
+ authHash,
783
+ iv: encrypted.iv,
784
+ ciphertext: encrypted.ciphertext,
785
+ tag: encrypted.tag,
786
+ };
787
+ }
788
+ const keyId = encrypted.keyId;
789
+ if (typeof keyId !== "string") {
790
+ throw new Error("encrypted session secret is malformed");
791
+ }
792
+ if (encrypted.schemaVersion === 2) {
793
+ const authHash = encrypted.authHash;
794
+ if (typeof authHash !== "string") {
795
+ throw new Error("encrypted session secret is malformed");
796
+ }
797
+ return {
798
+ schemaVersion: 2,
799
+ encryption: "aes-256-gcm",
800
+ keyId,
801
+ namespace: encrypted.namespace,
802
+ authId: encrypted.authId,
803
+ authHash,
804
+ iv: encrypted.iv,
805
+ ciphertext: encrypted.ciphertext,
806
+ tag: encrypted.tag,
807
+ };
808
+ }
809
+ const authIdentityHash = encrypted.authIdentityHash;
810
+ const generation = encrypted.generation;
811
+ if (
812
+ typeof authIdentityHash !== "string"
813
+ || typeof generation !== "string"
814
+ ) {
815
+ throw new Error("encrypted session secret is malformed");
816
+ }
817
+ return {
818
+ schemaVersion: 3,
819
+ encryption: "aes-256-gcm",
820
+ keyId,
821
+ namespace: encrypted.namespace,
822
+ authId: encrypted.authId,
823
+ authIdentityHash,
824
+ generation,
825
+ iv: encrypted.iv,
826
+ ciphertext: encrypted.ciphertext,
827
+ tag: encrypted.tag,
828
+ };
829
+ }
830
+
831
+ function encryptedAuthIdentityHash(encrypted: EncryptedSessionSecret): string {
832
+ return encrypted.schemaVersion === 3
833
+ ? encrypted.authIdentityHash
834
+ : encrypted.authHash;
835
+ }
836
+
837
+ function encryptedCoordinateGeneration(
838
+ encrypted: EncryptedSessionSecret,
839
+ ): string | null {
840
+ return encrypted.schemaVersion === 3 ? encrypted.generation : null;
841
+ }
842
+
843
+ function reclaimStaleEncryptedCoordinate(
844
+ coordinate: SessionSecretCoordinateSnapshot,
845
+ namespace: string,
846
+ authId: string,
847
+ expectedContentSha256: string,
848
+ environment: Environment,
849
+ ): SessionSecretCoordinateSnapshot {
850
+ const advanced = replaceCoordinateState(
851
+ coordinate,
852
+ namespace,
853
+ authId,
854
+ coordinate.value.authIdentityHash,
855
+ environment,
856
+ );
857
+ if (!removePrivateStateFileIfUnchanged(
858
+ secretPath(namespace, authId, environment),
859
+ { expectedCurrentContentSha256: expectedContentSha256 },
860
+ environment,
861
+ )) {
862
+ throw new Error(
863
+ "stale encrypted session secret changed during generation recovery",
864
+ );
865
+ }
866
+ return advanced;
867
+ }
868
+
869
+ function decryptSessionSecret(
870
+ encrypted: EncryptedSessionSecret,
871
+ key: SessionKey,
872
+ ): Buffer {
873
+ const iv = boundedBase64(encrypted.iv, "session-secret IV", 12);
874
+ const ciphertext = boundedBase64(
875
+ encrypted.ciphertext,
876
+ "session-secret ciphertext",
877
+ MAX_PLAINTEXT_BYTES + 16,
878
+ );
879
+ const tag = boundedBase64(
880
+ encrypted.tag,
881
+ "session-secret authentication tag",
882
+ 16,
883
+ );
884
+ if (iv.byteLength !== 12 || tag.byteLength !== 16) {
885
+ throw new Error("encrypted session secret is malformed");
886
+ }
887
+ if (encrypted.schemaVersion !== 1 && encrypted.keyId !== key.id) {
888
+ throw new Error(
889
+ "encrypted session state belongs to a different encryption key",
890
+ );
891
+ }
892
+ try {
893
+ const decipher = createDecipheriv("aes-256-gcm", key.value, iv);
894
+ decipher.setAAD(additionalData(
895
+ encrypted.schemaVersion,
896
+ encrypted.schemaVersion === 1 ? null : encrypted.keyId,
897
+ encrypted.namespace,
898
+ encrypted.authId,
899
+ encryptedAuthIdentityHash(encrypted),
900
+ encrypted.schemaVersion === 3 ? encrypted.generation : null,
901
+ ));
902
+ decipher.setAuthTag(tag);
903
+ const plaintext = Buffer.concat([
904
+ decipher.update(ciphertext),
905
+ decipher.final(),
906
+ ]);
907
+ if (plaintext.byteLength > MAX_PLAINTEXT_BYTES) {
908
+ throw new Error("decrypted session secret exceeded its byte bound");
909
+ }
910
+ return plaintext;
911
+ } catch (error) {
912
+ if (
913
+ error instanceof Error
914
+ && error.message === "decrypted session secret exceeded its byte bound"
915
+ ) throw error;
916
+ throw new Error("encrypted session secret failed authentication");
917
+ }
918
+ }
919
+
920
+ function parsedPlaintext(plaintext: Buffer): unknown {
921
+ try {
922
+ const value: unknown = JSON.parse(
923
+ new TextDecoder("utf-8", { fatal: true }).decode(plaintext),
924
+ );
925
+ return value;
926
+ } catch {
927
+ throw new Error("decrypted session secret is malformed");
928
+ }
929
+ }
930
+
931
+ function assertSessionKeyOwnsEncryptedStore(
932
+ key: SessionKey,
933
+ environment: Environment,
934
+ ): void {
935
+ const sessionDirectory = directory(environment);
936
+ const identity = ensurePrivateStateDirectory(sessionDirectory, environment);
937
+ for (const entry of listPrivateStateDirectory(
938
+ sessionDirectory,
939
+ environment,
940
+ identity,
941
+ )) {
942
+ if (
943
+ entry.name === SESSION_SECRET_COORDINATE_DIRECTORY
944
+ && entry.kind === "directory"
945
+ ) continue;
946
+ if (entry.kind !== "file") {
947
+ throw new Error("encrypted session store is malformed");
948
+ }
949
+ const text = readPrivateStateFileIfPresent(
950
+ join(sessionDirectory, entry.name),
951
+ MAX_ENCRYPTED_BYTES,
952
+ "encrypted session secret",
953
+ environment,
954
+ [identity],
955
+ );
956
+ if (text === null) {
957
+ throw new Error("encrypted session store changed while validating its key");
958
+ }
959
+ const encrypted = parseEncryptedSessionSecret(text);
960
+ if (entry.name !== `${encrypted.namespace}--${encrypted.authId}.json`) {
961
+ throw new Error("encrypted session store is malformed");
962
+ }
963
+ parsedPlaintext(decryptSessionSecret(encrypted, key));
964
+ }
965
+ }
966
+
967
+ export function readSessionSecretSnapshot(
968
+ namespace: string,
969
+ authId: string,
970
+ authHash: string,
971
+ environment: Environment = process.env,
972
+ ): SessionSecretSnapshot {
973
+ validateCoordinate(namespace, authId, authHash);
974
+ return withReadProjectionAuthAdmission(authId, environment, () => {
975
+ const authIdentityHash = projectionAuthIdentityHash(
976
+ authId,
977
+ authHash,
978
+ environment,
979
+ );
980
+ let coordinate = ensureCoordinateState(
981
+ namespace,
982
+ authId,
983
+ authIdentityHash,
984
+ environment,
985
+ );
986
+ ensurePrivateStateDirectory(directory(environment), environment);
987
+ const text = readPrivateStateFileIfPresent(
988
+ secretPath(namespace, authId, environment),
989
+ MAX_ENCRYPTED_BYTES,
990
+ "encrypted session secret",
991
+ environment,
992
+ );
993
+ if (text === null) {
994
+ if (coordinate.value.authIdentityHash !== authIdentityHash) {
995
+ coordinate = replaceCoordinateState(
996
+ coordinate,
997
+ namespace,
998
+ authId,
999
+ authIdentityHash,
1000
+ environment,
1001
+ );
1002
+ }
1003
+ return Object.freeze({
1004
+ value: null,
1005
+ contentSha256: absentContentSha256(
1006
+ namespace,
1007
+ authId,
1008
+ authIdentityHash,
1009
+ coordinate.value.generation,
1010
+ ),
1011
+ });
1012
+ }
1013
+ const contentSha256 = createHash("sha256")
1014
+ .update(text, "utf8")
1015
+ .digest("hex");
1016
+ const encrypted = parseEncryptedSessionSecret(text);
1017
+ if (encrypted.namespace !== namespace || encrypted.authId !== authId) {
1018
+ throw new Error("encrypted session secret is malformed");
1019
+ }
1020
+ const value = parsedPlaintext(
1021
+ decryptSessionSecret(encrypted, sessionKey(environment)),
1022
+ );
1023
+ if (
1024
+ encryptedCoordinateGeneration(encrypted)
1025
+ !== coordinate.value.generation
1026
+ || (
1027
+ coordinate.value.authIdentityHash === authIdentityHash
1028
+ && encryptedAuthIdentityHash(encrypted) !== authIdentityHash
1029
+ )
1030
+ ) {
1031
+ const recovered = reclaimStaleEncryptedCoordinate(
1032
+ coordinate,
1033
+ namespace,
1034
+ authId,
1035
+ contentSha256,
1036
+ environment,
1037
+ );
1038
+ return Object.freeze({
1039
+ value: null,
1040
+ contentSha256: absentContentSha256(
1041
+ namespace,
1042
+ authId,
1043
+ recovered.value.authIdentityHash,
1044
+ recovered.value.generation,
1045
+ ),
1046
+ });
1047
+ }
1048
+ if (coordinate.value.authIdentityHash !== authIdentityHash) {
1049
+ return Object.freeze({ value: null, contentSha256 });
1050
+ }
1051
+ return Object.freeze({ value, contentSha256 });
1052
+ });
1053
+ }
1054
+
1055
+ export function readSessionSecret(
1056
+ namespace: string,
1057
+ authId: string,
1058
+ authHash: string,
1059
+ environment: Environment = process.env,
1060
+ ): unknown {
1061
+ return readSessionSecretSnapshot(
1062
+ namespace,
1063
+ authId,
1064
+ authHash,
1065
+ environment,
1066
+ ).value;
1067
+ }
1068
+
1069
+ export function removeSessionSecret(
1070
+ namespace: string,
1071
+ authId: string,
1072
+ environment: Environment = process.env,
1073
+ ): boolean {
1074
+ validateCoordinate(namespace, authId, "0".repeat(64));
1075
+ return withReadProjectionAuthAdmission(authId, environment, () => {
1076
+ const coordinate = readCoordinateState(namespace, authId, environment);
1077
+ if (coordinate !== null) {
1078
+ replaceCoordinateState(
1079
+ coordinate,
1080
+ namespace,
1081
+ authId,
1082
+ coordinate.value.authIdentityHash,
1083
+ environment,
1084
+ );
1085
+ }
1086
+ return removePrivateStateFile(
1087
+ secretPath(namespace, authId, environment),
1088
+ environment,
1089
+ );
1090
+ });
1091
+ }
1092
+
1093
+ function coordinateStatesForAuth(
1094
+ authId: string,
1095
+ environment: Environment,
1096
+ ): readonly SessionSecretCoordinateSnapshot[] {
1097
+ const sessionIdentity = ensurePrivateStateDirectory(
1098
+ directory(environment),
1099
+ environment,
1100
+ );
1101
+ const controlDirectory = coordinateDirectory(environment);
1102
+ const controlIdentity = ensurePrivateStateDirectory(
1103
+ controlDirectory,
1104
+ environment,
1105
+ );
1106
+ const suffix = `--${authId}.json`;
1107
+ const snapshots: SessionSecretCoordinateSnapshot[] = [];
1108
+ for (const entry of listPrivateStateDirectory(
1109
+ controlDirectory,
1110
+ environment,
1111
+ controlIdentity,
1112
+ )) {
1113
+ if (!entry.name.endsWith(suffix)) continue;
1114
+ const namespace = entry.name.slice(0, -suffix.length);
1115
+ if (
1116
+ entry.kind !== "file"
1117
+ || !/^[a-z][a-z0-9-]{0,47}$/u.test(namespace)
1118
+ ) throw new Error("session-secret coordinate store is malformed");
1119
+ const text = readPrivateStateFileIfPresent(
1120
+ join(controlDirectory, entry.name),
1121
+ MAX_COORDINATE_BYTES,
1122
+ "session-secret coordinate state",
1123
+ environment,
1124
+ [sessionIdentity, controlIdentity],
1125
+ );
1126
+ if (text === null) {
1127
+ throw new Error(
1128
+ "session-secret coordinate store changed during auth cleanup",
1129
+ );
1130
+ }
1131
+ snapshots.push(parseCoordinateState(text, namespace, authId));
1132
+ }
1133
+ return Object.freeze(snapshots);
1134
+ }
1135
+
1136
+ /**
1137
+ * Remove every plugin-owned rotating secret for one auth realm.
1138
+ *
1139
+ * Session-secret filenames end in the validated auth ID, so cleanup can stay
1140
+ * provider-neutral without parsing or decrypting secret material. Directory
1141
+ * identity is held across the bounded listing and each removal.
1142
+ */
1143
+ export function removeSessionSecretsForAuth(
1144
+ authId: string,
1145
+ environment: Environment = process.env,
1146
+ ): number {
1147
+ validateCoordinate("plugin", authId, "0".repeat(64));
1148
+ return withReadProjectionAuthAdmission(authId, environment, () => {
1149
+ for (const coordinate of coordinateStatesForAuth(authId, environment)) {
1150
+ replaceCoordinateState(
1151
+ coordinate,
1152
+ coordinate.value.namespace,
1153
+ authId,
1154
+ coordinate.value.authIdentityHash,
1155
+ environment,
1156
+ );
1157
+ }
1158
+ const sessionDirectory = directory(environment);
1159
+ const directoryIdentity = ensurePrivateStateDirectory(
1160
+ sessionDirectory,
1161
+ environment,
1162
+ );
1163
+ const suffix = `--${authId}.json`;
1164
+ let removed = 0;
1165
+ for (const entry of listPrivateStateDirectory(
1166
+ sessionDirectory,
1167
+ environment,
1168
+ directoryIdentity,
1169
+ )) {
1170
+ if (entry.kind !== "file" || !entry.name.endsWith(suffix)) continue;
1171
+ const namespace = entry.name.slice(0, -suffix.length);
1172
+ if (!/^[a-z][a-z0-9-]{0,47}$/u.test(namespace)) continue;
1173
+ if (removePrivateStateFile(
1174
+ join(sessionDirectory, entry.name),
1175
+ environment,
1176
+ directoryIdentity,
1177
+ )) {
1178
+ removed += 1;
1179
+ }
1180
+ }
1181
+ return removed;
1182
+ });
1183
+ }