sealkeep 0.5.0 → 0.5.2

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/CHANGELOG.md CHANGED
@@ -3,6 +3,27 @@
3
3
  Notable changes, by published version. Sealkeep is pre-1.0: minor versions
4
4
  may change behavior, and say so here when they do.
5
5
 
6
+ ## 0.5.2 — 2026-08-19
7
+
8
+ - "Manage in cloud" now opens sealkeep.spala.ai — the panel URL was still
9
+ derived from the API origin, which the rename left behind. Self-hosted
10
+ planes keep deriving from their own origin.
11
+ - The cloud panel folds chunk families into one archive per row, edits the
12
+ account's storage routing (priorities, caps, project pins, Google Drive),
13
+ and gained a for-companies section.
14
+
15
+ ## 0.5.1 — 2026-08-19
16
+
17
+ The first release that lives entirely at its new addresses.
18
+
19
+ - Dashboard: the token gate probes once instead of eight times, so a first
20
+ visit no longer floods the console with 401s; every page carries the seal
21
+ favicon; panel and sign-in links point at sealkeep.spala.ai.
22
+ - Setup and token help name the real data folders — `~/.sealkeep`, or
23
+ `~/.vaultline` for vaults created before the rename.
24
+ - Google sign-in survives the old-domain bounce: the interim redirect at the
25
+ previous address forwards the OAuth code instead of dropping it.
26
+
6
27
  ## 0.5.0 — 2026-08-18
7
28
 
8
29
  **Vaultline is now Sealkeep** (by SPALA AI) — the old name is used by several
@@ -337,7 +337,7 @@
337
337
 
338
338
  <section>
339
339
  <h2>What it does</h2>
340
- <p class="sub">Three things, and it does them without ever seeing your work.</p>
340
+ <p class="sub">Five things, and it does them without ever seeing your work.</p>
341
341
 
342
342
  <div class="row-item duo">
343
343
  <div class="human">
@@ -365,8 +365,61 @@
365
365
  </div>
366
366
  <div class="machine"><div class="ev">ChaCha20-Poly1305 · per-archive key<br>key wrapped to your 24-word phrase<br><b>server sees ciphertext only</b></div></div>
367
367
  </div>
368
+
369
+ <div class="row-item duo">
370
+ <div class="human">
371
+ <span class="mark">Remember</span>
372
+ <h3>One memory across every machine</h3>
373
+ <p>The sealed index syncs beside your archives, so the laptop finds what the desktop sealed. Your agents search it too, over MCP — a collective memory of the project, not a folder on one disk.</p>
374
+ </div>
375
+ <div class="machine"><div class="ev">$ sealkeep search "why we forked the retry logic"<br><b>1 session</b> · sealed on your other machine<br>restore ✓ <b>byte-identical</b></div></div>
376
+ </div>
377
+
378
+ <div class="row-item duo">
379
+ <div class="human">
380
+ <span class="mark">Drive</span>
381
+ <h3>The Drive space you never use</h3>
382
+ <p>Point your agent history at the Google Drive storage you already pay for. One consent connects it; everything is sealed before upload, so Drive holds ciphertext. Cap it, pin projects to it, and let the rest go to managed cloud.</p>
383
+ </div>
384
+ <div class="machine"><div class="ev">$ sealkeep storage targets add gdrive --max-gb 50<br>connected · folder <b>Sealkeep</b> · cap 50 GB<br>project big-refactor → Drive · rest → cloud</div></div>
385
+ </div>
386
+
368
387
  </section>
369
388
 
389
+ <section id="teams"><div class="wrap">
390
+ <h2>For teams and companies</h2>
391
+ <p class="sub">Agent transcripts are source code, prompts, and pasted secrets. Treat them like it.</p>
392
+
393
+ <div class="row-item duo">
394
+ <div class="human">
395
+ <span class="mark">Custody</span>
396
+ <h3>Your data never changes hands</h3>
397
+ <p>Sealed before it leaves the machine, stored in a bucket you own if you choose, opened only by keys we never hold. Data residency is wherever you put the bucket. A secrets scan runs before anything is uploaded, and retention only ever stages to trash — there is no delete path.</p>
398
+ </div>
399
+ <div class="machine"><div class="ev">sealed on machine · keys stay yours<br>your bucket, your region, your retention<br>leak scan before every upload</div></div>
400
+ </div>
401
+
402
+ <div class="row-item duo">
403
+ <div class="human">
404
+ <span class="mark">Audit</span>
405
+ <h3>Every action, written down</h3>
406
+ <p>Each seal, upload, restore, and reclaim lands in a local audit log, and the control plane keeps its own account-scoped trail — identifiers and counts, never content. When someone asks what happened to a session, the answer is a query, not a shrug.</p>
407
+ </div>
408
+ <div class="machine"><div class="ev">$ sealkeep audit<br>sealed 9f2c1ab4 · uploaded · verified<br>restored by device macbook · 12 Aug</div></div>
409
+ </div>
410
+
411
+ <div class="row-item duo">
412
+ <div class="human">
413
+ <span class="mark">Share</span>
414
+ <h3>Hand a session over, not the vault</h3>
415
+ <p>One session goes to a teammate as a sealed bundle under a one-time passcode — your recovery phrase never travels. Team spaces — one sealed memory for a whole project, across people, with instant offboarding — are designed on the same envelope machinery and are next on the roadmap.</p>
416
+ </div>
417
+ <div class="machine"><div class="ev">$ sealkeep share 9f2c1ab4<br>bundle sealed · passcode shown once<br>opens without your phrase</div></div>
418
+ </div>
419
+
420
+ <p class="why">Straight talk: the encryption is built on published primitives with test vectors and a tamper suite, and has not yet had an independent audit. We say so here because your security review will ask.</p>
421
+ </div></section>
422
+
370
423
  <section id="plans">
371
424
  <h2>Two ways to run it</h2>
372
425
  <p class="sub">Same encryption either way. The only difference is which side of the line the bucket sits on.</p>
@@ -503,7 +556,7 @@ sealkeep open 08424eb6… ./session.jsonl \
503
556
  <div class="pane" id="p-account">
504
557
  <div class="ph" id="a-strip" tabindex="-1">
505
558
  <span class="name" id="a-label">—</span>
506
- <span class="chip" id="a-plan">—</span>
559
+ <button class="chip" id="a-plan" type="button" title="See your plan">—</button>
507
560
  <span class="who" id="a-who">—</span>
508
561
  </div>
509
562
 
@@ -540,18 +593,18 @@ sealkeep open 08424eb6… ./session.jsonl \
540
593
 
541
594
  <div class="card" id="c-archives" style="display:none">
542
595
  <span class="eyebrow">Archives</span>
543
- <p id="s-intro">Deleting one here removes the stored copy. If your machine already reclaimed the source, this is the last copy.</p>
596
+ <p id="s-intro">Names live on your machines — the cloud holds sealed archives it cannot read, listed here by reference. Deleting one removes the stored copy; if your machine already reclaimed the source, this is the last copy.</p>
544
597
  <p class="msg" id="s-msg">Loading…</p>
545
598
  <div id="s-wrap" style="display:none;overflow-x:auto">
546
599
  <table>
547
- <thead><tr><th>Ref</th><th>Size</th><th>State</th><th>Added</th><th></th></tr></thead>
600
+ <thead><tr><th>Archive</th><th>Contents</th><th>State</th><th>Added</th><th></th></tr></thead>
548
601
  <tbody id="s-body"></tbody>
549
602
  </table>
550
603
  <p class="msg" id="s-foot"></p>
551
604
  </div>
552
605
  </div>
553
606
 
554
- <div class="card">
607
+ <div class="card" id="c-plan">
555
608
  <span class="eyebrow">Plan</span>
556
609
  <div id="a-plan-free" style="display:none">
557
610
  <h3>Free — your bucket, your bill</h3>
@@ -639,6 +692,25 @@ sealkeep open 08424eb6… ./session.jsonl \
639
692
  <p class="why">Either way we hold no key that opens an archive. The difference is only who pays the storage bill and who can delete the bucket.</p>
640
693
  </div>
641
694
 
695
+ <div class="card" id="c-targets">
696
+ <span class="eyebrow">Routing</span>
697
+ <h3>Where new archives go</h3>
698
+ <p>Rules live on your account, so every machine of the vault follows the same ones. Lowest priority number wins; a project pinned to a target always goes there; a target at its cap passes to the next. Managed cloud is the fallback when nothing else claims an archive.</p>
699
+ <p class="msg" id="tg-msg">Loading…</p>
700
+ <div id="tg-wrap" style="display:none;overflow-x:auto">
701
+ <table>
702
+ <thead><tr><th>Target</th><th>Where</th><th>Priority</th><th>Cap (GB)</th><th>Only these projects</th><th></th></tr></thead>
703
+ <tbody id="tg-body"></tbody>
704
+ </table>
705
+ </div>
706
+ <div class="inline" style="margin-top:.9rem">
707
+ <button class="ghost" id="tg-add-drive" type="button">Add Google Drive</button>
708
+ <button class="primary" id="tg-save" type="button">Save routing</button>
709
+ <span class="saved" id="tg-save-msg" role="status" aria-live="polite"></span>
710
+ </div>
711
+ <p class="why" id="tg-note">Drive holds single objects and skips streaming; big under-pressure seals route to cloud or a bucket on their own. Adding Drive here sets the rule — the one-time consent happens on your machine: <code>sealkeep storage connect gdrive</code>.</p>
712
+ </div>
713
+
642
714
  <div class="card">
643
715
  <span class="eyebrow">Machines</span>
644
716
  <p>Each connected machine signs its own uploads. Revoking one stops it writing here; the archives it already stored are untouched.</p>
@@ -711,7 +783,9 @@ sealkeep open 08424eb6… ./session.jsonl \
711
783
 
712
784
  <script>
713
785
  const $ = (id) => document.getElementById(id);
714
- const API = "/p04946/api";
786
+ const API = location.hostname === "shared.spala.ai"
787
+ ? "/p04946/api"
788
+ : "https://shared.spala.ai/p04946/api";
715
789
  // sessionStorage, not localStorage: a bearer token should die with the tab
716
790
  // rather than rest on disk.
717
791
  const TOKEN = "vaultline.token";
@@ -903,6 +977,7 @@ sealkeep open 08424eb6… ./session.jsonl \
903
977
  $("a-name").value = user.label || "";
904
978
  $("a-plan").textContent = paid ? "Managed storage" : "Free";
905
979
  $("a-plan").className = "chip" + (paid ? " pro" : "");
980
+ $("a-plan").onclick = () => $("c-plan").scrollIntoView({ behavior: "smooth", block: "start" });
906
981
  $("a-who").textContent = [
907
982
  user.email,
908
983
  "joined " + day(user.created_at),
@@ -1074,6 +1149,109 @@ sealkeep open 08424eb6… ./session.jsonl \
1074
1149
  }
1075
1150
  }
1076
1151
 
1152
+ // Managed chunk folders arrive as one row per stored object —
1153
+ // <archive>.chunk-000001 … plus <archive>.envelope.vlmeta. A listing is
1154
+ // about archives, so fold each family into one row: the archive id, the
1155
+ // summed chunk bytes, and durable only when every member is durable.
1156
+ // Mirrors collapseCloudRows in the CLI, so both surfaces tell one story.
1157
+ const FAMILY_REF = /^(.+)\.(chunk-\d{6}|envelope\.vlmeta)$/;
1158
+ function foldFamilies(rows) {
1159
+ const families = new Map();
1160
+ const plain = [];
1161
+ for (const row of rows) {
1162
+ const match = FAMILY_REF.exec(String(row.vault_ref));
1163
+ if (!match) { plain.push(row); continue; }
1164
+ const fam = families.get(match[1]) ?? { rows: [], chunkBytes: 0, chunks: 0, members: [], allDurable: true };
1165
+ fam.rows.push(row);
1166
+ fam.members.push(row.vault_ref);
1167
+ if (match[2].startsWith("chunk-")) { fam.chunkBytes += Number(row.bytes) || 0; fam.chunks++; }
1168
+ if (row.state !== "durable") fam.allDurable = false;
1169
+ families.set(match[1], fam);
1170
+ }
1171
+ const folded = [...families.entries()].map(([id, fam]) => ({
1172
+ ...fam.rows[0], vault_ref: id, bytes: fam.chunkBytes, chunked: true,
1173
+ chunkCount: fam.chunks, members: fam.members, state: fam.allDurable ? "durable" : "pending"
1174
+ }));
1175
+ return [...plain, ...folded];
1176
+ }
1177
+
1178
+ /* ------------------------------------------------ routing (storage targets) */
1179
+ // The same rules the CLI syncs: GET/PUT /v1/cloud/storage-targets carries
1180
+ // {targets_json} — a JSON string of [{id, provider, bucket, prefix, maxGb,
1181
+ // projects, priority}]. Editing here lands on every machine of the vault,
1182
+ // because resolveTargets is cloud-first.
1183
+ let targetRows = [];
1184
+ const providerLabel = (p) => ({ vaultline: "Sealkeep cloud", gdrive: "Google Drive", r2: "Cloudflare R2", s3: "S3", gcs: "Google Cloud" }[p] || p);
1185
+ function renderTargets() {
1186
+ const body = $("tg-body");
1187
+ body.replaceChildren();
1188
+ if (!targetRows.length) {
1189
+ $("tg-msg").className = "empty";
1190
+ $("tg-msg").textContent = "No rules yet — everything goes to Sealkeep cloud. Add your Drive or set rules from a machine: sealkeep storage targets add …";
1191
+ $("tg-wrap").style.display = "none";
1192
+ return;
1193
+ }
1194
+ $("tg-msg").textContent = "";
1195
+ for (const t of targetRows) {
1196
+ const tr = document.createElement("tr");
1197
+ const name = document.createElement("td"); name.textContent = providerLabel(t.provider);
1198
+ const where = document.createElement("td");
1199
+ const wc = document.createElement("code"); wc.textContent = t.provider === "vaultline" ? "managed" : [t.bucket, t.prefix].filter(Boolean).join("/") || "—"; where.append(wc);
1200
+ const pr = document.createElement("td");
1201
+ const prIn = document.createElement("input"); prIn.type = "number"; prIn.className = "n"; prIn.min = "0"; prIn.max = "99";
1202
+ prIn.value = t.priority ?? (t.provider === "vaultline" ? 0 : 10);
1203
+ prIn.setAttribute("aria-label", "Priority for " + providerLabel(t.provider));
1204
+ prIn.addEventListener("input", () => { t.priority = Number(prIn.value); });
1205
+ pr.append(prIn);
1206
+ const cap = document.createElement("td");
1207
+ const capIn = document.createElement("input"); capIn.type = "number"; capIn.className = "n"; capIn.min = "1"; capIn.max = "99999"; capIn.placeholder = "none";
1208
+ if (t.maxGb) capIn.value = t.maxGb;
1209
+ capIn.setAttribute("aria-label", "Cap in GB for " + providerLabel(t.provider));
1210
+ capIn.addEventListener("input", () => { t.maxGb = capIn.value ? Number(capIn.value) : undefined; });
1211
+ cap.append(capIn);
1212
+ const proj = document.createElement("td");
1213
+ const projIn = document.createElement("input"); projIn.placeholder = "all projects"; projIn.style.minWidth = "9rem";
1214
+ projIn.value = (t.projects || []).join(", ");
1215
+ projIn.setAttribute("aria-label", "Projects pinned to " + providerLabel(t.provider));
1216
+ projIn.addEventListener("input", () => {
1217
+ const list = projIn.value.split(",").map((x) => x.trim()).filter(Boolean);
1218
+ if (list.length) t.projects = list; else delete t.projects;
1219
+ });
1220
+ proj.append(projIn);
1221
+ const act = document.createElement("td");
1222
+ const rm = document.createElement("button"); rm.className = "del"; rm.type = "button"; rm.textContent = "Remove";
1223
+ rm.addEventListener("click", () => { targetRows = targetRows.filter((x) => x !== t); renderTargets(); });
1224
+ act.append(rm);
1225
+ tr.append(name, where, pr, cap, proj, act);
1226
+ body.append(tr);
1227
+ }
1228
+ $("tg-wrap").style.display = "";
1229
+ }
1230
+ async function loadTargets(token) {
1231
+ try {
1232
+ const data = await api("/v1/cloud/storage-targets", { token });
1233
+ try { targetRows = JSON.parse(data.targets_json || "[]"); } catch { targetRows = []; }
1234
+ if (!Array.isArray(targetRows)) targetRows = [];
1235
+ renderTargets();
1236
+ } catch (error) { say("tg-msg", errText(error), "err"); }
1237
+ }
1238
+ $("tg-add-drive").addEventListener("click", () => {
1239
+ if (targetRows.some((t) => t.provider === "gdrive")) { say("tg-save-msg", "Drive is already in the rules."); return; }
1240
+ targetRows.push({ id: "gdrive-" + Math.random().toString(36).slice(2, 8), provider: "gdrive", bucket: "Sealkeep", maxGb: 50, priority: 10 });
1241
+ renderTargets();
1242
+ say("tg-save-msg", "Added — save, then run the one-time consent on your machine.");
1243
+ });
1244
+ $("tg-save").addEventListener("click", async () => {
1245
+ const token = sessionStorage.getItem(TOKEN);
1246
+ if (!token) { say("tg-save-msg", "Sign in again to save.", "err"); return; }
1247
+ $("tg-save").disabled = true;
1248
+ try {
1249
+ await api("/v1/cloud/storage-targets", { method: "PUT", token, body: { targets_json: JSON.stringify(targetRows) } });
1250
+ say("tg-save-msg", "Saved. Every machine follows these on its next seal.");
1251
+ } catch (error) { say("tg-save-msg", errText(error), "err"); }
1252
+ $("tg-save").disabled = false;
1253
+ });
1254
+
1077
1255
  async function loadStorage(token) {
1078
1256
  try {
1079
1257
  const rows = await api("/v1/cloud/archives", { token });
@@ -1091,12 +1269,13 @@ sealkeep open 08424eb6… ./session.jsonl \
1091
1269
  }
1092
1270
  on("s-intro", true);
1093
1271
 
1094
- for (const r of rows) {
1272
+ for (const r of foldFamilies(rows)) {
1095
1273
  const tr = document.createElement("tr");
1096
1274
  // textContent throughout: an archive ref is data, never markup.
1097
1275
  const ref = document.createElement("td");
1098
1276
  const code = document.createElement("code"); code.textContent = String(r.vault_ref).slice(0, 8); ref.append(code);
1099
- const size = document.createElement("td"); size.textContent = bytes(r.bytes);
1277
+ const size = document.createElement("td");
1278
+ size.textContent = r.chunked ? bytes(r.bytes) + " in " + r.chunkCount + " sealed chunks" : bytes(r.bytes);
1100
1279
  const st = document.createElement("td");
1101
1280
  const s = document.createElement("span");
1102
1281
  s.className = "state " + (r.state === "durable" ? "durable" : "pending");
@@ -1116,7 +1295,14 @@ sealkeep open 08424eb6… ./session.jsonl \
1116
1295
  }
1117
1296
  btn.disabled = true; btn.textContent = "Deleting";
1118
1297
  try {
1119
- await api("/v1/cloud/archives/" + encodeURIComponent(r.vault_ref), { method: "DELETE", token });
1298
+ // A chunked archive is many stored objects; removing it means
1299
+ // removing every member, envelope last so a crash leaves an
1300
+ // openable family rather than an orphaned envelope.
1301
+ const refs = r.members ? [...r.members].sort((a, b) => a.endsWith(".envelope.vlmeta") ? 1 : b.endsWith(".envelope.vlmeta") ? -1 : 0) : [r.vault_ref];
1302
+ for (let i = 0; i < refs.length; i++) {
1303
+ if (refs.length > 1) btn.textContent = "Deleting " + (i + 1) + "/" + refs.length;
1304
+ await api("/v1/cloud/archives/" + encodeURIComponent(refs[i]), { method: "DELETE", token });
1305
+ }
1120
1306
  await loadAccount(token);
1121
1307
  } catch (error) { say("s-msg", errText(error), "err"); btn.disabled = false; btn.textContent = "Delete"; }
1122
1308
  });
@@ -1142,6 +1328,7 @@ sealkeep open 08424eb6… ./session.jsonl \
1142
1328
  renderAccount(user);
1143
1329
  // Each section fails on its own. One dead panel must not blank the rest.
1144
1330
  if (user.plan === "pro") { $("a-count").textContent = "—"; loadStorage(token); }
1331
+ loadTargets(token);
1145
1332
  loadAudit(token);
1146
1333
  loadStorage2(token);
1147
1334
  const devices = await loadDevices(token);
package/dist/site.zip CHANGED
Binary file
@@ -645,11 +645,19 @@ async function cloudPlanAndQuota(dataDir) {
645
645
  const { account, source } = await resolveAccount(dataDir);
646
646
  return { plan: account.plan, quota: account.quota, email: account.email, source };
647
647
  }
648
- /** The account panel lives at the cloud API's origin with `/api` swapped for `#account`. */
648
+ /**
649
+ * The account panel lives on the product domain, which is no longer the API's
650
+ * origin — deriving it from the cloud URL sent people to the old address after
651
+ * the rename. A self-hosted plane (VAULTLINE_CLOUD_URL set) still derives from
652
+ * its own origin, because there is no sealkeep.spala.ai panel for it.
653
+ */
649
654
  async function panelUrl() {
650
- const { DEFAULT_CLOUD_URL } = await import("./cloud.js");
651
- const origin = (process.env.VAULTLINE_CLOUD_URL ?? DEFAULT_CLOUD_URL).replace(/\/+$/, "").replace(/\/api$/, "");
652
- return `${origin}/#account`;
655
+ if (process.env.VAULTLINE_CLOUD_URL) {
656
+ const origin = process.env.VAULTLINE_CLOUD_URL.replace(/\/+$/, "").replace(/\/api$/, "");
657
+ return `${origin}/#account`;
658
+ }
659
+ const { PRODUCT_URL } = await import("./branding.js");
660
+ return `${PRODUCT_URL}/#account`;
653
661
  }
654
662
  /**
655
663
  * Everything the settings screen shows about the account, and none of it
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sealkeep",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "type": "module",
5
5
  "description": "Sealkeep by SPALA AI \u2014 your AI coding-agent history, sealed, searchable, and shared across your machines.",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -62,4 +62,4 @@
62
62
  "sessions"
63
63
  ],
64
64
  "homepage": "https://sealkeep.spala.ai"
65
- }
65
+ }
package/web/app.js CHANGED
@@ -1361,7 +1361,17 @@ async function guard(action) {
1361
1361
  async function start() {
1362
1362
  // No token in hand is not yet a reason to gate: a browser that has been here
1363
1363
  // before holds the HttpOnly cookie, which the server accepts and this page
1364
- // cannot see. Try the API first; only a 401 shows the gate.
1364
+ // cannot see. But discovering that costs one request, not eight — probe
1365
+ // /v1/status alone, and only fan out the full refresh once it answers. The
1366
+ // browser prints every 401 to the console no matter what we do; one line on
1367
+ // a first visit is honest, eight looked like a malfunction.
1368
+ const probe = await fetch("/v1/status", {
1369
+ headers: { authorization: `Bearer ${token ?? ""}` }
1370
+ }).catch(() => null);
1371
+ if (!probe || probe.status === 401) {
1372
+ showGate(token ? "That token was not accepted." : undefined);
1373
+ return;
1374
+ }
1365
1375
  await guard(refresh);
1366
1376
  // Land on the view named in the URL (e.g. arriving at index.html#settings from
1367
1377
  // the Sessions page), not always Overview.
package/web/index.html CHANGED
@@ -5,6 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
6
6
  <meta name="referrer" content="no-referrer">
7
7
  <title>Sealkeep</title>
8
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 16 16%22%3E%3Ctext x=%228%22 y=%2213%22 font-size=%2214%22 text-anchor=%22middle%22%3E%E2%8C%91%3C/text%3E%3C/svg%3E">
8
9
  <link rel="stylesheet" href="./style.css">
9
10
  <style>
10
11
  /* Memory pool: the project column. The name answers "which repo", the dim
@@ -108,7 +109,7 @@
108
109
 
109
110
  <section id="gate" class="panel gate pad" hidden>
110
111
  <h3>Local token required</h3>
111
- <p class="note">This browser has not been given its key yet. Paste the token from <code id="token-path">~/.vaultline/local-api-token</code> — a local file, so nothing here is sent anywhere. The browser <code>vaultline ui</code> opened is signed in already.</p>
112
+ <p class="note">This browser has not been given its key yet. Paste the token from <code id="token-path">local-api-token</code> in your Sealkeep data folder (<code>~/.sealkeep</code>, or <code>~/.vaultline</code> for older vaults) — a local file, so nothing here is sent anywhere. The browser <code>sealkeep ui</code> opened is signed in already.</p>
112
113
  <form id="token-form">
113
114
  <input id="token-input" type="password" autocomplete="off" spellcheck="false" placeholder="local API token" aria-label="Local API token">
114
115
  <button type="submit" class="action">Connect</button>
@@ -172,7 +173,7 @@
172
173
  where work in motion belongs. -->
173
174
  <div class="panel" id="archives-panel"></div>
174
175
  <div class="panel pad" style="margin-top:var(--gap)">
175
- <p class="note"><b>If this machine is lost:</b> your recovery phrase plus <code>vaultline cloud login</code> on any machine reaches every copy in managed storage. The phrase is the only key — no copy anywhere can be opened without it.</p>
176
+ <p class="note"><b>If this machine is lost:</b> your recovery phrase plus <code>sealkeep cloud login</code> on any machine reaches every copy in managed storage. The phrase is the only key — no copy anywhere can be opened without it.</p>
176
177
  </div>
177
178
  </section>
178
179
 
@@ -264,7 +265,7 @@
264
265
 
265
266
  <h2>Account</h2>
266
267
  <div class="panel pad">
267
- <p class="note">Set from <a id="set-panel-link" href="https://shared.spala.ai" target="_blank" rel="noopener">the account panel</a>, not here — these apply to every machine on the account, and this machine only mirrors them.</p>
268
+ <p class="note">Set from <a id="set-panel-link" href="https://sealkeep.spala.ai" target="_blank" rel="noopener">the account panel</a>, not here — these apply to every machine on the account, and this machine only mirrors them.</p>
268
269
  <div class="metrics">
269
270
  <div class="metric"><h3>What leaves this machine</h3><b id="set-sync-mode" class="small">–</b><small id="set-sync-source"></small></div>
270
271
  <div class="metric"><h3>Plan</h3><b id="set-plan" class="small">–</b><small>&nbsp;</small></div>
@@ -3,6 +3,7 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <title>Free up disk — rules · Sealkeep</title>
6
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 16 16%22%3E%3Ctext x=%228%22 y=%2213%22 font-size=%2214%22 text-anchor=%22middle%22%3E%E2%8C%91%3C/text%3E%3C/svg%3E">
6
7
  <meta name="referrer" content="no-referrer">
7
8
  </head>
8
9
  <body>
package/web/sessions.html CHANGED
@@ -3,6 +3,7 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <title>Sealkeep — memory pool</title>
6
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 16 16%22%3E%3Ctext x=%228%22 y=%2213%22 font-size=%2214%22 text-anchor=%22middle%22%3E%E2%8C%91%3C/text%3E%3C/svg%3E">
6
7
  <meta name="referrer" content="no-referrer">
7
8
  </head>
8
9
  <body>
package/web/setup.html CHANGED
@@ -5,6 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
6
6
  <meta name="referrer" content="no-referrer">
7
7
  <title>Sealkeep — first run</title>
8
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 16 16%22%3E%3Ctext x=%228%22 y=%2213%22 font-size=%2214%22 text-anchor=%22middle%22%3E%E2%8C%91%3C/text%3E%3C/svg%3E">
8
9
  <link rel="stylesheet" href="./style.css">
9
10
  </head>
10
11
  <body>
@@ -175,8 +176,8 @@
175
176
  <!-- Both links name a tab, not just the section, so nobody lands on the
176
177
  page and has to work out which half they were sent for. -->
177
178
  <div class="actions">
178
- <a class="action primary" href="https://shared.spala.ai/p04946/#signup" target="_blank" rel="noreferrer noopener">Create an account</a>
179
- <a class="action" href="https://shared.spala.ai/p04946/#signin" target="_blank" rel="noreferrer noopener">I already have one — sign in</a>
179
+ <a class="action primary" href="https://sealkeep.spala.ai/#signup" target="_blank" rel="noreferrer noopener">Create an account</a>
180
+ <a class="action" href="https://sealkeep.spala.ai/#signin" target="_blank" rel="noreferrer noopener">I already have one — sign in</a>
180
181
  </div>
181
182
  <p class="note">The pairing code is single use, and it is not your recovery phrase.</p>
182
183
  <div class="actions">
@@ -194,12 +195,12 @@
194
195
  <div class="row">
195
196
  <div class="field">
196
197
  <label for="f-bucket">Bucket name</label>
197
- <input id="f-bucket" name="bucket" type="text" autocomplete="off" spellcheck="false" placeholder="my-vaultline-archives">
198
+ <input id="f-bucket" name="bucket" type="text" autocomplete="off" spellcheck="false" placeholder="my-sealkeep-archives">
198
199
  <p class="note error" id="e-bucket" hidden></p>
199
200
  </div>
200
201
  <div class="field">
201
202
  <label for="f-prefix">Folder inside it</label>
202
- <input id="f-prefix" name="prefix" type="text" autocomplete="off" spellcheck="false" value="vaultline">
203
+ <input id="f-prefix" name="prefix" type="text" autocomplete="off" spellcheck="false" value="sealkeep">
203
204
  <p class="note error" id="e-prefix" hidden></p>
204
205
  </div>
205
206
  </div>
@@ -372,16 +373,16 @@
372
373
  <!-- Shown instead of a blank page when the machine cannot answer. -->
373
374
  <!-- The token lives in sessionStorage, which belongs to one browser. Opening
374
375
  this address in a second one arrives with nothing, and telling that person
375
- to "run vaultline ui" is both a terminal instruction and the wrong one:
376
+ to "run sealkeep ui" is both a terminal instruction and the wrong one:
376
377
  it would open their default browser, not the one they are looking at. So
377
378
  the page offers the way in that works from here. -->
378
379
  <section class="screen" id="screen-gate" hidden>
379
380
  <header>
380
381
  <h1>Sign this browser in</h1>
381
- <p class="lede">Sealkeep is running on this machine — this browser just has not been given its key yet. That happens automatically in whichever browser <code>vaultline ui</code> opened.</p>
382
+ <p class="lede">Sealkeep is running on this machine — this browser just has not been given its key yet. That happens automatically in whichever browser <code>sealkeep ui</code> opened.</p>
382
383
  </header>
383
384
  <div class="panel pad">
384
- <p class="note">Paste the token from <code id="gate-token-path">~/.vaultline/local-api-token</code>. It is a local file; nothing here is sent anywhere.</p>
385
+ <p class="note">Paste the token from <code id="gate-token-path">local-api-token</code> in your Sealkeep data folder (<code>~/.sealkeep</code>, or <code>~/.vaultline</code> for older vaults). It is a local file; nothing here is sent anywhere.</p>
385
386
  <form id="gate-form">
386
387
  <div class="field">
387
388
  <label for="gate-token">Local API token</label>
@@ -390,7 +391,7 @@
390
391
  <div class="actions"><button type="submit" class="action primary">Connect this browser</button></div>
391
392
  </form>
392
393
  <p class="note error" id="gate-error" hidden></p>
393
- <p class="note">On macOS or Linux: <code>cat ~/.vaultline/local-api-token</code></p>
394
+ <p class="note">On macOS or Linux: <code>cat ~/.sealkeep/local-api-token</code> (older vaults: <code>~/.vaultline</code>)</p>
394
395
  </div>
395
396
  </section>
396
397
 
@@ -401,8 +402,8 @@
401
402
  </header>
402
403
  <div class="panel pad callout">
403
404
  <h3>What to do instead</h3>
404
- <p>Update to a build that has the setup endpoints: <code>npm i -g vaultline@latest</code>, then run <code>vaultline</code> again.</p>
405
- <p>Or set the same things up from a terminal — the wizard is a second way in, never the only one: <code>vaultline setup</code>, <code>vaultline storage configure</code>.</p>
405
+ <p>Update to a build that has the setup endpoints: <code>npm i -g sealkeep@latest</code>, then run <code>sealkeep</code> again.</p>
406
+ <p>Or set the same things up from a terminal — the wizard is a second way in, never the only one: <code>sealkeep setup</code>, <code>sealkeep storage configure</code>.</p>
406
407
  <p>Already have a vault? The <a href="./index.html">dashboard</a> works regardless.</p>
407
408
  </div>
408
409
  </section>
package/web/setup.js CHANGED
@@ -654,7 +654,7 @@ async function boot() {
654
654
  // here" ticked above a fully drawn R2 form.
655
655
  draft.intent = intentFor(draft.providerId);
656
656
  draft.storage.bucket = state.storage.bucket ?? "";
657
- draft.storage.prefix = state.storage.prefix ?? "vaultline";
657
+ draft.storage.prefix = state.storage.prefix ?? "sealkeep";
658
658
  }
659
659
  } catch (error) {
660
660
  if (error.missing) {