sealkeep 0.6.0 → 0.6.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 +18 -0
- package/dist/site/index.html +26 -22
- package/dist/site.zip +0 -0
- package/dist/src/cli.js +60 -0
- package/dist/src/errors.d.ts +1 -1
- package/dist/src/offload.js +13 -0
- package/package.json +1 -1
- package/web/index.html +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,24 @@
|
|
|
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.6.2 — 2026-08-20
|
|
7
|
+
|
|
8
|
+
- New: `sealkeep projects` lists the project names this vault knows, with how
|
|
9
|
+
many archives and how much history each holds. Routing rules can pin a
|
|
10
|
+
project to a destination, but the account panel cannot offer a list — project
|
|
11
|
+
names never reach us — so the machine that does know them prints the exact
|
|
12
|
+
strings to paste.
|
|
13
|
+
- The panel's pinning field says what a project is, where to find its name, and
|
|
14
|
+
what happens to sessions that recorded none, instead of an empty box.
|
|
15
|
+
|
|
16
|
+
## 0.6.1 — 2026-08-19
|
|
17
|
+
|
|
18
|
+
- Reading an offloaded archive now checks for free space first. An index build
|
|
19
|
+
reads each archive in full, and on a machine already short of space that
|
|
20
|
+
filled the disk and died mid-build — the failure this product exists to
|
|
21
|
+
prevent. It refuses in words that name the archive and the shortfall, keeps a
|
|
22
|
+
512 MB reserve, and lets the build carry on with the rest.
|
|
23
|
+
|
|
6
24
|
## 0.6.0 — 2026-08-19
|
|
7
25
|
|
|
8
26
|
**A second machine no longer wipes your search index.** The content index is
|
package/dist/site/index.html
CHANGED
|
@@ -59,7 +59,9 @@
|
|
|
59
59
|
nav{display:flex;align-items:center;gap:1.4rem;padding:1.5rem 0;font-size:.86rem}
|
|
60
60
|
nav .logo{font-weight:750;letter-spacing:-.03em;font-size:1rem}
|
|
61
61
|
nav .sp{flex:1}
|
|
62
|
-
|
|
62
|
+
/* A finger needs 24px; these were 22px of bare line-height. Padding buys the
|
|
63
|
+
height without moving the row, since the nav is already flex-aligned. */
|
|
64
|
+
nav a{color:var(--soft);text-decoration:none;display:inline-flex;align-items:center;min-height:26px;padding:.15rem .1rem}
|
|
63
65
|
nav a:hover{color:var(--ink)}
|
|
64
66
|
|
|
65
67
|
/* ------------------------------------------------------------- the spine
|
|
@@ -179,7 +181,7 @@
|
|
|
179
181
|
keyboard and be left without a ring — and a ring drawn on arrival would
|
|
180
182
|
compete with the one card that is meant to be asking for attention. */
|
|
181
183
|
.ph:focus,.ph:focus-visible{outline:none}
|
|
182
|
-
.chip{font-family:var(--mono);font-size:.7rem;border:1px solid var(--rule);border-radius:3px;
|
|
184
|
+
.chip{font-family:var(--mono);font-size:.7rem;min-height:26px;display:inline-flex;align-items:center;border:1px solid var(--rule);border-radius:3px;
|
|
183
185
|
color:var(--soft);padding:.2rem .55rem}
|
|
184
186
|
.chip.pro{border-color:var(--live);color:var(--live);font-weight:600}
|
|
185
187
|
.card{background:var(--card);border:1px solid var(--rule);border-radius:4px;padding:1.35rem;margin-bottom:.9rem}
|
|
@@ -198,7 +200,7 @@
|
|
|
198
200
|
.kv .cap{display:block;font-size:.78rem;color:var(--soft);margin-top:.25rem}
|
|
199
201
|
.kv .k{display:block;font-family:var(--mono);font-size:.72rem;color:var(--soft);margin-bottom:.2rem}
|
|
200
202
|
.kv b{font-family:var(--mono);font-size:1rem;font-weight:500;color:var(--soft);letter-spacing:-.02em}
|
|
201
|
-
.kv .lead b{font-size:2.2rem;font-weight:700;color:var(--ink);letter-spacing:-.04em;line-height:1.1}
|
|
203
|
+
.kv .lead b{font-size:clamp(1.55rem,2.1vw,2.2rem);font-weight:700;color:var(--ink);letter-spacing:-.04em;line-height:1.1;white-space:nowrap}
|
|
202
204
|
.bar{height:8px;background:var(--rule);border-radius:4px;margin:1.05rem 0 .4rem;overflow:hidden}
|
|
203
205
|
.bar i{display:block;height:100%;width:0;min-width:6px;background:var(--live);transition:width .5s ease}
|
|
204
206
|
|
|
@@ -212,9 +214,10 @@
|
|
|
212
214
|
.choice .t,.choice .d{font-family:var(--sans)}
|
|
213
215
|
.cellsub{display:block;font-family:var(--sans);font-size:.75rem;color:var(--soft);margin-top:.15rem}
|
|
214
216
|
.acct-nav{display:flex;gap:.35rem;flex-wrap:wrap;position:sticky;top:0;z-index:5;background:var(--paper);padding:.65rem 0;margin:.4rem 0 .2rem;border-bottom:1px solid var(--rule)}
|
|
215
|
-
.acct-nav a{font-family:var(--mono);font-size:.74rem;color:var(--soft);text-decoration:none;border:1px solid var(--rule);border-radius:999px;padding:.
|
|
217
|
+
.acct-nav a{font-family:var(--mono);font-size:.74rem;color:var(--soft);text-decoration:none;border:1px solid var(--rule);border-radius:999px;padding:.4rem .85rem;min-height:26px;display:inline-flex;align-items:center}
|
|
216
218
|
.acct-nav a:hover{color:var(--ink);border-color:var(--ink)}
|
|
217
219
|
.acct-hero{font-family:var(--mono);font-size:1.55rem;font-weight:700;letter-spacing:-.03em;margin:.9rem 0 0;line-height:1.3}
|
|
220
|
+
h1.acct-hero:empty{display:none}
|
|
218
221
|
.ruleslist{margin:.3rem 0 .9rem;padding-left:1.1rem}
|
|
219
222
|
.ruleslist li{margin:.25rem 0;color:var(--soft)}
|
|
220
223
|
.ruleslist b{color:var(--ink);font-weight:600}
|
|
@@ -229,7 +232,7 @@
|
|
|
229
232
|
.packchip span{font-size:.78rem;color:var(--soft)}
|
|
230
233
|
.packchip.now{border-color:var(--live)}
|
|
231
234
|
.packchip.now i{font-style:normal;font-family:var(--mono);font-size:.66rem;color:var(--live);letter-spacing:.03em}
|
|
232
|
-
#p-account .card
|
|
235
|
+
#p-account .card h2{font-size:1.02rem;margin:.15rem 0 .5rem}
|
|
233
236
|
[id^="anchor-"]{scroll-margin-top:3.4rem}
|
|
234
237
|
.state.durable{color:var(--live)}
|
|
235
238
|
.state.pending{color:var(--soft);opacity:.75}
|
|
@@ -584,7 +587,7 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
584
587
|
<button class="chip" id="a-plan" type="button" title="See your plan">—</button>
|
|
585
588
|
<span class="who" id="a-who">—</span>
|
|
586
589
|
</div>
|
|
587
|
-
<
|
|
590
|
+
<h1 class="acct-hero" id="a-hero"></h1>
|
|
588
591
|
<p class="msg" id="a-summary" style="margin:.4rem 0 0"></p>
|
|
589
592
|
<nav class="acct-nav" aria-label="Account sections">
|
|
590
593
|
<a href="#anchor-storage">Storage</a>
|
|
@@ -597,7 +600,7 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
597
600
|
|
|
598
601
|
<div class="card next" id="a-firstrun" style="display:none">
|
|
599
602
|
<span class="eyebrow">Set up</span>
|
|
600
|
-
<
|
|
603
|
+
<h2 class="lead">Connect your first machine</h2>
|
|
601
604
|
<p>Sealkeep runs on your machine — that is where your keys are made and where every session is sealed before it moves. Pair one machine and archiving carries on by itself.</p>
|
|
602
605
|
<div id="pair-home-first"></div>
|
|
603
606
|
</div>
|
|
@@ -611,12 +614,12 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
611
614
|
<div class="card" id="anchor-storage">
|
|
612
615
|
<span class="eyebrow">Storage</span>
|
|
613
616
|
<div id="a-store-free" style="display:none">
|
|
614
|
-
<
|
|
617
|
+
<h2>You run the bucket</h2>
|
|
615
618
|
<p>Archives go to storage you own, under your own credentials. Sealkeep never holds them, so there is nothing to meter here.</p>
|
|
616
619
|
<p class="empty">Your machine reports what it has stored. Run <code>sealkeep status</code> to see it.</p>
|
|
617
620
|
</div>
|
|
618
621
|
<div id="a-store-paid" style="display:none">
|
|
619
|
-
<
|
|
622
|
+
<h2>What you keep here</h2>
|
|
620
623
|
<div class="kv">
|
|
621
624
|
<div class="lead"><span class="k">Stored</span><b id="a-used">—</b><span class="cap" id="a-used-cap">sealed before it arrived</span></div>
|
|
622
625
|
<div><span class="k">Included</span><b id="a-quota-b">—</b><span class="cap">in your plan</span></div>
|
|
@@ -631,7 +634,7 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
631
634
|
|
|
632
635
|
<div class="card" id="c-targets"><span id="anchor-routing"></span>
|
|
633
636
|
<span class="eyebrow">Routing</span>
|
|
634
|
-
<
|
|
637
|
+
<h2>Where new archives go</h2>
|
|
635
638
|
<p>Rules live on your account — every machine of the vault follows the same ones.</p>
|
|
636
639
|
<ul class="ruleslist">
|
|
637
640
|
<li><b>Lowest priority number wins.</b></li>
|
|
@@ -641,7 +644,7 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
641
644
|
<p class="msg" id="tg-msg">Loading…</p>
|
|
642
645
|
<div id="tg-wrap" style="display:none;overflow-x:auto">
|
|
643
646
|
<table>
|
|
644
|
-
<thead><tr><th>Target</th><th>Where</th><th>Priority</th><th>Cap (GB)</th><th>
|
|
647
|
+
<thead><tr><th>Target</th><th>Where</th><th>Priority</th><th>Cap (GB)</th><th>Pinned projects</th><th></th></tr></thead>
|
|
645
648
|
<tbody id="tg-body"></tbody>
|
|
646
649
|
</table>
|
|
647
650
|
</div>
|
|
@@ -651,12 +654,13 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
651
654
|
<button class="primary" id="tg-save" type="button">Save routing</button>
|
|
652
655
|
<span class="saved" id="tg-save-msg" role="status" aria-live="polite"></span>
|
|
653
656
|
</div>
|
|
654
|
-
<p class="why" id="tg-note">
|
|
657
|
+
<p class="why" id="tg-note">A project is the folder a session ran in — <code>sealkeep</code>, <code>big-refactor</code>. Pin one and its sessions always go to that destination. Project names never reach us, which is why this page cannot list them for you: run <code>sealkeep projects</code> on your machine to see the exact names it knows, and paste them here separated by commas. Sessions that recorded no project are matched by no pin, and take the ordinary priority order.</p>
|
|
658
|
+
<p class="why">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>
|
|
655
659
|
</div>
|
|
656
660
|
|
|
657
661
|
<div class="card" id="anchor-machines">
|
|
658
662
|
<span class="eyebrow">Machines</span>
|
|
659
|
-
<
|
|
663
|
+
<h2>Connected machines</h2>
|
|
660
664
|
<p>Each connected machine signs its own uploads. Revoking one stops it writing here; the archives it already stored are untouched.</p>
|
|
661
665
|
<ul class="lines" id="d-list"></ul>
|
|
662
666
|
<p class="empty" id="d-msg">Loading…</p>
|
|
@@ -691,7 +695,7 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
691
695
|
|
|
692
696
|
<div class="card" id="anchor-archives" style="display:none">
|
|
693
697
|
<span class="eyebrow">Archives</span>
|
|
694
|
-
<
|
|
698
|
+
<h2>What is stored</h2>
|
|
695
699
|
<p id="s-intro">Names live on your machines — your storage 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>
|
|
696
700
|
<p class="msg" id="s-msg">Loading…</p>
|
|
697
701
|
<div id="s-wrap" style="display:none;overflow-x:auto">
|
|
@@ -706,12 +710,12 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
706
710
|
<div class="card" id="c-plan"><span id="anchor-plan"></span>
|
|
707
711
|
<span class="eyebrow">Plan</span>
|
|
708
712
|
<div id="a-plan-free" style="display:none">
|
|
709
|
-
<
|
|
713
|
+
<h2>Free — your bucket, your bill</h2>
|
|
710
714
|
<p>Unlimited archives with the same encryption. You supply the storage credentials and pay your provider directly.</p>
|
|
711
715
|
<p class="empty">Want us to run storage instead? Pricing is in Your package, below. Write to <span id="a-sales">hello@spala.ai</span> from this address and we will switch it on.</p>
|
|
712
716
|
</div>
|
|
713
717
|
<div id="a-plan-paid" style="display:none">
|
|
714
|
-
<
|
|
718
|
+
<h2>Managed storage</h2>
|
|
715
719
|
<p>We run the bucket and meter what you store. We still hold no key that opens any of it — the only difference from your own bucket is who pays the storage bill and who can delete it.</p>
|
|
716
720
|
<div class="kv" style="margin-top:1rem">
|
|
717
721
|
<div><span class="k">Subscription</span><b id="a-sub-state">—</b></div>
|
|
@@ -724,7 +728,7 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
724
728
|
|
|
725
729
|
<div class="card" id="a-pack-card">
|
|
726
730
|
<span class="eyebrow">Your package</span>
|
|
727
|
-
<
|
|
731
|
+
<h2 id="a-pack-current">—</h2>
|
|
728
732
|
<p class="msg" id="a-pack-context" style="margin:0 0 .8rem"></p>
|
|
729
733
|
<div class="packrow" id="a-packs"></div>
|
|
730
734
|
<p class="empty" id="a-pack-alt">Not storing with us? Your own bucket is free with the same encryption, and Pro Lite keeps key escrow and machine sign-in with no metered storage.</p>
|
|
@@ -733,7 +737,7 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
733
737
|
|
|
734
738
|
<div class="card">
|
|
735
739
|
<span class="eyebrow">What leaves this machine</span>
|
|
736
|
-
<
|
|
740
|
+
<h2>Your upload rules</h2>
|
|
737
741
|
<p>Everything you do not choose here stays local and is never uploaded.</p>
|
|
738
742
|
<p class="empty" id="r-note" style="margin-bottom:.9rem">Enforced on your machine, before a session is read or sealed. Anything held back is never uploaded, so this is a rule about what leaves — not a filter we apply after it arrives.</p>
|
|
739
743
|
<div class="rules">
|
|
@@ -762,13 +766,13 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
762
766
|
|
|
763
767
|
<div class="card" id="c-recovery"><span id="anchor-recovery"></span>
|
|
764
768
|
<span class="eyebrow">Recovery key</span>
|
|
765
|
-
<
|
|
769
|
+
<h2 id="e-head">Loading…</h2>
|
|
766
770
|
<p id="e-text"></p>
|
|
767
771
|
</div>
|
|
768
772
|
|
|
769
773
|
<div class="card">
|
|
770
774
|
<span class="eyebrow">Activity</span>
|
|
771
|
-
<
|
|
775
|
+
<h2>What happened lately</h2>
|
|
772
776
|
<p>Every change made to this account, newest first.</p>
|
|
773
777
|
<ul class="lines" id="v-list"></ul>
|
|
774
778
|
<p class="empty" id="v-msg">Loading…</p>
|
|
@@ -776,7 +780,7 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
776
780
|
|
|
777
781
|
<div class="card">
|
|
778
782
|
<span class="eyebrow">Account</span>
|
|
779
|
-
<
|
|
783
|
+
<h2>Name and email</h2>
|
|
780
784
|
<label for="a-name">Account name</label>
|
|
781
785
|
<div class="inline">
|
|
782
786
|
<input id="a-name" maxlength="60" autocomplete="off">
|
|
@@ -1313,7 +1317,7 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
1313
1317
|
capIn.addEventListener("input", () => { t.maxGb = capIn.value ? Number(capIn.value) : undefined; });
|
|
1314
1318
|
cap.append(capIn);
|
|
1315
1319
|
const proj = document.createElement("td");
|
|
1316
|
-
const projIn = document.createElement("input"); projIn.placeholder = "
|
|
1320
|
+
const projIn = document.createElement("input"); projIn.placeholder = "empty = every project"; projIn.style.minWidth = "9rem";
|
|
1317
1321
|
projIn.value = (t.projects || []).join(", ");
|
|
1318
1322
|
projIn.setAttribute("aria-label", "Projects pinned to " + providerLabel(t.provider));
|
|
1319
1323
|
projIn.addEventListener("input", () => {
|
package/dist/site.zip
CHANGED
|
Binary file
|
package/dist/src/cli.js
CHANGED
|
@@ -202,6 +202,7 @@ function usage() {
|
|
|
202
202
|
["scan <path>", "find pasted secrets in transcripts before they are sealed in"],
|
|
203
203
|
["list", "browse archives"],
|
|
204
204
|
["search <query>", "search metadata, or contents with --content"],
|
|
205
|
+
["projects", "the project names this vault knows — the ones routing rules pin"],
|
|
205
206
|
["index build", "build the local content index that --content searches"],
|
|
206
207
|
["index status", "what is searchable and what is not yet indexed"],
|
|
207
208
|
["storage targets", "several storages at once, with limits, pins, and priority"],
|
|
@@ -1003,6 +1004,65 @@ async function main() {
|
|
|
1003
1004
|
print(dim("\n On a package we run the storage and choose the provider. Your own bucket means\n your provider, your region, your bill. Either way we hold no key that opens an archive.\n"));
|
|
1004
1005
|
return;
|
|
1005
1006
|
}
|
|
1007
|
+
if (command === "projects") {
|
|
1008
|
+
// Routing rules can pin a project to a destination, and the panel cannot
|
|
1009
|
+
// offer a list: project names never reach the cloud — that is the point.
|
|
1010
|
+
// So this machine, which does know them, prints the exact strings to paste.
|
|
1011
|
+
const { listArchives } = await import("./vault.js");
|
|
1012
|
+
const { claudeProjectFromPath, codexProjectFromRollout } = await import("./adapters.js");
|
|
1013
|
+
const records = await listArchives(dataDir);
|
|
1014
|
+
const seen = new Map();
|
|
1015
|
+
let unknown = 0;
|
|
1016
|
+
let unknownBytes = 0;
|
|
1017
|
+
for (const record of records) {
|
|
1018
|
+
const agentName = record.source.agent;
|
|
1019
|
+
let project = null;
|
|
1020
|
+
try {
|
|
1021
|
+
if (agentName === "claude")
|
|
1022
|
+
project = claudeProjectFromPath(record.source.path).project;
|
|
1023
|
+
else if (agentName === "codex")
|
|
1024
|
+
project = (await codexProjectFromRollout(record.source.path, record.createdAt)).project;
|
|
1025
|
+
}
|
|
1026
|
+
catch {
|
|
1027
|
+
project = null;
|
|
1028
|
+
}
|
|
1029
|
+
if (!project) {
|
|
1030
|
+
unknown += 1;
|
|
1031
|
+
unknownBytes += record.source.bytes ?? 0;
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
const entry = seen.get(project) ?? { archives: 0, bytes: 0, agent: agentName };
|
|
1035
|
+
entry.archives += 1;
|
|
1036
|
+
entry.bytes += record.source.bytes ?? 0;
|
|
1037
|
+
seen.set(project, entry);
|
|
1038
|
+
}
|
|
1039
|
+
const rows = [...seen.entries()].sort((a, b) => b[1].bytes - a[1].bytes);
|
|
1040
|
+
if (json) {
|
|
1041
|
+
print(JSON.stringify({
|
|
1042
|
+
projects: rows.map(([name, v]) => ({ name, archives: v.archives, bytes: v.bytes, agent: v.agent })),
|
|
1043
|
+
withoutProject: { archives: unknown, bytes: unknownBytes }
|
|
1044
|
+
}, null, 2));
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
print(`\n${bold("Projects in this vault")}\n${dim("─".repeat(30))}`);
|
|
1048
|
+
if (rows.length === 0) {
|
|
1049
|
+
print(`\n ${dim("None of the archived sessions recorded a project.")}`);
|
|
1050
|
+
print(` ${dim("Codex records one only when the session wrote its working directory;")}`);
|
|
1051
|
+
print(` ${dim("Claude Code always does. Until a session carries one, a project pin")}`);
|
|
1052
|
+
print(` ${dim("in your routing rules matches nothing — leave that field empty.")}\n`);
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
print(table(rows, [
|
|
1056
|
+
{ header: "project", get: ([name]) => name },
|
|
1057
|
+
{ header: "archives", get: ([, v]) => String(v.archives), align: "right" },
|
|
1058
|
+
{ header: "size", get: ([, v]) => bytes(v.bytes), align: "right" },
|
|
1059
|
+
{ header: "agent", get: ([, v]) => dim(v.agent) }
|
|
1060
|
+
], "no project recorded yet"));
|
|
1061
|
+
if (unknown > 0)
|
|
1062
|
+
print(`\n ${dim(`${unknown} archive${unknown === 1 ? "" : "s"} recorded no project (${bytes(unknownBytes)}).`)}`);
|
|
1063
|
+
print(`\n ${dim("Paste a name into \"Only these projects\" in the account panel to pin it to a destination.")}\n`);
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1006
1066
|
if (command === "open") {
|
|
1007
1067
|
const [vaultRef, destination] = positionals(args);
|
|
1008
1068
|
if (!vaultRef || !destination)
|
package/dist/src/errors.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* between releases; these codes are a contract for the CLI, local API, and MCP.
|
|
4
4
|
* Details are operational only: never place plaintext, phrases, or keys here.
|
|
5
5
|
*/
|
|
6
|
-
export type VaultlineErrorCode = "invalid_argument" | "vault_not_initialized" | "vault_already_initialized" | "config_unsupported_version" | "recovery_phrase_missing" | "recovery_phrase_mismatch" | "archive_not_found" | "source_unreadable" | "destination_exists" | "destination_unwritable" | "path_not_permitted" | "native_restore_unavailable" | "ciphertext_integrity_failed" | "plaintext_integrity_failed" | "hook_payload_invalid" | "queue_job_not_found" | "queue_lease_invalid" | "queue_lease_expired" | "queue_job_exhausted" | "storage_not_configured" | "provider_unsupported" | "signer_not_configured" | "lease_expired" | "unauthorized" | "forbidden" | "internal";
|
|
6
|
+
export type VaultlineErrorCode = "invalid_argument" | "vault_not_initialized" | "vault_already_initialized" | "config_unsupported_version" | "recovery_phrase_missing" | "recovery_phrase_mismatch" | "archive_not_found" | "source_unreadable" | "destination_exists" | "destination_unwritable" | "insufficient_disk_space" | "path_not_permitted" | "native_restore_unavailable" | "ciphertext_integrity_failed" | "plaintext_integrity_failed" | "hook_payload_invalid" | "queue_job_not_found" | "queue_lease_invalid" | "queue_lease_expired" | "queue_job_exhausted" | "storage_not_configured" | "provider_unsupported" | "signer_not_configured" | "lease_expired" | "unauthorized" | "forbidden" | "internal";
|
|
7
7
|
export declare class VaultlineError extends Error {
|
|
8
8
|
readonly code: VaultlineErrorCode;
|
|
9
9
|
readonly details: Record<string, unknown>;
|
package/dist/src/offload.js
CHANGED
|
@@ -320,6 +320,19 @@ export async function checkRemoteCopyMany(dataDir, ids, client) {
|
|
|
320
320
|
export async function materialiseArchive(dataDir, record, client) {
|
|
321
321
|
if (!isV2(record) || !record.offloaded)
|
|
322
322
|
return { path: record.objectPath, release: async () => { } };
|
|
323
|
+
// Reading an offloaded archive means writing it back to disk in full, and
|
|
324
|
+
// this runs in a loop during an index build. On a machine that offloaded
|
|
325
|
+
// BECAUSE it was short of space, that filled the disk and died on ENOSPC
|
|
326
|
+
// mid-build — the exact failure this product exists to prevent. Ask first,
|
|
327
|
+
// and refuse in words that name the archive and the shortfall.
|
|
328
|
+
const { freeBytes } = await import("./disk.js");
|
|
329
|
+
const free = await freeBytes(tmpdir());
|
|
330
|
+
const needed = record.cipher.storedBytes ?? record.remote?.bytes ?? 0;
|
|
331
|
+
const RESERVE = 512 * 1024 * 1024;
|
|
332
|
+
if (free !== undefined && needed > 0 && free < needed + RESERVE) {
|
|
333
|
+
const gb = (n) => `${(n / 1024 ** 3).toFixed(1)} GB`;
|
|
334
|
+
fail("insufficient_disk_space", `Reading archive ${record.id.slice(0, 8)} needs about ${gb(needed)} of temporary space and this machine has ${gb(free)} free. Nothing was written. Free some space, or reclaim what is already archived, and try again.`, { archiveId: record.id, free, needed });
|
|
335
|
+
}
|
|
323
336
|
const ciphertext = await fetchCiphertext(dataDir, record, client);
|
|
324
337
|
if (sha256(ciphertext) !== record.cipher.ciphertextSha256) {
|
|
325
338
|
fail("ciphertext_integrity_failed", `The copy of archive ${record.id} in ${record.offloaded.bucket} is not the archive that was stored there`, { archiveId: record.id });
|
package/package.json
CHANGED
package/web/index.html
CHANGED
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
</header>
|
|
109
109
|
|
|
110
110
|
<section id="gate" class="panel gate pad" hidden>
|
|
111
|
-
<
|
|
111
|
+
<h2>Local token required</h2>
|
|
112
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>
|
|
113
113
|
<form id="token-form">
|
|
114
114
|
<input id="token-input" type="password" autocomplete="off" spellcheck="false" placeholder="local API token" aria-label="Local API token">
|