@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,973 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import {
|
|
5
|
+
chmodSync,
|
|
6
|
+
lstatSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
readdirSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from "node:fs";
|
|
12
|
+
import { isIP } from "node:net";
|
|
13
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
isProviderPluginId,
|
|
18
|
+
isProviderPluginOperationName,
|
|
19
|
+
isProviderPluginSurfaceId,
|
|
20
|
+
} from "../provider-plugin-identifiers";
|
|
21
|
+
|
|
22
|
+
type Risk = "R1" | "R2" | "R3";
|
|
23
|
+
|
|
24
|
+
export type ScaffoldOptions = {
|
|
25
|
+
readonly site: string;
|
|
26
|
+
readonly displayName: string;
|
|
27
|
+
readonly origin: string;
|
|
28
|
+
readonly operation: string;
|
|
29
|
+
readonly risk: Risk;
|
|
30
|
+
readonly evidencePath: string;
|
|
31
|
+
readonly candidateIndex: number;
|
|
32
|
+
readonly outputDirectory: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const SOURCE_PROVIDER_PLUGIN_FILES = Object.freeze([
|
|
36
|
+
"plugin.ts",
|
|
37
|
+
"runtime.ts",
|
|
38
|
+
"plugin.test.ts",
|
|
39
|
+
"runtime.internal.test.ts",
|
|
40
|
+
"wrench-adapter.json",
|
|
41
|
+
"promotion-checklist.json",
|
|
42
|
+
] as const);
|
|
43
|
+
|
|
44
|
+
export type SourceProviderPluginCheck = {
|
|
45
|
+
readonly status: "capture-required";
|
|
46
|
+
readonly executable: false;
|
|
47
|
+
readonly pluginId: string;
|
|
48
|
+
readonly directory: string;
|
|
49
|
+
readonly files: typeof SOURCE_PROVIDER_PLUGIN_FILES;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
type EvidenceSummary = {
|
|
53
|
+
readonly sha256: string;
|
|
54
|
+
readonly candidateIndex: number;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const templateDirectory = resolve(
|
|
58
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
59
|
+
"..",
|
|
60
|
+
"assets",
|
|
61
|
+
"code-owned-provider-template",
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const templateFiles = Object.freeze([
|
|
65
|
+
["plugin.ts.template", () => "plugin.ts"],
|
|
66
|
+
["runtime.ts.template", () => "runtime.ts"],
|
|
67
|
+
["plugin.test.ts.template", () => "plugin.test.ts"],
|
|
68
|
+
["runtime.internal.test.ts.template", () => "runtime.internal.test.ts"],
|
|
69
|
+
["wrench-adapter.json.template", () => "wrench-adapter.json"],
|
|
70
|
+
] as const);
|
|
71
|
+
|
|
72
|
+
const webPluginIdSuffix = "-web";
|
|
73
|
+
const webAdapterDisplayNameSuffix = " (Authenticated Web API)";
|
|
74
|
+
// These are the runtime manifest bounds enforced by model.ts. The focused
|
|
75
|
+
// round-trip test below keeps this pre-write validation synchronized with the
|
|
76
|
+
// shared parser until those schema bounds have a shared exported home.
|
|
77
|
+
const runtimeManifestIdMaxLength = 48;
|
|
78
|
+
const runtimeManifestDisplayNameMaxLength = 100;
|
|
79
|
+
const scaffoldSiteMaxLength =
|
|
80
|
+
runtimeManifestIdMaxLength - webPluginIdSuffix.length;
|
|
81
|
+
const scaffoldDisplayNameMaxLength =
|
|
82
|
+
runtimeManifestDisplayNameMaxLength - webAdapterDisplayNameSuffix.length;
|
|
83
|
+
|
|
84
|
+
function webPluginId(site: string): string {
|
|
85
|
+
return `${site}${webPluginIdSuffix}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function exactKeys(value: Record<string, unknown>, expected: readonly string[]): boolean {
|
|
89
|
+
return Object.keys(value).sort().join(",") === [...expected].sort().join(",");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function record(value: unknown, label: string): Record<string, unknown> {
|
|
93
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
94
|
+
throw new Error(`${label} must be an object`);
|
|
95
|
+
}
|
|
96
|
+
return value as Record<string, unknown>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function boundedString(
|
|
100
|
+
value: unknown,
|
|
101
|
+
label: string,
|
|
102
|
+
maximum: number,
|
|
103
|
+
): string {
|
|
104
|
+
if (
|
|
105
|
+
typeof value !== "string"
|
|
106
|
+
|| value.length < 1
|
|
107
|
+
|| value.length > maximum
|
|
108
|
+
|| /[\0\r\n]/u.test(value)
|
|
109
|
+
) throw new Error(`${label} must be bounded text`);
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function boundedStringArray(
|
|
114
|
+
value: unknown,
|
|
115
|
+
label: string,
|
|
116
|
+
maximumItems: number,
|
|
117
|
+
maximumItemLength: number,
|
|
118
|
+
): readonly string[] {
|
|
119
|
+
if (!Array.isArray(value) || value.length > maximumItems) {
|
|
120
|
+
throw new Error(`${label} exceeded its reviewed array bound`);
|
|
121
|
+
}
|
|
122
|
+
return value.map((item, index) =>
|
|
123
|
+
boundedString(item, `${label}[${index}]`, maximumItemLength));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function boundedIntegerArray(
|
|
127
|
+
value: unknown,
|
|
128
|
+
label: string,
|
|
129
|
+
maximumItems: number,
|
|
130
|
+
): readonly number[] {
|
|
131
|
+
if (!Array.isArray(value) || value.length > maximumItems) {
|
|
132
|
+
throw new Error(`${label} exceeded its reviewed array bound`);
|
|
133
|
+
}
|
|
134
|
+
return value.map((item, index) => {
|
|
135
|
+
if (!Number.isSafeInteger(item) || (item as number) < 0 || (item as number) > 999) {
|
|
136
|
+
throw new Error(`${label}[${index}] must be a bounded status integer`);
|
|
137
|
+
}
|
|
138
|
+
return item as number;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function parseEvidence(
|
|
143
|
+
path: string,
|
|
144
|
+
origin: string,
|
|
145
|
+
candidateIndex: number,
|
|
146
|
+
): EvidenceSummary {
|
|
147
|
+
if (!isAbsolute(path)) throw new Error("evidence path must be absolute");
|
|
148
|
+
let stats: ReturnType<typeof lstatSync>;
|
|
149
|
+
try {
|
|
150
|
+
stats = lstatSync(path);
|
|
151
|
+
} catch {
|
|
152
|
+
throw new Error("could not inspect the private evidence file");
|
|
153
|
+
}
|
|
154
|
+
if (!stats.isFile() || stats.isSymbolicLink()) {
|
|
155
|
+
throw new Error("evidence path must be a regular non-symlink file");
|
|
156
|
+
}
|
|
157
|
+
if (
|
|
158
|
+
typeof process.getuid === "function"
|
|
159
|
+
&& stats.uid !== process.getuid()
|
|
160
|
+
) throw new Error("evidence file must be owned by the current user");
|
|
161
|
+
if ((stats.mode & 0o077) !== 0) {
|
|
162
|
+
throw new Error("evidence file must not be readable or writable by group or others");
|
|
163
|
+
}
|
|
164
|
+
if (stats.size < 2 || stats.size > 16 * 1024 * 1024) {
|
|
165
|
+
throw new Error("evidence file exceeded its reviewed size bound");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let bytes: Buffer;
|
|
169
|
+
try {
|
|
170
|
+
bytes = readFileSync(path);
|
|
171
|
+
} catch {
|
|
172
|
+
throw new Error("could not read the private evidence file");
|
|
173
|
+
}
|
|
174
|
+
let parsed: unknown;
|
|
175
|
+
try {
|
|
176
|
+
parsed = JSON.parse(bytes.toString("utf8"));
|
|
177
|
+
} catch {
|
|
178
|
+
throw new Error("evidence file must contain valid JSON");
|
|
179
|
+
}
|
|
180
|
+
const evidence = record(parsed, "internal API evidence");
|
|
181
|
+
if (
|
|
182
|
+
!exactKeys(evidence, [
|
|
183
|
+
"adapterId",
|
|
184
|
+
"analyzedAt",
|
|
185
|
+
"candidates",
|
|
186
|
+
"observedEntries",
|
|
187
|
+
"schemaVersion",
|
|
188
|
+
"targetOrigin",
|
|
189
|
+
"warnings",
|
|
190
|
+
])
|
|
191
|
+
|| evidence.schemaVersion !== 1
|
|
192
|
+
|| boundedString(evidence.adapterId, "evidence adapter ID", 128).length < 1
|
|
193
|
+
|| boundedString(evidence.targetOrigin, "evidence target origin", 2_048) !== origin
|
|
194
|
+
|| Number.isNaN(Date.parse(boundedString(evidence.analyzedAt, "evidence timestamp", 64)))
|
|
195
|
+
|| !Number.isSafeInteger(evidence.observedEntries)
|
|
196
|
+
|| (evidence.observedEntries as number) < 0
|
|
197
|
+
|| (evidence.observedEntries as number) > 10_000
|
|
198
|
+
|| !Array.isArray(evidence.candidates)
|
|
199
|
+
|| evidence.candidates.length > 2_000
|
|
200
|
+
) {
|
|
201
|
+
throw new Error("evidence file does not match the reviewed internal-api-evidence schema and origin");
|
|
202
|
+
}
|
|
203
|
+
boundedStringArray(evidence.warnings, "evidence warnings", 2_000, 4_096);
|
|
204
|
+
if (
|
|
205
|
+
!Number.isSafeInteger(candidateIndex)
|
|
206
|
+
|| candidateIndex < 0
|
|
207
|
+
|| candidateIndex >= evidence.candidates.length
|
|
208
|
+
) throw new Error("candidate index is outside the sanitized evidence");
|
|
209
|
+
|
|
210
|
+
const candidate = record(evidence.candidates[candidateIndex], "selected evidence candidate");
|
|
211
|
+
if (
|
|
212
|
+
!exactKeys(candidate, [
|
|
213
|
+
"headerNames",
|
|
214
|
+
"method",
|
|
215
|
+
"operationType",
|
|
216
|
+
"origin",
|
|
217
|
+
"path",
|
|
218
|
+
"queryNames",
|
|
219
|
+
"requestFieldPaths",
|
|
220
|
+
"responseFieldPaths",
|
|
221
|
+
"reviewRequired",
|
|
222
|
+
"revisions",
|
|
223
|
+
"sampleCount",
|
|
224
|
+
"statuses",
|
|
225
|
+
])
|
|
226
|
+
|| candidate.reviewRequired !== true
|
|
227
|
+
|| boundedString(candidate.origin, "candidate origin", 2_048) !== origin
|
|
228
|
+
|| !/^(?:DELETE|GET|HEAD|OPTIONS|PATCH|POST|PUT)$/u.test(
|
|
229
|
+
boundedString(candidate.method, "candidate method", 16),
|
|
230
|
+
)
|
|
231
|
+
|| (
|
|
232
|
+
candidate.operationType !== "query"
|
|
233
|
+
&& candidate.operationType !== "mutation"
|
|
234
|
+
&& candidate.operationType !== "unknown"
|
|
235
|
+
)
|
|
236
|
+
|| !boundedString(candidate.path, "candidate path", 4_096).startsWith("/")
|
|
237
|
+
|| !Number.isSafeInteger(candidate.sampleCount)
|
|
238
|
+
|| (candidate.sampleCount as number) < 1
|
|
239
|
+
|| (candidate.sampleCount as number) > 10_000
|
|
240
|
+
) {
|
|
241
|
+
throw new Error("selected evidence candidate is not a review-required exact-origin exchange");
|
|
242
|
+
}
|
|
243
|
+
boundedIntegerArray(candidate.statuses, "candidate statuses", 100);
|
|
244
|
+
boundedStringArray(candidate.queryNames, "candidate query names", 1_000, 128);
|
|
245
|
+
boundedStringArray(candidate.headerNames, "candidate header names", 200, 128);
|
|
246
|
+
boundedStringArray(candidate.requestFieldPaths, "candidate request paths", 1_000, 4_096);
|
|
247
|
+
boundedStringArray(candidate.responseFieldPaths, "candidate response paths", 1_000, 4_096);
|
|
248
|
+
boundedStringArray(candidate.revisions, "candidate revisions", 1_000, 512);
|
|
249
|
+
return {
|
|
250
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
251
|
+
candidateIndex,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function exactOrigin(value: string): URL {
|
|
256
|
+
let url: URL;
|
|
257
|
+
try {
|
|
258
|
+
url = new URL(value);
|
|
259
|
+
} catch {
|
|
260
|
+
throw new Error("origin must be an absolute HTTPS origin");
|
|
261
|
+
}
|
|
262
|
+
const hostname = url.hostname.toLowerCase();
|
|
263
|
+
if (
|
|
264
|
+
url.protocol !== "https:"
|
|
265
|
+
|| url.username !== ""
|
|
266
|
+
|| url.password !== ""
|
|
267
|
+
|| url.pathname !== "/"
|
|
268
|
+
|| url.search !== ""
|
|
269
|
+
|| url.hash !== ""
|
|
270
|
+
|| hostname === "localhost"
|
|
271
|
+
|| hostname.endsWith(".localhost")
|
|
272
|
+
|| hostname.endsWith(".local")
|
|
273
|
+
|| hostname.endsWith(".internal")
|
|
274
|
+
|| isIP(hostname) !== 0
|
|
275
|
+
|| !hostname.includes(".")
|
|
276
|
+
) throw new Error("origin must be one exact public HTTPS origin without a path");
|
|
277
|
+
return url;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function siteId(value: string): string {
|
|
281
|
+
if (
|
|
282
|
+
value.length > scaffoldSiteMaxLength
|
|
283
|
+
|| !isProviderPluginSurfaceId(value)
|
|
284
|
+
|| !isProviderPluginId(webPluginId(value))
|
|
285
|
+
) {
|
|
286
|
+
throw new Error(
|
|
287
|
+
`site must start with a letter, use lowercase kebab-case, and be at most ${scaffoldSiteMaxLength} characters so its ${webPluginIdSuffix} manifest ID remains installable`,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
return value;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function semanticOperation(value: string): string {
|
|
294
|
+
if (!isProviderPluginOperationName(value)) {
|
|
295
|
+
throw new Error("operation must be a bounded dotted semantic outcome");
|
|
296
|
+
}
|
|
297
|
+
const transportWords = new Set([
|
|
298
|
+
"browser",
|
|
299
|
+
"cookie",
|
|
300
|
+
"dom",
|
|
301
|
+
"endpoint",
|
|
302
|
+
"graphql",
|
|
303
|
+
"header",
|
|
304
|
+
"http",
|
|
305
|
+
"api",
|
|
306
|
+
"javascript",
|
|
307
|
+
"raw",
|
|
308
|
+
"request",
|
|
309
|
+
"restli",
|
|
310
|
+
"selector",
|
|
311
|
+
"url",
|
|
312
|
+
]);
|
|
313
|
+
const tokens = value.split(/[.-]/u);
|
|
314
|
+
if (tokens.some((part) => transportWords.has(part))) {
|
|
315
|
+
throw new Error("operation must describe a semantic outcome, not a transport primitive");
|
|
316
|
+
}
|
|
317
|
+
if (tokens.some((part) =>
|
|
318
|
+
part === "admin"
|
|
319
|
+
|| part === "credential"
|
|
320
|
+
|| part === "delete"
|
|
321
|
+
|| part === "destroy"
|
|
322
|
+
|| part === "payment"
|
|
323
|
+
|| part === "permission"
|
|
324
|
+
|| part === "remove")) {
|
|
325
|
+
throw new Error("R4 or destructive outcomes cannot use the provider scaffold");
|
|
326
|
+
}
|
|
327
|
+
return value;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function displayName(value: string): string {
|
|
331
|
+
if (
|
|
332
|
+
value.length > scaffoldDisplayNameMaxLength
|
|
333
|
+
|| !/^[A-Za-z0-9][A-Za-z0-9 .&'()+_-]*$/u.test(value)
|
|
334
|
+
) {
|
|
335
|
+
throw new Error(
|
|
336
|
+
`display name must be one bounded line of at most ${scaffoldDisplayNameMaxLength} characters so the generated manifest display name remains installable`,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
return value;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function exactRisk(value: unknown): Risk {
|
|
343
|
+
if (value !== "R1" && value !== "R2" && value !== "R3") {
|
|
344
|
+
throw new Error("risk must be R1, R2, or R3");
|
|
345
|
+
}
|
|
346
|
+
return value;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function assertSemanticRisk(operation: string, risk: Risk): void {
|
|
350
|
+
const finalVerb = operation.split(".").at(-1);
|
|
351
|
+
const r1Verbs: ReadonlySet<string> = new Set(["list", "read"]);
|
|
352
|
+
const r2Verbs: ReadonlySet<string> = new Set(["save", "set"]);
|
|
353
|
+
const r3Verbs: ReadonlySet<string> = new Set([
|
|
354
|
+
"create",
|
|
355
|
+
"edit",
|
|
356
|
+
"publish",
|
|
357
|
+
"quote",
|
|
358
|
+
"repost",
|
|
359
|
+
"schedule",
|
|
360
|
+
"send",
|
|
361
|
+
"share",
|
|
362
|
+
"upload",
|
|
363
|
+
]);
|
|
364
|
+
let expected: Risk | null = null;
|
|
365
|
+
if (finalVerb !== undefined && r1Verbs.has(finalVerb)) expected = "R1";
|
|
366
|
+
else if (finalVerb !== undefined && r2Verbs.has(finalVerb)) expected = "R2";
|
|
367
|
+
else if (finalVerb !== undefined && r3Verbs.has(finalVerb)) expected = "R3";
|
|
368
|
+
if (expected !== null && risk !== expected) {
|
|
369
|
+
throw new Error(`${operation} must use ${expected}, not ${risk}`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function constantPrefix(value: string): string {
|
|
374
|
+
return value.split("-").map((part) => part.toUpperCase()).join("_");
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function pascalName(value: string): string {
|
|
378
|
+
return value
|
|
379
|
+
.split("-")
|
|
380
|
+
.map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`)
|
|
381
|
+
.join("");
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function camelName(value: string): string {
|
|
385
|
+
const pascal = pascalName(value);
|
|
386
|
+
return `${pascal[0]?.toLowerCase() ?? ""}${pascal.slice(1)}`;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function replacements(
|
|
390
|
+
options: ScaffoldOptions,
|
|
391
|
+
origin: URL,
|
|
392
|
+
): Readonly<Record<string, string>> {
|
|
393
|
+
const readOnly = options.risk === "R1";
|
|
394
|
+
return Object.freeze({
|
|
395
|
+
"__ADAPTER_DISPLAY_NAME_JSON__": JSON.stringify(
|
|
396
|
+
`${options.displayName}${webAdapterDisplayNameSuffix}`,
|
|
397
|
+
),
|
|
398
|
+
"__CAMEL_NAME__": camelName(options.site),
|
|
399
|
+
"__CONSTANT_PREFIX__": constantPrefix(options.site),
|
|
400
|
+
"__DEDUPE_WINDOW_MS__": readOnly ? "0" : "86400000",
|
|
401
|
+
"__DISPATCH_JSON__": JSON.stringify(readOnly ? "none" : "single"),
|
|
402
|
+
"__DISPLAY_NAME__": options.displayName,
|
|
403
|
+
"__EFFECT_JSON__": JSON.stringify(readOnly ? "read" : "write"),
|
|
404
|
+
"__HOSTNAME__": origin.hostname,
|
|
405
|
+
"__HOSTNAME_JSON__": JSON.stringify(origin.hostname),
|
|
406
|
+
"__IDEMPOTENCY__": readOnly ? "none" : "local-at-most-once",
|
|
407
|
+
"__OPERATION_JSON__": JSON.stringify(options.operation),
|
|
408
|
+
"__OPERATION__": options.operation,
|
|
409
|
+
"__ORIGIN_JSON__": JSON.stringify(origin.origin),
|
|
410
|
+
"__ORIGIN__": origin.origin,
|
|
411
|
+
"__PASCAL_NAME__": pascalName(options.site),
|
|
412
|
+
"__PLUGIN_ID_JSON__": JSON.stringify(webPluginId(options.site)),
|
|
413
|
+
"__PLAN_DISPATCHES__": readOnly
|
|
414
|
+
? "[]"
|
|
415
|
+
: `[{ id: "execute", description: ${JSON.stringify(
|
|
416
|
+
`Execute ${options.site} ${options.operation}`,
|
|
417
|
+
)} }]`,
|
|
418
|
+
"__RISK_JSON__": JSON.stringify(options.risk),
|
|
419
|
+
"__RISK__": options.risk,
|
|
420
|
+
"__SIDE_EFFECT__": readOnly
|
|
421
|
+
? "none"
|
|
422
|
+
: "Replace this placeholder with the exact bounded remote effect before promotion.",
|
|
423
|
+
"__SITE_ID_JSON__": JSON.stringify(options.site),
|
|
424
|
+
"__SITE_ID__": options.site,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function renderTemplate(
|
|
429
|
+
source: string,
|
|
430
|
+
values: Readonly<Record<string, string>>,
|
|
431
|
+
): string {
|
|
432
|
+
let rendered = source;
|
|
433
|
+
for (const [token, value] of Object.entries(values)) {
|
|
434
|
+
rendered = rendered.split(token).join(value);
|
|
435
|
+
}
|
|
436
|
+
const unresolved = rendered.match(/__[A-Z][A-Z0-9_]*__/gu);
|
|
437
|
+
if (unresolved !== null) {
|
|
438
|
+
throw new Error(`provider template retained unresolved token ${unresolved[0]}`);
|
|
439
|
+
}
|
|
440
|
+
return rendered;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function prepareOutputDirectory(path: string): string {
|
|
444
|
+
if (!isAbsolute(path)) throw new Error("output directory must be absolute");
|
|
445
|
+
try {
|
|
446
|
+
const stats = lstatSync(path);
|
|
447
|
+
if (stats.isSymbolicLink() || !stats.isDirectory()) {
|
|
448
|
+
throw new Error("output path must not be a symlink or non-directory");
|
|
449
|
+
}
|
|
450
|
+
if (readdirSync(path).length > 0) {
|
|
451
|
+
throw new Error("output directory must not already contain files");
|
|
452
|
+
}
|
|
453
|
+
} catch (error) {
|
|
454
|
+
if (
|
|
455
|
+
error instanceof Error
|
|
456
|
+
&& "code" in error
|
|
457
|
+
&& (error as NodeJS.ErrnoException).code === "ENOENT"
|
|
458
|
+
) {
|
|
459
|
+
mkdirSync(path, { recursive: true, mode: 0o700 });
|
|
460
|
+
chmodSync(path, 0o700);
|
|
461
|
+
return path;
|
|
462
|
+
}
|
|
463
|
+
if (error instanceof Error) throw error;
|
|
464
|
+
throw new Error("could not inspect output directory");
|
|
465
|
+
}
|
|
466
|
+
chmodSync(path, 0o700);
|
|
467
|
+
return path;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function readSourcePluginFile(
|
|
471
|
+
directory: string,
|
|
472
|
+
name: string,
|
|
473
|
+
): string {
|
|
474
|
+
const path = join(directory, name);
|
|
475
|
+
let stats: ReturnType<typeof lstatSync>;
|
|
476
|
+
try {
|
|
477
|
+
stats = lstatSync(path);
|
|
478
|
+
} catch {
|
|
479
|
+
throw new Error(`source provider plugin is missing ${name}`);
|
|
480
|
+
}
|
|
481
|
+
if (stats.isSymbolicLink() || !stats.isFile()) {
|
|
482
|
+
throw new Error(`source provider plugin ${name} must be a regular non-symlink file`);
|
|
483
|
+
}
|
|
484
|
+
if (stats.size < 1 || stats.size > 2 * 1024 * 1024) {
|
|
485
|
+
throw new Error(`source provider plugin ${name} exceeded its reviewed size bound`);
|
|
486
|
+
}
|
|
487
|
+
try {
|
|
488
|
+
return readFileSync(path, "utf8");
|
|
489
|
+
} catch {
|
|
490
|
+
throw new Error(`could not read source provider plugin ${name}`);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function sourceAdapterManifestName(directory: string): string {
|
|
495
|
+
const names = ["wrench-adapter.json", "oh-adapter.json"].filter((name) => {
|
|
496
|
+
try {
|
|
497
|
+
return lstatSync(join(directory, name)).isFile();
|
|
498
|
+
} catch {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
if (names.length === 0) {
|
|
503
|
+
throw new Error("source provider plugin is missing wrench-adapter.json");
|
|
504
|
+
}
|
|
505
|
+
if (names.length > 1) {
|
|
506
|
+
throw new Error(
|
|
507
|
+
"source provider plugin must contain exactly one of wrench-adapter.json or oh-adapter.json",
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
const name = names[0];
|
|
511
|
+
if (name === undefined) throw new Error("source adapter manifest selection failed");
|
|
512
|
+
return name;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function parseSourcePluginJson(source: string, name: string): Record<string, unknown> {
|
|
516
|
+
let value: unknown;
|
|
517
|
+
try {
|
|
518
|
+
value = JSON.parse(source);
|
|
519
|
+
} catch {
|
|
520
|
+
throw new Error(`source provider plugin ${name} must contain valid JSON`);
|
|
521
|
+
}
|
|
522
|
+
return record(value, `source provider plugin ${name}`);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function assertCanonicalSourcePluginLocation(
|
|
526
|
+
directory: string,
|
|
527
|
+
pluginId: string,
|
|
528
|
+
): void {
|
|
529
|
+
const cliSourceRoot = resolve(directory, "..", "..");
|
|
530
|
+
let providerPluginStats: ReturnType<typeof lstatSync>;
|
|
531
|
+
let authStats: ReturnType<typeof lstatSync>;
|
|
532
|
+
let webSessionStats: ReturnType<typeof lstatSync>;
|
|
533
|
+
try {
|
|
534
|
+
providerPluginStats = lstatSync(join(cliSourceRoot, "provider-plugin.ts"));
|
|
535
|
+
authStats = lstatSync(join(cliSourceRoot, "auth.ts"));
|
|
536
|
+
webSessionStats = lstatSync(join(cliSourceRoot, "web-session.ts"));
|
|
537
|
+
} catch {
|
|
538
|
+
throw new Error(
|
|
539
|
+
"source provider plugin must be checked at src/plugins/<plugin-id> so its ../../provider-plugin, ../../auth, and ../../web-session imports resolve",
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
if (
|
|
543
|
+
basename(directory) !== pluginId
|
|
544
|
+
|| basename(dirname(directory)) !== "plugins"
|
|
545
|
+
|| providerPluginStats.isSymbolicLink()
|
|
546
|
+
|| !providerPluginStats.isFile()
|
|
547
|
+
|| authStats.isSymbolicLink()
|
|
548
|
+
|| !authStats.isFile()
|
|
549
|
+
|| webSessionStats.isSymbolicLink()
|
|
550
|
+
|| !webSessionStats.isFile()
|
|
551
|
+
) {
|
|
552
|
+
throw new Error(
|
|
553
|
+
`source provider plugin ${pluginId} must live at src/plugins/${pluginId}`,
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function assertCaptureRequiredRuntimeIsInert(source: string): void {
|
|
559
|
+
const forbidden = [
|
|
560
|
+
/\bfetch\s*\(/u,
|
|
561
|
+
/\bBun\.connect\s*\(/u,
|
|
562
|
+
/\b(?:http|https)\.request\s*\(/u,
|
|
563
|
+
/\bnew\s+WebSocket\s*\(/u,
|
|
564
|
+
/\bpage\.(?:click|fill|goto|press|type|upload)\s*\(/u,
|
|
565
|
+
/\b(?:browser|context)\.newPage\s*\(/u,
|
|
566
|
+
] as const;
|
|
567
|
+
if (forbidden.some((pattern) => pattern.test(source))) {
|
|
568
|
+
throw new Error(
|
|
569
|
+
"capture-required source provider runtime must remain network-inert and contain no browser action",
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
if (!source.includes("capture-required")) {
|
|
573
|
+
throw new Error("capture-required source provider runtime must fail closed");
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Check the generated source unit without importing or executing plugin code.
|
|
579
|
+
* This is a layout and scaffold-conformance check, not a sandbox or trust
|
|
580
|
+
* boundary for executable source.
|
|
581
|
+
*/
|
|
582
|
+
export function checkSourceProviderPluginDirectory(
|
|
583
|
+
path: string,
|
|
584
|
+
): SourceProviderPluginCheck {
|
|
585
|
+
const directory = resolve(path);
|
|
586
|
+
let directoryStats: ReturnType<typeof lstatSync>;
|
|
587
|
+
try {
|
|
588
|
+
directoryStats = lstatSync(directory);
|
|
589
|
+
} catch {
|
|
590
|
+
throw new Error("source provider plugin directory does not exist");
|
|
591
|
+
}
|
|
592
|
+
if (directoryStats.isSymbolicLink() || !directoryStats.isDirectory()) {
|
|
593
|
+
throw new Error("source provider plugin path must be a regular non-symlink directory");
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const adapterManifestName = sourceAdapterManifestName(directory);
|
|
597
|
+
const sources = {
|
|
598
|
+
"plugin.ts": readSourcePluginFile(directory, "plugin.ts"),
|
|
599
|
+
"runtime.ts": readSourcePluginFile(directory, "runtime.ts"),
|
|
600
|
+
"plugin.test.ts": readSourcePluginFile(directory, "plugin.test.ts"),
|
|
601
|
+
"runtime.internal.test.ts": readSourcePluginFile(
|
|
602
|
+
directory,
|
|
603
|
+
"runtime.internal.test.ts",
|
|
604
|
+
),
|
|
605
|
+
"wrench-adapter.json": readSourcePluginFile(directory, adapterManifestName),
|
|
606
|
+
"promotion-checklist.json": readSourcePluginFile(
|
|
607
|
+
directory,
|
|
608
|
+
"promotion-checklist.json",
|
|
609
|
+
),
|
|
610
|
+
} satisfies Record<(typeof SOURCE_PROVIDER_PLUGIN_FILES)[number], string>;
|
|
611
|
+
const checklist = parseSourcePluginJson(
|
|
612
|
+
sources["promotion-checklist.json"],
|
|
613
|
+
"promotion-checklist.json",
|
|
614
|
+
);
|
|
615
|
+
if (
|
|
616
|
+
!exactKeys(checklist, [
|
|
617
|
+
"operation",
|
|
618
|
+
"origin",
|
|
619
|
+
"pluginId",
|
|
620
|
+
"proofs",
|
|
621
|
+
"risk",
|
|
622
|
+
"sanitizedEvidence",
|
|
623
|
+
"schemaVersion",
|
|
624
|
+
"site",
|
|
625
|
+
"state",
|
|
626
|
+
])
|
|
627
|
+
|| checklist.schemaVersion !== 1
|
|
628
|
+
|| checklist.state !== "capture-required"
|
|
629
|
+
) {
|
|
630
|
+
throw new Error("source provider plugin checklist must be schema-v1 capture-required state");
|
|
631
|
+
}
|
|
632
|
+
const site = siteId(boundedString(checklist.site, "checklist site", 63));
|
|
633
|
+
const pluginId = boundedString(
|
|
634
|
+
checklist.pluginId,
|
|
635
|
+
"checklist plugin ID",
|
|
636
|
+
runtimeManifestIdMaxLength,
|
|
637
|
+
);
|
|
638
|
+
if (!isProviderPluginId(pluginId) || pluginId !== webPluginId(site)) {
|
|
639
|
+
throw new Error("source provider plugin checklist ID must match its web surface");
|
|
640
|
+
}
|
|
641
|
+
const operation = semanticOperation(
|
|
642
|
+
boundedString(checklist.operation, "checklist operation", 163),
|
|
643
|
+
);
|
|
644
|
+
const risk = exactRisk(checklist.risk);
|
|
645
|
+
assertSemanticRisk(operation, risk);
|
|
646
|
+
const origin = exactOrigin(boundedString(checklist.origin, "checklist origin", 2_048));
|
|
647
|
+
|
|
648
|
+
const sanitizedEvidence = record(
|
|
649
|
+
checklist.sanitizedEvidence,
|
|
650
|
+
"checklist sanitized evidence",
|
|
651
|
+
);
|
|
652
|
+
if (
|
|
653
|
+
!exactKeys(sanitizedEvidence, ["candidateIndex", "sha256"])
|
|
654
|
+
|| !Number.isSafeInteger(sanitizedEvidence.candidateIndex)
|
|
655
|
+
|| (sanitizedEvidence.candidateIndex as number) < 0
|
|
656
|
+
|| !/^[a-f0-9]{64}$/u.test(
|
|
657
|
+
boundedString(sanitizedEvidence.sha256, "checklist evidence hash", 64),
|
|
658
|
+
)
|
|
659
|
+
) {
|
|
660
|
+
throw new Error("source provider plugin checklist has invalid sanitized evidence binding");
|
|
661
|
+
}
|
|
662
|
+
const proofNames = [
|
|
663
|
+
"accountBinding",
|
|
664
|
+
"actorBinding",
|
|
665
|
+
"authorizedLowStakesLiveFixture",
|
|
666
|
+
"deterministicTests",
|
|
667
|
+
"exactRequest",
|
|
668
|
+
"exactResponse",
|
|
669
|
+
"incidentalEffects",
|
|
670
|
+
"secretScan",
|
|
671
|
+
"targetBinding",
|
|
672
|
+
"uncertaintyBehavior",
|
|
673
|
+
] as const;
|
|
674
|
+
const proofs = record(checklist.proofs, "checklist proofs");
|
|
675
|
+
if (
|
|
676
|
+
!exactKeys(proofs, proofNames)
|
|
677
|
+
|| proofNames.some((name) => proofs[name] !== false)
|
|
678
|
+
) {
|
|
679
|
+
throw new Error("capture-required source provider plugin proofs must all remain false");
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const manifest = parseSourcePluginJson(
|
|
683
|
+
sources["wrench-adapter.json"],
|
|
684
|
+
adapterManifestName,
|
|
685
|
+
);
|
|
686
|
+
if (
|
|
687
|
+
!exactKeys(manifest, [
|
|
688
|
+
"browserDomains",
|
|
689
|
+
"displayName",
|
|
690
|
+
"id",
|
|
691
|
+
"operations",
|
|
692
|
+
"origins",
|
|
693
|
+
"schemaVersion",
|
|
694
|
+
"surfaceId",
|
|
695
|
+
"version",
|
|
696
|
+
])
|
|
697
|
+
|| manifest.schemaVersion !== 4
|
|
698
|
+
|| manifest.id !== pluginId
|
|
699
|
+
|| manifest.surfaceId !== site
|
|
700
|
+
|| !/^(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)$/u.test(
|
|
701
|
+
boundedString(manifest.version, "plugin manifest version", 64),
|
|
702
|
+
)
|
|
703
|
+
|| boundedString(
|
|
704
|
+
manifest.displayName,
|
|
705
|
+
"plugin manifest display name",
|
|
706
|
+
runtimeManifestDisplayNameMaxLength,
|
|
707
|
+
).length < 1
|
|
708
|
+
) {
|
|
709
|
+
throw new Error("source provider plugin manifest identity does not match its checklist");
|
|
710
|
+
}
|
|
711
|
+
const origins = boundedStringArray(manifest.origins, "plugin manifest origins", 1, 2_048);
|
|
712
|
+
const browserDomains = boundedStringArray(
|
|
713
|
+
manifest.browserDomains,
|
|
714
|
+
"plugin manifest browser domains",
|
|
715
|
+
1,
|
|
716
|
+
253,
|
|
717
|
+
);
|
|
718
|
+
if (
|
|
719
|
+
origins.length !== 1
|
|
720
|
+
|| origins[0] !== origin.origin
|
|
721
|
+
|| browserDomains.length !== 1
|
|
722
|
+
|| browserDomains[0] !== origin.hostname
|
|
723
|
+
) {
|
|
724
|
+
throw new Error("source provider plugin manifest origin does not match its checklist");
|
|
725
|
+
}
|
|
726
|
+
const operations = record(manifest.operations, "plugin manifest operations");
|
|
727
|
+
if (!exactKeys(operations, [operation])) {
|
|
728
|
+
throw new Error("source provider plugin manifest must reserve exactly its checklist operation");
|
|
729
|
+
}
|
|
730
|
+
const manifestOperation = record(
|
|
731
|
+
operations[operation],
|
|
732
|
+
"plugin manifest operation",
|
|
733
|
+
);
|
|
734
|
+
if (
|
|
735
|
+
!exactKeys(manifestOperation, [
|
|
736
|
+
"dedupeWindowMs",
|
|
737
|
+
"description",
|
|
738
|
+
"idempotency",
|
|
739
|
+
"input",
|
|
740
|
+
"risk",
|
|
741
|
+
"sideEffect",
|
|
742
|
+
"webSession",
|
|
743
|
+
])
|
|
744
|
+
|| manifestOperation.risk !== risk
|
|
745
|
+
|| !Number.isSafeInteger(manifestOperation.dedupeWindowMs)
|
|
746
|
+
|| (manifestOperation.dedupeWindowMs as number) < 0
|
|
747
|
+
|| (
|
|
748
|
+
manifestOperation.idempotency !== "none"
|
|
749
|
+
&& manifestOperation.idempotency !== "local-at-most-once"
|
|
750
|
+
)
|
|
751
|
+
) {
|
|
752
|
+
throw new Error("source provider plugin manifest operation is not a bounded reservation");
|
|
753
|
+
}
|
|
754
|
+
boundedString(manifestOperation.description, "plugin manifest operation description", 4_096);
|
|
755
|
+
boundedString(manifestOperation.sideEffect, "plugin manifest operation side effect", 4_096);
|
|
756
|
+
const input = record(manifestOperation.input, "plugin manifest operation input");
|
|
757
|
+
if (
|
|
758
|
+
!exactKeys(input, ["properties", "required"])
|
|
759
|
+
|| Array.isArray(input.properties)
|
|
760
|
+
|| typeof input.properties !== "object"
|
|
761
|
+
|| input.properties === null
|
|
762
|
+
|| !Array.isArray(input.required)
|
|
763
|
+
) {
|
|
764
|
+
throw new Error("source provider plugin manifest operation input is invalid");
|
|
765
|
+
}
|
|
766
|
+
record(input.properties, "plugin manifest operation input properties");
|
|
767
|
+
boundedStringArray(input.required, "plugin manifest required inputs", 1_000, 128);
|
|
768
|
+
const webSession = record(
|
|
769
|
+
manifestOperation.webSession,
|
|
770
|
+
"plugin manifest web-session selector",
|
|
771
|
+
);
|
|
772
|
+
if (
|
|
773
|
+
!exactKeys(webSession, [
|
|
774
|
+
"action",
|
|
775
|
+
"contractVersion",
|
|
776
|
+
"maxOutputBytes",
|
|
777
|
+
"site",
|
|
778
|
+
"timeoutMs",
|
|
779
|
+
])
|
|
780
|
+
|| webSession.site !== site
|
|
781
|
+
|| webSession.action !== operation
|
|
782
|
+
|| webSession.contractVersion !== 1
|
|
783
|
+
|| !Number.isSafeInteger(webSession.timeoutMs)
|
|
784
|
+
|| (webSession.timeoutMs as number) < 1
|
|
785
|
+
|| !Number.isSafeInteger(webSession.maxOutputBytes)
|
|
786
|
+
|| (webSession.maxOutputBytes as number) < 1
|
|
787
|
+
) {
|
|
788
|
+
throw new Error("source provider plugin manifest selector does not match its checklist");
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
assertCanonicalSourcePluginLocation(directory, pluginId);
|
|
792
|
+
const pluginSource = sources["plugin.ts"];
|
|
793
|
+
if (
|
|
794
|
+
!pluginSource.includes("defineProviderPlugin")
|
|
795
|
+
|| !pluginSource.includes("export default")
|
|
796
|
+
|| !pluginSource.includes(JSON.stringify(pluginId))
|
|
797
|
+
|| !pluginSource.includes(JSON.stringify(operation))
|
|
798
|
+
|| !pluginSource.includes('state: "capture-required"')
|
|
799
|
+
) {
|
|
800
|
+
throw new Error("source provider plugin definition is not a capture-required v1 plugin");
|
|
801
|
+
}
|
|
802
|
+
assertCaptureRequiredRuntimeIsInert(sources["runtime.ts"]);
|
|
803
|
+
const transpiler = new Bun.Transpiler({ loader: "ts" });
|
|
804
|
+
for (const name of [
|
|
805
|
+
"plugin.ts",
|
|
806
|
+
"runtime.ts",
|
|
807
|
+
"plugin.test.ts",
|
|
808
|
+
"runtime.internal.test.ts",
|
|
809
|
+
] as const) {
|
|
810
|
+
try {
|
|
811
|
+
transpiler.transformSync(sources[name]);
|
|
812
|
+
} catch {
|
|
813
|
+
throw new Error(`source provider plugin ${name} is not valid TypeScript syntax`);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
return Object.freeze({
|
|
817
|
+
status: "capture-required",
|
|
818
|
+
executable: false,
|
|
819
|
+
pluginId,
|
|
820
|
+
directory,
|
|
821
|
+
files: SOURCE_PROVIDER_PLUGIN_FILES,
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
export function scaffoldWebProvider(options: ScaffoldOptions): readonly string[] {
|
|
826
|
+
const normalized: ScaffoldOptions = {
|
|
827
|
+
...options,
|
|
828
|
+
site: siteId(options.site),
|
|
829
|
+
displayName: displayName(options.displayName),
|
|
830
|
+
operation: semanticOperation(options.operation),
|
|
831
|
+
risk: exactRisk(options.risk),
|
|
832
|
+
};
|
|
833
|
+
assertSemanticRisk(normalized.operation, normalized.risk);
|
|
834
|
+
const origin = exactOrigin(normalized.origin);
|
|
835
|
+
const evidence = parseEvidence(
|
|
836
|
+
normalized.evidencePath,
|
|
837
|
+
origin.origin,
|
|
838
|
+
normalized.candidateIndex,
|
|
839
|
+
);
|
|
840
|
+
const values = replacements(normalized, origin);
|
|
841
|
+
const rendered: { path: string; content: string }[] = templateFiles.map(
|
|
842
|
+
([templateName, outputName]) => {
|
|
843
|
+
const templatePath = join(templateDirectory, templateName);
|
|
844
|
+
const source = readFileSync(templatePath, "utf8");
|
|
845
|
+
return {
|
|
846
|
+
path: outputName(),
|
|
847
|
+
content: renderTemplate(source, values),
|
|
848
|
+
};
|
|
849
|
+
},
|
|
850
|
+
);
|
|
851
|
+
const checklist = {
|
|
852
|
+
schemaVersion: 1,
|
|
853
|
+
state: "capture-required",
|
|
854
|
+
pluginId: webPluginId(normalized.site),
|
|
855
|
+
site: normalized.site,
|
|
856
|
+
operation: normalized.operation,
|
|
857
|
+
origin: origin.origin,
|
|
858
|
+
risk: normalized.risk,
|
|
859
|
+
sanitizedEvidence: evidence,
|
|
860
|
+
proofs: {
|
|
861
|
+
exactRequest: false,
|
|
862
|
+
exactResponse: false,
|
|
863
|
+
accountBinding: false,
|
|
864
|
+
actorBinding: false,
|
|
865
|
+
targetBinding: false,
|
|
866
|
+
incidentalEffects: false,
|
|
867
|
+
uncertaintyBehavior: false,
|
|
868
|
+
deterministicTests: false,
|
|
869
|
+
authorizedLowStakesLiveFixture: false,
|
|
870
|
+
secretScan: false,
|
|
871
|
+
},
|
|
872
|
+
} as const;
|
|
873
|
+
rendered.push({
|
|
874
|
+
path: "promotion-checklist.json",
|
|
875
|
+
content: `${JSON.stringify(checklist, null, 2)}\n`,
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
const outputDirectory = prepareOutputDirectory(normalized.outputDirectory);
|
|
879
|
+
const written: string[] = [];
|
|
880
|
+
for (const file of rendered) {
|
|
881
|
+
const target = join(outputDirectory, file.path);
|
|
882
|
+
mkdirSync(dirname(target), { recursive: true, mode: 0o700 });
|
|
883
|
+
writeFileSync(target, file.content, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
884
|
+
written.push(target);
|
|
885
|
+
}
|
|
886
|
+
return Object.freeze(written);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
type ParsedArguments = ScaffoldOptions;
|
|
890
|
+
|
|
891
|
+
function parseArguments(argv: readonly string[]): ParsedArguments {
|
|
892
|
+
const values = new Map<string, string>();
|
|
893
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
894
|
+
const flag = argv[index];
|
|
895
|
+
if (
|
|
896
|
+
flag !== "--site"
|
|
897
|
+
&& flag !== "--display-name"
|
|
898
|
+
&& flag !== "--origin"
|
|
899
|
+
&& flag !== "--operation"
|
|
900
|
+
&& flag !== "--risk"
|
|
901
|
+
&& flag !== "--evidence"
|
|
902
|
+
&& flag !== "--candidate"
|
|
903
|
+
&& flag !== "--output"
|
|
904
|
+
) throw new Error(`unknown scaffold option ${flag ?? ""}`);
|
|
905
|
+
const value = argv[index + 1];
|
|
906
|
+
if (value === undefined || value.startsWith("--")) {
|
|
907
|
+
throw new Error(`${flag} requires a value`);
|
|
908
|
+
}
|
|
909
|
+
if (values.has(flag)) throw new Error(`${flag} may appear only once`);
|
|
910
|
+
values.set(flag, value);
|
|
911
|
+
index += 1;
|
|
912
|
+
}
|
|
913
|
+
const required = [
|
|
914
|
+
"--site",
|
|
915
|
+
"--display-name",
|
|
916
|
+
"--origin",
|
|
917
|
+
"--operation",
|
|
918
|
+
"--risk",
|
|
919
|
+
"--evidence",
|
|
920
|
+
"--candidate",
|
|
921
|
+
"--output",
|
|
922
|
+
] as const;
|
|
923
|
+
for (const flag of required) {
|
|
924
|
+
if (!values.has(flag)) throw new Error(`missing required scaffold option ${flag}`);
|
|
925
|
+
}
|
|
926
|
+
const risk = values.get("--risk");
|
|
927
|
+
if (risk !== "R1" && risk !== "R2" && risk !== "R3") {
|
|
928
|
+
throw new Error("--risk must be R1, R2, or R3");
|
|
929
|
+
}
|
|
930
|
+
const candidateText = values.get("--candidate")!;
|
|
931
|
+
if (!/^(?:0|[1-9][0-9]{0,5})$/u.test(candidateText)) {
|
|
932
|
+
throw new Error("--candidate must be a zero-based integer");
|
|
933
|
+
}
|
|
934
|
+
return {
|
|
935
|
+
site: values.get("--site")!,
|
|
936
|
+
displayName: values.get("--display-name")!,
|
|
937
|
+
origin: values.get("--origin")!,
|
|
938
|
+
operation: values.get("--operation")!,
|
|
939
|
+
risk,
|
|
940
|
+
evidencePath: resolve(values.get("--evidence")!),
|
|
941
|
+
candidateIndex: Number.parseInt(candidateText, 10),
|
|
942
|
+
outputDirectory: resolve(values.get("--output")!),
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function usage(): string {
|
|
947
|
+
return [
|
|
948
|
+
"Usage:",
|
|
949
|
+
" scaffold-web-provider.ts --site <kebab-id> --display-name <name> \\",
|
|
950
|
+
" --origin <https-origin> --operation <semantic.action> --risk <R1|R2|R3> \\",
|
|
951
|
+
" --evidence </absolute/internal-api-evidence.json> --candidate <index> \\",
|
|
952
|
+
" --output </absolute/empty-directory>",
|
|
953
|
+
].join("\n");
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
if (import.meta.main) {
|
|
957
|
+
try {
|
|
958
|
+
const options = parseArguments(process.argv.slice(2));
|
|
959
|
+
const files = scaffoldWebProvider(options);
|
|
960
|
+
process.stdout.write(
|
|
961
|
+
`${JSON.stringify({
|
|
962
|
+
status: "capture-required",
|
|
963
|
+
outputDirectory: options.outputDirectory,
|
|
964
|
+
files: files.map((file) => file.slice(options.outputDirectory.length + 1)),
|
|
965
|
+
})}\n`,
|
|
966
|
+
);
|
|
967
|
+
} catch (error) {
|
|
968
|
+
process.stderr.write(
|
|
969
|
+
`ghostget provider scaffold: ${error instanceof Error ? error.message : "unknown failure"}\n${usage()}\n`,
|
|
970
|
+
);
|
|
971
|
+
process.exitCode = 1;
|
|
972
|
+
}
|
|
973
|
+
}
|