+24
biome.json
+24
biome.json
···
1
+
{
2
+
"$schema": "https://biomejs.dev/schemas/2.3.11/schema.json",
3
+
"vcs": {
4
+
"enabled": true,
5
+
"clientKind": "git",
6
+
"useIgnoreFile": true
7
+
},
8
+
"formatter": {
9
+
"enabled": true,
10
+
"indentStyle": "space",
11
+
"indentWidth": 2
12
+
},
13
+
"linter": {
14
+
"enabled": true,
15
+
"rules": {
16
+
"recommended": true
17
+
}
18
+
},
19
+
"javascript": {
20
+
"formatter": {
21
+
"quoteStyle": "single"
22
+
}
23
+
}
24
+
}
+164
package-lock.json
+164
package-lock.json
···
8
8
"name": "pds.js",
9
9
"version": "0.1.0",
10
10
"devDependencies": {
11
+
"@biomejs/biome": "^2.3.11",
11
12
"wrangler": "^4.54.0"
13
+
}
14
+
},
15
+
"node_modules/@biomejs/biome": {
16
+
"version": "2.3.11",
17
+
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.11.tgz",
18
+
"integrity": "sha512-/zt+6qazBWguPG6+eWmiELqO+9jRsMZ/DBU3lfuU2ngtIQYzymocHhKiZRyrbra4aCOoyTg/BmY+6WH5mv9xmQ==",
19
+
"dev": true,
20
+
"license": "MIT OR Apache-2.0",
21
+
"bin": {
22
+
"biome": "bin/biome"
23
+
},
24
+
"engines": {
25
+
"node": ">=14.21.3"
26
+
},
27
+
"funding": {
28
+
"type": "opencollective",
29
+
"url": "https://opencollective.com/biome"
30
+
},
31
+
"optionalDependencies": {
32
+
"@biomejs/cli-darwin-arm64": "2.3.11",
33
+
"@biomejs/cli-darwin-x64": "2.3.11",
34
+
"@biomejs/cli-linux-arm64": "2.3.11",
35
+
"@biomejs/cli-linux-arm64-musl": "2.3.11",
36
+
"@biomejs/cli-linux-x64": "2.3.11",
37
+
"@biomejs/cli-linux-x64-musl": "2.3.11",
38
+
"@biomejs/cli-win32-arm64": "2.3.11",
39
+
"@biomejs/cli-win32-x64": "2.3.11"
40
+
}
41
+
},
42
+
"node_modules/@biomejs/cli-darwin-arm64": {
43
+
"version": "2.3.11",
44
+
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.11.tgz",
45
+
"integrity": "sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA==",
46
+
"cpu": [
47
+
"arm64"
48
+
],
49
+
"dev": true,
50
+
"license": "MIT OR Apache-2.0",
51
+
"optional": true,
52
+
"os": [
53
+
"darwin"
54
+
],
55
+
"engines": {
56
+
"node": ">=14.21.3"
57
+
}
58
+
},
59
+
"node_modules/@biomejs/cli-darwin-x64": {
60
+
"version": "2.3.11",
61
+
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.11.tgz",
62
+
"integrity": "sha512-fh7nnvbweDPm2xEmFjfmq7zSUiox88plgdHF9OIW4i99WnXrAC3o2P3ag9judoUMv8FCSUnlwJCM1B64nO5Fbg==",
63
+
"cpu": [
64
+
"x64"
65
+
],
66
+
"dev": true,
67
+
"license": "MIT OR Apache-2.0",
68
+
"optional": true,
69
+
"os": [
70
+
"darwin"
71
+
],
72
+
"engines": {
73
+
"node": ">=14.21.3"
74
+
}
75
+
},
76
+
"node_modules/@biomejs/cli-linux-arm64": {
77
+
"version": "2.3.11",
78
+
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.11.tgz",
79
+
"integrity": "sha512-l4xkGa9E7Uc0/05qU2lMYfN1H+fzzkHgaJoy98wO+b/7Gl78srbCRRgwYSW+BTLixTBrM6Ede5NSBwt7rd/i6g==",
80
+
"cpu": [
81
+
"arm64"
82
+
],
83
+
"dev": true,
84
+
"license": "MIT OR Apache-2.0",
85
+
"optional": true,
86
+
"os": [
87
+
"linux"
88
+
],
89
+
"engines": {
90
+
"node": ">=14.21.3"
91
+
}
92
+
},
93
+
"node_modules/@biomejs/cli-linux-arm64-musl": {
94
+
"version": "2.3.11",
95
+
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.11.tgz",
96
+
"integrity": "sha512-XPSQ+XIPZMLaZ6zveQdwNjbX+QdROEd1zPgMwD47zvHV+tCGB88VH+aynyGxAHdzL+Tm/+DtKST5SECs4iwCLg==",
97
+
"cpu": [
98
+
"arm64"
99
+
],
100
+
"dev": true,
101
+
"license": "MIT OR Apache-2.0",
102
+
"optional": true,
103
+
"os": [
104
+
"linux"
105
+
],
106
+
"engines": {
107
+
"node": ">=14.21.3"
108
+
}
109
+
},
110
+
"node_modules/@biomejs/cli-linux-x64": {
111
+
"version": "2.3.11",
112
+
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.11.tgz",
113
+
"integrity": "sha512-/1s9V/H3cSe0r0Mv/Z8JryF5x9ywRxywomqZVLHAoa/uN0eY7F8gEngWKNS5vbbN/BsfpCG5yeBT5ENh50Frxg==",
114
+
"cpu": [
115
+
"x64"
116
+
],
117
+
"dev": true,
118
+
"license": "MIT OR Apache-2.0",
119
+
"optional": true,
120
+
"os": [
121
+
"linux"
122
+
],
123
+
"engines": {
124
+
"node": ">=14.21.3"
125
+
}
126
+
},
127
+
"node_modules/@biomejs/cli-linux-x64-musl": {
128
+
"version": "2.3.11",
129
+
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.11.tgz",
130
+
"integrity": "sha512-vU7a8wLs5C9yJ4CB8a44r12aXYb8yYgBn+WeyzbMjaCMklzCv1oXr8x+VEyWodgJt9bDmhiaW/I0RHbn7rsNmw==",
131
+
"cpu": [
132
+
"x64"
133
+
],
134
+
"dev": true,
135
+
"license": "MIT OR Apache-2.0",
136
+
"optional": true,
137
+
"os": [
138
+
"linux"
139
+
],
140
+
"engines": {
141
+
"node": ">=14.21.3"
142
+
}
143
+
},
144
+
"node_modules/@biomejs/cli-win32-arm64": {
145
+
"version": "2.3.11",
146
+
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.11.tgz",
147
+
"integrity": "sha512-PZQ6ElCOnkYapSsysiTy0+fYX+agXPlWugh6+eQ6uPKI3vKAqNp6TnMhoM3oY2NltSB89hz59o8xIfOdyhi9Iw==",
148
+
"cpu": [
149
+
"arm64"
150
+
],
151
+
"dev": true,
152
+
"license": "MIT OR Apache-2.0",
153
+
"optional": true,
154
+
"os": [
155
+
"win32"
156
+
],
157
+
"engines": {
158
+
"node": ">=14.21.3"
159
+
}
160
+
},
161
+
"node_modules/@biomejs/cli-win32-x64": {
162
+
"version": "2.3.11",
163
+
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.11.tgz",
164
+
"integrity": "sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg==",
165
+
"cpu": [
166
+
"x64"
167
+
],
168
+
"dev": true,
169
+
"license": "MIT OR Apache-2.0",
170
+
"optional": true,
171
+
"os": [
172
+
"win32"
173
+
],
174
+
"engines": {
175
+
"node": ">=14.21.3"
12
176
}
13
177
},
14
178
"node_modules/@cloudflare/kv-asset-handler": {
+5
-1
package.json
+5
-1
package.json
···
8
8
"deploy": "wrangler deploy",
9
9
"test": "node --test test/*.test.js",
10
10
"test:e2e": "./test/e2e.sh",
11
-
"setup": "node scripts/setup.js"
11
+
"setup": "node scripts/setup.js",
12
+
"format": "biome format --write .",
13
+
"lint": "biome lint .",
14
+
"check": "biome check ."
12
15
},
13
16
"devDependencies": {
17
+
"@biomejs/biome": "^2.3.11",
14
18
"wrangler": "^4.54.0"
15
19
}
16
20
}
+246
-222
scripts/setup.js
+246
-222
scripts/setup.js
···
9
9
* Usage: node scripts/setup.js --handle alice --pds https://your-pds.workers.dev
10
10
*/
11
11
12
-
import { webcrypto } from 'crypto'
13
-
import { writeFileSync } from 'fs'
12
+
import { webcrypto } from 'node:crypto';
13
+
import { writeFileSync } from 'node:fs';
14
14
15
15
// === ARGUMENT PARSING ===
16
16
17
17
function parseArgs() {
18
-
const args = process.argv.slice(2)
18
+
const args = process.argv.slice(2);
19
19
const opts = {
20
20
handle: null,
21
21
pds: null,
22
22
plcUrl: 'https://plc.directory',
23
-
relayUrl: 'https://bsky.network'
24
-
}
23
+
relayUrl: 'https://bsky.network',
24
+
};
25
25
26
26
for (let i = 0; i < args.length; i++) {
27
27
if (args[i] === '--handle' && args[i + 1]) {
28
-
opts.handle = args[++i]
28
+
opts.handle = args[++i];
29
29
} else if (args[i] === '--pds' && args[i + 1]) {
30
-
opts.pds = args[++i]
30
+
opts.pds = args[++i];
31
31
} else if (args[i] === '--plc-url' && args[i + 1]) {
32
-
opts.plcUrl = args[++i]
32
+
opts.plcUrl = args[++i];
33
33
} else if (args[i] === '--relay-url' && args[i + 1]) {
34
-
opts.relayUrl = args[++i]
34
+
opts.relayUrl = args[++i];
35
35
}
36
36
}
37
37
38
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)
39
+
console.error(
40
+
'Usage: node scripts/setup.js --pds <pds-url> [--handle <subdomain>]',
41
+
);
42
+
console.error('');
43
+
console.error('Options:');
44
+
console.error(
45
+
' --pds PDS URL (e.g., "https://atproto-pds.chad-53c.workers.dev")',
46
+
);
47
+
console.error(
48
+
' --handle Subdomain handle (e.g., "alice") - optional, uses bare hostname if omitted',
49
+
);
50
+
console.error(
51
+
' --plc-url PLC directory URL (default: https://plc.directory)',
52
+
);
53
+
console.error(' --relay-url Relay URL (default: https://bsky.network)');
54
+
process.exit(1);
47
55
}
48
56
49
-
return opts
57
+
return opts;
50
58
}
51
59
52
60
// === KEY GENERATION ===
···
55
63
const keyPair = await webcrypto.subtle.generateKey(
56
64
{ name: 'ECDSA', namedCurve: 'P-256' },
57
65
true,
58
-
['sign', 'verify']
59
-
)
66
+
['sign', 'verify'],
67
+
);
60
68
61
69
// Export private key as raw 32 bytes
62
-
const privateJwk = await webcrypto.subtle.exportKey('jwk', keyPair.privateKey)
63
-
const privateBytes = base64UrlDecode(privateJwk.d)
70
+
const privateJwk = await webcrypto.subtle.exportKey(
71
+
'jwk',
72
+
keyPair.privateKey,
73
+
);
74
+
const privateBytes = base64UrlDecode(privateJwk.d);
64
75
65
76
// Export public key as uncompressed point (65 bytes)
66
-
const publicRaw = await webcrypto.subtle.exportKey('raw', keyPair.publicKey)
67
-
const publicBytes = new Uint8Array(publicRaw)
77
+
const publicRaw = await webcrypto.subtle.exportKey('raw', keyPair.publicKey);
78
+
const publicBytes = new Uint8Array(publicRaw);
68
79
69
80
// Compress public key to 33 bytes
70
-
const compressedPublic = compressPublicKey(publicBytes)
81
+
const compressedPublic = compressPublicKey(publicBytes);
71
82
72
83
return {
73
84
privateKey: privateBytes,
74
85
publicKey: compressedPublic,
75
-
cryptoKey: keyPair.privateKey
76
-
}
86
+
cryptoKey: keyPair.privateKey,
87
+
};
77
88
}
78
89
79
90
function compressPublicKey(uncompressed) {
80
91
// uncompressed is 65 bytes: 0x04 + x(32) + y(32)
81
-
const x = uncompressed.slice(1, 33)
82
-
const y = uncompressed.slice(33, 65)
83
-
const prefix = (y[31] & 1) === 0 ? 0x02 : 0x03
84
-
const compressed = new Uint8Array(33)
85
-
compressed[0] = prefix
86
-
compressed.set(x, 1)
87
-
return compressed
92
+
const x = uncompressed.slice(1, 33);
93
+
const y = uncompressed.slice(33, 65);
94
+
const prefix = (y[31] & 1) === 0 ? 0x02 : 0x03;
95
+
const compressed = new Uint8Array(33);
96
+
compressed[0] = prefix;
97
+
compressed.set(x, 1);
98
+
return compressed;
88
99
}
89
100
90
101
function base64UrlDecode(str) {
91
-
const base64 = str.replace(/-/g, '+').replace(/_/g, '/')
92
-
const binary = atob(base64)
93
-
const bytes = new Uint8Array(binary.length)
102
+
const base64 = str.replace(/-/g, '+').replace(/_/g, '/');
103
+
const binary = atob(base64);
104
+
const bytes = new Uint8Array(binary.length);
94
105
for (let i = 0; i < binary.length; i++) {
95
-
bytes[i] = binary.charCodeAt(i)
106
+
bytes[i] = binary.charCodeAt(i);
96
107
}
97
-
return bytes
108
+
return bytes;
98
109
}
99
110
100
111
function bytesToHex(bytes) {
101
-
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('')
112
+
return Array.from(bytes)
113
+
.map((b) => b.toString(16).padStart(2, '0'))
114
+
.join('');
102
115
}
103
116
104
117
// === DID:KEY ENCODING ===
105
118
106
119
// Multicodec prefix for P-256 public key (0x1200)
107
-
const P256_MULTICODEC = new Uint8Array([0x80, 0x24])
120
+
const P256_MULTICODEC = new Uint8Array([0x80, 0x24]);
108
121
109
122
function publicKeyToDidKey(compressedPublicKey) {
110
123
// did:key format: "did:key:" + multibase(base58btc) of multicodec + key
111
-
const keyWithCodec = new Uint8Array(P256_MULTICODEC.length + compressedPublicKey.length)
112
-
keyWithCodec.set(P256_MULTICODEC)
113
-
keyWithCodec.set(compressedPublicKey, P256_MULTICODEC.length)
124
+
const keyWithCodec = new Uint8Array(
125
+
P256_MULTICODEC.length + compressedPublicKey.length,
126
+
);
127
+
keyWithCodec.set(P256_MULTICODEC);
128
+
keyWithCodec.set(compressedPublicKey, P256_MULTICODEC.length);
114
129
115
-
return 'did:key:z' + base58btcEncode(keyWithCodec)
130
+
return `did:key:z${base58btcEncode(keyWithCodec)}`;
116
131
}
117
132
118
133
function base58btcEncode(bytes) {
119
-
const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
134
+
const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
120
135
121
136
// Count leading zeros
122
-
let zeros = 0
137
+
let zeros = 0;
123
138
for (const b of bytes) {
124
-
if (b === 0) zeros++
125
-
else break
139
+
if (b === 0) zeros++;
140
+
else break;
126
141
}
127
142
128
143
// Convert to base58
129
-
const digits = [0]
144
+
const digits = [0];
130
145
for (const byte of bytes) {
131
-
let carry = byte
146
+
let carry = byte;
132
147
for (let i = 0; i < digits.length; i++) {
133
-
carry += digits[i] << 8
134
-
digits[i] = carry % 58
135
-
carry = (carry / 58) | 0
148
+
carry += digits[i] << 8;
149
+
digits[i] = carry % 58;
150
+
carry = (carry / 58) | 0;
136
151
}
137
152
while (carry > 0) {
138
-
digits.push(carry % 58)
139
-
carry = (carry / 58) | 0
153
+
digits.push(carry % 58);
154
+
carry = (carry / 58) | 0;
140
155
}
141
156
}
142
157
143
158
// Convert to string
144
-
let result = '1'.repeat(zeros)
159
+
let result = '1'.repeat(zeros);
145
160
for (let i = digits.length - 1; i >= 0; i--) {
146
-
result += ALPHABET[digits[i]]
161
+
result += ALPHABET[digits[i]];
147
162
}
148
163
149
-
return result
164
+
return result;
150
165
}
151
166
152
167
// === CBOR ENCODING (dag-cbor compliant for PLC operations) ===
153
168
154
169
function cborEncodeKey(key) {
155
170
// Encode a string key to CBOR bytes (for sorting)
156
-
const bytes = new TextEncoder().encode(key)
157
-
const parts = []
158
-
const mt = 3 << 5 // major type 3 = text string
171
+
const bytes = new TextEncoder().encode(key);
172
+
const parts = [];
173
+
const mt = 3 << 5; // major type 3 = text string
159
174
if (bytes.length < 24) {
160
-
parts.push(mt | bytes.length)
175
+
parts.push(mt | bytes.length);
161
176
} else if (bytes.length < 256) {
162
-
parts.push(mt | 24, bytes.length)
177
+
parts.push(mt | 24, bytes.length);
163
178
} else if (bytes.length < 65536) {
164
-
parts.push(mt | 25, bytes.length >> 8, bytes.length & 0xff)
179
+
parts.push(mt | 25, bytes.length >> 8, bytes.length & 0xff);
165
180
}
166
-
parts.push(...bytes)
167
-
return new Uint8Array(parts)
181
+
parts.push(...bytes);
182
+
return new Uint8Array(parts);
168
183
}
169
184
170
185
function compareBytes(a, b) {
171
186
// dag-cbor: bytewise lexicographic order of encoded keys
172
-
const minLen = Math.min(a.length, b.length)
187
+
const minLen = Math.min(a.length, b.length);
173
188
for (let i = 0; i < minLen; i++) {
174
-
if (a[i] !== b[i]) return a[i] - b[i]
189
+
if (a[i] !== b[i]) return a[i] - b[i];
175
190
}
176
-
return a.length - b.length
191
+
return a.length - b.length;
177
192
}
178
193
179
194
function cborEncode(value) {
180
-
const parts = []
195
+
const parts = [];
181
196
182
197
function encode(val) {
183
198
if (val === null) {
184
-
parts.push(0xf6)
199
+
parts.push(0xf6);
185
200
} else if (typeof val === 'string') {
186
-
const bytes = new TextEncoder().encode(val)
187
-
encodeHead(3, bytes.length)
188
-
parts.push(...bytes)
201
+
const bytes = new TextEncoder().encode(val);
202
+
encodeHead(3, bytes.length);
203
+
parts.push(...bytes);
189
204
} else if (typeof val === 'number') {
190
205
if (Number.isInteger(val) && val >= 0) {
191
-
encodeHead(0, val)
206
+
encodeHead(0, val);
192
207
}
193
208
} else if (val instanceof Uint8Array) {
194
-
encodeHead(2, val.length)
195
-
parts.push(...val)
209
+
encodeHead(2, val.length);
210
+
parts.push(...val);
196
211
} else if (Array.isArray(val)) {
197
-
encodeHead(4, val.length)
198
-
for (const item of val) encode(item)
212
+
encodeHead(4, val.length);
213
+
for (const item of val) encode(item);
199
214
} else if (typeof val === 'object') {
200
215
// dag-cbor: sort keys by their CBOR-encoded bytes (length first, then lexicographic)
201
-
const keys = Object.keys(val)
202
-
const keysSorted = keys.sort((a, b) => compareBytes(cborEncodeKey(a), cborEncodeKey(b)))
203
-
encodeHead(5, keysSorted.length)
216
+
const keys = Object.keys(val);
217
+
const keysSorted = keys.sort((a, b) =>
218
+
compareBytes(cborEncodeKey(a), cborEncodeKey(b)),
219
+
);
220
+
encodeHead(5, keysSorted.length);
204
221
for (const key of keysSorted) {
205
-
encode(key)
206
-
encode(val[key])
222
+
encode(key);
223
+
encode(val[key]);
207
224
}
208
225
}
209
226
}
210
227
211
228
function encodeHead(majorType, length) {
212
-
const mt = majorType << 5
229
+
const mt = majorType << 5;
213
230
if (length < 24) {
214
-
parts.push(mt | length)
231
+
parts.push(mt | length);
215
232
} else if (length < 256) {
216
-
parts.push(mt | 24, length)
233
+
parts.push(mt | 24, length);
217
234
} else if (length < 65536) {
218
-
parts.push(mt | 25, length >> 8, length & 0xff)
235
+
parts.push(mt | 25, length >> 8, length & 0xff);
219
236
}
220
237
}
221
238
222
-
encode(value)
223
-
return new Uint8Array(parts)
239
+
encode(value);
240
+
return new Uint8Array(parts);
224
241
}
225
242
226
243
// === HASHING ===
227
244
228
245
async function sha256(data) {
229
-
const hash = await webcrypto.subtle.digest('SHA-256', data)
230
-
return new Uint8Array(hash)
246
+
const hash = await webcrypto.subtle.digest('SHA-256', data);
247
+
return new Uint8Array(hash);
231
248
}
232
249
233
250
// === PLC OPERATIONS ===
234
251
235
252
async function signPlcOperation(operation, privateKey) {
236
253
// Encode operation without sig field
237
-
const { sig, ...opWithoutSig } = operation
238
-
const encoded = cborEncode(opWithoutSig)
254
+
const { sig, ...opWithoutSig } = operation;
255
+
const encoded = cborEncode(opWithoutSig);
239
256
240
257
// Sign with P-256
241
258
const signature = await webcrypto.subtle.sign(
242
259
{ name: 'ECDSA', hash: 'SHA-256' },
243
260
privateKey,
244
-
encoded
245
-
)
261
+
encoded,
262
+
);
246
263
247
264
// Convert to low-S form and base64url encode
248
-
const sigBytes = ensureLowS(new Uint8Array(signature))
249
-
return base64UrlEncode(sigBytes)
265
+
const sigBytes = ensureLowS(new Uint8Array(signature));
266
+
return base64UrlEncode(sigBytes);
250
267
}
251
268
252
269
function ensureLowS(sig) {
253
270
// P-256 order N
254
-
const N = BigInt('0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551')
255
-
const halfN = N / 2n
271
+
const N = BigInt(
272
+
'0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551',
273
+
);
274
+
const halfN = N / 2n;
256
275
257
-
const r = sig.slice(0, 32)
258
-
const s = sig.slice(32, 64)
276
+
const r = sig.slice(0, 32);
277
+
const s = sig.slice(32, 64);
259
278
260
279
// Convert s to BigInt
261
-
let sInt = BigInt('0x' + bytesToHex(s))
280
+
let sInt = BigInt(`0x${bytesToHex(s)}`);
262
281
263
282
// If s > N/2, replace with N - s
264
283
if (sInt > halfN) {
265
-
sInt = N - sInt
266
-
const newS = hexToBytes(sInt.toString(16).padStart(64, '0'))
267
-
const result = new Uint8Array(64)
268
-
result.set(r)
269
-
result.set(newS, 32)
270
-
return result
284
+
sInt = N - sInt;
285
+
const newS = hexToBytes(sInt.toString(16).padStart(64, '0'));
286
+
const result = new Uint8Array(64);
287
+
result.set(r);
288
+
result.set(newS, 32);
289
+
return result;
271
290
}
272
291
273
-
return sig
292
+
return sig;
274
293
}
275
294
276
295
function hexToBytes(hex) {
277
-
const bytes = new Uint8Array(hex.length / 2)
296
+
const bytes = new Uint8Array(hex.length / 2);
278
297
for (let i = 0; i < hex.length; i += 2) {
279
-
bytes[i / 2] = parseInt(hex.substr(i, 2), 16)
298
+
bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
280
299
}
281
-
return bytes
300
+
return bytes;
282
301
}
283
302
284
303
function base64UrlEncode(bytes) {
285
-
const binary = String.fromCharCode(...bytes)
286
-
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
304
+
const binary = String.fromCharCode(...bytes);
305
+
return btoa(binary)
306
+
.replace(/\+/g, '-')
307
+
.replace(/\//g, '_')
308
+
.replace(/=+$/, '');
287
309
}
288
310
289
311
async function createGenesisOperation(opts) {
290
-
const { didKey, handle, pdsUrl, cryptoKey } = opts
312
+
const { didKey, handle, pdsUrl, cryptoKey } = opts;
291
313
292
314
// 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
315
+
const pdsHost = new URL(pdsUrl).host;
316
+
const fullHandle = handle ? `${handle}.${pdsHost}` : pdsHost;
295
317
296
318
const operation = {
297
319
type: 'plc_operation',
298
320
rotationKeys: [didKey],
299
321
verificationMethods: {
300
-
atproto: didKey
322
+
atproto: didKey,
301
323
},
302
324
alsoKnownAs: [`at://${fullHandle}`],
303
325
services: {
304
326
atproto_pds: {
305
327
type: 'AtprotoPersonalDataServer',
306
-
endpoint: pdsUrl
307
-
}
328
+
endpoint: pdsUrl,
329
+
},
308
330
},
309
-
prev: null
310
-
}
331
+
prev: null,
332
+
};
311
333
312
334
// Sign the operation
313
-
operation.sig = await signPlcOperation(operation, cryptoKey)
335
+
operation.sig = await signPlcOperation(operation, cryptoKey);
314
336
315
-
return { operation, fullHandle }
337
+
return { operation, fullHandle };
316
338
}
317
339
318
340
async function deriveDidFromOperation(operation) {
319
341
// DID is computed from the FULL operation INCLUDING the signature
320
-
const encoded = cborEncode(operation)
321
-
const hash = await sha256(encoded)
342
+
const encoded = cborEncode(operation);
343
+
const hash = await sha256(encoded);
322
344
// DID is base32 of first 15 bytes of hash (= 24 base32 chars)
323
-
return 'did:plc:' + base32Encode(hash.slice(0, 15))
345
+
return `did:plc:${base32Encode(hash.slice(0, 15))}`;
324
346
}
325
347
326
348
function base32Encode(bytes) {
327
-
const alphabet = 'abcdefghijklmnopqrstuvwxyz234567'
328
-
let result = ''
329
-
let bits = 0
330
-
let value = 0
349
+
const alphabet = 'abcdefghijklmnopqrstuvwxyz234567';
350
+
let result = '';
351
+
let bits = 0;
352
+
let value = 0;
331
353
332
354
for (const byte of bytes) {
333
-
value = (value << 8) | byte
334
-
bits += 8
355
+
value = (value << 8) | byte;
356
+
bits += 8;
335
357
while (bits >= 5) {
336
-
bits -= 5
337
-
result += alphabet[(value >> bits) & 31]
358
+
bits -= 5;
359
+
result += alphabet[(value >> bits) & 31];
338
360
}
339
361
}
340
362
341
363
if (bits > 0) {
342
-
result += alphabet[(value << (5 - bits)) & 31]
364
+
result += alphabet[(value << (5 - bits)) & 31];
343
365
}
344
366
345
-
return result
367
+
return result;
346
368
}
347
369
348
370
// === PLC DIRECTORY REGISTRATION ===
349
371
350
372
async function registerWithPlc(plcUrl, did, operation) {
351
-
const url = `${plcUrl}/${encodeURIComponent(did)}`
373
+
const url = `${plcUrl}/${encodeURIComponent(did)}`;
352
374
353
375
const response = await fetch(url, {
354
376
method: 'POST',
355
377
headers: {
356
-
'Content-Type': 'application/json'
378
+
'Content-Type': 'application/json',
357
379
},
358
-
body: JSON.stringify(operation)
359
-
})
380
+
body: JSON.stringify(operation),
381
+
});
360
382
361
383
if (!response.ok) {
362
-
const text = await response.text()
363
-
throw new Error(`PLC registration failed: ${response.status} ${text}`)
384
+
const text = await response.text();
385
+
throw new Error(`PLC registration failed: ${response.status} ${text}`);
364
386
}
365
387
366
-
return true
388
+
return true;
367
389
}
368
390
369
391
// === PDS INITIALIZATION ===
370
392
371
393
async function initializePds(pdsUrl, did, privateKeyHex, handle) {
372
-
const url = `${pdsUrl}/init?did=${encodeURIComponent(did)}`
394
+
const url = `${pdsUrl}/init?did=${encodeURIComponent(did)}`;
373
395
374
396
const response = await fetch(url, {
375
397
method: 'POST',
376
398
headers: {
377
-
'Content-Type': 'application/json'
399
+
'Content-Type': 'application/json',
378
400
},
379
401
body: JSON.stringify({
380
402
did,
381
403
privateKey: privateKeyHex,
382
-
handle
383
-
})
384
-
})
404
+
handle,
405
+
}),
406
+
});
385
407
386
408
if (!response.ok) {
387
-
const text = await response.text()
388
-
throw new Error(`PDS initialization failed: ${response.status} ${text}`)
409
+
const text = await response.text();
410
+
throw new Error(`PDS initialization failed: ${response.status} ${text}`);
389
411
}
390
412
391
-
return response.json()
413
+
return response.json();
392
414
}
393
415
394
416
// === HANDLE REGISTRATION ===
395
417
396
418
async function registerHandle(pdsUrl, handle, did) {
397
-
const url = `${pdsUrl}/register-handle`
419
+
const url = `${pdsUrl}/register-handle`;
398
420
399
421
const response = await fetch(url, {
400
422
method: 'POST',
401
423
headers: {
402
-
'Content-Type': 'application/json'
424
+
'Content-Type': 'application/json',
403
425
},
404
-
body: JSON.stringify({ handle, did })
405
-
})
426
+
body: JSON.stringify({ handle, did }),
427
+
});
406
428
407
429
if (!response.ok) {
408
-
const text = await response.text()
409
-
throw new Error(`Handle registration failed: ${response.status} ${text}`)
430
+
const text = await response.text();
431
+
throw new Error(`Handle registration failed: ${response.status} ${text}`);
410
432
}
411
433
412
-
return true
434
+
return true;
413
435
}
414
436
415
437
// === RELAY NOTIFICATION ===
416
438
417
439
async function notifyRelay(relayUrl, pdsHostname) {
418
-
const url = `${relayUrl}/xrpc/com.atproto.sync.requestCrawl`
440
+
const url = `${relayUrl}/xrpc/com.atproto.sync.requestCrawl`;
419
441
420
442
const response = await fetch(url, {
421
443
method: 'POST',
422
444
headers: {
423
-
'Content-Type': 'application/json'
445
+
'Content-Type': 'application/json',
424
446
},
425
447
body: JSON.stringify({
426
-
hostname: pdsHostname
427
-
})
428
-
})
448
+
hostname: pdsHostname,
449
+
}),
450
+
});
429
451
430
452
// Relay might return 200 or 202, both are OK
431
453
if (!response.ok && response.status !== 202) {
432
-
const text = await response.text()
433
-
console.warn(` Warning: Relay notification returned ${response.status}: ${text}`)
434
-
return false
454
+
const text = await response.text();
455
+
console.warn(
456
+
` Warning: Relay notification returned ${response.status}: ${text}`,
457
+
);
458
+
return false;
435
459
}
436
460
437
-
return true
461
+
return true;
438
462
}
439
463
440
464
// === CREDENTIALS OUTPUT ===
441
465
442
466
function saveCredentials(filename, credentials) {
443
-
writeFileSync(filename, JSON.stringify(credentials, null, 2))
467
+
writeFileSync(filename, JSON.stringify(credentials, null, 2));
444
468
}
445
469
446
470
// === MAIN ===
447
471
448
472
async function main() {
449
-
const opts = parseArgs()
473
+
const opts = parseArgs();
450
474
451
-
console.log('PDS Federation Setup')
452
-
console.log('====================')
453
-
console.log(`PDS: ${opts.pds}`)
454
-
console.log('')
475
+
console.log('PDS Federation Setup');
476
+
console.log('====================');
477
+
console.log(`PDS: ${opts.pds}`);
478
+
console.log('');
455
479
456
480
// Step 1: Generate keypair
457
-
console.log('Generating P-256 keypair...')
458
-
const keyPair = await generateP256Keypair()
459
-
const didKey = publicKeyToDidKey(keyPair.publicKey)
460
-
console.log(` did:key: ${didKey}`)
461
-
console.log('')
481
+
console.log('Generating P-256 keypair...');
482
+
const keyPair = await generateP256Keypair();
483
+
const didKey = publicKeyToDidKey(keyPair.publicKey);
484
+
console.log(` did:key: ${didKey}`);
485
+
console.log('');
462
486
463
487
// Step 2: Create genesis operation
464
-
console.log('Creating PLC genesis operation...')
488
+
console.log('Creating PLC genesis operation...');
465
489
const { operation, fullHandle } = await createGenesisOperation({
466
490
didKey,
467
491
handle: opts.handle,
468
492
pdsUrl: opts.pds,
469
-
cryptoKey: keyPair.cryptoKey
470
-
})
471
-
const did = await deriveDidFromOperation(operation)
472
-
console.log(` DID: ${did}`)
473
-
console.log(` Handle: ${fullHandle}`)
474
-
console.log('')
493
+
cryptoKey: keyPair.cryptoKey,
494
+
});
495
+
const did = await deriveDidFromOperation(operation);
496
+
console.log(` DID: ${did}`);
497
+
console.log(` Handle: ${fullHandle}`);
498
+
console.log('');
475
499
476
500
// Step 3: Register with PLC directory
477
-
console.log(`Registering with ${opts.plcUrl}...`)
478
-
await registerWithPlc(opts.plcUrl, did, operation)
479
-
console.log(' Registered successfully!')
480
-
console.log('')
501
+
console.log(`Registering with ${opts.plcUrl}...`);
502
+
await registerWithPlc(opts.plcUrl, did, operation);
503
+
console.log(' Registered successfully!');
504
+
console.log('');
481
505
482
506
// 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('')
507
+
console.log(`Initializing PDS at ${opts.pds}...`);
508
+
const privateKeyHex = bytesToHex(keyPair.privateKey);
509
+
await initializePds(opts.pds, did, privateKeyHex, fullHandle);
510
+
console.log(' PDS initialized!');
511
+
console.log('');
488
512
489
513
// Step 4b: Register handle -> DID mapping (only for subdomain handles)
490
514
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('')
515
+
console.log(`Registering handle mapping...`);
516
+
await registerHandle(opts.pds, opts.handle, did);
517
+
console.log(` Handle ${opts.handle} -> ${did}`);
518
+
console.log('');
495
519
}
496
520
497
521
// Step 5: Notify relay
498
-
const pdsHostname = new URL(opts.pds).host
499
-
console.log(`Notifying relay at ${opts.relayUrl}...`)
500
-
const relayOk = await notifyRelay(opts.relayUrl, pdsHostname)
522
+
const pdsHostname = new URL(opts.pds).host;
523
+
console.log(`Notifying relay at ${opts.relayUrl}...`);
524
+
const relayOk = await notifyRelay(opts.relayUrl, pdsHostname);
501
525
if (relayOk) {
502
-
console.log(' Relay notified!')
526
+
console.log(' Relay notified!');
503
527
}
504
-
console.log('')
528
+
console.log('');
505
529
506
530
// Step 6: Save credentials
507
531
const credentials = {
···
510
534
privateKeyHex: bytesToHex(keyPair.privateKey),
511
535
didKey,
512
536
pdsUrl: opts.pds,
513
-
createdAt: new Date().toISOString()
514
-
}
537
+
createdAt: new Date().toISOString(),
538
+
};
515
539
516
-
const credentialsFile = `./credentials-${opts.handle || new URL(opts.pds).host}.json`
517
-
saveCredentials(credentialsFile, credentials)
540
+
const credentialsFile = `./credentials-${opts.handle || new URL(opts.pds).host}.json`;
541
+
saveCredentials(credentialsFile, credentials);
518
542
519
543
// Final output
520
-
console.log('Setup Complete!')
521
-
console.log('===============')
522
-
console.log(`Handle: ${fullHandle}`)
523
-
console.log(`DID: ${did}`)
524
-
console.log(`PDS: ${opts.pds}`)
525
-
console.log('')
526
-
console.log(`Credentials saved to: ${credentialsFile}`)
527
-
console.log('Keep this file safe - it contains your private key!')
544
+
console.log('Setup Complete!');
545
+
console.log('===============');
546
+
console.log(`Handle: ${fullHandle}`);
547
+
console.log(`DID: ${did}`);
548
+
console.log(`PDS: ${opts.pds}`);
549
+
console.log('');
550
+
console.log(`Credentials saved to: ${credentialsFile}`);
551
+
console.log('Keep this file safe - it contains your private key!');
528
552
}
529
553
530
-
main().catch(err => {
531
-
console.error('Error:', err.message)
532
-
process.exit(1)
533
-
})
554
+
main().catch((err) => {
555
+
console.error('Error:', err.message);
556
+
process.exit(1);
557
+
});
+128
-117
scripts/update-did.js
+128
-117
scripts/update-did.js
···
6
6
* Usage: node scripts/update-did.js --credentials <file> --new-handle <handle> --new-pds <url>
7
7
*/
8
8
9
-
import { webcrypto } from 'crypto'
10
-
import { readFileSync, writeFileSync } from 'fs'
9
+
import { webcrypto } from 'node:crypto';
10
+
import { readFileSync, writeFileSync } from 'node:fs';
11
11
12
12
// === ARGUMENT PARSING ===
13
13
14
14
function parseArgs() {
15
-
const args = process.argv.slice(2)
15
+
const args = process.argv.slice(2);
16
16
const opts = {
17
17
credentials: null,
18
18
newHandle: null,
19
19
newPds: null,
20
-
plcUrl: 'https://plc.directory'
21
-
}
20
+
plcUrl: 'https://plc.directory',
21
+
};
22
22
23
23
for (let i = 0; i < args.length; i++) {
24
24
if (args[i] === '--credentials' && args[i + 1]) {
25
-
opts.credentials = args[++i]
25
+
opts.credentials = args[++i];
26
26
} else if (args[i] === '--new-handle' && args[i + 1]) {
27
-
opts.newHandle = args[++i]
27
+
opts.newHandle = args[++i];
28
28
} else if (args[i] === '--new-pds' && args[i + 1]) {
29
-
opts.newPds = args[++i]
29
+
opts.newPds = args[++i];
30
30
} else if (args[i] === '--plc-url' && args[i + 1]) {
31
-
opts.plcUrl = args[++i]
31
+
opts.plcUrl = args[++i];
32
32
}
33
33
}
34
34
35
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)
36
+
console.error(
37
+
'Usage: node scripts/update-did.js --credentials <file> --new-handle <handle> --new-pds <url>',
38
+
);
39
+
process.exit(1);
38
40
}
39
41
40
-
return opts
42
+
return opts;
41
43
}
42
44
43
45
// === CRYPTO HELPERS ===
44
46
45
47
function hexToBytes(hex) {
46
-
const bytes = new Uint8Array(hex.length / 2)
48
+
const bytes = new Uint8Array(hex.length / 2);
47
49
for (let i = 0; i < hex.length; i += 2) {
48
-
bytes[i / 2] = parseInt(hex.substr(i, 2), 16)
50
+
bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
49
51
}
50
-
return bytes
52
+
return bytes;
51
53
}
52
54
53
55
function bytesToHex(bytes) {
54
-
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('')
56
+
return Array.from(bytes)
57
+
.map((b) => b.toString(16).padStart(2, '0'))
58
+
.join('');
55
59
}
56
60
57
61
async function importPrivateKey(privateKeyBytes) {
58
62
const pkcs8Prefix = new Uint8Array([
59
63
0x30, 0x41, 0x02, 0x01, 0x00, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48,
60
64
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
-
])
65
+
0x01, 0x07, 0x04, 0x27, 0x30, 0x25, 0x02, 0x01, 0x01, 0x04, 0x20,
66
+
]);
63
67
64
-
const pkcs8 = new Uint8Array(pkcs8Prefix.length + 32)
65
-
pkcs8.set(pkcs8Prefix)
66
-
pkcs8.set(privateKeyBytes, pkcs8Prefix.length)
68
+
const pkcs8 = new Uint8Array(pkcs8Prefix.length + 32);
69
+
pkcs8.set(pkcs8Prefix);
70
+
pkcs8.set(privateKeyBytes, pkcs8Prefix.length);
67
71
68
72
return webcrypto.subtle.importKey(
69
73
'pkcs8',
70
74
pkcs8,
71
75
{ name: 'ECDSA', namedCurve: 'P-256' },
72
76
false,
73
-
['sign']
74
-
)
77
+
['sign'],
78
+
);
75
79
}
76
80
77
81
// === CBOR ENCODING ===
78
82
79
83
function cborEncodeKey(key) {
80
-
const bytes = new TextEncoder().encode(key)
81
-
const parts = []
82
-
const mt = 3 << 5
84
+
const bytes = new TextEncoder().encode(key);
85
+
const parts = [];
86
+
const mt = 3 << 5;
83
87
if (bytes.length < 24) {
84
-
parts.push(mt | bytes.length)
88
+
parts.push(mt | bytes.length);
85
89
} else if (bytes.length < 256) {
86
-
parts.push(mt | 24, bytes.length)
90
+
parts.push(mt | 24, bytes.length);
87
91
}
88
-
parts.push(...bytes)
89
-
return new Uint8Array(parts)
92
+
parts.push(...bytes);
93
+
return new Uint8Array(parts);
90
94
}
91
95
92
96
function compareBytes(a, b) {
93
-
const minLen = Math.min(a.length, b.length)
97
+
const minLen = Math.min(a.length, b.length);
94
98
for (let i = 0; i < minLen; i++) {
95
-
if (a[i] !== b[i]) return a[i] - b[i]
99
+
if (a[i] !== b[i]) return a[i] - b[i];
96
100
}
97
-
return a.length - b.length
101
+
return a.length - b.length;
98
102
}
99
103
100
104
function cborEncode(value) {
101
-
const parts = []
105
+
const parts = [];
102
106
103
107
function encode(val) {
104
108
if (val === null) {
105
-
parts.push(0xf6)
109
+
parts.push(0xf6);
106
110
} else if (typeof val === 'string') {
107
-
const bytes = new TextEncoder().encode(val)
108
-
encodeHead(3, bytes.length)
109
-
parts.push(...bytes)
111
+
const bytes = new TextEncoder().encode(val);
112
+
encodeHead(3, bytes.length);
113
+
parts.push(...bytes);
110
114
} else if (typeof val === 'number') {
111
115
if (Number.isInteger(val) && val >= 0) {
112
-
encodeHead(0, val)
116
+
encodeHead(0, val);
113
117
}
114
118
} else if (val instanceof Uint8Array) {
115
-
encodeHead(2, val.length)
116
-
parts.push(...val)
119
+
encodeHead(2, val.length);
120
+
parts.push(...val);
117
121
} else if (Array.isArray(val)) {
118
-
encodeHead(4, val.length)
119
-
for (const item of val) encode(item)
122
+
encodeHead(4, val.length);
123
+
for (const item of val) encode(item);
120
124
} 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)
125
+
const keys = Object.keys(val);
126
+
const keysSorted = keys.sort((a, b) =>
127
+
compareBytes(cborEncodeKey(a), cborEncodeKey(b)),
128
+
);
129
+
encodeHead(5, keysSorted.length);
124
130
for (const key of keysSorted) {
125
-
encode(key)
126
-
encode(val[key])
131
+
encode(key);
132
+
encode(val[key]);
127
133
}
128
134
}
129
135
}
130
136
131
137
function encodeHead(majorType, length) {
132
-
const mt = majorType << 5
138
+
const mt = majorType << 5;
133
139
if (length < 24) {
134
-
parts.push(mt | length)
140
+
parts.push(mt | length);
135
141
} else if (length < 256) {
136
-
parts.push(mt | 24, length)
142
+
parts.push(mt | 24, length);
137
143
} else if (length < 65536) {
138
-
parts.push(mt | 25, length >> 8, length & 0xff)
144
+
parts.push(mt | 25, length >> 8, length & 0xff);
139
145
}
140
146
}
141
147
142
-
encode(value)
143
-
return new Uint8Array(parts)
148
+
encode(value);
149
+
return new Uint8Array(parts);
144
150
}
145
151
146
152
// === SIGNING ===
147
153
148
-
const P256_N = BigInt('0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551')
154
+
const P256_N = BigInt(
155
+
'0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551',
156
+
);
149
157
150
158
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))
159
+
const halfN = P256_N / 2n;
160
+
const r = sig.slice(0, 32);
161
+
const s = sig.slice(32, 64);
162
+
let sInt = BigInt(`0x${bytesToHex(s)}`);
155
163
156
164
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
165
+
sInt = P256_N - sInt;
166
+
const newS = hexToBytes(sInt.toString(16).padStart(64, '0'));
167
+
const result = new Uint8Array(64);
168
+
result.set(r);
169
+
result.set(newS, 32);
170
+
return result;
163
171
}
164
-
return sig
172
+
return sig;
165
173
}
166
174
167
175
function base64UrlEncode(bytes) {
168
-
const binary = String.fromCharCode(...bytes)
169
-
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
176
+
const binary = String.fromCharCode(...bytes);
177
+
return btoa(binary)
178
+
.replace(/\+/g, '-')
179
+
.replace(/\//g, '_')
180
+
.replace(/=+$/, '');
170
181
}
171
182
172
183
async function signPlcOperation(operation, privateKey) {
173
-
const { sig, ...opWithoutSig } = operation
174
-
const encoded = cborEncode(opWithoutSig)
184
+
const { sig, ...opWithoutSig } = operation;
185
+
const encoded = cborEncode(opWithoutSig);
175
186
176
187
const signature = await webcrypto.subtle.sign(
177
188
{ name: 'ECDSA', hash: 'SHA-256' },
178
189
privateKey,
179
-
encoded
180
-
)
190
+
encoded,
191
+
);
181
192
182
-
const sigBytes = ensureLowS(new Uint8Array(signature))
183
-
return base64UrlEncode(sigBytes)
193
+
const sigBytes = ensureLowS(new Uint8Array(signature));
194
+
return base64UrlEncode(sigBytes);
184
195
}
185
196
186
197
// === MAIN ===
187
198
188
199
async function main() {
189
-
const opts = parseArgs()
200
+
const opts = parseArgs();
190
201
191
202
// 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('')
203
+
const creds = JSON.parse(readFileSync(opts.credentials, 'utf-8'));
204
+
console.log(`Updating DID: ${creds.did}`);
205
+
console.log(` Old handle: ${creds.handle}`);
206
+
console.log(` New handle: ${opts.newHandle}`);
207
+
console.log(` New PDS: ${opts.newPds}`);
208
+
console.log('');
198
209
199
210
// Fetch current operation log
200
-
console.log('Fetching current PLC operation log...')
201
-
const logRes = await fetch(`${opts.plcUrl}/${creds.did}/log/audit`)
211
+
console.log('Fetching current PLC operation log...');
212
+
const logRes = await fetch(`${opts.plcUrl}/${creds.did}/log/audit`);
202
213
if (!logRes.ok) {
203
-
throw new Error(`Failed to fetch PLC log: ${logRes.status}`)
214
+
throw new Error(`Failed to fetch PLC log: ${logRes.status}`);
204
215
}
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('')
216
+
const log = await logRes.json();
217
+
const lastOp = log[log.length - 1];
218
+
console.log(` Found ${log.length} operations`);
219
+
console.log(` Last CID: ${lastOp.cid}`);
220
+
console.log('');
210
221
211
222
// Import private key
212
-
const privateKey = await importPrivateKey(hexToBytes(creds.privateKeyHex))
223
+
const privateKey = await importPrivateKey(hexToBytes(creds.privateKeyHex));
213
224
214
225
// Create new operation
215
226
const newOp = {
···
220
231
services: {
221
232
atproto_pds: {
222
233
type: 'AtprotoPersonalDataServer',
223
-
endpoint: opts.newPds
224
-
}
234
+
endpoint: opts.newPds,
235
+
},
225
236
},
226
-
prev: lastOp.cid
227
-
}
237
+
prev: lastOp.cid,
238
+
};
228
239
229
240
// Sign the operation
230
-
console.log('Signing new operation...')
231
-
newOp.sig = await signPlcOperation(newOp, privateKey)
241
+
console.log('Signing new operation...');
242
+
newOp.sig = await signPlcOperation(newOp, privateKey);
232
243
233
244
// Submit to PLC
234
-
console.log('Submitting to PLC directory...')
245
+
console.log('Submitting to PLC directory...');
235
246
const submitRes = await fetch(`${opts.plcUrl}/${creds.did}`, {
236
247
method: 'POST',
237
248
headers: { 'Content-Type': 'application/json' },
238
-
body: JSON.stringify(newOp)
239
-
})
249
+
body: JSON.stringify(newOp),
250
+
});
240
251
241
252
if (!submitRes.ok) {
242
-
const text = await submitRes.text()
243
-
throw new Error(`PLC update failed: ${submitRes.status} ${text}`)
253
+
const text = await submitRes.text();
254
+
throw new Error(`PLC update failed: ${submitRes.status} ${text}`);
244
255
}
245
256
246
-
console.log(' Updated successfully!')
247
-
console.log('')
257
+
console.log(' Updated successfully!');
258
+
console.log('');
248
259
249
260
// 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}`)
261
+
creds.handle = opts.newHandle;
262
+
creds.pdsUrl = opts.newPds;
263
+
writeFileSync(opts.credentials, JSON.stringify(creds, null, 2));
264
+
console.log(`Updated credentials file: ${opts.credentials}`);
254
265
255
266
// 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}`)
267
+
console.log('');
268
+
console.log('Verifying...');
269
+
const verifyRes = await fetch(`${opts.plcUrl}/${creds.did}`);
270
+
const didDoc = await verifyRes.json();
271
+
console.log(` alsoKnownAs: ${didDoc.alsoKnownAs}`);
272
+
console.log(` PDS endpoint: ${didDoc.service[0].serviceEndpoint}`);
262
273
}
263
274
264
-
main().catch(err => {
265
-
console.error('Error:', err.message)
266
-
process.exit(1)
267
-
})
275
+
main().catch((err) => {
276
+
console.error('Error:', err.message);
277
+
process.exit(1);
278
+
});
+1182
-974
src/pds.js
+1182
-974
src/pds.js
···
16
16
17
17
// === CONSTANTS ===
18
18
// CBOR primitive markers (RFC 8949)
19
-
const CBOR_FALSE = 0xf4
20
-
const CBOR_TRUE = 0xf5
21
-
const CBOR_NULL = 0xf6
19
+
const CBOR_FALSE = 0xf4;
20
+
const CBOR_TRUE = 0xf5;
21
+
const CBOR_NULL = 0xf6;
22
22
23
23
// DAG-CBOR CID link tag
24
-
const CBOR_TAG_CID = 42
24
+
const CBOR_TAG_CID = 42;
25
25
26
26
// === ERROR HELPER ===
27
27
function errorResponse(error, message, status) {
28
-
return Response.json({ error, message }, { status })
28
+
return Response.json({ error, message }, { status });
29
29
}
30
30
31
31
// === CRAWLER NOTIFICATION ===
32
32
// Notify relays to come crawl us after writes (like official PDS)
33
-
let lastCrawlNotify = 0
34
-
const CRAWL_NOTIFY_THRESHOLD = 20 * 60 * 1000 // 20 minutes (matches official PDS)
33
+
let lastCrawlNotify = 0;
34
+
const CRAWL_NOTIFY_THRESHOLD = 20 * 60 * 1000; // 20 minutes (matches official PDS)
35
35
36
36
async function notifyCrawlers(env, hostname) {
37
-
const now = Date.now()
37
+
const now = Date.now();
38
38
if (now - lastCrawlNotify < CRAWL_NOTIFY_THRESHOLD) {
39
-
return // Throttle notifications
39
+
return; // Throttle notifications
40
40
}
41
41
42
-
const relayHost = env.RELAY_HOST
43
-
if (!relayHost) return
42
+
const relayHost = env.RELAY_HOST;
43
+
if (!relayHost) return;
44
44
45
-
lastCrawlNotify = now
45
+
lastCrawlNotify = now;
46
46
47
47
// Fire and forget - don't block writes on relay notification
48
48
fetch(`${relayHost}/xrpc/com.atproto.sync.requestCrawl`, {
49
49
method: 'POST',
50
50
headers: { 'Content-Type': 'application/json' },
51
-
body: JSON.stringify({ hostname })
52
-
}).catch(err => {
53
-
console.log('Failed to notify relay:', err.message)
54
-
})
51
+
body: JSON.stringify({ hostname }),
52
+
}).catch((err) => {
53
+
console.log('Failed to notify relay:', err.message);
54
+
});
55
55
}
56
56
57
57
// === CID WRAPPER ===
···
60
60
class CID {
61
61
constructor(bytes) {
62
62
if (!(bytes instanceof Uint8Array)) {
63
-
throw new Error('CID must be constructed with Uint8Array')
63
+
throw new Error('CID must be constructed with Uint8Array');
64
64
}
65
-
this.bytes = bytes
65
+
this.bytes = bytes;
66
66
}
67
67
}
68
68
···
76
76
* @param {number} length - Value or length to encode
77
77
*/
78
78
function encodeHead(parts, majorType, length) {
79
-
const mt = majorType << 5
79
+
const mt = majorType << 5;
80
80
if (length < 24) {
81
-
parts.push(mt | length)
81
+
parts.push(mt | length);
82
82
} else if (length < 256) {
83
-
parts.push(mt | 24, length)
83
+
parts.push(mt | 24, length);
84
84
} else if (length < 65536) {
85
-
parts.push(mt | 25, length >> 8, length & 0xff)
85
+
parts.push(mt | 25, length >> 8, length & 0xff);
86
86
} else if (length < 4294967296) {
87
87
// Use Math.floor instead of bitshift to avoid 32-bit signed integer overflow
88
-
parts.push(mt | 26,
88
+
parts.push(
89
+
mt | 26,
89
90
Math.floor(length / 0x1000000) & 0xff,
90
91
Math.floor(length / 0x10000) & 0xff,
91
92
Math.floor(length / 0x100) & 0xff,
92
-
length & 0xff)
93
+
length & 0xff,
94
+
);
93
95
}
94
96
}
95
97
···
99
101
* @returns {Uint8Array} CBOR-encoded bytes
100
102
*/
101
103
export function cborEncode(value) {
102
-
const parts = []
104
+
const parts = [];
103
105
104
106
function encode(val) {
105
107
if (val === null) {
106
-
parts.push(CBOR_NULL)
108
+
parts.push(CBOR_NULL);
107
109
} else if (val === true) {
108
-
parts.push(CBOR_TRUE)
110
+
parts.push(CBOR_TRUE);
109
111
} else if (val === false) {
110
-
parts.push(CBOR_FALSE)
112
+
parts.push(CBOR_FALSE);
111
113
} else if (typeof val === 'number') {
112
-
encodeInteger(val)
114
+
encodeInteger(val);
113
115
} else if (typeof val === 'string') {
114
-
const bytes = new TextEncoder().encode(val)
115
-
encodeHead(parts, 3, bytes.length) // major type 3 = text string
116
-
parts.push(...bytes)
116
+
const bytes = new TextEncoder().encode(val);
117
+
encodeHead(parts, 3, bytes.length); // major type 3 = text string
118
+
parts.push(...bytes);
117
119
} else if (val instanceof Uint8Array) {
118
-
encodeHead(parts, 2, val.length) // major type 2 = byte string
119
-
parts.push(...val)
120
+
encodeHead(parts, 2, val.length); // major type 2 = byte string
121
+
parts.push(...val);
120
122
} else if (Array.isArray(val)) {
121
-
encodeHead(parts, 4, val.length) // major type 4 = array
122
-
for (const item of val) encode(item)
123
+
encodeHead(parts, 4, val.length); // major type 4 = array
124
+
for (const item of val) encode(item);
123
125
} else if (typeof val === 'object') {
124
126
// Sort keys for deterministic encoding
125
-
const keys = Object.keys(val).sort()
126
-
encodeHead(parts, 5, keys.length) // major type 5 = map
127
+
const keys = Object.keys(val).sort();
128
+
encodeHead(parts, 5, keys.length); // major type 5 = map
127
129
for (const key of keys) {
128
-
encode(key)
129
-
encode(val[key])
130
+
encode(key);
131
+
encode(val[key]);
130
132
}
131
133
}
132
134
}
133
135
134
136
function encodeInteger(n) {
135
137
if (n >= 0) {
136
-
encodeHead(parts, 0, n) // major type 0 = unsigned int
138
+
encodeHead(parts, 0, n); // major type 0 = unsigned int
137
139
} else {
138
-
encodeHead(parts, 1, -n - 1) // major type 1 = negative int
140
+
encodeHead(parts, 1, -n - 1); // major type 1 = negative int
139
141
}
140
142
}
141
143
142
-
encode(value)
143
-
return new Uint8Array(parts)
144
+
encode(value);
145
+
return new Uint8Array(parts);
144
146
}
145
147
146
148
// DAG-CBOR encoder that handles CIDs with tag 42
147
149
function cborEncodeDagCbor(value) {
148
-
const parts = []
150
+
const parts = [];
149
151
150
152
function encode(val) {
151
153
if (val === null) {
152
-
parts.push(CBOR_NULL)
154
+
parts.push(CBOR_NULL);
153
155
} else if (val === true) {
154
-
parts.push(CBOR_TRUE)
156
+
parts.push(CBOR_TRUE);
155
157
} else if (val === false) {
156
-
parts.push(CBOR_FALSE)
158
+
parts.push(CBOR_FALSE);
157
159
} else if (typeof val === 'number') {
158
160
if (Number.isInteger(val) && val >= 0) {
159
-
encodeHead(parts, 0, val)
161
+
encodeHead(parts, 0, val);
160
162
} else if (Number.isInteger(val) && val < 0) {
161
-
encodeHead(parts, 1, -val - 1)
163
+
encodeHead(parts, 1, -val - 1);
162
164
}
163
165
} else if (typeof val === 'string') {
164
-
const bytes = new TextEncoder().encode(val)
165
-
encodeHead(parts, 3, bytes.length)
166
-
parts.push(...bytes)
166
+
const bytes = new TextEncoder().encode(val);
167
+
encodeHead(parts, 3, bytes.length);
168
+
parts.push(...bytes);
167
169
} else if (val instanceof CID) {
168
170
// CID links in DAG-CBOR use tag 42 + 0x00 multibase prefix
169
171
// The 0x00 prefix indicates "identity" multibase (raw bytes)
170
-
parts.push(0xd8, CBOR_TAG_CID)
171
-
encodeHead(parts, 2, val.bytes.length + 1) // +1 for 0x00 prefix
172
-
parts.push(0x00)
173
-
parts.push(...val.bytes)
172
+
parts.push(0xd8, CBOR_TAG_CID);
173
+
encodeHead(parts, 2, val.bytes.length + 1); // +1 for 0x00 prefix
174
+
parts.push(0x00);
175
+
parts.push(...val.bytes);
174
176
} else if (val instanceof Uint8Array) {
175
177
// Regular byte string
176
-
encodeHead(parts, 2, val.length)
177
-
parts.push(...val)
178
+
encodeHead(parts, 2, val.length);
179
+
parts.push(...val);
178
180
} else if (Array.isArray(val)) {
179
-
encodeHead(parts, 4, val.length)
180
-
for (const item of val) encode(item)
181
+
encodeHead(parts, 4, val.length);
182
+
for (const item of val) encode(item);
181
183
} else if (typeof val === 'object') {
182
184
// DAG-CBOR: sort keys by length first, then lexicographically
183
185
// (differs from standard CBOR which sorts lexicographically only)
184
-
const keys = Object.keys(val).filter(k => val[k] !== undefined)
186
+
const keys = Object.keys(val).filter((k) => val[k] !== undefined);
185
187
keys.sort((a, b) => {
186
-
if (a.length !== b.length) return a.length - b.length
187
-
return a < b ? -1 : a > b ? 1 : 0
188
-
})
189
-
encodeHead(parts, 5, keys.length)
188
+
if (a.length !== b.length) return a.length - b.length;
189
+
return a < b ? -1 : a > b ? 1 : 0;
190
+
});
191
+
encodeHead(parts, 5, keys.length);
190
192
for (const key of keys) {
191
-
const keyBytes = new TextEncoder().encode(key)
192
-
encodeHead(parts, 3, keyBytes.length)
193
-
parts.push(...keyBytes)
194
-
encode(val[key])
193
+
const keyBytes = new TextEncoder().encode(key);
194
+
encodeHead(parts, 3, keyBytes.length);
195
+
parts.push(...keyBytes);
196
+
encode(val[key]);
195
197
}
196
198
}
197
199
}
198
200
199
-
encode(value)
200
-
return new Uint8Array(parts)
201
+
encode(value);
202
+
return new Uint8Array(parts);
201
203
}
202
204
203
205
/**
···
206
208
* @returns {*} Decoded value
207
209
*/
208
210
export function cborDecode(bytes) {
209
-
let offset = 0
211
+
let offset = 0;
210
212
211
213
function read() {
212
-
const initial = bytes[offset++]
213
-
const major = initial >> 5
214
-
const info = initial & 0x1f
214
+
const initial = bytes[offset++];
215
+
const major = initial >> 5;
216
+
const info = initial & 0x1f;
215
217
216
-
let length = info
217
-
if (info === 24) length = bytes[offset++]
218
-
else if (info === 25) { length = (bytes[offset++] << 8) | bytes[offset++] }
219
-
else if (info === 26) {
218
+
let length = info;
219
+
if (info === 24) length = bytes[offset++];
220
+
else if (info === 25) {
221
+
length = (bytes[offset++] << 8) | bytes[offset++];
222
+
} else if (info === 26) {
220
223
// Use multiplication instead of bitshift to avoid 32-bit signed integer overflow
221
-
length = bytes[offset++] * 0x1000000 + bytes[offset++] * 0x10000 + bytes[offset++] * 0x100 + bytes[offset++]
224
+
length =
225
+
bytes[offset++] * 0x1000000 +
226
+
bytes[offset++] * 0x10000 +
227
+
bytes[offset++] * 0x100 +
228
+
bytes[offset++];
222
229
}
223
230
224
231
switch (major) {
225
-
case 0: return length // unsigned int
226
-
case 1: return -1 - length // negative int
227
-
case 2: { // byte string
228
-
const data = bytes.slice(offset, offset + length)
229
-
offset += length
230
-
return data
232
+
case 0:
233
+
return length; // unsigned int
234
+
case 1:
235
+
return -1 - length; // negative int
236
+
case 2: {
237
+
// byte string
238
+
const data = bytes.slice(offset, offset + length);
239
+
offset += length;
240
+
return data;
231
241
}
232
-
case 3: { // text string
233
-
const data = new TextDecoder().decode(bytes.slice(offset, offset + length))
234
-
offset += length
235
-
return data
242
+
case 3: {
243
+
// text string
244
+
const data = new TextDecoder().decode(
245
+
bytes.slice(offset, offset + length),
246
+
);
247
+
offset += length;
248
+
return data;
236
249
}
237
-
case 4: { // array
238
-
const arr = []
239
-
for (let i = 0; i < length; i++) arr.push(read())
240
-
return arr
250
+
case 4: {
251
+
// array
252
+
const arr = [];
253
+
for (let i = 0; i < length; i++) arr.push(read());
254
+
return arr;
241
255
}
242
-
case 5: { // map
243
-
const obj = {}
256
+
case 5: {
257
+
// map
258
+
const obj = {};
244
259
for (let i = 0; i < length; i++) {
245
-
const key = read()
246
-
obj[key] = read()
260
+
const key = read();
261
+
obj[key] = read();
247
262
}
248
-
return obj
263
+
return obj;
249
264
}
250
-
case 6: { // tag
265
+
case 6: {
266
+
// tag
251
267
// length is the tag number
252
-
const taggedValue = read()
268
+
const taggedValue = read();
253
269
if (length === CBOR_TAG_CID) {
254
270
// CID link: byte string with 0x00 multibase prefix, return raw CID bytes
255
-
return taggedValue.slice(1) // strip 0x00 prefix
271
+
return taggedValue.slice(1); // strip 0x00 prefix
256
272
}
257
-
return taggedValue
273
+
return taggedValue;
258
274
}
259
-
case 7: { // special
260
-
if (info === 20) return false
261
-
if (info === 21) return true
262
-
if (info === 22) return null
263
-
return undefined
275
+
case 7: {
276
+
// special
277
+
if (info === 20) return false;
278
+
if (info === 21) return true;
279
+
if (info === 22) return null;
280
+
return undefined;
264
281
}
265
282
}
266
283
}
267
284
268
-
return read()
285
+
return read();
269
286
}
270
287
271
288
// === CID GENERATION ===
···
277
294
* @returns {Promise<Uint8Array>} CID bytes (36 bytes: version + codec + multihash)
278
295
*/
279
296
export async function createCid(bytes) {
280
-
const hash = await crypto.subtle.digest('SHA-256', bytes)
281
-
const hashBytes = new Uint8Array(hash)
297
+
const hash = await crypto.subtle.digest('SHA-256', bytes);
298
+
const hashBytes = new Uint8Array(hash);
282
299
283
300
// CIDv1: version(1) + codec(dag-cbor=0x71) + multihash(sha256)
284
301
// Multihash: hash-type(0x12) + length(0x20=32) + digest
285
-
const cid = new Uint8Array(2 + 2 + 32)
286
-
cid[0] = 0x01 // CIDv1
287
-
cid[1] = 0x71 // dag-cbor codec
288
-
cid[2] = 0x12 // sha-256
289
-
cid[3] = 0x20 // 32 bytes
290
-
cid.set(hashBytes, 4)
302
+
const cid = new Uint8Array(2 + 2 + 32);
303
+
cid[0] = 0x01; // CIDv1
304
+
cid[1] = 0x71; // dag-cbor codec
305
+
cid[2] = 0x12; // sha-256
306
+
cid[3] = 0x20; // 32 bytes
307
+
cid.set(hashBytes, 4);
291
308
292
-
return cid
309
+
return cid;
293
310
}
294
311
295
312
/**
···
299
316
*/
300
317
export function cidToString(cid) {
301
318
// base32lower encoding for CIDv1
302
-
return 'b' + base32Encode(cid)
319
+
return `b${base32Encode(cid)}`;
303
320
}
304
321
305
322
/**
···
308
325
* @returns {string} Base32lower-encoded string
309
326
*/
310
327
export function base32Encode(bytes) {
311
-
const alphabet = 'abcdefghijklmnopqrstuvwxyz234567'
312
-
let result = ''
313
-
let bits = 0
314
-
let value = 0
328
+
const alphabet = 'abcdefghijklmnopqrstuvwxyz234567';
329
+
let result = '';
330
+
let bits = 0;
331
+
let value = 0;
315
332
316
333
for (const byte of bytes) {
317
-
value = (value << 8) | byte
318
-
bits += 8
334
+
value = (value << 8) | byte;
335
+
bits += 8;
319
336
while (bits >= 5) {
320
-
bits -= 5
321
-
result += alphabet[(value >> bits) & 31]
337
+
bits -= 5;
338
+
result += alphabet[(value >> bits) & 31];
322
339
}
323
340
}
324
341
325
342
if (bits > 0) {
326
-
result += alphabet[(value << (5 - bits)) & 31]
343
+
result += alphabet[(value << (5 - bits)) & 31];
327
344
}
328
345
329
-
return result
346
+
return result;
330
347
}
331
348
332
349
// === TID GENERATION ===
333
350
// Timestamp-based IDs: base32-sort encoded microseconds + clock ID
334
351
335
-
const TID_CHARS = '234567abcdefghijklmnopqrstuvwxyz'
336
-
let lastTimestamp = 0
337
-
let clockId = Math.floor(Math.random() * 1024)
352
+
const TID_CHARS = '234567abcdefghijklmnopqrstuvwxyz';
353
+
let lastTimestamp = 0;
354
+
const clockId = Math.floor(Math.random() * 1024);
338
355
339
356
/**
340
357
* Generate a timestamp-based ID (TID) for record keys
···
342
359
* @returns {string} 13-character base32-sort encoded TID
343
360
*/
344
361
export function createTid() {
345
-
let timestamp = Date.now() * 1000 // microseconds
362
+
let timestamp = Date.now() * 1000; // microseconds
346
363
347
364
// Ensure monotonic
348
365
if (timestamp <= lastTimestamp) {
349
-
timestamp = lastTimestamp + 1
366
+
timestamp = lastTimestamp + 1;
350
367
}
351
-
lastTimestamp = timestamp
368
+
lastTimestamp = timestamp;
352
369
353
370
// 13 chars: 11 for timestamp (64 bits but only ~53 used), 2 for clock ID
354
-
let tid = ''
371
+
let tid = '';
355
372
356
373
// Encode timestamp (high bits first for sortability)
357
-
let ts = timestamp
374
+
let ts = timestamp;
358
375
for (let i = 0; i < 11; i++) {
359
-
tid = TID_CHARS[ts & 31] + tid
360
-
ts = Math.floor(ts / 32)
376
+
tid = TID_CHARS[ts & 31] + tid;
377
+
ts = Math.floor(ts / 32);
361
378
}
362
379
363
380
// Append clock ID (2 chars)
364
-
tid += TID_CHARS[(clockId >> 5) & 31]
365
-
tid += TID_CHARS[clockId & 31]
381
+
tid += TID_CHARS[(clockId >> 5) & 31];
382
+
tid += TID_CHARS[clockId & 31];
366
383
367
-
return tid
384
+
return tid;
368
385
}
369
386
370
387
// === P-256 SIGNING ===
···
377
394
*/
378
395
export async function importPrivateKey(privateKeyBytes) {
379
396
// Validate private key length (P-256 requires exactly 32 bytes)
380
-
if (!(privateKeyBytes instanceof Uint8Array) || privateKeyBytes.length !== 32) {
381
-
throw new Error(`Invalid private key: expected 32 bytes, got ${privateKeyBytes?.length ?? 'non-Uint8Array'}`)
397
+
if (
398
+
!(privateKeyBytes instanceof Uint8Array) ||
399
+
privateKeyBytes.length !== 32
400
+
) {
401
+
throw new Error(
402
+
`Invalid private key: expected 32 bytes, got ${privateKeyBytes?.length ?? 'non-Uint8Array'}`,
403
+
);
382
404
}
383
405
384
406
// PKCS#8 wrapper for raw P-256 private key
385
407
const pkcs8Prefix = new Uint8Array([
386
408
0x30, 0x41, 0x02, 0x01, 0x00, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48,
387
409
0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03,
388
-
0x01, 0x07, 0x04, 0x27, 0x30, 0x25, 0x02, 0x01, 0x01, 0x04, 0x20
389
-
])
410
+
0x01, 0x07, 0x04, 0x27, 0x30, 0x25, 0x02, 0x01, 0x01, 0x04, 0x20,
411
+
]);
390
412
391
-
const pkcs8 = new Uint8Array(pkcs8Prefix.length + 32)
392
-
pkcs8.set(pkcs8Prefix)
393
-
pkcs8.set(privateKeyBytes, pkcs8Prefix.length)
413
+
const pkcs8 = new Uint8Array(pkcs8Prefix.length + 32);
414
+
pkcs8.set(pkcs8Prefix);
415
+
pkcs8.set(privateKeyBytes, pkcs8Prefix.length);
394
416
395
417
return crypto.subtle.importKey(
396
418
'pkcs8',
397
419
pkcs8,
398
420
{ name: 'ECDSA', namedCurve: 'P-256' },
399
421
false,
400
-
['sign']
401
-
)
422
+
['sign'],
423
+
);
402
424
}
403
425
404
426
// P-256 curve order N
405
-
const P256_N = BigInt('0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551')
406
-
const P256_N_DIV_2 = P256_N / 2n
427
+
const P256_N = BigInt(
428
+
'0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551',
429
+
);
430
+
const P256_N_DIV_2 = P256_N / 2n;
407
431
408
432
function bytesToBigInt(bytes) {
409
-
let result = 0n
433
+
let result = 0n;
410
434
for (const byte of bytes) {
411
-
result = (result << 8n) | BigInt(byte)
435
+
result = (result << 8n) | BigInt(byte);
412
436
}
413
-
return result
437
+
return result;
414
438
}
415
439
416
440
function bigIntToBytes(n, length) {
417
-
const bytes = new Uint8Array(length)
441
+
const bytes = new Uint8Array(length);
418
442
for (let i = length - 1; i >= 0; i--) {
419
-
bytes[i] = Number(n & 0xffn)
420
-
n >>= 8n
443
+
bytes[i] = Number(n & 0xffn);
444
+
n >>= 8n;
421
445
}
422
-
return bytes
446
+
return bytes;
423
447
}
424
448
425
449
/**
···
432
456
const signature = await crypto.subtle.sign(
433
457
{ name: 'ECDSA', hash: 'SHA-256' },
434
458
privateKey,
435
-
data
436
-
)
437
-
const sig = new Uint8Array(signature)
459
+
data,
460
+
);
461
+
const sig = new Uint8Array(signature);
438
462
439
-
const r = sig.slice(0, 32)
440
-
const s = sig.slice(32, 64)
441
-
const sBigInt = bytesToBigInt(s)
463
+
const r = sig.slice(0, 32);
464
+
const s = sig.slice(32, 64);
465
+
const sBigInt = bytesToBigInt(s);
442
466
443
467
// Low-S normalization: Bitcoin/ATProto require S <= N/2 to prevent
444
468
// signature malleability (two valid signatures for same message)
445
469
if (sBigInt > P256_N_DIV_2) {
446
-
const newS = P256_N - sBigInt
447
-
const newSBytes = bigIntToBytes(newS, 32)
448
-
const normalized = new Uint8Array(64)
449
-
normalized.set(r, 0)
450
-
normalized.set(newSBytes, 32)
451
-
return normalized
470
+
const newS = P256_N - sBigInt;
471
+
const newSBytes = bigIntToBytes(newS, 32);
472
+
const normalized = new Uint8Array(64);
473
+
normalized.set(r, 0);
474
+
normalized.set(newSBytes, 32);
475
+
return normalized;
452
476
}
453
477
454
-
return sig
478
+
return sig;
455
479
}
456
480
457
481
/**
···
462
486
const keyPair = await crypto.subtle.generateKey(
463
487
{ name: 'ECDSA', namedCurve: 'P-256' },
464
488
true,
465
-
['sign', 'verify']
466
-
)
489
+
['sign', 'verify'],
490
+
);
467
491
468
492
// Export private key as raw bytes
469
-
const privateJwk = await crypto.subtle.exportKey('jwk', keyPair.privateKey)
470
-
const privateBytes = base64UrlDecode(privateJwk.d)
493
+
const privateJwk = await crypto.subtle.exportKey('jwk', keyPair.privateKey);
494
+
const privateBytes = base64UrlDecode(privateJwk.d);
471
495
472
496
// Export public key as compressed point
473
-
const publicRaw = await crypto.subtle.exportKey('raw', keyPair.publicKey)
474
-
const publicBytes = new Uint8Array(publicRaw)
475
-
const compressed = compressPublicKey(publicBytes)
497
+
const publicRaw = await crypto.subtle.exportKey('raw', keyPair.publicKey);
498
+
const publicBytes = new Uint8Array(publicRaw);
499
+
const compressed = compressPublicKey(publicBytes);
476
500
477
-
return { privateKey: privateBytes, publicKey: compressed }
501
+
return { privateKey: privateBytes, publicKey: compressed };
478
502
}
479
503
480
504
function compressPublicKey(uncompressed) {
481
505
// uncompressed is 65 bytes: 0x04 + x(32) + y(32)
482
506
// compressed is 33 bytes: prefix(02 or 03) + x(32)
483
-
const x = uncompressed.slice(1, 33)
484
-
const y = uncompressed.slice(33, 65)
485
-
const prefix = (y[31] & 1) === 0 ? 0x02 : 0x03
486
-
const compressed = new Uint8Array(33)
487
-
compressed[0] = prefix
488
-
compressed.set(x, 1)
489
-
return compressed
507
+
const x = uncompressed.slice(1, 33);
508
+
const y = uncompressed.slice(33, 65);
509
+
const prefix = (y[31] & 1) === 0 ? 0x02 : 0x03;
510
+
const compressed = new Uint8Array(33);
511
+
compressed[0] = prefix;
512
+
compressed.set(x, 1);
513
+
return compressed;
490
514
}
491
515
492
516
/**
···
495
519
* @returns {string} Base64url-encoded string
496
520
*/
497
521
export function base64UrlEncode(bytes) {
498
-
let binary = ''
522
+
let binary = '';
499
523
for (const byte of bytes) {
500
-
binary += String.fromCharCode(byte)
524
+
binary += String.fromCharCode(byte);
501
525
}
502
-
const base64 = btoa(binary)
503
-
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
526
+
const base64 = btoa(binary);
527
+
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
504
528
}
505
529
506
530
/**
···
509
533
* @returns {Uint8Array} Decoded bytes
510
534
*/
511
535
export function base64UrlDecode(str) {
512
-
const base64 = str.replace(/-/g, '+').replace(/_/g, '/')
513
-
const pad = base64.length % 4
514
-
const padded = pad ? base64 + '='.repeat(4 - pad) : base64
515
-
const binary = atob(padded)
516
-
const bytes = new Uint8Array(binary.length)
536
+
const base64 = str.replace(/-/g, '+').replace(/_/g, '/');
537
+
const pad = base64.length % 4;
538
+
const padded = pad ? base64 + '='.repeat(4 - pad) : base64;
539
+
const binary = atob(padded);
540
+
const bytes = new Uint8Array(binary.length);
517
541
for (let i = 0; i < binary.length; i++) {
518
-
bytes[i] = binary.charCodeAt(i)
542
+
bytes[i] = binary.charCodeAt(i);
519
543
}
520
-
return bytes
544
+
return bytes;
521
545
}
522
546
523
547
/**
···
532
556
new TextEncoder().encode(secret),
533
557
{ name: 'HMAC', hash: 'SHA-256' },
534
558
false,
535
-
['sign']
536
-
)
537
-
const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(data))
538
-
return base64UrlEncode(new Uint8Array(sig))
559
+
['sign'],
560
+
);
561
+
const sig = await crypto.subtle.sign(
562
+
'HMAC',
563
+
key,
564
+
new TextEncoder().encode(data),
565
+
);
566
+
return base64UrlEncode(new Uint8Array(sig));
539
567
}
540
568
541
569
/**
···
546
574
* @returns {Promise<string>} Signed JWT
547
575
*/
548
576
export async function createAccessJwt(did, secret, expiresIn = 7200) {
549
-
const header = { typ: 'at+jwt', alg: 'HS256' }
550
-
const now = Math.floor(Date.now() / 1000)
577
+
const header = { typ: 'at+jwt', alg: 'HS256' };
578
+
const now = Math.floor(Date.now() / 1000);
551
579
const payload = {
552
580
scope: 'com.atproto.access',
553
581
sub: did,
554
582
aud: did,
555
583
iat: now,
556
-
exp: now + expiresIn
557
-
}
584
+
exp: now + expiresIn,
585
+
};
558
586
559
-
const headerB64 = base64UrlEncode(new TextEncoder().encode(JSON.stringify(header)))
560
-
const payloadB64 = base64UrlEncode(new TextEncoder().encode(JSON.stringify(payload)))
561
-
const signature = await hmacSign(`${headerB64}.${payloadB64}`, secret)
587
+
const headerB64 = base64UrlEncode(
588
+
new TextEncoder().encode(JSON.stringify(header)),
589
+
);
590
+
const payloadB64 = base64UrlEncode(
591
+
new TextEncoder().encode(JSON.stringify(payload)),
592
+
);
593
+
const signature = await hmacSign(`${headerB64}.${payloadB64}`, secret);
562
594
563
-
return `${headerB64}.${payloadB64}.${signature}`
595
+
return `${headerB64}.${payloadB64}.${signature}`;
564
596
}
565
597
566
598
/**
···
571
603
* @returns {Promise<string>} Signed JWT
572
604
*/
573
605
export async function createRefreshJwt(did, secret, expiresIn = 7776000) {
574
-
const header = { typ: 'refresh+jwt', alg: 'HS256' }
575
-
const now = Math.floor(Date.now() / 1000)
606
+
const header = { typ: 'refresh+jwt', alg: 'HS256' };
607
+
const now = Math.floor(Date.now() / 1000);
576
608
// Generate random jti (token ID)
577
-
const jtiBytes = new Uint8Array(32)
578
-
crypto.getRandomValues(jtiBytes)
579
-
const jti = base64UrlEncode(jtiBytes)
609
+
const jtiBytes = new Uint8Array(32);
610
+
crypto.getRandomValues(jtiBytes);
611
+
const jti = base64UrlEncode(jtiBytes);
580
612
581
613
const payload = {
582
614
scope: 'com.atproto.refresh',
···
584
616
aud: did,
585
617
jti,
586
618
iat: now,
587
-
exp: now + expiresIn
588
-
}
619
+
exp: now + expiresIn,
620
+
};
589
621
590
-
const headerB64 = base64UrlEncode(new TextEncoder().encode(JSON.stringify(header)))
591
-
const payloadB64 = base64UrlEncode(new TextEncoder().encode(JSON.stringify(payload)))
592
-
const signature = await hmacSign(`${headerB64}.${payloadB64}`, secret)
622
+
const headerB64 = base64UrlEncode(
623
+
new TextEncoder().encode(JSON.stringify(header)),
624
+
);
625
+
const payloadB64 = base64UrlEncode(
626
+
new TextEncoder().encode(JSON.stringify(payload)),
627
+
);
628
+
const signature = await hmacSign(`${headerB64}.${payloadB64}`, secret);
593
629
594
-
return `${headerB64}.${payloadB64}.${signature}`
630
+
return `${headerB64}.${payloadB64}.${signature}`;
595
631
}
596
632
597
633
/**
···
602
638
* @throws {Error} If token is invalid, expired, or wrong type
603
639
*/
604
640
export async function verifyAccessJwt(jwt, secret) {
605
-
const parts = jwt.split('.')
641
+
const parts = jwt.split('.');
606
642
if (parts.length !== 3) {
607
-
throw new Error('Invalid JWT format')
643
+
throw new Error('Invalid JWT format');
608
644
}
609
645
610
-
const [headerB64, payloadB64, signatureB64] = parts
646
+
const [headerB64, payloadB64, signatureB64] = parts;
611
647
612
648
// Verify signature
613
-
const expectedSig = await hmacSign(`${headerB64}.${payloadB64}`, secret)
649
+
const expectedSig = await hmacSign(`${headerB64}.${payloadB64}`, secret);
614
650
if (signatureB64 !== expectedSig) {
615
-
throw new Error('Invalid signature')
651
+
throw new Error('Invalid signature');
616
652
}
617
653
618
654
// Decode header and payload
619
-
const header = JSON.parse(new TextDecoder().decode(base64UrlDecode(headerB64)))
620
-
const payload = JSON.parse(new TextDecoder().decode(base64UrlDecode(payloadB64)))
655
+
const header = JSON.parse(
656
+
new TextDecoder().decode(base64UrlDecode(headerB64)),
657
+
);
658
+
const payload = JSON.parse(
659
+
new TextDecoder().decode(base64UrlDecode(payloadB64)),
660
+
);
621
661
622
662
// Check token type
623
663
if (header.typ !== 'at+jwt') {
624
-
throw new Error('Invalid token type: expected access token')
664
+
throw new Error('Invalid token type: expected access token');
625
665
}
626
666
627
667
// Check expiration
628
-
const now = Math.floor(Date.now() / 1000)
668
+
const now = Math.floor(Date.now() / 1000);
629
669
if (payload.exp && payload.exp < now) {
630
-
throw new Error('Token expired')
670
+
throw new Error('Token expired');
631
671
}
632
672
633
-
return payload
673
+
return payload;
634
674
}
635
675
636
676
/**
···
644
684
* @returns {Promise<string>} Signed JWT
645
685
*/
646
686
export async function createServiceJwt({ iss, aud, lxm, signingKey }) {
647
-
const header = { typ: 'JWT', alg: 'ES256' }
648
-
const now = Math.floor(Date.now() / 1000)
687
+
const header = { typ: 'JWT', alg: 'ES256' };
688
+
const now = Math.floor(Date.now() / 1000);
649
689
650
690
// Generate random jti
651
-
const jtiBytes = new Uint8Array(16)
652
-
crypto.getRandomValues(jtiBytes)
653
-
const jti = bytesToHex(jtiBytes)
691
+
const jtiBytes = new Uint8Array(16);
692
+
crypto.getRandomValues(jtiBytes);
693
+
const jti = bytesToHex(jtiBytes);
654
694
655
695
const payload = {
656
696
iss,
657
697
aud,
658
698
exp: now + 60, // 1 minute expiration
659
699
iat: now,
660
-
jti
661
-
}
662
-
if (lxm) payload.lxm = lxm
700
+
jti,
701
+
};
702
+
if (lxm) payload.lxm = lxm;
663
703
664
-
const headerB64 = base64UrlEncode(new TextEncoder().encode(JSON.stringify(header)))
665
-
const payloadB64 = base64UrlEncode(new TextEncoder().encode(JSON.stringify(payload)))
666
-
const toSign = new TextEncoder().encode(`${headerB64}.${payloadB64}`)
704
+
const headerB64 = base64UrlEncode(
705
+
new TextEncoder().encode(JSON.stringify(header)),
706
+
);
707
+
const payloadB64 = base64UrlEncode(
708
+
new TextEncoder().encode(JSON.stringify(payload)),
709
+
);
710
+
const toSign = new TextEncoder().encode(`${headerB64}.${payloadB64}`);
667
711
668
-
const sig = await sign(signingKey, toSign)
669
-
const sigB64 = base64UrlEncode(sig)
712
+
const sig = await sign(signingKey, toSign);
713
+
const sigB64 = base64UrlEncode(sig);
670
714
671
-
return `${headerB64}.${payloadB64}.${sigB64}`
715
+
return `${headerB64}.${payloadB64}.${sigB64}`;
672
716
}
673
717
674
718
/**
···
677
721
* @returns {string} Hex string
678
722
*/
679
723
export function bytesToHex(bytes) {
680
-
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('')
724
+
return Array.from(bytes)
725
+
.map((b) => b.toString(16).padStart(2, '0'))
726
+
.join('');
681
727
}
682
728
683
729
/**
···
686
732
* @returns {Uint8Array} Decoded bytes
687
733
*/
688
734
export function hexToBytes(hex) {
689
-
const bytes = new Uint8Array(hex.length / 2)
735
+
const bytes = new Uint8Array(hex.length / 2);
690
736
for (let i = 0; i < hex.length; i += 2) {
691
-
bytes[i / 2] = parseInt(hex.substr(i, 2), 16)
737
+
bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
692
738
}
693
-
return bytes
739
+
return bytes;
694
740
}
695
741
696
742
// === MERKLE SEARCH TREE ===
697
743
// ATProto-compliant MST implementation
698
744
699
745
async function sha256(data) {
700
-
const hash = await crypto.subtle.digest('SHA-256', data)
701
-
return new Uint8Array(hash)
746
+
const hash = await crypto.subtle.digest('SHA-256', data);
747
+
return new Uint8Array(hash);
702
748
}
703
749
704
750
// Cache for key depths (SHA-256 is expensive)
705
-
const keyDepthCache = new Map()
751
+
const keyDepthCache = new Map();
706
752
707
753
/**
708
754
* Get MST tree depth for a key based on leading zeros in SHA-256 hash
···
711
757
*/
712
758
export async function getKeyDepth(key) {
713
759
// Count leading zeros in SHA-256 hash, divide by 2
714
-
if (keyDepthCache.has(key)) return keyDepthCache.get(key)
760
+
if (keyDepthCache.has(key)) return keyDepthCache.get(key);
715
761
716
-
const keyBytes = new TextEncoder().encode(key)
717
-
const hash = await sha256(keyBytes)
762
+
const keyBytes = new TextEncoder().encode(key);
763
+
const hash = await sha256(keyBytes);
718
764
719
-
let zeros = 0
765
+
let zeros = 0;
720
766
for (const byte of hash) {
721
767
if (byte === 0) {
722
-
zeros += 8
768
+
zeros += 8;
723
769
} else {
724
770
// Count leading zeros in this byte
725
771
for (let i = 7; i >= 0; i--) {
726
-
if ((byte >> i) & 1) break
727
-
zeros++
772
+
if ((byte >> i) & 1) break;
773
+
zeros++;
728
774
}
729
-
break
775
+
break;
730
776
}
731
777
}
732
778
733
779
// MST depth = leading zeros in SHA-256 hash / 2
734
780
// This creates a probabilistic tree where ~50% of keys are at depth 0,
735
781
// ~25% at depth 1, etc., giving O(log n) lookups
736
-
const depth = Math.floor(zeros / 2)
737
-
keyDepthCache.set(key, depth)
738
-
return depth
782
+
const depth = Math.floor(zeros / 2);
783
+
keyDepthCache.set(key, depth);
784
+
return depth;
739
785
}
740
786
741
787
// Compute common prefix length between two byte arrays
742
788
function commonPrefixLen(a, b) {
743
-
const minLen = Math.min(a.length, b.length)
789
+
const minLen = Math.min(a.length, b.length);
744
790
for (let i = 0; i < minLen; i++) {
745
-
if (a[i] !== b[i]) return i
791
+
if (a[i] !== b[i]) return i;
746
792
}
747
-
return minLen
793
+
return minLen;
748
794
}
749
795
750
796
class MST {
751
797
constructor(sql) {
752
-
this.sql = sql
798
+
this.sql = sql;
753
799
}
754
800
755
801
async computeRoot() {
756
-
const records = this.sql.exec(`
802
+
const records = this.sql
803
+
.exec(`
757
804
SELECT collection, rkey, cid FROM records ORDER BY collection, rkey
758
-
`).toArray()
805
+
`)
806
+
.toArray();
759
807
760
808
if (records.length === 0) {
761
-
return null
809
+
return null;
762
810
}
763
811
764
812
// Build entries with pre-computed depths (heights)
765
813
// In ATProto MST, "height" determines which layer a key belongs to
766
814
// Layer 0 is at the BOTTOM, root is at the highest layer
767
-
const entries = []
768
-
let maxDepth = 0
815
+
const entries = [];
816
+
let maxDepth = 0;
769
817
for (const r of records) {
770
-
const key = `${r.collection}/${r.rkey}`
771
-
const depth = await getKeyDepth(key)
772
-
maxDepth = Math.max(maxDepth, depth)
818
+
const key = `${r.collection}/${r.rkey}`;
819
+
const depth = await getKeyDepth(key);
820
+
maxDepth = Math.max(maxDepth, depth);
773
821
entries.push({
774
822
key,
775
823
keyBytes: new TextEncoder().encode(key),
776
824
cid: r.cid,
777
-
depth
778
-
})
825
+
depth,
826
+
});
779
827
}
780
828
781
829
// Start building from the root (highest layer) going down to layer 0
782
-
return this.buildTree(entries, maxDepth)
830
+
return this.buildTree(entries, maxDepth);
783
831
}
784
832
785
833
async buildTree(entries, layer) {
786
-
if (entries.length === 0) return null
834
+
if (entries.length === 0) return null;
787
835
788
836
// Separate entries for this layer vs lower layers (subtrees)
789
837
// Keys with depth == layer stay at this node
790
838
// Keys with depth < layer go into subtrees (going down toward layer 0)
791
-
const thisLayer = []
792
-
let leftSubtree = []
839
+
const thisLayer = [];
840
+
let leftSubtree = [];
793
841
794
842
for (const entry of entries) {
795
843
if (entry.depth < layer) {
796
844
// This entry belongs to a lower layer - accumulate for subtree
797
-
leftSubtree.push(entry)
845
+
leftSubtree.push(entry);
798
846
} else {
799
847
// This entry belongs at current layer (depth == layer)
800
848
// Process accumulated left subtree first
801
849
if (leftSubtree.length > 0) {
802
-
const leftCid = await this.buildTree(leftSubtree, layer - 1)
803
-
thisLayer.push({ type: 'subtree', cid: leftCid })
804
-
leftSubtree = []
850
+
const leftCid = await this.buildTree(leftSubtree, layer - 1);
851
+
thisLayer.push({ type: 'subtree', cid: leftCid });
852
+
leftSubtree = [];
805
853
}
806
-
thisLayer.push({ type: 'entry', entry })
854
+
thisLayer.push({ type: 'entry', entry });
807
855
}
808
856
}
809
857
810
858
// Handle remaining left subtree
811
859
if (leftSubtree.length > 0) {
812
-
const leftCid = await this.buildTree(leftSubtree, layer - 1)
813
-
thisLayer.push({ type: 'subtree', cid: leftCid })
860
+
const leftCid = await this.buildTree(leftSubtree, layer - 1);
861
+
thisLayer.push({ type: 'subtree', cid: leftCid });
814
862
}
815
863
816
864
// Build node with proper ATProto format
817
-
const node = { e: [] }
818
-
let leftCid = null
819
-
let prevKeyBytes = new Uint8Array(0)
865
+
const node = { e: [] };
866
+
let leftCid = null;
867
+
let prevKeyBytes = new Uint8Array(0);
820
868
821
869
for (let i = 0; i < thisLayer.length; i++) {
822
-
const item = thisLayer[i]
870
+
const item = thisLayer[i];
823
871
824
872
if (item.type === 'subtree') {
825
873
if (node.e.length === 0) {
826
-
leftCid = item.cid
874
+
leftCid = item.cid;
827
875
} else {
828
876
// Attach to previous entry's 't' field
829
-
node.e[node.e.length - 1].t = new CID(cidToBytes(item.cid))
877
+
node.e[node.e.length - 1].t = new CID(cidToBytes(item.cid));
830
878
}
831
879
} else {
832
880
// Entry - compute prefix compression
833
-
const keyBytes = item.entry.keyBytes
834
-
const prefixLen = commonPrefixLen(prevKeyBytes, keyBytes)
835
-
const keySuffix = keyBytes.slice(prefixLen)
881
+
const keyBytes = item.entry.keyBytes;
882
+
const prefixLen = commonPrefixLen(prevKeyBytes, keyBytes);
883
+
const keySuffix = keyBytes.slice(prefixLen);
836
884
837
885
// ATProto requires t field to be present (can be null)
838
886
const e = {
839
887
p: prefixLen,
840
888
k: keySuffix,
841
889
v: new CID(cidToBytes(item.entry.cid)),
842
-
t: null // Will be updated if there's a subtree
843
-
}
890
+
t: null, // Will be updated if there's a subtree
891
+
};
844
892
845
-
node.e.push(e)
846
-
prevKeyBytes = keyBytes
893
+
node.e.push(e);
894
+
prevKeyBytes = keyBytes;
847
895
}
848
896
}
849
897
850
898
// ATProto requires l field to be present (can be null)
851
-
node.l = leftCid ? new CID(cidToBytes(leftCid)) : null
899
+
node.l = leftCid ? new CID(cidToBytes(leftCid)) : null;
852
900
853
901
// Encode node with proper MST CBOR format
854
-
const nodeBytes = cborEncodeDagCbor(node)
855
-
const nodeCid = await createCid(nodeBytes)
856
-
const cidStr = cidToString(nodeCid)
902
+
const nodeBytes = cborEncodeDagCbor(node);
903
+
const nodeCid = await createCid(nodeBytes);
904
+
const cidStr = cidToString(nodeCid);
857
905
858
906
this.sql.exec(
859
907
`INSERT OR REPLACE INTO blocks (cid, data) VALUES (?, ?)`,
860
908
cidStr,
861
-
nodeBytes
862
-
)
909
+
nodeBytes,
910
+
);
863
911
864
-
return cidStr
912
+
return cidStr;
865
913
}
866
914
}
867
915
···
873
921
* @returns {Uint8Array} Varint-encoded bytes
874
922
*/
875
923
export function varint(n) {
876
-
const bytes = []
924
+
const bytes = [];
877
925
while (n >= 0x80) {
878
-
bytes.push((n & 0x7f) | 0x80)
879
-
n >>>= 7
926
+
bytes.push((n & 0x7f) | 0x80);
927
+
n >>>= 7;
880
928
}
881
-
bytes.push(n)
882
-
return new Uint8Array(bytes)
929
+
bytes.push(n);
930
+
return new Uint8Array(bytes);
883
931
}
884
932
885
933
/**
···
889
937
*/
890
938
export function cidToBytes(cidStr) {
891
939
// Decode base32lower CID string to bytes
892
-
if (!cidStr.startsWith('b')) throw new Error('expected base32lower CID')
893
-
return base32Decode(cidStr.slice(1))
940
+
if (!cidStr.startsWith('b')) throw new Error('expected base32lower CID');
941
+
return base32Decode(cidStr.slice(1));
894
942
}
895
943
896
944
/**
···
899
947
* @returns {Uint8Array} Decoded bytes
900
948
*/
901
949
export function base32Decode(str) {
902
-
const alphabet = 'abcdefghijklmnopqrstuvwxyz234567'
903
-
let bits = 0
904
-
let value = 0
905
-
const output = []
950
+
const alphabet = 'abcdefghijklmnopqrstuvwxyz234567';
951
+
let bits = 0;
952
+
let value = 0;
953
+
const output = [];
906
954
907
955
for (const char of str) {
908
-
const idx = alphabet.indexOf(char)
909
-
if (idx === -1) continue
910
-
value = (value << 5) | idx
911
-
bits += 5
956
+
const idx = alphabet.indexOf(char);
957
+
if (idx === -1) continue;
958
+
value = (value << 5) | idx;
959
+
bits += 5;
912
960
if (bits >= 8) {
913
-
bits -= 8
914
-
output.push((value >> bits) & 0xff)
961
+
bits -= 8;
962
+
output.push((value >> bits) & 0xff);
915
963
}
916
964
}
917
965
918
-
return new Uint8Array(output)
966
+
return new Uint8Array(output);
919
967
}
920
968
921
969
/**
···
925
973
* @returns {Uint8Array} CAR file bytes
926
974
*/
927
975
export function buildCarFile(rootCid, blocks) {
928
-
const parts = []
976
+
const parts = [];
929
977
930
978
// Header: { version: 1, roots: [rootCid] }
931
-
const rootCidBytes = cidToBytes(rootCid)
932
-
const header = cborEncodeDagCbor({ version: 1, roots: [new CID(rootCidBytes)] })
933
-
parts.push(varint(header.length))
934
-
parts.push(header)
979
+
const rootCidBytes = cidToBytes(rootCid);
980
+
const header = cborEncodeDagCbor({
981
+
version: 1,
982
+
roots: [new CID(rootCidBytes)],
983
+
});
984
+
parts.push(varint(header.length));
985
+
parts.push(header);
935
986
936
987
// Blocks: varint(len) + cid + data
937
988
for (const block of blocks) {
938
-
const cidBytes = cidToBytes(block.cid)
939
-
const blockLen = cidBytes.length + block.data.length
940
-
parts.push(varint(blockLen))
941
-
parts.push(cidBytes)
942
-
parts.push(block.data)
989
+
const cidBytes = cidToBytes(block.cid);
990
+
const blockLen = cidBytes.length + block.data.length;
991
+
parts.push(varint(blockLen));
992
+
parts.push(cidBytes);
993
+
parts.push(block.data);
943
994
}
944
995
945
996
// Concatenate all parts
946
-
const totalLen = parts.reduce((sum, p) => sum + p.length, 0)
947
-
const car = new Uint8Array(totalLen)
948
-
let offset = 0
997
+
const totalLen = parts.reduce((sum, p) => sum + p.length, 0);
998
+
const car = new Uint8Array(totalLen);
999
+
let offset = 0;
949
1000
for (const part of parts) {
950
-
car.set(part, offset)
951
-
offset += part.length
1001
+
car.set(part, offset);
1002
+
offset += part.length;
952
1003
}
953
1004
954
-
return car
1005
+
return car;
955
1006
}
956
1007
957
1008
/**
···
972
1023
/** @type {Record<string, Route>} */
973
1024
const pdsRoutes = {
974
1025
'/.well-known/atproto-did': {
975
-
handler: (pds, req, url) => pds.handleAtprotoDid()
1026
+
handler: (pds, _req, _url) => pds.handleAtprotoDid(),
976
1027
},
977
1028
'/init': {
978
1029
method: 'POST',
979
-
handler: (pds, req, url) => pds.handleInit(req)
1030
+
handler: (pds, req, _url) => pds.handleInit(req),
980
1031
},
981
1032
'/status': {
982
-
handler: (pds, req, url) => pds.handleStatus()
1033
+
handler: (pds, _req, _url) => pds.handleStatus(),
983
1034
},
984
1035
'/reset-repo': {
985
-
handler: (pds, req, url) => pds.handleResetRepo()
1036
+
handler: (pds, _req, _url) => pds.handleResetRepo(),
986
1037
},
987
1038
'/forward-event': {
988
-
handler: (pds, req, url) => pds.handleForwardEvent(req)
1039
+
handler: (pds, req, _url) => pds.handleForwardEvent(req),
989
1040
},
990
1041
'/register-did': {
991
-
handler: (pds, req, url) => pds.handleRegisterDid(req)
1042
+
handler: (pds, req, _url) => pds.handleRegisterDid(req),
992
1043
},
993
1044
'/get-registered-dids': {
994
-
handler: (pds, req, url) => pds.handleGetRegisteredDids()
1045
+
handler: (pds, _req, _url) => pds.handleGetRegisteredDids(),
995
1046
},
996
1047
'/register-handle': {
997
1048
method: 'POST',
998
-
handler: (pds, req, url) => pds.handleRegisterHandle(req)
1049
+
handler: (pds, req, _url) => pds.handleRegisterHandle(req),
999
1050
},
1000
1051
'/resolve-handle': {
1001
-
handler: (pds, req, url) => pds.handleResolveHandle(url)
1052
+
handler: (pds, _req, url) => pds.handleResolveHandle(url),
1002
1053
},
1003
1054
'/repo-info': {
1004
-
handler: (pds, req, url) => pds.handleRepoInfo()
1055
+
handler: (pds, _req, _url) => pds.handleRepoInfo(),
1005
1056
},
1006
1057
'/xrpc/com.atproto.server.describeServer': {
1007
-
handler: (pds, req, url) => pds.handleDescribeServer(req)
1058
+
handler: (pds, req, _url) => pds.handleDescribeServer(req),
1008
1059
},
1009
1060
'/xrpc/com.atproto.server.createSession': {
1010
1061
method: 'POST',
1011
-
handler: (pds, req, url) => pds.handleCreateSession(req)
1062
+
handler: (pds, req, _url) => pds.handleCreateSession(req),
1012
1063
},
1013
1064
'/xrpc/com.atproto.server.getSession': {
1014
-
handler: (pds, req, url) => pds.handleGetSession(req)
1065
+
handler: (pds, req, _url) => pds.handleGetSession(req),
1015
1066
},
1016
1067
'/xrpc/app.bsky.actor.getPreferences': {
1017
-
handler: (pds, req, url) => pds.handleGetPreferences(req)
1068
+
handler: (pds, req, _url) => pds.handleGetPreferences(req),
1018
1069
},
1019
1070
'/xrpc/app.bsky.actor.putPreferences': {
1020
1071
method: 'POST',
1021
-
handler: (pds, req, url) => pds.handlePutPreferences(req)
1072
+
handler: (pds, req, _url) => pds.handlePutPreferences(req),
1022
1073
},
1023
1074
'/xrpc/com.atproto.sync.listRepos': {
1024
-
handler: (pds, req, url) => pds.handleListRepos()
1075
+
handler: (pds, _req, _url) => pds.handleListRepos(),
1025
1076
},
1026
1077
'/xrpc/com.atproto.repo.createRecord': {
1027
1078
method: 'POST',
1028
-
handler: (pds, req, url) => pds.handleCreateRecord(req)
1079
+
handler: (pds, req, _url) => pds.handleCreateRecord(req),
1029
1080
},
1030
1081
'/xrpc/com.atproto.repo.deleteRecord': {
1031
1082
method: 'POST',
1032
-
handler: (pds, req, url) => pds.handleDeleteRecord(req)
1083
+
handler: (pds, req, _url) => pds.handleDeleteRecord(req),
1033
1084
},
1034
1085
'/xrpc/com.atproto.repo.putRecord': {
1035
1086
method: 'POST',
1036
-
handler: (pds, req, url) => pds.handlePutRecord(req)
1087
+
handler: (pds, req, _url) => pds.handlePutRecord(req),
1037
1088
},
1038
1089
'/xrpc/com.atproto.repo.applyWrites': {
1039
1090
method: 'POST',
1040
-
handler: (pds, req, url) => pds.handleApplyWrites(req)
1091
+
handler: (pds, req, _url) => pds.handleApplyWrites(req),
1041
1092
},
1042
1093
'/xrpc/com.atproto.repo.getRecord': {
1043
-
handler: (pds, req, url) => pds.handleGetRecord(url)
1094
+
handler: (pds, _req, url) => pds.handleGetRecord(url),
1044
1095
},
1045
1096
'/xrpc/com.atproto.repo.describeRepo': {
1046
-
handler: (pds, req, url) => pds.handleDescribeRepo()
1097
+
handler: (pds, _req, _url) => pds.handleDescribeRepo(),
1047
1098
},
1048
1099
'/xrpc/com.atproto.repo.listRecords': {
1049
-
handler: (pds, req, url) => pds.handleListRecords(url)
1100
+
handler: (pds, _req, url) => pds.handleListRecords(url),
1050
1101
},
1051
1102
'/xrpc/com.atproto.sync.getLatestCommit': {
1052
-
handler: (pds, req, url) => pds.handleGetLatestCommit()
1103
+
handler: (pds, _req, _url) => pds.handleGetLatestCommit(),
1053
1104
},
1054
1105
'/xrpc/com.atproto.sync.getRepoStatus': {
1055
-
handler: (pds, req, url) => pds.handleGetRepoStatus()
1106
+
handler: (pds, _req, _url) => pds.handleGetRepoStatus(),
1056
1107
},
1057
1108
'/xrpc/com.atproto.sync.getRepo': {
1058
-
handler: (pds, req, url) => pds.handleGetRepo()
1109
+
handler: (pds, _req, _url) => pds.handleGetRepo(),
1059
1110
},
1060
1111
'/xrpc/com.atproto.sync.getRecord': {
1061
-
handler: (pds, req, url) => pds.handleSyncGetRecord(url)
1112
+
handler: (pds, _req, url) => pds.handleSyncGetRecord(url),
1062
1113
},
1063
1114
'/xrpc/com.atproto.sync.subscribeRepos': {
1064
-
handler: (pds, req, url) => pds.handleSubscribeRepos(req, url)
1065
-
}
1066
-
}
1115
+
handler: (pds, req, url) => pds.handleSubscribeRepos(req, url),
1116
+
},
1117
+
};
1067
1118
1068
1119
export class PersonalDataServer {
1069
1120
constructor(state, env) {
1070
-
this.state = state
1071
-
this.sql = state.storage.sql
1072
-
this.env = env
1121
+
this.state = state;
1122
+
this.sql = state.storage.sql;
1123
+
this.env = env;
1073
1124
1074
1125
// Initialize schema
1075
1126
this.sql.exec(`
···
1101
1152
);
1102
1153
1103
1154
CREATE INDEX IF NOT EXISTS idx_records_collection ON records(collection, rkey);
1104
-
`)
1155
+
`);
1105
1156
}
1106
1157
1107
1158
async initIdentity(did, privateKeyHex, handle = null) {
1108
-
await this.state.storage.put('did', did)
1109
-
await this.state.storage.put('privateKey', privateKeyHex)
1159
+
await this.state.storage.put('did', did);
1160
+
await this.state.storage.put('privateKey', privateKeyHex);
1110
1161
if (handle) {
1111
-
await this.state.storage.put('handle', handle)
1162
+
await this.state.storage.put('handle', handle);
1112
1163
}
1113
1164
}
1114
1165
1115
1166
async getDid() {
1116
1167
if (!this._did) {
1117
-
this._did = await this.state.storage.get('did')
1168
+
this._did = await this.state.storage.get('did');
1118
1169
}
1119
-
return this._did
1170
+
return this._did;
1120
1171
}
1121
1172
1122
1173
async getHandle() {
1123
-
return this.state.storage.get('handle')
1174
+
return this.state.storage.get('handle');
1124
1175
}
1125
1176
1126
1177
async getSigningKey() {
1127
-
const hex = await this.state.storage.get('privateKey')
1128
-
if (!hex) return null
1129
-
return importPrivateKey(hexToBytes(hex))
1178
+
const hex = await this.state.storage.get('privateKey');
1179
+
if (!hex) return null;
1180
+
return importPrivateKey(hexToBytes(hex));
1130
1181
}
1131
1182
1132
1183
// Collect MST node blocks for a given root CID
1133
1184
collectMstBlocks(rootCidStr) {
1134
-
const blocks = []
1135
-
const visited = new Set()
1185
+
const blocks = [];
1186
+
const visited = new Set();
1136
1187
1137
1188
const collect = (cidStr) => {
1138
-
if (visited.has(cidStr)) return
1139
-
visited.add(cidStr)
1189
+
if (visited.has(cidStr)) return;
1190
+
visited.add(cidStr);
1140
1191
1141
-
const rows = this.sql.exec(
1142
-
`SELECT data FROM blocks WHERE cid = ?`, cidStr
1143
-
).toArray()
1144
-
if (rows.length === 0) return
1192
+
const rows = this.sql
1193
+
.exec(`SELECT data FROM blocks WHERE cid = ?`, cidStr)
1194
+
.toArray();
1195
+
if (rows.length === 0) return;
1145
1196
1146
-
const data = new Uint8Array(rows[0].data)
1147
-
blocks.push({ cid: cidStr, data }) // Keep as string, buildCarFile will convert
1197
+
const data = new Uint8Array(rows[0].data);
1198
+
blocks.push({ cid: cidStr, data }); // Keep as string, buildCarFile will convert
1148
1199
1149
1200
// Decode and follow child CIDs (MST nodes have 'l' and 'e' with 't' subtrees)
1150
1201
try {
1151
-
const node = cborDecode(data)
1152
-
if (node.l) collect(cidToString(node.l))
1202
+
const node = cborDecode(data);
1203
+
if (node.l) collect(cidToString(node.l));
1153
1204
if (node.e) {
1154
1205
for (const entry of node.e) {
1155
-
if (entry.t) collect(cidToString(entry.t))
1206
+
if (entry.t) collect(cidToString(entry.t));
1156
1207
}
1157
1208
}
1158
-
} catch (e) {
1209
+
} catch (_e) {
1159
1210
// Not an MST node, ignore
1160
1211
}
1161
-
}
1212
+
};
1162
1213
1163
-
collect(rootCidStr)
1164
-
return blocks
1214
+
collect(rootCidStr);
1215
+
return blocks;
1165
1216
}
1166
1217
1167
1218
async createRecord(collection, record, rkey = null) {
1168
-
const did = await this.getDid()
1169
-
if (!did) throw new Error('PDS not initialized')
1219
+
const did = await this.getDid();
1220
+
if (!did) throw new Error('PDS not initialized');
1170
1221
1171
-
rkey = rkey || createTid()
1172
-
const uri = `at://${did}/${collection}/${rkey}`
1222
+
rkey = rkey || createTid();
1223
+
const uri = `at://${did}/${collection}/${rkey}`;
1173
1224
1174
1225
// Encode and hash record (must use DAG-CBOR for proper key ordering)
1175
-
const recordBytes = cborEncodeDagCbor(record)
1176
-
const recordCid = await createCid(recordBytes)
1177
-
const recordCidStr = cidToString(recordCid)
1226
+
const recordBytes = cborEncodeDagCbor(record);
1227
+
const recordCid = await createCid(recordBytes);
1228
+
const recordCidStr = cidToString(recordCid);
1178
1229
1179
1230
// Store block
1180
1231
this.sql.exec(
1181
1232
`INSERT OR REPLACE INTO blocks (cid, data) VALUES (?, ?)`,
1182
-
recordCidStr, recordBytes
1183
-
)
1233
+
recordCidStr,
1234
+
recordBytes,
1235
+
);
1184
1236
1185
1237
// Store record index
1186
1238
this.sql.exec(
1187
1239
`INSERT OR REPLACE INTO records (uri, cid, collection, rkey, value) VALUES (?, ?, ?, ?, ?)`,
1188
-
uri, recordCidStr, collection, rkey, recordBytes
1189
-
)
1240
+
uri,
1241
+
recordCidStr,
1242
+
collection,
1243
+
rkey,
1244
+
recordBytes,
1245
+
);
1190
1246
1191
1247
// Rebuild MST
1192
-
const mst = new MST(this.sql)
1193
-
const dataRoot = await mst.computeRoot()
1248
+
const mst = new MST(this.sql);
1249
+
const dataRoot = await mst.computeRoot();
1194
1250
1195
1251
// Get previous commit
1196
-
const prevCommits = this.sql.exec(
1197
-
`SELECT cid, rev FROM commits ORDER BY seq DESC LIMIT 1`
1198
-
).toArray()
1199
-
const prevCommit = prevCommits.length > 0 ? prevCommits[0] : null
1252
+
const prevCommits = this.sql
1253
+
.exec(`SELECT cid, rev FROM commits ORDER BY seq DESC LIMIT 1`)
1254
+
.toArray();
1255
+
const prevCommit = prevCommits.length > 0 ? prevCommits[0] : null;
1200
1256
1201
1257
// Create commit
1202
-
const rev = createTid()
1258
+
const rev = createTid();
1203
1259
// Build commit with CIDs wrapped in CID class (for dag-cbor tag 42 encoding)
1204
1260
const commit = {
1205
1261
did,
1206
1262
version: 3,
1207
-
data: new CID(cidToBytes(dataRoot)), // CID wrapped for explicit encoding
1263
+
data: new CID(cidToBytes(dataRoot)), // CID wrapped for explicit encoding
1208
1264
rev,
1209
-
prev: prevCommit?.cid ? new CID(cidToBytes(prevCommit.cid)) : null
1210
-
}
1265
+
prev: prevCommit?.cid ? new CID(cidToBytes(prevCommit.cid)) : null,
1266
+
};
1211
1267
1212
1268
// Sign commit (using dag-cbor encoder for CIDs)
1213
-
const commitBytes = cborEncodeDagCbor(commit)
1214
-
const signingKey = await this.getSigningKey()
1215
-
const sig = await sign(signingKey, commitBytes)
1269
+
const commitBytes = cborEncodeDagCbor(commit);
1270
+
const signingKey = await this.getSigningKey();
1271
+
const sig = await sign(signingKey, commitBytes);
1216
1272
1217
-
const signedCommit = { ...commit, sig }
1218
-
const signedBytes = cborEncodeDagCbor(signedCommit)
1219
-
const commitCid = await createCid(signedBytes)
1220
-
const commitCidStr = cidToString(commitCid)
1273
+
const signedCommit = { ...commit, sig };
1274
+
const signedBytes = cborEncodeDagCbor(signedCommit);
1275
+
const commitCid = await createCid(signedBytes);
1276
+
const commitCidStr = cidToString(commitCid);
1221
1277
1222
1278
// Store commit block
1223
1279
this.sql.exec(
1224
1280
`INSERT OR REPLACE INTO blocks (cid, data) VALUES (?, ?)`,
1225
-
commitCidStr, signedBytes
1226
-
)
1281
+
commitCidStr,
1282
+
signedBytes,
1283
+
);
1227
1284
1228
1285
// Store commit reference
1229
1286
this.sql.exec(
1230
1287
`INSERT INTO commits (cid, rev, prev) VALUES (?, ?, ?)`,
1231
-
commitCidStr, rev, prevCommit?.cid || null
1232
-
)
1288
+
commitCidStr,
1289
+
rev,
1290
+
prevCommit?.cid || null,
1291
+
);
1233
1292
1234
1293
// Update head and rev for listRepos
1235
-
await this.state.storage.put('head', commitCidStr)
1236
-
await this.state.storage.put('rev', rev)
1294
+
await this.state.storage.put('head', commitCidStr);
1295
+
await this.state.storage.put('rev', rev);
1237
1296
1238
1297
// Collect blocks for the event (record + commit + MST nodes)
1239
1298
// Build a mini CAR with just the new blocks - use string CIDs
1240
-
const newBlocks = []
1299
+
const newBlocks = [];
1241
1300
// Add record block
1242
-
newBlocks.push({ cid: recordCidStr, data: recordBytes })
1301
+
newBlocks.push({ cid: recordCidStr, data: recordBytes });
1243
1302
// Add commit block
1244
-
newBlocks.push({ cid: commitCidStr, data: signedBytes })
1303
+
newBlocks.push({ cid: commitCidStr, data: signedBytes });
1245
1304
// Add MST node blocks (get all blocks referenced by commit.data)
1246
-
const mstBlocks = this.collectMstBlocks(dataRoot)
1247
-
newBlocks.push(...mstBlocks)
1305
+
const mstBlocks = this.collectMstBlocks(dataRoot);
1306
+
newBlocks.push(...mstBlocks);
1248
1307
1249
1308
// Sequence event with blocks - store complete event data including rev and time
1250
1309
// blocks must be a full CAR file with header (roots = [commitCid])
1251
-
const eventTime = new Date().toISOString()
1310
+
const eventTime = new Date().toISOString();
1252
1311
const evt = cborEncode({
1253
-
ops: [{ action: 'create', path: `${collection}/${rkey}`, cid: recordCidStr }],
1254
-
blocks: buildCarFile(commitCidStr, newBlocks), // Full CAR with header
1255
-
rev, // Store the actual commit revision
1256
-
time: eventTime // Store the actual event time
1257
-
})
1312
+
ops: [
1313
+
{ action: 'create', path: `${collection}/${rkey}`, cid: recordCidStr },
1314
+
],
1315
+
blocks: buildCarFile(commitCidStr, newBlocks), // Full CAR with header
1316
+
rev, // Store the actual commit revision
1317
+
time: eventTime, // Store the actual event time
1318
+
});
1258
1319
this.sql.exec(
1259
1320
`INSERT INTO seq_events (did, commit_cid, evt) VALUES (?, ?, ?)`,
1260
-
did, commitCidStr, evt
1261
-
)
1321
+
did,
1322
+
commitCidStr,
1323
+
evt,
1324
+
);
1262
1325
1263
1326
// Broadcast to subscribers (both local and via default DO for relay)
1264
-
const evtRows = this.sql.exec(
1265
-
`SELECT * FROM seq_events ORDER BY seq DESC LIMIT 1`
1266
-
).toArray()
1327
+
const evtRows = this.sql
1328
+
.exec(`SELECT * FROM seq_events ORDER BY seq DESC LIMIT 1`)
1329
+
.toArray();
1267
1330
if (evtRows.length > 0) {
1268
-
this.broadcastEvent(evtRows[0])
1331
+
this.broadcastEvent(evtRows[0]);
1269
1332
// Also forward to default DO for relay subscribers
1270
1333
if (this.env?.PDS) {
1271
-
const defaultId = this.env.PDS.idFromName('default')
1272
-
const defaultPds = this.env.PDS.get(defaultId)
1334
+
const defaultId = this.env.PDS.idFromName('default');
1335
+
const defaultPds = this.env.PDS.get(defaultId);
1273
1336
// Convert ArrayBuffer to array for JSON serialization
1274
-
const row = evtRows[0]
1275
-
const evtArray = Array.from(new Uint8Array(row.evt))
1337
+
const row = evtRows[0];
1338
+
const evtArray = Array.from(new Uint8Array(row.evt));
1276
1339
// Fire and forget but log errors
1277
-
defaultPds.fetch(new Request('http://internal/forward-event', {
1278
-
method: 'POST',
1279
-
body: JSON.stringify({ ...row, evt: evtArray })
1280
-
})).then(r => r.json()).then(r => console.log('forward result:', r)).catch(e => console.log('forward error:', e))
1340
+
defaultPds
1341
+
.fetch(
1342
+
new Request('http://internal/forward-event', {
1343
+
method: 'POST',
1344
+
body: JSON.stringify({ ...row, evt: evtArray }),
1345
+
}),
1346
+
)
1347
+
.then((r) => r.json())
1348
+
.then((r) => console.log('forward result:', r))
1349
+
.catch((e) => console.log('forward error:', e));
1281
1350
}
1282
1351
}
1283
1352
1284
-
return { uri, cid: recordCidStr, commit: commitCidStr }
1353
+
return { uri, cid: recordCidStr, commit: commitCidStr };
1285
1354
}
1286
1355
1287
1356
async deleteRecord(collection, rkey) {
1288
-
const did = await this.getDid()
1289
-
if (!did) throw new Error('PDS not initialized')
1357
+
const did = await this.getDid();
1358
+
if (!did) throw new Error('PDS not initialized');
1290
1359
1291
-
const uri = `at://${did}/${collection}/${rkey}`
1360
+
const uri = `at://${did}/${collection}/${rkey}`;
1292
1361
1293
1362
// Check if record exists
1294
-
const existing = this.sql.exec(
1295
-
`SELECT cid FROM records WHERE uri = ?`, uri
1296
-
).toArray()
1363
+
const existing = this.sql
1364
+
.exec(`SELECT cid FROM records WHERE uri = ?`, uri)
1365
+
.toArray();
1297
1366
if (existing.length === 0) {
1298
-
return { error: 'RecordNotFound', message: 'record not found' }
1367
+
return { error: 'RecordNotFound', message: 'record not found' };
1299
1368
}
1300
1369
1301
1370
// Delete from records table
1302
-
this.sql.exec(`DELETE FROM records WHERE uri = ?`, uri)
1371
+
this.sql.exec(`DELETE FROM records WHERE uri = ?`, uri);
1303
1372
1304
1373
// Rebuild MST
1305
-
const mst = new MST(this.sql)
1306
-
const dataRoot = await mst.computeRoot()
1374
+
const mst = new MST(this.sql);
1375
+
const dataRoot = await mst.computeRoot();
1307
1376
1308
1377
// Get previous commit
1309
-
const prevCommits = this.sql.exec(
1310
-
`SELECT cid, rev FROM commits ORDER BY seq DESC LIMIT 1`
1311
-
).toArray()
1312
-
const prevCommit = prevCommits.length > 0 ? prevCommits[0] : null
1378
+
const prevCommits = this.sql
1379
+
.exec(`SELECT cid, rev FROM commits ORDER BY seq DESC LIMIT 1`)
1380
+
.toArray();
1381
+
const prevCommit = prevCommits.length > 0 ? prevCommits[0] : null;
1313
1382
1314
1383
// Create commit
1315
-
const rev = createTid()
1384
+
const rev = createTid();
1316
1385
const commit = {
1317
1386
did,
1318
1387
version: 3,
1319
1388
data: dataRoot ? new CID(cidToBytes(dataRoot)) : null,
1320
1389
rev,
1321
-
prev: prevCommit?.cid ? new CID(cidToBytes(prevCommit.cid)) : null
1322
-
}
1390
+
prev: prevCommit?.cid ? new CID(cidToBytes(prevCommit.cid)) : null,
1391
+
};
1323
1392
1324
1393
// Sign commit
1325
-
const commitBytes = cborEncodeDagCbor(commit)
1326
-
const signingKey = await this.getSigningKey()
1327
-
const sig = await sign(signingKey, commitBytes)
1394
+
const commitBytes = cborEncodeDagCbor(commit);
1395
+
const signingKey = await this.getSigningKey();
1396
+
const sig = await sign(signingKey, commitBytes);
1328
1397
1329
-
const signedCommit = { ...commit, sig }
1330
-
const signedBytes = cborEncodeDagCbor(signedCommit)
1331
-
const commitCid = await createCid(signedBytes)
1332
-
const commitCidStr = cidToString(commitCid)
1398
+
const signedCommit = { ...commit, sig };
1399
+
const signedBytes = cborEncodeDagCbor(signedCommit);
1400
+
const commitCid = await createCid(signedBytes);
1401
+
const commitCidStr = cidToString(commitCid);
1333
1402
1334
1403
// Store commit block
1335
1404
this.sql.exec(
1336
1405
`INSERT OR REPLACE INTO blocks (cid, data) VALUES (?, ?)`,
1337
-
commitCidStr, signedBytes
1338
-
)
1406
+
commitCidStr,
1407
+
signedBytes,
1408
+
);
1339
1409
1340
1410
// Store commit reference
1341
1411
this.sql.exec(
1342
1412
`INSERT INTO commits (cid, rev, prev) VALUES (?, ?, ?)`,
1343
-
commitCidStr, rev, prevCommit?.cid || null
1344
-
)
1413
+
commitCidStr,
1414
+
rev,
1415
+
prevCommit?.cid || null,
1416
+
);
1345
1417
1346
1418
// Update head and rev
1347
-
await this.state.storage.put('head', commitCidStr)
1348
-
await this.state.storage.put('rev', rev)
1419
+
await this.state.storage.put('head', commitCidStr);
1420
+
await this.state.storage.put('rev', rev);
1349
1421
1350
1422
// Collect blocks for the event (commit + MST nodes, no record block)
1351
-
const newBlocks = []
1352
-
newBlocks.push({ cid: commitCidStr, data: signedBytes })
1423
+
const newBlocks = [];
1424
+
newBlocks.push({ cid: commitCidStr, data: signedBytes });
1353
1425
if (dataRoot) {
1354
-
const mstBlocks = this.collectMstBlocks(dataRoot)
1355
-
newBlocks.push(...mstBlocks)
1426
+
const mstBlocks = this.collectMstBlocks(dataRoot);
1427
+
newBlocks.push(...mstBlocks);
1356
1428
}
1357
1429
1358
1430
// Sequence event with delete action
1359
-
const eventTime = new Date().toISOString()
1431
+
const eventTime = new Date().toISOString();
1360
1432
const evt = cborEncode({
1361
1433
ops: [{ action: 'delete', path: `${collection}/${rkey}`, cid: null }],
1362
1434
blocks: buildCarFile(commitCidStr, newBlocks),
1363
1435
rev,
1364
-
time: eventTime
1365
-
})
1436
+
time: eventTime,
1437
+
});
1366
1438
this.sql.exec(
1367
1439
`INSERT INTO seq_events (did, commit_cid, evt) VALUES (?, ?, ?)`,
1368
-
did, commitCidStr, evt
1369
-
)
1440
+
did,
1441
+
commitCidStr,
1442
+
evt,
1443
+
);
1370
1444
1371
1445
// Broadcast to subscribers
1372
-
const evtRows = this.sql.exec(
1373
-
`SELECT * FROM seq_events ORDER BY seq DESC LIMIT 1`
1374
-
).toArray()
1446
+
const evtRows = this.sql
1447
+
.exec(`SELECT * FROM seq_events ORDER BY seq DESC LIMIT 1`)
1448
+
.toArray();
1375
1449
if (evtRows.length > 0) {
1376
-
this.broadcastEvent(evtRows[0])
1450
+
this.broadcastEvent(evtRows[0]);
1377
1451
// Forward to default DO for relay subscribers
1378
1452
if (this.env?.PDS) {
1379
-
const defaultId = this.env.PDS.idFromName('default')
1380
-
const defaultPds = this.env.PDS.get(defaultId)
1381
-
const row = evtRows[0]
1382
-
const evtArray = Array.from(new Uint8Array(row.evt))
1383
-
defaultPds.fetch(new Request('http://internal/forward-event', {
1384
-
method: 'POST',
1385
-
body: JSON.stringify({ ...row, evt: evtArray })
1386
-
})).catch(e => console.log('forward error:', e))
1453
+
const defaultId = this.env.PDS.idFromName('default');
1454
+
const defaultPds = this.env.PDS.get(defaultId);
1455
+
const row = evtRows[0];
1456
+
const evtArray = Array.from(new Uint8Array(row.evt));
1457
+
defaultPds
1458
+
.fetch(
1459
+
new Request('http://internal/forward-event', {
1460
+
method: 'POST',
1461
+
body: JSON.stringify({ ...row, evt: evtArray }),
1462
+
}),
1463
+
)
1464
+
.catch((e) => console.log('forward error:', e));
1387
1465
}
1388
1466
}
1389
1467
1390
-
return { ok: true }
1468
+
return { ok: true };
1391
1469
}
1392
1470
1393
1471
formatEvent(evt) {
1394
1472
// AT Protocol frame format: header + body
1395
1473
// Use DAG-CBOR encoding for body (CIDs need tag 42 + 0x00 prefix)
1396
-
const header = cborEncode({ op: 1, t: '#commit' })
1474
+
const header = cborEncode({ op: 1, t: '#commit' });
1397
1475
1398
1476
// Decode stored event to get ops, blocks, rev, and time
1399
-
const evtData = cborDecode(new Uint8Array(evt.evt))
1400
-
const ops = evtData.ops.map(op => ({
1477
+
const evtData = cborDecode(new Uint8Array(evt.evt));
1478
+
const ops = evtData.ops.map((op) => ({
1401
1479
...op,
1402
-
cid: op.cid ? new CID(cidToBytes(op.cid)) : null // Wrap in CID class for tag 42 encoding
1403
-
}))
1480
+
cid: op.cid ? new CID(cidToBytes(op.cid)) : null, // Wrap in CID class for tag 42 encoding
1481
+
}));
1404
1482
// Get blocks from stored event (already in CAR format)
1405
-
const blocks = evtData.blocks || new Uint8Array(0)
1483
+
const blocks = evtData.blocks || new Uint8Array(0);
1406
1484
1407
1485
const body = cborEncodeDagCbor({
1408
1486
seq: evt.seq,
1409
1487
rebase: false,
1410
1488
tooBig: false,
1411
1489
repo: evt.did,
1412
-
commit: new CID(cidToBytes(evt.commit_cid)), // Wrap in CID class for tag 42 encoding
1413
-
rev: evtData.rev, // Use stored rev from commit creation
1490
+
commit: new CID(cidToBytes(evt.commit_cid)), // Wrap in CID class for tag 42 encoding
1491
+
rev: evtData.rev, // Use stored rev from commit creation
1414
1492
since: null,
1415
1493
blocks: blocks instanceof Uint8Array ? blocks : new Uint8Array(blocks),
1416
1494
ops,
1417
1495
blobs: [],
1418
-
time: evtData.time // Use stored time from event creation
1419
-
})
1496
+
time: evtData.time, // Use stored time from event creation
1497
+
});
1420
1498
1421
1499
// Concatenate header + body
1422
-
const frame = new Uint8Array(header.length + body.length)
1423
-
frame.set(header)
1424
-
frame.set(body, header.length)
1425
-
return frame
1500
+
const frame = new Uint8Array(header.length + body.length);
1501
+
frame.set(header);
1502
+
frame.set(body, header.length);
1503
+
return frame;
1426
1504
}
1427
1505
1428
1506
async webSocketMessage(ws, message) {
1429
1507
// Handle ping
1430
-
if (message === 'ping') ws.send('pong')
1508
+
if (message === 'ping') ws.send('pong');
1431
1509
}
1432
1510
1433
-
async webSocketClose(ws, code, reason) {
1511
+
async webSocketClose(_ws, _code, _reason) {
1434
1512
// Durable Object will hibernate when no connections remain
1435
1513
}
1436
1514
1437
1515
broadcastEvent(evt) {
1438
-
const frame = this.formatEvent(evt)
1516
+
const frame = this.formatEvent(evt);
1439
1517
for (const ws of this.state.getWebSockets()) {
1440
1518
try {
1441
-
ws.send(frame)
1442
-
} catch (e) {
1519
+
ws.send(frame);
1520
+
} catch (_e) {
1443
1521
// Client disconnected
1444
1522
}
1445
1523
}
1446
1524
}
1447
1525
1448
1526
async handleAtprotoDid() {
1449
-
let did = await this.getDid()
1527
+
let did = await this.getDid();
1450
1528
if (!did) {
1451
-
const registeredDids = await this.state.storage.get('registeredDids') || []
1452
-
did = registeredDids[0]
1529
+
const registeredDids =
1530
+
(await this.state.storage.get('registeredDids')) || [];
1531
+
did = registeredDids[0];
1453
1532
}
1454
1533
if (!did) {
1455
-
return new Response('User not found', { status: 404 })
1534
+
return new Response('User not found', { status: 404 });
1456
1535
}
1457
-
return new Response(did, { headers: { 'Content-Type': 'text/plain' } })
1536
+
return new Response(did, { headers: { 'Content-Type': 'text/plain' } });
1458
1537
}
1459
1538
1460
1539
async handleInit(request) {
1461
-
const body = await request.json()
1540
+
const body = await request.json();
1462
1541
if (!body.did || !body.privateKey) {
1463
-
return errorResponse('InvalidRequest', 'missing did or privateKey', 400)
1542
+
return errorResponse('InvalidRequest', 'missing did or privateKey', 400);
1464
1543
}
1465
-
await this.initIdentity(body.did, body.privateKey, body.handle || null)
1466
-
return Response.json({ ok: true, did: body.did, handle: body.handle || null })
1544
+
await this.initIdentity(body.did, body.privateKey, body.handle || null);
1545
+
return Response.json({
1546
+
ok: true,
1547
+
did: body.did,
1548
+
handle: body.handle || null,
1549
+
});
1467
1550
}
1468
1551
1469
1552
async handleStatus() {
1470
-
const did = await this.getDid()
1471
-
return Response.json({ initialized: !!did, did: did || null })
1553
+
const did = await this.getDid();
1554
+
return Response.json({ initialized: !!did, did: did || null });
1472
1555
}
1473
1556
1474
1557
async handleResetRepo() {
1475
-
this.sql.exec(`DELETE FROM blocks`)
1476
-
this.sql.exec(`DELETE FROM records`)
1477
-
this.sql.exec(`DELETE FROM commits`)
1478
-
this.sql.exec(`DELETE FROM seq_events`)
1479
-
await this.state.storage.delete('head')
1480
-
await this.state.storage.delete('rev')
1481
-
return Response.json({ ok: true, message: 'repo data cleared' })
1558
+
this.sql.exec(`DELETE FROM blocks`);
1559
+
this.sql.exec(`DELETE FROM records`);
1560
+
this.sql.exec(`DELETE FROM commits`);
1561
+
this.sql.exec(`DELETE FROM seq_events`);
1562
+
await this.state.storage.delete('head');
1563
+
await this.state.storage.delete('rev');
1564
+
return Response.json({ ok: true, message: 'repo data cleared' });
1482
1565
}
1483
1566
1484
1567
async handleForwardEvent(request) {
1485
-
const evt = await request.json()
1486
-
const numSockets = [...this.state.getWebSockets()].length
1487
-
console.log(`forward-event: received event seq=${evt.seq}, ${numSockets} connected sockets`)
1568
+
const evt = await request.json();
1569
+
const numSockets = [...this.state.getWebSockets()].length;
1570
+
console.log(
1571
+
`forward-event: received event seq=${evt.seq}, ${numSockets} connected sockets`,
1572
+
);
1488
1573
this.broadcastEvent({
1489
1574
seq: evt.seq,
1490
1575
did: evt.did,
1491
1576
commit_cid: evt.commit_cid,
1492
-
evt: new Uint8Array(Object.values(evt.evt))
1493
-
})
1494
-
return Response.json({ ok: true, sockets: numSockets })
1577
+
evt: new Uint8Array(Object.values(evt.evt)),
1578
+
});
1579
+
return Response.json({ ok: true, sockets: numSockets });
1495
1580
}
1496
1581
1497
1582
async handleRegisterDid(request) {
1498
-
const body = await request.json()
1499
-
const registeredDids = await this.state.storage.get('registeredDids') || []
1583
+
const body = await request.json();
1584
+
const registeredDids =
1585
+
(await this.state.storage.get('registeredDids')) || [];
1500
1586
if (!registeredDids.includes(body.did)) {
1501
-
registeredDids.push(body.did)
1502
-
await this.state.storage.put('registeredDids', registeredDids)
1587
+
registeredDids.push(body.did);
1588
+
await this.state.storage.put('registeredDids', registeredDids);
1503
1589
}
1504
-
return Response.json({ ok: true })
1590
+
return Response.json({ ok: true });
1505
1591
}
1506
1592
1507
1593
async handleGetRegisteredDids() {
1508
-
const registeredDids = await this.state.storage.get('registeredDids') || []
1509
-
return Response.json({ dids: registeredDids })
1594
+
const registeredDids =
1595
+
(await this.state.storage.get('registeredDids')) || [];
1596
+
return Response.json({ dids: registeredDids });
1510
1597
}
1511
1598
1512
1599
async handleRegisterHandle(request) {
1513
-
const body = await request.json()
1514
-
const { handle, did } = body
1600
+
const body = await request.json();
1601
+
const { handle, did } = body;
1515
1602
if (!handle || !did) {
1516
-
return errorResponse('InvalidRequest', 'missing handle or did', 400)
1603
+
return errorResponse('InvalidRequest', 'missing handle or did', 400);
1517
1604
}
1518
-
const handleMap = await this.state.storage.get('handleMap') || {}
1519
-
handleMap[handle] = did
1520
-
await this.state.storage.put('handleMap', handleMap)
1521
-
return Response.json({ ok: true })
1605
+
const handleMap = (await this.state.storage.get('handleMap')) || {};
1606
+
handleMap[handle] = did;
1607
+
await this.state.storage.put('handleMap', handleMap);
1608
+
return Response.json({ ok: true });
1522
1609
}
1523
1610
1524
1611
async handleResolveHandle(url) {
1525
-
const handle = url.searchParams.get('handle')
1612
+
const handle = url.searchParams.get('handle');
1526
1613
if (!handle) {
1527
-
return errorResponse('InvalidRequest', 'missing handle', 400)
1614
+
return errorResponse('InvalidRequest', 'missing handle', 400);
1528
1615
}
1529
-
const handleMap = await this.state.storage.get('handleMap') || {}
1530
-
const did = handleMap[handle]
1616
+
const handleMap = (await this.state.storage.get('handleMap')) || {};
1617
+
const did = handleMap[handle];
1531
1618
if (!did) {
1532
-
return errorResponse('NotFound', 'handle not found', 404)
1619
+
return errorResponse('NotFound', 'handle not found', 404);
1533
1620
}
1534
-
return Response.json({ did })
1621
+
return Response.json({ did });
1535
1622
}
1536
1623
1537
1624
async handleRepoInfo() {
1538
-
const head = await this.state.storage.get('head')
1539
-
const rev = await this.state.storage.get('rev')
1540
-
return Response.json({ head: head || null, rev: rev || null })
1625
+
const head = await this.state.storage.get('head');
1626
+
const rev = await this.state.storage.get('rev');
1627
+
return Response.json({ head: head || null, rev: rev || null });
1541
1628
}
1542
1629
1543
1630
handleDescribeServer(request) {
1544
-
const hostname = request.headers.get('x-hostname') || 'localhost'
1631
+
const hostname = request.headers.get('x-hostname') || 'localhost';
1545
1632
return Response.json({
1546
1633
did: `did:web:${hostname}`,
1547
1634
availableUserDomains: [`.${hostname}`],
1548
1635
inviteCodeRequired: false,
1549
1636
phoneVerificationRequired: false,
1550
1637
links: {},
1551
-
contact: {}
1552
-
})
1638
+
contact: {},
1639
+
});
1553
1640
}
1554
1641
1555
1642
async handleCreateSession(request) {
1556
-
const body = await request.json()
1557
-
const { identifier, password } = body
1643
+
const body = await request.json();
1644
+
const { identifier, password } = body;
1558
1645
1559
1646
if (!identifier || !password) {
1560
-
return errorResponse('InvalidRequest', 'Missing identifier or password', 400)
1647
+
return errorResponse(
1648
+
'InvalidRequest',
1649
+
'Missing identifier or password',
1650
+
400,
1651
+
);
1561
1652
}
1562
1653
1563
1654
// Check password against env var
1564
-
const expectedPassword = this.env?.PDS_PASSWORD
1655
+
const expectedPassword = this.env?.PDS_PASSWORD;
1565
1656
if (!expectedPassword || password !== expectedPassword) {
1566
-
return errorResponse('AuthenticationRequired', 'Invalid identifier or password', 401)
1657
+
return errorResponse(
1658
+
'AuthenticationRequired',
1659
+
'Invalid identifier or password',
1660
+
401,
1661
+
);
1567
1662
}
1568
1663
1569
1664
// Resolve identifier to DID
1570
-
let did = identifier
1665
+
let did = identifier;
1571
1666
if (!identifier.startsWith('did:')) {
1572
1667
// Try to resolve handle
1573
-
const handleMap = await this.state.storage.get('handleMap') || {}
1574
-
did = handleMap[identifier]
1668
+
const handleMap = (await this.state.storage.get('handleMap')) || {};
1669
+
did = handleMap[identifier];
1575
1670
if (!did) {
1576
-
return errorResponse('InvalidRequest', 'Unable to resolve handle', 400)
1671
+
return errorResponse('InvalidRequest', 'Unable to resolve handle', 400);
1577
1672
}
1578
1673
}
1579
1674
1580
1675
// Get handle for response
1581
-
const handle = await this.getHandleForDid(did)
1676
+
const handle = await this.getHandleForDid(did);
1582
1677
1583
1678
// Create tokens
1584
-
const jwtSecret = this.env?.JWT_SECRET
1679
+
const jwtSecret = this.env?.JWT_SECRET;
1585
1680
if (!jwtSecret) {
1586
-
return errorResponse('InternalServerError', 'Server not configured for authentication', 500)
1681
+
return errorResponse(
1682
+
'InternalServerError',
1683
+
'Server not configured for authentication',
1684
+
500,
1685
+
);
1587
1686
}
1588
1687
1589
-
const accessJwt = await createAccessJwt(did, jwtSecret)
1590
-
const refreshJwt = await createRefreshJwt(did, jwtSecret)
1688
+
const accessJwt = await createAccessJwt(did, jwtSecret);
1689
+
const refreshJwt = await createRefreshJwt(did, jwtSecret);
1591
1690
1592
1691
return Response.json({
1593
1692
accessJwt,
1594
1693
refreshJwt,
1595
1694
handle: handle || did,
1596
1695
did,
1597
-
active: true
1598
-
})
1696
+
active: true,
1697
+
});
1599
1698
}
1600
1699
1601
1700
async handleGetSession(request) {
1602
-
const authHeader = request.headers.get('Authorization')
1701
+
const authHeader = request.headers.get('Authorization');
1603
1702
if (!authHeader || !authHeader.startsWith('Bearer ')) {
1604
-
return errorResponse('AuthenticationRequired', 'Missing or invalid authorization header', 401)
1703
+
return errorResponse(
1704
+
'AuthenticationRequired',
1705
+
'Missing or invalid authorization header',
1706
+
401,
1707
+
);
1605
1708
}
1606
1709
1607
-
const token = authHeader.slice(7) // Remove 'Bearer '
1608
-
const jwtSecret = this.env?.JWT_SECRET
1710
+
const token = authHeader.slice(7); // Remove 'Bearer '
1711
+
const jwtSecret = this.env?.JWT_SECRET;
1609
1712
if (!jwtSecret) {
1610
-
return errorResponse('InternalServerError', 'Server not configured for authentication', 500)
1713
+
return errorResponse(
1714
+
'InternalServerError',
1715
+
'Server not configured for authentication',
1716
+
500,
1717
+
);
1611
1718
}
1612
1719
1613
1720
try {
1614
-
const payload = await verifyAccessJwt(token, jwtSecret)
1615
-
const did = payload.sub
1616
-
const handle = await this.getHandleForDid(did)
1721
+
const payload = await verifyAccessJwt(token, jwtSecret);
1722
+
const did = payload.sub;
1723
+
const handle = await this.getHandleForDid(did);
1617
1724
1618
1725
return Response.json({
1619
1726
handle: handle || did,
1620
1727
did,
1621
-
active: true
1622
-
})
1728
+
active: true,
1729
+
});
1623
1730
} catch (err) {
1624
-
return errorResponse('InvalidToken', err.message, 401)
1731
+
return errorResponse('InvalidToken', err.message, 401);
1625
1732
}
1626
1733
}
1627
1734
1628
-
async handleGetPreferences(request) {
1735
+
async handleGetPreferences(_request) {
1629
1736
// Preferences are stored per-user in their DO
1630
-
const preferences = await this.state.storage.get('preferences') || []
1631
-
return Response.json({ preferences })
1737
+
const preferences = (await this.state.storage.get('preferences')) || [];
1738
+
return Response.json({ preferences });
1632
1739
}
1633
1740
1634
1741
async handlePutPreferences(request) {
1635
-
const body = await request.json()
1636
-
const { preferences } = body
1742
+
const body = await request.json();
1743
+
const { preferences } = body;
1637
1744
if (!Array.isArray(preferences)) {
1638
-
return errorResponse('InvalidRequest', 'preferences must be an array', 400)
1745
+
return errorResponse(
1746
+
'InvalidRequest',
1747
+
'preferences must be an array',
1748
+
400,
1749
+
);
1639
1750
}
1640
-
await this.state.storage.put('preferences', preferences)
1641
-
return Response.json({})
1751
+
await this.state.storage.put('preferences', preferences);
1752
+
return Response.json({});
1642
1753
}
1643
1754
1644
1755
async getHandleForDid(did) {
1645
1756
// Check if this DID has a handle registered
1646
-
const handleMap = await this.state.storage.get('handleMap') || {}
1757
+
const handleMap = (await this.state.storage.get('handleMap')) || {};
1647
1758
for (const [handle, mappedDid] of Object.entries(handleMap)) {
1648
-
if (mappedDid === did) return handle
1759
+
if (mappedDid === did) return handle;
1649
1760
}
1650
1761
// Check instance's own handle
1651
-
const instanceDid = await this.getDid()
1762
+
const instanceDid = await this.getDid();
1652
1763
if (instanceDid === did) {
1653
-
return await this.state.storage.get('handle')
1764
+
return await this.state.storage.get('handle');
1654
1765
}
1655
-
return null
1766
+
return null;
1656
1767
}
1657
1768
1658
1769
async createServiceAuthForAppView(did, lxm) {
1659
-
const signingKey = await this.getSigningKey()
1770
+
const signingKey = await this.getSigningKey();
1660
1771
return createServiceJwt({
1661
1772
iss: did,
1662
1773
aud: 'did:web:api.bsky.app',
1663
1774
lxm,
1664
-
signingKey
1665
-
})
1775
+
signingKey,
1776
+
});
1666
1777
}
1667
1778
1668
1779
async handleAppViewProxy(request, userDid) {
1669
-
const url = new URL(request.url)
1780
+
const url = new URL(request.url);
1670
1781
// Extract lexicon method from path: /xrpc/app.bsky.actor.getPreferences -> app.bsky.actor.getPreferences
1671
-
const lxm = url.pathname.replace('/xrpc/', '')
1782
+
const lxm = url.pathname.replace('/xrpc/', '');
1672
1783
1673
1784
// Create service auth JWT
1674
-
const serviceJwt = await this.createServiceAuthForAppView(userDid, lxm)
1785
+
const serviceJwt = await this.createServiceAuthForAppView(userDid, lxm);
1675
1786
1676
1787
// Build AppView URL
1677
-
const appViewUrl = new URL(url.pathname + url.search, 'https://api.bsky.app')
1788
+
const appViewUrl = new URL(
1789
+
url.pathname + url.search,
1790
+
'https://api.bsky.app',
1791
+
);
1678
1792
1679
1793
// Forward request with service auth
1680
-
const headers = new Headers()
1681
-
headers.set('Authorization', `Bearer ${serviceJwt}`)
1682
-
headers.set('Content-Type', request.headers.get('Content-Type') || 'application/json')
1794
+
const headers = new Headers();
1795
+
headers.set('Authorization', `Bearer ${serviceJwt}`);
1796
+
headers.set(
1797
+
'Content-Type',
1798
+
request.headers.get('Content-Type') || 'application/json',
1799
+
);
1683
1800
if (request.headers.get('Accept')) {
1684
-
headers.set('Accept', request.headers.get('Accept'))
1801
+
headers.set('Accept', request.headers.get('Accept'));
1685
1802
}
1686
1803
if (request.headers.get('Accept-Language')) {
1687
-
headers.set('Accept-Language', request.headers.get('Accept-Language'))
1804
+
headers.set('Accept-Language', request.headers.get('Accept-Language'));
1688
1805
}
1689
1806
1690
1807
const proxyReq = new Request(appViewUrl.toString(), {
1691
1808
method: request.method,
1692
1809
headers,
1693
-
body: request.method !== 'GET' && request.method !== 'HEAD' ? request.body : undefined,
1694
-
})
1810
+
body:
1811
+
request.method !== 'GET' && request.method !== 'HEAD'
1812
+
? request.body
1813
+
: undefined,
1814
+
});
1695
1815
1696
1816
try {
1697
-
const response = await fetch(proxyReq)
1817
+
const response = await fetch(proxyReq);
1698
1818
// Return the response with CORS headers
1699
-
const responseHeaders = new Headers(response.headers)
1700
-
responseHeaders.set('Access-Control-Allow-Origin', '*')
1819
+
const responseHeaders = new Headers(response.headers);
1820
+
responseHeaders.set('Access-Control-Allow-Origin', '*');
1701
1821
return new Response(response.body, {
1702
1822
status: response.status,
1703
1823
statusText: response.statusText,
1704
-
headers: responseHeaders
1705
-
})
1824
+
headers: responseHeaders,
1825
+
});
1706
1826
} catch (err) {
1707
-
return errorResponse('UpstreamFailure', 'Failed to reach AppView: ' + err.message, 502)
1827
+
return errorResponse(
1828
+
'UpstreamFailure',
1829
+
`Failed to reach AppView: ${err.message}`,
1830
+
502,
1831
+
);
1708
1832
}
1709
1833
}
1710
1834
1711
1835
async handleListRepos() {
1712
-
const registeredDids = await this.state.storage.get('registeredDids') || []
1713
-
const did = await this.getDid()
1714
-
const repos = did ? [{ did, head: null, rev: null }] :
1715
-
registeredDids.map(d => ({ did: d, head: null, rev: null }))
1716
-
return Response.json({ repos })
1836
+
const registeredDids =
1837
+
(await this.state.storage.get('registeredDids')) || [];
1838
+
const did = await this.getDid();
1839
+
const repos = did
1840
+
? [{ did, head: null, rev: null }]
1841
+
: registeredDids.map((d) => ({ did: d, head: null, rev: null }));
1842
+
return Response.json({ repos });
1717
1843
}
1718
1844
1719
1845
async handleCreateRecord(request) {
1720
-
const body = await request.json()
1846
+
const body = await request.json();
1721
1847
if (!body.collection || !body.record) {
1722
-
return errorResponse('InvalidRequest', 'missing collection or record', 400)
1848
+
return errorResponse(
1849
+
'InvalidRequest',
1850
+
'missing collection or record',
1851
+
400,
1852
+
);
1723
1853
}
1724
1854
try {
1725
-
const result = await this.createRecord(body.collection, body.record, body.rkey)
1726
-
const head = await this.state.storage.get('head')
1727
-
const rev = await this.state.storage.get('rev')
1855
+
const result = await this.createRecord(
1856
+
body.collection,
1857
+
body.record,
1858
+
body.rkey,
1859
+
);
1860
+
const head = await this.state.storage.get('head');
1861
+
const rev = await this.state.storage.get('rev');
1728
1862
return Response.json({
1729
1863
uri: result.uri,
1730
1864
cid: result.cid,
1731
1865
commit: { cid: head, rev },
1732
-
validationStatus: 'valid'
1733
-
})
1866
+
validationStatus: 'valid',
1867
+
});
1734
1868
} catch (err) {
1735
-
return errorResponse('InternalError', err.message, 500)
1869
+
return errorResponse('InternalError', err.message, 500);
1736
1870
}
1737
1871
}
1738
1872
1739
1873
async handleDeleteRecord(request) {
1740
-
const body = await request.json()
1874
+
const body = await request.json();
1741
1875
if (!body.collection || !body.rkey) {
1742
-
return errorResponse('InvalidRequest', 'missing collection or rkey', 400)
1876
+
return errorResponse('InvalidRequest', 'missing collection or rkey', 400);
1743
1877
}
1744
1878
try {
1745
-
const result = await this.deleteRecord(body.collection, body.rkey)
1879
+
const result = await this.deleteRecord(body.collection, body.rkey);
1746
1880
if (result.error) {
1747
-
return Response.json(result, { status: 404 })
1881
+
return Response.json(result, { status: 404 });
1748
1882
}
1749
-
return Response.json({})
1883
+
return Response.json({});
1750
1884
} catch (err) {
1751
-
return errorResponse('InternalError', err.message, 500)
1885
+
return errorResponse('InternalError', err.message, 500);
1752
1886
}
1753
1887
}
1754
1888
1755
1889
async handlePutRecord(request) {
1756
-
const body = await request.json()
1890
+
const body = await request.json();
1757
1891
if (!body.collection || !body.rkey || !body.record) {
1758
-
return errorResponse('InvalidRequest', 'missing collection, rkey, or record', 400)
1892
+
return errorResponse(
1893
+
'InvalidRequest',
1894
+
'missing collection, rkey, or record',
1895
+
400,
1896
+
);
1759
1897
}
1760
1898
try {
1761
1899
// putRecord is like createRecord but with a specific rkey (upsert)
1762
-
const result = await this.createRecord(body.collection, body.record, body.rkey)
1763
-
const head = await this.state.storage.get('head')
1764
-
const rev = await this.state.storage.get('rev')
1900
+
const result = await this.createRecord(
1901
+
body.collection,
1902
+
body.record,
1903
+
body.rkey,
1904
+
);
1905
+
const head = await this.state.storage.get('head');
1906
+
const rev = await this.state.storage.get('rev');
1765
1907
return Response.json({
1766
1908
uri: result.uri,
1767
1909
cid: result.cid,
1768
1910
commit: { cid: head, rev },
1769
-
validationStatus: 'valid'
1770
-
})
1911
+
validationStatus: 'valid',
1912
+
});
1771
1913
} catch (err) {
1772
-
return errorResponse('InternalError', err.message, 500)
1914
+
return errorResponse('InternalError', err.message, 500);
1773
1915
}
1774
1916
}
1775
1917
1776
1918
async handleApplyWrites(request) {
1777
-
const body = await request.json()
1919
+
const body = await request.json();
1778
1920
if (!body.writes || !Array.isArray(body.writes)) {
1779
-
return errorResponse('InvalidRequest', 'missing writes array', 400)
1921
+
return errorResponse('InvalidRequest', 'missing writes array', 400);
1780
1922
}
1781
1923
try {
1782
-
const results = []
1924
+
const results = [];
1783
1925
for (const write of body.writes) {
1784
-
const type = write['$type']
1926
+
const type = write.$type;
1785
1927
if (type === 'com.atproto.repo.applyWrites#create') {
1786
-
const result = await this.createRecord(write.collection, write.value, write.rkey)
1928
+
const result = await this.createRecord(
1929
+
write.collection,
1930
+
write.value,
1931
+
write.rkey,
1932
+
);
1787
1933
results.push({
1788
1934
$type: 'com.atproto.repo.applyWrites#createResult',
1789
1935
uri: result.uri,
1790
1936
cid: result.cid,
1791
-
validationStatus: 'valid'
1792
-
})
1937
+
validationStatus: 'valid',
1938
+
});
1793
1939
} else if (type === 'com.atproto.repo.applyWrites#update') {
1794
-
const result = await this.createRecord(write.collection, write.value, write.rkey)
1940
+
const result = await this.createRecord(
1941
+
write.collection,
1942
+
write.value,
1943
+
write.rkey,
1944
+
);
1795
1945
results.push({
1796
1946
$type: 'com.atproto.repo.applyWrites#updateResult',
1797
1947
uri: result.uri,
1798
1948
cid: result.cid,
1799
-
validationStatus: 'valid'
1800
-
})
1949
+
validationStatus: 'valid',
1950
+
});
1801
1951
} else if (type === 'com.atproto.repo.applyWrites#delete') {
1802
-
await this.deleteRecord(write.collection, write.rkey)
1952
+
await this.deleteRecord(write.collection, write.rkey);
1803
1953
results.push({
1804
-
$type: 'com.atproto.repo.applyWrites#deleteResult'
1805
-
})
1954
+
$type: 'com.atproto.repo.applyWrites#deleteResult',
1955
+
});
1806
1956
} else {
1807
-
return errorResponse('InvalidRequest', `Unknown write operation type: ${type}`, 400)
1957
+
return errorResponse(
1958
+
'InvalidRequest',
1959
+
`Unknown write operation type: ${type}`,
1960
+
400,
1961
+
);
1808
1962
}
1809
1963
}
1810
1964
// Return commit info
1811
-
const head = await this.state.storage.get('head')
1812
-
const rev = await this.state.storage.get('rev')
1813
-
return Response.json({ commit: { cid: head, rev }, results })
1965
+
const head = await this.state.storage.get('head');
1966
+
const rev = await this.state.storage.get('rev');
1967
+
return Response.json({ commit: { cid: head, rev }, results });
1814
1968
} catch (err) {
1815
-
return errorResponse('InternalError', err.message, 500)
1969
+
return errorResponse('InternalError', err.message, 500);
1816
1970
}
1817
1971
}
1818
1972
1819
1973
async handleGetRecord(url) {
1820
-
const collection = url.searchParams.get('collection')
1821
-
const rkey = url.searchParams.get('rkey')
1974
+
const collection = url.searchParams.get('collection');
1975
+
const rkey = url.searchParams.get('rkey');
1822
1976
if (!collection || !rkey) {
1823
-
return errorResponse('InvalidRequest', 'missing collection or rkey', 400)
1977
+
return errorResponse('InvalidRequest', 'missing collection or rkey', 400);
1824
1978
}
1825
-
const did = await this.getDid()
1826
-
const uri = `at://${did}/${collection}/${rkey}`
1827
-
const rows = this.sql.exec(
1828
-
`SELECT cid, value FROM records WHERE uri = ?`, uri
1829
-
).toArray()
1979
+
const did = await this.getDid();
1980
+
const uri = `at://${did}/${collection}/${rkey}`;
1981
+
const rows = this.sql
1982
+
.exec(`SELECT cid, value FROM records WHERE uri = ?`, uri)
1983
+
.toArray();
1830
1984
if (rows.length === 0) {
1831
-
return errorResponse('RecordNotFound', 'record not found', 404)
1985
+
return errorResponse('RecordNotFound', 'record not found', 404);
1832
1986
}
1833
-
const row = rows[0]
1834
-
const value = cborDecode(new Uint8Array(row.value))
1835
-
return Response.json({ uri, cid: row.cid, value })
1987
+
const row = rows[0];
1988
+
const value = cborDecode(new Uint8Array(row.value));
1989
+
return Response.json({ uri, cid: row.cid, value });
1836
1990
}
1837
1991
1838
1992
async handleDescribeRepo() {
1839
-
const did = await this.getDid()
1993
+
const did = await this.getDid();
1840
1994
if (!did) {
1841
-
return errorResponse('RepoNotFound', 'repo not found', 404)
1995
+
return errorResponse('RepoNotFound', 'repo not found', 404);
1842
1996
}
1843
-
const handle = await this.state.storage.get('handle')
1997
+
const handle = await this.state.storage.get('handle');
1844
1998
// Get unique collections
1845
-
const collections = this.sql.exec(
1846
-
`SELECT DISTINCT collection FROM records`
1847
-
).toArray().map(r => r.collection)
1999
+
const collections = this.sql
2000
+
.exec(`SELECT DISTINCT collection FROM records`)
2001
+
.toArray()
2002
+
.map((r) => r.collection);
1848
2003
1849
2004
return Response.json({
1850
2005
handle: handle || did,
1851
2006
did,
1852
2007
didDoc: {},
1853
2008
collections,
1854
-
handleIsCorrect: !!handle
1855
-
})
2009
+
handleIsCorrect: !!handle,
2010
+
});
1856
2011
}
1857
2012
1858
2013
async handleListRecords(url) {
1859
-
const collection = url.searchParams.get('collection')
2014
+
const collection = url.searchParams.get('collection');
1860
2015
if (!collection) {
1861
-
return errorResponse('InvalidRequest', 'missing collection', 400)
2016
+
return errorResponse('InvalidRequest', 'missing collection', 400);
1862
2017
}
1863
-
const limit = Math.min(parseInt(url.searchParams.get('limit') || '50'), 100)
1864
-
const reverse = url.searchParams.get('reverse') === 'true'
1865
-
const cursor = url.searchParams.get('cursor')
2018
+
const limit = Math.min(
2019
+
parseInt(url.searchParams.get('limit') || '50', 10),
2020
+
100,
2021
+
);
2022
+
const reverse = url.searchParams.get('reverse') === 'true';
2023
+
const _cursor = url.searchParams.get('cursor');
1866
2024
1867
-
const did = await this.getDid()
1868
-
let query = `SELECT uri, cid, value FROM records WHERE collection = ? ORDER BY rkey ${reverse ? 'DESC' : 'ASC'} LIMIT ?`
1869
-
const params = [collection, limit + 1]
2025
+
const _did = await this.getDid();
2026
+
const query = `SELECT uri, cid, value FROM records WHERE collection = ? ORDER BY rkey ${reverse ? 'DESC' : 'ASC'} LIMIT ?`;
2027
+
const params = [collection, limit + 1];
1870
2028
1871
-
const rows = this.sql.exec(query, ...params).toArray()
1872
-
const hasMore = rows.length > limit
1873
-
const records = rows.slice(0, limit).map(r => ({
2029
+
const rows = this.sql.exec(query, ...params).toArray();
2030
+
const hasMore = rows.length > limit;
2031
+
const records = rows.slice(0, limit).map((r) => ({
1874
2032
uri: r.uri,
1875
2033
cid: r.cid,
1876
-
value: cborDecode(new Uint8Array(r.value))
1877
-
}))
2034
+
value: cborDecode(new Uint8Array(r.value)),
2035
+
}));
1878
2036
1879
2037
return Response.json({
1880
2038
records,
1881
-
cursor: hasMore ? records[records.length - 1]?.uri : undefined
1882
-
})
2039
+
cursor: hasMore ? records[records.length - 1]?.uri : undefined,
2040
+
});
1883
2041
}
1884
2042
1885
2043
handleGetLatestCommit() {
1886
-
const commits = this.sql.exec(
1887
-
`SELECT cid, rev FROM commits ORDER BY seq DESC LIMIT 1`
1888
-
).toArray()
2044
+
const commits = this.sql
2045
+
.exec(`SELECT cid, rev FROM commits ORDER BY seq DESC LIMIT 1`)
2046
+
.toArray();
1889
2047
if (commits.length === 0) {
1890
-
return errorResponse('RepoNotFound', 'repo not found', 404)
2048
+
return errorResponse('RepoNotFound', 'repo not found', 404);
1891
2049
}
1892
-
return Response.json({ cid: commits[0].cid, rev: commits[0].rev })
2050
+
return Response.json({ cid: commits[0].cid, rev: commits[0].rev });
1893
2051
}
1894
2052
1895
2053
async handleGetRepoStatus() {
1896
-
const did = await this.getDid()
1897
-
const commits = this.sql.exec(
1898
-
`SELECT cid, rev FROM commits ORDER BY seq DESC LIMIT 1`
1899
-
).toArray()
2054
+
const did = await this.getDid();
2055
+
const commits = this.sql
2056
+
.exec(`SELECT cid, rev FROM commits ORDER BY seq DESC LIMIT 1`)
2057
+
.toArray();
1900
2058
if (commits.length === 0 || !did) {
1901
-
return errorResponse('RepoNotFound', 'repo not found', 404)
2059
+
return errorResponse('RepoNotFound', 'repo not found', 404);
1902
2060
}
1903
-
return Response.json({ did, active: true, status: 'active', rev: commits[0].rev })
2061
+
return Response.json({
2062
+
did,
2063
+
active: true,
2064
+
status: 'active',
2065
+
rev: commits[0].rev,
2066
+
});
1904
2067
}
1905
2068
1906
2069
handleGetRepo() {
1907
-
const commits = this.sql.exec(
1908
-
`SELECT cid FROM commits ORDER BY seq DESC LIMIT 1`
1909
-
).toArray()
2070
+
const commits = this.sql
2071
+
.exec(`SELECT cid FROM commits ORDER BY seq DESC LIMIT 1`)
2072
+
.toArray();
1910
2073
if (commits.length === 0) {
1911
-
return errorResponse('RepoNotFound', 'repo not found', 404)
2074
+
return errorResponse('RepoNotFound', 'repo not found', 404);
1912
2075
}
1913
2076
1914
2077
// Only include blocks reachable from the current commit
1915
-
const commitCid = commits[0].cid
1916
-
const neededCids = new Set()
2078
+
const commitCid = commits[0].cid;
2079
+
const neededCids = new Set();
1917
2080
1918
2081
// Helper to get block data
1919
2082
const getBlock = (cid) => {
1920
-
const rows = this.sql.exec(`SELECT data FROM blocks WHERE cid = ?`, cid).toArray()
1921
-
return rows.length > 0 ? new Uint8Array(rows[0].data) : null
1922
-
}
2083
+
const rows = this.sql
2084
+
.exec(`SELECT data FROM blocks WHERE cid = ?`, cid)
2085
+
.toArray();
2086
+
return rows.length > 0 ? new Uint8Array(rows[0].data) : null;
2087
+
};
1923
2088
1924
2089
// Collect all reachable blocks starting from commit
1925
2090
const collectBlocks = (cid) => {
1926
-
if (neededCids.has(cid)) return
1927
-
neededCids.add(cid)
2091
+
if (neededCids.has(cid)) return;
2092
+
neededCids.add(cid);
1928
2093
1929
-
const data = getBlock(cid)
1930
-
if (!data) return
2094
+
const data = getBlock(cid);
2095
+
if (!data) return;
1931
2096
1932
2097
// Decode CBOR to find CID references
1933
2098
try {
1934
-
const decoded = cborDecode(data)
2099
+
const decoded = cborDecode(data);
1935
2100
if (decoded && typeof decoded === 'object') {
1936
2101
// Commit object - follow 'data' (MST root)
1937
2102
if (decoded.data instanceof Uint8Array) {
1938
-
collectBlocks(cidToString(decoded.data))
2103
+
collectBlocks(cidToString(decoded.data));
1939
2104
}
1940
2105
// MST node - follow 'l' and entries' 'v' and 't'
1941
2106
if (decoded.l instanceof Uint8Array) {
1942
-
collectBlocks(cidToString(decoded.l))
2107
+
collectBlocks(cidToString(decoded.l));
1943
2108
}
1944
2109
if (Array.isArray(decoded.e)) {
1945
2110
for (const entry of decoded.e) {
1946
2111
if (entry.v instanceof Uint8Array) {
1947
-
collectBlocks(cidToString(entry.v))
2112
+
collectBlocks(cidToString(entry.v));
1948
2113
}
1949
2114
if (entry.t instanceof Uint8Array) {
1950
-
collectBlocks(cidToString(entry.t))
2115
+
collectBlocks(cidToString(entry.t));
1951
2116
}
1952
2117
}
1953
2118
}
1954
2119
}
1955
-
} catch (e) {
2120
+
} catch (_e) {
1956
2121
// Not a structured block, that's fine
1957
2122
}
1958
-
}
2123
+
};
1959
2124
1960
-
collectBlocks(commitCid)
2125
+
collectBlocks(commitCid);
1961
2126
1962
2127
// Build CAR with only needed blocks
1963
-
const blocksForCar = []
2128
+
const blocksForCar = [];
1964
2129
for (const cid of neededCids) {
1965
-
const data = getBlock(cid)
2130
+
const data = getBlock(cid);
1966
2131
if (data) {
1967
-
blocksForCar.push({ cid, data })
2132
+
blocksForCar.push({ cid, data });
1968
2133
}
1969
2134
}
1970
2135
1971
-
const car = buildCarFile(commitCid, blocksForCar)
2136
+
const car = buildCarFile(commitCid, blocksForCar);
1972
2137
return new Response(car, {
1973
-
headers: { 'content-type': 'application/vnd.ipld.car' }
1974
-
})
2138
+
headers: { 'content-type': 'application/vnd.ipld.car' },
2139
+
});
1975
2140
}
1976
2141
1977
2142
async handleSyncGetRecord(url) {
1978
-
const collection = url.searchParams.get('collection')
1979
-
const rkey = url.searchParams.get('rkey')
2143
+
const collection = url.searchParams.get('collection');
2144
+
const rkey = url.searchParams.get('rkey');
1980
2145
if (!collection || !rkey) {
1981
-
return errorResponse('InvalidRequest', 'missing collection or rkey', 400)
2146
+
return errorResponse('InvalidRequest', 'missing collection or rkey', 400);
1982
2147
}
1983
-
const did = await this.getDid()
1984
-
const uri = `at://${did}/${collection}/${rkey}`
1985
-
const rows = this.sql.exec(
1986
-
`SELECT cid FROM records WHERE uri = ?`, uri
1987
-
).toArray()
2148
+
const did = await this.getDid();
2149
+
const uri = `at://${did}/${collection}/${rkey}`;
2150
+
const rows = this.sql
2151
+
.exec(`SELECT cid FROM records WHERE uri = ?`, uri)
2152
+
.toArray();
1988
2153
if (rows.length === 0) {
1989
-
return errorResponse('RecordNotFound', 'record not found', 404)
2154
+
return errorResponse('RecordNotFound', 'record not found', 404);
1990
2155
}
1991
-
const recordCid = rows[0].cid
2156
+
const recordCid = rows[0].cid;
1992
2157
1993
2158
// Get latest commit
1994
-
const commits = this.sql.exec(
1995
-
`SELECT cid FROM commits ORDER BY seq DESC LIMIT 1`
1996
-
).toArray()
2159
+
const commits = this.sql
2160
+
.exec(`SELECT cid FROM commits ORDER BY seq DESC LIMIT 1`)
2161
+
.toArray();
1997
2162
if (commits.length === 0) {
1998
-
return errorResponse('RepoNotFound', 'no commits', 404)
2163
+
return errorResponse('RepoNotFound', 'no commits', 404);
1999
2164
}
2000
-
const commitCid = commits[0].cid
2165
+
const commitCid = commits[0].cid;
2001
2166
2002
2167
// Build proof chain: commit -> MST path -> record
2003
2168
// Include commit block, all MST nodes on path to record, and record block
2004
-
const blocks = []
2005
-
const included = new Set()
2169
+
const blocks = [];
2170
+
const included = new Set();
2006
2171
2007
2172
const addBlock = (cidStr) => {
2008
-
if (included.has(cidStr)) return
2009
-
included.add(cidStr)
2010
-
const blockRows = this.sql.exec(
2011
-
`SELECT data FROM blocks WHERE cid = ?`, cidStr
2012
-
).toArray()
2173
+
if (included.has(cidStr)) return;
2174
+
included.add(cidStr);
2175
+
const blockRows = this.sql
2176
+
.exec(`SELECT data FROM blocks WHERE cid = ?`, cidStr)
2177
+
.toArray();
2013
2178
if (blockRows.length > 0) {
2014
-
blocks.push({ cid: cidStr, data: new Uint8Array(blockRows[0].data) })
2179
+
blocks.push({ cid: cidStr, data: new Uint8Array(blockRows[0].data) });
2015
2180
}
2016
-
}
2181
+
};
2017
2182
2018
2183
// Add commit block
2019
-
addBlock(commitCid)
2184
+
addBlock(commitCid);
2020
2185
2021
2186
// Get commit to find data root
2022
-
const commitRows = this.sql.exec(
2023
-
`SELECT data FROM blocks WHERE cid = ?`, commitCid
2024
-
).toArray()
2187
+
const commitRows = this.sql
2188
+
.exec(`SELECT data FROM blocks WHERE cid = ?`, commitCid)
2189
+
.toArray();
2025
2190
if (commitRows.length > 0) {
2026
-
const commit = cborDecode(new Uint8Array(commitRows[0].data))
2191
+
const commit = cborDecode(new Uint8Array(commitRows[0].data));
2027
2192
if (commit.data) {
2028
-
const dataRootCid = cidToString(commit.data)
2193
+
const dataRootCid = cidToString(commit.data);
2029
2194
// Collect MST path blocks (this includes all MST nodes)
2030
-
const mstBlocks = this.collectMstBlocks(dataRootCid)
2195
+
const mstBlocks = this.collectMstBlocks(dataRootCid);
2031
2196
for (const block of mstBlocks) {
2032
-
addBlock(block.cid)
2197
+
addBlock(block.cid);
2033
2198
}
2034
2199
}
2035
2200
}
2036
2201
2037
2202
// Add record block
2038
-
addBlock(recordCid)
2203
+
addBlock(recordCid);
2039
2204
2040
-
const car = buildCarFile(commitCid, blocks)
2205
+
const car = buildCarFile(commitCid, blocks);
2041
2206
return new Response(car, {
2042
-
headers: { 'content-type': 'application/vnd.ipld.car' }
2043
-
})
2207
+
headers: { 'content-type': 'application/vnd.ipld.car' },
2208
+
});
2044
2209
}
2045
2210
2046
2211
handleSubscribeRepos(request, url) {
2047
-
const upgradeHeader = request.headers.get('Upgrade')
2212
+
const upgradeHeader = request.headers.get('Upgrade');
2048
2213
if (upgradeHeader !== 'websocket') {
2049
-
return new Response('expected websocket', { status: 426 })
2214
+
return new Response('expected websocket', { status: 426 });
2050
2215
}
2051
-
const { 0: client, 1: server } = new WebSocketPair()
2052
-
this.state.acceptWebSocket(server)
2053
-
const cursor = url.searchParams.get('cursor')
2216
+
const { 0: client, 1: server } = new WebSocketPair();
2217
+
this.state.acceptWebSocket(server);
2218
+
const cursor = url.searchParams.get('cursor');
2054
2219
if (cursor) {
2055
-
const events = this.sql.exec(
2056
-
`SELECT * FROM seq_events WHERE seq > ? ORDER BY seq`,
2057
-
parseInt(cursor)
2058
-
).toArray()
2220
+
const events = this.sql
2221
+
.exec(
2222
+
`SELECT * FROM seq_events WHERE seq > ? ORDER BY seq`,
2223
+
parseInt(cursor, 10),
2224
+
)
2225
+
.toArray();
2059
2226
for (const evt of events) {
2060
-
server.send(this.formatEvent(evt))
2227
+
server.send(this.formatEvent(evt));
2061
2228
}
2062
2229
}
2063
-
return new Response(null, { status: 101, webSocket: client })
2230
+
return new Response(null, { status: 101, webSocket: client });
2064
2231
}
2065
2232
2066
2233
async fetch(request) {
2067
-
const url = new URL(request.url)
2068
-
const route = pdsRoutes[url.pathname]
2234
+
const url = new URL(request.url);
2235
+
const route = pdsRoutes[url.pathname];
2069
2236
2070
2237
// Check for local route first
2071
2238
if (route) {
2072
2239
if (route.method && request.method !== route.method) {
2073
-
return errorResponse('MethodNotAllowed', 'method not allowed', 405)
2240
+
return errorResponse('MethodNotAllowed', 'method not allowed', 405);
2074
2241
}
2075
-
return route.handler(this, request, url)
2242
+
return route.handler(this, request, url);
2076
2243
}
2077
2244
2078
2245
// Handle app.bsky.* proxy requests (only if no local route)
2079
2246
if (url.pathname.startsWith('/xrpc/app.bsky.')) {
2080
-
const userDid = request.headers.get('x-authed-did')
2247
+
const userDid = request.headers.get('x-authed-did');
2081
2248
if (!userDid) {
2082
-
return errorResponse('Unauthorized', 'Missing auth context', 401)
2249
+
return errorResponse('Unauthorized', 'Missing auth context', 401);
2083
2250
}
2084
-
return this.handleAppViewProxy(request, userDid)
2251
+
return this.handleAppViewProxy(request, userDid);
2085
2252
}
2086
2253
2087
-
return errorResponse('NotFound', 'not found', 404)
2254
+
return errorResponse('NotFound', 'not found', 404);
2088
2255
}
2089
2256
}
2090
2257
2091
2258
const corsHeaders = {
2092
2259
'Access-Control-Allow-Origin': '*',
2093
2260
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
2094
-
'Access-Control-Allow-Headers': 'Content-Type, Authorization, atproto-accept-labelers, atproto-proxy, x-bsky-topics',
2095
-
}
2261
+
'Access-Control-Allow-Headers':
2262
+
'Content-Type, Authorization, atproto-accept-labelers, atproto-proxy, x-bsky-topics',
2263
+
};
2096
2264
2097
2265
function addCorsHeaders(response) {
2098
-
const newHeaders = new Headers(response.headers)
2266
+
const newHeaders = new Headers(response.headers);
2099
2267
for (const [key, value] of Object.entries(corsHeaders)) {
2100
-
newHeaders.set(key, value)
2268
+
newHeaders.set(key, value);
2101
2269
}
2102
2270
return new Response(response.body, {
2103
2271
status: response.status,
2104
2272
statusText: response.statusText,
2105
-
headers: newHeaders
2106
-
})
2273
+
headers: newHeaders,
2274
+
});
2107
2275
}
2108
2276
2109
2277
export default {
2110
2278
async fetch(request, env) {
2111
2279
// Handle CORS preflight
2112
2280
if (request.method === 'OPTIONS') {
2113
-
return new Response(null, { headers: corsHeaders })
2281
+
return new Response(null, { headers: corsHeaders });
2114
2282
}
2115
2283
2116
-
const response = await handleRequest(request, env)
2284
+
const response = await handleRequest(request, env);
2117
2285
// Don't wrap WebSocket upgrades - they need the webSocket property preserved
2118
2286
if (response.status === 101) {
2119
-
return response
2287
+
return response;
2120
2288
}
2121
-
return addCorsHeaders(response)
2122
-
}
2123
-
}
2289
+
return addCorsHeaders(response);
2290
+
},
2291
+
};
2124
2292
2125
2293
// Extract subdomain from hostname (e.g., "alice" from "alice.foo.workers.dev")
2126
2294
function getSubdomain(hostname) {
2127
-
const parts = hostname.split('.')
2295
+
const parts = hostname.split('.');
2128
2296
// workers.dev domains: [subdomain?].[worker-name].[account].workers.dev
2129
2297
// If more than 4 parts, first part(s) are user subdomain
2130
2298
if (parts.length > 4 && parts.slice(-2).join('.') === 'workers.dev') {
2131
-
return parts.slice(0, -4).join('.')
2299
+
return parts.slice(0, -4).join('.');
2132
2300
}
2133
2301
// Custom domains: check if there's a subdomain before the base
2134
2302
// For now, assume no subdomain on custom domains
2135
-
return null
2303
+
return null;
2136
2304
}
2137
2305
2138
2306
/**
···
2142
2310
* @returns {Promise<{did: string} | {error: Response}>} DID or error response
2143
2311
*/
2144
2312
async function requireAuth(request, env) {
2145
-
const authHeader = request.headers.get('Authorization')
2313
+
const authHeader = request.headers.get('Authorization');
2146
2314
if (!authHeader || !authHeader.startsWith('Bearer ')) {
2147
2315
return {
2148
-
error: Response.json({
2149
-
error: 'AuthenticationRequired',
2150
-
message: 'Authentication required'
2151
-
}, { status: 401 })
2152
-
}
2316
+
error: Response.json(
2317
+
{
2318
+
error: 'AuthenticationRequired',
2319
+
message: 'Authentication required',
2320
+
},
2321
+
{ status: 401 },
2322
+
),
2323
+
};
2153
2324
}
2154
2325
2155
-
const token = authHeader.slice(7)
2156
-
const jwtSecret = env?.JWT_SECRET
2326
+
const token = authHeader.slice(7);
2327
+
const jwtSecret = env?.JWT_SECRET;
2157
2328
if (!jwtSecret) {
2158
2329
return {
2159
-
error: Response.json({
2160
-
error: 'InternalServerError',
2161
-
message: 'Server not configured for authentication'
2162
-
}, { status: 500 })
2163
-
}
2330
+
error: Response.json(
2331
+
{
2332
+
error: 'InternalServerError',
2333
+
message: 'Server not configured for authentication',
2334
+
},
2335
+
{ status: 500 },
2336
+
),
2337
+
};
2164
2338
}
2165
2339
2166
2340
try {
2167
-
const payload = await verifyAccessJwt(token, jwtSecret)
2168
-
return { did: payload.sub }
2341
+
const payload = await verifyAccessJwt(token, jwtSecret);
2342
+
return { did: payload.sub };
2169
2343
} catch (err) {
2170
2344
return {
2171
-
error: Response.json({
2172
-
error: 'InvalidToken',
2173
-
message: err.message
2174
-
}, { status: 401 })
2175
-
}
2345
+
error: Response.json(
2346
+
{
2347
+
error: 'InvalidToken',
2348
+
message: err.message,
2349
+
},
2350
+
{ status: 401 },
2351
+
),
2352
+
};
2176
2353
}
2177
2354
}
2178
2355
2179
2356
async function handleAuthenticatedRepoWrite(request, env) {
2180
-
const auth = await requireAuth(request, env)
2181
-
if (auth.error) return auth.error
2357
+
const auth = await requireAuth(request, env);
2358
+
if (auth.error) return auth.error;
2182
2359
2183
-
const body = await request.json()
2184
-
const repo = body.repo
2360
+
const body = await request.json();
2361
+
const repo = body.repo;
2185
2362
if (!repo) {
2186
-
return errorResponse('InvalidRequest', 'missing repo param', 400)
2363
+
return errorResponse('InvalidRequest', 'missing repo param', 400);
2187
2364
}
2188
2365
2189
2366
if (auth.did !== repo) {
2190
-
return errorResponse('Forbidden', 'Cannot modify another user\'s repo', 403)
2367
+
return errorResponse('Forbidden', "Cannot modify another user's repo", 403);
2191
2368
}
2192
2369
2193
-
const id = env.PDS.idFromName(repo)
2194
-
const pds = env.PDS.get(id)
2195
-
const response = await pds.fetch(new Request(request.url, {
2196
-
method: 'POST',
2197
-
headers: request.headers,
2198
-
body: JSON.stringify(body)
2199
-
}))
2370
+
const id = env.PDS.idFromName(repo);
2371
+
const pds = env.PDS.get(id);
2372
+
const response = await pds.fetch(
2373
+
new Request(request.url, {
2374
+
method: 'POST',
2375
+
headers: request.headers,
2376
+
body: JSON.stringify(body),
2377
+
}),
2378
+
);
2200
2379
2201
2380
// Notify relay of updates on successful writes
2202
2381
if (response.ok) {
2203
-
const url = new URL(request.url)
2204
-
notifyCrawlers(env, url.hostname)
2382
+
const url = new URL(request.url);
2383
+
notifyCrawlers(env, url.hostname);
2205
2384
}
2206
2385
2207
-
return response
2386
+
return response;
2208
2387
}
2209
2388
2210
2389
async function handleRequest(request, env) {
2211
-
const url = new URL(request.url)
2212
-
const subdomain = getSubdomain(url.hostname)
2390
+
const url = new URL(request.url);
2391
+
const subdomain = getSubdomain(url.hostname);
2213
2392
2214
2393
// Handle resolution via subdomain or bare domain
2215
2394
if (url.pathname === '/.well-known/atproto-did') {
2216
2395
// Look up handle -> DID in default DO
2217
2396
// Use subdomain if present, otherwise try bare hostname as handle
2218
-
const handleToResolve = subdomain || url.hostname
2219
-
const defaultId = env.PDS.idFromName('default')
2220
-
const defaultPds = env.PDS.get(defaultId)
2397
+
const handleToResolve = subdomain || url.hostname;
2398
+
const defaultId = env.PDS.idFromName('default');
2399
+
const defaultPds = env.PDS.get(defaultId);
2221
2400
const resolveRes = await defaultPds.fetch(
2222
-
new Request(`http://internal/resolve-handle?handle=${encodeURIComponent(handleToResolve)}`)
2223
-
)
2401
+
new Request(
2402
+
`http://internal/resolve-handle?handle=${encodeURIComponent(handleToResolve)}`,
2403
+
),
2404
+
);
2224
2405
if (!resolveRes.ok) {
2225
-
return new Response('Handle not found', { status: 404 })
2406
+
return new Response('Handle not found', { status: 404 });
2226
2407
}
2227
-
const { did } = await resolveRes.json()
2228
-
return new Response(did, { headers: { 'Content-Type': 'text/plain' } })
2408
+
const { did } = await resolveRes.json();
2409
+
return new Response(did, { headers: { 'Content-Type': 'text/plain' } });
2229
2410
}
2230
2411
2231
2412
// describeServer - works on bare domain
2232
2413
if (url.pathname === '/xrpc/com.atproto.server.describeServer') {
2233
-
const defaultId = env.PDS.idFromName('default')
2234
-
const defaultPds = env.PDS.get(defaultId)
2414
+
const defaultId = env.PDS.idFromName('default');
2415
+
const defaultPds = env.PDS.get(defaultId);
2235
2416
const newReq = new Request(request.url, {
2236
2417
method: request.method,
2237
-
headers: { ...Object.fromEntries(request.headers), 'x-hostname': url.hostname }
2238
-
})
2239
-
return defaultPds.fetch(newReq)
2418
+
headers: {
2419
+
...Object.fromEntries(request.headers),
2420
+
'x-hostname': url.hostname,
2421
+
},
2422
+
});
2423
+
return defaultPds.fetch(newReq);
2240
2424
}
2241
2425
2242
2426
// createSession - handle on default DO (has handleMap for identifier resolution)
2243
2427
if (url.pathname === '/xrpc/com.atproto.server.createSession') {
2244
-
const defaultId = env.PDS.idFromName('default')
2245
-
const defaultPds = env.PDS.get(defaultId)
2246
-
return defaultPds.fetch(request)
2428
+
const defaultId = env.PDS.idFromName('default');
2429
+
const defaultPds = env.PDS.get(defaultId);
2430
+
return defaultPds.fetch(request);
2247
2431
}
2248
2432
2249
2433
// getSession - route to default DO
2250
2434
if (url.pathname === '/xrpc/com.atproto.server.getSession') {
2251
-
const defaultId = env.PDS.idFromName('default')
2252
-
const defaultPds = env.PDS.get(defaultId)
2253
-
return defaultPds.fetch(request)
2435
+
const defaultId = env.PDS.idFromName('default');
2436
+
const defaultPds = env.PDS.get(defaultId);
2437
+
return defaultPds.fetch(request);
2254
2438
}
2255
2439
2256
2440
// Proxy app.bsky.* endpoints to Bluesky AppView
2257
2441
if (url.pathname.startsWith('/xrpc/app.bsky.')) {
2258
2442
// Authenticate the user first
2259
-
const auth = await requireAuth(request, env)
2260
-
if (auth.error) return auth.error
2443
+
const auth = await requireAuth(request, env);
2444
+
if (auth.error) return auth.error;
2261
2445
2262
2446
// Route to the user's DO instance to create service auth and proxy
2263
-
const id = env.PDS.idFromName(auth.did)
2264
-
const pds = env.PDS.get(id)
2265
-
return pds.fetch(new Request(request.url, {
2266
-
method: request.method,
2267
-
headers: {
2268
-
...Object.fromEntries(request.headers),
2269
-
'x-authed-did': auth.did // Pass the authenticated DID
2270
-
},
2271
-
body: request.method !== 'GET' && request.method !== 'HEAD' ? request.body : undefined,
2272
-
}))
2447
+
const id = env.PDS.idFromName(auth.did);
2448
+
const pds = env.PDS.get(id);
2449
+
return pds.fetch(
2450
+
new Request(request.url, {
2451
+
method: request.method,
2452
+
headers: {
2453
+
...Object.fromEntries(request.headers),
2454
+
'x-authed-did': auth.did, // Pass the authenticated DID
2455
+
},
2456
+
body:
2457
+
request.method !== 'GET' && request.method !== 'HEAD'
2458
+
? request.body
2459
+
: undefined,
2460
+
}),
2461
+
);
2273
2462
}
2274
2463
2275
2464
// Handle registration routes - go to default DO
2276
-
if (url.pathname === '/register-handle' || url.pathname === '/resolve-handle') {
2277
-
const defaultId = env.PDS.idFromName('default')
2278
-
const defaultPds = env.PDS.get(defaultId)
2279
-
return defaultPds.fetch(request)
2465
+
if (
2466
+
url.pathname === '/register-handle' ||
2467
+
url.pathname === '/resolve-handle'
2468
+
) {
2469
+
const defaultId = env.PDS.idFromName('default');
2470
+
const defaultPds = env.PDS.get(defaultId);
2471
+
return defaultPds.fetch(request);
2280
2472
}
2281
2473
2282
2474
// resolveHandle XRPC endpoint
2283
2475
if (url.pathname === '/xrpc/com.atproto.identity.resolveHandle') {
2284
-
const handle = url.searchParams.get('handle')
2476
+
const handle = url.searchParams.get('handle');
2285
2477
if (!handle) {
2286
-
return errorResponse('InvalidRequest', 'missing handle param', 400)
2478
+
return errorResponse('InvalidRequest', 'missing handle param', 400);
2287
2479
}
2288
-
const defaultId = env.PDS.idFromName('default')
2289
-
const defaultPds = env.PDS.get(defaultId)
2480
+
const defaultId = env.PDS.idFromName('default');
2481
+
const defaultPds = env.PDS.get(defaultId);
2290
2482
const resolveRes = await defaultPds.fetch(
2291
-
new Request(`http://internal/resolve-handle?handle=${encodeURIComponent(handle)}`)
2292
-
)
2483
+
new Request(
2484
+
`http://internal/resolve-handle?handle=${encodeURIComponent(handle)}`,
2485
+
),
2486
+
);
2293
2487
if (!resolveRes.ok) {
2294
-
return errorResponse('InvalidRequest', 'Unable to resolve handle', 400)
2488
+
return errorResponse('InvalidRequest', 'Unable to resolve handle', 400);
2295
2489
}
2296
-
const { did } = await resolveRes.json()
2297
-
return Response.json({ did })
2490
+
const { did } = await resolveRes.json();
2491
+
return Response.json({ did });
2298
2492
}
2299
2493
2300
2494
// subscribeRepos WebSocket - route to default instance for firehose
2301
2495
if (url.pathname === '/xrpc/com.atproto.sync.subscribeRepos') {
2302
-
const defaultId = env.PDS.idFromName('default')
2303
-
const defaultPds = env.PDS.get(defaultId)
2304
-
return defaultPds.fetch(request)
2496
+
const defaultId = env.PDS.idFromName('default');
2497
+
const defaultPds = env.PDS.get(defaultId);
2498
+
return defaultPds.fetch(request);
2305
2499
}
2306
2500
2307
2501
// listRepos needs to aggregate from all registered DIDs
2308
2502
if (url.pathname === '/xrpc/com.atproto.sync.listRepos') {
2309
-
const defaultId = env.PDS.idFromName('default')
2310
-
const defaultPds = env.PDS.get(defaultId)
2311
-
const regRes = await defaultPds.fetch(new Request('http://internal/get-registered-dids'))
2312
-
const { dids } = await regRes.json()
2503
+
const defaultId = env.PDS.idFromName('default');
2504
+
const defaultPds = env.PDS.get(defaultId);
2505
+
const regRes = await defaultPds.fetch(
2506
+
new Request('http://internal/get-registered-dids'),
2507
+
);
2508
+
const { dids } = await regRes.json();
2313
2509
2314
-
const repos = []
2510
+
const repos = [];
2315
2511
for (const did of dids) {
2316
-
const id = env.PDS.idFromName(did)
2317
-
const pds = env.PDS.get(id)
2318
-
const infoRes = await pds.fetch(new Request('http://internal/repo-info'))
2319
-
const info = await infoRes.json()
2512
+
const id = env.PDS.idFromName(did);
2513
+
const pds = env.PDS.get(id);
2514
+
const infoRes = await pds.fetch(new Request('http://internal/repo-info'));
2515
+
const info = await infoRes.json();
2320
2516
if (info.head) {
2321
-
repos.push({ did, head: info.head, rev: info.rev, active: true })
2517
+
repos.push({ did, head: info.head, rev: info.rev, active: true });
2322
2518
}
2323
2519
}
2324
-
return Response.json({ repos, cursor: undefined })
2520
+
return Response.json({ repos, cursor: undefined });
2325
2521
}
2326
2522
2327
2523
// Repo endpoints use ?repo= param instead of ?did=
2328
-
if (url.pathname === '/xrpc/com.atproto.repo.describeRepo' ||
2329
-
url.pathname === '/xrpc/com.atproto.repo.listRecords' ||
2330
-
url.pathname === '/xrpc/com.atproto.repo.getRecord') {
2331
-
const repo = url.searchParams.get('repo')
2524
+
if (
2525
+
url.pathname === '/xrpc/com.atproto.repo.describeRepo' ||
2526
+
url.pathname === '/xrpc/com.atproto.repo.listRecords' ||
2527
+
url.pathname === '/xrpc/com.atproto.repo.getRecord'
2528
+
) {
2529
+
const repo = url.searchParams.get('repo');
2332
2530
if (!repo) {
2333
-
return errorResponse('InvalidRequest', 'missing repo param', 400)
2531
+
return errorResponse('InvalidRequest', 'missing repo param', 400);
2334
2532
}
2335
-
const id = env.PDS.idFromName(repo)
2336
-
const pds = env.PDS.get(id)
2337
-
return pds.fetch(request)
2533
+
const id = env.PDS.idFromName(repo);
2534
+
const pds = env.PDS.get(id);
2535
+
return pds.fetch(request);
2338
2536
}
2339
2537
2340
2538
// Sync endpoints use ?did= param
2341
-
if (url.pathname === '/xrpc/com.atproto.sync.getLatestCommit' ||
2342
-
url.pathname === '/xrpc/com.atproto.sync.getRepoStatus' ||
2343
-
url.pathname === '/xrpc/com.atproto.sync.getRepo' ||
2344
-
url.pathname === '/xrpc/com.atproto.sync.getRecord') {
2345
-
const did = url.searchParams.get('did')
2539
+
if (
2540
+
url.pathname === '/xrpc/com.atproto.sync.getLatestCommit' ||
2541
+
url.pathname === '/xrpc/com.atproto.sync.getRepoStatus' ||
2542
+
url.pathname === '/xrpc/com.atproto.sync.getRepo' ||
2543
+
url.pathname === '/xrpc/com.atproto.sync.getRecord'
2544
+
) {
2545
+
const did = url.searchParams.get('did');
2346
2546
if (!did) {
2347
-
return errorResponse('InvalidRequest', 'missing did param', 400)
2547
+
return errorResponse('InvalidRequest', 'missing did param', 400);
2348
2548
}
2349
-
const id = env.PDS.idFromName(did)
2350
-
const pds = env.PDS.get(id)
2351
-
return pds.fetch(request)
2549
+
const id = env.PDS.idFromName(did);
2550
+
const pds = env.PDS.get(id);
2551
+
return pds.fetch(request);
2352
2552
}
2353
2553
2354
2554
// Authenticated repo write endpoints
···
2356
2556
'/xrpc/com.atproto.repo.createRecord',
2357
2557
'/xrpc/com.atproto.repo.deleteRecord',
2358
2558
'/xrpc/com.atproto.repo.putRecord',
2359
-
'/xrpc/com.atproto.repo.applyWrites'
2360
-
]
2559
+
'/xrpc/com.atproto.repo.applyWrites',
2560
+
];
2361
2561
if (repoWriteEndpoints.includes(url.pathname)) {
2362
-
return handleAuthenticatedRepoWrite(request, env)
2562
+
return handleAuthenticatedRepoWrite(request, env);
2363
2563
}
2364
2564
2365
2565
// Health check endpoint
2366
2566
if (url.pathname === '/xrpc/_health') {
2367
-
return Response.json({ version: '0.1.0' })
2567
+
return Response.json({ version: '0.1.0' });
2368
2568
}
2369
2569
2370
2570
// Root path - ASCII art
···
2378
2578
╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚════╝ ╚══════╝
2379
2579
2380
2580
ATProto PDS on Cloudflare Workers
2381
-
`
2382
-
return new Response(ascii, { headers: { 'Content-Type': 'text/plain; charset=utf-8' } })
2581
+
`;
2582
+
return new Response(ascii, {
2583
+
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
2584
+
});
2383
2585
}
2384
2586
2385
2587
// On init, register this DID with the default instance (requires ?did= param, no auth yet)
2386
2588
if (url.pathname === '/init' && request.method === 'POST') {
2387
-
const did = url.searchParams.get('did')
2589
+
const did = url.searchParams.get('did');
2388
2590
if (!did) {
2389
-
return errorResponse('InvalidRequest', 'missing did param', 400)
2591
+
return errorResponse('InvalidRequest', 'missing did param', 400);
2390
2592
}
2391
-
const body = await request.json()
2593
+
const body = await request.json();
2392
2594
2393
2595
// Register with default instance for discovery
2394
-
const defaultId = env.PDS.idFromName('default')
2395
-
const defaultPds = env.PDS.get(defaultId)
2396
-
await defaultPds.fetch(new Request('http://internal/register-did', {
2397
-
method: 'POST',
2398
-
body: JSON.stringify({ did })
2399
-
}))
2596
+
const defaultId = env.PDS.idFromName('default');
2597
+
const defaultPds = env.PDS.get(defaultId);
2598
+
await defaultPds.fetch(
2599
+
new Request('http://internal/register-did', {
2600
+
method: 'POST',
2601
+
body: JSON.stringify({ did }),
2602
+
}),
2603
+
);
2400
2604
2401
2605
// Register handle if provided
2402
2606
if (body.handle) {
2403
-
await defaultPds.fetch(new Request('http://internal/register-handle', {
2404
-
method: 'POST',
2405
-
body: JSON.stringify({ did, handle: body.handle })
2406
-
}))
2607
+
await defaultPds.fetch(
2608
+
new Request('http://internal/register-handle', {
2609
+
method: 'POST',
2610
+
body: JSON.stringify({ did, handle: body.handle }),
2611
+
}),
2612
+
);
2407
2613
}
2408
2614
2409
2615
// Forward to the actual PDS instance
2410
-
const id = env.PDS.idFromName(did)
2411
-
const pds = env.PDS.get(id)
2412
-
return pds.fetch(new Request(request.url, {
2413
-
method: 'POST',
2414
-
headers: request.headers,
2415
-
body: JSON.stringify(body)
2416
-
}))
2616
+
const id = env.PDS.idFromName(did);
2617
+
const pds = env.PDS.get(id);
2618
+
return pds.fetch(
2619
+
new Request(request.url, {
2620
+
method: 'POST',
2621
+
headers: request.headers,
2622
+
body: JSON.stringify(body),
2623
+
}),
2624
+
);
2417
2625
}
2418
2626
2419
2627
// Unknown endpoint
2420
-
return errorResponse('NotFound', 'Endpoint not found', 404)
2628
+
return errorResponse('NotFound', 'Endpoint not found', 404);
2421
2629
}
+312
-292
test/pds.test.js
+312
-292
test/pds.test.js
···
1
-
import { test, describe } from 'node:test'
2
-
import assert from 'node:assert'
1
+
import assert from 'node:assert';
2
+
import { describe, test } from 'node:test';
3
3
import {
4
-
cborEncode, cborDecode, createCid, cidToString, cidToBytes, base32Encode, createTid,
5
-
generateKeyPair, importPrivateKey, sign, bytesToHex, hexToBytes,
6
-
getKeyDepth, varint, base32Decode, buildCarFile,
7
-
base64UrlEncode, base64UrlDecode,
8
-
createAccessJwt, createRefreshJwt, verifyAccessJwt
9
-
} from '../src/pds.js'
4
+
base32Decode,
5
+
base32Encode,
6
+
base64UrlDecode,
7
+
base64UrlEncode,
8
+
buildCarFile,
9
+
bytesToHex,
10
+
cborDecode,
11
+
cborEncode,
12
+
cidToString,
13
+
createAccessJwt,
14
+
createCid,
15
+
createRefreshJwt,
16
+
createTid,
17
+
generateKeyPair,
18
+
getKeyDepth,
19
+
hexToBytes,
20
+
importPrivateKey,
21
+
sign,
22
+
varint,
23
+
verifyAccessJwt,
24
+
} from '../src/pds.js';
10
25
11
26
describe('CBOR Encoding', () => {
12
27
test('encodes simple map', () => {
13
-
const encoded = cborEncode({ hello: 'world', num: 42 })
28
+
const encoded = cborEncode({ hello: 'world', num: 42 });
14
29
// Expected: a2 65 68 65 6c 6c 6f 65 77 6f 72 6c 64 63 6e 75 6d 18 2a
15
30
const expected = new Uint8Array([
16
-
0xa2, 0x65, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x65, 0x77, 0x6f, 0x72, 0x6c, 0x64,
17
-
0x63, 0x6e, 0x75, 0x6d, 0x18, 0x2a
18
-
])
19
-
assert.deepStrictEqual(encoded, expected)
20
-
})
31
+
0xa2, 0x65, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x65, 0x77, 0x6f, 0x72, 0x6c,
32
+
0x64, 0x63, 0x6e, 0x75, 0x6d, 0x18, 0x2a,
33
+
]);
34
+
assert.deepStrictEqual(encoded, expected);
35
+
});
21
36
22
37
test('encodes null', () => {
23
-
const encoded = cborEncode(null)
24
-
assert.deepStrictEqual(encoded, new Uint8Array([0xf6]))
25
-
})
38
+
const encoded = cborEncode(null);
39
+
assert.deepStrictEqual(encoded, new Uint8Array([0xf6]));
40
+
});
26
41
27
42
test('encodes booleans', () => {
28
-
assert.deepStrictEqual(cborEncode(true), new Uint8Array([0xf5]))
29
-
assert.deepStrictEqual(cborEncode(false), new Uint8Array([0xf4]))
30
-
})
43
+
assert.deepStrictEqual(cborEncode(true), new Uint8Array([0xf5]));
44
+
assert.deepStrictEqual(cborEncode(false), new Uint8Array([0xf4]));
45
+
});
31
46
32
47
test('encodes small integers', () => {
33
-
assert.deepStrictEqual(cborEncode(0), new Uint8Array([0x00]))
34
-
assert.deepStrictEqual(cborEncode(1), new Uint8Array([0x01]))
35
-
assert.deepStrictEqual(cborEncode(23), new Uint8Array([0x17]))
36
-
})
48
+
assert.deepStrictEqual(cborEncode(0), new Uint8Array([0x00]));
49
+
assert.deepStrictEqual(cborEncode(1), new Uint8Array([0x01]));
50
+
assert.deepStrictEqual(cborEncode(23), new Uint8Array([0x17]));
51
+
});
37
52
38
53
test('encodes integers >= 24', () => {
39
-
assert.deepStrictEqual(cborEncode(24), new Uint8Array([0x18, 0x18]))
40
-
assert.deepStrictEqual(cborEncode(255), new Uint8Array([0x18, 0xff]))
41
-
})
54
+
assert.deepStrictEqual(cborEncode(24), new Uint8Array([0x18, 0x18]));
55
+
assert.deepStrictEqual(cborEncode(255), new Uint8Array([0x18, 0xff]));
56
+
});
42
57
43
58
test('encodes negative integers', () => {
44
-
assert.deepStrictEqual(cborEncode(-1), new Uint8Array([0x20]))
45
-
assert.deepStrictEqual(cborEncode(-10), new Uint8Array([0x29]))
46
-
})
59
+
assert.deepStrictEqual(cborEncode(-1), new Uint8Array([0x20]));
60
+
assert.deepStrictEqual(cborEncode(-10), new Uint8Array([0x29]));
61
+
});
47
62
48
63
test('encodes strings', () => {
49
-
const encoded = cborEncode('hello')
64
+
const encoded = cborEncode('hello');
50
65
// 0x65 = text string of length 5
51
-
assert.deepStrictEqual(encoded, new Uint8Array([0x65, 0x68, 0x65, 0x6c, 0x6c, 0x6f]))
52
-
})
66
+
assert.deepStrictEqual(
67
+
encoded,
68
+
new Uint8Array([0x65, 0x68, 0x65, 0x6c, 0x6c, 0x6f]),
69
+
);
70
+
});
53
71
54
72
test('encodes byte strings', () => {
55
-
const bytes = new Uint8Array([1, 2, 3])
56
-
const encoded = cborEncode(bytes)
73
+
const bytes = new Uint8Array([1, 2, 3]);
74
+
const encoded = cborEncode(bytes);
57
75
// 0x43 = byte string of length 3
58
-
assert.deepStrictEqual(encoded, new Uint8Array([0x43, 1, 2, 3]))
59
-
})
76
+
assert.deepStrictEqual(encoded, new Uint8Array([0x43, 1, 2, 3]));
77
+
});
60
78
61
79
test('encodes arrays', () => {
62
-
const encoded = cborEncode([1, 2, 3])
80
+
const encoded = cborEncode([1, 2, 3]);
63
81
// 0x83 = array of length 3
64
-
assert.deepStrictEqual(encoded, new Uint8Array([0x83, 0x01, 0x02, 0x03]))
65
-
})
82
+
assert.deepStrictEqual(encoded, new Uint8Array([0x83, 0x01, 0x02, 0x03]));
83
+
});
66
84
67
85
test('sorts map keys deterministically', () => {
68
-
const encoded1 = cborEncode({ z: 1, a: 2 })
69
-
const encoded2 = cborEncode({ a: 2, z: 1 })
70
-
assert.deepStrictEqual(encoded1, encoded2)
86
+
const encoded1 = cborEncode({ z: 1, a: 2 });
87
+
const encoded2 = cborEncode({ a: 2, z: 1 });
88
+
assert.deepStrictEqual(encoded1, encoded2);
71
89
// First key should be 'a' (0x61)
72
-
assert.strictEqual(encoded1[1], 0x61)
73
-
})
90
+
assert.strictEqual(encoded1[1], 0x61);
91
+
});
74
92
75
93
test('encodes large integers >= 2^31 without overflow', () => {
76
94
// 2^31 would overflow with bitshift operators (treated as signed 32-bit)
77
-
const twoTo31 = 2147483648
78
-
const encoded = cborEncode(twoTo31)
79
-
const decoded = cborDecode(encoded)
80
-
assert.strictEqual(decoded, twoTo31)
95
+
const twoTo31 = 2147483648;
96
+
const encoded = cborEncode(twoTo31);
97
+
const decoded = cborDecode(encoded);
98
+
assert.strictEqual(decoded, twoTo31);
81
99
82
100
// 2^32 - 1 (max unsigned 32-bit)
83
-
const maxU32 = 4294967295
84
-
const encoded2 = cborEncode(maxU32)
85
-
const decoded2 = cborDecode(encoded2)
86
-
assert.strictEqual(decoded2, maxU32)
87
-
})
101
+
const maxU32 = 4294967295;
102
+
const encoded2 = cborEncode(maxU32);
103
+
const decoded2 = cborDecode(encoded2);
104
+
assert.strictEqual(decoded2, maxU32);
105
+
});
88
106
89
107
test('encodes 2^31 with correct byte format', () => {
90
108
// 2147483648 = 0x80000000
91
109
// CBOR: major type 0 (unsigned int), additional info 26 (4-byte follows)
92
-
const encoded = cborEncode(2147483648)
93
-
assert.strictEqual(encoded[0], 0x1a) // type 0 | info 26
94
-
assert.strictEqual(encoded[1], 0x80)
95
-
assert.strictEqual(encoded[2], 0x00)
96
-
assert.strictEqual(encoded[3], 0x00)
97
-
assert.strictEqual(encoded[4], 0x00)
98
-
})
99
-
})
110
+
const encoded = cborEncode(2147483648);
111
+
assert.strictEqual(encoded[0], 0x1a); // type 0 | info 26
112
+
assert.strictEqual(encoded[1], 0x80);
113
+
assert.strictEqual(encoded[2], 0x00);
114
+
assert.strictEqual(encoded[3], 0x00);
115
+
assert.strictEqual(encoded[4], 0x00);
116
+
});
117
+
});
100
118
101
119
describe('Base32 Encoding', () => {
102
120
test('encodes bytes to base32lower', () => {
103
-
const bytes = new Uint8Array([0x01, 0x71, 0x12, 0x20])
104
-
const encoded = base32Encode(bytes)
105
-
assert.strictEqual(typeof encoded, 'string')
106
-
assert.match(encoded, /^[a-z2-7]+$/)
107
-
})
108
-
})
121
+
const bytes = new Uint8Array([0x01, 0x71, 0x12, 0x20]);
122
+
const encoded = base32Encode(bytes);
123
+
assert.strictEqual(typeof encoded, 'string');
124
+
assert.match(encoded, /^[a-z2-7]+$/);
125
+
});
126
+
});
109
127
110
128
describe('CID Generation', () => {
111
129
test('creates CIDv1 with dag-cbor codec', async () => {
112
-
const data = cborEncode({ test: 'data' })
113
-
const cid = await createCid(data)
130
+
const data = cborEncode({ test: 'data' });
131
+
const cid = await createCid(data);
114
132
115
-
assert.strictEqual(cid.length, 36) // 2 prefix + 2 multihash header + 32 hash
116
-
assert.strictEqual(cid[0], 0x01) // CIDv1
117
-
assert.strictEqual(cid[1], 0x71) // dag-cbor
118
-
assert.strictEqual(cid[2], 0x12) // sha-256
119
-
assert.strictEqual(cid[3], 0x20) // 32 bytes
120
-
})
133
+
assert.strictEqual(cid.length, 36); // 2 prefix + 2 multihash header + 32 hash
134
+
assert.strictEqual(cid[0], 0x01); // CIDv1
135
+
assert.strictEqual(cid[1], 0x71); // dag-cbor
136
+
assert.strictEqual(cid[2], 0x12); // sha-256
137
+
assert.strictEqual(cid[3], 0x20); // 32 bytes
138
+
});
121
139
122
140
test('cidToString returns base32lower with b prefix', async () => {
123
-
const data = cborEncode({ test: 'data' })
124
-
const cid = await createCid(data)
125
-
const cidStr = cidToString(cid)
141
+
const data = cborEncode({ test: 'data' });
142
+
const cid = await createCid(data);
143
+
const cidStr = cidToString(cid);
126
144
127
-
assert.strictEqual(cidStr[0], 'b')
128
-
assert.match(cidStr, /^b[a-z2-7]+$/)
129
-
})
145
+
assert.strictEqual(cidStr[0], 'b');
146
+
assert.match(cidStr, /^b[a-z2-7]+$/);
147
+
});
130
148
131
149
test('same input produces same CID', async () => {
132
-
const data1 = cborEncode({ test: 'data' })
133
-
const data2 = cborEncode({ test: 'data' })
134
-
const cid1 = cidToString(await createCid(data1))
135
-
const cid2 = cidToString(await createCid(data2))
150
+
const data1 = cborEncode({ test: 'data' });
151
+
const data2 = cborEncode({ test: 'data' });
152
+
const cid1 = cidToString(await createCid(data1));
153
+
const cid2 = cidToString(await createCid(data2));
136
154
137
-
assert.strictEqual(cid1, cid2)
138
-
})
155
+
assert.strictEqual(cid1, cid2);
156
+
});
139
157
140
158
test('different input produces different CID', async () => {
141
-
const cid1 = cidToString(await createCid(cborEncode({ a: 1 })))
142
-
const cid2 = cidToString(await createCid(cborEncode({ a: 2 })))
159
+
const cid1 = cidToString(await createCid(cborEncode({ a: 1 })));
160
+
const cid2 = cidToString(await createCid(cborEncode({ a: 2 })));
143
161
144
-
assert.notStrictEqual(cid1, cid2)
145
-
})
146
-
})
162
+
assert.notStrictEqual(cid1, cid2);
163
+
});
164
+
});
147
165
148
166
describe('TID Generation', () => {
149
167
test('creates 13-character TIDs', () => {
150
-
const tid = createTid()
151
-
assert.strictEqual(tid.length, 13)
152
-
})
168
+
const tid = createTid();
169
+
assert.strictEqual(tid.length, 13);
170
+
});
153
171
154
172
test('uses valid base32-sort characters', () => {
155
-
const tid = createTid()
156
-
assert.match(tid, /^[234567abcdefghijklmnopqrstuvwxyz]+$/)
157
-
})
173
+
const tid = createTid();
174
+
assert.match(tid, /^[234567abcdefghijklmnopqrstuvwxyz]+$/);
175
+
});
158
176
159
177
test('generates monotonically increasing TIDs', () => {
160
-
const tid1 = createTid()
161
-
const tid2 = createTid()
162
-
const tid3 = createTid()
178
+
const tid1 = createTid();
179
+
const tid2 = createTid();
180
+
const tid3 = createTid();
163
181
164
-
assert.ok(tid1 < tid2, `${tid1} should be less than ${tid2}`)
165
-
assert.ok(tid2 < tid3, `${tid2} should be less than ${tid3}`)
166
-
})
182
+
assert.ok(tid1 < tid2, `${tid1} should be less than ${tid2}`);
183
+
assert.ok(tid2 < tid3, `${tid2} should be less than ${tid3}`);
184
+
});
167
185
168
186
test('generates unique TIDs', () => {
169
-
const tids = new Set()
187
+
const tids = new Set();
170
188
for (let i = 0; i < 100; i++) {
171
-
tids.add(createTid())
189
+
tids.add(createTid());
172
190
}
173
-
assert.strictEqual(tids.size, 100)
174
-
})
175
-
})
191
+
assert.strictEqual(tids.size, 100);
192
+
});
193
+
});
176
194
177
195
describe('P-256 Signing', () => {
178
196
test('generates key pair with correct sizes', async () => {
179
-
const kp = await generateKeyPair()
197
+
const kp = await generateKeyPair();
180
198
181
-
assert.strictEqual(kp.privateKey.length, 32)
182
-
assert.strictEqual(kp.publicKey.length, 33) // compressed
183
-
assert.ok(kp.publicKey[0] === 0x02 || kp.publicKey[0] === 0x03)
184
-
})
199
+
assert.strictEqual(kp.privateKey.length, 32);
200
+
assert.strictEqual(kp.publicKey.length, 33); // compressed
201
+
assert.ok(kp.publicKey[0] === 0x02 || kp.publicKey[0] === 0x03);
202
+
});
185
203
186
204
test('can sign data with generated key', async () => {
187
-
const kp = await generateKeyPair()
188
-
const key = await importPrivateKey(kp.privateKey)
189
-
const data = new TextEncoder().encode('test message')
190
-
const sig = await sign(key, data)
205
+
const kp = await generateKeyPair();
206
+
const key = await importPrivateKey(kp.privateKey);
207
+
const data = new TextEncoder().encode('test message');
208
+
const sig = await sign(key, data);
191
209
192
-
assert.strictEqual(sig.length, 64) // r (32) + s (32)
193
-
})
210
+
assert.strictEqual(sig.length, 64); // r (32) + s (32)
211
+
});
194
212
195
213
test('different messages produce different signatures', async () => {
196
-
const kp = await generateKeyPair()
197
-
const key = await importPrivateKey(kp.privateKey)
214
+
const kp = await generateKeyPair();
215
+
const key = await importPrivateKey(kp.privateKey);
198
216
199
-
const sig1 = await sign(key, new TextEncoder().encode('message 1'))
200
-
const sig2 = await sign(key, new TextEncoder().encode('message 2'))
217
+
const sig1 = await sign(key, new TextEncoder().encode('message 1'));
218
+
const sig2 = await sign(key, new TextEncoder().encode('message 2'));
201
219
202
-
assert.notDeepStrictEqual(sig1, sig2)
203
-
})
220
+
assert.notDeepStrictEqual(sig1, sig2);
221
+
});
204
222
205
223
test('bytesToHex and hexToBytes roundtrip', () => {
206
-
const original = new Uint8Array([0x00, 0x0f, 0xf0, 0xff, 0xab, 0xcd])
207
-
const hex = bytesToHex(original)
208
-
const back = hexToBytes(hex)
224
+
const original = new Uint8Array([0x00, 0x0f, 0xf0, 0xff, 0xab, 0xcd]);
225
+
const hex = bytesToHex(original);
226
+
const back = hexToBytes(hex);
209
227
210
-
assert.strictEqual(hex, '000ff0ffabcd')
211
-
assert.deepStrictEqual(back, original)
212
-
})
228
+
assert.strictEqual(hex, '000ff0ffabcd');
229
+
assert.deepStrictEqual(back, original);
230
+
});
213
231
214
232
test('importPrivateKey rejects invalid key lengths', async () => {
215
233
// Too short
216
234
await assert.rejects(
217
235
() => importPrivateKey(new Uint8Array(31)),
218
-
/expected 32 bytes, got 31/
219
-
)
236
+
/expected 32 bytes, got 31/,
237
+
);
220
238
221
239
// Too long
222
240
await assert.rejects(
223
241
() => importPrivateKey(new Uint8Array(33)),
224
-
/expected 32 bytes, got 33/
225
-
)
242
+
/expected 32 bytes, got 33/,
243
+
);
226
244
227
245
// Empty
228
246
await assert.rejects(
229
247
() => importPrivateKey(new Uint8Array(0)),
230
-
/expected 32 bytes, got 0/
231
-
)
232
-
})
248
+
/expected 32 bytes, got 0/,
249
+
);
250
+
});
233
251
234
252
test('importPrivateKey rejects non-Uint8Array input', async () => {
235
253
// Arrays have .length but aren't Uint8Array
236
254
await assert.rejects(
237
255
() => importPrivateKey([1, 2, 3]),
238
-
/Invalid private key/
239
-
)
256
+
/Invalid private key/,
257
+
);
240
258
241
259
// Strings don't work either
242
260
await assert.rejects(
243
261
() => importPrivateKey('not bytes'),
244
-
/Invalid private key/
245
-
)
262
+
/Invalid private key/,
263
+
);
246
264
247
265
// null/undefined
248
-
await assert.rejects(
249
-
() => importPrivateKey(null),
250
-
/Invalid private key/
251
-
)
252
-
})
253
-
})
266
+
await assert.rejects(() => importPrivateKey(null), /Invalid private key/);
267
+
});
268
+
});
254
269
255
270
describe('MST Key Depth', () => {
256
271
test('returns a non-negative integer', async () => {
257
-
const depth = await getKeyDepth('app.bsky.feed.post/abc123')
258
-
assert.strictEqual(typeof depth, 'number')
259
-
assert.ok(depth >= 0)
260
-
})
272
+
const depth = await getKeyDepth('app.bsky.feed.post/abc123');
273
+
assert.strictEqual(typeof depth, 'number');
274
+
assert.ok(depth >= 0);
275
+
});
261
276
262
277
test('is deterministic for same key', async () => {
263
-
const key = 'app.bsky.feed.post/test123'
264
-
const depth1 = await getKeyDepth(key)
265
-
const depth2 = await getKeyDepth(key)
266
-
assert.strictEqual(depth1, depth2)
267
-
})
278
+
const key = 'app.bsky.feed.post/test123';
279
+
const depth1 = await getKeyDepth(key);
280
+
const depth2 = await getKeyDepth(key);
281
+
assert.strictEqual(depth1, depth2);
282
+
});
268
283
269
284
test('different keys can have different depths', async () => {
270
285
// Generate many keys and check we get some variation
271
-
const depths = new Set()
286
+
const depths = new Set();
272
287
for (let i = 0; i < 100; i++) {
273
-
depths.add(await getKeyDepth(`collection/key${i}`))
288
+
depths.add(await getKeyDepth(`collection/key${i}`));
274
289
}
275
290
// Should have at least 1 unique depth (realistically more)
276
-
assert.ok(depths.size >= 1)
277
-
})
291
+
assert.ok(depths.size >= 1);
292
+
});
278
293
279
294
test('handles empty string', async () => {
280
-
const depth = await getKeyDepth('')
281
-
assert.strictEqual(typeof depth, 'number')
282
-
assert.ok(depth >= 0)
283
-
})
295
+
const depth = await getKeyDepth('');
296
+
assert.strictEqual(typeof depth, 'number');
297
+
assert.ok(depth >= 0);
298
+
});
284
299
285
300
test('handles unicode strings', async () => {
286
-
const depth = await getKeyDepth('app.bsky.feed.post/émoji🎉')
287
-
assert.strictEqual(typeof depth, 'number')
288
-
assert.ok(depth >= 0)
289
-
})
290
-
})
301
+
const depth = await getKeyDepth('app.bsky.feed.post/émoji🎉');
302
+
assert.strictEqual(typeof depth, 'number');
303
+
assert.ok(depth >= 0);
304
+
});
305
+
});
291
306
292
307
describe('CBOR Decoding', () => {
293
308
test('decodes what encode produces (roundtrip)', () => {
294
-
const original = { hello: 'world', num: 42 }
295
-
const encoded = cborEncode(original)
296
-
const decoded = cborDecode(encoded)
297
-
assert.deepStrictEqual(decoded, original)
298
-
})
309
+
const original = { hello: 'world', num: 42 };
310
+
const encoded = cborEncode(original);
311
+
const decoded = cborDecode(encoded);
312
+
assert.deepStrictEqual(decoded, original);
313
+
});
299
314
300
315
test('decodes null', () => {
301
-
const encoded = cborEncode(null)
302
-
const decoded = cborDecode(encoded)
303
-
assert.strictEqual(decoded, null)
304
-
})
316
+
const encoded = cborEncode(null);
317
+
const decoded = cborDecode(encoded);
318
+
assert.strictEqual(decoded, null);
319
+
});
305
320
306
321
test('decodes booleans', () => {
307
-
assert.strictEqual(cborDecode(cborEncode(true)), true)
308
-
assert.strictEqual(cborDecode(cborEncode(false)), false)
309
-
})
322
+
assert.strictEqual(cborDecode(cborEncode(true)), true);
323
+
assert.strictEqual(cborDecode(cborEncode(false)), false);
324
+
});
310
325
311
326
test('decodes integers', () => {
312
-
assert.strictEqual(cborDecode(cborEncode(0)), 0)
313
-
assert.strictEqual(cborDecode(cborEncode(42)), 42)
314
-
assert.strictEqual(cborDecode(cborEncode(255)), 255)
315
-
assert.strictEqual(cborDecode(cborEncode(-1)), -1)
316
-
assert.strictEqual(cborDecode(cborEncode(-10)), -10)
317
-
})
327
+
assert.strictEqual(cborDecode(cborEncode(0)), 0);
328
+
assert.strictEqual(cborDecode(cborEncode(42)), 42);
329
+
assert.strictEqual(cborDecode(cborEncode(255)), 255);
330
+
assert.strictEqual(cborDecode(cborEncode(-1)), -1);
331
+
assert.strictEqual(cborDecode(cborEncode(-10)), -10);
332
+
});
318
333
319
334
test('decodes strings', () => {
320
-
assert.strictEqual(cborDecode(cborEncode('hello')), 'hello')
321
-
assert.strictEqual(cborDecode(cborEncode('')), '')
322
-
})
335
+
assert.strictEqual(cborDecode(cborEncode('hello')), 'hello');
336
+
assert.strictEqual(cborDecode(cborEncode('')), '');
337
+
});
323
338
324
339
test('decodes arrays', () => {
325
-
assert.deepStrictEqual(cborDecode(cborEncode([1, 2, 3])), [1, 2, 3])
326
-
assert.deepStrictEqual(cborDecode(cborEncode([])), [])
327
-
})
340
+
assert.deepStrictEqual(cborDecode(cborEncode([1, 2, 3])), [1, 2, 3]);
341
+
assert.deepStrictEqual(cborDecode(cborEncode([])), []);
342
+
});
328
343
329
344
test('decodes nested structures', () => {
330
-
const original = { arr: [1, { nested: true }], str: 'test' }
331
-
const decoded = cborDecode(cborEncode(original))
332
-
assert.deepStrictEqual(decoded, original)
333
-
})
334
-
})
345
+
const original = { arr: [1, { nested: true }], str: 'test' };
346
+
const decoded = cborDecode(cborEncode(original));
347
+
assert.deepStrictEqual(decoded, original);
348
+
});
349
+
});
335
350
336
351
describe('CAR File Builder', () => {
337
352
test('varint encodes small numbers', () => {
338
-
assert.deepStrictEqual(varint(0), new Uint8Array([0]))
339
-
assert.deepStrictEqual(varint(1), new Uint8Array([1]))
340
-
assert.deepStrictEqual(varint(127), new Uint8Array([127]))
341
-
})
353
+
assert.deepStrictEqual(varint(0), new Uint8Array([0]));
354
+
assert.deepStrictEqual(varint(1), new Uint8Array([1]));
355
+
assert.deepStrictEqual(varint(127), new Uint8Array([127]));
356
+
});
342
357
343
358
test('varint encodes multi-byte numbers', () => {
344
359
// 128 = 0x80 -> [0x80 | 0x00, 0x01] = [0x80, 0x01]
345
-
assert.deepStrictEqual(varint(128), new Uint8Array([0x80, 0x01]))
360
+
assert.deepStrictEqual(varint(128), new Uint8Array([0x80, 0x01]));
346
361
// 300 = 0x12c -> [0xac, 0x02]
347
-
assert.deepStrictEqual(varint(300), new Uint8Array([0xac, 0x02]))
348
-
})
362
+
assert.deepStrictEqual(varint(300), new Uint8Array([0xac, 0x02]));
363
+
});
349
364
350
365
test('base32 encode/decode roundtrip', () => {
351
-
const original = new Uint8Array([0x01, 0x71, 0x12, 0x20, 0xab, 0xcd])
352
-
const encoded = base32Encode(original)
353
-
const decoded = base32Decode(encoded)
354
-
assert.deepStrictEqual(decoded, original)
355
-
})
366
+
const original = new Uint8Array([0x01, 0x71, 0x12, 0x20, 0xab, 0xcd]);
367
+
const encoded = base32Encode(original);
368
+
const decoded = base32Decode(encoded);
369
+
assert.deepStrictEqual(decoded, original);
370
+
});
356
371
357
372
test('buildCarFile produces valid structure', async () => {
358
-
const data = cborEncode({ test: 'data' })
359
-
const cid = await createCid(data)
360
-
const cidStr = cidToString(cid)
373
+
const data = cborEncode({ test: 'data' });
374
+
const cid = await createCid(data);
375
+
const cidStr = cidToString(cid);
361
376
362
-
const car = buildCarFile(cidStr, [{ cid: cidStr, data }])
377
+
const car = buildCarFile(cidStr, [{ cid: cidStr, data }]);
363
378
364
-
assert.ok(car instanceof Uint8Array)
365
-
assert.ok(car.length > 0)
379
+
assert.ok(car instanceof Uint8Array);
380
+
assert.ok(car.length > 0);
366
381
// First byte should be varint of header length
367
-
assert.ok(car[0] > 0)
368
-
})
369
-
})
382
+
assert.ok(car[0] > 0);
383
+
});
384
+
});
370
385
371
386
describe('JWT Base64URL', () => {
372
387
test('base64UrlEncode encodes bytes correctly', () => {
373
-
const input = new TextEncoder().encode('hello world')
374
-
const encoded = base64UrlEncode(input)
375
-
assert.strictEqual(encoded, 'aGVsbG8gd29ybGQ')
376
-
assert.ok(!encoded.includes('+'))
377
-
assert.ok(!encoded.includes('/'))
378
-
assert.ok(!encoded.includes('='))
379
-
})
388
+
const input = new TextEncoder().encode('hello world');
389
+
const encoded = base64UrlEncode(input);
390
+
assert.strictEqual(encoded, 'aGVsbG8gd29ybGQ');
391
+
assert.ok(!encoded.includes('+'));
392
+
assert.ok(!encoded.includes('/'));
393
+
assert.ok(!encoded.includes('='));
394
+
});
380
395
381
396
test('base64UrlDecode decodes string correctly', () => {
382
-
const decoded = base64UrlDecode('aGVsbG8gd29ybGQ')
383
-
const str = new TextDecoder().decode(decoded)
384
-
assert.strictEqual(str, 'hello world')
385
-
})
397
+
const decoded = base64UrlDecode('aGVsbG8gd29ybGQ');
398
+
const str = new TextDecoder().decode(decoded);
399
+
assert.strictEqual(str, 'hello world');
400
+
});
386
401
387
402
test('base64url roundtrip', () => {
388
-
const original = new Uint8Array([0, 1, 2, 255, 254, 253])
389
-
const encoded = base64UrlEncode(original)
390
-
const decoded = base64UrlDecode(encoded)
391
-
assert.deepStrictEqual(decoded, original)
392
-
})
393
-
})
403
+
const original = new Uint8Array([0, 1, 2, 255, 254, 253]);
404
+
const encoded = base64UrlEncode(original);
405
+
const decoded = base64UrlDecode(encoded);
406
+
assert.deepStrictEqual(decoded, original);
407
+
});
408
+
});
394
409
395
410
describe('JWT Creation', () => {
396
411
test('createAccessJwt creates valid JWT structure', async () => {
397
-
const did = 'did:web:test.example'
398
-
const secret = 'test-secret-key'
399
-
const jwt = await createAccessJwt(did, secret)
412
+
const did = 'did:web:test.example';
413
+
const secret = 'test-secret-key';
414
+
const jwt = await createAccessJwt(did, secret);
400
415
401
-
const parts = jwt.split('.')
402
-
assert.strictEqual(parts.length, 3)
416
+
const parts = jwt.split('.');
417
+
assert.strictEqual(parts.length, 3);
403
418
404
419
// Decode header
405
-
const header = JSON.parse(new TextDecoder().decode(base64UrlDecode(parts[0])))
406
-
assert.strictEqual(header.typ, 'at+jwt')
407
-
assert.strictEqual(header.alg, 'HS256')
420
+
const header = JSON.parse(
421
+
new TextDecoder().decode(base64UrlDecode(parts[0])),
422
+
);
423
+
assert.strictEqual(header.typ, 'at+jwt');
424
+
assert.strictEqual(header.alg, 'HS256');
408
425
409
426
// Decode payload
410
-
const payload = JSON.parse(new TextDecoder().decode(base64UrlDecode(parts[1])))
411
-
assert.strictEqual(payload.scope, 'com.atproto.access')
412
-
assert.strictEqual(payload.sub, did)
413
-
assert.strictEqual(payload.aud, did)
414
-
assert.ok(payload.iat > 0)
415
-
assert.ok(payload.exp > payload.iat)
416
-
})
427
+
const payload = JSON.parse(
428
+
new TextDecoder().decode(base64UrlDecode(parts[1])),
429
+
);
430
+
assert.strictEqual(payload.scope, 'com.atproto.access');
431
+
assert.strictEqual(payload.sub, did);
432
+
assert.strictEqual(payload.aud, did);
433
+
assert.ok(payload.iat > 0);
434
+
assert.ok(payload.exp > payload.iat);
435
+
});
417
436
418
437
test('createRefreshJwt creates valid JWT with jti', async () => {
419
-
const did = 'did:web:test.example'
420
-
const secret = 'test-secret-key'
421
-
const jwt = await createRefreshJwt(did, secret)
438
+
const did = 'did:web:test.example';
439
+
const secret = 'test-secret-key';
440
+
const jwt = await createRefreshJwt(did, secret);
422
441
423
-
const parts = jwt.split('.')
424
-
const header = JSON.parse(new TextDecoder().decode(base64UrlDecode(parts[0])))
425
-
assert.strictEqual(header.typ, 'refresh+jwt')
442
+
const parts = jwt.split('.');
443
+
const header = JSON.parse(
444
+
new TextDecoder().decode(base64UrlDecode(parts[0])),
445
+
);
446
+
assert.strictEqual(header.typ, 'refresh+jwt');
426
447
427
-
const payload = JSON.parse(new TextDecoder().decode(base64UrlDecode(parts[1])))
428
-
assert.strictEqual(payload.scope, 'com.atproto.refresh')
429
-
assert.ok(payload.jti) // has unique token ID
430
-
})
431
-
})
448
+
const payload = JSON.parse(
449
+
new TextDecoder().decode(base64UrlDecode(parts[1])),
450
+
);
451
+
assert.strictEqual(payload.scope, 'com.atproto.refresh');
452
+
assert.ok(payload.jti); // has unique token ID
453
+
});
454
+
});
432
455
433
456
describe('JWT Verification', () => {
434
457
test('verifyAccessJwt returns payload for valid token', async () => {
435
-
const did = 'did:web:test.example'
436
-
const secret = 'test-secret-key'
437
-
const jwt = await createAccessJwt(did, secret)
458
+
const did = 'did:web:test.example';
459
+
const secret = 'test-secret-key';
460
+
const jwt = await createAccessJwt(did, secret);
438
461
439
-
const payload = await verifyAccessJwt(jwt, secret)
440
-
assert.strictEqual(payload.sub, did)
441
-
assert.strictEqual(payload.scope, 'com.atproto.access')
442
-
})
462
+
const payload = await verifyAccessJwt(jwt, secret);
463
+
assert.strictEqual(payload.sub, did);
464
+
assert.strictEqual(payload.scope, 'com.atproto.access');
465
+
});
443
466
444
467
test('verifyAccessJwt throws for wrong secret', async () => {
445
-
const did = 'did:web:test.example'
446
-
const jwt = await createAccessJwt(did, 'correct-secret')
468
+
const did = 'did:web:test.example';
469
+
const jwt = await createAccessJwt(did, 'correct-secret');
447
470
448
471
await assert.rejects(
449
472
() => verifyAccessJwt(jwt, 'wrong-secret'),
450
-
/invalid signature/i
451
-
)
452
-
})
473
+
/invalid signature/i,
474
+
);
475
+
});
453
476
454
477
test('verifyAccessJwt throws for expired token', async () => {
455
-
const did = 'did:web:test.example'
456
-
const secret = 'test-secret-key'
478
+
const did = 'did:web:test.example';
479
+
const secret = 'test-secret-key';
457
480
// Create token that expired 1 second ago
458
-
const jwt = await createAccessJwt(did, secret, -1)
481
+
const jwt = await createAccessJwt(did, secret, -1);
459
482
460
-
await assert.rejects(
461
-
() => verifyAccessJwt(jwt, secret),
462
-
/expired/i
463
-
)
464
-
})
483
+
await assert.rejects(() => verifyAccessJwt(jwt, secret), /expired/i);
484
+
});
465
485
466
486
test('verifyAccessJwt throws for refresh token', async () => {
467
-
const did = 'did:web:test.example'
468
-
const secret = 'test-secret-key'
469
-
const jwt = await createRefreshJwt(did, secret)
487
+
const did = 'did:web:test.example';
488
+
const secret = 'test-secret-key';
489
+
const jwt = await createRefreshJwt(did, secret);
470
490
471
491
await assert.rejects(
472
492
() => verifyAccessJwt(jwt, secret),
473
-
/invalid token type/i
474
-
)
475
-
})
476
-
})
493
+
/invalid token type/i,
494
+
);
495
+
});
496
+
});