@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,994 @@
|
|
|
1
|
+
import type { InputField, InputSchema, ParseResult } from "./model";
|
|
2
|
+
|
|
3
|
+
export const WEB_SESSION_TEMPLATE_SCHEMA_VERSION = 1 as const;
|
|
4
|
+
|
|
5
|
+
export const webSessionMethods = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"] as const;
|
|
6
|
+
export type WebSessionMethod = (typeof webSessionMethods)[number];
|
|
7
|
+
|
|
8
|
+
export const webSessionInputValueTypes = [
|
|
9
|
+
"string",
|
|
10
|
+
"number",
|
|
11
|
+
"boolean",
|
|
12
|
+
"string[]",
|
|
13
|
+
"number[]",
|
|
14
|
+
"boolean[]",
|
|
15
|
+
] as const;
|
|
16
|
+
export type WebSessionInputValueType = (typeof webSessionInputValueTypes)[number];
|
|
17
|
+
|
|
18
|
+
export type WebSessionInputSource = {
|
|
19
|
+
readonly kind: "input";
|
|
20
|
+
readonly name: string;
|
|
21
|
+
/** Repeated in the reviewed AST and checked against the operation input schema. */
|
|
22
|
+
readonly valueType: WebSessionInputValueType;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type WebSessionLiteral = null | string | number | boolean;
|
|
26
|
+
|
|
27
|
+
export type WebSessionValueTemplate =
|
|
28
|
+
| { readonly kind: "literal"; readonly value: WebSessionLiteral }
|
|
29
|
+
| WebSessionInputSource
|
|
30
|
+
| {
|
|
31
|
+
readonly kind: "object";
|
|
32
|
+
readonly entries: readonly {
|
|
33
|
+
readonly name: string;
|
|
34
|
+
readonly value: WebSessionValueTemplate;
|
|
35
|
+
}[];
|
|
36
|
+
}
|
|
37
|
+
| { readonly kind: "array"; readonly items: readonly WebSessionValueTemplate[] };
|
|
38
|
+
|
|
39
|
+
export type WebSessionPathSegment =
|
|
40
|
+
| { readonly kind: "literal"; readonly value: string }
|
|
41
|
+
| (WebSessionInputSource & { readonly valueType: "string" });
|
|
42
|
+
|
|
43
|
+
export type WebSessionQueryParameter = {
|
|
44
|
+
readonly name: string;
|
|
45
|
+
/** `json` serializes the structural value; `scalar` accepts one primitive scalar only. */
|
|
46
|
+
readonly encoding: "scalar" | "json";
|
|
47
|
+
readonly value: WebSessionValueTemplate;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type WebSessionBrowserStorageSource = {
|
|
51
|
+
readonly kind: "storage";
|
|
52
|
+
readonly area: "local" | "session";
|
|
53
|
+
readonly key: string;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type WebSessionCsrfSource =
|
|
57
|
+
| { readonly kind: "cookie"; readonly name: string }
|
|
58
|
+
| { readonly kind: "meta"; readonly name: string }
|
|
59
|
+
| WebSessionBrowserStorageSource;
|
|
60
|
+
|
|
61
|
+
export type WebSessionAuthorizationSource =
|
|
62
|
+
| { readonly kind: "captured-header"; readonly name: "authorization" }
|
|
63
|
+
| WebSessionBrowserStorageSource;
|
|
64
|
+
|
|
65
|
+
export type WebSessionHeaderValue =
|
|
66
|
+
| { readonly kind: "literal"; readonly value: string }
|
|
67
|
+
| {
|
|
68
|
+
readonly kind: "browser-csrf";
|
|
69
|
+
readonly source: WebSessionCsrfSource;
|
|
70
|
+
readonly transform: "identity" | "strip-surrounding-quotes" | "url-decode";
|
|
71
|
+
}
|
|
72
|
+
| {
|
|
73
|
+
readonly kind: "browser-authorization";
|
|
74
|
+
readonly source: WebSessionAuthorizationSource;
|
|
75
|
+
readonly transform: "identity" | "bearer";
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export type WebSessionHeader = {
|
|
79
|
+
/** Canonical lower-case and fixed by the reviewed template. */
|
|
80
|
+
readonly name: string;
|
|
81
|
+
readonly value: WebSessionHeaderValue;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export type WebSessionRequestBody =
|
|
85
|
+
| { readonly kind: "none" }
|
|
86
|
+
| { readonly kind: "json"; readonly value: WebSessionValueTemplate }
|
|
87
|
+
| {
|
|
88
|
+
readonly kind: "form";
|
|
89
|
+
readonly fields: readonly {
|
|
90
|
+
readonly name: string;
|
|
91
|
+
readonly value: WebSessionValueTemplate;
|
|
92
|
+
}[];
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export type WebSessionRequestTemplate = {
|
|
96
|
+
readonly method: WebSessionMethod;
|
|
97
|
+
/** Segments are encoded independently and joined beneath the template's exact origin. */
|
|
98
|
+
readonly path: readonly WebSessionPathSegment[];
|
|
99
|
+
readonly query: readonly WebSessionQueryParameter[];
|
|
100
|
+
readonly headers: readonly WebSessionHeader[];
|
|
101
|
+
readonly body: WebSessionRequestBody;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export type WebSessionJsonPathSegment =
|
|
105
|
+
| { readonly kind: "key"; readonly key: string }
|
|
106
|
+
| { readonly kind: "index"; readonly index: number };
|
|
107
|
+
|
|
108
|
+
export const webSessionProjectionValueTypes = ["string", "number", "boolean", "null", "object", "array"] as const;
|
|
109
|
+
export type WebSessionProjectionValueType = (typeof webSessionProjectionValueTypes)[number];
|
|
110
|
+
|
|
111
|
+
export type WebSessionProjection = {
|
|
112
|
+
readonly name: string;
|
|
113
|
+
readonly path: readonly WebSessionJsonPathSegment[];
|
|
114
|
+
readonly valueType: WebSessionProjectionValueType;
|
|
115
|
+
readonly required: boolean;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export type WebSessionResponseBinding = {
|
|
119
|
+
readonly path: readonly WebSessionJsonPathSegment[];
|
|
120
|
+
/** Missing paths and non-strictly-equal values fail the response contract. */
|
|
121
|
+
readonly expected: Extract<WebSessionValueTemplate, { readonly kind: "literal" | "input" }>;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export type WebSessionResponseBody =
|
|
125
|
+
| { readonly kind: "empty" }
|
|
126
|
+
| { readonly kind: "discard" }
|
|
127
|
+
| {
|
|
128
|
+
readonly kind: "json";
|
|
129
|
+
readonly projections: readonly WebSessionProjection[];
|
|
130
|
+
readonly bindings: readonly WebSessionResponseBinding[];
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export type WebSessionResponseVariant = {
|
|
134
|
+
readonly status: number;
|
|
135
|
+
/** A lower-case media-type essence, or null to require a missing Content-Type header. */
|
|
136
|
+
readonly contentType: string | null;
|
|
137
|
+
readonly body: WebSessionResponseBody;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export type WebSessionResponseTemplate = {
|
|
141
|
+
readonly maxBytes: number;
|
|
142
|
+
/** Each status/content-type pair is exact; redirects and implicit fallbacks are impossible. */
|
|
143
|
+
readonly variants: readonly WebSessionResponseVariant[];
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export type WebSessionTemplate = {
|
|
147
|
+
readonly schemaVersion: typeof WEB_SESSION_TEMPLATE_SCHEMA_VERSION;
|
|
148
|
+
/** Exact, canonical HTTPS origin; every request path is origin-relative. */
|
|
149
|
+
readonly origin: string;
|
|
150
|
+
readonly request: WebSessionRequestTemplate;
|
|
151
|
+
readonly response: WebSessionResponseTemplate;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export type ParseWebSessionTemplateOptions = {
|
|
155
|
+
readonly input: InputSchema;
|
|
156
|
+
/** Code-owned reviewed origins. Wildcards and origin normalization are intentionally absent. */
|
|
157
|
+
readonly allowedOrigins: readonly string[];
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const MAX_ISSUES = 100;
|
|
161
|
+
const MAX_VALUE_DEPTH = 12;
|
|
162
|
+
const MAX_VALUE_NODES = 512;
|
|
163
|
+
const MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
|
|
164
|
+
const dangerousObjectKeys = new Set(["__proto__", "constructor", "prototype"]);
|
|
165
|
+
const managedHeaderNames = new Set([
|
|
166
|
+
"accept-encoding",
|
|
167
|
+
"connection",
|
|
168
|
+
"content-length",
|
|
169
|
+
"content-type",
|
|
170
|
+
"cookie",
|
|
171
|
+
"forwarded",
|
|
172
|
+
"host",
|
|
173
|
+
"origin",
|
|
174
|
+
"proxy-authorization",
|
|
175
|
+
"referer",
|
|
176
|
+
"set-cookie",
|
|
177
|
+
"te",
|
|
178
|
+
"trailer",
|
|
179
|
+
"transfer-encoding",
|
|
180
|
+
"upgrade",
|
|
181
|
+
"user-agent",
|
|
182
|
+
]);
|
|
183
|
+
|
|
184
|
+
type ParseContext = {
|
|
185
|
+
readonly input: InputSchema;
|
|
186
|
+
readonly requiredInputs: ReadonlySet<string>;
|
|
187
|
+
readonly issues: string[];
|
|
188
|
+
valueNodes: number;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
function addIssue(issues: string[], issue: string): void {
|
|
192
|
+
if (issues.length < MAX_ISSUES) issues.push(issue);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
196
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function isUnknownArray(value: unknown): value is readonly unknown[] {
|
|
200
|
+
return Array.isArray(value);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function exactKeys(value: Record<string, unknown>, allowedKeys: readonly string[], path: string, issues: string[]): void {
|
|
204
|
+
const allowed = new Set(allowedKeys);
|
|
205
|
+
for (const key of Object.keys(value)) {
|
|
206
|
+
if (!allowed.has(key)) addIssue(issues, `${path}.${key} is not supported`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function hasUnpairedSurrogate(value: string): boolean {
|
|
211
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
212
|
+
const code = value.charCodeAt(index);
|
|
213
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
214
|
+
const next = value.charCodeAt(index + 1);
|
|
215
|
+
if (!(next >= 0xdc00 && next <= 0xdfff)) return true;
|
|
216
|
+
index += 1;
|
|
217
|
+
} else if (code >= 0xdc00 && code <= 0xdfff) return true;
|
|
218
|
+
}
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function controlFreeString(
|
|
223
|
+
value: unknown,
|
|
224
|
+
path: string,
|
|
225
|
+
issues: string[],
|
|
226
|
+
minimum: number,
|
|
227
|
+
maximum: number,
|
|
228
|
+
): string | null {
|
|
229
|
+
if (typeof value !== "string" || value.length < minimum || value.length > maximum) {
|
|
230
|
+
addIssue(issues, `${path} must be a ${minimum}-${maximum} character string without control characters`);
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
234
|
+
const code = value.charCodeAt(index);
|
|
235
|
+
if (code <= 0x1f || code === 0x7f) {
|
|
236
|
+
addIssue(issues, `${path} must be a ${minimum}-${maximum} character string without control characters`);
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (hasUnpairedSurrogate(value)) {
|
|
241
|
+
addIssue(issues, `${path} must contain well-formed Unicode`);
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
return value;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function jsonString(value: unknown, path: string, issues: string[]): string | null {
|
|
248
|
+
if (typeof value !== "string" || value.length > 65_536 || value.includes("\u0000") || hasUnpairedSurrogate(value)) {
|
|
249
|
+
addIssue(issues, `${path} must be a well-formed JSON string of at most 65536 characters without NUL`);
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
return value;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function safeInteger(value: unknown, path: string, minimum: number, maximum: number, issues: string[]): number | null {
|
|
256
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
257
|
+
addIssue(issues, `${path} must be an integer between ${minimum} and ${maximum}`);
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
return value;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function boundedArray(value: unknown, path: string, maximum: number, issues: string[]): readonly unknown[] | null {
|
|
264
|
+
if (!isUnknownArray(value) || value.length > maximum) {
|
|
265
|
+
addIssue(issues, `${path} must be an array with at most ${maximum} entries`);
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
return value;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function normalizedCredentialName(value: string): string {
|
|
272
|
+
return value
|
|
273
|
+
.replaceAll("_", "-")
|
|
274
|
+
.replace(/([a-z0-9])([A-Z])/gu, "$1-$2")
|
|
275
|
+
.toLowerCase();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function isCredentialName(value: string): boolean {
|
|
279
|
+
const normalized = normalizedCredentialName(value);
|
|
280
|
+
return /(?:^|-)(?:authorization|cookie|csrf|xsrf|password|secret|credential|access-token|refresh-token|id-token|auth-token|api-key|authenticity-token)(?:-|$)/u.test(normalized);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function isCsrfHeaderName(value: string): boolean {
|
|
284
|
+
const normalized = value.toLowerCase();
|
|
285
|
+
return normalized.includes("csrf") || normalized.includes("xsrf");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function isManagedHeaderName(value: string): boolean {
|
|
289
|
+
return managedHeaderNames.has(value)
|
|
290
|
+
|| value.startsWith("sec-")
|
|
291
|
+
|| value.startsWith("proxy-")
|
|
292
|
+
|| value.startsWith("x-forwarded-")
|
|
293
|
+
|| value.startsWith("access-control-request-");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function canonicalHttpsOrigin(value: unknown, path: string, issues: string[]): string | null {
|
|
297
|
+
const origin = controlFreeString(value, path, issues, 1, 2_048);
|
|
298
|
+
if (origin === null) return null;
|
|
299
|
+
try {
|
|
300
|
+
const parsed = new URL(origin);
|
|
301
|
+
if (
|
|
302
|
+
parsed.protocol !== "https:"
|
|
303
|
+
|| parsed.username !== ""
|
|
304
|
+
|| parsed.password !== ""
|
|
305
|
+
|| parsed.hostname.includes("*")
|
|
306
|
+
|| parsed.pathname !== "/"
|
|
307
|
+
|| parsed.search !== ""
|
|
308
|
+
|| parsed.hash !== ""
|
|
309
|
+
|| parsed.origin !== origin
|
|
310
|
+
) {
|
|
311
|
+
addIssue(issues, `${path} must be one exact canonical HTTPS origin without credentials, path, query, or fragment`);
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
return origin;
|
|
315
|
+
} catch {
|
|
316
|
+
addIssue(issues, `${path} must be one exact canonical HTTPS origin without credentials, path, query, or fragment`);
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function inputValueType(field: InputField): WebSessionInputValueType | null {
|
|
322
|
+
if (field.type === "string" || field.type === "number" || field.type === "boolean") return field.type;
|
|
323
|
+
if (field.type !== "array" || field.items.type === "file") return null;
|
|
324
|
+
if (field.items.type === "string") return "string[]";
|
|
325
|
+
if (field.items.type === "number") return "number[]";
|
|
326
|
+
return "boolean[]";
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function isWebSessionMethod(value: unknown): value is WebSessionMethod {
|
|
330
|
+
return webSessionMethods.some((candidate) => candidate === value);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function isProjectionValueType(value: unknown): value is WebSessionProjectionValueType {
|
|
334
|
+
return webSessionProjectionValueTypes.some((candidate) => candidate === value);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function parseInputSource(value: unknown, path: string, context: ParseContext): WebSessionInputSource | null {
|
|
338
|
+
if (!isRecord(value)) {
|
|
339
|
+
addIssue(context.issues, `${path} must be a typed input source object`);
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
exactKeys(value, ["kind", "name", "valueType"], path, context.issues);
|
|
343
|
+
if (value.kind !== "input") {
|
|
344
|
+
addIssue(context.issues, `${path}.kind must be input`);
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
const name = controlFreeString(value.name, `${path}.name`, context.issues, 1, 64);
|
|
348
|
+
const declaredType = value.valueType;
|
|
349
|
+
if (!webSessionInputValueTypes.some((candidate) => candidate === declaredType)) {
|
|
350
|
+
addIssue(context.issues, `${path}.valueType must be ${webSessionInputValueTypes.join(", ")}`);
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
if (name === null) return null;
|
|
354
|
+
if (isCredentialName(name)) {
|
|
355
|
+
addIssue(context.issues, `${path}.name cannot designate authentication material`);
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
const field = Object.hasOwn(context.input.properties, name) ? context.input.properties[name] : undefined;
|
|
359
|
+
if (field === undefined) {
|
|
360
|
+
addIssue(context.issues, `${path}.name must name a declared input field`);
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
const actualType = inputValueType(field);
|
|
364
|
+
if (actualType === null) {
|
|
365
|
+
addIssue(context.issues, `${path}.name cannot expose file input bytes to a web-session request`);
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
if (!context.requiredInputs.has(name)) {
|
|
369
|
+
addIssue(context.issues, `${path}.name must name a required input field`);
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
if (declaredType !== actualType) {
|
|
373
|
+
addIssue(context.issues, `${path}.valueType must match input.${name} (${actualType})`);
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
return { kind: "input", name, valueType: actualType };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function parseLiteral(value: Record<string, unknown>, path: string, context: ParseContext): WebSessionValueTemplate | null {
|
|
380
|
+
exactKeys(value, ["kind", "value"], path, context.issues);
|
|
381
|
+
const literal = value.value;
|
|
382
|
+
if (literal === null || typeof literal === "boolean") return { kind: "literal", value: literal };
|
|
383
|
+
if (typeof literal === "number") {
|
|
384
|
+
if (!Number.isFinite(literal)) {
|
|
385
|
+
addIssue(context.issues, `${path}.value must be a finite JSON primitive`);
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
return { kind: "literal", value: literal };
|
|
389
|
+
}
|
|
390
|
+
const text = jsonString(literal, `${path}.value`, context.issues);
|
|
391
|
+
return text === null ? null : { kind: "literal", value: text };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function parseValueTemplate(value: unknown, path: string, context: ParseContext, depth = 0): WebSessionValueTemplate | null {
|
|
395
|
+
context.valueNodes += 1;
|
|
396
|
+
if (context.valueNodes > MAX_VALUE_NODES) {
|
|
397
|
+
addIssue(context.issues, `${path} exceeds the ${MAX_VALUE_NODES}-node value-template budget`);
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
if (depth > MAX_VALUE_DEPTH) {
|
|
401
|
+
addIssue(context.issues, `${path} exceeds the ${MAX_VALUE_DEPTH}-level value-template depth`);
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
if (!isRecord(value)) {
|
|
405
|
+
addIssue(context.issues, `${path} must be a declarative value-template object`);
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
if (value.kind === "literal") return parseLiteral(value, path, context);
|
|
409
|
+
if (value.kind === "input") return parseInputSource(value, path, context);
|
|
410
|
+
if (value.kind === "array") {
|
|
411
|
+
exactKeys(value, ["kind", "items"], path, context.issues);
|
|
412
|
+
const items = boundedArray(value.items, `${path}.items`, 100, context.issues);
|
|
413
|
+
if (items === null) return null;
|
|
414
|
+
const parsed: WebSessionValueTemplate[] = [];
|
|
415
|
+
for (const [index, item] of items.entries()) {
|
|
416
|
+
const next = parseValueTemplate(item, `${path}.items[${index}]`, context, depth + 1);
|
|
417
|
+
if (next !== null) parsed.push(next);
|
|
418
|
+
}
|
|
419
|
+
return { kind: "array", items: parsed };
|
|
420
|
+
}
|
|
421
|
+
if (value.kind === "object") {
|
|
422
|
+
exactKeys(value, ["kind", "entries"], path, context.issues);
|
|
423
|
+
const entries = boundedArray(value.entries, `${path}.entries`, 100, context.issues);
|
|
424
|
+
if (entries === null) return null;
|
|
425
|
+
const parsed: { name: string; value: WebSessionValueTemplate }[] = [];
|
|
426
|
+
const names = new Set<string>();
|
|
427
|
+
for (const [index, entry] of entries.entries()) {
|
|
428
|
+
const entryPath = `${path}.entries[${index}]`;
|
|
429
|
+
if (!isRecord(entry)) {
|
|
430
|
+
addIssue(context.issues, `${entryPath} must be an object`);
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
exactKeys(entry, ["name", "value"], entryPath, context.issues);
|
|
434
|
+
const name = controlFreeString(entry.name, `${entryPath}.name`, context.issues, 1, 128);
|
|
435
|
+
const parsedValue = parseValueTemplate(entry.value, `${entryPath}.value`, context, depth + 1);
|
|
436
|
+
if (name === null || parsedValue === null) continue;
|
|
437
|
+
if (dangerousObjectKeys.has(name)) {
|
|
438
|
+
addIssue(context.issues, `${entryPath}.name cannot be a prototype-mutating key`);
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if (isCredentialName(name)) {
|
|
442
|
+
addIssue(context.issues, `${entryPath}.name cannot be a credential sink`);
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (names.has(name)) {
|
|
446
|
+
addIssue(context.issues, `${entryPath}.name duplicates ${name}`);
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
names.add(name);
|
|
450
|
+
parsed.push({ name, value: parsedValue });
|
|
451
|
+
}
|
|
452
|
+
return { kind: "object", entries: parsed };
|
|
453
|
+
}
|
|
454
|
+
addIssue(context.issues, `${path}.kind must be literal, input, object, or array`);
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function isScalarTemplate(value: WebSessionValueTemplate): value is Extract<WebSessionValueTemplate, { readonly kind: "literal" | "input" }> {
|
|
459
|
+
if (value.kind === "literal") return value.value !== null;
|
|
460
|
+
return value.kind === "input" && !value.valueType.endsWith("[]");
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function parsePath(value: unknown, path: string, context: ParseContext): readonly WebSessionPathSegment[] | null {
|
|
464
|
+
const segments = boundedArray(value, path, 64, context.issues);
|
|
465
|
+
if (segments === null) return null;
|
|
466
|
+
const parsed: WebSessionPathSegment[] = [];
|
|
467
|
+
for (const [index, segment] of segments.entries()) {
|
|
468
|
+
const segmentPath = `${path}[${index}]`;
|
|
469
|
+
if (!isRecord(segment)) {
|
|
470
|
+
addIssue(context.issues, `${segmentPath} must be a literal or typed input segment`);
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
if (segment.kind === "literal") {
|
|
474
|
+
exactKeys(segment, ["kind", "value"], segmentPath, context.issues);
|
|
475
|
+
const text = controlFreeString(segment.value, `${segmentPath}.value`, context.issues, 1, 256);
|
|
476
|
+
if (text === null) continue;
|
|
477
|
+
if (text === "." || text === ".." || /[/\\%?#]/u.test(text)) {
|
|
478
|
+
addIssue(context.issues, `${segmentPath}.value must be one unescaped path segment`);
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
parsed.push({ kind: "literal", value: text });
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
const source = parseInputSource(segment, segmentPath, context);
|
|
485
|
+
if (source === null) continue;
|
|
486
|
+
const field = context.input.properties[source.name];
|
|
487
|
+
if (source.valueType !== "string" || field?.type !== "string" || field.format !== "path-segment") {
|
|
488
|
+
addIssue(context.issues, `${segmentPath} must reference a string input with format path-segment`);
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
parsed.push({ kind: "input", name: source.name, valueType: "string" });
|
|
492
|
+
}
|
|
493
|
+
return parsed;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function parseQuery(value: unknown, path: string, context: ParseContext): readonly WebSessionQueryParameter[] | null {
|
|
497
|
+
const entries = boundedArray(value, path, 50, context.issues);
|
|
498
|
+
if (entries === null) return null;
|
|
499
|
+
const parsed: WebSessionQueryParameter[] = [];
|
|
500
|
+
const names = new Set<string>();
|
|
501
|
+
for (const [index, entry] of entries.entries()) {
|
|
502
|
+
const entryPath = `${path}[${index}]`;
|
|
503
|
+
if (!isRecord(entry)) {
|
|
504
|
+
addIssue(context.issues, `${entryPath} must be an object`);
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
exactKeys(entry, ["name", "encoding", "value"], entryPath, context.issues);
|
|
508
|
+
const name = controlFreeString(entry.name, `${entryPath}.name`, context.issues, 1, 128);
|
|
509
|
+
const encoding = entry.encoding;
|
|
510
|
+
if (encoding !== "scalar" && encoding !== "json") {
|
|
511
|
+
addIssue(context.issues, `${entryPath}.encoding must be scalar or json`);
|
|
512
|
+
}
|
|
513
|
+
const parsedValue = parseValueTemplate(entry.value, `${entryPath}.value`, context);
|
|
514
|
+
if (name === null || parsedValue === null || (encoding !== "scalar" && encoding !== "json")) continue;
|
|
515
|
+
if (/[&=#]/u.test(name) || isCredentialName(name)) {
|
|
516
|
+
addIssue(context.issues, `${entryPath}.name must be a fixed non-credential query name`);
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
if (names.has(name)) {
|
|
520
|
+
addIssue(context.issues, `${entryPath}.name duplicates ${name}`);
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
if (encoding === "scalar" && !isScalarTemplate(parsedValue)) {
|
|
524
|
+
addIssue(context.issues, `${entryPath}.value must be one non-null scalar for scalar encoding`);
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
names.add(name);
|
|
528
|
+
parsed.push({ name, encoding, value: parsedValue });
|
|
529
|
+
}
|
|
530
|
+
return parsed;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function parseBrowserStorageSource(value: Record<string, unknown>, path: string, issues: string[]): WebSessionBrowserStorageSource | null {
|
|
534
|
+
exactKeys(value, ["kind", "area", "key"], path, issues);
|
|
535
|
+
if (value.kind !== "storage") {
|
|
536
|
+
addIssue(issues, `${path}.kind must be storage`);
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
if (value.area !== "local" && value.area !== "session") {
|
|
540
|
+
addIssue(issues, `${path}.area must be local or session`);
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
const key = controlFreeString(value.key, `${path}.key`, issues, 1, 256);
|
|
544
|
+
return key === null ? null : { kind: "storage", area: value.area, key };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function parseCsrfSource(value: unknown, path: string, issues: string[]): WebSessionCsrfSource | null {
|
|
548
|
+
if (!isRecord(value)) {
|
|
549
|
+
addIssue(issues, `${path} must be a browser-only CSRF source`);
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
if (value.kind === "storage") return parseBrowserStorageSource(value, path, issues);
|
|
553
|
+
if (value.kind === "cookie" || value.kind === "meta") {
|
|
554
|
+
exactKeys(value, ["kind", "name"], path, issues);
|
|
555
|
+
const name = controlFreeString(value.name, `${path}.name`, issues, 1, 256);
|
|
556
|
+
return name === null ? null : { kind: value.kind, name };
|
|
557
|
+
}
|
|
558
|
+
addIssue(issues, `${path}.kind must be cookie, meta, or storage`);
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function parseAuthorizationSource(value: unknown, path: string, issues: string[]): WebSessionAuthorizationSource | null {
|
|
563
|
+
if (!isRecord(value)) {
|
|
564
|
+
addIssue(issues, `${path} must be a browser-only authorization source`);
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
if (value.kind === "storage") return parseBrowserStorageSource(value, path, issues);
|
|
568
|
+
if (value.kind === "captured-header") {
|
|
569
|
+
exactKeys(value, ["kind", "name"], path, issues);
|
|
570
|
+
if (value.name !== "authorization") {
|
|
571
|
+
addIssue(issues, `${path}.name must be authorization`);
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
return { kind: "captured-header", name: "authorization" };
|
|
575
|
+
}
|
|
576
|
+
addIssue(issues, `${path}.kind must be captured-header or storage`);
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function parseHeaderValue(value: unknown, headerName: string, path: string, issues: string[]): WebSessionHeaderValue | null {
|
|
581
|
+
if (!isRecord(value)) {
|
|
582
|
+
addIssue(issues, `${path} must be a fixed literal or browser-only credential source`);
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
if (value.kind === "literal") {
|
|
586
|
+
exactKeys(value, ["kind", "value"], path, issues);
|
|
587
|
+
const literal = controlFreeString(value.value, `${path}.value`, issues, 0, 4_096);
|
|
588
|
+
if (literal === null) return null;
|
|
589
|
+
if (headerName === "authorization" || isCsrfHeaderName(headerName) || isCredentialName(headerName)) {
|
|
590
|
+
addIssue(issues, `${path} cannot put a literal into a credential-bearing header`);
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
return { kind: "literal", value: literal };
|
|
594
|
+
}
|
|
595
|
+
if (value.kind === "browser-csrf") {
|
|
596
|
+
exactKeys(value, ["kind", "source", "transform"], path, issues);
|
|
597
|
+
if (!isCsrfHeaderName(headerName)) {
|
|
598
|
+
addIssue(issues, `${path} may terminate only in a fixed CSRF/XSRF header`);
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
const source = parseCsrfSource(value.source, `${path}.source`, issues);
|
|
602
|
+
const transform = value.transform;
|
|
603
|
+
if (transform !== "identity" && transform !== "strip-surrounding-quotes" && transform !== "url-decode") {
|
|
604
|
+
addIssue(issues, `${path}.transform must be identity, strip-surrounding-quotes, or url-decode`);
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
return source === null ? null : { kind: "browser-csrf", source, transform };
|
|
608
|
+
}
|
|
609
|
+
if (value.kind === "browser-authorization") {
|
|
610
|
+
exactKeys(value, ["kind", "source", "transform"], path, issues);
|
|
611
|
+
if (headerName !== "authorization") {
|
|
612
|
+
addIssue(issues, `${path} may terminate only in the fixed authorization header`);
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
const source = parseAuthorizationSource(value.source, `${path}.source`, issues);
|
|
616
|
+
const transform = value.transform;
|
|
617
|
+
if (transform !== "identity" && transform !== "bearer") {
|
|
618
|
+
addIssue(issues, `${path}.transform must be identity or bearer`);
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
if (source?.kind === "captured-header" && transform !== "identity") {
|
|
622
|
+
addIssue(issues, `${path}.transform must be identity for a captured authorization header`);
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
return source === null ? null : { kind: "browser-authorization", source, transform };
|
|
626
|
+
}
|
|
627
|
+
addIssue(issues, `${path}.kind must be literal, browser-csrf, or browser-authorization`);
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function parseHeaders(value: unknown, path: string, issues: string[]): readonly WebSessionHeader[] | null {
|
|
632
|
+
const entries = boundedArray(value, path, 50, issues);
|
|
633
|
+
if (entries === null) return null;
|
|
634
|
+
const parsed: WebSessionHeader[] = [];
|
|
635
|
+
const names = new Set<string>();
|
|
636
|
+
for (const [index, entry] of entries.entries()) {
|
|
637
|
+
const entryPath = `${path}[${index}]`;
|
|
638
|
+
if (!isRecord(entry)) {
|
|
639
|
+
addIssue(issues, `${entryPath} must be an object`);
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
exactKeys(entry, ["name", "value"], entryPath, issues);
|
|
643
|
+
const name = controlFreeString(entry.name, `${entryPath}.name`, issues, 1, 128);
|
|
644
|
+
if (name === null) continue;
|
|
645
|
+
if (!/^[a-z0-9!#$%&'*+.^_`|~-]+$/u.test(name) || name !== name.toLowerCase()) {
|
|
646
|
+
addIssue(issues, `${entryPath}.name must be one canonical lower-case HTTP field name`);
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
if (isManagedHeaderName(name) && name !== "proxy-authorization") {
|
|
650
|
+
addIssue(issues, `${entryPath}.name is browser-managed and cannot be supplied by a template`);
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
if (name === "proxy-authorization") {
|
|
654
|
+
addIssue(issues, `${entryPath}.name cannot designate proxy credentials`);
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
if (names.has(name)) {
|
|
658
|
+
addIssue(issues, `${entryPath}.name duplicates ${name}`);
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
const headerValue = parseHeaderValue(entry.value, name, `${entryPath}.value`, issues);
|
|
662
|
+
if (headerValue === null) continue;
|
|
663
|
+
names.add(name);
|
|
664
|
+
parsed.push({ name, value: headerValue });
|
|
665
|
+
}
|
|
666
|
+
return parsed;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function parseBody(value: unknown, path: string, context: ParseContext): WebSessionRequestBody | null {
|
|
670
|
+
if (!isRecord(value)) {
|
|
671
|
+
addIssue(context.issues, `${path} must be a declarative request body`);
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
if (value.kind === "none") {
|
|
675
|
+
exactKeys(value, ["kind"], path, context.issues);
|
|
676
|
+
return { kind: "none" };
|
|
677
|
+
}
|
|
678
|
+
if (value.kind === "json") {
|
|
679
|
+
exactKeys(value, ["kind", "value"], path, context.issues);
|
|
680
|
+
const parsedValue = parseValueTemplate(value.value, `${path}.value`, context);
|
|
681
|
+
return parsedValue === null ? null : { kind: "json", value: parsedValue };
|
|
682
|
+
}
|
|
683
|
+
if (value.kind === "form") {
|
|
684
|
+
exactKeys(value, ["kind", "fields"], path, context.issues);
|
|
685
|
+
const fields = boundedArray(value.fields, `${path}.fields`, 100, context.issues);
|
|
686
|
+
if (fields === null) return null;
|
|
687
|
+
const parsed: { name: string; value: WebSessionValueTemplate }[] = [];
|
|
688
|
+
const names = new Set<string>();
|
|
689
|
+
for (const [index, field] of fields.entries()) {
|
|
690
|
+
const fieldPath = `${path}.fields[${index}]`;
|
|
691
|
+
if (!isRecord(field)) {
|
|
692
|
+
addIssue(context.issues, `${fieldPath} must be an object`);
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
exactKeys(field, ["name", "value"], fieldPath, context.issues);
|
|
696
|
+
const name = controlFreeString(field.name, `${fieldPath}.name`, context.issues, 1, 128);
|
|
697
|
+
const fieldValue = parseValueTemplate(field.value, `${fieldPath}.value`, context);
|
|
698
|
+
if (name === null || fieldValue === null) continue;
|
|
699
|
+
if (/[&=]/u.test(name) || isCredentialName(name)) {
|
|
700
|
+
addIssue(context.issues, `${fieldPath}.name must be a fixed non-credential form name`);
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
if (!isScalarTemplate(fieldValue)) {
|
|
704
|
+
addIssue(context.issues, `${fieldPath}.value must be one non-null scalar`);
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
if (names.has(name)) {
|
|
708
|
+
addIssue(context.issues, `${fieldPath}.name duplicates ${name}`);
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
names.add(name);
|
|
712
|
+
parsed.push({ name, value: fieldValue });
|
|
713
|
+
}
|
|
714
|
+
return { kind: "form", fields: parsed };
|
|
715
|
+
}
|
|
716
|
+
addIssue(context.issues, `${path}.kind must be none, json, or form`);
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function parseRequest(value: unknown, path: string, context: ParseContext): WebSessionRequestTemplate | null {
|
|
721
|
+
if (!isRecord(value)) {
|
|
722
|
+
addIssue(context.issues, `${path} must be an object`);
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
exactKeys(value, ["method", "path", "query", "headers", "body"], path, context.issues);
|
|
726
|
+
const method = value.method;
|
|
727
|
+
if (!isWebSessionMethod(method)) {
|
|
728
|
+
addIssue(context.issues, `${path}.method must be one fixed reviewed method: ${webSessionMethods.join(", ")}`);
|
|
729
|
+
}
|
|
730
|
+
const parsedPath = parsePath(value.path, `${path}.path`, context);
|
|
731
|
+
const query = parseQuery(value.query, `${path}.query`, context);
|
|
732
|
+
const headers = parseHeaders(value.headers, `${path}.headers`, context.issues);
|
|
733
|
+
const body = parseBody(value.body, `${path}.body`, context);
|
|
734
|
+
if ((method === "GET" || method === "HEAD") && body?.kind !== "none") {
|
|
735
|
+
addIssue(context.issues, `${path}.body must be none for ${method}`);
|
|
736
|
+
}
|
|
737
|
+
if (
|
|
738
|
+
!isWebSessionMethod(method)
|
|
739
|
+
|| parsedPath === null
|
|
740
|
+
|| query === null
|
|
741
|
+
|| headers === null
|
|
742
|
+
|| body === null
|
|
743
|
+
) return null;
|
|
744
|
+
return { method, path: parsedPath, query, headers, body };
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function parseJsonPath(value: unknown, path: string, issues: string[]): readonly WebSessionJsonPathSegment[] | null {
|
|
748
|
+
const segments = boundedArray(value, path, 16, issues);
|
|
749
|
+
if (segments === null || segments.length === 0) {
|
|
750
|
+
if (segments !== null) addIssue(issues, `${path} must contain at least one fixed segment`);
|
|
751
|
+
return null;
|
|
752
|
+
}
|
|
753
|
+
const parsed: WebSessionJsonPathSegment[] = [];
|
|
754
|
+
for (const [index, segment] of segments.entries()) {
|
|
755
|
+
const segmentPath = `${path}[${index}]`;
|
|
756
|
+
if (!isRecord(segment)) {
|
|
757
|
+
addIssue(issues, `${segmentPath} must be a fixed key or index segment`);
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
if (segment.kind === "key") {
|
|
761
|
+
exactKeys(segment, ["kind", "key"], segmentPath, issues);
|
|
762
|
+
const key = controlFreeString(segment.key, `${segmentPath}.key`, issues, 1, 128);
|
|
763
|
+
if (key === null) continue;
|
|
764
|
+
if (dangerousObjectKeys.has(key) || isCredentialName(key)) {
|
|
765
|
+
addIssue(issues, `${segmentPath}.key cannot select a prototype or credential-bearing field`);
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
parsed.push({ kind: "key", key });
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
if (segment.kind === "index") {
|
|
772
|
+
exactKeys(segment, ["kind", "index"], segmentPath, issues);
|
|
773
|
+
const parsedIndex = safeInteger(segment.index, `${segmentPath}.index`, 0, 10_000, issues);
|
|
774
|
+
if (parsedIndex !== null) parsed.push({ kind: "index", index: parsedIndex });
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
addIssue(issues, `${segmentPath}.kind must be key or index`);
|
|
778
|
+
}
|
|
779
|
+
return parsed.length === segments.length ? parsed : null;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function parseProjections(value: unknown, path: string, issues: string[]): readonly WebSessionProjection[] | null {
|
|
783
|
+
const projections = boundedArray(value, path, 64, issues);
|
|
784
|
+
if (projections === null) return null;
|
|
785
|
+
const parsed: WebSessionProjection[] = [];
|
|
786
|
+
const names = new Set<string>();
|
|
787
|
+
for (const [index, projection] of projections.entries()) {
|
|
788
|
+
const projectionPath = `${path}[${index}]`;
|
|
789
|
+
if (!isRecord(projection)) {
|
|
790
|
+
addIssue(issues, `${projectionPath} must be an object`);
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
exactKeys(projection, ["name", "path", "valueType", "required"], projectionPath, issues);
|
|
794
|
+
const name = controlFreeString(projection.name, `${projectionPath}.name`, issues, 1, 64);
|
|
795
|
+
const jsonPath = parseJsonPath(projection.path, `${projectionPath}.path`, issues);
|
|
796
|
+
const valueType = projection.valueType;
|
|
797
|
+
if (!isProjectionValueType(valueType)) {
|
|
798
|
+
addIssue(issues, `${projectionPath}.valueType must be ${webSessionProjectionValueTypes.join(", ")}`);
|
|
799
|
+
}
|
|
800
|
+
if (typeof projection.required !== "boolean") addIssue(issues, `${projectionPath}.required must be boolean`);
|
|
801
|
+
if (name === null || jsonPath === null || typeof projection.required !== "boolean" || !isProjectionValueType(valueType)) continue;
|
|
802
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/u.test(name) || isCredentialName(name)) {
|
|
803
|
+
addIssue(issues, `${projectionPath}.name must be a safe non-credential output name`);
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
if (names.has(name)) {
|
|
807
|
+
addIssue(issues, `${projectionPath}.name duplicates ${name}`);
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
names.add(name);
|
|
811
|
+
parsed.push({ name, path: jsonPath, valueType, required: projection.required });
|
|
812
|
+
}
|
|
813
|
+
return parsed;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function parseBindings(value: unknown, path: string, context: ParseContext): readonly WebSessionResponseBinding[] | null {
|
|
817
|
+
const bindings = boundedArray(value, path, 32, context.issues);
|
|
818
|
+
if (bindings === null) return null;
|
|
819
|
+
const parsed: WebSessionResponseBinding[] = [];
|
|
820
|
+
const paths = new Set<string>();
|
|
821
|
+
for (const [index, binding] of bindings.entries()) {
|
|
822
|
+
const bindingPath = `${path}[${index}]`;
|
|
823
|
+
if (!isRecord(binding)) {
|
|
824
|
+
addIssue(context.issues, `${bindingPath} must be an object`);
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
exactKeys(binding, ["path", "expected"], bindingPath, context.issues);
|
|
828
|
+
const jsonPath = parseJsonPath(binding.path, `${bindingPath}.path`, context.issues);
|
|
829
|
+
const expected = parseValueTemplate(binding.expected, `${bindingPath}.expected`, context);
|
|
830
|
+
if (jsonPath === null || expected === null) continue;
|
|
831
|
+
if (!isScalarTemplate(expected)) {
|
|
832
|
+
addIssue(context.issues, `${bindingPath}.expected must be one non-null scalar literal or scalar input`);
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
const signature = JSON.stringify(jsonPath);
|
|
836
|
+
if (paths.has(signature)) {
|
|
837
|
+
addIssue(context.issues, `${bindingPath}.path duplicates another binding path`);
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
paths.add(signature);
|
|
841
|
+
parsed.push({ path: jsonPath, expected });
|
|
842
|
+
}
|
|
843
|
+
return parsed;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function isJsonContentType(value: string): boolean {
|
|
847
|
+
const subtype = value.slice(value.indexOf("/") + 1);
|
|
848
|
+
return subtype === "json" || subtype.endsWith("+json");
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function parseContentType(value: unknown, path: string, issues: string[]): string | null | undefined {
|
|
852
|
+
if (value === null) return null;
|
|
853
|
+
const contentType = controlFreeString(value, path, issues, 3, 128);
|
|
854
|
+
if (
|
|
855
|
+
contentType === null
|
|
856
|
+
|| contentType !== contentType.toLowerCase()
|
|
857
|
+
|| !/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+*-]+$/u.test(contentType)
|
|
858
|
+
) {
|
|
859
|
+
if (contentType !== null) addIssue(issues, `${path} must be null or one lower-case media-type essence without parameters`);
|
|
860
|
+
return undefined;
|
|
861
|
+
}
|
|
862
|
+
return contentType;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function parseResponseBody(
|
|
866
|
+
value: unknown,
|
|
867
|
+
contentType: string | null,
|
|
868
|
+
path: string,
|
|
869
|
+
context: ParseContext,
|
|
870
|
+
): WebSessionResponseBody | null {
|
|
871
|
+
if (!isRecord(value)) {
|
|
872
|
+
addIssue(context.issues, `${path} must be an object`);
|
|
873
|
+
return null;
|
|
874
|
+
}
|
|
875
|
+
if (value.kind === "empty") {
|
|
876
|
+
exactKeys(value, ["kind"], path, context.issues);
|
|
877
|
+
if (contentType !== null) {
|
|
878
|
+
addIssue(context.issues, `${path}.kind empty requires a null contentType`);
|
|
879
|
+
return null;
|
|
880
|
+
}
|
|
881
|
+
return { kind: "empty" };
|
|
882
|
+
}
|
|
883
|
+
if (value.kind === "discard") {
|
|
884
|
+
exactKeys(value, ["kind"], path, context.issues);
|
|
885
|
+
if (contentType === null) {
|
|
886
|
+
addIssue(context.issues, `${path}.kind discard requires an exact contentType`);
|
|
887
|
+
return null;
|
|
888
|
+
}
|
|
889
|
+
return { kind: "discard" };
|
|
890
|
+
}
|
|
891
|
+
if (value.kind === "json") {
|
|
892
|
+
exactKeys(value, ["kind", "projections", "bindings"], path, context.issues);
|
|
893
|
+
if (contentType === null || !isJsonContentType(contentType)) {
|
|
894
|
+
addIssue(context.issues, `${path}.kind json requires an exact JSON contentType`);
|
|
895
|
+
return null;
|
|
896
|
+
}
|
|
897
|
+
const projections = parseProjections(value.projections, `${path}.projections`, context.issues);
|
|
898
|
+
const bindings = parseBindings(value.bindings, `${path}.bindings`, context);
|
|
899
|
+
return projections === null || bindings === null ? null : { kind: "json", projections, bindings };
|
|
900
|
+
}
|
|
901
|
+
addIssue(context.issues, `${path}.kind must be empty, discard, or json`);
|
|
902
|
+
return null;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function parseResponse(value: unknown, path: string, context: ParseContext): WebSessionResponseTemplate | null {
|
|
906
|
+
if (!isRecord(value)) {
|
|
907
|
+
addIssue(context.issues, `${path} must be an object`);
|
|
908
|
+
return null;
|
|
909
|
+
}
|
|
910
|
+
exactKeys(value, ["maxBytes", "variants"], path, context.issues);
|
|
911
|
+
const maxBytes = safeInteger(value.maxBytes, `${path}.maxBytes`, 1, MAX_RESPONSE_BYTES, context.issues);
|
|
912
|
+
const variants = boundedArray(value.variants, `${path}.variants`, 16, context.issues);
|
|
913
|
+
if (variants !== null && variants.length === 0) addIssue(context.issues, `${path}.variants must contain at least one exact response variant`);
|
|
914
|
+
if (maxBytes === null || variants === null || variants.length === 0) return null;
|
|
915
|
+
const parsed: WebSessionResponseVariant[] = [];
|
|
916
|
+
const signatures = new Set<string>();
|
|
917
|
+
for (const [index, variant] of variants.entries()) {
|
|
918
|
+
const variantPath = `${path}.variants[${index}]`;
|
|
919
|
+
if (!isRecord(variant)) {
|
|
920
|
+
addIssue(context.issues, `${variantPath} must be an object`);
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
exactKeys(variant, ["status", "contentType", "body"], variantPath, context.issues);
|
|
924
|
+
const status = safeInteger(variant.status, `${variantPath}.status`, 200, 299, context.issues);
|
|
925
|
+
const contentType = parseContentType(variant.contentType, `${variantPath}.contentType`, context.issues);
|
|
926
|
+
if (status === null || contentType === undefined) continue;
|
|
927
|
+
const body = parseResponseBody(variant.body, contentType, `${variantPath}.body`, context);
|
|
928
|
+
if (body === null) continue;
|
|
929
|
+
const signature = `${status}\u0000${contentType ?? "<missing>"}`;
|
|
930
|
+
if (signatures.has(signature)) {
|
|
931
|
+
addIssue(context.issues, `${variantPath} duplicates an exact status/contentType pair`);
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
934
|
+
signatures.add(signature);
|
|
935
|
+
parsed.push({ status, contentType, body });
|
|
936
|
+
}
|
|
937
|
+
return { maxBytes, variants: parsed };
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* Parse an untrusted reviewed exchange template into a closed request/response AST.
|
|
942
|
+
*
|
|
943
|
+
* Browser cookies are sent by the later same-origin executor, never represented in
|
|
944
|
+
* this AST. CSRF and authorization material can be read only from the dedicated
|
|
945
|
+
* browser sources above and can terminate only in their corresponding headers.
|
|
946
|
+
*/
|
|
947
|
+
export function parseWebSessionTemplate(
|
|
948
|
+
value: unknown,
|
|
949
|
+
options: ParseWebSessionTemplateOptions,
|
|
950
|
+
): ParseResult<WebSessionTemplate> {
|
|
951
|
+
const issues: string[] = [];
|
|
952
|
+
if (options.allowedOrigins.length < 1 || options.allowedOrigins.length > 32) {
|
|
953
|
+
addIssue(issues, "$policy.allowedOrigins must contain 1-32 exact reviewed origins");
|
|
954
|
+
}
|
|
955
|
+
const allowedOrigins = new Set<string>();
|
|
956
|
+
for (const [index, candidate] of options.allowedOrigins.entries()) {
|
|
957
|
+
const origin = canonicalHttpsOrigin(candidate, `$policy.allowedOrigins[${index}]`, issues);
|
|
958
|
+
if (origin !== null) allowedOrigins.add(origin);
|
|
959
|
+
}
|
|
960
|
+
if (!isRecord(value)) return { ok: false, issues: ["$ must be an object"] };
|
|
961
|
+
exactKeys(value, ["schemaVersion", "origin", "request", "response"], "$", issues);
|
|
962
|
+
if (value.schemaVersion !== WEB_SESSION_TEMPLATE_SCHEMA_VERSION) {
|
|
963
|
+
addIssue(issues, `$.schemaVersion must be ${WEB_SESSION_TEMPLATE_SCHEMA_VERSION}`);
|
|
964
|
+
}
|
|
965
|
+
const origin = canonicalHttpsOrigin(value.origin, "$.origin", issues);
|
|
966
|
+
if (origin !== null && !allowedOrigins.has(origin)) addIssue(issues, "$.origin is not one of the exact reviewed origins");
|
|
967
|
+
const context: ParseContext = {
|
|
968
|
+
input: options.input,
|
|
969
|
+
requiredInputs: new Set(options.input.required),
|
|
970
|
+
issues,
|
|
971
|
+
valueNodes: 0,
|
|
972
|
+
};
|
|
973
|
+
const request = parseRequest(value.request, "$.request", context);
|
|
974
|
+
const response = parseResponse(value.response, "$.response", context);
|
|
975
|
+
if (request?.method === "HEAD" && response !== null && response.variants.some((variant) => variant.body.kind === "json")) {
|
|
976
|
+
addIssue(issues, "$.response cannot project a JSON body for a HEAD request");
|
|
977
|
+
}
|
|
978
|
+
if (
|
|
979
|
+
issues.length > 0
|
|
980
|
+
|| value.schemaVersion !== WEB_SESSION_TEMPLATE_SCHEMA_VERSION
|
|
981
|
+
|| origin === null
|
|
982
|
+
|| request === null
|
|
983
|
+
|| response === null
|
|
984
|
+
) return { ok: false, issues };
|
|
985
|
+
return {
|
|
986
|
+
ok: true,
|
|
987
|
+
value: {
|
|
988
|
+
schemaVersion: WEB_SESSION_TEMPLATE_SCHEMA_VERSION,
|
|
989
|
+
origin,
|
|
990
|
+
request,
|
|
991
|
+
response,
|
|
992
|
+
},
|
|
993
|
+
};
|
|
994
|
+
}
|