Bind auth completions to their attempt handles
This commit is contained in:
parent
b9ab8c0022
commit
f2eec21c57
3 changed files with 41 additions and 7 deletions
|
|
@ -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"] },
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue