+183
-2
scripts/setup.js
+183
-2
scripts/setup.js
···
148
148
return result
149
149
}
150
150
151
+
// === CBOR ENCODING (minimal for PLC operations) ===
152
+
153
+
function cborEncode(value) {
154
+
const parts = []
155
+
156
+
function encode(val) {
157
+
if (val === null) {
158
+
parts.push(0xf6)
159
+
} else if (typeof val === 'string') {
160
+
const bytes = new TextEncoder().encode(val)
161
+
encodeHead(3, bytes.length)
162
+
parts.push(...bytes)
163
+
} else if (typeof val === 'number') {
164
+
if (Number.isInteger(val) && val >= 0) {
165
+
encodeHead(0, val)
166
+
}
167
+
} else if (val instanceof Uint8Array) {
168
+
encodeHead(2, val.length)
169
+
parts.push(...val)
170
+
} else if (Array.isArray(val)) {
171
+
encodeHead(4, val.length)
172
+
for (const item of val) encode(item)
173
+
} else if (typeof val === 'object') {
174
+
const keys = Object.keys(val).sort()
175
+
encodeHead(5, keys.length)
176
+
for (const key of keys) {
177
+
encode(key)
178
+
encode(val[key])
179
+
}
180
+
}
181
+
}
182
+
183
+
function encodeHead(majorType, length) {
184
+
const mt = majorType << 5
185
+
if (length < 24) {
186
+
parts.push(mt | length)
187
+
} else if (length < 256) {
188
+
parts.push(mt | 24, length)
189
+
} else if (length < 65536) {
190
+
parts.push(mt | 25, length >> 8, length & 0xff)
191
+
}
192
+
}
193
+
194
+
encode(value)
195
+
return new Uint8Array(parts)
196
+
}
197
+
198
+
// === HASHING ===
199
+
200
+
async function sha256(data) {
201
+
const hash = await webcrypto.subtle.digest('SHA-256', data)
202
+
return new Uint8Array(hash)
203
+
}
204
+
205
+
// === PLC OPERATIONS ===
206
+
207
+
async function signPlcOperation(operation, privateKey) {
208
+
// Encode operation without sig field
209
+
const { sig, ...opWithoutSig } = operation
210
+
const encoded = cborEncode(opWithoutSig)
211
+
212
+
// Sign with P-256
213
+
const signature = await webcrypto.subtle.sign(
214
+
{ name: 'ECDSA', hash: 'SHA-256' },
215
+
privateKey,
216
+
encoded
217
+
)
218
+
219
+
// Convert to low-S form and base64url encode
220
+
const sigBytes = ensureLowS(new Uint8Array(signature))
221
+
return base64UrlEncode(sigBytes)
222
+
}
223
+
224
+
function ensureLowS(sig) {
225
+
// P-256 order N
226
+
const N = BigInt('0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551')
227
+
const halfN = N / 2n
228
+
229
+
const r = sig.slice(0, 32)
230
+
const s = sig.slice(32, 64)
231
+
232
+
// Convert s to BigInt
233
+
let sInt = BigInt('0x' + bytesToHex(s))
234
+
235
+
// If s > N/2, replace with N - s
236
+
if (sInt > halfN) {
237
+
sInt = N - sInt
238
+
const newS = hexToBytes(sInt.toString(16).padStart(64, '0'))
239
+
const result = new Uint8Array(64)
240
+
result.set(r)
241
+
result.set(newS, 32)
242
+
return result
243
+
}
244
+
245
+
return sig
246
+
}
247
+
248
+
function hexToBytes(hex) {
249
+
const bytes = new Uint8Array(hex.length / 2)
250
+
for (let i = 0; i < hex.length; i += 2) {
251
+
bytes[i / 2] = parseInt(hex.substr(i, 2), 16)
252
+
}
253
+
return bytes
254
+
}
255
+
256
+
function base64UrlEncode(bytes) {
257
+
const binary = String.fromCharCode(...bytes)
258
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
259
+
}
260
+
261
+
async function createGenesisOperation(opts) {
262
+
const { didKey, handle, pdsUrl, cryptoKey } = opts
263
+
264
+
// Build the full handle
265
+
const pdsHost = new URL(pdsUrl).host
266
+
const fullHandle = `${handle}.${pdsHost}`
267
+
268
+
const operation = {
269
+
type: 'plc_operation',
270
+
rotationKeys: [didKey],
271
+
verificationMethods: {
272
+
atproto: didKey
273
+
},
274
+
alsoKnownAs: [`at://${fullHandle}`],
275
+
services: {
276
+
atproto_pds: {
277
+
type: 'AtprotoPersonalDataServer',
278
+
endpoint: pdsUrl
279
+
}
280
+
},
281
+
prev: null
282
+
}
283
+
284
+
// Sign the operation
285
+
operation.sig = await signPlcOperation(operation, cryptoKey)
286
+
287
+
return { operation, fullHandle }
288
+
}
289
+
290
+
async function deriveDidFromOperation(operation) {
291
+
const { sig, ...opWithoutSig } = operation
292
+
const encoded = cborEncode(opWithoutSig)
293
+
const hash = await sha256(encoded)
294
+
// DID is base32 of first 24 bytes of hash
295
+
return 'did:plc:' + base32Encode(hash.slice(0, 24))
296
+
}
297
+
298
+
function base32Encode(bytes) {
299
+
const alphabet = 'abcdefghijklmnopqrstuvwxyz234567'
300
+
let result = ''
301
+
let bits = 0
302
+
let value = 0
303
+
304
+
for (const byte of bytes) {
305
+
value = (value << 8) | byte
306
+
bits += 8
307
+
while (bits >= 5) {
308
+
bits -= 5
309
+
result += alphabet[(value >> bits) & 31]
310
+
}
311
+
}
312
+
313
+
if (bits > 0) {
314
+
result += alphabet[(value << (5 - bits)) & 31]
315
+
}
316
+
317
+
return result
318
+
}
319
+
151
320
// === MAIN ===
152
321
153
322
async function main() {
···
164
333
const keyPair = await generateP256Keypair()
165
334
const didKey = publicKeyToDidKey(keyPair.publicKey)
166
335
console.log(` did:key: ${didKey}`)
167
-
console.log(` Private key: ${bytesToHex(keyPair.privateKey)}`)
336
+
console.log('')
337
+
338
+
// Step 2: Create genesis operation
339
+
console.log('Creating PLC genesis operation...')
340
+
const { operation, fullHandle } = await createGenesisOperation({
341
+
didKey,
342
+
handle: opts.handle,
343
+
pdsUrl: opts.pds,
344
+
cryptoKey: keyPair.cryptoKey
345
+
})
346
+
const did = await deriveDidFromOperation(operation)
347
+
console.log(` DID: ${did}`)
348
+
console.log(` Handle: ${fullHandle}`)
168
349
console.log('')
169
350
170
-
// TODO: Register DID:PLC
351
+
// TODO: Register with plc.directory
171
352
// TODO: Initialize PDS
172
353
// TODO: Notify relay
173
354
}