From a46d2d6f8d25592b2eb83b4e45dc90771fda6e01 Mon Sep 17 00:00:00 2001 From: lewis Date: Wed, 31 Dec 2025 17:01:20 +0200 Subject: [PATCH] Age assurance override env var --- .env.example | 9 ++ ...0150271613b6779178816d9acfb244b48066c.json | 16 +++ ...e17c93d702ff1a8be8018f3f21e8fd3d550a8.json | 16 +++ ...a66ce90ae92d28b41886c5bb9b81e4b53eaa2.json | 22 ++++ frontend/src/lib/migration/atproto-client.ts | 59 ++++++--- frontend/src/lib/migration/flow.svelte.ts | 27 ++-- frontend/src/styles/migration.css | 4 +- frontend/src/tests/Dashboard.test.ts | 3 +- frontend/src/tests/Login.test.ts | 14 ++- frontend/src/tests/Settings.test.ts | 28 +++-- .../tests/migration/atproto-client.test.ts | 10 +- frontend/src/tests/migration/storage.test.ts | 2 +- frontend/src/tests/migration/types.test.ts | 4 +- src/api/age_assurance.rs | 119 ++++++++++++++++++ src/api/identity/account.rs | 18 +++ src/api/mod.rs | 1 + src/api/repo/import.rs | 21 ++++ src/api/server/passkey_account.rs | 19 +++ src/delegation/audit.rs | 1 + src/lib.rs | 8 ++ 20 files changed, 353 insertions(+), 48 deletions(-) create mode 100644 .sqlx/query-6efda9a01aff3277386c617e8500150271613b6779178816d9acfb244b48066c.json create mode 100644 .sqlx/query-839b7593dd13cfc4cd303a626c7e17c93d702ff1a8be8018f3f21e8fd3d550a8.json create mode 100644 .sqlx/query-b2294557cfcc57a9fa2ed90602ea66ce90ae92d28b41886c5bb9b81e4b53eaa2.json create mode 100644 src/api/age_assurance.rs diff --git a/.env.example b/.env.example index a2e05ad..c158a89 100644 --- a/.env.example +++ b/.env.example @@ -139,6 +139,15 @@ AWS_SECRET_ACCESS_KEY=minioadmin # REPORT_SERVICE_URL=https://mod.bsky.app # REPORT_SERVICE_DID=did:plc:ar7c4by46qjdydhdevvrndac # ============================================================================= +# Age Assurance Override +# ============================================================================= +# Enable this if you have separately assured the ages of your users +# (e.g., through your own age verification process). When enabled, the PDS +# will return "assured" status for age assurance checks instead of proxying +# to the appview. This helps migrated users avoid the age assurance +# catch-22 on bsky.app. +# PDS_AGE_ASSURANCE_OVERRIDE=1 +# ============================================================================= # Miscellaneous # ============================================================================= # Allow HTTP for proxy requests (development only) diff --git a/.sqlx/query-6efda9a01aff3277386c617e8500150271613b6779178816d9acfb244b48066c.json b/.sqlx/query-6efda9a01aff3277386c617e8500150271613b6779178816d9acfb244b48066c.json new file mode 100644 index 0000000..bb48782 --- /dev/null +++ b/.sqlx/query-6efda9a01aff3277386c617e8500150271613b6779178816d9acfb244b48066c.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)\n ON CONFLICT (user_id, name) DO NOTHING", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Jsonb" + ] + }, + "nullable": [] + }, + "hash": "6efda9a01aff3277386c617e8500150271613b6779178816d9acfb244b48066c" +} diff --git a/.sqlx/query-839b7593dd13cfc4cd303a626c7e17c93d702ff1a8be8018f3f21e8fd3d550a8.json b/.sqlx/query-839b7593dd13cfc4cd303a626c7e17c93d702ff1a8be8018f3f21e8fd3d550a8.json new file mode 100644 index 0000000..22f842f --- /dev/null +++ b/.sqlx/query-839b7593dd13cfc4cd303a626c7e17c93d702ff1a8be8018f3f21e8fd3d550a8.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)\n ON CONFLICT (user_id, name) DO NOTHING", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Jsonb" + ] + }, + "nullable": [] + }, + "hash": "839b7593dd13cfc4cd303a626c7e17c93d702ff1a8be8018f3f21e8fd3d550a8" +} diff --git a/.sqlx/query-b2294557cfcc57a9fa2ed90602ea66ce90ae92d28b41886c5bb9b81e4b53eaa2.json b/.sqlx/query-b2294557cfcc57a9fa2ed90602ea66ce90ae92d28b41886c5bb9b81e4b53eaa2.json new file mode 100644 index 0000000..4f41b08 --- /dev/null +++ b/.sqlx/query-b2294557cfcc57a9fa2ed90602ea66ce90ae92d28b41886c5bb9b81e4b53eaa2.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT created_at FROM users WHERE did = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "b2294557cfcc57a9fa2ed90602ea66ce90ae92d28b41886c5bb9b81e4b53eaa2" +} diff --git a/frontend/src/lib/migration/atproto-client.ts b/frontend/src/lib/migration/atproto-client.ts index 0a86812..87e5c63 100644 --- a/frontend/src/lib/migration/atproto-client.ts +++ b/frontend/src/lib/migration/atproto-client.ts @@ -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 { - 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(); diff --git a/frontend/src/lib/migration/flow.svelte.ts b/frontend/src/lib/migration/flow.svelte.ts index 3dff981..4919a9d 100644 --- a/frontend/src/lib/migration/flow.svelte.ts +++ b/frontend/src/lib/migration/flow.svelte.ts @@ -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"); diff --git a/frontend/src/styles/migration.css b/frontend/src/styles/migration.css index bb377ca..ebdeb9a 100644 --- a/frontend/src/styles/migration.css +++ b/frontend/src/styles/migration.css @@ -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 { diff --git a/frontend/src/tests/Dashboard.test.ts b/frontend/src/tests/Dashboard.test.ts index 60f7dad..54d1198 100644 --- a/frontend/src/tests/Dashboard.test.ts +++ b/frontend/src/tests/Dashboard.test.ts @@ -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(() => { diff --git a/frontend/src/tests/Login.test.ts b/frontend/src/tests/Login.test.ts index 8c55462..4e445f2 100644 --- a/frontend/src/tests/Login.test.ts +++ b/frontend/src/tests/Login.test.ts @@ -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 () => { diff --git a/frontend/src/tests/Settings.test.ts b/frontend/src/tests/Settings.test.ts index d0dd7e5..3e9b6ed 100644 --- a/frontend/src/tests/Settings.test.ts +++ b/frontend/src/tests/Settings.test.ts @@ -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(); diff --git a/frontend/src/tests/migration/atproto-client.test.ts b/frontend/src/tests/migration/atproto-client.test.ts index 35e29f1..b4130b6 100644 --- a/frontend/src/tests/migration/atproto-client.test.ts +++ b/frontend/src/tests/migration/atproto-client.test.ts @@ -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, diff --git a/frontend/src/tests/migration/storage.test.ts b/frontend/src/tests/migration/storage.test.ts index 4ca2124..7411809 100644 --- a/frontend/src/tests/migration/storage.test.ts +++ b/frontend/src/tests/migration/storage.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it } from "vitest"; import { clearMigrationState, getResumeInfo, diff --git a/frontend/src/tests/migration/types.test.ts b/frontend/src/tests/migration/types.test.ts index 7fdebdf..7a33697 100644 --- a/frontend/src/tests/migration/types.test.ts +++ b/frontend/src/tests/migration/types.test.ts @@ -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"); diff --git a/src/api/age_assurance.rs b/src/api/age_assurance.rs new file mode 100644 index 0000000..d09f26d --- /dev/null +++ b/src/api/age_assurance.rs @@ -0,0 +1,119 @@ +use crate::auth::{extract_bearer_token_from_header, validate_bearer_token}; +use crate::state::AppState; +use axum::{ + Json, + body::Bytes, + extract::{Path, RawQuery, State}, + http::{HeaderMap, Method, StatusCode}, + response::{IntoResponse, Response}, +}; +use serde_json::json; + +pub async fn get_state( + State(state): State, + headers: HeaderMap, + RawQuery(query): RawQuery, +) -> Response { + if std::env::var("PDS_AGE_ASSURANCE_OVERRIDE").is_err() { + return proxy_to_appview(state, headers, "app.bsky.ageassurance.getState", query).await; + } + + let created_at = get_account_created_at(&state, &headers).await; + let now = chrono::Utc::now().to_rfc3339(); + + ( + StatusCode::OK, + Json(json!({ + "state": { + "status": "assured", + "access": "full", + "lastInitiatedAt": now + }, + "metadata": { + "accountCreatedAt": created_at + } + })), + ) + .into_response() +} + +pub async fn get_age_assurance_state( + State(state): State, + headers: HeaderMap, + RawQuery(query): RawQuery, +) -> Response { + if std::env::var("PDS_AGE_ASSURANCE_OVERRIDE").is_err() { + return proxy_to_appview( + state, + headers, + "app.bsky.unspecced.getAgeAssuranceState", + query, + ) + .await; + } + + (StatusCode::OK, Json(json!({"status": "assured"}))).into_response() +} + +async fn get_account_created_at(state: &AppState, headers: &HeaderMap) -> Option { + let auth_header = headers.get("Authorization").and_then(|h| h.to_str().ok()); + tracing::debug!(?auth_header, "age assurance: extracting token"); + + let token = extract_bearer_token_from_header(auth_header)?; + tracing::debug!("age assurance: got token, validating"); + + let auth_user = match validate_bearer_token(&state.db, &token).await { + Ok(user) => { + tracing::debug!(did = %user.did, "age assurance: validated user"); + user + } + Err(e) => { + tracing::warn!(?e, "age assurance: token validation failed"); + return None; + } + }; + + let row = match sqlx::query!("SELECT created_at FROM users WHERE did = $1", auth_user.did) + .fetch_optional(&state.db) + .await + { + Ok(r) => { + tracing::debug!(?r, "age assurance: query result"); + r + } + Err(e) => { + tracing::warn!(?e, "age assurance: query failed"); + return None; + } + }; + + row.map(|r| r.created_at.to_rfc3339()) +} + +async fn proxy_to_appview( + state: AppState, + headers: HeaderMap, + method: &str, + query: Option, +) -> Response { + if headers.get("atproto-proxy").is_none() { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "error": "InvalidRequest", + "message": "Missing required atproto-proxy header" + })), + ) + .into_response(); + } + + crate::api::proxy::proxy_handler( + State(state), + Path(method.to_string()), + Method::GET, + headers, + RawQuery(query), + Bytes::new(), + ) + .await +} diff --git a/src/api/identity/account.rs b/src/api/identity/account.rs index d227cf2..469f5f4 100644 --- a/src/api/identity/account.rs +++ b/src/api/identity/account.rs @@ -986,6 +986,24 @@ pub async fn create_account( .into_response(); } } + if std::env::var("PDS_AGE_ASSURANCE_OVERRIDE").is_ok() { + let birthdate_pref = json!({ + "$type": "app.bsky.actor.defs#personalDetailsPref", + "birthDate": "1998-05-06T00:00:00.000Z" + }); + if let Err(e) = sqlx::query!( + "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3) + ON CONFLICT (user_id, name) DO NOTHING", + user_id, + "app.bsky.actor.defs#personalDetailsPref", + birthdate_pref + ) + .execute(&mut *tx) + .await + { + warn!("Failed to set default birthdate preference: {:?}", e); + } + } if let Err(e) = tx.commit().await { error!("Error committing transaction: {:?}", e); return ( diff --git a/src/api/mod.rs b/src/api/mod.rs index 2a3f69f..be3ef4c 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,5 +1,6 @@ pub mod actor; pub mod admin; +pub mod age_assurance; pub mod delegation; pub mod error; pub mod identity; diff --git a/src/api/repo/import.rs b/src/api/repo/import.rs index a93fe56..37695d2 100644 --- a/src/api/repo/import.rs +++ b/src/api/repo/import.rs @@ -478,6 +478,27 @@ pub async fn import_repo( { warn!("Failed to sequence import event: {:?}", e); } + if std::env::var("PDS_AGE_ASSURANCE_OVERRIDE").is_ok() { + let birthdate_pref = json!({ + "$type": "app.bsky.actor.defs#personalDetailsPref", + "birthDate": "1998-05-06T00:00:00.000Z" + }); + if let Err(e) = sqlx::query!( + "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3) + ON CONFLICT (user_id, name) DO NOTHING", + user_id, + "app.bsky.actor.defs#personalDetailsPref", + birthdate_pref + ) + .execute(&state.db) + .await + { + warn!( + "Failed to set default birthdate preference for migrated user: {:?}", + e + ); + } + } (StatusCode::OK, Json(json!({}))).into_response() } Err(ImportError::SizeLimitExceeded) => ( diff --git a/src/api/server/passkey_account.rs b/src/api/server/passkey_account.rs index 3590683..f5be66b 100644 --- a/src/api/server/passkey_account.rs +++ b/src/api/server/passkey_account.rs @@ -706,6 +706,25 @@ pub async fn create_passkey_account( .await; } + if std::env::var("PDS_AGE_ASSURANCE_OVERRIDE").is_ok() { + let birthdate_pref = json!({ + "$type": "app.bsky.actor.defs#personalDetailsPref", + "birthDate": "1998-05-06T00:00:00.000Z" + }); + if let Err(e) = sqlx::query!( + "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3) + ON CONFLICT (user_id, name) DO NOTHING", + user_id, + "app.bsky.actor.defs#personalDetailsPref", + birthdate_pref + ) + .execute(&mut *tx) + .await + { + warn!("Failed to set default birthdate preference: {:?}", e); + } + } + if let Err(e) = tx.commit().await { error!("Error committing transaction: {:?}", e); return ( diff --git a/src/delegation/audit.rs b/src/delegation/audit.rs index fcba187..92fd826 100644 --- a/src/delegation/audit.rs +++ b/src/delegation/audit.rs @@ -28,6 +28,7 @@ pub struct AuditLogEntry { pub created_at: DateTime, } +#[allow(clippy::too_many_arguments)] pub async fn log_delegation_action( pool: &PgPool, delegated_did: &str, diff --git a/src/lib.rs b/src/lib.rs index 74c1197..fdea658 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -626,6 +626,14 @@ pub fn app(state: AppState) -> Router { "/xrpc/com.tranquil.delegation.createDelegatedAccount", post(api::delegation::create_delegated_account), ) + .route( + "/xrpc/app.bsky.ageassurance.getState", + get(api::age_assurance::get_state), + ) + .route( + "/xrpc/app.bsky.unspecced.getAgeAssuranceState", + get(api::age_assurance::get_age_assurance_state), + ) .route("/xrpc/{*method}", any(api::proxy::proxy_handler)) .layer(DefaultBodyLimit::max(util::get_max_blob_size())) .layer(middleware::from_fn(metrics::metrics_middleware))