@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,1272 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { GhostgetAuth } from "./auth";
|
|
5
|
+
import { canonicalJson } from "./canonical-json";
|
|
6
|
+
import type { OperationRisk } from "./model";
|
|
7
|
+
import {
|
|
8
|
+
isProviderPluginOperationName,
|
|
9
|
+
type ProviderPluginOperationName,
|
|
10
|
+
} from "./provider-plugin-identifiers";
|
|
11
|
+
import {
|
|
12
|
+
parsePortableOperationIdentityV1,
|
|
13
|
+
type PortableOperationIdentityV1,
|
|
14
|
+
} from "./provider-plugin-portable-identity";
|
|
15
|
+
import {
|
|
16
|
+
parseLocalCliContractIdentityV1,
|
|
17
|
+
type LocalCliContractIdentityV1,
|
|
18
|
+
} from "./local-cli-contracts";
|
|
19
|
+
import {
|
|
20
|
+
createPrivateJsonIfAbsent,
|
|
21
|
+
ensurePrivateStateDirectory,
|
|
22
|
+
ghostgetStateHome,
|
|
23
|
+
readPrivateStateFilesBatched,
|
|
24
|
+
readPrivateStateFileIfPresent,
|
|
25
|
+
snapshotPrivateStateDirectory,
|
|
26
|
+
writePrivateJsonIfUnchanged,
|
|
27
|
+
} from "./storage";
|
|
28
|
+
import {
|
|
29
|
+
processOwnerStatus,
|
|
30
|
+
type ProcessOwnerStatus,
|
|
31
|
+
} from "./process-identity";
|
|
32
|
+
|
|
33
|
+
type Environment = Readonly<Record<string, string | undefined>>;
|
|
34
|
+
type JsonRecord = Record<string, unknown>;
|
|
35
|
+
|
|
36
|
+
const RUN_JOURNAL_DIRECTORY = "run-journals";
|
|
37
|
+
const MAX_RUN_JOURNAL_BYTES = 64 * 1024;
|
|
38
|
+
const MAX_JOURNAL_ERROR_BYTES = 16 * 1024;
|
|
39
|
+
const MAX_JOURNAL_ORIGIN_BYTES = 8 * 1024;
|
|
40
|
+
|
|
41
|
+
export type RunJournalContract =
|
|
42
|
+
| {
|
|
43
|
+
readonly transport: "provider-api";
|
|
44
|
+
readonly hash: string;
|
|
45
|
+
}
|
|
46
|
+
| {
|
|
47
|
+
readonly transport: "web-session-api";
|
|
48
|
+
readonly hash: string;
|
|
49
|
+
}
|
|
50
|
+
| {
|
|
51
|
+
readonly transport: "reviewed-template-api";
|
|
52
|
+
readonly hash: string;
|
|
53
|
+
}
|
|
54
|
+
| {
|
|
55
|
+
readonly transport: "portable-provider-plugin";
|
|
56
|
+
readonly identity: PortableOperationIdentityV1;
|
|
57
|
+
}
|
|
58
|
+
| {
|
|
59
|
+
readonly transport: "local-cli";
|
|
60
|
+
readonly identity: LocalCliContractIdentityV1;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type RunJournalDispatch = {
|
|
64
|
+
readonly planned: number;
|
|
65
|
+
readonly started: number;
|
|
66
|
+
readonly verified: number;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type DuplicateIntentV1 = {
|
|
70
|
+
readonly schemaVersion: 1;
|
|
71
|
+
readonly intentHash: string;
|
|
72
|
+
readonly sourceRunId: string;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export type DuplicateSuccessorV1 = DuplicateIntentV1 & {
|
|
76
|
+
readonly runId: string;
|
|
77
|
+
readonly claimedAt: string;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export type RunJournal = {
|
|
81
|
+
readonly schemaVersion: 1;
|
|
82
|
+
readonly revision: number;
|
|
83
|
+
readonly runId: string;
|
|
84
|
+
readonly planDigest: string;
|
|
85
|
+
readonly adapter: {
|
|
86
|
+
readonly id: string;
|
|
87
|
+
readonly version: string;
|
|
88
|
+
readonly hash: string;
|
|
89
|
+
};
|
|
90
|
+
readonly operation: ProviderPluginOperationName;
|
|
91
|
+
readonly risk: Extract<OperationRisk, "R2" | "R3">;
|
|
92
|
+
readonly inputHash: string;
|
|
93
|
+
readonly auth: {
|
|
94
|
+
readonly id: string;
|
|
95
|
+
readonly hash: string;
|
|
96
|
+
readonly kind: GhostgetAuth["kind"];
|
|
97
|
+
};
|
|
98
|
+
readonly contract: RunJournalContract;
|
|
99
|
+
readonly duplicateIntent?: DuplicateIntentV1;
|
|
100
|
+
readonly duplicateSuccessor?: DuplicateSuccessorV1;
|
|
101
|
+
readonly planHasAssets: boolean;
|
|
102
|
+
/** Whether the encrypted confirmation plan is still independently usable. */
|
|
103
|
+
readonly planState: "available" | "consumed";
|
|
104
|
+
readonly phase: "prepared" | "claimed" | "ready" | "dispatching" | "terminal";
|
|
105
|
+
readonly status: "pending" | "succeeded" | "submitted" | "failed" | "partial" | "indeterminate";
|
|
106
|
+
readonly dispatch: RunJournalDispatch;
|
|
107
|
+
readonly ledgerRelativePath: string | null;
|
|
108
|
+
readonly ledgerState: "unclaimed" | "pending" | "succeeded" | "partial" | "indeterminate" | "released";
|
|
109
|
+
readonly recoveryState: "absent" | "present" | "retained" | "released";
|
|
110
|
+
readonly assetState: "none" | "bound" | "retained" | "released";
|
|
111
|
+
readonly owner: {
|
|
112
|
+
readonly pid: number;
|
|
113
|
+
readonly token: string;
|
|
114
|
+
readonly bootId: string;
|
|
115
|
+
readonly processStartId: string;
|
|
116
|
+
readonly leaseUntil: string;
|
|
117
|
+
};
|
|
118
|
+
readonly startedAt: string;
|
|
119
|
+
readonly updatedAt: string;
|
|
120
|
+
readonly dedupeExpiresAt: string;
|
|
121
|
+
readonly finalOrigin: string | null;
|
|
122
|
+
readonly error: string | null;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export type RunJournalSnapshot = {
|
|
126
|
+
readonly journal: RunJournal;
|
|
127
|
+
readonly contentSha256: string;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export type StartRunJournal = {
|
|
131
|
+
readonly runId: string;
|
|
132
|
+
readonly planDigest: string;
|
|
133
|
+
readonly adapter: RunJournal["adapter"];
|
|
134
|
+
readonly operation: ProviderPluginOperationName;
|
|
135
|
+
readonly risk: Extract<OperationRisk, "R2" | "R3">;
|
|
136
|
+
readonly inputHash: string;
|
|
137
|
+
readonly auth: RunJournal["auth"];
|
|
138
|
+
readonly contract: RunJournalContract;
|
|
139
|
+
readonly duplicateIntent?: DuplicateIntentV1;
|
|
140
|
+
readonly plannedDispatches: number;
|
|
141
|
+
readonly hasPlanAssets: boolean;
|
|
142
|
+
readonly owner: RunJournal["owner"];
|
|
143
|
+
readonly startedAt: string;
|
|
144
|
+
readonly dedupeExpiresAt: string;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export type RunJournalEvent =
|
|
148
|
+
| {
|
|
149
|
+
readonly type: "confirmation-consumed";
|
|
150
|
+
readonly at: string;
|
|
151
|
+
}
|
|
152
|
+
| {
|
|
153
|
+
readonly type: "ledger-claimed";
|
|
154
|
+
readonly ledgerRelativePath: string;
|
|
155
|
+
readonly at: string;
|
|
156
|
+
}
|
|
157
|
+
| {
|
|
158
|
+
readonly type: "recovery-stored";
|
|
159
|
+
readonly at: string;
|
|
160
|
+
}
|
|
161
|
+
| {
|
|
162
|
+
readonly type: "dispatch-started";
|
|
163
|
+
readonly index: number;
|
|
164
|
+
readonly at: string;
|
|
165
|
+
}
|
|
166
|
+
| {
|
|
167
|
+
readonly type: "dispatch-verified";
|
|
168
|
+
readonly index: number;
|
|
169
|
+
readonly at: string;
|
|
170
|
+
}
|
|
171
|
+
| {
|
|
172
|
+
readonly type: "finished";
|
|
173
|
+
readonly status: "succeeded" | "submitted" | "failed" | "partial" | "indeterminate";
|
|
174
|
+
readonly finalOrigin: string | null;
|
|
175
|
+
readonly error: string | null;
|
|
176
|
+
readonly noOp?: true;
|
|
177
|
+
readonly at: string;
|
|
178
|
+
}
|
|
179
|
+
| {
|
|
180
|
+
/** A successful read-only reconciliation authorized local cleanup. */
|
|
181
|
+
readonly type: "recovery-released";
|
|
182
|
+
/**
|
|
183
|
+
* Explicit not-applied evidence also releases the idempotency ledger,
|
|
184
|
+
* authorizing a fresh confirmed attempt. Omitted means the provider
|
|
185
|
+
* effect was observed/applied and the at-most-once fence remains.
|
|
186
|
+
*/
|
|
187
|
+
readonly outcome?: "applied" | "not-applied";
|
|
188
|
+
readonly at: string;
|
|
189
|
+
}
|
|
190
|
+
| {
|
|
191
|
+
readonly type: "lease-renewed";
|
|
192
|
+
readonly leaseUntil: string;
|
|
193
|
+
readonly at: string;
|
|
194
|
+
}
|
|
195
|
+
| {
|
|
196
|
+
/** Permanently elect the sole duplicate-tolerant successor intent. */
|
|
197
|
+
readonly type: "duplicate-successor-claimed";
|
|
198
|
+
readonly intentHash: string;
|
|
199
|
+
readonly runId: string;
|
|
200
|
+
readonly at: string;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
function dataRecord(value: unknown, label: string): JsonRecord {
|
|
204
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
205
|
+
throw new Error(`${label} must be an object`);
|
|
206
|
+
}
|
|
207
|
+
const prototype: unknown = Object.getPrototypeOf(value) as unknown;
|
|
208
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
209
|
+
throw new Error(`${label} has an unsupported prototype`);
|
|
210
|
+
}
|
|
211
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
212
|
+
if (Reflect.ownKeys(descriptors).some((key) => typeof key !== "string")) {
|
|
213
|
+
throw new Error(`${label} has unsupported symbol fields`);
|
|
214
|
+
}
|
|
215
|
+
const result: JsonRecord = {};
|
|
216
|
+
for (const [key, descriptor] of Object.entries(descriptors)) {
|
|
217
|
+
if (!descriptor.enumerable || !("value" in descriptor)) {
|
|
218
|
+
throw new Error(`${label} has unsupported accessor fields`);
|
|
219
|
+
}
|
|
220
|
+
Object.defineProperty(result, key, {
|
|
221
|
+
value: descriptor.value,
|
|
222
|
+
enumerable: true,
|
|
223
|
+
configurable: false,
|
|
224
|
+
writable: false,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function exactKeys(
|
|
231
|
+
value: JsonRecord,
|
|
232
|
+
expected: readonly string[],
|
|
233
|
+
label: string,
|
|
234
|
+
): void {
|
|
235
|
+
const actual = Object.keys(value).sort();
|
|
236
|
+
const sortedExpected = [...expected].sort();
|
|
237
|
+
if (
|
|
238
|
+
actual.length !== sortedExpected.length
|
|
239
|
+
|| actual.some((key, index) => key !== sortedExpected[index])
|
|
240
|
+
) {
|
|
241
|
+
throw new Error(`${label} has unsupported fields`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function timestamp(value: unknown, label: string): string {
|
|
246
|
+
if (
|
|
247
|
+
typeof value !== "string"
|
|
248
|
+
|| value.length > 64
|
|
249
|
+
|| !Number.isFinite(Date.parse(value))
|
|
250
|
+
) {
|
|
251
|
+
throw new Error(`${label} is malformed`);
|
|
252
|
+
}
|
|
253
|
+
return value;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function digest(value: unknown, label: string): string {
|
|
257
|
+
if (typeof value !== "string" || !/^[a-f0-9]{64}$/u.test(value)) {
|
|
258
|
+
throw new Error(`${label} is malformed`);
|
|
259
|
+
}
|
|
260
|
+
return value;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function runId(value: unknown, label: string): string {
|
|
264
|
+
if (
|
|
265
|
+
typeof value !== "string"
|
|
266
|
+
|| !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u.test(value)
|
|
267
|
+
) throw new Error(`${label} is malformed`);
|
|
268
|
+
return value;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function parseDuplicateIntent(
|
|
272
|
+
value: unknown,
|
|
273
|
+
label: string,
|
|
274
|
+
): DuplicateIntentV1 {
|
|
275
|
+
const record = dataRecord(value, label);
|
|
276
|
+
exactKeys(record, ["schemaVersion", "intentHash", "sourceRunId"], label);
|
|
277
|
+
if (record.schemaVersion !== 1) throw new Error(`${label} is malformed`);
|
|
278
|
+
return Object.freeze({
|
|
279
|
+
schemaVersion: 1,
|
|
280
|
+
intentHash: digest(record.intentHash, `${label} intent hash`),
|
|
281
|
+
sourceRunId: runId(record.sourceRunId, `${label} source run ID`),
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function parseDuplicateSuccessor(
|
|
286
|
+
value: unknown,
|
|
287
|
+
): DuplicateSuccessorV1 {
|
|
288
|
+
const label = "run journal duplicate successor";
|
|
289
|
+
const record = dataRecord(value, label);
|
|
290
|
+
exactKeys(
|
|
291
|
+
record,
|
|
292
|
+
["schemaVersion", "intentHash", "sourceRunId", "runId", "claimedAt"],
|
|
293
|
+
label,
|
|
294
|
+
);
|
|
295
|
+
if (record.schemaVersion !== 1) throw new Error(`${label} is malformed`);
|
|
296
|
+
return Object.freeze({
|
|
297
|
+
schemaVersion: 1,
|
|
298
|
+
intentHash: digest(record.intentHash, `${label} intent hash`),
|
|
299
|
+
sourceRunId: runId(record.sourceRunId, `${label} source run ID`),
|
|
300
|
+
runId: runId(record.runId, `${label} run ID`),
|
|
301
|
+
claimedAt: timestamp(record.claimedAt, `${label} claim time`),
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function boundedString(
|
|
306
|
+
value: unknown,
|
|
307
|
+
label: string,
|
|
308
|
+
maximumBytes: number,
|
|
309
|
+
): string {
|
|
310
|
+
const hasUnsafeControl = typeof value === "string"
|
|
311
|
+
&& [...value].some((character) => {
|
|
312
|
+
const codePoint = character.codePointAt(0);
|
|
313
|
+
return codePoint !== undefined
|
|
314
|
+
&& (
|
|
315
|
+
codePoint <= 0x08
|
|
316
|
+
|| codePoint === 0x0b
|
|
317
|
+
|| codePoint === 0x0c
|
|
318
|
+
|| (codePoint >= 0x0e && codePoint <= 0x1f)
|
|
319
|
+
|| codePoint === 0x7f
|
|
320
|
+
);
|
|
321
|
+
});
|
|
322
|
+
if (
|
|
323
|
+
typeof value !== "string"
|
|
324
|
+
|| value.length < 1
|
|
325
|
+
|| Buffer.byteLength(value, "utf8") > maximumBytes
|
|
326
|
+
|| hasUnsafeControl
|
|
327
|
+
) {
|
|
328
|
+
throw new Error(`${label} is malformed`);
|
|
329
|
+
}
|
|
330
|
+
return value;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function nullableBoundedString(
|
|
334
|
+
value: unknown,
|
|
335
|
+
label: string,
|
|
336
|
+
maximumBytes: number,
|
|
337
|
+
): string | null {
|
|
338
|
+
return value === null ? null : boundedString(value, label, maximumBytes);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function parseAdapter(value: unknown): RunJournal["adapter"] {
|
|
342
|
+
const record = dataRecord(value, "run journal adapter");
|
|
343
|
+
exactKeys(record, ["id", "version", "hash"], "run journal adapter");
|
|
344
|
+
const id = boundedString(record.id, "run journal adapter ID", 128);
|
|
345
|
+
const version = boundedString(record.version, "run journal adapter version", 128);
|
|
346
|
+
if (!/^[a-z][a-z0-9-]{0,127}$/u.test(id)) {
|
|
347
|
+
throw new Error("run journal adapter ID is malformed");
|
|
348
|
+
}
|
|
349
|
+
return { id, version, hash: digest(record.hash, "run journal adapter hash") };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const authKinds = new Set<GhostgetAuth["kind"]>([
|
|
353
|
+
"browser-profile",
|
|
354
|
+
"cookie-source",
|
|
355
|
+
"cookies-file",
|
|
356
|
+
"linked-device-store",
|
|
357
|
+
"oauth-token-file",
|
|
358
|
+
]);
|
|
359
|
+
|
|
360
|
+
function parseAuth(value: unknown): RunJournal["auth"] {
|
|
361
|
+
const record = dataRecord(value, "run journal auth");
|
|
362
|
+
exactKeys(record, ["id", "hash", "kind"], "run journal auth");
|
|
363
|
+
const id = boundedString(record.id, "run journal auth ID", 48);
|
|
364
|
+
if (!/^[a-z][a-z0-9-]{0,47}$/u.test(id) || !authKinds.has(record.kind as GhostgetAuth["kind"])) {
|
|
365
|
+
throw new Error("run journal auth is malformed");
|
|
366
|
+
}
|
|
367
|
+
return {
|
|
368
|
+
id,
|
|
369
|
+
hash: digest(record.hash, "run journal auth hash"),
|
|
370
|
+
kind: record.kind as GhostgetAuth["kind"],
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function parseContract(value: unknown): RunJournalContract {
|
|
375
|
+
const record = dataRecord(value, "run journal contract");
|
|
376
|
+
if (record.transport === "portable-provider-plugin") {
|
|
377
|
+
exactKeys(
|
|
378
|
+
record,
|
|
379
|
+
["transport", "identity"],
|
|
380
|
+
"run journal contract",
|
|
381
|
+
);
|
|
382
|
+
return Object.freeze({
|
|
383
|
+
transport: "portable-provider-plugin",
|
|
384
|
+
identity: parsePortableOperationIdentityV1(record.identity),
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
if (record.transport === "local-cli") {
|
|
388
|
+
exactKeys(record, ["transport", "identity"], "run journal contract");
|
|
389
|
+
return Object.freeze({
|
|
390
|
+
transport: "local-cli",
|
|
391
|
+
identity: parseLocalCliContractIdentityV1(record.identity),
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
exactKeys(record, ["transport", "hash"], "run journal contract");
|
|
395
|
+
if (
|
|
396
|
+
record.transport !== "provider-api"
|
|
397
|
+
&& record.transport !== "web-session-api"
|
|
398
|
+
&& record.transport !== "reviewed-template-api"
|
|
399
|
+
) {
|
|
400
|
+
throw new Error("run journal contract transport is malformed");
|
|
401
|
+
}
|
|
402
|
+
return Object.freeze({
|
|
403
|
+
transport: record.transport,
|
|
404
|
+
hash: digest(record.hash, "run journal contract hash"),
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function parseDispatch(value: unknown): RunJournalDispatch {
|
|
409
|
+
const record = dataRecord(value, "run journal dispatch");
|
|
410
|
+
exactKeys(record, ["planned", "started", "verified"], "run journal dispatch");
|
|
411
|
+
if (
|
|
412
|
+
!Number.isSafeInteger(record.planned)
|
|
413
|
+
|| typeof record.planned !== "number"
|
|
414
|
+
|| record.planned < 1
|
|
415
|
+
|| record.planned > 25
|
|
416
|
+
|| !Number.isSafeInteger(record.started)
|
|
417
|
+
|| typeof record.started !== "number"
|
|
418
|
+
|| record.started < 0
|
|
419
|
+
|| record.started > record.planned
|
|
420
|
+
|| !Number.isSafeInteger(record.verified)
|
|
421
|
+
|| typeof record.verified !== "number"
|
|
422
|
+
|| record.verified < 0
|
|
423
|
+
|| record.verified > record.started
|
|
424
|
+
) {
|
|
425
|
+
throw new Error("run journal dispatch is malformed");
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
planned: record.planned,
|
|
429
|
+
started: record.started,
|
|
430
|
+
verified: record.verified,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function parseOwner(value: unknown): RunJournal["owner"] {
|
|
435
|
+
const record = dataRecord(value, "run journal owner");
|
|
436
|
+
exactKeys(
|
|
437
|
+
record,
|
|
438
|
+
["pid", "token", "bootId", "processStartId", "leaseUntil"],
|
|
439
|
+
"run journal owner",
|
|
440
|
+
);
|
|
441
|
+
if (
|
|
442
|
+
!Number.isSafeInteger(record.pid)
|
|
443
|
+
|| typeof record.pid !== "number"
|
|
444
|
+
|| record.pid < 1
|
|
445
|
+
|| record.pid > 2_147_483_647
|
|
446
|
+
|| typeof record.token !== "string"
|
|
447
|
+
|| !/^[0-9a-f-]{36}$/u.test(record.token)
|
|
448
|
+
|| typeof record.bootId !== "string"
|
|
449
|
+
|| !/^[a-f0-9]{64}$/u.test(record.bootId)
|
|
450
|
+
|| typeof record.processStartId !== "string"
|
|
451
|
+
|| !/^[a-f0-9]{64}$/u.test(record.processStartId)
|
|
452
|
+
) {
|
|
453
|
+
throw new Error("run journal owner is malformed");
|
|
454
|
+
}
|
|
455
|
+
return {
|
|
456
|
+
pid: record.pid,
|
|
457
|
+
token: record.token,
|
|
458
|
+
bootId: record.bootId,
|
|
459
|
+
processStartId: record.processStartId,
|
|
460
|
+
leaseUntil: timestamp(record.leaseUntil, "run journal owner lease"),
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function ledgerRelativePath(value: unknown): string | null {
|
|
465
|
+
if (value === null) return null;
|
|
466
|
+
if (
|
|
467
|
+
typeof value !== "string"
|
|
468
|
+
|| !/^idempotency\/[a-f0-9]{2}\/[a-f0-9]{64}(?:\.[a-f0-9]{64})?\.json$/u.test(value)
|
|
469
|
+
) {
|
|
470
|
+
throw new Error("run journal ledger path is malformed");
|
|
471
|
+
}
|
|
472
|
+
return value;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function assertJournalInvariants(value: RunJournal): void {
|
|
476
|
+
const { dispatch } = value;
|
|
477
|
+
if (
|
|
478
|
+
value.duplicateIntent !== undefined
|
|
479
|
+
&& (
|
|
480
|
+
value.duplicateIntent.sourceRunId === value.runId
|
|
481
|
+
|| value.operation !== "posts.publish"
|
|
482
|
+
|| value.risk !== "R3"
|
|
483
|
+
|| value.contract.transport !== "web-session-api"
|
|
484
|
+
|| dispatch.planned !== 1
|
|
485
|
+
)
|
|
486
|
+
) {
|
|
487
|
+
throw new Error("duplicate intent has contradictory successor state");
|
|
488
|
+
}
|
|
489
|
+
if (
|
|
490
|
+
value.duplicateSuccessor !== undefined
|
|
491
|
+
&& (
|
|
492
|
+
value.duplicateSuccessor.sourceRunId !== value.runId
|
|
493
|
+
|| value.duplicateSuccessor.runId === value.runId
|
|
494
|
+
|| Date.parse(value.duplicateSuccessor.claimedAt) < Date.parse(value.updatedAt)
|
|
495
|
+
|| value.operation !== "posts.publish"
|
|
496
|
+
|| value.risk !== "R3"
|
|
497
|
+
|| value.contract.transport !== "web-session-api"
|
|
498
|
+
|| value.phase !== "terminal"
|
|
499
|
+
|| value.status !== "indeterminate"
|
|
500
|
+
|| dispatch.planned !== 1
|
|
501
|
+
|| dispatch.started !== 1
|
|
502
|
+
|| value.ledgerState !== "indeterminate"
|
|
503
|
+
|| value.recoveryState !== "retained"
|
|
504
|
+
)
|
|
505
|
+
) {
|
|
506
|
+
throw new Error("duplicate successor claim has contradictory source state");
|
|
507
|
+
}
|
|
508
|
+
if (Date.parse(value.updatedAt) < Date.parse(value.startedAt)) {
|
|
509
|
+
throw new Error("run journal update precedes its start");
|
|
510
|
+
}
|
|
511
|
+
if (Date.parse(value.dedupeExpiresAt) < Date.parse(value.startedAt)) {
|
|
512
|
+
throw new Error("run journal dedupe expiry precedes its start");
|
|
513
|
+
}
|
|
514
|
+
if (Date.parse(value.owner.leaseUntil) < Date.parse(value.startedAt)) {
|
|
515
|
+
throw new Error("run journal owner lease precedes its start");
|
|
516
|
+
}
|
|
517
|
+
if (!value.planHasAssets && value.assetState !== "none") {
|
|
518
|
+
throw new Error("asset-free run journal has contradictory asset state");
|
|
519
|
+
}
|
|
520
|
+
if (
|
|
521
|
+
value.planHasAssets
|
|
522
|
+
&& value.planState === "available"
|
|
523
|
+
&& value.assetState !== "none"
|
|
524
|
+
) {
|
|
525
|
+
throw new Error("available confirmation cannot own plan assets");
|
|
526
|
+
}
|
|
527
|
+
if (
|
|
528
|
+
value.planHasAssets
|
|
529
|
+
&& value.planState === "consumed"
|
|
530
|
+
&& value.phase !== "terminal"
|
|
531
|
+
&& value.assetState !== "bound"
|
|
532
|
+
) {
|
|
533
|
+
throw new Error("consumed confirmation lost its bound plan assets");
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (value.phase === "prepared") {
|
|
537
|
+
if (
|
|
538
|
+
value.status !== "pending"
|
|
539
|
+
|| value.ledgerRelativePath !== null
|
|
540
|
+
|| value.ledgerState !== "unclaimed"
|
|
541
|
+
|| value.recoveryState !== "absent"
|
|
542
|
+
|| (
|
|
543
|
+
value.planState === "available"
|
|
544
|
+
&& value.assetState !== "none"
|
|
545
|
+
)
|
|
546
|
+
|| (
|
|
547
|
+
value.planState === "consumed"
|
|
548
|
+
&& value.assetState !== "none"
|
|
549
|
+
&& value.assetState !== "bound"
|
|
550
|
+
)
|
|
551
|
+
|| dispatch.started !== 0
|
|
552
|
+
|| dispatch.verified !== 0
|
|
553
|
+
) throw new Error("prepared run journal has contradictory state");
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
if (value.phase === "claimed") {
|
|
557
|
+
if (
|
|
558
|
+
value.status !== "pending"
|
|
559
|
+
|| value.planState !== "consumed"
|
|
560
|
+
|| value.ledgerRelativePath === null
|
|
561
|
+
|| value.ledgerState !== "pending"
|
|
562
|
+
|| value.recoveryState !== "absent"
|
|
563
|
+
|| dispatch.started !== 0
|
|
564
|
+
|| dispatch.verified !== 0
|
|
565
|
+
) throw new Error("claimed run journal has contradictory state");
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
if (value.phase === "ready") {
|
|
569
|
+
if (
|
|
570
|
+
value.status !== "pending"
|
|
571
|
+
|| value.planState !== "consumed"
|
|
572
|
+
|| value.ledgerRelativePath === null
|
|
573
|
+
|| value.ledgerState !== "pending"
|
|
574
|
+
|| value.recoveryState !== "present"
|
|
575
|
+
|| dispatch.started !== 0
|
|
576
|
+
|| dispatch.verified !== 0
|
|
577
|
+
) throw new Error("ready run journal has contradictory state");
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (value.phase === "dispatching") {
|
|
581
|
+
if (
|
|
582
|
+
value.status !== "pending"
|
|
583
|
+
|| value.planState !== "consumed"
|
|
584
|
+
|| value.ledgerRelativePath === null
|
|
585
|
+
|| value.ledgerState !== "pending"
|
|
586
|
+
|| value.recoveryState !== "present"
|
|
587
|
+
|| dispatch.started < 1
|
|
588
|
+
) throw new Error("dispatching run journal has contradictory state");
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (value.status === "pending") {
|
|
593
|
+
throw new Error("terminal run journal cannot remain pending");
|
|
594
|
+
}
|
|
595
|
+
if (value.status === "failed") {
|
|
596
|
+
if (
|
|
597
|
+
dispatch.started !== 0
|
|
598
|
+
|| dispatch.verified !== 0
|
|
599
|
+
|| value.ledgerState !== "released"
|
|
600
|
+
|| value.recoveryState !== "released"
|
|
601
|
+
|| (
|
|
602
|
+
value.planState === "available"
|
|
603
|
+
? value.assetState !== "none"
|
|
604
|
+
: value.planHasAssets
|
|
605
|
+
? value.assetState !== "released"
|
|
606
|
+
: value.assetState !== "none"
|
|
607
|
+
)
|
|
608
|
+
) throw new Error("failed run journal has contradictory state");
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
if (value.status === "succeeded" || value.status === "submitted") {
|
|
612
|
+
const verifiedAll = dispatch.started === dispatch.planned
|
|
613
|
+
&& dispatch.verified === dispatch.planned;
|
|
614
|
+
const verifiedNoOp = value.status === "succeeded"
|
|
615
|
+
&& dispatch.started === 0
|
|
616
|
+
&& dispatch.verified === 0;
|
|
617
|
+
if (
|
|
618
|
+
(!verifiedAll && !verifiedNoOp)
|
|
619
|
+
|| value.planState !== "consumed"
|
|
620
|
+
|| value.ledgerState !== "succeeded"
|
|
621
|
+
|| value.recoveryState !== "released"
|
|
622
|
+
|| (
|
|
623
|
+
value.planHasAssets
|
|
624
|
+
? value.assetState !== "released"
|
|
625
|
+
: value.assetState !== "none"
|
|
626
|
+
)
|
|
627
|
+
) throw new Error("successful run journal has contradictory state");
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
if (value.status === "partial") {
|
|
631
|
+
if (
|
|
632
|
+
dispatch.verified < 1
|
|
633
|
+
|| value.planState !== "consumed"
|
|
634
|
+
|| dispatch.started !== dispatch.verified
|
|
635
|
+
|| dispatch.verified >= dispatch.planned
|
|
636
|
+
|| (
|
|
637
|
+
value.ledgerState !== "partial"
|
|
638
|
+
&& !(
|
|
639
|
+
value.ledgerState === "released"
|
|
640
|
+
&& value.recoveryState === "released"
|
|
641
|
+
)
|
|
642
|
+
)
|
|
643
|
+
|| (
|
|
644
|
+
value.recoveryState !== "retained"
|
|
645
|
+
&& value.recoveryState !== "released"
|
|
646
|
+
)
|
|
647
|
+
|| (
|
|
648
|
+
value.recoveryState === "retained"
|
|
649
|
+
? value.planHasAssets
|
|
650
|
+
? value.assetState !== "retained"
|
|
651
|
+
: value.assetState !== "none"
|
|
652
|
+
: value.planHasAssets
|
|
653
|
+
? value.assetState !== "released"
|
|
654
|
+
: value.assetState !== "none"
|
|
655
|
+
)
|
|
656
|
+
) throw new Error("partial run journal has contradictory state");
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
if (
|
|
660
|
+
dispatch.started < 1
|
|
661
|
+
|| value.planState !== "consumed"
|
|
662
|
+
|| (
|
|
663
|
+
value.ledgerState !== "indeterminate"
|
|
664
|
+
&& !(
|
|
665
|
+
value.ledgerState === "released"
|
|
666
|
+
&& value.recoveryState === "released"
|
|
667
|
+
)
|
|
668
|
+
)
|
|
669
|
+
|| (
|
|
670
|
+
value.recoveryState !== "retained"
|
|
671
|
+
&& value.recoveryState !== "released"
|
|
672
|
+
)
|
|
673
|
+
|| (
|
|
674
|
+
value.recoveryState === "retained"
|
|
675
|
+
? value.planHasAssets
|
|
676
|
+
? value.assetState !== "retained"
|
|
677
|
+
: value.assetState !== "none"
|
|
678
|
+
: value.planHasAssets
|
|
679
|
+
? value.assetState !== "released"
|
|
680
|
+
: value.assetState !== "none"
|
|
681
|
+
)
|
|
682
|
+
) throw new Error("indeterminate run journal has contradictory state");
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
export function parseRunJournal(value: unknown): RunJournal {
|
|
686
|
+
const record = dataRecord(value, "run journal");
|
|
687
|
+
const keys = [
|
|
688
|
+
"schemaVersion",
|
|
689
|
+
"revision",
|
|
690
|
+
"runId",
|
|
691
|
+
"planDigest",
|
|
692
|
+
"adapter",
|
|
693
|
+
"operation",
|
|
694
|
+
"risk",
|
|
695
|
+
"inputHash",
|
|
696
|
+
"auth",
|
|
697
|
+
"contract",
|
|
698
|
+
"planHasAssets",
|
|
699
|
+
"planState",
|
|
700
|
+
"phase",
|
|
701
|
+
"status",
|
|
702
|
+
"dispatch",
|
|
703
|
+
"ledgerRelativePath",
|
|
704
|
+
"ledgerState",
|
|
705
|
+
"recoveryState",
|
|
706
|
+
"assetState",
|
|
707
|
+
"owner",
|
|
708
|
+
"startedAt",
|
|
709
|
+
"updatedAt",
|
|
710
|
+
"dedupeExpiresAt",
|
|
711
|
+
"finalOrigin",
|
|
712
|
+
"error",
|
|
713
|
+
];
|
|
714
|
+
if (Object.hasOwn(record, "duplicateIntent")) keys.push("duplicateIntent");
|
|
715
|
+
if (Object.hasOwn(record, "duplicateSuccessor")) keys.push("duplicateSuccessor");
|
|
716
|
+
exactKeys(record, keys, "run journal");
|
|
717
|
+
if (
|
|
718
|
+
record.schemaVersion !== 1
|
|
719
|
+
|| !Number.isSafeInteger(record.revision)
|
|
720
|
+
|| typeof record.revision !== "number"
|
|
721
|
+
|| record.revision < 0
|
|
722
|
+
|| typeof record.runId !== "string"
|
|
723
|
+
|| !/^[0-9a-f-]{36}$/u.test(record.runId)
|
|
724
|
+
|| !isProviderPluginOperationName(record.operation)
|
|
725
|
+
|| (record.risk !== "R2" && record.risk !== "R3")
|
|
726
|
+
|| (
|
|
727
|
+
record.phase !== "prepared"
|
|
728
|
+
&& record.phase !== "claimed"
|
|
729
|
+
&& record.phase !== "ready"
|
|
730
|
+
&& record.phase !== "dispatching"
|
|
731
|
+
&& record.phase !== "terminal"
|
|
732
|
+
)
|
|
733
|
+
|| (
|
|
734
|
+
record.status !== "pending"
|
|
735
|
+
&& record.status !== "succeeded"
|
|
736
|
+
&& record.status !== "submitted"
|
|
737
|
+
&& record.status !== "failed"
|
|
738
|
+
&& record.status !== "partial"
|
|
739
|
+
&& record.status !== "indeterminate"
|
|
740
|
+
)
|
|
741
|
+
|| (record.planState !== "available" && record.planState !== "consumed")
|
|
742
|
+
|| typeof record.planHasAssets !== "boolean"
|
|
743
|
+
|| (
|
|
744
|
+
record.ledgerState !== "unclaimed"
|
|
745
|
+
&& record.ledgerState !== "pending"
|
|
746
|
+
&& record.ledgerState !== "succeeded"
|
|
747
|
+
&& record.ledgerState !== "partial"
|
|
748
|
+
&& record.ledgerState !== "indeterminate"
|
|
749
|
+
&& record.ledgerState !== "released"
|
|
750
|
+
)
|
|
751
|
+
|| (
|
|
752
|
+
record.recoveryState !== "absent"
|
|
753
|
+
&& record.recoveryState !== "present"
|
|
754
|
+
&& record.recoveryState !== "retained"
|
|
755
|
+
&& record.recoveryState !== "released"
|
|
756
|
+
)
|
|
757
|
+
|| (
|
|
758
|
+
record.assetState !== "none"
|
|
759
|
+
&& record.assetState !== "bound"
|
|
760
|
+
&& record.assetState !== "retained"
|
|
761
|
+
&& record.assetState !== "released"
|
|
762
|
+
)
|
|
763
|
+
) {
|
|
764
|
+
throw new Error("run journal is malformed");
|
|
765
|
+
}
|
|
766
|
+
const journal: RunJournal = {
|
|
767
|
+
schemaVersion: 1,
|
|
768
|
+
revision: record.revision,
|
|
769
|
+
runId: record.runId,
|
|
770
|
+
planDigest: digest(record.planDigest, "run journal plan digest"),
|
|
771
|
+
adapter: parseAdapter(record.adapter),
|
|
772
|
+
operation: record.operation,
|
|
773
|
+
risk: record.risk,
|
|
774
|
+
inputHash: digest(record.inputHash, "run journal input hash"),
|
|
775
|
+
auth: parseAuth(record.auth),
|
|
776
|
+
contract: parseContract(record.contract),
|
|
777
|
+
...(Object.hasOwn(record, "duplicateIntent")
|
|
778
|
+
? {
|
|
779
|
+
duplicateIntent: parseDuplicateIntent(
|
|
780
|
+
record.duplicateIntent,
|
|
781
|
+
"run journal duplicate intent",
|
|
782
|
+
),
|
|
783
|
+
}
|
|
784
|
+
: {}),
|
|
785
|
+
...(Object.hasOwn(record, "duplicateSuccessor")
|
|
786
|
+
? { duplicateSuccessor: parseDuplicateSuccessor(record.duplicateSuccessor) }
|
|
787
|
+
: {}),
|
|
788
|
+
planHasAssets: record.planHasAssets,
|
|
789
|
+
planState: record.planState,
|
|
790
|
+
phase: record.phase,
|
|
791
|
+
status: record.status,
|
|
792
|
+
dispatch: parseDispatch(record.dispatch),
|
|
793
|
+
ledgerRelativePath: ledgerRelativePath(record.ledgerRelativePath),
|
|
794
|
+
ledgerState: record.ledgerState,
|
|
795
|
+
recoveryState: record.recoveryState,
|
|
796
|
+
assetState: record.assetState,
|
|
797
|
+
owner: parseOwner(record.owner),
|
|
798
|
+
startedAt: timestamp(record.startedAt, "run journal start"),
|
|
799
|
+
updatedAt: timestamp(record.updatedAt, "run journal update"),
|
|
800
|
+
dedupeExpiresAt: timestamp(record.dedupeExpiresAt, "run journal dedupe expiry"),
|
|
801
|
+
finalOrigin: nullableBoundedString(
|
|
802
|
+
record.finalOrigin,
|
|
803
|
+
"run journal final origin",
|
|
804
|
+
MAX_JOURNAL_ORIGIN_BYTES,
|
|
805
|
+
),
|
|
806
|
+
error: nullableBoundedString(
|
|
807
|
+
record.error,
|
|
808
|
+
"run journal error",
|
|
809
|
+
MAX_JOURNAL_ERROR_BYTES,
|
|
810
|
+
),
|
|
811
|
+
};
|
|
812
|
+
if (
|
|
813
|
+
journal.finalOrigin !== null
|
|
814
|
+
&& new URL(journal.finalOrigin).origin !== journal.finalOrigin
|
|
815
|
+
) {
|
|
816
|
+
throw new Error("run journal final origin is malformed");
|
|
817
|
+
}
|
|
818
|
+
assertJournalInvariants(journal);
|
|
819
|
+
if (Buffer.byteLength(canonicalJson(journal), "utf8") > MAX_RUN_JOURNAL_BYTES) {
|
|
820
|
+
throw new Error("run journal exceeds its byte bound");
|
|
821
|
+
}
|
|
822
|
+
const frozenContract: RunJournalContract = journal.contract.transport
|
|
823
|
+
=== "portable-provider-plugin"
|
|
824
|
+
? Object.freeze({
|
|
825
|
+
transport: "portable-provider-plugin",
|
|
826
|
+
identity: journal.contract.identity,
|
|
827
|
+
})
|
|
828
|
+
: journal.contract.transport === "local-cli"
|
|
829
|
+
? Object.freeze({
|
|
830
|
+
transport: "local-cli",
|
|
831
|
+
identity: journal.contract.identity,
|
|
832
|
+
})
|
|
833
|
+
: Object.freeze({ ...journal.contract });
|
|
834
|
+
return Object.freeze({
|
|
835
|
+
...journal,
|
|
836
|
+
adapter: Object.freeze({ ...journal.adapter }),
|
|
837
|
+
auth: Object.freeze({ ...journal.auth }),
|
|
838
|
+
contract: frozenContract,
|
|
839
|
+
...(journal.duplicateIntent === undefined
|
|
840
|
+
? {}
|
|
841
|
+
: { duplicateIntent: Object.freeze({ ...journal.duplicateIntent }) }),
|
|
842
|
+
...(journal.duplicateSuccessor === undefined
|
|
843
|
+
? {}
|
|
844
|
+
: { duplicateSuccessor: Object.freeze({ ...journal.duplicateSuccessor }) }),
|
|
845
|
+
dispatch: Object.freeze({ ...journal.dispatch }),
|
|
846
|
+
owner: Object.freeze({ ...journal.owner }),
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
export function initialRunJournal(value: StartRunJournal): RunJournal {
|
|
851
|
+
return parseRunJournal({
|
|
852
|
+
schemaVersion: 1,
|
|
853
|
+
revision: 0,
|
|
854
|
+
runId: value.runId,
|
|
855
|
+
planDigest: value.planDigest,
|
|
856
|
+
adapter: value.adapter,
|
|
857
|
+
operation: value.operation,
|
|
858
|
+
risk: value.risk,
|
|
859
|
+
inputHash: value.inputHash,
|
|
860
|
+
auth: value.auth,
|
|
861
|
+
contract: value.contract,
|
|
862
|
+
...(value.duplicateIntent === undefined
|
|
863
|
+
? {}
|
|
864
|
+
: { duplicateIntent: value.duplicateIntent }),
|
|
865
|
+
planHasAssets: value.hasPlanAssets,
|
|
866
|
+
planState: "available",
|
|
867
|
+
phase: "prepared",
|
|
868
|
+
status: "pending",
|
|
869
|
+
dispatch: {
|
|
870
|
+
planned: value.plannedDispatches,
|
|
871
|
+
started: 0,
|
|
872
|
+
verified: 0,
|
|
873
|
+
},
|
|
874
|
+
ledgerRelativePath: null,
|
|
875
|
+
ledgerState: "unclaimed",
|
|
876
|
+
recoveryState: "absent",
|
|
877
|
+
assetState: "none",
|
|
878
|
+
owner: value.owner,
|
|
879
|
+
startedAt: value.startedAt,
|
|
880
|
+
updatedAt: value.startedAt,
|
|
881
|
+
dedupeExpiresAt: value.dedupeExpiresAt,
|
|
882
|
+
finalOrigin: null,
|
|
883
|
+
error: "execution was prepared but no durable final outcome was recorded",
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function terminalTransition(
|
|
888
|
+
current: RunJournal,
|
|
889
|
+
event: Extract<RunJournalEvent, { readonly type: "finished" }>,
|
|
890
|
+
): RunJournal {
|
|
891
|
+
if (current.phase === "terminal") {
|
|
892
|
+
throw new Error("run journal already has a terminal outcome");
|
|
893
|
+
}
|
|
894
|
+
const noOp = event.noOp === true;
|
|
895
|
+
const { dispatch } = current;
|
|
896
|
+
if (
|
|
897
|
+
event.status === "failed"
|
|
898
|
+
&& dispatch.started !== 0
|
|
899
|
+
) {
|
|
900
|
+
throw new Error("a run with started dispatches cannot finish as failed");
|
|
901
|
+
}
|
|
902
|
+
if (
|
|
903
|
+
(event.status === "succeeded" || event.status === "submitted")
|
|
904
|
+
&& !(
|
|
905
|
+
(event.status === "succeeded" && noOp && dispatch.started === 0 && dispatch.verified === 0)
|
|
906
|
+
|| (
|
|
907
|
+
!noOp
|
|
908
|
+
&& dispatch.started === dispatch.planned
|
|
909
|
+
&& dispatch.verified === dispatch.planned
|
|
910
|
+
)
|
|
911
|
+
)
|
|
912
|
+
) {
|
|
913
|
+
throw new Error("a successful run must verify its complete dispatch schedule or be an explicit no-op");
|
|
914
|
+
}
|
|
915
|
+
if (
|
|
916
|
+
event.status === "partial"
|
|
917
|
+
&& !(
|
|
918
|
+
dispatch.verified > 0
|
|
919
|
+
&& dispatch.started === dispatch.verified
|
|
920
|
+
&& dispatch.verified < dispatch.planned
|
|
921
|
+
)
|
|
922
|
+
) {
|
|
923
|
+
throw new Error("a partial run must stop after one or more verified dispatches");
|
|
924
|
+
}
|
|
925
|
+
if (event.status === "indeterminate" && dispatch.started < 1) {
|
|
926
|
+
throw new Error("an indeterminate run must have crossed a dispatch boundary");
|
|
927
|
+
}
|
|
928
|
+
const retainsRecovery = event.status === "partial" || event.status === "indeterminate";
|
|
929
|
+
const ledgerState: RunJournal["ledgerState"] = event.status === "succeeded"
|
|
930
|
+
|| event.status === "submitted"
|
|
931
|
+
? "succeeded"
|
|
932
|
+
: event.status === "partial"
|
|
933
|
+
? "partial"
|
|
934
|
+
: event.status === "indeterminate" ? "indeterminate" : "released";
|
|
935
|
+
return parseRunJournal({
|
|
936
|
+
...current,
|
|
937
|
+
revision: current.revision + 1,
|
|
938
|
+
phase: "terminal",
|
|
939
|
+
status: event.status,
|
|
940
|
+
ledgerState,
|
|
941
|
+
recoveryState: retainsRecovery ? "retained" : "released",
|
|
942
|
+
assetState: current.assetState === "none"
|
|
943
|
+
? "none"
|
|
944
|
+
: retainsRecovery ? "retained" : "released",
|
|
945
|
+
updatedAt: event.at,
|
|
946
|
+
finalOrigin: event.finalOrigin,
|
|
947
|
+
error: event.error,
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
export function transitionRunJournal(
|
|
952
|
+
currentValue: RunJournal,
|
|
953
|
+
event: RunJournalEvent,
|
|
954
|
+
): RunJournal {
|
|
955
|
+
const current = parseRunJournal(currentValue);
|
|
956
|
+
const at = timestamp(event.at, "run journal transition time");
|
|
957
|
+
if (Date.parse(at) < Date.parse(current.updatedAt)) {
|
|
958
|
+
throw new Error("run journal transition time moved backward");
|
|
959
|
+
}
|
|
960
|
+
if (event.type === "finished") return terminalTransition(current, event);
|
|
961
|
+
if (event.type === "duplicate-successor-claimed") {
|
|
962
|
+
const intentHash = digest(
|
|
963
|
+
event.intentHash,
|
|
964
|
+
"run journal duplicate successor intent hash",
|
|
965
|
+
);
|
|
966
|
+
const successorRunId = runId(
|
|
967
|
+
event.runId,
|
|
968
|
+
"run journal duplicate successor run ID",
|
|
969
|
+
);
|
|
970
|
+
const existing = current.duplicateSuccessor;
|
|
971
|
+
if (existing !== undefined) {
|
|
972
|
+
if (
|
|
973
|
+
existing.intentHash === intentHash
|
|
974
|
+
&& existing.runId === successorRunId
|
|
975
|
+
) return current;
|
|
976
|
+
throw new Error("run journal already elected a different duplicate successor");
|
|
977
|
+
}
|
|
978
|
+
if (
|
|
979
|
+
current.operation !== "posts.publish"
|
|
980
|
+
|| current.risk !== "R3"
|
|
981
|
+
|| current.contract.transport !== "web-session-api"
|
|
982
|
+
|| current.phase !== "terminal"
|
|
983
|
+
|| current.status !== "indeterminate"
|
|
984
|
+
|| current.dispatch.planned !== 1
|
|
985
|
+
|| current.dispatch.started !== 1
|
|
986
|
+
|| current.ledgerState !== "indeterminate"
|
|
987
|
+
|| current.recoveryState !== "retained"
|
|
988
|
+
) {
|
|
989
|
+
throw new Error(
|
|
990
|
+
"only one retained terminal indeterminate posts.publish dispatch can elect a duplicate successor",
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
return parseRunJournal({
|
|
994
|
+
...current,
|
|
995
|
+
revision: current.revision + 1,
|
|
996
|
+
duplicateSuccessor: {
|
|
997
|
+
schemaVersion: 1,
|
|
998
|
+
intentHash,
|
|
999
|
+
sourceRunId: current.runId,
|
|
1000
|
+
runId: successorRunId,
|
|
1001
|
+
claimedAt: at,
|
|
1002
|
+
},
|
|
1003
|
+
// Claiming lineage must not rewrite the immutable receipt's finish time.
|
|
1004
|
+
updatedAt: current.updatedAt,
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
if (event.type === "recovery-released") {
|
|
1008
|
+
if (current.duplicateSuccessor !== undefined) {
|
|
1009
|
+
throw new Error(
|
|
1010
|
+
"run recovery is retained because a duplicate successor intent was claimed",
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
if (
|
|
1014
|
+
current.phase !== "terminal"
|
|
1015
|
+
|| (current.status !== "partial" && current.status !== "indeterminate")
|
|
1016
|
+
) {
|
|
1017
|
+
throw new Error(
|
|
1018
|
+
"only a reconciled partial or indeterminate run can release recovery resources",
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
const releaseLedger = event.outcome === "not-applied";
|
|
1022
|
+
if (releaseLedger && current.dispatch.verified !== 0) {
|
|
1023
|
+
throw new Error(
|
|
1024
|
+
"a run with a verified dispatch cannot release its at-most-once ledger",
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
1027
|
+
if (current.recoveryState === "released") {
|
|
1028
|
+
if (
|
|
1029
|
+
releaseLedger === (current.ledgerState === "released")
|
|
1030
|
+
) return current;
|
|
1031
|
+
throw new Error(
|
|
1032
|
+
"run recovery was already released with a different reconciliation outcome",
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
return parseRunJournal({
|
|
1036
|
+
...current,
|
|
1037
|
+
revision: current.revision + 1,
|
|
1038
|
+
ledgerState: releaseLedger ? "released" : current.ledgerState,
|
|
1039
|
+
recoveryState: "released",
|
|
1040
|
+
assetState: current.assetState === "retained" ? "released" : "none",
|
|
1041
|
+
// `updatedAt` is the terminal outcome time and is projected into the
|
|
1042
|
+
// immutable receipt's `finishedAt`. Recovery release is later
|
|
1043
|
+
// maintenance, not a new execution outcome. Advancing the revision
|
|
1044
|
+
// fences concurrent writers without rewriting the terminal receipt.
|
|
1045
|
+
updatedAt: current.updatedAt,
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
if (current.phase === "terminal") {
|
|
1049
|
+
throw new Error("terminal run journals cannot transition");
|
|
1050
|
+
}
|
|
1051
|
+
if (event.type === "confirmation-consumed") {
|
|
1052
|
+
if (current.phase !== "prepared" || current.planState !== "available") {
|
|
1053
|
+
throw new Error("run journal confirmation can only be consumed once after preparation");
|
|
1054
|
+
}
|
|
1055
|
+
return parseRunJournal({
|
|
1056
|
+
...current,
|
|
1057
|
+
revision: current.revision + 1,
|
|
1058
|
+
planState: "consumed",
|
|
1059
|
+
assetState: current.planHasAssets ? "bound" : "none",
|
|
1060
|
+
updatedAt: at,
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
if (event.type === "ledger-claimed") {
|
|
1064
|
+
if (current.phase !== "prepared" || current.planState !== "consumed") {
|
|
1065
|
+
throw new Error("run journal ledger can only be claimed after preparation");
|
|
1066
|
+
}
|
|
1067
|
+
return parseRunJournal({
|
|
1068
|
+
...current,
|
|
1069
|
+
revision: current.revision + 1,
|
|
1070
|
+
phase: "claimed",
|
|
1071
|
+
ledgerRelativePath: ledgerRelativePath(event.ledgerRelativePath),
|
|
1072
|
+
ledgerState: "pending",
|
|
1073
|
+
updatedAt: at,
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
if (event.type === "recovery-stored") {
|
|
1077
|
+
if (current.phase !== "claimed") {
|
|
1078
|
+
throw new Error("run journal recovery can only be stored after ledger claim");
|
|
1079
|
+
}
|
|
1080
|
+
return parseRunJournal({
|
|
1081
|
+
...current,
|
|
1082
|
+
revision: current.revision + 1,
|
|
1083
|
+
phase: "ready",
|
|
1084
|
+
recoveryState: "present",
|
|
1085
|
+
updatedAt: at,
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
if (event.type === "dispatch-started") {
|
|
1089
|
+
if (
|
|
1090
|
+
(current.phase !== "ready" && current.phase !== "dispatching")
|
|
1091
|
+
|| event.index !== current.dispatch.started + 1
|
|
1092
|
+
|| current.dispatch.started !== current.dispatch.verified
|
|
1093
|
+
|| event.index > current.dispatch.planned
|
|
1094
|
+
) {
|
|
1095
|
+
throw new Error("run journal dispatch start is out of order");
|
|
1096
|
+
}
|
|
1097
|
+
return parseRunJournal({
|
|
1098
|
+
...current,
|
|
1099
|
+
revision: current.revision + 1,
|
|
1100
|
+
phase: "dispatching",
|
|
1101
|
+
dispatch: {
|
|
1102
|
+
...current.dispatch,
|
|
1103
|
+
started: event.index,
|
|
1104
|
+
},
|
|
1105
|
+
updatedAt: at,
|
|
1106
|
+
error: "a dispatch crossed its durable start boundary; a missing final outcome requires reconciliation",
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
if (event.type === "dispatch-verified") {
|
|
1110
|
+
if (
|
|
1111
|
+
current.phase !== "dispatching"
|
|
1112
|
+
|| event.index !== current.dispatch.verified + 1
|
|
1113
|
+
|| event.index !== current.dispatch.started
|
|
1114
|
+
) {
|
|
1115
|
+
throw new Error("run journal dispatch verification is out of order");
|
|
1116
|
+
}
|
|
1117
|
+
return parseRunJournal({
|
|
1118
|
+
...current,
|
|
1119
|
+
revision: current.revision + 1,
|
|
1120
|
+
dispatch: {
|
|
1121
|
+
...current.dispatch,
|
|
1122
|
+
verified: event.index,
|
|
1123
|
+
},
|
|
1124
|
+
updatedAt: at,
|
|
1125
|
+
error: "verified dispatch progress was stored; execution has not reached a durable final outcome",
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
const leaseUntil = timestamp(event.leaseUntil, "run journal renewed lease");
|
|
1129
|
+
if (Date.parse(leaseUntil) < Date.parse(at)) {
|
|
1130
|
+
throw new Error("run journal renewed lease precedes its transition");
|
|
1131
|
+
}
|
|
1132
|
+
return parseRunJournal({
|
|
1133
|
+
...current,
|
|
1134
|
+
revision: current.revision + 1,
|
|
1135
|
+
owner: {
|
|
1136
|
+
...current.owner,
|
|
1137
|
+
leaseUntil,
|
|
1138
|
+
},
|
|
1139
|
+
updatedAt: at,
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
function journalDirectory(environment: Environment): string {
|
|
1144
|
+
return join(ghostgetStateHome(environment), RUN_JOURNAL_DIRECTORY);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
function journalPath(runId: string, environment: Environment): string {
|
|
1148
|
+
if (!/^[0-9a-f-]{36}$/u.test(runId)) throw new Error("run journal ID is invalid");
|
|
1149
|
+
return join(journalDirectory(environment), `${runId}.json`);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function snapshot(journal: RunJournal): RunJournalSnapshot {
|
|
1153
|
+
const content = `${canonicalJson(journal)}\n`;
|
|
1154
|
+
return Object.freeze({
|
|
1155
|
+
journal,
|
|
1156
|
+
contentSha256: createHash("sha256").update(content).digest("hex"),
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
export function createRunJournal(
|
|
1161
|
+
journalValue: RunJournal,
|
|
1162
|
+
environment: Environment = process.env,
|
|
1163
|
+
): RunJournalSnapshot {
|
|
1164
|
+
const journal = parseRunJournal(journalValue);
|
|
1165
|
+
ensurePrivateStateDirectory(journalDirectory(environment), environment);
|
|
1166
|
+
const created = createPrivateJsonIfAbsent(
|
|
1167
|
+
journalPath(journal.runId, environment),
|
|
1168
|
+
journal,
|
|
1169
|
+
{ environment, privateParent: true },
|
|
1170
|
+
);
|
|
1171
|
+
if (!created.created) throw new Error("run journal ID already exists");
|
|
1172
|
+
return snapshot(journal);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
export function readRunJournal(
|
|
1176
|
+
runId: string,
|
|
1177
|
+
environment: Environment = process.env,
|
|
1178
|
+
): RunJournalSnapshot | null {
|
|
1179
|
+
ensurePrivateStateDirectory(journalDirectory(environment), environment);
|
|
1180
|
+
const text = readPrivateStateFileIfPresent(
|
|
1181
|
+
journalPath(runId, environment),
|
|
1182
|
+
MAX_RUN_JOURNAL_BYTES,
|
|
1183
|
+
"run journal",
|
|
1184
|
+
environment,
|
|
1185
|
+
);
|
|
1186
|
+
if (text === null) return null;
|
|
1187
|
+
return parseRunJournalSnapshotText(text);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function parseRunJournalSnapshotText(text: string): RunJournalSnapshot {
|
|
1191
|
+
let value: unknown;
|
|
1192
|
+
try {
|
|
1193
|
+
value = JSON.parse(text) as unknown;
|
|
1194
|
+
} catch {
|
|
1195
|
+
throw new Error("run journal is malformed");
|
|
1196
|
+
}
|
|
1197
|
+
return Object.freeze({
|
|
1198
|
+
journal: parseRunJournal(value),
|
|
1199
|
+
contentSha256: createHash("sha256").update(text).digest("hex"),
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
export function updateRunJournal(
|
|
1204
|
+
current: RunJournalSnapshot,
|
|
1205
|
+
event: RunJournalEvent,
|
|
1206
|
+
environment: Environment = process.env,
|
|
1207
|
+
): RunJournalSnapshot {
|
|
1208
|
+
const next = transitionRunJournal(current.journal, event);
|
|
1209
|
+
const written = writePrivateJsonIfUnchanged(
|
|
1210
|
+
journalPath(current.journal.runId, environment),
|
|
1211
|
+
next,
|
|
1212
|
+
{ expectedCurrentContentSha256: current.contentSha256 },
|
|
1213
|
+
);
|
|
1214
|
+
if (!written) {
|
|
1215
|
+
throw new Error("run journal changed concurrently; reload it before continuing");
|
|
1216
|
+
}
|
|
1217
|
+
return snapshot(next);
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
export function listRunJournalSnapshots(
|
|
1221
|
+
environment: Environment = process.env,
|
|
1222
|
+
): readonly (RunJournalSnapshot | { readonly runId: string; readonly invalid: true })[] {
|
|
1223
|
+
const directory = journalDirectory(environment);
|
|
1224
|
+
const identity = ensurePrivateStateDirectory(directory, environment);
|
|
1225
|
+
const directorySnapshot = snapshotPrivateStateDirectory(
|
|
1226
|
+
directory,
|
|
1227
|
+
environment,
|
|
1228
|
+
identity,
|
|
1229
|
+
);
|
|
1230
|
+
if (directorySnapshot.identity === null) return Object.freeze([]);
|
|
1231
|
+
const names = directorySnapshot.entries
|
|
1232
|
+
.filter((entry) => (
|
|
1233
|
+
entry.kind === "file"
|
|
1234
|
+
&& /^[0-9a-f-]{36}\.json$/u.test(entry.name)
|
|
1235
|
+
))
|
|
1236
|
+
.map((entry) => entry.name);
|
|
1237
|
+
return Object.freeze(
|
|
1238
|
+
readPrivateStateFilesBatched(directory, names, {
|
|
1239
|
+
maximumBytesPerFile: MAX_RUN_JOURNAL_BYTES,
|
|
1240
|
+
environment,
|
|
1241
|
+
expectedDirectoryIdentity: directorySnapshot.identity,
|
|
1242
|
+
}).map((file) => {
|
|
1243
|
+
const runId = file.name.slice(0, -5);
|
|
1244
|
+
if (file.status !== "present") {
|
|
1245
|
+
return { runId, invalid: true as const };
|
|
1246
|
+
}
|
|
1247
|
+
try {
|
|
1248
|
+
const value = parseRunJournalSnapshotText(file.content);
|
|
1249
|
+
return value.journal.runId === runId
|
|
1250
|
+
? value
|
|
1251
|
+
: { runId, invalid: true as const };
|
|
1252
|
+
} catch {
|
|
1253
|
+
return { runId, invalid: true as const };
|
|
1254
|
+
}
|
|
1255
|
+
}),
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
export function runJournalNeedsRepair(
|
|
1260
|
+
journalValue: RunJournal,
|
|
1261
|
+
now = new Date(),
|
|
1262
|
+
inspectOwner: (owner: RunJournal["owner"]) => ProcessOwnerStatus
|
|
1263
|
+
= processOwnerStatus,
|
|
1264
|
+
): boolean {
|
|
1265
|
+
const journal = parseRunJournal(journalValue);
|
|
1266
|
+
if (journal.phase === "terminal") return false;
|
|
1267
|
+
void now;
|
|
1268
|
+
// A wall-clock lease is diagnostic only. A suspended but exact-live owner
|
|
1269
|
+
// may resume with in-memory state, so another process must never mutate its
|
|
1270
|
+
// journal or release its claims solely because time elapsed.
|
|
1271
|
+
return inspectOwner(journal.owner) === "different-or-dead";
|
|
1272
|
+
}
|