Skip to content

kit: harden the Amazon RVS lane — sandbox bypass, expiry assumption, state staleness #311

Description

@hyochan

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)

  • Run a real RVS sandbox verification end to end (App Tester)
  • Capture real receipt JSON for an expired and a grace-period subscription; confirm whether cancelDate is set at natural expiry; convert captures into fixtures
  • Confirm production RVS behavior with the real shared secret

🤖 Findings from an internal multi-agent audit; file:line references verified against main at the time of filing.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    kitIAPKit (receipt-validation SaaS)🐛 bugSomething isn't working🤖 androidRelated to android🧪 testIssue or pr related to testing

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions