Bind auth completions to their attempt handles

This commit is contained in:
Meta Vens 2026-08-22 12:01:56 +02:00
commit f2eec21c57
3 changed files with 41 additions and 7 deletions

View file

@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "heimdall.auth_completion_receipt.v1", "$id": "heimdall.auth_completion_receipt.v1",
"type": "object", "type": "object",
"required": ["schema", "status"], "required": ["schema", "status", "handle"],
"properties": { "properties": {
"schema": { "const": "heimdall.auth_completion_receipt.v1" }, "schema": { "const": "heimdall.auth_completion_receipt.v1" },
"status": { "enum": ["pending", "authenticated", "denied"] }, "status": { "enum": ["pending", "authenticated", "denied"] },

View file

@ -234,7 +234,7 @@ async function refreshAuth(
const refreshedPayload = refreshed.json<Record<string, unknown>>(); const refreshedPayload = refreshed.json<Record<string, unknown>>();
if (refreshed.statusCode !== 201) { if (refreshed.statusCode !== 201) {
return { return {
status: "denied", status: "accepted",
payloadSchema: "heimdall.auth_refresh_receipt.v1", payloadSchema: "heimdall.auth_refresh_receipt.v1",
payload: { payload: {
schema: "heimdall.auth_refresh_receipt.v1", schema: "heimdall.auth_refresh_receipt.v1",
@ -326,22 +326,29 @@ async function completeAuth(
const now = new Date().toISOString(); const now = new Date().toISOString();
if (attempt.expiresAt <= now && attempt.status !== "completed") { if (attempt.expiresAt <= now && attempt.status !== "completed") {
await store.updateAuthAttempt(appSlug, handle, { status: "expired", at: now }); 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 === "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({ const redemption = await app.inject({
method: "POST", method: "POST",
url: `/v1/apps/${appSlug}/auth-completions/redeem`, url: `/v1/apps/${appSlug}/auth-completions/redeem`,
payload: { completionCode: handle }, payload: { completionCode: handle },
}); });
if (redemption.statusCode !== 201) return authCompletion("denied", { error: "invalid_or_expired_completion" }); if (redemption.statusCode !== 201) {
return authCompletion("authenticated", redemption.json<Record<string, unknown>>()); return authCompletion("denied", { handle, error: "invalid_or_expired_completion" });
}
return authCompletion("authenticated", { handle, ...redemption.json<Record<string, unknown>>() });
} }
function authCompletion(status: string, values: Record<string, unknown>) { function authCompletion(status: string, values: Record<string, unknown>) {
return { 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", payloadSchema: "heimdall.auth_completion_receipt.v1",
payload: { ...values, schema: "heimdall.auth_completion_receipt.v1", status }, payload: { ...values, schema: "heimdall.auth_completion_receipt.v1", status },
}; };

View file

@ -111,6 +111,19 @@ describe("Heimdall Eve access plugin", () => {
const state = new URL(navigation.url).searchParams.get("state"); const state = new URL(navigation.url).searchParams.get("state");
expect(state).toBeTruthy(); 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({ const callback = await app.inject({
method: "GET", method: "GET",
url: `/v1/oauth/discord/callback?code=test-code&state=${encodeURIComponent(state ?? "")}`, 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"); expect(completionWire.contentSchema).toBe("heimdall.auth_completion_receipt.v1");
const completion = openPrivateEnvelope(completionWire, secret); const completion = openPrivateEnvelope(completionWire, secret);
expect(completion.status).toBe("authenticated"); expect(completion.status).toBe("authenticated");
expect(completion.handle).toBe(handle);
expect(completion.accessToken).toEqual(expect.any(String)); 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({ const refreshEnvelope = sealPrivateEnvelope({
appSlug: "ghostlight", appSlug: "ghostlight",
operation: "heimdall.auth.refresh", operation: "heimdall.auth.refresh",