三条线合并入库 —— 均已完成并上线(源码与生产一致,此前只部署未入仓)。 ⚠️ 其中域名迁移线为**另一会话**产出,本会话只做入库、**未复验其正确性**(它自报零回归)。 【档案 134 · 注册页人机验证 + 邮箱验证码】 - DB 迁移 v10:users.email(唯一索引 LOWER(email))+ email_codes 事件表(2 索引) - 新增模块 src/web/{register-guard,mail,turnstile,email-code}.ts - routes/auth.ts:新增 GET /api/auth/register/config、POST /api/auth/register/email-code; 注册接口加人机验证与验证码校验;config.ts 新增 12 项配置(默认空 ⇒ 不配 = 老行为) - 邮件走**可插拔驱动**(brevo/http/log),发件人 [email protected](Brevo 域名已认证 + DKIM + SPF) - 防爆破:三层配额(邮箱 6/h、8/天;IP 20/h;全局 200/h)+ 递增冷却阶梯 (60→60→180→300→900→1800s)+ 试错 5 次作废 + 码只存哈希 + 单次使用 + 与用户名绑定 - Turnstile 服务端校 **success + action + hostname 三项**:sitekey 是公开的, 只校 success 时"拿我们的 sitekey 在自己站点替真人取合法 token 再打我们接口"这条路是通的 - 新增 test/register-guard.test.mjs(19 用例) 【档案 137 · 品牌标识改造 — 去 DeepSeek 图形】 - login/register/admin 页头:删 DeepSeek 鲸鱼图标 + 「DeepSeek」文字图形 → 平台标识(中文「能力枢纽」/英语及其他语言「CapabilityNet」,走 i18n 词条 brand.name) - portal 顶栏换图标(页面名「管理门户」保留) - 新建 web/favicon.svg(平台自有 hub 图标,避开 DeepSeek 蓝)+ 四页 favicon 指向它 - 新增 test/i18n-brand.test.mjs(node:vm 跑真实 i18n.js,六条语言路径断言渲染结果) - scripts/verify-static.mjs 新增 SVG 段:XML 注释不得含 ASCII 双连字符(否则整份 SVG 解析失败、图标静默不显示 —— 实际踩到过) - 🔴 会话页面(实例内官方 dsh 界面)的标识**按用户要求未动**(也受 R2 约束) 【档案 135/136 · 域名迁移线(另一会话产出)】 - 域名收敛为 ai1net.com;旧域 alotbuy.com 降级为 301 过渡装置 - src/net/relay/{addr-override,directory,rendezvous,switcher}.ts 种子与候选链更新; src/web/server.ts、src/worker/relay-tunnel.ts、scripts/verify-cluster-domain.mjs - 档案 136 = 控制面按两台中继取并集(**已立项、未落地**) 验证(本会话两条线):新增单测 21 条全通过|全量 221 pass / 0 fail / 1 skipped| verify-static 全合格|其余 10 个 verify 脚本全 OK|线上实测:Turnstile 假 token 403、 发码 delivered、四页 deepseek 命中 0、favicon 200。
440 lines
14 KiB
TypeScript
440 lines
14 KiB
TypeScript
/**
|
||
* SQLite backend for {@link DbAdapter}. Wraps the synchronous better-sqlite3
|
||
* repo in async methods and maps constraint errors onto the shared hierarchy.
|
||
* Used when `deployMode=local` (no `DSHS_DB_URL`).
|
||
*
|
||
* NOTE: better-sqlite3 is synchronous; the `async` here only matches the
|
||
* interface — the underlying calls still block the event loop. Fine for the
|
||
* small single-machine local mode this backend targets (see docs/k8s.md §5.1).
|
||
* @module dshs/db/sqlite
|
||
*/
|
||
|
||
import type { DbAdapter } from './adapter.js'
|
||
import { openDatabase, type Database } from './connection.js'
|
||
import { mapSqliteError } from './errors.js'
|
||
import {
|
||
audit as auditSync,
|
||
bumpEmailCodeAttempts as bumpEmailCodeAttemptsSync,
|
||
consumeEmailCode as consumeEmailCodeSync,
|
||
countAdmins as countAdminsSync,
|
||
createSession as createSessionSync,
|
||
createUser as createUserSync,
|
||
deleteBusinessPlugin as deleteBusinessPluginSync,
|
||
deleteCredentialKey as deleteCredentialKeySync,
|
||
deleteInstance as deleteInstanceSync,
|
||
deleteSession as deleteSessionSync,
|
||
deleteUser as deleteUserSync,
|
||
deleteUserInstances as deleteUserInstancesSync,
|
||
deleteUserSessions as deleteUserSessionsSync,
|
||
emailCodeCounts as emailCodeCountsSync,
|
||
findBusinessPlugin as findBusinessPluginSync,
|
||
findDomainById as findDomainByIdSync,
|
||
findDomainByUser as findDomainByUserSync,
|
||
findInstance as findInstanceSync,
|
||
findSession as findSessionSync,
|
||
findSessionWithUser as findSessionWithUserSync,
|
||
hasActiveSession as hasActiveSessionSync,
|
||
findUserByEmail as findUserByEmailSync,
|
||
findUserById as findUserByIdSync,
|
||
findUserBySlug as findUserBySlugSync,
|
||
findUserByUsername as findUserByUsernameSync,
|
||
findUserInstance as findUserInstanceSync,
|
||
findWorkspaceByPath as findWorkspaceByPathSync,
|
||
getEnabledCredentialKeyRef as getEnabledCredentialKeyRefSync,
|
||
getEnabledPluginIds as getEnabledPluginIdsSync,
|
||
getOrCreateWorkspace as getOrCreateWorkspaceSync,
|
||
getSharedModelEnabled as getSharedModelEnabledSync,
|
||
latestSentEmailCode as latestSentEmailCodeSync,
|
||
listBusinessPlugins as listBusinessPluginsSync,
|
||
listCredentialKeys as listCredentialKeysSync,
|
||
listCredentialLandingRows as listCredentialLandingRowsSync,
|
||
listDomains as listDomainsSync,
|
||
listEnabledCredentialKeys as listEnabledCredentialKeysSync,
|
||
listInstancesByRole as listInstancesByRoleSync,
|
||
listPublicUsers as listPublicUsersSync,
|
||
listUsersWithoutUid as listUsersWithoutUidSync,
|
||
purgeEmailCodes as purgeEmailCodesSync,
|
||
recordEmailCode as recordEmailCodeSync,
|
||
selectCredentialKey as selectCredentialKeySync,
|
||
setCredentialKey as setCredentialKeySync,
|
||
setDomainVerified as setDomainVerifiedSync,
|
||
setFolderPlugins as setFolderPluginsSync,
|
||
setInstanceStatus as setInstanceStatusSync,
|
||
setSharedModelEnabled as setSharedModelEnabledSync,
|
||
setUserRole as setUserRoleSync,
|
||
setUserUid as setUserUidSync,
|
||
toggleCredentialKey as toggleCredentialKeySync,
|
||
// 集群化(v7;T08 S2)
|
||
claimInstance as claimInstanceSync,
|
||
findDshHost as findDshHostSync,
|
||
listDshHosts as listDshHostsSync,
|
||
listExpiredInstanceLeases as listExpiredInstanceLeasesSync,
|
||
listInstancesByHost as listInstancesByHostSync,
|
||
pinInstanceHost as pinInstanceHostSync,
|
||
releaseInstanceLease as releaseInstanceLeaseSync,
|
||
renewInstanceLease as renewInstanceLeaseSync,
|
||
setDshHostStatus as setDshHostStatusSync,
|
||
upsertDshHost as upsertDshHostSync,
|
||
upsertBusinessPlugin as upsertBusinessPluginSync,
|
||
upsertDomain as upsertDomainSync,
|
||
upsertInstance as upsertInstanceSync,
|
||
} from './repo.js'
|
||
import type {
|
||
BusinessPlugin,
|
||
ClaimResult,
|
||
CredentialKey,
|
||
CredentialKeyMeta,
|
||
CredentialLandingRow,
|
||
CreateSessionInput,
|
||
CreateUserInput,
|
||
Domain,
|
||
DshHost,
|
||
DshHostStatus,
|
||
DshInstance,
|
||
DshInstanceRole,
|
||
DshInstanceStatus,
|
||
EmailCodeCounts,
|
||
EmailCodeRow,
|
||
PublicUser,
|
||
RecordEmailCodeInput,
|
||
SessionRow,
|
||
SessionUser,
|
||
UpsertBusinessPluginInput,
|
||
UpsertDshHostInput,
|
||
UpsertDshInstanceInput,
|
||
User,
|
||
UserRole,
|
||
Workspace,
|
||
} from './types.js'
|
||
|
||
export class SqliteAdapter implements DbAdapter {
|
||
private readonly db: Database
|
||
|
||
constructor(path: string, private readonly baseUid: number) {
|
||
this.db = openDatabase(path)
|
||
}
|
||
|
||
async createUser(input: CreateUserInput): Promise<User> {
|
||
try {
|
||
return createUserSync(this.db, input, this.baseUid)
|
||
} catch (e) {
|
||
mapSqliteError(e)
|
||
}
|
||
}
|
||
|
||
async findUserByUsername(username: string): Promise<User | undefined> {
|
||
return findUserByUsernameSync(this.db, username)
|
||
}
|
||
|
||
async findUserBySlug(slug: string): Promise<User | undefined> {
|
||
return findUserBySlugSync(this.db, slug)
|
||
}
|
||
|
||
async findUserById(id: string): Promise<User | undefined> {
|
||
return findUserByIdSync(this.db, id)
|
||
}
|
||
|
||
async listPublicUsers(): Promise<PublicUser[]> {
|
||
return listPublicUsersSync(this.db)
|
||
}
|
||
|
||
async countAdmins(): Promise<number> {
|
||
return countAdminsSync(this.db)
|
||
}
|
||
|
||
async setUserRole(id: string, role: UserRole, approvedBy?: string): Promise<boolean> {
|
||
return setUserRoleSync(this.db, id, role, approvedBy)
|
||
}
|
||
|
||
async setUserUid(userId: string, uid: number): Promise<void> {
|
||
setUserUidSync(this.db, userId, uid)
|
||
}
|
||
|
||
async listUsersWithoutUid(): Promise<string[]> {
|
||
return listUsersWithoutUidSync(this.db)
|
||
}
|
||
|
||
async createSession(input: CreateSessionInput): Promise<void> {
|
||
try {
|
||
createSessionSync(this.db, input)
|
||
} catch (e) {
|
||
mapSqliteError(e)
|
||
}
|
||
}
|
||
|
||
async findSession(tokenHash: string): Promise<SessionRow | undefined> {
|
||
return findSessionSync(this.db, tokenHash)
|
||
}
|
||
|
||
async deleteSession(tokenHash: string): Promise<void> {
|
||
deleteSessionSync(this.db, tokenHash)
|
||
}
|
||
|
||
async deleteUserSessions(userId: string): Promise<void> {
|
||
deleteUserSessionsSync(this.db, userId)
|
||
}
|
||
|
||
async findSessionWithUser(tokenHash: string): Promise<SessionUser | undefined> {
|
||
return findSessionWithUserSync(this.db, tokenHash)
|
||
}
|
||
|
||
async hasActiveSession(userId: string): Promise<boolean> {
|
||
return hasActiveSessionSync(this.db, userId)
|
||
}
|
||
|
||
async audit(actor: string | null, action: string, detail?: string | null): Promise<void> {
|
||
auditSync(this.db, actor, action, detail)
|
||
}
|
||
|
||
// ── v10 邮箱验证码(注册页)────────────────────────────────────────────────
|
||
async findUserByEmail(email: string): Promise<User | undefined> {
|
||
return findUserByEmailSync(this.db, email)
|
||
}
|
||
|
||
async recordEmailCode(input: RecordEmailCodeInput): Promise<void> {
|
||
recordEmailCodeSync(this.db, input)
|
||
}
|
||
|
||
async emailCodeCounts(email: string, ip: string | null, since: number): Promise<EmailCodeCounts> {
|
||
return emailCodeCountsSync(this.db, email, ip, since)
|
||
}
|
||
|
||
async latestSentEmailCode(email: string, purpose: string): Promise<EmailCodeRow | undefined> {
|
||
return latestSentEmailCodeSync(this.db, email, purpose)
|
||
}
|
||
|
||
async bumpEmailCodeAttempts(id: string, consume: boolean, now: number): Promise<boolean> {
|
||
return bumpEmailCodeAttemptsSync(this.db, id, consume, now)
|
||
}
|
||
|
||
async consumeEmailCode(id: string, now: number): Promise<boolean> {
|
||
return consumeEmailCodeSync(this.db, id, now)
|
||
}
|
||
|
||
async purgeEmailCodes(before: number): Promise<number> {
|
||
return purgeEmailCodesSync(this.db, before)
|
||
}
|
||
|
||
async findWorkspaceByPath(userId: string, relPath: string): Promise<Workspace | undefined> {
|
||
return findWorkspaceByPathSync(this.db, userId, relPath)
|
||
}
|
||
|
||
async getOrCreateWorkspace(userId: string, relPath: string): Promise<Workspace> {
|
||
try {
|
||
return getOrCreateWorkspaceSync(this.db, userId, relPath)
|
||
} catch (e) {
|
||
mapSqliteError(e)
|
||
}
|
||
}
|
||
|
||
async setFolderPlugins(
|
||
workspaceId: string,
|
||
selections: ReadonlyArray<{ id: string; enabled: boolean }>,
|
||
): Promise<void> {
|
||
try {
|
||
setFolderPluginsSync(this.db, workspaceId, selections)
|
||
} catch (e) {
|
||
mapSqliteError(e)
|
||
}
|
||
}
|
||
|
||
async getEnabledPluginIds(workspaceId: string): Promise<string[]> {
|
||
return getEnabledPluginIdsSync(this.db, workspaceId)
|
||
}
|
||
|
||
async listBusinessPlugins(): Promise<BusinessPlugin[]> {
|
||
return listBusinessPluginsSync(this.db)
|
||
}
|
||
|
||
async findBusinessPlugin(id: string): Promise<BusinessPlugin | undefined> {
|
||
return findBusinessPluginSync(this.db, id)
|
||
}
|
||
|
||
async upsertBusinessPlugin(input: UpsertBusinessPluginInput): Promise<BusinessPlugin> {
|
||
try {
|
||
return upsertBusinessPluginSync(this.db, input)
|
||
} catch (e) {
|
||
mapSqliteError(e)
|
||
}
|
||
}
|
||
|
||
async deleteBusinessPlugin(id: string): Promise<boolean> {
|
||
return deleteBusinessPluginSync(this.db, id)
|
||
}
|
||
|
||
async findDomainByUser(userId: string): Promise<Domain | undefined> {
|
||
return findDomainByUserSync(this.db, userId)
|
||
}
|
||
|
||
async findDomainById(id: string): Promise<Domain | undefined> {
|
||
return findDomainByIdSync(this.db, id)
|
||
}
|
||
|
||
async listDomains(): Promise<Domain[]> {
|
||
return listDomainsSync(this.db)
|
||
}
|
||
|
||
async upsertDomain(userId: string, domain: string, nginxConfig: string): Promise<Domain> {
|
||
try {
|
||
return upsertDomainSync(this.db, userId, domain, nginxConfig)
|
||
} catch (e) {
|
||
mapSqliteError(e)
|
||
}
|
||
}
|
||
|
||
async setDomainVerified(id: string, verified: boolean): Promise<boolean> {
|
||
return setDomainVerifiedSync(this.db, id, verified)
|
||
}
|
||
|
||
async listCredentialKeys(userId: string): Promise<CredentialKey[]> {
|
||
return listCredentialKeysSync(this.db, userId)
|
||
}
|
||
|
||
async listEnabledCredentialKeys(userId: string): Promise<CredentialKey[]> {
|
||
return listEnabledCredentialKeysSync(this.db, userId)
|
||
}
|
||
|
||
async listCredentialLandingRows(userId: string): Promise<CredentialLandingRow[]> {
|
||
return listCredentialLandingRowsSync(this.db, userId)
|
||
}
|
||
|
||
async getEnabledCredentialKeyRef(userId: string): Promise<string | null> {
|
||
return getEnabledCredentialKeyRefSync(this.db, userId)
|
||
}
|
||
|
||
async setCredentialKey(
|
||
userId: string,
|
||
name: string,
|
||
encryptedRef: string,
|
||
meta?: CredentialKeyMeta,
|
||
): Promise<CredentialKey> {
|
||
try {
|
||
return setCredentialKeySync(this.db, userId, name, encryptedRef, meta)
|
||
} catch (e) {
|
||
mapSqliteError(e)
|
||
}
|
||
}
|
||
|
||
async selectCredentialKey(userId: string, id: string): Promise<boolean> {
|
||
return selectCredentialKeySync(this.db, userId, id)
|
||
}
|
||
|
||
async toggleCredentialKey(userId: string, id: string, enabled: boolean): Promise<boolean> {
|
||
return toggleCredentialKeySync(this.db, userId, id, enabled)
|
||
}
|
||
|
||
async getSharedModelEnabled(userId: string): Promise<boolean> {
|
||
return getSharedModelEnabledSync(this.db, userId)
|
||
}
|
||
|
||
async setSharedModelEnabled(userId: string, enabled: boolean): Promise<boolean> {
|
||
return setSharedModelEnabledSync(this.db, userId, enabled)
|
||
}
|
||
|
||
async deleteCredentialKey(userId: string, id: string): Promise<boolean> {
|
||
return deleteCredentialKeySync(this.db, userId, id)
|
||
}
|
||
|
||
async deleteUser(userId: string): Promise<boolean> {
|
||
return deleteUserSync(this.db, userId)
|
||
}
|
||
|
||
async upsertInstance(input: UpsertDshInstanceInput): Promise<void> {
|
||
try {
|
||
upsertInstanceSync(this.db, input)
|
||
} catch (e) {
|
||
mapSqliteError(e)
|
||
}
|
||
}
|
||
|
||
async findInstance(id: string): Promise<DshInstance | undefined> {
|
||
return findInstanceSync(this.db, id)
|
||
}
|
||
|
||
async findUserInstance(userId: string, role: DshInstanceRole): Promise<DshInstance | undefined> {
|
||
return findUserInstanceSync(this.db, userId, role)
|
||
}
|
||
|
||
async listInstancesByRole(role: DshInstanceRole): Promise<DshInstance[]> {
|
||
return listInstancesByRoleSync(this.db, role)
|
||
}
|
||
|
||
async setInstanceStatus(
|
||
id: string,
|
||
status: DshInstanceStatus,
|
||
outcome?: { exitCode?: number; lastError?: string },
|
||
): Promise<boolean> {
|
||
return setInstanceStatusSync(this.db, id, status, outcome)
|
||
}
|
||
|
||
async deleteInstance(id: string): Promise<boolean> {
|
||
return deleteInstanceSync(this.db, id)
|
||
}
|
||
|
||
async deleteUserInstances(userId: string): Promise<void> {
|
||
deleteUserInstancesSync(this.db, userId)
|
||
}
|
||
|
||
// ── 集群化:worker 注册表 + 归属/租约(v7;T08 S2)────────────────────────
|
||
// local 模式不会走到这些方法(`LocalSpawner` 不写库),它们只是让
|
||
// **SQLite 侧与 PG 侧行为一致** —— 测试与单机试跑都需要。
|
||
|
||
async upsertDshHost(input: UpsertDshHostInput): Promise<DshHost> {
|
||
try {
|
||
return upsertDshHostSync(this.db, input)
|
||
} catch (e) {
|
||
mapSqliteError(e)
|
||
}
|
||
}
|
||
|
||
async findDshHost(id: string): Promise<DshHost | undefined> {
|
||
return findDshHostSync(this.db, id)
|
||
}
|
||
|
||
async listDshHosts(): Promise<DshHost[]> {
|
||
return listDshHostsSync(this.db)
|
||
}
|
||
|
||
async setDshHostStatus(
|
||
id: string,
|
||
status: DshHostStatus,
|
||
usedMb?: number,
|
||
heartbeatAt?: number,
|
||
): Promise<boolean> {
|
||
return setDshHostStatusSync(this.db, id, status, usedMb, heartbeatAt)
|
||
}
|
||
|
||
async claimInstance(
|
||
userId: string,
|
||
hostId: string,
|
||
ttlMs: number,
|
||
meta?: { folder?: string; patch?: string },
|
||
): Promise<ClaimResult> {
|
||
return claimInstanceSync(this.db, userId, hostId, ttlMs, meta)
|
||
}
|
||
|
||
async renewInstanceLease(userId: string, hostId: string, epoch: number, ttlMs: number): Promise<boolean> {
|
||
return renewInstanceLeaseSync(this.db, userId, hostId, epoch, ttlMs)
|
||
}
|
||
|
||
async releaseInstanceLease(userId: string, hostId: string, epoch: number): Promise<boolean> {
|
||
return releaseInstanceLeaseSync(this.db, userId, hostId, epoch)
|
||
}
|
||
|
||
async pinInstanceHost(userId: string, hostId: string): Promise<void> {
|
||
pinInstanceHostSync(this.db, userId, hostId)
|
||
}
|
||
|
||
async listExpiredInstanceLeases(now: number): Promise<DshInstance[]> {
|
||
return listExpiredInstanceLeasesSync(this.db, now)
|
||
}
|
||
|
||
async listInstancesByHost(hostId: string): Promise<DshInstance[]> {
|
||
return listInstancesByHostSync(this.db, hostId)
|
||
}
|
||
|
||
async close(): Promise<void> {
|
||
this.db.close()
|
||
}
|
||
}
|