docs: harden auth identity payment design

This commit is contained in:
IanShaw027
2026-04-20 14:41:12 +08:00
parent b6751f7ebc
commit 584ded2182
2 changed files with 199 additions and 37 deletions

View File

@@ -2,7 +2,7 @@
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` (recommended) or `superpowers:executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. > **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` (recommended) or `superpowers:executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Rebuild the auth identity, profile binding, payment routing, and OpenAI advanced scheduler foundation on top of a clean `origin/main` branch while preserving historical compatibility for existing email users and historical LinuxDo users. **Goal:** Rebuild the auth identity, profile binding, payment routing, and OpenAI advanced scheduler foundation on top of a clean `origin/main` branch while preserving historical compatibility for existing email users, existing LinuxDo users, historical LinuxDo/WeChat/OIDC synthetic-email users, and historical WeChat `openid`-only records.
**Architecture:** A unified identity foundation centered on durable provider subjects (`email`, `linuxdo`, `oidc`, `wechat`) and transactional pending-auth sessions; backend-owned payment source routing behind stable frontend methods (`alipay`, `wxpay`); compatibility-first migration/backfill before feature enablement. **Architecture:** A unified identity foundation centered on durable provider subjects (`email`, `linuxdo`, `oidc`, `wechat`) and transactional pending-auth sessions; backend-owned payment source routing behind stable frontend methods (`alipay`, `wxpay`); compatibility-first migration/backfill before feature enablement.
**Tech Stack:** Go, Gin, Ent, PostgreSQL, Redis, Vue 3, Pinia, TypeScript, Vitest, pnpm. **Tech Stack:** Go, Gin, Ent, PostgreSQL, Redis, Vue 3, Pinia, TypeScript, Vitest, pnpm.
@@ -10,8 +10,9 @@
## Non-Negotiable Product Rules ## Non-Negotiable Product Rules
- [ ] Preserve login continuity for existing email users and historical LinuxDo users. - [ ] Preserve login continuity for existing email users, existing LinuxDo users, and historically migrated third-party users.
- [ ] During migration, backfill historical LinuxDo synthetic-email users into explicit LinuxDo identities before first post-upgrade login. - [ ] During migration, backfill historical LinuxDo/WeChat/OIDC synthetic-email users into explicit third-party identities before first post-upgrade login whenever deterministic recovery is possible.
- [ ] During migration, surface historical WeChat `openid`-only records through explicit migration reports and remediation rules; do not silently reinterpret them as valid canonical identities.
- [ ] Keep existing email login and add third-party login/bind for `linuxdo`, `oidc`, and `wechat`. - [ ] Keep existing email login and add third-party login/bind for `linuxdo`, `oidc`, and `wechat`.
- [ ] On first third-party login: - [ ] On first third-party login:
- identity exists: direct login. - identity exists: direct login.
@@ -37,6 +38,11 @@
- non-WeChat browser uses Open/QR login. - non-WeChat browser uses Open/QR login.
- canonical identity uses `unionid`. - canonical identity uses `unionid`.
- when `unionid` is unavailable, fail the login/bind flow under the approved option-1 policy. - when `unionid` is unavailable, fail the login/bind flow under the approved option-1 policy.
- [ ] OIDC rules:
- browser authorization-code flow always uses PKCE `S256`.
- discovery issuer and ID token `iss` must match exactly.
- `userinfo.sub` must match ID token `sub` when UserInfo is used.
- upstream `email_verified` does not satisfy local email verification.
- [ ] Payment UI rules: - [ ] Payment UI rules:
- user-facing methods stay `支付宝` and `微信支付`. - user-facing methods stay `支付宝` and `微信支付`.
- backend decides whether each method routes to official provider instance or EasyPay. - backend decides whether each method routes to official provider instance or EasyPay.
@@ -49,20 +55,26 @@
- WeChat H5: MP/JSAPI first, fallback to H5 pay. - WeChat H5: MP/JSAPI first, fallback to H5 pay.
- non-WeChat H5: H5 pay, or prompt to open in WeChat when unavailable. - non-WeChat H5: H5 pay, or prompt to open in WeChat when unavailable.
- [ ] Payment success pages are informational only; actual fulfillment depends on webhook or server-side reconciliation. - [ ] Payment success pages are informational only; actual fulfillment depends on webhook or server-side reconciliation.
- [ ] WeChat in-app payment requiring `openid` must use a dedicated server-backed payment OAuth resume flow rather than frontend-only recovery state.
- [ ] OpenAI advanced scheduler is available but default-disabled. - [ ] OpenAI advanced scheduler is available but default-disabled.
## Hard Technical Constraints From Audit ## Hard Technical Constraints From Audit
- [ ] Browser-based third-party auth must use Authorization Code + PKCE `S256`. - [ ] Browser-based third-party auth must use Authorization Code + PKCE `S256`.
- [ ] PKCE must not be admin-configurable off for browser authorization-code providers.
- [ ] OIDC identity primary key must be `(issuer, subject)`, not email. - [ ] OIDC identity primary key must be `(issuer, subject)`, not email.
- [ ] Email equality must never auto-link accounts. - [ ] Email equality must never auto-link accounts.
- [ ] Bind-existing-account must require explicit local re-authentication and TOTP verification when enabled. - [ ] Bind-existing-account must require explicit local re-authentication and TOTP verification when enabled.
- [ ] Bind-current-user must originate from an already-authenticated local user and preserve explicit bind intent across callback completion.
- [ ] OAuth redirect URI must be fixed server config, exact-match, and never derived from user input. - [ ] OAuth redirect URI must be fixed server config, exact-match, and never derived from user input.
- [ ] User-supplied redirect may only choose a normalized same-origin internal route after completion. - [ ] User-supplied redirect may only choose a normalized same-origin internal route after completion.
- [ ] WeChat canonical identity must be `unionid`; `openid` remains channel/app-scoped support data only. - [ ] WeChat canonical identity must be `unionid`; `openid` remains channel/app-scoped support data only.
- [ ] Every payment order must snapshot the selected provider instance and reuse that exact instance for callback verification, reconciliation, refund, and audit. - [ ] Every canonical identity uniqueness rule must include provider namespace (`provider_key`) consistently.
- [ ] Callback completion must use backend session completion or a one-time opaque exchange code that is short-lived, one-time, browser-session-bound, `POST`-redeemed, and unusable as a bearer token.
- [ ] Every payment order must snapshot the selected provider instance plus the order-time verification inputs required for callback verification, reconciliation, refund, and audit.
- [ ] Frontend must not receive first-party bearer tokens through callback URL fragments in the rebuilt flow. - [ ] Frontend must not receive first-party bearer tokens through callback URL fragments in the rebuilt flow.
- [ ] Public payment result polling must not expose order data by raw `out_trade_no` alone; use authenticated lookup or signed opaque result token. - [ ] Public payment result polling must not expose order data by raw `out_trade_no` alone; use authenticated lookup or signed opaque result token.
- [ ] WeChat Pay webhook handling must verify signature, decrypt payload, and compare `appid`, `mchid`, `out_trade_no`, `amount`, `currency`, and provider trade state against the order snapshot before fulfillment.
## Baseline Notes ## Baseline Notes
@@ -100,6 +112,8 @@
- [ ] `backend/internal/service/payment_order.go` - [ ] `backend/internal/service/payment_order.go`
- [ ] `backend/internal/service/payment_order_lifecycle.go` - [ ] `backend/internal/service/payment_order_lifecycle.go`
- [ ] `backend/internal/service/payment_fulfillment.go` - [ ] `backend/internal/service/payment_fulfillment.go`
- [ ] `backend/internal/service/payment_resume_service.go`
- [ ] `backend/internal/service/payment_resume_service_test.go`
- [ ] `backend/internal/service/openai_account_scheduler.go` - [ ] `backend/internal/service/openai_account_scheduler.go`
- [ ] `backend/internal/handler/auth_pending_identity_flow.go` - [ ] `backend/internal/handler/auth_pending_identity_flow.go`
- [ ] `backend/internal/handler/auth_linuxdo_oauth.go` - [ ] `backend/internal/handler/auth_linuxdo_oauth.go`
@@ -148,9 +162,10 @@
- `users.last_active_at` - `users.last_active_at`
- grant-tracking columns/tables required to prevent double-award - grant-tracking columns/tables required to prevent double-award
- [ ] Add uniqueness/index rules: - [ ] Add uniqueness/index rules:
- one canonical identity per `(provider, provider_subject)` - one canonical identity per `(provider, provider_key, provider_subject)`
- one channel record per `(provider, provider_channel, provider_app_id, provider_channel_subject)` - one channel record per `(provider, provider_channel, provider_app_id, provider_channel_subject)`
- one adoption decision per pending session - one adoption decision per pending session
- [ ] Model `pending_auth_sessions` so immutable upstream claims and mutable local flow state are stored separately; do not reintroduce a mixed `metadata` catch-all.
- [ ] Preserve null-safe compatibility defaults so historical rows remain readable before backfill finishes. - [ ] Preserve null-safe compatibility defaults so historical rows remain readable before backfill finishes.
- [ ] Add explicit rollback blocks only where safe; never repeat the destructive pattern observed in old `112_update_pending_auth_sessions.sql`. - [ ] Add explicit rollback blocks only where safe; never repeat the destructive pattern observed in old `112_update_pending_auth_sessions.sql`.
@@ -160,8 +175,10 @@
- existing email users into `auth_identities(provider=email, provider_subject=normalized_email)` - existing email users into `auth_identities(provider=email, provider_subject=normalized_email)`
- historical LinuxDo users into `auth_identities(provider=linuxdo, provider_subject=linuxdo_subject)` - historical LinuxDo users into `auth_identities(provider=linuxdo, provider_subject=linuxdo_subject)`
- historical synthetic-email LinuxDo users into explicit LinuxDo identity rows by parsing legacy email mode and legacy provider metadata - historical synthetic-email LinuxDo users into explicit LinuxDo identity rows by parsing legacy email mode and legacy provider metadata
- historical synthetic-email WeChat users into explicit WeChat identities where `unionid` or equivalent deterministic provider identity is recoverable
- historical synthetic-email OIDC users into explicit OIDC identities where deterministic provider identity is recoverable
- profile/channel rows from historical `user_external_identities`-style data when present in upgraded databases - profile/channel rows from historical `user_external_identities`-style data when present in upgraded databases
- [ ] Write migration report output in `backend/internal/repository/auth_identity_migration_report.go` so production can inspect unmatched rows instead of silently skipping them. - [ ] Write migration report output in `backend/internal/repository/auth_identity_migration_report.go` so production can inspect unmatched rows, `openid`-only WeChat rows, and non-deterministic synthetic-email rows instead of silently skipping them.
- [ ] Set `signup_source` and provider provenance when recoverable from historical data. Do not flatten everything to `email`. - [ ] Set `signup_source` and provider provenance when recoverable from historical data. Do not flatten everything to `email`.
### Task 3. Provider default grant and scheduler config migration ### Task 3. Provider default grant and scheduler config migration
@@ -173,6 +190,7 @@
- profile avatar storage columns/settings - profile avatar storage columns/settings
- [ ] Implement `backend/migrations/111_payment_routing_and_scheduler_flags.sql` for: - [ ] Implement `backend/migrations/111_payment_routing_and_scheduler_flags.sql` for:
- stable payment method to provider-instance routing - stable payment method to provider-instance routing
- visible-method normalization from historical `supported_types`, `payment_mode`, and legacy aliases such as `wxpay_direct`
- admin exclusivity flags for `alipay` and `wxpay` - admin exclusivity flags for `alipay` and `wxpay`
- advanced scheduler enable flag defaulting to disabled - advanced scheduler enable flag defaulting to disabled
@@ -213,6 +231,7 @@
- update `last_login_at` and `last_active_at` - update `last_login_at` and `last_active_at`
- [ ] Add repository contract coverage in `backend/internal/repository/user_profile_identity_repo_contract_test.go`. - [ ] Add repository contract coverage in `backend/internal/repository/user_profile_identity_repo_contract_test.go`.
- [ ] Enforce dual-write for email registration/login so `users.email` and `auth_identities(provider=email, ...)` stay consistent from this phase onward. - [ ] Enforce dual-write for email registration/login so `users.email` and `auth_identities(provider=email, ...)` stay consistent from this phase onward.
- [ ] Add repository coverage proving `last_login_at` and `last_active_at` use the required field names and are not silently replaced by derived `last_used_at` logic.
### Task 6. Rebuild transactional pending-auth service ### Task 6. Rebuild transactional pending-auth service
@@ -223,8 +242,15 @@
- bind pending identity to existing account after password/TOTP re-auth - bind pending identity to existing account after password/TOTP re-auth
- apply configured provider defaults on the correct trigger only once - apply configured provider defaults on the correct trigger only once
- store provider nickname/avatar candidates and user opt-in replacement decisions independently - store provider nickname/avatar candidates and user opt-in replacement decisions independently
- [ ] Implement callback completion so pending auth can finish through backend session completion or a one-time exchange code:
- short TTL
- one-time use
- browser-session binding
- `POST` redemption only
- safe mixed-version bridge to legacy pending-token aliases during rollout
- [ ] Keep pending session payload normalized: - [ ] Keep pending session payload normalized:
- provider identity fields live in typed columns/JSON structure - provider identity fields live in typed columns/JSON structure
- mutable local progression lives separately from immutable upstream claims
- avoid the old branchs mixed `metadata` and `upstream_identity_payload` ambiguity - avoid the old branchs mixed `metadata` and `upstream_identity_payload` ambiguity
- [ ] Do not call plain email registration helpers from this flow. The old feature branch bug where pending third-party signup fell back to `RegisterWithVerification` must not reappear. - [ ] Do not call plain email registration helpers from this flow. The old feature branch bug where pending third-party signup fell back to `RegisterWithVerification` must not reappear.
@@ -236,11 +262,13 @@
- `backend/internal/handler/auth_wechat_oauth.go` - `backend/internal/handler/auth_wechat_oauth.go`
- [ ] For OIDC: - [ ] For OIDC:
- require PKCE `S256`, `state`, and `nonce` - require PKCE `S256`, `state`, and `nonce`
- validate `iss`, `aud`, optional `azp`, `exp`, `nonce` - validate discovery issuer, `iss`, `aud`, optional `azp`, `exp`, and `nonce`
- verify `userinfo.sub == id_token.sub` when UserInfo is used
- persist canonical identity as `(issuer, sub)` - persist canonical identity as `(issuer, sub)`
- [ ] For WeChat: - [ ] For WeChat:
- MP flow in WeChat UA - MP flow in WeChat UA
- Open/QR flow outside WeChat UA - Open/QR flow outside WeChat UA
- website login uses authorization-code flow and persists channel/app binding
- persist channel identity by `(channel, appid, openid)` - persist channel identity by `(channel, appid, openid)`
- persist canonical identity by `unionid` - persist canonical identity by `unionid`
- hard-fail when `unionid` is absent under the approved product policy - hard-fail when `unionid` is absent under the approved product policy
@@ -253,6 +281,10 @@
- submit verified email - submit verified email
- choose create-new-account or bind-existing-account - choose create-new-account or bind-existing-account
- submit nickname/avatar replacement choices - submit nickname/avatar replacement choices
- [ ] Make bind-existing-account and bind-current-user flows explicit:
- no automatic linking on matching email
- fresh password/TOTP proof is scoped to the intended target account only
- no automatic metadata merge beyond explicitly selected nickname/avatar adoption
- [ ] Update `backend/internal/handler/auth_handler.go` and `backend/internal/handler/user_handler.go` to expose: - [ ] Update `backend/internal/handler/auth_handler.go` and `backend/internal/handler/user_handler.go` to expose:
- current bindings summary - current bindings summary
- start-bind endpoints for LinuxDo/OIDC/WeChat - start-bind endpoints for LinuxDo/OIDC/WeChat
@@ -312,6 +344,7 @@
- `frontend/src/api/auth.ts` - `frontend/src/api/auth.ts`
- `frontend/src/stores/auth.ts` - `frontend/src/stores/auth.ts`
- [ ] Replace any token-fragment bootstrap with backend session completion or one-time exchange code flow. - [ ] Replace any token-fragment bootstrap with backend session completion or one-time exchange code flow.
- [ ] During rollout, keep temporary compatibility readers for legacy pending-token aliases behind a bounded bridge contract and explicit removal step.
### Task 12. Rebuild profile account binding and avatar UX ### Task 12. Rebuild profile account binding and avatar UX
@@ -351,11 +384,17 @@
- frontend visible methods remain `alipay` and `wxpay` - frontend visible methods remain `alipay` and `wxpay`
- admin chooses which provider instance serves each method - admin chooses which provider instance serves each method
- runtime validation guarantees only one active source per visible method - runtime validation guarantees only one active source per visible method
- [ ] Add migration logic and tests to normalize historical provider-instance config:
- `supported_types`
- `payment_mode`
- legacy aliases such as `wxpay_direct`
- historical limit config
- [ ] Rebuild `backend/internal/service/payment_order.go` and `backend/internal/service/payment_order_lifecycle.go` so order creation snapshots: - [ ] Rebuild `backend/internal/service/payment_order.go` and `backend/internal/service/payment_order_lifecycle.go` so order creation snapshots:
- visible method - visible method
- selected provider instance id - selected provider instance id
- provider type - provider type
- provider capability mode - provider capability mode
- verification-critical provider fields needed for later callback/query/refund validation
- [ ] Rebuild `backend/internal/handler/payment_handler.go` for UX rules: - [ ] Rebuild `backend/internal/handler/payment_handler.go` for UX rules:
- Alipay PC: QR page - Alipay PC: QR page
- Alipay mobile: direct jump - Alipay mobile: direct jump
@@ -363,6 +402,11 @@
- WeChat H5 in WeChat: MP/JSAPI first, fallback to H5 - WeChat H5 in WeChat: MP/JSAPI first, fallback to H5
- WeChat H5 outside WeChat: H5 or “open in WeChat” prompt when unavailable - WeChat H5 outside WeChat: H5 or “open in WeChat” prompt when unavailable
- [ ] Never derive canonical return URL from `Referer`; use configured or signed internal callback targets only. - [ ] Never derive canonical return URL from `Referer`; use configured or signed internal callback targets only.
- [ ] Implement `backend/internal/service/payment_resume_service.go` so WeChat in-app payment OAuth resume is server-backed rather than localStorage-backed:
- create `oauth_required` resume context
- persist amount/order_type/plan_id/visible method/redirect/state
- redeem callback into same-origin internal resume target
- expire and consume resume context safely
### Task 15. Make fulfillment and reconciliation provider-instance-safe ### Task 15. Make fulfillment and reconciliation provider-instance-safe
@@ -370,6 +414,12 @@
- verification uses the orders original provider instance - verification uses the orders original provider instance
- webhook processing is idempotent by provider event id and internal order id - webhook processing is idempotent by provider event id and internal order id
- missed webhook recovery uses server-side provider query, not frontend success return - missed webhook recovery uses server-side provider query, not frontend success return
- [ ] For WeChat Pay specifically, enforce:
- fixed HTTPS `notify_url` with no query params
- no dependency on user login state
- signature verification before decrypt
- APIv3 decrypt before business parsing
- comparison of `appid`, `mchid`, `out_trade_no`, `amount`, `currency`, and trade state against the order snapshot
- [ ] Harden `frontend/src/views/user/PaymentResultView.vue` and `frontend/src/api/payment.ts` so result polling uses an authenticated order lookup or signed opaque token, not a raw public `out_trade_no` query. - [ ] Harden `frontend/src/views/user/PaymentResultView.vue` and `frontend/src/api/payment.ts` so result polling uses an authenticated order lookup or signed opaque token, not a raw public `out_trade_no` query.
### Task 16. Rebuild payment frontend views ### Task 16. Rebuild payment frontend views
@@ -378,6 +428,10 @@
- only two buttons are shown to user: `支付宝` and `微信支付` - only two buttons are shown to user: `支付宝` and `微信支付`
- frontend does not leak official-vs-EasyPay distinction - frontend does not leak official-vs-EasyPay distinction
- route-specific copy handles QR, jump, MP, H5 fallback correctly - route-specific copy handles QR, jump, MP, H5 fallback correctly
- [ ] Rebuild WeChat in-app payment resume UX around the server-backed resume context:
- handle `oauth_required`
- continue from same-origin resume target
- avoid long-lived localStorage as the source of truth
- [ ] Add or update: - [ ] Add or update:
- `frontend/src/views/user/__tests__/PaymentView.spec.ts` - `frontend/src/views/user/__tests__/PaymentView.spec.ts`
- `frontend/src/views/user/__tests__/PaymentResultView.spec.ts` - `frontend/src/views/user/__tests__/PaymentResultView.spec.ts`
@@ -416,16 +470,22 @@
- historical email-only user login after upgrade - historical email-only user login after upgrade
- historical LinuxDo user login after upgrade - historical LinuxDo user login after upgrade
- historical synthetic-email LinuxDo user login after upgrade - historical synthetic-email LinuxDo user login after upgrade
- historical synthetic-email WeChat user login after upgrade
- historical synthetic-email OIDC user login after upgrade
- historical WeChat `openid`-only rows are reported or explicitly remediated
- no retroactive grant replay during migration - no retroactive grant replay during migration
- first-bind grant fires once only when enabled - first-bind grant fires once only when enabled
- email identity dual-write stays consistent - email identity dual-write stays consistent
- bind-existing-account requires password and TOTP where configured - bind-existing-account requires password and TOTP where configured
- mixed-version callback token bridge works during rollout and is removable afterward
- historical payment config is normalized into visible-method routing without refund/query regression
- [ ] Add deploy sequencing note to release docs or internal runbook: - [ ] Add deploy sequencing note to release docs or internal runbook:
1. deploy schema and backfill release. 1. deploy schema and backfill release.
2. inspect migration report for unmatched rows. 2. inspect migration report for unmatched rows.
3. deploy backend identity/payment compatibility code. 3. deploy backend identity/payment compatibility code with exchange bridge and legacy token aliases still enabled.
4. deploy frontend callback/profile/payment UI. 4. deploy frontend callback/profile/payment UI using session completion, exchange code, and server-backed WeChat payment resume.
5. enable strict email-required signup or provider bind grants only after metrics are healthy. 5. remove legacy callback/token parsing after mixed-version window closes.
6. enable strict email-required signup or provider bind grants only after metrics are healthy.
### Task 20. Final verification and handoff ### Task 20. Final verification and handoff
@@ -449,6 +509,8 @@
- [ ] Run focused manual smoke checks: - [ ] Run focused manual smoke checks:
- email login with existing account - email login with existing account
- LinuxDo existing-account login after migration - LinuxDo existing-account login after migration
- WeChat synthetic-email account login after migration
- OIDC synthetic-email account login after migration
- third-party first login create-new-account path - third-party first login create-new-account path
- third-party first login bind-existing-account path - third-party first login bind-existing-account path
- first third-party bind with optional nickname/avatar replacement - first third-party bind with optional nickname/avatar replacement
@@ -456,6 +518,7 @@
- mobile Alipay jump - mobile Alipay jump
- PC WeChat QR - PC WeChat QR
- WeChat H5 MP/JSAPI path - WeChat H5 MP/JSAPI path
- WeChat in-app OAuth resume path
- non-WeChat H5 fallback path - non-WeChat H5 fallback path
- [ ] Commit final checkpoint: - [ ] Commit final checkpoint:
```bash ```bash
@@ -468,9 +531,9 @@
- [ ] No flow still relies on provider email equality for account linking. - [ ] No flow still relies on provider email equality for account linking.
- [ ] No flow still creates third-party users through plain email registration helpers. - [ ] No flow still creates third-party users through plain email registration helpers.
- [ ] No callback still returns first-party bearer tokens in URL fragments. - [ ] No callback still returns first-party bearer tokens in URL fragments.
- [ ] No callback completion path can be replayed as a bearer token substitute.
- [ ] No payment result view trusts provider return page as authoritative fulfillment. - [ ] No payment result view trusts provider return page as authoritative fulfillment.
- [ ] No webhook verification path selects provider credentials from “currently active config” instead of the order snapshot. - [ ] No webhook verification path selects provider credentials from “currently active config” instead of the order snapshot.
- [ ] Existing email users and historical LinuxDo users are covered by migration tests. - [ ] Existing email users, historical LinuxDo/WeChat/OIDC users, and `openid`-only WeChat remediation cases are covered by migration tests.
- [ ] Avatar adoption and deletion semantics are explicit and reversible. - [ ] Avatar adoption and deletion semantics are explicit and reversible.
- [ ] Grant timing is source-aware and one-time only. - [ ] Grant timing is source-aware and one-time only.

View File

@@ -20,10 +20,10 @@ This design includes:
- Profile binding management and avatar upload/delete - Profile binding management and avatar upload/delete
- Source-based initial grants for balance, concurrency, and subscriptions - Source-based initial grants for balance, concurrency, and subscriptions
- User management support for `last_login_at` and `last_active_at` sorting - User management support for `last_login_at` and `last_active_at` sorting
- Unified payment display methods (`alipay`, `wechat`) mapped to a single active backend source each - Unified payment display methods (`alipay`, `wxpay`) mapped to a single active backend source each
- Alipay and WeChat UX routing rules across PC, mobile, H5, and WeChat environments - Alipay and WeChat UX routing rules across PC, mobile, H5, and WeChat environments
- Admin settings for auth providers, source defaults, payment sources, and OpenAI advanced scheduling - Admin settings for auth providers, source defaults, payment sources, and OpenAI advanced scheduling
- Incremental migration and compatibility for existing email users and historical LinuxDo synthetic-email users - Incremental migration and compatibility for existing email users, existing LinuxDo users, historical LinuxDo/WeChat/OIDC synthetic-email users, and historical WeChat `openid`-only identity records
This design does not treat unrelated upstream merges, docs churn, or license changes from the old branch as required scope. This design does not treat unrelated upstream merges, docs churn, or license changes from the old branch as required scope.
@@ -32,9 +32,11 @@ This design does not treat unrelated upstream merges, docs churn, or license cha
### Auth and identity ### Auth and identity
- Existing email users remain valid and continue to log in with no manual action. - Existing email users remain valid and continue to log in with no manual action.
- Existing LinuxDo, OIDC, and WeChat users represented by historical third-party or synthetic-email data must remain recoverable during migration.
- Third-party first login behavior: - Third-party first login behavior:
- Existing bound identity: direct login - Existing bound identity: direct login
- Missing identity: start first-login flow - Missing identity: start first-login flow
- Browser-based third-party authorization-code login always uses PKCE `S256`; this is not an admin-toggleable feature.
- If `force_email_on_third_party_signup` is disabled, a first-login user may create an account without binding an email. - If `force_email_on_third_party_signup` is disabled, a first-login user may create an account without binding an email.
- If `force_email_on_third_party_signup` is enabled, the user must provide an email. - If `force_email_on_third_party_signup` is enabled, the user must provide an email.
- If the provided and verified email already exists: - If the provided and verified email already exists:
@@ -43,11 +45,25 @@ This design does not treat unrelated upstream merges, docs churn, or license cha
- allow "change email and continue registration" - allow "change email and continue registration"
- do not allow bypassing the email requirement - do not allow bypassing the email requirement
- Upstream provider email verification is not trusted as a local bound email. - Upstream provider email verification is not trusted as a local bound email.
- Matching upstream email must never auto-link to an existing local account.
- Linking to an existing local account is allowed only when:
- the user explicitly chooses that target account
- the target account passes fresh local re-authentication
- required TOTP verification succeeds
- New third-party bind initiated from profile must start from an already logged-in local account and preserve explicit bind intent end-to-end.
- `redirect_to` may only represent a normalized same-origin internal route. It must never contain a third-party URL and must never be derived from `Referer`.
- OIDC validation rules:
- canonical identity key is `issuer + sub`
- discovery issuer and ID token `iss` must match exactly
- `userinfo.sub` must match ID token `sub` when UserInfo is used
- upstream `email_verified` may improve UX copy but does not satisfy local email-binding requirements
- WeChat login chooses channel by environment: - WeChat login chooses channel by environment:
- in WeChat environment: `mp` - in WeChat environment: `mp`
- outside WeChat: `open` - outside WeChat: `open`
- WeChat primary identity key is `unionid`. - WeChat primary identity key is `unionid`.
- If a WeChat login/bind flow cannot produce `unionid`, the flow fails and no fallback `openid` identity is created. - If a WeChat login/bind flow cannot produce `unionid`, the flow fails and no fallback `openid` identity is created.
- Historical WeChat records that only contain `openid` are treated as migration-remediation cases, not as a valid long-term canonical identity model.
- WeChat website login uses authorization code flow, random `state`, and the provider channel/app binding must be persisted alongside the resolved identity.
### Profile adoption ### Profile adoption
@@ -85,7 +101,7 @@ This design does not treat unrelated upstream merges, docs churn, or license cha
- Frontend shows only two display methods: - Frontend shows only two display methods:
- `alipay` - `alipay`
- `wechat` - `wxpay`
- Users never choose between official providers and EasyPay explicitly. - Users never choose between official providers and EasyPay explicitly.
- Backend allows only one active source per display method at a time. - Backend allows only one active source per display method at a time.
- Alipay UX: - Alipay UX:
@@ -96,6 +112,10 @@ This design does not treat unrelated upstream merges, docs churn, or license cha
- non-WeChat H5: prefer H5 pay; if unavailable, tell the user to open in WeChat - non-WeChat H5: prefer H5 pay; if unavailable, tell the user to open in WeChat
- WeChat environment: prefer MP/JSAPI pay; if unavailable, fall back to H5 pay - WeChat environment: prefer MP/JSAPI pay; if unavailable, fall back to H5 pay
- Payment success is confirmed by backend order state, webhook, and/or query, not only frontend return. - Payment success is confirmed by backend order state, webhook, and/or query, not only frontend return.
- Frontend-visible labels remain `支付宝` and `微信支付`, while internal visible-method identifiers remain `alipay` and `wxpay`.
- Public result pages must not verify order state by exposing raw `out_trade_no`; they use authenticated lookup or a signed opaque result token instead.
- Payment callback or return URLs must be fixed same-origin internal targets. They must not be inferred from `Referer`.
- WeChat payment webhook handling must use a fixed HTTPS `notify_url` with no query parameters and must not depend on user login state.
### OpenAI advanced scheduling ### OpenAI advanced scheduling
@@ -105,9 +125,9 @@ This design does not treat unrelated upstream merges, docs churn, or license cha
## Architecture ## Architecture
Keep `users` as the account owner table and move login identities, channel mappings, pending auth state, and first-bind grant idempotency into dedicated tables and services. Keep email login working while progressively introducing unified identity reads and writes. Keep `users` as the account owner table and move login identities, channel mappings, pending auth state, callback completion state, and first-bind grant idempotency into dedicated tables and services. Keep email login working while progressively introducing unified identity reads and writes.
Payment uses a similar split between user-visible display methods and backend provider sources. Frontend works only with stable display methods while backend resolves to the currently active source and capability matrix. Payment uses a similar split between user-visible display methods and backend provider sources. Frontend works only with stable display methods while backend resolves to the currently active source and capability matrix, and stores enough order-time snapshot data to survive later provider-config changes.
Compatibility is a first-class concern: migrations are additive, reads are compatibility-aware, and rollout must tolerate existing `main` data and short-lived frontend/backend version skew. Compatibility is a first-class concern: migrations are additive, reads are compatibility-aware, and rollout must tolerate existing `main` data and short-lived frontend/backend version skew.
@@ -148,9 +168,9 @@ Uniqueness:
Rules: Rules:
- email identity uses canonicalized local email - email identity uses canonicalized local email
- LinuxDo uses stable provider subject - LinuxDo uses stable provider subject under the configured provider namespace
- OIDC uses stable issuer + subject - OIDC uses stable issuer + subject, with issuer namespace represented consistently through `provider_key` and `issuer`
- WeChat uses `unionid` as canonical subject - WeChat uses `unionid` as canonical subject under the configured Open Platform namespace
### `auth_identity_channels` ### `auth_identity_channels`
@@ -189,9 +209,12 @@ Fields:
- `target_user_id` - `target_user_id`
- `redirect_to` - `redirect_to`
- `resolved_email` - `resolved_email`
- `pending_password_hash` - `registration_password_hash`
- `upstream_identity_payload` - `upstream_identity_claims`
- `metadata` - `local_flow_state`
- `browser_session_key`
- `completion_code_hash`
- `completion_code_expires_at`
- `email_verified_at` - `email_verified_at`
- `password_verified_at` - `password_verified_at`
- `totp_verified_at` - `totp_verified_at`
@@ -205,19 +228,33 @@ Responsibilities:
- persist nickname/avatar suggestions - persist nickname/avatar suggestions
- persist explicit adoption decisions - persist explicit adoption decisions
- survive navigation between auth pages - survive navigation between auth pages
- support mixed-version rollout through short-lived legacy token aliases when required
Security rules:
- callback completion uses backend session completion or a one-time exchange code
- exchange codes are short-lived, one-time, bound to browser session and pending session, and redeemed via `POST`
- exchange codes must not behave as bearer tokens and must not be logged, stored in URL fragments, or reused after redemption
- `local_flow_state` stores mutable local progression only; immutable upstream claims remain in `upstream_identity_claims`
### `identity_adoption_decisions` ### `identity_adoption_decisions`
Persists user adoption preference for a specific identity. Persists user adoption preference collected during a pending-auth flow and resolved onto the bound identity.
Fields: Fields:
- `pending_auth_session_id`
- `identity_id` - `identity_id`
- `adopt_display_name` - `adopt_display_name`
- `adopt_avatar` - `adopt_avatar`
- `decided_at` - `decided_at`
- timestamps - timestamps
Rules:
- one adoption-decision row exists per pending session
- `identity_id` is filled once final account creation or bind succeeds
### `user_avatars` ### `user_avatars`
Stores the currently effective custom avatar. Stores the currently effective custom avatar.
@@ -265,6 +302,7 @@ WeChat-specific rule:
- `openid` never becomes the primary stored identity key - `openid` never becomes the primary stored identity key
- if only `openid` is available, login/bind fails with a configuration/identity error - if only `openid` is available, login/bind fails with a configuration/identity error
- historical `openid`-only records must be reported and either remediated during migration or explicitly blocked from silent auto-upgrade
## Core Flows ## Core Flows
@@ -285,6 +323,7 @@ WeChat-specific rule:
- Create `pending_auth_session` - Create `pending_auth_session`
- Frontend callback flow decides next action - Frontend callback flow decides next action
- Pending session creation stores immutable upstream claims separately from mutable local progress fields
Branches: Branches:
@@ -303,6 +342,7 @@ On new account creation:
- create `users` row - create `users` row
- create canonical third-party identity - create canonical third-party identity
- create or update canonical email identity when local email binding succeeds
- apply source signup grants - apply source signup grants
- apply adoption choices if selected - apply adoption choices if selected
@@ -310,21 +350,34 @@ On new account creation:
- current user starts bind flow - current user starts bind flow
- callback resolves to `bind_current_user` - callback resolves to `bind_current_user`
- bind intent is tied to the initiating local user session and cannot be re-targeted by email match
- bind canonical identity to current user - bind canonical identity to current user
- if configured and first bind for that provider, apply first-bind grants - if configured and first bind for that provider, apply first-bind grants
- present nickname/avatar replacement choice - present nickname/avatar replacement choice
### Bind existing account during first-login flow ### Bind existing account during first-login flow
- user explicitly selects bind-existing-account
- verify password for existing account - verify password for existing account
- if account requires TOTP, verify TOTP - if account requires TOTP, verify TOTP
- bind canonical identity to target account - bind canonical identity to target account
- optionally apply first-bind grants - optionally apply first-bind grants
- present nickname/avatar replacement choice - present nickname/avatar replacement choice
- no automatic profile or metadata merge occurs beyond explicitly selected nickname/avatar replacement
### Callback completion and exchange flow
- third-party callback never returns first-party bearer tokens in URL fragments
- callback completion uses either:
- backend session completion tied to the initiating browser session
- one-time opaque exchange code redeemed by `POST`
- mixed-version rollout may temporarily emit legacy pending token aliases in addition to the new completion path
- legacy alias support is transitional and bounded to rollout windows only
### WeChat login and channel mapping ### WeChat login and channel mapping
- environment chooses `mp` or `open` - environment chooses `mp` or `open`
- website login uses authorization-code flow with provider-configured app/channel binding
- callback must resolve to `unionid` - callback must resolve to `unionid`
- channel `openid` is optionally recorded in `auth_identity_channels` - channel `openid` is optionally recorded in `auth_identity_channels`
- failure to obtain `unionid` aborts flow - failure to obtain `unionid` aborts flow
@@ -344,7 +397,7 @@ On new account creation:
### User-visible methods ### User-visible methods
- `alipay` - `alipay`
- `wechat` - `wxpay`
### Backend source abstraction ### Backend source abstraction
@@ -357,6 +410,12 @@ Each display method maps to exactly one active configured backend source:
Frontend submits display method only. Backend resolves display method to active source and capability set. Frontend submits display method only. Backend resolves display method to active source and capability set.
### Legacy payment-config normalization
- existing provider-instance `supported_types`, legacy aliases such as `wxpay_direct`, and per-type limit structures are migrated into the visible-method model
- migration preserves historical payment capability and refund semantics
- the system keeps one normalized visible-method mapping per provider instance for rollout and audit
### Alipay routing ### Alipay routing
- PC: create QR-oriented result and show QR in page - PC: create QR-oriented result and show QR in page
@@ -372,11 +431,25 @@ Frontend submits display method only. Backend resolves display method to active
- prefer MP/JSAPI - prefer MP/JSAPI
- if unavailable, fall back to H5 pay - if unavailable, fall back to H5 pay
### WeChat payment OAuth recovery
- if WeChat in-app payment requires `openid` and the current request does not already hold it, backend returns an `oauth_required` response instead of guessing
- backend creates a server-backed payment-resume context containing:
- target visible method
- amount/order type/plan context
- redirect target
- anti-replay state
- backend redirects through a dedicated WeChat payment OAuth start endpoint
- callback exchanges the provider code server-side, stores `openid` in the payment-resume context, and returns a same-origin internal resume target
- frontend resumes the original order flow through the resume context instead of trusting raw callback query state or long-lived local storage
### Payment completion ### Payment completion
- frontend return restores context and UI state - frontend return restores context and UI state
- backend order state remains source of truth - backend order state remains source of truth
- webhook and/or order query remain authoritative for fulfillment - webhook and/or order query remain authoritative for fulfillment
- order fulfillment validates webhook or query payload against order-time snapshot data including provider instance, merchant identifiers, amount, currency, and provider order references
- result pages use authenticated lookup or signed opaque result tokens, never raw public `out_trade_no`
## Admin Configuration Model ## Admin Configuration Model
@@ -420,6 +493,9 @@ Compatibility is mandatory, especially for:
- existing email users - existing email users
- existing LinuxDo users - existing LinuxDo users
- historical LinuxDo synthetic-email accounts - historical LinuxDo synthetic-email accounts
- historical WeChat synthetic-email accounts
- historical OIDC synthetic-email accounts
- historical WeChat `openid`-only records created by older branches
### Additive migrations ### Additive migrations
@@ -431,6 +507,8 @@ Compatibility is mandatory, especially for:
- backfill canonical `email` identities for valid existing email users - backfill canonical `email` identities for valid existing email users
- backfill canonical `linuxdo` identities during migration for historical synthetic-email LinuxDo users - backfill canonical `linuxdo` identities during migration for historical synthetic-email LinuxDo users
- backfill canonical `wechat` and `oidc` identities when historical synthetic-email or `user_external_identities` data allows deterministic reconstruction
- emit migration reports for historical WeChat `openid`-only records that cannot be safely promoted to canonical `unionid`
- backfill must be idempotent and repeatable - backfill must be idempotent and repeatable
### Compatibility reads ### Compatibility reads
@@ -438,7 +516,7 @@ Compatibility is mandatory, especially for:
During rollout: During rollout:
- read new identity model first - read new identity model first
- where necessary, retain compatibility logic for existing email and historical LinuxDo synthetic-email recognition - where necessary, retain compatibility logic for existing email and historical LinuxDo/WeChat/OIDC synthetic-email recognition
### Grant idempotency ### Grant idempotency
@@ -453,17 +531,20 @@ Retain transitional support for legacy/new request and response shapes where nee
- `pending_oauth_token` - `pending_oauth_token`
- old callback parsing expectations - old callback parsing expectations
- historical profile field mappings - historical profile field mappings
- legacy callback fragment readers during the bounded rollout window
### Settings and payment compatibility ### Settings and payment compatibility
- preserve existing payment configs and order semantics from `main` - preserve existing payment configs and order semantics from `main`
- add new settings incrementally - add new settings incrementally
- avoid rewriting the entire settings schema in one cutover - avoid rewriting the entire settings schema in one cutover
- preserve legacy provider-instance capabilities by explicitly mapping historical `supported_types`, `payment_mode`, and limit config into normalized visible-method routing
### Rolling upgrade tolerance ### Rolling upgrade tolerance
- do not assume simultaneous frontend/backend deployment - do not assume simultaneous frontend/backend deployment
- new backend must tolerate short-lived older frontend request shapes - new backend must tolerate short-lived older frontend request shapes
- rollout must define the deployment order and removal point for legacy callback token parsing and legacy payment resume parsing
## Testing Strategy ## Testing Strategy
@@ -509,9 +590,13 @@ Retain transitional support for legacy/new request and response shapes where nee
- existing email users - existing email users
- historical LinuxDo synthetic-email users - historical LinuxDo synthetic-email users
- historical WeChat synthetic-email users
- historical OIDC synthetic-email users
- historical WeChat `openid`-only records reported or remediated correctly
- historical payment config - historical payment config
- legacy auth payload field names - legacy auth payload field names
- historical payment result handling - historical payment result handling
- mixed-version callback token bridge behavior
## Implementation Phases ## Implementation Phases
@@ -528,9 +613,12 @@ Retain transitional support for legacy/new request and response shapes where nee
Implementation must follow current primary-source guidance: Implementation must follow current primary-source guidance:
- OAuth 2.0 Security BCP (RFC 9700): strict redirect handling, state protection, mix-up resistant design - OAuth 2.0 Security BCP (RFC 9700): strict redirect handling, state protection, mix-up resistant design
- PKCE (RFC 7636): use on authorization code flows where applicable - PKCE (RFC 7636): require `S256` on browser authorization-code flows
- OpenID Connect Core: stable issuer/subject handling for OIDC identities - OpenID Connect Core: stable issuer/subject handling for OIDC identities
- Account linking best practice: require explicit user confirmation or re-authentication before linking to existing accounts - Account linking best practice: require explicit user confirmation or re-authentication before linking to existing accounts
- WeChat UnionID and website-login guidance: treat `unionid` as canonical cross-channel subject and persist channel/app binding with website login responses
- WeChat Pay webhook guidance: verify signatures, decrypt payloads, and confirm merchant/order/amount fields against order-time state before fulfillment
- Payment success-page guidance: custom success pages are informational and must not be the only fulfillment trigger
References: References:
@@ -538,6 +626,10 @@ References:
- RFC 7636: <https://www.rfc-editor.org/rfc/rfc7636> - RFC 7636: <https://www.rfc-editor.org/rfc/rfc7636>
- OpenID Connect Core 1.0: <https://openid.net/specs/openid-connect-core-1_0.html> - OpenID Connect Core 1.0: <https://openid.net/specs/openid-connect-core-1_0.html>
- Auth0 account linking guidance: <https://auth0.com/docs/manage-users/user-accounts/user-account-linking> - Auth0 account linking guidance: <https://auth0.com/docs/manage-users/user-accounts/user-account-linking>
- WeChat UnionID guidance: <https://developers.weixin.qq.com/doc/service/guide/product/unionid.html>
- WeChat website login guidance: <https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html>
- WeChat Pay callback/signature guidance: <https://pay.weixin.qq.com/doc/v3/merchant/4012075249>
- Stripe Checkout fulfillment guidance: <https://docs.stripe.com/checkout/fulfillment>
## Audit Synthesis ## Audit Synthesis
@@ -553,7 +645,7 @@ The clean rebuild direction is not to copy either existing branch directly.
- `personal-dev-branch` has the better real-world closure: - `personal-dev-branch` has the better real-world closure:
- LinuxDo and WeChat callback flows are more operationally complete - LinuxDo and WeChat callback flows are more operationally complete
- profile binding and avatar UX is more complete - profile binding and avatar UX is more complete
- historical synthetic-email users are recognized and recovered in live flows - historical synthetic-email users across multiple providers are recognized and recovered in live flows
- WeChat payment OAuth and recovery behavior is more complete - WeChat payment OAuth and recovery behavior is more complete
- Primary-source guidance supplies hard constraints for OAuth/OIDC, account linking, WeChat identity handling, and payment completion semantics. - Primary-source guidance supplies hard constraints for OAuth/OIDC, account linking, WeChat identity handling, and payment completion semantics.
@@ -584,6 +676,7 @@ Keep these operational flow ideas from `personal-dev-branch`:
- profile bindings UX and “cannot disconnect last usable login method” rule - profile bindings UX and “cannot disconnect last usable login method” rule
- separate WeChat login OAuth and WeChat payment OAuth entry points - separate WeChat login OAuth and WeChat payment OAuth entry points
- historical synthetic-email recognition logic as a migration bridge - historical synthetic-email recognition logic as a migration bridge
- explicit WeChat payment OAuth recovery protocol as a product requirement, but reimplemented with server-backed resume state
### Adapt ### Adapt
@@ -595,6 +688,7 @@ These areas must be reimplemented with the same intent but stricter boundaries:
- WeChat payment recovery state must move from frontend-only storage to server-backed continuation state - WeChat payment recovery state must move from frontend-only storage to server-backed continuation state
- avatar adoption fetches must be security-hardened and failure-visible - avatar adoption fetches must be security-hardened and failure-visible
- pending-auth payload modeling must clearly separate immutable upstream payload from mutable local metadata - pending-auth payload modeling must clearly separate immutable upstream payload from mutable local metadata
- callback completion must use a real exchange/session model instead of fragment-delivered bearer tokens
- profile binding/avatar DTOs must be simplified to one authoritative backend contract instead of sprawling frontend fallback parsing - profile binding/avatar DTOs must be simplified to one authoritative backend contract instead of sprawling frontend fallback parsing
- admin settings should preserve capability while reducing duplicated or transitional config branches - admin settings should preserve capability while reducing duplicated or transitional config branches
@@ -614,7 +708,7 @@ The audit and source review establish these hard constraints:
### Auth ### Auth
- all authorization-code providers use PKCE where applicable - all browser authorization-code providers use PKCE `S256` and do not expose an admin-off switch
- callback handling uses strict `redirect_uri` discipline and state validation - callback handling uses strict `redirect_uri` discipline and state validation
- OIDC identity key is `issuer + sub` - OIDC identity key is `issuer + sub`
- existing-account linking after email conflict must require explicit user action plus local-account verification - existing-account linking after email conflict must require explicit user action plus local-account verification
@@ -624,7 +718,8 @@ The audit and source review establish these hard constraints:
- existing email users must continue to work with no manual intervention - existing email users must continue to work with no manual intervention
- existing LinuxDo users must not split into duplicate accounts - existing LinuxDo users must not split into duplicate accounts
- historical LinuxDo synthetic-email users must be backfilled into canonical LinuxDo identities during migration, not only lazily on next login - historical LinuxDo/WeChat/OIDC synthetic-email users must be backfilled into canonical identities during migration when deterministic recovery is possible
- historical WeChat `openid`-only records must be surfaced through migration reporting and explicit remediation rules
- migration backfills must not trigger signup or first-bind grants - migration backfills must not trigger signup or first-bind grants
- legacy `pending_auth_token` and `pending_oauth_token` contracts must remain accepted during rollout - legacy `pending_auth_token` and `pending_oauth_token` contracts must remain accepted during rollout
- legacy auth/public setting aliases needed by older frontend builds must remain available during rollout - legacy auth/public setting aliases needed by older frontend builds must remain available during rollout
@@ -634,7 +729,9 @@ The audit and source review establish these hard constraints:
- frontend return pages do not determine final payment success - frontend return pages do not determine final payment success
- backend order state, webhook processing, and/or provider status query remain authoritative - backend order state, webhook processing, and/or provider status query remain authoritative
- each visible method (`alipay`, `wechat`) may have only one active backend source at a time - each visible method (`alipay`, `wxpay`) may have only one active backend source at a time
- public result pages must not expose raw `out_trade_no` lookup
- WeChat Pay callback handling must verify signature, decrypt payload, and compare order fields against order-time snapshot data
## Known Risks To Eliminate In Implementation ## Known Risks To Eliminate In Implementation
@@ -649,6 +746,7 @@ These are specifically observed problems in the existing branches that the clean
- WeChat payment recovery in `personal-dev-branch` is frontend-local and not robust across tabs or concurrent attempts - WeChat payment recovery in `personal-dev-branch` is frontend-local and not robust across tabs or concurrent attempts
- the existing pending-auth migration update is too destructive to reuse unchanged in a safer rollout - the existing pending-auth migration update is too destructive to reuse unchanged in a safer rollout
- historical provider provenance should not be permanently flattened to `signup_source = email` - historical provider provenance should not be permanently flattened to `signup_source = email`
- design/plan drift can reintroduce ambiguous identity uniqueness or ambiguous adoption-decision ownership if not aligned before implementation
## Rollout Gates ## Rollout Gates
@@ -656,9 +754,10 @@ The rebuild is not ready for rollout until all of these are satisfied:
1. Identity schema and migration chain are linearized and production-safe. 1. Identity schema and migration chain are linearized and production-safe.
2. Email identity backfill is complete and idempotent. 2. Email identity backfill is complete and idempotent.
3. Historical LinuxDo synthetic-email backfill to canonical LinuxDo identity is complete and idempotent. 3. Historical LinuxDo/WeChat/OIDC synthetic-email backfill to canonical identity is complete where deterministic, and non-recoverable rows are reported.
4. `signup_source` backfill is accurate for known historical provider-created users. 4. Historical WeChat `openid`-only rows are either remediated or explicitly blocked with operator-visible reporting.
5. Dual token acceptance and required legacy field aliases are present. 5. `signup_source` backfill is accurate for known historical provider-created users.
6. Existing payment configs are normalized and verified against current frontend-visible capabilities. 6. Dual token acceptance, exchange bridge behavior, and required legacy field aliases are present for the bounded rollout window.
7. New frontend flows are verified against mixed-version backend compatibility windows. 7. Existing payment configs are normalized and verified against current frontend-visible capabilities.
8. Duplicate-account creation, first-bind grants, and payment route selection have regression coverage. 8. New frontend flows are verified against mixed-version backend compatibility windows.
9. Duplicate-account creation, first-bind grants, and payment route selection have regression coverage.