checkpoint before checking out feature/account-managed-gateway

This commit is contained in:
Rocky
2026-08-19 15:53:08 +08:00
parent 25cfbfa4e6
commit 1737106560
51 changed files with 1837 additions and 52 deletions
+10
View File
@@ -0,0 +1,10 @@
# Account data lifecycle
- Apple subjects, refresh tokens, and account nicknames are encrypted at rest.
- Apple email and avatar data are not requested or stored.
- Deleting an account removes its session, profile, referral, grant, and mutable
account records in the same local transaction before Apple revocation is retried.
- Pseudonymous immutable credit-ledger entries, StoreKit transaction audit data,
and time-limited anti-abuse tombstones remain after deletion where required to
prevent replay, preserve financial integrity, and stop repeated trial abuse.
- Logs must never include Apple subjects, credentials, tokens, or nicknames.
+49
View File
@@ -0,0 +1,49 @@
# StoreKit credit product
The voluntary `ByRockyACoffee` tip remains independent and never grants credits.
Credit products are separate consumables:
- Product ID `500tks`: 500 integer credits at USD 0.99
- Product ID `1500tks`: 1,500 integer credits at USD 1.99 / CNY 18
- Product ID `3000tks`: 3,000 integer credits at USD 2.99 / CNY 28
- Territory prices remain controlled by App Store Connect.
- Restore Purchases: not offered for this consumable
## Cost basis
Reviewed on 2026-08-18 against the provider pricing pages:
- DeepSeek V4 Flash peak pricing is CNY 3 per million cache-miss input tokens
and CNY 9 per million output tokens. Off-peak pricing is half.
<https://api-docs.deepseek.com/zh-cn/quick_start/pricing>
- Doubao SeedASR 2.0 streaming recognition is CNY 4.5 per hour.
<https://ai.volcengine.com/model>
The V10 immutable rate card charges:
- ASR: one credit per started three-second interval. The 3,000-credit pack
provides up to 150 minutes and has a worst-case provider cost of CNY 11.25.
- DeepSeek: one credit per 1,000 input tokens plus one credit per 400 output
tokens, with each dimension rounded upward. At peak pricing, using all 3,000
credits exclusively on input or output costs at most about CNY 9.00 or
CNY 10.80 respectively.
- New signup, inviter and invitee grants are 1,000 credits each. Existing
immutable balances are adjusted only through explicit admin grants.
- Existing immutable ledger balances are grandfathered and are not rewritten
during the denomination change.
At a CNY 28 sale price, the ASR-heavy worst case leaves CNY 12.55 after a 15%
App Store commission, or CNY 8.35 after a 30% commission, before tax and
infrastructure costs. USD 2.99 territories are tighter at the worst-case ASR
mix and require ongoing margin monitoring.
## Transaction rules
- The app supplies the authenticated account UUID as StoreKit `appAccountToken`.
- The server verifies Apple's JWS signature, certificate chain, bundle ID,
App Apple ID, environment, consumable type, account token, and product ID.
- The App Store transaction ID is globally unique and idempotent.
- Credit balance and append-only purchase/ledger records commit in one database
transaction. A client retry returns the original grant.
- The app finishes the StoreKit transaction only after server acknowledgement.
- Signed transaction bodies and Apple certificate contents are never logged.
+4
View File
@@ -34,10 +34,12 @@ GRANT SELECT ON osg_account.app_attest_challenges TO 'osg_account_runtime'@'10.2
GRANT SELECT ON osg_account.app_attest_keys TO 'osg_account_runtime'@'10.20.%';
GRANT SELECT ON osg_account.account_identity_tombstones TO 'osg_account_runtime'@'10.20.%';
GRANT SELECT ON osg_account.apple_revocation_outbox TO 'osg_account_runtime'@'10.20.%';
GRANT SELECT ON osg_account.account_profiles TO 'osg_account_runtime'@'10.20.%';
GRANT SELECT ON osg_account.admin_operators TO 'osg_account_runtime'@'10.20.%';
GRANT SELECT ON osg_account.admin_sessions TO 'osg_account_runtime'@'10.20.%';
GRANT SELECT ON osg_account.admin_audit_log TO 'osg_account_runtime'@'10.20.%';
GRANT SELECT ON osg_account.admin_credit_grants TO 'osg_account_runtime'@'10.20.%';
GRANT SELECT ON osg_account.storekit_credit_purchases TO 'osg_account_runtime'@'10.20.%';
GRANT INSERT, UPDATE, DELETE ON osg_account.accounts TO 'osg_account_runtime'@'10.20.%';
GRANT INSERT, UPDATE ON osg_account.apple_credentials TO 'osg_account_runtime'@'10.20.%';
@@ -60,12 +62,14 @@ GRANT INSERT, UPDATE ON osg_account.app_attest_challenges TO 'osg_account_runtim
GRANT INSERT, UPDATE ON osg_account.app_attest_keys TO 'osg_account_runtime'@'10.20.%';
GRANT INSERT, UPDATE ON osg_account.account_identity_tombstones TO 'osg_account_runtime'@'10.20.%';
GRANT INSERT, UPDATE ON osg_account.apple_revocation_outbox TO 'osg_account_runtime'@'10.20.%';
GRANT INSERT, UPDATE ON osg_account.account_profiles TO 'osg_account_runtime'@'10.20.%';
-- Operators and sessions are mutable authentication state. Audit and grant
-- records remain append-only and deliberately receive no UPDATE or DELETE.
GRANT INSERT, UPDATE ON osg_account.admin_operators TO 'osg_account_runtime'@'10.20.%';
GRANT INSERT, UPDATE, DELETE ON osg_account.admin_sessions TO 'osg_account_runtime'@'10.20.%';
GRANT INSERT ON osg_account.admin_audit_log TO 'osg_account_runtime'@'10.20.%';
GRANT INSERT ON osg_account.admin_credit_grants TO 'osg_account_runtime'@'10.20.%';
GRANT INSERT ON osg_account.storekit_credit_purchases TO 'osg_account_runtime'@'10.20.%';
-- Deliberately absent: global privileges, GRANT OPTION, FILE, PROCESS, SUPER,
-- CREATE USER, and UPDATE/DELETE on immutable ledger or usage-history tables.
+106 -4
View File
@@ -75,7 +75,7 @@ paths:
default: { $ref: "#/components/responses/Error" }
/v1/account:
get:
summary: Return the minimal account profile
summary: Return the account profile
responses:
"200":
description: Account profile
@@ -83,6 +83,20 @@ paths:
application/json:
schema: { $ref: "#/components/schemas/AccountEnvelope" }
default: { $ref: "#/components/responses/Error" }
patch:
summary: Update the current account nickname
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/UpdateAccountProfileRequest" }
responses:
"200":
description: Updated account profile
content:
application/json:
schema: { $ref: "#/components/schemas/AccountEnvelope" }
default: { $ref: "#/components/responses/Error" }
delete:
summary: Reauthenticate with Apple, delete the account, and revoke authorization
requestBody:
@@ -112,7 +126,7 @@ paths:
default: { $ref: "#/components/responses/Error" }
/v1/credits/balance:
get:
summary: Return available integer credits
summary: Return available and consumed integer credits
responses:
"200":
description: Credit account
@@ -146,6 +160,44 @@ paths:
type: array
items: { type: object, additionalProperties: true }
default: { $ref: "#/components/responses/Error" }
/v1/storekit/products:
get:
summary: Return enabled consumable credit products
description: |
Current catalog: `500tks` grants 500 credits, `1500tks` grants 1,500
credits, and `3000tks` grants 3,000 credits. Localized prices are
supplied by StoreKit.
responses:
"200":
description: StoreKit credit product catalog
content:
application/json:
schema:
type: array
items: { $ref: "#/components/schemas/StoreKitProduct" }
default: { $ref: "#/components/responses/Error" }
/v1/storekit/transactions:
post:
summary: Verify an App Store transaction and idempotently grant credits
description: |
Submit the StoreKit 2 `VerificationResult.jwsRepresentation` before
finishing the consumable transaction. The purchase must include an
`appAccountToken` equal to the authenticated account UUID. Replaying
the same App Store transaction returns the original grant.
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/StoreKitTransactionRequest" }
responses:
"200":
description: Verified purchase grant or idempotent replay
content:
application/json:
schema: { $ref: "#/components/schemas/StoreKitPurchase" }
"409": { $ref: "#/components/responses/Error" }
"422": { $ref: "#/components/responses/Error" }
default: { $ref: "#/components/responses/Error" }
/v1/referrals:
get:
summary: List invitees without exposing their Apple identity
@@ -162,7 +214,7 @@ paths:
default: { $ref: "#/components/responses/Error" }
/v1/referrals/me:
get:
summary: Return the current referral code and binding
summary: Return the referral profile and idempotently provision its invite code
responses:
"200":
description: Referral profile
@@ -1088,6 +1140,10 @@ components:
nonce:
type: string
description: Raw nonce whose lowercase SHA-256 hex digest was sent to Apple.
displayName:
type: ["string", "null"]
maxLength: 128
description: Optional first-authorization Apple name used only to seed the nickname.
deviceCheckToken:
type: ["string", "null"]
description: Ephemeral DeviceCheck token; never persisted in plaintext.
@@ -1143,12 +1199,24 @@ components:
properties:
id: { type: string, format: uuid }
createdAtEpochSeconds: { type: integer, format: int64 }
displayName:
type: ["string", "null"]
maxLength: 64
AccountEnvelope:
type: object
additionalProperties: false
required: [data]
properties:
data: { $ref: "#/components/schemas/Account" }
UpdateAccountProfileRequest:
type: object
additionalProperties: false
required: [displayName]
properties:
displayName:
type: string
minLength: 1
maxLength: 64
DeleteAccountRequest:
type: object
additionalProperties: false
@@ -1160,10 +1228,15 @@ components:
CreditAccount:
type: object
additionalProperties: true
required: [userId, balance]
required: [userId, balance, lifetimeUsed]
properties:
userId: { type: string, format: uuid }
balance: { type: integer, format: int64, minimum: 0 }
lifetimeUsed:
type: integer
format: int64
minimum: 0
description: Settled usage minus credits returned by refunds
LedgerEntry:
type: object
additionalProperties: true
@@ -1172,6 +1245,35 @@ components:
id: { type: string, format: uuid }
amountDelta: { type: integer, format: int64 }
balanceAfter: { type: integer, format: int64, minimum: 0 }
StoreKitProduct:
type: object
additionalProperties: false
required: [productId, credits]
properties:
productId:
type: string
enum: [500tks, 1500tks, 3000tks]
credits: { type: integer, format: int64, minimum: 1 }
StoreKitTransactionRequest:
type: object
additionalProperties: false
required: [signedTransaction]
properties:
signedTransaction:
type: string
minLength: 100
maxLength: 32768
description: StoreKit 2 VerificationResult.jwsRepresentation
StoreKitPurchase:
type: object
additionalProperties: false
required: [transactionId, productId, creditsGranted, balanceAfter, replayed]
properties:
transactionId: { type: string, pattern: "^[0-9]{1,64}$" }
productId: { type: string, minLength: 3, maxLength: 128 }
creditsGranted: { type: integer, format: int64, minimum: 1 }
balanceAfter: { type: integer, format: int64, minimum: 0 }
replayed: { type: boolean }
AppleAppSiteAssociation:
type: object
additionalProperties: false