+420
-74
islands/DidPlcProgress.tsx
+420
-74
islands/DidPlcProgress.tsx
···
2
2
3
3
interface PlcUpdateStep {
4
4
name: string;
5
-
status: "pending" | "in-progress" | "completed" | "error";
5
+
status: "pending" | "in-progress" | "verifying" | "completed" | "error";
6
6
error?: string;
7
7
}
8
8
···
10
10
const [hasStarted, setHasStarted] = useState(false);
11
11
const [steps, setSteps] = useState<PlcUpdateStep[]>([
12
12
{ name: "Generate PLC key", status: "pending" },
13
-
{ name: "Update PLC key", status: "pending" },
13
+
{ name: "Start PLC update", status: "pending" },
14
+
{ name: "Complete PLC update", status: "pending" },
14
15
]);
15
16
const [generatedKey, setGeneratedKey] = useState<string>("");
16
-
const [updateKey, setUpdateKey] = useState<string>("");
17
+
const [keyJson, setKeyJson] = useState<any>(null);
18
+
const [emailToken, setEmailToken] = useState<string>("");
17
19
const [updateResult, setUpdateResult] = useState<string>("");
20
+
const [showDownload, setShowDownload] = useState(false);
21
+
const [showKeyInfo, setShowKeyInfo] = useState(false);
18
22
19
23
const updateStepStatus = (
20
24
index: number,
21
25
status: PlcUpdateStep["status"],
22
26
error?: string
23
27
) => {
28
+
console.log(
29
+
`Updating step ${index} to ${status}${
30
+
error ? ` with error: ${error}` : ""
31
+
}`
32
+
);
24
33
setSteps((prevSteps) =>
25
34
prevSteps.map((step, i) =>
26
-
i === index ? { ...step, status, error } : step
35
+
i === index
36
+
? { ...step, status, error }
37
+
: i > index
38
+
? { ...step, status: "pending", error: undefined }
39
+
: step
27
40
)
28
41
);
29
42
};
30
43
31
44
const handleStart = () => {
32
45
setHasStarted(true);
46
+
// Automatically start the first step
47
+
setTimeout(() => {
48
+
handleGenerateKey();
49
+
}, 100);
50
+
};
51
+
52
+
const getStepDisplayName = (step: PlcUpdateStep, index: number) => {
53
+
if (step.status === "completed") {
54
+
switch (index) {
55
+
case 0:
56
+
return "PLC Key Generated";
57
+
case 1:
58
+
return "PLC Update Started";
59
+
case 2:
60
+
return "PLC Update Completed";
61
+
}
62
+
}
63
+
64
+
if (step.status === "in-progress") {
65
+
switch (index) {
66
+
case 0:
67
+
return "Generating PLC key...";
68
+
case 1:
69
+
return "Starting PLC update...";
70
+
case 2:
71
+
return step.name ===
72
+
"Enter the token sent to your email to complete PLC update"
73
+
? step.name
74
+
: "Completing PLC update...";
75
+
}
76
+
}
77
+
78
+
if (step.status === "verifying") {
79
+
switch (index) {
80
+
case 0:
81
+
return "Verifying key generation...";
82
+
case 1:
83
+
return "Verifying PLC update start...";
84
+
case 2:
85
+
return "Verifying PLC update completion...";
86
+
}
87
+
}
88
+
89
+
return step.name;
33
90
};
34
91
35
92
const handleGenerateKey = async () => {
36
93
updateStepStatus(0, "in-progress");
94
+
setShowDownload(false);
95
+
setKeyJson(null);
96
+
setGeneratedKey("");
37
97
try {
38
98
const res = await fetch("/api/plc/keys");
39
99
const text = await res.text();
···
51
111
} catch {
52
112
throw new Error("Invalid response from /api/plc/keys");
53
113
}
54
-
if (!data.did || !data.signature) {
55
-
throw new Error("Key generation failed: missing did or signature");
114
+
if (!data.publicKeyDid || !data.privateKeyHex) {
115
+
throw new Error("Key generation failed: missing key data");
56
116
}
57
-
setGeneratedKey(data.did);
58
-
setUpdateKey(data.did);
117
+
setGeneratedKey(data.publicKeyDid);
118
+
setKeyJson(data);
119
+
setShowDownload(true);
59
120
updateStepStatus(0, "completed");
121
+
122
+
// Auto-download the key
123
+
setTimeout(() => {
124
+
console.log("Attempting auto-download with keyJson:", keyJson);
125
+
handleDownload();
126
+
}, 500);
127
+
128
+
// Auto-continue to next step with the generated key
129
+
setTimeout(() => {
130
+
handleStartPlcUpdate(data.publicKeyDid);
131
+
}, 1000);
60
132
} catch (error) {
61
133
updateStepStatus(
62
134
0,
···
66
138
}
67
139
};
68
140
69
-
const handleUpdateKey = async () => {
141
+
const handleStartPlcUpdate = async (keyToUse?: string) => {
142
+
const key = keyToUse || generatedKey;
143
+
if (!key) {
144
+
console.log("No key generated yet", { key, generatedKey });
145
+
updateStepStatus(1, "error", "No key generated yet");
146
+
return;
147
+
}
148
+
70
149
updateStepStatus(1, "in-progress");
71
-
setUpdateResult("");
72
150
try {
73
151
const res = await fetch("/api/plc/update", {
74
152
method: "POST",
75
153
headers: { "Content-Type": "application/json" },
76
-
body: JSON.stringify({ key: updateKey }),
154
+
body: JSON.stringify({ key: key }),
77
155
});
78
156
const text = await res.text();
79
157
if (!res.ok) {
80
158
try {
81
159
const json = JSON.parse(text);
82
-
throw new Error(json.message || "Failed to update key");
160
+
throw new Error(json.message || "Failed to start PLC update");
83
161
} catch {
84
-
throw new Error(text || "Failed to update key");
162
+
throw new Error(text || "Failed to start PLC update");
85
163
}
86
164
}
87
-
setUpdateResult("Key updated successfully!");
165
+
166
+
// Update step name to prompt for token
167
+
setSteps((prevSteps) =>
168
+
prevSteps.map((step, i) =>
169
+
i === 1
170
+
? {
171
+
...step,
172
+
name: "Enter the token sent to your email to complete PLC update",
173
+
}
174
+
: step
175
+
)
176
+
);
88
177
updateStepStatus(1, "completed");
89
178
} catch (error) {
90
179
updateStepStatus(
···
92
181
"error",
93
182
error instanceof Error ? error.message : String(error)
94
183
);
184
+
}
185
+
};
186
+
187
+
const handleCompletePlcUpdate = async () => {
188
+
if (!emailToken) {
189
+
updateStepStatus(2, "error", "Please enter the email token");
190
+
return;
191
+
}
192
+
193
+
updateStepStatus(2, "in-progress");
194
+
try {
195
+
const res = await fetch(
196
+
`/api/plc/update/complete?token=${encodeURIComponent(emailToken)}`,
197
+
{
198
+
method: "POST",
199
+
headers: { "Content-Type": "application/json" },
200
+
}
201
+
);
202
+
const text = await res.text();
203
+
if (!res.ok) {
204
+
try {
205
+
const json = JSON.parse(text);
206
+
throw new Error(json.message || "Failed to complete PLC update");
207
+
} catch {
208
+
throw new Error(text || "Failed to complete PLC update");
209
+
}
210
+
}
211
+
212
+
let data;
213
+
try {
214
+
data = JSON.parse(text);
215
+
if (!data.success) {
216
+
throw new Error(data.message || "PLC update failed");
217
+
}
218
+
} catch {
219
+
throw new Error("Invalid response from server");
220
+
}
221
+
222
+
setUpdateResult("PLC update completed successfully!");
223
+
updateStepStatus(2, "completed");
224
+
} catch (error) {
225
+
updateStepStatus(
226
+
2,
227
+
"error",
228
+
error instanceof Error ? error.message : String(error)
229
+
);
95
230
setUpdateResult(error instanceof Error ? error.message : String(error));
96
231
}
97
232
};
98
233
234
+
const handleDownload = () => {
235
+
console.log("handleDownload called with keyJson:", keyJson);
236
+
if (!keyJson) {
237
+
console.error("No key JSON to download");
238
+
return;
239
+
}
240
+
try {
241
+
const jsonString = JSON.stringify(keyJson, null, 2);
242
+
console.log("JSON string to download:", jsonString);
243
+
const blob = new Blob([jsonString], {
244
+
type: "application/json",
245
+
});
246
+
const url = URL.createObjectURL(blob);
247
+
const a = document.createElement("a");
248
+
a.href = url;
249
+
a.download = `plc-key-${keyJson.publicKeyDid || "unknown"}.json`;
250
+
a.style.display = "none";
251
+
document.body.appendChild(a);
252
+
console.log("Download link created, clicking...");
253
+
a.click();
254
+
document.body.removeChild(a);
255
+
URL.revokeObjectURL(url);
256
+
console.log("Key downloaded successfully:", keyJson.publicKeyDid);
257
+
} catch (error) {
258
+
console.error("Download failed:", error);
259
+
}
260
+
};
261
+
262
+
const getStepIcon = (status: PlcUpdateStep["status"]) => {
263
+
switch (status) {
264
+
case "pending":
265
+
return (
266
+
<div class="w-8 h-8 rounded-full border-2 border-gray-300 dark:border-gray-600 flex items-center justify-center">
267
+
<div class="w-3 h-3 rounded-full bg-gray-300 dark:bg-gray-600" />
268
+
</div>
269
+
);
270
+
case "in-progress":
271
+
return (
272
+
<div class="w-8 h-8 rounded-full border-2 border-blue-500 border-t-transparent animate-spin flex items-center justify-center">
273
+
<div class="w-3 h-3 rounded-full bg-blue-500" />
274
+
</div>
275
+
);
276
+
case "verifying":
277
+
return (
278
+
<div class="w-8 h-8 rounded-full border-2 border-yellow-500 border-t-transparent animate-spin flex items-center justify-center">
279
+
<div class="w-3 h-3 rounded-full bg-yellow-500" />
280
+
</div>
281
+
);
282
+
case "completed":
283
+
return (
284
+
<div class="w-8 h-8 rounded-full bg-green-500 flex items-center justify-center">
285
+
<svg
286
+
class="w-5 h-5 text-white"
287
+
fill="none"
288
+
stroke="currentColor"
289
+
viewBox="0 0 24 24"
290
+
>
291
+
<path
292
+
stroke-linecap="round"
293
+
stroke-linejoin="round"
294
+
stroke-width="2"
295
+
d="M5 13l4 4L19 7"
296
+
/>
297
+
</svg>
298
+
</div>
299
+
);
300
+
case "error":
301
+
return (
302
+
<div class="w-8 h-8 rounded-full bg-red-500 flex items-center justify-center">
303
+
<svg
304
+
class="w-5 h-5 text-white"
305
+
fill="none"
306
+
stroke="currentColor"
307
+
viewBox="0 0 24 24"
308
+
>
309
+
<path
310
+
stroke-linecap="round"
311
+
stroke-linejoin="round"
312
+
stroke-width="2"
313
+
d="M6 18L18 6M6 6l12 12"
314
+
/>
315
+
</svg>
316
+
</div>
317
+
);
318
+
}
319
+
};
320
+
321
+
const getStepClasses = (status: PlcUpdateStep["status"]) => {
322
+
const baseClasses =
323
+
"flex items-center space-x-3 p-4 rounded-lg transition-colors duration-200";
324
+
switch (status) {
325
+
case "pending":
326
+
return `${baseClasses} bg-gray-50 dark:bg-gray-800`;
327
+
case "in-progress":
328
+
return `${baseClasses} bg-blue-50 dark:bg-blue-900`;
329
+
case "verifying":
330
+
return `${baseClasses} bg-yellow-50 dark:bg-yellow-900`;
331
+
case "completed":
332
+
return `${baseClasses} bg-green-50 dark:bg-green-900`;
333
+
case "error":
334
+
return `${baseClasses} bg-red-50 dark:bg-red-900`;
335
+
}
336
+
};
337
+
99
338
if (!hasStarted) {
100
339
return (
101
340
<div class="space-y-6">
···
111
350
<p>
112
351
• Generate a new PLC key with cryptographic signature verification
113
352
</p>
114
-
<p>• Update your existing DID with the new key</p>
353
+
<p>• Start PLC update process (sends email with token)</p>
354
+
<p>• Complete PLC update using email token</p>
115
355
<p>• All operations require authentication</p>
116
356
</div>
117
357
<button
···
127
367
128
368
return (
129
369
<div class="space-y-8">
370
+
{/* Steps Section */}
130
371
<div class="space-y-4">
131
-
{/* Step 1: Generate PLC key */}
132
-
<div
133
-
class={`flex items-center space-x-3 p-4 rounded-lg ${
134
-
steps[0].status === "completed"
135
-
? "bg-green-50 dark:bg-green-900"
136
-
: steps[0].status === "in-progress"
137
-
? "bg-blue-50 dark:bg-blue-900"
138
-
: steps[0].status === "error"
139
-
? "bg-red-50 dark:bg-red-900"
140
-
: "bg-gray-50 dark:bg-gray-800"
141
-
}`}
142
-
>
372
+
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">
373
+
PLC Update Process
374
+
</h3>
375
+
{steps.map((step, index) => (
376
+
<div key={step.name} class={getStepClasses(step.status)}>
377
+
{getStepIcon(step.status)}
378
+
<div class="flex-1">
379
+
<p
380
+
class={`font-medium ${
381
+
step.status === "error"
382
+
? "text-red-900 dark:text-red-200"
383
+
: step.status === "completed"
384
+
? "text-green-900 dark:text-green-200"
385
+
: step.status === "in-progress"
386
+
? "text-blue-900 dark:text-blue-200"
387
+
: "text-gray-900 dark:text-gray-200"
388
+
}`}
389
+
>
390
+
{getStepDisplayName(step, index)}
391
+
</p>
392
+
{step.error && (
393
+
<p class="text-sm text-red-600 dark:text-red-400 mt-1">
394
+
{(() => {
395
+
try {
396
+
const err = JSON.parse(step.error);
397
+
return err.message || step.error;
398
+
} catch {
399
+
return step.error;
400
+
}
401
+
})()}
402
+
</p>
403
+
)}
404
+
{index === 1 && step.status === "completed" && (
405
+
<div class="mt-4">
406
+
<button
407
+
type="button"
408
+
onClick={() => handleStartPlcUpdate()}
409
+
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors duration-200"
410
+
>
411
+
Retry PLC Update
412
+
</button>
413
+
</div>
414
+
)}
415
+
{index === 1 &&
416
+
step.status === "in-progress" &&
417
+
step.name ===
418
+
"Enter the token sent to your email to complete PLC update" && (
419
+
<div class="mt-4 space-y-4">
420
+
<p class="text-sm text-blue-800 dark:text-blue-200">
421
+
Please check your email for the PLC update token and enter
422
+
it below:
423
+
</p>
424
+
<div class="flex space-x-2">
425
+
<input
426
+
type="text"
427
+
value={emailToken}
428
+
onChange={(e) => setEmailToken(e.currentTarget.value)}
429
+
placeholder="Enter token"
430
+
class="flex-1 rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:focus:border-blue-400 dark:focus:ring-blue-400"
431
+
/>
432
+
<button
433
+
type="button"
434
+
onClick={handleCompletePlcUpdate}
435
+
class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-colors duration-200"
436
+
>
437
+
Submit Token
438
+
</button>
439
+
</div>
440
+
</div>
441
+
)}
442
+
</div>
443
+
</div>
444
+
))}
445
+
</div>
446
+
447
+
{/* Key Information Section - Collapsible at bottom */}
448
+
{keyJson && (
449
+
<div class="border border-gray-200 dark:border-gray-700 rounded-lg">
143
450
<button
144
-
class="px-4 py-2 bg-blue-600 text-white rounded-md"
145
-
onClick={handleGenerateKey}
146
-
disabled={steps[0].status === "in-progress"}
451
+
onClick={() => setShowKeyInfo(!showKeyInfo)}
452
+
class="w-full p-4 text-left bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-t-lg flex items-center justify-between"
147
453
>
148
-
Generate PLC Key
454
+
<span class="font-medium text-gray-900 dark:text-gray-100">
455
+
Generated Key Information
456
+
</span>
457
+
<svg
458
+
class={`w-5 h-5 text-gray-500 transition-transform ${
459
+
showKeyInfo ? "rotate-180" : ""
460
+
}`}
461
+
fill="none"
462
+
stroke="currentColor"
463
+
viewBox="0 0 24 24"
464
+
>
465
+
<path
466
+
stroke-linecap="round"
467
+
stroke-linejoin="round"
468
+
stroke-width="2"
469
+
d="M19 9l-7 7-7-7"
470
+
/>
471
+
</svg>
149
472
</button>
150
-
{steps[0].status === "completed" && (
151
-
<span class="text-green-700 ml-4">Key generated!</span>
152
-
)}
153
-
{steps[0].status === "error" && (
154
-
<span class="text-red-700 ml-4">{steps[0].error}</span>
473
+
{showKeyInfo && (
474
+
<div class="p-4 bg-white dark:bg-gray-900 rounded-b-lg">
475
+
<div class="space-y-3 text-sm text-gray-700 dark:text-gray-300">
476
+
<div>
477
+
<b>Key type:</b> {keyJson.keyType}
478
+
</div>
479
+
<div>
480
+
<b>Public key (did:key):</b>{" "}
481
+
<span class="break-all font-mono">
482
+
{keyJson.publicKeyDid}
483
+
</span>
484
+
</div>
485
+
<div>
486
+
<b>Private key (hex):</b>{" "}
487
+
<span class="break-all font-mono">
488
+
{keyJson.privateKeyHex}
489
+
</span>
490
+
</div>
491
+
<div>
492
+
<b>Private key (multikey):</b>{" "}
493
+
<span class="break-all font-mono">
494
+
{keyJson.privateKeyMultikey}
495
+
</span>
496
+
</div>
497
+
</div>
498
+
<div class="mt-4">
499
+
<button
500
+
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md text-sm"
501
+
onClick={handleDownload}
502
+
>
503
+
Download Key JSON
504
+
</button>
505
+
</div>
506
+
</div>
155
507
)}
156
508
</div>
157
-
{generatedKey && (
158
-
<div class="p-2 bg-gray-100 dark:bg-gray-700 rounded">
159
-
<div class="text-xs text-gray-700 dark:text-gray-200 break-all">
160
-
<b>Generated DID:</b> {generatedKey}
161
-
</div>
162
-
</div>
163
-
)}
164
-
{/* Step 2: Update PLC key */}
165
-
<div
166
-
class={`flex flex-col space-y-2 p-4 rounded-lg ${
167
-
steps[1].status === "completed"
168
-
? "bg-green-50 dark:bg-green-900"
169
-
: steps[1].status === "in-progress"
170
-
? "bg-blue-50 dark:bg-blue-900"
171
-
: steps[1].status === "error"
172
-
? "bg-red-50 dark:bg-red-900"
173
-
: "bg-gray-50 dark:bg-gray-800"
174
-
}`}
175
-
>
176
-
<label class="text-sm mb-1">DID to update:</label>
177
-
<input
178
-
class="p-2 rounded border border-gray-300 dark:border-gray-600"
179
-
type="text"
180
-
value={updateKey}
181
-
onInput={(e) => setUpdateKey(e.currentTarget.value)}
182
-
placeholder="Paste or use generated DID"
183
-
/>
509
+
)}
510
+
511
+
{steps[2].status === "completed" && (
512
+
<div class="p-4 bg-green-50 dark:bg-green-900 rounded-lg border-2 border-green-200 dark:border-green-800">
513
+
<p class="text-sm text-green-800 dark:text-green-200">
514
+
PLC update completed successfully! You can now close this page.
515
+
</p>
184
516
<button
185
-
class="mt-2 px-4 py-2 bg-blue-600 text-white rounded-md"
186
-
onClick={handleUpdateKey}
187
-
disabled={steps[1].status === "in-progress" || !updateKey}
517
+
type="button"
518
+
onClick={async () => {
519
+
try {
520
+
const response = await fetch("/api/logout", {
521
+
method: "POST",
522
+
credentials: "include",
523
+
});
524
+
if (!response.ok) {
525
+
throw new Error("Logout failed");
526
+
}
527
+
globalThis.location.href = "/";
528
+
} catch (error) {
529
+
console.error("Failed to logout:", error);
530
+
}
531
+
}}
532
+
class="mt-4 mr-4 px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors duration-200"
188
533
>
189
-
Update PLC Key
534
+
Sign Out
190
535
</button>
191
-
{steps[1].status === "completed" && (
192
-
<span class="text-green-700 mt-2">{updateResult}</span>
193
-
)}
194
-
{steps[1].status === "error" && (
195
-
<span class="text-red-700 mt-2">{steps[1].error}</span>
196
-
)}
536
+
<a
537
+
href="https://ko-fi.com/knotbin"
538
+
target="_blank"
539
+
class="mt-4 px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors duration-200"
540
+
>
541
+
Donate
542
+
</a>
197
543
</div>
198
-
</div>
544
+
)}
199
545
</div>
200
546
);
201
547
}
+21
-29
lib/cred/sessions.ts
+21
-29
lib/cred/sessions.ts
···
17
17
}
18
18
return migrationSessionOptions;
19
19
}
20
-
20
+
21
21
if (!credentialSessionOptions) {
22
22
credentialSessionOptions = await createSessionOptions("cred_sid");
23
23
}
···
37
37
isMigration: boolean = false
38
38
) {
39
39
const options = await getOptions(isMigration);
40
-
return getIronSession<CredentialSession>(
41
-
req,
42
-
res,
43
-
options,
44
-
);
40
+
return getIronSession<CredentialSession>(req, res, options);
45
41
}
46
42
47
43
/**
···
54
50
export async function getCredentialAgent(
55
51
req: Request,
56
52
res: Response = new Response(),
57
-
isMigration: boolean = false,
53
+
isMigration: boolean = false
58
54
) {
59
-
const session = await getCredentialSession(
60
-
req,
61
-
res,
62
-
isMigration
63
-
);
64
-
if (!session.did || !session.service || !session.handle || !session.password) {
55
+
const session = await getCredentialSession(req, res, isMigration);
56
+
if (
57
+
!session.did ||
58
+
!session.service ||
59
+
!session.handle ||
60
+
!session.password
61
+
) {
65
62
return null;
66
63
}
67
64
···
107
104
req: Request,
108
105
res: Response,
109
106
data: CredentialSession,
110
-
isMigration: boolean = false,
107
+
isMigration: boolean = false
111
108
) {
112
-
const session = await getCredentialSession(
113
-
req,
114
-
res,
115
-
isMigration
116
-
);
109
+
const session = await getCredentialSession(req, res, isMigration);
117
110
session.did = data.did;
118
111
session.handle = data.handle;
119
112
session.service = data.service;
···
132
125
export async function getCredentialSessionAgent(
133
126
req: Request,
134
127
res: Response = new Response(),
135
-
isMigration: boolean = false,
128
+
isMigration: boolean = false
136
129
) {
137
-
const session = await getCredentialSession(
138
-
req,
139
-
res,
140
-
isMigration
141
-
);
130
+
const session = await getCredentialSession(req, res, isMigration);
142
131
143
132
console.log("Session state:", {
144
133
hasDid: !!session.did,
···
147
136
hasPassword: !!session.password,
148
137
hasAccessJwt: !!session.accessJwt,
149
138
service: session.service,
150
-
handle: session.handle
139
+
handle: session.handle,
151
140
});
152
141
153
142
if (
154
-
!session.did || !session.service || !session.handle || !session.password
143
+
!session.did ||
144
+
!session.service ||
145
+
!session.handle ||
146
+
!session.password
155
147
) {
156
148
console.log("Missing required session fields");
157
149
return null;
···
170
162
const sessionInfo = await agent.com.atproto.server.getSession();
171
163
console.log("Stored JWT is valid, session info:", {
172
164
did: sessionInfo.data.did,
173
-
handle: sessionInfo.data.handle
165
+
handle: sessionInfo.data.handle,
174
166
});
175
167
return agent;
176
168
} catch (err) {
···
190
182
console.log("Session created successfully:", {
191
183
did: sessionRes.data.did,
192
184
handle: sessionRes.data.handle,
193
-
hasAccessJwt: !!sessionRes.data.accessJwt
185
+
hasAccessJwt: !!sessionRes.data.accessJwt,
194
186
});
195
187
196
188
// Store the new token
+12
-12
routes/api/plc/keys.ts
+12
-12
routes/api/plc/keys.ts
···
1
1
import { Secp256k1Keypair } from "@atproto/crypto";
2
2
import { getSessionAgent } from "../../../lib/sessions.ts";
3
3
import { define } from "../../../utils.ts";
4
+
import * as ui8 from "npm:uint8arrays";
4
5
5
6
/**
6
7
* Generate and return PLC keys for the authenticated user
···
15
16
// Create a new keypair
16
17
const keypair = await Secp256k1Keypair.create({ exportable: true });
17
18
18
-
// sign binary data, resulting signature bytes.
19
-
// SHA-256 hash of data is what actually gets signed.
20
-
// signature output is often base64-encoded.
21
-
const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
22
-
const sig = await keypair.sign(data);
19
+
// Export private key bytes
20
+
const privateKeyBytes = await keypair.export();
21
+
const privateKeyHex = ui8.toString(privateKeyBytes, "hex");
23
22
24
-
// serialize the public key as a did:key string, which includes key type metadata
25
-
const pubDidKey = keypair.did();
26
-
console.log(pubDidKey);
23
+
// Get public key as DID
24
+
const publicKeyDid = keypair.did();
27
25
28
-
// output would look something like: 'did:key:zQ3shVRtgqTRHC7Lj4DYScoDgReNpsDp3HBnuKBKt1FSXKQ38'
26
+
// Convert private key to multikey format (base58btc)
27
+
const privateKeyMultikey = ui8.toString(privateKeyBytes, "base58btc");
29
28
30
29
// Return the key information
31
30
return new Response(
32
31
JSON.stringify({
33
-
did: pubDidKey,
34
-
signature: btoa(String.fromCharCode(...sig)),
35
-
data: Array.from(data),
32
+
keyType: "secp256k1",
33
+
publicKeyDid: publicKeyDid,
34
+
privateKeyHex: privateKeyHex,
35
+
privateKeyMultikey: privateKeyMultikey,
36
36
}),
37
37
{
38
38
headers: { "Content-Type": "application/json" },
+25
-48
routes/api/plc/update.ts
+25
-48
routes/api/plc/update.ts
···
1
1
import { Agent } from "@atproto/api";
2
2
import { getSessionAgent } from "../../../lib/sessions.ts";
3
3
import { define } from "../../../utils.ts";
4
-
import * as plc from "@did-plc/lib";
5
4
6
5
/**
7
-
* Update PLC rotation keys for the authenticated user
6
+
* Start PLC update process - sends email with token
8
7
*/
9
8
export const handler = define.handlers({
10
9
async POST(ctx) {
10
+
const res = new Response();
11
11
try {
12
12
const { key: newKey } = await ctx.req.json();
13
13
···
24
24
);
25
25
}
26
26
27
-
const agent = await getSessionAgent(ctx.req);
27
+
const agent = await getSessionAgent(ctx.req, res, true);
28
28
if (!agent) {
29
29
return new Response(
30
30
JSON.stringify({
···
38
38
);
39
39
}
40
40
41
-
const did = agent.did;
42
-
if (!did) {
43
-
return new Response(
44
-
JSON.stringify({
45
-
success: false,
46
-
message: "No DID found in session",
47
-
}),
48
-
{
49
-
status: 400,
50
-
headers: { "Content-Type": "application/json" },
51
-
}
52
-
);
53
-
}
41
+
// Get recommended credentials first
42
+
console.log("Getting recommended credentials...");
43
+
const getDidCredentials =
44
+
await agent.com.atproto.identity.getRecommendedDidCredentials();
45
+
console.log("Got recommended credentials:", getDidCredentials.data);
54
46
55
-
const client = new plc.Client("https://plc.directory");
56
-
57
-
// Fetch current DID document
58
-
const didDoc = await client.getDocumentData(did);
59
-
if (!didDoc) {
60
-
return new Response(
61
-
JSON.stringify({
62
-
success: false,
63
-
message: "DID document not found",
64
-
}),
65
-
{
66
-
status: 404,
67
-
headers: { "Content-Type": "application/json" },
68
-
}
69
-
);
47
+
const rotationKeys = getDidCredentials.data.rotationKeys ?? [];
48
+
if (!rotationKeys.length) {
49
+
throw new Error("No rotation keys provided in recommended credentials");
70
50
}
71
51
72
-
// Create new rotation keys array with the new key at the beginning
73
-
const newKeys = [newKey, ...didDoc.rotationKeys];
74
-
75
-
// Create the update operation
76
-
const updateOp = plc.updateRotationKeysOp(
77
-
did,
78
-
didDoc.rotationKeys,
79
-
newKeys
80
-
);
81
-
82
-
// Submit the operation to the PLC directory
83
-
await client.sendOperation(updateOp);
52
+
// Request PLC operation token (this will send email)
53
+
const plcOp = await agent.com.atproto.identity.signPlcOperation({
54
+
token: "request", // This will trigger email token generation
55
+
rotationKeys: [newKey, ...rotationKeys],
56
+
...getDidCredentials.data,
57
+
});
84
58
85
59
return new Response(
86
60
JSON.stringify({
87
61
success: true,
88
-
message: "PLC rotation keys updated successfully",
89
-
did,
62
+
message:
63
+
"Email sent with PLC update token. Please check your email and enter the token to complete the update.",
64
+
did: plcOp.data,
90
65
newKey,
91
-
totalKeys: newKeys.length,
92
66
}),
93
67
{
94
68
status: 200,
95
-
headers: { "Content-Type": "application/json" },
69
+
headers: {
70
+
"Content-Type": "application/json",
71
+
...Object.fromEntries(res.headers), // Include session cookie headers
72
+
},
96
73
}
97
74
);
98
75
} catch (error) {
···
103
80
return new Response(
104
81
JSON.stringify({
105
82
success: false,
106
-
message: `Failed to update PLC keys: ${message}`,
83
+
message: `Failed to start PLC update: ${message}`,
107
84
}),
108
85
{
109
86
status: 500,
+92
routes/api/plc/update/complete.ts
+92
routes/api/plc/update/complete.ts
···
1
+
import { Agent } from "@atproto/api";
2
+
import { getSessionAgent } from "../../../../lib/sessions.ts";
3
+
import { define } from "../../../../utils.ts";
4
+
5
+
/**
6
+
* Complete PLC update using email token
7
+
*/
8
+
export const handler = define.handlers({
9
+
async POST(ctx) {
10
+
const res = new Response();
11
+
try {
12
+
const url = new URL(ctx.req.url);
13
+
const token = url.searchParams.get("token");
14
+
15
+
if (!token) {
16
+
return new Response(
17
+
JSON.stringify({
18
+
success: false,
19
+
message: "Missing token parameter",
20
+
}),
21
+
{
22
+
status: 400,
23
+
headers: { "Content-Type": "application/json" },
24
+
}
25
+
);
26
+
}
27
+
28
+
const agent = await getSessionAgent(ctx.req, res, true);
29
+
if (!agent) {
30
+
return new Response(
31
+
JSON.stringify({
32
+
success: false,
33
+
message: "Unauthorized",
34
+
}),
35
+
{
36
+
status: 401,
37
+
headers: { "Content-Type": "application/json" },
38
+
}
39
+
);
40
+
}
41
+
42
+
const did = agent.did;
43
+
if (!did) {
44
+
return new Response(
45
+
JSON.stringify({
46
+
success: false,
47
+
message: "No DID found in session",
48
+
}),
49
+
{
50
+
status: 400,
51
+
headers: { "Content-Type": "application/json" },
52
+
}
53
+
);
54
+
}
55
+
56
+
// Submit the PLC operation with the token
57
+
await agent!.com.atproto.identity.submitPlcOperation({
58
+
operation: { token: token },
59
+
});
60
+
61
+
return new Response(
62
+
JSON.stringify({
63
+
success: true,
64
+
message: "PLC update completed successfully",
65
+
did,
66
+
}),
67
+
{
68
+
status: 200,
69
+
headers: {
70
+
"Content-Type": "application/json",
71
+
...Object.fromEntries(res.headers), // Include session cookie headers
72
+
},
73
+
}
74
+
);
75
+
} catch (error) {
76
+
console.error("PLC update completion error:", error);
77
+
const message =
78
+
error instanceof Error ? error.message : "Unknown error occurred";
79
+
80
+
return new Response(
81
+
JSON.stringify({
82
+
success: false,
83
+
message: `Failed to complete PLC update: ${message}`,
84
+
}),
85
+
{
86
+
status: 500,
87
+
headers: { "Content-Type": "application/json" },
88
+
}
89
+
);
90
+
}
91
+
},
92
+
});