Skip to main content

max / makenotwork

2.7 KB · 79 lines History Blame Raw
1 // SyncKit "Keys Secret" action for the dashboard app rows.
2 //
3 // The credential for POST /api/sync/keys/{claim,release,list}. Deliberately
4 // separate from the app's API key: that key is compiled into shipped clients
5 // and can be read out of a binary, so it cannot gate calls that spend the app's
6 // key cap. This one belongs on a backend the developer controls.
7 //
8 // Registered through the dispatcher rather than added to the legacy
9 // static/synckit-tabs.js globals, per the frontend_globals ratchet.
10
11 import { register } from '../core/dispatch.ts';
12 import { csrfHeaders } from '../core/net.ts';
13 import { showToast } from '../core/toast.ts';
14 import { copyWithFeedback } from '../core/clipboard.ts';
15
16 /** A "Copy" button bound to `text`. Built in JS so the secret never lands in
17 * markup the template rendered. */
18 function copyButton(text: string): HTMLButtonElement {
19 const btn = document.createElement('button');
20 btn.type = 'button';
21 btn.className = 'btn-small btn-secondary';
22 btn.textContent = 'Copy';
23 btn.addEventListener('click', () => copyWithFeedback(btn, text, 'Copied'));
24 return btn;
25 }
26
27 /** Replace the row's secret cell with the plaintext, a copy button, and the
28 * save-it-now warning. The server returns the secret exactly once. */
29 function revealSecret(appId: string, secret: string): void {
30 const cell = document.getElementById(`synckit-keys-secret-${appId}`);
31 if (!cell) return;
32 cell.replaceChildren();
33
34 const code = document.createElement('code');
35 code.textContent = secret;
36 cell.appendChild(code);
37 cell.appendChild(document.createTextNode(' '));
38 cell.appendChild(copyButton(secret));
39
40 const warn = document.createElement('div');
41 warn.className = 'form-hint';
42 warn.textContent =
43 'Save it now: it is not shown again. Keep it on a server, never in a shipped client.';
44 cell.appendChild(warn);
45 }
46
47 register('syncKitRegenKeysSecret', function (appId: string) {
48 if (!appId) return;
49 if (
50 !confirm(
51 'Generate a new keys secret? Any backend using the current one will stop working.',
52 )
53 ) {
54 return;
55 }
56
57 void fetch(`/api/sync/apps/${appId}/keys-secret`, {
58 method: 'POST',
59 credentials: 'same-origin',
60 headers: csrfHeaders(),
61 })
62 .then(async (res) => {
63 if (!res.ok) {
64 showToast('Failed to generate keys secret.');
65 return;
66 }
67 const data = (await res.json()) as { app_secret?: string };
68 if (!data.app_secret) {
69 showToast('No secret returned.');
70 return;
71 }
72 revealSecret(appId, data.app_secret);
73 showToast('Keys secret generated. Copy it now: it will not be shown again.');
74 })
75 .catch(() => {
76 showToast('Network error. Please check your connection and try again.');
77 });
78 });
79