Files
multica/server/pkg/db/generated/daemon_token.sql.go
Jiayuan Zhang f52db9e96c feat(server): install_token mint+exchange + daemon_token revoke (MUL-2305)
Phase 1 of RFC MUL-2297 — DB and credential contract for the new runtime
install flow. CLI/daemon/UI plumbing lands in later phases.

Schema (migration 091):
- daemon_token.revoked_at — explicit revoke replaces TTL-based expiry; the
  exchange path now mints daemon_token rows with a ~100y expires_at so the
  cleanup query stays intact while the credential is effectively long-lived
  until revoked.
- install_token — short-lived (15m) single-use credential. used_at IS NULL
  is the atomic gate enforced inside the UPDATE so a concurrent second
  exchange returns zero rows.

API:
- POST /api/workspaces/{id}/install-tokens — admin-only mint, returns mit_
  once; only the hash is stored.
- POST /api/install-tokens/exchange — public (the mit_ is the credential);
  atomically consumes the install_token and returns a fresh mdt_.

Error contract for Phase 2 daemon installer:
- 401 invalid_install_token — unknown hash OR expired
- 401 install_token_already_used — hash exists but used_at IS NOT NULL

Co-authored-by: multica-agent <github@multica.ai>
2026-05-17 00:09:48 +08:00

145 lines
4.3 KiB
Go

// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: daemon_token.sql
package db
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const createDaemonToken = `-- name: CreateDaemonToken :one
INSERT INTO daemon_token (token_hash, workspace_id, daemon_id, expires_at)
VALUES ($1, $2, $3, $4)
RETURNING id, token_hash, workspace_id, daemon_id, expires_at, created_at, revoked_at
`
type CreateDaemonTokenParams struct {
TokenHash string `json:"token_hash"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
DaemonID string `json:"daemon_id"`
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
}
func (q *Queries) CreateDaemonToken(ctx context.Context, arg CreateDaemonTokenParams) (DaemonToken, error) {
row := q.db.QueryRow(ctx, createDaemonToken,
arg.TokenHash,
arg.WorkspaceID,
arg.DaemonID,
arg.ExpiresAt,
)
var i DaemonToken
err := row.Scan(
&i.ID,
&i.TokenHash,
&i.WorkspaceID,
&i.DaemonID,
&i.ExpiresAt,
&i.CreatedAt,
&i.RevokedAt,
)
return i, err
}
const deleteDaemonTokensByWorkspaceAndDaemons = `-- name: DeleteDaemonTokensByWorkspaceAndDaemons :many
DELETE FROM daemon_token
WHERE workspace_id = $1
AND daemon_id = ANY($2::text[])
RETURNING token_hash
`
type DeleteDaemonTokensByWorkspaceAndDaemonsParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
DaemonIds []string `json:"daemon_ids"`
}
// Deletes every daemon_token row matching the (workspace_id, daemon_id)
// pairs implied by `daemon_ids`. Used by the member-revocation flow to
// nuke tokens for all runtimes a leaving member owned in one shot.
// Returns token_hash so the caller can invalidate auth.DaemonTokenCache
// before the 10-minute TTL expires — without that invalidate, a daemon
// can keep using its stale token until cache eviction even though the
// DB row is gone.
func (q *Queries) DeleteDaemonTokensByWorkspaceAndDaemons(ctx context.Context, arg DeleteDaemonTokensByWorkspaceAndDaemonsParams) ([]string, error) {
rows, err := q.db.Query(ctx, deleteDaemonTokensByWorkspaceAndDaemons, arg.WorkspaceID, arg.DaemonIds)
if err != nil {
return nil, err
}
defer rows.Close()
items := []string{}
for rows.Next() {
var token_hash string
if err := rows.Scan(&token_hash); err != nil {
return nil, err
}
items = append(items, token_hash)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const deleteExpiredDaemonTokens = `-- name: DeleteExpiredDaemonTokens :exec
DELETE FROM daemon_token
WHERE expires_at <= now()
`
func (q *Queries) DeleteExpiredDaemonTokens(ctx context.Context) error {
_, err := q.db.Exec(ctx, deleteExpiredDaemonTokens)
return err
}
const getDaemonTokenByHash = `-- name: GetDaemonTokenByHash :one
SELECT id, token_hash, workspace_id, daemon_id, expires_at, created_at, revoked_at FROM daemon_token
WHERE token_hash = $1
AND revoked_at IS NULL
AND expires_at > now()
`
// revoked_at IS NULL filters out tokens explicitly revoked via the runtime
// UX flow (RFC MUL-2297). expires_at > now() stays for defense in depth even
// though Phase 1 mints tokens with a ~100-year expiry — the legacy short-TTL
// behavior is still legal and the cleanup query depends on it.
func (q *Queries) GetDaemonTokenByHash(ctx context.Context, tokenHash string) (DaemonToken, error) {
row := q.db.QueryRow(ctx, getDaemonTokenByHash, tokenHash)
var i DaemonToken
err := row.Scan(
&i.ID,
&i.TokenHash,
&i.WorkspaceID,
&i.DaemonID,
&i.ExpiresAt,
&i.CreatedAt,
&i.RevokedAt,
)
return i, err
}
const revokeDaemonTokenByID = `-- name: RevokeDaemonTokenByID :one
UPDATE daemon_token
SET revoked_at = now()
WHERE id = $1
AND workspace_id = $2
AND revoked_at IS NULL
RETURNING token_hash
`
type RevokeDaemonTokenByIDParams struct {
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
}
// Marks a single daemon_token revoked. Returns token_hash so the caller can
// invalidate auth.DaemonTokenCache before the 10-minute TTL would otherwise
// let a revoked token keep authenticating on a cached lookup.
func (q *Queries) RevokeDaemonTokenByID(ctx context.Context, arg RevokeDaemonTokenByIDParams) (string, error) {
row := q.db.QueryRow(ctx, revokeDaemonTokenByID, arg.ID, arg.WorkspaceID)
var token_hash string
err := row.Scan(&token_hash)
return token_hash, err
}