@danceiny/gotry 0.0.1-rc.9 → 0.2.0-rc.27
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/README.md +172 -219
- package/README.zh-CN.md +222 -0
- package/bin/gotry-backend.js +53 -0
- package/bin/gotry-booking-copilot.js +53 -0
- package/bin/gotry-bootstrap.js +1744 -0
- package/bin/gotry-inner.js +733 -83
- package/bin/gotry-process-liveness.d.ts +27 -0
- package/bin/gotry-process-liveness.js +97 -0
- package/bin/gotry-runtime-resolution.d.ts +27 -0
- package/bin/gotry-runtime-resolution.js +49 -0
- package/bin/gotry.js +1 -1
- package/client/client.js +203 -0
- package/cordis.gotry-patch.yml +80 -56
- package/data/airline-airports.json +39 -0
- package/data/stations-12306-verify.json +524 -0
- package/dist/capabilities/agent-reach-deep.js +45 -122
- package/dist/capabilities/agent-reach.js +101 -146
- package/dist/capabilities/anything.js +135 -94
- package/dist/capabilities/artifacts.js +370 -0
- package/dist/capabilities/channel-health.js +131 -0
- package/dist/capabilities/channel-registry.js +256 -0
- package/dist/capabilities/doctor.js +546 -0
- package/dist/capabilities/effect.js +394 -0
- package/dist/capabilities/external-event.js +149 -0
- package/dist/capabilities/fact-log.js +55 -0
- package/dist/capabilities/flyai-config.js +337 -0
- package/dist/capabilities/flyai.js +947 -110
- package/dist/capabilities/fx-contract.js +293 -0
- package/dist/capabilities/geo-atlas.js +246 -0
- package/dist/capabilities/ground-transfer.js +509 -0
- package/dist/capabilities/hbcli.js +260 -123
- package/dist/capabilities/hotelbyte-transaction.js +194 -0
- package/dist/capabilities/incident-log.js +131 -61
- package/dist/capabilities/itinerary-artifact.js +190 -0
- package/dist/capabilities/itinerary-deck-artifact.js +194 -0
- package/dist/capabilities/itinerary-deck-export.js +339 -0
- package/dist/capabilities/lavish-local.js +924 -0
- package/dist/capabilities/model-override.js +38 -0
- package/dist/capabilities/opensky.js +54 -43
- package/dist/capabilities/resilience.js +191 -0
- package/dist/capabilities/session/action-cache.js +50 -44
- package/dist/capabilities/session/adapters/ctrip-flight.js +120 -62
- package/dist/capabilities/session/adapters/ctrip-hotel.js +222 -0
- package/dist/capabilities/session/adapters/dida-portal.js +274 -0
- package/dist/capabilities/session/adapters/meituan-local.js +35 -54
- package/dist/capabilities/session/adapters/rail-12306.js +342 -0
- package/dist/capabilities/session/benchmark.js +189 -0
- package/dist/capabilities/session/bridge-ledger.js +224 -0
- package/dist/capabilities/session/extension-bridge.js +636 -0
- package/dist/capabilities/session/extension-channel.js +109 -0
- package/dist/capabilities/session/extension-distribution.js +230 -0
- package/dist/capabilities/session/extract.js +31 -21
- package/dist/capabilities/session/golden-score.js +114 -0
- package/dist/capabilities/session/health-watch.js +215 -0
- package/dist/capabilities/session/read-guard.js +75 -24
- package/dist/capabilities/session/static-flight-golden.js +150 -0
- package/dist/capabilities/session/transport.js +97 -83
- package/dist/capabilities/session/wizard.js +117 -0
- package/dist/capabilities/session-consent.js +178 -0
- package/dist/capabilities/session-login.js +170 -0
- package/dist/capabilities/session-search.js +635 -44
- package/dist/capabilities/spawn-bounded.js +203 -0
- package/dist/capabilities/sponsor-plugin.js +341 -0
- package/dist/capabilities/visa-policy.js +140 -0
- package/dist/capabilities/weather.js +243 -138
- package/dist/data/session-golden-20.json +25 -0
- package/dist/data/sf-golden-manifest.json +103 -0
- package/dist/data/sf-static-routes.json +91 -0
- package/dist/scripts/action-cache-tests.js +24 -59
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +242 -0
- package/dist/scripts/agent-planning-turn-deadline-tests.js +185 -0
- package/dist/scripts/agent-reach-deep-tests.js +30 -40
- package/dist/scripts/agent-reach-error-tests.js +354 -0
- package/dist/scripts/agent-reach-tests.js +26 -18
- package/dist/scripts/agent-reach-wrapper-tests.js +50 -50
- package/dist/scripts/anything-tests.js +116 -60
- package/dist/scripts/artifact-client-contract-tests.js +220 -0
- package/dist/scripts/artifacts-capability-tests.js +727 -0
- package/dist/scripts/async-collect.js +50 -11
- package/dist/scripts/benchmark-environment-bridge-e2e.js +741 -0
- package/dist/scripts/benchmark-environment-bridge-tests.js +1452 -0
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +126 -0
- package/dist/scripts/booking-copilot-availability-policy-tests.js +265 -0
- package/dist/scripts/booking-copilot-bin-proof-tests.js +66 -0
- package/dist/scripts/booking-copilot-cleanup-proof-helper.js +76 -0
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +144 -0
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +424 -0
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +2839 -0
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +309 -0
- package/dist/scripts/booking-copilot-dsh-readiness-proof.js +238 -0
- package/dist/scripts/booking-copilot-dsh-warmer-proof.js +123 -0
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +156 -0
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +70 -0
- package/dist/scripts/booking-copilot-intent-proof-tests.js +422 -0
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +184 -0
- package/dist/scripts/booking-copilot-planner-live-probe.js +95 -0
- package/dist/scripts/booking-copilot-proof-runtime.js +42 -0
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +210 -0
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +2349 -0
- package/dist/scripts/booking-copilot-server-proof-tests.js +784 -0
- package/dist/scripts/booking-copilot-startup-proof-tests.js +94 -0
- package/dist/scripts/booking-executor-tests.js +80 -0
- package/dist/scripts/booking-recovery-chain-tests.js +182 -0
- package/dist/scripts/booking-saga-tests.js +125 -0
- package/dist/scripts/booking-surface-contract-proof-tests.js +212 -0
- package/dist/scripts/bootstrap-tests.js +1479 -0
- package/dist/scripts/bridge-events-tests.js +239 -0
- package/dist/scripts/bridge-ledger-tests.js +428 -0
- package/dist/scripts/build-changelog.js +238 -0
- package/dist/scripts/build-metrics-report.js +331 -0
- package/dist/scripts/changelog-tests.js +118 -0
- package/dist/scripts/channel-probe-tests.js +103 -0
- package/dist/scripts/channel-probe.js +169 -0
- package/dist/scripts/channel-registry-tests.js +208 -0
- package/dist/scripts/companion-tests.js +25 -70
- package/dist/scripts/dida-cookie-probe.js +87 -0
- package/dist/scripts/dida-runner-stop-tests.js +311 -0
- package/dist/scripts/dida-sw-probe.js +114 -0
- package/dist/scripts/diff-test.js +17 -12
- package/dist/scripts/doctor-tests.js +564 -0
- package/dist/scripts/dsh-artifact-e2e.js +279 -0
- package/dist/scripts/dsh-artifact-web-e2e.js +675 -0
- package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
- package/dist/scripts/dsh-runtime-closure-tests.js +182 -0
- package/dist/scripts/dsh-runtime-closure.js +162 -0
- package/dist/scripts/dsh-session-v3-migration-proof.js +109 -0
- package/dist/scripts/dsh-subprocess-local-proof.js +150 -0
- package/dist/scripts/dsh-target-closure-proof.js +178 -0
- package/dist/scripts/effect-tests.js +539 -0
- package/dist/scripts/engine-run.js +1 -3
- package/dist/scripts/engine-tests.js +16 -10
- package/dist/scripts/evaluation-cadence-tests.js +174 -0
- package/dist/scripts/evaluation-contract-tests.js +268 -0
- package/dist/scripts/extension-distribution-cli.js +39 -0
- package/dist/scripts/extension-distribution-tests.js +443 -0
- package/dist/scripts/extension-tests.js +988 -0
- package/dist/scripts/external-event-tests.js +350 -0
- package/dist/scripts/fact-gate-tests.js +869 -0
- package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
- package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
- package/dist/scripts/flight-malformed-tests.js +159 -0
- package/dist/scripts/flyai-setup-tests.js +228 -0
- package/dist/scripts/flyai-setup-tool-tests.js +54 -0
- package/dist/scripts/flyai-tests.js +692 -0
- package/dist/scripts/flyai-tool-registration-contract-tests.js +91 -0
- package/dist/scripts/fx-contract-tests.js +201 -0
- package/dist/scripts/g5-guard-tests.js +131 -0
- package/dist/scripts/g5-guard.js +189 -0
- package/dist/scripts/gen-lunar-anchors.js +106 -0
- package/dist/scripts/geo-atlas-tests.js +283 -0
- package/dist/scripts/gotry-backend-kernel-tests.js +206 -0
- package/dist/scripts/gotry-try-demo-tests.js +79 -0
- package/dist/scripts/gotry-try-demo.js +49 -0
- package/dist/scripts/ground-transfer-tests.js +956 -0
- package/dist/scripts/hbcli-e2e-tests.js +159 -0
- package/dist/scripts/hbcli-live-optin-tests.js +96 -0
- package/dist/scripts/hbcli-release-contract-tests.js +122 -0
- package/dist/scripts/hbcli-tests.js +121 -69
- package/dist/scripts/health-watch-cli.js +73 -0
- package/dist/scripts/hotel-date-gate-tests.js +496 -0
- package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
- package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
- package/dist/scripts/i18n-tests.js +41 -0
- package/dist/scripts/incident-tests.js +281 -87
- package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
- package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
- package/dist/scripts/issue-271-liveness-tests.js +569 -0
- package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
- package/dist/scripts/issue-289-model-retry-tests.js +441 -0
- package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
- package/dist/scripts/issue-338-home-city-e2e.js +577 -0
- package/dist/scripts/issue343-real-entry-e2e.js +160 -0
- package/dist/scripts/issue436-persisted-routing-e2e.js +247 -0
- package/dist/scripts/itinerary-artifact-tests.js +368 -0
- package/dist/scripts/itinerary-deck-artifact-tests.js +416 -0
- package/dist/scripts/itinerary-deck-export-tests.js +552 -0
- package/dist/scripts/itinerary-deck-tests.js +435 -0
- package/dist/scripts/itinerary-html-tests.js +426 -0
- package/dist/scripts/journey-tests.js +22 -27
- package/dist/scripts/kernel-manifest-evidence.js +109 -0
- package/dist/scripts/kernel-manifest-gate.js +126 -0
- package/dist/scripts/kernel-manifest-tests.js +212 -0
- package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
- package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
- package/dist/scripts/kernel-manifest.js +369 -0
- package/dist/scripts/lavish-local-tests.js +1226 -0
- package/dist/scripts/lavish-product-tools-tests.js +537 -0
- package/dist/scripts/ledger-repair-apply-tests.js +316 -0
- package/dist/scripts/ledger-repair-plan-tests.js +375 -0
- package/dist/scripts/ledger-tests.js +382 -246
- package/dist/scripts/ledger-workflow-crash.js +10 -20
- package/dist/scripts/managed-dsh-cleanup-diagnostic-proof.js +184 -0
- package/dist/scripts/managed-dsh-close-failure-proof.js +172 -0
- package/dist/scripts/managed-dsh-run-port-proof.js +153 -0
- package/dist/scripts/managed-dsh-terminal-proof.js +252 -0
- package/dist/scripts/map-tools-vendor-package-proof.js +365 -0
- package/dist/scripts/memory-capture-tests.js +242 -61
- package/dist/scripts/memory-decay-tests.js +18 -52
- package/dist/scripts/memory-lifecycle-tests.js +657 -0
- package/dist/scripts/memory-lifecycle.js +196 -0
- package/dist/scripts/memory-metrics.js +24 -12
- package/dist/scripts/memory-value-report.js +631 -0
- package/dist/scripts/memory-value-tests.js +328 -0
- package/dist/scripts/metrics-report-tests.js +151 -0
- package/dist/scripts/model-override-e2e.js +137 -0
- package/dist/scripts/nightly-evidence-tests.js +161 -0
- package/dist/scripts/nightly-evidence.js +286 -0
- package/dist/scripts/nudge-digest.js +39 -28
- package/dist/scripts/observability-tests.js +381 -0
- package/dist/scripts/onboarding-tests.js +161 -0
- package/dist/scripts/opensky-check.js +18 -13
- package/dist/scripts/opensky-tests.js +28 -28
- package/dist/scripts/persona-surface-guard-tests.js +49 -0
- package/dist/scripts/pnpm-dsh-closure-proof.js +17 -0
- package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
- package/dist/scripts/price-drift-tests.js +340 -0
- package/dist/scripts/price-drift-watch.js +447 -0
- package/dist/scripts/probe-poi-tests.js +53 -45
- package/dist/scripts/product-metrics.js +483 -0
- package/dist/scripts/publish-preverify.js +110 -0
- package/dist/scripts/realtime-pricing-tests.js +76 -0
- package/dist/scripts/recall-tests.js +513 -0
- package/dist/scripts/replay-async.js +20 -18
- package/dist/scripts/replay-real.js +18 -18
- package/dist/scripts/replay.js +52 -82
- package/dist/scripts/session-attach-diagnose.js +20 -20
- package/dist/scripts/session-attach-poc.js +34 -37
- package/dist/scripts/session-benchmark.js +180 -0
- package/dist/scripts/session-dida-live-e2e.js +199 -0
- package/dist/scripts/session-extract-tests.js +14 -35
- package/dist/scripts/session-link-tests.js +478 -0
- package/dist/scripts/session-login.js +27 -46
- package/dist/scripts/session-tests.js +796 -96
- package/dist/scripts/session-zones-tests.js +358 -0
- package/dist/scripts/sf-live-benchmark.js +367 -0
- package/dist/scripts/sf-live-challenge-stop-tests.js +453 -0
- package/dist/scripts/sf-live-cli-tests.js +92 -0
- package/dist/scripts/sf-live-options.js +46 -0
- package/dist/scripts/sf-manifest-tests.js +30 -0
- package/dist/scripts/sf-manifest.js +33 -0
- package/dist/scripts/sf-soft-score-tests.js +67 -0
- package/dist/scripts/sf-summary-tests.js +324 -0
- package/dist/scripts/sf-summary.js +525 -0
- package/dist/scripts/share-tests.js +432 -0
- package/dist/scripts/skeleton-check.js +16 -14
- package/dist/scripts/skeleton-integration-test.js +28 -11
- package/dist/scripts/skills-contract-tests.js +51 -46
- package/dist/scripts/smoke-session-gate-tests.js +21 -0
- package/dist/scripts/smoke.js +690 -246
- package/dist/scripts/spawn-bounded-abort-tests.js +241 -0
- package/dist/scripts/sponsor-reuse-tests.js +262 -0
- package/dist/scripts/state-cli-tests.js +226 -101
- package/dist/scripts/state-cli.js +550 -199
- package/dist/scripts/state-repair-cli.js +66 -0
- package/dist/scripts/state-repair-tests.js +140 -0
- package/dist/scripts/static-golden-tests.js +165 -0
- package/dist/scripts/time-eval-tests.js +210 -192
- package/dist/scripts/travel-timeline-tests.js +29 -93
- package/dist/scripts/turn-handoff-collect-tests.js +169 -0
- package/dist/scripts/turn-handoff-collect.js +161 -0
- package/dist/scripts/turn-policy-tests.js +55 -0
- package/dist/scripts/typed-contract-canary.js +176 -0
- package/dist/scripts/tz-resolver-sanity.js +50 -0
- package/dist/scripts/tz-resolver-tests.js +584 -0
- package/dist/scripts/unified-tests.js +23 -19
- package/dist/scripts/visa-policy-tests.js +62 -0
- package/dist/scripts/weather-tests.js +276 -41
- package/dist/scripts/wish-channel-gate-tests.js +54 -0
- package/dist/scripts/write-gate-tests.js +522 -0
- package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
- package/dist/scripts/z3-lifecycle-tests.js +170 -0
- package/dist/scripts/z3-race-repeat-tests.js +39 -0
- package/dist/scripts/z3-race-tests.js +41 -0
- package/dist/src/artifact-gate.js +829 -0
- package/dist/src/backend/kernel.js +91 -0
- package/dist/src/backend/modules/booking-copilot.js +92 -0
- package/dist/src/backend/modules/booking-executor.js +124 -0
- package/dist/src/backend/modules/session-search.js +551 -0
- package/dist/src/benchmark-agent-conformance.js +578 -0
- package/dist/src/benchmark-environment-bridge.js +515 -0
- package/dist/src/benchmark-headless-child-diagnostics.js +166 -0
- package/dist/src/benchmark-tool-isolation.js +151 -0
- package/dist/src/bookable-facts.js +533 -0
- package/dist/src/booking-saga.js +97 -0
- package/dist/src/booking-surface/availability-policy.js +697 -0
- package/dist/src/booking-surface/booking-intent.js +43 -0
- package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
- package/dist/src/booking-surface/canonical-schema.js +116 -0
- package/dist/src/booking-surface/contracts.js +75 -0
- package/dist/src/booking-surface/dsh-planner.js +1854 -0
- package/dist/src/booking-surface/dsh-plugin.js +378 -0
- package/dist/src/booking-surface/dsh-worker.js +98 -0
- package/dist/src/booking-surface/error-codes.js +231 -0
- package/dist/src/booking-surface/index.js +9 -0
- package/dist/src/booking-surface/managed-dsh-cleanup-diagnostic.js +97 -0
- package/dist/src/booking-surface/managed-dsh-run-port.js +244 -0
- package/dist/src/booking-surface/profile.js +24 -0
- package/dist/src/booking-surface/recovery-chain.js +367 -0
- package/dist/src/booking-surface/runtime.js +1968 -0
- package/dist/src/booking-surface/server.js +674 -0
- package/dist/src/booking-surface/startup.js +168 -0
- package/dist/src/booking-surface/validation.js +261 -0
- package/dist/src/bridge.js +20 -13
- package/dist/src/companions.js +48 -74
- package/dist/src/contracts.js +21 -43
- package/dist/src/dsh-llm.js +97 -111
- package/dist/src/engine.js +276 -193
- package/dist/src/evaluation-cadence.js +97 -0
- package/dist/src/evaluation-contracts.js +283 -0
- package/dist/src/flight-pack-adapter.js +54 -0
- package/dist/src/flight-pack-contract.js +2 -0
- package/dist/src/flyai-setup-tool.js +70 -0
- package/dist/src/gotry-backend.js +125 -0
- package/dist/src/hotel-date-gate.js +118 -0
- package/dist/src/i18n.js +88 -0
- package/dist/src/index.js +2227 -882
- package/dist/src/itinerary-deck.js +139 -0
- package/dist/src/itinerary-doc-shared.js +661 -0
- package/dist/src/itinerary-html.js +131 -0
- package/dist/src/journey.js +155 -108
- package/dist/src/lavish-tools.js +400 -0
- package/dist/src/ledger-repair-apply.js +538 -0
- package/dist/src/ledger-repair-plan.js +463 -0
- package/dist/src/loop.js +319 -160
- package/dist/src/memory-capture.js +180 -32
- package/dist/src/memory-decay.js +30 -14
- package/dist/src/memory-lifecycle.js +1310 -0
- package/dist/src/memory-utility.js +27 -40
- package/dist/src/mock-llm.js +36 -61
- package/dist/src/model.js +81 -36
- package/dist/src/realtime-pricing.js +103 -0
- package/dist/src/recall/card.js +74 -0
- package/dist/src/recall/evaluator.js +140 -0
- package/dist/src/recall/integration.js +17 -0
- package/dist/src/recall/tick.js +166 -0
- package/dist/src/session-link/plan-it-action.js +48 -0
- package/dist/src/session-link/session-link.js +248 -0
- package/dist/src/session-zones.js +507 -0
- package/dist/src/share/adapters.js +51 -0
- package/dist/src/share/share-consent.js +76 -0
- package/dist/src/share/share-deck.js +58 -0
- package/dist/src/share/share-token.js +146 -0
- package/dist/src/slot-spec.js +84 -106
- package/dist/src/state-ledger.js +435 -516
- package/dist/src/state-repair.js +299 -0
- package/dist/src/subagent-job-id-guard.js +58 -0
- package/dist/src/time-anchor.js +231 -33
- package/dist/src/tool-packet.js +20 -8
- package/dist/src/travel-slots.js +58 -35
- package/dist/src/travel-timeline.js +44 -63
- package/dist/src/turn-deadline.js +287 -0
- package/dist/src/turn-policy.js +118 -0
- package/dist/src/tz-resolver.js +143 -0
- package/dist/src/unified.js +535 -359
- package/dist/src/wish-pool.js +42 -12
- package/dist/src/write-gate.js +495 -0
- package/dist/src/z3-shared.js +284 -0
- package/extension/README.md +21 -0
- package/extension/README.zh-CN.md +21 -0
- package/extension/background.js +463 -0
- package/extension/content-bridge.js +270 -0
- package/extension/content-main.js +111 -0
- package/extension/manifest.json +60 -0
- package/package.json +407 -14
- package/schemas/booking.intent.schema.json +193 -0
- package/schemas/booking.surface.schema.json +2622 -0
- package/ts/capabilities/agent-reach-deep.ts +114 -0
- package/ts/capabilities/agent-reach.ts +198 -0
- package/ts/capabilities/anything.ts +255 -0
- package/ts/capabilities/artifacts.ts +428 -0
- package/ts/capabilities/fact-log.ts +54 -0
- package/ts/capabilities/flyai-config.ts +388 -0
- package/ts/capabilities/flyai.ts +1274 -0
- package/ts/capabilities/hbcli.ts +253 -71
- package/ts/capabilities/incident-log.ts +88 -59
- package/ts/capabilities/itinerary-artifact.ts +214 -0
- package/ts/capabilities/itinerary-deck-artifact.ts +218 -0
- package/ts/capabilities/itinerary-deck-export.ts +378 -0
- package/ts/capabilities/opensky.ts +121 -0
- package/ts/capabilities/session/action-cache.ts +148 -0
- package/ts/capabilities/session/adapters/ctrip-flight.ts +181 -0
- package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
- package/ts/capabilities/session/adapters/dida-portal.ts +338 -0
- package/ts/capabilities/session/adapters/meituan-local.ts +70 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +392 -0
- package/ts/capabilities/session/benchmark.ts +276 -0
- package/ts/capabilities/session/bridge-ledger.ts +333 -0
- package/ts/capabilities/session/extension-bridge.ts +794 -0
- package/ts/capabilities/session/extension-channel.ts +144 -0
- package/ts/capabilities/session/extension-distribution.ts +266 -0
- package/ts/capabilities/session/extract.ts +54 -0
- package/ts/capabilities/session/golden-score.ts +139 -0
- package/ts/capabilities/session/health-watch.ts +264 -0
- package/ts/capabilities/session/read-guard.ts +141 -0
- package/ts/capabilities/session/static-flight-golden.ts +209 -0
- package/ts/capabilities/session/transport.ts +146 -0
- package/ts/capabilities/session/wizard.ts +140 -0
- package/ts/capabilities/session-consent.ts +211 -0
- package/ts/capabilities/session-login.ts +208 -0
- package/ts/capabilities/session-search.ts +893 -0
- package/ts/capabilities/weather.ts +330 -0
- package/ts/dsh-runtime/vendor/README.md +68 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/LICENSE +21 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +405 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +404 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/client/client.js +1584 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/cordis.patch.yml +5 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/icon.svg +5 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/amap.js +595 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/nominatim.js +63 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/osrm.js +58 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/photon.js +55 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-file.js +87 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-route.js +131 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config.js +16 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/geo.js +232 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js +120 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +40 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/staticmap-route.js +115 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/geocode.js +127 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/poi.js +84 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/routes.js +267 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/amap.d.ts +90 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/nominatim.d.ts +17 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/osrm.d.ts +15 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/photon.d.ts +22 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-file.d.ts +33 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-route.d.ts +23 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config.d.ts +23 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/geo.d.ts +61 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/index.d.ts +18 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +14 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/staticmap-route.d.ts +31 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/geocode.d.ts +11 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/poi.d.ts +9 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/routes.d.ts +75 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/types.d.ts +68 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types.js +16 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +97 -0
- package/ts/package.json +293 -3
- package/ts/scripts/async-collect.ts +91 -0
- package/ts/scripts/map-tools-vendor-package-proof.ts +404 -0
- package/ts/scripts/skeleton-check.ts +1 -1
- package/ts/scripts/skeleton-integration-test.ts +23 -1
- package/ts/scripts/state-cli.ts +568 -0
- package/ts/scripts/turn-handoff-collect.ts +178 -0
- package/ts/src/artifact-gate.ts +991 -0
- package/ts/src/benchmark-agent-conformance.ts +619 -0
- package/ts/src/benchmark-environment-bridge.ts +538 -0
- package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
- package/ts/src/benchmark-tool-isolation.ts +166 -0
- package/ts/src/bookable-facts.ts +837 -0
- package/ts/src/booking-saga.ts +133 -0
- package/ts/src/booking-surface/availability-policy.ts +522 -0
- package/ts/src/booking-surface/booking-intent.ts +86 -0
- package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
- package/ts/src/booking-surface/canonical-schema.js +116 -0
- package/ts/src/booking-surface/contracts.ts +475 -0
- package/ts/src/booking-surface/dsh-planner.ts +1949 -0
- package/ts/src/booking-surface/dsh-plugin.js +378 -0
- package/ts/src/booking-surface/dsh-worker.ts +110 -0
- package/ts/src/booking-surface/error-codes.ts +241 -0
- package/ts/src/booking-surface/index.ts +9 -0
- package/ts/src/booking-surface/managed-dsh-cleanup-diagnostic.ts +151 -0
- package/ts/src/booking-surface/managed-dsh-run-port.ts +273 -0
- package/ts/src/booking-surface/profile.ts +42 -0
- package/ts/src/booking-surface/recovery-chain.ts +409 -0
- package/ts/src/booking-surface/runtime.ts +1793 -0
- package/ts/src/booking-surface/server.ts +733 -0
- package/ts/src/booking-surface/startup.ts +201 -0
- package/ts/src/booking-surface/validation.ts +217 -0
- package/ts/src/companions.ts +113 -0
- package/ts/src/contracts.ts +13 -3
- package/ts/src/dsh-llm.ts +44 -11
- package/ts/src/flyai-setup-tool.ts +77 -0
- package/ts/src/index.ts +1948 -198
- package/ts/src/itinerary-deck.ts +189 -0
- package/ts/src/itinerary-doc-shared.ts +741 -0
- package/ts/src/itinerary-html.ts +171 -0
- package/ts/src/loop.ts +223 -25
- package/ts/src/memory-capture.ts +283 -0
- package/ts/src/memory-decay.ts +55 -0
- package/ts/src/memory-utility.ts +70 -0
- package/ts/src/mock-llm.ts +4 -8
- package/ts/src/model.ts +88 -2
- package/ts/src/recall/card.ts +106 -0
- package/ts/src/recall/evaluator.ts +170 -0
- package/ts/src/recall/integration.ts +35 -0
- package/ts/src/recall/tick.ts +213 -0
- package/ts/src/session-link/plan-it-action.ts +78 -0
- package/ts/src/session-link/session-link.ts +275 -0
- package/ts/src/share/adapters.ts +93 -0
- package/ts/src/share/share-consent.ts +105 -0
- package/ts/src/share/share-deck.ts +85 -0
- package/ts/src/share/share-token.ts +166 -0
- package/ts/src/slot-spec.ts +172 -0
- package/ts/src/state-ledger.ts +912 -0
- package/ts/src/subagent-job-id-guard.ts +72 -0
- package/ts/src/time-anchor.ts +338 -0
- package/ts/src/tool-packet.ts +32 -0
- package/ts/src/travel-slots.ts +219 -0
- package/ts/src/travel-timeline.ts +83 -0
- package/ts/src/turn-deadline.ts +366 -0
- package/ts/src/turn-policy.ts +154 -0
- package/ts/src/unified.ts +354 -88
- package/ts/src/wish-pool.ts +79 -0
- package/dist/scripts/session-attach-wait.js +0 -41
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session link token 签发/校验(issue #580,Phase E;研究决策见
|
|
3
|
+
* docs/research/karpo-deck-web-research.md §3 Phase E——one-tap 深链回本地
|
|
4
|
+
* session,不碰写路径,WriteGate 保持 sealed;消费端注册是 M4 激活)。
|
|
5
|
+
*
|
|
6
|
+
* HMAC-SHA256 over base64url(payload-json),签名为 base64url。
|
|
7
|
+
* Token 形如 `${body}.${sig}`(body = base64url(JSON.stringify(payload)))。
|
|
8
|
+
*
|
|
9
|
+
* 与 share-token(Phase C)的差异(刻意,非偶然):
|
|
10
|
+
* - **schema 闭集到未知键拒绝**:verify 对 payload 里出现 schema 之外的键
|
|
11
|
+
* 直接 link_invalid——session link 是「de-identified by construction」面,
|
|
12
|
+
* 链接只携带引用(link_id/session_ref/wish_id)不携带事实/记忆,闭集
|
|
13
|
+
* schema 让这句承诺变成可测试字面真(share token 允许附加键,因为 share
|
|
14
|
+
* manifest 本身就需要开放扩展;两者威胁模型不同,不共享校验器);
|
|
15
|
+
* - **action 词位闭集 = WriteGate 链接层结构性封死**:`plan_it` | `open`,
|
|
16
|
+
* 词位里不存在 book/pay/confirm——任何写动词连构造都构造不出来
|
|
17
|
+
* (sign 层抛错,不只是 verify 层拒绝);
|
|
18
|
+
* - **plan_it ⇔ wish_id 成对**:`plan_it` 必须带非空 wish_id(防「规划
|
|
19
|
+
* 空 wish」退化),`open` 必须不带(防通用打开链接夹带定向意图);
|
|
20
|
+
* - **session_ref 路径护栏**:不透明引用,拒绝含 `/` `\` `..` NUL——
|
|
21
|
+
* 「不写共享状态」的链接层版:链接永远无法命名 state 路径;
|
|
22
|
+
* - 失败闭集只有 2 类(`link_invalid` | `link_expired`)——session link
|
|
23
|
+
* 无 channel/adapter/consent 维度,不需要 share 的 8 类;
|
|
24
|
+
* - token 不进 URL:scheme/宿主是消费端渲染期选择(`formatSessionLink`),
|
|
25
|
+
* 链接产物跨消费端可移植,签名载荷不含宿主细节。
|
|
26
|
+
*
|
|
27
|
+
* 硬化清单与 share-token 同源:恰一个点、timingSafeEqual(先长度)、
|
|
28
|
+
* ttl 非负整数上限 2^31-1(防 expiresMs 溢出 Infinity 永不过期)、
|
|
29
|
+
* 时间注入(测试不依赖 wall clock)、secret 缺省且 NODE_ENV=production
|
|
30
|
+
* 时 fail-closed(绝不静默用仓库里公开的 dev 常量签生产链接)——刻意抛错
|
|
31
|
+
* 保留在签发面(resolve/sign);verify 面把同样的配置错误收敛为 link_invalid,
|
|
32
|
+
* 不让异常越出「永不抛错」的校验边界。注入时钟本身失效(getTime 非有限值
|
|
33
|
+
* 或回调抛错)→ fail-closed link_invalid:NaN 与 expiresMs 比较恒 false,
|
|
34
|
+
* 曾把过期链接静默判成有效——无效时间上不产生任何接受。verify 期 secret
|
|
35
|
+
* 为非字符串运行期键(数字/对象/Symbol 等 JS 畸形输入)同样收敛 link_invalid,
|
|
36
|
+
* HMAC 计算本身也在守卫内——任何调用方/配置输入都无法越出「永不抛错」边界。
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { createHmac, timingSafeEqual } from 'node:crypto'
|
|
40
|
+
|
|
41
|
+
/** 行动闭集:`plan_it`(回 session 继续规划某条 wish)| `open`(通用打开)。
|
|
42
|
+
* **词位即安全边界**——新增行动先 review;book/pay/confirm 等写动词
|
|
43
|
+
* 永不入集(WriteGate sealed 在链接层的结构性表达)。 */
|
|
44
|
+
export const SESSION_LINK_ACTIONS = ['plan_it', 'open'] as const
|
|
45
|
+
export type SessionLinkAction = (typeof SESSION_LINK_ACTIONS)[number]
|
|
46
|
+
|
|
47
|
+
/** 失败闭集(2 类;session link 无 channel/adapter/consent 维度) */
|
|
48
|
+
export type SessionLinkFailure = 'link_invalid' | 'link_expired'
|
|
49
|
+
|
|
50
|
+
export type SessionLinkVerifyResult =
|
|
51
|
+
| { ok: true; payload: SessionLinkPayload }
|
|
52
|
+
| { ok: false; reason: SessionLinkFailure }
|
|
53
|
+
|
|
54
|
+
/** payload 闭集 schema(**恰好这 6 个键**;verify 对未知键拒绝——
|
|
55
|
+
* de-identified by construction 的可测试形式:链接只携带引用,不携带事实) */
|
|
56
|
+
export interface SessionLinkPayload {
|
|
57
|
+
/** 调用方给的幂等 id(非空字符串) */
|
|
58
|
+
link_id: string
|
|
59
|
+
/** 不透明 session 引用(非路径;护栏见 isSafeSessionRef) */
|
|
60
|
+
session_ref: string
|
|
61
|
+
/** 行动(闭集;写动词不在词位) */
|
|
62
|
+
action: SessionLinkAction
|
|
63
|
+
/** plan_it 时必填非空;open 时必须缺省(成对校验) */
|
|
64
|
+
wish_id?: string
|
|
65
|
+
/** 签发时刻(ISO;Date.parse 必须有限) */
|
|
66
|
+
created_at: string
|
|
67
|
+
/** 有效秒数:非负整数,上限 SESSION_LINK_TTL_MAX_SECONDS */
|
|
68
|
+
ttl_seconds: number
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 显式 dev 默认 secret——生产环境必须由 `SESSION_LINK_HMAC_SECRET` env 覆盖 */
|
|
72
|
+
export const DEFAULT_DEV_SESSION_LINK_SECRET = 'gotry-session-link-dev-secret-DO-NOT-USE-IN-PROD'
|
|
73
|
+
|
|
74
|
+
/** ttl 上界(秒):2^31-1,防 created_at + ttl*1000 溢出 Number 上限 */
|
|
75
|
+
export const SESSION_LINK_TTL_MAX_SECONDS = 2_147_483_647
|
|
76
|
+
|
|
77
|
+
/** payload schema 的键闭集(verify 用:未知键 → link_invalid) */
|
|
78
|
+
const PAYLOAD_KEYS = ['link_id', 'session_ref', 'action', 'wish_id', 'created_at', 'ttl_seconds'] as const
|
|
79
|
+
|
|
80
|
+
/** 获取运行期 secret:env 优先;production 缺 env 时 fail-closed */
|
|
81
|
+
export function resolveSessionLinkSecret(env: NodeJS.ProcessEnv = process.env): string {
|
|
82
|
+
const s = env.SESSION_LINK_HMAC_SECRET
|
|
83
|
+
if (typeof s === 'string' && s.length > 0) return s
|
|
84
|
+
if (env.NODE_ENV === 'production') {
|
|
85
|
+
throw new Error('SESSION_LINK_HMAC_SECRET 未设置且 NODE_ENV=production:拒绝回落公开的 dev 默认 secret(fail-closed;任何读到仓库的人都能伪造 session link)')
|
|
86
|
+
}
|
|
87
|
+
return DEFAULT_DEV_SESSION_LINK_SECRET
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** session_ref 路径护栏:不透明引用——含路径分隔/上跳/NUL 即拒(链接永远无法命名 state 路径) */
|
|
91
|
+
function isSafeSessionRef(value: unknown): value is string {
|
|
92
|
+
return typeof value === 'string' && value.length > 0 && value.length <= 128
|
|
93
|
+
&& !value.includes('/') && !value.includes('\\') && !value.includes('..')
|
|
94
|
+
&& !value.includes('\0')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 签发:payload 校验(闭集 action / plan_it⇔wish_id 成对 / session_ref 护栏 / ttl 上界)
|
|
98
|
+
* 不过期检查由 verify 做(签名时刻的时钟是消费端的)。非法 payload 抛错——
|
|
99
|
+
* 构造层把坏链接挡在签名之前,不是等到校验才发现。 */
|
|
100
|
+
export function signSessionLink(
|
|
101
|
+
payload: SessionLinkPayload,
|
|
102
|
+
secret: string = resolveSessionLinkSecret(),
|
|
103
|
+
): string {
|
|
104
|
+
if (typeof payload !== 'object' || payload === null) {
|
|
105
|
+
throw new Error(`signSessionLink:payload 必须是对象(实测 ${String(payload)})`)
|
|
106
|
+
}
|
|
107
|
+
if (typeof payload.link_id !== 'string' || payload.link_id.length === 0) {
|
|
108
|
+
throw new Error('signSessionLink:link_id 必须是非空字符串')
|
|
109
|
+
}
|
|
110
|
+
if (!isSafeSessionRef(payload.session_ref)) {
|
|
111
|
+
throw new Error(`signSessionLink:session_ref 必须是不含路径分隔符的不透明引用(实测 ${String(payload.session_ref)})`)
|
|
112
|
+
}
|
|
113
|
+
if (!SESSION_LINK_ACTIONS.includes(payload.action)) {
|
|
114
|
+
throw new Error(`signSessionLink:action 不在闭集(实测 ${String(payload.action)};允许:${SESSION_LINK_ACTIONS.join(' / ')})。写动词(book/pay/confirm)永不入集——WriteGate 在链接层结构性封死`)
|
|
115
|
+
}
|
|
116
|
+
// plan_it ⇔ wish_id 成对(双向:plan_it 必须有目标;open 必须不夹带定向意图)
|
|
117
|
+
if (payload.action === 'plan_it' && (typeof payload.wish_id !== 'string' || payload.wish_id.length === 0)) {
|
|
118
|
+
throw new Error('signSessionLink:action=plan_it 必须携带非空 wish_id(否则「规划」无对象,是退化链接)')
|
|
119
|
+
}
|
|
120
|
+
if (payload.action === 'open' && payload.wish_id !== undefined) {
|
|
121
|
+
throw new Error('signSessionLink:action=open 必须不携带 wish_id(通用打开链接夹带定向意图 = 混淆行动语义)')
|
|
122
|
+
}
|
|
123
|
+
if (typeof payload.created_at !== 'string' || payload.created_at.length === 0 || !Number.isFinite(Date.parse(payload.created_at))) {
|
|
124
|
+
throw new Error('signSessionLink:created_at 必须是可解析的 ISO 时间字符串')
|
|
125
|
+
}
|
|
126
|
+
if (typeof payload.ttl_seconds !== 'number' || !Number.isInteger(payload.ttl_seconds)
|
|
127
|
+
|| payload.ttl_seconds < 0 || payload.ttl_seconds > SESSION_LINK_TTL_MAX_SECONDS) {
|
|
128
|
+
throw new Error(`signSessionLink:ttl_seconds 必须为 0..${SESSION_LINK_TTL_MAX_SECONDS} 的整数(实测 ${String(payload.ttl_seconds)})`)
|
|
129
|
+
}
|
|
130
|
+
// 序列化前剥掉 undefined 键(wish_id 缺省时),保证 body 可复现
|
|
131
|
+
const clean: Record<string, unknown> = {}
|
|
132
|
+
const raw = payload as unknown as Record<string, unknown>
|
|
133
|
+
for (const k of PAYLOAD_KEYS) {
|
|
134
|
+
const v = raw[k]
|
|
135
|
+
if (v !== undefined) clean[k] = v
|
|
136
|
+
}
|
|
137
|
+
const body = Buffer.from(JSON.stringify(clean), 'utf-8').toString('base64url')
|
|
138
|
+
const sig = createHmac('sha256', secret).update(body).digest('base64url')
|
|
139
|
+
return `${body}.${sig}`
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** 校验:secret 解析(守卫体)→ 格式(恰一个点)→ HMAC 等值(timingSafeEqual)
|
|
143
|
+
* → schema 闭集(未知键拒绝)→ 字段形态 → plan_it⇔wish_id 成对 → session_ref
|
|
144
|
+
* 护栏 → 过期(注入时钟失效 = link_invalid)。**永不抛错**:production 缺 env
|
|
145
|
+
* secret 与失效时钟都是操作面错误,收敛进 link_invalid(消费端拿到 link_invalid
|
|
146
|
+
* 只能 fail-closed 不打开任何 session),刻意的配置/签发异常只保留在 resolve/sign 侧。 */
|
|
147
|
+
export function verifySessionLink(
|
|
148
|
+
token: string,
|
|
149
|
+
secret?: string,
|
|
150
|
+
now: () => Date = () => new Date(),
|
|
151
|
+
): SessionLinkVerifyResult {
|
|
152
|
+
// 运行期 secret 解析放进守卫体:production 缺 env 的配置错在验证面 fail-closed
|
|
153
|
+
// 为 link_invalid,不抛错(若静默回落公开 dev 常量,伪造链接将被放行);
|
|
154
|
+
// 非字符串运行期键(数字/对象/Symbol 等 JS 畸形调用方输入)同样收敛
|
|
155
|
+
// link_invalid——createHmac 对它们会抛 TypeError,绝不让异常越出本函数
|
|
156
|
+
let hmacSecret: string
|
|
157
|
+
try {
|
|
158
|
+
const resolved = secret === undefined ? resolveSessionLinkSecret() : secret
|
|
159
|
+
if (typeof resolved !== 'string') return { ok: false, reason: 'link_invalid' }
|
|
160
|
+
hmacSecret = resolved
|
|
161
|
+
} catch {
|
|
162
|
+
return { ok: false, reason: 'link_invalid' }
|
|
163
|
+
}
|
|
164
|
+
if (typeof token !== 'string' || token.length === 0) return { ok: false, reason: 'link_invalid' }
|
|
165
|
+
const dot = token.indexOf('.')
|
|
166
|
+
// 恰一个点:多点(base64url 解码器会静默吞非法字符)与无点都拒绝
|
|
167
|
+
if (dot <= 0 || dot === token.length - 1) return { ok: false, reason: 'link_invalid' }
|
|
168
|
+
if (token.indexOf('.', dot + 1) !== -1) return { ok: false, reason: 'link_invalid' }
|
|
169
|
+
const body = token.slice(0, dot)
|
|
170
|
+
const sig = token.slice(dot + 1)
|
|
171
|
+
|
|
172
|
+
// HMAC 等值比较(先长度,再 timingSafeEqual;Buffer-vs-Buffer);加密计算
|
|
173
|
+
// 本身也在守卫内——任何运行期键异常收敛 link_invalid,绝不外泄异常
|
|
174
|
+
let expected: Buffer
|
|
175
|
+
try {
|
|
176
|
+
expected = createHmac('sha256', hmacSecret).update(body).digest()
|
|
177
|
+
} catch {
|
|
178
|
+
return { ok: false, reason: 'link_invalid' }
|
|
179
|
+
}
|
|
180
|
+
let actual: Buffer
|
|
181
|
+
try {
|
|
182
|
+
actual = Buffer.from(sig, 'base64url')
|
|
183
|
+
} catch {
|
|
184
|
+
return { ok: false, reason: 'link_invalid' }
|
|
185
|
+
}
|
|
186
|
+
if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) {
|
|
187
|
+
return { ok: false, reason: 'link_invalid' }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 解析 payload
|
|
191
|
+
let payload: Record<string, unknown>
|
|
192
|
+
try {
|
|
193
|
+
const json = Buffer.from(body, 'base64url').toString('utf-8')
|
|
194
|
+
payload = JSON.parse(json)
|
|
195
|
+
} catch {
|
|
196
|
+
return { ok: false, reason: 'link_invalid' }
|
|
197
|
+
}
|
|
198
|
+
if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {
|
|
199
|
+
return { ok: false, reason: 'link_invalid' }
|
|
200
|
+
}
|
|
201
|
+
// schema 闭集:未知键拒绝(de-identified by construction 的可测试形式)
|
|
202
|
+
for (const key of Object.keys(payload)) {
|
|
203
|
+
if (!(PAYLOAD_KEYS as readonly string[]).includes(key)) return { ok: false, reason: 'link_invalid' }
|
|
204
|
+
}
|
|
205
|
+
// 字段形态
|
|
206
|
+
if (typeof payload.link_id !== 'string' || payload.link_id.length === 0
|
|
207
|
+
|| !isSafeSessionRef(payload.session_ref)
|
|
208
|
+
|| typeof payload.action !== 'string' || !SESSION_LINK_ACTIONS.includes(payload.action as SessionLinkAction)
|
|
209
|
+
|| typeof payload.created_at !== 'string' || payload.created_at.length === 0) {
|
|
210
|
+
return { ok: false, reason: 'link_invalid' }
|
|
211
|
+
}
|
|
212
|
+
// plan_it ⇔ wish_id 成对(与 sign 同规则,双向)
|
|
213
|
+
if (payload.action === 'plan_it' && (typeof payload.wish_id !== 'string' || payload.wish_id.length === 0)) {
|
|
214
|
+
return { ok: false, reason: 'link_invalid' }
|
|
215
|
+
}
|
|
216
|
+
if (payload.action === 'open' && payload.wish_id !== undefined) {
|
|
217
|
+
return { ok: false, reason: 'link_invalid' }
|
|
218
|
+
}
|
|
219
|
+
// ttl:非负整数且不溢出(防 created_at + ttl*1000 = Infinity 永不过期)
|
|
220
|
+
const ttl = payload.ttl_seconds
|
|
221
|
+
if (typeof ttl !== 'number' || !Number.isInteger(ttl) || ttl < 0 || ttl > SESSION_LINK_TTL_MAX_SECONDS) {
|
|
222
|
+
return { ok: false, reason: 'link_invalid' }
|
|
223
|
+
}
|
|
224
|
+
const createdMs = Date.parse(payload.created_at)
|
|
225
|
+
if (!Number.isFinite(createdMs)) return { ok: false, reason: 'link_invalid' }
|
|
226
|
+
const expiresMs = createdMs + ttl * 1000
|
|
227
|
+
if (!Number.isFinite(expiresMs)) return { ok: false, reason: 'link_invalid' }
|
|
228
|
+
// 注入时钟失效(抛错/非有限值)fail-closed 为 link_invalid——
|
|
229
|
+
// NaN 与 expiresMs 比较恒 false,失效时钟不得让任何链接通过
|
|
230
|
+
let nowMs: number
|
|
231
|
+
try {
|
|
232
|
+
nowMs = now().getTime()
|
|
233
|
+
} catch {
|
|
234
|
+
return { ok: false, reason: 'link_invalid' }
|
|
235
|
+
}
|
|
236
|
+
if (!Number.isFinite(nowMs)) return { ok: false, reason: 'link_invalid' }
|
|
237
|
+
if (nowMs > expiresMs) return { ok: false, reason: 'link_expired' }
|
|
238
|
+
|
|
239
|
+
return { ok: true, payload: payload as unknown as SessionLinkPayload }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** 默认深链 base(scheme handler 注册是 M4 激活;消费端可传自己的 base) */
|
|
243
|
+
export const DEFAULT_SESSION_LINK_BASE = 'gotry://session'
|
|
244
|
+
|
|
245
|
+
/** 渲染期拼 URL:`${base}/${token}`——token 不进 URL 的签名,宿主是消费端选择 */
|
|
246
|
+
export function formatSessionLink(token: string, base: string = DEFAULT_SESSION_LINK_BASE): string {
|
|
247
|
+
if (typeof token !== 'string' || token.length === 0) {
|
|
248
|
+
throw new Error('formatSessionLink:token 必须是非空字符串')
|
|
249
|
+
}
|
|
250
|
+
if (typeof base !== 'string' || base.length === 0 || base.endsWith('/')) {
|
|
251
|
+
throw new Error(`formatSessionLink:base 必须是非空字符串且不以 '/' 结尾(实测 ${String(base)})`)
|
|
252
|
+
}
|
|
253
|
+
if (base.includes('?') || base.includes('#')) {
|
|
254
|
+
throw new Error('formatSessionLink:base 不得含 query/hash(产出即 parseSessionLink 拒绝的形态,锋利边角)')
|
|
255
|
+
}
|
|
256
|
+
return `${base}/${token}`
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** 从 URL 严格反解 token:路径最后一段,且段本身须呈 `body.sig` 形态(恰
|
|
260
|
+
* 一个非空 body、非空 sig;段内多点由 verify 终裁——本函数只保证不把一个
|
|
261
|
+
* 裸 base(`gotry://session`)的最后一段误当 token)。**拒绝 query/hash**——
|
|
262
|
+
* 拼接歧义(如 `?x=../../etc`)一律 fail-closed;形态不符返回 null(消费端
|
|
263
|
+
* 不打开)。 */
|
|
264
|
+
export function parseSessionLink(url: string): string | null {
|
|
265
|
+
if (typeof url !== 'string' || url.length === 0) return null
|
|
266
|
+
const q = url.indexOf('?')
|
|
267
|
+
const h = url.indexOf('#')
|
|
268
|
+
if (q !== -1 || h !== -1) return null
|
|
269
|
+
const segments = url.split('/').filter(s => s.length > 0)
|
|
270
|
+
const token = segments[segments.length - 1]
|
|
271
|
+
if (segments.length < 2 || token === undefined || token.length === 0) return null
|
|
272
|
+
const dot = token.indexOf('.')
|
|
273
|
+
if (dot <= 0 || dot === token.length - 1) return null
|
|
274
|
+
return token
|
|
275
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Share outbound adapter 契约层(issue #573,Phase C 切片;研究决策见
|
|
3
|
+
* docs/research/karpo-deck-web-research.md 决策 §3 Phase C;自检 review 修复见 PR #574 评论)。
|
|
4
|
+
*
|
|
5
|
+
* 设计契约:**所有 outbound 外发必经同意卡 + 签名 token**,且必经这个接口。
|
|
6
|
+
* 4 个内置适配器(iMessage / SMS / Slack / webhook)只实现契约形状、**不接
|
|
7
|
+
* 真实 SDK、不发网络**——统一返回 `{ delivered: false, reason: 'not_activated' }`,
|
|
8
|
+
* 由 share-deck.ts 主流程在 M4 阶段才决定哪些 adapter 真正激活。
|
|
9
|
+
*
|
|
10
|
+
* 三条硬纪律(继承自单页入口 + deck 入口):
|
|
11
|
+
* 1. payload 是**显式结构**(token 绑定 target/body)——调用方不传 raw HTML,
|
|
12
|
+
* 只传路径(产物可被 list/read 单独审计);
|
|
13
|
+
* 2. token 校验(含 target 绑定)由 share-deck 在选 adapter 之前完成——未通过
|
|
14
|
+
* 校验的 adapter.send 永远不会被调用;
|
|
15
|
+
* 3. adapter.send 返回 `delivered:false` 或抛错都算合法完成(抛错被主流程
|
|
16
|
+
* 收敛为 `adapter_error`);调用方据此决定 UI 反馈与重试策略。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export type ShareChannel = 'imessage' | 'sms' | 'slack' | 'webhook'
|
|
20
|
+
|
|
21
|
+
export interface SharePayload {
|
|
22
|
+
/** 签名后的 share token(由 share-token.ts 签发,payload 绑定 target) */
|
|
23
|
+
token: string
|
|
24
|
+
/** 通道与地址(channel 与 adapter 表一对一;address 含义由各 adapter 解释) */
|
|
25
|
+
target: { channel: ShareChannel; address: string }
|
|
26
|
+
/** 产物引用——adapter 据此拿到 deck / manifest / qr 的实际路径 */
|
|
27
|
+
body: { html_path?: string; manifest_path?: string; qr_path?: string; note?: string }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 失败原因闭集——任何 adapter / share-deck 返回的拒绝都必须落进这个集合(便于 UI 与
|
|
31
|
+
* 审计分桶);扩展前先 review(扩展 = 增加通道语义;不是字符串自由枚举) */
|
|
32
|
+
export type ShareFailureReason =
|
|
33
|
+
| 'not_activated' // adapter 存在但未激活(M4 才接)
|
|
34
|
+
| 'unknown_channel' // target.channel 不在 ADAPTERS 表内(含原型键/非自有键)
|
|
35
|
+
| 'consent_required' // consent state = 'ask' 但未发卡
|
|
36
|
+
| 'consent_revoked' // consent 曾被拒绝/撤回/卡 id 不匹配
|
|
37
|
+
| 'share_consent_off' // 总开关关
|
|
38
|
+
| 'token_invalid' // token 签名/tampered/target 绑定不匹配/格式/字段
|
|
39
|
+
| 'token_expired' // token 过了 ttl
|
|
40
|
+
| 'adapter_error' // adapter.send 抛错(主流程收敛,绝不外泄异常)
|
|
41
|
+
|
|
42
|
+
export type ShareResult =
|
|
43
|
+
| { delivered: true; adapter_id: ShareChannel; sent_at: string }
|
|
44
|
+
| { delivered: false; reason: ShareFailureReason }
|
|
45
|
+
|
|
46
|
+
export interface ShareAdapter {
|
|
47
|
+
readonly id: ShareChannel
|
|
48
|
+
send(payload: SharePayload): Promise<ShareResult>
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---- 4 个 stub 适配器(issue #573 Phase C):契约形状 + no-op;不接 SDK、不发网络 ----
|
|
52
|
+
|
|
53
|
+
export const iMessageAdapter: ShareAdapter = {
|
|
54
|
+
id: 'imessage',
|
|
55
|
+
send: async () => ({ delivered: false, reason: 'not_activated' as const }),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const smsAdapter: ShareAdapter = {
|
|
59
|
+
id: 'sms',
|
|
60
|
+
send: async () => ({ delivered: false, reason: 'not_activated' as const }),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const slackAdapter: ShareAdapter = {
|
|
64
|
+
id: 'slack',
|
|
65
|
+
send: async () => ({ delivered: false, reason: 'not_activated' as const }),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const webhookAdapter: ShareAdapter = {
|
|
69
|
+
id: 'webhook',
|
|
70
|
+
send: async () => ({ delivered: false, reason: 'not_activated' as const }),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 通道 → adapter 表(主流程 share-deck 用此表分发)。
|
|
74
|
+
* 运行时 Object.freeze(设计文档「冻结 record」的字面兑现——编译期 Readonly 只挡 TS)。
|
|
75
|
+
* 扩通道须同时扩 ShareChannel 联合与这个表;SHARE_CHANNELS 由表键派生(单一事实源)。 */
|
|
76
|
+
export const ADAPTERS: Readonly<Record<ShareChannel, ShareAdapter>> = Object.freeze({
|
|
77
|
+
imessage: iMessageAdapter,
|
|
78
|
+
sms: smsAdapter,
|
|
79
|
+
slack: slackAdapter,
|
|
80
|
+
webhook: webhookAdapter,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
/** 通道闭集:从 ADAPTERS 键派生(自检 review:手列第二份清单必然漂移) */
|
|
84
|
+
export const SHARE_CHANNELS: readonly ShareChannel[] = Object.freeze(
|
|
85
|
+
Object.keys(ADAPTERS) as ShareChannel[],
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
/** 自有键判定:分发守卫用它挡原型键('__proto__'/'constructor'/'toString' 经
|
|
89
|
+
* Record 索引会命中 Object.prototype 继承成员,直接 lookup 会拿到真值绕过
|
|
90
|
+
* unknown_channel 检查然后 adapter.send undefined 崩溃)。 */
|
|
91
|
+
export function hasAdapterChannel(table: Readonly<Record<string, unknown>>, channel: unknown): boolean {
|
|
92
|
+
return typeof channel === 'string' && Object.prototype.hasOwnProperty.call(table, channel)
|
|
93
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Share consent state machine(issue #573,Phase C 切片;自检 review 修复见 PR #574)。
|
|
3
|
+
*
|
|
4
|
+
* 模式:`mode: 'ask' | 'allow' | 'off'` 三态总开关 + 每 share_id 一次性同意卡。
|
|
5
|
+
* 借鉴 `session-consent.ts` 的「一次一卡 + 拒绝即撤回 + 总开关 off」范式,
|
|
6
|
+
* 但与 session 授权**完全独立**(不合并——session 是「我能不能读你的浏览器」,
|
|
7
|
+
* share 是「我能不能替你外发到第三方」,两类授权的语义与撤销粒度都不同)。
|
|
8
|
+
*
|
|
9
|
+
* 三条硬纪律:
|
|
10
|
+
* 1. **默认 deny**:off 一律拒;ask/allow 下该 share_id 无存活卡 → consent_required;
|
|
11
|
+
* 2. **拒绝即撤回**:card_id 进 revoked 后,该卡即死;调用方出示的 card_id 必须
|
|
12
|
+
* 本身存活(在哪张卡上匹配就以哪张为准——不是「第一张活卡」);
|
|
13
|
+
* 3. **状态纯函数**:grant/revoke/setMode 返回新 state,不原地改;grant 幂等
|
|
14
|
+
* (同 share 已有存活卡则复用该卡,不再追加——防多卡漂移破坏撤销全量性)。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { randomUUID } from 'node:crypto'
|
|
18
|
+
|
|
19
|
+
export type ShareConsentMode = 'ask' | 'allow' | 'off'
|
|
20
|
+
|
|
21
|
+
export interface ShareConsentCard {
|
|
22
|
+
card_id: string
|
|
23
|
+
share_id: string
|
|
24
|
+
granted_at: string // ISO
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ShareConsentState {
|
|
28
|
+
mode: ShareConsentMode
|
|
29
|
+
/** append-only,granted 过的卡 */
|
|
30
|
+
granted: ShareConsentCard[]
|
|
31
|
+
/** 已撤销的 card_id(可能与 granted 重叠——grant 后被 revoke 即同时存在) */
|
|
32
|
+
revoked: string[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type ShareConsentCheckResult =
|
|
36
|
+
| { ok: true }
|
|
37
|
+
| { ok: false; reason: 'share_consent_off' | 'consent_required' | 'consent_revoked' }
|
|
38
|
+
|
|
39
|
+
export function defaultShareConsentState(): ShareConsentState {
|
|
40
|
+
return { mode: 'ask', granted: [], revoked: [] }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 校验:off 一律拒;该 share_id 无任何卡 → consent_required(从未发卡);
|
|
44
|
+
* 有卡但全部被撤 → consent_revoked(曾发卡已撤);调用方出示 card_id 时,
|
|
45
|
+
* 该卡必须属于此 share 且自身未撤销(按卡匹配,不按第一张活卡——防多卡下
|
|
46
|
+
* 合法卡被误判 revoked、被撤卡被其它活卡顶替)。 */
|
|
47
|
+
export function checkShareConsent(state: ShareConsentState, share_id: string, card_id?: string): ShareConsentCheckResult {
|
|
48
|
+
if (state.mode === 'off') return { ok: false, reason: 'share_consent_off' }
|
|
49
|
+
// 边界守卫:畸形持久化 state(字段非数组)fail closed 视为空——绝不抛错
|
|
50
|
+
const grantedList = Array.isArray(state?.granted) ? state.granted : []
|
|
51
|
+
const revokedList = Array.isArray(state?.revoked) ? state.revoked : []
|
|
52
|
+
const allForShare = grantedList.filter(c => c.share_id === share_id)
|
|
53
|
+
if (allForShare.length === 0) {
|
|
54
|
+
return { ok: false, reason: 'consent_required' }
|
|
55
|
+
}
|
|
56
|
+
if (card_id !== undefined) {
|
|
57
|
+
const given = allForShare.find(c => c.card_id === card_id)
|
|
58
|
+
if (!given || revokedList.includes(card_id)) {
|
|
59
|
+
return { ok: false, reason: 'consent_revoked' }
|
|
60
|
+
}
|
|
61
|
+
return { ok: true }
|
|
62
|
+
}
|
|
63
|
+
const anyLive = allForShare.some(c => !revokedList.includes(c.card_id))
|
|
64
|
+
if (!anyLive) {
|
|
65
|
+
return { ok: false, reason: 'consent_revoked' }
|
|
66
|
+
}
|
|
67
|
+
return { ok: true }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 发卡:幂等——同 share_id 已有存活卡则复用(不再追加,防多卡漂移);
|
|
71
|
+
* 否则追加新卡。返回 state 与卡本身(调用方持有 card_id 用于后续校验)。 */
|
|
72
|
+
export function grantShareConsent(
|
|
73
|
+
state: ShareConsentState,
|
|
74
|
+
share_id: string,
|
|
75
|
+
now: () => Date = () => new Date(),
|
|
76
|
+
randomCardId: () => string = defaultCardId,
|
|
77
|
+
): { state: ShareConsentState; card: ShareConsentCard } {
|
|
78
|
+
const existing = state.granted.find(c => c.share_id === share_id && !state.revoked.includes(c.card_id))
|
|
79
|
+
if (existing) return { state, card: existing }
|
|
80
|
+
const card: ShareConsentCard = {
|
|
81
|
+
card_id: randomCardId(),
|
|
82
|
+
share_id,
|
|
83
|
+
granted_at: now().toISOString(),
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
state: { mode: state.mode, granted: [...state.granted, card], revoked: state.revoked },
|
|
87
|
+
card,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 撤回:把 card_id 加入 revoked(幂等——重复 revoke 不变) */
|
|
92
|
+
export function revokeShareConsent(state: ShareConsentState, card_id: string): ShareConsentState {
|
|
93
|
+
if (state.revoked.includes(card_id)) return state
|
|
94
|
+
return { mode: state.mode, granted: state.granted, revoked: [...state.revoked, card_id] }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 切总开关:返回新 state(不原地改) */
|
|
98
|
+
export function setShareConsentMode(state: ShareConsentState, mode: ShareConsentMode): ShareConsentState {
|
|
99
|
+
return { mode, granted: state.granted, revoked: state.revoked }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** 默认 card_id 生成器(测试可注入);Node 原生 crypto.randomUUID */
|
|
103
|
+
export function defaultCardId(): string {
|
|
104
|
+
return randomUUID()
|
|
105
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Share deck 主入口(issue #573,Phase C 切片;自检 review 修复见 PR #574):
|
|
3
|
+
* 对外发起一次 share 动作的契约编排。
|
|
4
|
+
*
|
|
5
|
+
* 流程(每步都 fail closed,无副作用):
|
|
6
|
+
* 1. verifyShareToken(token, secret) → 失败返回 reason
|
|
7
|
+
* 2. tokenTargetMatches(payload.target) → 目的地与签名不符返回 token_invalid
|
|
8
|
+
* 3. checkShareConsent(state, share_id, deps.card_id?) → 失败返回 reason
|
|
9
|
+
* 4. 分发守卫(hasAdapterChannel:自有键判定,挡原型键)→ unknown_channel
|
|
10
|
+
* 5. adapter.send(payload) **在 try/catch 内** → 抛错收敛为 adapter_error
|
|
11
|
+
*
|
|
12
|
+
* 设计要点:
|
|
13
|
+
* - 纯异步编排,无 IO / 无网络 / 不写 state(consent state 由调用方持有);
|
|
14
|
+
* - secret + state + now + card_id + adapters 全部依赖注入,测试可全离线确定性;
|
|
15
|
+
* - **永不抛错**:任何 adapter 异常都被收敛为 ShareResult(契约出口唯一);
|
|
16
|
+
* - **不挂 dsh 工具面**(留给 M4);Phase C 只交付契约 + stub + 流程。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
ADAPTERS,
|
|
21
|
+
hasAdapterChannel,
|
|
22
|
+
type ShareAdapter,
|
|
23
|
+
type ShareChannel,
|
|
24
|
+
type SharePayload,
|
|
25
|
+
type ShareResult,
|
|
26
|
+
} from './adapters.ts'
|
|
27
|
+
import { tokenTargetMatches, verifyShareToken } from './share-token.ts'
|
|
28
|
+
import { checkShareConsent, type ShareConsentState } from './share-consent.ts'
|
|
29
|
+
|
|
30
|
+
export interface ShareDeckDeps {
|
|
31
|
+
/** HMAC secret(由调用方经 share-token.resolveShareSecret 解析) */
|
|
32
|
+
secret: string
|
|
33
|
+
/** consent state(由调用方持有 + 持久化;本模块不读不写) */
|
|
34
|
+
state: ShareConsentState
|
|
35
|
+
/** 时间注入(测试用) */
|
|
36
|
+
now?: () => Date
|
|
37
|
+
/** 可选:调用方持有的同意卡 id(二次校验——出示的卡必须属于该 share 且未撤销) */
|
|
38
|
+
card_id?: string
|
|
39
|
+
/** 可选:适配器表注入(测试/M4 用;缺省走冻结的 ADAPTERS) */
|
|
40
|
+
adapters?: Readonly<Record<ShareChannel, ShareAdapter>>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 公开的 payload 形状(不含 token —— token 单独传) */
|
|
44
|
+
export type ShareDeckRequest = Omit<SharePayload, 'token'>
|
|
45
|
+
|
|
46
|
+
export async function shareDeck(token: string, payload: ShareDeckRequest, deps: ShareDeckDeps): Promise<ShareResult> {
|
|
47
|
+
// 1. token 校验(签名/格式/ttl/过期)
|
|
48
|
+
const tokenCheck = verifyShareToken(token, deps.secret, deps.now)
|
|
49
|
+
if (!tokenCheck.ok) {
|
|
50
|
+
return { delivered: false, reason: tokenCheck.reason }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 2. 目的地绑定:token 签发时的 target 必须与本次请求完全一致(防换收件人重放)
|
|
54
|
+
if (!tokenTargetMatches(tokenCheck.payload, payload.target)) {
|
|
55
|
+
return { delivered: false, reason: 'token_invalid' }
|
|
56
|
+
}
|
|
57
|
+
const shareId = tokenCheck.payload.share_id
|
|
58
|
+
|
|
59
|
+
// 3. consent 校验(share_id + 调用方出示的可选 card_id 二次校验)
|
|
60
|
+
const consent = checkShareConsent(deps.state, shareId, deps.card_id)
|
|
61
|
+
if (!consent.ok) {
|
|
62
|
+
return { delivered: false, reason: consent.reason }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 4. 分发守卫:自有键判定(原型键 '__proto__'/'constructor' 等经 Record 索引会
|
|
66
|
+
// 命中继承成员,直接 lookup 拿到真值后 adapter.send undefined 会崩——先挡)
|
|
67
|
+
const table = deps.adapters ?? ADAPTERS
|
|
68
|
+
const channel = payload.target.channel
|
|
69
|
+
if (!hasAdapterChannel(table, channel)) {
|
|
70
|
+
return { delivered: false, reason: 'unknown_channel' }
|
|
71
|
+
}
|
|
72
|
+
const adapter = (table as Readonly<Record<string, ShareAdapter>>)[channel as string]
|
|
73
|
+
|
|
74
|
+
// 5. adapter.send 在 try/catch 内:任何异常收敛为 adapter_error,绝不外泄
|
|
75
|
+
const fullPayload: SharePayload = { token, target: payload.target, body: payload.body }
|
|
76
|
+
const sentAt = (deps.now ?? (() => new Date()))().toISOString()
|
|
77
|
+
let result: ShareResult
|
|
78
|
+
try {
|
|
79
|
+
result = await adapter.send(fullPayload)
|
|
80
|
+
} catch {
|
|
81
|
+
return { delivered: false, reason: 'adapter_error' }
|
|
82
|
+
}
|
|
83
|
+
if (result.delivered) return { ...result, sent_at: result.sent_at ?? sentAt }
|
|
84
|
+
return { delivered: false, reason: result.reason }
|
|
85
|
+
}
|