Companion to #310, from the same internal audit. The Amazon (Fire OS RVS) verify lane is wired end to end (store:"amazon" intake → verifyAmazonReceiptInternalV1 → RVS → persistence with store:"amazon"), but it has never been manually verified against a real RVS sandbox and carries the following issues.
Major
1. Client-controlled sandbox:true bypasses the shared secret and is invisible in the response
When the request sets sandbox:true, the AmazonSharedSecretNotConfiguredError gate is skipped and the RVS Cloud Sandbox URL is built with the placeholder secret "iapkit-sandbox" — no developer-held credential involved. The public verify response carries no environment field, so a consumer cannot distinguish a sandbox verification from production. A holder of the shipped client API key can generate an App Tester pseudo-receipt on a Fire device and obtain {isValid:true, state:"ENTITLED"}.
packages/kit/convex/purchases/amazon.ts:159-183
packages/kit/server/api/v1/route-response-schemas.ts (no environment field)
Fix direction: echo an environment field in the amazon verify response and store it as a first-class column; gate sandbox verification behind an explicit project setting so production projects can disable it.
2. Subscription expiry rests on an unverified cancelDate assumption
mapAmazonReceiptState returns CANCELED only when cancelDate is set; otherwise SUBSCRIPTION maps unconditionally to ENTITLED. renewalDate, gracePeriodEndDate, and term are parsed but never consulted (a test locks in "renewalDate in the past → ENTITLED"). This is correct only if RVS reliably sets cancelDate at natural expiry — never confirmed against a real receipt. If wrong, expired Amazon subscriptions verify as ENTITLED forever.
packages/kit/convex/purchases/amazon.ts:94-112, amazon.test.ts:53-60
3. No lifecycle source at all — unbounded state staleness
Apple has ASN v2, Google has RTDN, Horizon has a 6-hour reconciler cron; Amazon has neither a webhook receiver nor a reconciler. Post-verification state never updates.
packages/kit/server/api/v1/webhooks.ts:40-77, packages/kit/convex/crons.ts:69-79
Test coverage (see also #312)
verifyAmazonReceiptInternalV1 has zero test coverage (23% file stmts; the 7 existing tests cover only the three pure helpers). Untested: the RVS status-code ladder (400/497→INAUTHENTIC, 410→CANCELED, 496→verification error), retry predicate, 10s timeout, sandbox URL segment, saveFailedReceipt persistence, happy-path save wiring. Suggested: fetch-stubbed suite + a store:"amazon" save-path integration test mirroring save-purchase-idempotency.test.ts; also assert route→action argument forwarding (incl. sandbox passthrough) in routes.test.ts.
Minor
- No app-identity binding: RVS authenticates developer-level secret + (userId, receiptId); any receipt from the same Amazon developer account verifies.
expectedProductId is not accepted for the amazon variant.
- Amazon purchases count only toward total/valid/invalid in
purchaseStats (no per-store counter) and never appear in the /v1/subscription status/entitlements lane.
Manual E2E checklist (requires Fire device + Amazon developer account)
🤖 Findings from an internal multi-agent audit; file:line references verified against main at the time of filing.
Companion to #310, from the same internal audit. The Amazon (Fire OS RVS) verify lane is wired end to end (
store:"amazon"intake →verifyAmazonReceiptInternalV1→ RVS → persistence withstore:"amazon"), but it has never been manually verified against a real RVS sandbox and carries the following issues.Major
1. Client-controlled
sandbox:truebypasses the shared secret and is invisible in the responseWhen the request sets
sandbox:true, theAmazonSharedSecretNotConfiguredErrorgate is skipped and the RVS Cloud Sandbox URL is built with the placeholder secret"iapkit-sandbox"— no developer-held credential involved. The public verify response carries no environment field, so a consumer cannot distinguish a sandbox verification from production. A holder of the shipped client API key can generate an App Tester pseudo-receipt on a Fire device and obtain{isValid:true, state:"ENTITLED"}.packages/kit/convex/purchases/amazon.ts:159-183packages/kit/server/api/v1/route-response-schemas.ts(no environment field)Fix direction: echo an
environmentfield in the amazon verify response and store it as a first-class column; gate sandbox verification behind an explicit project setting so production projects can disable it.2. Subscription expiry rests on an unverified
cancelDateassumptionmapAmazonReceiptStatereturnsCANCELEDonly whencancelDateis set; otherwise SUBSCRIPTION maps unconditionally toENTITLED.renewalDate,gracePeriodEndDate, andtermare parsed but never consulted (a test locks in "renewalDate in the past → ENTITLED"). This is correct only if RVS reliably setscancelDateat natural expiry — never confirmed against a real receipt. If wrong, expired Amazon subscriptions verify as ENTITLED forever.packages/kit/convex/purchases/amazon.ts:94-112,amazon.test.ts:53-603. No lifecycle source at all — unbounded state staleness
Apple has ASN v2, Google has RTDN, Horizon has a 6-hour reconciler cron; Amazon has neither a webhook receiver nor a reconciler. Post-verification state never updates.
packages/kit/server/api/v1/webhooks.ts:40-77,packages/kit/convex/crons.ts:69-79Test coverage (see also #312)
verifyAmazonReceiptInternalV1has zero test coverage (23% file stmts; the 7 existing tests cover only the three pure helpers). Untested: the RVS status-code ladder (400/497→INAUTHENTIC, 410→CANCELED, 496→verification error), retry predicate, 10s timeout, sandbox URL segment,saveFailedReceiptpersistence, happy-path save wiring. Suggested: fetch-stubbed suite + astore:"amazon"save-path integration test mirroringsave-purchase-idempotency.test.ts; also assert route→action argument forwarding (incl.sandboxpassthrough) inroutes.test.ts.Minor
expectedProductIdis not accepted for the amazon variant.purchaseStats(no per-store counter) and never appear in the/v1/subscriptionstatus/entitlements lane.Manual E2E checklist (requires Fire device + Amazon developer account)
cancelDateis set at natural expiry; convert captures into fixtures🤖 Findings from an internal multi-agent audit; file:line references verified against main at the time of filing.