sealkeep 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +201 -0
- package/CHANGELOG.md +218 -0
- package/CONTROL_PLANE.md +86 -0
- package/LICENSE +34 -0
- package/README.md +249 -0
- package/THIRD_PARTY.md +22 -0
- package/THREAT_MODEL.md +107 -0
- package/dist/packages/vaultline-crypto/src/aead.d.ts +12 -0
- package/dist/packages/vaultline-crypto/src/aead.js +24 -0
- package/dist/packages/vaultline-crypto/src/chunk-access.d.ts +39 -0
- package/dist/packages/vaultline-crypto/src/chunk-access.js +93 -0
- package/dist/packages/vaultline-crypto/src/envelope.d.ts +71 -0
- package/dist/packages/vaultline-crypto/src/envelope.js +188 -0
- package/dist/packages/vaultline-crypto/src/format.d.ts +106 -0
- package/dist/packages/vaultline-crypto/src/format.js +43 -0
- package/dist/packages/vaultline-crypto/src/index.d.ts +5 -0
- package/dist/packages/vaultline-crypto/src/index.js +5 -0
- package/dist/packages/vaultline-crypto/src/recipients.d.ts +42 -0
- package/dist/packages/vaultline-crypto/src/recipients.js +129 -0
- package/dist/packages/vaultline-crypto/src/sha256-stream.d.ts +41 -0
- package/dist/packages/vaultline-crypto/src/sha256-stream.js +206 -0
- package/dist/packages/vaultline-crypto/src/stream.d.ts +139 -0
- package/dist/packages/vaultline-crypto/src/stream.js +477 -0
- package/dist/site/index.html +1542 -0
- package/dist/site.zip +0 -0
- package/dist/src/activity.d.ts +22 -0
- package/dist/src/activity.js +52 -0
- package/dist/src/adapters.d.ts +212 -0
- package/dist/src/adapters.js +533 -0
- package/dist/src/audit.d.ts +24 -0
- package/dist/src/audit.js +41 -0
- package/dist/src/autopilot.d.ts +77 -0
- package/dist/src/autopilot.js +148 -0
- package/dist/src/bip39-wordlist.d.ts +15 -0
- package/dist/src/bip39-wordlist.js +272 -0
- package/dist/src/branding.d.ts +31 -0
- package/dist/src/branding.js +31 -0
- package/dist/src/chunk-store.d.ts +142 -0
- package/dist/src/chunk-store.js +502 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +2035 -0
- package/dist/src/cloud.d.ts +434 -0
- package/dist/src/cloud.js +851 -0
- package/dist/src/control-plane/auth.d.ts +62 -0
- package/dist/src/control-plane/auth.js +123 -0
- package/dist/src/control-plane/server.d.ts +31 -0
- package/dist/src/control-plane/server.js +263 -0
- package/dist/src/control-plane/store.d.ts +101 -0
- package/dist/src/control-plane/store.js +82 -0
- package/dist/src/control-plane-cli.d.ts +2 -0
- package/dist/src/control-plane-cli.js +37 -0
- package/dist/src/control-plane-server.d.ts +10 -0
- package/dist/src/control-plane-server.js +11 -0
- package/dist/src/control-plane.d.ts +78 -0
- package/dist/src/control-plane.js +61 -0
- package/dist/src/crypto.d.ts +56 -0
- package/dist/src/crypto.js +132 -0
- package/dist/src/daemon.d.ts +52 -0
- package/dist/src/daemon.js +142 -0
- package/dist/src/dashboard-cli.d.ts +2 -0
- package/dist/src/dashboard-cli.js +20 -0
- package/dist/src/disk.d.ts +110 -0
- package/dist/src/disk.js +169 -0
- package/dist/src/doctor.d.ts +11 -0
- package/dist/src/doctor.js +198 -0
- package/dist/src/enroll.d.ts +27 -0
- package/dist/src/enroll.js +136 -0
- package/dist/src/errors.d.ts +26 -0
- package/dist/src/errors.js +23 -0
- package/dist/src/heartbeat.d.ts +89 -0
- package/dist/src/heartbeat.js +120 -0
- package/dist/src/index-sync.d.ts +53 -0
- package/dist/src/index-sync.js +147 -0
- package/dist/src/leakscan.d.ts +48 -0
- package/dist/src/leakscan.js +222 -0
- package/dist/src/local-api.d.ts +132 -0
- package/dist/src/local-api.js +1757 -0
- package/dist/src/managed-chunks.d.ts +55 -0
- package/dist/src/managed-chunks.js +108 -0
- package/dist/src/mcp-install.d.ts +52 -0
- package/dist/src/mcp-install.js +140 -0
- package/dist/src/mcp.d.ts +1 -0
- package/dist/src/mcp.js +59 -0
- package/dist/src/migrate.d.ts +35 -0
- package/dist/src/migrate.js +88 -0
- package/dist/src/mnemonic.d.ts +60 -0
- package/dist/src/mnemonic.js +134 -0
- package/dist/src/net.d.ts +2 -0
- package/dist/src/net.js +16 -0
- package/dist/src/notify.d.ts +46 -0
- package/dist/src/notify.js +84 -0
- package/dist/src/offload.d.ts +117 -0
- package/dist/src/offload.js +331 -0
- package/dist/src/onboarding.d.ts +10 -0
- package/dist/src/onboarding.js +44 -0
- package/dist/src/packages.d.ts +126 -0
- package/dist/src/packages.js +114 -0
- package/dist/src/passkey.d.ts +26 -0
- package/dist/src/passkey.js +54 -0
- package/dist/src/password-lock.d.ts +19 -0
- package/dist/src/password-lock.js +156 -0
- package/dist/src/paths.d.ts +9 -0
- package/dist/src/paths.js +24 -0
- package/dist/src/providers/gcs.d.ts +133 -0
- package/dist/src/providers/gcs.js +235 -0
- package/dist/src/providers/gdrive.d.ts +156 -0
- package/dist/src/providers/gdrive.js +335 -0
- package/dist/src/providers/index.d.ts +45 -0
- package/dist/src/providers/index.js +74 -0
- package/dist/src/providers/s3.d.ts +174 -0
- package/dist/src/providers/s3.js +345 -0
- package/dist/src/providers/sigv4.d.ts +78 -0
- package/dist/src/providers/sigv4.js +112 -0
- package/dist/src/queue.d.ts +185 -0
- package/dist/src/queue.js +286 -0
- package/dist/src/recovery.d.ts +40 -0
- package/dist/src/recovery.js +132 -0
- package/dist/src/rehydrate.d.ts +43 -0
- package/dist/src/rehydrate.js +66 -0
- package/dist/src/restore.d.ts +34 -0
- package/dist/src/restore.js +80 -0
- package/dist/src/retention.d.ts +251 -0
- package/dist/src/retention.js +446 -0
- package/dist/src/rotate.d.ts +47 -0
- package/dist/src/rotate.js +95 -0
- package/dist/src/search.d.ts +147 -0
- package/dist/src/search.js +677 -0
- package/dist/src/secrets.d.ts +86 -0
- package/dist/src/secrets.js +220 -0
- package/dist/src/service.d.ts +73 -0
- package/dist/src/service.js +197 -0
- package/dist/src/share.d.ts +34 -0
- package/dist/src/share.js +68 -0
- package/dist/src/spool.d.ts +97 -0
- package/dist/src/spool.js +213 -0
- package/dist/src/start-tui.d.ts +17 -0
- package/dist/src/start-tui.js +113 -0
- package/dist/src/start.d.ts +75 -0
- package/dist/src/start.js +101 -0
- package/dist/src/storage-setup.d.ts +49 -0
- package/dist/src/storage-setup.js +222 -0
- package/dist/src/storage-targets.d.ts +40 -0
- package/dist/src/storage-targets.js +147 -0
- package/dist/src/stream-to-cloud.d.ts +76 -0
- package/dist/src/stream-to-cloud.js +820 -0
- package/dist/src/sync-rules.d.ts +85 -0
- package/dist/src/sync-rules.js +125 -0
- package/dist/src/trash.d.ts +15 -0
- package/dist/src/trash.js +63 -0
- package/dist/src/tui.d.ts +18 -0
- package/dist/src/tui.js +179 -0
- package/dist/src/types.d.ts +191 -0
- package/dist/src/types.js +3 -0
- package/dist/src/ui-server.d.ts +187 -0
- package/dist/src/ui-server.js +293 -0
- package/dist/src/ui.d.ts +41 -0
- package/dist/src/ui.js +102 -0
- package/dist/src/update.d.ts +30 -0
- package/dist/src/update.js +56 -0
- package/dist/src/upload.d.ts +46 -0
- package/dist/src/upload.js +80 -0
- package/dist/src/vault.d.ts +208 -0
- package/dist/src/vault.js +812 -0
- package/dist/src/watcher.d.ts +34 -0
- package/dist/src/watcher.js +121 -0
- package/dist/src/worker.d.ts +52 -0
- package/dist/src/worker.js +190 -0
- package/package.json +65 -0
- package/web/app.js +1372 -0
- package/web/index.html +476 -0
- package/web/rail.js +308 -0
- package/web/retention.html +17 -0
- package/web/rules-view.js +249 -0
- package/web/sessions-view.js +448 -0
- package/web/sessions.html +17 -0
- package/web/setup-api.js +181 -0
- package/web/setup-logic.js +394 -0
- package/web/setup.html +419 -0
- package/web/setup.js +697 -0
- package/web/style.css +990 -0
package/web/setup.js
ADDED
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sealkeep first run.
|
|
3
|
+
*
|
|
4
|
+
* Five screens, in the order the decisions depend on each other: what this
|
|
5
|
+
* does, how archives are stored, where they go, the key, and what is now
|
|
6
|
+
* running. The vault is created on screen 4 and everything else is applied
|
|
7
|
+
* after it, which is what makes "stop once your key exists and you still have
|
|
8
|
+
* a working local-only vault" true rather than aspirational.
|
|
9
|
+
*
|
|
10
|
+
* The phrase lives in exactly one variable in this file and is written to at
|
|
11
|
+
* most one of two lists — the ceremony's, or the fold a password vault offers
|
|
12
|
+
* — and all of it is cleared the moment the last screen appears. It is never
|
|
13
|
+
* stored, never re-requested, and never put anywhere a URL, a log or a
|
|
14
|
+
* browser's session restore could reach. The password is never kept at all:
|
|
15
|
+
* it is read from its field, sent once, and the field is emptied.
|
|
16
|
+
*/
|
|
17
|
+
import * as api from "./setup-api.js";
|
|
18
|
+
import {
|
|
19
|
+
buildStoragePayload, checkPhraseAnswers, doneHeadline, INTENTS, intentFor,
|
|
20
|
+
keyMode, needsBucket, ownProviders, passwordHint, passwordProblem,
|
|
21
|
+
phraseProblem, provider, summariseDone, validateStorage, verificationPositions
|
|
22
|
+
} from "./setup-logic.js";
|
|
23
|
+
|
|
24
|
+
const $ = (id) => document.getElementById(id);
|
|
25
|
+
const TOTAL_STEPS = 5;
|
|
26
|
+
|
|
27
|
+
/** Everything the wizard has been told, none of it secret. */
|
|
28
|
+
const draft = {
|
|
29
|
+
/** Made once at creation. Only the literal "plain" changes anything. */
|
|
30
|
+
storageMode: "sealed",
|
|
31
|
+
providerId: "local",
|
|
32
|
+
/** The top-level answer. The provider is only a detail underneath one of them. */
|
|
33
|
+
intent: "local",
|
|
34
|
+
storage: {},
|
|
35
|
+
/** Held until the vault exists: linking needs somewhere to keep the token. */
|
|
36
|
+
enrollEmail: "",
|
|
37
|
+
enrollCode: "",
|
|
38
|
+
vault: null,
|
|
39
|
+
/** Which ceremony made the vault, so re-entering screen 4 redraws the right half. */
|
|
40
|
+
keyBranch: null,
|
|
41
|
+
createdAlready: false
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** The one secret this page ever holds. Cleared by `forgetPhrase()`. */
|
|
45
|
+
let phraseWords = null;
|
|
46
|
+
let verifyPositions = [];
|
|
47
|
+
let confirmed = false;
|
|
48
|
+
let step = 1;
|
|
49
|
+
|
|
50
|
+
/* ── Chrome ──────────────────────────────────────────────────────────── */
|
|
51
|
+
|
|
52
|
+
function setStatus(text, tone) {
|
|
53
|
+
const pill = $("connection");
|
|
54
|
+
pill.textContent = text;
|
|
55
|
+
pill.className = `pill ${tone ?? ""}`.trim();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function stepButtons() { return [...document.querySelectorAll("nav.steps .step")]; }
|
|
59
|
+
|
|
60
|
+
function paintRail() {
|
|
61
|
+
for (const button of stepButtons()) {
|
|
62
|
+
const index = Number(button.dataset.step);
|
|
63
|
+
const done = index < step;
|
|
64
|
+
button.classList.toggle("current", index === step);
|
|
65
|
+
button.classList.toggle("done", done);
|
|
66
|
+
// Once the vault exists the earlier screens are history: their answers are
|
|
67
|
+
// already committed or about to be, and stepping back is a way to lose the
|
|
68
|
+
// phrase — the fold's copy of it included.
|
|
69
|
+
const reachable = done && !phraseWords && !(step === TOTAL_STEPS);
|
|
70
|
+
button.classList.toggle("locked", !reachable && index !== step);
|
|
71
|
+
button.disabled = !reachable;
|
|
72
|
+
if (index === step) button.setAttribute("aria-current", "step");
|
|
73
|
+
else button.removeAttribute("aria-current");
|
|
74
|
+
button.querySelector("span").textContent = done ? "✓" : String(index);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function show(next) {
|
|
79
|
+
step = next;
|
|
80
|
+
for (let index = 1; index <= TOTAL_STEPS; index += 1) $(`screen-${index}`).hidden = index !== next;
|
|
81
|
+
$("screen-unavailable").hidden = true;
|
|
82
|
+
$("screen-gate").hidden = true;
|
|
83
|
+
paintRail();
|
|
84
|
+
const heading = $(`screen-${next}`).querySelector("h1");
|
|
85
|
+
if (heading && typeof heading.focus === "function") { heading.tabIndex = -1; heading.focus(); }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Asks this browser for the token, rather than telling it to go and be a
|
|
90
|
+
* different browser.
|
|
91
|
+
*
|
|
92
|
+
* `sealkeep ui` signs in whichever browser it opened, by way of a fragment
|
|
93
|
+
* that lands in that browser's sessionStorage. Anyone who then types the
|
|
94
|
+
* address into a second one — a perfectly reasonable thing to do with a URL
|
|
95
|
+
* that is now stable and memorable — arrives with nothing, and the old advice
|
|
96
|
+
* was a terminal command that would have opened the first browser again.
|
|
97
|
+
*/
|
|
98
|
+
function showGate(message) {
|
|
99
|
+
for (let index = 1; index <= TOTAL_STEPS; index += 1) $(`screen-${index}`).hidden = true;
|
|
100
|
+
$("screen-unavailable").hidden = true;
|
|
101
|
+
$("screen-gate").hidden = false;
|
|
102
|
+
setError("gate-error", message ?? "");
|
|
103
|
+
for (const button of stepButtons()) { button.disabled = true; button.classList.add("locked"); }
|
|
104
|
+
setStatus("Not signed in", "warn");
|
|
105
|
+
$("gate-token").focus();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function showUnavailable(message) {
|
|
109
|
+
for (let index = 1; index <= TOTAL_STEPS; index += 1) $(`screen-${index}`).hidden = true;
|
|
110
|
+
$("screen-gate").hidden = true;
|
|
111
|
+
$("screen-unavailable").hidden = false;
|
|
112
|
+
$("unavailable-detail").textContent = message;
|
|
113
|
+
for (const button of stepButtons()) { button.disabled = true; button.classList.add("locked"); }
|
|
114
|
+
setStatus("Setup unavailable", "warn");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const el = (tag, className, text) => {
|
|
118
|
+
const node = document.createElement(tag);
|
|
119
|
+
if (className) node.className = className;
|
|
120
|
+
if (text !== undefined) node.textContent = text;
|
|
121
|
+
return node;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const badgeNode = (badge) => el("span", `badge badge-${badge.kind}`, badge.text);
|
|
125
|
+
|
|
126
|
+
function setError(id, message) {
|
|
127
|
+
const node = $(id);
|
|
128
|
+
node.textContent = message ?? "";
|
|
129
|
+
node.hidden = !message;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* ── 2 · How archives are stored ─────────────────────────────────────── */
|
|
133
|
+
|
|
134
|
+
// The radios are static markup — two fixed answers need no renderer — so the
|
|
135
|
+
// only work is keeping the draft honest about which one is pressed.
|
|
136
|
+
for (const radio of document.querySelectorAll('#mode-choices input[name="storageMode"]')) {
|
|
137
|
+
radio.onchange = () => { if (radio.checked) draft.storageMode = radio.value === "plain" ? "plain" : "sealed"; };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* ── 3 · Storage ─────────────────────────────────────────────────────── */
|
|
141
|
+
|
|
142
|
+
/** The decision: three answers, no vendors. */
|
|
143
|
+
function renderProviders() {
|
|
144
|
+
const choices = $("provider-choices");
|
|
145
|
+
choices.replaceChildren(choices.querySelector("legend") ?? el("legend", "sr-only", "Where archives go"));
|
|
146
|
+
|
|
147
|
+
for (const entry of INTENTS) {
|
|
148
|
+
const card = el("label", "choice");
|
|
149
|
+
const radio = document.createElement("input");
|
|
150
|
+
radio.type = "radio";
|
|
151
|
+
radio.name = "intent";
|
|
152
|
+
radio.value = entry.id;
|
|
153
|
+
radio.id = `i-${entry.id}`;
|
|
154
|
+
radio.checked = entry.id === draft.intent;
|
|
155
|
+
radio.onchange = () => {
|
|
156
|
+
draft.intent = entry.id;
|
|
157
|
+
// Each answer carries its own default provider. Choosing "my own bucket"
|
|
158
|
+
// does not choose a vendor, so the sub-list below opens unanswered rather
|
|
159
|
+
// than silently picking one on the person's behalf.
|
|
160
|
+
draft.providerId = entry.id === "own" ? (intentFor(draft.providerId) === "own" ? draft.providerId : "") : entry.id;
|
|
161
|
+
renderProviders();
|
|
162
|
+
renderOwnProviders();
|
|
163
|
+
renderProviderFields();
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const body = el("div", "choice-body");
|
|
167
|
+
body.append(el("b", null, entry.label), el("p", "note", entry.blurb), el("p", "note", entry.detail));
|
|
168
|
+
card.append(radio, body);
|
|
169
|
+
choices.append(card);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The detail: which bucket, shown only to someone who asked for one. */
|
|
174
|
+
function renderOwnProviders() {
|
|
175
|
+
const holder = $("own-providers");
|
|
176
|
+
holder.hidden = draft.intent !== "own";
|
|
177
|
+
holder.replaceChildren();
|
|
178
|
+
if (holder.hidden) return;
|
|
179
|
+
|
|
180
|
+
holder.append(el("p", "note", "Which one is yours?"));
|
|
181
|
+
for (const entry of ownProviders()) {
|
|
182
|
+
const card = el("label", "choice");
|
|
183
|
+
const radio = document.createElement("input");
|
|
184
|
+
radio.type = "radio";
|
|
185
|
+
radio.name = "provider";
|
|
186
|
+
radio.value = entry.id;
|
|
187
|
+
radio.id = `p-${entry.id}`;
|
|
188
|
+
radio.checked = entry.id === draft.providerId;
|
|
189
|
+
radio.onchange = () => { draft.providerId = entry.id; renderOwnProviders(); renderProviderFields(); };
|
|
190
|
+
|
|
191
|
+
const body = el("div", "choice-body");
|
|
192
|
+
body.append(el("b", null, entry.label), el("p", "note", entry.blurb));
|
|
193
|
+
card.append(radio, body);
|
|
194
|
+
holder.append(card);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function renderProviderFields() {
|
|
199
|
+
const form = $("storage-form");
|
|
200
|
+
// Managed storage is remote and has no form: the bucket is ours to provision.
|
|
201
|
+
// Neither has "my own bucket" until a vendor is named — the fields differ per
|
|
202
|
+
// provider, so there is nothing truthful to draw yet.
|
|
203
|
+
form.hidden = !needsBucket(draft.providerId) || !provider(draft.providerId);
|
|
204
|
+
// The account panel is the managed equivalent of the credentials form — the
|
|
205
|
+
// one thing that choice does need from the person.
|
|
206
|
+
$("account-panel").hidden = draft.intent !== "managed";
|
|
207
|
+
setError("storage-error", "");
|
|
208
|
+
setError("e-bucket", "");
|
|
209
|
+
setError("e-prefix", "");
|
|
210
|
+
if (form.hidden) return;
|
|
211
|
+
|
|
212
|
+
const entry = provider(draft.providerId);
|
|
213
|
+
$("storage-form-title").textContent = `${entry.label} — bucket and credentials`;
|
|
214
|
+
|
|
215
|
+
const holder = $("provider-fields");
|
|
216
|
+
holder.replaceChildren();
|
|
217
|
+
for (const field of entry.fields) {
|
|
218
|
+
const wrap = el("div", "field");
|
|
219
|
+
const label = el("label", null, field.label);
|
|
220
|
+
label.htmlFor = `f-${field.name}`;
|
|
221
|
+
|
|
222
|
+
const input = document.createElement(field.multiline ? "textarea" : "input");
|
|
223
|
+
input.id = `f-${field.name}`;
|
|
224
|
+
input.name = field.name;
|
|
225
|
+
if (!field.multiline) input.type = field.secret ? "password" : "text";
|
|
226
|
+
else input.rows = 4;
|
|
227
|
+
input.placeholder = field.placeholder ?? "";
|
|
228
|
+
input.autocomplete = "off";
|
|
229
|
+
input.spellcheck = false;
|
|
230
|
+
input.value = draft.storage[field.name] ?? "";
|
|
231
|
+
input.oninput = () => { draft.storage[field.name] = input.value; };
|
|
232
|
+
|
|
233
|
+
wrap.append(label, input);
|
|
234
|
+
if (field.hint) wrap.append(el("p", "note", field.hint));
|
|
235
|
+
wrap.append(Object.assign(el("p", "note error"), { id: `e-${field.name}`, hidden: true }));
|
|
236
|
+
holder.append(wrap);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Reads the two fixed fields, then the provider's own. Returns false and paints the problems. */
|
|
241
|
+
function collectStorage() {
|
|
242
|
+
draft.storage.bucket = $("f-bucket").value;
|
|
243
|
+
draft.storage.prefix = $("f-prefix").value;
|
|
244
|
+
const { ok, errors } = validateStorage(draft.providerId, draft.storage);
|
|
245
|
+
|
|
246
|
+
for (const field of ["bucket", "prefix", ...provider(draft.providerId).fields.map((entry) => entry.name)]) {
|
|
247
|
+
const node = $(`e-${field}`);
|
|
248
|
+
if (!node) continue;
|
|
249
|
+
node.textContent = errors[field] ?? "";
|
|
250
|
+
node.hidden = !errors[field];
|
|
251
|
+
}
|
|
252
|
+
setError("storage-error", ok ? "" : "Fix the fields marked above, or choose to keep archives on this disk for now.");
|
|
253
|
+
return ok;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Links this machine to an account someone already has.
|
|
258
|
+
*
|
|
259
|
+
* A code, not a password and not a recovery phrase: it grants quota, storage
|
|
260
|
+
* and settings, never the ability to open an archive. That distinction is what
|
|
261
|
+
* lets a returning user finish here instead of in a terminal without weakening
|
|
262
|
+
* the rule that no endpoint accepts a phrase.
|
|
263
|
+
*/
|
|
264
|
+
$("enroll-go").onclick = () => {
|
|
265
|
+
const email = $("f-enroll-email").value.trim();
|
|
266
|
+
const code = $("f-enroll-code").value.trim();
|
|
267
|
+
// Both, because the exchange needs the pair — the panel hands them over
|
|
268
|
+
// together, in the command it prints beside the code.
|
|
269
|
+
const missing = !email ? "Enter the email your account uses." : !code ? "Press “Show a pairing code” on your account page, then paste it here." : "";
|
|
270
|
+
setError("e-enroll-code", missing);
|
|
271
|
+
if (missing) return;
|
|
272
|
+
|
|
273
|
+
// Held, not sent. Linking stores a token, and there is nowhere to keep it
|
|
274
|
+
// until the vault exists — which happens at step 4, after this. The code is
|
|
275
|
+
// single use, so spending it here would burn it on a machine that could not
|
|
276
|
+
// have kept the result.
|
|
277
|
+
draft.enrollEmail = email;
|
|
278
|
+
draft.enrollCode = code;
|
|
279
|
+
// Not "Saved" — nothing has been checked yet. The pairing code is single-use
|
|
280
|
+
// and is only sent when the vault is created, so a wrong or expired one shows
|
|
281
|
+
// up on the last screen (where the account row now reports it), not here.
|
|
282
|
+
$("enroll-result").textContent = "Got it — this machine links to your account when the vault is created at the end. If the code is wrong, you'll see it on the last screen.";
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Proves the bucket works before anyone trusts an archive to it.
|
|
287
|
+
*
|
|
288
|
+
* Nothing is saved by this: the credential is sent, used for one small write
|
|
289
|
+
* and read, and dropped. That is deliberate — a test that stored the target
|
|
290
|
+
* would leave a broken configuration behind on the attempt that proved it
|
|
291
|
+
* broken.
|
|
292
|
+
*/
|
|
293
|
+
$("storage-test").onclick = async () => {
|
|
294
|
+
const result = $("storage-test-result");
|
|
295
|
+
if (!collectStorage()) { result.textContent = ""; return; }
|
|
296
|
+
|
|
297
|
+
$("storage-test").disabled = true;
|
|
298
|
+
result.className = "note";
|
|
299
|
+
result.textContent = "Writing a small file, then reading it back…";
|
|
300
|
+
try {
|
|
301
|
+
const outcome = await api.testStorage({ ...buildStoragePayload(draft.providerId, draft.storage), confirm: true });
|
|
302
|
+
result.className = outcome.ok ? "note" : "note error";
|
|
303
|
+
result.textContent = outcome.detail;
|
|
304
|
+
} catch (error) {
|
|
305
|
+
result.className = "note error";
|
|
306
|
+
result.textContent = error.message;
|
|
307
|
+
} finally {
|
|
308
|
+
$("storage-test").disabled = false;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
/* ── 4 · Your key ────────────────────────────────────────────────────── */
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Which half of the key screen is on show.
|
|
316
|
+
*
|
|
317
|
+
* Before the vault exists the destination decides the ceremony; after it
|
|
318
|
+
* exists the ceremony that ran decides, because the intent radio could in
|
|
319
|
+
* principle change while the created vault cannot. An existing vault outranks
|
|
320
|
+
* both: it gets neither a second phrase nor a password form this page has no
|
|
321
|
+
* endpoint to honour.
|
|
322
|
+
*/
|
|
323
|
+
function armKeyScreen() {
|
|
324
|
+
const made = Boolean(phraseWords);
|
|
325
|
+
const branch = draft.createdAlready ? "existing" : made ? draft.keyBranch : keyMode(draft.intent);
|
|
326
|
+
$("phrase-existing").hidden = branch !== "existing";
|
|
327
|
+
$("phrase-before").hidden = branch !== "phrase" || made;
|
|
328
|
+
$("phrase-after").hidden = branch !== "phrase" || !made;
|
|
329
|
+
$("password-before").hidden = branch !== "password" || made;
|
|
330
|
+
$("password-after").hidden = branch !== "password" || !made;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** The browser's own "leave site?" prompt, armed only while an unconfirmed phrase is on screen. */
|
|
334
|
+
const guardUnload = (event) => { event.preventDefault(); event.returnValue = ""; return ""; };
|
|
335
|
+
|
|
336
|
+
function armUnloadGuard() { if (typeof addEventListener === "function") addEventListener("beforeunload", guardUnload); }
|
|
337
|
+
function disarmUnloadGuard() { if (typeof removeEventListener === "function") removeEventListener("beforeunload", guardUnload); }
|
|
338
|
+
|
|
339
|
+
/** One renderer for both places the words may appear, so neither can drift. */
|
|
340
|
+
function wordList(target, words) {
|
|
341
|
+
target.replaceChildren();
|
|
342
|
+
for (const [index, word] of words.entries()) {
|
|
343
|
+
const item = el("li", "word");
|
|
344
|
+
item.append(el("span", "ord", String(index + 1)), el("b", null, word));
|
|
345
|
+
target.append(item);
|
|
346
|
+
}
|
|
347
|
+
// Counted from the phrase in hand, never written as a literal — the same
|
|
348
|
+
// rule as the lede below, and for the same reason.
|
|
349
|
+
target.setAttribute("aria-label", `Your ${words.length}-word recovery phrase`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function renderPhrase(words) {
|
|
353
|
+
wordList($("phrase-words"), words);
|
|
354
|
+
|
|
355
|
+
verifyPositions = verificationPositions(words.length);
|
|
356
|
+
const fields = $("verify-fields");
|
|
357
|
+
fields.replaceChildren();
|
|
358
|
+
for (const position of verifyPositions) {
|
|
359
|
+
const wrap = el("div", "field");
|
|
360
|
+
const label = el("label", null, `Word ${position}`);
|
|
361
|
+
label.htmlFor = `v-${position}`;
|
|
362
|
+
const input = document.createElement("input");
|
|
363
|
+
input.type = "text";
|
|
364
|
+
input.id = `v-${position}`;
|
|
365
|
+
input.autocomplete = "off";
|
|
366
|
+
input.spellcheck = false;
|
|
367
|
+
input.oninput = gatePhrase;
|
|
368
|
+
wrap.append(label, input);
|
|
369
|
+
fields.append(wrap);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
armKeyScreen();
|
|
373
|
+
// Counted from the phrase in hand, never written as a literal. The wizard
|
|
374
|
+
// said "17 words" for a day after the phrase became 24, so a person was told
|
|
375
|
+
// to write down seventeen while twenty-four sat on the screen — at the exact
|
|
376
|
+
// moment they had been told there is no reset and no support ticket. A number
|
|
377
|
+
// that can disagree with the thing it describes should not be typed twice.
|
|
378
|
+
$("phrase-lede").textContent = `These ${words.length} words are the only key to everything Sealkeep will seal here. Write them down now — this page will not show them twice.`;
|
|
379
|
+
armUnloadGuard();
|
|
380
|
+
paintRail();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* The fold's version of the words: offered, not imposed, and never guarded.
|
|
385
|
+
*
|
|
386
|
+
* No unload guard here, deliberately — the password already opens this vault,
|
|
387
|
+
* so leaving without the words costs a fallback, not the archives, and a
|
|
388
|
+
* browser prompt insisting otherwise would be a lie.
|
|
389
|
+
*/
|
|
390
|
+
function renderFold(words) {
|
|
391
|
+
wordList($("fold-phrase-words"), words);
|
|
392
|
+
$("fold-note").textContent = `These ${words.length} words also open the vault — write them down or skip. Skipping is fine: your password already opens it on this machine. The words are the only key that works anywhere else, and this is the only time they are shown.`;
|
|
393
|
+
armKeyScreen();
|
|
394
|
+
paintRail();
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** Continue stays shut until the words are transcribed and the sentence is ticked. */
|
|
398
|
+
function gatePhrase() {
|
|
399
|
+
const answers = verifyPositions.map((position) => $(`v-${position}`).value);
|
|
400
|
+
const typed = answers.some((value) => value.trim().length > 0);
|
|
401
|
+
const check = checkPhraseAnswers(phraseWords ?? [], verifyPositions, answers);
|
|
402
|
+
confirmed = check.ok && $("f-understood").checked;
|
|
403
|
+
|
|
404
|
+
setError("verify-error", typed && !check.ok ? phraseProblem(check.wrong) : "");
|
|
405
|
+
$("confirm-note").textContent = confirmed
|
|
406
|
+
? "Ready. Finishing clears these words from the page for good."
|
|
407
|
+
: check.ok
|
|
408
|
+
? "The words match. Tick the box below and you are through."
|
|
409
|
+
: "Continue unlocks once both of those are true.";
|
|
410
|
+
$("finish").disabled = !confirmed;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** After this returns, the phrase does not exist anywhere in this page. */
|
|
414
|
+
function forgetPhrase() {
|
|
415
|
+
phraseWords = null;
|
|
416
|
+
verifyPositions = [];
|
|
417
|
+
$("phrase-words").replaceChildren();
|
|
418
|
+
$("fold-phrase-words").replaceChildren();
|
|
419
|
+
$("fold-note").textContent = "";
|
|
420
|
+
$("verify-fields").replaceChildren();
|
|
421
|
+
disarmUnloadGuard();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/* ── Committing ──────────────────────────────────────────────────────── */
|
|
425
|
+
|
|
426
|
+
async function createVault({ password } = {}) {
|
|
427
|
+
const errorId = password === undefined ? "vault-error" : "password-error";
|
|
428
|
+
const buttons = [$("generate"), $("create-with-password")];
|
|
429
|
+
setError(errorId, "");
|
|
430
|
+
for (const button of buttons) button.disabled = true;
|
|
431
|
+
setStatus("Creating the vault", "busy");
|
|
432
|
+
try {
|
|
433
|
+
const created = await api.createVault({ password, storageMode: draft.storageMode });
|
|
434
|
+
// `recoveryPhrase` is the server's field name. This line once read a
|
|
435
|
+
// shorter field of the stub's invention, which did not exist on the wire —
|
|
436
|
+
// so the vault was created, the words came back, and the one showing of
|
|
437
|
+
// the phrase silently never happened, while the error path promised a
|
|
438
|
+
// reload would mint a fresh vault that the existing one made impossible.
|
|
439
|
+
// The shape is pinned by test now; do not "tolerate" both names — one
|
|
440
|
+
// canonical field, and a loud failure if it ever moves.
|
|
441
|
+
const words = String(created.recoveryPhrase ?? "").trim().split(/\s+/).filter(Boolean);
|
|
442
|
+
if (words.length === 0) {
|
|
443
|
+
// Two different disasters, two different truths. If the server confirmed
|
|
444
|
+
// a vault, it exists and its phrase can never be fetched again — saying
|
|
445
|
+
// "reload for a fresh phrase" would be false and costly. Only when
|
|
446
|
+
// nothing was created is starting over a real remedy.
|
|
447
|
+
throw new Error(created.vaultId
|
|
448
|
+
? "The vault was created, but Sealkeep could not display its recovery phrase — and it can never be shown again. Do not use this vault: nothing has been sealed in it, so delete the vault folder on this machine and run setup again for a fresh vault and a fresh phrase."
|
|
449
|
+
: "Sealkeep could not create the vault, so nothing exists yet and nothing is lost. Reload this page and try again.");
|
|
450
|
+
}
|
|
451
|
+
draft.vault = { dataDir: created.dataDir, vaultId: created.vaultId };
|
|
452
|
+
draft.keyBranch = password === undefined ? "phrase" : "password";
|
|
453
|
+
phraseWords = words;
|
|
454
|
+
if (draft.keyBranch === "phrase") {
|
|
455
|
+
renderPhrase(words);
|
|
456
|
+
setStatus("Vault created — write the phrase down", "ok");
|
|
457
|
+
} else {
|
|
458
|
+
// The password did its one job; keeping it in a field is keeping a
|
|
459
|
+
// secret in a place session restore can resurrect.
|
|
460
|
+
$("f-password").value = "";
|
|
461
|
+
$("f-password-again").value = "";
|
|
462
|
+
renderFold(words);
|
|
463
|
+
setStatus("Vault created — it opens with your password", "ok");
|
|
464
|
+
}
|
|
465
|
+
} catch (error) {
|
|
466
|
+
for (const button of buttons) button.disabled = false;
|
|
467
|
+
setError(errorId, error.message);
|
|
468
|
+
// Neutral on purpose: one of the two failure branches above happens after
|
|
469
|
+
// the vault exists, and a pill insisting "not created" there was a lie.
|
|
470
|
+
setStatus("Setup could not finish this step", "warn");
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Everything after the vault, in dependency order, each failure survivable.
|
|
476
|
+
*
|
|
477
|
+
* A bucket that will not answer must not undo the vault — so nothing here
|
|
478
|
+
* throws out of the function; it all lands on the last screen as a row that
|
|
479
|
+
* says what is true.
|
|
480
|
+
*/
|
|
481
|
+
async function commitRest() {
|
|
482
|
+
const result = {
|
|
483
|
+
vault: draft.vault ?? {},
|
|
484
|
+
// Unknown for a vault made before this page ran: guessing a mode on the
|
|
485
|
+
// summary would be inventing history.
|
|
486
|
+
storageMode: draft.createdAlready ? null : draft.storageMode,
|
|
487
|
+
password: null,
|
|
488
|
+
storage: null,
|
|
489
|
+
service: null
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
// Enrolment first, because it is what tells this machine it has managed
|
|
493
|
+
// storage at all: the account's own settings arrive with it.
|
|
494
|
+
if (draft.enrollCode) {
|
|
495
|
+
try {
|
|
496
|
+
const enrolled = await api.enrollDevice(draft.enrollEmail, draft.enrollCode);
|
|
497
|
+
result.account = { plan: enrolled.account?.plan ?? "unknown", deviceId: enrolled.deviceId };
|
|
498
|
+
} catch (error) {
|
|
499
|
+
result.account = { error: error.message };
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (needsBucket(draft.providerId)) {
|
|
504
|
+
const payload = buildStoragePayload(draft.providerId, draft.storage);
|
|
505
|
+
try {
|
|
506
|
+
const saved = await api.saveStorage(payload);
|
|
507
|
+
result.storage = { provider: payload.provider, bucket: payload.bucket, prefix: payload.prefix, verified: saved.verified !== false };
|
|
508
|
+
} catch (error) {
|
|
509
|
+
result.storage = { provider: payload.provider, bucket: payload.bucket, prefix: payload.prefix, error: error.message };
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Asked, not assumed: the server refuses to fail the create over a lock it
|
|
514
|
+
// could not write, so whether the password really took is only knowable by
|
|
515
|
+
// asking the machine afterwards.
|
|
516
|
+
if (draft.keyBranch === "password") {
|
|
517
|
+
try {
|
|
518
|
+
const check = await api.fetchUnlockCheck();
|
|
519
|
+
result.password = { locked: check.passwordLock === true };
|
|
520
|
+
} catch {
|
|
521
|
+
result.password = { locked: null };
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
try {
|
|
526
|
+
const service = await api.startService();
|
|
527
|
+
result.service = { running: service.running !== false, detail: service.detail };
|
|
528
|
+
} catch (error) {
|
|
529
|
+
result.service = { running: false, error: error.message };
|
|
530
|
+
}
|
|
531
|
+
return result;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function renderDone(result) {
|
|
535
|
+
const rows = summariseDone(result);
|
|
536
|
+
const headline = doneHeadline(rows);
|
|
537
|
+
$("done-headline").textContent = headline;
|
|
538
|
+
const list = $("done-list");
|
|
539
|
+
list.replaceChildren();
|
|
540
|
+
for (const row of rows) {
|
|
541
|
+
const line = el("div", "done-row");
|
|
542
|
+
const head = el("div", "agent-head");
|
|
543
|
+
head.append(el("b", null, row.label), badgeNode(row.badge));
|
|
544
|
+
line.append(head, el("p", "note", row.detail));
|
|
545
|
+
list.append(line);
|
|
546
|
+
}
|
|
547
|
+
const offMachine = (result.storage && !result.storage.error) || (result.account && !result.account.error);
|
|
548
|
+
$("next-detail").textContent = offMachine
|
|
549
|
+
? "Nothing else is needed. The next time one of your agents finishes a session, the transcript is queued here, archived, and copied to your storage. It shows up on the dashboard within a few minutes."
|
|
550
|
+
: "Nothing else is needed. The next time one of your agents finishes a session, the transcript is queued here and archived onto this disk. It shows up on the dashboard within a few minutes.";
|
|
551
|
+
// A password vault may have skipped the words, and that was allowed out
|
|
552
|
+
// loud — so the advice bends rather than scolding.
|
|
553
|
+
$("today-key").textContent = result.password
|
|
554
|
+
? "If you kept the recovery phrase, put it somewhere you will still have it in a year — a drawer beats a laptop."
|
|
555
|
+
: "Check that your recovery phrase is somewhere you will still have it in a year — a drawer beats a laptop.";
|
|
556
|
+
return headline;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async function finish() {
|
|
560
|
+
for (const id of ["finish", "finish-existing", "finish-password"]) {
|
|
561
|
+
const button = $(id);
|
|
562
|
+
if (button) button.disabled = true;
|
|
563
|
+
}
|
|
564
|
+
setStatus("Finishing setup", "busy");
|
|
565
|
+
// The phrase goes before the network does: whatever happens to the bucket or
|
|
566
|
+
// the service, it is already out of this page and cannot be shown again.
|
|
567
|
+
forgetPhrase();
|
|
568
|
+
const result = await commitRest();
|
|
569
|
+
const headline = renderDone(result);
|
|
570
|
+
show(5);
|
|
571
|
+
setStatus(headline === "Sealkeep is running" ? "Done" : "Done, with something left to look at", "ok");
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/* ── Navigation ──────────────────────────────────────────────────────── */
|
|
575
|
+
|
|
576
|
+
function go(next) {
|
|
577
|
+
// Leaving the storage step: an unanswered sub-choice is its own error, and a
|
|
578
|
+
// named provider still has to have its fields filled in.
|
|
579
|
+
if (next === 4 && step === 3) {
|
|
580
|
+
if (draft.intent === "own" && !provider(draft.providerId)) {
|
|
581
|
+
setError("storage-error", "Choose which bucket is yours, or pick one of the other two answers above.");
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (needsBucket(draft.providerId) && !collectStorage()) return;
|
|
585
|
+
}
|
|
586
|
+
if (next === 4) armKeyScreen();
|
|
587
|
+
show(next);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
for (const button of document.querySelectorAll("[data-go]")) {
|
|
591
|
+
button.onclick = () => go(Number(button.dataset.go));
|
|
592
|
+
}
|
|
593
|
+
for (const button of stepButtons()) {
|
|
594
|
+
button.onclick = () => { if (!button.disabled) go(Number(button.dataset.step)); };
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
$("generate").onclick = () => void createVault();
|
|
598
|
+
$("f-understood").onchange = gatePhrase;
|
|
599
|
+
$("confirm-form").onsubmit = (event) => { event.preventDefault(); if (confirmed) void finish(); };
|
|
600
|
+
$("finish-existing").onclick = () => void finish();
|
|
601
|
+
$("finish-password").onclick = () => void finish();
|
|
602
|
+
|
|
603
|
+
// The password is judged as it is typed — by length, the one property the
|
|
604
|
+
// hint admits to caring about — and judged again on submit, so the create
|
|
605
|
+
// request only ever carries a password the schema will accept.
|
|
606
|
+
$("f-password").oninput = () => {
|
|
607
|
+
$("password-hint").textContent = passwordHint($("f-password").value.length);
|
|
608
|
+
setError("password-error", "");
|
|
609
|
+
};
|
|
610
|
+
$("f-password-again").oninput = () => setError("password-error", "");
|
|
611
|
+
$("password-form").onsubmit = (event) => {
|
|
612
|
+
event.preventDefault();
|
|
613
|
+
const password = $("f-password").value;
|
|
614
|
+
const problem = passwordProblem(password, $("f-password-again").value);
|
|
615
|
+
setError("password-error", problem);
|
|
616
|
+
if (problem) return;
|
|
617
|
+
void createVault({ password });
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
$("copy-phrase").onclick = async () => {
|
|
621
|
+
if (!phraseWords) return;
|
|
622
|
+
try {
|
|
623
|
+
await navigator.clipboard.writeText(phraseWords.join(" "));
|
|
624
|
+
$("copy-note").textContent = "Copied. A clipboard is not private and outlives this page — paste it, then copy something else.";
|
|
625
|
+
} catch {
|
|
626
|
+
$("copy-note").textContent = "This browser would not give the page your clipboard. Select the words above and copy them yourself.";
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
$("print-phrase").onclick = () => { if (typeof print === "function") print(); };
|
|
630
|
+
|
|
631
|
+
/* ── Boot ────────────────────────────────────────────────────────────── */
|
|
632
|
+
|
|
633
|
+
async function boot() {
|
|
634
|
+
api.readTokenFromLocation();
|
|
635
|
+
// Nothing works without it, so ask for it up front rather than letting every
|
|
636
|
+
// panel fail separately and leaving the reader to assemble the reason.
|
|
637
|
+
if (!api.hasToken()) { showGate(); return; }
|
|
638
|
+
try {
|
|
639
|
+
const state = await api.fetchState();
|
|
640
|
+
draft.createdAlready = Boolean(state.vault?.created);
|
|
641
|
+
if (draft.createdAlready) {
|
|
642
|
+
// The mode chooser collects an answer creation would use, and there is
|
|
643
|
+
// no creation left to use it: swap it for the truth.
|
|
644
|
+
$("mode-choices").hidden = true;
|
|
645
|
+
$("mode-existing").hidden = false;
|
|
646
|
+
$("existing-detail").textContent = state.vault?.createdAt
|
|
647
|
+
? `It was created on ${new Date(state.vault.createdAt).toLocaleDateString()}, and its recovery phrase was shown once at that moment.`
|
|
648
|
+
: "It was created earlier, and its recovery phrase was shown once at that moment.";
|
|
649
|
+
draft.vault = { dataDir: state.vault?.dataDir };
|
|
650
|
+
}
|
|
651
|
+
if (state.storage?.configured) {
|
|
652
|
+
draft.providerId = state.storage.provider ?? draft.providerId;
|
|
653
|
+
// Resumed as the answer it belongs to, or the screen shows "keep it
|
|
654
|
+
// here" ticked above a fully drawn R2 form.
|
|
655
|
+
draft.intent = intentFor(draft.providerId);
|
|
656
|
+
draft.storage.bucket = state.storage.bucket ?? "";
|
|
657
|
+
draft.storage.prefix = state.storage.prefix ?? "vaultline";
|
|
658
|
+
}
|
|
659
|
+
} catch (error) {
|
|
660
|
+
if (error.missing) {
|
|
661
|
+
showUnavailable(`Sealkeep answered on this machine, but it has no ${error.route} endpoint. This build predates the setup wizard.`);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
setStatus(error.message, "warn");
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
renderProviders();
|
|
668
|
+
renderOwnProviders();
|
|
669
|
+
renderProviderFields();
|
|
670
|
+
if ($("f-bucket").value === "") $("f-bucket").value = draft.storage.bucket ?? "";
|
|
671
|
+
show(1);
|
|
672
|
+
setStatus("Connected to this machine", "ok");
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* A pasted token is tried before it is trusted: a wrong one that merely got
|
|
677
|
+
* stored would put someone back on step 1 with every panel empty and no reason
|
|
678
|
+
* given, which is the failure this screen exists to end.
|
|
679
|
+
*/
|
|
680
|
+
$("gate-form").addEventListener("submit", async (event) => {
|
|
681
|
+
event.preventDefault();
|
|
682
|
+
const value = $("gate-token").value.trim();
|
|
683
|
+
if (!value) { setError("gate-error", "Paste the token from the file named above."); return; }
|
|
684
|
+
|
|
685
|
+
api.setToken(value);
|
|
686
|
+
try {
|
|
687
|
+
await api.fetchState();
|
|
688
|
+
} catch (error) {
|
|
689
|
+
api.setToken("");
|
|
690
|
+
setError("gate-error", error.message.includes("not signed in") ? "That token was not accepted." : error.message);
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
$("gate-token").value = "";
|
|
694
|
+
await boot();
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
void boot();
|