Age assurance override env var

This commit is contained in:
lewis
2025-12-31 17:01:20 +02:00
parent 3bf974a705
commit a46d2d6f8d
20 changed files with 353 additions and 48 deletions
+39 -20
View File
@@ -131,10 +131,12 @@ export class AtprotoClient {
error: "Unknown",
message: res.statusText,
}));
const error = new Error(err.message || err.error || res.statusText) as Error & {
status: number;
error: string;
};
const error = new Error(err.message || err.error || res.statusText) as
& Error
& {
status: number;
error: string;
};
error.status = res.status;
error.error = err.error;
throw error;
@@ -272,10 +274,12 @@ export class AtprotoClient {
error: "Unknown",
message: res.statusText,
}));
const error = new Error(err.message || err.error || res.statusText) as Error & {
status: number;
error: string;
};
const error = new Error(err.message || err.error || res.statusText) as
& Error
& {
status: number;
error: string;
};
error.status = res.status;
error.error = err.error;
throw error;
@@ -369,9 +373,13 @@ export class AtprotoClient {
}
async deactivateAccount(migratingTo?: string): Promise<void> {
apiLog("POST", `${this.baseUrl}/xrpc/com.atproto.server.deactivateAccount`, {
migratingTo,
});
apiLog(
"POST",
`${this.baseUrl}/xrpc/com.atproto.server.deactivateAccount`,
{
migratingTo,
},
);
const start = Date.now();
try {
const body: { migratingTo?: string } = {};
@@ -503,10 +511,12 @@ export class AtprotoClient {
error: "Unknown",
message: res.statusText,
}));
const error = new Error(err.message || err.error || res.statusText) as Error & {
status: number;
error: string;
};
const error = new Error(err.message || err.error || res.statusText) as
& Error
& {
status: number;
error: string;
};
error.status = res.status;
error.error = err.error;
throw error;
@@ -549,7 +559,8 @@ export async function getOAuthServerMetadata(
return directRes.json();
}
const protectedResourceUrl = `${pdsUrl}/.well-known/oauth-protected-resource`;
const protectedResourceUrl =
`${pdsUrl}/.well-known/oauth-protected-resource`;
const protectedRes = await fetch(protectedResourceUrl);
if (!protectedRes.ok) {
return null;
@@ -561,7 +572,9 @@ export async function getOAuthServerMetadata(
return null;
}
const authServerUrl = `${authServers[0]}/.well-known/oauth-authorization-server`;
const authServerUrl = `${
authServers[0]
}/.well-known/oauth-authorization-server`;
const authServerRes = await fetch(authServerUrl);
if (!authServerRes.ok) {
return null;
@@ -595,7 +608,10 @@ export function base64UrlEncode(buffer: Uint8Array | ArrayBuffer): string {
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(
/=+$/,
"",
);
}
export function base64UrlDecode(base64url: string): Uint8Array {
@@ -730,14 +746,17 @@ export async function exchangeOAuthCode(
error_description: res.statusText,
}));
throw new Error(
retryErr.error_description || retryErr.error || "Token exchange failed",
retryErr.error_description || retryErr.error ||
"Token exchange failed",
);
}
return res.json();
}
}
throw new Error(err.error_description || err.error || "Token exchange failed");
throw new Error(
err.error_description || err.error || "Token exchange failed",
);
}
return res.json();
+19 -8
View File
@@ -2,7 +2,6 @@ import type {
InboundMigrationState,
InboundStep,
MigrationProgress,
OAuthServerMetadata,
OutboundMigrationState,
OutboundStep,
PasskeyAccountSetup,
@@ -86,7 +85,6 @@ export function createInboundMigrationFlow() {
let sourceClient: AtprotoClient | null = null;
let localClient: AtprotoClient | null = null;
let localServerInfo: ServerDescription | null = null;
let sourceOAuthMetadata: OAuthServerMetadata | null = null;
function setStep(step: InboundStep) {
state.step = step;
@@ -271,10 +269,14 @@ export function createInboundMigrationFlow() {
if (state.authMethod === "passkey" && state.passkeySetupToken) {
localClient = createLocalClient();
setStep("passkey-setup");
migrationLog("handleOAuthCallback: Resuming passkey flow at passkey-setup");
migrationLog(
"handleOAuthCallback: Resuming passkey flow at passkey-setup",
);
} else {
setStep("email-verify");
migrationLog("handleOAuthCallback: Resuming at email-verify for re-auth");
migrationLog(
"handleOAuthCallback: Resuming at email-verify for re-auth",
);
}
} else {
setStep(targetStep);
@@ -337,7 +339,9 @@ export function createInboundMigrationFlow() {
serverDid: serverInfo.did,
});
migrationLog("startMigration: Getting service auth token from source PDS");
migrationLog(
"startMigration: Getting service auth token from source PDS",
);
const { token } = await sourceClient.getServiceAuth(
serverInfo.did,
"com.atproto.server.createAccount",
@@ -361,7 +365,10 @@ export function createInboundMigrationFlow() {
inviteCode: passkeyParams.inviteCode,
stateInviteCode: state.inviteCode,
});
passkeySetup = await localClient.createPasskeyAccount(passkeyParams, token);
passkeySetup = await localClient.createPasskeyAccount(
passkeyParams,
token,
);
migrationLog("startMigration: Passkey account created on NEW PDS", {
did: passkeySetup.did,
hasAccessJwt: !!passkeySetup.accessJwt,
@@ -743,7 +750,9 @@ export function createInboundMigrationFlow() {
migrationLog("Activating account on NEW PDS");
const activateStart = Date.now();
await localClient.activateAccount();
migrationLog("Account activated", { durationMs: Date.now() - activateStart });
migrationLog("Account activated", {
durationMs: Date.now() - activateStart,
});
setProgress({ activated: true });
setProgress({ currentOperation: "Deactivating old account..." });
@@ -757,7 +766,9 @@ export function createInboundMigrationFlow() {
setProgress({ deactivated: true });
} catch (deactivateErr) {
const err = deactivateErr as Error & { error?: string };
migrationLog("Could not deactivate on source PDS", { error: err.message });
migrationLog("Could not deactivate on source PDS", {
error: err.message,
});
}
migrationLog("completeDidWebMigration SUCCESS");
+3 -1
View File
@@ -352,7 +352,9 @@ label.auth-option {
border-radius: var(--radius-lg);
cursor: pointer;
margin-bottom: 0;
transition: border-color var(--transition-normal), background-color var(--transition-normal);
transition:
border-color var(--transition-normal),
background-color var(--transition-normal);
}
.auth-option:hover {
+2 -1
View File
@@ -77,7 +77,8 @@ describe("Dashboard", () => {
setupAuthenticatedUser({ isAdmin: true });
mockEndpoint(
"com.atproto.server.describeServer",
() => jsonResponse(mockData.describeServer({ inviteCodeRequired: true })),
() =>
jsonResponse(mockData.describeServer({ inviteCodeRequired: true })),
);
render(Dashboard);
await waitFor(() => {
+9 -5
View File
@@ -1,4 +1,4 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it } from "vitest";
import { fireEvent, render, screen, waitFor } from "@testing-library/svelte";
import Login from "../routes/Login.svelte";
import {
@@ -15,8 +15,9 @@ describe("Login", () => {
clearMocks();
setupFetchMock();
globalThis.location.hash = "";
mockEndpoint("/oauth/par", () =>
jsonResponse({ request_uri: "urn:mock:request" })
mockEndpoint(
"/oauth/par",
() => jsonResponse({ request_uri: "urn:mock:request" }),
);
});
@@ -85,8 +86,11 @@ describe("Login", () => {
error: null,
savedAccounts,
});
mockEndpoint("com.atproto.server.getSession", () =>
jsonResponse(mockData.session({ handle: "alice.test.tranquil.dev" })));
mockEndpoint(
"com.atproto.server.getSession",
() =>
jsonResponse(mockData.session({ handle: "alice.test.tranquil.dev" })),
);
});
it("displays saved accounts list", async () => {
+19 -9
View File
@@ -110,8 +110,10 @@ describe("Settings", () => {
capturedBody = JSON.parse((options?.body as string) || "{}");
return jsonResponse({});
});
mockEndpoint("com.atproto.server.getSession", () =>
jsonResponse(mockData.session()));
mockEndpoint(
"com.atproto.server.getSession",
() => jsonResponse(mockData.session()),
);
render(Settings);
await waitFor(() => {
expect(screen.getByRole("button", { name: /change email/i }))
@@ -144,8 +146,10 @@ describe("Settings", () => {
() => jsonResponse({ tokenRequired: true }),
);
mockEndpoint("com.atproto.server.updateEmail", () => jsonResponse({}));
mockEndpoint("com.atproto.server.getSession", () =>
jsonResponse(mockData.session()));
mockEndpoint(
"com.atproto.server.getSession",
() => jsonResponse(mockData.session()),
);
render(Settings);
await waitFor(() => {
expect(screen.getByRole("button", { name: /change email/i }))
@@ -188,7 +192,9 @@ describe("Settings", () => {
expect(screen.getByRole("button", { name: /cancel/i }))
.toBeInTheDocument();
});
const emailSection = screen.getByRole("heading", { name: /change email/i })
const emailSection = screen.getByRole("heading", {
name: /change email/i,
})
.closest("section");
const cancelButton = emailSection?.querySelector("button.secondary");
if (cancelButton) {
@@ -220,8 +226,10 @@ describe("Settings", () => {
describe("handle change", () => {
beforeEach(() => {
setupAuthenticatedUser();
mockEndpoint("com.atproto.server.describeServer", () =>
jsonResponse(mockData.describeServer()));
mockEndpoint(
"com.atproto.server.describeServer",
() => jsonResponse(mockData.describeServer()),
);
});
it("displays current handle", async () => {
render(Settings);
@@ -255,8 +263,10 @@ describe("Settings", () => {
});
it("shows success message after handle change", async () => {
mockEndpoint("com.atproto.identity.updateHandle", () => jsonResponse({}));
mockEndpoint("com.atproto.server.getSession", () =>
jsonResponse(mockData.session()));
mockEndpoint(
"com.atproto.server.getSession",
() => jsonResponse(mockData.session()),
);
render(Settings);
await waitFor(() => {
expect(screen.getByLabelText(/new handle/i)).toBeInTheDocument();
@@ -1,4 +1,4 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it } from "vitest";
import {
base64UrlDecode,
base64UrlEncode,
@@ -351,7 +351,13 @@ describe("migration/atproto-client", () => {
it("returns null and clears storage for expired key (> 24 hours)", async () => {
const stored = {
privateJwk: { kty: "EC", crv: "P-256", x: "test", y: "test", d: "test" },
privateJwk: {
kty: "EC",
crv: "P-256",
x: "test",
y: "test",
d: "test",
},
publicJwk: { kty: "EC", crv: "P-256", x: "test", y: "test" },
thumbprint: "test-thumb",
createdAt: Date.now() - 25 * 60 * 60 * 1000,
+1 -1
View File
@@ -1,4 +1,4 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it } from "vitest";
import {
clearMigrationState,
getResumeInfo,
+3 -1
View File
@@ -63,7 +63,9 @@ describe("migration/types", () => {
});
it("can check if error is MigrationError", () => {
const error = new MigrationError("Test", "ERR_TEST", true, { foo: "bar" });
const error = new MigrationError("Test", "ERR_TEST", true, {
foo: "bar",
});
if (error instanceof MigrationError) {
expect(error.code).toBe("ERR_TEST");