From f2eec21c57d192258741d055dee3c918c57988e7 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 22 Aug 2026 12:01:56 +0200 Subject: [PATCH] Bind auth completions to their attempt handles --- ...all.auth_completion_receipt.v1.schema.json | 2 +- src/private-command-plane.ts | 19 ++++++++----- tests/access-plugin.test.ts | 27 +++++++++++++++++++ 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/schemas/heimdall.auth_completion_receipt.v1.schema.json b/schemas/heimdall.auth_completion_receipt.v1.schema.json index 2ff939a..2029ee8 100644 --- a/schemas/heimdall.auth_completion_receipt.v1.schema.json +++ b/schemas/heimdall.auth_completion_receipt.v1.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "heimdall.auth_completion_receipt.v1", "type": "object", - "required": ["schema", "status"], + "required": ["schema", "status", "handle"], "properties": { "schema": { "const": "heimdall.auth_completion_receipt.v1" }, "status": { "enum": ["pending", "authenticated", "denied"] }, diff --git a/src/private-command-plane.ts b/src/private-command-plane.ts index 0309f12..492d6bc 100644 --- a/src/private-command-plane.ts +++ b/src/private-command-plane.ts @@ -234,7 +234,7 @@ async function refreshAuth( const refreshedPayload = refreshed.json>(); if (refreshed.statusCode !== 201) { return { - status: "denied", + status: "accepted", payloadSchema: "heimdall.auth_refresh_receipt.v1", payload: { schema: "heimdall.auth_refresh_receipt.v1", @@ -326,22 +326,29 @@ async function completeAuth( const now = new Date().toISOString(); if (attempt.expiresAt <= now && attempt.status !== "completed") { await store.updateAuthAttempt(appSlug, handle, { status: "expired", at: now }); - return authCompletion("denied", { error: "expired_attempt" }); + return authCompletion("denied", { handle, error: "expired_attempt" }); } if (attempt.status === "pending") return authCompletion("pending", { handle }); - if (attempt.status !== "completed") return authCompletion("denied", { error: attempt.denialCode ?? `attempt_${attempt.status}` }); + if (attempt.status !== "completed") { + return authCompletion("denied", { handle, error: attempt.denialCode ?? `attempt_${attempt.status}` }); + } const redemption = await app.inject({ method: "POST", url: `/v1/apps/${appSlug}/auth-completions/redeem`, payload: { completionCode: handle }, }); - if (redemption.statusCode !== 201) return authCompletion("denied", { error: "invalid_or_expired_completion" }); - return authCompletion("authenticated", redemption.json>()); + if (redemption.statusCode !== 201) { + return authCompletion("denied", { handle, error: "invalid_or_expired_completion" }); + } + return authCompletion("authenticated", { handle, ...redemption.json>() }); } function authCompletion(status: string, values: Record) { return { - status: status === "authenticated" ? "accepted" : status, + // CultNet reports whether the private command executed. Authentication state + // remains inside the typed receipt so callers can decode pending and denied + // outcomes instead of mistaking them for transport failures. + status: "accepted", payloadSchema: "heimdall.auth_completion_receipt.v1", payload: { ...values, schema: "heimdall.auth_completion_receipt.v1", status }, }; diff --git a/tests/access-plugin.test.ts b/tests/access-plugin.test.ts index bd1f2a2..178432b 100644 --- a/tests/access-plugin.test.ts +++ b/tests/access-plugin.test.ts @@ -111,6 +111,19 @@ describe("Heimdall Eve access plugin", () => { const state = new URL(navigation.url).searchParams.get("state"); expect(state).toBeTruthy(); + const pendingEnvelope = sealPrivateEnvelope({ + appSlug: "ghostlight", + operation: "heimdall.auth.complete", + contentSchema: "heimdall.auth_complete_command.v1", + idempotencyKey: "complete-pending-1", + secret, + payload: { handle }, + }); + const pendingResponse = await invoke(plane.endpoint, "heimdall.auth.complete", "transport-complete-pending-1", pendingEnvelope); + expect(pendingResponse.status).toBe("accepted"); + const pending = openPrivateEnvelope(decodeEnvelope(pendingResponse.payload), secret); + expect(pending).toMatchObject({ status: "pending", handle }); + const callback = await app.inject({ method: "GET", url: `/v1/oauth/discord/callback?code=test-code&state=${encodeURIComponent(state ?? "")}`, @@ -134,8 +147,22 @@ describe("Heimdall Eve access plugin", () => { expect(completionWire.contentSchema).toBe("heimdall.auth_completion_receipt.v1"); const completion = openPrivateEnvelope(completionWire, secret); expect(completion.status).toBe("authenticated"); + expect(completion.handle).toBe(handle); expect(completion.accessToken).toEqual(expect.any(String)); + const consumedEnvelope = sealPrivateEnvelope({ + appSlug: "ghostlight", + operation: "heimdall.auth.complete", + contentSchema: "heimdall.auth_complete_command.v1", + idempotencyKey: "complete-consumed-1", + secret, + payload: { handle }, + }); + const consumedResponse = await invoke(plane.endpoint, "heimdall.auth.complete", "transport-complete-consumed-1", consumedEnvelope); + expect(consumedResponse.status).toBe("accepted"); + const consumed = openPrivateEnvelope(decodeEnvelope(consumedResponse.payload), secret); + expect(consumed).toMatchObject({ status: "denied", handle, error: "attempt_consumed" }); + const refreshEnvelope = sealPrivateEnvelope({ appSlug: "ghostlight", operation: "heimdall.auth.refresh",