@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.
- package/CHANGELOG.md +458 -0
- package/LICENSE +21 -0
- package/README.md +1359 -0
- package/bunfig.toml +2 -0
- package/dist/apple-photos-client.js +704 -0
- package/dist/beeper-client.js +4602 -0
- package/dist/client.js +1605 -0
- package/dist/index-26yq8q16.js +31 -0
- package/dist/index-9t13b4k7.js +508 -0
- package/dist/index-axwm9b3v.js +5 -0
- package/dist/index-gwk7rbyj.js +30 -0
- package/dist/index.js +701 -0
- package/dist/messaging.js +1130 -0
- package/dist/omni-client.js +1406 -0
- package/dist/whatsapp-client.js +355 -0
- package/docs/imessage-direct-provider.md +106 -0
- package/docs/rental-listings.md +94 -0
- package/package.json +600 -0
- package/skills/ghostget/SKILL.md +251 -0
- package/skills/ghostget/agents/openai.yaml +4 -0
- package/skills/ghostget/references/adapter-contract.md +181 -0
- package/skills/ghostget/references/apple-photos.md +115 -0
- package/skills/ghostget/references/article-drafts.md +235 -0
- package/skills/ghostget/references/code-owned-provider-scaffold.md +134 -0
- package/skills/ghostget/references/cross-posting-video.md +139 -0
- package/skills/ghostget/references/cross-posting.md +82 -0
- package/skills/ghostget/references/derivation.md +390 -0
- package/skills/ghostget/references/hraness-social-profile-stats.json +269 -0
- package/skills/ghostget/references/install.md +66 -0
- package/skills/ghostget/references/linkedin-adapter.md +442 -0
- package/skills/ghostget/references/messaging.md +168 -0
- package/skills/ghostget/references/meta-comet-contract.md +153 -0
- package/skills/ghostget/references/platform-patterns.md +122 -0
- package/skills/ghostget/references/provider-plugins.md +496 -0
- package/skills/ghostget/references/reddit-flair.md +78 -0
- package/skills/ghostget/references/safety-and-state.md +214 -0
- package/skills/ghostget/references/settlement-and-duplicate-cleanup.md +79 -0
- package/skills/ghostget/references/social-platform-routing.md +66 -0
- package/skills/ghostget/references/social-profile-stats.md +195 -0
- package/skills/ghostget/references/social-video-platform-routing.md +89 -0
- package/skills/ghostget/references/whatsapp-adapter.md +101 -0
- package/skills/ghostget/references/x-adapter.md +332 -0
- package/skills/ghostget/references/x-ai-disclosure.md +71 -0
- package/src/apple-photos-cli.ts +639 -0
- package/src/apple-photos-client-types.ts +168 -0
- package/src/apple-photos-client.ts +201 -0
- package/src/apple-photos-contact-evidence.ts +838 -0
- package/src/apple-photos-local-source.ts +1131 -0
- package/src/args.ts +2053 -0
- package/src/article-draft-document.ts +417 -0
- package/src/article-draft-embeds.ts +123 -0
- package/src/article-draft-images.ts +214 -0
- package/src/assets/adapter-template/wrench-adapter.json +40 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.json +1500 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v1.0.0.json +129 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v1.1.0.json +207 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v2.0.0.json +1463 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v2.1.0.json +1463 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v2.2.0.json +1475 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v2.3.0.json +1488 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.json +684 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.0.0.json +581 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.1.0.json +581 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.2.0.json +581 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.3.0.json +615 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.4.0.json +684 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.5.0.json +684 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.6.0.json +684 -0
- package/src/assets/adapters/clasificados/wrench-web-adapter.json +54 -0
- package/src/assets/adapters/facebook/wrench-web-adapter.json +750 -0
- package/src/assets/adapters/facebook/wrench-web-adapter.v1.0.0.json +706 -0
- package/src/assets/adapters/facebook/wrench-web-adapter.v1.1.0.json +703 -0
- package/src/assets/adapters/facebook/wrench-web-adapter.v1.2.0.json +750 -0
- package/src/assets/adapters/facebook-group/wrench-web-adapter.json +682 -0
- package/src/assets/adapters/facebook-group/wrench-web-adapter.v1.0.0.json +583 -0
- package/src/assets/adapters/facebook-group/wrench-web-adapter.v1.1.0.json +682 -0
- package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.json +437 -0
- package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.v1.0.0.json +383 -0
- package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.v1.1.0.json +437 -0
- package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.v1.1.1.json +437 -0
- package/src/assets/adapters/facebook-page/wrench-web-adapter.json +880 -0
- package/src/assets/adapters/facebook-page/wrench-web-adapter.v1.0.0.json +740 -0
- package/src/assets/adapters/facebook-page/wrench-web-adapter.v1.1.0.json +880 -0
- package/src/assets/adapters/github/wrench-web-adapter.json +69 -0
- package/src/assets/adapters/github/wrench-web-adapter.v1.0.0.json +42 -0
- package/src/assets/adapters/github/wrench-web-adapter.v1.1.0.json +69 -0
- package/src/assets/adapters/gmail/wrench-adapter.json +161 -0
- package/src/assets/adapters/gmail/wrench-adapter.v1.2.0.json +157 -0
- package/src/assets/adapters/hacker-news/wrench-web-adapter.json +315 -0
- package/src/assets/adapters/hacker-news/wrench-web-adapter.v1.0.0.json +315 -0
- package/src/assets/adapters/imessage/wrench-web-adapter.json +236 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.json +719 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.0.0.json +627 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.1.0.json +609 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.2.0.json +640 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.3.0.json +671 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.4.0.json +710 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.5.0.json +710 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.6.0.json +719 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.7.0.json +719 -0
- package/src/assets/adapters/linkedin/wrench-adapter.json +442 -0
- package/src/assets/adapters/linkedin/wrench-adapter.v0.4.0.json +124 -0
- package/src/assets/adapters/linkedin/wrench-adapter.v1.0.0.json +411 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.json +914 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.0.0.json +604 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.1.0.json +604 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.10.0.json +809 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.11.0.json +809 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.12.0.json +820 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.13.0.json +819 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.14.0.json +819 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.15.0.json +819 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.16.0.json +874 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.17.0.json +874 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.18.0.json +874 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.19.0.json +874 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.2.0.json +731 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.3.0.json +768 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.4.0.json +808 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.5.0.json +808 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.6.0.json +808 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.7.0.json +825 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.8.0.json +792 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.9.0.json +809 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.json +855 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.0.0.json +577 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.1.0.json +604 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.10.0.json +712 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.11.0.json +712 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.12.0.json +712 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.2.0.json +653 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.3.0.json +712 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.4.0.json +713 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.5.0.json +712 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.6.0.json +713 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.7.0.json +713 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.8.0.json +713 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.9.0.json +713 -0
- package/src/assets/adapters/substack/wrench-web-adapter.json +936 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.0.0.json +817 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.1.0.json +812 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.2.0.json +812 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.3.0.json +902 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.4.0.json +936 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.5.0.json +936 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.6.0.json +936 -0
- package/src/assets/adapters/threads/wrench-web-adapter.json +692 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.0.0.json +629 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.1.0.json +623 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.2.0.json +622 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.3.0.json +622 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.4.0.json +622 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.5.0.json +699 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.6.0.json +693 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.7.0.json +692 -0
- package/src/assets/adapters/tiktok/wrench-web-adapter.json +762 -0
- package/src/assets/adapters/tiktok/wrench-web-adapter.v1.0.0.json +681 -0
- package/src/assets/adapters/tiktok/wrench-web-adapter.v1.1.0.json +708 -0
- package/src/assets/adapters/tiktok/wrench-web-adapter.v1.2.0.json +762 -0
- package/src/assets/adapters/tiktok/wrench-web-adapter.v1.3.0.json +762 -0
- package/src/assets/adapters/twitch/wrench-web-adapter.json +42 -0
- package/src/assets/adapters/twitch/wrench-web-adapter.v1.0.0.json +42 -0
- package/src/assets/adapters/whatsapp/wrench-web-adapter.json +400 -0
- package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.0.0.json +355 -0
- package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.1.0.json +386 -0
- package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.2.0.json +386 -0
- package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.3.0.json +400 -0
- package/src/assets/adapters/x/wrench-adapter.json +749 -0
- package/src/assets/adapters/x/wrench-adapter.v1.0.0.json +700 -0
- package/src/assets/adapters/x/wrench-adapter.v1.1.0.json +707 -0
- package/src/assets/adapters/x/wrench-web-adapter.json +712 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.1.0.json +579 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.10.0.json +685 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.11.0.json +712 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.12.0.json +712 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.13.0.json +712 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.2.0.json +578 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.3.0.json +610 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.4.0.json +617 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.5.0.json +634 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.6.0.json +632 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.7.0.json +649 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.8.0.json +649 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.9.0.json +683 -0
- package/src/assets/adapters/youtube/wrench-web-adapter.json +561 -0
- package/src/assets/adapters/youtube/wrench-web-adapter.v1.0.0.json +465 -0
- package/src/assets/adapters/youtube/wrench-web-adapter.v1.1.0.json +519 -0
- package/src/assets/adapters/youtube/wrench-web-adapter.v1.2.0.json +561 -0
- package/src/assets/adapters/youtube/wrench-web-adapter.v1.3.0.json +561 -0
- package/src/assets/code-owned-provider-template/plugin.test.ts.template +54 -0
- package/src/assets/code-owned-provider-template/plugin.ts.template +102 -0
- package/src/assets/code-owned-provider-template/runtime.internal.test.ts.template +44 -0
- package/src/assets/code-owned-provider-template/runtime.ts.template +59 -0
- package/src/assets/code-owned-provider-template/wrench-adapter.json.template +33 -0
- package/src/assets/fonts/nebula-sans/LICENSE.txt +96 -0
- package/src/assets/fonts/nebula-sans/NebulaSans-Book.woff2 +0 -0
- package/src/assets/fonts/nebula-sans/PROVENANCE.md +14 -0
- package/src/assets.d.ts +4 -0
- package/src/auth.ts +1157 -0
- package/src/beeper-client-types.ts +169 -0
- package/src/beeper-client.ts +250 -0
- package/src/beeper-contact-interactions-cli.ts +180 -0
- package/src/beeper-contact-interactions.ts +1612 -0
- package/src/beeper-message-bundle-v1.ts +257 -0
- package/src/beeper-message-like-me-cli.ts +84 -0
- package/src/beeper-message-like-me-export.ts +2274 -0
- package/src/beeper-message-like-me-recovery.ts +931 -0
- package/src/beeper-message-like-me-source.ts +4034 -0
- package/src/browser-admission.ts +928 -0
- package/src/browser-snapshots.ts +260 -0
- package/src/browser.ts +3873 -0
- package/src/canonical-json.ts +60 -0
- package/src/catalog-cli.ts +638 -0
- package/src/cli.ts +208 -0
- package/src/client-types.ts +301 -0
- package/src/client.ts +2726 -0
- package/src/confirmed-write-failure.ts +44 -0
- package/src/confirmed-write-model.ts +72 -0
- package/src/confirmed-write-platform.ts +1055 -0
- package/src/confirmed-write-program.ts +224 -0
- package/src/confirmed-write-runtime.ts +16 -0
- package/src/cursor-token.ts +228 -0
- package/src/derivation-dnr-readiness.ts +367 -0
- package/src/derivation-file-chooser.ts +384 -0
- package/src/derivation-network-boundary.ts +835 -0
- package/src/derivation-network-guard.ts +782 -0
- package/src/derivation-network-proxy-helper.ts +440 -0
- package/src/derivation-network-proxy.ts +385 -0
- package/src/derive-command-helper.ts +693 -0
- package/src/derive-fixtures.ts +343 -0
- package/src/derive-review.ts +980 -0
- package/src/derive.ts +4754 -0
- package/src/fixtures/beeper-message-like-me-v1/accounts.ndjson +2 -0
- package/src/fixtures/beeper-message-like-me-v1/conversations.ndjson +1 -0
- package/src/fixtures/beeper-message-like-me-v1/manifest.json +1 -0
- package/src/fixtures/beeper-message-like-me-v1/messages.ndjson +2 -0
- package/src/fixtures/beeper-message-like-me-v1/participants.ndjson +3 -0
- package/src/fixtures/beeper-message-like-me-v1/reactions.ndjson +1 -0
- package/src/fixtures/beeper-message-like-me-v1/tombstones.ndjson +1 -0
- package/src/ghostget.ts +3672 -0
- package/src/gmail-capture.ts +1244 -0
- package/src/har-internal.ts +1242 -0
- package/src/har.ts +743 -0
- package/src/index.ts +218 -0
- package/src/invocation-read-platform.ts +55 -0
- package/src/invocation-read-program.ts +93 -0
- package/src/invocation-read-runtime.ts +11 -0
- package/src/linked-device-lifecycle-admission.ts +450 -0
- package/src/linked-device-lifecycle-journal.ts +1560 -0
- package/src/linked-device-lifecycle-runtime.ts +1602 -0
- package/src/local-cli-admission.ts +135 -0
- package/src/local-cli-contracts.ts +271 -0
- package/src/local-cli-execution.ts +98 -0
- package/src/local-cli-surface-contract.ts +1510 -0
- package/src/local-cli-tool-identity.ts +336 -0
- package/src/media/archive.ts +2325 -0
- package/src/media/args.ts +303 -0
- package/src/media/cli.ts +287 -0
- package/src/media/doctor.ts +475 -0
- package/src/media/ffmpeg.ts +1330 -0
- package/src/media/http-capture.ts +711 -0
- package/src/media/http-probe.ts +593 -0
- package/src/media/http.ts +387 -0
- package/src/media/index.ts +10 -0
- package/src/media/local-transcription.ts +613 -0
- package/src/media/lock.ts +321 -0
- package/src/media/manifest.ts +1712 -0
- package/src/media/metadata.ts +906 -0
- package/src/media/process.ts +723 -0
- package/src/media/revision.ts +166 -0
- package/src/media/runtime-closure.ts +1063 -0
- package/src/media/source-router.ts +187 -0
- package/src/media/transcriber-config.ts +1122 -0
- package/src/media/transcript-persistence-model.ts +15 -0
- package/src/media/transcript-persistence-platform.ts +89 -0
- package/src/media/transcript-persistence-program.ts +39 -0
- package/src/media/transcript-persistence-runtime.ts +24 -0
- package/src/media/transcript.ts +616 -0
- package/src/media/utf8-order.ts +30 -0
- package/src/media/whisper-cpp.ts +464 -0
- package/src/media/whisper-language.ts +58 -0
- package/src/media/yt-dlp.ts +439 -0
- package/src/message-like-me-agentic-messaging.ts +882 -0
- package/src/messaging-action-store.ts +793 -0
- package/src/messaging-runtime.ts +2136 -0
- package/src/messaging-store.ts +539 -0
- package/src/messaging-types.ts +1663 -0
- package/src/messaging.ts +334 -0
- package/src/model.ts +2671 -0
- package/src/oauth-google.ts +888 -0
- package/src/omni-client-types.ts +306 -0
- package/src/omni-client.ts +2225 -0
- package/src/omni-limits.ts +34 -0
- package/src/omni-model.ts +1614 -0
- package/src/omni-request.ts +379 -0
- package/src/omni-runtime.ts +1632 -0
- package/src/operation-deadline.ts +189 -0
- package/src/path-helper.ts +1656 -0
- package/src/pinned-https.ts +194 -0
- package/src/plan-assets.ts +832 -0
- package/src/platform-catalog.ts +1832 -0
- package/src/plugins/beeper-linked-device/plugin.ts +310 -0
- package/src/plugins/bluesky-web/plugin.ts +178 -0
- package/src/plugins/clasificados-web/plugin.ts +66 -0
- package/src/plugins/github-web/plugin.ts +80 -0
- package/src/plugins/gmail-official/plugin.ts +110 -0
- package/src/plugins/hacker-news-web/plugin.ts +52 -0
- package/src/plugins/imessage-direct/plugin.ts +195 -0
- package/src/plugins/imessage-direct/vendor/0001-fix-keep-AppleScript-send-payloads-out-of-child-argv.patch +1430 -0
- package/src/plugins/imessage-direct/vendor/0002-feat-rpc-add-exact-chat-lookup.patch +225 -0
- package/src/plugins/imessage-direct/vendor/provenance.json +80 -0
- package/src/plugins/linkedin-official/plugin.ts +84 -0
- package/src/plugins/linkedin-web/plugin.ts +412 -0
- package/src/plugins/meta-web/plugin.ts +317 -0
- package/src/plugins/reddit-web/flair.ts +492 -0
- package/src/plugins/reddit-web/plugin.ts +477 -0
- package/src/plugins/substack-web/plugin.ts +124 -0
- package/src/plugins/tiktok-web/plugin.ts +67 -0
- package/src/plugins/twitch-web/plugin.ts +54 -0
- package/src/plugins/whatsapp-linked-device/plugin.ts +105 -0
- package/src/plugins/x-official/plugin.ts +64 -0
- package/src/plugins/x-web/plugin.ts +488 -0
- package/src/plugins/youtube-web/plugin.ts +101 -0
- package/src/portable-run-recovery.ts +466 -0
- package/src/process-identity.ts +501 -0
- package/src/profile-clone-helper.ts +104 -0
- package/src/provider-catalog-views.ts +51 -0
- package/src/provider-context.ts +69 -0
- package/src/provider-contract-definitions-x.ts +45 -0
- package/src/provider-contract-definitions.ts +948 -0
- package/src/provider-contract-input-gmail.ts +176 -0
- package/src/provider-contract-input-linkedin.ts +77 -0
- package/src/provider-contract-input-x.ts +304 -0
- package/src/provider-contract-planning.ts +23 -0
- package/src/provider-contract-semantic-identity.ts +54 -0
- package/src/provider-contracts.ts +251 -0
- package/src/provider-http.ts +723 -0
- package/src/provider-plugin-auth.ts +36 -0
- package/src/provider-plugin-builtins.ts +229 -0
- package/src/provider-plugin-cleanup-barrier.ts +229 -0
- package/src/provider-plugin-cleanup-execution.ts +79 -0
- package/src/provider-plugin-cleanup-resource.ts +470 -0
- package/src/provider-plugin-contract-identity.ts +707 -0
- package/src/provider-plugin-host.ts +1972 -0
- package/src/provider-plugin-identifiers.ts +60 -0
- package/src/provider-plugin-invocation-lease.ts +889 -0
- package/src/provider-plugin-lifecycle-kernel.ts +566 -0
- package/src/provider-plugin-lifecycle.ts +1455 -0
- package/src/provider-plugin-module-analysis.ts +336 -0
- package/src/provider-plugin-package.ts +2530 -0
- package/src/provider-plugin-portable-authority.ts +106 -0
- package/src/provider-plugin-portable-catalog.ts +94 -0
- package/src/provider-plugin-portable-identity.ts +315 -0
- package/src/provider-plugin-portable-runtime.ts +2057 -0
- package/src/provider-plugin-protocol.ts +1625 -0
- package/src/provider-plugin-registry.ts +3342 -0
- package/src/provider-plugin-store.ts +2155 -0
- package/src/provider-plugin-test-fixture.ts +5 -0
- package/src/provider-plugin.ts +4907 -0
- package/src/provider-plugins.generated.ts +43 -0
- package/src/provider-plugins.ts +8 -0
- package/src/provider-subject.ts +41 -0
- package/src/provider.ts +315 -0
- package/src/providers/beeper-local-runtime.ts +5426 -0
- package/src/providers/beeper-local.ts +2739 -0
- package/src/providers/beeper-messaging.ts +462 -0
- package/src/providers/beeper-omni.ts +1159 -0
- package/src/providers/bluesky-web-runtime.ts +2895 -0
- package/src/providers/bluesky-web.ts +1614 -0
- package/src/providers/clasificados-web-runtime.ts +255 -0
- package/src/providers/clasificados-web.ts +503 -0
- package/src/providers/contact-projection.ts +301 -0
- package/src/providers/github-read-model.ts +94 -0
- package/src/providers/github-read-platform.ts +165 -0
- package/src/providers/github-read-program.ts +97 -0
- package/src/providers/github-web-runtime.ts +320 -0
- package/src/providers/github-web.ts +403 -0
- package/src/providers/gmail-api.ts +2834 -0
- package/src/providers/gmail-omni.ts +830 -0
- package/src/providers/gmail.ts +1130 -0
- package/src/providers/hacker-news-web-runtime.ts +220 -0
- package/src/providers/hacker-news-web.ts +809 -0
- package/src/providers/imessage-direct-install.ts +354 -0
- package/src/providers/imessage-direct-messaging.ts +532 -0
- package/src/providers/imessage-direct-omni.ts +400 -0
- package/src/providers/imessage-direct-runtime.ts +1498 -0
- package/src/providers/imessage-direct.ts +351 -0
- package/src/providers/instagram-video-foundations.ts +913 -0
- package/src/providers/instagram-web-profile-browser.ts +600 -0
- package/src/providers/iso-bmff.ts +338 -0
- package/src/providers/linkedin-company-platform.ts +42 -0
- package/src/providers/linkedin-company-program.ts +88 -0
- package/src/providers/linkedin-contact-failure.ts +84 -0
- package/src/providers/linkedin-contact-platform.ts +45 -0
- package/src/providers/linkedin-contact-program.ts +91 -0
- package/src/providers/linkedin-profile-activity-failure.ts +76 -0
- package/src/providers/linkedin-profile-activity-platform.ts +41 -0
- package/src/providers/linkedin-profile-activity-program.ts +67 -0
- package/src/providers/linkedin-read-failure.ts +52 -0
- package/src/providers/linkedin-self-platform.ts +53 -0
- package/src/providers/linkedin-self-program.ts +122 -0
- package/src/providers/linkedin-web-article-browser.ts +794 -0
- package/src/providers/linkedin-web-bootstrap.ts +146 -0
- package/src/providers/linkedin-web-contact.ts +830 -0
- package/src/providers/linkedin-web-feed-browser.ts +832 -0
- package/src/providers/linkedin-web-feed.ts +864 -0
- package/src/providers/linkedin-web-post-browser.ts +653 -0
- package/src/providers/linkedin-web-profile-browser.ts +683 -0
- package/src/providers/linkedin-web-runtime.ts +2388 -0
- package/src/providers/linkedin-web.ts +3305 -0
- package/src/providers/linkedin.ts +1885 -0
- package/src/providers/meta-bootstrap.ts +1043 -0
- package/src/providers/meta-facebook-group.ts +690 -0
- package/src/providers/meta-marketplace-relay.ts +747 -0
- package/src/providers/meta-omni.ts +373 -0
- package/src/providers/meta-relay-bundle.ts +1049 -0
- package/src/providers/meta-web-descriptors.ts +1740 -0
- package/src/providers/meta-web-runtime.ts +3388 -0
- package/src/providers/meta-web.ts +2931 -0
- package/src/providers/read-failure.ts +139 -0
- package/src/providers/reddit-omni.ts +462 -0
- package/src/providers/reddit-web-runtime.ts +1929 -0
- package/src/providers/reddit-web.ts +1611 -0
- package/src/providers/rental-listings.ts +460 -0
- package/src/providers/substack-omni.ts +406 -0
- package/src/providers/substack-video-mp4.ts +33 -0
- package/src/providers/substack-web-runtime.ts +2812 -0
- package/src/providers/substack-web.ts +1502 -0
- package/src/providers/tiktok-video-mp4.ts +33 -0
- package/src/providers/tiktok-web-runtime.ts +770 -0
- package/src/providers/tiktok-web.ts +2786 -0
- package/src/providers/twitch-web-runtime.ts +226 -0
- package/src/providers/twitch-web.ts +274 -0
- package/src/providers/whatsapp-account-identity.ts +55 -0
- package/src/providers/whatsapp-contact-projection-helper.ts +874 -0
- package/src/providers/whatsapp-contact-projection-protocol.ts +403 -0
- package/src/providers/whatsapp-interaction-projection-helper.ts +1452 -0
- package/src/providers/whatsapp-interaction-projection-protocol.ts +462 -0
- package/src/providers/whatsapp-message-export-projection-protocol.ts +532 -0
- package/src/providers/whatsapp-messaging.ts +132 -0
- package/src/providers/whatsapp-omni.ts +586 -0
- package/src/providers/whatsapp-web-runtime.ts +3108 -0
- package/src/providers/whatsapp-web.ts +1037 -0
- package/src/providers/x-image-provenance.ts +523 -0
- package/src/providers/x-made-with-ai.ts +106 -0
- package/src/providers/x-transaction-id.ts +413 -0
- package/src/providers/x-web-runtime.ts +3485 -0
- package/src/providers/x-web.ts +2332 -0
- package/src/providers/x.ts +1889 -0
- package/src/providers/youtube-web-runtime.ts +1400 -0
- package/src/providers/youtube-web.ts +1221 -0
- package/src/read-admission-runtime.ts +91 -0
- package/src/read-client.ts +521 -0
- package/src/read-effect-platform.ts +7 -0
- package/src/read-effect-runtime.ts +14 -0
- package/src/read-effect.ts +52 -0
- package/src/read-projection-admission.ts +866 -0
- package/src/read-projections.ts +3151 -0
- package/src/recovery.ts +1492 -0
- package/src/reviewed-template.ts +146 -0
- package/src/run-journal.ts +1272 -0
- package/src/runtime.ts +5116 -0
- package/src/scripts/generate-provider-plugin-catalog.ts +121 -0
- package/src/scripts/install-whatsapp-protocol.sh +232 -0
- package/src/scripts/recover-install-stages.ts +1491 -0
- package/src/scripts/resolve-state-home.ts +30 -0
- package/src/scripts/scaffold-web-provider.ts +973 -0
- package/src/scripts/sync-bundled-adapters.ts +695 -0
- package/src/session-secrets.ts +1183 -0
- package/src/state-helper.bunfig.toml +2 -0
- package/src/state-helper.ts +2914 -0
- package/src/storage.ts +3842 -0
- package/src/transport-policy.ts +2 -0
- package/src/usage.ts +182 -0
- package/src/version.ts +2 -0
- package/src/web-session-authentication-policy.ts +136 -0
- package/src/web-session-cleanup-admission.ts +2344 -0
- package/src/web-session-cleanup-join.ts +99 -0
- package/src/web-session-client.ts +996 -0
- package/src/web-session-contract-definitions.ts +820 -0
- package/src/web-session-contract-planning.ts +42 -0
- package/src/web-session-contracts.ts +228 -0
- package/src/web-session-cookies.ts +500 -0
- package/src/web-session-execution.ts +385 -0
- package/src/web-session-http.ts +373 -0
- package/src/web-session-read-errors.ts +19 -0
- package/src/web-session-read-runtime.ts +124 -0
- package/src/web-session-recovery.ts +818 -0
- package/src/web-session-sites.ts +58 -0
- package/src/web-session-template.ts +994 -0
- package/src/web-session.ts +93 -0
- package/src/whatsapp-client-binding.ts +21 -0
- package/src/whatsapp-client-types.ts +75 -0
- package/src/whatsapp-client.ts +395 -0
- package/src/whatsapp-export-coordinate.ts +17 -0
- package/src/whatsapp-message-bundle-v2.ts +195 -0
- package/src/whatsapp-message-like-me-cli.ts +195 -0
- package/src/whatsapp-message-like-me-export.ts +1072 -0
- package/src/whatsapp-message-like-me-source.ts +1012 -0
- package/tsconfig.json +67 -0
|
@@ -0,0 +1,928 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
runCapture as runKbCapture,
|
|
6
|
+
type CaptureArguments,
|
|
7
|
+
type CaptureOutcome,
|
|
8
|
+
} from "@hraness/kb/capture";
|
|
9
|
+
import {
|
|
10
|
+
acquireBrowser as acquireKbBrowser,
|
|
11
|
+
type AcquiredPage,
|
|
12
|
+
} from "@hraness/kb/clip/acquire";
|
|
13
|
+
import { canonicalJson, sha256 } from "./canonical-json";
|
|
14
|
+
import {
|
|
15
|
+
currentProcessStartIdentity,
|
|
16
|
+
processOwnerStatus,
|
|
17
|
+
type ProcessOwnerIdentity,
|
|
18
|
+
type ProcessOwnerStatus,
|
|
19
|
+
type ProcessStartIdentity,
|
|
20
|
+
} from "./process-identity";
|
|
21
|
+
import {
|
|
22
|
+
createPrivateJsonIfAbsent,
|
|
23
|
+
ensurePrivateStateDirectory,
|
|
24
|
+
readPrivateStateFileIfPresent,
|
|
25
|
+
removePrivateStateFileIfUnchanged,
|
|
26
|
+
ghostgetStateHome,
|
|
27
|
+
} from "./storage";
|
|
28
|
+
|
|
29
|
+
type Environment = Readonly<Record<string, string | undefined>>;
|
|
30
|
+
type JsonRecord = Record<string, unknown>;
|
|
31
|
+
|
|
32
|
+
const ADMISSION_SCHEMA_VERSION = 1 as const;
|
|
33
|
+
const MAX_CLAIM_BYTES = 4 * 1024;
|
|
34
|
+
const MIN_RETRY_DELAY_MS = 25;
|
|
35
|
+
const MAX_RETRY_DELAY_MS = 250;
|
|
36
|
+
const MAX_WAIT_MS = 30_000;
|
|
37
|
+
// storage.ts gives one state-helper invocation 30 seconds; allow recovery margin.
|
|
38
|
+
const RELEASE_SETTLEMENT_WAIT_MS = 35_000;
|
|
39
|
+
const RELEASE_SETTLEMENT_SLICE_MS = 10;
|
|
40
|
+
const releaseSettlementState = new Int32Array(new SharedArrayBuffer(4));
|
|
41
|
+
const uuidPattern =
|
|
42
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
|
|
43
|
+
const digestPattern = /^[a-f0-9]{64}$/u;
|
|
44
|
+
|
|
45
|
+
export const LOCAL_BROWSER_ADMISSION_LIMIT = 2 as const;
|
|
46
|
+
export const BROWSER_ADMISSION_STATE_DIRECTORY =
|
|
47
|
+
"captures/browser-admissions" as const;
|
|
48
|
+
|
|
49
|
+
export type BrowserAdmissionSlot = 0 | 1;
|
|
50
|
+
|
|
51
|
+
export type BrowserAdmissionClaim = {
|
|
52
|
+
readonly schemaVersion: typeof ADMISSION_SCHEMA_VERSION;
|
|
53
|
+
readonly slot: BrowserAdmissionSlot;
|
|
54
|
+
readonly acquiredAt: string;
|
|
55
|
+
readonly owner: ProcessOwnerIdentity & {
|
|
56
|
+
readonly token: string;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
type BrowserAdmissionClaimSnapshot = {
|
|
61
|
+
readonly claim: BrowserAdmissionClaim;
|
|
62
|
+
readonly contentSha256: string;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type BrowserAdmission = {
|
|
66
|
+
readonly slot: BrowserAdmissionSlot;
|
|
67
|
+
readonly acquiredAt: string;
|
|
68
|
+
readonly owner: BrowserAdmissionClaim["owner"];
|
|
69
|
+
readonly release: () => void;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export type BrowserAdmissionDeadline = {
|
|
73
|
+
readonly signal: AbortSignal;
|
|
74
|
+
readonly remainingTimeMs: () => number;
|
|
75
|
+
readonly throwIfUnavailable: (label?: string) => void;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export type BrowserAdmissionDependencies = {
|
|
79
|
+
readonly monotonicNow?: () => number;
|
|
80
|
+
readonly wallClockNow?: () => Date;
|
|
81
|
+
readonly random?: () => number;
|
|
82
|
+
readonly randomToken?: () => string;
|
|
83
|
+
readonly currentProcessIdentity?: () => ProcessStartIdentity;
|
|
84
|
+
readonly ownerStatus?: (owner: ProcessOwnerIdentity) => ProcessOwnerStatus;
|
|
85
|
+
readonly sleep?: (
|
|
86
|
+
milliseconds: number,
|
|
87
|
+
signals: readonly AbortSignal[],
|
|
88
|
+
) => Promise<void>;
|
|
89
|
+
/** Test-only seam invoked immediately before an absent-slot create. */
|
|
90
|
+
readonly beforeCreateForTest?: (slot: BrowserAdmissionSlot) => void;
|
|
91
|
+
/** Test-only seam invoked after a durable create and before it is returned. */
|
|
92
|
+
readonly afterCreateCommitForTest?: () => void;
|
|
93
|
+
/** Test-only seam invoked before the first read of a slot in one attempt. */
|
|
94
|
+
readonly beforeClaimReadForTest?: (slot: BrowserAdmissionSlot) => void;
|
|
95
|
+
/** Test-only seam invoked before rereading a slot after create contention. */
|
|
96
|
+
readonly beforeContendedClaimReadForTest?: (
|
|
97
|
+
slot: BrowserAdmissionSlot,
|
|
98
|
+
) => void;
|
|
99
|
+
/** Test-only seam invoked once when an exact release loses mutation arbitration. */
|
|
100
|
+
readonly afterReleaseContentionForTest?: () => void;
|
|
101
|
+
/** Test-only monotonic clock for bounded release settlement. */
|
|
102
|
+
readonly releaseMonotonicNowForTest?: () => number;
|
|
103
|
+
/** Test-only synchronous wait for bounded release settlement. */
|
|
104
|
+
readonly releaseWaitForTest?: (milliseconds: number) => void;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export type AcquireBrowserAdmissionOptions = {
|
|
108
|
+
readonly timeoutMs: number;
|
|
109
|
+
readonly environment?: Environment;
|
|
110
|
+
readonly signal?: AbortSignal;
|
|
111
|
+
readonly deadline?: BrowserAdmissionDeadline;
|
|
112
|
+
readonly dependencies?: BrowserAdmissionDependencies;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export class BrowserAdmissionError extends Error {
|
|
116
|
+
readonly failure: "cancelled" | "timed-out";
|
|
117
|
+
|
|
118
|
+
constructor(failure: "cancelled" | "timed-out") {
|
|
119
|
+
super(
|
|
120
|
+
failure === "cancelled"
|
|
121
|
+
? "local browser admission was cancelled"
|
|
122
|
+
: `local browser admission polling timed out at the ${LOCAL_BROWSER_ADMISSION_LIMIT}-browser limit (polling budget cap: ${MAX_WAIT_MS / 1_000} seconds)`,
|
|
123
|
+
);
|
|
124
|
+
this.name = "BrowserAdmissionError";
|
|
125
|
+
this.failure = failure;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
class BrowserAdmissionClaimError extends Error {
|
|
130
|
+
constructor(message: string) {
|
|
131
|
+
super(message);
|
|
132
|
+
this.name = "BrowserAdmissionClaimError";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function strictRecord(value: unknown, label: string): JsonRecord {
|
|
137
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
138
|
+
throw new BrowserAdmissionClaimError(`${label} must be an object`);
|
|
139
|
+
}
|
|
140
|
+
const prototype: unknown = Object.getPrototypeOf(value) as unknown;
|
|
141
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
142
|
+
throw new BrowserAdmissionClaimError(`${label} has an unsupported prototype`);
|
|
143
|
+
}
|
|
144
|
+
const result = Object.create(null) as JsonRecord;
|
|
145
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
146
|
+
if (typeof key !== "string") {
|
|
147
|
+
throw new BrowserAdmissionClaimError(`${label} has unsupported symbol fields`);
|
|
148
|
+
}
|
|
149
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
150
|
+
if (
|
|
151
|
+
descriptor === undefined
|
|
152
|
+
|| !descriptor.enumerable
|
|
153
|
+
|| !("value" in descriptor)
|
|
154
|
+
) {
|
|
155
|
+
throw new BrowserAdmissionClaimError(`${label} has unsupported accessor fields`);
|
|
156
|
+
}
|
|
157
|
+
result[key] = descriptor.value as unknown;
|
|
158
|
+
}
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function exactKeys(
|
|
163
|
+
value: JsonRecord,
|
|
164
|
+
expected: readonly string[],
|
|
165
|
+
label: string,
|
|
166
|
+
): void {
|
|
167
|
+
const actual = Object.keys(value).sort();
|
|
168
|
+
const wanted = [...expected].sort();
|
|
169
|
+
if (
|
|
170
|
+
actual.length !== wanted.length
|
|
171
|
+
|| actual.some((key, index) => key !== wanted[index])
|
|
172
|
+
) {
|
|
173
|
+
throw new BrowserAdmissionClaimError(`${label} has unsupported fields`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function parseDigest(value: unknown, label: string): string {
|
|
178
|
+
if (typeof value !== "string" || !digestPattern.test(value)) {
|
|
179
|
+
throw new BrowserAdmissionClaimError(`${label} is malformed`);
|
|
180
|
+
}
|
|
181
|
+
return value;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function parseTimestamp(value: unknown): string {
|
|
185
|
+
if (
|
|
186
|
+
typeof value !== "string"
|
|
187
|
+
|| !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(value)
|
|
188
|
+
|| !Number.isFinite(Date.parse(value))
|
|
189
|
+
|| new Date(value).toISOString() !== value
|
|
190
|
+
) {
|
|
191
|
+
throw new BrowserAdmissionClaimError(
|
|
192
|
+
"browser admission acquisition time is malformed",
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
return value;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function parseOwner(value: unknown): BrowserAdmissionClaim["owner"] {
|
|
199
|
+
const owner = strictRecord(value, "browser admission owner");
|
|
200
|
+
exactKeys(
|
|
201
|
+
owner,
|
|
202
|
+
["pid", "token", "bootId", "processStartId"],
|
|
203
|
+
"browser admission owner",
|
|
204
|
+
);
|
|
205
|
+
if (
|
|
206
|
+
typeof owner.pid !== "number"
|
|
207
|
+
|| !Number.isSafeInteger(owner.pid)
|
|
208
|
+
|| owner.pid < 1
|
|
209
|
+
) {
|
|
210
|
+
throw new BrowserAdmissionClaimError("browser admission owner PID is malformed");
|
|
211
|
+
}
|
|
212
|
+
if (typeof owner.token !== "string" || !uuidPattern.test(owner.token)) {
|
|
213
|
+
throw new BrowserAdmissionClaimError("browser admission owner token is malformed");
|
|
214
|
+
}
|
|
215
|
+
return Object.freeze({
|
|
216
|
+
pid: owner.pid,
|
|
217
|
+
token: owner.token,
|
|
218
|
+
bootId: parseDigest(owner.bootId, "browser admission boot identity"),
|
|
219
|
+
processStartId: parseDigest(
|
|
220
|
+
owner.processStartId,
|
|
221
|
+
"browser admission process-start identity",
|
|
222
|
+
),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function parseBrowserAdmissionClaim(
|
|
227
|
+
value: unknown,
|
|
228
|
+
): BrowserAdmissionClaim {
|
|
229
|
+
const claim = strictRecord(value, "browser admission claim");
|
|
230
|
+
exactKeys(
|
|
231
|
+
claim,
|
|
232
|
+
["schemaVersion", "slot", "acquiredAt", "owner"],
|
|
233
|
+
"browser admission claim",
|
|
234
|
+
);
|
|
235
|
+
if (claim.schemaVersion !== ADMISSION_SCHEMA_VERSION) {
|
|
236
|
+
throw new BrowserAdmissionClaimError("browser admission schema version is invalid");
|
|
237
|
+
}
|
|
238
|
+
if (claim.slot !== 0 && claim.slot !== 1) {
|
|
239
|
+
throw new BrowserAdmissionClaimError("browser admission slot is malformed");
|
|
240
|
+
}
|
|
241
|
+
return Object.freeze({
|
|
242
|
+
schemaVersion: ADMISSION_SCHEMA_VERSION,
|
|
243
|
+
slot: claim.slot,
|
|
244
|
+
acquiredAt: parseTimestamp(claim.acquiredAt),
|
|
245
|
+
owner: parseOwner(claim.owner),
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function claimDirectory(environment: Environment): string {
|
|
250
|
+
return join(
|
|
251
|
+
ghostgetStateHome(environment),
|
|
252
|
+
...BROWSER_ADMISSION_STATE_DIRECTORY.split("/"),
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function claimPath(
|
|
257
|
+
slot: BrowserAdmissionSlot,
|
|
258
|
+
environment: Environment,
|
|
259
|
+
): string {
|
|
260
|
+
return join(claimDirectory(environment), `slot-${slot}.json`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function claimSnapshot(claim: BrowserAdmissionClaim): BrowserAdmissionClaimSnapshot {
|
|
264
|
+
const content = `${canonicalJson(claim)}\n`;
|
|
265
|
+
return Object.freeze({ claim, contentSha256: sha256(content) });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function readClaim(
|
|
269
|
+
slot: BrowserAdmissionSlot,
|
|
270
|
+
environment: Environment,
|
|
271
|
+
): BrowserAdmissionClaimSnapshot | null {
|
|
272
|
+
const content = readPrivateStateFileIfPresent(
|
|
273
|
+
claimPath(slot, environment),
|
|
274
|
+
MAX_CLAIM_BYTES,
|
|
275
|
+
"browser admission claim",
|
|
276
|
+
environment,
|
|
277
|
+
);
|
|
278
|
+
if (content === null) return null;
|
|
279
|
+
let value: unknown;
|
|
280
|
+
try {
|
|
281
|
+
value = JSON.parse(content) as unknown;
|
|
282
|
+
} catch {
|
|
283
|
+
throw new BrowserAdmissionClaimError("browser admission claim is malformed JSON");
|
|
284
|
+
}
|
|
285
|
+
const claim = parseBrowserAdmissionClaim(value);
|
|
286
|
+
if (claim.slot !== slot) {
|
|
287
|
+
throw new BrowserAdmissionClaimError(
|
|
288
|
+
"browser admission claim does not match its slot",
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
if (content !== `${canonicalJson(claim)}\n`) {
|
|
292
|
+
throw new BrowserAdmissionClaimError(
|
|
293
|
+
"browser admission claim is not canonical JSON",
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
return Object.freeze({ claim, contentSha256: sha256(content) });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function validateTimeout(timeoutMs: number): number {
|
|
300
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 0) {
|
|
301
|
+
throw new Error("browser admission timeout must be a non-negative safe integer");
|
|
302
|
+
}
|
|
303
|
+
return Math.min(timeoutMs, MAX_WAIT_MS);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function validateMonotonicTime(value: number): number {
|
|
307
|
+
if (!Number.isFinite(value)) {
|
|
308
|
+
throw new Error("browser admission monotonic clock returned an invalid time");
|
|
309
|
+
}
|
|
310
|
+
return value;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function defaultSleep(
|
|
314
|
+
milliseconds: number,
|
|
315
|
+
signals: readonly AbortSignal[],
|
|
316
|
+
): Promise<void> {
|
|
317
|
+
return new Promise((resolve, reject) => {
|
|
318
|
+
let settled = false;
|
|
319
|
+
const cleanup = (): void => {
|
|
320
|
+
clearTimeout(timer);
|
|
321
|
+
for (const signal of signals) signal.removeEventListener("abort", onAbort);
|
|
322
|
+
};
|
|
323
|
+
const settle = (operation: () => void): void => {
|
|
324
|
+
if (settled) return;
|
|
325
|
+
settled = true;
|
|
326
|
+
cleanup();
|
|
327
|
+
operation();
|
|
328
|
+
};
|
|
329
|
+
const onAbort = (): void => settle(() => reject(new BrowserAdmissionError("cancelled")));
|
|
330
|
+
const timer = setTimeout(() => settle(resolve), milliseconds);
|
|
331
|
+
for (const signal of signals) {
|
|
332
|
+
if (signal.aborted) {
|
|
333
|
+
onAbort();
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function activeSignals(options: AcquireBrowserAdmissionOptions): readonly AbortSignal[] {
|
|
342
|
+
return [options.signal, options.deadline?.signal].filter(
|
|
343
|
+
(signal): signal is AbortSignal => signal !== undefined,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function throwIfUnavailable(
|
|
348
|
+
options: AcquireBrowserAdmissionOptions,
|
|
349
|
+
now: () => number,
|
|
350
|
+
expiresAt: number,
|
|
351
|
+
): void {
|
|
352
|
+
options.deadline?.throwIfUnavailable("local browser admission");
|
|
353
|
+
if (options.signal?.aborted === true) {
|
|
354
|
+
throw new BrowserAdmissionError("cancelled");
|
|
355
|
+
}
|
|
356
|
+
if (validateMonotonicTime(now()) >= expiresAt) {
|
|
357
|
+
throw new BrowserAdmissionError("timed-out");
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function remainingWait(
|
|
362
|
+
options: AcquireBrowserAdmissionOptions,
|
|
363
|
+
now: () => number,
|
|
364
|
+
expiresAt: number,
|
|
365
|
+
): number {
|
|
366
|
+
const localRemaining = Math.max(
|
|
367
|
+
0,
|
|
368
|
+
Math.floor(expiresAt - validateMonotonicTime(now())),
|
|
369
|
+
);
|
|
370
|
+
if (options.deadline === undefined) return localRemaining;
|
|
371
|
+
options.deadline.throwIfUnavailable("local browser admission");
|
|
372
|
+
const deadlineRemaining = options.deadline.remainingTimeMs();
|
|
373
|
+
if (!Number.isSafeInteger(deadlineRemaining) || deadlineRemaining < 0) {
|
|
374
|
+
throw new Error("browser admission deadline returned invalid remaining time");
|
|
375
|
+
}
|
|
376
|
+
return Math.max(
|
|
377
|
+
0,
|
|
378
|
+
Math.min(localRemaining, deadlineRemaining),
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function retryDelay(attempt: number, random: () => number): number {
|
|
383
|
+
const randomValue = random();
|
|
384
|
+
if (!Number.isFinite(randomValue) || randomValue < 0 || randomValue >= 1) {
|
|
385
|
+
throw new Error("browser admission random source returned an invalid value");
|
|
386
|
+
}
|
|
387
|
+
const exponential = Math.min(
|
|
388
|
+
MAX_RETRY_DELAY_MS,
|
|
389
|
+
MIN_RETRY_DELAY_MS * (2 ** Math.min(attempt, 3)),
|
|
390
|
+
);
|
|
391
|
+
return Math.max(
|
|
392
|
+
1,
|
|
393
|
+
Math.round(exponential * (0.75 + randomValue * 0.5)),
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function stateHelperErrorDetail(error: unknown): string | null {
|
|
398
|
+
if (!(error instanceof Error)) return null;
|
|
399
|
+
return error.message.startsWith("state helper: ")
|
|
400
|
+
? error.message.slice("state helper: ".length)
|
|
401
|
+
: error.message;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function isConcurrentStateDirectoryCreation(error: unknown): boolean {
|
|
405
|
+
const detail = stateHelperErrorDetail(error);
|
|
406
|
+
return detail === "state directory appeared where absence was required"
|
|
407
|
+
|| detail === "state directory appeared while being created"
|
|
408
|
+
|| detail === "state directory appeared where absence was expected";
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function isActiveStateFileMutation(error: unknown): boolean {
|
|
412
|
+
return stateHelperErrorDetail(error) === "state file mutation is already active";
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function isStateFileReadDrift(error: unknown): boolean {
|
|
416
|
+
const seen = new Set<Error>();
|
|
417
|
+
let current = error;
|
|
418
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
419
|
+
seen.add(current);
|
|
420
|
+
if (
|
|
421
|
+
current.message
|
|
422
|
+
=== "state helper: state file changed while it was read"
|
|
423
|
+
) return true;
|
|
424
|
+
current = current.cause;
|
|
425
|
+
}
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async function ensureAdmissionStateDirectory(
|
|
430
|
+
environment: Environment,
|
|
431
|
+
options: AcquireBrowserAdmissionOptions,
|
|
432
|
+
now: () => number,
|
|
433
|
+
expiresAt: number,
|
|
434
|
+
random: () => number,
|
|
435
|
+
sleep: NonNullable<BrowserAdmissionDependencies["sleep"]>,
|
|
436
|
+
): Promise<void> {
|
|
437
|
+
let attempt = 0;
|
|
438
|
+
for (;;) {
|
|
439
|
+
throwIfUnavailable(options, now, expiresAt);
|
|
440
|
+
try {
|
|
441
|
+
ensurePrivateStateDirectory(claimDirectory(environment), environment);
|
|
442
|
+
return;
|
|
443
|
+
} catch (error) {
|
|
444
|
+
if (!isConcurrentStateDirectoryCreation(error)) throw error;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const remaining = remainingWait(options, now, expiresAt);
|
|
448
|
+
if (remaining < 1) throw new BrowserAdmissionError("timed-out");
|
|
449
|
+
const delay = Math.min(remaining, retryDelay(attempt, random));
|
|
450
|
+
attempt += 1;
|
|
451
|
+
try {
|
|
452
|
+
await sleep(delay, activeSignals(options));
|
|
453
|
+
} catch (error) {
|
|
454
|
+
options.deadline?.throwIfUnavailable("local browser admission");
|
|
455
|
+
if (activeSignals(options).some((signal) => signal.aborted)) {
|
|
456
|
+
throw new BrowserAdmissionError("cancelled");
|
|
457
|
+
}
|
|
458
|
+
throw error;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function exactOwnerStatus(
|
|
464
|
+
owner: ProcessOwnerIdentity,
|
|
465
|
+
inspect: (owner: ProcessOwnerIdentity) => ProcessOwnerStatus,
|
|
466
|
+
): ProcessOwnerStatus {
|
|
467
|
+
try {
|
|
468
|
+
return inspect(owner);
|
|
469
|
+
} catch {
|
|
470
|
+
return "unknown";
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function newClaim(
|
|
475
|
+
slot: BrowserAdmissionSlot,
|
|
476
|
+
acquiredAt: string,
|
|
477
|
+
owner: BrowserAdmissionClaim["owner"],
|
|
478
|
+
): BrowserAdmissionClaimSnapshot {
|
|
479
|
+
return claimSnapshot(parseBrowserAdmissionClaim({
|
|
480
|
+
schemaVersion: ADMISSION_SCHEMA_VERSION,
|
|
481
|
+
slot,
|
|
482
|
+
acquiredAt,
|
|
483
|
+
owner,
|
|
484
|
+
}));
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function sameClaim(
|
|
488
|
+
left: BrowserAdmissionClaim,
|
|
489
|
+
right: BrowserAdmissionClaim,
|
|
490
|
+
): boolean {
|
|
491
|
+
return left.schemaVersion === right.schemaVersion
|
|
492
|
+
&& left.slot === right.slot
|
|
493
|
+
&& left.acquiredAt === right.acquiredAt
|
|
494
|
+
&& left.owner.pid === right.owner.pid
|
|
495
|
+
&& left.owner.token === right.owner.token
|
|
496
|
+
&& left.owner.bootId === right.owner.bootId
|
|
497
|
+
&& left.owner.processStartId === right.owner.processStartId;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function exactSnapshotStillHeld(
|
|
501
|
+
current: BrowserAdmissionClaimSnapshot | null,
|
|
502
|
+
expected: BrowserAdmissionClaimSnapshot,
|
|
503
|
+
inspectOwner: (owner: ProcessOwnerIdentity) => ProcessOwnerStatus,
|
|
504
|
+
): boolean {
|
|
505
|
+
return current !== null
|
|
506
|
+
&& current.contentSha256 === expected.contentSha256
|
|
507
|
+
&& sameClaim(current.claim, expected.claim)
|
|
508
|
+
&& exactOwnerStatus(current.claim.owner, inspectOwner) === "exact-live-owner";
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function admissionFromSnapshot(
|
|
512
|
+
snapshot: BrowserAdmissionClaimSnapshot,
|
|
513
|
+
environment: Environment,
|
|
514
|
+
inspectOwner: (owner: ProcessOwnerIdentity) => ProcessOwnerStatus,
|
|
515
|
+
afterReleaseContentionForTest?: () => void,
|
|
516
|
+
releaseNow: () => number = () => performance.now(),
|
|
517
|
+
releaseWait: (milliseconds: number) => void = (milliseconds) => {
|
|
518
|
+
Atomics.wait(releaseSettlementState, 0, 0, milliseconds);
|
|
519
|
+
},
|
|
520
|
+
): BrowserAdmission {
|
|
521
|
+
let released = false;
|
|
522
|
+
return Object.freeze({
|
|
523
|
+
slot: snapshot.claim.slot,
|
|
524
|
+
acquiredAt: snapshot.claim.acquiredAt,
|
|
525
|
+
owner: snapshot.claim.owner,
|
|
526
|
+
release: () => {
|
|
527
|
+
if (released) return;
|
|
528
|
+
const deadline = validateMonotonicTime(releaseNow())
|
|
529
|
+
+ RELEASE_SETTLEMENT_WAIT_MS;
|
|
530
|
+
let contentionReported = false;
|
|
531
|
+
for (;;) {
|
|
532
|
+
if (!exactSnapshotStillHeld(
|
|
533
|
+
readClaim(snapshot.claim.slot, environment),
|
|
534
|
+
snapshot,
|
|
535
|
+
inspectOwner,
|
|
536
|
+
)) {
|
|
537
|
+
throw new Error("browser admission authority is not exact and live");
|
|
538
|
+
}
|
|
539
|
+
if (removePrivateStateFileIfUnchanged(
|
|
540
|
+
claimPath(snapshot.claim.slot, environment),
|
|
541
|
+
{ expectedCurrentContentSha256: snapshot.contentSha256 },
|
|
542
|
+
environment,
|
|
543
|
+
)) {
|
|
544
|
+
released = true;
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (!exactSnapshotStillHeld(
|
|
549
|
+
readClaim(snapshot.claim.slot, environment),
|
|
550
|
+
snapshot,
|
|
551
|
+
inspectOwner,
|
|
552
|
+
)) {
|
|
553
|
+
throw new Error("browser admission changed before release");
|
|
554
|
+
}
|
|
555
|
+
if (!contentionReported) {
|
|
556
|
+
afterReleaseContentionForTest?.();
|
|
557
|
+
contentionReported = true;
|
|
558
|
+
}
|
|
559
|
+
const remaining = deadline - validateMonotonicTime(releaseNow());
|
|
560
|
+
if (remaining <= 0) {
|
|
561
|
+
throw new Error(
|
|
562
|
+
"browser admission could not be released within its bounded wait",
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
releaseWait(
|
|
566
|
+
Math.min(RELEASE_SETTLEMENT_SLICE_MS, Math.ceil(remaining)),
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
},
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function errorValue(error: unknown, label: string): Error {
|
|
574
|
+
return error instanceof Error ? error : new Error(label, { cause: error });
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function admissionWithinDeadline(
|
|
578
|
+
snapshot: BrowserAdmissionClaimSnapshot,
|
|
579
|
+
environment: Environment,
|
|
580
|
+
options: AcquireBrowserAdmissionOptions,
|
|
581
|
+
now: () => number,
|
|
582
|
+
expiresAt: number,
|
|
583
|
+
inspectOwner: (owner: ProcessOwnerIdentity) => ProcessOwnerStatus,
|
|
584
|
+
afterReleaseContentionForTest?: () => void,
|
|
585
|
+
releaseNow?: () => number,
|
|
586
|
+
releaseWait?: (milliseconds: number) => void,
|
|
587
|
+
): BrowserAdmission {
|
|
588
|
+
const admission = admissionFromSnapshot(
|
|
589
|
+
snapshot,
|
|
590
|
+
environment,
|
|
591
|
+
inspectOwner,
|
|
592
|
+
afterReleaseContentionForTest,
|
|
593
|
+
releaseNow,
|
|
594
|
+
releaseWait,
|
|
595
|
+
);
|
|
596
|
+
try {
|
|
597
|
+
throwIfUnavailable(options, now, expiresAt);
|
|
598
|
+
} catch (error) {
|
|
599
|
+
try {
|
|
600
|
+
admission.release();
|
|
601
|
+
} catch (releaseFailure) {
|
|
602
|
+
throw new AggregateError(
|
|
603
|
+
[error, releaseFailure],
|
|
604
|
+
"browser admission expired after acquisition and could not be released",
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
throw error;
|
|
608
|
+
}
|
|
609
|
+
return admission;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
export function assertBrowserAdmissionHeld(
|
|
613
|
+
admission: BrowserAdmission,
|
|
614
|
+
environment: Environment = process.env,
|
|
615
|
+
): void {
|
|
616
|
+
const snapshot = readClaim(admission.slot, environment);
|
|
617
|
+
if (
|
|
618
|
+
snapshot === null
|
|
619
|
+
|| snapshot.claim.acquiredAt !== admission.acquiredAt
|
|
620
|
+
|| snapshot.claim.owner.pid !== admission.owner.pid
|
|
621
|
+
|| snapshot.claim.owner.token !== admission.owner.token
|
|
622
|
+
|| snapshot.claim.owner.bootId !== admission.owner.bootId
|
|
623
|
+
|| snapshot.claim.owner.processStartId !== admission.owner.processStartId
|
|
624
|
+
|| processOwnerStatus(snapshot.claim.owner) !== "exact-live-owner"
|
|
625
|
+
) {
|
|
626
|
+
throw new Error("browser admission authority is not exact and live");
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
export async function acquireBrowserAdmission(
|
|
631
|
+
options: AcquireBrowserAdmissionOptions,
|
|
632
|
+
): Promise<BrowserAdmission> {
|
|
633
|
+
const environment = options.environment ?? process.env;
|
|
634
|
+
const dependencies = options.dependencies ?? {};
|
|
635
|
+
const now = dependencies.monotonicNow ?? (() => performance.now());
|
|
636
|
+
const timeoutMs = validateTimeout(options.timeoutMs);
|
|
637
|
+
const startedAt = validateMonotonicTime(now());
|
|
638
|
+
const expiresAt = startedAt + timeoutMs;
|
|
639
|
+
if (!Number.isFinite(expiresAt)) {
|
|
640
|
+
throw new Error("browser admission deadline exceeded the supported time range");
|
|
641
|
+
}
|
|
642
|
+
options.deadline?.throwIfUnavailable("local browser admission");
|
|
643
|
+
if (options.signal?.aborted === true) {
|
|
644
|
+
throw new BrowserAdmissionError("cancelled");
|
|
645
|
+
}
|
|
646
|
+
if (timeoutMs === 0) throw new BrowserAdmissionError("timed-out");
|
|
647
|
+
|
|
648
|
+
const currentIdentity = (
|
|
649
|
+
dependencies.currentProcessIdentity ?? currentProcessStartIdentity
|
|
650
|
+
)();
|
|
651
|
+
const token = (dependencies.randomToken ?? randomUUID)();
|
|
652
|
+
if (!uuidPattern.test(token)) {
|
|
653
|
+
throw new Error("browser admission token source returned a malformed UUID");
|
|
654
|
+
}
|
|
655
|
+
const acquiredAt = parseTimestamp(
|
|
656
|
+
(dependencies.wallClockNow ?? (() => new Date()))().toISOString(),
|
|
657
|
+
);
|
|
658
|
+
const owner = Object.freeze({
|
|
659
|
+
pid: process.pid,
|
|
660
|
+
token,
|
|
661
|
+
bootId: currentIdentity.bootId,
|
|
662
|
+
processStartId: currentIdentity.processStartId,
|
|
663
|
+
});
|
|
664
|
+
const inspectOwner = dependencies.ownerStatus ?? processOwnerStatus;
|
|
665
|
+
const random = dependencies.random ?? Math.random;
|
|
666
|
+
const sleep = dependencies.sleep ?? defaultSleep;
|
|
667
|
+
await ensureAdmissionStateDirectory(
|
|
668
|
+
environment,
|
|
669
|
+
options,
|
|
670
|
+
now,
|
|
671
|
+
expiresAt,
|
|
672
|
+
random,
|
|
673
|
+
sleep,
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
let attempt = 0;
|
|
677
|
+
for (;;) {
|
|
678
|
+
throwIfUnavailable(options, now, expiresAt);
|
|
679
|
+
let stateChanged = false;
|
|
680
|
+
let storageContention = false;
|
|
681
|
+
for (const slot of [0, 1] as const) {
|
|
682
|
+
throwIfUnavailable(options, now, expiresAt);
|
|
683
|
+
let existing: BrowserAdmissionClaimSnapshot | null;
|
|
684
|
+
try {
|
|
685
|
+
dependencies.beforeClaimReadForTest?.(slot);
|
|
686
|
+
existing = readClaim(slot, environment);
|
|
687
|
+
} catch (error) {
|
|
688
|
+
if (error instanceof BrowserAdmissionClaimError) {
|
|
689
|
+
// Malformed claims occupy their slot until a human repairs them.
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
if (isStateFileReadDrift(error)) {
|
|
693
|
+
// Eight overlapping owners can rewrite a slot between helper open
|
|
694
|
+
// and helper close. Retry the same occupancy rules after backoff.
|
|
695
|
+
storageContention = true;
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
throw error;
|
|
699
|
+
}
|
|
700
|
+
if (existing === null) {
|
|
701
|
+
const proposed = newClaim(slot, acquiredAt, owner);
|
|
702
|
+
let created = false;
|
|
703
|
+
try {
|
|
704
|
+
dependencies.beforeCreateForTest?.(slot);
|
|
705
|
+
created = createPrivateJsonIfAbsent(
|
|
706
|
+
claimPath(slot, environment),
|
|
707
|
+
proposed.claim,
|
|
708
|
+
{ environment, privateParent: true },
|
|
709
|
+
).created;
|
|
710
|
+
if (created) dependencies.afterCreateCommitForTest?.();
|
|
711
|
+
} catch (error) {
|
|
712
|
+
let committed: BrowserAdmissionClaimSnapshot | null;
|
|
713
|
+
try {
|
|
714
|
+
committed = readClaim(slot, environment);
|
|
715
|
+
} catch (reconciliationError) {
|
|
716
|
+
if (isStateFileReadDrift(reconciliationError)) {
|
|
717
|
+
storageContention = true;
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
throw new AggregateError(
|
|
721
|
+
[
|
|
722
|
+
errorValue(error, "browser admission creation failed"),
|
|
723
|
+
errorValue(
|
|
724
|
+
reconciliationError,
|
|
725
|
+
"browser admission creation reconciliation failed",
|
|
726
|
+
),
|
|
727
|
+
],
|
|
728
|
+
"browser admission creation could not be reconciled",
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
if (
|
|
732
|
+
committed !== null
|
|
733
|
+
&& committed.contentSha256 === proposed.contentSha256
|
|
734
|
+
&& sameClaim(committed.claim, proposed.claim)
|
|
735
|
+
&& exactOwnerStatus(committed.claim.owner, inspectOwner)
|
|
736
|
+
=== "exact-live-owner"
|
|
737
|
+
) {
|
|
738
|
+
return admissionWithinDeadline(
|
|
739
|
+
proposed,
|
|
740
|
+
environment,
|
|
741
|
+
options,
|
|
742
|
+
now,
|
|
743
|
+
expiresAt,
|
|
744
|
+
inspectOwner,
|
|
745
|
+
dependencies.afterReleaseContentionForTest,
|
|
746
|
+
dependencies.releaseMonotonicNowForTest,
|
|
747
|
+
dependencies.releaseWaitForTest,
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
if (!isActiveStateFileMutation(error) && !isStateFileReadDrift(error)) {
|
|
751
|
+
throw error;
|
|
752
|
+
}
|
|
753
|
+
storageContention = true;
|
|
754
|
+
existing = committed;
|
|
755
|
+
}
|
|
756
|
+
if (created) {
|
|
757
|
+
return admissionWithinDeadline(
|
|
758
|
+
proposed,
|
|
759
|
+
environment,
|
|
760
|
+
options,
|
|
761
|
+
now,
|
|
762
|
+
expiresAt,
|
|
763
|
+
inspectOwner,
|
|
764
|
+
dependencies.afterReleaseContentionForTest,
|
|
765
|
+
dependencies.releaseMonotonicNowForTest,
|
|
766
|
+
dependencies.releaseWaitForTest,
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
if (existing === null) {
|
|
770
|
+
try {
|
|
771
|
+
dependencies.beforeContendedClaimReadForTest?.(slot);
|
|
772
|
+
existing = readClaim(slot, environment);
|
|
773
|
+
} catch (error) {
|
|
774
|
+
if (error instanceof BrowserAdmissionClaimError) {
|
|
775
|
+
// A competing malformed claim occupies the slot.
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
if (isStateFileReadDrift(error)) {
|
|
779
|
+
// The just-contended slot remains occupied until one stable read
|
|
780
|
+
// proves otherwise. Retry without weakening claim validation.
|
|
781
|
+
storageContention = true;
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
throw error;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
if (existing === null) {
|
|
789
|
+
stateChanged = true;
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
const status = exactOwnerStatus(existing.claim.owner, inspectOwner);
|
|
793
|
+
if (
|
|
794
|
+
existing.claim.owner.bootId === currentIdentity.bootId
|
|
795
|
+
|| status !== "different-or-dead"
|
|
796
|
+
) {
|
|
797
|
+
// Same-boot, exact-live, and unverifiable owners all retain their slot.
|
|
798
|
+
// A same-boot browser daemon can outlive its Ghostget owner process.
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
let removed: boolean;
|
|
802
|
+
try {
|
|
803
|
+
removed = removePrivateStateFileIfUnchanged(
|
|
804
|
+
claimPath(slot, environment),
|
|
805
|
+
{ expectedCurrentContentSha256: existing.contentSha256 },
|
|
806
|
+
environment,
|
|
807
|
+
);
|
|
808
|
+
} catch (error) {
|
|
809
|
+
if (!isActiveStateFileMutation(error)) throw error;
|
|
810
|
+
storageContention = true;
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
if (removed) {
|
|
814
|
+
stateChanged = true;
|
|
815
|
+
break;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
if (stateChanged && !storageContention) continue;
|
|
819
|
+
|
|
820
|
+
const remaining = remainingWait(options, now, expiresAt);
|
|
821
|
+
if (remaining < 1) throw new BrowserAdmissionError("timed-out");
|
|
822
|
+
const delay = Math.min(remaining, retryDelay(attempt, random));
|
|
823
|
+
attempt += 1;
|
|
824
|
+
try {
|
|
825
|
+
await sleep(delay, activeSignals(options));
|
|
826
|
+
} catch (error) {
|
|
827
|
+
options.deadline?.throwIfUnavailable("local browser admission");
|
|
828
|
+
if (activeSignals(options).some((signal) => signal.aborted)) {
|
|
829
|
+
throw new BrowserAdmissionError("cancelled");
|
|
830
|
+
}
|
|
831
|
+
throw error;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
export type BrowserCaptureAdmissionDependencies = {
|
|
837
|
+
readonly acquireAdmission?: typeof acquireBrowserAdmission;
|
|
838
|
+
readonly acquireBrowser?: typeof acquireKbBrowser;
|
|
839
|
+
readonly runCapture?: typeof runKbCapture;
|
|
840
|
+
readonly monotonicNow?: () => number;
|
|
841
|
+
readonly signal?: AbortSignal;
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* The slot remains held through settlement of the upstream browser, process,
|
|
846
|
+
* proxy, and isolation cleanup attempt.
|
|
847
|
+
* Attached browsers do not consume a locally owned browser slot.
|
|
848
|
+
*/
|
|
849
|
+
export async function acquireCaptureBrowserWithAdmission(
|
|
850
|
+
options: CaptureArguments,
|
|
851
|
+
temporaryDirectory: string,
|
|
852
|
+
useDiscoveredProfile: boolean,
|
|
853
|
+
environment: Environment = process.env,
|
|
854
|
+
dependencies: BrowserCaptureAdmissionDependencies = {},
|
|
855
|
+
): Promise<AcquiredPage> {
|
|
856
|
+
const acquireBrowser = dependencies.acquireBrowser ?? acquireKbBrowser;
|
|
857
|
+
if (options.browserLive || options.cdp !== undefined) {
|
|
858
|
+
return acquireBrowser(options, temporaryDirectory, useDiscoveredProfile);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
const monotonicNow = dependencies.monotonicNow ?? (() => performance.now());
|
|
862
|
+
const startedAt = validateMonotonicTime(monotonicNow());
|
|
863
|
+
const admission = await (
|
|
864
|
+
dependencies.acquireAdmission ?? acquireBrowserAdmission
|
|
865
|
+
)({
|
|
866
|
+
timeoutMs: options.timeoutMs,
|
|
867
|
+
environment,
|
|
868
|
+
...(dependencies.signal === undefined ? {} : { signal: dependencies.signal }),
|
|
869
|
+
});
|
|
870
|
+
let operation:
|
|
871
|
+
| { readonly ok: true; readonly value: AcquiredPage }
|
|
872
|
+
| { readonly ok: false; readonly error: unknown };
|
|
873
|
+
try {
|
|
874
|
+
if (dependencies.signal?.aborted === true) {
|
|
875
|
+
throw new BrowserAdmissionError("cancelled");
|
|
876
|
+
}
|
|
877
|
+
const elapsed = Math.max(
|
|
878
|
+
0,
|
|
879
|
+
Math.ceil(validateMonotonicTime(monotonicNow()) - startedAt),
|
|
880
|
+
);
|
|
881
|
+
const remainingTimeoutMs = options.timeoutMs - elapsed;
|
|
882
|
+
if (remainingTimeoutMs < 1) {
|
|
883
|
+
throw new BrowserAdmissionError("timed-out");
|
|
884
|
+
}
|
|
885
|
+
operation = {
|
|
886
|
+
ok: true,
|
|
887
|
+
value: await acquireBrowser(
|
|
888
|
+
{ ...options, timeoutMs: remainingTimeoutMs },
|
|
889
|
+
temporaryDirectory,
|
|
890
|
+
useDiscoveredProfile,
|
|
891
|
+
),
|
|
892
|
+
};
|
|
893
|
+
} catch (error) {
|
|
894
|
+
operation = { ok: false, error };
|
|
895
|
+
}
|
|
896
|
+
try {
|
|
897
|
+
admission.release();
|
|
898
|
+
} catch (releaseFailure) {
|
|
899
|
+
if (operation.ok) throw releaseFailure;
|
|
900
|
+
throw new AggregateError(
|
|
901
|
+
[operation.error, releaseFailure],
|
|
902
|
+
"browser acquisition failed and its admission could not be released",
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
if (!operation.ok) throw operation.error;
|
|
906
|
+
return operation.value;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
export async function runCaptureWithBrowserAdmission(
|
|
910
|
+
options: CaptureArguments,
|
|
911
|
+
environment: Environment = process.env,
|
|
912
|
+
dependencies: BrowserCaptureAdmissionDependencies = {},
|
|
913
|
+
): Promise<CaptureOutcome> {
|
|
914
|
+
const runCapture = dependencies.runCapture ?? runKbCapture;
|
|
915
|
+
return runCapture(options, {
|
|
916
|
+
acquireBrowser: (
|
|
917
|
+
browserOptions,
|
|
918
|
+
temporaryDirectory,
|
|
919
|
+
useDiscoveredProfile,
|
|
920
|
+
) => acquireCaptureBrowserWithAdmission(
|
|
921
|
+
browserOptions,
|
|
922
|
+
temporaryDirectory,
|
|
923
|
+
useDiscoveredProfile === true,
|
|
924
|
+
environment,
|
|
925
|
+
dependencies,
|
|
926
|
+
),
|
|
927
|
+
});
|
|
928
|
+
}
|