fix(auth): confirm passkey operations and preserve safe legacy migration
Fence displayed identity and native prompt generations, classify secret-free outcomes, recover atomic addition receipts without restoring old credentials, and expose truthful own-key verification metadata. Preserve required verifier and enrollment authority boundaries. Include real-store, production browser, native CTAP2 and visible responsive regressions. Validation: root check/fmt; API 115; passkey security 25; Web unit 712/component 605; auth/entry/activation/hosting browsers; product auth E2E 8 and two explicit browser suites. Server suite: 1262 lib+12 CLI, 3 ignored and 2 environment-specific exclusions. State-name checker retains 15 byte-identical baseline findings; no new exceptions. Physical keys/mobile and hosted CI remain unverified.
This commit is contained in:
@@ -41,6 +41,7 @@ Deno.serve({ hostname: "127.0.0.1", port: Number(port) }, async (request) => {
|
||||
if (path === "/api/workspaces") return new Response(null, { status: 503 });
|
||||
if (path === "/api/auth/whoami") {
|
||||
return Response.json({
|
||||
authentication_reference: mode !== "anonymous" ? "fixture-authentication" : null,
|
||||
session: mode !== "anonymous"
|
||||
? { kind: "authenticated", expires_at: "2099-01-01T00:00:00Z" }
|
||||
: { kind: "anonymous" },
|
||||
@@ -89,7 +90,7 @@ Deno.serve({ hostname: "127.0.0.1", port: Number(port) }, async (request) => {
|
||||
if (mode === "activation-unknown") {
|
||||
return new Response(null, { status: 503 });
|
||||
}
|
||||
return Response.json({ user });
|
||||
return Response.json({ user, authentication_reference: "fixture-authentication" });
|
||||
}
|
||||
if (path.startsWith("/api/")) return new Response(null, { status: 404 });
|
||||
const file = join(root, decodeURIComponent(path));
|
||||
|
||||
@@ -34,6 +34,11 @@ const register = async (page: Page) => {
|
||||
await page.getByRole("button", { name: "Register new account", exact: true })
|
||||
.click();
|
||||
};
|
||||
const additionConfirmed = (page: Page) =>
|
||||
page.getByRole("status").filter({
|
||||
hasText:
|
||||
/^Passkey added to your account; the expected session was confirmed\.$/,
|
||||
});
|
||||
const count = (mutations: Mutation[], path: string) =>
|
||||
mutations.filter((entry) => entry.path === path).length;
|
||||
const at = async (page: Page, path: string) => {
|
||||
@@ -151,7 +156,7 @@ Deno.test("standalone auth separates login, registration, own account and explic
|
||||
readyUrl: `${origin}/health`,
|
||||
}],
|
||||
`${root}/tools/web-ux/scenarios/anonymous-entry.json`,
|
||||
`${root}/target/web-ux/t767-validation/auth-interactions/processes`,
|
||||
`${root}/target/t783/web-shell-final/processes`,
|
||||
[],
|
||||
);
|
||||
let browser: Browser | undefined;
|
||||
@@ -229,9 +234,7 @@ Deno.test("standalone auth separates login, registration, own account and explic
|
||||
name: "Add passkey to alice",
|
||||
exact: true,
|
||||
}).click();
|
||||
await page.getByRole("status").filter({
|
||||
hasText: /added|registered/i,
|
||||
}).waitFor();
|
||||
await additionConfirmed(page).waitFor();
|
||||
const additions = mutations.filter((entry) =>
|
||||
entry.path.includes("/addition/")
|
||||
);
|
||||
@@ -239,7 +242,12 @@ Deno.test("standalone auth separates login, registration, own account and explic
|
||||
"/api/auth/passkeys/addition/options",
|
||||
"/api/auth/passkeys/addition/complete",
|
||||
]);
|
||||
assertEquals(additions[0].keys, ["browser_origin"]);
|
||||
assertEquals(additions[0].keys, [
|
||||
"browser_origin",
|
||||
"expected_account_id",
|
||||
"expected_authentication_reference",
|
||||
"expected_user_id",
|
||||
]);
|
||||
assertEquals(additions[1].keys, ["challenge_id", "credential"]);
|
||||
assertEquals(
|
||||
mutations.some((entry) => entry.path.includes("/registration/")),
|
||||
@@ -357,8 +365,7 @@ Deno.test("standalone auth separates login, registration, own account and explic
|
||||
name: "Add passkey to alice",
|
||||
exact: true,
|
||||
}).click();
|
||||
await page.getByRole("status").filter({ hasText: /added/i })
|
||||
.waitFor();
|
||||
await additionConfirmed(page).waitFor();
|
||||
assertEquals(
|
||||
count(mutations, "/api/auth/passkeys/addition/complete"),
|
||||
1,
|
||||
@@ -784,9 +791,7 @@ Deno.test("standalone auth separates login, registration, own account and explic
|
||||
.__authFixtureRelease()
|
||||
);
|
||||
if (purpose === "addition") {
|
||||
await page.getByRole("status").filter({
|
||||
hasText: /added|registered/i,
|
||||
}).waitFor();
|
||||
await additionConfirmed(page).waitFor();
|
||||
} else await at(page, "/dashboard");
|
||||
assertEquals(
|
||||
count(mutations, `/api/auth/passkeys/${purpose}/options`),
|
||||
@@ -813,8 +818,18 @@ Deno.test("standalone auth separates login, registration, own account and explic
|
||||
name: "Approve device login",
|
||||
exact: true,
|
||||
}).click();
|
||||
await page.getByRole("alert").filter({ hasText: /invalid|expired/i })
|
||||
.waitFor();
|
||||
await page.getByRole("alert").filter({
|
||||
hasText:
|
||||
/^Authentication request was rejected \(400\)\. Check your session or restart the operation\.$/,
|
||||
}).waitFor();
|
||||
assertEquals(new URL(page.url()).pathname, "/login/device");
|
||||
assertEquals(
|
||||
await page.getByText(
|
||||
"Device login approved. You can return to the CLI.",
|
||||
{ exact: true },
|
||||
).count(),
|
||||
0,
|
||||
);
|
||||
assertEquals(count(mutations, "/api/auth/device-login/approve"), 1);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -549,7 +549,7 @@ Deno.test("public entry isolates Workspace authority and authenticated dashboard
|
||||
},
|
||||
);
|
||||
await step(
|
||||
"SPA page access checks once without hiding the admitted page",
|
||||
"SPA page access checks once and fences key-list reads without hiding the admitted page",
|
||||
async (page) => {
|
||||
let checks = 0;
|
||||
page.on("request", (request) => {
|
||||
@@ -577,6 +577,7 @@ Deno.test("public entry isolates Workspace authority and authenticated dashboard
|
||||
await page.getByRole("link", { name: "Account", exact: true })
|
||||
.click();
|
||||
await requested;
|
||||
assertEquals(checks, 2);
|
||||
assertEquals(
|
||||
await page.getByRole("heading", {
|
||||
name: "Dashboard",
|
||||
@@ -594,7 +595,10 @@ Deno.test("public entry isolates Workspace authority and authenticated dashboard
|
||||
exact: true,
|
||||
}).waitFor();
|
||||
assertEquals(new URL(page.url()).pathname, "/account");
|
||||
assertEquals(checks, 2);
|
||||
await page.getByRole("list", { name: "Registered keys" })
|
||||
.waitFor();
|
||||
// page入場の一回に加え、本人鍵一覧は読取りの前後で本人bindingを確認する。
|
||||
assertEquals(checks, 4);
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
|
||||
@@ -158,9 +158,17 @@ Deno.serve({ hostname: "127.0.0.1", port: Number(port) }, async (request) => {
|
||||
return new Response(null, { status: 503 });
|
||||
}
|
||||
if (mode === "session-malformed") {
|
||||
return Response.json({ actor: { handle: "untrusted" } });
|
||||
return Response.json({
|
||||
authentication_reference: state.signedIn
|
||||
? "fixture-authentication"
|
||||
: null,
|
||||
actor: { handle: "untrusted" },
|
||||
});
|
||||
}
|
||||
return Response.json({
|
||||
authentication_reference: state.signedIn
|
||||
? "fixture-authentication"
|
||||
: null,
|
||||
actor: state.signedIn
|
||||
? { ...user, auth_method: "browser_session" }
|
||||
: null,
|
||||
@@ -168,11 +176,25 @@ Deno.serve({ hostname: "127.0.0.1", port: Number(port) }, async (request) => {
|
||||
? {
|
||||
kind: "authenticated",
|
||||
expires_at: state.expiresAt ??
|
||||
new Date(Date.now() + 86_400_000).toISOString(),
|
||||
"2099-01-01T00:00:00Z",
|
||||
}
|
||||
: { kind: state.expired ? "expired" : "anonymous" },
|
||||
});
|
||||
}
|
||||
if (path === "/api/auth/passkeys") {
|
||||
return Response.json({
|
||||
user,
|
||||
keys: [{
|
||||
key_reference: "synthetic-safe-key-reference",
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
last_used_at: null,
|
||||
normal_login_verified_at: null,
|
||||
}],
|
||||
});
|
||||
}
|
||||
if (path === "/api/auth/passkeys/addition/result") {
|
||||
return Response.json({ status: "unknown" });
|
||||
}
|
||||
if (path === "/api/auth/registration-policy") {
|
||||
if (mode === "policy-loading") return await new Promise<Response>(() => {});
|
||||
if (mode === "policy-error") return new Response(null, { status: 503 });
|
||||
@@ -227,7 +249,11 @@ Deno.serve({ hostname: "127.0.0.1", port: Number(port) }, async (request) => {
|
||||
}
|
||||
if (path === "/api/auth/device-login/approve") {
|
||||
if (!state.signedIn) return new Response(null, { status: 401 });
|
||||
if (mode === "device-expired") return new Response(null, { status: 410 });
|
||||
if (mode === "device-expired") {
|
||||
return Response.json({
|
||||
message: "expired_device_login: device login code expired",
|
||||
}, { status: 400 });
|
||||
}
|
||||
return Response.json({ status: "approved", user });
|
||||
}
|
||||
const passkey = path.match(
|
||||
@@ -263,7 +289,10 @@ Deno.serve({ hostname: "127.0.0.1", port: Number(port) }, async (request) => {
|
||||
state.expired = false;
|
||||
state.cookieSession = false;
|
||||
state.expiresAt = null;
|
||||
return Response.json({ user });
|
||||
return Response.json({
|
||||
user,
|
||||
authentication_reference: "fixture-authentication",
|
||||
});
|
||||
}
|
||||
if (path === "/api/auth/activation/options") {
|
||||
return Response.json(options(false));
|
||||
@@ -273,7 +302,10 @@ Deno.serve({ hostname: "127.0.0.1", port: Number(port) }, async (request) => {
|
||||
state.expired = false;
|
||||
state.cookieSession = false;
|
||||
state.expiresAt = null;
|
||||
return Response.json({ user });
|
||||
return Response.json({
|
||||
user,
|
||||
authentication_reference: "fixture-authentication",
|
||||
});
|
||||
}
|
||||
// Workspace画面の既存fixtureを再利用する。認証/登録/作成APIの代わりにはしない。
|
||||
if (workspaceFixture && path.startsWith("/api/w/")) {
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
/// <reference lib="dom" />
|
||||
// 本番shellと可視状態の試験。HTTP/認証器を合成fixtureへ置換するため、暗号/保存の成功根拠ではない。
|
||||
// native CTAP2 + 実Backend経路はtests/e2eの別suiteが所有する。
|
||||
import { assert, assertEquals } from "@std/assert";
|
||||
import { resolve } from "@std/path";
|
||||
import { type Browser, chromium } from "playwright";
|
||||
import {
|
||||
startOwnedProcesses,
|
||||
stopOwnedProcesses,
|
||||
} from "../../../../tools/web-ux/src/processes.ts";
|
||||
|
||||
Deno.test("account addition keeps pending and recovery visible at desktop and mobile widths", async (t) => {
|
||||
const root = resolve(import.meta.dirname!, "../../../..");
|
||||
const reservation = Deno.listen({ hostname: "127.0.0.1", port: 0 });
|
||||
const port = (reservation.addr as Deno.NetAddr).port;
|
||||
reservation.close();
|
||||
const origin = `http://127.0.0.1:${port}`;
|
||||
const out = `${root}/target/web-ux/t783-after`;
|
||||
const processes = await startOwnedProcesses(
|
||||
[{
|
||||
id: "passkey-state",
|
||||
command: "deno",
|
||||
cwd: root,
|
||||
args: [
|
||||
"run",
|
||||
"--config",
|
||||
"tools/web-ux/deno.json",
|
||||
"--allow-read",
|
||||
"--allow-net",
|
||||
"web/workspace/test/auth-browser/fixture-server.ts",
|
||||
String(port),
|
||||
"web/workspace/build",
|
||||
],
|
||||
readyUrl: `${origin}/health`,
|
||||
}],
|
||||
`${root}/tools/web-ux/scenarios/anonymous-entry.json`,
|
||||
`${out}/processes`,
|
||||
[],
|
||||
);
|
||||
let browser: Browser | undefined;
|
||||
const records: unknown[] = [];
|
||||
try {
|
||||
browser = await chromium.launch();
|
||||
for (const theme of ["light", "dark"] as const) {
|
||||
for (const width of [1440, 768, 390]) {
|
||||
const passed = await t.step(
|
||||
`${theme}/${width}: visible pending, stop and migration metadata`,
|
||||
async () => {
|
||||
const context = await browser!.newContext({
|
||||
viewport: { width, height: 900 },
|
||||
colorScheme: theme,
|
||||
reducedMotion: "reduce",
|
||||
});
|
||||
try {
|
||||
const page = await context.newPage();
|
||||
page.setDefaultTimeout(5000);
|
||||
const pageErrors: string[] = [];
|
||||
const statuses: number[] = [];
|
||||
const consoleErrors: string[] = [];
|
||||
const requestErrors: string[] = [];
|
||||
page.on(
|
||||
"requestfailed",
|
||||
() => requestErrors.push("request-failed"),
|
||||
);
|
||||
page.on("pageerror", () => pageErrors.push("page-error"));
|
||||
page.on("console", (msg) => {
|
||||
if (msg.type() === "error") {
|
||||
consoleErrors.push("console-error");
|
||||
}
|
||||
});
|
||||
page.on("response", (response) => {
|
||||
if (response.status() >= 400) statuses.push(response.status());
|
||||
});
|
||||
await context.route(
|
||||
"**/*",
|
||||
(route) =>
|
||||
new URL(route.request().url()).origin === origin
|
||||
? route.continue()
|
||||
: route.abort(),
|
||||
);
|
||||
await page.request.get(`${origin}/?fixture=addition-pending`);
|
||||
const documentResponse = await page.goto(`${origin}/account`);
|
||||
const add = page.getByRole("button", {
|
||||
name: "Add passkey to alice",
|
||||
exact: true,
|
||||
});
|
||||
await add.waitFor();
|
||||
await page.getByRole("list", { name: "Registered keys" })
|
||||
.waitFor();
|
||||
await page.screenshot({
|
||||
path: `${out}/account-${theme}-${width}.png`,
|
||||
fullPage: true,
|
||||
});
|
||||
await add.click();
|
||||
const pending = page.getByRole("status").filter({
|
||||
hasText: "Waiting for passkey addition",
|
||||
});
|
||||
await pending.waitFor({ state: "visible" });
|
||||
const box = await pending.boundingBox();
|
||||
assert(
|
||||
box && box.y >= 0 && box.y + box.height <= 900,
|
||||
"pending must be in the initial visible viewport",
|
||||
);
|
||||
assertEquals(await add.isDisabled(), true);
|
||||
assertEquals(
|
||||
await page.getByText("Loading session…", { exact: true })
|
||||
.isVisible(),
|
||||
false,
|
||||
);
|
||||
await page.screenshot({
|
||||
path: `${out}/pending-${theme}-${width}.png`,
|
||||
fullPage: true,
|
||||
});
|
||||
const pendingAccessibility = await page.locator("main")
|
||||
.ariaSnapshot();
|
||||
await page.getByRole("button", {
|
||||
name: "Stop waiting",
|
||||
exact: true,
|
||||
}).click();
|
||||
await page.evaluate(() =>
|
||||
(window as unknown as { __authFixtureRelease: () => void })
|
||||
.__authFixtureRelease()
|
||||
);
|
||||
await page.getByRole("alert").filter({
|
||||
hasText: "Completion was not sent",
|
||||
}).waitFor();
|
||||
await page.getByRole("button", {
|
||||
name: "Refresh registered keys",
|
||||
exact: true,
|
||||
}).click();
|
||||
await page.getByText(
|
||||
"Not yet verified — storage alone does not prove support",
|
||||
{ exact: true },
|
||||
).waitFor();
|
||||
await page.getByText(
|
||||
"Verify migration before retiring older-key login",
|
||||
{ exact: true },
|
||||
).click();
|
||||
await page.screenshot({
|
||||
path: `${out}/migration-${theme}-${width}.png`,
|
||||
fullPage: true,
|
||||
});
|
||||
assertEquals(
|
||||
await page.evaluate(() =>
|
||||
document.documentElement.scrollWidth > innerWidth
|
||||
),
|
||||
false,
|
||||
);
|
||||
assertEquals(documentResponse?.status(), 200);
|
||||
assertEquals(pageErrors, []);
|
||||
assertEquals(consoleErrors, []);
|
||||
assertEquals(statuses, []);
|
||||
assertEquals(requestErrors, []);
|
||||
const images = [];
|
||||
for (
|
||||
const name of [
|
||||
`account-${theme}-${width}.png`,
|
||||
`pending-${theme}-${width}.png`,
|
||||
`migration-${theme}-${width}.png`,
|
||||
]
|
||||
) {
|
||||
const digest = await crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
await Deno.readFile(`${out}/${name}`),
|
||||
);
|
||||
images.push({
|
||||
name,
|
||||
sha256: Array.from(
|
||||
new Uint8Array(digest),
|
||||
(byte) => byte.toString(16).padStart(2, "0"),
|
||||
).join(""),
|
||||
});
|
||||
}
|
||||
records.push({
|
||||
route: "/account",
|
||||
actor: "synthetic Alice",
|
||||
theme,
|
||||
width,
|
||||
documentStatus: documentResponse?.status(),
|
||||
pageErrors,
|
||||
consoleErrors,
|
||||
requestErrors,
|
||||
statuses,
|
||||
pendingAccessibility,
|
||||
visible: await page.locator("main").innerText(),
|
||||
images,
|
||||
});
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
if (!passed) {
|
||||
throw new Error(
|
||||
"Stop on the first failed visibility condition; investigate before repeating the matrix",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await browser?.close();
|
||||
await stopOwnedProcesses(processes);
|
||||
await Deno.writeTextFile(
|
||||
`${out}/review-context.json`,
|
||||
JSON.stringify(records, null, 2),
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -431,7 +431,7 @@ export async function sessionNavigation(
|
||||
},
|
||||
);
|
||||
await step(
|
||||
"challenge 401 leaves an authenticated account in place and permits retry",
|
||||
"challenge 401 leaves an authenticated account in place but requires result recovery before another addition",
|
||||
async ({ page, go }) => {
|
||||
const target = "/account?tab=keys#challenge";
|
||||
await go(target, "addition-expired");
|
||||
@@ -451,7 +451,21 @@ export async function sessionNavigation(
|
||||
name: "Add passkey to alice",
|
||||
exact: true,
|
||||
}).isEnabled(),
|
||||
true,
|
||||
false,
|
||||
);
|
||||
await page.getByRole("button", {
|
||||
name: "Check addition result",
|
||||
exact: true,
|
||||
}).click();
|
||||
await page.getByRole("status").filter({
|
||||
hasText: "addition result is still unknown",
|
||||
}).waitFor();
|
||||
assertEquals(
|
||||
await page.getByRole("button", {
|
||||
name: "Add passkey to alice",
|
||||
exact: true,
|
||||
}).isEnabled(),
|
||||
false,
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -464,7 +478,11 @@ export async function sessionNavigation(
|
||||
await at(page, "/");
|
||||
const wire = await (await page.request.get(`${origin}/api/auth/whoami`))
|
||||
.json();
|
||||
assertEquals(wire, { actor: null, session: { kind: "anonymous" } });
|
||||
assertEquals(wire, {
|
||||
actor: null,
|
||||
authentication_reference: null,
|
||||
session: { kind: "anonymous" },
|
||||
});
|
||||
await page.evaluate(() => {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
|
||||
@@ -19,7 +19,7 @@ const mime: Record<string, string> = {
|
||||
};
|
||||
const policies = new Map<string, boolean>();
|
||||
// 各fixture processはscenarioを逐次実行する。setupのBearer-only要求はcookieを
|
||||
// 送らないので、秘密やheadersを読まず直前の合成page条件で応答を選ぶ。
|
||||
// 送らないので、Bearer有無と直前の合成page条件で応答を選ぶ。header値は読まない。
|
||||
let setupMode: string | null = null;
|
||||
Deno.serve({ hostname: "127.0.0.1", port: Number(port) }, async (request) => {
|
||||
const url = new URL(request.url);
|
||||
@@ -28,19 +28,27 @@ Deno.serve({ hostname: "127.0.0.1", port: Number(port) }, async (request) => {
|
||||
if (url.pathname === "/hosting/setup" && selected?.startsWith("setup-")) {
|
||||
setupMode = selected;
|
||||
}
|
||||
const tokenRequest = request.headers.has("authorization");
|
||||
const mode = selected ??
|
||||
(tokenRequest ? setupMode : null) ??
|
||||
request.headers.get("cookie")?.match(/ui-fixture=([\w-]+)/)?.[1] ??
|
||||
(url.pathname.startsWith("/api/auth/passkeys/addition/")
|
||||
? setupMode
|
||||
: null) ??
|
||||
"admin-on";
|
||||
if (selected) policies.delete(mode);
|
||||
const setup = setupProjectionResponse(url.pathname, mode, selected !== null);
|
||||
const setup = setupProjectionResponse(
|
||||
url.pathname,
|
||||
mode,
|
||||
selected !== null,
|
||||
tokenRequest,
|
||||
);
|
||||
if (setup) return setup;
|
||||
const anonymous = mode.startsWith("anonymous") || mode.startsWith("account-");
|
||||
const ordinary = mode.startsWith("ordinary");
|
||||
if (url.pathname === "/api/auth/whoami") {
|
||||
return Response.json({
|
||||
authentication_reference: anonymous ? null : "fixture-authentication",
|
||||
session: !anonymous
|
||||
? { kind: "authenticated", expires_at: "2099-01-01T00:00:00Z" }
|
||||
: { kind: "anonymous" },
|
||||
@@ -53,6 +61,17 @@ Deno.serve({ hostname: "127.0.0.1", port: Number(port) }, async (request) => {
|
||||
},
|
||||
});
|
||||
}
|
||||
if (url.pathname === "/api/auth/passkeys") {
|
||||
return Response.json({
|
||||
user: {
|
||||
user_id: "fixture-user",
|
||||
account_id: "fixture-account",
|
||||
handle: "alice",
|
||||
display_name: "Alice",
|
||||
},
|
||||
keys: [],
|
||||
});
|
||||
}
|
||||
if (url.pathname === "/api/workspaces") return Response.json([]);
|
||||
if (url.pathname === "/api/hosting/access") {
|
||||
return Response.json({ is_admin: !ordinary && !anonymous });
|
||||
|
||||
@@ -19,6 +19,7 @@ export function setupProjectionResponse(
|
||||
path: string,
|
||||
mode: string,
|
||||
reset: boolean,
|
||||
tokenRequest = false,
|
||||
): Response | null {
|
||||
if (!mode.startsWith("setup-")) return null;
|
||||
if (reset) stages.delete(mode);
|
||||
@@ -28,7 +29,17 @@ export function setupProjectionResponse(
|
||||
});
|
||||
}
|
||||
if (path === "/api/auth/whoami") {
|
||||
if (tokenRequest) {
|
||||
return Response.json({
|
||||
actor: { ...user, auth_method: "api_token" },
|
||||
authentication_reference: "fixture-token-binding",
|
||||
session: { kind: "authenticated", expires_at: "2099-01-01T00:00:00Z" },
|
||||
});
|
||||
}
|
||||
return Response.json({
|
||||
authentication_reference: stages.get(mode) === "login"
|
||||
? "fixture-authentication"
|
||||
: null,
|
||||
session: stages.get(mode) === "login"
|
||||
? { kind: "authenticated", expires_at: "2099-01-01T00:00:00Z" }
|
||||
: { kind: "anonymous" },
|
||||
@@ -56,7 +67,10 @@ export function setupProjectionResponse(
|
||||
}
|
||||
if (path === "/api/auth/passkeys/addition/complete") {
|
||||
stages.set(mode, "enrolled");
|
||||
return Response.json({ user });
|
||||
return Response.json({
|
||||
user,
|
||||
authentication_reference: "fixture-authentication",
|
||||
});
|
||||
}
|
||||
if (path === "/api/auth/passkeys/login/options") {
|
||||
return Response.json({
|
||||
@@ -66,7 +80,10 @@ export function setupProjectionResponse(
|
||||
}
|
||||
if (path === "/api/auth/passkeys/login/complete") {
|
||||
stages.set(mode, "login");
|
||||
return Response.json({ user });
|
||||
return Response.json({
|
||||
user,
|
||||
authentication_reference: "fixture-authentication",
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ Deno.test("authentication and hosting shells never require Workspace catalog aut
|
||||
display_name: "Alice",
|
||||
auth_method: "browser_session",
|
||||
},
|
||||
authentication_reference: "fixture-authentication",
|
||||
session: { kind: "authenticated", expires_at: "2099-01-01T00:00:00Z" },
|
||||
});
|
||||
browserSession.invalidate();
|
||||
@@ -68,7 +69,11 @@ Deno.test("root admission never starts private catalog while anonymous, expired,
|
||||
if (condition === "loading") await pending;
|
||||
return condition === "error" || condition === "loading"
|
||||
? new Response(null, { status: 503 })
|
||||
: Response.json({ actor: null, session: { kind: condition } });
|
||||
: Response.json({
|
||||
actor: null,
|
||||
authentication_reference: null,
|
||||
session: { kind: condition },
|
||||
});
|
||||
};
|
||||
let privateCalls = 0;
|
||||
const event = {
|
||||
|
||||
Reference in New Issue
Block a user