Store: mint auth-completion codes server-side; drop the registration writer

The completion code's `code?` input let a caller choose it; app.ts used
that to make handoff.attemptId become the code verbatim, which is the
account-takeover primitive Soul's probes proved. The store now always
mints the code itself. attemptId survives only as a separate
correlation field (StoredAuthCompletion.attemptId /
CreateAuthCompletionInput.attemptId), and a new
consumeAuthCompletionByAttempt(appSlug, attemptId, at) lets a caller
that already knows the attempt handle (the private command plane)
redeem the completion it created without ever touching the code.

Also deletes HeimdallStore.registerApp: the runtime app-registration
HTTP surface that minted it is already gone from this branch, and
nothing else in the codebase writes to registered_apps (it sits empty
on the live host). findRegisteredApp/listRegisteredApps stay: AppSlug
is a plain string (contracts.ts), so registered_apps remains the real
extension point for a slug Heimdall was never rebuilt to know about; a
row is now provisioned by direct SQL, or InMemoryStore.seedRegisteredApp
in tests, not by any app-facing writer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-08 14:22:12 +02:00
commit 5a4da2ae30
4 changed files with 93 additions and 74 deletions

View file

@ -15,7 +15,6 @@ import {
type StoredAuthCompletion,
type StoredCapabilityGrant,
type StoredLinkedIdentity,
type RegisterAppInput,
type StoredRegisteredApp,
type StoredSession,
type StoredPrivateCommandReceipt,
@ -47,25 +46,18 @@ export class InMemoryStore implements HeimdallStore {
private readonly authAttempts = new Map<string, StoredAuthAttempt>();
private readonly privateCommandReceipts = new Map<string, StoredPrivateCommandReceipt>();
private readonly authCompletions = new Map<string, StoredAuthCompletion>();
private readonly authCompletionsByAttempt = new Map<string, string>();
private readonly entitlementSnapshots = new Map<string, CreateEntitlementSnapshotInput>();
private readonly auditEvents = new Map<string, CreateAuditEventInput>();
async registerApp(input: RegisterAppInput): Promise<StoredRegisteredApp> {
const existing = this.registeredApps.get(input.slug);
const record: StoredRegisteredApp = {
slug: input.slug,
displayName: input.displayName,
profileVersion: input.profileVersion,
createdAt: existing?.createdAt ?? input.registeredAt,
updatedAt: input.registeredAt,
identityProviders: [...input.identityProviders],
entitlementSources: [...input.entitlementSources],
managedConnectionProviders: [...input.managedConnectionProviders],
capabilities: structuredClone(input.capabilities),
redirectUris: [...input.redirectUris],
};
this.registeredApps.set(record.slug, record);
return structuredClone(record);
/**
* Test/ops seam only — not part of HeimdallStore. There is no production
* writer for `registered_apps` anymore; a real deployment provisions a row
* directly against Postgres. This lets fixtures exercise resolveAppProfile's
* non-built-in branch the same way.
*/
seedRegisteredApp(record: StoredRegisteredApp): void {
this.registeredApps.set(record.slug, structuredClone(record));
}
async findRegisteredApp(slug: string): Promise<StoredRegisteredApp | null> {
@ -359,8 +351,11 @@ export class InMemoryStore implements HeimdallStore {
}
async createAuthCompletion(input: CreateAuthCompletionInput): Promise<StoredAuthCompletion> {
// The code is always minted here; nothing upstream may choose it (see
// src/app.ts createAuthCompletion call site for why that mattered).
const completion: StoredAuthCompletion = {
code: input.code ?? randomUUID(),
code: randomUUID(),
...(input.attemptId ? { attemptId: input.attemptId } : {}),
appSlug: input.appSlug,
provider: input.provider,
mode: input.mode,
@ -373,6 +368,9 @@ export class InMemoryStore implements HeimdallStore {
};
this.authCompletions.set(completion.code, clone(completion));
if (input.attemptId) {
this.authCompletionsByAttempt.set(`${input.appSlug}:${input.attemptId}`, completion.code);
}
return clone(completion);
}
@ -390,6 +388,14 @@ export class InMemoryStore implements HeimdallStore {
return clone(completion);
}
async consumeAuthCompletionByAttempt(appSlug: AppSlug, attemptId: string, at: string): Promise<StoredAuthCompletion | null> {
const code = this.authCompletionsByAttempt.get(`${appSlug}:${attemptId}`);
if (!code) {
return null;
}
return this.consumeAuthCompletion(appSlug, code, at);
}
async upsertEntitlementSnapshot(input: CreateEntitlementSnapshotInput): Promise<void> {
const key = `${input.accountId}:${input.provider}:${input.scope}`;
this.entitlementSnapshots.set(key, clone(input));

View file

@ -18,7 +18,6 @@ import {
type StoredAuthCompletion,
type StoredCapabilityGrant,
type StoredLinkedIdentity,
type RegisterAppInput,
type StoredRegisteredApp,
type StoredSession,
type StoredPrivateCommandReceipt,
@ -90,6 +89,7 @@ interface SessionRow extends QueryResultRow {
interface AuthCompletionRow extends QueryResultRow {
code: string;
attempt_id: string | null;
app_slug: AppSlug;
provider: Provider;
mode: "sign_in" | "link" | "connect";
@ -258,6 +258,10 @@ function mapAuthCompletionRow(row: AuthCompletionRow): StoredAuthCompletion {
payloadJson: row.payload_json,
};
if (row.attempt_id) {
completion.attemptId = row.attempt_id;
}
if (row.consumed_at) {
completion.consumedAt = normalizeTimestamp(row.consumed_at);
}
@ -298,44 +302,11 @@ function mapAuthAttemptRow(row: AuthAttemptRow): StoredAuthAttempt {
export class PostgresStore implements HeimdallStore {
constructor(private readonly pool: Pick<Pool, "query" | "end">) {}
async registerApp(input: RegisterAppInput): Promise<StoredRegisteredApp> {
// Re-registering an app keeps its original created_at: the record is the
// same app being updated, not a new one, and RFC 7592 treats management as
// an update to an existing registration.
const result = await this.pool.query<RegisteredAppRow>(
`
INSERT INTO registered_apps (
slug, display_name, profile_version, created_at, updated_at,
identity_providers, entitlement_sources, managed_connection_providers,
capabilities_json, redirect_uris
)
VALUES ($1, $2, $3, $4, $4, $5, $6, $7, $8, $9)
ON CONFLICT (slug)
DO UPDATE SET
display_name = EXCLUDED.display_name,
profile_version = EXCLUDED.profile_version,
updated_at = EXCLUDED.updated_at,
identity_providers = EXCLUDED.identity_providers,
entitlement_sources = EXCLUDED.entitlement_sources,
managed_connection_providers = EXCLUDED.managed_connection_providers,
capabilities_json = EXCLUDED.capabilities_json,
redirect_uris = EXCLUDED.redirect_uris
RETURNING *
`,
[
input.slug,
input.displayName,
input.profileVersion,
input.registeredAt,
JSON.stringify(input.identityProviders),
JSON.stringify(input.entitlementSources),
JSON.stringify(input.managedConnectionProviders),
JSON.stringify(input.capabilities),
JSON.stringify(input.redirectUris),
],
);
return mapRegisteredApp(result.rows[0]!);
}
// There is no production writer for registered_apps anymore (the
// caller-identity cut deleted the runtime registration surface that minted
// a client_secret nobody verified). A row can still be provisioned by
// direct SQL against this table; findRegisteredApp/listRegisteredApps below
// keep reading it as a profile source beneath the built-in profiles.
async findRegisteredApp(slug: string): Promise<StoredRegisteredApp | null> {
const result = await this.pool.query<RegisteredAppRow>(
@ -742,18 +713,20 @@ export class PostgresStore implements HeimdallStore {
}
async createAuthCompletion(input: CreateAuthCompletionInput): Promise<StoredAuthCompletion> {
const code = input.code ?? randomUUID();
// 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, app_slug, provider, mode, account_id, session_id,
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)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING *
`,
[
code,
input.attemptId ?? null,
input.appSlug,
input.provider,
input.mode,
@ -786,6 +759,23 @@ export class PostgresStore implements HeimdallStore {
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(
`

View file

@ -109,6 +109,16 @@ CREATE TABLE IF NOT EXISTS auth_completions (
CREATE INDEX IF NOT EXISTS auth_completions_lookup_idx
ON auth_completions(app_slug, expires_at, consumed_at);
-- Correlation handle for the browser_completion handoff. Before the
-- caller-identity cut, the completion code itself was the caller-chosen
-- attemptId (an unauthenticated account-takeover primitive); the code is now
-- always minted by the store, and attempt_id lets the private command plane
-- redeem the completion it created without ever needing the code.
ALTER TABLE auth_completions ADD COLUMN IF NOT EXISTS attempt_id TEXT;
CREATE INDEX IF NOT EXISTS auth_completions_attempt_idx
ON auth_completions(app_slug, attempt_id);
CREATE TABLE IF NOT EXISTS capability_grants (
id TEXT PRIMARY KEY,
account_id TEXT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,

View file

@ -28,18 +28,6 @@ export interface StoredRegisteredApp {
redirectUris: string[];
}
export interface RegisterAppInput {
slug: string;
displayName: string;
profileVersion: string;
registeredAt: string;
identityProviders: Provider[];
entitlementSources: Provider[];
managedConnectionProviders: Provider[];
capabilities: CapabilityDefinition[];
redirectUris: string[];
}
export interface StoredAccount {
id: string;
createdAt: string;
@ -163,6 +151,13 @@ export interface CreateAuditEventInput {
export interface StoredAuthCompletion {
code: string;
/**
* The browser-carried attempt handle this completion correlates to, if the
* handoff supplied one. Correlation only: it is never the redemption
* secret, and never used to look up a completion by anyone but the
* envelope-authenticated private command plane.
*/
attemptId?: string;
appSlug: AppSlug;
provider: Provider;
mode: OAuthMode;
@ -176,7 +171,11 @@ export interface StoredAuthCompletion {
}
export interface CreateAuthCompletionInput {
code?: string;
/**
* Correlation handle only (see StoredAuthCompletion.attemptId). The store
* always mints `code` itself; a caller cannot choose it.
*/
attemptId?: string;
appSlug: AppSlug;
provider: Provider;
mode: OAuthMode;
@ -226,7 +225,14 @@ export interface StoredPrivateCommandReceipt {
export type CreatePrivateCommandReceiptInput = StoredPrivateCommandReceipt;
export interface HeimdallStore {
registerApp(input: RegisterAppInput): Promise<StoredRegisteredApp>;
/**
* There is no runtime writer for this table anymore (the caller-identity
* cut deleted the registration surface that minted a client_secret nobody
* verified). A profile row can still be provisioned directly against the
* store outside the app, which is how the InMemoryStore test double and any
* future ops-side seeding populate it; `resolveAppProfile` keeps consulting
* it below the built-in profiles.
*/
findRegisteredApp(slug: string): Promise<StoredRegisteredApp | null>;
listRegisteredApps(): Promise<StoredRegisteredApp[]>;
ensureSchema(): Promise<void>;
@ -262,6 +268,13 @@ export interface HeimdallStore {
findPrivateCommandReceipt(appSlug: AppSlug, idempotencyKey: string): Promise<StoredPrivateCommandReceipt | null>;
createAuthCompletion(input: CreateAuthCompletionInput): Promise<StoredAuthCompletion>;
consumeAuthCompletion(appSlug: AppSlug, code: string, at: string): Promise<StoredAuthCompletion | null>;
/**
* Consume a completion by its correlated attempt handle instead of its
* code. The private command plane already authenticated `appSlug` by
* opening the envelope; this lets its `completeAuth` redeem the completion
* it created without ever needing to know (or forge) the code.
*/
consumeAuthCompletionByAttempt(appSlug: AppSlug, attemptId: string, at: string): Promise<StoredAuthCompletion | null>;
upsertEntitlementSnapshot(input: CreateEntitlementSnapshotInput): Promise<void>;
createAuditEvent(input: CreateAuditEventInput): Promise<void>;
}