checkpoint before checking out feature/account-managed-gateway
This commit is contained in:
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user