Establish secure account and managed AI backend
Provide the production foundation for Apple identity, immutable credits, referrals, integrity checks, managed providers, and hardened Docker deployment.
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICITCCAaegAwIBAgIQC/O+DvHN0uD7jG5yH2IXmDAKBggqhkjOPQQDAzBSMSYw
|
||||
JAYDVQQDDB1BcHBsZSBBcHAgQXR0ZXN0YXRpb24gUm9vdCBDQTETMBEGA1UECgwK
|
||||
QXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTAeFw0yMDAzMTgxODMyNTNa
|
||||
Fw00NTAzMTUwMDAwMDBaMFIxJjAkBgNVBAMMHUFwcGxlIEFwcCBBdHRlc3RhdGlv
|
||||
biBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJbmMuMRMwEQYDVQQIDApDYWxpZm9y
|
||||
bmlhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAERTHhmLW07ATaFQIEVwTtT4dyctdh
|
||||
NbJhFs/Ii2FdCgAHGbpphY3+d8qjuDngIN3WVhQUBHAoMeQ/cLiP1sOUtgjqK9au
|
||||
Yen1mMEvRq9Sk3Jm5X8U62H+xTD3FE9TgS41o0IwQDAPBgNVHRMBAf8EBTADAQH/
|
||||
MB0GA1UdDgQWBBSskRBTM72+aEH/pwyp5frq5eWKoTAOBgNVHQ8BAf8EBAMCAQYw
|
||||
CgYIKoZIzj0EAwMDaAAwZQIwQgFGnByvsiVbpTKwSga0kP0e8EeDS4+sQmTvb7vn
|
||||
53O5+FRXgeLhpJ06ysC5PrOyAjEAp5U4xDgEgllF7En3VcE3iexZZtKeYnpqtijV
|
||||
oyFraWVIyd/dganmrduC1bmTBGwD
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,61 @@
|
||||
ktor:
|
||||
application:
|
||||
modules:
|
||||
- com.osglab.account.ApplicationKt.module
|
||||
deployment:
|
||||
host: 0.0.0.0
|
||||
port: "$PORT:8080"
|
||||
|
||||
app:
|
||||
environment: "$APP_ENV:development"
|
||||
publicBaseUrl: "$PUBLIC_BASE_URL:https://account.osglab.com"
|
||||
inviteBaseUrl: "$INVITE_BASE_URL:https://osglab.com/i"
|
||||
appStoreUrl: "$APP_STORE_URL:https://apps.apple.com/app/id0000000000"
|
||||
database:
|
||||
jdbcUrl: "$DATABASE_URL:jdbc:mysql://localhost:3306/osg_account?useUnicode=true&characterEncoding=utf8&connectionTimeZone=UTC&forceConnectionTimeZoneToSession=true"
|
||||
username: "$DATABASE_USER:osg_account"
|
||||
password: "$DATABASE_PASSWORD"
|
||||
migrationUsername: "$DATABASE_MIGRATION_USER:"
|
||||
migrationPassword: "$DATABASE_MIGRATION_PASSWORD:"
|
||||
maximumPoolSize: "$DATABASE_POOL_SIZE:10"
|
||||
session:
|
||||
issuer: "$JWT_ISSUER:https://account.osglab.com"
|
||||
audience: "$JWT_AUDIENCE:osgkeyboard-ios"
|
||||
secret: "$JWT_SECRET"
|
||||
accessMinutes: "$ACCESS_TOKEN_MINUTES:15"
|
||||
refreshDays: "$REFRESH_TOKEN_DAYS:30"
|
||||
gatewayGrantDays: "$GATEWAY_GRANT_DAYS:30"
|
||||
encryption:
|
||||
keyBase64: "$FIELD_ENCRYPTION_KEY"
|
||||
antiAbuse:
|
||||
identityHmacKeyBase64: "$IDENTITY_HMAC_KEY"
|
||||
tombstoneRetentionDays: "$IDENTITY_TOMBSTONE_RETENTION_DAYS:365"
|
||||
apple:
|
||||
teamId: "$APPLE_TEAM_ID:"
|
||||
keyId: "$APPLE_KEY_ID:"
|
||||
clientId: "$APPLE_CLIENT_ID:com.osgkeyboard.ios"
|
||||
privateKeyPem: "$APPLE_PRIVATE_KEY_PEM:"
|
||||
jwksUrl: "$APPLE_JWKS_URL:https://appleid.apple.com/auth/keys"
|
||||
tokenUrl: "$APPLE_TOKEN_URL:https://appleid.apple.com/auth/token"
|
||||
revokeUrl: "$APPLE_REVOKE_URL:https://appleid.apple.com/auth/revoke"
|
||||
credits:
|
||||
signupTrial: "$SIGNUP_TRIAL_CREDITS:1000"
|
||||
referralInviter: "$REFERRAL_INVITER_CREDITS:3000"
|
||||
referralInvitee: "$REFERRAL_INVITEE_CREDITS:3000"
|
||||
referralBindingDays: "$REFERRAL_BINDING_DAYS:7"
|
||||
providers:
|
||||
volcengine:
|
||||
endpoint: "$VOLCENGINE_ASR_ENDPOINT:wss://openspeech.bytedance.com/api/v3/sauc/bigmodel_async"
|
||||
appId: "$VOLCENGINE_APP_ID:"
|
||||
accessToken: "$VOLCENGINE_ACCESS_TOKEN:"
|
||||
apiKey: "$VOLCENGINE_API_KEY:"
|
||||
resourceId: "$VOLCENGINE_RESOURCE_ID:volc.seedasr.sauc.duration"
|
||||
deepseek:
|
||||
endpoint: "$DEEPSEEK_ENDPOINT:https://api.deepseek.com/v1"
|
||||
apiKey: "$DEEPSEEK_API_KEY:"
|
||||
model: "$DEEPSEEK_MODEL:deepseek-v4-flash"
|
||||
integrity:
|
||||
enforceDeviceCheck: "$ENFORCE_DEVICE_CHECK:false"
|
||||
enforceAppAttest: "$ENFORCE_APP_ATTEST:false"
|
||||
appleEnvironment: "$APPLE_INTEGRITY_ENVIRONMENT:development"
|
||||
challengeLifetimeSeconds: "$APP_ATTEST_CHALLENGE_TTL_SECONDS:300"
|
||||
@@ -0,0 +1,47 @@
|
||||
CREATE TABLE accounts (
|
||||
id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
apple_sub VARCHAR(255) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
created_at TIMESTAMP(6) NOT NULL,
|
||||
updated_at TIMESTAMP(6) NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_accounts_apple_sub (apple_sub)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE apple_credentials (
|
||||
account_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
encrypted_refresh_token MEDIUMTEXT NOT NULL,
|
||||
created_at TIMESTAMP(6) NOT NULL,
|
||||
updated_at TIMESTAMP(6) NOT NULL,
|
||||
PRIMARY KEY (account_id),
|
||||
CONSTRAINT fk_apple_credentials_account
|
||||
FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE sessions (
|
||||
id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
account_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
family_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
refresh_token_hash CHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
replaced_by_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NULL,
|
||||
created_at TIMESTAMP(6) NOT NULL,
|
||||
expires_at TIMESTAMP(6) NOT NULL,
|
||||
revoked_at TIMESTAMP(6) NULL,
|
||||
reuse_detected_at TIMESTAMP(6) NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_sessions_refresh_token_hash (refresh_token_hash),
|
||||
KEY ix_sessions_account_id (account_id),
|
||||
KEY ix_sessions_family_id (family_id),
|
||||
KEY ix_sessions_account_active (account_id, revoked_at, expires_at),
|
||||
CONSTRAINT fk_sessions_account
|
||||
FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_sessions_replaced_by
|
||||
FOREIGN KEY (replaced_by_id) REFERENCES sessions (id) ON DELETE SET NULL,
|
||||
CONSTRAINT chk_sessions_expiry CHECK (expires_at > created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE apple_event_receipts (
|
||||
event_id VARCHAR(255) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
event_type VARCHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
received_at TIMESTAMP(6) NOT NULL,
|
||||
PRIMARY KEY (event_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
@@ -0,0 +1,352 @@
|
||||
CREATE TABLE credit_accounts (
|
||||
user_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
balance BIGINT NOT NULL DEFAULT 0,
|
||||
updated_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (user_id),
|
||||
CONSTRAINT chk_credit_accounts_non_negative CHECK (balance >= 0)
|
||||
) ENGINE = InnoDB;
|
||||
|
||||
CREATE TABLE credit_rate_versions (
|
||||
id CHAR(36) NOT NULL,
|
||||
kind VARCHAR(8) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
provider VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
|
||||
model VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
|
||||
effective_from DATETIME(6) NOT NULL,
|
||||
effective_until DATETIME(6) NULL,
|
||||
asr_credits_numerator BIGINT NULL,
|
||||
asr_millis_denominator BIGINT NULL,
|
||||
input_credits_numerator BIGINT NULL,
|
||||
input_tokens_denominator BIGINT NULL,
|
||||
output_credits_numerator BIGINT NULL,
|
||||
output_tokens_denominator BIGINT NULL,
|
||||
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_credit_rates_effective (
|
||||
kind, provider, model, effective_from
|
||||
),
|
||||
INDEX idx_credit_rates_lookup (kind, provider, model, effective_from),
|
||||
CONSTRAINT chk_credit_rates_interval
|
||||
CHECK (effective_until IS NULL OR effective_until > effective_from),
|
||||
CONSTRAINT chk_credit_rates_shape CHECK (
|
||||
(
|
||||
kind = 'ASR'
|
||||
AND asr_credits_numerator > 0
|
||||
AND asr_millis_denominator > 0
|
||||
AND input_credits_numerator IS NULL
|
||||
AND input_tokens_denominator IS NULL
|
||||
AND output_credits_numerator IS NULL
|
||||
AND output_tokens_denominator IS NULL
|
||||
)
|
||||
OR
|
||||
(
|
||||
kind = 'LLM'
|
||||
AND asr_credits_numerator IS NULL
|
||||
AND asr_millis_denominator IS NULL
|
||||
AND input_credits_numerator > 0
|
||||
AND input_tokens_denominator > 0
|
||||
AND output_credits_numerator > 0
|
||||
AND output_tokens_denominator > 0
|
||||
)
|
||||
)
|
||||
) ENGINE = InnoDB;
|
||||
|
||||
CREATE TABLE credit_reservations (
|
||||
id CHAR(36) NOT NULL,
|
||||
user_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
rate_version_id CHAR(36) NOT NULL,
|
||||
provider VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
|
||||
model VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
|
||||
usage_kind VARCHAR(8) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
estimated_asr_millis BIGINT NULL,
|
||||
estimated_input_tokens BIGINT NULL,
|
||||
estimated_output_tokens BIGINT NULL,
|
||||
actual_asr_millis BIGINT NULL,
|
||||
actual_input_tokens BIGINT NULL,
|
||||
actual_output_tokens BIGINT NULL,
|
||||
reserved_credits BIGINT NOT NULL,
|
||||
settled_credits BIGINT NULL,
|
||||
status VARCHAR(16) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
managed_call BOOLEAN NOT NULL,
|
||||
reserve_idempotency_key VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
|
||||
settle_idempotency_key VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL,
|
||||
release_idempotency_key VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL,
|
||||
refund_idempotency_key VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL,
|
||||
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
updated_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_credit_reservation_reserve (user_id, reserve_idempotency_key),
|
||||
UNIQUE KEY uk_credit_reservation_settle (user_id, settle_idempotency_key),
|
||||
UNIQUE KEY uk_credit_reservation_release (user_id, release_idempotency_key),
|
||||
UNIQUE KEY uk_credit_reservation_refund (user_id, refund_idempotency_key),
|
||||
INDEX idx_credit_reservations_user_status (user_id, status),
|
||||
CONSTRAINT fk_credit_reservations_rate
|
||||
FOREIGN KEY (rate_version_id) REFERENCES credit_rate_versions (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT chk_credit_reservations_positive CHECK (reserved_credits > 0),
|
||||
CONSTRAINT chk_credit_reservations_estimated_usage CHECK (
|
||||
(
|
||||
usage_kind = 'ASR'
|
||||
AND estimated_asr_millis >= 0
|
||||
AND estimated_input_tokens IS NULL
|
||||
AND estimated_output_tokens IS NULL
|
||||
)
|
||||
OR
|
||||
(
|
||||
usage_kind = 'LLM'
|
||||
AND estimated_asr_millis IS NULL
|
||||
AND estimated_input_tokens >= 0
|
||||
AND estimated_output_tokens >= 0
|
||||
)
|
||||
),
|
||||
CONSTRAINT chk_credit_reservations_state CHECK (
|
||||
(
|
||||
status = 'RESERVED'
|
||||
AND actual_asr_millis IS NULL
|
||||
AND actual_input_tokens IS NULL
|
||||
AND actual_output_tokens IS NULL
|
||||
AND settled_credits IS NULL
|
||||
AND settle_idempotency_key IS NULL
|
||||
AND release_idempotency_key IS NULL
|
||||
AND refund_idempotency_key IS NULL
|
||||
)
|
||||
OR
|
||||
(
|
||||
status = 'SETTLED'
|
||||
AND settled_credits >= 0
|
||||
AND settle_idempotency_key IS NOT NULL
|
||||
AND release_idempotency_key IS NULL
|
||||
AND refund_idempotency_key IS NULL
|
||||
AND (
|
||||
(
|
||||
usage_kind = 'ASR'
|
||||
AND actual_asr_millis >= 0
|
||||
AND actual_input_tokens IS NULL
|
||||
AND actual_output_tokens IS NULL
|
||||
)
|
||||
OR
|
||||
(
|
||||
usage_kind = 'LLM'
|
||||
AND actual_asr_millis IS NULL
|
||||
AND actual_input_tokens >= 0
|
||||
AND actual_output_tokens >= 0
|
||||
)
|
||||
)
|
||||
)
|
||||
OR
|
||||
(
|
||||
status = 'RELEASED'
|
||||
AND actual_asr_millis IS NULL
|
||||
AND actual_input_tokens IS NULL
|
||||
AND actual_output_tokens IS NULL
|
||||
AND settled_credits IS NULL
|
||||
AND settle_idempotency_key IS NULL
|
||||
AND release_idempotency_key IS NOT NULL
|
||||
AND refund_idempotency_key IS NULL
|
||||
)
|
||||
OR
|
||||
(
|
||||
status = 'REFUNDED'
|
||||
AND settled_credits >= 0
|
||||
AND settle_idempotency_key IS NOT NULL
|
||||
AND release_idempotency_key IS NULL
|
||||
AND refund_idempotency_key IS NOT NULL
|
||||
AND (
|
||||
(
|
||||
usage_kind = 'ASR'
|
||||
AND actual_asr_millis >= 0
|
||||
AND actual_input_tokens IS NULL
|
||||
AND actual_output_tokens IS NULL
|
||||
)
|
||||
OR
|
||||
(
|
||||
usage_kind = 'LLM'
|
||||
AND actual_asr_millis IS NULL
|
||||
AND actual_input_tokens >= 0
|
||||
AND actual_output_tokens >= 0
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
) ENGINE = InnoDB;
|
||||
|
||||
CREATE TABLE referral_campaigns (
|
||||
id CHAR(36) NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
starts_at DATETIME(6) NOT NULL,
|
||||
ends_at DATETIME(6) NULL,
|
||||
binding_window_seconds BIGINT NOT NULL,
|
||||
inviter_reward_credits BIGINT NOT NULL,
|
||||
invitee_reward_credits BIGINT NOT NULL,
|
||||
max_rewarded_bindings BIGINT NULL,
|
||||
budget_credits BIGINT NULL,
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (id),
|
||||
INDEX idx_referral_campaigns_active (enabled, starts_at, ends_at),
|
||||
CONSTRAINT chk_referral_campaign_interval
|
||||
CHECK (ends_at IS NULL OR ends_at > starts_at),
|
||||
CONSTRAINT chk_referral_campaign_values CHECK (
|
||||
binding_window_seconds > 0
|
||||
AND inviter_reward_credits > 0
|
||||
AND invitee_reward_credits > 0
|
||||
AND inviter_reward_credits <= 9223372036854775807 - invitee_reward_credits
|
||||
AND (max_rewarded_bindings IS NULL OR max_rewarded_bindings > 0)
|
||||
AND (
|
||||
budget_credits IS NULL
|
||||
OR (
|
||||
budget_credits >= inviter_reward_credits
|
||||
AND budget_credits - inviter_reward_credits >= invitee_reward_credits
|
||||
)
|
||||
)
|
||||
)
|
||||
) ENGINE = InnoDB;
|
||||
|
||||
CREATE TABLE referral_campaign_budgets (
|
||||
campaign_id CHAR(36) NOT NULL,
|
||||
rewarded_bindings BIGINT NOT NULL DEFAULT 0,
|
||||
spent_credits BIGINT NOT NULL DEFAULT 0,
|
||||
updated_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (campaign_id),
|
||||
CONSTRAINT fk_referral_campaign_budget_campaign
|
||||
FOREIGN KEY (campaign_id) REFERENCES referral_campaigns (id) ON DELETE CASCADE,
|
||||
CONSTRAINT chk_referral_campaign_budget_non_negative
|
||||
CHECK (rewarded_bindings >= 0 AND spent_credits >= 0)
|
||||
) ENGINE = InnoDB;
|
||||
|
||||
INSERT INTO referral_campaigns (
|
||||
id, name, starts_at, binding_window_seconds,
|
||||
inviter_reward_credits, invitee_reward_credits, enabled
|
||||
) VALUES (
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'Default referral campaign',
|
||||
'1970-01-01 00:00:00.000000',
|
||||
604800,
|
||||
3000,
|
||||
3000,
|
||||
TRUE
|
||||
);
|
||||
|
||||
INSERT INTO referral_campaign_budgets (campaign_id)
|
||||
VALUES ('00000000-0000-0000-0000-000000000001');
|
||||
|
||||
CREATE TABLE referral_codes (
|
||||
id CHAR(36) NOT NULL,
|
||||
owner_user_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
campaign_id CHAR(36) NOT NULL DEFAULT '00000000-0000-0000-0000-000000000001',
|
||||
code VARCHAR(32) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_referral_codes_owner_campaign (owner_user_id, campaign_id),
|
||||
UNIQUE KEY uk_referral_codes_code (code),
|
||||
CONSTRAINT fk_referral_codes_campaign
|
||||
FOREIGN KEY (campaign_id) REFERENCES referral_campaigns (id) ON DELETE RESTRICT
|
||||
) ENGINE = InnoDB;
|
||||
|
||||
CREATE TABLE referral_bindings (
|
||||
id CHAR(36) NOT NULL,
|
||||
inviter_user_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
invitee_user_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
code_id CHAR(36) NOT NULL,
|
||||
campaign_id CHAR(36) NOT NULL DEFAULT '00000000-0000-0000-0000-000000000001',
|
||||
bound_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
rewarded_at DATETIME(6) NULL,
|
||||
reward_settlement_id CHAR(36) NULL,
|
||||
reward_status VARCHAR(24) CHARACTER SET ascii COLLATE ascii_bin
|
||||
NOT NULL DEFAULT 'PENDING',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_referral_bindings_invitee (invitee_user_id),
|
||||
UNIQUE KEY uk_referral_bindings_settlement (reward_settlement_id),
|
||||
INDEX idx_referral_bindings_inviter (inviter_user_id),
|
||||
CONSTRAINT fk_referral_bindings_code
|
||||
FOREIGN KEY (code_id) REFERENCES referral_codes (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT fk_referral_bindings_campaign
|
||||
FOREIGN KEY (campaign_id) REFERENCES referral_campaigns (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT chk_referral_bindings_no_self CHECK (inviter_user_id <> invitee_user_id),
|
||||
CONSTRAINT chk_referral_bindings_reward_pair CHECK (
|
||||
(
|
||||
reward_status IN ('PENDING', 'INELIGIBLE_BUDGET')
|
||||
AND rewarded_at IS NULL
|
||||
AND reward_settlement_id IS NULL
|
||||
)
|
||||
OR
|
||||
(
|
||||
reward_status = 'REWARDED'
|
||||
AND rewarded_at IS NOT NULL
|
||||
AND reward_settlement_id IS NOT NULL
|
||||
)
|
||||
)
|
||||
) ENGINE = InnoDB;
|
||||
|
||||
CREATE TABLE credit_usage_records (
|
||||
id CHAR(36) NOT NULL,
|
||||
reservation_id CHAR(36) NOT NULL,
|
||||
user_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
rate_version_id CHAR(36) NOT NULL,
|
||||
usage_kind VARCHAR(8) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
asr_millis BIGINT NULL,
|
||||
input_tokens BIGINT NULL,
|
||||
output_tokens BIGINT NULL,
|
||||
charged_credits BIGINT NOT NULL,
|
||||
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_credit_usage_reservation (reservation_id),
|
||||
INDEX idx_credit_usage_user_created (user_id, created_at),
|
||||
CONSTRAINT fk_credit_usage_reservation
|
||||
FOREIGN KEY (reservation_id) REFERENCES credit_reservations (id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_credit_usage_rate
|
||||
FOREIGN KEY (rate_version_id) REFERENCES credit_rate_versions (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT fk_credit_usage_account
|
||||
FOREIGN KEY (user_id) REFERENCES credit_accounts (user_id) ON DELETE CASCADE,
|
||||
CONSTRAINT chk_credit_usage_credits CHECK (charged_credits >= 0),
|
||||
CONSTRAINT chk_credit_usage_shape CHECK (
|
||||
(
|
||||
usage_kind = 'ASR'
|
||||
AND asr_millis >= 0
|
||||
AND input_tokens IS NULL
|
||||
AND output_tokens IS NULL
|
||||
)
|
||||
OR
|
||||
(
|
||||
usage_kind = 'LLM'
|
||||
AND asr_millis IS NULL
|
||||
AND input_tokens >= 0
|
||||
AND output_tokens >= 0
|
||||
)
|
||||
)
|
||||
) ENGINE = InnoDB;
|
||||
|
||||
CREATE TABLE credit_ledger (
|
||||
id CHAR(36) NOT NULL,
|
||||
user_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
entry_type VARCHAR(32) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
amount_delta BIGINT NOT NULL,
|
||||
balance_after BIGINT NOT NULL,
|
||||
idempotency_key VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
|
||||
reference_id CHAR(36) NULL,
|
||||
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_credit_ledger_idempotency (user_id, idempotency_key),
|
||||
INDEX idx_credit_ledger_user_created (user_id, created_at, id),
|
||||
INDEX idx_credit_ledger_reference (reference_id),
|
||||
CONSTRAINT chk_credit_ledger_balance CHECK (balance_after >= 0),
|
||||
CONSTRAINT chk_credit_ledger_key_length
|
||||
CHECK (CHAR_LENGTH(idempotency_key) BETWEEN 8 AND 128),
|
||||
CONSTRAINT chk_credit_ledger_type CHECK (
|
||||
entry_type IN (
|
||||
'SIGNUP_TRIAL',
|
||||
'MANUAL_GRANT',
|
||||
'USAGE_RESERVE',
|
||||
'USAGE_SETTLE',
|
||||
'USAGE_RELEASE',
|
||||
'USAGE_REFUND',
|
||||
'REFERRAL_INVITER',
|
||||
'REFERRAL_INVITEE',
|
||||
'STOREKIT_PURCHASE',
|
||||
'SUBSCRIPTION_GRANT'
|
||||
)
|
||||
)
|
||||
) ENGINE = InnoDB;
|
||||
|
||||
-- The application repository exposes append-only operations for ledger, rate,
|
||||
-- and usage tables. Production additionally grants the runtime user SELECT and
|
||||
-- INSERT only on these tables. Avoiding stored triggers keeps migrations
|
||||
-- compatible with binary-logged MySQL without granting global SUPER privilege.
|
||||
@@ -0,0 +1,76 @@
|
||||
CREATE TABLE provider_requests (
|
||||
request_id VARCHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
account_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
provider_id VARCHAR(64) NOT NULL,
|
||||
capability VARCHAR(32) NOT NULL,
|
||||
status VARCHAR(24) NOT NULL,
|
||||
provider_request_id VARCHAR(128) NULL,
|
||||
server_duration_millis BIGINT UNSIGNED NULL,
|
||||
error_code VARCHAR(96) NULL,
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
completed_at TIMESTAMP(6) NULL,
|
||||
PRIMARY KEY (request_id),
|
||||
INDEX idx_provider_requests_account_created (account_id, created_at),
|
||||
INDEX idx_provider_requests_provider_created (provider_id, created_at),
|
||||
CONSTRAINT fk_provider_requests_account
|
||||
FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE usage_records (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
request_id VARCHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
meter VARCHAR(32) NOT NULL,
|
||||
units BIGINT UNSIGNED NOT NULL,
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_usage_records_request_meter (request_id, meter),
|
||||
CONSTRAINT fk_usage_records_provider_request
|
||||
FOREIGN KEY (request_id) REFERENCES provider_requests (request_id)
|
||||
ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE gateway_grants (
|
||||
id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
account_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
idempotency_key VARCHAR(128) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
expires_at TIMESTAMP(6) NOT NULL,
|
||||
revoked_at TIMESTAMP(6) NULL,
|
||||
created_at TIMESTAMP(6) NOT NULL,
|
||||
updated_at TIMESTAMP(6) NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_gateway_grants_account_idempotency (account_id, idempotency_key),
|
||||
INDEX idx_gateway_grants_account_active (account_id, revoked_at, expires_at),
|
||||
CONSTRAINT fk_gateway_grants_account
|
||||
FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE gateway_grant_scopes (
|
||||
grant_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
capability VARCHAR(32) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
PRIMARY KEY (grant_id, capability),
|
||||
CONSTRAINT fk_gateway_grant_scopes_grant
|
||||
FOREIGN KEY (grant_id) REFERENCES gateway_grants (id) ON DELETE CASCADE,
|
||||
CONSTRAINT chk_gateway_grant_scope
|
||||
CHECK (capability IN ('POLISH', 'AI', 'AGENT', 'ASR'))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE gateway_refresh_tokens (
|
||||
id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
grant_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
family_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
token_hash CHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
replaced_by_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NULL,
|
||||
rotation_idempotency_key VARCHAR(128) CHARACTER SET ascii COLLATE ascii_bin NULL,
|
||||
expires_at TIMESTAMP(6) NOT NULL,
|
||||
revoked_at TIMESTAMP(6) NULL,
|
||||
reuse_detected_at TIMESTAMP(6) NULL,
|
||||
created_at TIMESTAMP(6) NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_gateway_refresh_token_hash (token_hash),
|
||||
INDEX idx_gateway_refresh_grant (grant_id),
|
||||
INDEX idx_gateway_refresh_family (family_id),
|
||||
CONSTRAINT fk_gateway_refresh_grant
|
||||
FOREIGN KEY (grant_id) REFERENCES gateway_grants (id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_gateway_refresh_replacement
|
||||
FOREIGN KEY (replaced_by_id) REFERENCES gateway_refresh_tokens (id) ON DELETE RESTRICT
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@@ -0,0 +1,51 @@
|
||||
CREATE TABLE devicecheck_trial_claims (
|
||||
device_token_hash CHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
account_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
status VARCHAR(16) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
created_at DATETIME(6) NOT NULL,
|
||||
updated_at DATETIME(6) NOT NULL,
|
||||
PRIMARY KEY (device_token_hash),
|
||||
KEY ix_devicecheck_trial_account (account_id),
|
||||
CONSTRAINT fk_devicecheck_trial_account
|
||||
FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE CASCADE,
|
||||
CONSTRAINT chk_devicecheck_trial_status
|
||||
CHECK (status IN ('RESERVED', 'APPLE_MARKED', 'COMPLETED', 'REJECTED'))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE app_attest_challenges (
|
||||
id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
key_id VARCHAR(128) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
account_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NULL,
|
||||
purpose VARCHAR(16) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
challenge_hash CHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
status VARCHAR(16) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
expires_at DATETIME(6) NOT NULL,
|
||||
consumed_at DATETIME(6) NULL,
|
||||
created_at DATETIME(6) NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY ix_app_attest_challenge_expiry (expires_at),
|
||||
KEY ix_app_attest_challenge_account (account_id),
|
||||
CONSTRAINT fk_app_attest_challenge_account
|
||||
FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE CASCADE,
|
||||
CONSTRAINT chk_app_attest_challenge_purpose
|
||||
CHECK (purpose IN ('ATTESTATION', 'ASSERTION')),
|
||||
CONSTRAINT chk_app_attest_challenge_status
|
||||
CHECK (status IN ('ISSUED', 'CONSUMED', 'EXPIRED'))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE app_attest_keys (
|
||||
key_id VARCHAR(128) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
public_key_base64 VARCHAR(512) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
receipt_base64 MEDIUMTEXT CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
sign_counter BIGINT NOT NULL DEFAULT 0,
|
||||
account_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NULL,
|
||||
status VARCHAR(16) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
created_at DATETIME(6) NOT NULL,
|
||||
updated_at DATETIME(6) NOT NULL,
|
||||
PRIMARY KEY (key_id),
|
||||
KEY ix_app_attest_keys_account (account_id),
|
||||
CONSTRAINT fk_app_attest_keys_account
|
||||
FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE SET NULL,
|
||||
CONSTRAINT chk_app_attest_counter_non_negative CHECK (sign_counter >= 0),
|
||||
CONSTRAINT chk_app_attest_key_status CHECK (status IN ('ACTIVE', 'REVOKED'))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
@@ -0,0 +1,113 @@
|
||||
ALTER TABLE accounts
|
||||
ADD COLUMN identity_fingerprint CHAR(64) CHARACTER SET ascii COLLATE ascii_bin NULL,
|
||||
ADD COLUMN anti_abuse_restricted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
ADD UNIQUE KEY uq_accounts_identity_fingerprint (identity_fingerprint);
|
||||
|
||||
CREATE TABLE account_identity_tombstones (
|
||||
identity_fingerprint CHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
deleted_at DATETIME(6) NOT NULL,
|
||||
expires_at DATETIME(6) NOT NULL,
|
||||
PRIMARY KEY (identity_fingerprint),
|
||||
INDEX ix_account_tombstones_expiry (expires_at),
|
||||
CONSTRAINT chk_account_tombstone_interval CHECK (expires_at > deleted_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE apple_revocation_outbox (
|
||||
id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
encrypted_refresh_token MEDIUMTEXT NULL,
|
||||
created_at DATETIME(6) NOT NULL,
|
||||
next_attempt_at DATETIME(6) NOT NULL,
|
||||
attempt_count INT NOT NULL DEFAULT 0,
|
||||
completed_at DATETIME(6) NULL,
|
||||
PRIMARY KEY (id),
|
||||
INDEX ix_apple_revocation_pending (completed_at, next_attempt_at),
|
||||
CONSTRAINT chk_apple_revocation_attempts CHECK (attempt_count >= 0),
|
||||
CONSTRAINT chk_apple_revocation_token_lifecycle CHECK (
|
||||
(completed_at IS NULL AND encrypted_refresh_token IS NOT NULL)
|
||||
OR
|
||||
(completed_at IS NOT NULL AND encrypted_refresh_token IS NULL)
|
||||
)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- Existing V2/V3 tables predated account foreign keys. Remove only already-orphaned
|
||||
-- mutable state before enforcing referential integrity. The immutable credit ledger
|
||||
-- intentionally remains pseudonymized by its now-unmapped random account UUID.
|
||||
DELETE ur
|
||||
FROM usage_records ur
|
||||
LEFT JOIN provider_requests pr ON pr.request_id = ur.request_id
|
||||
WHERE pr.request_id IS NULL;
|
||||
|
||||
DELETE rb
|
||||
FROM referral_bindings rb
|
||||
LEFT JOIN accounts inviter ON inviter.id = rb.inviter_user_id
|
||||
LEFT JOIN accounts invitee ON invitee.id = rb.invitee_user_id
|
||||
LEFT JOIN referral_codes rc ON rc.id = rb.code_id
|
||||
LEFT JOIN accounts code_owner ON code_owner.id = rc.owner_user_id
|
||||
WHERE inviter.id IS NULL
|
||||
OR invitee.id IS NULL
|
||||
OR rc.id IS NULL
|
||||
OR code_owner.id IS NULL
|
||||
OR rc.owner_user_id <> rb.inviter_user_id;
|
||||
|
||||
DELETE rc
|
||||
FROM referral_codes rc
|
||||
LEFT JOIN accounts a ON a.id = rc.owner_user_id
|
||||
WHERE a.id IS NULL;
|
||||
|
||||
DELETE cr
|
||||
FROM credit_reservations cr
|
||||
LEFT JOIN accounts a ON a.id = cr.user_id
|
||||
WHERE a.id IS NULL;
|
||||
|
||||
DELETE ca
|
||||
FROM credit_accounts ca
|
||||
LEFT JOIN accounts a ON a.id = ca.user_id
|
||||
WHERE a.id IS NULL;
|
||||
|
||||
DELETE ur
|
||||
FROM usage_records ur
|
||||
JOIN provider_requests pr ON pr.request_id = ur.request_id
|
||||
LEFT JOIN accounts a ON a.id = pr.account_id
|
||||
WHERE a.id IS NULL;
|
||||
|
||||
DELETE pr
|
||||
FROM provider_requests pr
|
||||
LEFT JOIN accounts a ON a.id = pr.account_id
|
||||
WHERE a.id IS NULL;
|
||||
|
||||
DELETE gg
|
||||
FROM gateway_grants gg
|
||||
LEFT JOIN accounts a ON a.id = gg.account_id
|
||||
WHERE a.id IS NULL;
|
||||
|
||||
ALTER TABLE credit_accounts
|
||||
ADD CONSTRAINT fk_credit_accounts_user
|
||||
FOREIGN KEY (user_id) REFERENCES accounts (id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE credit_reservations
|
||||
ADD CONSTRAINT fk_credit_reservations_user
|
||||
FOREIGN KEY (user_id) REFERENCES accounts (id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE referral_bindings
|
||||
DROP FOREIGN KEY fk_referral_bindings_code;
|
||||
|
||||
ALTER TABLE referral_bindings
|
||||
ADD CONSTRAINT fk_referral_bindings_code
|
||||
FOREIGN KEY (code_id) REFERENCES referral_codes (id) ON DELETE CASCADE,
|
||||
ADD CONSTRAINT fk_referral_bindings_inviter
|
||||
FOREIGN KEY (inviter_user_id) REFERENCES accounts (id) ON DELETE CASCADE,
|
||||
ADD CONSTRAINT fk_referral_bindings_invitee
|
||||
FOREIGN KEY (invitee_user_id) REFERENCES accounts (id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE referral_codes
|
||||
ADD COLUMN owner_identity_fingerprint CHAR(64) CHARACTER SET ascii COLLATE ascii_bin NULL,
|
||||
ADD CONSTRAINT fk_referral_codes_owner
|
||||
FOREIGN KEY (owner_user_id) REFERENCES accounts (id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE usage_records
|
||||
DROP FOREIGN KEY fk_usage_records_provider_request;
|
||||
|
||||
ALTER TABLE usage_records
|
||||
ADD CONSTRAINT fk_usage_records_provider_request
|
||||
FOREIGN KEY (request_id) REFERENCES provider_requests (request_id) ON DELETE CASCADE;
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
-- Make client request IDs idempotent within an account, not globally.
|
||||
ALTER TABLE usage_records
|
||||
DROP FOREIGN KEY fk_usage_records_provider_request,
|
||||
DROP INDEX uq_usage_records_request_meter,
|
||||
ADD COLUMN account_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NULL AFTER id;
|
||||
|
||||
UPDATE usage_records ur
|
||||
JOIN provider_requests pr ON pr.request_id = ur.request_id
|
||||
SET ur.account_id = pr.account_id;
|
||||
|
||||
ALTER TABLE usage_records
|
||||
MODIFY account_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL;
|
||||
|
||||
-- Normalize every legacy value before installing the binary status column and
|
||||
-- stricter CHECK. Unknown historical states are held for manual review rather
|
||||
-- than making a non-transactional MySQL migration fail halfway through.
|
||||
UPDATE provider_requests
|
||||
SET status = CASE UPPER(status)
|
||||
WHEN 'SUCCEEDED' THEN 'SETTLED'
|
||||
WHEN 'FAILED' THEN 'MANUAL_REVIEW'
|
||||
WHEN 'CLAIMED' THEN 'CLAIMED'
|
||||
WHEN 'STARTED' THEN 'STARTED'
|
||||
WHEN 'SETTLEMENT_PENDING' THEN 'SETTLEMENT_PENDING'
|
||||
WHEN 'SETTLED' THEN 'SETTLED'
|
||||
WHEN 'RELEASED' THEN 'RELEASED'
|
||||
WHEN 'MANUAL_REVIEW' THEN 'MANUAL_REVIEW'
|
||||
ELSE 'MANUAL_REVIEW'
|
||||
END;
|
||||
|
||||
ALTER TABLE provider_requests
|
||||
ADD COLUMN reservation_id CHAR(36) NULL AFTER account_id,
|
||||
ADD COLUMN usage_meter VARCHAR(32) NULL AFTER provider_request_id,
|
||||
ADD COLUMN usage_units BIGINT UNSIGNED NULL AFTER usage_meter,
|
||||
ADD COLUMN usage_input_units BIGINT UNSIGNED NULL AFTER usage_units,
|
||||
ADD COLUMN usage_output_units BIGINT UNSIGNED NULL AFTER usage_input_units,
|
||||
MODIFY COLUMN status VARCHAR(24) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
DROP PRIMARY KEY,
|
||||
ADD PRIMARY KEY (account_id, request_id),
|
||||
ADD UNIQUE KEY uq_provider_requests_reservation (reservation_id),
|
||||
ADD INDEX idx_provider_requests_status_created (status, created_at),
|
||||
ADD CONSTRAINT chk_provider_request_status CHECK (
|
||||
status IN (
|
||||
'CLAIMED',
|
||||
'STARTED',
|
||||
'SETTLEMENT_PENDING',
|
||||
'SETTLED',
|
||||
'RELEASED',
|
||||
'MANUAL_REVIEW'
|
||||
)
|
||||
);
|
||||
|
||||
ALTER TABLE usage_records
|
||||
ADD UNIQUE KEY uq_usage_records_account_request_meter (account_id, request_id, meter),
|
||||
ADD CONSTRAINT fk_usage_records_provider_request
|
||||
FOREIGN KEY (account_id, request_id)
|
||||
REFERENCES provider_requests (account_id, request_id)
|
||||
ON DELETE CASCADE;
|
||||
|
||||
-- A rotated refresh row points to its replacement. RESTRICT can block the
|
||||
-- account -> grant -> refresh cascade when an account is deleted.
|
||||
ALTER TABLE gateway_refresh_tokens
|
||||
DROP FOREIGN KEY fk_gateway_refresh_replacement,
|
||||
ADD CONSTRAINT fk_gateway_refresh_replacement
|
||||
FOREIGN KEY (replaced_by_id) REFERENCES gateway_refresh_tokens (id)
|
||||
ON DELETE SET NULL;
|
||||
@@ -0,0 +1,49 @@
|
||||
-- Initial integer-credit rate cards. Future price changes must insert a new
|
||||
-- immutable version and close the previous effective interval.
|
||||
INSERT INTO credit_rate_versions (
|
||||
id,
|
||||
kind,
|
||||
provider,
|
||||
model,
|
||||
effective_from,
|
||||
effective_until,
|
||||
asr_credits_numerator,
|
||||
asr_millis_denominator,
|
||||
created_at
|
||||
) VALUES (
|
||||
'10000000-0000-0000-0000-000000000001',
|
||||
'ASR',
|
||||
'volcengine-sauc-v3',
|
||||
'volc.seedasr.sauc.duration',
|
||||
'1970-01-01 00:00:00.000000',
|
||||
NULL,
|
||||
1,
|
||||
1000,
|
||||
UTC_TIMESTAMP(6)
|
||||
);
|
||||
|
||||
INSERT INTO credit_rate_versions (
|
||||
id,
|
||||
kind,
|
||||
provider,
|
||||
model,
|
||||
effective_from,
|
||||
effective_until,
|
||||
input_credits_numerator,
|
||||
input_tokens_denominator,
|
||||
output_credits_numerator,
|
||||
output_tokens_denominator,
|
||||
created_at
|
||||
) VALUES (
|
||||
'10000000-0000-0000-0000-000000000002',
|
||||
'LLM',
|
||||
'deepseek',
|
||||
'deepseek-v4-flash',
|
||||
'1970-01-01 00:00:00.000000',
|
||||
NULL,
|
||||
1,
|
||||
100,
|
||||
2,
|
||||
100,
|
||||
UTC_TIMESTAMP(6)
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"applinks": {
|
||||
"details": [
|
||||
{
|
||||
"appIDs": [
|
||||
"{{APPLE_APP_ID}}"
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"/": "/i/*",
|
||||
"comment": "Open first-party OSG invitation links in the app"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex,nofollow,noarchive">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>OSG 邀请 / Invitation</title>
|
||||
<style nonce="{{CSP_NONCE}}">
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
background: #f5f5f7;
|
||||
color: #1d1d1f;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: max(20px, env(safe-area-inset-top)) max(20px, env(safe-area-inset-right))
|
||||
max(20px, env(safe-area-inset-bottom)) max(20px, env(safe-area-inset-left));
|
||||
}
|
||||
main {
|
||||
width: min(100%, 460px);
|
||||
padding: clamp(24px, 7vw, 40px);
|
||||
border: 1px solid #dedee3;
|
||||
border-radius: 24px;
|
||||
background: #fff;
|
||||
text-align: center;
|
||||
box-shadow: 0 16px 48px rgb(0 0 0 / 8%);
|
||||
}
|
||||
.language-switcher {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
margin: -8px -8px 20px 0;
|
||||
}
|
||||
.language-button {
|
||||
min-height: 36px;
|
||||
padding: 7px 11px;
|
||||
border: 1px solid #d2d2d7;
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
font-size: .82rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.language-button[aria-pressed="true"] {
|
||||
border-color: #1d1d1f;
|
||||
background: #1d1d1f;
|
||||
color: #fff;
|
||||
}
|
||||
h1 { margin: 0 0 12px; font-size: clamp(1.65rem, 7vw, 2.25rem); line-height: 1.15; }
|
||||
p { margin: 10px 0; color: #6e6e73; line-height: 1.55; }
|
||||
code {
|
||||
display: block;
|
||||
margin: 24px 0;
|
||||
padding: 17px 10px;
|
||||
overflow-wrap: anywhere;
|
||||
border: 1px solid #e5e5ea;
|
||||
border-radius: 14px;
|
||||
background: #f5f5f7;
|
||||
color: #1d1d1f;
|
||||
font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
|
||||
font-size: clamp(1rem, 4.8vw, 1.35rem);
|
||||
letter-spacing: .05em;
|
||||
user-select: all;
|
||||
}
|
||||
.actions { display: grid; gap: 12px; }
|
||||
.action {
|
||||
display: grid;
|
||||
min-height: 50px;
|
||||
place-items: center;
|
||||
padding: 13px 18px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 14px;
|
||||
font: inherit;
|
||||
font-weight: 650;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
.primary { background: #0071e3; color: #fff; }
|
||||
.secondary { border-color: #d2d2d7; background: #fff; color: #1d1d1f; }
|
||||
.store-link { background: #1d1d1f; color: #fff; }
|
||||
.action:focus-visible, .language-button:focus-visible {
|
||||
outline: 3px solid #69aaf5;
|
||||
outline-offset: 3px;
|
||||
}
|
||||
.hint { margin-top: 22px; font-size: .88rem; }
|
||||
#copy-status { min-height: 1.4em; margin-bottom: 0; font-size: .9rem; }
|
||||
[data-language] { display: none; }
|
||||
html[lang="zh-CN"] [data-language="zh-CN"],
|
||||
html[lang="en"] [data-language="en"] { display: inline; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root { background: #101214; color: #f4f5f7; }
|
||||
main { border-color: #30343a; background: #1a1d21; box-shadow: none; }
|
||||
p { color: #b5bbc4; }
|
||||
code { border-color: #3a3f46; background: #282c32; color: #f4f5f7; }
|
||||
.language-button { border-color: #555b64; }
|
||||
.language-button[aria-pressed="true"] { border-color: #f4f5f7; background: #f4f5f7; color: #17191c; }
|
||||
.secondary { border-color: #555b64; background: #282c32; color: #f4f5f7; }
|
||||
.store-link { background: #f4f5f7; color: #17191c; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after { scroll-behavior: auto !important; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<nav class="language-switcher" aria-label="Language / 语言">
|
||||
<button class="language-button" id="language-zh" type="button" aria-pressed="true">中文</button>
|
||||
<button class="language-button" id="language-en" type="button" aria-pressed="false">English</button>
|
||||
</nav>
|
||||
<h1>
|
||||
<span data-language="zh-CN">加入 OSG</span>
|
||||
<span data-language="en">Join OSG</span>
|
||||
</h1>
|
||||
<p>
|
||||
<span data-language="zh-CN">使用此邀请码开始体验。</span>
|
||||
<span data-language="en">Use this invitation code to get started.</span>
|
||||
</p>
|
||||
<code id="invite-code" aria-label="邀请码 / Invitation code">{{INVITE_CODE}}</code>
|
||||
<div class="actions">
|
||||
<button class="action primary" id="copy-button" type="button">
|
||||
<span data-language="zh-CN">复制邀请码</span>
|
||||
<span data-language="en">Copy invitation code</span>
|
||||
</button>
|
||||
<a class="action secondary" href="{{UNIVERSAL_LINK}}" rel="noopener">
|
||||
<span data-language="zh-CN">打开 App</span>
|
||||
<span data-language="en">Open App</span>
|
||||
</a>
|
||||
<a class="action store-link" href="{{APP_STORE_URL}}" rel="noopener noreferrer">
|
||||
<span data-language="zh-CN">前往 App Store</span>
|
||||
<span data-language="en">Download on the App Store</span>
|
||||
</a>
|
||||
</div>
|
||||
<p id="copy-status" role="status" aria-live="polite"></p>
|
||||
<p class="hint">
|
||||
<span data-language="zh-CN">若“打开 App”仍停留在浏览器,请从信息或邮件中再次轻点原邀请链接。</span>
|
||||
<span data-language="en">If “Open App” stays in the browser, tap the original invitation link again from Messages or Mail.</span>
|
||||
</p>
|
||||
</main>
|
||||
<script nonce="{{CSP_NONCE}}">
|
||||
const copyButton = document.getElementById("copy-button");
|
||||
const inviteCode = document.getElementById("invite-code").textContent;
|
||||
const copyStatus = document.getElementById("copy-status");
|
||||
const zhButton = document.getElementById("language-zh");
|
||||
const enButton = document.getElementById("language-en");
|
||||
let language = navigator.language.toLowerCase().startsWith("zh") ? "zh-CN" : "en";
|
||||
|
||||
function setLanguage(nextLanguage) {
|
||||
language = nextLanguage;
|
||||
document.documentElement.lang = language;
|
||||
zhButton.setAttribute("aria-pressed", String(language === "zh-CN"));
|
||||
enButton.setAttribute("aria-pressed", String(language === "en"));
|
||||
copyStatus.textContent = "";
|
||||
}
|
||||
|
||||
zhButton.addEventListener("click", () => setLanguage("zh-CN"));
|
||||
enButton.addEventListener("click", () => setLanguage("en"));
|
||||
setLanguage(language);
|
||||
|
||||
copyButton.addEventListener("click", async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(inviteCode);
|
||||
copyStatus.textContent = language === "zh-CN" ? "已复制" : "Copied";
|
||||
} catch {
|
||||
const selection = window.getSelection();
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(document.getElementById("invite-code"));
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
copyStatus.textContent = language === "zh-CN"
|
||||
? "无法自动复制,邀请码已选中"
|
||||
: "Automatic copy failed; the code is selected";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,45 @@
|
||||
<configuration>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>{"time":"%date{ISO8601}","level":"%level","logger":"%logger{36}","message":"%replace(%msg){'[\r\n]+',' '}"}%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger name="io.netty" level="WARN"/>
|
||||
<logger name="org.jetbrains.exposed" level="WARN"/>
|
||||
<logger name="com.zaxxer.hikari" level="INFO"/>
|
||||
|
||||
<root level="${LOG_LEVEL:-INFO}">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</root>
|
||||
</configuration>
|
||||
<configuration>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%date{ISO8601} %-5level [%thread] %logger{24} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger name="io.netty" level="WARN"/>
|
||||
<logger name="org.jetbrains.exposed" level="WARN"/>
|
||||
<logger name="com.zaxxer.hikari" level="INFO"/>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</root>
|
||||
</configuration>
|
||||
<configuration>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd'T'HH:mm:ss.SSSXXX} %-5level [%thread] %logger{36} requestId=%X{requestId:-} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger name="io.netty" level="WARN"/>
|
||||
<logger name="org.jetbrains.exposed" level="WARN"/>
|
||||
<logger name="com.zaxxer.hikari" level="INFO"/>
|
||||
|
||||
<root level="${LOG_LEVEL:-INFO}">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</root>
|
||||
</configuration>
|
||||
Reference in New Issue
Block a user