sealkeep 0.5.1 → 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 +9 -0
- package/dist/site/index.html +173 -7
- package/dist/site.zip +0 -0
- package/dist/src/local-api.js +12 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,15 @@
|
|
|
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
|
+
|
|
6
15
|
## 0.5.1 — 2026-08-19
|
|
7
16
|
|
|
8
17
|
The first release that lives entirely at its new addresses.
|
package/dist/site/index.html
CHANGED
|
@@ -386,6 +386,40 @@
|
|
|
386
386
|
|
|
387
387
|
</section>
|
|
388
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
|
+
|
|
389
423
|
<section id="plans">
|
|
390
424
|
<h2>Two ways to run it</h2>
|
|
391
425
|
<p class="sub">Same encryption either way. The only difference is which side of the line the bucket sits on.</p>
|
|
@@ -522,7 +556,7 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
522
556
|
<div class="pane" id="p-account">
|
|
523
557
|
<div class="ph" id="a-strip" tabindex="-1">
|
|
524
558
|
<span class="name" id="a-label">—</span>
|
|
525
|
-
<
|
|
559
|
+
<button class="chip" id="a-plan" type="button" title="See your plan">—</button>
|
|
526
560
|
<span class="who" id="a-who">—</span>
|
|
527
561
|
</div>
|
|
528
562
|
|
|
@@ -559,18 +593,18 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
559
593
|
|
|
560
594
|
<div class="card" id="c-archives" style="display:none">
|
|
561
595
|
<span class="eyebrow">Archives</span>
|
|
562
|
-
<p id="s-intro">Deleting one
|
|
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>
|
|
563
597
|
<p class="msg" id="s-msg">Loading…</p>
|
|
564
598
|
<div id="s-wrap" style="display:none;overflow-x:auto">
|
|
565
599
|
<table>
|
|
566
|
-
<thead><tr><th>
|
|
600
|
+
<thead><tr><th>Archive</th><th>Contents</th><th>State</th><th>Added</th><th></th></tr></thead>
|
|
567
601
|
<tbody id="s-body"></tbody>
|
|
568
602
|
</table>
|
|
569
603
|
<p class="msg" id="s-foot"></p>
|
|
570
604
|
</div>
|
|
571
605
|
</div>
|
|
572
606
|
|
|
573
|
-
<div class="card">
|
|
607
|
+
<div class="card" id="c-plan">
|
|
574
608
|
<span class="eyebrow">Plan</span>
|
|
575
609
|
<div id="a-plan-free" style="display:none">
|
|
576
610
|
<h3>Free — your bucket, your bill</h3>
|
|
@@ -658,6 +692,25 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
658
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>
|
|
659
693
|
</div>
|
|
660
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
|
+
|
|
661
714
|
<div class="card">
|
|
662
715
|
<span class="eyebrow">Machines</span>
|
|
663
716
|
<p>Each connected machine signs its own uploads. Revoking one stops it writing here; the archives it already stored are untouched.</p>
|
|
@@ -924,6 +977,7 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
924
977
|
$("a-name").value = user.label || "";
|
|
925
978
|
$("a-plan").textContent = paid ? "Managed storage" : "Free";
|
|
926
979
|
$("a-plan").className = "chip" + (paid ? " pro" : "");
|
|
980
|
+
$("a-plan").onclick = () => $("c-plan").scrollIntoView({ behavior: "smooth", block: "start" });
|
|
927
981
|
$("a-who").textContent = [
|
|
928
982
|
user.email,
|
|
929
983
|
"joined " + day(user.created_at),
|
|
@@ -1095,6 +1149,109 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
1095
1149
|
}
|
|
1096
1150
|
}
|
|
1097
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
|
+
|
|
1098
1255
|
async function loadStorage(token) {
|
|
1099
1256
|
try {
|
|
1100
1257
|
const rows = await api("/v1/cloud/archives", { token });
|
|
@@ -1112,12 +1269,13 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
1112
1269
|
}
|
|
1113
1270
|
on("s-intro", true);
|
|
1114
1271
|
|
|
1115
|
-
for (const r of rows) {
|
|
1272
|
+
for (const r of foldFamilies(rows)) {
|
|
1116
1273
|
const tr = document.createElement("tr");
|
|
1117
1274
|
// textContent throughout: an archive ref is data, never markup.
|
|
1118
1275
|
const ref = document.createElement("td");
|
|
1119
1276
|
const code = document.createElement("code"); code.textContent = String(r.vault_ref).slice(0, 8); ref.append(code);
|
|
1120
|
-
const size = document.createElement("td");
|
|
1277
|
+
const size = document.createElement("td");
|
|
1278
|
+
size.textContent = r.chunked ? bytes(r.bytes) + " in " + r.chunkCount + " sealed chunks" : bytes(r.bytes);
|
|
1121
1279
|
const st = document.createElement("td");
|
|
1122
1280
|
const s = document.createElement("span");
|
|
1123
1281
|
s.className = "state " + (r.state === "durable" ? "durable" : "pending");
|
|
@@ -1137,7 +1295,14 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
1137
1295
|
}
|
|
1138
1296
|
btn.disabled = true; btn.textContent = "Deleting";
|
|
1139
1297
|
try {
|
|
1140
|
-
|
|
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
|
+
}
|
|
1141
1306
|
await loadAccount(token);
|
|
1142
1307
|
} catch (error) { say("s-msg", errText(error), "err"); btn.disabled = false; btn.textContent = "Delete"; }
|
|
1143
1308
|
});
|
|
@@ -1163,6 +1328,7 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
1163
1328
|
renderAccount(user);
|
|
1164
1329
|
// Each section fails on its own. One dead panel must not blank the rest.
|
|
1165
1330
|
if (user.plan === "pro") { $("a-count").textContent = "—"; loadStorage(token); }
|
|
1331
|
+
loadTargets(token);
|
|
1166
1332
|
loadAudit(token);
|
|
1167
1333
|
loadStorage2(token);
|
|
1168
1334
|
const devices = await loadDevices(token);
|
package/dist/site.zip
CHANGED
|
Binary file
|
package/dist/src/local-api.js
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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