+226
-92
Diff
round #0
+15
-15
Cargo.lock
+15
-15
Cargo.lock
···
6094
6094
6095
6095
[[package]]
6096
6096
name = "tranquil-auth"
6097
-
version = "0.3.0"
6097
+
version = "0.3.1"
6098
6098
dependencies = [
6099
6099
"anyhow",
6100
6100
"base32",
···
6117
6117
6118
6118
[[package]]
6119
6119
name = "tranquil-cache"
6120
-
version = "0.3.0"
6120
+
version = "0.3.1"
6121
6121
dependencies = [
6122
6122
"async-trait",
6123
6123
"base64 0.22.1",
···
6131
6131
6132
6132
[[package]]
6133
6133
name = "tranquil-comms"
6134
-
version = "0.3.0"
6134
+
version = "0.3.1"
6135
6135
dependencies = [
6136
6136
"async-trait",
6137
6137
"base64 0.22.1",
···
6146
6146
6147
6147
[[package]]
6148
6148
name = "tranquil-config"
6149
-
version = "0.3.0"
6149
+
version = "0.3.1"
6150
6150
dependencies = [
6151
6151
"confique",
6152
6152
"serde",
···
6154
6154
6155
6155
[[package]]
6156
6156
name = "tranquil-crypto"
6157
-
version = "0.3.0"
6157
+
version = "0.3.1"
6158
6158
dependencies = [
6159
6159
"aes-gcm",
6160
6160
"base64 0.22.1",
···
6170
6170
6171
6171
[[package]]
6172
6172
name = "tranquil-db"
6173
-
version = "0.3.0"
6173
+
version = "0.3.1"
6174
6174
dependencies = [
6175
6175
"async-trait",
6176
6176
"chrono",
···
6187
6187
6188
6188
[[package]]
6189
6189
name = "tranquil-db-traits"
6190
-
version = "0.3.0"
6190
+
version = "0.3.1"
6191
6191
dependencies = [
6192
6192
"async-trait",
6193
6193
"base64 0.22.1",
···
6203
6203
6204
6204
[[package]]
6205
6205
name = "tranquil-infra"
6206
-
version = "0.3.0"
6206
+
version = "0.3.1"
6207
6207
dependencies = [
6208
6208
"async-trait",
6209
6209
"bytes",
···
6214
6214
6215
6215
[[package]]
6216
6216
name = "tranquil-oauth"
6217
-
version = "0.3.0"
6217
+
version = "0.3.1"
6218
6218
dependencies = [
6219
6219
"anyhow",
6220
6220
"axum",
···
6237
6237
6238
6238
[[package]]
6239
6239
name = "tranquil-pds"
6240
-
version = "0.3.0"
6240
+
version = "0.3.1"
6241
6241
dependencies = [
6242
6242
"aes-gcm",
6243
6243
"anyhow",
···
6324
6324
6325
6325
[[package]]
6326
6326
name = "tranquil-repo"
6327
-
version = "0.3.0"
6327
+
version = "0.3.1"
6328
6328
dependencies = [
6329
6329
"bytes",
6330
6330
"cid",
···
6336
6336
6337
6337
[[package]]
6338
6338
name = "tranquil-ripple"
6339
-
version = "0.3.0"
6339
+
version = "0.3.1"
6340
6340
dependencies = [
6341
6341
"async-trait",
6342
6342
"backon",
···
6361
6361
6362
6362
[[package]]
6363
6363
name = "tranquil-scopes"
6364
-
version = "0.3.0"
6364
+
version = "0.3.1"
6365
6365
dependencies = [
6366
6366
"axum",
6367
6367
"futures",
···
6377
6377
6378
6378
[[package]]
6379
6379
name = "tranquil-storage"
6380
-
version = "0.3.0"
6380
+
version = "0.3.1"
6381
6381
dependencies = [
6382
6382
"async-trait",
6383
6383
"aws-config",
···
6394
6394
6395
6395
[[package]]
6396
6396
name = "tranquil-types"
6397
-
version = "0.3.0"
6397
+
version = "0.3.1"
6398
6398
dependencies = [
6399
6399
"chrono",
6400
6400
"cid",
+1
-1
Cargo.toml
+1
-1
Cargo.toml
+8
-22
crates/tranquil-pds/src/api/identity/account.rs
+8
-22
crates/tranquil-pds/src/api/identity/account.rs
···
140
140
}
141
141
}
142
142
143
-
let available_domains = tranquil_config::get().server.available_user_domain_list();
143
+
let cfg = tranquil_config::get();
144
+
let available_domains = cfg.server.available_user_domain_list();
144
145
let matched_domain = available_domains
145
146
.iter()
146
147
.filter(|d| input.handle.ends_with(&format!(".{}", d)))
···
163
164
}
164
165
}
165
166
} else {
166
-
if input.handle.contains(' ') || input.handle.contains('\t') {
167
-
return ApiError::InvalidRequest("Handle cannot contain spaces".into()).into_response();
168
-
}
169
-
if let Some(c) = input
170
-
.handle
171
-
.chars()
172
-
.find(|c| !c.is_ascii_alphanumeric() && *c != '.' && *c != '-')
173
-
{
174
-
return ApiError::InvalidRequest(format!("Handle contains invalid character: {}", c))
175
-
.into_response();
176
-
}
177
-
let handle_lower = input.handle.to_lowercase();
178
-
if crate::moderation::has_explicit_slur(&handle_lower) {
179
-
return ApiError::InvalidRequest("Inappropriate language in handle".into())
180
-
.into_response();
167
+
match crate::api::validation::validate_full_domain_handle(&input.handle) {
168
+
Ok(h) => h,
169
+
Err(e) => return ApiError::from(e).into_response(),
181
170
}
182
-
handle_lower
183
171
};
184
172
let email: Option<String> = input
185
173
.email
···
234
222
},
235
223
})
236
224
};
237
-
let hostname = &tranquil_config::get().server.hostname;
225
+
let hostname = &cfg.server.hostname;
238
226
let pds_endpoint = format!("https://{}", hostname);
239
227
let handle = match matched_domain {
240
228
Some(domain) => format!("{}.{}", validated_short_handle, domain),
···
274
262
if !crate::api::server::meta::is_self_hosted_did_web_enabled() {
275
263
return ApiError::SelfHostedDidWebDisabled.into_response();
276
264
}
277
-
let pds_hostname = tranquil_config::get().server.hostname_without_port();
278
-
let subdomain_host = format!("{}.{}", input.handle, pds_hostname);
279
-
let encoded_subdomain = subdomain_host.replace(':', "%3A");
280
-
let self_hosted_did = format!("did:web:{}", encoded_subdomain);
265
+
let encoded_handle = handle.replace(':', "%3A");
266
+
let self_hosted_did = format!("did:web:{}", encoded_handle);
281
267
info!(did = %self_hosted_did, "Creating self-hosted did:web account (subdomain)");
282
268
self_hosted_did
283
269
}
+16
-14
crates/tranquil-pds/src/api/identity/did.rs
+16
-14
crates/tranquil-pds/src/api/identity/did.rs
···
122
122
}
123
123
124
124
pub async fn well_known_did(State(state): State<AppState>, headers: HeaderMap) -> Response {
125
-
let hostname = &tranquil_config::get().server.hostname;
126
-
let hostname_without_port = tranquil_config::get().server.hostname_without_port();
125
+
let cfg = tranquil_config::get();
126
+
let hostname = &cfg.server.hostname;
127
+
let hostname_without_port = cfg.server.hostname_without_port();
127
128
let host_header = get_header_str(&headers, http::header::HOST).unwrap_or(hostname);
128
129
let host_without_port = host_header.split(':').next().unwrap_or(host_header);
129
-
if host_without_port != hostname_without_port
130
-
&& host_without_port.ends_with(&format!(".{}", hostname_without_port))
131
-
{
132
-
let handle = host_without_port
133
-
.strip_suffix(&format!(".{}", hostname_without_port))
134
-
.unwrap_or(host_without_port);
135
-
return serve_subdomain_did_doc(&state, handle, hostname).await;
130
+
if host_without_port != hostname_without_port {
131
+
let is_subdomain = cfg
132
+
.server
133
+
.available_user_domain_list()
134
+
.into_iter()
135
+
.chain(std::iter::once(hostname_without_port.to_string()))
136
+
.any(|d| host_without_port.ends_with(&format!(".{}", d)));
137
+
if is_subdomain {
138
+
return serve_handle_did_doc(&state, host_without_port, hostname).await;
139
+
}
136
140
}
137
141
let did = if hostname.contains(':') {
138
142
format!("did:web:{}", hostname.replace(':', "%3A"))
···
151
155
.into_response()
152
156
}
153
157
154
-
async fn serve_subdomain_did_doc(state: &AppState, subdomain: &str, hostname: &str) -> Response {
155
-
let hostname_for_handles = hostname.split(':').next().unwrap_or(hostname);
156
-
let subdomain_host = format!("{}.{}", subdomain, hostname_for_handles);
157
-
let encoded_subdomain = subdomain_host.replace(':', "%3A");
158
-
let expected_did = format!("did:web:{}", encoded_subdomain);
158
+
async fn serve_handle_did_doc(state: &AppState, handle: &str, hostname: &str) -> Response {
159
+
let encoded_handle = handle.replace(':', "%3A");
160
+
let expected_did = format!("did:web:{}", encoded_handle);
159
161
let expected_did_typed: crate::types::Did = match expected_did.parse() {
160
162
Ok(d) => d,
161
163
Err(_) => return ApiError::InvalidRequest("Invalid DID format".into()).into_response(),
+18
-6
crates/tranquil-pds/src/api/proxy_client.rs
+18
-6
crates/tranquil-pds/src/api/proxy_client.rs
···
215
215
use super::*;
216
216
#[test]
217
217
fn test_ssrf_safe_https() {
218
-
assert!(is_ssrf_safe("https://api.bsky.app/xrpc/test").is_ok());
218
+
assert!(is_ssrf_safe("https://1.1.1.1/xrpc/test").is_ok());
219
219
}
220
220
#[test]
221
221
fn test_ssrf_blocks_http_by_default() {
222
-
let result = is_ssrf_safe("http://external.example.com/xrpc/test");
223
-
assert!(matches!(
224
-
result,
225
-
Err(SsrfError::InsecureProtocol(_)) | Err(SsrfError::DnsResolutionFailed(_))
226
-
));
222
+
let result = is_ssrf_safe("http://93.184.216.34/xrpc/test");
223
+
assert!(matches!(result, Err(SsrfError::InsecureProtocol(_))));
227
224
}
228
225
#[test]
229
226
fn test_ssrf_allows_localhost_http() {
···
231
228
assert!(is_ssrf_safe("http://localhost:8080/test").is_ok());
232
229
}
233
230
#[test]
231
+
fn test_ssrf_blocks_non_unicast_ip() {
232
+
assert!(matches!(
233
+
is_ssrf_safe("https://0.0.0.0/test"),
234
+
Err(SsrfError::NonUnicastIp(_))
235
+
));
236
+
assert!(matches!(
237
+
is_ssrf_safe("https://224.0.0.1/test"),
238
+
Err(SsrfError::NonUnicastIp(_))
239
+
));
240
+
assert!(matches!(
241
+
is_ssrf_safe("https://255.255.255.255/test"),
242
+
Err(SsrfError::NonUnicastIp(_))
243
+
));
244
+
}
245
+
#[test]
234
246
fn test_validate_at_uri() {
235
247
let result = validate_at_uri("at://did:plc:test/app.bsky.feed.post/abc123");
236
248
assert!(result.is_ok());
+12
-7
crates/tranquil-pds/src/api/server/passkey_account.rs
+12
-7
crates/tranquil-pds/src/api/server/passkey_account.rs
···
112
112
.map(|d| d.starts_with("did:web:"))
113
113
.unwrap_or(false);
114
114
115
-
let hostname = &tranquil_config::get().server.hostname;
116
-
let available_domains = tranquil_config::get().server.available_user_domain_list();
115
+
let cfg = tranquil_config::get();
116
+
let hostname = &cfg.server.hostname;
117
+
let available_domains = cfg.server.available_user_domain_list();
117
118
let matched_domain = available_domains
118
119
.iter()
119
120
.filter(|d| input.handle.ends_with(&format!(".{}", d)))
···
134
135
}
135
136
}
136
137
} else {
137
-
input.handle.to_lowercase()
138
+
match crate::api::validation::validate_full_domain_handle(&input.handle) {
139
+
Ok(h) => h,
140
+
Err(_) => return ApiError::InvalidHandle(None).into_response(),
141
+
}
138
142
};
139
143
140
144
let email = input
···
246
250
247
251
let did = match did_type {
248
252
"web" => {
249
-
let pds_hostname = tranquil_config::get().server.hostname_without_port();
250
-
let subdomain_host = format!("{}.{}", input.handle, pds_hostname);
251
-
let encoded_subdomain = subdomain_host.replace(':', "%3A");
252
-
let self_hosted_did = format!("did:web:{}", encoded_subdomain);
253
+
if !crate::api::server::meta::is_self_hosted_did_web_enabled() {
254
+
return ApiError::SelfHostedDidWebDisabled.into_response();
255
+
}
256
+
let encoded_handle = handle.replace(':', "%3A");
257
+
let self_hosted_did = format!("did:web:{}", encoded_handle);
253
258
info!(did = %self_hosted_did, "Creating self-hosted did:web passkey account");
254
259
self_hosted_did
255
260
}
+43
crates/tranquil-pds/src/api/validation.rs
+43
crates/tranquil-pds/src/api/validation.rs
···
258
258
Reject,
259
259
}
260
260
261
+
pub fn validate_full_domain_handle(handle: &str) -> Result<String, HandleValidationError> {
262
+
let handle = handle.trim();
263
+
264
+
if handle.is_empty() {
265
+
return Err(HandleValidationError::Empty);
266
+
}
267
+
268
+
if handle.contains(' ') || handle.contains('\t') || handle.contains('\n') {
269
+
return Err(HandleValidationError::ContainsSpaces);
270
+
}
271
+
272
+
if handle.len() > MAX_HANDLE_LENGTH {
273
+
return Err(HandleValidationError::TooLong);
274
+
}
275
+
276
+
if handle
277
+
.chars()
278
+
.any(|c| !c.is_ascii_alphanumeric() && c != '.' && c != '-')
279
+
{
280
+
return Err(HandleValidationError::InvalidCharacters);
281
+
}
282
+
283
+
if !handle.contains('.') {
284
+
return Err(HandleValidationError::InvalidCharacters);
285
+
}
286
+
287
+
let labels: Vec<&str> = handle.split('.').collect();
288
+
let has_invalid_label = labels
289
+
.iter()
290
+
.any(|label| label.is_empty() || label.len() > MAX_DOMAIN_LABEL_LENGTH || label.starts_with('-') || label.ends_with('-'));
291
+
if has_invalid_label {
292
+
return Err(HandleValidationError::InvalidCharacters);
293
+
}
294
+
295
+
let handle_lower = handle.to_lowercase();
296
+
297
+
if crate::moderation::has_explicit_slur(&handle_lower) {
298
+
return Err(HandleValidationError::BannedWord);
299
+
}
300
+
301
+
Ok(handle_lower)
302
+
}
303
+
261
304
pub fn validate_short_handle(handle: &str) -> Result<String, HandleValidationError> {
262
305
validate_service_handle(handle, ReservedHandlePolicy::Reject)
263
306
}
+43
-10
crates/tranquil-pds/src/sso/endpoints.rs
+43
-10
crates/tranquil-pds/src/sso/endpoints.rs
···
743
743
#[derive(Debug, Deserialize)]
744
744
pub struct CheckHandleQuery {
745
745
pub handle: String,
746
+
pub domain: Option<String>,
746
747
}
747
748
748
749
#[derive(Debug, Serialize)]
···
773
774
};
774
775
775
776
let available_domains = tranquil_config::get().server.available_user_domain_list();
776
-
let full_handle = format!("{}.{}", validated, &available_domains[0]);
777
+
if let Some(ref d) = query.domain {
778
+
if !available_domains.iter().any(|ad| ad == d) {
779
+
return Err(ApiError::InvalidRequest(
780
+
"Unknown user domain".into(),
781
+
));
782
+
}
783
+
}
784
+
let domain = query
785
+
.domain
786
+
.as_deref()
787
+
.unwrap_or(&available_domains[0]);
788
+
let full_handle = format!("{}.{}", validated, domain);
777
789
let handle_typed: crate::types::Handle = match full_handle.parse() {
778
790
Ok(h) => h,
779
791
Err(_) => return Err(ApiError::InvalidHandle(None)),
···
855
867
.await?
856
868
.ok_or(ApiError::SsoSessionExpired)?;
857
869
858
-
let hostname = &tranquil_config::get().server.hostname;
859
-
let available_domains = tranquil_config::get().server.available_user_domain_list();
870
+
let cfg = tranquil_config::get();
871
+
let hostname = &cfg.server.hostname;
872
+
let available_domains = cfg.server.available_user_domain_list();
860
873
861
-
let handle = match crate::api::validation::validate_short_handle(&input.handle) {
862
-
Ok(h) => format!("{}.{}", h, &available_domains[0]),
863
-
Err(_) => return Err(ApiError::InvalidHandle(None)),
874
+
let matched_domain = available_domains
875
+
.iter()
876
+
.filter(|d| input.handle.ends_with(&format!(".{}", d)))
877
+
.max_by_key(|d| d.len());
878
+
879
+
let handle = if !input.handle.contains('.') || matched_domain.is_some() {
880
+
let handle_to_validate = match matched_domain {
881
+
Some(domain) => input
882
+
.handle
883
+
.strip_suffix(&format!(".{}", domain))
884
+
.unwrap_or(&input.handle),
885
+
None => &input.handle,
886
+
};
887
+
match crate::api::validation::validate_short_handle(handle_to_validate) {
888
+
Ok(h) => format!("{}.{}", h, matched_domain.unwrap_or(&available_domains[0])),
889
+
Err(_) => return Err(ApiError::InvalidHandle(None)),
890
+
}
891
+
} else {
892
+
match crate::api::validation::validate_full_domain_handle(&input.handle) {
893
+
Ok(h) => h,
894
+
Err(_) => return Err(ApiError::InvalidHandle(None)),
895
+
}
864
896
};
865
897
866
898
let verification_channel = input
···
981
1013
982
1014
let did = match did_type {
983
1015
"web" => {
984
-
let pds_hostname = tranquil_config::get().server.hostname_without_port();
985
-
let subdomain_host = format!("{}.{}", input.handle, pds_hostname);
986
-
let encoded_subdomain = subdomain_host.replace(':', "%3A");
987
-
let self_hosted_did = format!("did:web:{}", encoded_subdomain);
1016
+
if !crate::api::server::meta::is_self_hosted_did_web_enabled() {
1017
+
return Err(ApiError::SelfHostedDidWebDisabled);
1018
+
}
1019
+
let encoded_handle = handle.replace(':', "%3A");
1020
+
let self_hosted_did = format!("did:web:{}", encoded_handle);
988
1021
tracing::info!(did = %self_hosted_did, "Creating self-hosted did:web SSO account");
989
1022
self_hosted_did
990
1023
}
-4
crates/tranquil-pds/tests/firehose/mod.rs
-4
crates/tranquil-pds/tests/firehose/mod.rs
+34
crates/tranquil-pds/tests/handle_domains.rs
+34
crates/tranquil-pds/tests/handle_domains.rs
···
276
276
"updateHandle with bare handle should use configured domain, not PDS hostname"
277
277
);
278
278
}
279
+
280
+
#[tokio::test]
281
+
async fn did_web_uses_handle_domain_not_hostname() {
282
+
unsafe {
283
+
std::env::set_var("ENABLE_PDS_HOSTED_DID_WEB", "true");
284
+
}
285
+
let client = client();
286
+
let base = base_url_with_domain().await;
287
+
let short_handle = format!("hd{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
288
+
let payload = json!({
289
+
"handle": short_handle,
290
+
"email": format!("{}@example.com", short_handle),
291
+
"password": "Testpass123!",
292
+
"didType": "web"
293
+
});
294
+
let res = client
295
+
.post(format!(
296
+
"{}/xrpc/com.atproto.server.createAccount",
297
+
base
298
+
))
299
+
.json(&payload)
300
+
.send()
301
+
.await
302
+
.expect("createAccount request failed");
303
+
assert_eq!(res.status(), StatusCode::OK);
304
+
let body: Value = res.json().await.expect("Invalid JSON");
305
+
let did = body["did"].as_str().expect("No DID in response");
306
+
let expected_did = format!("did:web:{}.{}", short_handle, HANDLE_DOMAIN);
307
+
assert_eq!(
308
+
did, expected_did,
309
+
"did:web should use handle domain '{}', not PDS hostname",
310
+
HANDLE_DOMAIN
311
+
);
312
+
}
+14
-6
frontend/src/lib/registration/flow.svelte.ts
+14
-6
frontend/src/lib/registration/flow.svelte.ts
···
34
34
error: string | null;
35
35
submitting: boolean;
36
36
pdsHostname: string;
37
+
selectedDomain: string;
37
38
handleAvailable: boolean | null;
38
39
checkingHandle: boolean;
39
40
discordInUse: boolean;
···
68
69
error: null,
69
70
submitting: false,
70
71
pdsHostname,
72
+
selectedDomain: "",
71
73
handleAvailable: null,
72
74
checkingHandle: false,
73
75
discordInUse: false,
···
84
86
}
85
87
86
88
function getFullHandle(): string {
87
-
return `${state.info.handle.trim()}.${state.pdsHostname}`;
89
+
const handle = state.info.handle.trim();
90
+
if (handle.includes('.')) return handle;
91
+
const domain = state.selectedDomain || state.pdsHostname;
92
+
return `${handle}.${domain}`;
88
93
}
89
94
90
95
function extractDomain(did: string): string {
···
132
137
}
133
138
state.checkingHandle = true;
134
139
try {
140
+
const params = new URLSearchParams({ handle });
141
+
if (state.selectedDomain) params.set("domain", state.selectedDomain);
135
142
const response = await fetch(
136
-
`${getPdsEndpoint()}/oauth/sso/check-handle-available?handle=${
137
-
encodeURIComponent(handle)
138
-
}`,
143
+
`${getPdsEndpoint()}/oauth/sso/check-handle-available?${params}`,
139
144
);
140
145
const data = await response.json();
141
146
state.handleAvailable = data.available === true;
···
239
244
}
240
245
241
246
const result = await api.createAccount({
242
-
handle: state.info.handle.trim(),
247
+
handle: getFullHandle(),
243
248
email: state.info.email.trim(),
244
249
password: state.info.password!,
245
250
inviteCode: state.info.inviteCode?.trim() || undefined,
···
291
296
}
292
297
293
298
const result = await api.createPasskeyAccount({
294
-
handle: unsafeAsHandle(state.info.handle.trim()),
299
+
handle: unsafeAsHandle(getFullHandle()),
295
300
email: state.info.email?.trim()
296
301
? unsafeAsEmail(state.info.email.trim())
297
302
: undefined,
···
532
537
getPdsDid,
533
538
getFullHandle,
534
539
extractDomain,
540
+
setSelectedDomain(domain: string) {
541
+
state.selectedDomain = domain;
542
+
},
535
543
536
544
proceedFromInfo,
537
545
selectKeyMode,
+2
-1
frontend/src/routes/OAuthRegister.svelte
+2
-1
frontend/src/routes/OAuthRegister.svelte
···
102
102
flow = createRegistrationFlow('passkey', hostname)
103
103
}
104
104
selectedDomain = serverInfo?.availableUserDomains?.[0] || window.location.hostname
105
+
if (flow) flow.setSelectedDomain(selectedDomain)
105
106
} catch (e) {
106
107
console.error('Failed to load server info:', e)
107
108
} finally {
···
353
354
placeholder={$_('register.handlePlaceholder')}
354
355
disabled={flow.state.submitting}
355
356
onInput={(v) => { flow!.info.handle = v }}
356
-
onDomainChange={(d) => { selectedDomain = d }}
357
+
onDomainChange={(d) => { selectedDomain = d; flow!.setSelectedDomain(d) }}
357
358
/>
358
359
{#if fullHandle()}
359
360
<p class="hint">{$_('register.handleHint', { values: { handle: fullHandle() } })}</p>
+8
-2
frontend/src/routes/OAuthSsoRegister.svelte
+8
-2
frontend/src/routes/OAuthSsoRegister.svelte
···
150
150
let checkHandleTimeout: ReturnType<typeof setTimeout> | null = null
151
151
152
152
$effect(() => {
153
+
void selectedDomain
153
154
if (checkHandleTimeout) {
154
155
clearTimeout(checkHandleTimeout)
155
156
}
···
167
168
handleError = null
168
169
169
170
try {
170
-
const response = await fetch(`/oauth/sso/check-handle-available?handle=${encodeURIComponent(handle)}`)
171
+
const params = new URLSearchParams({ handle })
172
+
if (selectedDomain) params.set('domain', selectedDomain)
173
+
const response = await fetch(`/oauth/sso/check-handle-available?${params}`)
171
174
const data = await response.json()
172
175
handleAvailable = data.available
173
176
if (!data.available && data.reason) {
···
222
225
return
223
226
}
224
227
228
+
const fullHandle = !handle.includes('.') && selectedDomain
229
+
? `${handle.trim()}.${selectedDomain}`
230
+
: handle.trim()
225
231
submitting = true
226
232
227
233
try {
···
233
239
},
234
240
body: JSON.stringify({
235
241
token,
236
-
handle,
242
+
handle: fullHandle,
237
243
email: email || null,
238
244
invite_code: inviteCode || null,
239
245
verification_channel: verificationChannel,
+2
-1
frontend/src/routes/Register.svelte
+2
-1
frontend/src/routes/Register.svelte
···
115
115
flow = createRegistrationFlow('passkey', hostname)
116
116
}
117
117
selectedDomain = serverInfo?.availableUserDomains?.[0] || window.location.hostname
118
+
if (flow) flow.setSelectedDomain(selectedDomain)
118
119
} catch (e) {
119
120
console.error('Failed to load server info:', e)
120
121
} finally {
···
368
369
placeholder={$_('register.handlePlaceholder')}
369
370
disabled={flow.state.submitting}
370
371
onInput={(v) => { flow!.info.handle = v }}
371
-
onDomainChange={(d) => { selectedDomain = d }}
372
+
onDomainChange={(d) => { selectedDomain = d; flow!.setSelectedDomain(d) }}
372
373
/>
373
374
{#if flow.info.handle.includes('.')}
374
375
<p class="hint warning">{$_('register.handleDotWarning')}</p>
+2
-1
frontend/src/routes/RegisterPassword.svelte
+2
-1
frontend/src/routes/RegisterPassword.svelte
···
109
109
flow = createRegistrationFlow('password', hostname)
110
110
}
111
111
selectedDomain = serverInfo?.availableUserDomains?.[0] || window.location.hostname
112
+
if (flow) flow.setSelectedDomain(selectedDomain)
112
113
} catch (e) {
113
114
console.error('Failed to load server info:', e)
114
115
} finally {
···
313
314
placeholder={$_('register.handlePlaceholder')}
314
315
disabled={flow.state.submitting}
315
316
onInput={(v) => { flow!.info.handle = v }}
316
-
onDomainChange={(d) => { selectedDomain = d }}
317
+
onDomainChange={(d) => { selectedDomain = d; flow!.setSelectedDomain(d) }}
317
318
/>
318
319
{#if flow.info.handle.includes('.')}
319
320
<p class="hint warning">{$_('register.handleDotWarning')}</p>
+8
-2
frontend/src/routes/SsoRegisterComplete.svelte
+8
-2
frontend/src/routes/SsoRegisterComplete.svelte
···
165
165
let checkHandleTimeout: ReturnType<typeof setTimeout> | null = null
166
166
167
167
$effect(() => {
168
+
void selectedDomain
168
169
if (checkHandleTimeout) {
169
170
clearTimeout(checkHandleTimeout)
170
171
}
···
182
183
handleError = null
183
184
184
185
try {
185
-
const response = await fetch(`/oauth/sso/check-handle-available?handle=${encodeURIComponent(handle)}`)
186
+
const params = new URLSearchParams({ handle })
187
+
if (selectedDomain) params.set('domain', selectedDomain)
188
+
const response = await fetch(`/oauth/sso/check-handle-available?${params}`)
186
189
const data = await response.json()
187
190
handleAvailable = data.available
188
191
if (!data.available && data.reason) {
···
269
272
return
270
273
}
271
274
275
+
const fullHandle = !handle.includes('.') && selectedDomain
276
+
? `${handle.trim()}.${selectedDomain}`
277
+
: handle.trim()
272
278
submitting = true
273
279
274
280
try {
···
280
286
},
281
287
body: JSON.stringify({
282
288
token,
283
-
handle,
289
+
handle: fullHandle,
284
290
email: email || null,
285
291
invite_code: inviteCode || null,
286
292
verification_channel: verificationChannel,
History
1 round
0 comments
oyster.cafe
submitted
#0
1 commit
expand
collapse
fix: did:web also uses handle domains not hostname
expand 0 comments
pull request successfully merged