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,66 @@
|
||||
# OSGAccountServer architecture
|
||||
|
||||
OSGAccountServer is a modular Ktor application. It owns OSG accounts, credits,
|
||||
referrals, integrity checks, and metered access to OSG-hosted providers.
|
||||
|
||||
It does not replace the app's local ASR, BYOK provider access, or iCloud sync.
|
||||
|
||||
## Trust boundaries
|
||||
|
||||
1. Sign in with Apple identifies a user. The service stores only Apple's stable
|
||||
subject, encrypted Apple refresh credentials, and internal identifiers.
|
||||
2. The host app owns account-management credentials in its private Keychain.
|
||||
3. The keyboard extension receives a separately revocable gateway grant. That
|
||||
grant cannot delete an account, redeem referrals, or mutate credits directly.
|
||||
4. Provider credentials exist only in the server process.
|
||||
5. Audio and text are transient request data. They must not be persisted or
|
||||
included in logs, traces, metrics labels, or error messages.
|
||||
|
||||
## Modules
|
||||
|
||||
- `auth` verifies Apple credentials and manages rotating application sessions.
|
||||
- `account` reads and deletes accounts.
|
||||
- `appleevents` handles consent and account lifecycle notifications.
|
||||
- `credits` owns the immutable ledger and its balance projection.
|
||||
- `referrals` binds one inviter and awards both users after qualified usage.
|
||||
- `integrity` evaluates DeviceCheck and App Attest evidence.
|
||||
- `gateway` proxies Volcengine ASR and DeepSeek text requests.
|
||||
- `inviteweb` serves the first-party invitation landing page.
|
||||
|
||||
Modules communicate through narrow ports. Provider clients, Apple clients,
|
||||
integrity clients, clocks, token generators, and repositories are replaceable in
|
||||
tests.
|
||||
|
||||
## Monetary invariants
|
||||
|
||||
- Credit amounts are signed 64-bit integers; public inputs never accept decimal
|
||||
credit values.
|
||||
- Ledger rows are immutable.
|
||||
- Every mutation has a unique idempotency key.
|
||||
- Available and reserved balances never become negative.
|
||||
- Reservation, settlement, release, refund, and referral rewards run in database
|
||||
transactions with the account rows locked.
|
||||
- A referral can transition to `rewarded` exactly once.
|
||||
- Usage records reference the exact rate-card version used for settlement.
|
||||
|
||||
## Hosted request lifecycle
|
||||
|
||||
1. Authenticate a gateway-scoped principal and verify request integrity.
|
||||
2. Validate payload size and feature-specific policy.
|
||||
3. Reserve the maximum expected credits.
|
||||
4. Call the allowlisted upstream provider.
|
||||
5. Settle from provider usage or server-observed ASR duration.
|
||||
6. Release on transport/provider failure or empty output.
|
||||
7. Qualify a pending referral only after a successful non-zero settlement.
|
||||
|
||||
## Data retention
|
||||
|
||||
- No audio, transcript, prompt, context, or generated response body is stored.
|
||||
- Authentication and integrity payloads are retained only as hashes or validated
|
||||
claims required for replay protection.
|
||||
- Usage metadata contains feature, model, metering units, latency, status, and
|
||||
rate-card version.
|
||||
- Operational logs use request IDs and internal opaque IDs, never Apple subjects
|
||||
or bearer credentials.
|
||||
- Account deletion revokes credentials and removes user-linked records. Only
|
||||
non-identifying aggregate service metrics may remain.
|
||||
@@ -0,0 +1,49 @@
|
||||
# MySQL 8.4 备份与恢复
|
||||
|
||||
## 策略
|
||||
|
||||
- 由 1Panel 每日执行一次完整备份,保留 30 天;备份写入与应用主机隔离的对象存储。
|
||||
- MySQL 启用 binary log,至少保留 7 天,用于完整备份后的时间点恢复。
|
||||
- 数据库备份和对象存储均启用加密;备份账号只授予备份所需权限。
|
||||
- 每月至少在隔离环境恢复一次,并记录恢复点、耗时和校验结果。未做恢复演练的备份不能视为可用。
|
||||
- 发布含 Flyway 迁移的镜像前,先完成一次可验证的备份。Flyway 迁移只向前执行。
|
||||
|
||||
## 命令行完整备份
|
||||
|
||||
先用 `mysql_config_editor` 为受限备份账号建立本机登录路径,避免把密码写入命令行或脚本:
|
||||
|
||||
```bash
|
||||
mysql_config_editor set --login-path=osg-backup \
|
||||
--host=127.0.0.1 --user=osg_backup --password
|
||||
```
|
||||
|
||||
在只允许管理员读取的目录执行一致性备份:
|
||||
|
||||
```bash
|
||||
umask 077
|
||||
mysqldump --login-path=osg-backup \
|
||||
--single-transaction --quick --routines --triggers --events \
|
||||
--set-gtid-purged=OFF --default-character-set=utf8mb4 \
|
||||
osg_account | gzip -9 > "osg_account-$(date -u +%Y%m%dT%H%M%SZ).sql.gz"
|
||||
```
|
||||
|
||||
备份任务应在上传后生成 SHA-256 校验值,并按保留策略删除过期副本。不要将备份、登录路径文件或校验清单提交到 Git。
|
||||
|
||||
## 隔离恢复演练
|
||||
|
||||
恢复目标必须是新建的隔离 MySQL 8.4 实例,不能覆盖生产库:
|
||||
|
||||
```bash
|
||||
mysql --login-path=osg-restore -e \
|
||||
"CREATE DATABASE osg_account_restore CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci"
|
||||
gzip -dc osg_account-YYYYMMDDTHHMMSSZ.sql.gz | \
|
||||
mysql --login-path=osg-restore osg_account_restore
|
||||
mysql --login-path=osg-restore osg_account_restore -e \
|
||||
"SELECT version, success FROM flyway_schema_history ORDER BY installed_rank"
|
||||
```
|
||||
|
||||
恢复后执行应用只读验收、表行数抽样、外键检查和邀请查询测试。时间点恢复应先还原完整备份,再仅回放目标时间之前的 binary log。确认结果后销毁隔离实例及明文恢复文件。
|
||||
|
||||
## 1Panel
|
||||
|
||||
1Panel 的数据库备份任务使用相同的每日频率、异地存储、加密和保留期。启用任务成功/失败通知,并每月下载一份备份到隔离 MySQL 8.4 完成恢复演练。1Panel 面板、工单和截图中不得出现数据库密码、Apple 私钥或供应商 API Key。
|
||||
@@ -0,0 +1,198 @@
|
||||
# OSG Account Server 部署指南
|
||||
|
||||
本方案使用一个非 root、只读文件系统兼容的 JDK 21 容器。OpenResty 终止 TLS 并反向代理,
|
||||
MySQL 复用已有实例。`compose.yaml` 不会创建数据库容器。
|
||||
|
||||
## 1. 前置条件
|
||||
|
||||
- `account.osglab.com` 与 `osglab.com` 已解析到 1Panel 主机。
|
||||
- Docker Engine、Docker Compose v2 和 1Panel OpenResty 已安装。
|
||||
- 已有 MySQL 8 实例可从应用容器通过 Docker 外部网络或私网访问。
|
||||
- 服务器时钟同步,Apple、DeepSeek、Volcengine 的出站 HTTPS/WSS 可用。
|
||||
- App 的 Associated Domains 包含 `applinks:osglab.com`。
|
||||
|
||||
邀请使用一方 Universal Link,不依赖 Firebase Dynamic Links 或 Branch。
|
||||
|
||||
## 2. 注册邀请网页路由
|
||||
|
||||
网页实现在 `features/inviteweb`,资源在 `src/main/resources/invite/index.html`。组合根应只注册
|
||||
一个 `/i/{code}` 路由;不要与旧 `features/invite` 路由同时挂载。`ReferralLookupPort` 是只读、
|
||||
大小写敏感的验证边界,适配器应在 referrals 模块中用有界查询检查邀请码及活动状态:
|
||||
|
||||
先在 Koin 注册一个 `ReferralLookupPort` 实现;公共接线会从现有 `AppConfig` 读取 App Store URL、
|
||||
邀请域名和 Apple Team ID/bundle ID:
|
||||
|
||||
```kotlin
|
||||
routing {
|
||||
configureInviteWebRoutes()
|
||||
}
|
||||
```
|
||||
|
||||
测试或不使用 Koin 的宿主可调用显式重载
|
||||
`configureInviteWebRoutes(referralLookup, InviteWebConfig(...))`。
|
||||
|
||||
邀请码规则与当前生成器一致:16 字节随机值编码为无填充 Base64URL,即固定 22 位、区分大小写,
|
||||
字符白名单为 `A-Z`、`a-z`、`0-9`、`_`、`-`。结构非法或查无记录均返回相同 404;查询超时或
|
||||
失败返回不泄露内部信息的 503。页面和两个 AASA endpoint 均发送 `no-store`。
|
||||
|
||||
## 3. 准备现有 MySQL
|
||||
|
||||
先创建数据库,字符集使用 `utf8mb4`。使用 `docs/mysql-minimum-privileges.sql` 创建相互独立的
|
||||
迁移账号和运行账号,并将示例来源网段替换为实际应用容器网段。不要使用公网来源或 `%`:
|
||||
|
||||
```sql
|
||||
CREATE DATABASE osg_account
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_0900_ai_ci;
|
||||
```
|
||||
|
||||
应用启动时使用迁移账号执行 Flyway,随后 Hikari 连接池只使用运行账号。运行账号不能更新或删除
|
||||
不可变 ledger/usage 历史,也不能访问 Flyway 元数据或未来未审查的表。禁止授予全局权限、
|
||||
`FILE`、`PROCESS`、`SUPER`、`CREATE USER`、`GRANT OPTION`。不要复用 root 或其他应用用户。
|
||||
|
||||
若 MySQL 是容器,把它和应用接入同一个已存在的外部网络:
|
||||
|
||||
```bash
|
||||
docker network create account-backend
|
||||
docker network connect account-backend <existing-mysql-container>
|
||||
```
|
||||
|
||||
已存在网络时不要重复创建。`DATABASE_URL` 中使用该 MySQL 容器在网络内可解析的名称。若 MySQL
|
||||
位于私网主机,使用私有 DNS/IP,并在数据库防火墙中只允许应用主机或容器网段。
|
||||
|
||||
## 4. 配置环境与秘密
|
||||
|
||||
复制部署所需变量到项目根目录的未跟踪 `.env`,或使用 1Panel 的环境变量/秘密管理。不要把真实
|
||||
值写入 YAML、镜像层或 Git。
|
||||
|
||||
必须设置:
|
||||
|
||||
- `DATABASE_URL=jdbc:mysql://<existing-mysql>:3306/osg_account?useUnicode=true&characterEncoding=utf8&connectionTimeZone=UTC&forceConnectionTimeZoneToSession=true`
|
||||
- `DATABASE_USER`、`DATABASE_PASSWORD`
|
||||
- `DATABASE_MIGRATION_USER`、`DATABASE_MIGRATION_PASSWORD`:必须与运行账号及其密码不同
|
||||
- `JWT_SECRET`:至少 32 个随机字节
|
||||
- `FIELD_ENCRYPTION_KEY`:恰好 32 个随机字节的 Base64
|
||||
- `IDENTITY_HMAC_KEY`:至少 32 个随机字节的独立 Base64,不得复用字段加密密钥
|
||||
- `APPLE_TEAM_ID`、`APPLE_KEY_ID`、`APPLE_PRIVATE_KEY_PEM`
|
||||
- `VOLCENGINE_API_KEY`、`DEEPSEEK_API_KEY`
|
||||
- `APP_STORE_URL`:正式 App Store HTTPS 地址;生产 Compose 不接受缺失值
|
||||
|
||||
生成新秘密的示例:
|
||||
|
||||
```bash
|
||||
openssl rand -base64 48
|
||||
openssl rand -base64 32
|
||||
openssl rand -base64 32
|
||||
```
|
||||
|
||||
三个结果分别用于 JWT、字段加密和身份 HMAC,不能相互复用。`APPLE_PRIVATE_KEY_PEM` 可使用包含
|
||||
字面量 `\n` 的单行值;应用配置会在内存中还原换行。限制 `.env` 权限:
|
||||
|
||||
```bash
|
||||
chmod 600 .env
|
||||
```
|
||||
|
||||
生产 Compose 已固定 `APP_ENV=production`、Apple production 环境及两项完整性强制开关。
|
||||
Apple 配置使用所属开发者账号的 Team ID、Key ID、bundle ID 和 `.p8` 私钥;火山引擎使用 SAUC
|
||||
v3 WSS endpoint、资源 ID 和 API Key;DeepSeek 使用 HTTPS endpoint、已开通模型名和 API Key。
|
||||
三方凭据分别创建、分别轮换,不得复用。
|
||||
|
||||
## 5. 构建与启动
|
||||
|
||||
先检查变量插值。`docker compose config` 会展开秘密,不要把输出上传或粘贴到工单:
|
||||
|
||||
```bash
|
||||
./gradlew test
|
||||
docker compose config --quiet
|
||||
docker compose build --pull
|
||||
docker compose up -d
|
||||
docker compose ps
|
||||
curl --fail http://127.0.0.1:18080/health/ready
|
||||
```
|
||||
|
||||
容器以 UID/GID 10001 运行,根文件系统只读,仅 `/tmp` 是带大小限制的 tmpfs。应用端口只映射到
|
||||
宿主机回环地址,不能改成 `0.0.0.0`。
|
||||
|
||||
查看日志时不得记录或复制 token、Apple subject、音频、prompt、转录或模型响应:
|
||||
|
||||
```bash
|
||||
docker compose logs --since=10m account-server
|
||||
```
|
||||
|
||||
## 6. 配置 1Panel / OpenResty
|
||||
|
||||
在 1Panel 创建 `account.osglab.com`、`osglab.com` 两个 HTTPS 网站并签发证书。使用
|
||||
`deploy/openresty-account.conf`:
|
||||
|
||||
1. 将 `map`、`limit_req_zone`、`limit_req_status`、`upstream` 放入 OpenResty `http` 上下文。
|
||||
2. 将三个 `server` 块作为站点配置;按 1Panel 实际证书路径调整 `ssl_certificate`。
|
||||
3. 示例 upstream 指向宿主机 `127.0.0.1:18080`。若 OpenResty 自身在容器中,则将其加入
|
||||
`account-backend`,并改为 `account-server:8080`。
|
||||
4. 配置明确对 `/admin`、`/internal`、`/v1/admin` 返回 404;不要新增绕过该规则的泛域名代理。
|
||||
5. API 示例按 IP 限制 20 请求/秒,邀请页限制 5 请求/秒,可基于真实流量谨慎调整。
|
||||
6. 代理统一支持 HTTP/1.1 Upgrade/Connection,因此当前 HTTP API 与后续 WebSocket 入口都可用。
|
||||
|
||||
两个 AASA 地址由 Ktor 根据 `appleAppId` 模板输出,不需要复制静态文件。配置检查成功后再通过
|
||||
1Panel 重载 OpenResty:
|
||||
|
||||
```bash
|
||||
openresty -t
|
||||
```
|
||||
|
||||
## 7. 验证
|
||||
|
||||
检查健康状态、反向代理、隐藏路径、限流和 AASA:
|
||||
|
||||
```bash
|
||||
curl --fail https://account.osglab.com/health/ready
|
||||
curl -i https://account.osglab.com/internal/
|
||||
curl -i https://osglab.com/.well-known/apple-app-site-association
|
||||
curl -i https://osglab.com/i/AbCdEf0123456789_-AbCd
|
||||
```
|
||||
|
||||
预期结果:
|
||||
|
||||
- 健康检查返回 200。
|
||||
- 内部路径返回 404。
|
||||
- AASA 返回 200、`Content-Type: application/json`,且不发生重定向。
|
||||
- 合法 22 位邀请码返回双语 HTML;非法、未知或失效邀请码统一返回 404。
|
||||
- 邀请页包含 nonce CSP、`Cache-Control: no-store`、`X-Robots-Tag`,无第三方请求。
|
||||
|
||||
使用真机验证 Universal Link。安装带 Associated Domains entitlement 的 App 后,从信息或邮件点击
|
||||
`https://osglab.com/i/<code>` 应直接进入 App;未安装时应显示网页。AASA 更新受系统缓存影响,
|
||||
首次验证应预留传播时间。
|
||||
|
||||
## 8. 防火墙
|
||||
|
||||
- 公网入站仅允许 TCP 80/443。
|
||||
- SSH 仅允许管理员固定 IP 或 VPN;不公开 18080、8080、3306。
|
||||
- MySQL 3306 仅允许 Docker 私网或指定应用主机。
|
||||
- 出站允许 DNS、NTP、Apple HTTPS、DeepSeek HTTPS、Volcengine HTTPS/WSS。
|
||||
- 云安全组、主机防火墙和 1Panel 防火墙应采用相同边界,避免其中一层意外放行。
|
||||
|
||||
## 9. 更新与回滚
|
||||
|
||||
更新前备份 MySQL 并记录当前镜像标签。使用不可变标签构建:
|
||||
|
||||
```bash
|
||||
IMAGE_TAG=<release-tag> docker compose build
|
||||
IMAGE_TAG=<release-tag> docker compose up -d
|
||||
```
|
||||
|
||||
Flyway 迁移只向前执行。若新版本包含数据库迁移,应用镜像回滚不等于数据库回滚;应先按迁移影响
|
||||
制定恢复方案。无数据库变更时,可把 `IMAGE_TAG` 切回上一版本并重新执行 `docker compose up -d`。
|
||||
|
||||
备份、时间点恢复和每月恢复演练按 `docs/BACKUP.md` 执行。
|
||||
|
||||
## 10. 分阶段验收
|
||||
|
||||
1. **本地阶段**:`./gradlew test` 通过;合法、未知、格式错误和 lookup 异常路径符合预期;HTML
|
||||
不加载第三方资源。
|
||||
2. **容器阶段**:镜像以 UID/GID 10001 运行;根文件系统只读;`/health/live` 与
|
||||
`/health/ready` 通过;容器无法取得额外 Linux capability。
|
||||
3. **预发布阶段**:Flyway 使用迁移账号成功,应用使用运行账号成功;撤销运行账号 DDL 权限后服务
|
||||
仍正常;OpenResty HTTP、HTTPS 和 WSS 代理验证通过。
|
||||
4. **Apple 阶段**:两个 AASA 地址返回无重定向 JSON;真机从信息或邮件点击
|
||||
`https://osglab.com/i/<code>` 可打开 App;未安装 App 时显示双语落地页。
|
||||
5. **生产阶段**:公网仅开放 80/443,3306/8080/18080 不可达;邀请 URL 不出现在访问日志;
|
||||
供应商凭据可用;完成加密备份并记录一次隔离恢复结果。
|
||||
@@ -0,0 +1,59 @@
|
||||
-- Run as a MySQL administrator after replacing the host pattern and generated
|
||||
-- passwords. Keep both users restricted to the private application subnet.
|
||||
CREATE USER 'osg_account_runtime'@'10.20.%'
|
||||
IDENTIFIED BY 'REPLACE_WITH_RUNTIME_PASSWORD';
|
||||
CREATE USER 'osg_account_migrator'@'10.20.%'
|
||||
IDENTIFIED BY 'REPLACE_WITH_MIGRATION_PASSWORD';
|
||||
|
||||
-- Flyway owns schema evolution. This account is not used by the Hikari runtime pool.
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX, REFERENCES, TRIGGER
|
||||
ON osg_account.* TO 'osg_account_migrator'@'10.20.%';
|
||||
|
||||
-- Runtime reads are explicit so the account cannot read Flyway metadata or future
|
||||
-- tables until an administrator reviews and grants access.
|
||||
GRANT SELECT ON osg_account.accounts TO 'osg_account_runtime'@'10.20.%';
|
||||
GRANT SELECT ON osg_account.apple_credentials TO 'osg_account_runtime'@'10.20.%';
|
||||
GRANT SELECT ON osg_account.sessions TO 'osg_account_runtime'@'10.20.%';
|
||||
GRANT SELECT ON osg_account.apple_event_receipts TO 'osg_account_runtime'@'10.20.%';
|
||||
GRANT SELECT ON osg_account.credit_accounts TO 'osg_account_runtime'@'10.20.%';
|
||||
GRANT SELECT ON osg_account.credit_rate_versions TO 'osg_account_runtime'@'10.20.%';
|
||||
GRANT SELECT ON osg_account.credit_reservations TO 'osg_account_runtime'@'10.20.%';
|
||||
GRANT SELECT ON osg_account.referral_campaigns TO 'osg_account_runtime'@'10.20.%';
|
||||
GRANT SELECT ON osg_account.referral_campaign_budgets TO 'osg_account_runtime'@'10.20.%';
|
||||
GRANT SELECT ON osg_account.referral_codes TO 'osg_account_runtime'@'10.20.%';
|
||||
GRANT SELECT ON osg_account.referral_bindings TO 'osg_account_runtime'@'10.20.%';
|
||||
GRANT SELECT ON osg_account.credit_usage_records TO 'osg_account_runtime'@'10.20.%';
|
||||
GRANT SELECT ON osg_account.credit_ledger TO 'osg_account_runtime'@'10.20.%';
|
||||
GRANT SELECT ON osg_account.provider_requests TO 'osg_account_runtime'@'10.20.%';
|
||||
GRANT SELECT ON osg_account.usage_records TO 'osg_account_runtime'@'10.20.%';
|
||||
GRANT SELECT ON osg_account.gateway_grants TO 'osg_account_runtime'@'10.20.%';
|
||||
GRANT SELECT ON osg_account.devicecheck_trial_claims TO 'osg_account_runtime'@'10.20.%';
|
||||
GRANT SELECT ON osg_account.app_attest_challenges TO 'osg_account_runtime'@'10.20.%';
|
||||
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 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.%';
|
||||
GRANT INSERT, UPDATE ON osg_account.sessions TO 'osg_account_runtime'@'10.20.%';
|
||||
GRANT INSERT ON osg_account.apple_event_receipts TO 'osg_account_runtime'@'10.20.%';
|
||||
GRANT INSERT, UPDATE ON osg_account.credit_accounts TO 'osg_account_runtime'@'10.20.%';
|
||||
GRANT INSERT, UPDATE ON osg_account.credit_reservations TO 'osg_account_runtime'@'10.20.%';
|
||||
GRANT UPDATE ON osg_account.referral_campaign_budgets TO 'osg_account_runtime'@'10.20.%';
|
||||
GRANT INSERT ON osg_account.referral_codes TO 'osg_account_runtime'@'10.20.%';
|
||||
GRANT INSERT, UPDATE ON osg_account.referral_bindings TO 'osg_account_runtime'@'10.20.%';
|
||||
GRANT INSERT ON osg_account.credit_usage_records TO 'osg_account_runtime'@'10.20.%';
|
||||
GRANT INSERT ON osg_account.credit_ledger TO 'osg_account_runtime'@'10.20.%';
|
||||
GRANT INSERT, UPDATE ON osg_account.provider_requests TO 'osg_account_runtime'@'10.20.%';
|
||||
GRANT INSERT ON osg_account.usage_records TO 'osg_account_runtime'@'10.20.%';
|
||||
GRANT INSERT, UPDATE ON osg_account.gateway_grants TO 'osg_account_runtime'@'10.20.%';
|
||||
GRANT INSERT, UPDATE ON osg_account.devicecheck_trial_claims TO 'osg_account_runtime'@'10.20.%';
|
||||
GRANT INSERT, UPDATE ON osg_account.app_attest_challenges TO 'osg_account_runtime'@'10.20.%';
|
||||
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.%';
|
||||
|
||||
-- Deliberately absent: global privileges, GRANT OPTION, FILE, PROCESS, SUPER,
|
||||
-- CREATE USER, and UPDATE/DELETE on immutable ledger or usage-history tables.
|
||||
SHOW GRANTS FOR 'osg_account_runtime'@'10.20.%';
|
||||
SHOW GRANTS FOR 'osg_account_migrator'@'10.20.%';
|
||||
@@ -0,0 +1,678 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: OSG Account Server API
|
||||
version: 0.1.0
|
||||
description: |
|
||||
Account, credit, referral, integrity, and managed AI APIs for OSGKeyboard.
|
||||
Local features and user-owned API keys remain independent of this service.
|
||||
servers:
|
||||
- url: https://account.osglab.com
|
||||
security:
|
||||
- bearerAuth: []
|
||||
paths:
|
||||
/health:
|
||||
get:
|
||||
security: []
|
||||
summary: Compatibility health check
|
||||
responses:
|
||||
"200": { $ref: "#/components/responses/HealthUp" }
|
||||
/health/live:
|
||||
get:
|
||||
security: []
|
||||
summary: Process liveness
|
||||
responses:
|
||||
"200": { $ref: "#/components/responses/HealthUp" }
|
||||
/health/ready:
|
||||
get:
|
||||
security: []
|
||||
summary: Database and migration readiness
|
||||
responses:
|
||||
"200": { $ref: "#/components/responses/HealthUp" }
|
||||
"503":
|
||||
description: Database is unavailable or migrations failed.
|
||||
/v1/auth/apple:
|
||||
post:
|
||||
security: []
|
||||
summary: Exchange Sign in with Apple credentials for an OSG session
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/AppleSignInRequest" }
|
||||
responses:
|
||||
"200":
|
||||
description: Authenticated session
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/SessionTokenEnvelope" }
|
||||
default: { $ref: "#/components/responses/Error" }
|
||||
/v1/auth/refresh:
|
||||
post:
|
||||
security: []
|
||||
summary: Rotate a refresh token
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [refreshToken]
|
||||
properties:
|
||||
refreshToken: { type: string, minLength: 32 }
|
||||
responses:
|
||||
"200":
|
||||
description: Rotated session
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/SessionTokenEnvelope" }
|
||||
default: { $ref: "#/components/responses/Error" }
|
||||
/v1/auth/logout:
|
||||
post:
|
||||
summary: Revoke the current session family
|
||||
responses:
|
||||
"204": { description: Session revoked }
|
||||
default: { $ref: "#/components/responses/Error" }
|
||||
/v1/account:
|
||||
get:
|
||||
summary: Return the minimal account profile
|
||||
responses:
|
||||
"200":
|
||||
description: 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:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/DeleteAccountRequest" }
|
||||
responses:
|
||||
"204": { description: Account deleted }
|
||||
default: { $ref: "#/components/responses/Error" }
|
||||
/v1/apple/events:
|
||||
post:
|
||||
security: []
|
||||
summary: Receive a signed Apple server-to-server account event
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [payload]
|
||||
properties:
|
||||
payload: { type: string }
|
||||
responses:
|
||||
"204": { description: Event accepted }
|
||||
default: { $ref: "#/components/responses/Error" }
|
||||
/v1/credits/balance:
|
||||
get:
|
||||
summary: Return available integer credits
|
||||
responses:
|
||||
"200":
|
||||
description: Credit account
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/CreditAccount" }
|
||||
default: { $ref: "#/components/responses/Error" }
|
||||
/v1/credits/ledger:
|
||||
get:
|
||||
summary: Return immutable credit history
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
responses:
|
||||
"200":
|
||||
description: Ledger entries
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items: { $ref: "#/components/schemas/LedgerEntry" }
|
||||
default: { $ref: "#/components/responses/Error" }
|
||||
/v1/credits/rates:
|
||||
get:
|
||||
summary: Return effective managed-provider rate cards
|
||||
responses:
|
||||
"200":
|
||||
description: Effective rate cards
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items: { type: object, additionalProperties: true }
|
||||
default: { $ref: "#/components/responses/Error" }
|
||||
/v1/referrals:
|
||||
get:
|
||||
summary: List invitees without exposing their Apple identity
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
responses:
|
||||
"200":
|
||||
description: Referral bindings
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items: { type: object, additionalProperties: true }
|
||||
default: { $ref: "#/components/responses/Error" }
|
||||
/v1/referrals/me:
|
||||
get:
|
||||
summary: Return the current referral code and binding
|
||||
responses:
|
||||
"200":
|
||||
description: Referral profile
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, additionalProperties: true }
|
||||
default: { $ref: "#/components/responses/Error" }
|
||||
/v1/referrals/code:
|
||||
post:
|
||||
summary: Idempotently create the current campaign invite code
|
||||
responses:
|
||||
"200":
|
||||
description: Invite code
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/ReferralCode" }
|
||||
default: { $ref: "#/components/responses/Error" }
|
||||
/v1/referrals/redeem:
|
||||
post:
|
||||
summary: Bind the account to an inviter during the eligibility window
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/RedeemReferralRequest" }
|
||||
responses:
|
||||
"200": { description: Referral binding }
|
||||
default: { $ref: "#/components/responses/Error" }
|
||||
/v1/referrals/bind:
|
||||
post:
|
||||
deprecated: true
|
||||
summary: Compatibility alias for referral redemption
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/RedeemReferralRequest" }
|
||||
responses:
|
||||
"200": { description: Referral binding }
|
||||
default: { $ref: "#/components/responses/Error" }
|
||||
/v1/referrals/campaigns:
|
||||
get:
|
||||
summary: Return active referral campaigns
|
||||
responses:
|
||||
"200": { description: Active campaigns }
|
||||
default: { $ref: "#/components/responses/Error" }
|
||||
/v1/integrity/challenges:
|
||||
post:
|
||||
security: []
|
||||
summary: Issue a single-use App Attest challenge
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/AppAttestChallengeRequest" }
|
||||
responses:
|
||||
"201":
|
||||
description: Challenge issued
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/AppAttestChallengeResponse" }
|
||||
default: { $ref: "#/components/responses/Error" }
|
||||
/v1/integrity/attest:
|
||||
post:
|
||||
security: []
|
||||
summary: Register a validated App Attest key
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/AppAttestationRequest" }
|
||||
responses:
|
||||
"204": { description: Key registered }
|
||||
default: { $ref: "#/components/responses/Error" }
|
||||
/v1/integrity/assert:
|
||||
post:
|
||||
security: []
|
||||
summary: Validate a challenge-only App Attest assertion
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/AppAssertionRequest" }
|
||||
responses:
|
||||
"200": { description: Assertion counter advanced }
|
||||
default: { $ref: "#/components/responses/Error" }
|
||||
/v1/gateway/catalog:
|
||||
get:
|
||||
summary: Return configured managed-provider capabilities
|
||||
responses:
|
||||
"200": { description: Provider catalog }
|
||||
default: { $ref: "#/components/responses/Error" }
|
||||
/v1/gateway/grants:
|
||||
post:
|
||||
summary: Create an idempotent managed-provider grant
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/IdempotencyKey"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/CreateGatewayGrantRequest" }
|
||||
responses:
|
||||
"201":
|
||||
description: Gateway grant and rotating credentials
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/GatewayGrantTokens" }
|
||||
default: { $ref: "#/components/responses/GatewayError" }
|
||||
/v1/gateway/grants/refresh:
|
||||
post:
|
||||
security: []
|
||||
summary: Rotate a gateway refresh token
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/IdempotencyKey"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/RefreshGatewayGrantRequest" }
|
||||
responses:
|
||||
"200":
|
||||
description: Rotated gateway credentials
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/GatewayGrantTokens" }
|
||||
default: { $ref: "#/components/responses/GatewayError" }
|
||||
/v1/gateway/grants/{grantId}:
|
||||
delete:
|
||||
summary: Revoke a gateway grant
|
||||
parameters:
|
||||
- name: grantId
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: string, format: uuid }
|
||||
responses:
|
||||
"204": { description: Gateway grant revoked }
|
||||
default: { $ref: "#/components/responses/GatewayError" }
|
||||
/v1/gateway/llm/{capability}:
|
||||
post:
|
||||
summary: Run a metered polish, AI, or agent request
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/RequestId"
|
||||
- name: capability
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: string, enum: [polish, ai, agent] }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/TextGatewayRequest" }
|
||||
responses:
|
||||
"200":
|
||||
description: JSON response, or SSE when stream is true
|
||||
default: { $ref: "#/components/responses/GatewayError" }
|
||||
/v1/gateway/asr:
|
||||
post:
|
||||
summary: Run buffered metered ASR
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/RequestId"
|
||||
- { name: X-Audio-Duration-Ms, in: header, required: true, schema: { type: integer, minimum: 1, maximum: 600000 } }
|
||||
- { name: X-Audio-Format, in: header, schema: { type: string, enum: [pcm, wav, ogg, mp3], default: pcm } }
|
||||
- { name: X-Audio-Codec, in: header, schema: { type: string, enum: [raw, opus], default: raw } }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/octet-stream:
|
||||
schema: { type: string, contentEncoding: binary }
|
||||
responses:
|
||||
"200": { description: Newline-delimited Volcengine ASR result frames }
|
||||
default: { $ref: "#/components/responses/GatewayError" }
|
||||
/v1/gateway/asr/sessions:
|
||||
post:
|
||||
summary: Reserve credits and create a one-shot streaming ASR session
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/RequestId"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/CreateAsrSessionRequest" }
|
||||
responses:
|
||||
"201":
|
||||
description: Session created
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/CreateAsrSessionResponse" }
|
||||
default: { $ref: "#/components/responses/GatewayError" }
|
||||
/v1/gateway/asr/sessions/{sessionId}/stream:
|
||||
get:
|
||||
summary: Upgrade to WebSocket and stream binary audio frames
|
||||
description: |
|
||||
Send authenticated binary audio frames, then the exact text frame
|
||||
`{"type":"end"}`. The server forwards binary provider-result frames and
|
||||
closes the one-shot session after settlement.
|
||||
parameters:
|
||||
- name: sessionId
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: string, format: uuid }
|
||||
responses:
|
||||
"101": { description: WebSocket upgrade }
|
||||
default: { $ref: "#/components/responses/GatewayError" }
|
||||
/.well-known/apple-app-site-association:
|
||||
get:
|
||||
servers:
|
||||
- url: https://osglab.com
|
||||
security: []
|
||||
summary: Return the Apple Universal Links association document
|
||||
responses:
|
||||
"200":
|
||||
description: AASA JSON without a redirect
|
||||
headers:
|
||||
Cache-Control:
|
||||
schema: { type: string, const: "no-store, max-age=0" }
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/AppleAppSiteAssociation" }
|
||||
/apple-app-site-association:
|
||||
get:
|
||||
servers:
|
||||
- url: https://osglab.com
|
||||
security: []
|
||||
summary: Return the root compatibility AASA document
|
||||
responses:
|
||||
"200":
|
||||
description: AASA JSON without a redirect
|
||||
headers:
|
||||
Cache-Control:
|
||||
schema: { type: string, const: "no-store, max-age=0" }
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/AppleAppSiteAssociation" }
|
||||
/i/{code}:
|
||||
get:
|
||||
servers:
|
||||
- url: https://osglab.com
|
||||
security: []
|
||||
summary: Privacy-preserving invitation landing page
|
||||
parameters:
|
||||
- name: code
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: string, pattern: "^[A-Za-z0-9_-]{22}$" }
|
||||
responses:
|
||||
"200": { description: Bilingual HTML landing page }
|
||||
"404": { description: Invalid, unknown, or expired invitation }
|
||||
"503": { description: Invitation lookup is temporarily unavailable }
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
parameters:
|
||||
Limit:
|
||||
name: limit
|
||||
in: query
|
||||
schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
|
||||
RequestId:
|
||||
name: X-Request-ID
|
||||
in: header
|
||||
required: true
|
||||
schema: { type: string, pattern: "^[A-Za-z0-9_-]{8,64}$" }
|
||||
IdempotencyKey:
|
||||
name: Idempotency-Key
|
||||
in: header
|
||||
required: true
|
||||
schema: { type: string, minLength: 1, maxLength: 255 }
|
||||
responses:
|
||||
HealthUp:
|
||||
description: Service is healthy
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [status]
|
||||
properties:
|
||||
status: { type: string, const: UP }
|
||||
Error:
|
||||
description: API error
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/ApiErrorEnvelope" }
|
||||
GatewayError:
|
||||
description: Managed-provider error
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/GatewayError" }
|
||||
schemas:
|
||||
AppleSignInRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [identityToken, authorizationCode, nonce]
|
||||
properties:
|
||||
identityToken: { type: string }
|
||||
authorizationCode: { type: string }
|
||||
nonce: { type: string }
|
||||
deviceCheckToken:
|
||||
type: ["string", "null"]
|
||||
description: Ephemeral DeviceCheck token; never persisted in plaintext.
|
||||
appAttest:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/AppAttestAssertion"
|
||||
- type: "null"
|
||||
AppAttestAssertion:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [keyId, challengeId, challenge, assertion]
|
||||
properties:
|
||||
keyId: { type: string, maxLength: 128 }
|
||||
challengeId: { type: string, format: uuid }
|
||||
challenge: { type: string, description: Base64URL challenge returned by the server }
|
||||
assertion: { type: string, contentEncoding: base64 }
|
||||
SessionTokenResponse:
|
||||
type: object
|
||||
required: [accountId, tokenType, accessToken, accessTokenExpiresAtEpochSeconds, refreshToken, refreshTokenExpiresAtEpochSeconds]
|
||||
properties:
|
||||
accountId: { type: string, format: uuid }
|
||||
tokenType: { type: string, const: Bearer }
|
||||
accessToken: { type: string }
|
||||
accessTokenExpiresAtEpochSeconds: { type: integer, format: int64 }
|
||||
refreshToken: { type: string }
|
||||
refreshTokenExpiresAtEpochSeconds: { type: integer, format: int64 }
|
||||
SessionTokenEnvelope:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [data]
|
||||
properties:
|
||||
data: { $ref: "#/components/schemas/SessionTokenResponse" }
|
||||
Account:
|
||||
type: object
|
||||
required: [id, createdAtEpochSeconds]
|
||||
properties:
|
||||
id: { type: string, format: uuid }
|
||||
createdAtEpochSeconds: { type: integer, format: int64 }
|
||||
AccountEnvelope:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [data]
|
||||
properties:
|
||||
data: { $ref: "#/components/schemas/Account" }
|
||||
DeleteAccountRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [identityToken, authorizationCode, nonce]
|
||||
properties:
|
||||
identityToken: { type: string, maxLength: 16384 }
|
||||
authorizationCode: { type: string, maxLength: 4096 }
|
||||
nonce: { type: string, maxLength: 512 }
|
||||
CreditAccount:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
required: [userId, balance]
|
||||
properties:
|
||||
userId: { type: string, format: uuid }
|
||||
balance: { type: integer, format: int64, minimum: 0 }
|
||||
LedgerEntry:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
required: [id, amountDelta, balanceAfter]
|
||||
properties:
|
||||
id: { type: string, format: uuid }
|
||||
amountDelta: { type: integer, format: int64 }
|
||||
balanceAfter: { type: integer, format: int64, minimum: 0 }
|
||||
AppleAppSiteAssociation:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [applinks]
|
||||
properties:
|
||||
applinks:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [details]
|
||||
properties:
|
||||
details:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required: [appIDs, components]
|
||||
properties:
|
||||
appIDs:
|
||||
type: array
|
||||
items: { type: string }
|
||||
components:
|
||||
type: array
|
||||
items: { type: object, additionalProperties: true }
|
||||
ReferralCode:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
properties:
|
||||
code: { type: string, pattern: "^[A-Za-z0-9_-]{22}$" }
|
||||
RedeemReferralRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [code]
|
||||
properties:
|
||||
code: { type: string, pattern: "^[A-Za-z0-9_-]{22}$" }
|
||||
AppAttestChallengeRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [purpose, keyId]
|
||||
properties:
|
||||
purpose: { type: string, enum: [attestation, assertion] }
|
||||
keyId: { type: string, maxLength: 128 }
|
||||
AppAttestChallengeResponse:
|
||||
type: object
|
||||
required: [challengeId, challenge, expiresAtEpochSeconds]
|
||||
properties:
|
||||
challengeId: { type: string, format: uuid }
|
||||
challenge: { type: string }
|
||||
expiresAtEpochSeconds: { type: integer, format: int64 }
|
||||
AppAttestationRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [challengeId, challenge, keyId, attestationObject]
|
||||
properties:
|
||||
challengeId: { type: string, format: uuid }
|
||||
challenge: { type: string }
|
||||
keyId: { type: string }
|
||||
attestationObject: { type: string, contentEncoding: base64 }
|
||||
AppAssertionRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [challengeId, challenge, keyId, assertion, clientDataHash]
|
||||
properties:
|
||||
challengeId: { type: string, format: uuid }
|
||||
challenge: { type: string }
|
||||
keyId: { type: string }
|
||||
assertion: { type: string, contentEncoding: base64 }
|
||||
clientDataHash: { type: string, description: Base64URL SHA-256 of the echoed challenge }
|
||||
TextGatewayRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [input]
|
||||
properties:
|
||||
input: { type: string, minLength: 1, maxLength: 32000 }
|
||||
context: { type: ["string", "null"], maxLength: 32000 }
|
||||
maxOutputTokens: { type: integer, minimum: 1, maximum: 4096, default: 512 }
|
||||
temperature: { type: number, minimum: 0, maximum: 1, default: 0.2 }
|
||||
stream: { type: boolean, default: false }
|
||||
CreateGatewayGrantRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [scopes]
|
||||
properties:
|
||||
scopes:
|
||||
type: array
|
||||
uniqueItems: true
|
||||
minItems: 1
|
||||
items: { type: string, enum: [polish, ai, agent, asr] }
|
||||
lifetimeSeconds: { type: ["integer", "null"], format: int64, minimum: 1 }
|
||||
RefreshGatewayGrantRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [refreshToken]
|
||||
properties:
|
||||
refreshToken: { type: string, minLength: 32 }
|
||||
GatewayGrantTokens:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [grantId, scopes, accessToken, accessExpiresAt, refreshToken, refreshExpiresAt]
|
||||
properties:
|
||||
grantId: { type: string, format: uuid }
|
||||
scopes:
|
||||
type: array
|
||||
uniqueItems: true
|
||||
items: { type: string, enum: [polish, ai, agent, asr] }
|
||||
accessToken: { type: string }
|
||||
accessExpiresAt: { type: string, format: date-time }
|
||||
refreshToken: { type: string }
|
||||
refreshExpiresAt: { type: string, format: date-time }
|
||||
CreateAsrSessionRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [estimatedDurationMillis]
|
||||
properties:
|
||||
format: { type: string, enum: [pcm, wav, ogg, mp3], default: pcm }
|
||||
codec: { type: string, enum: [raw, opus], default: raw }
|
||||
sampleRate: { type: integer, const: 16000 }
|
||||
bits: { type: integer, const: 16 }
|
||||
channels: { type: integer, minimum: 1, maximum: 2, default: 1 }
|
||||
language: { type: ["string", "null"], maxLength: 32 }
|
||||
estimatedDurationMillis: { type: integer, minimum: 1, maximum: 600000 }
|
||||
CreateAsrSessionResponse:
|
||||
type: object
|
||||
required: [sessionId, websocketPath, maxFrameBytes, idleTimeoutMillis]
|
||||
properties:
|
||||
sessionId: { type: string, format: uuid }
|
||||
websocketPath: { type: string }
|
||||
maxFrameBytes: { type: integer }
|
||||
idleTimeoutMillis: { type: integer, format: int64 }
|
||||
ApiError:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [code, message]
|
||||
properties:
|
||||
code: { type: string }
|
||||
message: { type: string }
|
||||
ApiErrorEnvelope:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [error]
|
||||
properties:
|
||||
error: { $ref: "#/components/schemas/ApiError" }
|
||||
GatewayError:
|
||||
type: object
|
||||
required: [code, message, requestId]
|
||||
properties:
|
||||
code: { type: string }
|
||||
message: { type: string }
|
||||
requestId: { type: string }
|
||||
Reference in New Issue
Block a user