ci / vet-and-test (pull_request) Successful in 51s
Why: The login recovery contract leftovers and KTD12/KTD14 require: concurrent same-name account creation must not overwrite the winner, first-admin bootstrap must serialize across processes, the redeem endpoint backoff must be shared across instances, revoked API keys must close established SSE streams, and CLI credentials need an env-DSN path plus a non-interactive 0600 file delivery mode. Evidence: go test ./... green; new tests cover 16-way concurrent create (1 win/15 conflict, winner credentials intact), 8-way concurrent bootstrap (single admin, refuse takeover after), redeem guard shared across two store handles on one DB, SSE stream closed after key revocation within the recheck window, and HTTP 429 after the failure cap (SELF worktree run; CI is the GATE, PG via test-pg). Impact: Migration v29 (redeem_attempts); CreateAccount becomes a single-row INSERT with unique-violation conflict; BootstrapFirstAdmin advisory-locks (PG) / rechecks (SQLite) and commits admin+key+audit in one transaction; SSE revalidation covers API-key streams; redeem backoff moves to the shared DB; CLI gains AGENT_SYSTEM_DSN fallback and --out file delivery for create/reset-password/issue-login-key/ bootstrap-admin. Refs: docs/svc-deploy/2026-10-13-login-recovery-contract.md leftovers; proposal r2 KTD12/KTD14/R5/R11.
75 lines
2.4 KiB
Go
75 lines
2.4 KiB
Go
package store
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// dialect 收敛 SQLite / PostgreSQL 差异。全部查询以 ? 占位符书写,PG 执行前 rebind。
|
|
type dialect interface {
|
|
name() string
|
|
rebind(query string) string
|
|
// lockConversation 在事务内锁定会话的事件序列分配(跨实例安全)。
|
|
// PG:事务级咨询锁;SQLite:单写连接天然串行,无需额外锁。
|
|
lockConversation(tx *sql.Tx, conversationID string) error
|
|
// lockScope 锁定设置 scope 的读改写(owner+scope)。
|
|
lockScope(tx *sql.Tx, owner, scope string) error
|
|
// lockAdmission 锁定执行许可账本的清点与登记(U3 admission)。
|
|
lockAdmission(tx *sql.Tx) error
|
|
// lockBootstrap 锁定首次管理员引导的「核验无用户→建号→激活 Key」串行段
|
|
//(KTD12:防两个进程并发引导出双管理员)。
|
|
lockBootstrap(tx *sql.Tx) error
|
|
}
|
|
|
|
// ---- SQLite(嵌入式档)----
|
|
|
|
type sqliteDialect struct{}
|
|
|
|
func (sqliteDialect) name() string { return "sqlite" }
|
|
func (sqliteDialect) rebind(q string) string { return q }
|
|
func (sqliteDialect) lockConversation(*sql.Tx, string) error { return nil }
|
|
func (sqliteDialect) lockScope(*sql.Tx, string, string) error { return nil }
|
|
func (sqliteDialect) lockAdmission(*sql.Tx) error { return nil }
|
|
func (sqliteDialect) lockBootstrap(*sql.Tx) error { return nil }
|
|
|
|
// ---- PostgreSQL(多用户/多实例档)----
|
|
|
|
type postgresDialect struct{}
|
|
|
|
func (postgresDialect) name() string { return "postgres" }
|
|
|
|
func (postgresDialect) rebind(q string) string {
|
|
var b strings.Builder
|
|
n := 0
|
|
for i := 0; i < len(q); i++ {
|
|
if q[i] == '?' {
|
|
n++
|
|
fmt.Fprintf(&b, "$%d", n)
|
|
continue
|
|
}
|
|
b.WriteByte(q[i])
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func (postgresDialect) lockConversation(tx *sql.Tx, conversationID string) error {
|
|
_, err := tx.Exec(`SELECT pg_advisory_xact_lock(hashtext($1))`, "conv:"+conversationID)
|
|
return err
|
|
}
|
|
|
|
func (postgresDialect) lockScope(tx *sql.Tx, owner, scope string) error {
|
|
_, err := tx.Exec(`SELECT pg_advisory_xact_lock(hashtext($1))`, "scope:"+owner+"/"+scope)
|
|
return err
|
|
}
|
|
|
|
func (postgresDialect) lockAdmission(tx *sql.Tx) error {
|
|
_, err := tx.Exec(`SELECT pg_advisory_xact_lock(hashtext($1))`, "admission:exec-slots")
|
|
return err
|
|
}
|
|
|
|
func (postgresDialect) lockBootstrap(tx *sql.Tx) error {
|
|
_, err := tx.Exec(`SELECT pg_advisory_xact_lock(hashtext($1))`, "bootstrap:first-admin")
|
|
return err
|
|
}
|