@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,1560 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { canonicalJson } from "./canonical-json";
|
|
5
|
+
import {
|
|
6
|
+
currentProcessStartIdentity,
|
|
7
|
+
processOwnerStatus,
|
|
8
|
+
type ProcessOwnerIdentity,
|
|
9
|
+
type ProcessOwnerStatus,
|
|
10
|
+
} from "./process-identity";
|
|
11
|
+
import {
|
|
12
|
+
isPortableProviderPluginVersion,
|
|
13
|
+
isProviderPluginId,
|
|
14
|
+
isProviderPluginSurfaceId,
|
|
15
|
+
} from "./provider-plugin-identifiers";
|
|
16
|
+
import {
|
|
17
|
+
createPrivateJsonIfAbsent,
|
|
18
|
+
ensurePrivateStateDirectory,
|
|
19
|
+
ghostgetStateHome,
|
|
20
|
+
readPrivateStateFileIfPresent,
|
|
21
|
+
readPrivateStateFilesBatched,
|
|
22
|
+
snapshotPrivateStateDirectory,
|
|
23
|
+
writePrivateJsonIfUnchanged,
|
|
24
|
+
} from "./storage";
|
|
25
|
+
|
|
26
|
+
type Environment = Readonly<Record<string, string | undefined>>;
|
|
27
|
+
type JsonRecord = Record<string, unknown>;
|
|
28
|
+
type OwnerInspector = (owner: ProcessOwnerIdentity) => ProcessOwnerStatus;
|
|
29
|
+
|
|
30
|
+
export const LINKED_DEVICE_LIFECYCLE_JOURNAL_SCHEMA_VERSION = 1 as const;
|
|
31
|
+
export const LINKED_DEVICE_LIFECYCLE_JOURNAL_STATE_DIRECTORY =
|
|
32
|
+
"run-journals/linked-device-lifecycle" as const;
|
|
33
|
+
|
|
34
|
+
const MAX_JOURNAL_BYTES = 32 * 1024;
|
|
35
|
+
const uuidPattern =
|
|
36
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
|
|
37
|
+
const sha256Pattern = /^[a-f0-9]{64}$/u;
|
|
38
|
+
const authIdPattern = /^[a-z][a-z0-9-]{0,47}$/u;
|
|
39
|
+
|
|
40
|
+
export type LinkedDeviceLifecycleOwner = ProcessOwnerIdentity & {
|
|
41
|
+
readonly token: string;
|
|
42
|
+
readonly leaseUntil: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type LinkedDevicePairResult = {
|
|
46
|
+
readonly kind: "pair";
|
|
47
|
+
readonly resultingAuthContentHash: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type LinkedDeviceSyncResult = {
|
|
51
|
+
readonly kind: "sync";
|
|
52
|
+
readonly itemsStored: number;
|
|
53
|
+
readonly projection: "linked-device-local-store";
|
|
54
|
+
readonly emitsProtocolAcknowledgements: true;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type LinkedDeviceLifecycleResult =
|
|
58
|
+
| LinkedDevicePairResult
|
|
59
|
+
| LinkedDeviceSyncResult;
|
|
60
|
+
|
|
61
|
+
export type LinkedDeviceLifecycleReasonCode =
|
|
62
|
+
| "prepared"
|
|
63
|
+
| "external-outcome-pending"
|
|
64
|
+
| "external-returned-commit-pending"
|
|
65
|
+
| "preflight-failed"
|
|
66
|
+
| "cancelled-before-begin"
|
|
67
|
+
| "owner-exited-before-begin"
|
|
68
|
+
| "runtime-error-after-begin"
|
|
69
|
+
| "cancelled-after-begin"
|
|
70
|
+
| "deadline-after-begin"
|
|
71
|
+
| "external-returned-before-completion-persisted"
|
|
72
|
+
| "owner-exited-after-begin"
|
|
73
|
+
| "reconciled-not-applied";
|
|
74
|
+
|
|
75
|
+
export type LinkedDeviceLifecycleJournal = {
|
|
76
|
+
readonly schemaVersion:
|
|
77
|
+
typeof LINKED_DEVICE_LIFECYCLE_JOURNAL_SCHEMA_VERSION;
|
|
78
|
+
readonly revision: number;
|
|
79
|
+
readonly journalId: string;
|
|
80
|
+
readonly kind: "pair" | "sync-once";
|
|
81
|
+
readonly pluginId: string;
|
|
82
|
+
readonly pluginVersion: string;
|
|
83
|
+
readonly pluginImplementationHash: string;
|
|
84
|
+
readonly lifecycleContractVersion: 1;
|
|
85
|
+
readonly surfaceId: string;
|
|
86
|
+
readonly authId: string;
|
|
87
|
+
readonly authRealmHash: string;
|
|
88
|
+
readonly authContentHash: string;
|
|
89
|
+
readonly initialSubjectState: "bound" | "unbound";
|
|
90
|
+
/** Pairing input is represented only by presence; raw phone data is forbidden. */
|
|
91
|
+
readonly phoneProvided: boolean;
|
|
92
|
+
readonly phase:
|
|
93
|
+
| "prepared"
|
|
94
|
+
| "external-begun"
|
|
95
|
+
| "external-completed"
|
|
96
|
+
| "terminal";
|
|
97
|
+
readonly status:
|
|
98
|
+
| "pending"
|
|
99
|
+
| "succeeded"
|
|
100
|
+
| "safe-retry"
|
|
101
|
+
| "indeterminate";
|
|
102
|
+
readonly reconciliation:
|
|
103
|
+
| "not-required"
|
|
104
|
+
| "required"
|
|
105
|
+
| "resolved-applied"
|
|
106
|
+
| "resolved-not-applied";
|
|
107
|
+
readonly reconciliationHash: string | null;
|
|
108
|
+
readonly owner: LinkedDeviceLifecycleOwner;
|
|
109
|
+
readonly startedAt: string;
|
|
110
|
+
readonly updatedAt: string;
|
|
111
|
+
readonly externalStartedAt: string | null;
|
|
112
|
+
readonly externalCompletedAt: string | null;
|
|
113
|
+
readonly finishedAt: string | null;
|
|
114
|
+
readonly reconciledAt: string | null;
|
|
115
|
+
readonly result: LinkedDeviceLifecycleResult | null;
|
|
116
|
+
readonly reasonCode: LinkedDeviceLifecycleReasonCode | null;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export type StartLinkedDeviceLifecycleJournal = {
|
|
120
|
+
readonly journalId: string;
|
|
121
|
+
readonly kind: LinkedDeviceLifecycleJournal["kind"];
|
|
122
|
+
readonly pluginId: string;
|
|
123
|
+
readonly pluginVersion: string;
|
|
124
|
+
readonly pluginImplementationHash: string;
|
|
125
|
+
readonly lifecycleContractVersion: 1;
|
|
126
|
+
readonly surfaceId: string;
|
|
127
|
+
readonly authId: string;
|
|
128
|
+
readonly authRealmHash: string;
|
|
129
|
+
readonly authContentHash: string;
|
|
130
|
+
readonly initialSubjectState: "bound" | "unbound";
|
|
131
|
+
readonly phoneProvided: boolean;
|
|
132
|
+
readonly owner: LinkedDeviceLifecycleOwner;
|
|
133
|
+
readonly startedAt: string;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export type LinkedDeviceLifecycleJournalEvent =
|
|
137
|
+
| {
|
|
138
|
+
readonly type: "external-begin";
|
|
139
|
+
readonly at: string;
|
|
140
|
+
}
|
|
141
|
+
| {
|
|
142
|
+
readonly type: "external-complete";
|
|
143
|
+
readonly result: LinkedDeviceLifecycleResult;
|
|
144
|
+
readonly at: string;
|
|
145
|
+
}
|
|
146
|
+
| {
|
|
147
|
+
readonly type: "committed";
|
|
148
|
+
readonly result: LinkedDeviceLifecycleResult;
|
|
149
|
+
readonly at: string;
|
|
150
|
+
}
|
|
151
|
+
| {
|
|
152
|
+
readonly type: "aborted-before-external";
|
|
153
|
+
readonly reasonCode: "preflight-failed" | "cancelled-before-begin";
|
|
154
|
+
readonly at: string;
|
|
155
|
+
}
|
|
156
|
+
| {
|
|
157
|
+
readonly type: "outcome-not-durable";
|
|
158
|
+
readonly reasonCode:
|
|
159
|
+
| "runtime-error-after-begin"
|
|
160
|
+
| "cancelled-after-begin"
|
|
161
|
+
| "deadline-after-begin"
|
|
162
|
+
| "external-returned-before-completion-persisted";
|
|
163
|
+
readonly at: string;
|
|
164
|
+
}
|
|
165
|
+
| {
|
|
166
|
+
readonly type: "reconciled";
|
|
167
|
+
readonly outcome: "applied";
|
|
168
|
+
readonly evidenceHash: string;
|
|
169
|
+
readonly result: LinkedDeviceLifecycleResult;
|
|
170
|
+
readonly at: string;
|
|
171
|
+
}
|
|
172
|
+
| {
|
|
173
|
+
readonly type: "reconciled";
|
|
174
|
+
readonly outcome: "not-applied";
|
|
175
|
+
readonly evidenceHash: string;
|
|
176
|
+
readonly at: string;
|
|
177
|
+
}
|
|
178
|
+
| {
|
|
179
|
+
readonly type: "lease-renewed";
|
|
180
|
+
readonly leaseUntil: string;
|
|
181
|
+
readonly at: string;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
export type LinkedDeviceLifecycleJournalSnapshot = {
|
|
185
|
+
readonly journal: LinkedDeviceLifecycleJournal;
|
|
186
|
+
readonly contentSha256: string;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export type LinkedDeviceLifecycleRestartDisposition =
|
|
190
|
+
| {
|
|
191
|
+
readonly kind: "complete";
|
|
192
|
+
readonly reason: string;
|
|
193
|
+
}
|
|
194
|
+
| {
|
|
195
|
+
readonly kind: "live-owner";
|
|
196
|
+
readonly reason: string;
|
|
197
|
+
}
|
|
198
|
+
| {
|
|
199
|
+
readonly kind: "owner-unknown";
|
|
200
|
+
readonly reason: string;
|
|
201
|
+
}
|
|
202
|
+
| {
|
|
203
|
+
readonly kind: "safe-retry";
|
|
204
|
+
readonly reason: string;
|
|
205
|
+
}
|
|
206
|
+
| {
|
|
207
|
+
readonly kind: "reconciliation-required";
|
|
208
|
+
readonly reason: string;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export type LinkedDeviceLifecycleJournalListEntry =
|
|
212
|
+
| LinkedDeviceLifecycleJournalSnapshot
|
|
213
|
+
| {
|
|
214
|
+
readonly journalId: string;
|
|
215
|
+
readonly invalid: true;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
export type LinkedDeviceLifecycleWriteAuthority = {
|
|
219
|
+
readonly owner: LinkedDeviceLifecycleOwner;
|
|
220
|
+
readonly environment?: Environment;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
function dataRecord(value: unknown, label: string): JsonRecord {
|
|
224
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
225
|
+
throw new Error(`${label} must be an object`);
|
|
226
|
+
}
|
|
227
|
+
const prototype: unknown = Object.getPrototypeOf(value) as unknown;
|
|
228
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
229
|
+
throw new Error(`${label} has an unsupported prototype`);
|
|
230
|
+
}
|
|
231
|
+
const result: JsonRecord = {};
|
|
232
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
233
|
+
if (typeof key !== "string") {
|
|
234
|
+
throw new Error(`${label} has unsupported symbol fields`);
|
|
235
|
+
}
|
|
236
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
237
|
+
if (
|
|
238
|
+
descriptor === undefined
|
|
239
|
+
|| !descriptor.enumerable
|
|
240
|
+
|| !("value" in descriptor)
|
|
241
|
+
) {
|
|
242
|
+
throw new Error(`${label} has unsupported accessor fields`);
|
|
243
|
+
}
|
|
244
|
+
Object.defineProperty(result, key, {
|
|
245
|
+
value: descriptor.value,
|
|
246
|
+
enumerable: true,
|
|
247
|
+
configurable: false,
|
|
248
|
+
writable: false,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function exactKeys(
|
|
255
|
+
value: Readonly<JsonRecord>,
|
|
256
|
+
expected: readonly string[],
|
|
257
|
+
label: string,
|
|
258
|
+
): void {
|
|
259
|
+
const actual = Object.keys(value).sort();
|
|
260
|
+
const wanted = [...expected].sort();
|
|
261
|
+
if (
|
|
262
|
+
actual.length !== wanted.length
|
|
263
|
+
|| actual.some((key, index) => key !== wanted[index])
|
|
264
|
+
) {
|
|
265
|
+
throw new Error(`${label} has unsupported fields`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function canonicalTimestamp(value: unknown, label: string): string {
|
|
270
|
+
if (
|
|
271
|
+
typeof value !== "string"
|
|
272
|
+
|| !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(value)
|
|
273
|
+
|| !Number.isFinite(Date.parse(value))
|
|
274
|
+
|| new Date(value).toISOString() !== value
|
|
275
|
+
) {
|
|
276
|
+
throw new Error(`${label} must be one canonical UTC timestamp`);
|
|
277
|
+
}
|
|
278
|
+
return value;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function nullableTimestamp(value: unknown, label: string): string | null {
|
|
282
|
+
return value === null ? null : canonicalTimestamp(value, label);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function digest(value: unknown, label: string): string {
|
|
286
|
+
if (typeof value !== "string" || !sha256Pattern.test(value)) {
|
|
287
|
+
throw new Error(`${label} must be a lowercase SHA-256 digest`);
|
|
288
|
+
}
|
|
289
|
+
return value;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function nullableDigest(value: unknown, label: string): string | null {
|
|
293
|
+
return value === null ? null : digest(value, label);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function strictPluginVersion(value: unknown): string {
|
|
297
|
+
if (!isPortableProviderPluginVersion(value)) {
|
|
298
|
+
throw new Error("linked-device lifecycle plugin version is malformed");
|
|
299
|
+
}
|
|
300
|
+
return value;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function journalId(value: unknown): string {
|
|
304
|
+
if (typeof value !== "string" || !uuidPattern.test(value)) {
|
|
305
|
+
throw new Error("linked-device lifecycle journal ID is malformed");
|
|
306
|
+
}
|
|
307
|
+
return value;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function parseOwner(value: unknown): LinkedDeviceLifecycleOwner {
|
|
311
|
+
const record = dataRecord(value, "linked-device lifecycle journal owner");
|
|
312
|
+
exactKeys(
|
|
313
|
+
record,
|
|
314
|
+
["pid", "token", "bootId", "processStartId", "leaseUntil"],
|
|
315
|
+
"linked-device lifecycle journal owner",
|
|
316
|
+
);
|
|
317
|
+
if (
|
|
318
|
+
typeof record.pid !== "number"
|
|
319
|
+
|| !Number.isSafeInteger(record.pid)
|
|
320
|
+
|| record.pid < 1
|
|
321
|
+
|| record.pid > 2_147_483_647
|
|
322
|
+
|| typeof record.token !== "string"
|
|
323
|
+
|| !uuidPattern.test(record.token)
|
|
324
|
+
) {
|
|
325
|
+
throw new Error("linked-device lifecycle journal owner is malformed");
|
|
326
|
+
}
|
|
327
|
+
return Object.freeze({
|
|
328
|
+
pid: record.pid,
|
|
329
|
+
token: record.token,
|
|
330
|
+
bootId: digest(
|
|
331
|
+
record.bootId,
|
|
332
|
+
"linked-device lifecycle journal owner boot identity",
|
|
333
|
+
),
|
|
334
|
+
processStartId: digest(
|
|
335
|
+
record.processStartId,
|
|
336
|
+
"linked-device lifecycle journal owner process identity",
|
|
337
|
+
),
|
|
338
|
+
leaseUntil: canonicalTimestamp(
|
|
339
|
+
record.leaseUntil,
|
|
340
|
+
"linked-device lifecycle journal owner lease",
|
|
341
|
+
),
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function parseLinkedDeviceLifecycleOwner(
|
|
346
|
+
value: unknown,
|
|
347
|
+
): LinkedDeviceLifecycleOwner {
|
|
348
|
+
return parseOwner(value);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function parseResult(value: unknown): LinkedDeviceLifecycleResult {
|
|
352
|
+
const record = dataRecord(value, "linked-device lifecycle result");
|
|
353
|
+
if (record.kind === "pair") {
|
|
354
|
+
exactKeys(
|
|
355
|
+
record,
|
|
356
|
+
["kind", "resultingAuthContentHash"],
|
|
357
|
+
"linked-device pair result",
|
|
358
|
+
);
|
|
359
|
+
return Object.freeze({
|
|
360
|
+
kind: "pair",
|
|
361
|
+
resultingAuthContentHash: digest(
|
|
362
|
+
record.resultingAuthContentHash,
|
|
363
|
+
"linked-device pair resulting auth content hash",
|
|
364
|
+
),
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
if (record.kind === "sync") {
|
|
368
|
+
exactKeys(
|
|
369
|
+
record,
|
|
370
|
+
["kind", "itemsStored", "projection", "emitsProtocolAcknowledgements"],
|
|
371
|
+
"linked-device sync result",
|
|
372
|
+
);
|
|
373
|
+
if (
|
|
374
|
+
typeof record.itemsStored !== "number"
|
|
375
|
+
|| !Number.isSafeInteger(record.itemsStored)
|
|
376
|
+
|| record.itemsStored < 0
|
|
377
|
+
|| record.itemsStored > 1_000_000_000
|
|
378
|
+
|| record.projection !== "linked-device-local-store"
|
|
379
|
+
|| record.emitsProtocolAcknowledgements !== true
|
|
380
|
+
) {
|
|
381
|
+
throw new Error("linked-device sync result is malformed");
|
|
382
|
+
}
|
|
383
|
+
return Object.freeze({
|
|
384
|
+
kind: "sync",
|
|
385
|
+
itemsStored: record.itemsStored,
|
|
386
|
+
projection: record.projection,
|
|
387
|
+
emitsProtocolAcknowledgements: record.emitsProtocolAcknowledgements,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
throw new Error("linked-device lifecycle result kind is unsupported");
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function nullableResult(value: unknown): LinkedDeviceLifecycleResult | null {
|
|
394
|
+
return value === null ? null : parseResult(value);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function sameResult(
|
|
398
|
+
left: LinkedDeviceLifecycleResult,
|
|
399
|
+
right: LinkedDeviceLifecycleResult,
|
|
400
|
+
): boolean {
|
|
401
|
+
return canonicalJson(left) === canonicalJson(right);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function resultMatchesJournalKind(
|
|
405
|
+
result: LinkedDeviceLifecycleResult,
|
|
406
|
+
kind: LinkedDeviceLifecycleJournal["kind"],
|
|
407
|
+
): boolean {
|
|
408
|
+
return (result.kind === "pair" && kind === "pair")
|
|
409
|
+
|| (result.kind === "sync" && kind === "sync-once");
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function reasonCode(
|
|
413
|
+
value: unknown,
|
|
414
|
+
): LinkedDeviceLifecycleReasonCode | null {
|
|
415
|
+
if (value === null) return null;
|
|
416
|
+
if (
|
|
417
|
+
value === "prepared"
|
|
418
|
+
|| value === "external-outcome-pending"
|
|
419
|
+
|| value === "external-returned-commit-pending"
|
|
420
|
+
|| value === "preflight-failed"
|
|
421
|
+
|| value === "cancelled-before-begin"
|
|
422
|
+
|| value === "owner-exited-before-begin"
|
|
423
|
+
|| value === "runtime-error-after-begin"
|
|
424
|
+
|| value === "cancelled-after-begin"
|
|
425
|
+
|| value === "deadline-after-begin"
|
|
426
|
+
|| value === "external-returned-before-completion-persisted"
|
|
427
|
+
|| value === "owner-exited-after-begin"
|
|
428
|
+
|| value === "reconciled-not-applied"
|
|
429
|
+
) {
|
|
430
|
+
return value;
|
|
431
|
+
}
|
|
432
|
+
throw new Error("linked-device lifecycle reason code is unsupported");
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function preBeginReasonCode(
|
|
436
|
+
value: unknown,
|
|
437
|
+
): "preflight-failed" | "cancelled-before-begin" {
|
|
438
|
+
if (value !== "preflight-failed" && value !== "cancelled-before-begin") {
|
|
439
|
+
throw new Error(
|
|
440
|
+
"linked-device lifecycle pre-begin reason code is unsupported",
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
return value;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function postBeginReasonCode(
|
|
447
|
+
value: unknown,
|
|
448
|
+
):
|
|
449
|
+
| "runtime-error-after-begin"
|
|
450
|
+
| "cancelled-after-begin"
|
|
451
|
+
| "deadline-after-begin"
|
|
452
|
+
| "external-returned-before-completion-persisted" {
|
|
453
|
+
if (
|
|
454
|
+
value !== "runtime-error-after-begin"
|
|
455
|
+
&& value !== "cancelled-after-begin"
|
|
456
|
+
&& value !== "deadline-after-begin"
|
|
457
|
+
&& value !== "external-returned-before-completion-persisted"
|
|
458
|
+
) {
|
|
459
|
+
throw new Error(
|
|
460
|
+
"linked-device lifecycle post-begin reason code is unsupported",
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
return value;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function assertTimestampOrder(
|
|
467
|
+
earlier: string,
|
|
468
|
+
later: string,
|
|
469
|
+
label: string,
|
|
470
|
+
): void {
|
|
471
|
+
if (Date.parse(later) < Date.parse(earlier)) {
|
|
472
|
+
throw new Error(`${label} moved backward`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function assertJournalInvariants(journal: LinkedDeviceLifecycleJournal): void {
|
|
477
|
+
assertTimestampOrder(
|
|
478
|
+
journal.startedAt,
|
|
479
|
+
journal.updatedAt,
|
|
480
|
+
"linked-device lifecycle journal update",
|
|
481
|
+
);
|
|
482
|
+
assertTimestampOrder(
|
|
483
|
+
journal.startedAt,
|
|
484
|
+
journal.owner.leaseUntil,
|
|
485
|
+
"linked-device lifecycle journal owner lease",
|
|
486
|
+
);
|
|
487
|
+
if (
|
|
488
|
+
journal.kind === "sync-once"
|
|
489
|
+
&& (
|
|
490
|
+
journal.phoneProvided
|
|
491
|
+
|| journal.initialSubjectState !== "bound"
|
|
492
|
+
)
|
|
493
|
+
) {
|
|
494
|
+
throw new Error(
|
|
495
|
+
"linked-device sync lifecycle requires bound auth and forbids phone input",
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
if (
|
|
499
|
+
journal.result !== null
|
|
500
|
+
&& !resultMatchesJournalKind(journal.result, journal.kind)
|
|
501
|
+
) {
|
|
502
|
+
throw new Error("linked-device lifecycle result kind contradicts its journal");
|
|
503
|
+
}
|
|
504
|
+
if (journal.externalStartedAt !== null) {
|
|
505
|
+
assertTimestampOrder(
|
|
506
|
+
journal.startedAt,
|
|
507
|
+
journal.externalStartedAt,
|
|
508
|
+
"linked-device lifecycle external start",
|
|
509
|
+
);
|
|
510
|
+
assertTimestampOrder(
|
|
511
|
+
journal.externalStartedAt,
|
|
512
|
+
journal.updatedAt,
|
|
513
|
+
"linked-device lifecycle post-external update",
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
if (journal.externalCompletedAt !== null) {
|
|
517
|
+
if (journal.externalStartedAt === null) {
|
|
518
|
+
throw new Error("linked-device lifecycle completion has no external start");
|
|
519
|
+
}
|
|
520
|
+
assertTimestampOrder(
|
|
521
|
+
journal.externalStartedAt,
|
|
522
|
+
journal.externalCompletedAt,
|
|
523
|
+
"linked-device lifecycle external completion",
|
|
524
|
+
);
|
|
525
|
+
assertTimestampOrder(
|
|
526
|
+
journal.externalCompletedAt,
|
|
527
|
+
journal.updatedAt,
|
|
528
|
+
"linked-device lifecycle post-completion update",
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
if (journal.finishedAt !== null) {
|
|
532
|
+
assertTimestampOrder(
|
|
533
|
+
journal.startedAt,
|
|
534
|
+
journal.finishedAt,
|
|
535
|
+
"linked-device lifecycle finish",
|
|
536
|
+
);
|
|
537
|
+
assertTimestampOrder(
|
|
538
|
+
journal.finishedAt,
|
|
539
|
+
journal.updatedAt,
|
|
540
|
+
"linked-device lifecycle post-finish update",
|
|
541
|
+
);
|
|
542
|
+
if (journal.externalStartedAt !== null) {
|
|
543
|
+
assertTimestampOrder(
|
|
544
|
+
journal.externalStartedAt,
|
|
545
|
+
journal.finishedAt,
|
|
546
|
+
"linked-device lifecycle external-to-finish order",
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
if (journal.externalCompletedAt !== null) {
|
|
550
|
+
assertTimestampOrder(
|
|
551
|
+
journal.externalCompletedAt,
|
|
552
|
+
journal.finishedAt,
|
|
553
|
+
"linked-device lifecycle completion-to-finish order",
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (journal.reconciledAt !== null) {
|
|
558
|
+
if (journal.finishedAt === null) {
|
|
559
|
+
throw new Error("linked-device lifecycle reconciliation has no finish");
|
|
560
|
+
}
|
|
561
|
+
assertTimestampOrder(
|
|
562
|
+
journal.finishedAt,
|
|
563
|
+
journal.reconciledAt,
|
|
564
|
+
"linked-device lifecycle reconciliation",
|
|
565
|
+
);
|
|
566
|
+
assertTimestampOrder(
|
|
567
|
+
journal.reconciledAt,
|
|
568
|
+
journal.updatedAt,
|
|
569
|
+
"linked-device lifecycle post-reconciliation update",
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (journal.phase === "prepared") {
|
|
574
|
+
if (
|
|
575
|
+
journal.status !== "pending"
|
|
576
|
+
|| journal.reconciliation !== "not-required"
|
|
577
|
+
|| journal.reconciliationHash !== null
|
|
578
|
+
|| journal.externalStartedAt !== null
|
|
579
|
+
|| journal.externalCompletedAt !== null
|
|
580
|
+
|| journal.finishedAt !== null
|
|
581
|
+
|| journal.reconciledAt !== null
|
|
582
|
+
|| journal.result !== null
|
|
583
|
+
|| journal.reasonCode !== "prepared"
|
|
584
|
+
) throw new Error("prepared linked-device lifecycle journal is contradictory");
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
if (journal.phase === "external-begun") {
|
|
588
|
+
if (
|
|
589
|
+
journal.revision < 1
|
|
590
|
+
|| journal.status !== "pending"
|
|
591
|
+
|| journal.reconciliation !== "not-required"
|
|
592
|
+
|| journal.reconciliationHash !== null
|
|
593
|
+
|| journal.externalStartedAt === null
|
|
594
|
+
|| journal.externalCompletedAt !== null
|
|
595
|
+
|| journal.finishedAt !== null
|
|
596
|
+
|| journal.reconciledAt !== null
|
|
597
|
+
|| journal.result !== null
|
|
598
|
+
|| journal.reasonCode !== "external-outcome-pending"
|
|
599
|
+
) throw new Error("begun linked-device lifecycle journal is contradictory");
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
if (journal.phase === "external-completed") {
|
|
603
|
+
if (
|
|
604
|
+
journal.revision < 2
|
|
605
|
+
|| journal.status !== "pending"
|
|
606
|
+
|| journal.reconciliation !== "not-required"
|
|
607
|
+
|| journal.reconciliationHash !== null
|
|
608
|
+
|| journal.externalStartedAt === null
|
|
609
|
+
|| journal.externalCompletedAt === null
|
|
610
|
+
|| journal.finishedAt !== null
|
|
611
|
+
|| journal.reconciledAt !== null
|
|
612
|
+
|| journal.result === null
|
|
613
|
+
|| journal.reasonCode !== "external-returned-commit-pending"
|
|
614
|
+
) throw new Error("completed linked-device lifecycle journal is contradictory");
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (journal.status === "pending" || journal.finishedAt === null) {
|
|
618
|
+
throw new Error("terminal linked-device lifecycle journal is contradictory");
|
|
619
|
+
}
|
|
620
|
+
if (journal.status === "indeterminate") {
|
|
621
|
+
const returnedWithoutDurableBegin =
|
|
622
|
+
journal.revision === 1
|
|
623
|
+
&& journal.reasonCode
|
|
624
|
+
=== "external-returned-before-completion-persisted"
|
|
625
|
+
&& journal.externalCompletedAt === null
|
|
626
|
+
&& journal.result === null;
|
|
627
|
+
if (
|
|
628
|
+
(journal.revision < 2 && !returnedWithoutDurableBegin)
|
|
629
|
+
|| journal.externalStartedAt === null
|
|
630
|
+
|| journal.reconciliation !== "required"
|
|
631
|
+
|| journal.reconciliationHash !== null
|
|
632
|
+
|| journal.reconciledAt !== null
|
|
633
|
+
|| (
|
|
634
|
+
journal.reasonCode !== "runtime-error-after-begin"
|
|
635
|
+
&& journal.reasonCode !== "cancelled-after-begin"
|
|
636
|
+
&& journal.reasonCode !== "deadline-after-begin"
|
|
637
|
+
&& journal.reasonCode
|
|
638
|
+
!== "external-returned-before-completion-persisted"
|
|
639
|
+
&& journal.reasonCode !== "owner-exited-after-begin"
|
|
640
|
+
)
|
|
641
|
+
|| (
|
|
642
|
+
(journal.externalCompletedAt === null) !== (journal.result === null)
|
|
643
|
+
)
|
|
644
|
+
) {
|
|
645
|
+
throw new Error(
|
|
646
|
+
"indeterminate linked-device lifecycle journal is contradictory",
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
if (journal.status === "succeeded") {
|
|
652
|
+
if (
|
|
653
|
+
journal.revision < 3
|
|
654
|
+
|| journal.externalStartedAt === null
|
|
655
|
+
|| journal.result === null
|
|
656
|
+
|| journal.reasonCode !== null
|
|
657
|
+
|| (
|
|
658
|
+
journal.reconciliation === "not-required"
|
|
659
|
+
? journal.reconciliationHash !== null
|
|
660
|
+
|| journal.reconciledAt !== null
|
|
661
|
+
|| journal.externalCompletedAt === null
|
|
662
|
+
: journal.reconciliation !== "resolved-applied"
|
|
663
|
+
|| journal.reconciliationHash === null
|
|
664
|
+
|| journal.reconciledAt === null
|
|
665
|
+
)
|
|
666
|
+
) {
|
|
667
|
+
throw new Error(
|
|
668
|
+
"successful linked-device lifecycle journal is contradictory",
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
if (
|
|
674
|
+
journal.status !== "safe-retry"
|
|
675
|
+
|| journal.result !== null
|
|
676
|
+
|| (
|
|
677
|
+
journal.reconciliation === "not-required"
|
|
678
|
+
? journal.revision < 1
|
|
679
|
+
|| journal.reconciliationHash !== null
|
|
680
|
+
|| journal.reconciledAt !== null
|
|
681
|
+
|| journal.externalStartedAt !== null
|
|
682
|
+
|| journal.externalCompletedAt !== null
|
|
683
|
+
|| (
|
|
684
|
+
journal.reasonCode !== "preflight-failed"
|
|
685
|
+
&& journal.reasonCode !== "cancelled-before-begin"
|
|
686
|
+
&& journal.reasonCode !== "owner-exited-before-begin"
|
|
687
|
+
)
|
|
688
|
+
: journal.reconciliation !== "resolved-not-applied"
|
|
689
|
+
|| journal.revision < 3
|
|
690
|
+
|| journal.reconciliationHash === null
|
|
691
|
+
|| journal.reconciledAt === null
|
|
692
|
+
|| journal.externalStartedAt === null
|
|
693
|
+
|| journal.externalCompletedAt !== null
|
|
694
|
+
|| journal.reasonCode !== "reconciled-not-applied"
|
|
695
|
+
)
|
|
696
|
+
) {
|
|
697
|
+
throw new Error("retryable linked-device lifecycle journal is contradictory");
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
export function parseLinkedDeviceLifecycleJournal(
|
|
702
|
+
value: unknown,
|
|
703
|
+
): LinkedDeviceLifecycleJournal {
|
|
704
|
+
const record = dataRecord(value, "linked-device lifecycle journal");
|
|
705
|
+
exactKeys(record, [
|
|
706
|
+
"schemaVersion",
|
|
707
|
+
"revision",
|
|
708
|
+
"journalId",
|
|
709
|
+
"kind",
|
|
710
|
+
"pluginId",
|
|
711
|
+
"pluginVersion",
|
|
712
|
+
"pluginImplementationHash",
|
|
713
|
+
"lifecycleContractVersion",
|
|
714
|
+
"surfaceId",
|
|
715
|
+
"authId",
|
|
716
|
+
"authRealmHash",
|
|
717
|
+
"authContentHash",
|
|
718
|
+
"initialSubjectState",
|
|
719
|
+
"phoneProvided",
|
|
720
|
+
"phase",
|
|
721
|
+
"status",
|
|
722
|
+
"reconciliation",
|
|
723
|
+
"reconciliationHash",
|
|
724
|
+
"owner",
|
|
725
|
+
"startedAt",
|
|
726
|
+
"updatedAt",
|
|
727
|
+
"externalStartedAt",
|
|
728
|
+
"externalCompletedAt",
|
|
729
|
+
"finishedAt",
|
|
730
|
+
"reconciledAt",
|
|
731
|
+
"result",
|
|
732
|
+
"reasonCode",
|
|
733
|
+
], "linked-device lifecycle journal");
|
|
734
|
+
if (
|
|
735
|
+
record.schemaVersion !== LINKED_DEVICE_LIFECYCLE_JOURNAL_SCHEMA_VERSION
|
|
736
|
+
|| typeof record.revision !== "number"
|
|
737
|
+
|| !Number.isSafeInteger(record.revision)
|
|
738
|
+
|| record.revision < 0
|
|
739
|
+
|| record.revision >= Number.MAX_SAFE_INTEGER
|
|
740
|
+
|| (record.kind !== "pair" && record.kind !== "sync-once")
|
|
741
|
+
|| !isProviderPluginId(record.pluginId)
|
|
742
|
+
|| record.lifecycleContractVersion !== 1
|
|
743
|
+
|| !isProviderPluginSurfaceId(record.surfaceId)
|
|
744
|
+
|| typeof record.authId !== "string"
|
|
745
|
+
|| !authIdPattern.test(record.authId)
|
|
746
|
+
|| (
|
|
747
|
+
record.initialSubjectState !== "bound"
|
|
748
|
+
&& record.initialSubjectState !== "unbound"
|
|
749
|
+
)
|
|
750
|
+
|| typeof record.phoneProvided !== "boolean"
|
|
751
|
+
|| (
|
|
752
|
+
record.phase !== "prepared"
|
|
753
|
+
&& record.phase !== "external-begun"
|
|
754
|
+
&& record.phase !== "external-completed"
|
|
755
|
+
&& record.phase !== "terminal"
|
|
756
|
+
)
|
|
757
|
+
|| (
|
|
758
|
+
record.status !== "pending"
|
|
759
|
+
&& record.status !== "succeeded"
|
|
760
|
+
&& record.status !== "safe-retry"
|
|
761
|
+
&& record.status !== "indeterminate"
|
|
762
|
+
)
|
|
763
|
+
|| (
|
|
764
|
+
record.reconciliation !== "not-required"
|
|
765
|
+
&& record.reconciliation !== "required"
|
|
766
|
+
&& record.reconciliation !== "resolved-applied"
|
|
767
|
+
&& record.reconciliation !== "resolved-not-applied"
|
|
768
|
+
)
|
|
769
|
+
) {
|
|
770
|
+
throw new Error("linked-device lifecycle journal is malformed");
|
|
771
|
+
}
|
|
772
|
+
const journal: LinkedDeviceLifecycleJournal = {
|
|
773
|
+
schemaVersion: LINKED_DEVICE_LIFECYCLE_JOURNAL_SCHEMA_VERSION,
|
|
774
|
+
revision: record.revision,
|
|
775
|
+
journalId: journalId(record.journalId),
|
|
776
|
+
kind: record.kind,
|
|
777
|
+
pluginId: record.pluginId,
|
|
778
|
+
pluginVersion: strictPluginVersion(record.pluginVersion),
|
|
779
|
+
pluginImplementationHash: digest(
|
|
780
|
+
record.pluginImplementationHash,
|
|
781
|
+
"linked-device lifecycle plugin implementation hash",
|
|
782
|
+
),
|
|
783
|
+
lifecycleContractVersion: 1,
|
|
784
|
+
surfaceId: record.surfaceId,
|
|
785
|
+
authId: record.authId,
|
|
786
|
+
authRealmHash: digest(
|
|
787
|
+
record.authRealmHash,
|
|
788
|
+
"linked-device lifecycle auth realm hash",
|
|
789
|
+
),
|
|
790
|
+
authContentHash: digest(
|
|
791
|
+
record.authContentHash,
|
|
792
|
+
"linked-device lifecycle auth content hash",
|
|
793
|
+
),
|
|
794
|
+
initialSubjectState: record.initialSubjectState,
|
|
795
|
+
phoneProvided: record.phoneProvided,
|
|
796
|
+
phase: record.phase,
|
|
797
|
+
status: record.status,
|
|
798
|
+
reconciliation: record.reconciliation,
|
|
799
|
+
reconciliationHash: nullableDigest(
|
|
800
|
+
record.reconciliationHash,
|
|
801
|
+
"linked-device lifecycle reconciliation hash",
|
|
802
|
+
),
|
|
803
|
+
owner: parseOwner(record.owner),
|
|
804
|
+
startedAt: canonicalTimestamp(
|
|
805
|
+
record.startedAt,
|
|
806
|
+
"linked-device lifecycle start",
|
|
807
|
+
),
|
|
808
|
+
updatedAt: canonicalTimestamp(
|
|
809
|
+
record.updatedAt,
|
|
810
|
+
"linked-device lifecycle update",
|
|
811
|
+
),
|
|
812
|
+
externalStartedAt: nullableTimestamp(
|
|
813
|
+
record.externalStartedAt,
|
|
814
|
+
"linked-device lifecycle external start",
|
|
815
|
+
),
|
|
816
|
+
externalCompletedAt: nullableTimestamp(
|
|
817
|
+
record.externalCompletedAt,
|
|
818
|
+
"linked-device lifecycle external completion",
|
|
819
|
+
),
|
|
820
|
+
finishedAt: nullableTimestamp(
|
|
821
|
+
record.finishedAt,
|
|
822
|
+
"linked-device lifecycle finish",
|
|
823
|
+
),
|
|
824
|
+
reconciledAt: nullableTimestamp(
|
|
825
|
+
record.reconciledAt,
|
|
826
|
+
"linked-device lifecycle reconciliation",
|
|
827
|
+
),
|
|
828
|
+
result: nullableResult(record.result),
|
|
829
|
+
reasonCode: reasonCode(record.reasonCode),
|
|
830
|
+
};
|
|
831
|
+
assertJournalInvariants(journal);
|
|
832
|
+
if (Buffer.byteLength(canonicalJson(journal), "utf8") > MAX_JOURNAL_BYTES) {
|
|
833
|
+
throw new Error("linked-device lifecycle journal exceeds its byte bound");
|
|
834
|
+
}
|
|
835
|
+
return Object.freeze({
|
|
836
|
+
...journal,
|
|
837
|
+
owner: Object.freeze({ ...journal.owner }),
|
|
838
|
+
...(journal.result === null
|
|
839
|
+
? { result: null }
|
|
840
|
+
: { result: Object.freeze({ ...journal.result }) }),
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
export function createLinkedDeviceLifecycleOwner(
|
|
845
|
+
leaseUntilValue: string,
|
|
846
|
+
): LinkedDeviceLifecycleOwner {
|
|
847
|
+
const identity = currentProcessStartIdentity();
|
|
848
|
+
return parseOwner({
|
|
849
|
+
pid: process.pid,
|
|
850
|
+
token: randomUUID(),
|
|
851
|
+
bootId: identity.bootId,
|
|
852
|
+
processStartId: identity.processStartId,
|
|
853
|
+
leaseUntil: leaseUntilValue,
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
export function initialLinkedDeviceLifecycleJournal(
|
|
858
|
+
value: StartLinkedDeviceLifecycleJournal,
|
|
859
|
+
): LinkedDeviceLifecycleJournal {
|
|
860
|
+
const input = dataRecord(
|
|
861
|
+
value,
|
|
862
|
+
"linked-device lifecycle journal start",
|
|
863
|
+
);
|
|
864
|
+
exactKeys(input, [
|
|
865
|
+
"journalId",
|
|
866
|
+
"kind",
|
|
867
|
+
"pluginId",
|
|
868
|
+
"pluginVersion",
|
|
869
|
+
"pluginImplementationHash",
|
|
870
|
+
"lifecycleContractVersion",
|
|
871
|
+
"surfaceId",
|
|
872
|
+
"authId",
|
|
873
|
+
"authRealmHash",
|
|
874
|
+
"authContentHash",
|
|
875
|
+
"initialSubjectState",
|
|
876
|
+
"phoneProvided",
|
|
877
|
+
"owner",
|
|
878
|
+
"startedAt",
|
|
879
|
+
], "linked-device lifecycle journal start");
|
|
880
|
+
return parseLinkedDeviceLifecycleJournal({
|
|
881
|
+
schemaVersion: LINKED_DEVICE_LIFECYCLE_JOURNAL_SCHEMA_VERSION,
|
|
882
|
+
revision: 0,
|
|
883
|
+
journalId: input.journalId,
|
|
884
|
+
kind: input.kind,
|
|
885
|
+
pluginId: input.pluginId,
|
|
886
|
+
pluginVersion: input.pluginVersion,
|
|
887
|
+
pluginImplementationHash: input.pluginImplementationHash,
|
|
888
|
+
lifecycleContractVersion: input.lifecycleContractVersion,
|
|
889
|
+
surfaceId: input.surfaceId,
|
|
890
|
+
authId: input.authId,
|
|
891
|
+
authRealmHash: input.authRealmHash,
|
|
892
|
+
authContentHash: input.authContentHash,
|
|
893
|
+
initialSubjectState: input.initialSubjectState,
|
|
894
|
+
phoneProvided: input.phoneProvided,
|
|
895
|
+
phase: "prepared",
|
|
896
|
+
status: "pending",
|
|
897
|
+
reconciliation: "not-required",
|
|
898
|
+
reconciliationHash: null,
|
|
899
|
+
owner: input.owner,
|
|
900
|
+
startedAt: input.startedAt,
|
|
901
|
+
updatedAt: input.startedAt,
|
|
902
|
+
externalStartedAt: null,
|
|
903
|
+
externalCompletedAt: null,
|
|
904
|
+
finishedAt: null,
|
|
905
|
+
reconciledAt: null,
|
|
906
|
+
result: null,
|
|
907
|
+
reasonCode: "prepared",
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function parseEvent(value: unknown): LinkedDeviceLifecycleJournalEvent {
|
|
912
|
+
const record = dataRecord(value, "linked-device lifecycle journal event");
|
|
913
|
+
if (record.type === "external-begin") {
|
|
914
|
+
exactKeys(record, ["type", "at"], "linked-device lifecycle journal event");
|
|
915
|
+
return {
|
|
916
|
+
type: "external-begin",
|
|
917
|
+
at: canonicalTimestamp(record.at, "linked-device lifecycle event time"),
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
if (record.type === "committed") {
|
|
921
|
+
exactKeys(
|
|
922
|
+
record,
|
|
923
|
+
["type", "result", "at"],
|
|
924
|
+
"linked-device lifecycle journal event",
|
|
925
|
+
);
|
|
926
|
+
return {
|
|
927
|
+
type: "committed",
|
|
928
|
+
result: parseResult(record.result),
|
|
929
|
+
at: canonicalTimestamp(record.at, "linked-device lifecycle event time"),
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
if (record.type === "external-complete") {
|
|
933
|
+
exactKeys(
|
|
934
|
+
record,
|
|
935
|
+
["type", "result", "at"],
|
|
936
|
+
"linked-device lifecycle journal event",
|
|
937
|
+
);
|
|
938
|
+
return {
|
|
939
|
+
type: "external-complete",
|
|
940
|
+
result: parseResult(record.result),
|
|
941
|
+
at: canonicalTimestamp(record.at, "linked-device lifecycle event time"),
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
if (record.type === "aborted-before-external") {
|
|
945
|
+
exactKeys(
|
|
946
|
+
record,
|
|
947
|
+
["type", "reasonCode", "at"],
|
|
948
|
+
"linked-device lifecycle journal event",
|
|
949
|
+
);
|
|
950
|
+
return {
|
|
951
|
+
type: "aborted-before-external",
|
|
952
|
+
reasonCode: preBeginReasonCode(record.reasonCode),
|
|
953
|
+
at: canonicalTimestamp(record.at, "linked-device lifecycle event time"),
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
if (record.type === "outcome-not-durable") {
|
|
957
|
+
exactKeys(
|
|
958
|
+
record,
|
|
959
|
+
["type", "reasonCode", "at"],
|
|
960
|
+
"linked-device lifecycle journal event",
|
|
961
|
+
);
|
|
962
|
+
return {
|
|
963
|
+
type: "outcome-not-durable",
|
|
964
|
+
reasonCode: postBeginReasonCode(record.reasonCode),
|
|
965
|
+
at: canonicalTimestamp(record.at, "linked-device lifecycle event time"),
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
if (record.type === "reconciled" && record.outcome === "applied") {
|
|
969
|
+
exactKeys(
|
|
970
|
+
record,
|
|
971
|
+
["type", "outcome", "evidenceHash", "result", "at"],
|
|
972
|
+
"linked-device lifecycle journal event",
|
|
973
|
+
);
|
|
974
|
+
return {
|
|
975
|
+
type: "reconciled",
|
|
976
|
+
outcome: "applied",
|
|
977
|
+
evidenceHash: digest(
|
|
978
|
+
record.evidenceHash,
|
|
979
|
+
"linked-device reconciliation evidence hash",
|
|
980
|
+
),
|
|
981
|
+
result: parseResult(record.result),
|
|
982
|
+
at: canonicalTimestamp(record.at, "linked-device lifecycle event time"),
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
if (record.type === "reconciled" && record.outcome === "not-applied") {
|
|
986
|
+
exactKeys(
|
|
987
|
+
record,
|
|
988
|
+
["type", "outcome", "evidenceHash", "at"],
|
|
989
|
+
"linked-device lifecycle journal event",
|
|
990
|
+
);
|
|
991
|
+
return {
|
|
992
|
+
type: "reconciled",
|
|
993
|
+
outcome: "not-applied",
|
|
994
|
+
evidenceHash: digest(
|
|
995
|
+
record.evidenceHash,
|
|
996
|
+
"linked-device reconciliation evidence hash",
|
|
997
|
+
),
|
|
998
|
+
at: canonicalTimestamp(record.at, "linked-device lifecycle event time"),
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
if (record.type === "lease-renewed") {
|
|
1002
|
+
exactKeys(
|
|
1003
|
+
record,
|
|
1004
|
+
["type", "leaseUntil", "at"],
|
|
1005
|
+
"linked-device lifecycle journal event",
|
|
1006
|
+
);
|
|
1007
|
+
return {
|
|
1008
|
+
type: "lease-renewed",
|
|
1009
|
+
leaseUntil: canonicalTimestamp(
|
|
1010
|
+
record.leaseUntil,
|
|
1011
|
+
"linked-device lifecycle renewed lease",
|
|
1012
|
+
),
|
|
1013
|
+
at: canonicalTimestamp(record.at, "linked-device lifecycle event time"),
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
throw new Error("linked-device lifecycle journal event is malformed");
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
function nextRevision(current: LinkedDeviceLifecycleJournal): number {
|
|
1020
|
+
if (current.revision >= Number.MAX_SAFE_INTEGER - 1) {
|
|
1021
|
+
throw new Error("linked-device lifecycle journal revision is exhausted");
|
|
1022
|
+
}
|
|
1023
|
+
return current.revision + 1;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
export function transitionLinkedDeviceLifecycleJournal(
|
|
1027
|
+
currentValue: LinkedDeviceLifecycleJournal,
|
|
1028
|
+
eventValue: LinkedDeviceLifecycleJournalEvent,
|
|
1029
|
+
): LinkedDeviceLifecycleJournal {
|
|
1030
|
+
const current = parseLinkedDeviceLifecycleJournal(currentValue);
|
|
1031
|
+
const event = parseEvent(eventValue);
|
|
1032
|
+
assertTimestampOrder(
|
|
1033
|
+
current.updatedAt,
|
|
1034
|
+
event.at,
|
|
1035
|
+
"linked-device lifecycle transition time",
|
|
1036
|
+
);
|
|
1037
|
+
const revision = nextRevision(current);
|
|
1038
|
+
if (event.type === "external-begin") {
|
|
1039
|
+
if (current.phase !== "prepared" || current.status !== "pending") {
|
|
1040
|
+
throw new Error("external begin is legal only from prepared lifecycle state");
|
|
1041
|
+
}
|
|
1042
|
+
return parseLinkedDeviceLifecycleJournal({
|
|
1043
|
+
...current,
|
|
1044
|
+
revision,
|
|
1045
|
+
phase: "external-begun",
|
|
1046
|
+
externalStartedAt: event.at,
|
|
1047
|
+
updatedAt: event.at,
|
|
1048
|
+
reasonCode: "external-outcome-pending",
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
if (event.type === "external-complete") {
|
|
1052
|
+
if (current.phase !== "external-begun" || current.status !== "pending") {
|
|
1053
|
+
throw new Error("external completion is legal only after external begin");
|
|
1054
|
+
}
|
|
1055
|
+
if (!resultMatchesJournalKind(event.result, current.kind)) {
|
|
1056
|
+
throw new Error("external result kind does not match the lifecycle journal");
|
|
1057
|
+
}
|
|
1058
|
+
return parseLinkedDeviceLifecycleJournal({
|
|
1059
|
+
...current,
|
|
1060
|
+
revision,
|
|
1061
|
+
phase: "external-completed",
|
|
1062
|
+
externalCompletedAt: event.at,
|
|
1063
|
+
result: event.result,
|
|
1064
|
+
updatedAt: event.at,
|
|
1065
|
+
reasonCode: "external-returned-commit-pending",
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
if (event.type === "committed") {
|
|
1069
|
+
if (
|
|
1070
|
+
current.phase !== "external-completed"
|
|
1071
|
+
|| current.status !== "pending"
|
|
1072
|
+
|| current.result === null
|
|
1073
|
+
) {
|
|
1074
|
+
throw new Error("lifecycle commit is legal only after external completion");
|
|
1075
|
+
}
|
|
1076
|
+
if (!sameResult(current.result, event.result)) {
|
|
1077
|
+
throw new Error(
|
|
1078
|
+
"lifecycle commit result does not match the externally completed result",
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
return parseLinkedDeviceLifecycleJournal({
|
|
1082
|
+
...current,
|
|
1083
|
+
revision,
|
|
1084
|
+
phase: "terminal",
|
|
1085
|
+
status: "succeeded",
|
|
1086
|
+
finishedAt: event.at,
|
|
1087
|
+
updatedAt: event.at,
|
|
1088
|
+
reasonCode: null,
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
if (event.type === "aborted-before-external") {
|
|
1092
|
+
if (current.phase !== "prepared" || current.status !== "pending") {
|
|
1093
|
+
throw new Error("safe abort is legal only before external begin");
|
|
1094
|
+
}
|
|
1095
|
+
return parseLinkedDeviceLifecycleJournal({
|
|
1096
|
+
...current,
|
|
1097
|
+
revision,
|
|
1098
|
+
phase: "terminal",
|
|
1099
|
+
status: "safe-retry",
|
|
1100
|
+
finishedAt: event.at,
|
|
1101
|
+
updatedAt: event.at,
|
|
1102
|
+
reasonCode: event.reasonCode,
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
if (event.type === "outcome-not-durable") {
|
|
1106
|
+
const returnedBeforeDurableBegin =
|
|
1107
|
+
current.phase === "prepared"
|
|
1108
|
+
&& event.reasonCode
|
|
1109
|
+
=== "external-returned-before-completion-persisted";
|
|
1110
|
+
if (
|
|
1111
|
+
(
|
|
1112
|
+
!returnedBeforeDurableBegin
|
|
1113
|
+
&& current.phase !== "external-begun"
|
|
1114
|
+
&& current.phase !== "external-completed"
|
|
1115
|
+
)
|
|
1116
|
+
|| current.status !== "pending"
|
|
1117
|
+
) {
|
|
1118
|
+
throw new Error(
|
|
1119
|
+
"indeterminate outcome is legal only after external begin or a returned external call",
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1122
|
+
return parseLinkedDeviceLifecycleJournal({
|
|
1123
|
+
...current,
|
|
1124
|
+
revision,
|
|
1125
|
+
phase: "terminal",
|
|
1126
|
+
status: "indeterminate",
|
|
1127
|
+
reconciliation: "required",
|
|
1128
|
+
externalStartedAt: returnedBeforeDurableBegin
|
|
1129
|
+
? event.at
|
|
1130
|
+
: current.externalStartedAt,
|
|
1131
|
+
finishedAt: event.at,
|
|
1132
|
+
updatedAt: event.at,
|
|
1133
|
+
reasonCode: event.reasonCode,
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
if (event.type === "reconciled") {
|
|
1137
|
+
if (
|
|
1138
|
+
current.phase !== "terminal"
|
|
1139
|
+
|| current.status !== "indeterminate"
|
|
1140
|
+
|| current.reconciliation !== "required"
|
|
1141
|
+
) {
|
|
1142
|
+
throw new Error(
|
|
1143
|
+
"reconciliation is legal only for an indeterminate lifecycle",
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
if (event.outcome === "applied") {
|
|
1147
|
+
if (!resultMatchesJournalKind(event.result, current.kind)) {
|
|
1148
|
+
throw new Error(
|
|
1149
|
+
"reconciled result kind does not match the lifecycle journal",
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1152
|
+
if (
|
|
1153
|
+
current.result !== null
|
|
1154
|
+
&& !sameResult(current.result, event.result)
|
|
1155
|
+
) {
|
|
1156
|
+
throw new Error(
|
|
1157
|
+
"reconciled result contradicts the previously completed result",
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
return parseLinkedDeviceLifecycleJournal({
|
|
1161
|
+
...current,
|
|
1162
|
+
revision,
|
|
1163
|
+
status: "succeeded",
|
|
1164
|
+
reconciliation: "resolved-applied",
|
|
1165
|
+
reconciliationHash: event.evidenceHash,
|
|
1166
|
+
reconciledAt: event.at,
|
|
1167
|
+
result: event.result,
|
|
1168
|
+
updatedAt: event.at,
|
|
1169
|
+
reasonCode: null,
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
if (
|
|
1173
|
+
current.externalCompletedAt !== null
|
|
1174
|
+
|| current.result !== null
|
|
1175
|
+
|| current.reasonCode
|
|
1176
|
+
=== "external-returned-before-completion-persisted"
|
|
1177
|
+
) {
|
|
1178
|
+
throw new Error(
|
|
1179
|
+
"not-applied reconciliation contradicts durable external completion",
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
return parseLinkedDeviceLifecycleJournal({
|
|
1183
|
+
...current,
|
|
1184
|
+
revision,
|
|
1185
|
+
status: "safe-retry",
|
|
1186
|
+
reconciliation: "resolved-not-applied",
|
|
1187
|
+
reconciliationHash: event.evidenceHash,
|
|
1188
|
+
reconciledAt: event.at,
|
|
1189
|
+
result: null,
|
|
1190
|
+
updatedAt: event.at,
|
|
1191
|
+
reasonCode: "reconciled-not-applied",
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
if (
|
|
1195
|
+
current.phase === "terminal"
|
|
1196
|
+
&& current.status !== "indeterminate"
|
|
1197
|
+
) {
|
|
1198
|
+
throw new Error("resolved linked-device lifecycle journals cannot renew");
|
|
1199
|
+
}
|
|
1200
|
+
assertTimestampOrder(
|
|
1201
|
+
event.at,
|
|
1202
|
+
event.leaseUntil,
|
|
1203
|
+
"linked-device lifecycle renewed lease",
|
|
1204
|
+
);
|
|
1205
|
+
assertTimestampOrder(
|
|
1206
|
+
current.owner.leaseUntil,
|
|
1207
|
+
event.leaseUntil,
|
|
1208
|
+
"linked-device lifecycle renewed lease",
|
|
1209
|
+
);
|
|
1210
|
+
return parseLinkedDeviceLifecycleJournal({
|
|
1211
|
+
...current,
|
|
1212
|
+
revision,
|
|
1213
|
+
owner: {
|
|
1214
|
+
...current.owner,
|
|
1215
|
+
leaseUntil: event.leaseUntil,
|
|
1216
|
+
},
|
|
1217
|
+
updatedAt: event.at,
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function journalDirectory(environment: Environment): string {
|
|
1222
|
+
return join(
|
|
1223
|
+
ghostgetStateHome(environment),
|
|
1224
|
+
...LINKED_DEVICE_LIFECYCLE_JOURNAL_STATE_DIRECTORY.split("/"),
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
function journalPath(journalIdValue: string, environment: Environment): string {
|
|
1229
|
+
return join(journalDirectory(environment), `${journalId(journalIdValue)}.json`);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
function snapshot(
|
|
1233
|
+
journalValue: LinkedDeviceLifecycleJournal,
|
|
1234
|
+
): LinkedDeviceLifecycleJournalSnapshot {
|
|
1235
|
+
const journal = parseLinkedDeviceLifecycleJournal(journalValue);
|
|
1236
|
+
const content = `${canonicalJson(journal)}\n`;
|
|
1237
|
+
return Object.freeze({
|
|
1238
|
+
journal,
|
|
1239
|
+
contentSha256: createHash("sha256").update(content).digest("hex"),
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
function assertSnapshot(
|
|
1244
|
+
value: LinkedDeviceLifecycleJournalSnapshot,
|
|
1245
|
+
): LinkedDeviceLifecycleJournalSnapshot {
|
|
1246
|
+
const record = dataRecord(
|
|
1247
|
+
value,
|
|
1248
|
+
"linked-device lifecycle journal snapshot",
|
|
1249
|
+
);
|
|
1250
|
+
exactKeys(
|
|
1251
|
+
record,
|
|
1252
|
+
["journal", "contentSha256"],
|
|
1253
|
+
"linked-device lifecycle journal snapshot",
|
|
1254
|
+
);
|
|
1255
|
+
if (
|
|
1256
|
+
typeof record.contentSha256 !== "string"
|
|
1257
|
+
|| !sha256Pattern.test(record.contentSha256)
|
|
1258
|
+
) {
|
|
1259
|
+
throw new Error("linked-device lifecycle journal snapshot is malformed");
|
|
1260
|
+
}
|
|
1261
|
+
const canonical = snapshot(
|
|
1262
|
+
parseLinkedDeviceLifecycleJournal(record.journal),
|
|
1263
|
+
);
|
|
1264
|
+
if (canonical.contentSha256 !== record.contentSha256) {
|
|
1265
|
+
throw new Error(
|
|
1266
|
+
"linked-device lifecycle journal snapshot is not content-bound",
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
return canonical;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
function parseSnapshotText(text: string): LinkedDeviceLifecycleJournalSnapshot {
|
|
1273
|
+
let value: unknown;
|
|
1274
|
+
try {
|
|
1275
|
+
value = JSON.parse(text) as unknown;
|
|
1276
|
+
} catch {
|
|
1277
|
+
throw new Error("linked-device lifecycle journal is malformed");
|
|
1278
|
+
}
|
|
1279
|
+
const parsed = parseLinkedDeviceLifecycleJournal(value);
|
|
1280
|
+
const canonical = `${canonicalJson(parsed)}\n`;
|
|
1281
|
+
if (text !== canonical) {
|
|
1282
|
+
throw new Error("linked-device lifecycle journal is not canonical JSON");
|
|
1283
|
+
}
|
|
1284
|
+
return Object.freeze({
|
|
1285
|
+
journal: parsed,
|
|
1286
|
+
contentSha256: createHash("sha256").update(text).digest("hex"),
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function sameOwnerAuthority(
|
|
1291
|
+
left: LinkedDeviceLifecycleOwner,
|
|
1292
|
+
right: LinkedDeviceLifecycleOwner,
|
|
1293
|
+
): boolean {
|
|
1294
|
+
return left.pid === right.pid
|
|
1295
|
+
&& left.token === right.token
|
|
1296
|
+
&& left.bootId === right.bootId
|
|
1297
|
+
&& left.processStartId === right.processStartId;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
function isCurrentProcessOwner(owner: ProcessOwnerIdentity): boolean {
|
|
1301
|
+
const identity = currentProcessStartIdentity();
|
|
1302
|
+
return owner.pid === process.pid
|
|
1303
|
+
&& owner.bootId === identity.bootId
|
|
1304
|
+
&& owner.processStartId === identity.processStartId;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
function requireExactLiveOwner(
|
|
1308
|
+
ownerValue: LinkedDeviceLifecycleOwner,
|
|
1309
|
+
): LinkedDeviceLifecycleOwner {
|
|
1310
|
+
const owner = parseOwner(ownerValue);
|
|
1311
|
+
if (!isCurrentProcessOwner(owner)) {
|
|
1312
|
+
throw new Error(
|
|
1313
|
+
"linked-device lifecycle write owner is not the current process",
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
if (processOwnerStatus(owner) !== "exact-live-owner") {
|
|
1317
|
+
throw new Error("linked-device lifecycle write owner is not exact and live");
|
|
1318
|
+
}
|
|
1319
|
+
return owner;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function requireWriteAuthority(
|
|
1323
|
+
journal: LinkedDeviceLifecycleJournal,
|
|
1324
|
+
authority: LinkedDeviceLifecycleWriteAuthority,
|
|
1325
|
+
): LinkedDeviceLifecycleOwner {
|
|
1326
|
+
const owner = requireExactLiveOwner(authority.owner);
|
|
1327
|
+
if (!sameOwnerAuthority(journal.owner, owner)) {
|
|
1328
|
+
throw new Error("linked-device lifecycle write authority does not own the journal");
|
|
1329
|
+
}
|
|
1330
|
+
return owner;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
export function createLinkedDeviceLifecycleJournal(
|
|
1334
|
+
journalValue: LinkedDeviceLifecycleJournal,
|
|
1335
|
+
environment: Environment = process.env,
|
|
1336
|
+
): LinkedDeviceLifecycleJournalSnapshot {
|
|
1337
|
+
const journal = parseLinkedDeviceLifecycleJournal(journalValue);
|
|
1338
|
+
if (journal.revision !== 0 || journal.phase !== "prepared") {
|
|
1339
|
+
throw new Error(
|
|
1340
|
+
"new linked-device lifecycle journals must start prepared at revision zero",
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
requireExactLiveOwner(journal.owner);
|
|
1344
|
+
ensurePrivateStateDirectory(journalDirectory(environment), environment);
|
|
1345
|
+
const created = createPrivateJsonIfAbsent(
|
|
1346
|
+
journalPath(journal.journalId, environment),
|
|
1347
|
+
journal,
|
|
1348
|
+
{ environment, privateParent: true },
|
|
1349
|
+
);
|
|
1350
|
+
if (!created.created) {
|
|
1351
|
+
throw new Error("linked-device lifecycle journal ID already exists");
|
|
1352
|
+
}
|
|
1353
|
+
return snapshot(journal);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
export function readLinkedDeviceLifecycleJournal(
|
|
1357
|
+
journalIdValue: string,
|
|
1358
|
+
environment: Environment = process.env,
|
|
1359
|
+
): LinkedDeviceLifecycleJournalSnapshot | null {
|
|
1360
|
+
ensurePrivateStateDirectory(journalDirectory(environment), environment);
|
|
1361
|
+
const text = readPrivateStateFileIfPresent(
|
|
1362
|
+
journalPath(journalIdValue, environment),
|
|
1363
|
+
MAX_JOURNAL_BYTES,
|
|
1364
|
+
"linked-device lifecycle journal",
|
|
1365
|
+
environment,
|
|
1366
|
+
);
|
|
1367
|
+
return text === null ? null : parseSnapshotText(text);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
export function updateLinkedDeviceLifecycleJournal(
|
|
1371
|
+
currentValue: LinkedDeviceLifecycleJournalSnapshot,
|
|
1372
|
+
event: LinkedDeviceLifecycleJournalEvent,
|
|
1373
|
+
authority: LinkedDeviceLifecycleWriteAuthority,
|
|
1374
|
+
): LinkedDeviceLifecycleJournalSnapshot {
|
|
1375
|
+
const current = assertSnapshot(currentValue);
|
|
1376
|
+
requireWriteAuthority(current.journal, authority);
|
|
1377
|
+
const next = transitionLinkedDeviceLifecycleJournal(current.journal, event);
|
|
1378
|
+
const written = writePrivateJsonIfUnchanged(
|
|
1379
|
+
journalPath(
|
|
1380
|
+
current.journal.journalId,
|
|
1381
|
+
authority.environment ?? process.env,
|
|
1382
|
+
),
|
|
1383
|
+
next,
|
|
1384
|
+
{ expectedCurrentContentSha256: current.contentSha256 },
|
|
1385
|
+
);
|
|
1386
|
+
if (!written) {
|
|
1387
|
+
throw new Error(
|
|
1388
|
+
"linked-device lifecycle journal changed concurrently; reload before continuing",
|
|
1389
|
+
);
|
|
1390
|
+
}
|
|
1391
|
+
return snapshot(next);
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
export function listLinkedDeviceLifecycleJournalSnapshots(
|
|
1395
|
+
environment: Environment = process.env,
|
|
1396
|
+
): readonly LinkedDeviceLifecycleJournalListEntry[] {
|
|
1397
|
+
const directory = journalDirectory(environment);
|
|
1398
|
+
const identity = ensurePrivateStateDirectory(directory, environment);
|
|
1399
|
+
const directorySnapshot = snapshotPrivateStateDirectory(
|
|
1400
|
+
directory,
|
|
1401
|
+
environment,
|
|
1402
|
+
identity,
|
|
1403
|
+
);
|
|
1404
|
+
if (directorySnapshot.identity === null) return Object.freeze([]);
|
|
1405
|
+
const expectedNamePattern =
|
|
1406
|
+
/^([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})\.json$/u;
|
|
1407
|
+
const fileNames = directorySnapshot.entries
|
|
1408
|
+
.filter((entry) =>
|
|
1409
|
+
entry.kind === "file"
|
|
1410
|
+
&& expectedNamePattern.test(entry.name))
|
|
1411
|
+
.map((entry) => entry.name);
|
|
1412
|
+
const fileByName = new Map(
|
|
1413
|
+
readPrivateStateFilesBatched(directory, fileNames, {
|
|
1414
|
+
maximumBytesPerFile: MAX_JOURNAL_BYTES,
|
|
1415
|
+
environment,
|
|
1416
|
+
expectedDirectoryIdentity: directorySnapshot.identity,
|
|
1417
|
+
}).map((file) => [file.name, file] as const),
|
|
1418
|
+
);
|
|
1419
|
+
return Object.freeze(directorySnapshot.entries.map((entry) => {
|
|
1420
|
+
const match = expectedNamePattern.exec(entry.name);
|
|
1421
|
+
if (match === null || match[1] === undefined) {
|
|
1422
|
+
return {
|
|
1423
|
+
journalId: `invalid-${
|
|
1424
|
+
createHash("sha256")
|
|
1425
|
+
.update("linked-device-lifecycle-invalid-entry\0", "utf8")
|
|
1426
|
+
.update(entry.name, "utf8")
|
|
1427
|
+
.digest("hex")
|
|
1428
|
+
}`,
|
|
1429
|
+
invalid: true as const,
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
const id = match[1];
|
|
1433
|
+
if (entry.kind !== "file") {
|
|
1434
|
+
return { journalId: id, invalid: true as const };
|
|
1435
|
+
}
|
|
1436
|
+
const file = fileByName.get(entry.name);
|
|
1437
|
+
if (file === undefined || file.status !== "present") {
|
|
1438
|
+
return { journalId: id, invalid: true as const };
|
|
1439
|
+
}
|
|
1440
|
+
try {
|
|
1441
|
+
const parsed = parseSnapshotText(file.content);
|
|
1442
|
+
return parsed.journal.journalId === id
|
|
1443
|
+
? parsed
|
|
1444
|
+
: { journalId: id, invalid: true as const };
|
|
1445
|
+
} catch {
|
|
1446
|
+
return { journalId: id, invalid: true as const };
|
|
1447
|
+
}
|
|
1448
|
+
}));
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
export function classifyLinkedDeviceLifecycleRestart(
|
|
1452
|
+
journalValue: LinkedDeviceLifecycleJournal,
|
|
1453
|
+
inspectOwner: OwnerInspector = processOwnerStatus,
|
|
1454
|
+
): LinkedDeviceLifecycleRestartDisposition {
|
|
1455
|
+
const journal = parseLinkedDeviceLifecycleJournal(journalValue);
|
|
1456
|
+
if (journal.phase === "terminal" && journal.status !== "indeterminate") {
|
|
1457
|
+
return Object.freeze({
|
|
1458
|
+
kind: "complete",
|
|
1459
|
+
reason: "the linked-device lifecycle already has a resolved terminal outcome",
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
const ownerStatus = inspectOwner(journal.owner);
|
|
1463
|
+
if (ownerStatus === "exact-live-owner") {
|
|
1464
|
+
return Object.freeze({
|
|
1465
|
+
kind: "live-owner",
|
|
1466
|
+
reason: "the exact process owner is still live; restart repair is forbidden",
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
if (ownerStatus === "unknown") {
|
|
1470
|
+
return Object.freeze({
|
|
1471
|
+
kind: "owner-unknown",
|
|
1472
|
+
reason: "process ownership is unknown; restart repair must fail closed",
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
if (journal.phase === "prepared") {
|
|
1476
|
+
return Object.freeze({
|
|
1477
|
+
kind: "safe-retry",
|
|
1478
|
+
reason: "the previous owner ended before the durable external begin boundary",
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
return Object.freeze({
|
|
1482
|
+
kind: "reconciliation-required",
|
|
1483
|
+
reason: "the durable external begin boundary was crossed without a resolved outcome",
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
export function repairInterruptedLinkedDeviceLifecycleJournal(
|
|
1488
|
+
currentValue: LinkedDeviceLifecycleJournalSnapshot,
|
|
1489
|
+
options: {
|
|
1490
|
+
readonly owner: LinkedDeviceLifecycleOwner;
|
|
1491
|
+
readonly at: string;
|
|
1492
|
+
readonly environment?: Environment;
|
|
1493
|
+
},
|
|
1494
|
+
): LinkedDeviceLifecycleJournalSnapshot {
|
|
1495
|
+
const current = assertSnapshot(currentValue);
|
|
1496
|
+
const at = canonicalTimestamp(
|
|
1497
|
+
options.at,
|
|
1498
|
+
"linked-device lifecycle repair time",
|
|
1499
|
+
);
|
|
1500
|
+
assertTimestampOrder(
|
|
1501
|
+
current.journal.updatedAt,
|
|
1502
|
+
at,
|
|
1503
|
+
"linked-device lifecycle repair time",
|
|
1504
|
+
);
|
|
1505
|
+
const repairOwner = requireExactLiveOwner(options.owner);
|
|
1506
|
+
assertTimestampOrder(
|
|
1507
|
+
at,
|
|
1508
|
+
repairOwner.leaseUntil,
|
|
1509
|
+
"linked-device lifecycle repair owner lease",
|
|
1510
|
+
);
|
|
1511
|
+
const disposition = classifyLinkedDeviceLifecycleRestart(
|
|
1512
|
+
current.journal,
|
|
1513
|
+
processOwnerStatus,
|
|
1514
|
+
);
|
|
1515
|
+
if (
|
|
1516
|
+
disposition.kind !== "safe-retry"
|
|
1517
|
+
&& disposition.kind !== "reconciliation-required"
|
|
1518
|
+
) {
|
|
1519
|
+
throw new Error(`linked-device lifecycle cannot be repaired: ${disposition.reason}`);
|
|
1520
|
+
}
|
|
1521
|
+
const revision = nextRevision(current.journal);
|
|
1522
|
+
const next = disposition.kind === "safe-retry"
|
|
1523
|
+
? parseLinkedDeviceLifecycleJournal({
|
|
1524
|
+
...current.journal,
|
|
1525
|
+
revision,
|
|
1526
|
+
phase: "terminal",
|
|
1527
|
+
status: "safe-retry",
|
|
1528
|
+
owner: repairOwner,
|
|
1529
|
+
finishedAt: at,
|
|
1530
|
+
updatedAt: at,
|
|
1531
|
+
reasonCode: "owner-exited-before-begin",
|
|
1532
|
+
})
|
|
1533
|
+
: parseLinkedDeviceLifecycleJournal({
|
|
1534
|
+
...current.journal,
|
|
1535
|
+
revision,
|
|
1536
|
+
phase: "terminal",
|
|
1537
|
+
status: "indeterminate",
|
|
1538
|
+
reconciliation: "required",
|
|
1539
|
+
owner: repairOwner,
|
|
1540
|
+
finishedAt: current.journal.finishedAt ?? at,
|
|
1541
|
+
updatedAt: at,
|
|
1542
|
+
reasonCode: current.journal.status === "indeterminate"
|
|
1543
|
+
? current.journal.reasonCode
|
|
1544
|
+
: "owner-exited-after-begin",
|
|
1545
|
+
});
|
|
1546
|
+
const written = writePrivateJsonIfUnchanged(
|
|
1547
|
+
journalPath(
|
|
1548
|
+
current.journal.journalId,
|
|
1549
|
+
options.environment ?? process.env,
|
|
1550
|
+
),
|
|
1551
|
+
next,
|
|
1552
|
+
{ expectedCurrentContentSha256: current.contentSha256 },
|
|
1553
|
+
);
|
|
1554
|
+
if (!written) {
|
|
1555
|
+
throw new Error(
|
|
1556
|
+
"linked-device lifecycle journal changed concurrently; reload before repair",
|
|
1557
|
+
);
|
|
1558
|
+
}
|
|
1559
|
+
return snapshot(next);
|
|
1560
|
+
}
|