+46
-15
scripts/setup.js
+46
-15
scripts/setup.js
···
17
17
function parseArgs() {
18
18
const args = process.argv.slice(2)
19
19
const opts = {
20
+
handle: null,
20
21
pds: null,
21
22
plcUrl: 'https://plc.directory',
22
23
relayUrl: 'https://bsky.network'
23
24
}
24
25
25
26
for (let i = 0; i < args.length; i++) {
26
-
if (args[i] === '--pds' && args[i + 1]) {
27
+
if (args[i] === '--handle' && args[i + 1]) {
28
+
opts.handle = args[++i]
29
+
} else if (args[i] === '--pds' && args[i + 1]) {
27
30
opts.pds = args[++i]
28
31
} else if (args[i] === '--plc-url' && args[i + 1]) {
29
32
opts.plcUrl = args[++i]
···
32
35
}
33
36
}
34
37
35
-
if (!opts.pds) {
36
-
console.error('Usage: node scripts/setup.js --pds <pds-url>')
38
+
if (!opts.handle || !opts.pds) {
39
+
console.error('Usage: node scripts/setup.js --handle <subdomain> --pds <pds-url>')
37
40
console.error('')
38
41
console.error('Options:')
42
+
console.error(' --handle Subdomain handle (e.g., "alice")')
39
43
console.error(' --pds PDS URL (e.g., "https://atproto-pds.chad-53c.workers.dev")')
40
44
console.error(' --plc-url PLC directory URL (default: https://plc.directory)')
41
45
console.error(' --relay-url Relay URL (default: https://bsky.network)')
42
46
process.exit(1)
43
47
}
44
-
45
-
// Handle is just the PDS hostname
46
-
opts.handle = new URL(opts.pds).host
47
48
48
49
return opts
49
50
}
···
288
289
async function createGenesisOperation(opts) {
289
290
const { didKey, handle, pdsUrl, cryptoKey } = opts
290
291
291
-
// Handle is already the full hostname
292
+
// Build full handle: subdomain.pds-hostname
293
+
const pdsHost = new URL(pdsUrl).host
294
+
const fullHandle = `${handle}.${pdsHost}`
295
+
292
296
const operation = {
293
297
type: 'plc_operation',
294
298
rotationKeys: [didKey],
295
299
verificationMethods: {
296
300
atproto: didKey
297
301
},
298
-
alsoKnownAs: [`at://${handle}`],
302
+
alsoKnownAs: [`at://${fullHandle}`],
299
303
services: {
300
304
atproto_pds: {
301
305
type: 'AtprotoPersonalDataServer',
···
308
312
// Sign the operation
309
313
operation.sig = await signPlcOperation(operation, cryptoKey)
310
314
311
-
return { operation, handle }
315
+
return { operation, fullHandle }
312
316
}
313
317
314
318
async function deriveDidFromOperation(operation) {
···
387
391
return response.json()
388
392
}
389
393
394
+
// === HANDLE REGISTRATION ===
395
+
396
+
async function registerHandle(pdsUrl, handle, did) {
397
+
const url = `${pdsUrl}/register-handle`
398
+
399
+
const response = await fetch(url, {
400
+
method: 'POST',
401
+
headers: {
402
+
'Content-Type': 'application/json'
403
+
},
404
+
body: JSON.stringify({ handle, did })
405
+
})
406
+
407
+
if (!response.ok) {
408
+
const text = await response.text()
409
+
throw new Error(`Handle registration failed: ${response.status} ${text}`)
410
+
}
411
+
412
+
return true
413
+
}
414
+
390
415
// === RELAY NOTIFICATION ===
391
416
392
417
async function notifyRelay(relayUrl, pdsHostname) {
···
437
462
438
463
// Step 2: Create genesis operation
439
464
console.log('Creating PLC genesis operation...')
440
-
const { operation, handle } = await createGenesisOperation({
465
+
const { operation, fullHandle } = await createGenesisOperation({
441
466
didKey,
442
467
handle: opts.handle,
443
468
pdsUrl: opts.pds,
···
445
470
})
446
471
const did = await deriveDidFromOperation(operation)
447
472
console.log(` DID: ${did}`)
448
-
console.log(` Handle: ${handle}`)
473
+
console.log(` Handle: ${fullHandle}`)
449
474
console.log('')
450
475
451
476
// Step 3: Register with PLC directory
···
457
482
// Step 4: Initialize PDS
458
483
console.log(`Initializing PDS at ${opts.pds}...`)
459
484
const privateKeyHex = bytesToHex(keyPair.privateKey)
460
-
await initializePds(opts.pds, did, privateKeyHex, handle)
485
+
await initializePds(opts.pds, did, privateKeyHex, fullHandle)
461
486
console.log(' PDS initialized!')
462
487
console.log('')
463
488
489
+
// Step 4b: Register handle -> DID mapping
490
+
console.log(`Registering handle mapping...`)
491
+
await registerHandle(opts.pds, opts.handle, did)
492
+
console.log(` Handle ${opts.handle} -> ${did}`)
493
+
console.log('')
494
+
464
495
// Step 5: Notify relay
465
496
const pdsHostname = new URL(opts.pds).host
466
497
console.log(`Notifying relay at ${opts.relayUrl}...`)
···
472
503
473
504
// Step 6: Save credentials
474
505
const credentials = {
475
-
handle,
506
+
handle: fullHandle,
476
507
did,
477
508
privateKeyHex: bytesToHex(keyPair.privateKey),
478
509
didKey,
···
480
511
createdAt: new Date().toISOString()
481
512
}
482
513
483
-
const credentialsFile = `./credentials.json`
514
+
const credentialsFile = `./credentials-${opts.handle}.json`
484
515
saveCredentials(credentialsFile, credentials)
485
516
486
517
// Final output
487
518
console.log('Setup Complete!')
488
519
console.log('===============')
489
-
console.log(`Handle: ${handle}`)
520
+
console.log(`Handle: ${fullHandle}`)
490
521
console.log(`DID: ${did}`)
491
522
console.log(`PDS: ${opts.pds}`)
492
523
console.log('')
+77
-8
src/pds.js
+77
-8
src/pds.js
···
878
878
'/get-registered-dids': {
879
879
handler: (pds, req, url) => pds.handleGetRegisteredDids()
880
880
},
881
+
'/register-handle': {
882
+
method: 'POST',
883
+
handler: (pds, req, url) => pds.handleRegisterHandle(req)
884
+
},
885
+
'/resolve-handle': {
886
+
handler: (pds, req, url) => pds.handleResolveHandle(url)
887
+
},
881
888
'/repo-info': {
882
889
handler: (pds, req, url) => pds.handleRepoInfo()
883
890
},
···
1255
1262
return Response.json({ dids: registeredDids })
1256
1263
}
1257
1264
1265
+
async handleRegisterHandle(request) {
1266
+
const body = await request.json()
1267
+
const { handle, did } = body
1268
+
if (!handle || !did) {
1269
+
return Response.json({ error: 'missing handle or did' }, { status: 400 })
1270
+
}
1271
+
const handleMap = await this.state.storage.get('handleMap') || {}
1272
+
handleMap[handle] = did
1273
+
await this.state.storage.put('handleMap', handleMap)
1274
+
return Response.json({ ok: true })
1275
+
}
1276
+
1277
+
async handleResolveHandle(url) {
1278
+
const handle = url.searchParams.get('handle')
1279
+
if (!handle) {
1280
+
return Response.json({ error: 'missing handle' }, { status: 400 })
1281
+
}
1282
+
const handleMap = await this.state.storage.get('handleMap') || {}
1283
+
const did = handleMap[handle]
1284
+
if (!did) {
1285
+
return Response.json({ error: 'handle not found' }, { status: 404 })
1286
+
}
1287
+
return Response.json({ did })
1288
+
}
1289
+
1258
1290
async handleRepoInfo() {
1259
1291
const head = await this.state.storage.get('head')
1260
1292
const rev = await this.state.storage.get('rev')
···
1536
1568
}
1537
1569
}
1538
1570
1571
+
// Extract subdomain from hostname (e.g., "alice" from "alice.foo.workers.dev")
1572
+
function getSubdomain(hostname) {
1573
+
const parts = hostname.split('.')
1574
+
// workers.dev domains: [subdomain?].[worker-name].[account].workers.dev
1575
+
// If more than 4 parts, first part(s) are user subdomain
1576
+
if (parts.length > 4 && parts.slice(-2).join('.') === 'workers.dev') {
1577
+
return parts.slice(0, -4).join('.')
1578
+
}
1579
+
// Custom domains: check if there's a subdomain before the base
1580
+
// For now, assume no subdomain on custom domains
1581
+
return null
1582
+
}
1583
+
1539
1584
async function handleRequest(request, env) {
1540
1585
const url = new URL(request.url)
1586
+
const subdomain = getSubdomain(url.hostname)
1541
1587
1542
-
// Endpoints that don't require ?did= param (for relay/federation)
1543
-
if (url.pathname === '/.well-known/atproto-did' ||
1544
-
url.pathname === '/xrpc/com.atproto.server.describeServer') {
1545
-
const did = url.searchParams.get('did') || 'default'
1546
-
const id = env.PDS.idFromName(did)
1547
-
const pds = env.PDS.get(id)
1548
-
// Pass hostname for describeServer
1588
+
// Handle resolution via subdomain
1589
+
if (url.pathname === '/.well-known/atproto-did') {
1590
+
if (!subdomain) {
1591
+
// Bare domain - no user here
1592
+
return new Response('No user at bare domain. Use a subdomain like alice.' + url.hostname, { status: 404 })
1593
+
}
1594
+
// Look up handle -> DID in default DO
1595
+
const defaultId = env.PDS.idFromName('default')
1596
+
const defaultPds = env.PDS.get(defaultId)
1597
+
const resolveRes = await defaultPds.fetch(
1598
+
new Request(`http://internal/resolve-handle?handle=${encodeURIComponent(subdomain)}`)
1599
+
)
1600
+
if (!resolveRes.ok) {
1601
+
return new Response('Handle not found', { status: 404 })
1602
+
}
1603
+
const { did } = await resolveRes.json()
1604
+
return new Response(did, { headers: { 'Content-Type': 'text/plain' } })
1605
+
}
1606
+
1607
+
// describeServer - works on bare domain
1608
+
if (url.pathname === '/xrpc/com.atproto.server.describeServer') {
1609
+
const defaultId = env.PDS.idFromName('default')
1610
+
const defaultPds = env.PDS.get(defaultId)
1549
1611
const newReq = new Request(request.url, {
1550
1612
method: request.method,
1551
1613
headers: { ...Object.fromEntries(request.headers), 'x-hostname': url.hostname }
1552
1614
})
1553
-
return pds.fetch(newReq)
1615
+
return defaultPds.fetch(newReq)
1616
+
}
1617
+
1618
+
// Handle registration routes - go to default DO
1619
+
if (url.pathname === '/register-handle' || url.pathname === '/resolve-handle') {
1620
+
const defaultId = env.PDS.idFromName('default')
1621
+
const defaultPds = env.PDS.get(defaultId)
1622
+
return defaultPds.fetch(request)
1554
1623
}
1555
1624
1556
1625
// subscribeRepos WebSocket - route to default instance for firehose