this repo has no description

Age assurance override env var

lewis 61985665 9b5a310b

+9
.env.example
··· 139 # REPORT_SERVICE_URL=https://mod.bsky.app 140 # REPORT_SERVICE_DID=did:plc:ar7c4by46qjdydhdevvrndac 141 # ============================================================================= 142 # Miscellaneous 143 # ============================================================================= 144 # Allow HTTP for proxy requests (development only)
··· 139 # REPORT_SERVICE_URL=https://mod.bsky.app 140 # REPORT_SERVICE_DID=did:plc:ar7c4by46qjdydhdevvrndac 141 # ============================================================================= 142 + # Age Assurance Override 143 + # ============================================================================= 144 + # Enable this if you have separately assured the ages of your users 145 + # (e.g., through your own age verification process). When enabled, the PDS 146 + # will return "assured" status for age assurance checks instead of proxying 147 + # to the appview. This helps migrated users avoid the age assurance 148 + # catch-22 on bsky.app. 149 + # PDS_AGE_ASSURANCE_OVERRIDE=1 150 + # ============================================================================= 151 # Miscellaneous 152 # ============================================================================= 153 # Allow HTTP for proxy requests (development only)
+16
.sqlx/query-6efda9a01aff3277386c617e8500150271613b6779178816d9acfb244b48066c.json
···
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)\n ON CONFLICT (user_id, name) DO NOTHING", 4 + "describe": { 5 + "columns": [], 6 + "parameters": { 7 + "Left": [ 8 + "Uuid", 9 + "Text", 10 + "Jsonb" 11 + ] 12 + }, 13 + "nullable": [] 14 + }, 15 + "hash": "6efda9a01aff3277386c617e8500150271613b6779178816d9acfb244b48066c" 16 + }
+16
.sqlx/query-839b7593dd13cfc4cd303a626c7e17c93d702ff1a8be8018f3f21e8fd3d550a8.json
···
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)\n ON CONFLICT (user_id, name) DO NOTHING", 4 + "describe": { 5 + "columns": [], 6 + "parameters": { 7 + "Left": [ 8 + "Uuid", 9 + "Text", 10 + "Jsonb" 11 + ] 12 + }, 13 + "nullable": [] 14 + }, 15 + "hash": "839b7593dd13cfc4cd303a626c7e17c93d702ff1a8be8018f3f21e8fd3d550a8" 16 + }
+22
.sqlx/query-b2294557cfcc57a9fa2ed90602ea66ce90ae92d28b41886c5bb9b81e4b53eaa2.json
···
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "SELECT created_at FROM users WHERE did = $1", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "created_at", 9 + "type_info": "Timestamptz" 10 + } 11 + ], 12 + "parameters": { 13 + "Left": [ 14 + "Text" 15 + ] 16 + }, 17 + "nullable": [ 18 + false 19 + ] 20 + }, 21 + "hash": "b2294557cfcc57a9fa2ed90602ea66ce90ae92d28b41886c5bb9b81e4b53eaa2" 22 + }
+39 -20
frontend/src/lib/migration/atproto-client.ts
··· 131 error: "Unknown", 132 message: res.statusText, 133 })); 134 - const error = new Error(err.message || err.error || res.statusText) as Error & { 135 - status: number; 136 - error: string; 137 - }; 138 error.status = res.status; 139 error.error = err.error; 140 throw error; ··· 272 error: "Unknown", 273 message: res.statusText, 274 })); 275 - const error = new Error(err.message || err.error || res.statusText) as Error & { 276 - status: number; 277 - error: string; 278 - }; 279 error.status = res.status; 280 error.error = err.error; 281 throw error; ··· 369 } 370 371 async deactivateAccount(migratingTo?: string): Promise<void> { 372 - apiLog("POST", `${this.baseUrl}/xrpc/com.atproto.server.deactivateAccount`, { 373 - migratingTo, 374 - }); 375 const start = Date.now(); 376 try { 377 const body: { migratingTo?: string } = {}; ··· 503 error: "Unknown", 504 message: res.statusText, 505 })); 506 - const error = new Error(err.message || err.error || res.statusText) as Error & { 507 - status: number; 508 - error: string; 509 - }; 510 error.status = res.status; 511 error.error = err.error; 512 throw error; ··· 549 return directRes.json(); 550 } 551 552 - const protectedResourceUrl = `${pdsUrl}/.well-known/oauth-protected-resource`; 553 const protectedRes = await fetch(protectedResourceUrl); 554 if (!protectedRes.ok) { 555 return null; ··· 561 return null; 562 } 563 564 - const authServerUrl = `${authServers[0]}/.well-known/oauth-authorization-server`; 565 const authServerRes = await fetch(authServerUrl); 566 if (!authServerRes.ok) { 567 return null; ··· 595 for (let i = 0; i < bytes.length; i++) { 596 binary += String.fromCharCode(bytes[i]); 597 } 598 - return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); 599 } 600 601 export function base64UrlDecode(base64url: string): Uint8Array { ··· 730 error_description: res.statusText, 731 })); 732 throw new Error( 733 - retryErr.error_description || retryErr.error || "Token exchange failed", 734 ); 735 } 736 return res.json(); 737 } 738 } 739 740 - throw new Error(err.error_description || err.error || "Token exchange failed"); 741 } 742 743 return res.json();
··· 131 error: "Unknown", 132 message: res.statusText, 133 })); 134 + const error = new Error(err.message || err.error || res.statusText) as 135 + & Error 136 + & { 137 + status: number; 138 + error: string; 139 + }; 140 error.status = res.status; 141 error.error = err.error; 142 throw error; ··· 274 error: "Unknown", 275 message: res.statusText, 276 })); 277 + const error = new Error(err.message || err.error || res.statusText) as 278 + & Error 279 + & { 280 + status: number; 281 + error: string; 282 + }; 283 error.status = res.status; 284 error.error = err.error; 285 throw error; ··· 373 } 374 375 async deactivateAccount(migratingTo?: string): Promise<void> { 376 + apiLog( 377 + "POST", 378 + `${this.baseUrl}/xrpc/com.atproto.server.deactivateAccount`, 379 + { 380 + migratingTo, 381 + }, 382 + ); 383 const start = Date.now(); 384 try { 385 const body: { migratingTo?: string } = {}; ··· 511 error: "Unknown", 512 message: res.statusText, 513 })); 514 + const error = new Error(err.message || err.error || res.statusText) as 515 + & Error 516 + & { 517 + status: number; 518 + error: string; 519 + }; 520 error.status = res.status; 521 error.error = err.error; 522 throw error; ··· 559 return directRes.json(); 560 } 561 562 + const protectedResourceUrl = 563 + `${pdsUrl}/.well-known/oauth-protected-resource`; 564 const protectedRes = await fetch(protectedResourceUrl); 565 if (!protectedRes.ok) { 566 return null; ··· 572 return null; 573 } 574 575 + const authServerUrl = `${ 576 + authServers[0] 577 + }/.well-known/oauth-authorization-server`; 578 const authServerRes = await fetch(authServerUrl); 579 if (!authServerRes.ok) { 580 return null; ··· 608 for (let i = 0; i < bytes.length; i++) { 609 binary += String.fromCharCode(bytes[i]); 610 } 611 + return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace( 612 + /=+$/, 613 + "", 614 + ); 615 } 616 617 export function base64UrlDecode(base64url: string): Uint8Array { ··· 746 error_description: res.statusText, 747 })); 748 throw new Error( 749 + retryErr.error_description || retryErr.error || 750 + "Token exchange failed", 751 ); 752 } 753 return res.json(); 754 } 755 } 756 757 + throw new Error( 758 + err.error_description || err.error || "Token exchange failed", 759 + ); 760 } 761 762 return res.json();
+19 -8
frontend/src/lib/migration/flow.svelte.ts
··· 2 InboundMigrationState, 3 InboundStep, 4 MigrationProgress, 5 - OAuthServerMetadata, 6 OutboundMigrationState, 7 OutboundStep, 8 PasskeyAccountSetup, ··· 86 let sourceClient: AtprotoClient | null = null; 87 let localClient: AtprotoClient | null = null; 88 let localServerInfo: ServerDescription | null = null; 89 - let sourceOAuthMetadata: OAuthServerMetadata | null = null; 90 91 function setStep(step: InboundStep) { 92 state.step = step; ··· 271 if (state.authMethod === "passkey" && state.passkeySetupToken) { 272 localClient = createLocalClient(); 273 setStep("passkey-setup"); 274 - migrationLog("handleOAuthCallback: Resuming passkey flow at passkey-setup"); 275 } else { 276 setStep("email-verify"); 277 - migrationLog("handleOAuthCallback: Resuming at email-verify for re-auth"); 278 } 279 } else { 280 setStep(targetStep); ··· 337 serverDid: serverInfo.did, 338 }); 339 340 - migrationLog("startMigration: Getting service auth token from source PDS"); 341 const { token } = await sourceClient.getServiceAuth( 342 serverInfo.did, 343 "com.atproto.server.createAccount", ··· 361 inviteCode: passkeyParams.inviteCode, 362 stateInviteCode: state.inviteCode, 363 }); 364 - passkeySetup = await localClient.createPasskeyAccount(passkeyParams, token); 365 migrationLog("startMigration: Passkey account created on NEW PDS", { 366 did: passkeySetup.did, 367 hasAccessJwt: !!passkeySetup.accessJwt, ··· 743 migrationLog("Activating account on NEW PDS"); 744 const activateStart = Date.now(); 745 await localClient.activateAccount(); 746 - migrationLog("Account activated", { durationMs: Date.now() - activateStart }); 747 setProgress({ activated: true }); 748 749 setProgress({ currentOperation: "Deactivating old account..." }); ··· 757 setProgress({ deactivated: true }); 758 } catch (deactivateErr) { 759 const err = deactivateErr as Error & { error?: string }; 760 - migrationLog("Could not deactivate on source PDS", { error: err.message }); 761 } 762 763 migrationLog("completeDidWebMigration SUCCESS");
··· 2 InboundMigrationState, 3 InboundStep, 4 MigrationProgress, 5 OutboundMigrationState, 6 OutboundStep, 7 PasskeyAccountSetup, ··· 85 let sourceClient: AtprotoClient | null = null; 86 let localClient: AtprotoClient | null = null; 87 let localServerInfo: ServerDescription | null = null; 88 89 function setStep(step: InboundStep) { 90 state.step = step; ··· 269 if (state.authMethod === "passkey" && state.passkeySetupToken) { 270 localClient = createLocalClient(); 271 setStep("passkey-setup"); 272 + migrationLog( 273 + "handleOAuthCallback: Resuming passkey flow at passkey-setup", 274 + ); 275 } else { 276 setStep("email-verify"); 277 + migrationLog( 278 + "handleOAuthCallback: Resuming at email-verify for re-auth", 279 + ); 280 } 281 } else { 282 setStep(targetStep); ··· 339 serverDid: serverInfo.did, 340 }); 341 342 + migrationLog( 343 + "startMigration: Getting service auth token from source PDS", 344 + ); 345 const { token } = await sourceClient.getServiceAuth( 346 serverInfo.did, 347 "com.atproto.server.createAccount", ··· 365 inviteCode: passkeyParams.inviteCode, 366 stateInviteCode: state.inviteCode, 367 }); 368 + passkeySetup = await localClient.createPasskeyAccount( 369 + passkeyParams, 370 + token, 371 + ); 372 migrationLog("startMigration: Passkey account created on NEW PDS", { 373 did: passkeySetup.did, 374 hasAccessJwt: !!passkeySetup.accessJwt, ··· 750 migrationLog("Activating account on NEW PDS"); 751 const activateStart = Date.now(); 752 await localClient.activateAccount(); 753 + migrationLog("Account activated", { 754 + durationMs: Date.now() - activateStart, 755 + }); 756 setProgress({ activated: true }); 757 758 setProgress({ currentOperation: "Deactivating old account..." }); ··· 766 setProgress({ deactivated: true }); 767 } catch (deactivateErr) { 768 const err = deactivateErr as Error & { error?: string }; 769 + migrationLog("Could not deactivate on source PDS", { 770 + error: err.message, 771 + }); 772 } 773 774 migrationLog("completeDidWebMigration SUCCESS");
+3 -1
frontend/src/styles/migration.css
··· 352 border-radius: var(--radius-lg); 353 cursor: pointer; 354 margin-bottom: 0; 355 - transition: border-color var(--transition-normal), background-color var(--transition-normal); 356 } 357 358 .auth-option:hover {
··· 352 border-radius: var(--radius-lg); 353 cursor: pointer; 354 margin-bottom: 0; 355 + transition: 356 + border-color var(--transition-normal), 357 + background-color var(--transition-normal); 358 } 359 360 .auth-option:hover {
+2 -1
frontend/src/tests/Dashboard.test.ts
··· 77 setupAuthenticatedUser({ isAdmin: true }); 78 mockEndpoint( 79 "com.atproto.server.describeServer", 80 - () => jsonResponse(mockData.describeServer({ inviteCodeRequired: true })), 81 ); 82 render(Dashboard); 83 await waitFor(() => {
··· 77 setupAuthenticatedUser({ isAdmin: true }); 78 mockEndpoint( 79 "com.atproto.server.describeServer", 80 + () => 81 + jsonResponse(mockData.describeServer({ inviteCodeRequired: true })), 82 ); 83 render(Dashboard); 84 await waitFor(() => {
+9 -5
frontend/src/tests/Login.test.ts
··· 1 - import { beforeEach, describe, expect, it, vi } from "vitest"; 2 import { fireEvent, render, screen, waitFor } from "@testing-library/svelte"; 3 import Login from "../routes/Login.svelte"; 4 import { ··· 15 clearMocks(); 16 setupFetchMock(); 17 globalThis.location.hash = ""; 18 - mockEndpoint("/oauth/par", () => 19 - jsonResponse({ request_uri: "urn:mock:request" }) 20 ); 21 }); 22 ··· 85 error: null, 86 savedAccounts, 87 }); 88 - mockEndpoint("com.atproto.server.getSession", () => 89 - jsonResponse(mockData.session({ handle: "alice.test.tranquil.dev" }))); 90 }); 91 92 it("displays saved accounts list", async () => {
··· 1 + import { beforeEach, describe, expect, it } from "vitest"; 2 import { fireEvent, render, screen, waitFor } from "@testing-library/svelte"; 3 import Login from "../routes/Login.svelte"; 4 import { ··· 15 clearMocks(); 16 setupFetchMock(); 17 globalThis.location.hash = ""; 18 + mockEndpoint( 19 + "/oauth/par", 20 + () => jsonResponse({ request_uri: "urn:mock:request" }), 21 ); 22 }); 23 ··· 86 error: null, 87 savedAccounts, 88 }); 89 + mockEndpoint( 90 + "com.atproto.server.getSession", 91 + () => 92 + jsonResponse(mockData.session({ handle: "alice.test.tranquil.dev" })), 93 + ); 94 }); 95 96 it("displays saved accounts list", async () => {
+19 -9
frontend/src/tests/Settings.test.ts
··· 110 capturedBody = JSON.parse((options?.body as string) || "{}"); 111 return jsonResponse({}); 112 }); 113 - mockEndpoint("com.atproto.server.getSession", () => 114 - jsonResponse(mockData.session())); 115 render(Settings); 116 await waitFor(() => { 117 expect(screen.getByRole("button", { name: /change email/i })) ··· 144 () => jsonResponse({ tokenRequired: true }), 145 ); 146 mockEndpoint("com.atproto.server.updateEmail", () => jsonResponse({})); 147 - mockEndpoint("com.atproto.server.getSession", () => 148 - jsonResponse(mockData.session())); 149 render(Settings); 150 await waitFor(() => { 151 expect(screen.getByRole("button", { name: /change email/i })) ··· 188 expect(screen.getByRole("button", { name: /cancel/i })) 189 .toBeInTheDocument(); 190 }); 191 - const emailSection = screen.getByRole("heading", { name: /change email/i }) 192 .closest("section"); 193 const cancelButton = emailSection?.querySelector("button.secondary"); 194 if (cancelButton) { ··· 220 describe("handle change", () => { 221 beforeEach(() => { 222 setupAuthenticatedUser(); 223 - mockEndpoint("com.atproto.server.describeServer", () => 224 - jsonResponse(mockData.describeServer())); 225 }); 226 it("displays current handle", async () => { 227 render(Settings); ··· 255 }); 256 it("shows success message after handle change", async () => { 257 mockEndpoint("com.atproto.identity.updateHandle", () => jsonResponse({})); 258 - mockEndpoint("com.atproto.server.getSession", () => 259 - jsonResponse(mockData.session())); 260 render(Settings); 261 await waitFor(() => { 262 expect(screen.getByLabelText(/new handle/i)).toBeInTheDocument();
··· 110 capturedBody = JSON.parse((options?.body as string) || "{}"); 111 return jsonResponse({}); 112 }); 113 + mockEndpoint( 114 + "com.atproto.server.getSession", 115 + () => jsonResponse(mockData.session()), 116 + ); 117 render(Settings); 118 await waitFor(() => { 119 expect(screen.getByRole("button", { name: /change email/i })) ··· 146 () => jsonResponse({ tokenRequired: true }), 147 ); 148 mockEndpoint("com.atproto.server.updateEmail", () => jsonResponse({})); 149 + mockEndpoint( 150 + "com.atproto.server.getSession", 151 + () => jsonResponse(mockData.session()), 152 + ); 153 render(Settings); 154 await waitFor(() => { 155 expect(screen.getByRole("button", { name: /change email/i })) ··· 192 expect(screen.getByRole("button", { name: /cancel/i })) 193 .toBeInTheDocument(); 194 }); 195 + const emailSection = screen.getByRole("heading", { 196 + name: /change email/i, 197 + }) 198 .closest("section"); 199 const cancelButton = emailSection?.querySelector("button.secondary"); 200 if (cancelButton) { ··· 226 describe("handle change", () => { 227 beforeEach(() => { 228 setupAuthenticatedUser(); 229 + mockEndpoint( 230 + "com.atproto.server.describeServer", 231 + () => jsonResponse(mockData.describeServer()), 232 + ); 233 }); 234 it("displays current handle", async () => { 235 render(Settings); ··· 263 }); 264 it("shows success message after handle change", async () => { 265 mockEndpoint("com.atproto.identity.updateHandle", () => jsonResponse({})); 266 + mockEndpoint( 267 + "com.atproto.server.getSession", 268 + () => jsonResponse(mockData.session()), 269 + ); 270 render(Settings); 271 await waitFor(() => { 272 expect(screen.getByLabelText(/new handle/i)).toBeInTheDocument();
+8 -2
frontend/src/tests/migration/atproto-client.test.ts
··· 1 - import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; 2 import { 3 base64UrlDecode, 4 base64UrlEncode, ··· 351 352 it("returns null and clears storage for expired key (> 24 hours)", async () => { 353 const stored = { 354 - privateJwk: { kty: "EC", crv: "P-256", x: "test", y: "test", d: "test" }, 355 publicJwk: { kty: "EC", crv: "P-256", x: "test", y: "test" }, 356 thumbprint: "test-thumb", 357 createdAt: Date.now() - 25 * 60 * 60 * 1000,
··· 1 + import { beforeEach, describe, expect, it } from "vitest"; 2 import { 3 base64UrlDecode, 4 base64UrlEncode, ··· 351 352 it("returns null and clears storage for expired key (> 24 hours)", async () => { 353 const stored = { 354 + privateJwk: { 355 + kty: "EC", 356 + crv: "P-256", 357 + x: "test", 358 + y: "test", 359 + d: "test", 360 + }, 361 publicJwk: { kty: "EC", crv: "P-256", x: "test", y: "test" }, 362 thumbprint: "test-thumb", 363 createdAt: Date.now() - 25 * 60 * 60 * 1000,
+1 -1
frontend/src/tests/migration/storage.test.ts
··· 1 - import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; 2 import { 3 clearMigrationState, 4 getResumeInfo,
··· 1 + import { beforeEach, describe, expect, it } from "vitest"; 2 import { 3 clearMigrationState, 4 getResumeInfo,
+3 -1
frontend/src/tests/migration/types.test.ts
··· 63 }); 64 65 it("can check if error is MigrationError", () => { 66 - const error = new MigrationError("Test", "ERR_TEST", true, { foo: "bar" }); 67 68 if (error instanceof MigrationError) { 69 expect(error.code).toBe("ERR_TEST");
··· 63 }); 64 65 it("can check if error is MigrationError", () => { 66 + const error = new MigrationError("Test", "ERR_TEST", true, { 67 + foo: "bar", 68 + }); 69 70 if (error instanceof MigrationError) { 71 expect(error.code).toBe("ERR_TEST");
+119
src/api/age_assurance.rs
···
··· 1 + use crate::auth::{extract_bearer_token_from_header, validate_bearer_token}; 2 + use crate::state::AppState; 3 + use axum::{ 4 + Json, 5 + body::Bytes, 6 + extract::{Path, RawQuery, State}, 7 + http::{HeaderMap, Method, StatusCode}, 8 + response::{IntoResponse, Response}, 9 + }; 10 + use serde_json::json; 11 + 12 + pub async fn get_state( 13 + State(state): State<AppState>, 14 + headers: HeaderMap, 15 + RawQuery(query): RawQuery, 16 + ) -> Response { 17 + if std::env::var("PDS_AGE_ASSURANCE_OVERRIDE").is_err() { 18 + return proxy_to_appview(state, headers, "app.bsky.ageassurance.getState", query).await; 19 + } 20 + 21 + let created_at = get_account_created_at(&state, &headers).await; 22 + let now = chrono::Utc::now().to_rfc3339(); 23 + 24 + ( 25 + StatusCode::OK, 26 + Json(json!({ 27 + "state": { 28 + "status": "assured", 29 + "access": "full", 30 + "lastInitiatedAt": now 31 + }, 32 + "metadata": { 33 + "accountCreatedAt": created_at 34 + } 35 + })), 36 + ) 37 + .into_response() 38 + } 39 + 40 + pub async fn get_age_assurance_state( 41 + State(state): State<AppState>, 42 + headers: HeaderMap, 43 + RawQuery(query): RawQuery, 44 + ) -> Response { 45 + if std::env::var("PDS_AGE_ASSURANCE_OVERRIDE").is_err() { 46 + return proxy_to_appview( 47 + state, 48 + headers, 49 + "app.bsky.unspecced.getAgeAssuranceState", 50 + query, 51 + ) 52 + .await; 53 + } 54 + 55 + (StatusCode::OK, Json(json!({"status": "assured"}))).into_response() 56 + } 57 + 58 + async fn get_account_created_at(state: &AppState, headers: &HeaderMap) -> Option<String> { 59 + let auth_header = headers.get("Authorization").and_then(|h| h.to_str().ok()); 60 + tracing::debug!(?auth_header, "age assurance: extracting token"); 61 + 62 + let token = extract_bearer_token_from_header(auth_header)?; 63 + tracing::debug!("age assurance: got token, validating"); 64 + 65 + let auth_user = match validate_bearer_token(&state.db, &token).await { 66 + Ok(user) => { 67 + tracing::debug!(did = %user.did, "age assurance: validated user"); 68 + user 69 + } 70 + Err(e) => { 71 + tracing::warn!(?e, "age assurance: token validation failed"); 72 + return None; 73 + } 74 + }; 75 + 76 + let row = match sqlx::query!("SELECT created_at FROM users WHERE did = $1", auth_user.did) 77 + .fetch_optional(&state.db) 78 + .await 79 + { 80 + Ok(r) => { 81 + tracing::debug!(?r, "age assurance: query result"); 82 + r 83 + } 84 + Err(e) => { 85 + tracing::warn!(?e, "age assurance: query failed"); 86 + return None; 87 + } 88 + }; 89 + 90 + row.map(|r| r.created_at.to_rfc3339()) 91 + } 92 + 93 + async fn proxy_to_appview( 94 + state: AppState, 95 + headers: HeaderMap, 96 + method: &str, 97 + query: Option<String>, 98 + ) -> Response { 99 + if headers.get("atproto-proxy").is_none() { 100 + return ( 101 + StatusCode::BAD_REQUEST, 102 + Json(json!({ 103 + "error": "InvalidRequest", 104 + "message": "Missing required atproto-proxy header" 105 + })), 106 + ) 107 + .into_response(); 108 + } 109 + 110 + crate::api::proxy::proxy_handler( 111 + State(state), 112 + Path(method.to_string()), 113 + Method::GET, 114 + headers, 115 + RawQuery(query), 116 + Bytes::new(), 117 + ) 118 + .await 119 + }
+18
src/api/identity/account.rs
··· 986 .into_response(); 987 } 988 } 989 if let Err(e) = tx.commit().await { 990 error!("Error committing transaction: {:?}", e); 991 return (
··· 986 .into_response(); 987 } 988 } 989 + if std::env::var("PDS_AGE_ASSURANCE_OVERRIDE").is_ok() { 990 + let birthdate_pref = json!({ 991 + "$type": "app.bsky.actor.defs#personalDetailsPref", 992 + "birthDate": "1998-05-06T00:00:00.000Z" 993 + }); 994 + if let Err(e) = sqlx::query!( 995 + "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3) 996 + ON CONFLICT (user_id, name) DO NOTHING", 997 + user_id, 998 + "app.bsky.actor.defs#personalDetailsPref", 999 + birthdate_pref 1000 + ) 1001 + .execute(&mut *tx) 1002 + .await 1003 + { 1004 + warn!("Failed to set default birthdate preference: {:?}", e); 1005 + } 1006 + } 1007 if let Err(e) = tx.commit().await { 1008 error!("Error committing transaction: {:?}", e); 1009 return (
+1
src/api/mod.rs
··· 1 pub mod actor; 2 pub mod admin; 3 pub mod delegation; 4 pub mod error; 5 pub mod identity;
··· 1 pub mod actor; 2 pub mod admin; 3 + pub mod age_assurance; 4 pub mod delegation; 5 pub mod error; 6 pub mod identity;
+21
src/api/repo/import.rs
··· 478 { 479 warn!("Failed to sequence import event: {:?}", e); 480 } 481 (StatusCode::OK, Json(json!({}))).into_response() 482 } 483 Err(ImportError::SizeLimitExceeded) => (
··· 478 { 479 warn!("Failed to sequence import event: {:?}", e); 480 } 481 + if std::env::var("PDS_AGE_ASSURANCE_OVERRIDE").is_ok() { 482 + let birthdate_pref = json!({ 483 + "$type": "app.bsky.actor.defs#personalDetailsPref", 484 + "birthDate": "1998-05-06T00:00:00.000Z" 485 + }); 486 + if let Err(e) = sqlx::query!( 487 + "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3) 488 + ON CONFLICT (user_id, name) DO NOTHING", 489 + user_id, 490 + "app.bsky.actor.defs#personalDetailsPref", 491 + birthdate_pref 492 + ) 493 + .execute(&state.db) 494 + .await 495 + { 496 + warn!( 497 + "Failed to set default birthdate preference for migrated user: {:?}", 498 + e 499 + ); 500 + } 501 + } 502 (StatusCode::OK, Json(json!({}))).into_response() 503 } 504 Err(ImportError::SizeLimitExceeded) => (
+19
src/api/server/passkey_account.rs
··· 706 .await; 707 } 708 709 if let Err(e) = tx.commit().await { 710 error!("Error committing transaction: {:?}", e); 711 return (
··· 706 .await; 707 } 708 709 + if std::env::var("PDS_AGE_ASSURANCE_OVERRIDE").is_ok() { 710 + let birthdate_pref = json!({ 711 + "$type": "app.bsky.actor.defs#personalDetailsPref", 712 + "birthDate": "1998-05-06T00:00:00.000Z" 713 + }); 714 + if let Err(e) = sqlx::query!( 715 + "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3) 716 + ON CONFLICT (user_id, name) DO NOTHING", 717 + user_id, 718 + "app.bsky.actor.defs#personalDetailsPref", 719 + birthdate_pref 720 + ) 721 + .execute(&mut *tx) 722 + .await 723 + { 724 + warn!("Failed to set default birthdate preference: {:?}", e); 725 + } 726 + } 727 + 728 if let Err(e) = tx.commit().await { 729 error!("Error committing transaction: {:?}", e); 730 return (
+1
src/delegation/audit.rs
··· 28 pub created_at: DateTime<Utc>, 29 } 30 31 pub async fn log_delegation_action( 32 pool: &PgPool, 33 delegated_did: &str,
··· 28 pub created_at: DateTime<Utc>, 29 } 30 31 + #[allow(clippy::too_many_arguments)] 32 pub async fn log_delegation_action( 33 pool: &PgPool, 34 delegated_did: &str,
+8
src/lib.rs
··· 626 "/xrpc/com.tranquil.delegation.createDelegatedAccount", 627 post(api::delegation::create_delegated_account), 628 ) 629 .route("/xrpc/{*method}", any(api::proxy::proxy_handler)) 630 .layer(DefaultBodyLimit::max(util::get_max_blob_size())) 631 .layer(middleware::from_fn(metrics::metrics_middleware))
··· 626 "/xrpc/com.tranquil.delegation.createDelegatedAccount", 627 post(api::delegation::create_delegated_account), 628 ) 629 + .route( 630 + "/xrpc/app.bsky.ageassurance.getState", 631 + get(api::age_assurance::get_state), 632 + ) 633 + .route( 634 + "/xrpc/app.bsky.unspecced.getAgeAssuranceState", 635 + get(api::age_assurance::get_age_assurance_state), 636 + ) 637 .route("/xrpc/{*method}", any(api::proxy::proxy_handler)) 638 .layer(DefaultBodyLimit::max(util::get_max_blob_size())) 639 .layer(middleware::from_fn(metrics::metrics_middleware))