+13
-11
scripts/setup.js
+13
-11
scripts/setup.js
···
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)
···
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',
···
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
···
511
createdAt: new Date().toISOString()
512
}
513
514
-
const credentialsFile = `./credentials-${opts.handle}.json`
515
saveCredentials(credentialsFile, credentials)
516
517
// Final output
···
35
}
36
}
37
38
+
if (!opts.pds) {
39
+
console.error('Usage: node scripts/setup.js --pds <pds-url> [--handle <subdomain>]')
40
console.error('')
41
console.error('Options:')
42
console.error(' --pds PDS URL (e.g., "https://atproto-pds.chad-53c.workers.dev")')
43
+
console.error(' --handle Subdomain handle (e.g., "alice") - optional, uses bare hostname if omitted')
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)
···
289
async function createGenesisOperation(opts) {
290
const { didKey, handle, pdsUrl, cryptoKey } = opts
291
292
+
// Build full handle: subdomain.pds-hostname, or just pds-hostname if no subdomain
293
const pdsHost = new URL(pdsUrl).host
294
+
const fullHandle = handle ? `${handle}.${pdsHost}` : pdsHost
295
296
const operation = {
297
type: 'plc_operation',
···
486
console.log(' PDS initialized!')
487
console.log('')
488
489
+
// Step 4b: Register handle -> DID mapping (only for subdomain handles)
490
+
if (opts.handle) {
491
+
console.log(`Registering handle mapping...`)
492
+
await registerHandle(opts.pds, opts.handle, did)
493
+
console.log(` Handle ${opts.handle} -> ${did}`)
494
+
console.log('')
495
+
}
496
497
// Step 5: Notify relay
498
const pdsHostname = new URL(opts.pds).host
···
513
createdAt: new Date().toISOString()
514
}
515
516
+
const credentialsFile = `./credentials-${opts.handle || new URL(opts.pds).host}.json`
517
saveCredentials(credentialsFile, credentials)
518
519
// Final output
+267
scripts/update-did.js
+267
scripts/update-did.js
···
···
1
+
#!/usr/bin/env node
2
+
3
+
/**
4
+
* Update DID handle and PDS endpoint
5
+
*
6
+
* Usage: node scripts/update-did.js --credentials <file> --new-handle <handle> --new-pds <url>
7
+
*/
8
+
9
+
import { webcrypto } from 'crypto'
10
+
import { readFileSync, writeFileSync } from 'fs'
11
+
12
+
// === ARGUMENT PARSING ===
13
+
14
+
function parseArgs() {
15
+
const args = process.argv.slice(2)
16
+
const opts = {
17
+
credentials: null,
18
+
newHandle: null,
19
+
newPds: null,
20
+
plcUrl: 'https://plc.directory'
21
+
}
22
+
23
+
for (let i = 0; i < args.length; i++) {
24
+
if (args[i] === '--credentials' && args[i + 1]) {
25
+
opts.credentials = args[++i]
26
+
} else if (args[i] === '--new-handle' && args[i + 1]) {
27
+
opts.newHandle = args[++i]
28
+
} else if (args[i] === '--new-pds' && args[i + 1]) {
29
+
opts.newPds = args[++i]
30
+
} else if (args[i] === '--plc-url' && args[i + 1]) {
31
+
opts.plcUrl = args[++i]
32
+
}
33
+
}
34
+
35
+
if (!opts.credentials || !opts.newHandle || !opts.newPds) {
36
+
console.error('Usage: node scripts/update-did.js --credentials <file> --new-handle <handle> --new-pds <url>')
37
+
process.exit(1)
38
+
}
39
+
40
+
return opts
41
+
}
42
+
43
+
// === CRYPTO HELPERS ===
44
+
45
+
function hexToBytes(hex) {
46
+
const bytes = new Uint8Array(hex.length / 2)
47
+
for (let i = 0; i < hex.length; i += 2) {
48
+
bytes[i / 2] = parseInt(hex.substr(i, 2), 16)
49
+
}
50
+
return bytes
51
+
}
52
+
53
+
function bytesToHex(bytes) {
54
+
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('')
55
+
}
56
+
57
+
async function importPrivateKey(privateKeyBytes) {
58
+
const pkcs8Prefix = new Uint8Array([
59
+
0x30, 0x41, 0x02, 0x01, 0x00, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48,
60
+
0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03,
61
+
0x01, 0x07, 0x04, 0x27, 0x30, 0x25, 0x02, 0x01, 0x01, 0x04, 0x20
62
+
])
63
+
64
+
const pkcs8 = new Uint8Array(pkcs8Prefix.length + 32)
65
+
pkcs8.set(pkcs8Prefix)
66
+
pkcs8.set(privateKeyBytes, pkcs8Prefix.length)
67
+
68
+
return webcrypto.subtle.importKey(
69
+
'pkcs8',
70
+
pkcs8,
71
+
{ name: 'ECDSA', namedCurve: 'P-256' },
72
+
false,
73
+
['sign']
74
+
)
75
+
}
76
+
77
+
// === CBOR ENCODING ===
78
+
79
+
function cborEncodeKey(key) {
80
+
const bytes = new TextEncoder().encode(key)
81
+
const parts = []
82
+
const mt = 3 << 5
83
+
if (bytes.length < 24) {
84
+
parts.push(mt | bytes.length)
85
+
} else if (bytes.length < 256) {
86
+
parts.push(mt | 24, bytes.length)
87
+
}
88
+
parts.push(...bytes)
89
+
return new Uint8Array(parts)
90
+
}
91
+
92
+
function compareBytes(a, b) {
93
+
const minLen = Math.min(a.length, b.length)
94
+
for (let i = 0; i < minLen; i++) {
95
+
if (a[i] !== b[i]) return a[i] - b[i]
96
+
}
97
+
return a.length - b.length
98
+
}
99
+
100
+
function cborEncode(value) {
101
+
const parts = []
102
+
103
+
function encode(val) {
104
+
if (val === null) {
105
+
parts.push(0xf6)
106
+
} else if (typeof val === 'string') {
107
+
const bytes = new TextEncoder().encode(val)
108
+
encodeHead(3, bytes.length)
109
+
parts.push(...bytes)
110
+
} else if (typeof val === 'number') {
111
+
if (Number.isInteger(val) && val >= 0) {
112
+
encodeHead(0, val)
113
+
}
114
+
} else if (val instanceof Uint8Array) {
115
+
encodeHead(2, val.length)
116
+
parts.push(...val)
117
+
} else if (Array.isArray(val)) {
118
+
encodeHead(4, val.length)
119
+
for (const item of val) encode(item)
120
+
} else if (typeof val === 'object') {
121
+
const keys = Object.keys(val)
122
+
const keysSorted = keys.sort((a, b) => compareBytes(cborEncodeKey(a), cborEncodeKey(b)))
123
+
encodeHead(5, keysSorted.length)
124
+
for (const key of keysSorted) {
125
+
encode(key)
126
+
encode(val[key])
127
+
}
128
+
}
129
+
}
130
+
131
+
function encodeHead(majorType, length) {
132
+
const mt = majorType << 5
133
+
if (length < 24) {
134
+
parts.push(mt | length)
135
+
} else if (length < 256) {
136
+
parts.push(mt | 24, length)
137
+
} else if (length < 65536) {
138
+
parts.push(mt | 25, length >> 8, length & 0xff)
139
+
}
140
+
}
141
+
142
+
encode(value)
143
+
return new Uint8Array(parts)
144
+
}
145
+
146
+
// === SIGNING ===
147
+
148
+
const P256_N = BigInt('0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551')
149
+
150
+
function ensureLowS(sig) {
151
+
const halfN = P256_N / 2n
152
+
const r = sig.slice(0, 32)
153
+
const s = sig.slice(32, 64)
154
+
let sInt = BigInt('0x' + bytesToHex(s))
155
+
156
+
if (sInt > halfN) {
157
+
sInt = P256_N - sInt
158
+
const newS = hexToBytes(sInt.toString(16).padStart(64, '0'))
159
+
const result = new Uint8Array(64)
160
+
result.set(r)
161
+
result.set(newS, 32)
162
+
return result
163
+
}
164
+
return sig
165
+
}
166
+
167
+
function base64UrlEncode(bytes) {
168
+
const binary = String.fromCharCode(...bytes)
169
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
170
+
}
171
+
172
+
async function signPlcOperation(operation, privateKey) {
173
+
const { sig, ...opWithoutSig } = operation
174
+
const encoded = cborEncode(opWithoutSig)
175
+
176
+
const signature = await webcrypto.subtle.sign(
177
+
{ name: 'ECDSA', hash: 'SHA-256' },
178
+
privateKey,
179
+
encoded
180
+
)
181
+
182
+
const sigBytes = ensureLowS(new Uint8Array(signature))
183
+
return base64UrlEncode(sigBytes)
184
+
}
185
+
186
+
// === MAIN ===
187
+
188
+
async function main() {
189
+
const opts = parseArgs()
190
+
191
+
// Load credentials
192
+
const creds = JSON.parse(readFileSync(opts.credentials, 'utf-8'))
193
+
console.log(`Updating DID: ${creds.did}`)
194
+
console.log(` Old handle: ${creds.handle}`)
195
+
console.log(` New handle: ${opts.newHandle}`)
196
+
console.log(` New PDS: ${opts.newPds}`)
197
+
console.log('')
198
+
199
+
// Fetch current operation log
200
+
console.log('Fetching current PLC operation log...')
201
+
const logRes = await fetch(`${opts.plcUrl}/${creds.did}/log/audit`)
202
+
if (!logRes.ok) {
203
+
throw new Error(`Failed to fetch PLC log: ${logRes.status}`)
204
+
}
205
+
const log = await logRes.json()
206
+
const lastOp = log[log.length - 1]
207
+
console.log(` Found ${log.length} operations`)
208
+
console.log(` Last CID: ${lastOp.cid}`)
209
+
console.log('')
210
+
211
+
// Import private key
212
+
const privateKey = await importPrivateKey(hexToBytes(creds.privateKeyHex))
213
+
214
+
// Create new operation
215
+
const newOp = {
216
+
type: 'plc_operation',
217
+
rotationKeys: lastOp.operation.rotationKeys,
218
+
verificationMethods: lastOp.operation.verificationMethods,
219
+
alsoKnownAs: [`at://${opts.newHandle}`],
220
+
services: {
221
+
atproto_pds: {
222
+
type: 'AtprotoPersonalDataServer',
223
+
endpoint: opts.newPds
224
+
}
225
+
},
226
+
prev: lastOp.cid
227
+
}
228
+
229
+
// Sign the operation
230
+
console.log('Signing new operation...')
231
+
newOp.sig = await signPlcOperation(newOp, privateKey)
232
+
233
+
// Submit to PLC
234
+
console.log('Submitting to PLC directory...')
235
+
const submitRes = await fetch(`${opts.plcUrl}/${creds.did}`, {
236
+
method: 'POST',
237
+
headers: { 'Content-Type': 'application/json' },
238
+
body: JSON.stringify(newOp)
239
+
})
240
+
241
+
if (!submitRes.ok) {
242
+
const text = await submitRes.text()
243
+
throw new Error(`PLC update failed: ${submitRes.status} ${text}`)
244
+
}
245
+
246
+
console.log(' Updated successfully!')
247
+
console.log('')
248
+
249
+
// Update credentials file
250
+
creds.handle = opts.newHandle
251
+
creds.pdsUrl = opts.newPds
252
+
writeFileSync(opts.credentials, JSON.stringify(creds, null, 2))
253
+
console.log(`Updated credentials file: ${opts.credentials}`)
254
+
255
+
// Verify
256
+
console.log('')
257
+
console.log('Verifying...')
258
+
const verifyRes = await fetch(`${opts.plcUrl}/${creds.did}`)
259
+
const didDoc = await verifyRes.json()
260
+
console.log(` alsoKnownAs: ${didDoc.alsoKnownAs}`)
261
+
console.log(` PDS endpoint: ${didDoc.service[0].serviceEndpoint}`)
262
+
}
263
+
264
+
main().catch(err => {
265
+
console.error('Error:', err.message)
266
+
process.exit(1)
267
+
})
+4
-6
src/pds.js
+4
-6
src/pds.js
···
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 })
···
1585
const url = new URL(request.url)
1586
const subdomain = getSubdomain(url.hostname)
1587
1588
+
// Handle resolution via subdomain or bare domain
1589
if (url.pathname === '/.well-known/atproto-did') {
1590
// Look up handle -> DID in default DO
1591
+
// Use subdomain if present, otherwise try bare hostname as handle
1592
+
const handleToResolve = subdomain || url.hostname
1593
const defaultId = env.PDS.idFromName('default')
1594
const defaultPds = env.PDS.get(defaultId)
1595
const resolveRes = await defaultPds.fetch(
1596
+
new Request(`http://internal/resolve-handle?handle=${encodeURIComponent(handleToResolve)}`)
1597
)
1598
if (!resolveRes.ok) {
1599
return new Response('Handle not found', { status: 404 })