Repository navigation
Native iOS app: merged App Store and Stripe entitlements (backend) - #141
Conversation
app_store_subscriptions records each App Store subscription with its product, expiry, environment, revocation and family-sharing state, written only by the service role through apply_app_store_transaction, which orders updates by expiry and signedDate and records notificationUUIDs once. effective_entitlement() returns the highest active tier across Stripe, promo, trial, legacy and App Store sources; with no App Store row it keeps the rules _shared/entitlements.ts applied. Product ids are placeholders until App Store Connect has the real ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
_shared/app-store.ts checks signed notifications, transactions and renewal info the way Apple's App Store Server Library does offline: ES256, a three-certificate x5c chain whose intermediate is signed by Apple Root CA - G3 (pinned as a PEM constant), Apple's leaf and intermediate OIDs, validity at signedDate, then bundle id, environment and, in Production, the app's Apple id. Roots are an argument, never configuration; tests sign with a chain they generate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
getSubscriptionInfo, which the ai, ai-stream and voice quota checks use, now calls effective_entitlement() instead of recomputing the tier from user_settings and subscriptions, and still fails closed when the plan cannot be read. Handler tests answer that RPC through one helper; the tier rules they used to pin moved to the pgTAP suite. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
app-store-notifications takes App Store Server Notifications V2, verifies the payload and the transaction and renewal info inside it, and applies them through apply_app_store_transaction, which makes Apple's retries idempotent on notificationUUID. app-store-purchase lets the signed-in app send the signed transaction StoreKit returned; it is applied only when its appAccountToken is the caller's user id, and the answer is the plan. Both wait with 503 until APP_STORE_BUNDLE_ID (and APP_STORE_APP_APPLE_ID for Production) are set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
useSubscription, the settings store's Pro-model check, and the app's plan, default-model and feature-config reads all call effective_entitlement through readEntitlement in @debatelab/core, so an App Store subscription unlocks the website too. pro-access.ts, which recomputed Pro access from user_settings and Stripe rows, is gone. Basic gets its label in en, es and hu. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A yearly-to-monthly upgrade is charged later but expires sooner, so apply_app_store_transaction now orders transactions by purchaseDate, not expiry. account_deletion_plan reports whether Apple may still bill the user (not revoked, auto-renew not off, ended under 60 days ago), and the account function returns it so the app can send the user to Apple's subscription settings. Both changes came from an automated security review of this branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Billing in docs/architecture.md now describes effective_entitlement, the App Store endpoints and the deletion caveat; supabase/README.md lists both functions, their configuration names, the placeholder products and that deploying them needs Adam's authorization. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The security-hardening check looked for the old user_settings branch in _shared/entitlements.ts; it now pins the NO_ACCESS fallback the RPC read returns when the plan is missing or unreadable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ai, ai-stream, voice and the website now read effective_entitlement and fail closed without it, so the migration has to reach a project before those functions or the frontend do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…/08a-entitlements
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Note 🎁 Summarized by CodeRabbit Free
Comment |
|
Codex review r1 at c524ed2 (gpt-6.1-sol, xhigh): 3 findings, accepted; fixes in progress.
|
Hydration now awaits the plan RPC before it applies the row. If trial user A's load was still waiting when B signed in, it replaced B's local settings with A's and saved A's migrated feature_configs to B's row, because the save looked up the signed-in user again. Hydration now reads the account once and passes it to every write, and a generation counter that reset and each new hydration bump makes a superseded run stop after every await. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A signature segment that is not base64url made atob throw InvalidCharacterError, and a signed date past Date's range made toISOString throw RangeError. Neither was the verifier's error type, so both functions answered 500 instead of their documented 400. Both now raise a malformed verification error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a customer upgrades within the subscription group, Apple re-signs the replaced transaction with isUpgraded. The verifier dropped that flag, so if the re-signed copy arrived before the upgrade itself, the replaced plan kept granting access until its old expiry. The verifier now passes isUpgraded through, apply_app_store_transaction stores it in a new upgraded column, and effective_entitlement skips upgraded rows, as StoreKit's currentEntitlements does. The account deletion flag still counts them, because Apple keeps billing the subscription after an upgrade. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Round-1 fixes pushed: 71ee8dd (finding 1: settings hydration drops a run an account switch overtook), 04e9abf (finding 2: a transaction Apple marks isUpgraded grants nothing; new upgraded column, effective_entitlement skips it, deletion flag still counts it), 6f58fc2 (finding 3: undecodable signature or date returns 400, not 500). Local at 04e9abf: npm run verify exit 0 (vitest 312, collaboration 30, edge 371 passed, smoke 30), database gate 502 pgTAP PASS, knip and eslint apps/mobile exit 0. Codex r2 next. |
|
Codex review r2 at 04e9abf (gpt-6.1-sol, xhigh): no findings. Checked the three r1 fixes, auth, RLS, idempotency, transaction and renewal ordering, fail-closed reads, and migration/function/pgTAP consistency; 46 focused Deno tests passed. Ready, but held from merging into feat/native-ios on purpose. Every tier read (website and app) moves to the effective_entitlement RPC, which staging doesn't have yet (staging syncs from main). The Expo Go app runs against staging, so merging first would fail closed and turn off AI there. Order needed: the migration on staging, then ai, ai-stream, voice and account functions, then merge; production follows the same order with Adam's authorization. Open items: real App Store product ids and notification URLs, APP_STORE_* function settings, whether production accepts Sandbox purchases, and Basic's 20/day placeholder. |
|
Round-1 fixes pushed at 04e9abf: 71ee8dd drops a settings hydration an account switch overtook, 6f58fc2 reports undecodable App Store data as malformed (400), 04e9abf grants nothing for a transaction Apple marks upgraded. The 8a agent's final local checks at 04e9abf passed (report in .verify/native-ios-research/pr8a-report-final.md). Codex r2 (xhigh) running. |
|
Codex review r2 at 04e9abf (gpt-6.1-sol, xhigh): no findings. All three r1 fixes checked (account-switch guard and captured-user writes; isUpgraded propagation, storage and exclusion; malformed-signature and invalid-date handling); 38 focused Deno tests and 7 settings probes passed. Merging into feat/native-ios. Nothing is deployed: production needs Adam's authorization, and the App Store product ids and secrets are placeholders until App Store Connect exists (see the PR description and .verify/native-ios-research/pr8a-report-final.md for the open items: family-shared transactions without appAccountToken, restore after delete and re-signup, unknown products consuming the notification UUID). |
Brings in Sparring (#138), development-build prep (#142), App Store entitlements (#141) and offline reading (#139). - The push migration becomes 20261014130000_push_tokens.sql so it sorts after 20261014123154_app_store_entitlements.sql. - auth.tsx and _layout.tsx keep both the push wiring (release on sign-out, refresh on launch and language change) and setCacheOwner/watchNetwork. - package-lock.json is feat's lock plus `npm install` for expo-notifications. - app.config.ts is unchanged: #142 already strips aps-environment from development builds. The mobile guide now says so, and that real push needs a profile with push and an APNs key from Adam's Apple Developer account. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Server-side entitlements that combine Stripe, App Store, promo and trial into one plan, plus the App Store purchase and notification endpoints. Backend only: the StoreKit UI waits for the development build and App Store Connect. Nothing is deployed.
Deploy order (read before this reaches main)
ai,ai-stream,voiceandaccountfunctions.Without the migration, the AI functions fail closed and every signed-in user loses AI. The staging sync from
maincounts as a deploy.What's in it
app_store_products: product id tobasicorpro. The ids are placeholders until App Store Connect has the real ones.app_store_subscriptions: one row per original transaction. Users read their own rows; only the service role writes.app_store_notifications: idempotency on notificationUUID.effective_entitlement(user_id): the highest active tier across Stripe, App Store, promo, trial and legacy. A missing settings row still fails closed. It runs with the caller's privileges, so it reveals nothing about other users._shared/entitlements.ts(ai, ai-stream, voice), the website'suse-subscriptionand settings store, and the app's subscription, feature-config and settings.packages/core/src/ai/pro-access.tsis removed.app-store-notifications. It handles V2 notifications and applies the signed transaction only when it was charged later than the stored one. An exact replay and a refund of an older period change nothing.app-store-purchase. It's authenticated, and the signed appAccountToken must equal the caller's user id, otherwise it returns 403.account_deletion_planand theaccountfunction reportappStoreSubscriptionActive, so the app can send the user to Apple's subscription settings.Checks (local; GitHub Actions is out of minutes)
At this head (after merging feat), each run under the shared lock:
npm run verify: exit 0. Vitest 311/311, collaboration 30/30, edge green, build OK, smoke 30/30../.superset/db-gate.sh --db-only: exit 0, pgTAP 20 files and 498 tests. The effective-tier cases cover:npm run typecheck, the mobile typecheck andnpx knip: exit 0.not_configuredand GET 405.Needs Adam
APP_STORE_BUNDLE_ID(com.adam.debatelab),APP_STORE_ENVIRONMENTSandAPP_STORE_APP_APPLE_ID.Known limits
Codex review rounds (xhigh, billing is release-critical) follow as comments.
🤖 Generated with Claude Code