+46
-15
scripts/setup.js
+46
-15
scripts/setup.js
···
17
function parseArgs() {
18
const args = process.argv.slice(2)
19
const opts = {
20
pds: null,
21
plcUrl: 'https://plc.directory',
22
relayUrl: 'https://bsky.network'
23
}
24
25
for (let i = 0; i < args.length; i++) {
26
-
if (args[i] === '--pds' && args[i + 1]) {
27
opts.pds = args[++i]
28
} else if (args[i] === '--plc-url' && args[i + 1]) {
29
opts.plcUrl = args[++i]
···
32
}
33
}
34
35
-
if (!opts.pds) {
36
-
console.error('Usage: node scripts/setup.js --pds <pds-url>')
37
console.error('')
38
console.error('Options:')
39
console.error(' --pds PDS URL (e.g., "https://atproto-pds.chad-53c.workers.dev")')
40
console.error(' --plc-url PLC directory URL (default: https://plc.directory)')
41
console.error(' --relay-url Relay URL (default: https://bsky.network)')
42
process.exit(1)
43
}
44
-
45
-
// Handle is just the PDS hostname
46
-
opts.handle = new URL(opts.pds).host
47
48
return opts
49
}
···
288
async function createGenesisOperation(opts) {
289
const { didKey, handle, pdsUrl, cryptoKey } = opts
290
291
-
// Handle is already the full hostname
292
const operation = {
293
type: 'plc_operation',
294
rotationKeys: [didKey],
295
verificationMethods: {
296
atproto: didKey
297
},
298
-
alsoKnownAs: [`at://${handle}`],
299
services: {
300
atproto_pds: {
301
type: 'AtprotoPersonalDataServer',
···
308
// Sign the operation
309
operation.sig = await signPlcOperation(operation, cryptoKey)
310
311
-
return { operation, handle }
312
}
313
314
async function deriveDidFromOperation(operation) {
···
387
return response.json()
388
}
389
390
// === RELAY NOTIFICATION ===
391
392
async function notifyRelay(relayUrl, pdsHostname) {
···
437
438
// Step 2: Create genesis operation
439
console.log('Creating PLC genesis operation...')
440
-
const { operation, handle } = await createGenesisOperation({
441
didKey,
442
handle: opts.handle,
443
pdsUrl: opts.pds,
···
445
})
446
const did = await deriveDidFromOperation(operation)
447
console.log(` DID: ${did}`)
448
-
console.log(` Handle: ${handle}`)
449
console.log('')
450
451
// Step 3: Register with PLC directory
···
457
// Step 4: Initialize PDS
458
console.log(`Initializing PDS at ${opts.pds}...`)
459
const privateKeyHex = bytesToHex(keyPair.privateKey)
460
-
await initializePds(opts.pds, did, privateKeyHex, handle)
461
console.log(' PDS initialized!')
462
console.log('')
463
464
// Step 5: Notify relay
465
const pdsHostname = new URL(opts.pds).host
466
console.log(`Notifying relay at ${opts.relayUrl}...`)
···
472
473
// Step 6: Save credentials
474
const credentials = {
475
-
handle,
476
did,
477
privateKeyHex: bytesToHex(keyPair.privateKey),
478
didKey,
···
480
createdAt: new Date().toISOString()
481
}
482
483
-
const credentialsFile = `./credentials.json`
484
saveCredentials(credentialsFile, credentials)
485
486
// Final output
487
console.log('Setup Complete!')
488
console.log('===============')
489
-
console.log(`Handle: ${handle}`)
490
console.log(`DID: ${did}`)
491
console.log(`PDS: ${opts.pds}`)
492
console.log('')
···
17
function parseArgs() {
18
const args = process.argv.slice(2)
19
const opts = {
20
+
handle: null,
21
pds: null,
22
plcUrl: 'https://plc.directory',
23
relayUrl: 'https://bsky.network'
24
}
25
26
for (let i = 0; i < args.length; i++) {
27
+
if (args[i] === '--handle' && args[i + 1]) {
28
+
opts.handle = args[++i]
29
+
} else if (args[i] === '--pds' && args[i + 1]) {
30
opts.pds = args[++i]
31
} else if (args[i] === '--plc-url' && args[i + 1]) {
32
opts.plcUrl = args[++i]
···
35
}
36
}
37
38
+
if (!opts.handle || !opts.pds) {
39
+
console.error('Usage: node scripts/setup.js --handle <subdomain> --pds <pds-url>')
40
console.error('')
41
console.error('Options:')
42
+
console.error(' --handle Subdomain handle (e.g., "alice")')
43
console.error(' --pds PDS URL (e.g., "https://atproto-pds.chad-53c.workers.dev")')
44
console.error(' --plc-url PLC directory URL (default: https://plc.directory)')
45
console.error(' --relay-url Relay URL (default: https://bsky.network)')
46
process.exit(1)
47
}
48
49
return opts
50
}
···
289
async function createGenesisOperation(opts) {
290
const { didKey, handle, pdsUrl, cryptoKey } = opts
291
292
+
// Build full handle: subdomain.pds-hostname
293
+
const pdsHost = new URL(pdsUrl).host
294
+
const fullHandle = `${handle}.${pdsHost}`
295
+
296
const operation = {
297
type: 'plc_operation',
298
rotationKeys: [didKey],
299
verificationMethods: {
300
atproto: didKey
301
},
302
+
alsoKnownAs: [`at://${fullHandle}`],
303
services: {
304
atproto_pds: {
305
type: 'AtprotoPersonalDataServer',
···
312
// Sign the operation
313
operation.sig = await signPlcOperation(operation, cryptoKey)
314
315
+
return { operation, fullHandle }
316
}
317
318
async function deriveDidFromOperation(operation) {
···
391
return response.json()
392
}
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
+
415
// === RELAY NOTIFICATION ===
416
417
async function notifyRelay(relayUrl, pdsHostname) {
···
462
463
// Step 2: Create genesis operation
464
console.log('Creating PLC genesis operation...')
465
+
const { operation, fullHandle } = await createGenesisOperation({
466
didKey,
467
handle: opts.handle,
468
pdsUrl: opts.pds,
···
470
})
471
const did = await deriveDidFromOperation(operation)
472
console.log(` DID: ${did}`)
473
+
console.log(` Handle: ${fullHandle}`)
474
console.log('')
475
476
// Step 3: Register with PLC directory
···
482
// Step 4: Initialize PDS
483
console.log(`Initializing PDS at ${opts.pds}...`)
484
const privateKeyHex = bytesToHex(keyPair.privateKey)
485
+
await initializePds(opts.pds, did, privateKeyHex, fullHandle)
486
console.log(' PDS initialized!')
487
console.log('')
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
+
495
// Step 5: Notify relay
496
const pdsHostname = new URL(opts.pds).host
497
console.log(`Notifying relay at ${opts.relayUrl}...`)
···
503
504
// Step 6: Save credentials
505
const credentials = {
506
+
handle: fullHandle,
507
did,
508
privateKeyHex: bytesToHex(keyPair.privateKey),
509
didKey,
···
511
createdAt: new Date().toISOString()
512
}
513
514
+
const credentialsFile = `./credentials-${opts.handle}.json`
515
saveCredentials(credentialsFile, credentials)
516
517
// Final output
518
console.log('Setup Complete!')
519
console.log('===============')
520
+
console.log(`Handle: ${fullHandle}`)
521
console.log(`DID: ${did}`)
522
console.log(`PDS: ${opts.pds}`)
523
console.log('')
+77
-8
src/pds.js
+77
-8
src/pds.js
···
878
'/get-registered-dids': {
879
handler: (pds, req, url) => pds.handleGetRegisteredDids()
880
},
881
'/repo-info': {
882
handler: (pds, req, url) => pds.handleRepoInfo()
883
},
···
1255
return Response.json({ dids: registeredDids })
1256
}
1257
1258
async handleRepoInfo() {
1259
const head = await this.state.storage.get('head')
1260
const rev = await this.state.storage.get('rev')
···
1536
}
1537
}
1538
1539
async function handleRequest(request, env) {
1540
const url = new URL(request.url)
1541
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
1549
const newReq = new Request(request.url, {
1550
method: request.method,
1551
headers: { ...Object.fromEntries(request.headers), 'x-hostname': url.hostname }
1552
})
1553
-
return pds.fetch(newReq)
1554
}
1555
1556
// subscribeRepos WebSocket - route to default instance for firehose
···
878
'/get-registered-dids': {
879
handler: (pds, req, url) => pds.handleGetRegisteredDids()
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
+
},
888
'/repo-info': {
889
handler: (pds, req, url) => pds.handleRepoInfo()
890
},
···
1262
return Response.json({ dids: registeredDids })
1263
}
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
+
1290
async handleRepoInfo() {
1291
const head = await this.state.storage.get('head')
1292
const rev = await this.state.storage.get('rev')
···
1568
}
1569
}
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
+
1584
async function handleRequest(request, env) {
1585
const url = new URL(request.url)
1586
+
const subdomain = getSubdomain(url.hostname)
1587
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)
1611
const newReq = new Request(request.url, {
1612
method: request.method,
1613
headers: { ...Object.fromEntries(request.headers), 'x-hostname': url.hostname }
1614
})
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)
1623
}
1624
1625
// subscribeRepos WebSocket - route to default instance for firehose