@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
package/src/auth.ts
ADDED
|
@@ -0,0 +1,1157 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
lstatSync,
|
|
5
|
+
realpathSync,
|
|
6
|
+
} from "node:fs";
|
|
7
|
+
import {
|
|
8
|
+
basename,
|
|
9
|
+
dirname,
|
|
10
|
+
isAbsolute,
|
|
11
|
+
join,
|
|
12
|
+
resolve,
|
|
13
|
+
} from "node:path";
|
|
14
|
+
|
|
15
|
+
import { cookieSources, type CookieSource } from "@hraness/kb/clip/args";
|
|
16
|
+
import { canonicalJson } from "./canonical-json";
|
|
17
|
+
import {
|
|
18
|
+
assertLinkedDeviceLifecycleAdmissionHeld,
|
|
19
|
+
linkedDeviceLifecycleAdmissionStore,
|
|
20
|
+
type LinkedDeviceLifecycleAdmission,
|
|
21
|
+
} from "./linked-device-lifecycle-admission";
|
|
22
|
+
import {
|
|
23
|
+
listLinkedDeviceLifecycleJournalSnapshots,
|
|
24
|
+
} from "./linked-device-lifecycle-journal";
|
|
25
|
+
import {
|
|
26
|
+
MAX_WRENCH_JSON_BYTES,
|
|
27
|
+
createPrivateJsonIfAbsent,
|
|
28
|
+
readPrivateStateFilesBatched,
|
|
29
|
+
readPrivateStateFileIfPresent,
|
|
30
|
+
removePrivateStateFileIfUnchanged,
|
|
31
|
+
ghostgetStateHome,
|
|
32
|
+
snapshotPrivateStateDirectory,
|
|
33
|
+
writePrivateJsonIfUnchanged,
|
|
34
|
+
} from "./storage";
|
|
35
|
+
import { removeSessionSecretsForAuth } from "./session-secrets";
|
|
36
|
+
import {
|
|
37
|
+
removeReadProjectionsForAuth,
|
|
38
|
+
removeReadProjectionAuthIncarnation,
|
|
39
|
+
rotateReadProjectionAuthIncarnation,
|
|
40
|
+
withReadProjectionAuthAdmission,
|
|
41
|
+
} from "./read-projections";
|
|
42
|
+
import {
|
|
43
|
+
isProviderPluginSurfaceId,
|
|
44
|
+
type ProviderPluginSurfaceId,
|
|
45
|
+
} from "./provider-plugin-identifiers";
|
|
46
|
+
export {
|
|
47
|
+
isGmailAccountSubject,
|
|
48
|
+
isLinkedInProviderActorSubject,
|
|
49
|
+
isLinkedInWebAccountSubject,
|
|
50
|
+
isXAccountSubject,
|
|
51
|
+
} from "./provider-subject";
|
|
52
|
+
|
|
53
|
+
export type OAuthProvider = ProviderPluginSurfaceId;
|
|
54
|
+
|
|
55
|
+
export type LinkedDeviceProvider = ProviderPluginSurfaceId;
|
|
56
|
+
|
|
57
|
+
export type GhostgetAuth =
|
|
58
|
+
| {
|
|
59
|
+
readonly schemaVersion: 1;
|
|
60
|
+
readonly id: string;
|
|
61
|
+
readonly kind: "cookie-source";
|
|
62
|
+
readonly source: CookieSource;
|
|
63
|
+
readonly profile?: string;
|
|
64
|
+
readonly subject?: string;
|
|
65
|
+
}
|
|
66
|
+
| {
|
|
67
|
+
readonly schemaVersion: 1;
|
|
68
|
+
readonly id: string;
|
|
69
|
+
readonly kind: "cookies-file";
|
|
70
|
+
readonly path: string;
|
|
71
|
+
readonly subject?: string;
|
|
72
|
+
}
|
|
73
|
+
| {
|
|
74
|
+
readonly schemaVersion: 1;
|
|
75
|
+
readonly id: string;
|
|
76
|
+
readonly kind: "browser-profile";
|
|
77
|
+
readonly profile: string;
|
|
78
|
+
readonly browserExecutable?: string;
|
|
79
|
+
readonly trustUnfilteredEgress: true;
|
|
80
|
+
readonly cookieSource?: CookieSource;
|
|
81
|
+
readonly cookieProfile?: string;
|
|
82
|
+
readonly subject?: string;
|
|
83
|
+
}
|
|
84
|
+
| {
|
|
85
|
+
readonly schemaVersion: 1;
|
|
86
|
+
readonly id: string;
|
|
87
|
+
readonly kind: "oauth-token-file";
|
|
88
|
+
readonly provider: OAuthProvider;
|
|
89
|
+
readonly path: string;
|
|
90
|
+
readonly scopes: readonly string[];
|
|
91
|
+
/** The credential lifecycle and token file are owned by Ghostget. */
|
|
92
|
+
readonly managed?: true;
|
|
93
|
+
readonly subject?: string;
|
|
94
|
+
}
|
|
95
|
+
| {
|
|
96
|
+
readonly schemaVersion: 1;
|
|
97
|
+
readonly id: string;
|
|
98
|
+
readonly kind: "linked-device-store";
|
|
99
|
+
readonly provider: LinkedDeviceProvider;
|
|
100
|
+
readonly path: string;
|
|
101
|
+
/** Stable, secret-free lifecycle serialization coordinate. */
|
|
102
|
+
readonly realmKey?: string;
|
|
103
|
+
readonly subject?: string;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export type AuthInput =
|
|
107
|
+
| { readonly source: CookieSource; readonly profile?: string; readonly subject?: string }
|
|
108
|
+
| { readonly cookiesFile: string; readonly subject?: string }
|
|
109
|
+
| {
|
|
110
|
+
readonly browserProfile: string;
|
|
111
|
+
readonly browserExecutable?: string;
|
|
112
|
+
readonly trustUnfilteredEgress: boolean;
|
|
113
|
+
readonly cookieSource?: CookieSource;
|
|
114
|
+
readonly cookieProfile?: string;
|
|
115
|
+
readonly subject?: string;
|
|
116
|
+
}
|
|
117
|
+
| {
|
|
118
|
+
readonly oauthProvider: OAuthProvider;
|
|
119
|
+
readonly tokenFile: string;
|
|
120
|
+
readonly scopes: readonly string[];
|
|
121
|
+
readonly managed?: true;
|
|
122
|
+
readonly subject?: string;
|
|
123
|
+
}
|
|
124
|
+
| {
|
|
125
|
+
readonly linkedDeviceProvider: LinkedDeviceProvider;
|
|
126
|
+
readonly deviceStore: string;
|
|
127
|
+
readonly subject?: string;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export type AuthSnapshot = {
|
|
131
|
+
readonly auth: GhostgetAuth;
|
|
132
|
+
/** SHA-256 of the exact canonical file bytes, including the final LF. */
|
|
133
|
+
readonly contentSha256: string;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export type AuthReplaceResult =
|
|
137
|
+
| {
|
|
138
|
+
readonly replaced: true;
|
|
139
|
+
readonly snapshot: AuthSnapshot;
|
|
140
|
+
}
|
|
141
|
+
| { readonly replaced: false };
|
|
142
|
+
|
|
143
|
+
export type AuthReplaceAuthority = {
|
|
144
|
+
readonly lifecycleAdmission: LinkedDeviceLifecycleAdmission;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
type LinkedDeviceAuth = Extract<
|
|
148
|
+
GhostgetAuth,
|
|
149
|
+
{ readonly kind: "linked-device-store" }
|
|
150
|
+
>;
|
|
151
|
+
|
|
152
|
+
function authPath(id: string, environment: Readonly<Record<string, string | undefined>>): string {
|
|
153
|
+
if (!/^[a-z][a-z0-9-]{0,47}$/u.test(id)) throw new Error("auth ID must be lowercase kebab-case");
|
|
154
|
+
return join(ghostgetStateHome(environment), "auth", `${id}.json`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Resolve an existing store to its physical path. A store that has not been
|
|
159
|
+
* created yet is bound to the real nearest ancestor plus its missing suffix,
|
|
160
|
+
* so two locator aliases cannot acquire different lifecycle coordinates.
|
|
161
|
+
*/
|
|
162
|
+
export function canonicalLinkedDeviceStorePath(pathValue: string): string {
|
|
163
|
+
const suffix: string[] = [];
|
|
164
|
+
let ancestor = resolve(pathValue);
|
|
165
|
+
while (!existsSync(ancestor)) {
|
|
166
|
+
try {
|
|
167
|
+
if (lstatSync(ancestor).isSymbolicLink()) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
"linked-device store path contains an unresolved symbolic link",
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
} catch (error) {
|
|
173
|
+
if (
|
|
174
|
+
typeof error !== "object"
|
|
175
|
+
|| error === null
|
|
176
|
+
|| !("code" in error)
|
|
177
|
+
|| error.code !== "ENOENT"
|
|
178
|
+
) {
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const parent = dirname(ancestor);
|
|
183
|
+
if (parent === ancestor) {
|
|
184
|
+
throw new Error("linked-device store path has no existing ancestor");
|
|
185
|
+
}
|
|
186
|
+
suffix.unshift(ancestor.slice(parent.length + (
|
|
187
|
+
parent.endsWith("/") ? 0 : 1
|
|
188
|
+
)));
|
|
189
|
+
ancestor = parent;
|
|
190
|
+
}
|
|
191
|
+
return resolve(realpathSync(ancestor), ...suffix);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Secret-free, auth-ID-independent serialization identity for one physical
|
|
196
|
+
* linked-device provider realm.
|
|
197
|
+
*/
|
|
198
|
+
export function linkedDeviceRealmKey(auth: LinkedDeviceAuth): string {
|
|
199
|
+
const canonicalPath = canonicalLinkedDeviceStorePath(auth.path);
|
|
200
|
+
if (
|
|
201
|
+
auth.realmKey !== undefined
|
|
202
|
+
&& canonicalPath !== auth.path
|
|
203
|
+
) {
|
|
204
|
+
throw new Error(
|
|
205
|
+
"linked-device realm key requires a persisted canonical store path",
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
const expected = deriveLinkedDeviceRealmKey(
|
|
209
|
+
auth.provider,
|
|
210
|
+
canonicalPath,
|
|
211
|
+
);
|
|
212
|
+
if (auth.realmKey !== undefined) {
|
|
213
|
+
if (!/^[a-f0-9]{64}$/u.test(auth.realmKey)) {
|
|
214
|
+
throw new Error("linked-device auth has a malformed stable realm key");
|
|
215
|
+
}
|
|
216
|
+
if (auth.realmKey !== expected) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
"linked-device auth realm key does not match its provider and canonical store path",
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return expected;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function deriveLinkedDeviceRealmKey(
|
|
226
|
+
provider: LinkedDeviceProvider,
|
|
227
|
+
canonicalStorePath: string,
|
|
228
|
+
): string {
|
|
229
|
+
return createHash("sha256")
|
|
230
|
+
.update("io-linked-device-realm-v1\0", "utf8")
|
|
231
|
+
.update(canonicalJson({
|
|
232
|
+
kind: "linked-device-store",
|
|
233
|
+
provider,
|
|
234
|
+
// Linked-device auth records persist only canonical physical locators.
|
|
235
|
+
// Hashing those stored bytes keeps this coordinate stable even if the
|
|
236
|
+
// named directory is later replaced or an old alias is retargeted.
|
|
237
|
+
storePath: canonicalStorePath,
|
|
238
|
+
}), "utf8")
|
|
239
|
+
.digest("hex");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function createAuth(id: string, input: AuthInput): GhostgetAuth {
|
|
243
|
+
if (!/^[a-z][a-z0-9-]{0,47}$/u.test(id)) throw new Error("auth ID must be lowercase kebab-case");
|
|
244
|
+
const subject = input.subject === undefined ? undefined : normalizeAuthSubject(input.subject);
|
|
245
|
+
if ("linkedDeviceProvider" in input) {
|
|
246
|
+
if (!isProviderPluginSurfaceId(input.linkedDeviceProvider)) {
|
|
247
|
+
throw new Error("linked-device auth has an invalid provider");
|
|
248
|
+
}
|
|
249
|
+
if (!isSafeAuthPath(input.deviceStore)) {
|
|
250
|
+
throw new Error("linked-device auth has an invalid store path");
|
|
251
|
+
}
|
|
252
|
+
const path = canonicalLinkedDeviceStorePath(input.deviceStore);
|
|
253
|
+
return {
|
|
254
|
+
schemaVersion: 1,
|
|
255
|
+
id,
|
|
256
|
+
kind: "linked-device-store",
|
|
257
|
+
provider: input.linkedDeviceProvider,
|
|
258
|
+
path,
|
|
259
|
+
realmKey: deriveLinkedDeviceRealmKey(
|
|
260
|
+
input.linkedDeviceProvider,
|
|
261
|
+
path,
|
|
262
|
+
),
|
|
263
|
+
...(subject === undefined ? {} : { subject }),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
if ("oauthProvider" in input) {
|
|
267
|
+
if (!isProviderPluginSurfaceId(input.oauthProvider)) {
|
|
268
|
+
throw new Error("OAuth token-file auth has an invalid provider");
|
|
269
|
+
}
|
|
270
|
+
if (!isSafeOAuthTokenPath(input.tokenFile)) throw new Error("OAuth token-file auth has an invalid path");
|
|
271
|
+
return {
|
|
272
|
+
schemaVersion: 1,
|
|
273
|
+
id,
|
|
274
|
+
kind: "oauth-token-file",
|
|
275
|
+
provider: input.oauthProvider,
|
|
276
|
+
path: resolve(input.tokenFile),
|
|
277
|
+
scopes: normalizeOAuthScopes(input.scopes),
|
|
278
|
+
...(input.managed === true ? { managed: true as const } : {}),
|
|
279
|
+
...(subject === undefined ? {} : { subject }),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
if ("browserProfile" in input) {
|
|
283
|
+
if (!input.trustUnfilteredEgress) {
|
|
284
|
+
throw new Error("browser profiles require --trust-profile-egress because profile sessions cannot use agent-browser domain containment");
|
|
285
|
+
}
|
|
286
|
+
if (!isSafeString(input.browserProfile, 4_096) || !isSafeBrowserProfile(input.browserProfile)) {
|
|
287
|
+
throw new Error("browser profile is invalid or unsafe");
|
|
288
|
+
}
|
|
289
|
+
if (
|
|
290
|
+
input.browserExecutable !== undefined
|
|
291
|
+
&& (!isSafeAuthPath(input.browserExecutable) || input.browserExecutable.trim() !== input.browserExecutable)
|
|
292
|
+
) {
|
|
293
|
+
throw new Error("browser executable is invalid or unsafe");
|
|
294
|
+
}
|
|
295
|
+
if (input.cookieSource !== undefined && !cookieSources.includes(input.cookieSource)) {
|
|
296
|
+
throw new Error("browser-profile auth has an invalid cookie source");
|
|
297
|
+
}
|
|
298
|
+
if (input.cookieProfile !== undefined && (input.cookieSource === undefined || !isSafeString(input.cookieProfile, 4_096))) {
|
|
299
|
+
throw new Error("browser-profile cookie profile requires a valid cookie source");
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
schemaVersion: 1,
|
|
303
|
+
id,
|
|
304
|
+
kind: "browser-profile",
|
|
305
|
+
profile: browserProfileLocator(input.browserProfile),
|
|
306
|
+
...(input.browserExecutable === undefined
|
|
307
|
+
? {}
|
|
308
|
+
: { browserExecutable: resolve(input.browserExecutable) }),
|
|
309
|
+
trustUnfilteredEgress: true,
|
|
310
|
+
...(input.cookieSource === undefined ? {} : { cookieSource: input.cookieSource }),
|
|
311
|
+
...(input.cookieProfile === undefined ? {} : { cookieProfile: input.cookieProfile }),
|
|
312
|
+
...(subject === undefined ? {} : { subject }),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
if ("source" in input) {
|
|
316
|
+
if (!cookieSources.includes(input.source) || (input.profile !== undefined && !isSafeString(input.profile, 4_096))) {
|
|
317
|
+
throw new Error("cookie-source auth has an invalid source or profile");
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
schemaVersion: 1,
|
|
321
|
+
id,
|
|
322
|
+
kind: "cookie-source",
|
|
323
|
+
source: input.source,
|
|
324
|
+
...(input.profile === undefined ? {} : { profile: browserProfileLocator(input.profile) }),
|
|
325
|
+
...(subject === undefined ? {} : { subject }),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
if ("cookiesFile" in input) {
|
|
329
|
+
if (!isSafeString(input.cookiesFile, 4_096)) throw new Error("cookies-file auth has an invalid path");
|
|
330
|
+
return {
|
|
331
|
+
schemaVersion: 1,
|
|
332
|
+
id,
|
|
333
|
+
kind: "cookies-file",
|
|
334
|
+
path: resolve(input.cookiesFile),
|
|
335
|
+
...(subject === undefined ? {} : { subject }),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
throw new Error("auth locator kind is not supported");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function saveAuth(
|
|
342
|
+
authValue: GhostgetAuth,
|
|
343
|
+
environment: Readonly<Record<string, string | undefined>> = process.env,
|
|
344
|
+
options: { readonly force?: boolean } = {},
|
|
345
|
+
): string {
|
|
346
|
+
const replacement = canonicalAuthSnapshot(authValue);
|
|
347
|
+
const auth = replacement.auth;
|
|
348
|
+
const path = authPath(auth.id, environment);
|
|
349
|
+
let current: AuthSnapshot | null;
|
|
350
|
+
try {
|
|
351
|
+
current = loadAuthSnapshotIfPresent(auth.id, environment);
|
|
352
|
+
} catch {
|
|
353
|
+
throw new Error(`auth locator ${auth.id} already exists but is invalid; inspect or remove it before replacement`);
|
|
354
|
+
}
|
|
355
|
+
if (
|
|
356
|
+
current !== null
|
|
357
|
+
&& options.force !== true
|
|
358
|
+
&& current.contentSha256 !== replacement.contentSha256
|
|
359
|
+
) {
|
|
360
|
+
throw new Error(
|
|
361
|
+
`auth locator ${auth.id} already names a different access realm; use a new ID or repeat with --force after reviewing cache and action identity`,
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
if (
|
|
365
|
+
current !== null
|
|
366
|
+
&& current.contentSha256 === replacement.contentSha256
|
|
367
|
+
) return path;
|
|
368
|
+
|
|
369
|
+
return withLinkedDeviceAuthMutationAdmissions(
|
|
370
|
+
current?.auth,
|
|
371
|
+
auth,
|
|
372
|
+
environment,
|
|
373
|
+
() => withReadProjectionAuthAdmission(auth.id, environment, () => {
|
|
374
|
+
const observed = loadAuthSnapshotIfPresent(auth.id, environment);
|
|
375
|
+
if (current === null) {
|
|
376
|
+
if (observed !== null) {
|
|
377
|
+
throw new Error(
|
|
378
|
+
`auth locator ${auth.id} changed concurrently before creation`,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
rotateReadProjectionAuthIncarnation(auth.id, environment);
|
|
382
|
+
removePrivateAuthState(auth.id, environment);
|
|
383
|
+
if (!createPrivateJsonIfAbsent(
|
|
384
|
+
path,
|
|
385
|
+
auth,
|
|
386
|
+
{ privateParent: true, environment },
|
|
387
|
+
).created) {
|
|
388
|
+
throw new Error(
|
|
389
|
+
`auth locator ${auth.id} changed concurrently before creation`,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
return path;
|
|
393
|
+
}
|
|
394
|
+
if (
|
|
395
|
+
observed === null
|
|
396
|
+
|| observed.contentSha256 !== current.contentSha256
|
|
397
|
+
) {
|
|
398
|
+
throw new Error(
|
|
399
|
+
`auth locator ${auth.id} changed concurrently before replacement`,
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
rotateReadProjectionAuthIncarnation(auth.id, environment);
|
|
403
|
+
removePrivateAuthState(auth.id, environment);
|
|
404
|
+
if (!writePrivateJsonIfUnchanged(
|
|
405
|
+
path,
|
|
406
|
+
auth,
|
|
407
|
+
{ expectedCurrentContentSha256: current.contentSha256 },
|
|
408
|
+
)) {
|
|
409
|
+
throw new Error(
|
|
410
|
+
`auth locator ${auth.id} changed concurrently before replacement`,
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
return path;
|
|
414
|
+
}),
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function isSafeString(value: unknown, maximum: number): value is string {
|
|
419
|
+
if (typeof value !== "string" || value.length < 1 || value.length > maximum) return false;
|
|
420
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
421
|
+
const code = value.charCodeAt(index);
|
|
422
|
+
if (code <= 0x1f || code === 0x7f) return false;
|
|
423
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
424
|
+
const next = value.charCodeAt(index + 1);
|
|
425
|
+
if (!(next >= 0xdc00 && next <= 0xdfff)) return false;
|
|
426
|
+
index += 1;
|
|
427
|
+
} else if (code >= 0xdc00 && code <= 0xdfff) return false;
|
|
428
|
+
}
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export function normalizeAuthSubject(value: string): string {
|
|
433
|
+
if (
|
|
434
|
+
!isSafeString(value, 512)
|
|
435
|
+
|| value.trim() !== value
|
|
436
|
+
|| !/^[A-Za-z0-9.!#$%&'*+/=?^_`{|}~:@-]{1,512}$/u.test(value)
|
|
437
|
+
) throw new Error("auth locator has an invalid subject");
|
|
438
|
+
return value;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export const normalizeOAuthSubject = normalizeAuthSubject;
|
|
442
|
+
|
|
443
|
+
function isSafeAuthPath(value: unknown): value is string {
|
|
444
|
+
if (!isSafeString(value, 4_096)) return false;
|
|
445
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
446
|
+
const code = value.charCodeAt(index);
|
|
447
|
+
if (
|
|
448
|
+
(code >= 0x80 && code <= 0x9f)
|
|
449
|
+
|| code === 0x061c
|
|
450
|
+
|| code === 0x200e
|
|
451
|
+
|| code === 0x200f
|
|
452
|
+
|| (code >= 0x2028 && code <= 0x202e)
|
|
453
|
+
|| (code >= 0x2066 && code <= 0x2069)
|
|
454
|
+
) return false;
|
|
455
|
+
}
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const isSafeOAuthTokenPath = isSafeAuthPath;
|
|
460
|
+
|
|
461
|
+
function isCanonicalHttpsOAuthScope(value: string): boolean {
|
|
462
|
+
if (value === "https://mail.google.com/") return true;
|
|
463
|
+
let url: URL;
|
|
464
|
+
try {
|
|
465
|
+
url = new URL(value);
|
|
466
|
+
} catch {
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
return url.protocol === "https:"
|
|
470
|
+
&& url.username === ""
|
|
471
|
+
&& url.password === ""
|
|
472
|
+
&& url.port === ""
|
|
473
|
+
&& url.search === ""
|
|
474
|
+
&& url.hash === ""
|
|
475
|
+
&& url.pathname !== "/"
|
|
476
|
+
&& url.href === value
|
|
477
|
+
&& /^[a-z0-9](?:[a-z0-9.-]{0,251}[a-z0-9])?$/u.test(url.hostname)
|
|
478
|
+
&& url.hostname.includes(".")
|
|
479
|
+
&& /^\/[A-Za-z0-9._~!$&'()*+,;=:@/-]+$/u.test(url.pathname);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export function normalizeOAuthScopes(values: readonly string[]): readonly string[] {
|
|
483
|
+
if (!Array.isArray(values) || values.length < 1 || values.length > 64) {
|
|
484
|
+
throw new Error("OAuth token-file auth requires between 1 and 64 scopes");
|
|
485
|
+
}
|
|
486
|
+
const scopes = values.map((value) => {
|
|
487
|
+
if (
|
|
488
|
+
!isSafeString(value, 128)
|
|
489
|
+
|| value.trim() !== value
|
|
490
|
+
|| (
|
|
491
|
+
!/^[A-Za-z][A-Za-z0-9._:-]{0,127}$/u.test(value)
|
|
492
|
+
&& !isCanonicalHttpsOAuthScope(value)
|
|
493
|
+
)
|
|
494
|
+
) {
|
|
495
|
+
throw new Error(
|
|
496
|
+
"OAuth scopes must be provider scope names or canonical HTTPS scope URLs without whitespace or control characters",
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
return value;
|
|
500
|
+
});
|
|
501
|
+
if (new Set(scopes).size !== scopes.length) throw new Error("OAuth scopes must not contain duplicates");
|
|
502
|
+
return [...scopes].sort();
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function isSafeBrowserProfile(value: string): boolean {
|
|
506
|
+
const named = !value.includes("/") && !value.includes("\\");
|
|
507
|
+
if (!named) return true;
|
|
508
|
+
if (value.length > 256 || value.trim() !== value || value.startsWith("-") || value === "." || value === "..") return false;
|
|
509
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
510
|
+
const code = value.charCodeAt(index);
|
|
511
|
+
if (
|
|
512
|
+
(code >= 0x80 && code <= 0x9f)
|
|
513
|
+
|| code === 0x061c
|
|
514
|
+
|| code === 0x200e
|
|
515
|
+
|| code === 0x200f
|
|
516
|
+
|| (code >= 0x2028 && code <= 0x202e)
|
|
517
|
+
|| (code >= 0x2066 && code <= 0x2069)
|
|
518
|
+
) return false;
|
|
519
|
+
}
|
|
520
|
+
return true;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function browserProfileLocator(value: string): string {
|
|
524
|
+
const pathLike = isAbsolute(value)
|
|
525
|
+
|| value.startsWith("./")
|
|
526
|
+
|| value.startsWith("../")
|
|
527
|
+
|| value.includes("/")
|
|
528
|
+
|| value.includes("\\");
|
|
529
|
+
return pathLike ? resolve(value) : value;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function exactKeys(value: Record<string, unknown>, expected: readonly string[]): boolean {
|
|
533
|
+
const keys = Object.keys(value).sort();
|
|
534
|
+
const allowed = [...expected].sort();
|
|
535
|
+
return keys.length === allowed.length && keys.every((key, index) => key === allowed[index]);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export function parseAuth(value: unknown): GhostgetAuth {
|
|
539
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("auth record must be an object");
|
|
540
|
+
const record = value as Record<string, unknown>;
|
|
541
|
+
if (record.schemaVersion !== 1 || !isSafeString(record.id, 48) || !/^[a-z][a-z0-9-]*$/u.test(record.id)) {
|
|
542
|
+
throw new Error("auth record has an invalid schema version or ID");
|
|
543
|
+
}
|
|
544
|
+
if (record.kind === "cookie-source") {
|
|
545
|
+
const expected = ["schemaVersion", "id", "kind", "source"];
|
|
546
|
+
if (record.profile !== undefined) expected.push("profile");
|
|
547
|
+
if (record.subject !== undefined) expected.push("subject");
|
|
548
|
+
if (!exactKeys(record, expected)) throw new Error("auth record has unsupported fields");
|
|
549
|
+
if (typeof record.source !== "string" || !cookieSources.includes(record.source as CookieSource)) {
|
|
550
|
+
throw new Error("auth record has an invalid cookie source");
|
|
551
|
+
}
|
|
552
|
+
if (record.profile !== undefined && !isSafeString(record.profile, 4_096)) throw new Error("auth record has an invalid profile");
|
|
553
|
+
if (
|
|
554
|
+
typeof record.profile === "string"
|
|
555
|
+
&& (record.profile.includes("/") || record.profile.includes("\\"))
|
|
556
|
+
&& !isAbsolute(record.profile)
|
|
557
|
+
) throw new Error("auth record cookie profile paths must be absolute");
|
|
558
|
+
if (record.subject !== undefined && typeof record.subject !== "string") {
|
|
559
|
+
throw new Error("auth record has an invalid subject");
|
|
560
|
+
}
|
|
561
|
+
const subject = typeof record.subject === "string" ? normalizeAuthSubject(record.subject) : undefined;
|
|
562
|
+
return {
|
|
563
|
+
schemaVersion: 1,
|
|
564
|
+
id: record.id,
|
|
565
|
+
kind: "cookie-source",
|
|
566
|
+
source: record.source as CookieSource,
|
|
567
|
+
...(typeof record.profile !== "string" ? {} : { profile: record.profile }),
|
|
568
|
+
...(subject === undefined ? {} : { subject }),
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
if (record.kind === "cookies-file") {
|
|
572
|
+
const expected = ["schemaVersion", "id", "kind", "path"];
|
|
573
|
+
if (record.subject !== undefined) expected.push("subject");
|
|
574
|
+
if (!exactKeys(record, expected)) throw new Error("auth record has unsupported fields");
|
|
575
|
+
if (!isSafeString(record.path, 4_096) || !isAbsolute(record.path)) throw new Error("auth record has an invalid or non-absolute cookie file path");
|
|
576
|
+
if (record.subject !== undefined && typeof record.subject !== "string") {
|
|
577
|
+
throw new Error("auth record has an invalid subject");
|
|
578
|
+
}
|
|
579
|
+
const subject = typeof record.subject === "string" ? normalizeAuthSubject(record.subject) : undefined;
|
|
580
|
+
return {
|
|
581
|
+
schemaVersion: 1,
|
|
582
|
+
id: record.id,
|
|
583
|
+
kind: "cookies-file",
|
|
584
|
+
path: record.path,
|
|
585
|
+
...(subject === undefined ? {} : { subject }),
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
if (record.kind === "browser-profile") {
|
|
589
|
+
const expected = ["schemaVersion", "id", "kind", "profile", "trustUnfilteredEgress"];
|
|
590
|
+
if (record.browserExecutable !== undefined) expected.push("browserExecutable");
|
|
591
|
+
if (record.cookieSource !== undefined) expected.push("cookieSource");
|
|
592
|
+
if (record.cookieProfile !== undefined) expected.push("cookieProfile");
|
|
593
|
+
if (record.subject !== undefined) expected.push("subject");
|
|
594
|
+
if (!exactKeys(record, expected)) throw new Error("auth record has unsupported fields");
|
|
595
|
+
if (!isSafeString(record.profile, 4_096) || !isSafeBrowserProfile(record.profile) || record.trustUnfilteredEgress !== true) {
|
|
596
|
+
throw new Error("auth record has an invalid browser profile or trust acknowledgement");
|
|
597
|
+
}
|
|
598
|
+
if ((record.profile.includes("/") || record.profile.includes("\\")) && !isAbsolute(record.profile)) {
|
|
599
|
+
throw new Error("auth record browser profile paths must be absolute");
|
|
600
|
+
}
|
|
601
|
+
if (
|
|
602
|
+
record.browserExecutable !== undefined
|
|
603
|
+
&& (
|
|
604
|
+
!isSafeAuthPath(record.browserExecutable)
|
|
605
|
+
|| !isAbsolute(record.browserExecutable)
|
|
606
|
+
|| record.browserExecutable.trim() !== record.browserExecutable
|
|
607
|
+
)
|
|
608
|
+
) throw new Error("auth record has an invalid browser executable");
|
|
609
|
+
if (
|
|
610
|
+
record.cookieSource !== undefined
|
|
611
|
+
&& (typeof record.cookieSource !== "string" || !cookieSources.includes(record.cookieSource as CookieSource))
|
|
612
|
+
) throw new Error("auth record has an invalid browser-profile cookie source");
|
|
613
|
+
if (
|
|
614
|
+
record.cookieProfile !== undefined
|
|
615
|
+
&& (record.cookieSource === undefined || !isSafeString(record.cookieProfile, 4_096))
|
|
616
|
+
) throw new Error("auth record has an invalid browser-profile cookie profile");
|
|
617
|
+
if (record.subject !== undefined && typeof record.subject !== "string") {
|
|
618
|
+
throw new Error("auth record has an invalid subject");
|
|
619
|
+
}
|
|
620
|
+
const subject = typeof record.subject === "string" ? normalizeAuthSubject(record.subject) : undefined;
|
|
621
|
+
return {
|
|
622
|
+
schemaVersion: 1,
|
|
623
|
+
id: record.id,
|
|
624
|
+
kind: "browser-profile",
|
|
625
|
+
profile: record.profile,
|
|
626
|
+
...(typeof record.browserExecutable !== "string"
|
|
627
|
+
? {}
|
|
628
|
+
: { browserExecutable: record.browserExecutable }),
|
|
629
|
+
trustUnfilteredEgress: true,
|
|
630
|
+
...(typeof record.cookieSource !== "string" ? {} : { cookieSource: record.cookieSource as CookieSource }),
|
|
631
|
+
...(typeof record.cookieProfile !== "string" ? {} : { cookieProfile: record.cookieProfile }),
|
|
632
|
+
...(subject === undefined ? {} : { subject }),
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
if (record.kind === "oauth-token-file") {
|
|
636
|
+
const expected = ["schemaVersion", "id", "kind", "provider", "path", "scopes"];
|
|
637
|
+
if (record.managed !== undefined) expected.push("managed");
|
|
638
|
+
if (record.subject !== undefined) expected.push("subject");
|
|
639
|
+
if (!exactKeys(record, expected)) throw new Error("auth record has unsupported fields");
|
|
640
|
+
if (!isProviderPluginSurfaceId(record.provider)) {
|
|
641
|
+
throw new Error("auth record has an invalid OAuth provider");
|
|
642
|
+
}
|
|
643
|
+
if (!isSafeOAuthTokenPath(record.path) || !isAbsolute(record.path)) {
|
|
644
|
+
throw new Error("auth record has an invalid or non-absolute OAuth token file path");
|
|
645
|
+
}
|
|
646
|
+
if (record.managed !== undefined && record.managed !== true) {
|
|
647
|
+
throw new Error("auth record has an invalid managed OAuth lifecycle marker");
|
|
648
|
+
}
|
|
649
|
+
const rawScopes = record.scopes;
|
|
650
|
+
if (!Array.isArray(rawScopes) || !rawScopes.every((scope) => typeof scope === "string")) {
|
|
651
|
+
throw new Error("auth record has invalid OAuth scopes");
|
|
652
|
+
}
|
|
653
|
+
const scopes = normalizeOAuthScopes(rawScopes);
|
|
654
|
+
if (scopes.some((scope, index) => scope !== rawScopes[index])) {
|
|
655
|
+
throw new Error("auth record OAuth scopes must be sorted");
|
|
656
|
+
}
|
|
657
|
+
if (record.subject !== undefined && typeof record.subject !== "string") {
|
|
658
|
+
throw new Error("auth record has an invalid OAuth subject");
|
|
659
|
+
}
|
|
660
|
+
const subject = typeof record.subject === "string" ? normalizeAuthSubject(record.subject) : undefined;
|
|
661
|
+
return {
|
|
662
|
+
schemaVersion: 1,
|
|
663
|
+
id: record.id,
|
|
664
|
+
kind: "oauth-token-file",
|
|
665
|
+
provider: record.provider,
|
|
666
|
+
path: record.path,
|
|
667
|
+
scopes,
|
|
668
|
+
...(record.managed === true ? { managed: true as const } : {}),
|
|
669
|
+
...(subject === undefined ? {} : { subject }),
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
if (record.kind === "linked-device-store") {
|
|
673
|
+
const expected = ["schemaVersion", "id", "kind", "provider", "path"];
|
|
674
|
+
if (record.realmKey !== undefined) expected.push("realmKey");
|
|
675
|
+
if (record.subject !== undefined) expected.push("subject");
|
|
676
|
+
if (!exactKeys(record, expected)) throw new Error("auth record has unsupported fields");
|
|
677
|
+
if (!isProviderPluginSurfaceId(record.provider)) {
|
|
678
|
+
throw new Error("auth record has an invalid linked-device provider");
|
|
679
|
+
}
|
|
680
|
+
if (!isSafeAuthPath(record.path) || !isAbsolute(record.path)) {
|
|
681
|
+
throw new Error("auth record has an invalid or non-absolute linked-device store path");
|
|
682
|
+
}
|
|
683
|
+
const canonicalPath = canonicalLinkedDeviceStorePath(record.path);
|
|
684
|
+
if (
|
|
685
|
+
record.realmKey !== undefined
|
|
686
|
+
&& canonicalPath !== record.path
|
|
687
|
+
) {
|
|
688
|
+
throw new Error(
|
|
689
|
+
"auth record linked-device store path must be canonical",
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
if (
|
|
693
|
+
record.realmKey !== undefined
|
|
694
|
+
&& (
|
|
695
|
+
typeof record.realmKey !== "string"
|
|
696
|
+
|| !/^[a-f0-9]{64}$/u.test(record.realmKey)
|
|
697
|
+
)
|
|
698
|
+
) {
|
|
699
|
+
throw new Error("auth record has an invalid linked-device realm key");
|
|
700
|
+
}
|
|
701
|
+
const realmKey = deriveLinkedDeviceRealmKey(
|
|
702
|
+
record.provider,
|
|
703
|
+
canonicalPath,
|
|
704
|
+
);
|
|
705
|
+
if (
|
|
706
|
+
typeof record.realmKey === "string"
|
|
707
|
+
&& record.realmKey !== realmKey
|
|
708
|
+
) {
|
|
709
|
+
throw new Error(
|
|
710
|
+
"auth record linked-device realm key does not match its provider and canonical store path",
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
if (record.subject !== undefined && typeof record.subject !== "string") {
|
|
714
|
+
throw new Error("auth record has an invalid linked-device subject");
|
|
715
|
+
}
|
|
716
|
+
const subject = typeof record.subject === "string"
|
|
717
|
+
? normalizeAuthSubject(record.subject)
|
|
718
|
+
: undefined;
|
|
719
|
+
return {
|
|
720
|
+
schemaVersion: 1,
|
|
721
|
+
id: record.id,
|
|
722
|
+
kind: "linked-device-store",
|
|
723
|
+
provider: record.provider,
|
|
724
|
+
path: record.path,
|
|
725
|
+
...(record.realmKey === undefined ? {} : { realmKey }),
|
|
726
|
+
...(subject === undefined ? {} : { subject }),
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
throw new Error("auth record kind is not supported");
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function canonicalAuthSnapshot(authValue: unknown): AuthSnapshot {
|
|
733
|
+
const parsed = parseAuth(authValue);
|
|
734
|
+
const auth: GhostgetAuth = parsed.kind === "oauth-token-file"
|
|
735
|
+
? Object.freeze({
|
|
736
|
+
...parsed,
|
|
737
|
+
scopes: Object.freeze([...parsed.scopes]),
|
|
738
|
+
})
|
|
739
|
+
: Object.freeze({ ...parsed });
|
|
740
|
+
const content = `${canonicalJson(auth)}\n`;
|
|
741
|
+
return Object.freeze({
|
|
742
|
+
auth,
|
|
743
|
+
contentSha256: createHash("sha256").update(content, "utf8").digest("hex"),
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function parseAuthSnapshotText(
|
|
748
|
+
id: string,
|
|
749
|
+
content: string,
|
|
750
|
+
): AuthSnapshot {
|
|
751
|
+
let value: unknown;
|
|
752
|
+
try {
|
|
753
|
+
value = JSON.parse(content) as unknown;
|
|
754
|
+
} catch {
|
|
755
|
+
throw new Error("auth record is malformed JSON");
|
|
756
|
+
}
|
|
757
|
+
const snapshot = canonicalAuthSnapshot(value);
|
|
758
|
+
if (snapshot.auth.id !== id) {
|
|
759
|
+
throw new Error("auth record ID does not match its filename");
|
|
760
|
+
}
|
|
761
|
+
if (content !== `${canonicalJson(snapshot.auth)}\n`) {
|
|
762
|
+
throw new Error("auth record is not canonical JSON");
|
|
763
|
+
}
|
|
764
|
+
return Object.freeze({
|
|
765
|
+
auth: snapshot.auth,
|
|
766
|
+
contentSha256: createHash("sha256")
|
|
767
|
+
.update(content, "utf8")
|
|
768
|
+
.digest("hex"),
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
export function loadAuthSnapshotIfPresent(
|
|
773
|
+
id: string,
|
|
774
|
+
environment: Readonly<Record<string, string | undefined>>,
|
|
775
|
+
): AuthSnapshot | null {
|
|
776
|
+
const content = readPrivateStateFileIfPresent(
|
|
777
|
+
authPath(id, environment),
|
|
778
|
+
MAX_WRENCH_JSON_BYTES,
|
|
779
|
+
"auth record",
|
|
780
|
+
environment,
|
|
781
|
+
);
|
|
782
|
+
return content === null ? null : parseAuthSnapshotText(id, content);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function linkedDeviceMutationRealms(
|
|
786
|
+
current: GhostgetAuth | undefined,
|
|
787
|
+
replacement: GhostgetAuth | undefined,
|
|
788
|
+
authIds: ReadonlySet<string>,
|
|
789
|
+
journals: ReturnType<typeof listLinkedDeviceLifecycleJournalSnapshots>,
|
|
790
|
+
): readonly {
|
|
791
|
+
readonly key: string;
|
|
792
|
+
readonly authId: string;
|
|
793
|
+
}[] {
|
|
794
|
+
const byKey = new Map<string, string>();
|
|
795
|
+
for (const auth of [current, replacement]) {
|
|
796
|
+
if (auth?.kind !== "linked-device-store") continue;
|
|
797
|
+
const key = linkedDeviceRealmKey(auth);
|
|
798
|
+
const existing = byKey.get(key);
|
|
799
|
+
if (existing === undefined || auth.id < existing) {
|
|
800
|
+
byKey.set(key, auth.id);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
for (const entry of journals) {
|
|
804
|
+
if ("invalid" in entry) {
|
|
805
|
+
throw new Error(
|
|
806
|
+
"linked-device lifecycle journals contain invalid state; auth mutation is blocked",
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
if (!authIds.has(entry.journal.authId)) continue;
|
|
810
|
+
const existing = byKey.get(entry.journal.authRealmHash);
|
|
811
|
+
if (
|
|
812
|
+
existing === undefined
|
|
813
|
+
|| entry.journal.authId < existing
|
|
814
|
+
) {
|
|
815
|
+
byKey.set(entry.journal.authRealmHash, entry.journal.authId);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
return Object.freeze(
|
|
819
|
+
[...byKey.entries()]
|
|
820
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
821
|
+
.map(([key, authIdValue]) =>
|
|
822
|
+
Object.freeze({ key, authId: authIdValue })
|
|
823
|
+
),
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function assertLinkedDeviceMutationJournalsSettled(
|
|
828
|
+
realmKeys: ReadonlySet<string>,
|
|
829
|
+
authIds: ReadonlySet<string>,
|
|
830
|
+
environment: Readonly<Record<string, string | undefined>>,
|
|
831
|
+
): void {
|
|
832
|
+
if (realmKeys.size === 0 && authIds.size === 0) return;
|
|
833
|
+
for (const entry of listLinkedDeviceLifecycleJournalSnapshots(environment)) {
|
|
834
|
+
if ("invalid" in entry) {
|
|
835
|
+
throw new Error(
|
|
836
|
+
"linked-device lifecycle journals contain invalid state; auth mutation is blocked",
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
const { journal } = entry;
|
|
840
|
+
if (
|
|
841
|
+
!realmKeys.has(journal.authRealmHash)
|
|
842
|
+
&& !authIds.has(journal.authId)
|
|
843
|
+
) continue;
|
|
844
|
+
if (
|
|
845
|
+
journal.phase === "terminal"
|
|
846
|
+
&& journal.status !== "indeterminate"
|
|
847
|
+
) continue;
|
|
848
|
+
throw new Error(
|
|
849
|
+
`auth locator ${journal.authId} has an active or unreconciled linked-device lifecycle`,
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function withLinkedDeviceAuthMutationAdmissions<T>(
|
|
855
|
+
current: GhostgetAuth | undefined,
|
|
856
|
+
replacement: GhostgetAuth | undefined,
|
|
857
|
+
environment: Readonly<Record<string, string | undefined>>,
|
|
858
|
+
operation: () => T,
|
|
859
|
+
): T {
|
|
860
|
+
const authIds = new Set(
|
|
861
|
+
[current?.id, replacement?.id].filter(
|
|
862
|
+
(id): id is string => id !== undefined,
|
|
863
|
+
),
|
|
864
|
+
);
|
|
865
|
+
const journals = listLinkedDeviceLifecycleJournalSnapshots(environment);
|
|
866
|
+
const realms = linkedDeviceMutationRealms(
|
|
867
|
+
current,
|
|
868
|
+
replacement,
|
|
869
|
+
authIds,
|
|
870
|
+
journals,
|
|
871
|
+
);
|
|
872
|
+
if (realms.length === 0) return operation();
|
|
873
|
+
const recovery = linkedDeviceLifecycleAdmissionStore.recover(environment);
|
|
874
|
+
if (recovery.issues.length > 0) {
|
|
875
|
+
throw new Error(
|
|
876
|
+
"linked-device lifecycle admissions contain unresolved state; auth mutation is blocked",
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
const acquiredAt = new Date().toISOString();
|
|
880
|
+
const admissions: LinkedDeviceLifecycleAdmission[] = [];
|
|
881
|
+
let result: T | undefined;
|
|
882
|
+
let operationError: unknown;
|
|
883
|
+
let operationFailed = false;
|
|
884
|
+
try {
|
|
885
|
+
for (const realm of realms) {
|
|
886
|
+
admissions.push(linkedDeviceLifecycleAdmissionStore.acquire(
|
|
887
|
+
realm.key,
|
|
888
|
+
realm.authId,
|
|
889
|
+
acquiredAt,
|
|
890
|
+
environment,
|
|
891
|
+
));
|
|
892
|
+
}
|
|
893
|
+
assertLinkedDeviceMutationJournalsSettled(
|
|
894
|
+
new Set(realms.map((realm) => realm.key)),
|
|
895
|
+
authIds,
|
|
896
|
+
environment,
|
|
897
|
+
);
|
|
898
|
+
result = operation();
|
|
899
|
+
} catch (error) {
|
|
900
|
+
operationFailed = true;
|
|
901
|
+
operationError = error;
|
|
902
|
+
}
|
|
903
|
+
const releaseErrors: unknown[] = [];
|
|
904
|
+
for (const admission of admissions.reverse()) {
|
|
905
|
+
try {
|
|
906
|
+
admission.release();
|
|
907
|
+
} catch (error) {
|
|
908
|
+
releaseErrors.push(error);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
if (operationFailed) {
|
|
912
|
+
if (releaseErrors.length === 0) throw operationError;
|
|
913
|
+
throw new AggregateError(
|
|
914
|
+
[operationError, ...releaseErrors],
|
|
915
|
+
"linked-device auth mutation failed and its admissions could not be released",
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
if (releaseErrors.length > 0) {
|
|
919
|
+
throw new AggregateError(
|
|
920
|
+
releaseErrors,
|
|
921
|
+
"linked-device auth mutation admissions could not be released",
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
return result as T;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function assertAuthSnapshot(value: AuthSnapshot): AuthSnapshot {
|
|
928
|
+
if (
|
|
929
|
+
typeof value !== "object"
|
|
930
|
+
|| value === null
|
|
931
|
+
|| Array.isArray(value)
|
|
932
|
+
|| Object.keys(value).sort().join(",") !== "auth,contentSha256"
|
|
933
|
+
|| typeof value.contentSha256 !== "string"
|
|
934
|
+
|| !/^[a-f0-9]{64}$/u.test(value.contentSha256)
|
|
935
|
+
) throw new Error("auth snapshot is malformed");
|
|
936
|
+
const canonical = canonicalAuthSnapshot(value.auth);
|
|
937
|
+
if (canonical.contentSha256 !== value.contentSha256) {
|
|
938
|
+
throw new Error("auth snapshot is not content-bound");
|
|
939
|
+
}
|
|
940
|
+
return canonical;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
export function loadAuthSnapshot(
|
|
944
|
+
id: string,
|
|
945
|
+
environment: Readonly<Record<string, string | undefined>> = process.env,
|
|
946
|
+
): AuthSnapshot {
|
|
947
|
+
const snapshot = loadAuthSnapshotIfPresent(id, environment);
|
|
948
|
+
if (snapshot === null) throw new Error(`auth locator ${id} was not found`);
|
|
949
|
+
return snapshot;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
export function replaceAuthIfUnchanged(
|
|
953
|
+
currentValue: AuthSnapshot,
|
|
954
|
+
replacementValue: GhostgetAuth,
|
|
955
|
+
environment: Readonly<Record<string, string | undefined>> = process.env,
|
|
956
|
+
authority?: AuthReplaceAuthority,
|
|
957
|
+
): AuthReplaceResult {
|
|
958
|
+
const current = assertAuthSnapshot(currentValue);
|
|
959
|
+
const replacement = canonicalAuthSnapshot(replacementValue);
|
|
960
|
+
if (replacement.auth.id !== current.auth.id) {
|
|
961
|
+
throw new Error("auth replacement ID does not match its snapshot");
|
|
962
|
+
}
|
|
963
|
+
const replace = (): AuthReplaceResult =>
|
|
964
|
+
withReadProjectionAuthAdmission(current.auth.id, environment, () => {
|
|
965
|
+
const observed = loadAuthSnapshotIfPresent(current.auth.id, environment);
|
|
966
|
+
if (
|
|
967
|
+
observed === null
|
|
968
|
+
|| observed.contentSha256 !== current.contentSha256
|
|
969
|
+
) return Object.freeze({ replaced: false as const });
|
|
970
|
+
rotateReadProjectionAuthIncarnation(current.auth.id, environment);
|
|
971
|
+
removePrivateAuthState(current.auth.id, environment);
|
|
972
|
+
const replaced = writePrivateJsonIfUnchanged(
|
|
973
|
+
authPath(current.auth.id, environment),
|
|
974
|
+
replacement.auth,
|
|
975
|
+
{ expectedCurrentContentSha256: current.contentSha256 },
|
|
976
|
+
);
|
|
977
|
+
if (!replaced) return Object.freeze({ replaced: false as const });
|
|
978
|
+
return Object.freeze({ replaced: true as const, snapshot: replacement });
|
|
979
|
+
});
|
|
980
|
+
const linkedMutation = current.auth.kind === "linked-device-store"
|
|
981
|
+
|| replacement.auth.kind === "linked-device-store";
|
|
982
|
+
if (!linkedMutation) return replace();
|
|
983
|
+
if (authority !== undefined) {
|
|
984
|
+
if (
|
|
985
|
+
current.auth.kind !== "linked-device-store"
|
|
986
|
+
|| replacement.auth.kind !== "linked-device-store"
|
|
987
|
+
|| current.auth.id !== authority.lifecycleAdmission.authId
|
|
988
|
+
|| linkedDeviceRealmKey(current.auth)
|
|
989
|
+
!== authority.lifecycleAdmission.realmKey
|
|
990
|
+
|| linkedDeviceRealmKey(replacement.auth)
|
|
991
|
+
!== authority.lifecycleAdmission.realmKey
|
|
992
|
+
) {
|
|
993
|
+
throw new Error(
|
|
994
|
+
"linked-device auth replacement authority does not match one stable realm",
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
assertLinkedDeviceLifecycleAdmissionHeld(
|
|
998
|
+
authority.lifecycleAdmission,
|
|
999
|
+
environment,
|
|
1000
|
+
);
|
|
1001
|
+
return replace();
|
|
1002
|
+
}
|
|
1003
|
+
return withLinkedDeviceAuthMutationAdmissions(
|
|
1004
|
+
current.auth,
|
|
1005
|
+
replacement.auth,
|
|
1006
|
+
environment,
|
|
1007
|
+
replace,
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
export function loadAuth(
|
|
1012
|
+
id: string,
|
|
1013
|
+
environment: Readonly<Record<string, string | undefined>> = process.env,
|
|
1014
|
+
): GhostgetAuth {
|
|
1015
|
+
return loadAuthSnapshot(id, environment).auth;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
export function listAuth(environment: Readonly<Record<string, string | undefined>> = process.env): readonly GhostgetAuth[] {
|
|
1019
|
+
const directory = join(ghostgetStateHome(environment), "auth");
|
|
1020
|
+
const snapshot = snapshotPrivateStateDirectory(directory, environment);
|
|
1021
|
+
if (snapshot.identity === null) return [];
|
|
1022
|
+
const names = snapshot.entries
|
|
1023
|
+
.filter((entry) => entry.kind === "file" && entry.name.endsWith(".json"))
|
|
1024
|
+
.map((entry) => entry.name);
|
|
1025
|
+
return readPrivateStateFilesBatched(directory, names, {
|
|
1026
|
+
maximumBytesPerFile: MAX_WRENCH_JSON_BYTES,
|
|
1027
|
+
environment,
|
|
1028
|
+
expectedDirectoryIdentity: snapshot.identity,
|
|
1029
|
+
}).flatMap((file) => {
|
|
1030
|
+
const id = basename(file.name, ".json");
|
|
1031
|
+
if (file.status !== "present") return [];
|
|
1032
|
+
try {
|
|
1033
|
+
return [parseAuthSnapshotText(id, file.content).auth];
|
|
1034
|
+
} catch {
|
|
1035
|
+
return [];
|
|
1036
|
+
}
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function managedOAuthCredentialSnapshot(
|
|
1041
|
+
auth: GhostgetAuth,
|
|
1042
|
+
environment: Readonly<Record<string, string | undefined>>,
|
|
1043
|
+
): Readonly<{ path: string; contentSha256: string }> | null {
|
|
1044
|
+
if (auth.kind !== "oauth-token-file" || auth.managed !== true) return null;
|
|
1045
|
+
const expectedDirectory = join(ghostgetStateHome(environment), "auth", "oauth-tokens");
|
|
1046
|
+
if (
|
|
1047
|
+
dirname(auth.path) !== expectedDirectory
|
|
1048
|
+
|| !new RegExp(`^${auth.id}-[0-9a-f-]{36}\\.json$`, "u").test(basename(auth.path))
|
|
1049
|
+
) throw new Error("managed OAuth credential path does not match its auth locator");
|
|
1050
|
+
const content = readPrivateStateFileIfPresent(
|
|
1051
|
+
auth.path,
|
|
1052
|
+
64 * 1024,
|
|
1053
|
+
"managed OAuth credential",
|
|
1054
|
+
environment,
|
|
1055
|
+
);
|
|
1056
|
+
return content === null
|
|
1057
|
+
? null
|
|
1058
|
+
: Object.freeze({
|
|
1059
|
+
path: auth.path,
|
|
1060
|
+
contentSha256: createHash("sha256").update(content, "utf8").digest("hex"),
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
export function removeAuth(id: string, environment: Readonly<Record<string, string | undefined>> = process.env): boolean {
|
|
1065
|
+
const path = authPath(id, environment);
|
|
1066
|
+
let current: AuthSnapshot | null;
|
|
1067
|
+
try {
|
|
1068
|
+
current = loadAuthSnapshotIfPresent(id, environment);
|
|
1069
|
+
} catch {
|
|
1070
|
+
throw new Error(
|
|
1071
|
+
`auth locator ${id} is invalid; removal cannot prove that no linked-device lifecycle owns it`,
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
if (current === null) {
|
|
1075
|
+
return withReadProjectionAuthAdmission(id, environment, () => {
|
|
1076
|
+
const observed = loadAuthSnapshotIfPresent(id, environment);
|
|
1077
|
+
if (observed !== null) {
|
|
1078
|
+
throw new Error(
|
|
1079
|
+
`auth locator ${id} changed concurrently before removal`,
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
removePrivateAuthState(id, environment);
|
|
1083
|
+
removeReadProjectionAuthIncarnation(id, environment);
|
|
1084
|
+
return false;
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
const managedCredential = managedOAuthCredentialSnapshot(
|
|
1088
|
+
current.auth,
|
|
1089
|
+
environment,
|
|
1090
|
+
);
|
|
1091
|
+
return withLinkedDeviceAuthMutationAdmissions(
|
|
1092
|
+
current.auth,
|
|
1093
|
+
undefined,
|
|
1094
|
+
environment,
|
|
1095
|
+
() => withReadProjectionAuthAdmission(id, environment, () => {
|
|
1096
|
+
const observed = loadAuthSnapshotIfPresent(id, environment);
|
|
1097
|
+
if (
|
|
1098
|
+
observed === null
|
|
1099
|
+
|| observed.contentSha256 !== current.contentSha256
|
|
1100
|
+
) {
|
|
1101
|
+
throw new Error(
|
|
1102
|
+
`auth locator ${id} changed concurrently before removal`,
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
rotateReadProjectionAuthIncarnation(id, environment);
|
|
1106
|
+
removePrivateAuthState(id, environment);
|
|
1107
|
+
removeReadProjectionAuthIncarnation(id, environment);
|
|
1108
|
+
const removed = removePrivateStateFileIfUnchanged(
|
|
1109
|
+
path,
|
|
1110
|
+
{ expectedCurrentContentSha256: current.contentSha256 },
|
|
1111
|
+
environment,
|
|
1112
|
+
);
|
|
1113
|
+
if (!removed) {
|
|
1114
|
+
throw new Error(
|
|
1115
|
+
`auth locator ${id} changed concurrently before removal`,
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
if (
|
|
1119
|
+
managedCredential !== null
|
|
1120
|
+
&& !removePrivateStateFileIfUnchanged(
|
|
1121
|
+
managedCredential.path,
|
|
1122
|
+
{ expectedCurrentContentSha256: managedCredential.contentSha256 },
|
|
1123
|
+
environment,
|
|
1124
|
+
)
|
|
1125
|
+
) {
|
|
1126
|
+
throw new Error(
|
|
1127
|
+
`managed OAuth credential for ${id} changed concurrently before removal`,
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
return true;
|
|
1131
|
+
}),
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function removePrivateAuthState(
|
|
1136
|
+
id: string,
|
|
1137
|
+
environment: Readonly<Record<string, string | undefined>>,
|
|
1138
|
+
): void {
|
|
1139
|
+
const failures: unknown[] = [];
|
|
1140
|
+
try {
|
|
1141
|
+
removeSessionSecretsForAuth(id, environment);
|
|
1142
|
+
} catch (error) {
|
|
1143
|
+
failures.push(error);
|
|
1144
|
+
}
|
|
1145
|
+
try {
|
|
1146
|
+
removeReadProjectionsForAuth(id, environment);
|
|
1147
|
+
} catch (error) {
|
|
1148
|
+
failures.push(error);
|
|
1149
|
+
}
|
|
1150
|
+
if (failures.length === 1) throw failures[0];
|
|
1151
|
+
if (failures.length > 1) {
|
|
1152
|
+
throw new AggregateError(
|
|
1153
|
+
failures,
|
|
1154
|
+
`private state cleanup for auth locator ${id} failed`,
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
}
|