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",
"$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"] },

View file

@ -234,7 +234,7 @@ async function refreshAuth(
const refreshedPayload = refreshed.json<Record<string, unknown>>();
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<Record<string, unknown>>());
if (redemption.statusCode !== 201) {
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>) {
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 },
};

View file

@ -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",