Skip to content

feat: complete commerce implementation guide - #445

Merged
hyochan merged 19 commits into
mainfrom
codex/commerce-protocol-review
Sep 9, 2026
Merged

hyochan merged 19 commits into
mainfrom
codex/commerce-protocol-review

Conversation

@hyochan

@hyochan hyochan commented Sep 8, 2026

Copy link
Copy Markdown
Member

Commerce Protocol now walks readers through purchase, verification, ownership, access, events, and account deletion, with IAPKit and the runnable example alongside each step. The architecture opens explanations in place; navigation, nested accordions, and transitions follow the same documentation layout.

IAPKit binds Amazon and Horizon purchases to authenticated app accounts and rechecks ownership before returning access. Only entitled evidence binds, at most 20 purchases per app account; the 21st binding answers bound: false (SPEC §4.4) and is logged, and the read keeps the same bound as a backstop. Rechecks draw on their own admission bucket (300 tokens, 5/s, one per bound purchase) before any store call and do not write back an unchanged verdict. Erasure refuses the erased app user id while its job is retained; another app account may bind the same Amazon or Horizon evidence afterwards as a first binding. Apple and Google subscription rows keep their permanent erasure marker.

The example → IAPKit → example check keeps one app backend and receiver unchanged: 170 assertions, recorded at the committed sources. The docs build verifies that the report, snapshots, hashes, and #L<n> anchors agree with each other; freshness against the current IAPKit sources is the advisory bun run audit:commerce-evidence, run with continue-on-error in the web E2E job, and a recording from an uncommitted tree is marked -dirty. Static HTML, canonical metadata, sitemap generation, and readable AI entry points make the same guides available without JavaScript.

Companion example: hyodotdev/openiap-commerce-protocol-example#1.

Checks: IAPKit lint and the full IAPKit test suite (one existing skip), compiled-server smoke, the protocol suite, 170 provider-replacement assertions, the composition and source-provenance checks, 144 prerendered pages, and the SDK parity, docs, layout, CI-path, and agent-surface audits pass locally.

Review: CodeRabbit skips this diff (over its 100-file limit) and Codex is over its usage limit until Sep 15, so this head was reviewed by three independent read-only review-self lenses (kit correctness, tests and tooling, docs and protocol) and by Grok on the pasted diff. Every finding that did not need a product decision was fixed in 3bf91680 and the follow-up commit; the Grok notes left as they are, with reasons, are in the PR comments.

Merge gate: device regression remains pending for the packages/kit Martie live-receipt rows on iOS and Android/Play. The local four-store checks use synthetic evidence and mocked store responses; no real Amazon, Horizon, Apple, or Google checkout is claimed. Cross-company adoption and production migration are not established by this run.

Preview

commerce-docs.webm

🤖 Generated with Claude Code

@hyochan hyochan added 🎯 feature New feature 💨 ci Cloud integration 📖 documentation Improvements or additions to documentation 🕶️ meta ⬡ gql kit IAPKit (receipt-validation SaaS) labels Sep 8, 2026
@hyodotdev hyodotdev deleted a comment from coderabbitai Bot Sep 8, 2026
@hyodotdev hyodotdev deleted a comment from coderabbitai Bot Sep 8, 2026
Connect the guided documentation to the runnable example and IAPKit. Add four-store ownership and erasure handling, reproducible provider replacement evidence, and static discovery with consistent navigation and transitions.
@hyochan
hyochan force-pushed the codex/commerce-protocol-review branch from e959b1a to f0703f7 Compare September 9, 2026 00:14
@coderabbitai

coderabbitai Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Too many files!

This PR contains 196 files, which is 96 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 1f98a1f0-9537-47d6-a065-f21e8af3e79b

📥 Commits

Reviewing files that changed from the base of the PR and between e338ca6 and 8ac75fc.

⛔ Files ignored due to path filters (27)
  • packages/docs/public/commerce-composition/source.tar.gz is excluded by !**/*.gz
  • packages/docs/public/commerce-example/07-account-erasure-interoperable-2/mobile.png is excluded by !**/*.png
  • packages/docs/public/commerce-example/07-account-erasure-interoperable-2/screen.png is excluded by !**/*.png
  • packages/docs/public/commerce-example/07-account-erasure-interoperable-2/source.tar.gz is excluded by !**/*.gz
  • packages/docs/public/commerce-example/07-account-erasure-interoperable-3/mobile.png is excluded by !**/*.png
  • packages/docs/public/commerce-example/07-account-erasure-interoperable-3/screen.png is excluded by !**/*.png
  • packages/docs/public/commerce-example/07-account-erasure-interoperable-3/source.tar.gz is excluded by !**/*.gz
  • packages/docs/public/commerce-example/07-account-erasure-interoperable-4/mobile.png is excluded by !**/*.png
  • packages/docs/public/commerce-example/07-account-erasure-interoperable-4/screen.png is excluded by !**/*.png
  • packages/docs/public/commerce-example/07-account-erasure-interoperable-4/source.tar.gz is excluded by !**/*.gz
  • packages/docs/public/commerce-example/07-account-erasure-interoperable-5/mobile.png is excluded by !**/*.png
  • packages/docs/public/commerce-example/07-account-erasure-interoperable-5/screen.png is excluded by !**/*.png
  • packages/docs/public/commerce-example/07-account-erasure-interoperable-5/source.tar.gz is excluded by !**/*.gz
  • packages/docs/public/commerce-example/07-account-erasure-interoperable-6/mobile.png is excluded by !**/*.png
  • packages/docs/public/commerce-example/07-account-erasure-interoperable-6/screen.png is excluded by !**/*.png
  • packages/docs/public/commerce-example/07-account-erasure-interoperable-6/source.tar.gz is excluded by !**/*.gz
  • packages/docs/public/commerce-example/07-account-erasure-interoperable/mobile.png is excluded by !**/*.png
  • packages/docs/public/commerce-example/07-account-erasure-interoperable/screen.png is excluded by !**/*.png
  • packages/docs/public/commerce-example/07-account-erasure-interoperable/source.tar.gz is excluded by !**/*.gz
  • packages/docs/public/commerce-example/07-account-erasure-reviewed/mobile.png is excluded by !**/*.png
  • packages/docs/public/commerce-example/07-account-erasure-reviewed/screen.png is excluded by !**/*.png
  • packages/docs/public/commerce-example/07-account-erasure-reviewed/source.tar.gz is excluded by !**/*.gz
  • packages/docs/public/commerce-example/07-account-erasure/mobile.png is excluded by !**/*.png
  • packages/docs/public/commerce-example/07-account-erasure/screen.png is excluded by !**/*.png
  • packages/docs/public/commerce-example/07-account-erasure/source.tar.gz is excluded by !**/*.gz
  • packages/docs/public/commerce-example/ai-reproduction-source.tar.gz is excluded by !**/*.gz
  • packages/docs/public/commerce-example/source.tar.gz is excluded by !**/*.gz
📒 Files selected for processing (196)
  • .claude/commands/e2e-tests.md
  • .claude/commands/verify-all.md
  • .codex/skills/iapkit-e2e-martie/SKILL.md
  • .codex/skills/ship-release/SKILL.md
  • .github/ISSUE_TEMPLATE/commerce_protocol.yml
  • .github/workflows/ci.yml
  • .github/workflows/deploy-kit.yml
  • CONTRIBUTING.md
  • knowledge/_agent-context/context.md
  • knowledge/internal/05-docs-patterns.md
  • package.json
  • packages/apple/Example/OpenIapExample/Screens/AllProductsView.swift
  • packages/apple/Example/OpenIapExample/Screens/SubscriptionFlowScreen.swift
  • packages/apple/Example/OpenIapExample/Screens/uis/PurchaseDetailSheet.swift
  • packages/docs/CONVENTION.md
  • packages/docs/index.html
  • packages/docs/package.json
  • packages/docs/public/commerce-composition/README.md
  • packages/docs/public/commerce-composition/iapkit-run.json
  • packages/docs/public/commerce-composition/iapkit-source-manifest.json
  • packages/docs/public/commerce-composition/iapkit-source.json
  • packages/docs/public/commerce-composition/run.json
  • packages/docs/public/commerce-composition/source.json
  • packages/docs/public/commerce-example/07-account-erasure-interoperable-2/attempt-1.txt
  • packages/docs/public/commerce-example/07-account-erasure-interoperable-2/changes.patch
  • packages/docs/public/commerce-example/07-account-erasure-interoperable-2/run.json
  • packages/docs/public/commerce-example/07-account-erasure-interoperable-3/attempt-1.txt
  • packages/docs/public/commerce-example/07-account-erasure-interoperable-3/changes.patch
  • packages/docs/public/commerce-example/07-account-erasure-interoperable-3/run.json
  • packages/docs/public/commerce-example/07-account-erasure-interoperable-4/attempt-1.txt
  • packages/docs/public/commerce-example/07-account-erasure-interoperable-4/changes.patch
  • packages/docs/public/commerce-example/07-account-erasure-interoperable-4/run.json
  • packages/docs/public/commerce-example/07-account-erasure-interoperable-5/attempt-1.txt
  • packages/docs/public/commerce-example/07-account-erasure-interoperable-5/changes.patch
  • packages/docs/public/commerce-example/07-account-erasure-interoperable-5/run.json
  • packages/docs/public/commerce-example/07-account-erasure-interoperable-6/attempt-1.txt
  • packages/docs/public/commerce-example/07-account-erasure-interoperable-6/changes.patch
  • packages/docs/public/commerce-example/07-account-erasure-interoperable-6/run.json
  • packages/docs/public/commerce-example/07-account-erasure-interoperable/attempt-1.txt
  • packages/docs/public/commerce-example/07-account-erasure-interoperable/changes.patch
  • packages/docs/public/commerce-example/07-account-erasure-interoperable/run.json
  • packages/docs/public/commerce-example/07-account-erasure-reviewed/attempt-1.txt
  • packages/docs/public/commerce-example/07-account-erasure-reviewed/changes.patch
  • packages/docs/public/commerce-example/07-account-erasure-reviewed/run.json
  • packages/docs/public/commerce-example/07-account-erasure/attempt-1.txt
  • packages/docs/public/commerce-example/07-account-erasure/changes.patch
  • packages/docs/public/commerce-example/07-account-erasure/run.json
  • packages/docs/public/commerce-example/README.md
  • packages/docs/public/commerce-example/ai-reproduction.json
  • packages/docs/public/commerce-example/ai-reproduction.md
  • packages/docs/public/commerce-example/build-brief.md
  • packages/docs/public/commerce-example/consumer-run.json
  • packages/docs/public/commerce-example/integration-brief.md
  • packages/docs/public/commerce-example/run.json
  • packages/docs/public/commerce-example/verification.json
  • packages/docs/public/commerce-source/example/README.md.html
  • packages/docs/public/commerce-source/example/client-bridge.mjs.html
  • packages/docs/public/commerce-source/example/composition/app-backend.mjs.html
  • packages/docs/public/commerce-source/example/composition/commerce-client.mjs.html
  • packages/docs/public/commerce-source/example/consumer.mjs.html
  • packages/docs/public/commerce-source/example/contract.mjs.html
  • packages/docs/public/commerce-source/example/erasure.mjs.html
  • packages/docs/public/commerce-source/example/provider.mjs.html
  • packages/docs/public/commerce-source/example/scenario.mjs.html
  • packages/docs/public/commerce-source/example/verify-erasure.mjs.html
  • packages/docs/public/commerce-source/example/verify-stores.mjs.html
  • packages/docs/public/commerce-source/example/verify.mjs.html
  • packages/docs/public/commerce-source/example/webhooks.mjs.html
  • packages/docs/public/commerce-source/kit/convex/commerce/capabilities.ts.html
  • packages/docs/public/commerce-source/kit/convex/commerce/delivery.test.ts.html
  • packages/docs/public/commerce-source/kit/convex/commerce/delivery.ts.html
  • packages/docs/public/commerce-source/kit/convex/commerce/spec.conformance.test.ts.html
  • packages/docs/public/commerce-source/kit/convex/purchases/action.ts.html
  • packages/docs/public/commerce-source/kit/convex/purchases/mutation.ts.html
  • packages/docs/public/commerce-source/kit/convex/purchases/ownership.test.ts.html
  • packages/docs/public/commerce-source/kit/convex/subscriptions/internal.test.ts.html
  • packages/docs/public/commerce-source/kit/convex/subscriptions/internal.ts.html
  • packages/docs/public/commerce-source/kit/convex/subscriptions/mutation.test.ts.html
  • packages/docs/public/commerce-source/kit/convex/subscriptions/mutation.ts.html
  • packages/docs/public/commerce-source/kit/convex/subscriptions/query.test.ts.html
  • packages/docs/public/commerce-source/kit/convex/subscriptions/query.ts.html
  • packages/docs/public/commerce-source/kit/scripts/docs/capture-public.ts.html
  • packages/docs/public/commerce-source/kit/scripts/docs/capture.ts.html
  • packages/docs/public/commerce-source/kit/scripts/docs/commerce-interop-fixture.ts.html
  • packages/docs/public/commerce-source/kit/scripts/docs/commerce-source-snapshot.mjs.html
  • packages/docs/public/commerce-source/kit/scripts/docs/commerce-store-coverage.mjs.html
  • packages/docs/public/commerce-source/kit/scripts/docs/compare-commerce-example.ts.html
  • packages/docs/public/commerce-source/kit/scripts/docs/export-commerce-interop.mjs.html
  • packages/docs/public/commerce-source/kit/scripts/docs/render-mockups.ts.html
  • packages/docs/public/commerce-source/kit/scripts/docs/run-commerce-interop.mjs.html
  • packages/docs/public/commerce-source/kit/scripts/docs/seed-testapp.ts.html
  • packages/docs/public/commerce-source/kit/server/api/commerce/conformance.test.ts.html
  • packages/docs/public/commerce-source/kit/server/api/commerce/graphql.ts.html
  • packages/docs/public/commerce-source/kit/server/api/commerce/handlers.ts.html
  • packages/docs/public/commerce-source/kit/server/api/commerce/routes.test.ts.html
  • packages/docs/public/commerce-source/kit/server/api/commerce/routes.ts.html
  • packages/docs/public/llms-full.txt
  • packages/docs/public/llms.txt
  • packages/docs/public/robots.txt
  • packages/docs/public/sitemap.xml
  • packages/docs/scripts/check-commerce-composition.mjs
  • packages/docs/scripts/prerender.mjs
  • packages/docs/scripts/prerender.test.mjs
  • packages/docs/src/App.tsx
  • packages/docs/src/components/AnchorLink.tsx
  • packages/docs/src/components/CommerceArchitectureModal.tsx
  • packages/docs/src/components/CommerceBuildWalkthrough.tsx
  • packages/docs/src/components/CommerceCompositionProof.tsx
  • packages/docs/src/components/CommerceEcosystem.tsx
  • packages/docs/src/components/CommerceImplementationComparison.tsx
  • packages/docs/src/components/CommerceProtocolDiagram.tsx
  • packages/docs/src/components/CommercePurchaseJourney.tsx
  • packages/docs/src/components/DocsShell.tsx
  • packages/docs/src/components/MenuDropdown.tsx
  • packages/docs/src/components/SEO.tsx
  • packages/docs/src/components/StoreExampleTemplate.tsx
  • packages/docs/src/components/VideoSlot.tsx
  • packages/docs/src/entry-server.tsx
  • packages/docs/src/hooks/useDetailsTransition.ts
  • packages/docs/src/hooks/useScrollToHash.ts
  • packages/docs/src/lib/commerceEcosystem.ts
  • packages/docs/src/lib/commerceImplementations.ts
  • packages/docs/src/lib/searchData.ts
  • packages/docs/src/lib/signals.ts
  • packages/docs/src/main.tsx
  • packages/docs/src/pages/404.tsx
  • packages/docs/src/pages/commerce-protocol/authentication.tsx
  • packages/docs/src/pages/commerce-protocol/capabilities.tsx
  • packages/docs/src/pages/commerce-protocol/conformance.tsx
  • packages/docs/src/pages/commerce-protocol/ecosystem.tsx
  • packages/docs/src/pages/commerce-protocol/getting-started.tsx
  • packages/docs/src/pages/commerce-protocol/graphql.tsx
  • packages/docs/src/pages/commerce-protocol/implementation.tsx
  • packages/docs/src/pages/commerce-protocol/index.tsx
  • packages/docs/src/pages/commerce-protocol/operations.tsx
  • packages/docs/src/pages/commerce-protocol/overview.tsx
  • packages/docs/src/pages/commerce-protocol/profiles.tsx
  • packages/docs/src/pages/commerce-protocol/rest.tsx
  • packages/docs/src/pages/commerce-protocol/versioning.tsx
  • packages/docs/src/pages/commerce-protocol/webhooks.tsx
  • packages/docs/src/pages/commerce-protocol/whitepaper.tsx
  • packages/docs/src/pages/docs/examples/android.tsx
  • packages/docs/src/pages/docs/examples/fireos.tsx
  • packages/docs/src/pages/docs/examples/horizon.tsx
  • packages/docs/src/pages/docs/examples/index.tsx
  • packages/docs/src/pages/docs/examples/ios.tsx
  • packages/docs/src/pages/docs/foundation/governance.tsx
  • packages/docs/src/pages/docs/guides/ai-assistants.tsx
  • packages/docs/src/pages/docs/setup/index.tsx
  • packages/docs/src/pages/languages.tsx
  • packages/docs/src/pages/showcase.tsx
  • packages/docs/src/styles/commerce-protocol.css
  • packages/docs/src/styles/documentation.css
  • packages/docs/src/styles/home.css
  • packages/docs/vercel.json
  • packages/docs/vite.config.ts
  • packages/kit/COMMERCE-EVENTS.md
  • packages/kit/convex/projects/query.test.ts
  • packages/kit/convex/projects/query.ts
  • packages/kit/convex/purchases/action.ts
  • packages/kit/convex/purchases/amazon-reconciliation.test.ts
  • packages/kit/convex/purchases/amazon.test.ts
  • packages/kit/convex/purchases/amazon.ts
  • packages/kit/convex/purchases/horizon.test.ts
  • packages/kit/convex/purchases/horizon.ts
  • packages/kit/convex/purchases/identity.ts
  • packages/kit/convex/purchases/internal.ts
  • packages/kit/convex/purchases/mutation.ts
  • packages/kit/convex/purchases/ownership.test.ts
  • packages/kit/convex/purchases/save-purchase-idempotency.test.ts
  • packages/kit/convex/purchases/shared.ts
  • packages/kit/convex/purchases/verificationAdmission.test.ts
  • packages/kit/convex/purchases/verificationAdmission.ts
  • packages/kit/convex/schema.ts
  • packages/kit/convex/subscriptions/erasure.ts
  • packages/kit/convex/subscriptions/internal.test.ts
  • packages/kit/convex/subscriptions/internal.ts
  • packages/kit/convex/subscriptions/limits.ts
  • packages/kit/convex/subscriptions/user-erasure.test.ts
  • packages/kit/scripts/docs/commerce-interop-fixture.ts
  • packages/kit/scripts/docs/commerce-interop.md
  • packages/kit/scripts/docs/commerce-source-snapshot.mjs
  • packages/kit/scripts/docs/commerce-source-snapshot.test.mjs
  • packages/kit/scripts/docs/commerce-store-coverage.mjs
  • packages/kit/scripts/docs/export-commerce-interop.mjs
  • packages/kit/scripts/docs/run-commerce-interop.mjs
  • packages/kit/server/api/commerce/conformance.test.ts
  • packages/kit/server/api/commerce/errors.ts
  • packages/kit/server/api/commerce/handlers.ts
  • packages/kit/server/api/commerce/routes.test.ts
  • scripts/agent/compile-context.ts
  • scripts/agent/tests/llms-content.test.ts
  • scripts/audit-commerce-evidence.mjs
  • scripts/audit-commerce-evidence.test.mjs
  • scripts/e2e-web-sites.mjs
  • specs/commerce-protocol/CONVENTION.md

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 76.16%. Comparing base (1368877) to head (8ac75fc).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #445      +/-   ##
==========================================
+ Coverage   76.00%   76.16%   +0.15%     
==========================================
  Files         155      157       +2     
  Lines       16561    16674     +113     
  Branches     4763     4807      +44     
==========================================
+ Hits        12588    12700     +112     
- Misses       3973     3974       +1     
Flag Coverage Δ
iapkit 67.50% <ø> (+0.33%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
React Native IAP 93.11% <ø> (ø)
Expo IAP 90.29% <ø> (ø)
flutter_inapp_purchase 90.42% <ø> (ø)
IAPKit Server 92.05% <ø> (-0.08%) ⬇️
IAPKit Convex 61.52% <ø> (+0.49%) ⬆️
Files with missing lines Coverage Δ
packages/kit/convex/projects/query.ts 23.07% <ø> (+1.50%) ⬆️
packages/kit/convex/purchases/action.ts 95.45% <ø> (+95.45%) ⬆️
packages/kit/convex/purchases/amazon.ts 94.88% <ø> (ø)
packages/kit/convex/purchases/horizon.ts 89.04% <ø> (+0.15%) ⬆️
packages/kit/convex/purchases/identity.ts 100.00% <ø> (ø)
packages/kit/convex/purchases/internal.ts 94.40% <ø> (-1.90%) ⬇️
packages/kit/convex/purchases/mutation.ts 88.63% <ø> (+21.96%) ⬆️
packages/kit/convex/purchases/shared.ts 84.87% <ø> (+0.07%) ⬆️
...ages/kit/convex/purchases/verificationAdmission.ts 100.00% <ø> (+11.11%) ⬆️
packages/kit/convex/schema.ts 100.00% <ø> (ø)
... and 5 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

hyochan and others added 2 commits September 9, 2026 09:27
…sed base

The docs build pins the interop run's monorepo inputs, and #444 changed the
root bun.lock and package.json that the recording had pinned, so the check
refused the rebased tree. Re-ran the example -> IAPKit -> example harness on
this base with the example at f23f663 (the revision the recording already
matched) and exported it: 162 checks, identical outcomes and store coverage;
only timestamps, job ids and the recorded revisions moved.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…it reads

Kit: an ownership recheck that cannot reach Amazon or Horizon now fails the
entitlements read as VERIFICATION_FAILED (502) like verify, instead of a
generic 500; the ownership-moved-mid-read race is CONFLICT (409) instead of a
400 that no client would retry. Route tests cover both plus the Amazon binding
identity. COMMERCE-EVENTS.md states what the erasure tombstone means for a
returning customer.

Docs: the purchase walkthrough names the provider role, not IAPKit, in the
Amazon/Horizon recheck step; store labels come from one constant; prose that
narrated how the evidence was produced is gone; the implementation page has one
name and correct heading levels; collapsing the walkthrough no longer scrolls
the viewport to the diagram. The build now checks that every source deep link
(#L<n>) still lands on its symbol, so a regenerated snapshot cannot silently
move them. Unknown and client-redirected routes fall back to a bare shell
(dist/_app.html) rather than the prerendered homepage with its canonical URL,
and SVG icon titles no longer count as the document title.

Rules: the acceptance section asks for the runnable example plus at least one
independent implementation rather than naming one vendor, and its evidence
requirements are ones a PR can actually carry. The interop evidence is
re-recorded on these sources.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@hyochan
hyochan marked this pull request as ready for review September 9, 2026 08:05
hyochan and others added 4 commits September 9, 2026 18:23
… rebind, and evidence gate

- bindPurchase binds only ENTITLED evidence and caps 20 bound purchases per
  app account; the 21st answers bound:false (SPEC 4.4) and is logged
- entitlements rechecks pay their own admission bucket (300 tokens, 5/s, one
  per bound purchase) before any store call and skip unchanged verdicts
- erased Amazon/Horizon evidence may bind again to another app user; the
  erased user id stays refused while its erasure job is retained
- the docs build checks only the evidence's own consistency; the new
  `bun run audit:commerce-evidence` reports drift against the current sources
  as an advisory CI warning, and recordings from an uncommitted tree are
  marked -dirty
- re-recorded interop evidence: 170 checks

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Same 170 checks; the recording now names commit 3bf9168 instead of an
uncommitted tree.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- the cap check runs before the evidence lookup, so neither the answer nor
  the operator log depends on someone else's purchase (SPEC 4.4)
- binding also requires isValid; revoked rows still occupy the cap
- pin the Amazon recheck skip, the recheck budget on failed reads, the log
  payload, shortRevision, and the harness inventory in tests
- COMMERCE-EVENTS: the erasure window refuses the erased user's own retries;
  the read keeps the 20 bound as a backstop

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Same 170 checks; the recording names the committed sources instead of an
uncommitted tree.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@hyochan

hyochan commented Sep 9, 2026

Copy link
Copy Markdown
Member Author

Grok review of the changes in 3bf91680 (diff pasted inline, tools disabled) returned six notes. Fixed in the follow-up commit: the cap is decided from the caller's own rows before any evidence lookup, isValid is required again for binding, the store-coverage predicate names Amazon and Horizon explicitly, the erasure sentence and the read backstop are back in COMMERCE-EVENTS, and the Amazon recheck skip, the failed-read admission, the warn payload, shortRevision, and the harness inventory are pinned by tests.

Left as they are: a skipped recheck does not touch nextAmazonReconcileAt, because the reconciler reschedules a row itself when it runs and a read must stay a read; an erasure job pages rows by the erased appUserId, so it cannot re-tombstone evidence another account has since bound; the runner already fails on inventory additions through pinSources.assertUnchanged, so assertSourceHashes keeps ignoring added; and the verification bucket's 10/s refill predates this change.

hyochan and others added 9 commits September 10, 2026 00:28
The SwiftUI example under `packages/apple/Example` no longer compiled,
so
the iOS row of the device regression could not run at all.

Two drifts had accumulated against the generated types:

- The example read `purchase.platform`. That field is now `store`, typed
  `IapStore`. Three call sites still used the old name.
- Two `ProductTypeIOS` switches predate the `subscription-bundle` and
  `subscription-suite` cases, so they stopped being exhaustive.

Checks: `xcodebuild -project Martie.xcodeproj -scheme OpenIapExample
-configuration Debug -destination "generic/platform=iOS"` now succeeds.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added display support for subscription bundle and subscription suite
product types, including labels and color indicators.

* **Bug Fixes**
* Updated purchase details and success logging to show the correct store
information.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
)

Three environment conditions each cost a full debugging session during a
Martie live-receipt run, and each looked like a store or account
failure.

- The generated `android/` directory keeps whichever store it was last
prebuilt for. After the FireOS or Horizon row, a Play run links the
wrong
  `openiap-google` flavor and stalls on `Connecting to Store...`.
- iOS restores a scene session left by another app sharing the
`dev.hyo.martie` bundle id, so UIKit attaches that app's scene delegate
and
the example's own `SceneDelegate` never starts React Native. Black
screen,
live process, no bundle request, no crash. Only a real uninstall clears
it.
- Expo links React Native as a prebuilt binary, and that slice compiles
  without `DEBUG`, so a Debug build can never reach Metro.

Two smaller ones join them: reinstalling resets the iOS local-network
grant,
and the local origin is inlined when Metro starts, so a stale value
quietly
sends verification to the hosted service.

Also records that a Play "not compatible with your device" banner does
not
block billing for a side-loaded license-tester build.

Checks: `bunx prettier --check` on both files; the pre-commit
agent-surface,
layout, parity, sponsor, and kit-contract audits ran clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Documentation**
- Added troubleshooting guidance for environment and workspace states
that can cause misleading store or account failures during local receipt
testing.
- Documented Android store-flavor mismatches, Play compatibility
warnings, persistent iOS scene sessions, prebuilt React Native
limitations, local-network permission resets, and platform-specific
local origins.
- Added a prerequisite directing operators to review these
troubleshooting notes before running the local receipt workflow.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The ownership recheck added two codes the operation never declares: a store
fault surfaced as VERIFICATION_FAILED (502) and a purchase bound mid-read as
CONFLICT (409). The Commerce Protocol conformance runner rejects any code a
manifest does not list, so a real run against this build would have failed on
its own specification.

A store fault now answers INTERNAL_ERROR, which the operation does declare and
which is what an unreachable dependency is. The mid-read binding no longer
errors at all: the re-read keeps only the rows this pass actually reconfirmed,
so a purchase that appeared after the recheck is simply absent, which is what
fail-close already means for this operation.

The bound-purchase read cap also answered INVALID_REQUEST, blaming the caller
for stored rows drifting past a limit that binding enforces. It is ours, so it
reports INTERNAL_ERROR too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Re-runs the 170 interoperability checks against the committed sources and
moves the storeAccess anchor to the line the action now starts on. The
description picks up the one behaviour that changed: a purchase bound after
the recheck pass is absent from the answer instead of failing the read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… an account

Three defects the review found, none of them new to this branch.

An Amazon receipt the account no longer holds kept its slot in the 20-purchase
bind cap for good: nothing ever cleared `appUserId`, and there is no unbind
operation, so twenty refunds answered `bound: false` for the twenty-first
legitimate purchase — indistinguishable from unknown evidence under §4.4.
A CANCELED or INAUTHENTIC Amazon verdict now releases the binding, in every
lane and before the unchanged-verdict return, since a resubscribe issues a new
receipt id. Horizon keeps its binding: its rejection is a point-in-time answer
on a row keyed by user and sku, which the same customer reuses.

Subscription erasure meant something different from purchase erasure. Purchases
treat it as unlinking the previous owner; subscriptions refused every later bind
forever, and a token rotation could carry one row's marker onto the other row's
live binding and revoke it. Both handlers now bind and clear the marker as
purchases do, the pending-erasure gate is untouched, and a live binding survives
a merge instead of being overwritten by a marker.

The entitlements read keeps failing closed. An earlier attempt here omitted an
un-integrated store instead; a project cannot hold bound rows for a store it
never integrated, so the only case that fired was a store disabled after it had
granted access — a partial answer the specification forbids.

Also moves three shared components out of the docs `pages/` tree, which holds
route files only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…verdict

Releasing the binding the moment a store said CANCELED changed what the
entitlements read means: a later positive recheck could no longer restore
access, and the interop run proved the two implementations had diverged on it.

Reclaim at the point of contention instead. When the twenty-first binding
arrives, IAPKit first releases the caller's own bound purchases the store no
longer honours, and refuses only if none can be freed. A row keeps its binding
until the cap is actually contended, so the read is untouched, and refunded
receipts can no longer hold an account's slots for good.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Re-runs the 170 interoperability checks against the committed sources and moves
the erasure anchor to the line its function now starts on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`playwright install --with-deps` runs `apt-get update`, which fails whenever
Google's Chrome repository serves an index whose hash does not match what its
Release file promises. That happened today and failed the docs E2E and kit
build jobs twice with nothing wrong in the tree.

Playwright downloads its own chromium, so the runner image's Chrome apt source
is dead weight here. Remove it before the install.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…pping it

The recheck loop passed over a bound row whose stored store is neither amazon
nor horizon. Binding admits only those two, so such a row is state the read
cannot classify — exactly what SPEC "Fail-close" says must fail the operation
rather than be answered around. It now raises INTERNAL_ERROR.

Also corrects two claims. The handler comment still described the per-store
skip that was reverted, and the documentation told operators to release the
rows of a disabled store: no such lever exists, because the recheck throws
before anything can mark them invalid, so the bind cap never reclaims them and
only eraseUser clears the binding. Re-enabling the store is the way back.

Tests: the reclaim case now pins that the live rows keep their binding, the
fail-closed case names the error and proves the read stopped before re-reading
ownership, and a duplicated rate-limit case is gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
hyochan and others added 3 commits September 10, 2026 03:36
…-ups

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…es not exist

It offered "release the rows" and then said nothing can release them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@hyochan
hyochan merged commit 6e9eebe into main Sep 9, 2026
48 checks passed
@hyochan
hyochan deleted the codex/commerce-protocol-review branch September 9, 2026 19:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💨 ci Cloud integration 📖 documentation Improvements or additions to documentation 🎯 feature New feature ⬡ gql kit IAPKit (receipt-validation SaaS) 🕶️ meta

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant