@vellumai/assistant 0.3.4 → 0.3.6
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/Dockerfile +2 -0
- package/README.md +88 -2
- package/eslint.config.mjs +31 -0
- package/package.json +1 -1
- package/scripts/ipc/check-swift-decoder-drift.ts +4 -1
- package/scripts/ipc/generate-swift.ts +31 -2
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +438 -1
- package/src/__tests__/approval-conversation-turn.test.ts +214 -0
- package/src/__tests__/approval-hardcoded-copy-guard.test.ts +41 -0
- package/src/__tests__/approval-message-composer.test.ts +253 -0
- package/src/__tests__/browser-manager.test.ts +1 -0
- package/src/__tests__/call-conversation-messages.test.ts +130 -0
- package/src/__tests__/call-domain.test.ts +12 -2
- package/src/__tests__/call-orchestrator.test.ts +799 -249
- package/src/__tests__/call-pointer-messages.test.ts +148 -0
- package/src/__tests__/call-recovery.test.ts +3 -0
- package/src/__tests__/call-routes-http.test.ts +32 -2
- package/src/__tests__/call-store.test.ts +3 -0
- package/src/__tests__/channel-approval-routes.test.ts +1277 -98
- package/src/__tests__/channel-approval.test.ts +37 -0
- package/src/__tests__/channel-approvals.test.ts +36 -50
- package/src/__tests__/channel-guardian.test.ts +630 -22
- package/src/__tests__/channel-readiness-service.test.ts +324 -0
- package/src/__tests__/checker.test.ts +14 -7
- package/src/__tests__/clarification-resolver.test.ts +44 -24
- package/src/__tests__/commit-message-enrichment-service.test.ts +9 -4
- package/src/__tests__/computer-use-session-working-dir.test.ts +8 -0
- package/src/__tests__/config-schema.test.ts +14 -8
- package/src/__tests__/context-window-manager.test.ts +30 -2
- package/src/__tests__/contradiction-checker.test.ts +20 -5
- package/src/__tests__/credential-security-invariants.test.ts +7 -2
- package/src/__tests__/daemon-lifecycle.test.ts +13 -12
- package/src/__tests__/db-migration-rollback.test.ts +752 -0
- package/src/__tests__/dictation-mode-detection.test.ts +63 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +2 -0
- package/src/__tests__/entity-search.test.ts +615 -0
- package/src/__tests__/fuzzy-match-property.test.ts +5 -5
- package/src/__tests__/guardian-action-store.test.ts +123 -0
- package/src/__tests__/guardian-action-sweep.test.ts +277 -0
- package/src/__tests__/guardian-dispatch.test.ts +389 -0
- package/src/__tests__/guardian-question-copy.test.ts +47 -0
- package/src/__tests__/handlers-telegram-config.test.ts +4 -2
- package/src/__tests__/handlers-twilio-config.test.ts +533 -0
- package/src/__tests__/intent-routing.test.ts +2 -0
- package/src/__tests__/ipc-snapshot.test.ts +291 -1
- package/src/__tests__/memory-upsert-concurrency.test.ts +828 -0
- package/src/__tests__/messaging-send-tool.test.ts +65 -0
- package/src/__tests__/model-intents.test.ts +96 -0
- package/src/__tests__/no-direct-anthropic-sdk-imports.test.ts +42 -0
- package/src/__tests__/oauth2-gateway-transport.test.ts +130 -0
- package/src/__tests__/onboarding-starter-tasks.test.ts +2 -0
- package/src/__tests__/provider-commit-message-generator.test.ts +89 -13
- package/src/__tests__/provider-error-scenarios.test.ts +621 -0
- package/src/__tests__/provider-fail-open-selection.test.ts +119 -0
- package/src/__tests__/qdrant-manager.test.ts +27 -20
- package/src/__tests__/relay-server.test.ts +779 -40
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +6 -0
- package/src/__tests__/run-orchestrator.test.ts +42 -4
- package/src/__tests__/runtime-runs-http.test.ts +17 -1
- package/src/__tests__/runtime-runs.test.ts +16 -0
- package/src/__tests__/schedule-store.test.ts +18 -4
- package/src/__tests__/scheduler-recurrence.test.ts +13 -4
- package/src/__tests__/session-abort-tool-results.test.ts +6 -0
- package/src/__tests__/session-agent-loop.test.ts +857 -0
- package/src/__tests__/session-conflict-gate.test.ts +6 -0
- package/src/__tests__/session-pre-run-repair.test.ts +6 -0
- package/src/__tests__/session-profile-injection.test.ts +6 -0
- package/src/__tests__/session-provider-retry-repair.test.ts +6 -0
- package/src/__tests__/session-queue.test.ts +6 -0
- package/src/__tests__/session-runtime-assembly.test.ts +321 -13
- package/src/__tests__/session-slash-known.test.ts +6 -0
- package/src/__tests__/session-slash-queue.test.ts +6 -0
- package/src/__tests__/session-slash-unknown.test.ts +6 -0
- package/src/__tests__/session-surfaces-task-progress.test.ts +2 -0
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +1 -0
- package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -0
- package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -0
- package/src/__tests__/session-workspace-injection.test.ts +6 -0
- package/src/__tests__/session-workspace-tool-tracking.test.ts +6 -0
- package/src/__tests__/skills.test.ts +2 -0
- package/src/__tests__/sms-messaging-provider.test.ts +126 -0
- package/src/__tests__/starter-task-flow.test.ts +2 -0
- package/src/__tests__/swarm-dag-pathological.test.ts +535 -0
- package/src/__tests__/system-prompt.test.ts +2 -0
- package/src/__tests__/task-management-tools.test.ts +2 -2
- package/src/__tests__/task-runner.test.ts +14 -4
- package/src/__tests__/terminal-tools.test.ts +25 -19
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +545 -0
- package/src/__tests__/tool-executor-shell-integration.test.ts +11 -11
- package/src/__tests__/tool-executor.test.ts +23 -24
- package/src/__tests__/trust-store.test.ts +3 -3
- package/src/__tests__/twilio-rest.test.ts +29 -0
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +3 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +11 -0
- package/src/__tests__/twilio-routes.test.ts +167 -11
- package/src/__tests__/twitter-cli-error-shaping.test.ts +2 -2
- package/src/__tests__/user-reference.test.ts +2 -0
- package/src/__tests__/voice-quality.test.ts +222 -0
- package/src/__tests__/web-search.test.ts +46 -30
- package/src/__tests__/work-item-output.test.ts +110 -0
- package/src/agent/loop.ts +1 -1
- package/src/agent-heartbeat/agent-heartbeat-service.ts +2 -10
- package/src/amazon/client.ts +1418 -0
- package/src/amazon/request-extractor.ts +135 -0
- package/src/amazon/session.ts +109 -0
- package/src/autonomy/autonomy-store.ts +5 -5
- package/src/browser-extension-relay/client.ts +124 -0
- package/src/browser-extension-relay/protocol.ts +63 -0
- package/src/browser-extension-relay/server.ts +177 -0
- package/src/bundler/app-bundler.ts +3 -3
- package/src/bundler/bundle-signer.ts +1 -1
- package/src/bundler/signature-verifier.ts +1 -1
- package/src/calls/call-conversation-messages.ts +33 -0
- package/src/calls/call-domain.ts +114 -10
- package/src/calls/call-orchestrator.ts +268 -59
- package/src/calls/call-pointer-messages.ts +53 -0
- package/src/calls/call-recovery.ts +3 -8
- package/src/calls/call-store.ts +69 -87
- package/src/calls/elevenlabs-config.ts +3 -2
- package/src/calls/guardian-action-sweep.ts +105 -0
- package/src/calls/guardian-dispatch.ts +203 -0
- package/src/calls/guardian-question-copy.ts +133 -0
- package/src/calls/relay-server.ts +466 -8
- package/src/calls/speaker-identification.ts +1 -1
- package/src/calls/twilio-config.ts +22 -14
- package/src/calls/twilio-provider.ts +6 -4
- package/src/calls/twilio-rest.ts +308 -7
- package/src/calls/twilio-routes.ts +65 -12
- package/src/calls/types.ts +3 -1
- package/src/channels/types.ts +25 -0
- package/src/cli/amazon.ts +815 -0
- package/src/cli/config-commands.ts +2 -2
- package/src/cli/core-commands.ts +4 -3
- package/src/cli/influencer.ts +244 -0
- package/src/cli/map.ts +89 -6
- package/src/cli.ts +1 -1
- package/src/config/agent-schema.ts +171 -0
- package/src/config/bundled-skills/amazon/SKILL.md +127 -0
- package/src/config/bundled-skills/amazon/icon.svg +13 -0
- package/src/config/bundled-skills/api-mapping/SKILL.md +78 -0
- package/src/config/bundled-skills/browser/SKILL.md +1 -0
- package/src/config/bundled-skills/browser/TOOLS.json +17 -0
- package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +25 -0
- package/src/config/bundled-skills/doordash/SKILL.md +51 -51
- package/src/config/bundled-skills/email-setup/SKILL.md +14 -5
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +183 -0
- package/src/config/bundled-skills/influencer/SKILL.md +144 -0
- package/src/config/bundled-skills/knowledge-graph/SKILL.md +15 -0
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +56 -0
- package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +185 -0
- package/src/config/bundled-skills/macos-automation/icon.svg +12 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +176 -0
- package/src/config/bundled-skills/media-processing/TOOLS.json +230 -0
- package/src/config/bundled-skills/media-processing/__tests__/concurrency-pool.test.ts +77 -0
- package/src/config/bundled-skills/media-processing/__tests__/cost-tracker.test.ts +69 -0
- package/src/config/bundled-skills/media-processing/__tests__/preprocess.test.ts +303 -0
- package/src/config/bundled-skills/media-processing/services/concurrency-pool.ts +55 -0
- package/src/config/bundled-skills/media-processing/services/cost-tracker.ts +86 -0
- package/src/config/bundled-skills/media-processing/services/gemini-map.ts +339 -0
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +551 -0
- package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +259 -0
- package/src/config/bundled-skills/media-processing/services/reduce.ts +197 -0
- package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +136 -0
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +59 -0
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +195 -0
- package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +197 -0
- package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +143 -0
- package/src/config/bundled-skills/media-processing/tools/media-status.ts +75 -0
- package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +65 -0
- package/src/config/bundled-skills/messaging/SKILL.md +33 -8
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -7
- package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +2 -1
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +88 -23
- package/src/config/bundled-skills/twitter/SKILL.md +19 -3
- package/src/config/bundled-skills/twitter/icon.svg +14 -0
- package/src/config/bundled-tool-registry.ts +310 -0
- package/src/config/calls-schema.ts +181 -0
- package/src/config/core-schema.ts +309 -0
- package/src/config/defaults.ts +28 -3
- package/src/config/env-registry.ts +162 -0
- package/src/config/env.ts +175 -0
- package/src/config/loader.ts +6 -6
- package/src/config/memory-schema.ts +528 -0
- package/src/config/sandbox-schema.ts +55 -0
- package/src/config/schema.ts +158 -1133
- package/src/config/skill-state.ts +1 -1
- package/src/config/skills-schema.ts +32 -0
- package/src/config/skills.ts +35 -24
- package/src/config/system-prompt.ts +131 -56
- package/src/config/templates/IDENTITY.md +2 -2
- package/src/config/templates/SOUL.md +1 -1
- package/src/config/types.ts +1 -0
- package/src/config/user-reference.ts +4 -9
- package/src/config/vellum-skills/catalog.json +6 -7
- package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +5 -1
- package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +4 -3
- package/src/config/vellum-skills/sms-setup/SKILL.md +216 -0
- package/src/config/vellum-skills/twilio-setup/SKILL.md +40 -8
- package/src/context/window-manager.ts +27 -7
- package/src/daemon/approval-generators.ts +186 -0
- package/src/daemon/approved-devices-store.ts +140 -0
- package/src/daemon/assistant-attachments.ts +1 -1
- package/src/daemon/classifier.ts +35 -32
- package/src/daemon/config-watcher.ts +1 -1
- package/src/daemon/daemon-control.ts +217 -0
- package/src/daemon/handlers/apps.ts +2 -3
- package/src/daemon/handlers/config-channels.ts +158 -0
- package/src/daemon/handlers/config-inbox.ts +540 -0
- package/src/daemon/handlers/config-ingress.ts +231 -0
- package/src/daemon/handlers/config-integrations.ts +258 -0
- package/src/daemon/handlers/config-model.ts +143 -0
- package/src/daemon/handlers/config-parental.ts +163 -0
- package/src/daemon/handlers/config-scheduling.ts +172 -0
- package/src/daemon/handlers/config-slack.ts +92 -0
- package/src/daemon/handlers/config-telegram.ts +301 -0
- package/src/daemon/handlers/config-tools.ts +177 -0
- package/src/daemon/handlers/config-trust.ts +104 -0
- package/src/daemon/handlers/config-twilio.ts +1080 -0
- package/src/daemon/handlers/config.ts +53 -1689
- package/src/daemon/handlers/diagnostics.ts +1 -1
- package/src/daemon/handlers/dictation.ts +180 -0
- package/src/daemon/handlers/documents.ts +18 -32
- package/src/daemon/handlers/identity.ts +14 -23
- package/src/daemon/handlers/index.ts +11 -0
- package/src/daemon/handlers/misc.ts +3 -5
- package/src/daemon/handlers/pairing.ts +98 -0
- package/src/daemon/handlers/sessions.ts +56 -5
- package/src/daemon/handlers/shared.ts +6 -1
- package/src/daemon/handlers/skills.ts +1 -1
- package/src/daemon/handlers/twitter-auth.ts +2 -0
- package/src/daemon/handlers/work-items.ts +17 -9
- package/src/daemon/handlers/workspace-files.ts +4 -3
- package/src/daemon/install-cli-launchers.ts +113 -0
- package/src/daemon/ipc-contract/apps.ts +356 -0
- package/src/daemon/ipc-contract/browser.ts +74 -0
- package/src/daemon/ipc-contract/computer-use.ts +151 -0
- package/src/daemon/ipc-contract/diagnostics.ts +56 -0
- package/src/daemon/ipc-contract/documents.ts +74 -0
- package/src/daemon/ipc-contract/inbox.ts +209 -0
- package/src/daemon/ipc-contract/integrations.ts +284 -0
- package/src/daemon/ipc-contract/memory.ts +48 -0
- package/src/daemon/ipc-contract/messages.ts +211 -0
- package/src/daemon/ipc-contract/pairing.ts +45 -0
- package/src/daemon/ipc-contract/parental-control.ts +95 -0
- package/src/daemon/ipc-contract/schedules.ts +97 -0
- package/src/daemon/ipc-contract/sessions.ts +315 -0
- package/src/daemon/ipc-contract/shared.ts +42 -0
- package/src/daemon/ipc-contract/skills.ts +120 -0
- package/src/daemon/ipc-contract/subagents.ts +58 -0
- package/src/daemon/ipc-contract/surfaces.ts +250 -0
- package/src/daemon/ipc-contract/trust.ts +60 -0
- package/src/daemon/ipc-contract/work-items.ts +225 -0
- package/src/daemon/ipc-contract/workspace.ts +113 -0
- package/src/daemon/ipc-contract-inventory.json +70 -0
- package/src/daemon/ipc-contract-inventory.ts +55 -29
- package/src/daemon/ipc-contract.ts +229 -2426
- package/src/daemon/ipc-protocol.ts +1 -1
- package/src/daemon/ipc-validate.ts +7 -0
- package/src/daemon/lifecycle.ts +97 -377
- package/src/daemon/pairing-store.ts +177 -0
- package/src/daemon/providers-setup.ts +43 -0
- package/src/daemon/ride-shotgun-handler.ts +68 -3
- package/src/daemon/server.ts +66 -46
- package/src/daemon/session-agent-loop-handlers.ts +421 -0
- package/src/daemon/session-agent-loop.ts +117 -275
- package/src/daemon/session-dynamic-profile.ts +1 -1
- package/src/daemon/session-history.ts +1 -1
- package/src/daemon/session-media-retry.ts +1 -1
- package/src/daemon/session-messaging.ts +37 -2
- package/src/daemon/session-notifiers.ts +5 -25
- package/src/daemon/session-process.ts +99 -59
- package/src/daemon/session-queue-manager.ts +96 -4
- package/src/daemon/session-runtime-assembly.ts +199 -10
- package/src/daemon/session-surfaces.ts +19 -4
- package/src/daemon/session-tool-setup.ts +30 -30
- package/src/daemon/session-workspace.ts +1 -1
- package/src/daemon/session.ts +35 -2
- package/src/daemon/shutdown-handlers.ts +122 -0
- package/src/daemon/trace-emitter.ts +1 -1
- package/src/daemon/watch-handler.ts +36 -33
- package/src/doordash/cart-queries.ts +787 -0
- package/src/doordash/client.ts +144 -127
- package/src/doordash/order-queries.ts +85 -0
- package/src/doordash/queries.ts +10 -1308
- package/src/doordash/search-queries.ts +203 -0
- package/src/doordash/session.ts +3 -2
- package/src/doordash/store-queries.ts +246 -0
- package/src/doordash/types.ts +367 -0
- package/src/email/providers/agentmail.ts +2 -1
- package/src/email/providers/index.ts +3 -2
- package/src/email/service.ts +3 -2
- package/src/errors.ts +43 -0
- package/src/home-base/prebuilt/seed.ts +1 -1
- package/src/hooks/cli.ts +6 -5
- package/src/hooks/config.ts +6 -8
- package/src/hooks/discovery.ts +6 -5
- package/src/hooks/manager.ts +4 -3
- package/src/hooks/runner.ts +2 -2
- package/src/hooks/templates.ts +5 -5
- package/src/inbound/public-ingress-urls.ts +6 -4
- package/src/index.ts +4 -2
- package/src/influencer/client.ts +1104 -0
- package/src/instrument.ts +4 -3
- package/src/logfire.ts +4 -3
- package/src/memory/admin.ts +25 -35
- package/src/memory/attachments-store.ts +4 -7
- package/src/memory/channel-delivery-store.ts +30 -1
- package/src/memory/channel-guardian-store.ts +202 -2
- package/src/memory/clarification-resolver.ts +37 -33
- package/src/memory/conflict-store.ts +67 -61
- package/src/memory/contradiction-checker.ts +141 -117
- package/src/memory/conversation-store.ts +335 -51
- package/src/memory/db-connection.ts +27 -4
- package/src/memory/db-init.ts +265 -4
- package/src/memory/db.ts +14 -1
- package/src/memory/embedding-backend.ts +27 -5
- package/src/memory/embedding-ollama.ts +2 -1
- package/src/memory/entity-extractor.ts +38 -35
- package/src/memory/guardian-action-store.ts +430 -0
- package/src/memory/inbox-escalation-projection.ts +59 -0
- package/src/memory/inbox-thread-store.ts +218 -0
- package/src/memory/ingress-invite-store.ts +338 -0
- package/src/memory/ingress-member-store.ts +350 -0
- package/src/memory/items-extractor.ts +91 -97
- package/src/memory/job-handlers/index-maintenance.ts +3 -3
- package/src/memory/job-handlers/media-processing.ts +69 -0
- package/src/memory/job-handlers/summarization.ts +32 -26
- package/src/memory/job-utils.ts +3 -10
- package/src/memory/jobs-store.ts +8 -10
- package/src/memory/jobs-worker.ts +55 -36
- package/src/memory/media-store.ts +759 -0
- package/src/memory/migrations/001-job-deferrals.ts +45 -0
- package/src/memory/migrations/002-tool-invocations-fk.ts +43 -0
- package/src/memory/migrations/003-memory-fts-backfill.ts +24 -0
- package/src/memory/migrations/004-entity-relation-dedup.ts +87 -0
- package/src/memory/migrations/005-fingerprint-scope-unique.ts +80 -0
- package/src/memory/migrations/006-scope-salted-fingerprints.ts +62 -0
- package/src/memory/migrations/007-assistant-id-to-self.ts +254 -0
- package/src/memory/migrations/008-remove-assistant-id-columns.ts +208 -0
- package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +83 -0
- package/src/memory/migrations/010-ext-conv-bindings-channel-chat-unique.ts +56 -0
- package/src/memory/migrations/011-call-sessions-provider-sid-dedup.ts +63 -0
- package/src/memory/migrations/012-call-sessions-add-initiated-from.ts +19 -0
- package/src/memory/migrations/013-guardian-action-tables.ts +68 -0
- package/src/memory/migrations/014-backfill-inbox-thread-state.ts +76 -0
- package/src/memory/migrations/015-drop-active-search-index.ts +27 -0
- package/src/memory/migrations/016-memory-segments-indexes.ts +11 -0
- package/src/memory/migrations/017-memory-items-indexes.ts +10 -0
- package/src/memory/migrations/018-remaining-table-indexes.ts +13 -0
- package/src/memory/migrations/index.ts +24 -0
- package/src/memory/migrations/registry.ts +79 -0
- package/src/memory/migrations/validate-migration-state.ts +69 -0
- package/src/memory/qdrant-manager.ts +49 -8
- package/src/memory/query-builder.ts +1 -1
- package/src/memory/raw-query.ts +119 -0
- package/src/memory/recall-cache.ts +4 -1
- package/src/memory/retriever.ts +165 -47
- package/src/memory/schema-migration.ts +25 -984
- package/src/memory/schema.ts +228 -7
- package/src/memory/search/entity.ts +205 -31
- package/src/memory/search/lexical.ts +81 -52
- package/src/memory/search/ranking.ts +27 -23
- package/src/memory/search/semantic.ts +157 -19
- package/src/memory/search/types.ts +24 -0
- package/src/memory/shared-app-links-store.ts +4 -5
- package/src/memory/validation.ts +19 -0
- package/src/messaging/draft-store.ts +5 -6
- package/src/messaging/provider-types.ts +2 -0
- package/src/messaging/providers/sms/adapter.ts +201 -0
- package/src/messaging/providers/sms/client.ts +93 -0
- package/src/messaging/providers/sms/types.ts +7 -0
- package/src/messaging/providers/telegram-bot/adapter.ts +2 -5
- package/src/messaging/providers/whatsapp/adapter.ts +136 -0
- package/src/messaging/providers/whatsapp/client.ts +67 -0
- package/src/messaging/style-analyzer.ts +5 -4
- package/src/messaging/thread-summarizer.ts +61 -69
- package/src/messaging/triage-engine.ts +62 -71
- package/src/migrations/config-merge.ts +53 -0
- package/src/migrations/data-layout.ts +68 -0
- package/src/migrations/data-merge.ts +33 -0
- package/src/migrations/hooks-merge.ts +90 -0
- package/src/migrations/index.ts +6 -0
- package/src/migrations/log.ts +23 -0
- package/src/migrations/skills-merge.ts +33 -0
- package/src/migrations/workspace-layout.ts +79 -0
- package/src/permissions/checker.ts +133 -11
- package/src/permissions/prompter.ts +14 -0
- package/src/permissions/shell-identity.ts +31 -1
- package/src/permissions/trust-store.ts +21 -1
- package/src/providers/anthropic/client.ts +4 -4
- package/src/providers/failover.ts +2 -2
- package/src/providers/model-intents.ts +70 -0
- package/src/providers/ollama/client.ts +2 -1
- package/src/providers/provider-send-message.ts +176 -0
- package/src/providers/registry.ts +71 -30
- package/src/providers/retry.ts +35 -1
- package/src/providers/types.ts +12 -1
- package/src/runtime/approval-conversation-turn.ts +97 -0
- package/src/runtime/approval-message-composer.ts +253 -0
- package/src/runtime/channel-approval-parser.ts +36 -2
- package/src/runtime/channel-approvals.ts +11 -24
- package/src/runtime/channel-guardian-service.ts +88 -21
- package/src/runtime/channel-readiness-service.ts +418 -0
- package/src/runtime/channel-readiness-types.ts +35 -0
- package/src/runtime/channel-retry-sweep.ts +184 -0
- package/src/runtime/guardian-context-resolver.ts +108 -0
- package/src/runtime/http-server.ts +275 -717
- package/src/runtime/http-types.ts +59 -3
- package/src/runtime/middleware/auth.ts +116 -0
- package/src/runtime/middleware/error-handler.ts +33 -0
- package/src/runtime/middleware/twilio-validation.ts +127 -0
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/call-routes.ts +51 -7
- package/src/runtime/routes/channel-delivery-routes.ts +170 -0
- package/src/runtime/routes/channel-guardian-routes.ts +1191 -0
- package/src/runtime/routes/channel-inbound-routes.ts +1152 -0
- package/src/runtime/routes/channel-route-shared.ts +144 -0
- package/src/runtime/routes/channel-routes.ts +32 -1588
- package/src/runtime/routes/conversation-routes.ts +50 -7
- package/src/runtime/routes/events-routes.ts +2 -2
- package/src/runtime/routes/identity-routes.ts +126 -0
- package/src/runtime/routes/pairing-routes.ts +143 -0
- package/src/runtime/routes/run-routes.ts +15 -1
- package/src/runtime/run-orchestrator.ts +86 -35
- package/src/schedule/schedule-store.ts +36 -32
- package/src/schedule/scheduler.ts +3 -3
- package/src/security/encrypted-store.ts +5 -7
- package/src/security/oauth2.ts +45 -15
- package/src/security/parental-control-store.ts +183 -0
- package/src/security/secret-allowlist.ts +4 -3
- package/src/security/secret-scanner.ts +5 -5
- package/src/security/secure-keys.ts +1 -1
- package/src/security/token-manager.ts +3 -2
- package/src/services/vercel-deploy.ts +6 -2
- package/src/skills/tool-manifest.ts +3 -3
- package/src/skills/vellum-catalog-remote.ts +75 -16
- package/src/slack/slack-webhook.ts +2 -1
- package/src/swarm/orchestrator.ts +92 -1
- package/src/swarm/router-planner.ts +6 -9
- package/src/swarm/worker-prompts.ts +9 -12
- package/src/tasks/task-compiler.ts +19 -28
- package/src/tasks/task-runner.ts +1 -1
- package/src/tools/assets/materialize.ts +2 -2
- package/src/tools/assets/search.ts +15 -14
- package/src/tools/browser/__tests__/auth-detector.test.ts +1 -0
- package/src/tools/browser/auto-navigate.ts +1 -0
- package/src/tools/browser/browser-execution.ts +10 -1
- package/src/tools/browser/browser-manager.ts +119 -4
- package/src/tools/browser/network-recorder.ts +5 -0
- package/src/tools/calls/call-start.ts +1 -0
- package/src/tools/credentials/broker.ts +11 -2
- package/src/tools/credentials/metadata-store.ts +18 -14
- package/src/tools/credentials/post-connect-hooks.ts +61 -0
- package/src/tools/credentials/vault.ts +49 -23
- package/src/tools/execution-target.ts +11 -1
- package/src/tools/executor.ts +68 -9
- package/src/tools/host-terminal/cli-discover.ts +1 -1
- package/src/tools/network/script-proxy/http-forwarder.ts +1 -1
- package/src/tools/network/script-proxy/mitm-handler.ts +1 -1
- package/src/tools/network/script-proxy/server.ts +1 -1
- package/src/tools/network/script-proxy/session-manager.ts +6 -5
- package/src/tools/network/web-fetch.ts +18 -2
- package/src/tools/network/web-search.ts +8 -4
- package/src/tools/reminder/reminder-store.ts +14 -15
- package/src/tools/schedule/create.ts +1 -0
- package/src/tools/schedule/list.ts +2 -1
- package/src/tools/shared/filesystem/file-ops-service.ts +5 -7
- package/src/tools/skills/skill-script-runner.ts +24 -9
- package/src/tools/skills/skill-tool-factory.ts +1 -0
- package/src/tools/tasks/work-item-enqueue.ts +2 -2
- package/src/tools/terminal/evaluate-typescript.ts +21 -12
- package/src/tools/terminal/parser.ts +50 -0
- package/src/tools/types.ts +2 -0
- package/src/tools/watcher/delete.ts +6 -0
- package/src/tools/weather/service.ts +1 -1
- package/src/twitter/client.ts +190 -24
- package/src/twitter/router.ts +1 -1
- package/src/twitter/session.ts +4 -3
- package/src/util/clipboard.ts +1 -1
- package/src/util/errors.ts +65 -8
- package/src/util/fs.ts +40 -0
- package/src/util/json.ts +10 -0
- package/src/util/log-redact.ts +189 -0
- package/src/util/logger.ts +19 -17
- package/src/util/object.ts +3 -0
- package/src/util/platform.ts +105 -363
- package/src/util/pricing.ts +1 -1
- package/src/util/promise-guard.ts +1 -1
- package/src/util/retry.ts +19 -0
- package/src/util/row-mapper.ts +79 -0
- package/src/util/silently.ts +21 -0
- package/src/watcher/engine.ts +5 -1
- package/src/watcher/provider-types.ts +20 -0
- package/src/watcher/providers/github.ts +156 -0
- package/src/watcher/providers/gmail.ts +1 -0
- package/src/watcher/providers/google-calendar.ts +1 -0
- package/src/watcher/providers/linear.ts +460 -0
- package/src/watcher/providers/slack.ts +1 -0
- package/src/work-items/work-item-runner.ts +1 -1
- package/src/workspace/git-service.ts +1 -1
- package/src/workspace/provider-commit-message-generator.ts +51 -22
- package/src/__tests__/call-bridge.test.ts +0 -517
- package/src/__tests__/session-process-bridge.test.ts +0 -244
- package/src/calls/call-bridge.ts +0 -168
- package/src/config/vellum-skills/google-oauth-setup/SKILL.md +0 -199
|
@@ -0,0 +1,1418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Amazon REST API client.
|
|
3
|
+
*
|
|
4
|
+
* ARCHITECTURE
|
|
5
|
+
* ============
|
|
6
|
+
* All requests run inside a Chrome browser tab via CDP Runtime.evaluate(), NOT
|
|
7
|
+
* from Node.js directly. This means:
|
|
8
|
+
*
|
|
9
|
+
* 1. There are TWO DOMs in every function:
|
|
10
|
+
* - `document` = the browser's currently-rendered page (e.g. amazon.com homepage)
|
|
11
|
+
* - `doc` = a DOMParser-parsed document from a fetch() response (e.g. product page)
|
|
12
|
+
* CSRF tokens, offer IDs, and form fields must be extracted from `doc` (the fetched
|
|
13
|
+
* page), NOT from `document`. The browser's live page rarely has the data we need.
|
|
14
|
+
*
|
|
15
|
+
* 2. Session cookies live in the Chrome-CDP browser profile
|
|
16
|
+
* (~Library/Application Support/Google/Chrome-CDP). The session.json on disk is only
|
|
17
|
+
* used to validate that a session exists. Actual auth goes through the browser's cookies.
|
|
18
|
+
*
|
|
19
|
+
* AMAZON FRESH vs REGULAR CART
|
|
20
|
+
* ============================
|
|
21
|
+
* Fresh and regular Amazon use completely different cart APIs:
|
|
22
|
+
* - Fresh: POST /alm/addtofreshcart (JSON body)
|
|
23
|
+
* - Regular: POST /gp/add-to-cart/json (form-encoded body)
|
|
24
|
+
*
|
|
25
|
+
* Fresh cart POST requires ALL of these fields or it silently fails:
|
|
26
|
+
* - `offerListingDiscriminator` (short code like "A0P3", from escaped JSON in product HTML)
|
|
27
|
+
* - `offerListingID` (long URL-encoded hash, from escaped JSON in product HTML)
|
|
28
|
+
* - `anti-csrftoken-a2z` header (from <input> or <meta> in the FETCHED product page doc)
|
|
29
|
+
* - `csrfToken` in payload (from escaped JSON in product HTML)
|
|
30
|
+
*
|
|
31
|
+
* DEBUGGING
|
|
32
|
+
* =========
|
|
33
|
+
* Use `--verbose` on `cart add` to dump all extracted fields and raw responses.
|
|
34
|
+
* If a field shows "EMPTY", the product page format likely changed and the
|
|
35
|
+
* extraction regex needs updating. Check the escaped JSON patterns in the HTML:
|
|
36
|
+
* - Fields are typically in: \\"fieldName\\":\\"value\\" (backslash-escaped quotes)
|
|
37
|
+
* - Or HTML entities: "fieldName":"value"
|
|
38
|
+
*
|
|
39
|
+
* ERROR HANDLING
|
|
40
|
+
* ==============
|
|
41
|
+
* NEVER silently fall through to stale data. If a POST fails, throw an error with
|
|
42
|
+
* the extracted field values so the caller knows exactly what went wrong. The
|
|
43
|
+
* get-cart-items fallback endpoint returns whatever is already in the cart, NOT
|
|
44
|
+
* what was just added. Always validate the target ASIN is present before returning.
|
|
45
|
+
*
|
|
46
|
+
* runWithBackoff() retries on HTTP 403, but not all 403s are rate limits. A 403
|
|
47
|
+
* from /alm/addtofreshcart with "fakeOfferId" means the request payload was wrong,
|
|
48
|
+
* not that we're rate-limited. Check the response body before classifying the error.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
import {
|
|
52
|
+
loadSession,
|
|
53
|
+
type AmazonSession,
|
|
54
|
+
} from './session.js';
|
|
55
|
+
import type { ExtractedCredential } from '../tools/browser/network-recording-types.js';
|
|
56
|
+
import { extensionRelayServer } from '../browser-extension-relay/server.js';
|
|
57
|
+
import type { ExtensionCommand, ExtensionResponse } from '../browser-extension-relay/protocol.js';
|
|
58
|
+
import { readHttpToken } from '../util/platform.js';
|
|
59
|
+
import { getRuntimeHttpPort } from '../config/env.js';
|
|
60
|
+
|
|
61
|
+
const AMAZON_BASE = 'https://www.amazon.com';
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Relay command routing
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// When running inside the daemon, extensionRelayServer has a live WebSocket.
|
|
67
|
+
// When running out-of-process (CLI), the relay isn't available, so we fall
|
|
68
|
+
// back to the daemon's HTTP endpoint POST /v1/browser-relay/command.
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
async function sendRelayCommand(command: Record<string, unknown>): Promise<ExtensionResponse> {
|
|
72
|
+
// Try in-process relay first (works when running inside the daemon)
|
|
73
|
+
const status = extensionRelayServer.getStatus();
|
|
74
|
+
if (status.connected) {
|
|
75
|
+
return extensionRelayServer.sendCommand(command as Omit<ExtensionCommand, 'id'>);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Fall back to HTTP relay endpoint on the daemon
|
|
79
|
+
const token = readHttpToken();
|
|
80
|
+
if (!token) {
|
|
81
|
+
throw new Error('Browser extension relay is not connected and no HTTP token found. Is the daemon running?');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const port = getRuntimeHttpPort() ?? 7821;
|
|
85
|
+
const resp = await fetch(`http://127.0.0.1:${port}/v1/browser-relay/command`, {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: {
|
|
88
|
+
'Content-Type': 'application/json',
|
|
89
|
+
'Authorization': `Bearer ${token}`,
|
|
90
|
+
},
|
|
91
|
+
body: JSON.stringify(command),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (!resp.ok) {
|
|
95
|
+
const body = await resp.text();
|
|
96
|
+
throw new Error(`Relay HTTP command failed (${resp.status}): ${body}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return await resp.json() as ExtensionResponse;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Thrown when the session is missing or expired. The CLI handles this specially. */
|
|
103
|
+
export class SessionExpiredError extends Error {
|
|
104
|
+
constructor(reason: string) {
|
|
105
|
+
super(reason);
|
|
106
|
+
this.name = 'SessionExpiredError';
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Thrown when Amazon returns HTTP 403 (rate limited or bot detected). */
|
|
111
|
+
export class RateLimitError extends Error {
|
|
112
|
+
constructor(reason: string) {
|
|
113
|
+
super(reason);
|
|
114
|
+
this.name = 'RateLimitError';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function requireSession(): AmazonSession {
|
|
119
|
+
const session = loadSession();
|
|
120
|
+
if (!session) {
|
|
121
|
+
throw new SessionExpiredError('No Amazon session found.');
|
|
122
|
+
}
|
|
123
|
+
return session;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Prepare for an Amazon request: validate session, find a Chrome tab,
|
|
128
|
+
* and sync session cookies into the browser. Returns the tab ID.
|
|
129
|
+
*/
|
|
130
|
+
async function prepareRequest(): Promise<{ tabId: number; session: AmazonSession }> {
|
|
131
|
+
const session = requireSession();
|
|
132
|
+
const tabId = await findAmazonTab();
|
|
133
|
+
// Skip cookie sync — use Chrome's own live cookies instead of overwriting with stale CLI ones
|
|
134
|
+
// await syncCookiesToBrowser(session.cookies);
|
|
135
|
+
return { tabId, session };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Find a Chrome tab on amazon.com via the browser-relay extension.
|
|
140
|
+
* Opens a new Amazon tab if none is currently open.
|
|
141
|
+
*/
|
|
142
|
+
async function findAmazonTab(): Promise<number> {
|
|
143
|
+
const resp = await sendRelayCommand({ action: 'find_tab', url: '*://*.amazon.com/*' });
|
|
144
|
+
if (resp.success && resp.tabId !== undefined) {
|
|
145
|
+
return resp.tabId;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// No Amazon tab open — create one
|
|
149
|
+
const newTab = await sendRelayCommand({
|
|
150
|
+
action: 'new_tab',
|
|
151
|
+
url: 'https://www.amazon.com',
|
|
152
|
+
});
|
|
153
|
+
if (!newTab.success || newTab.tabId === undefined) {
|
|
154
|
+
throw new SessionExpiredError('Could not open an Amazon tab in Chrome.');
|
|
155
|
+
}
|
|
156
|
+
return newTab.tabId;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Inject saved session cookies into Chrome via the browser-relay extension.
|
|
161
|
+
* Uses chrome.cookies.set so fetch() calls in the tab context carry the session.
|
|
162
|
+
*/
|
|
163
|
+
let lastCookieSyncTime = 0;
|
|
164
|
+
const COOKIE_SYNC_INTERVAL = 60_000; // re-sync at most once per minute
|
|
165
|
+
|
|
166
|
+
async function _syncCookiesToBrowser(cookies: ExtractedCredential[]): Promise<void> {
|
|
167
|
+
const now = Date.now();
|
|
168
|
+
if (now - lastCookieSyncTime < COOKIE_SYNC_INTERVAL) return;
|
|
169
|
+
|
|
170
|
+
for (const cookie of cookies) {
|
|
171
|
+
const domain = cookie.domain || '.amazon.com';
|
|
172
|
+
const cleanDomain = domain.startsWith('.') ? domain.slice(1) : domain;
|
|
173
|
+
await extensionRelayServer.sendCommand({
|
|
174
|
+
action: 'set_cookie',
|
|
175
|
+
cookie: {
|
|
176
|
+
url: `https://${cleanDomain}`,
|
|
177
|
+
name: cookie.name,
|
|
178
|
+
value: cookie.value,
|
|
179
|
+
domain,
|
|
180
|
+
path: cookie.path || '/',
|
|
181
|
+
secure: cookie.secure ?? true,
|
|
182
|
+
httpOnly: cookie.httpOnly ?? false,
|
|
183
|
+
...(cookie.expires ? { expirationDate: cookie.expires } : {}),
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
lastCookieSyncTime = Date.now();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Execute a JavaScript expression inside a Chrome tab via the browser-relay extension.
|
|
193
|
+
* Drop-in replacement for the former CDP Runtime.evaluate path.
|
|
194
|
+
* Returns the JSON-parsed result value.
|
|
195
|
+
*/
|
|
196
|
+
async function cdpEval(tabId: number, script: string): Promise<unknown> {
|
|
197
|
+
let resp: ExtensionResponse;
|
|
198
|
+
try {
|
|
199
|
+
resp = await sendRelayCommand({ action: 'evaluate', tabId, code: script });
|
|
200
|
+
} catch (err) {
|
|
201
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
202
|
+
if (msg.includes('not connected')) {
|
|
203
|
+
throw new SessionExpiredError(
|
|
204
|
+
'Browser extension relay is not connected. Load the Vellum extension in Chrome.',
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
throw err;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!resp.success) {
|
|
211
|
+
throw new Error(`Browser eval failed: ${resp.error ?? 'unknown error'}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const value = resp.result;
|
|
215
|
+
if (value == null) {
|
|
216
|
+
throw new Error('Empty browser eval response');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
return typeof value === 'string' ? JSON.parse(value) : value;
|
|
221
|
+
} catch {
|
|
222
|
+
return value;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Handle the raw result object returned from cdpEval scripts.
|
|
228
|
+
* Throws appropriate errors for auth failures, rate limits, and other errors.
|
|
229
|
+
*/
|
|
230
|
+
function handleResult(result: Record<string, unknown>): void {
|
|
231
|
+
if (result.__error) {
|
|
232
|
+
if (result.__status === 401) {
|
|
233
|
+
throw new SessionExpiredError('Amazon session has expired.');
|
|
234
|
+
}
|
|
235
|
+
if (result.__status === 403) {
|
|
236
|
+
throw new RateLimitError('Amazon rate limit hit (HTTP 403).');
|
|
237
|
+
}
|
|
238
|
+
throw new Error(
|
|
239
|
+
(result.__message as string | undefined) ??
|
|
240
|
+
`Amazon request failed with status ${result.__status ?? 'unknown'}`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let lastRequestTime = 0;
|
|
246
|
+
|
|
247
|
+
async function runWithBackoff<T>(fn: () => Promise<T>): Promise<T> {
|
|
248
|
+
const backoffSchedule = [5000, 10000, 20000];
|
|
249
|
+
|
|
250
|
+
for (let attempt = 0; ; attempt++) {
|
|
251
|
+
// Inter-request delay
|
|
252
|
+
const now = Date.now();
|
|
253
|
+
const elapsed = now - lastRequestTime;
|
|
254
|
+
if (lastRequestTime > 0 && elapsed < 2000) {
|
|
255
|
+
await new Promise(r => setTimeout(r, 2000 - elapsed));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
lastRequestTime = Date.now();
|
|
260
|
+
return await fn();
|
|
261
|
+
} catch (err) {
|
|
262
|
+
if (err instanceof RateLimitError && attempt < backoffSchedule.length) {
|
|
263
|
+
const delay = backoffSchedule[attempt];
|
|
264
|
+
process.stderr.write(
|
|
265
|
+
`[amazon] Rate limited, retrying in ${delay / 1000}s... (attempt ${attempt + 1}/${backoffSchedule.length})\n`,
|
|
266
|
+
);
|
|
267
|
+
await new Promise(r => setTimeout(r, delay));
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
throw err;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
// Public types
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
export interface ProductSearchResult {
|
|
280
|
+
asin: string;
|
|
281
|
+
title: string;
|
|
282
|
+
price: string;
|
|
283
|
+
priceValue: number | null;
|
|
284
|
+
isPrime: boolean;
|
|
285
|
+
isFresh: boolean;
|
|
286
|
+
imageUrl?: string;
|
|
287
|
+
rating?: string;
|
|
288
|
+
reviewCount?: string;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export interface ProductVariation {
|
|
292
|
+
dimensionName: string;
|
|
293
|
+
value: string;
|
|
294
|
+
asin: string;
|
|
295
|
+
isAvailable: boolean;
|
|
296
|
+
priceValue: number | null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export interface ProductDetails {
|
|
300
|
+
asin: string;
|
|
301
|
+
parentAsin?: string;
|
|
302
|
+
title: string;
|
|
303
|
+
price: string;
|
|
304
|
+
priceValue: number | null;
|
|
305
|
+
variations: ProductVariation[];
|
|
306
|
+
isFresh: boolean;
|
|
307
|
+
imageUrl?: string;
|
|
308
|
+
rating?: string;
|
|
309
|
+
reviewCount?: string;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export interface CartItem {
|
|
313
|
+
cartItemId: string;
|
|
314
|
+
asin: string;
|
|
315
|
+
title: string;
|
|
316
|
+
quantity: number;
|
|
317
|
+
price: string;
|
|
318
|
+
isFresh: boolean;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export interface CartSummary {
|
|
322
|
+
items: CartItem[];
|
|
323
|
+
subtotal: string;
|
|
324
|
+
itemCount: number;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export interface DeliverySlot {
|
|
328
|
+
slotId: string;
|
|
329
|
+
date: string;
|
|
330
|
+
timeWindow: string;
|
|
331
|
+
price: string;
|
|
332
|
+
isAvailable: boolean;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export interface PaymentMethod {
|
|
336
|
+
paymentMethodId: string;
|
|
337
|
+
type: string;
|
|
338
|
+
last4: string;
|
|
339
|
+
isDefault: boolean;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export interface CheckoutSummary {
|
|
343
|
+
subtotal: string;
|
|
344
|
+
shipping: string;
|
|
345
|
+
tax: string;
|
|
346
|
+
total: string;
|
|
347
|
+
paymentMethods: PaymentMethod[];
|
|
348
|
+
deliveryDate?: string;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export interface PlaceOrderResult {
|
|
352
|
+
orderId: string;
|
|
353
|
+
estimatedDelivery?: string;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
// Public API
|
|
358
|
+
// ---------------------------------------------------------------------------
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Search Amazon for products.
|
|
362
|
+
* Use isFresh: true to search Amazon Fresh grocery items.
|
|
363
|
+
*/
|
|
364
|
+
export async function search(
|
|
365
|
+
query: string,
|
|
366
|
+
opts: { isFresh?: boolean; limit?: number } = {},
|
|
367
|
+
): Promise<ProductSearchResult[]> {
|
|
368
|
+
const { tabId } = await prepareRequest();
|
|
369
|
+
|
|
370
|
+
return runWithBackoff(async () => {
|
|
371
|
+
const url = opts.isFresh
|
|
372
|
+
? `${AMAZON_BASE}/s?k=${encodeURIComponent(query)}&i=fresh-foods`
|
|
373
|
+
: `${AMAZON_BASE}/s?k=${encodeURIComponent(query)}`;
|
|
374
|
+
const limit = opts.limit ?? 20;
|
|
375
|
+
const isFreshFlag = JSON.stringify(!!opts.isFresh);
|
|
376
|
+
|
|
377
|
+
const script = `
|
|
378
|
+
(async function() {
|
|
379
|
+
try {
|
|
380
|
+
var resp = await fetch(${JSON.stringify(url)}, { credentials: 'include' });
|
|
381
|
+
if (resp.status === 401) return JSON.stringify({ __status: 401, __error: true });
|
|
382
|
+
if (resp.status === 403) return JSON.stringify({ __status: 403, __error: true });
|
|
383
|
+
var html = await resp.text();
|
|
384
|
+
var parser = new DOMParser();
|
|
385
|
+
var doc = parser.parseFromString(html, 'text/html');
|
|
386
|
+
var results = [];
|
|
387
|
+
var cards = doc.querySelectorAll('[data-component-type="s-search-result"][data-asin]');
|
|
388
|
+
for (var i = 0; i < Math.min(cards.length, ${limit}); i++) {
|
|
389
|
+
var el = cards[i];
|
|
390
|
+
var asin = el.getAttribute('data-asin');
|
|
391
|
+
if (!asin || asin.length < 6) continue;
|
|
392
|
+
var titleEl = el.querySelector('h2 .a-text-normal') || el.querySelector('h2 a span') || el.querySelector('.s-title-instructions-style');
|
|
393
|
+
var priceEl = el.querySelector('.a-price .a-offscreen');
|
|
394
|
+
var imgEl = el.querySelector('img.s-image');
|
|
395
|
+
var ratingEl = el.querySelector('.a-icon-star-small .a-icon-alt') || el.querySelector('[aria-label*="stars"]');
|
|
396
|
+
var reviewEl = el.querySelector('[aria-label*="reviews"]') || el.querySelector('.s-underline-text');
|
|
397
|
+
var isPrime = !!el.querySelector('.a-icon-prime');
|
|
398
|
+
var isFreshEl = !!el.querySelector('[aria-label*="Fresh"]') || html.includes('amazon.com/fresh') && i < 5;
|
|
399
|
+
var priceText = priceEl ? priceEl.textContent.trim() : '';
|
|
400
|
+
var priceNum = priceText ? parseFloat(priceText.replace(/[^0-9.]/g, '')) : null;
|
|
401
|
+
results.push({
|
|
402
|
+
asin: asin,
|
|
403
|
+
title: titleEl ? titleEl.textContent.trim() : '',
|
|
404
|
+
price: priceText,
|
|
405
|
+
priceValue: isNaN(priceNum) ? null : priceNum,
|
|
406
|
+
isPrime: isPrime,
|
|
407
|
+
isFresh: ${isFreshFlag} || isFreshEl,
|
|
408
|
+
imageUrl: imgEl ? imgEl.getAttribute('src') : undefined,
|
|
409
|
+
rating: ratingEl ? ratingEl.getAttribute('aria-label') || ratingEl.textContent.trim() : undefined,
|
|
410
|
+
reviewCount: reviewEl ? reviewEl.textContent.trim() : undefined,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
return JSON.stringify({ __status: resp.status, __data: results });
|
|
414
|
+
} catch(e) {
|
|
415
|
+
return JSON.stringify({ __error: true, __message: e.message });
|
|
416
|
+
}
|
|
417
|
+
})()
|
|
418
|
+
`;
|
|
419
|
+
|
|
420
|
+
const result = await cdpEval(tabId, script) as Record<string, unknown>;
|
|
421
|
+
handleResult(result);
|
|
422
|
+
return result.__data as ProductSearchResult[];
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Get product details for a specific ASIN, including variations.
|
|
428
|
+
*/
|
|
429
|
+
export async function getProductDetails(
|
|
430
|
+
asin: string,
|
|
431
|
+
opts: { isFresh?: boolean } = {},
|
|
432
|
+
): Promise<ProductDetails> {
|
|
433
|
+
const { tabId } = await prepareRequest();
|
|
434
|
+
|
|
435
|
+
return runWithBackoff(async () => {
|
|
436
|
+
const url = `${AMAZON_BASE}/dp/${asin}`;
|
|
437
|
+
const isFreshFlag = JSON.stringify(!!opts.isFresh);
|
|
438
|
+
|
|
439
|
+
const script = `
|
|
440
|
+
(async function() {
|
|
441
|
+
try {
|
|
442
|
+
var resp = await fetch(${JSON.stringify(url)}, {
|
|
443
|
+
credentials: 'include',
|
|
444
|
+
headers: { 'Accept': 'text/html,application/xhtml+xml' }
|
|
445
|
+
});
|
|
446
|
+
if (resp.status === 401) return JSON.stringify({ __status: 401, __error: true });
|
|
447
|
+
if (resp.status === 403) return JSON.stringify({ __status: 403, __error: true });
|
|
448
|
+
var html = await resp.text();
|
|
449
|
+
var parser = new DOMParser();
|
|
450
|
+
var doc = parser.parseFromString(html, 'text/html');
|
|
451
|
+
|
|
452
|
+
// Title
|
|
453
|
+
var titleEl = doc.getElementById('productTitle') ||
|
|
454
|
+
doc.querySelector('.product-title-word-break') ||
|
|
455
|
+
doc.querySelector('h1#title');
|
|
456
|
+
var title = titleEl ? titleEl.textContent.trim() : '';
|
|
457
|
+
|
|
458
|
+
// Price
|
|
459
|
+
var priceEl = doc.querySelector('#priceblock_ourprice .a-offscreen') ||
|
|
460
|
+
doc.querySelector('#priceblock_dealprice .a-offscreen') ||
|
|
461
|
+
doc.querySelector('.a-price .a-offscreen') ||
|
|
462
|
+
doc.querySelector('#price_inside_buybox');
|
|
463
|
+
var priceText = priceEl ? priceEl.textContent.trim() : '';
|
|
464
|
+
var priceNum = priceText ? parseFloat(priceText.replace(/[^0-9.]/g, '')) : null;
|
|
465
|
+
|
|
466
|
+
// Image
|
|
467
|
+
var imgEl = doc.getElementById('landingImage') || doc.querySelector('#imgBlkFront');
|
|
468
|
+
var imageUrl = imgEl ? (imgEl.getAttribute('data-a-dynamic-image') ? Object.keys(JSON.parse(imgEl.getAttribute('data-a-dynamic-image') || '{}'))[0] : imgEl.getAttribute('src')) : undefined;
|
|
469
|
+
|
|
470
|
+
// Rating
|
|
471
|
+
var ratingEl = doc.querySelector('#acrPopover') || doc.querySelector('[data-hook="rating-out-of-text"]');
|
|
472
|
+
var rating = ratingEl ? ratingEl.getAttribute('title') || ratingEl.textContent.trim() : undefined;
|
|
473
|
+
|
|
474
|
+
var reviewEl = doc.getElementById('acrCustomerReviewText');
|
|
475
|
+
var reviewCount = reviewEl ? reviewEl.textContent.trim() : undefined;
|
|
476
|
+
|
|
477
|
+
// Parent ASIN (for variation child products)
|
|
478
|
+
var parentAsinEl = doc.querySelector('[data-asin]') || doc.querySelector('[name="ASIN"]');
|
|
479
|
+
var parentAsin = undefined;
|
|
480
|
+
var m = html.match(/"parentAsin"\s*:\s*"([A-Z0-9]+)"/);
|
|
481
|
+
if (m) parentAsin = m[1];
|
|
482
|
+
|
|
483
|
+
// Detect Fresh
|
|
484
|
+
var isFresh = ${isFreshFlag} || html.includes('amazon.com/fresh') || !!doc.querySelector('[aria-label*="Fresh"]');
|
|
485
|
+
|
|
486
|
+
// Variations — parse from inline JS objects
|
|
487
|
+
var variations = [];
|
|
488
|
+
try {
|
|
489
|
+
var dimMatch = html.match(/dimensionRelationshipsStr\s*=\s*'([^']+)'/);
|
|
490
|
+
if (!dimMatch) dimMatch = html.match(/"dimensionRelationshipsStr"\s*:\s*"([^"]+)"/);
|
|
491
|
+
if (dimMatch) {
|
|
492
|
+
var dimStr = dimMatch[1].replace(/\\\\/g, '\\\\').replace(/\\'/g, "'");
|
|
493
|
+
var dimData = JSON.parse(dimStr);
|
|
494
|
+
if (Array.isArray(dimData)) {
|
|
495
|
+
for (var i = 0; i < dimData.length; i++) {
|
|
496
|
+
var dimItem = dimData[i];
|
|
497
|
+
var attrs = dimItem.variationAttributes || [];
|
|
498
|
+
for (var j = 0; j < attrs.length; j++) {
|
|
499
|
+
variations.push({
|
|
500
|
+
dimensionName: attrs[j].variationName || '',
|
|
501
|
+
value: attrs[j].value || '',
|
|
502
|
+
asin: dimItem.asin || '',
|
|
503
|
+
isAvailable: dimItem.isPrime !== undefined ? true : !dimItem.unavailable,
|
|
504
|
+
priceValue: dimItem.price ? parseFloat(String(dimItem.price).replace(/[^0-9.]/g, '')) : null,
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
} catch(ve) { /* skip variation parsing errors */ }
|
|
511
|
+
|
|
512
|
+
// Fallback: look for asinVariationValues
|
|
513
|
+
if (variations.length === 0) {
|
|
514
|
+
try {
|
|
515
|
+
var asinVarMatch = html.match(/asinVariationValues\s*=\s*(\{[^;]+\})/);
|
|
516
|
+
if (asinVarMatch) {
|
|
517
|
+
var asinVarData = JSON.parse(asinVarMatch[1]);
|
|
518
|
+
var dims = Object.keys(asinVarData);
|
|
519
|
+
for (var di = 0; di < dims.length; di++) {
|
|
520
|
+
var dim = dims[di];
|
|
521
|
+
var asins = Object.keys(asinVarData[dim]);
|
|
522
|
+
for (var ai = 0; ai < asins.length; ai++) {
|
|
523
|
+
variations.push({
|
|
524
|
+
dimensionName: dim,
|
|
525
|
+
value: asinVarData[dim][asins[ai]],
|
|
526
|
+
asin: asins[ai],
|
|
527
|
+
isAvailable: true,
|
|
528
|
+
priceValue: null,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
} catch(ve2) { /* skip */ }
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return JSON.stringify({
|
|
537
|
+
__status: resp.status,
|
|
538
|
+
__data: {
|
|
539
|
+
asin: ${JSON.stringify(asin)},
|
|
540
|
+
parentAsin: parentAsin,
|
|
541
|
+
title: title,
|
|
542
|
+
price: priceText,
|
|
543
|
+
priceValue: isNaN(priceNum) ? null : priceNum,
|
|
544
|
+
variations: variations,
|
|
545
|
+
isFresh: isFresh,
|
|
546
|
+
imageUrl: imageUrl,
|
|
547
|
+
rating: rating,
|
|
548
|
+
reviewCount: reviewCount,
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
} catch(e) {
|
|
552
|
+
return JSON.stringify({ __error: true, __message: e.message });
|
|
553
|
+
}
|
|
554
|
+
})()
|
|
555
|
+
`;
|
|
556
|
+
|
|
557
|
+
const result = await cdpEval(tabId, script) as Record<string, unknown>;
|
|
558
|
+
handleResult(result);
|
|
559
|
+
return result.__data as ProductDetails;
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Add an item to the Amazon cart.
|
|
565
|
+
* First fetches the product page to extract the offerListingID required by Amazon.
|
|
566
|
+
*/
|
|
567
|
+
export async function addToCart(opts: {
|
|
568
|
+
asin: string;
|
|
569
|
+
quantity?: number;
|
|
570
|
+
isFresh?: boolean;
|
|
571
|
+
verbose?: boolean;
|
|
572
|
+
}): Promise<CartSummary> {
|
|
573
|
+
const { tabId } = await prepareRequest();
|
|
574
|
+
const quantity = opts.quantity ?? 1;
|
|
575
|
+
const productUrl = `${AMAZON_BASE}/dp/${opts.asin}`;
|
|
576
|
+
|
|
577
|
+
// ─── Non-Fresh: navigate + click approach ───────────────────────────
|
|
578
|
+
// Amazon's handle-buy-box endpoint rejects fetch() requests (returns 404)
|
|
579
|
+
// because it checks Sec-Fetch-* headers that only real browser form
|
|
580
|
+
// submissions provide. So for non-Fresh items we navigate the actual
|
|
581
|
+
// Chrome tab to the product page and click the Add to Cart button.
|
|
582
|
+
if (!opts.isFresh) {
|
|
583
|
+
return runWithBackoff(async () => {
|
|
584
|
+
// Step 1: Navigate to the product page
|
|
585
|
+
await sendRelayCommand({ action: 'navigate', tabId, url: productUrl });
|
|
586
|
+
|
|
587
|
+
// Step 2: Wait for the page to load and the Add to Cart button to appear
|
|
588
|
+
// Poll up to 10 seconds for the button
|
|
589
|
+
let buttonClicked = false;
|
|
590
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
591
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
592
|
+
const clickResult = await cdpEval(tabId, `
|
|
593
|
+
(function() {
|
|
594
|
+
try {
|
|
595
|
+
// Check if we're on the right product page
|
|
596
|
+
var titleEl = document.querySelector('#productTitle');
|
|
597
|
+
if (!titleEl) return JSON.stringify({ ready: false, reason: 'no product title yet' });
|
|
598
|
+
|
|
599
|
+
// Set quantity if needed
|
|
600
|
+
${quantity > 1 ? `
|
|
601
|
+
var qtySelect = document.querySelector('#quantity');
|
|
602
|
+
if (qtySelect) {
|
|
603
|
+
qtySelect.value = '${quantity}';
|
|
604
|
+
qtySelect.dispatchEvent(new Event('change', { bubbles: true }));
|
|
605
|
+
}
|
|
606
|
+
` : ''}
|
|
607
|
+
|
|
608
|
+
// Find and click the Add to Cart button
|
|
609
|
+
var btn = document.querySelector('#add-to-cart-button')
|
|
610
|
+
|| document.querySelector('input[name="submit.add-to-cart"]')
|
|
611
|
+
|| document.querySelector('#submit\\.add-to-cart');
|
|
612
|
+
if (!btn) return JSON.stringify({ ready: true, clicked: false, reason: 'no add-to-cart button found' });
|
|
613
|
+
|
|
614
|
+
btn.click();
|
|
615
|
+
return JSON.stringify({ ready: true, clicked: true, buttonId: btn.id || btn.name });
|
|
616
|
+
} catch(e) {
|
|
617
|
+
return JSON.stringify({ ready: false, reason: e.message });
|
|
618
|
+
}
|
|
619
|
+
})()
|
|
620
|
+
`) as Record<string, unknown>;
|
|
621
|
+
|
|
622
|
+
if (clickResult && clickResult.clicked) {
|
|
623
|
+
buttonClicked = true;
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (!buttonClicked) {
|
|
629
|
+
throw new Error('Could not find or click the Add to Cart button on the product page after 10 seconds.');
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Step 3: Wait for the cart confirmation page to load and extract cart info
|
|
633
|
+
// Poll up to 8 seconds for the confirmation
|
|
634
|
+
await new Promise(r => setTimeout(r, 2000)); // initial wait for navigation
|
|
635
|
+
let cartData: Record<string, unknown> | null = null;
|
|
636
|
+
for (let attempt = 0; attempt < 6; attempt++) {
|
|
637
|
+
const confirmResult = await cdpEval(tabId, `
|
|
638
|
+
(function() {
|
|
639
|
+
try {
|
|
640
|
+
var title = document.title || '';
|
|
641
|
+
// Check for cart/confirmation page indicators
|
|
642
|
+
var confirmEl = document.querySelector('#huc-v2-order-row-confirm-text')
|
|
643
|
+
|| document.querySelector('#NATC_SMART_WAGON_CONF_MSG_SUCCESS')
|
|
644
|
+
|| document.querySelector('#sw-atc-confirmation')
|
|
645
|
+
|| document.querySelector('.a-alert-heading');
|
|
646
|
+
var isCartPage = title.toLowerCase().indexOf('cart') !== -1
|
|
647
|
+
|| title.toLowerCase().indexOf('added') !== -1;
|
|
648
|
+
var confirmText = confirmEl ? confirmEl.textContent.trim().substring(0, 100) : '';
|
|
649
|
+
var isConfirmed = confirmText.toLowerCase().indexOf('added') !== -1 || isCartPage;
|
|
650
|
+
|
|
651
|
+
if (!isConfirmed) return JSON.stringify({ confirmed: false, pageTitle: title });
|
|
652
|
+
|
|
653
|
+
// Extract cart count
|
|
654
|
+
var cartCountEl = document.querySelector('#nav-cart-count');
|
|
655
|
+
var cartCount = cartCountEl ? cartCountEl.textContent.trim() : '0';
|
|
656
|
+
|
|
657
|
+
// Extract subtotal if visible
|
|
658
|
+
var subtotalEl = document.querySelector('#sc-subtotal-amount-activecart')
|
|
659
|
+
|| document.querySelector('.a-text-bold .sc-price');
|
|
660
|
+
var subtotal = subtotalEl ? subtotalEl.textContent.trim() : '';
|
|
661
|
+
|
|
662
|
+
return JSON.stringify({
|
|
663
|
+
confirmed: true,
|
|
664
|
+
pageTitle: title,
|
|
665
|
+
confirmText: confirmText.substring(0, 50),
|
|
666
|
+
cartCount: cartCount,
|
|
667
|
+
subtotal: subtotal,
|
|
668
|
+
});
|
|
669
|
+
} catch(e) {
|
|
670
|
+
return JSON.stringify({ confirmed: false, reason: e.message });
|
|
671
|
+
}
|
|
672
|
+
})()
|
|
673
|
+
`) as Record<string, unknown>;
|
|
674
|
+
|
|
675
|
+
if (confirmResult && confirmResult.confirmed) {
|
|
676
|
+
cartData = confirmResult;
|
|
677
|
+
break;
|
|
678
|
+
}
|
|
679
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Build the cart summary
|
|
683
|
+
const items = [{
|
|
684
|
+
cartItemId: opts.asin,
|
|
685
|
+
asin: opts.asin,
|
|
686
|
+
title: '',
|
|
687
|
+
quantity: quantity,
|
|
688
|
+
price: '',
|
|
689
|
+
isFresh: false,
|
|
690
|
+
}];
|
|
691
|
+
|
|
692
|
+
const cart: CartSummary & { __debug?: unknown; __verbose?: unknown } = {
|
|
693
|
+
items,
|
|
694
|
+
subtotal: (cartData?.subtotal as string) || '',
|
|
695
|
+
itemCount: parseInt((cartData?.cartCount as string) || '0', 10) || items.length,
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
if (!cartData?.confirmed) {
|
|
699
|
+
// Button was clicked but we couldn't confirm. It likely still worked
|
|
700
|
+
// (Amazon sometimes shows interstitials). Return optimistic result.
|
|
701
|
+
cart.__debug = { warning: 'Could not confirm cart page, but button click succeeded.' };
|
|
702
|
+
} else {
|
|
703
|
+
cart.__debug = {
|
|
704
|
+
confirmText: cartData.confirmText,
|
|
705
|
+
cartCount: cartData.cartCount,
|
|
706
|
+
pageTitle: cartData.pageTitle,
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return cart;
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// ─── Fresh items: fetch-based approach (works fine) ─────────────────
|
|
715
|
+
return runWithBackoff(async () => {
|
|
716
|
+
const script = `
|
|
717
|
+
(async function() {
|
|
718
|
+
try {
|
|
719
|
+
// Fetch the product page to extract Fresh-specific payload
|
|
720
|
+
var dpResp = await fetch(${JSON.stringify(productUrl)}, {
|
|
721
|
+
credentials: 'include',
|
|
722
|
+
headers: { 'Accept': 'text/html,application/xhtml+xml' }
|
|
723
|
+
});
|
|
724
|
+
if (dpResp.status === 401) return JSON.stringify({ __status: 401, __error: true });
|
|
725
|
+
if (dpResp.status === 403) return JSON.stringify({ __status: 403, __error: true });
|
|
726
|
+
|
|
727
|
+
var html = await dpResp.text();
|
|
728
|
+
var parser = new DOMParser();
|
|
729
|
+
var doc = parser.parseFromString(html, 'text/html');
|
|
730
|
+
|
|
731
|
+
// Extract anti-CSRF token
|
|
732
|
+
var antiCsrf = '';
|
|
733
|
+
var csrfMeta = doc.querySelector('meta[name="anti-csrftoken-a2z"]');
|
|
734
|
+
if (csrfMeta && csrfMeta.content) {
|
|
735
|
+
antiCsrf = csrfMeta.content;
|
|
736
|
+
} else {
|
|
737
|
+
var csrfInp = doc.querySelector('input[name="anti-csrftoken-a2z"]');
|
|
738
|
+
if (csrfInp && csrfInp.value) { antiCsrf = csrfInp.value; }
|
|
739
|
+
else { var m = document.cookie.match(/anti-csrftoken-a2z=([^;]+)/); if (m) antiCsrf = decodeURIComponent(m[1]); }
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Extract csrfToken from product page
|
|
743
|
+
var csrfInput = doc.querySelector('input[name="csrfToken"]');
|
|
744
|
+
var csrfToken = csrfInput ? csrfInput.value : '';
|
|
745
|
+
if (!csrfToken) {
|
|
746
|
+
var csrfMatch = html.match(/"csrfToken"\\s*:\\s*"([^"\\\\]+)"/);
|
|
747
|
+
if (csrfMatch) csrfToken = csrfMatch[1];
|
|
748
|
+
}
|
|
749
|
+
if (!csrfToken) { var ck = document.cookie.match(/csrf-main=([^;]+)/); if (ck) csrfToken = decodeURIComponent(ck[1]); }
|
|
750
|
+
|
|
751
|
+
// Fresh add-to-cart: extract the EXACT payload Amazon embeds in the
|
|
752
|
+
// data-fresh-add-to-cart attribute on the product page.
|
|
753
|
+
var freshAtcEl = doc.querySelector('[data-action="fresh-add-to-cart"]');
|
|
754
|
+
var freshPayload;
|
|
755
|
+
if (freshAtcEl && freshAtcEl.getAttribute('data-fresh-add-to-cart')) {
|
|
756
|
+
freshPayload = JSON.parse(freshAtcEl.getAttribute('data-fresh-add-to-cart'));
|
|
757
|
+
freshPayload.qsUID = 'atfc-' + (freshPayload.clientID || 'fresh-dp') + '-' + Date.now();
|
|
758
|
+
freshPayload.prevSelectedQty = 0;
|
|
759
|
+
freshPayload.isStepperFlag = false;
|
|
760
|
+
freshPayload.setQuantityFlag = false;
|
|
761
|
+
freshPayload.quantityData = {
|
|
762
|
+
quantity: String(${quantity}),
|
|
763
|
+
quantitySuffix: '',
|
|
764
|
+
price: '',
|
|
765
|
+
renderableSellingQuantity: String(${quantity}),
|
|
766
|
+
};
|
|
767
|
+
freshPayload.sellingUnit = freshPayload.sellingUnit || 'units';
|
|
768
|
+
freshPayload.sellingDimension = freshPayload.sellingDimension || 'count';
|
|
769
|
+
} else {
|
|
770
|
+
var discMatch = html.match(/\\"offerListingDiscriminator\\":\\"([^\\"]+)\\"/)
|
|
771
|
+
|| html.match(/"offerListingDiscriminator":"([^&]+)"/);
|
|
772
|
+
var offerDiscriminator = discMatch ? discMatch[1] : '';
|
|
773
|
+
var freshOfferIdMatch = html.match(/\\"offerListingID\\":\\"([^\\"]+)\\"/)
|
|
774
|
+
|| html.match(/"offerListingID":"([^&]+)"/);
|
|
775
|
+
var freshOfferListingID = freshOfferIdMatch ? freshOfferIdMatch[1] : '';
|
|
776
|
+
var sessionMatch = document.cookie.match(/session-id=([^;]+)/);
|
|
777
|
+
var sessionId = sessionMatch ? decodeURIComponent(sessionMatch[1]) : '';
|
|
778
|
+
|
|
779
|
+
freshPayload = {
|
|
780
|
+
qsUID: 'atfc-alm-mod-dp-' + Date.now(),
|
|
781
|
+
prevSelectedQty: 0,
|
|
782
|
+
isStepperFlag: false,
|
|
783
|
+
setQuantityFlag: false,
|
|
784
|
+
quantityData: {
|
|
785
|
+
quantity: String(${quantity}),
|
|
786
|
+
quantitySuffix: '',
|
|
787
|
+
price: '',
|
|
788
|
+
renderableSellingQuantity: String(${quantity}),
|
|
789
|
+
},
|
|
790
|
+
sellingUnit: 'units',
|
|
791
|
+
sellingDimension: 'count',
|
|
792
|
+
reftag: 'alm-dp-atc-so-fs',
|
|
793
|
+
csrfToken: csrfToken,
|
|
794
|
+
clientID: 'alm-mod-dp',
|
|
795
|
+
isItemSoldByCount: 'true',
|
|
796
|
+
brandId: 'QW1hem9uIEZyZXNo',
|
|
797
|
+
asin: ${JSON.stringify(opts.asin)},
|
|
798
|
+
sessionID: sessionId,
|
|
799
|
+
storeId: 'dc6d4e0d03d7c0a581c85a754396fe17eb8e54f3',
|
|
800
|
+
promotionId: 'any',
|
|
801
|
+
};
|
|
802
|
+
if (offerDiscriminator) freshPayload.offerListingDiscriminator = offerDiscriminator;
|
|
803
|
+
if (freshOfferListingID) freshPayload.offerListingID = freshOfferListingID;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
var freshReftag = freshPayload.reftag || 'alm-dp-atc-so-fs';
|
|
807
|
+
var addResp = await fetch('${AMAZON_BASE}/alm/addtofreshcart?ref_=' + freshReftag + '&discoveredAsins.0=' + encodeURIComponent(${JSON.stringify(opts.asin)}) + '&almBrandId=QW1hem9uIEZyZXNo', {
|
|
808
|
+
method: 'POST',
|
|
809
|
+
headers: {
|
|
810
|
+
'Content-Type': 'application/json',
|
|
811
|
+
'Accept': 'application/json, */*',
|
|
812
|
+
'anti-csrftoken-a2z': antiCsrf,
|
|
813
|
+
},
|
|
814
|
+
body: JSON.stringify(freshPayload),
|
|
815
|
+
credentials: 'include',
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
if (addResp.status === 401) return JSON.stringify({ __status: 401, __error: true });
|
|
819
|
+
if (addResp.status === 403) return JSON.stringify({ __status: 403, __error: true });
|
|
820
|
+
|
|
821
|
+
var addText = await addResp.text();
|
|
822
|
+
var addOk = addResp.ok;
|
|
823
|
+
var cartJson = null;
|
|
824
|
+
try { cartJson = JSON.parse(addText); } catch(e) {}
|
|
825
|
+
|
|
826
|
+
var items = [];
|
|
827
|
+
if (cartJson && cartJson.clientResponseModel && Array.isArray(cartJson.clientResponseModel.items)) {
|
|
828
|
+
items = cartJson.clientResponseModel.items.map(function(item) {
|
|
829
|
+
return {
|
|
830
|
+
cartItemId: item.itemId || item.ASIN || '',
|
|
831
|
+
asin: item.ASIN || '',
|
|
832
|
+
title: '',
|
|
833
|
+
quantity: item.quantity || 1,
|
|
834
|
+
price: '',
|
|
835
|
+
isFresh: true,
|
|
836
|
+
};
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Fresh fallback: get-cart-items
|
|
841
|
+
if (items.length === 0) {
|
|
842
|
+
try {
|
|
843
|
+
var freshCartResp = await fetch('${AMAZON_BASE}/cart/add-to-cart/get-cart-items?clientName=SiteWideActionExecutor&_=' + Date.now(), {
|
|
844
|
+
credentials: 'include',
|
|
845
|
+
headers: { 'Accept': 'application/json, */*' }
|
|
846
|
+
});
|
|
847
|
+
if (freshCartResp.ok) {
|
|
848
|
+
var freshCartData = await freshCartResp.json();
|
|
849
|
+
if (Array.isArray(freshCartData)) {
|
|
850
|
+
var allFreshItems = freshCartData.filter(function(i) { return i.cartType === 'LOCAL_MARKET'; });
|
|
851
|
+
var targetFound = allFreshItems.some(function(i) { return i.asin === ${JSON.stringify(opts.asin)}; });
|
|
852
|
+
if (!targetFound) {
|
|
853
|
+
return JSON.stringify({
|
|
854
|
+
__error: true,
|
|
855
|
+
__message: 'Add-to-cart failed: ASIN ' + ${JSON.stringify(opts.asin)} + ' was not found in the Fresh cart after adding. The item may be unavailable or the session cookies may be stale. Try running vellum amazon refresh.'
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
items = allFreshItems.map(function(item) {
|
|
859
|
+
return { cartItemId: item.asin || '', asin: item.asin || '', title: '', quantity: item.quantity || 1, price: '', isFresh: true };
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
} catch(fe) { /* ignore */ }
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
return JSON.stringify({
|
|
867
|
+
__status: addResp.status,
|
|
868
|
+
__ok: addOk,
|
|
869
|
+
__data: { items: items, subtotal: '', itemCount: items.length },
|
|
870
|
+
__addCartJson: cartJson ? JSON.stringify(cartJson).substring(0, 500) : null,
|
|
871
|
+
});
|
|
872
|
+
} catch(e) {
|
|
873
|
+
return JSON.stringify({ __error: true, __message: e.message });
|
|
874
|
+
}
|
|
875
|
+
})()
|
|
876
|
+
`;
|
|
877
|
+
|
|
878
|
+
const result = await cdpEval(tabId, script) as Record<string, unknown>;
|
|
879
|
+
handleResult(result);
|
|
880
|
+
|
|
881
|
+
const cart = result.__data as CartSummary & { __debug?: unknown };
|
|
882
|
+
|
|
883
|
+
if (result.__ok === false) {
|
|
884
|
+
const rawSnippet = result.__addCartJson ? ` | raw: ${(result.__addCartJson as string).substring(0, 150)}` : '';
|
|
885
|
+
throw new Error(`Fresh add-to-cart POST failed (status=${result.__status}${rawSnippet}).`);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
cart.__debug = {
|
|
889
|
+
addCartJson: result.__addCartJson,
|
|
890
|
+
httpStatus: result.__status,
|
|
891
|
+
httpOk: result.__ok,
|
|
892
|
+
};
|
|
893
|
+
return cart;
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Remove an item from the Amazon cart by cart item ID.
|
|
899
|
+
*/
|
|
900
|
+
export async function removeFromCart(opts: {
|
|
901
|
+
cartItemId: string;
|
|
902
|
+
}): Promise<CartSummary> {
|
|
903
|
+
const { tabId } = await prepareRequest();
|
|
904
|
+
|
|
905
|
+
return runWithBackoff(async () => {
|
|
906
|
+
const url = `${AMAZON_BASE}/gp/cart/view.html`;
|
|
907
|
+
const body = `cartItemId.${opts.cartItemId}=${opts.cartItemId}&quantity.${opts.cartItemId}=0&submit.delete.${opts.cartItemId}=Delete&ie=UTF8&action=delete`;
|
|
908
|
+
|
|
909
|
+
const script = `
|
|
910
|
+
(async function() {
|
|
911
|
+
try {
|
|
912
|
+
var antiCsrf = '';
|
|
913
|
+
var csrfMeta = document.querySelector('meta[name="anti-csrftoken-a2z"]');
|
|
914
|
+
if (csrfMeta && csrfMeta.content) {
|
|
915
|
+
antiCsrf = csrfMeta.content;
|
|
916
|
+
} else {
|
|
917
|
+
var csrfInp = document.querySelector('input[name="anti-csrftoken-a2z"]');
|
|
918
|
+
if (csrfInp && csrfInp.value) { antiCsrf = csrfInp.value; }
|
|
919
|
+
else { var m = document.cookie.match(/anti-csrftoken-a2z=([^;]+)/); if (m) antiCsrf = decodeURIComponent(m[1]); }
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
var resp = await fetch(${JSON.stringify(url)}, {
|
|
923
|
+
method: 'POST',
|
|
924
|
+
headers: {
|
|
925
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
926
|
+
'anti-csrftoken-a2z': antiCsrf,
|
|
927
|
+
},
|
|
928
|
+
body: ${JSON.stringify(body)},
|
|
929
|
+
credentials: 'include',
|
|
930
|
+
});
|
|
931
|
+
if (resp.status === 401) return JSON.stringify({ __status: 401, __error: true });
|
|
932
|
+
if (resp.status === 403) return JSON.stringify({ __status: 403, __error: true });
|
|
933
|
+
return JSON.stringify({ __status: resp.status, __ok: resp.ok });
|
|
934
|
+
} catch(e) {
|
|
935
|
+
return JSON.stringify({ __error: true, __message: e.message });
|
|
936
|
+
}
|
|
937
|
+
})()
|
|
938
|
+
`;
|
|
939
|
+
|
|
940
|
+
const result = await cdpEval(tabId, script) as Record<string, unknown>;
|
|
941
|
+
handleResult(result);
|
|
942
|
+
return viewCart();
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* View the current Amazon cart contents.
|
|
948
|
+
*/
|
|
949
|
+
export async function viewCart(): Promise<CartSummary> {
|
|
950
|
+
const { tabId } = await prepareRequest();
|
|
951
|
+
|
|
952
|
+
return runWithBackoff(async () => {
|
|
953
|
+
// Combine two sources:
|
|
954
|
+
// 1. /gp/cart/view.html HTML page — regular Amazon items
|
|
955
|
+
// 2. get-cart-items JSON endpoint — Fresh (LOCAL_MARKET) items only
|
|
956
|
+
const script = `
|
|
957
|
+
(async function() {
|
|
958
|
+
try {
|
|
959
|
+
var items = [];
|
|
960
|
+
var subtotalText = '';
|
|
961
|
+
|
|
962
|
+
// --- Regular cart: parse /gp/cart/view.html HTML ---
|
|
963
|
+
try {
|
|
964
|
+
var cartResp = await fetch('${AMAZON_BASE}/gp/cart/view.html', {
|
|
965
|
+
credentials: 'include',
|
|
966
|
+
headers: { 'Accept': 'text/html,application/xhtml+xml,*/*' }
|
|
967
|
+
});
|
|
968
|
+
if (cartResp.status === 401) return JSON.stringify({ __status: 401, __error: true });
|
|
969
|
+
if (cartResp.status === 403) return JSON.stringify({ __status: 403, __error: true });
|
|
970
|
+
if (cartResp.ok) {
|
|
971
|
+
var cartHtml = await cartResp.text();
|
|
972
|
+
var parser = new DOMParser();
|
|
973
|
+
var doc = parser.parseFromString(cartHtml, 'text/html');
|
|
974
|
+
// Each cart item row has data-asin and a quantity input
|
|
975
|
+
doc.querySelectorAll('[data-asin]').forEach(function(el) {
|
|
976
|
+
var asin = el.getAttribute('data-asin');
|
|
977
|
+
if (!asin || asin.length < 6) return;
|
|
978
|
+
var qtyInput = el.querySelector('[name^="quantity."]') || el.querySelector('input[type="text"]');
|
|
979
|
+
var qty = qtyInput ? (parseInt(qtyInput.value, 10) || 1) : 1;
|
|
980
|
+
var titleEl = el.querySelector('.a-truncate-full') || el.querySelector('.sc-product-title') || el.querySelector('[class*="product-title"]');
|
|
981
|
+
var priceEl = el.querySelector('.a-price .a-offscreen') || el.querySelector('[class*="price"]');
|
|
982
|
+
var cartItemIdMatch = (el.innerHTML || '').match(/cartItemId[=\\s:"]+([A-Z0-9]+)/i);
|
|
983
|
+
var cartItemId = cartItemIdMatch ? cartItemIdMatch[1] : asin;
|
|
984
|
+
items.push({
|
|
985
|
+
cartItemId: cartItemId,
|
|
986
|
+
asin: asin,
|
|
987
|
+
title: titleEl ? titleEl.textContent.trim() : '',
|
|
988
|
+
quantity: qty,
|
|
989
|
+
price: priceEl ? priceEl.textContent.trim() : '',
|
|
990
|
+
isFresh: false,
|
|
991
|
+
});
|
|
992
|
+
});
|
|
993
|
+
// Subtotal
|
|
994
|
+
var subtotalEl = doc.querySelector('#sc-subtotal-amount-activecart .a-price .a-offscreen') ||
|
|
995
|
+
doc.querySelector('[id*="subtotal"] .a-price .a-offscreen') ||
|
|
996
|
+
doc.querySelector('.sc-price-sign');
|
|
997
|
+
if (subtotalEl) subtotalText = subtotalEl.textContent.trim();
|
|
998
|
+
}
|
|
999
|
+
} catch(re) { /* ignore regular cart errors, still try Fresh */ }
|
|
1000
|
+
|
|
1001
|
+
// --- Fresh cart: get-cart-items JSON endpoint (LOCAL_MARKET only) ---
|
|
1002
|
+
try {
|
|
1003
|
+
var freshResp = await fetch('${AMAZON_BASE}/cart/add-to-cart/get-cart-items?clientName=SiteWideActionExecutor&_=' + Date.now(), {
|
|
1004
|
+
credentials: 'include',
|
|
1005
|
+
headers: { 'Accept': 'application/json, */*' }
|
|
1006
|
+
});
|
|
1007
|
+
if (freshResp.ok) {
|
|
1008
|
+
var freshData = await freshResp.json();
|
|
1009
|
+
if (Array.isArray(freshData)) {
|
|
1010
|
+
freshData.forEach(function(item) {
|
|
1011
|
+
if (item.cartType === 'LOCAL_MARKET') {
|
|
1012
|
+
items.push({
|
|
1013
|
+
cartItemId: item.asin || '',
|
|
1014
|
+
asin: item.asin || '',
|
|
1015
|
+
title: '',
|
|
1016
|
+
quantity: item.quantity || 1,
|
|
1017
|
+
price: '',
|
|
1018
|
+
isFresh: true,
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
} catch(fe) { /* ignore Fresh cart errors */ }
|
|
1025
|
+
|
|
1026
|
+
return JSON.stringify({
|
|
1027
|
+
__status: 200,
|
|
1028
|
+
__data: { items: items, subtotal: subtotalText, itemCount: items.length }
|
|
1029
|
+
});
|
|
1030
|
+
} catch(e) {
|
|
1031
|
+
return JSON.stringify({ __error: true, __message: e.message });
|
|
1032
|
+
}
|
|
1033
|
+
})()
|
|
1034
|
+
`;
|
|
1035
|
+
|
|
1036
|
+
const result = await cdpEval(tabId, script) as Record<string, unknown>;
|
|
1037
|
+
handleResult(result);
|
|
1038
|
+
return result.__data as CartSummary;
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/**
|
|
1043
|
+
* Get available Amazon Fresh delivery slots.
|
|
1044
|
+
*/
|
|
1045
|
+
export async function getFreshDeliverySlots(): Promise<DeliverySlot[]> {
|
|
1046
|
+
const { tabId } = await prepareRequest();
|
|
1047
|
+
|
|
1048
|
+
return runWithBackoff(async () => {
|
|
1049
|
+
// Amazon Fresh delivery windows API
|
|
1050
|
+
const url = `${AMAZON_BASE}/fresh/deliverywindows`;
|
|
1051
|
+
|
|
1052
|
+
const script = `
|
|
1053
|
+
(async function() {
|
|
1054
|
+
try {
|
|
1055
|
+
var resp = await fetch(${JSON.stringify(url)}, {
|
|
1056
|
+
credentials: 'include',
|
|
1057
|
+
headers: {
|
|
1058
|
+
'Accept': 'application/json, text/html,*/*',
|
|
1059
|
+
'x-requested-with': 'XMLHttpRequest',
|
|
1060
|
+
}
|
|
1061
|
+
});
|
|
1062
|
+
if (resp.status === 401) return JSON.stringify({ __status: 401, __error: true });
|
|
1063
|
+
if (resp.status === 403) return JSON.stringify({ __status: 403, __error: true });
|
|
1064
|
+
var text = await resp.text();
|
|
1065
|
+
|
|
1066
|
+
var slots = [];
|
|
1067
|
+
try {
|
|
1068
|
+
var data = JSON.parse(text);
|
|
1069
|
+
// Normalize various possible response shapes
|
|
1070
|
+
var windows = data.deliveryWindows || data.windows || data.slots || (Array.isArray(data) ? data : []);
|
|
1071
|
+
windows.forEach(function(w) {
|
|
1072
|
+
slots.push({
|
|
1073
|
+
slotId: w.windowId || w.slotId || w.id || '',
|
|
1074
|
+
date: w.date || w.windowStartDate || (w.windowStartDateTimeUtc || '').split('T')[0] || '',
|
|
1075
|
+
timeWindow: w.timeWindow || w.displayString || (w.windowStartDateTimeUtc && w.windowEndDateTimeUtc
|
|
1076
|
+
? w.windowStartDateTimeUtc.split('T')[1].substring(0,5) + ' - ' + w.windowEndDateTimeUtc.split('T')[1].substring(0,5)
|
|
1077
|
+
: ''),
|
|
1078
|
+
price: (w.price && w.price.localizedDisplayString) ? w.price.localizedDisplayString
|
|
1079
|
+
: (w.deliveryFee || w.fee || 'FREE'),
|
|
1080
|
+
isAvailable: w.isAvailable !== false && !w.isFull,
|
|
1081
|
+
});
|
|
1082
|
+
});
|
|
1083
|
+
} catch(pe) {
|
|
1084
|
+
// HTML response — parse from page
|
|
1085
|
+
var parser = new DOMParser();
|
|
1086
|
+
var doc = parser.parseFromString(text, 'text/html');
|
|
1087
|
+
doc.querySelectorAll('[data-slot-id], [data-window-id]').forEach(function(el) {
|
|
1088
|
+
slots.push({
|
|
1089
|
+
slotId: el.getAttribute('data-slot-id') || el.getAttribute('data-window-id') || '',
|
|
1090
|
+
date: el.getAttribute('data-date') || '',
|
|
1091
|
+
timeWindow: el.getAttribute('data-time-window') || el.textContent.trim(),
|
|
1092
|
+
price: (el.querySelector('.a-price') || el.querySelector('.slot-price') || {textContent: 'FREE'}).textContent.trim(),
|
|
1093
|
+
isAvailable: !el.classList.contains('unavailable') && !el.hasAttribute('disabled'),
|
|
1094
|
+
});
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
return JSON.stringify({ __status: resp.status, __data: slots });
|
|
1099
|
+
} catch(e) {
|
|
1100
|
+
return JSON.stringify({ __error: true, __message: e.message });
|
|
1101
|
+
}
|
|
1102
|
+
})()
|
|
1103
|
+
`;
|
|
1104
|
+
|
|
1105
|
+
const result = await cdpEval(tabId, script) as Record<string, unknown>;
|
|
1106
|
+
handleResult(result);
|
|
1107
|
+
return result.__data as DeliverySlot[];
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
/**
|
|
1112
|
+
* Select an Amazon Fresh delivery slot.
|
|
1113
|
+
*/
|
|
1114
|
+
export async function selectFreshDeliverySlot(slotId: string): Promise<{ ok: boolean }> {
|
|
1115
|
+
const { tabId } = await prepareRequest();
|
|
1116
|
+
|
|
1117
|
+
return runWithBackoff(async () => {
|
|
1118
|
+
// Amazon Fresh slot selection endpoint
|
|
1119
|
+
const url = `${AMAZON_BASE}/fresh/api/deliverywindows/select`;
|
|
1120
|
+
const body = JSON.stringify({ windowId: slotId });
|
|
1121
|
+
|
|
1122
|
+
const script = `
|
|
1123
|
+
(async function() {
|
|
1124
|
+
try {
|
|
1125
|
+
var antiCsrf = '';
|
|
1126
|
+
var csrfMeta = document.querySelector('meta[name="anti-csrftoken-a2z"]');
|
|
1127
|
+
if (csrfMeta && csrfMeta.content) {
|
|
1128
|
+
antiCsrf = csrfMeta.content;
|
|
1129
|
+
} else {
|
|
1130
|
+
var csrfInp = document.querySelector('input[name="anti-csrftoken-a2z"]');
|
|
1131
|
+
if (csrfInp && csrfInp.value) { antiCsrf = csrfInp.value; }
|
|
1132
|
+
else { var m = document.cookie.match(/anti-csrftoken-a2z=([^;]+)/); if (m) antiCsrf = decodeURIComponent(m[1]); }
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
var resp = await fetch(${JSON.stringify(url)}, {
|
|
1136
|
+
method: 'POST',
|
|
1137
|
+
headers: {
|
|
1138
|
+
'Content-Type': 'application/json',
|
|
1139
|
+
'Accept': 'application/json',
|
|
1140
|
+
'anti-csrftoken-a2z': antiCsrf,
|
|
1141
|
+
'x-requested-with': 'XMLHttpRequest',
|
|
1142
|
+
},
|
|
1143
|
+
body: ${JSON.stringify(body)},
|
|
1144
|
+
credentials: 'include',
|
|
1145
|
+
});
|
|
1146
|
+
if (resp.status === 401) return JSON.stringify({ __status: 401, __error: true });
|
|
1147
|
+
if (resp.status === 403) return JSON.stringify({ __status: 403, __error: true });
|
|
1148
|
+
var text = await resp.text();
|
|
1149
|
+
return JSON.stringify({ __status: resp.status, __ok: resp.ok, __body: text.substring(0, 500) });
|
|
1150
|
+
} catch(e) {
|
|
1151
|
+
return JSON.stringify({ __error: true, __message: e.message });
|
|
1152
|
+
}
|
|
1153
|
+
})()
|
|
1154
|
+
`;
|
|
1155
|
+
|
|
1156
|
+
const result = await cdpEval(tabId, script) as Record<string, unknown>;
|
|
1157
|
+
handleResult(result);
|
|
1158
|
+
return { ok: Boolean(result.__ok) };
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
/**
|
|
1163
|
+
* Get payment methods from the checkout page.
|
|
1164
|
+
*/
|
|
1165
|
+
export async function getPaymentMethods(): Promise<PaymentMethod[]> {
|
|
1166
|
+
const { tabId } = await prepareRequest();
|
|
1167
|
+
|
|
1168
|
+
return runWithBackoff(async () => {
|
|
1169
|
+
const url = `${AMAZON_BASE}/gp/buy/payselect/handlers/display.html`;
|
|
1170
|
+
|
|
1171
|
+
const script = `
|
|
1172
|
+
(async function() {
|
|
1173
|
+
try {
|
|
1174
|
+
var resp = await fetch(${JSON.stringify(url)}, {
|
|
1175
|
+
credentials: 'include',
|
|
1176
|
+
headers: { 'Accept': 'text/html,application/xhtml+xml' }
|
|
1177
|
+
});
|
|
1178
|
+
if (resp.status === 401) return JSON.stringify({ __status: 401, __error: true });
|
|
1179
|
+
if (resp.status === 403) return JSON.stringify({ __status: 403, __error: true });
|
|
1180
|
+
var html = await resp.text();
|
|
1181
|
+
var parser = new DOMParser();
|
|
1182
|
+
var doc = parser.parseFromString(html, 'text/html');
|
|
1183
|
+
|
|
1184
|
+
var methods = [];
|
|
1185
|
+
var seen = new Set();
|
|
1186
|
+
|
|
1187
|
+
// Look for payment instruments in the page
|
|
1188
|
+
doc.querySelectorAll('[data-pmid], [id^="payment-instrument-"]').forEach(function(el) {
|
|
1189
|
+
var pmid = el.getAttribute('data-pmid') || el.id.replace('payment-instrument-', '');
|
|
1190
|
+
if (!pmid || seen.has(pmid)) return;
|
|
1191
|
+
seen.add(pmid);
|
|
1192
|
+
|
|
1193
|
+
var textContent = el.textContent || '';
|
|
1194
|
+
var last4Match = textContent.match(/(?:ending|\\*{3,})(\\d{4})/);
|
|
1195
|
+
var last4 = last4Match ? last4Match[1] : '';
|
|
1196
|
+
|
|
1197
|
+
var type = 'Card';
|
|
1198
|
+
if (textContent.toLowerCase().includes('visa')) type = 'Visa';
|
|
1199
|
+
else if (textContent.toLowerCase().includes('mastercard')) type = 'Mastercard';
|
|
1200
|
+
else if (textContent.toLowerCase().includes('amex') || textContent.toLowerCase().includes('american express')) type = 'AmEx';
|
|
1201
|
+
else if (textContent.toLowerCase().includes('discover')) type = 'Discover';
|
|
1202
|
+
|
|
1203
|
+
var isDefault = el.classList.contains('pmts-selected') || !!el.querySelector('[selected]') || false;
|
|
1204
|
+
|
|
1205
|
+
if (last4 || pmid) {
|
|
1206
|
+
methods.push({ paymentMethodId: pmid, type, last4, isDefault });
|
|
1207
|
+
}
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
// Fallback: look for payment method data in inline JSON
|
|
1211
|
+
if (methods.length === 0) {
|
|
1212
|
+
var jsonMatch = html.match(/"paymentInstruments"\s*:\s*(\[[^\]]+\])/);
|
|
1213
|
+
if (jsonMatch) {
|
|
1214
|
+
try {
|
|
1215
|
+
var instruments = JSON.parse(jsonMatch[1]);
|
|
1216
|
+
instruments.forEach(function(inst) {
|
|
1217
|
+
methods.push({
|
|
1218
|
+
paymentMethodId: inst.paymentMethodId || inst.id || '',
|
|
1219
|
+
type: inst.cardType || inst.type || 'Card',
|
|
1220
|
+
last4: inst.last4 || inst.maskedCardNumber || '',
|
|
1221
|
+
isDefault: !!inst.isDefault,
|
|
1222
|
+
});
|
|
1223
|
+
});
|
|
1224
|
+
} catch(e) {}
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
return JSON.stringify({ __status: resp.status, __data: methods });
|
|
1229
|
+
} catch(e) {
|
|
1230
|
+
return JSON.stringify({ __error: true, __message: e.message });
|
|
1231
|
+
}
|
|
1232
|
+
})()
|
|
1233
|
+
`;
|
|
1234
|
+
|
|
1235
|
+
const result = await cdpEval(tabId, script) as Record<string, unknown>;
|
|
1236
|
+
handleResult(result);
|
|
1237
|
+
return result.__data as PaymentMethod[];
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
/**
|
|
1242
|
+
* Get the checkout summary (totals, shipping, payment options).
|
|
1243
|
+
*/
|
|
1244
|
+
export async function getCheckoutSummary(): Promise<CheckoutSummary> {
|
|
1245
|
+
const { tabId } = await prepareRequest();
|
|
1246
|
+
|
|
1247
|
+
return runWithBackoff(async () => {
|
|
1248
|
+
const url = `${AMAZON_BASE}/gp/buy/spc/handlers/static-submit-merchantId-data.html`;
|
|
1249
|
+
|
|
1250
|
+
const script = `
|
|
1251
|
+
(async function() {
|
|
1252
|
+
try {
|
|
1253
|
+
var resp = await fetch(${JSON.stringify(url)}, {
|
|
1254
|
+
credentials: 'include',
|
|
1255
|
+
headers: { 'Accept': 'text/html,application/xhtml+xml' }
|
|
1256
|
+
});
|
|
1257
|
+
if (resp.status === 401) return JSON.stringify({ __status: 401, __error: true });
|
|
1258
|
+
if (resp.status === 403) return JSON.stringify({ __status: 403, __error: true });
|
|
1259
|
+
var html = await resp.text();
|
|
1260
|
+
var parser = new DOMParser();
|
|
1261
|
+
var doc = parser.parseFromString(html, 'text/html');
|
|
1262
|
+
|
|
1263
|
+
var getPrice = function(selector) {
|
|
1264
|
+
var el = doc.querySelector(selector);
|
|
1265
|
+
return el ? el.textContent.trim() : '';
|
|
1266
|
+
};
|
|
1267
|
+
|
|
1268
|
+
var subtotal = getPrice('#subtotals-marketplace-table tr:first-child td:last-child') ||
|
|
1269
|
+
getPrice('.order-summary-line-item-price') ||
|
|
1270
|
+
getPrice('[data-component="subtotalAmount"]');
|
|
1271
|
+
var shipping = getPrice('#subtotals-marketplace-table .shipping-row td:last-child') ||
|
|
1272
|
+
getPrice('[data-component="shippingAmount"]') || 'FREE';
|
|
1273
|
+
var tax = getPrice('#subtotals-marketplace-table .tax-row td:last-child') ||
|
|
1274
|
+
getPrice('[data-component="taxAmount"]') || '';
|
|
1275
|
+
var total = getPrice('#subtotals-marketplace-table .grand-total-price') ||
|
|
1276
|
+
getPrice('[data-component="orderTotalAmount"]') ||
|
|
1277
|
+
getPrice('.grand-total-price');
|
|
1278
|
+
|
|
1279
|
+
var deliveryDateEl = doc.querySelector('.delivery-date') || doc.querySelector('[class*="delivery-date"]');
|
|
1280
|
+
var deliveryDate = deliveryDateEl ? deliveryDateEl.textContent.trim() : '';
|
|
1281
|
+
|
|
1282
|
+
// Payment methods
|
|
1283
|
+
var methods = [];
|
|
1284
|
+
doc.querySelectorAll('[data-pmid], .payment-instrument').forEach(function(el) {
|
|
1285
|
+
var pmid = el.getAttribute('data-pmid') || '';
|
|
1286
|
+
if (!pmid) return;
|
|
1287
|
+
var text = el.textContent || '';
|
|
1288
|
+
var last4Match = text.match(/(?:ending|\\*{3,})(\\d{4})/);
|
|
1289
|
+
methods.push({
|
|
1290
|
+
paymentMethodId: pmid,
|
|
1291
|
+
type: text.toLowerCase().includes('visa') ? 'Visa' :
|
|
1292
|
+
text.toLowerCase().includes('mastercard') ? 'Mastercard' : 'Card',
|
|
1293
|
+
last4: last4Match ? last4Match[1] : '',
|
|
1294
|
+
isDefault: !!el.querySelector('[selected]') || el.classList.contains('selected'),
|
|
1295
|
+
});
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
return JSON.stringify({
|
|
1299
|
+
__status: resp.status,
|
|
1300
|
+
__data: { subtotal, shipping, tax, total, paymentMethods: methods, deliveryDate }
|
|
1301
|
+
});
|
|
1302
|
+
} catch(e) {
|
|
1303
|
+
return JSON.stringify({ __error: true, __message: e.message });
|
|
1304
|
+
}
|
|
1305
|
+
})()
|
|
1306
|
+
`;
|
|
1307
|
+
|
|
1308
|
+
const result = await cdpEval(tabId, script) as Record<string, unknown>;
|
|
1309
|
+
handleResult(result);
|
|
1310
|
+
return result.__data as CheckoutSummary;
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
/**
|
|
1315
|
+
* Place an Amazon order.
|
|
1316
|
+
* WARNING: This submits a real order. Always confirm with the user first.
|
|
1317
|
+
*/
|
|
1318
|
+
export async function placeOrder(opts: {
|
|
1319
|
+
paymentMethodId?: string;
|
|
1320
|
+
deliverySlotId?: string;
|
|
1321
|
+
} = {}): Promise<PlaceOrderResult> {
|
|
1322
|
+
const { tabId } = await prepareRequest();
|
|
1323
|
+
|
|
1324
|
+
return runWithBackoff(async () => {
|
|
1325
|
+
// First load the SPC page to get the order submission token
|
|
1326
|
+
const spcUrl = `${AMAZON_BASE}/gp/buy/spc/handlers/static-submit-merchantId-data.html`;
|
|
1327
|
+
|
|
1328
|
+
const script = `
|
|
1329
|
+
(async function() {
|
|
1330
|
+
try {
|
|
1331
|
+
var antiCsrf = '';
|
|
1332
|
+
var csrfMeta = document.querySelector('meta[name="anti-csrftoken-a2z"]');
|
|
1333
|
+
if (csrfMeta && csrfMeta.content) {
|
|
1334
|
+
antiCsrf = csrfMeta.content;
|
|
1335
|
+
} else {
|
|
1336
|
+
var csrfInp = document.querySelector('input[name="anti-csrftoken-a2z"]');
|
|
1337
|
+
if (csrfInp && csrfInp.value) { antiCsrf = csrfInp.value; }
|
|
1338
|
+
else { var m = document.cookie.match(/anti-csrftoken-a2z=([^;]+)/); if (m) antiCsrf = decodeURIComponent(m[1]); }
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// Load checkout page to get form token
|
|
1342
|
+
var spcResp = await fetch(${JSON.stringify(spcUrl)}, {
|
|
1343
|
+
credentials: 'include',
|
|
1344
|
+
headers: { 'Accept': 'text/html,application/xhtml+xml' }
|
|
1345
|
+
});
|
|
1346
|
+
if (spcResp.status === 401) return JSON.stringify({ __status: 401, __error: true });
|
|
1347
|
+
if (spcResp.status === 403) return JSON.stringify({ __status: 403, __error: true });
|
|
1348
|
+
var spcHtml = await spcResp.text();
|
|
1349
|
+
|
|
1350
|
+
// Extract form action and hidden fields
|
|
1351
|
+
var parser = new DOMParser();
|
|
1352
|
+
var doc = parser.parseFromString(spcHtml, 'text/html');
|
|
1353
|
+
var form = doc.querySelector('form#turbo-checkout-pyo-form') || doc.querySelector('form[name="checkout"]') || doc.querySelector('#placeYourOrder form');
|
|
1354
|
+
|
|
1355
|
+
if (!form) {
|
|
1356
|
+
return JSON.stringify({ __error: true, __message: 'Could not find order form on checkout page. Please complete checkout manually in the browser.' });
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
var formAction = form.getAttribute('action') || '/gp/buy/spc/handlers/static-submit-merchantId-data.html';
|
|
1360
|
+
if (!formAction.startsWith('http')) formAction = 'https://www.amazon.com' + formAction;
|
|
1361
|
+
|
|
1362
|
+
// Build form data from hidden inputs
|
|
1363
|
+
var formData = new URLSearchParams();
|
|
1364
|
+
form.querySelectorAll('input[type="hidden"]').forEach(function(inp) {
|
|
1365
|
+
formData.set(inp.name, inp.value);
|
|
1366
|
+
});
|
|
1367
|
+
|
|
1368
|
+
// Apply payment method if specified
|
|
1369
|
+
if (${JSON.stringify(opts.paymentMethodId || '')}) {
|
|
1370
|
+
formData.set('ppw-instrumentId', ${JSON.stringify(opts.paymentMethodId || '')});
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// Submit order
|
|
1374
|
+
var submitResp = await fetch(formAction, {
|
|
1375
|
+
method: 'POST',
|
|
1376
|
+
headers: {
|
|
1377
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
1378
|
+
'anti-csrftoken-a2z': antiCsrf,
|
|
1379
|
+
},
|
|
1380
|
+
body: formData.toString(),
|
|
1381
|
+
credentials: 'include',
|
|
1382
|
+
});
|
|
1383
|
+
if (submitResp.status === 401) return JSON.stringify({ __status: 401, __error: true });
|
|
1384
|
+
if (submitResp.status === 403) return JSON.stringify({ __status: 403, __error: true });
|
|
1385
|
+
var resultHtml = await submitResp.text();
|
|
1386
|
+
var resultDoc = parser.parseFromString(resultHtml, 'text/html');
|
|
1387
|
+
|
|
1388
|
+
// Extract order ID from confirmation page
|
|
1389
|
+
var orderIdEl = resultDoc.querySelector('[class*="order-id"]') ||
|
|
1390
|
+
resultDoc.querySelector('[data-order-id]') ||
|
|
1391
|
+
resultDoc.querySelector('[class*="confirmation"]');
|
|
1392
|
+
var orderId = '';
|
|
1393
|
+
if (orderIdEl) {
|
|
1394
|
+
var oidMatch = (orderIdEl.textContent || '').match(/\\d{3}-\\d{7}-\\d{7}/);
|
|
1395
|
+
if (oidMatch) orderId = oidMatch[0];
|
|
1396
|
+
}
|
|
1397
|
+
// Also check URL for order ID
|
|
1398
|
+
var urlMatch = submitResp.url.match(/orderId=([\\d-]+)/);
|
|
1399
|
+
if (!orderId && urlMatch) orderId = urlMatch[1];
|
|
1400
|
+
|
|
1401
|
+
var deliveryEl = resultDoc.querySelector('[class*="delivery-date"]') || resultDoc.querySelector('[class*="estimated-delivery"]');
|
|
1402
|
+
var estimatedDelivery = deliveryEl ? deliveryEl.textContent.trim() : '';
|
|
1403
|
+
|
|
1404
|
+
return JSON.stringify({
|
|
1405
|
+
__status: submitResp.status,
|
|
1406
|
+
__data: { orderId: orderId || 'confirmed', estimatedDelivery }
|
|
1407
|
+
});
|
|
1408
|
+
} catch(e) {
|
|
1409
|
+
return JSON.stringify({ __error: true, __message: e.message });
|
|
1410
|
+
}
|
|
1411
|
+
})()
|
|
1412
|
+
`;
|
|
1413
|
+
|
|
1414
|
+
const result = await cdpEval(tabId, script) as Record<string, unknown>;
|
|
1415
|
+
handleResult(result);
|
|
1416
|
+
return result.__data as PlaceOrderResult;
|
|
1417
|
+
});
|
|
1418
|
+
}
|