Skip to content

feat(migrate): grants revoke endpoint and revoked grant state - #406

Merged
ABB65 merged 4 commits into
mainfrom
feat/migrate-revoke
Oct 3, 2026
Merged

ABB65 merged 4 commits into
mainfrom
feat/migrate-revoke

Conversation

@ABB65

@ABB65 ABB65 commented Oct 3, 2026

Copy link
Copy Markdown
Member

What

POST /api/migrate/grants/revoke (S2S, signed, single-use jti): Migrate's half of a refund or failed delivery.

  • Cancels only the subscription the grant is bound to (redeemed_subscription_id), then marks the grant revoked (migration 045: revoked_at, revoked_reason).
  • Idempotent: repeat on a revoked grant cancels nothing and answers revoked. Polar failure -> 502, grant stays live, jti given back so Migrate retries.
  • Status route answers revoked (installed fact kept); checkout/provision refuse with 409 migrate.grant_revoked; install-url gated by state.
  • Refund of a non-bound (duplicate) payment never revokes: the request carries no payment id, nothing else is cancelled; a test pins it. Payments arriving after a revoke are alarmed duplicates (ALARM payment after revoke), no account write.
  • @contentrain/types 1.46.0.

Tests

New route test (9), plus status/checkout/provision/bundle-subscription cases and a contract test (postgres:16). Full unit/integration/nuxt/contract green; eslint, typecheck clean.

ABB65 added 4 commits October 3, 2026 19:23
Pin @contentrain/types 1.45.0 (renewal_cents) and drop the local extra
type. Supabase auth refuses to hand an email account with another GitHub
identity to a stranger. The webhook carries the checkout id: a second
subscription for a grant, or a payment from a stale checkout, raises an
ALARM (Polar cannot expire a checkout); a redeemed bundle without a
subscription id alarms in the reconciler.
…ount

subscription.created/updated ask first (isDuplicateBundleSubscription) and
skip every account write for a second subscription on a bundle grant, so
refunding it cannot cancel the valid plan. Supabase identity check reads
the user identities instead of the last sign-in provider metadata.
POST /api/migrate/grants/revoke (S2S, purpose 'revoke'): cancels only the
subscription the grant is bound to, then marks the grant revoked (migration
045: revoked_at, revoked_reason). Idempotent; a Polar failure leaves the
grant live (502) and gives the jti back so Migrate can retry.

A refund of a payment that is not the bound subscription (duplicate
checkout) never revokes: the request carries no payment id and nothing
else is cancelled. Revoked grants answer state 'revoked' on status, refuse
checkout/provision (409 migrate.grant_revoked) and treat later payments as
alarmed duplicates. @contentrain/types 1.46.0.
# Conflicts:
#	package.json
#	pnpm-lock.yaml
#	server/utils/migrate-bundle-subscription.ts
#	tests/unit/migrate-bundle-subscription.test.ts
@ABB65

ABB65 commented Oct 3, 2026

Copy link
Copy Markdown
Member Author

t6 review @ 4bff14e — ONAY, with two findings to track

Evidence at this head: studio CI ci pass (13m56s) and postgres-lineage pass. Local pnpm test:ci: integration 52 files / 487 tests, nuxt 43 / 274, and unit all green except 9 files (28 tests) that timed out under load in the first full run; rerun with --testTimeout=120000 --maxWorkers=2 those 9 files pass (95/95). eslint on changed files and vue-tsc --noEmit clean. Migration 045 sits after 044 on main with no clash; lineage job green.

Checks asked for:

  1. Duplicate / unbound refund. Studio's request carries only order_id + reason, so Studio cannot tell which payment was refunded. The test pins what Studio can: only redeemed_subscription_id is ever cancelled, and a payment id smuggled into the token is ignored. "A duplicate refund does not revoke" therefore holds only if Migrate does not call revoke for it. A call for a refunded duplicate would revoke the valid grant. Not Studio's bug, but the Migrate caller (not written yet) must carry that rule and a test; the comment in migrate-revoke.ts should say so.
  2. Idempotency / 502. Polar failure: nothing marked, grant live, jti released (status >= 500), retry works. Repeat on a revoked grant: revoked, no Polar call, nothing re-marked, first reason kept. Never-paid grant: revoked with no Polar call.
  3. Payment after revoke. isDuplicateBundleSubscription returns true for a revoked grant (ALARM), and the webhook skips the account write on created/updated; unit test present.
  4. Lineage / full CI. See evidence above.

Findings (not blocking this merge, fix before Migrate wires the caller):

  • Already-ended subscription is not tolerated. cancelSubscription is polar.subscriptions.revoke; if the subscription already ended (operator refunded and Polar ended it, or a prior call revoked it but markMigrateGrantRevoked then failed), Polar errors, the route answers 502 forever and the grant stays live. The contract describes subscription_canceled: false for "had already ended", but the code can only produce it for a never-paid or already-revoked grant. Treat Polar's already-ended error (or a status read showing ended) as success.
  • Narrow race: a revoke landing between the webhook's duplicate check and upsertPaymentAccount leaves that account stored; the redeem step then returns on the revoked flag, but the account row stays. The next subscription.canceled from the revoke cleans it up.

@ABB65
ABB65 merged commit bcdde54 into main Oct 3, 2026
2 checks passed
@ABB65
ABB65 deleted the feat/migrate-revoke branch October 3, 2026 17:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant