@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,996 @@
1
+ import { WebSessionAuthStateError, WebSessionResponseRejectedError, WebSessionReadTransportError } from "./web-session-read-errors";
2
+ import {
3
+ acquireCookieRecords,
4
+ type CookieRecordReader,
5
+ type CookieSelection,
6
+ } from "@hraness/kb/clip/acquire";
7
+ import { BoundedByteBuffer } from "@hraness/kb/clip/bounded-byte-buffer";
8
+ import {
9
+ filterCookies,
10
+ renderCookieHeader,
11
+ type StrictCookie,
12
+ } from "@hraness/kb/clip/cookies";
13
+
14
+ import type { GhostgetAuth } from "./auth";
15
+ import { OperationDeadline, OperationDeadlineError } from "./operation-deadline";
16
+ import { pinnedHttpsFetch } from "./pinned-https";
17
+ import type { WebSessionOperationDeadline } from "./web-session-execution";
18
+
19
+ const WEB_SESSION_OPERATION_LABEL = "authenticated web operation deadline";
20
+ const MIN_PINNED_HTTPS_TIMEOUT_MS = 1_000;
21
+
22
+ export type WebSessionFetch = (
23
+ input: string | URL | Request,
24
+ init?: RequestInit,
25
+ ) => Promise<Response>;
26
+
27
+ export type WebSessionNetworkDependencies = {
28
+ readonly acquireCookies: CookieRecordReader;
29
+ readonly fetch: WebSessionFetch;
30
+ };
31
+
32
+ export type WebSessionCookieRotationEntry = {
33
+ /** Time at which ghostget accepted this exact provider response value. */
34
+ readonly acceptedAtSeconds: number;
35
+ readonly cookie: StrictCookie;
36
+ };
37
+
38
+ export type WebSessionCookieRotationTombstone = {
39
+ /** Time at which ghostget accepted the provider's exact deletion response. */
40
+ readonly acceptedAtSeconds: number;
41
+ readonly domain: string;
42
+ readonly hostOnly: boolean;
43
+ readonly name: string;
44
+ readonly path: string;
45
+ };
46
+
47
+ export type WebSessionCookieRotationState = {
48
+ readonly cookies: readonly WebSessionCookieRotationEntry[];
49
+ readonly tombstones: readonly WebSessionCookieRotationTombstone[];
50
+ };
51
+
52
+ export type WebSessionCookieRotation = {
53
+ /**
54
+ * Exact response-cookie names that one reviewed provider contract may
55
+ * absorb. Every other Set-Cookie field is ignored.
56
+ */
57
+ readonly allowedNames: readonly string[];
58
+ /**
59
+ * Authenticated, provider-parsed response state from a prior invocation.
60
+ * Cache membership is provenance: a live cached value or tombstone is
61
+ * authoritative over the independently acquired browser snapshot.
62
+ */
63
+ readonly cachedState: WebSessionCookieRotationState;
64
+ /** Bound session-cookie persistence when the provider omitted an expiry. */
65
+ readonly maxCachedCookieAgeSeconds: number;
66
+ /** Bound deletion suppression so a tombstone cannot outlive review policy. */
67
+ readonly tombstoneTtlSeconds: number;
68
+ /** Persist only reviewed response state, never the source cookie jar. */
69
+ readonly save: (state: WebSessionCookieRotationState) => void | Promise<void>;
70
+ };
71
+
72
+ export type WebSessionClient = {
73
+ readonly origin: string;
74
+ readonly cookies: readonly StrictCookie[];
75
+ readonly requestText: (request: {
76
+ readonly url: URL;
77
+ readonly method?: "GET" | "POST";
78
+ readonly headers?: Readonly<Record<string, string>>;
79
+ readonly body?: string;
80
+ readonly expectedContentTypes: readonly string[];
81
+ readonly maxBytes: number;
82
+ }) => Promise<string>;
83
+ readonly requestJson: (request: {
84
+ readonly url: URL;
85
+ readonly method: "GET" | "POST";
86
+ readonly headers: Readonly<Record<string, string>>;
87
+ readonly body?: string | Uint8Array;
88
+ readonly expectedStatuses?: readonly number[];
89
+ readonly expectedContentTypes?: readonly string[];
90
+ readonly maxBytes: number;
91
+ }) => Promise<unknown>;
92
+ readonly requestJsonResponse: (request: {
93
+ readonly url: URL;
94
+ readonly method: "GET" | "POST";
95
+ readonly headers: Readonly<Record<string, string>>;
96
+ readonly body?: string | Uint8Array;
97
+ readonly expectedStatuses?: readonly number[];
98
+ readonly expectedContentTypes?: readonly string[];
99
+ readonly maxBytes: number;
100
+ }) => Promise<Readonly<{ status: number; value: unknown }>>;
101
+ readonly requestStatus: (request: {
102
+ readonly url: URL;
103
+ readonly method: "GET" | "POST";
104
+ readonly headers: Readonly<Record<string, string>>;
105
+ readonly body?: string | Uint8Array;
106
+ readonly expectedStatuses: readonly number[];
107
+ /** One reviewed first-party create identifier; never projected to CLI output directly. */
108
+ readonly reviewedResponseIdHeader?: "x-restli-id";
109
+ }) => Promise<{
110
+ readonly status: number;
111
+ readonly location: string | null;
112
+ readonly responseId?: string | null;
113
+ }>;
114
+ };
115
+
116
+ function requestInputUrl(input: string | URL | Request): URL {
117
+ if (input instanceof Request) {
118
+ if (input.body !== null || input.bodyUsed) {
119
+ throw new Error("authenticated pinned transport does not accept a Request body wrapper");
120
+ }
121
+ return new URL(input.url);
122
+ }
123
+ return new URL(input);
124
+ }
125
+
126
+ type BoundWebSessionNetworkDependencies = {
127
+ readonly acquireCookies: CookieRecordReader;
128
+ readonly fetch: (
129
+ input: string | URL | Request,
130
+ init: RequestInit,
131
+ timeoutMs: number,
132
+ ) => Promise<Response>;
133
+ };
134
+
135
+ function networkDependencies(
136
+ overrides: Partial<WebSessionNetworkDependencies> | undefined,
137
+ ): BoundWebSessionNetworkDependencies {
138
+ return {
139
+ acquireCookies: overrides?.acquireCookies ?? acquireCookieRecords,
140
+ fetch: overrides?.fetch === undefined
141
+ ? (input, init, timeoutMs) => {
142
+ if (timeoutMs < MIN_PINNED_HTTPS_TIMEOUT_MS) {
143
+ throw new Error(
144
+ "authenticated web operation has insufficient time for its next request",
145
+ );
146
+ }
147
+ return pinnedHttpsFetch(requestInputUrl(input), init, timeoutMs);
148
+ }
149
+ : (input, init) => overrides.fetch!(input, init),
150
+ };
151
+ }
152
+
153
+ type WebSessionDeadlineOptions = {
154
+ readonly timeoutMs: number;
155
+ readonly signal?: AbortSignal;
156
+ readonly operationDeadline?: WebSessionOperationDeadline;
157
+ };
158
+
159
+ async function withWebSessionDeadline<T>(
160
+ options: WebSessionDeadlineOptions,
161
+ work: (deadline: WebSessionOperationDeadline) => Promise<T>,
162
+ ): Promise<T> {
163
+ const ownedDeadline = options.operationDeadline === undefined
164
+ ? new OperationDeadline(options.timeoutMs, {
165
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
166
+ })
167
+ : null;
168
+ const deadline = options.operationDeadline ?? ownedDeadline;
169
+ if (deadline === null) {
170
+ throw new Error("authenticated web operation deadline is unavailable");
171
+ }
172
+ try {
173
+ return await deadline.run(
174
+ () => work(deadline),
175
+ WEB_SESSION_OPERATION_LABEL,
176
+ );
177
+ } finally {
178
+ ownedDeadline?.dispose();
179
+ }
180
+ }
181
+
182
+ function remainingRequestTimeMs(
183
+ deadline: WebSessionOperationDeadline,
184
+ ): number {
185
+ deadline.throwIfUnavailable(WEB_SESSION_OPERATION_LABEL);
186
+ const remaining = deadline.remainingTimeMs();
187
+ if (remaining < 1) {
188
+ throw new Error(
189
+ "authenticated web operation timed out before its next request",
190
+ );
191
+ }
192
+ return remaining;
193
+ }
194
+
195
+ function cookieSelection(auth: GhostgetAuth, timeoutMs: number): CookieSelection {
196
+ if (auth.kind === "cookie-source") {
197
+ return {
198
+ cookieSources: [auth.source],
199
+ cookiesFile: undefined,
200
+ cookieProfile: auth.profile,
201
+ timeoutMs,
202
+ requireExplicitCookieScope: true,
203
+ };
204
+ }
205
+ if (auth.kind === "cookies-file") {
206
+ return {
207
+ cookieSources: [],
208
+ cookiesFile: auth.path,
209
+ cookieProfile: undefined,
210
+ timeoutMs,
211
+ requireExplicitCookieScope: true,
212
+ };
213
+ }
214
+ if (auth.kind === "browser-profile" && auth.cookieSource !== undefined) {
215
+ return {
216
+ cookieSources: [auth.cookieSource],
217
+ cookiesFile: undefined,
218
+ cookieProfile: auth.cookieProfile,
219
+ timeoutMs,
220
+ requireExplicitCookieScope: true,
221
+ };
222
+ }
223
+ if (auth.kind === "browser-profile") {
224
+ throw new WebSessionAuthStateError("authenticated web API execution requires the browser auth locator to name a cookie source");
225
+ }
226
+ throw new WebSessionAuthStateError("authenticated web API execution requires browser-session or cookie auth");
227
+ }
228
+
229
+ function contentTypeEssence(response: Response): string | null {
230
+ const raw = response.headers.get("content-type");
231
+ if (raw === null) return null;
232
+ const essence = raw.split(";", 1)[0]?.trim().toLowerCase() ?? "";
233
+ return essence === "" ? null : essence;
234
+ }
235
+
236
+ function isJsonContentType(value: string | null): boolean {
237
+ if (value === null) return false;
238
+ const slash = value.indexOf("/");
239
+ if (slash < 0) return false;
240
+ const subtype = value.slice(slash + 1);
241
+ return subtype === "json" || subtype.endsWith("+json");
242
+ }
243
+
244
+ async function boundedBytes(
245
+ response: Response,
246
+ maximum: number,
247
+ deadline: WebSessionOperationDeadline,
248
+ ): Promise<Uint8Array> {
249
+ deadline.throwIfUnavailable(WEB_SESSION_OPERATION_LABEL);
250
+ if (!Number.isSafeInteger(maximum) || maximum < 1 || maximum > 16 * 1024 * 1024) {
251
+ throw new Error("authenticated web response byte limit is invalid");
252
+ }
253
+ const declared = response.headers.get("content-length");
254
+ if (declared !== null) {
255
+ const bytes = Number(declared);
256
+ if (!Number.isSafeInteger(bytes) || bytes < 0 || bytes > maximum) {
257
+ response.body?.cancel().catch(() => undefined);
258
+ throw new Error("authenticated web response exceeded its reviewed byte limit");
259
+ }
260
+ }
261
+ if (response.body === null) return new Uint8Array();
262
+ const reader = response.body.getReader();
263
+ const buffer = new BoundedByteBuffer(maximum);
264
+ try {
265
+ for (;;) {
266
+ const item = await (async () => {
267
+ try {
268
+ return await deadline.run(
269
+ () => reader.read(),
270
+ WEB_SESSION_OPERATION_LABEL,
271
+ );
272
+ } catch (error) {
273
+ if (error instanceof OperationDeadlineError) throw error;
274
+ throw new WebSessionReadTransportError(
275
+ "authenticated web response body stream failed before completion",
276
+ error,
277
+ );
278
+ }
279
+ })();
280
+ if (item.done) break;
281
+ const value: unknown = item.value;
282
+ if (!(value instanceof Uint8Array)) {
283
+ void reader.cancel().catch(() => undefined);
284
+ throw new Error("authenticated web response yielded a non-byte chunk");
285
+ }
286
+ if (!buffer.append(value)) {
287
+ void reader.cancel().catch(() => undefined);
288
+ throw new Error("authenticated web response exceeded its reviewed byte limit");
289
+ }
290
+ }
291
+ } catch (error) {
292
+ void reader.cancel().catch(() => undefined);
293
+ throw error;
294
+ } finally {
295
+ reader.releaseLock();
296
+ }
297
+ deadline.throwIfUnavailable(WEB_SESSION_OPERATION_LABEL);
298
+ return buffer.toUint8Array();
299
+ }
300
+
301
+ function parseJson(bytes: Uint8Array): unknown {
302
+ let text: string;
303
+ try {
304
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
305
+ } catch {
306
+ throw new Error("authenticated web API returned invalid UTF-8 JSON");
307
+ }
308
+ try {
309
+ return JSON.parse(text) as unknown;
310
+ } catch {
311
+ throw new Error("authenticated web API returned malformed JSON");
312
+ }
313
+ }
314
+
315
+ function exactCookie(cookies: readonly StrictCookie[], name: string): string {
316
+ const matches = cookies.filter((cookie) => cookie.name === name);
317
+ if (matches.length !== 1) throw new Error(`authenticated session must contain exactly one ${name} cookie`);
318
+ return matches[0]?.value ?? "";
319
+ }
320
+
321
+ function cookieIdentity(cookie: StrictCookie): string {
322
+ return `${cookie.domain}\0${cookie.hostOnly ? "host" : "domain"}\0${cookie.path}\0${cookie.name}`;
323
+ }
324
+
325
+ function responseSetCookieHeaders(headers: Headers): readonly string[] {
326
+ const extended = headers as Headers & { readonly getSetCookie?: () => string[] };
327
+ const values = typeof extended.getSetCookie === "function"
328
+ ? extended.getSetCookie()
329
+ : (() => {
330
+ const value = headers.get("set-cookie");
331
+ return value === null ? [] : [value];
332
+ })();
333
+ if (
334
+ values.length > 64
335
+ || values.some((value) => typeof value !== "string" || Buffer.byteLength(value, "utf8") > 64 * 1024)
336
+ ) throw new Error("authenticated web response Set-Cookie fields exceeded their reviewed bounds");
337
+ return values;
338
+ }
339
+
340
+ type ParsedResponseCookie =
341
+ | { readonly kind: "set"; readonly cookie: StrictCookie }
342
+ | {
343
+ readonly kind: "remove";
344
+ readonly name: string;
345
+ readonly domain: string;
346
+ readonly hostOnly: boolean;
347
+ readonly path: string;
348
+ };
349
+
350
+ function parseResponseCookie(
351
+ value: string,
352
+ target: URL,
353
+ allowedNames: ReadonlySet<string>,
354
+ nowSeconds: number,
355
+ ): ParsedResponseCookie | null {
356
+ const fields = value.split(";");
357
+ const pair = fields.shift()?.trim() ?? "";
358
+ const separator = pair.indexOf("=");
359
+ if (separator < 1) return null;
360
+ const name = pair.slice(0, separator).trim();
361
+ if (!allowedNames.has(name)) return null;
362
+ const candidate: Record<string, unknown> = {
363
+ name,
364
+ value: pair.slice(separator + 1).trim(),
365
+ domain: target.hostname,
366
+ hostOnly: true,
367
+ path: "/",
368
+ secure: false,
369
+ httpOnly: false,
370
+ sameSite: null,
371
+ };
372
+ let expiresAttribute: number | undefined;
373
+ let maxAgeAttribute: number | undefined;
374
+ const seen = new Set<string>();
375
+ for (const rawField of fields) {
376
+ const field = rawField.trim();
377
+ if (field === "") continue;
378
+ const attributeSeparator = field.indexOf("=");
379
+ const rawName = attributeSeparator < 0 ? field : field.slice(0, attributeSeparator);
380
+ const attribute = rawName.trim().toLowerCase();
381
+ const attributeValue = attributeSeparator < 0 ? null : field.slice(attributeSeparator + 1).trim();
382
+ if (seen.has(attribute)) {
383
+ throw new Error(`reviewed rotating cookie ${name} repeated an attribute`);
384
+ }
385
+ seen.add(attribute);
386
+ if (attribute === "domain") {
387
+ if (attributeValue === null || attributeValue === "") {
388
+ throw new Error(`reviewed rotating cookie ${name} has an invalid Domain`);
389
+ }
390
+ candidate.domain = attributeValue;
391
+ candidate.hostOnly = false;
392
+ } else if (attribute === "path") {
393
+ if (attributeValue === null || attributeValue === "") {
394
+ throw new Error(`reviewed rotating cookie ${name} has an invalid Path`);
395
+ }
396
+ candidate.path = attributeValue;
397
+ } else if (attribute === "secure") {
398
+ if (attributeValue !== null) throw new Error(`reviewed rotating cookie ${name} has an invalid Secure flag`);
399
+ candidate.secure = true;
400
+ } else if (attribute === "httponly") {
401
+ if (attributeValue !== null) throw new Error(`reviewed rotating cookie ${name} has an invalid HttpOnly flag`);
402
+ candidate.httpOnly = true;
403
+ } else if (attribute === "samesite") {
404
+ if (attributeValue === null || !["strict", "lax", "none"].includes(attributeValue.toLowerCase())) {
405
+ throw new Error(`reviewed rotating cookie ${name} has an invalid SameSite`);
406
+ }
407
+ candidate.sameSite = `${attributeValue[0]?.toUpperCase() ?? ""}${attributeValue.slice(1).toLowerCase()}`;
408
+ } else if (attribute === "expires") {
409
+ if (attributeValue === null) throw new Error(`reviewed rotating cookie ${name} has an invalid Expires`);
410
+ const milliseconds = Date.parse(attributeValue);
411
+ if (!Number.isFinite(milliseconds)) throw new Error(`reviewed rotating cookie ${name} has an invalid Expires`);
412
+ expiresAttribute = Math.trunc(milliseconds / 1_000);
413
+ } else if (attribute === "max-age") {
414
+ if (attributeValue === null || !/^-?[0-9]{1,12}$/u.test(attributeValue)) {
415
+ throw new Error(`reviewed rotating cookie ${name} has an invalid Max-Age`);
416
+ }
417
+ const seconds = Number(attributeValue);
418
+ if (!Number.isSafeInteger(seconds)) throw new Error(`reviewed rotating cookie ${name} has an invalid Max-Age`);
419
+ maxAgeAttribute = seconds;
420
+ } else if (attribute === "priority") {
421
+ if (attributeValue === null || !["low", "medium", "high"].includes(attributeValue.toLowerCase())) {
422
+ throw new Error(`reviewed rotating cookie ${name} has an invalid Priority`);
423
+ }
424
+ } else {
425
+ throw new Error(`reviewed rotating cookie ${name} added an unsupported attribute`);
426
+ }
427
+ }
428
+ // RFC 6265 gives Max-Age precedence over Expires regardless of their wire
429
+ // order. Resolve them only after parsing every attribute so an expired
430
+ // Expires cannot turn a positive Max-Age into a deletion (or vice versa).
431
+ const effectiveExpires = maxAgeAttribute === undefined
432
+ ? expiresAttribute
433
+ : maxAgeAttribute <= 0
434
+ ? nowSeconds
435
+ : nowSeconds + maxAgeAttribute;
436
+ const remove = effectiveExpires !== undefined && effectiveExpires <= nowSeconds;
437
+ if (!remove && effectiveExpires !== undefined) candidate.expires = effectiveExpires;
438
+ const validated = filterCookies([candidate], target, nowSeconds);
439
+ if (validated.rejected !== 0 || validated.cookies.length !== 1) {
440
+ throw new Error(`reviewed rotating cookie ${name} escaped its exact origin scope`);
441
+ }
442
+ const cookie = validated.cookies[0];
443
+ if (cookie === undefined) throw new Error(`reviewed rotating cookie ${name} disappeared during validation`);
444
+ return remove
445
+ ? {
446
+ kind: "remove",
447
+ name: cookie.name,
448
+ domain: cookie.domain,
449
+ hostOnly: cookie.hostOnly,
450
+ path: cookie.path,
451
+ }
452
+ : { kind: "set", cookie };
453
+ }
454
+
455
+ export function webSessionCookie(cookies: readonly StrictCookie[], name: string): string {
456
+ try { return exactCookie(cookies, name); }
457
+ catch (cause) {
458
+ if (cause instanceof Error) throw new WebSessionAuthStateError(cause.message, { cause });
459
+ throw cause;
460
+ }
461
+ }
462
+
463
+ export function webSessionAuthSubject(auth: GhostgetAuth): string | null {
464
+ if (!("subject" in auth) || typeof auth.subject !== "string" || auth.subject.length === 0) return null;
465
+ return auth.subject;
466
+ }
467
+
468
+ export async function createWebSessionClient(
469
+ origin: string,
470
+ auth: GhostgetAuth,
471
+ options: {
472
+ readonly timeoutMs: number;
473
+ readonly signal?: AbortSignal;
474
+ readonly operationDeadline?: WebSessionOperationDeadline;
475
+ readonly dependencies?: Partial<WebSessionNetworkDependencies>;
476
+ readonly cookieRotation?: WebSessionCookieRotation;
477
+ },
478
+ ): Promise<WebSessionClient> {
479
+ const parsedOrigin = new URL(origin);
480
+ if (
481
+ parsedOrigin.protocol !== "https:"
482
+ || parsedOrigin.origin !== origin
483
+ || parsedOrigin.pathname !== "/"
484
+ || parsedOrigin.search !== ""
485
+ || parsedOrigin.hash !== ""
486
+ ) throw new Error("authenticated web client origin must be exact canonical HTTPS");
487
+ const dependencies = networkDependencies(options.dependencies);
488
+ const cookieResult = await withWebSessionDeadline(
489
+ options,
490
+ (deadline) => dependencies.acquireCookies(
491
+ cookieSelection(auth, deadline.remainingTimeMs()),
492
+ parsedOrigin,
493
+ ),
494
+ );
495
+ const validatedSource = filterCookies(cookieResult.cookies, parsedOrigin);
496
+ if (
497
+ validatedSource.rejected !== 0
498
+ || validatedSource.cookies.length !== cookieResult.cookies.length
499
+ ) throw new WebSessionAuthStateError("authenticated web cookie source returned malformed or out-of-scope records");
500
+ const rotation = options.cookieRotation;
501
+ const allowedNames = new Set(rotation?.allowedNames ?? []);
502
+ if (
503
+ rotation !== undefined
504
+ && (
505
+ allowedNames.size !== rotation.allowedNames.length
506
+ || allowedNames.size < 1
507
+ || allowedNames.size > 16
508
+ || [...allowedNames].some((name) => !/^[!#$%&'*+\-.^_`|~0-9A-Za-z]{1,128}$/u.test(name))
509
+ || !Number.isSafeInteger(rotation.maxCachedCookieAgeSeconds)
510
+ || rotation.maxCachedCookieAgeSeconds < 1
511
+ || rotation.maxCachedCookieAgeSeconds > 31 * 24 * 60 * 60
512
+ || !Number.isSafeInteger(rotation.tombstoneTtlSeconds)
513
+ || rotation.tombstoneTtlSeconds < 1
514
+ || rotation.tombstoneTtlSeconds > 31 * 24 * 60 * 60
515
+ )
516
+ ) throw new WebSessionAuthStateError("authenticated web rotating-cookie allowlist is invalid");
517
+ const nowSeconds = Math.floor(Date.now() / 1_000);
518
+ const cachedCookies = new Map<string, WebSessionCookieRotationEntry>();
519
+ const cachedTombstones = new Map<string, WebSessionCookieRotationTombstone>();
520
+ if (rotation !== undefined) {
521
+ if (
522
+ rotation.cachedState.cookies.length > 64
523
+ || rotation.cachedState.tombstones.length > 64
524
+ ) throw new Error("authenticated web rotating-cookie cache exceeded its reviewed bounds");
525
+ for (const entry of rotation.cachedState.cookies) {
526
+ if (
527
+ !Number.isSafeInteger(entry.acceptedAtSeconds)
528
+ || entry.acceptedAtSeconds < 0
529
+ || entry.acceptedAtSeconds > nowSeconds + 300
530
+ || !allowedNames.has(entry.cookie.name)
531
+ ) throw new WebSessionAuthStateError("authenticated web rotating-cookie cache is invalid");
532
+ const validated = filterCookies([entry.cookie], parsedOrigin, nowSeconds);
533
+ if (validated.rejected !== 0 || validated.cookies.length !== 1) {
534
+ throw new WebSessionAuthStateError("authenticated web rotating-cookie cache is invalid");
535
+ }
536
+ if (nowSeconds - entry.acceptedAtSeconds > rotation.maxCachedCookieAgeSeconds) continue;
537
+ const cookie = validated.cookies[0];
538
+ if (cookie === undefined) throw new WebSessionAuthStateError("authenticated web rotating-cookie cache is invalid");
539
+ const key = cookieIdentity(cookie);
540
+ if (cachedCookies.has(key)) throw new Error("authenticated web rotating-cookie cache contains a duplicate");
541
+ cachedCookies.set(key, Object.freeze({
542
+ acceptedAtSeconds: entry.acceptedAtSeconds,
543
+ cookie,
544
+ }));
545
+ }
546
+ for (const tombstone of rotation.cachedState.tombstones) {
547
+ if (
548
+ !Number.isSafeInteger(tombstone.acceptedAtSeconds)
549
+ || tombstone.acceptedAtSeconds < 0
550
+ || tombstone.acceptedAtSeconds > nowSeconds + 300
551
+ || !allowedNames.has(tombstone.name)
552
+ ) throw new WebSessionAuthStateError("authenticated web rotating-cookie cache is invalid");
553
+ const validated = filterCookies([{
554
+ name: tombstone.name,
555
+ value: "",
556
+ domain: tombstone.domain,
557
+ hostOnly: tombstone.hostOnly,
558
+ path: tombstone.path,
559
+ secure: parsedOrigin.protocol === "https:",
560
+ httpOnly: false,
561
+ sameSite: null,
562
+ expires: 0,
563
+ }], parsedOrigin, nowSeconds);
564
+ const identity = validated.cookies[0];
565
+ if (validated.rejected !== 0 || validated.cookies.length !== 1 || identity === undefined) {
566
+ throw new WebSessionAuthStateError("authenticated web rotating-cookie cache is invalid");
567
+ }
568
+ const key = cookieIdentity(identity);
569
+ if (
570
+ identity.name !== tombstone.name
571
+ || identity.domain !== tombstone.domain
572
+ || identity.hostOnly !== tombstone.hostOnly
573
+ || identity.path !== tombstone.path
574
+ || cachedTombstones.has(key)
575
+ || cachedCookies.has(key)
576
+ ) throw new WebSessionAuthStateError("authenticated web rotating-cookie cache is invalid");
577
+ if (nowSeconds - tombstone.acceptedAtSeconds > rotation.tombstoneTtlSeconds) continue;
578
+ cachedTombstones.set(key, Object.freeze({ ...tombstone }));
579
+ }
580
+ }
581
+ let rotatingCookies = cachedCookies;
582
+ let rotatingTombstones = cachedTombstones;
583
+ const combinedCookies = (): readonly StrictCookie[] => {
584
+ const combined = new Map<string, StrictCookie>();
585
+ for (const cookie of validatedSource.cookies) {
586
+ const key = cookieIdentity(cookie);
587
+ if (!rotatingTombstones.has(key) && !rotatingCookies.has(key)) combined.set(key, cookie);
588
+ }
589
+ for (const { cookie } of rotatingCookies.values()) {
590
+ combined.set(cookieIdentity(cookie), cookie);
591
+ }
592
+ return [...combined.values()];
593
+ };
594
+ const applyResponseCookies = async (response: Response, target: URL): Promise<void> => {
595
+ if (rotation === undefined) return;
596
+ const changes = responseSetCookieHeaders(response.headers)
597
+ .map((value) => parseResponseCookie(value, target, allowedNames, Math.floor(Date.now() / 1_000)))
598
+ .filter((value): value is ParsedResponseCookie => value !== null);
599
+ if (changes.length === 0) return;
600
+ const acceptedAtSeconds = Math.floor(Date.now() / 1_000);
601
+ const nextCookies = new Map(rotatingCookies);
602
+ const nextTombstones = new Map(rotatingTombstones);
603
+ for (const change of changes) {
604
+ const key = change.kind === "set"
605
+ ? cookieIdentity(change.cookie)
606
+ : `${change.domain}\0${change.hostOnly ? "host" : "domain"}\0${change.path}\0${change.name}`;
607
+ if (change.kind === "set") {
608
+ nextCookies.set(key, Object.freeze({
609
+ acceptedAtSeconds,
610
+ cookie: change.cookie,
611
+ }));
612
+ nextTombstones.delete(key);
613
+ } else {
614
+ nextCookies.delete(key);
615
+ nextTombstones.set(key, Object.freeze({
616
+ acceptedAtSeconds,
617
+ domain: change.domain,
618
+ hostOnly: change.hostOnly,
619
+ name: change.name,
620
+ path: change.path,
621
+ }));
622
+ }
623
+ }
624
+ rotatingCookies = nextCookies;
625
+ rotatingTombstones = nextTombstones;
626
+ await rotation.save(Object.freeze({
627
+ cookies: Object.freeze([...rotatingCookies.values()]),
628
+ tombstones: Object.freeze([...rotatingTombstones.values()]),
629
+ }));
630
+ };
631
+ const requestJsonResponse: WebSessionClient["requestJsonResponse"] = async (request) => {
632
+ if (request.url.origin !== origin || request.url.username !== "" || request.url.password !== "" || request.url.hash !== "") {
633
+ throw new Error("authenticated web API request escaped its reviewed origin");
634
+ }
635
+ if (request.body !== undefined && request.method !== "POST") {
636
+ throw new Error("authenticated web API request body requires POST");
637
+ }
638
+ const headers = new Headers(request.headers);
639
+ headers.set("cookie", renderCookieHeader(combinedCookies()));
640
+ return withWebSessionDeadline(options, async (deadline) => {
641
+ let response: Response | undefined;
642
+ try {
643
+ try {
644
+ response = await deadline.run(
645
+ () => dependencies.fetch(
646
+ request.url,
647
+ {
648
+ method: request.method,
649
+ headers,
650
+ ...(request.body === undefined ? {} : { body: request.body }),
651
+ redirect: "error",
652
+ signal: deadline.signal,
653
+ },
654
+ remainingRequestTimeMs(deadline),
655
+ ),
656
+ WEB_SESSION_OPERATION_LABEL,
657
+ );
658
+ } catch (error) {
659
+ throw new WebSessionReadTransportError("authenticated web API request failed before a reviewed response was received", error);
660
+ }
661
+ const expected = request.expectedStatuses ?? [200];
662
+ const contentType = contentTypeEssence(response);
663
+ const contentTypeAllowed = request.expectedContentTypes === undefined
664
+ ? isJsonContentType(contentType)
665
+ : contentType !== null && request.expectedContentTypes.includes(contentType);
666
+ if (!expected.includes(response.status) || !contentTypeAllowed) {
667
+ response.body?.cancel().catch(() => undefined);
668
+ throw new WebSessionResponseRejectedError(`authenticated web API returned unreviewed status/content type ${response.status}/${contentType ?? "missing"}`, response.status, contentType);
669
+ }
670
+ await applyResponseCookies(response, request.url);
671
+ const status = response.status;
672
+ const value = parseJson(await boundedBytes(
673
+ response,
674
+ request.maxBytes,
675
+ deadline,
676
+ ));
677
+ return Object.freeze({ status, value });
678
+ } finally {
679
+ if (deadline.signal.aborted) {
680
+ void response?.body?.cancel().catch(() => undefined);
681
+ }
682
+ }
683
+ });
684
+ };
685
+ return {
686
+ origin,
687
+ get cookies() {
688
+ return combinedCookies();
689
+ },
690
+ requestText: async (request) => {
691
+ if (request.url.origin !== origin || request.url.username !== "" || request.url.password !== "" || request.url.hash !== "") {
692
+ throw new Error("authenticated web request escaped its reviewed origin");
693
+ }
694
+ const method = request.method ?? "GET";
695
+ if (request.body !== undefined && method !== "POST") {
696
+ throw new Error("authenticated web text request body requires POST");
697
+ }
698
+ if (method === "POST" && request.body === undefined) {
699
+ throw new Error("authenticated web text POST requires a body");
700
+ }
701
+ const headers = new Headers(request.headers);
702
+ headers.set("cookie", renderCookieHeader(combinedCookies()));
703
+ return withWebSessionDeadline(options, async (deadline) => {
704
+ let response: Response | undefined;
705
+ try {
706
+ try {
707
+ response = await deadline.run(
708
+ () => dependencies.fetch(
709
+ request.url,
710
+ {
711
+ method,
712
+ headers,
713
+ ...(request.body === undefined ? {} : { body: request.body }),
714
+ redirect: "error",
715
+ signal: deadline.signal,
716
+ },
717
+ remainingRequestTimeMs(deadline),
718
+ ),
719
+ WEB_SESSION_OPERATION_LABEL,
720
+ );
721
+ } catch (error) {
722
+ throw new WebSessionReadTransportError("authenticated web request failed before a reviewed response was received", error);
723
+ }
724
+ const contentType = contentTypeEssence(response);
725
+ if (response.status !== 200 || contentType === null || !request.expectedContentTypes.includes(contentType)) {
726
+ response.body?.cancel().catch(() => undefined);
727
+ throw new WebSessionResponseRejectedError(`authenticated web request returned unreviewed status/content type ${response.status}/${contentType ?? "missing"}`, response.status, contentType);
728
+ }
729
+ await applyResponseCookies(response, request.url);
730
+ const bytes = await boundedBytes(
731
+ response,
732
+ request.maxBytes,
733
+ deadline,
734
+ );
735
+ try {
736
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
737
+ } catch {
738
+ throw new Error("authenticated web request returned invalid UTF-8");
739
+ }
740
+ } finally {
741
+ if (deadline.signal.aborted) {
742
+ void response?.body?.cancel().catch(() => undefined);
743
+ }
744
+ }
745
+ });
746
+ },
747
+ requestJson: async (request) => (await requestJsonResponse(request)).value,
748
+ requestJsonResponse,
749
+ requestStatus: async (request) => {
750
+ if (request.url.origin !== origin || request.url.username !== "" || request.url.password !== "" || request.url.hash !== "") {
751
+ throw new Error("authenticated web API request escaped its reviewed origin");
752
+ }
753
+ if (request.body !== undefined && request.method !== "POST") {
754
+ throw new Error("authenticated web API request body requires POST");
755
+ }
756
+ if (
757
+ request.expectedStatuses.length < 1
758
+ || request.expectedStatuses.length > 10
759
+ || request.expectedStatuses.some((status) => !Number.isSafeInteger(status) || status < 100 || status > 599)
760
+ ) throw new Error("authenticated web API expected status list is invalid");
761
+ const headers = new Headers(request.headers);
762
+ headers.set("cookie", renderCookieHeader(combinedCookies()));
763
+ return withWebSessionDeadline(options, async (deadline) => {
764
+ let response: Response | undefined;
765
+ try {
766
+ try {
767
+ response = await deadline.run(
768
+ () => dependencies.fetch(
769
+ request.url,
770
+ {
771
+ method: request.method,
772
+ headers,
773
+ ...(request.body === undefined ? {} : { body: request.body }),
774
+ redirect: "error",
775
+ signal: deadline.signal,
776
+ },
777
+ remainingRequestTimeMs(deadline),
778
+ ),
779
+ WEB_SESSION_OPERATION_LABEL,
780
+ );
781
+ } catch (error) {
782
+ throw new WebSessionReadTransportError("authenticated web API request failed before a reviewed response was received", error);
783
+ }
784
+ if (!request.expectedStatuses.includes(response.status)) {
785
+ response.body?.cancel().catch(() => undefined);
786
+ throw new WebSessionResponseRejectedError(`authenticated web API request returned unreviewed status ${response.status}`, response.status, null);
787
+ }
788
+ await applyResponseCookies(response, request.url);
789
+ let responseId: string | null | undefined;
790
+ if (request.reviewedResponseIdHeader !== undefined) {
791
+ const rawResponseId = response.headers.get(request.reviewedResponseIdHeader);
792
+ if (
793
+ rawResponseId !== null
794
+ && (rawResponseId.length < 1
795
+ || rawResponseId.length > 512
796
+ || /[\0\r\n]/u.test(rawResponseId))
797
+ ) {
798
+ response.body?.cancel().catch(() => undefined);
799
+ throw new Error("authenticated web API response returned an invalid reviewed identifier");
800
+ }
801
+ responseId = rawResponseId;
802
+ }
803
+ const rawLocation = response.headers.get("location");
804
+ let location: string | null = null;
805
+ if (rawLocation !== null) {
806
+ const parsed = new URL(rawLocation, origin);
807
+ if (
808
+ parsed.origin !== origin
809
+ || parsed.username !== ""
810
+ || parsed.password !== ""
811
+ || parsed.hash !== ""
812
+ ) {
813
+ response.body?.cancel().catch(() => undefined);
814
+ throw new Error("authenticated web API response attempted an unreviewed redirect");
815
+ }
816
+ location = `${parsed.pathname}${parsed.search}`;
817
+ }
818
+ void response.body?.cancel().catch(() => undefined);
819
+ deadline.throwIfUnavailable(WEB_SESSION_OPERATION_LABEL);
820
+ return {
821
+ status: response.status,
822
+ location,
823
+ ...(request.reviewedResponseIdHeader === undefined ? {} : { responseId: responseId ?? null }),
824
+ };
825
+ } finally {
826
+ if (deadline.signal.aborted) {
827
+ void response?.body?.cancel().catch(() => undefined);
828
+ }
829
+ }
830
+ });
831
+ },
832
+ };
833
+ }
834
+
835
+ export async function fetchPublicWebAsset(
836
+ url: URL,
837
+ options: {
838
+ readonly allowedOrigin: string;
839
+ readonly contentTypes: readonly string[];
840
+ readonly maxBytes: number;
841
+ readonly timeoutMs: number;
842
+ readonly signal?: AbortSignal;
843
+ readonly operationDeadline?: WebSessionOperationDeadline;
844
+ readonly dependencies?: Partial<WebSessionNetworkDependencies>;
845
+ },
846
+ ): Promise<string> {
847
+ if (url.origin !== options.allowedOrigin || url.username !== "" || url.password !== "" || url.hash !== "") {
848
+ throw new Error("public web asset escaped its reviewed origin");
849
+ }
850
+ const dependencies = networkDependencies(options.dependencies);
851
+ return withWebSessionDeadline(options, async (deadline) => {
852
+ let response: Response | undefined;
853
+ try {
854
+ try {
855
+ response = await deadline.run(
856
+ () => dependencies.fetch(
857
+ url,
858
+ {
859
+ method: "GET",
860
+ redirect: "error",
861
+ signal: deadline.signal,
862
+ },
863
+ remainingRequestTimeMs(deadline),
864
+ ),
865
+ WEB_SESSION_OPERATION_LABEL,
866
+ );
867
+ } catch (error) {
868
+ throw new Error("public first-party web asset request failed", { cause: error });
869
+ }
870
+ const contentType = contentTypeEssence(response);
871
+ if (response.status !== 200 || contentType === null || !options.contentTypes.includes(contentType)) {
872
+ response.body?.cancel().catch(() => undefined);
873
+ throw new Error(`public first-party web asset returned unreviewed status/content type ${response.status}/${contentType ?? "missing"}`);
874
+ }
875
+ const bytes = await boundedBytes(
876
+ response,
877
+ options.maxBytes,
878
+ deadline,
879
+ );
880
+ try {
881
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
882
+ } catch {
883
+ throw new Error("public first-party web asset returned invalid UTF-8");
884
+ }
885
+ } finally {
886
+ if (deadline.signal.aborted) {
887
+ void response?.body?.cancel().catch(() => undefined);
888
+ }
889
+ }
890
+ });
891
+ }
892
+
893
+ /**
894
+ * Transfer one bounded byte asset to an exact reviewed public origin without
895
+ * acquiring or attaching browser cookies. This is for provider-issued signed
896
+ * upload forms whose body itself carries the narrowly scoped authorization;
897
+ * it is not an authenticated provider API transport.
898
+ */
899
+ export async function uploadPublicWebAsset(
900
+ url: URL,
901
+ options: {
902
+ readonly allowedOrigin: string;
903
+ readonly body: Uint8Array;
904
+ readonly contentType: string;
905
+ readonly expectedStatus: number;
906
+ readonly maxBytes: number;
907
+ readonly timeoutMs: number;
908
+ readonly userAgent?: string;
909
+ readonly signal?: AbortSignal;
910
+ readonly operationDeadline?: WebSessionOperationDeadline;
911
+ readonly dependencies?: Partial<WebSessionNetworkDependencies>;
912
+ },
913
+ ): Promise<Readonly<{ status: number }>> {
914
+ const parsedOrigin = new URL(options.allowedOrigin);
915
+ if (
916
+ parsedOrigin.protocol !== "https:"
917
+ || parsedOrigin.origin !== options.allowedOrigin
918
+ || parsedOrigin.pathname !== "/"
919
+ || parsedOrigin.search !== ""
920
+ || parsedOrigin.hash !== ""
921
+ || url.origin !== options.allowedOrigin
922
+ || url.username !== ""
923
+ || url.password !== ""
924
+ || url.hash !== ""
925
+ ) throw new Error("public web asset upload escaped its reviewed origin");
926
+ if (
927
+ !(options.body instanceof Uint8Array)
928
+ || !Number.isSafeInteger(options.maxBytes)
929
+ || options.maxBytes < 1
930
+ || options.maxBytes > 513 * 1024 * 1024
931
+ || options.body.byteLength < 1
932
+ || options.body.byteLength > options.maxBytes
933
+ ) throw new Error("public web asset upload exceeded its reviewed byte limit");
934
+ if (
935
+ typeof options.contentType !== "string"
936
+ || options.contentType.length < 1
937
+ || options.contentType.length > 512
938
+ || /[\0\r\n]/u.test(options.contentType)
939
+ ) throw new Error("public web asset upload content type is invalid");
940
+ if (
941
+ !Number.isSafeInteger(options.expectedStatus)
942
+ || options.expectedStatus < 200
943
+ || options.expectedStatus > 299
944
+ ) throw new Error("public web asset upload expected status is invalid");
945
+ if (
946
+ options.userAgent !== undefined
947
+ && (
948
+ options.userAgent.length < 1
949
+ || options.userAgent.length > 512
950
+ || /[\0\r\n]/u.test(options.userAgent)
951
+ )
952
+ ) throw new Error("public web asset upload user agent is invalid");
953
+ const dependencies = networkDependencies(options.dependencies);
954
+ return withWebSessionDeadline(options, async (deadline) => {
955
+ let response: Response | undefined;
956
+ try {
957
+ const headers = new Headers({
958
+ accept: "application/xml",
959
+ "content-type": options.contentType,
960
+ ...(options.userAgent === undefined ? {} : { "user-agent": options.userAgent }),
961
+ });
962
+ try {
963
+ response = await deadline.run(
964
+ () => dependencies.fetch(
965
+ url,
966
+ {
967
+ method: "POST",
968
+ headers,
969
+ body: options.body,
970
+ redirect: "error",
971
+ signal: deadline.signal,
972
+ },
973
+ remainingRequestTimeMs(deadline),
974
+ ),
975
+ WEB_SESSION_OPERATION_LABEL,
976
+ );
977
+ } catch (error) {
978
+ throw new Error(
979
+ "public web asset upload failed before a reviewed response was received",
980
+ { cause: error },
981
+ );
982
+ }
983
+ if (response.status !== options.expectedStatus) {
984
+ response.body?.cancel().catch(() => undefined);
985
+ throw new Error(`public web asset upload returned unreviewed status ${response.status}`);
986
+ }
987
+ void response.body?.cancel().catch(() => undefined);
988
+ deadline.throwIfUnavailable(WEB_SESSION_OPERATION_LABEL);
989
+ return Object.freeze({ status: response.status });
990
+ } finally {
991
+ if (deadline.signal.aborted) {
992
+ void response?.body?.cancel().catch(() => undefined);
993
+ }
994
+ }
995
+ });
996
+ }