+9
.env.example
+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
+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
+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
+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
+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
+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
+3
-1
frontend/src/styles/migration.css
+2
-1
frontend/src/tests/Dashboard.test.ts
+2
-1
frontend/src/tests/Dashboard.test.ts
+9
-5
frontend/src/tests/Login.test.ts
+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
+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
+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
-1
frontend/src/tests/migration/storage.test.ts
+3
-1
frontend/src/tests/migration/types.test.ts
+3
-1
frontend/src/tests/migration/types.test.ts
+119
src/api/age_assurance.rs
+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
+18
src/api/identity/account.rs
···
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
src/api/mod.rs
+21
src/api/repo/import.rs
+21
src/api/repo/import.rs
···
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
+19
src/api/server/passkey_account.rs
···
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
+1
src/delegation/audit.rs
+8
src/lib.rs
+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))