Heimdall/src/store/postgres.ts
Meta Vens 0f5ffd9e69 Every start connects to Postgres; pin the signing key's read refusal
Soul pass 3 (R1, R4). With GC_ACCESS_APPLY_SCHEMA_ON_STARTUP=0, startup
never opened a connection, so a misbound database URL surfaced only in a
request's error answer. createStore now connects on every Postgres start:
it applies the schema when asked and otherwise runs `SELECT 1`, and either
failure is the same fixed message naming GC_ACCESS_DATABASE_URL_FILE and a
code. The entrypoint harness gains that case with the canary in the host.

The harness also pins the signing key's read-failure branch with a key file
of mode 000. Root reads such a file, so the case runs the entrypoint as a
user a mode can refuse: the test process itself when it is not root, or
else the checkout's owner. It is skipped, with the reason in its title, on
Windows or as root in a root-owned checkout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:30:10 +02:00

804 lines
24 KiB
TypeScript

import { randomUUID } from "node:crypto";
import { Pool, type PoolClient, type QueryResultRow } from "pg";
import { type AppSlug, type HeimdallAuthAttemptStatus, type LinkedIdentityInput, type Provider } from "../contracts.js";
import { CREATE_SCHEMA_SQL } from "./schema.js";
import {
type CreateAccountInput,
type CreateAuthAttemptInput,
type CreateAuthCompletionInput,
type CreateAuditEventInput,
type CreateCapabilityGrantInput,
type CreateEntitlementSnapshotInput,
type CreateSessionInput,
type CreatePrivateCommandReceiptInput,
type HeimdallStore,
type StoredAccount,
type StoredAuthAttempt,
type StoredAuthCompletion,
type StoredCapabilityGrant,
type StoredLinkedIdentity,
type StoredSession,
type StoredPrivateCommandReceipt,
type UpsertLinkedIdentityInput,
} from "./types.js";
interface AccountRow extends QueryResultRow {
id: string;
created_at: string;
last_seen_at: string;
display_name: string | null;
primary_email: string | null;
}
interface LinkedIdentityRow extends QueryResultRow {
id: string;
account_id: string;
provider: Provider;
provider_user_id: string;
username: string | null;
display_name: string | null;
primary_email: string | null;
access_token_encrypted: string | null;
refresh_token_encrypted: string | null;
token_expires_at: string | null;
scopes: string;
profile_json: Record<string, unknown>;
created_at: string;
updated_at: string;
}
interface GrantRow extends QueryResultRow {
id: string;
account_id: string;
scope_type: "global" | "app";
scope_id: AppSlug | null;
capability: string;
source: string;
status: "active" | "revoked";
expires_at: string | null;
note: string | null;
created_at: string;
updated_at: string;
}
interface SessionRow extends QueryResultRow {
id: string;
account_id: string;
app_slug: AppSlug;
created_at: string;
last_seen_at: string;
expires_at: string;
access_revision: number;
claims_json: Record<string, unknown>;
}
interface AuthCompletionRow extends QueryResultRow {
code: string;
attempt_id: string | null;
app_slug: AppSlug;
provider: Provider;
mode: "sign_in" | "link" | "connect";
account_id: string;
session_id: string;
return_to: string;
payload_json: Record<string, unknown>;
created_at: string | Date;
expires_at: string | Date;
consumed_at: string | Date | null;
}
interface AuthAttemptRow extends QueryResultRow {
handle: string;
app_slug: AppSlug;
provider: Provider;
mode: "sign_in" | "link" | "connect";
return_to: string;
status: HeimdallAuthAttemptStatus;
created_at: string | Date;
expires_at: string | Date;
completed_at: string | Date | null;
consumed_at: string | Date | null;
denial_code: string | null;
}
interface PrivateCommandReceiptRow extends QueryResultRow {
app_slug: AppSlug;
idempotency_key: string;
request_fingerprint: string;
status: string;
content_schema: string;
envelope_base64: string;
created_at: string | Date;
expires_at: string | Date;
}
function expectRow<T>(row: T | undefined, label: string): T {
if (!row) {
throw new Error(`${label} query returned no rows.`);
}
return row;
}
function nullable<T>(value: T | undefined): T | null {
return value ?? null;
}
function normalizeTimestamp(value: string | Date): string {
return value instanceof Date ? value.toISOString() : value;
}
function parseScopes(value: string): string[] {
return value.split(" ").map((item) => item.trim()).filter(Boolean);
}
function mapAccountRow(row: AccountRow): StoredAccount {
const account: StoredAccount = {
id: row.id,
createdAt: row.created_at,
lastSeenAt: row.last_seen_at,
};
if (row.display_name) {
account.displayName = row.display_name;
}
if (row.primary_email) {
account.primaryEmail = row.primary_email;
}
return account;
}
function mapLinkedIdentityRow(row: LinkedIdentityRow): StoredLinkedIdentity {
const identity: StoredLinkedIdentity = {
id: row.id,
accountId: row.account_id,
provider: row.provider,
providerUserId: row.provider_user_id,
scopes: parseScopes(row.scopes),
profileJson: row.profile_json,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
if (row.username) {
identity.username = row.username;
}
if (row.display_name) {
identity.displayName = row.display_name;
}
if (row.primary_email) {
identity.primaryEmail = row.primary_email;
}
if (row.access_token_encrypted) {
identity.accessTokenEncrypted = row.access_token_encrypted;
}
if (row.refresh_token_encrypted) {
identity.refreshTokenEncrypted = row.refresh_token_encrypted;
}
if (row.token_expires_at) {
identity.tokenExpiresAt = row.token_expires_at;
}
return identity;
}
function mapGrantRow(row: GrantRow): StoredCapabilityGrant {
const grant: StoredCapabilityGrant = {
id: row.id,
accountId: row.account_id,
scopeType: row.scope_type,
capability: row.capability,
source: row.source,
status: row.status,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
if (row.scope_id) {
grant.scopeId = row.scope_id;
}
if (row.expires_at) {
grant.expiresAt = row.expires_at;
}
if (row.note) {
grant.note = row.note;
}
return grant;
}
function mapSessionRow(row: SessionRow): StoredSession {
return {
id: row.id,
accountId: row.account_id,
appSlug: row.app_slug,
createdAt: row.created_at,
lastSeenAt: row.last_seen_at,
expiresAt: row.expires_at,
accessRevision: row.access_revision,
claimsJson: row.claims_json,
};
}
function mapAuthCompletionRow(row: AuthCompletionRow): StoredAuthCompletion {
const completion: StoredAuthCompletion = {
code: row.code,
appSlug: row.app_slug,
provider: row.provider,
mode: row.mode,
accountId: row.account_id,
sessionId: row.session_id,
returnTo: row.return_to,
createdAt: normalizeTimestamp(row.created_at),
expiresAt: normalizeTimestamp(row.expires_at),
payloadJson: row.payload_json,
};
if (row.attempt_id) {
completion.attemptId = row.attempt_id;
}
if (row.consumed_at) {
completion.consumedAt = normalizeTimestamp(row.consumed_at);
}
return completion;
}
function mapPrivateCommandReceiptRow(row: PrivateCommandReceiptRow): StoredPrivateCommandReceipt {
return {
appSlug: row.app_slug,
idempotencyKey: row.idempotency_key,
requestFingerprint: row.request_fingerprint,
status: row.status,
contentSchema: row.content_schema,
envelopeBase64: row.envelope_base64,
createdAt: normalizeTimestamp(row.created_at),
expiresAt: normalizeTimestamp(row.expires_at),
};
}
function mapAuthAttemptRow(row: AuthAttemptRow): StoredAuthAttempt {
const attempt: StoredAuthAttempt = {
handle: row.handle,
appSlug: row.app_slug,
provider: row.provider,
mode: row.mode,
returnTo: row.return_to,
status: row.status,
createdAt: normalizeTimestamp(row.created_at),
expiresAt: normalizeTimestamp(row.expires_at),
};
if (row.completed_at) attempt.completedAt = normalizeTimestamp(row.completed_at);
if (row.consumed_at) attempt.consumedAt = normalizeTimestamp(row.consumed_at);
if (row.denial_code) attempt.denialCode = row.denial_code;
return attempt;
}
export class PostgresStore implements HeimdallStore {
constructor(private readonly pool: Pick<Pool, "query" | "end">) {}
async ensureSchema(): Promise<void> {
await this.pool.query(CREATE_SCHEMA_SQL);
}
/** Opens a connection and runs a trivial query, so a start that does not apply the schema still proves the URL. */
async checkConnection(): Promise<void> {
await this.pool.query("SELECT 1");
}
async close(): Promise<void> {
await this.pool.end();
}
async createAccount(input: CreateAccountInput): Promise<StoredAccount> {
const id = input.id ?? randomUUID();
const result = await this.pool.query<AccountRow>(
`
INSERT INTO accounts (id, created_at, last_seen_at, display_name, primary_email)
VALUES ($1, $2, $3, $4, $5)
RETURNING *
`,
[id, input.createdAt, input.lastSeenAt, nullable(input.displayName), nullable(input.primaryEmail)]
);
return mapAccountRow(expectRow(result.rows[0], "createAccount"));
}
async touchAccount(accountId: string, at: string, updates?: { displayName?: string; primaryEmail?: string }): Promise<void> {
await this.pool.query(
`
UPDATE accounts
SET last_seen_at = $2,
display_name = COALESCE($3, display_name),
primary_email = COALESCE($4, primary_email)
WHERE id = $1
`,
[accountId, at, nullable(updates?.displayName), nullable(updates?.primaryEmail)]
);
}
async findAccountById(accountId: string): Promise<StoredAccount | null> {
const result = await this.pool.query<AccountRow>(
`
SELECT *
FROM accounts
WHERE id = $1
LIMIT 1
`,
[accountId]
);
return result.rowCount ? mapAccountRow(expectRow(result.rows[0], "findAccountById")) : null;
}
async findAccountByLinkedIdentity(provider: Provider, providerUserId: string): Promise<StoredAccount | null> {
const result = await this.pool.query<AccountRow>(
`
SELECT accounts.*
FROM accounts
INNER JOIN linked_identities ON linked_identities.account_id = accounts.id
WHERE linked_identities.provider = $1
AND linked_identities.provider_user_id = $2
LIMIT 1
`,
[provider, providerUserId]
);
return result.rowCount ? mapAccountRow(expectRow(result.rows[0], "findAccountByLinkedIdentity")) : null;
}
async findStoredLinkedIdentity(provider: Provider, providerUserId: string): Promise<StoredLinkedIdentity | null> {
const result = await this.pool.query<LinkedIdentityRow>(
`
SELECT *
FROM linked_identities
WHERE provider = $1
AND provider_user_id = $2
LIMIT 1
`,
[provider, providerUserId]
);
return result.rowCount ? mapLinkedIdentityRow(expectRow(result.rows[0], "findStoredLinkedIdentity")) : null;
}
async findStoredLinkedIdentityForAccount(accountId: string, provider: Provider): Promise<StoredLinkedIdentity | null> {
const result = await this.pool.query<LinkedIdentityRow>(
`
SELECT *
FROM linked_identities
WHERE account_id = $1
AND provider = $2
ORDER BY updated_at DESC
LIMIT 1
`,
[accountId, provider]
);
return result.rowCount ? mapLinkedIdentityRow(expectRow(result.rows[0], "findStoredLinkedIdentityForAccount")) : null;
}
async listStoredLinkedIdentitiesForAccount(accountId: string): Promise<StoredLinkedIdentity[]> {
const result = await this.pool.query<LinkedIdentityRow>(
`
SELECT *
FROM linked_identities
WHERE account_id = $1
ORDER BY created_at ASC
`,
[accountId]
);
return result.rows.map(mapLinkedIdentityRow);
}
async upsertLinkedIdentity(input: UpsertLinkedIdentityInput): Promise<StoredLinkedIdentity> {
const id = input.id ?? randomUUID();
const result = await this.pool.query<LinkedIdentityRow>(
`
INSERT INTO linked_identities (
id,
account_id,
provider,
provider_user_id,
username,
display_name,
primary_email,
access_token_encrypted,
refresh_token_encrypted,
token_expires_at,
scopes,
profile_json,
created_at,
updated_at
)
VALUES (
$1, $2, $3, $4, $5, $6, $7,
$8, $9, $10, $11, $12, $13, $14
)
ON CONFLICT (provider, provider_user_id)
DO UPDATE SET
account_id = EXCLUDED.account_id,
username = EXCLUDED.username,
display_name = EXCLUDED.display_name,
primary_email = EXCLUDED.primary_email,
access_token_encrypted = EXCLUDED.access_token_encrypted,
refresh_token_encrypted = EXCLUDED.refresh_token_encrypted,
token_expires_at = EXCLUDED.token_expires_at,
scopes = EXCLUDED.scopes,
profile_json = EXCLUDED.profile_json,
updated_at = EXCLUDED.updated_at
RETURNING *
`,
[
id,
input.accountId,
input.provider,
input.providerUserId,
nullable(input.username),
nullable(input.displayName),
nullable(input.primaryEmail),
nullable(input.accessTokenEncrypted),
nullable(input.refreshTokenEncrypted),
nullable(input.tokenExpiresAt),
input.scopes.join(" "),
JSON.stringify(input.profileJson),
input.createdAt,
input.updatedAt,
]
);
return mapLinkedIdentityRow(expectRow(result.rows[0], "upsertLinkedIdentity"));
}
async listLinkedIdentitiesForAccount(accountId: string): Promise<LinkedIdentityInput[]> {
const result = await this.pool.query<LinkedIdentityRow>(
`
SELECT *
FROM linked_identities
WHERE account_id = $1
ORDER BY created_at ASC
`,
[accountId]
);
return result.rows.map((row) => {
const linkedIdentity: LinkedIdentityInput = {
provider: row.provider,
providerUserId: row.provider_user_id,
};
if (row.username) {
linkedIdentity.username = row.username;
}
if (row.display_name) {
linkedIdentity.displayName = row.display_name;
}
return linkedIdentity;
});
}
async createCapabilityGrant(input: CreateCapabilityGrantInput): Promise<StoredCapabilityGrant> {
const id = input.id ?? randomUUID();
const result = await this.pool.query<GrantRow>(
`
INSERT INTO capability_grants (
id, account_id, scope_type, scope_id, capability,
source, status, expires_at, note, created_at, updated_at
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING *
`,
[
id,
input.accountId,
input.scopeType,
nullable(input.scopeId),
input.capability,
input.source,
input.status,
nullable(input.expiresAt),
nullable(input.note),
input.createdAt,
input.updatedAt,
]
);
return mapGrantRow(expectRow(result.rows[0], "createCapabilityGrant"));
}
async listActiveGrants(accountId: string, appSlug: AppSlug, at: string): Promise<StoredCapabilityGrant[]> {
const result = await this.pool.query<GrantRow>(
`
SELECT *
FROM capability_grants
WHERE account_id = $1
AND status = 'active'
AND (expires_at IS NULL OR expires_at > $3)
AND (
scope_type = 'global'
OR (scope_type = 'app' AND scope_id = $2)
)
ORDER BY created_at ASC
`,
[accountId, appSlug, at]
);
return result.rows.map(mapGrantRow);
}
async createSession(input: CreateSessionInput): Promise<StoredSession> {
const result = await this.pool.query<SessionRow>(
`
INSERT INTO sessions (
id, account_id, app_slug, created_at, last_seen_at,
expires_at, claims_json, access_revision
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
ON CONFLICT (id)
DO UPDATE SET
last_seen_at = EXCLUDED.last_seen_at,
expires_at = EXCLUDED.expires_at,
claims_json = EXCLUDED.claims_json,
access_revision = EXCLUDED.access_revision
WHERE sessions.account_id = EXCLUDED.account_id
AND sessions.app_slug = EXCLUDED.app_slug
AND sessions.access_revision <= EXCLUDED.access_revision
RETURNING *
`,
[
input.id,
input.accountId,
input.appSlug,
input.createdAt,
input.lastSeenAt,
input.expiresAt,
JSON.stringify(input.claimsJson),
input.accessRevision,
]
);
return mapSessionRow(expectRow(result.rows[0], "createSession"));
}
async findSession(appSlug: AppSlug, sessionId: string): Promise<StoredSession | null> {
const result = await this.pool.query<SessionRow>(
"SELECT * FROM sessions WHERE app_slug = $1 AND id = $2",
[appSlug, sessionId],
);
return result.rowCount ? mapSessionRow(expectRow(result.rows[0], "findSession")) : null;
}
async revokeSession(
appSlug: AppSlug,
sessionId: string,
accountId: string,
expectedAccessRevision: number,
at: string,
): Promise<StoredSession | null> {
const result = await this.pool.query<SessionRow>(
`UPDATE sessions
SET last_seen_at = $5, expires_at = $5, access_revision = access_revision + 1
WHERE app_slug = $1 AND id = $2 AND account_id = $3 AND access_revision = $4
RETURNING *`,
[appSlug, sessionId, accountId, expectedAccessRevision, at],
);
return result.rowCount ? mapSessionRow(expectRow(result.rows[0], "revokeSession")) : null;
}
async createAuthAttempt(input: CreateAuthAttemptInput): Promise<StoredAuthAttempt> {
const handle = input.handle ?? randomUUID();
const result = await this.pool.query<AuthAttemptRow>(
`INSERT INTO auth_attempts (
handle, app_slug, provider, mode, return_to, status, created_at, expires_at
) VALUES ($1, $2, $3, $4, $5, 'pending', $6, $7) RETURNING *`,
[handle, input.appSlug, input.provider, input.mode, input.returnTo, input.createdAt, input.expiresAt],
);
return mapAuthAttemptRow(expectRow(result.rows[0], "createAuthAttempt"));
}
async findAuthAttempt(appSlug: AppSlug, handle: string): Promise<StoredAuthAttempt | null> {
const result = await this.pool.query<AuthAttemptRow>(
"SELECT * FROM auth_attempts WHERE app_slug = $1 AND handle = $2",
[appSlug, handle],
);
return result.rowCount ? mapAuthAttemptRow(expectRow(result.rows[0], "findAuthAttempt")) : null;
}
async updateAuthAttempt(
appSlug: AppSlug,
handle: string,
update: { status: HeimdallAuthAttemptStatus; at: string; denialCode?: string },
): Promise<StoredAuthAttempt | null> {
const result = await this.pool.query<AuthAttemptRow>(
`UPDATE auth_attempts SET
status = $3,
completed_at = CASE WHEN $3 = 'completed' THEN $4 ELSE completed_at END,
consumed_at = CASE WHEN $3 = 'consumed' THEN $4 ELSE consumed_at END,
denial_code = COALESCE($5, denial_code)
WHERE app_slug = $1 AND handle = $2
AND (
status = $3 OR
(status = 'pending' AND $3 IN ('completed', 'denied', 'expired')) OR
(status = 'completed' AND $3 = 'consumed')
)
RETURNING *`,
[appSlug, handle, update.status, update.at, nullable(update.denialCode)],
);
if (result.rowCount) return mapAuthAttemptRow(expectRow(result.rows[0], "updateAuthAttempt"));
return await this.findAuthAttempt(appSlug, handle);
}
async createPrivateCommandReceipt(input: CreatePrivateCommandReceiptInput): Promise<StoredPrivateCommandReceipt> {
const result = await this.pool.query<PrivateCommandReceiptRow>(
`INSERT INTO private_command_receipts (
app_slug, idempotency_key, request_fingerprint, status,
content_schema, envelope_base64, created_at, expires_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
ON CONFLICT (app_slug, idempotency_key) DO NOTHING
RETURNING *`,
[
input.appSlug,
input.idempotencyKey,
input.requestFingerprint,
input.status,
input.contentSchema,
input.envelopeBase64,
input.createdAt,
input.expiresAt,
],
);
const existing = result.rowCount
? mapPrivateCommandReceiptRow(expectRow(result.rows[0], "createPrivateCommandReceipt"))
: await this.findPrivateCommandReceipt(input.appSlug, input.idempotencyKey);
if (!existing) throw new Error("Private command receipt conflict returned no record.");
if (existing.requestFingerprint !== input.requestFingerprint) {
throw new Error("Idempotency key was reused with different command content.");
}
return existing;
}
async findPrivateCommandReceipt(appSlug: AppSlug, idempotencyKey: string): Promise<StoredPrivateCommandReceipt | null> {
const result = await this.pool.query<PrivateCommandReceiptRow>(
"SELECT * FROM private_command_receipts WHERE app_slug = $1 AND idempotency_key = $2",
[appSlug, idempotencyKey],
);
return result.rowCount
? mapPrivateCommandReceiptRow(expectRow(result.rows[0], "findPrivateCommandReceipt"))
: null;
}
async createAuthCompletion(input: CreateAuthCompletionInput): Promise<StoredAuthCompletion> {
// The code is always minted here; nothing upstream may choose it.
const code = randomUUID();
const result = await this.pool.query<AuthCompletionRow>(
`
INSERT INTO auth_completions (
code, attempt_id, app_slug, provider, mode, account_id, session_id,
return_to, payload_json, created_at, expires_at
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING *
`,
[
code,
input.attemptId ?? null,
input.appSlug,
input.provider,
input.mode,
input.accountId,
input.sessionId,
input.returnTo,
JSON.stringify(input.payloadJson),
input.createdAt,
input.expiresAt,
]
);
return mapAuthCompletionRow(expectRow(result.rows[0], "createAuthCompletion"));
}
async consumeAuthCompletion(appSlug: AppSlug, code: string, at: string): Promise<StoredAuthCompletion | null> {
const result = await this.pool.query<AuthCompletionRow>(
`
UPDATE auth_completions
SET consumed_at = $3
WHERE code = $1
AND app_slug = $2
AND consumed_at IS NULL
AND expires_at > $3
RETURNING *
`,
[code, appSlug, at]
);
return result.rowCount ? mapAuthCompletionRow(expectRow(result.rows[0], "consumeAuthCompletion")) : null;
}
async consumeAuthCompletionByAttempt(appSlug: AppSlug, attemptId: string, at: string): Promise<StoredAuthCompletion | null> {
const result = await this.pool.query<AuthCompletionRow>(
`
UPDATE auth_completions
SET consumed_at = $3
WHERE attempt_id = $1
AND app_slug = $2
AND consumed_at IS NULL
AND expires_at > $3
RETURNING *
`,
[attemptId, appSlug, at]
);
return result.rowCount ? mapAuthCompletionRow(expectRow(result.rows[0], "consumeAuthCompletionByAttempt")) : null;
}
async upsertEntitlementSnapshot(input: CreateEntitlementSnapshotInput): Promise<void> {
await this.pool.query(
`
INSERT INTO entitlement_snapshots (
id, account_id, provider, scope, evaluated_at,
is_allowed, reason_code, reason_detail, raw_summary_json
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
ON CONFLICT (account_id, provider, scope)
DO UPDATE SET
evaluated_at = EXCLUDED.evaluated_at,
is_allowed = EXCLUDED.is_allowed,
reason_code = EXCLUDED.reason_code,
reason_detail = EXCLUDED.reason_detail,
raw_summary_json = EXCLUDED.raw_summary_json
`,
[
randomUUID(),
input.accountId,
input.provider,
input.scope,
input.evaluatedAt,
input.isAllowed,
input.reasonCode,
nullable(input.reasonDetail),
JSON.stringify(input.rawSummaryJson),
]
);
}
async createAuditEvent(input: CreateAuditEventInput): Promise<void> {
await this.pool.query(
`
INSERT INTO audit_events (
id, account_id, session_id, app_slug, event_type,
event_payload_json, created_at
)
VALUES ($1, $2, $3, $4, $5, $6, $7)
`,
[
input.id ?? randomUUID(),
nullable(input.accountId),
nullable(input.sessionId),
nullable(input.appSlug),
input.eventType,
JSON.stringify(input.eventPayloadJson),
input.createdAt,
]
);
}
}
export function createPostgresStore(databaseUrl: string): PostgresStore {
const pool = new Pool({
connectionString: databaseUrl,
});
return new PostgresStore(pool);
}