sealkeep 0.6.1 → 0.7.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/CHANGELOG.md +31 -0
- package/dist/site/index.html +104 -51
- package/dist/site.zip +0 -0
- package/dist/src/cli.js +60 -0
- package/dist/src/doctor.js +38 -0
- package/package.json +1 -1
- package/web/index.html +1 -1
- package/web/setup.html +2 -2
- package/web/style.css +10 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,37 @@
|
|
|
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.7.0 — 2026-08-20
|
|
7
|
+
|
|
8
|
+
Two reviews — does each section deserve to exist, and can a person finish the
|
|
9
|
+
job it promises — and the answers, applied.
|
|
10
|
+
|
|
11
|
+
- **Setup no longer argues for the unrecoverable mistake.** The password path
|
|
12
|
+
said skipping the recovery phrase was "fine". Your password opens the vault on
|
|
13
|
+
that machine only; the screen now says so, names the day it matters, and shows
|
|
14
|
+
the 24 words as what survives it.
|
|
15
|
+
- The account panel leads with the Recovery key card instead of burying it ninth
|
|
16
|
+
of eleven, merges Plan into Your package (they answered the same question),
|
|
17
|
+
makes the package tiers actually start a change instead of looking clickable
|
|
18
|
+
and doing nothing, warns "Delete forever?" with the stake named, puts
|
|
19
|
+
connecting a machine above the list of machines, and stops calling an
|
|
20
|
+
account-wide upload rule "what leaves this machine".
|
|
21
|
+
- Revoked machines fold into one line — they are kept as the record, never
|
|
22
|
+
deleted, but no longer bury the machines that can actually write.
|
|
23
|
+
- `sealkeep doctor` warns when a routing rule pins a project this machine has
|
|
24
|
+
never sealed. The panel cannot check that — project names never reach us —
|
|
25
|
+
so the machine that knows does.
|
|
26
|
+
|
|
27
|
+
## 0.6.2 — 2026-08-20
|
|
28
|
+
|
|
29
|
+
- New: `sealkeep projects` lists the project names this vault knows, with how
|
|
30
|
+
many archives and how much history each holds. Routing rules can pin a
|
|
31
|
+
project to a destination, but the account panel cannot offer a list — project
|
|
32
|
+
names never reach us — so the machine that does know them prints the exact
|
|
33
|
+
strings to paste.
|
|
34
|
+
- The panel's pinning field says what a project is, where to find its name, and
|
|
35
|
+
what happens to sessions that recorded none, instead of an empty box.
|
|
36
|
+
|
|
6
37
|
## 0.6.1 — 2026-08-19
|
|
7
38
|
|
|
8
39
|
- Reading an offloaded archive now checks for free space first. An index build
|
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,14 @@
|
|
|
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)}
|
|
219
|
+
#d-retired details{margin:.5rem 0 .2rem}
|
|
220
|
+
#d-retired summary{font-family:var(--mono);font-size:.76rem;color:var(--soft);cursor:pointer;padding:.35rem 0}
|
|
221
|
+
#d-retired summary:hover{color:var(--ink)}
|
|
222
|
+
#d-retired .lines li{opacity:.75}
|
|
217
223
|
.acct-hero{font-family:var(--mono);font-size:1.55rem;font-weight:700;letter-spacing:-.03em;margin:.9rem 0 0;line-height:1.3}
|
|
224
|
+
h1.acct-hero:empty{display:none}
|
|
218
225
|
.ruleslist{margin:.3rem 0 .9rem;padding-left:1.1rem}
|
|
219
226
|
.ruleslist li{margin:.25rem 0;color:var(--soft)}
|
|
220
227
|
.ruleslist b{color:var(--ink);font-weight:600}
|
|
@@ -229,7 +236,7 @@
|
|
|
229
236
|
.packchip span{font-size:.78rem;color:var(--soft)}
|
|
230
237
|
.packchip.now{border-color:var(--live)}
|
|
231
238
|
.packchip.now i{font-style:normal;font-family:var(--mono);font-size:.66rem;color:var(--live);letter-spacing:.03em}
|
|
232
|
-
#p-account .card
|
|
239
|
+
#p-account .card h2{font-size:1.02rem;margin:.15rem 0 .5rem}
|
|
233
240
|
[id^="anchor-"]{scroll-margin-top:3.4rem}
|
|
234
241
|
.state.durable{color:var(--live)}
|
|
235
242
|
.state.pending{color:var(--soft);opacity:.75}
|
|
@@ -584,7 +591,7 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
584
591
|
<button class="chip" id="a-plan" type="button" title="See your plan">—</button>
|
|
585
592
|
<span class="who" id="a-who">—</span>
|
|
586
593
|
</div>
|
|
587
|
-
<
|
|
594
|
+
<h1 class="acct-hero" id="a-hero"></h1>
|
|
588
595
|
<p class="msg" id="a-summary" style="margin:.4rem 0 0"></p>
|
|
589
596
|
<nav class="acct-nav" aria-label="Account sections">
|
|
590
597
|
<a href="#anchor-storage">Storage</a>
|
|
@@ -597,7 +604,7 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
597
604
|
|
|
598
605
|
<div class="card next" id="a-firstrun" style="display:none">
|
|
599
606
|
<span class="eyebrow">Set up</span>
|
|
600
|
-
<
|
|
607
|
+
<h2 class="lead">Connect your first machine</h2>
|
|
601
608
|
<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
609
|
<div id="pair-home-first"></div>
|
|
603
610
|
</div>
|
|
@@ -608,15 +615,21 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
608
615
|
<span class="saved" id="a-verify-msg" role="status" aria-live="polite"></span>
|
|
609
616
|
</div>
|
|
610
617
|
|
|
618
|
+
<div class="card" id="c-recovery"><span id="anchor-recovery"></span>
|
|
619
|
+
<span class="eyebrow">Recovery key</span>
|
|
620
|
+
<h2 id="e-head">Loading…</h2>
|
|
621
|
+
<p id="e-text"></p>
|
|
622
|
+
</div>
|
|
623
|
+
|
|
611
624
|
<div class="card" id="anchor-storage">
|
|
612
625
|
<span class="eyebrow">Storage</span>
|
|
613
626
|
<div id="a-store-free" style="display:none">
|
|
614
|
-
<
|
|
627
|
+
<h2>You run the bucket</h2>
|
|
615
628
|
<p>Archives go to storage you own, under your own credentials. Sealkeep never holds them, so there is nothing to meter here.</p>
|
|
616
629
|
<p class="empty">Your machine reports what it has stored. Run <code>sealkeep status</code> to see it.</p>
|
|
617
630
|
</div>
|
|
618
631
|
<div id="a-store-paid" style="display:none">
|
|
619
|
-
<
|
|
632
|
+
<h2>What you keep here</h2>
|
|
620
633
|
<div class="kv">
|
|
621
634
|
<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
635
|
<div><span class="k">Included</span><b id="a-quota-b">—</b><span class="cap">in your plan</span></div>
|
|
@@ -631,7 +644,7 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
631
644
|
|
|
632
645
|
<div class="card" id="c-targets"><span id="anchor-routing"></span>
|
|
633
646
|
<span class="eyebrow">Routing</span>
|
|
634
|
-
<
|
|
647
|
+
<h2>Where new archives go</h2>
|
|
635
648
|
<p>Rules live on your account — every machine of the vault follows the same ones.</p>
|
|
636
649
|
<ul class="ruleslist">
|
|
637
650
|
<li><b>Lowest priority number wins.</b></li>
|
|
@@ -641,7 +654,7 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
641
654
|
<p class="msg" id="tg-msg">Loading…</p>
|
|
642
655
|
<div id="tg-wrap" style="display:none;overflow-x:auto">
|
|
643
656
|
<table>
|
|
644
|
-
<thead><tr><th>Target</th><th>Where</th><th>Priority</th><th>Cap (GB)</th><th>
|
|
657
|
+
<thead><tr><th>Target</th><th>Where</th><th>Priority</th><th>Cap (GB)</th><th>Pinned projects</th><th></th></tr></thead>
|
|
645
658
|
<tbody id="tg-body"></tbody>
|
|
646
659
|
</table>
|
|
647
660
|
</div>
|
|
@@ -651,14 +664,17 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
651
664
|
<button class="primary" id="tg-save" type="button">Save routing</button>
|
|
652
665
|
<span class="saved" id="tg-save-msg" role="status" aria-live="polite"></span>
|
|
653
666
|
</div>
|
|
654
|
-
<p class="why" id="tg-note">
|
|
667
|
+
<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>
|
|
668
|
+
<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
669
|
</div>
|
|
656
670
|
|
|
657
671
|
<div class="card" id="anchor-machines">
|
|
658
672
|
<span class="eyebrow">Machines</span>
|
|
659
|
-
<
|
|
673
|
+
<h2>Connected machines</h2>
|
|
660
674
|
<p>Each connected machine signs its own uploads. Revoking one stops it writing here; the archives it already stored are untouched.</p>
|
|
675
|
+
<div id="pair-top"></div>
|
|
661
676
|
<ul class="lines" id="d-list"></ul>
|
|
677
|
+
<div id="d-retired"></div>
|
|
662
678
|
<p class="empty" id="d-msg">Loading…</p>
|
|
663
679
|
|
|
664
680
|
<!-- One pairing block exists on the page. It leads the panel while no
|
|
@@ -691,49 +707,41 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
691
707
|
|
|
692
708
|
<div class="card" id="anchor-archives" style="display:none">
|
|
693
709
|
<span class="eyebrow">Archives</span>
|
|
694
|
-
<
|
|
710
|
+
<h2>What is stored</h2>
|
|
695
711
|
<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
712
|
<p class="msg" id="s-msg">Loading…</p>
|
|
697
713
|
<div id="s-wrap" style="display:none;overflow-x:auto">
|
|
698
714
|
<table>
|
|
699
|
-
<thead><tr><th>Archive</th><th>
|
|
715
|
+
<thead><tr><th>Archive</th><th>Size</th><th>State</th><th>Added</th><th></th></tr></thead>
|
|
700
716
|
<tbody id="s-body"></tbody>
|
|
701
717
|
</table>
|
|
702
718
|
<p class="msg" id="s-foot"></p>
|
|
703
719
|
</div>
|
|
704
720
|
</div>
|
|
705
721
|
|
|
706
|
-
|
|
707
|
-
<span class="eyebrow">Plan</span>
|
|
708
|
-
<div id="a-plan-free" style="display:none">
|
|
709
|
-
<h3>Free — your bucket, your bill</h3>
|
|
710
|
-
<p>Unlimited archives with the same encryption. You supply the storage credentials and pay your provider directly.</p>
|
|
711
|
-
<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
|
-
</div>
|
|
713
|
-
<div id="a-plan-paid" style="display:none">
|
|
714
|
-
<h3>Managed storage</h3>
|
|
715
|
-
<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
|
-
<div class="kv" style="margin-top:1rem">
|
|
717
|
-
<div><span class="k">Subscription</span><b id="a-sub-state">—</b></div>
|
|
718
|
-
<div><span class="k">Renews</span><b id="a-sub-end">—</b></div>
|
|
719
|
-
<div><span class="k">Billed through</span><b id="a-sub-prov">—</b></div>
|
|
720
|
-
</div>
|
|
721
|
-
<p class="empty" id="a-sub-note"></p>
|
|
722
|
-
</div>
|
|
723
|
-
</div>
|
|
722
|
+
|
|
724
723
|
|
|
725
724
|
<div class="card" id="a-pack-card">
|
|
725
|
+
<span id="anchor-plan"></span>
|
|
726
726
|
<span class="eyebrow">Your package</span>
|
|
727
|
-
<
|
|
727
|
+
<h2 id="a-pack-current">—</h2>
|
|
728
728
|
<p class="msg" id="a-pack-context" style="margin:0 0 .8rem"></p>
|
|
729
729
|
<div class="packrow" id="a-packs"></div>
|
|
730
|
+
<p class="empty" id="a-plan-free-note"></p>
|
|
730
731
|
<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>
|
|
732
|
+
<div class="kv" id="a-sub-facts" style="margin-top:1rem">
|
|
733
|
+
<div><span class="k">Subscription</span><b id="a-sub-state">—</b></div>
|
|
734
|
+
<div><span class="k">Renews</span><b id="a-sub-end">—</b></div>
|
|
735
|
+
<div><span class="k">Billed through</span><b id="a-sub-prov">—</b></div>
|
|
736
|
+
</div>
|
|
737
|
+
<p class="empty" id="a-sub-note"></p>
|
|
731
738
|
<p class="why" id="a-pack-note">Changing your package is handled by hand today. Email hello@spala.ai from this address and we will move you within one business day — your archives keep uploading throughout.</p>
|
|
732
739
|
</div>
|
|
733
740
|
|
|
734
741
|
<div class="card">
|
|
735
|
-
<span class="eyebrow">What
|
|
736
|
-
<
|
|
742
|
+
<span class="eyebrow">What gets uploaded</span>
|
|
743
|
+
<h2>Your upload rules</h2>
|
|
744
|
+
<p class="msg" style="margin:.1rem 0 .6rem">Saved to your account — every machine you connect follows these, the same way routing rules do.</p>
|
|
737
745
|
<p>Everything you do not choose here stays local and is never uploaded.</p>
|
|
738
746
|
<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
747
|
<div class="rules">
|
|
@@ -760,15 +768,11 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
760
768
|
</div>
|
|
761
769
|
</div>
|
|
762
770
|
|
|
763
|
-
|
|
764
|
-
<span class="eyebrow">Recovery key</span>
|
|
765
|
-
<h3 id="e-head">Loading…</h3>
|
|
766
|
-
<p id="e-text"></p>
|
|
767
|
-
</div>
|
|
771
|
+
|
|
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">
|
|
@@ -1047,7 +1051,7 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
1047
1051
|
$("a-name").value = user.label || "";
|
|
1048
1052
|
$("a-plan").textContent = paid ? "Managed storage" : "Free";
|
|
1049
1053
|
$("a-plan").className = "chip" + (paid ? " pro" : "");
|
|
1050
|
-
$("a-plan").onclick = () => goTo($("
|
|
1054
|
+
$("a-plan").onclick = () => goTo($("anchor-plan"));
|
|
1051
1055
|
$("a-who").textContent = [
|
|
1052
1056
|
user.email,
|
|
1053
1057
|
"joined " + day(user.created_at),
|
|
@@ -1065,8 +1069,13 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
1065
1069
|
lastPaid = paid;
|
|
1066
1070
|
applyStorageView();
|
|
1067
1071
|
on("anchor-archives", paid); on("nav-archives", paid);
|
|
1068
|
-
|
|
1069
|
-
|
|
1072
|
+
// One card carries billing now. A free account is told what managed
|
|
1073
|
+
// storage would give it; a paying one is told the state of what it bought.
|
|
1074
|
+
$("a-plan-free-note").textContent = paid
|
|
1075
|
+
? ""
|
|
1076
|
+
: "You are on your own storage: unlimited archives, same encryption, your provider's bill. Managed storage means we run the bucket and meter what you store — we still hold no key that opens any of it.";
|
|
1077
|
+
on("a-sub-facts", paid);
|
|
1078
|
+
on("a-plan-free-note", !paid);
|
|
1070
1079
|
|
|
1071
1080
|
if (paid) {
|
|
1072
1081
|
const quota = Number(user.quota_bytes) || 0, used = Number(user.used_bytes) || 0;
|
|
@@ -1113,7 +1122,37 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
1113
1122
|
return deviceCount;
|
|
1114
1123
|
}
|
|
1115
1124
|
$("d-msg").textContent = "";
|
|
1116
|
-
|
|
1125
|
+
// Revocation never deletes a machine — the row stays as the record that
|
|
1126
|
+
// it once had access. On an account that has re-paired a few times that
|
|
1127
|
+
// buries the two machines actually running under a wall of history: 29
|
|
1128
|
+
// rows, 1666 px, nearly two screens, all looking alike. Active machines
|
|
1129
|
+
// are the answer to "what can write to my account right now"; the rest
|
|
1130
|
+
// fold away, counted and reachable, deleted by nobody.
|
|
1131
|
+
const active = devices.filter((d) => d.state !== "revoked");
|
|
1132
|
+
const retired = devices.filter((d) => d.state === "revoked");
|
|
1133
|
+
const foldHost = $("d-retired");
|
|
1134
|
+
foldHost.replaceChildren();
|
|
1135
|
+
if (retired.length) {
|
|
1136
|
+
const fold = document.createElement("details");
|
|
1137
|
+
const summary = document.createElement("summary");
|
|
1138
|
+
summary.textContent = `${retired.length} revoked machine${retired.length === 1 ? "" : "s"} — kept as the record, none can write`;
|
|
1139
|
+
const inner = document.createElement("ul");
|
|
1140
|
+
inner.className = "lines";
|
|
1141
|
+
for (const d of retired) {
|
|
1142
|
+
const li = document.createElement("li");
|
|
1143
|
+
const name = document.createElement("span");
|
|
1144
|
+
name.className = "grow";
|
|
1145
|
+
name.textContent = d.label || "Unnamed machine";
|
|
1146
|
+
const when = document.createElement("span");
|
|
1147
|
+
when.className = "when";
|
|
1148
|
+
when.textContent = "revoked " + ago(d.revoked_at);
|
|
1149
|
+
li.append(name, when);
|
|
1150
|
+
inner.append(li);
|
|
1151
|
+
}
|
|
1152
|
+
fold.append(summary, inner);
|
|
1153
|
+
foldHost.append(fold);
|
|
1154
|
+
}
|
|
1155
|
+
for (const d of active) {
|
|
1117
1156
|
const li = document.createElement("li");
|
|
1118
1157
|
const name = document.createElement("span");
|
|
1119
1158
|
name.className = "grow";
|
|
@@ -1134,7 +1173,13 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
1134
1173
|
// Two-step, like archive deletion: revoking locks a machine out of
|
|
1135
1174
|
// its own backups, so a stray click must not do it.
|
|
1136
1175
|
if (btn.dataset.armed !== "yes") {
|
|
1137
|
-
btn.dataset.armed = "yes";
|
|
1176
|
+
btn.dataset.armed = "yes";
|
|
1177
|
+
// The local dashboard escalates to "Destroy forever?" when it can
|
|
1178
|
+
// prove a copy is the last one. This page cannot see the customer's
|
|
1179
|
+
// disk, so it must not claim either way — it names the stake and
|
|
1180
|
+
// lets them decide.
|
|
1181
|
+
btn.textContent = "Delete forever?";
|
|
1182
|
+
btn.title = "If your machine already reclaimed the source, this is the only copy";
|
|
1138
1183
|
setTimeout(() => {
|
|
1139
1184
|
if (btn.dataset.armed === "yes") { btn.dataset.armed = "no"; btn.textContent = "Revoke"; }
|
|
1140
1185
|
}, 4000);
|
|
@@ -1313,7 +1358,7 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
1313
1358
|
capIn.addEventListener("input", () => { t.maxGb = capIn.value ? Number(capIn.value) : undefined; });
|
|
1314
1359
|
cap.append(capIn);
|
|
1315
1360
|
const proj = document.createElement("td");
|
|
1316
|
-
const projIn = document.createElement("input"); projIn.placeholder = "
|
|
1361
|
+
const projIn = document.createElement("input"); projIn.placeholder = "empty = every project"; projIn.style.minWidth = "9rem";
|
|
1317
1362
|
projIn.value = (t.projects || []).join(", ");
|
|
1318
1363
|
projIn.setAttribute("aria-label", "Projects pinned to " + providerLabel(t.provider));
|
|
1319
1364
|
projIn.addEventListener("input", () => {
|
|
@@ -1485,7 +1530,7 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
1485
1530
|
: paidPlan ? "Sealkeep cloud" : "the bucket each machine is configured with");
|
|
1486
1531
|
const nothingYet = devices === 0 && Number(user.used_bytes) === 0;
|
|
1487
1532
|
on("a-firstrun", nothingYet);
|
|
1488
|
-
$(nothingYet ? "pair-home-first" : "pair-
|
|
1533
|
+
$(nothingYet ? "pair-home-first" : "pair-top").append($("pair"));
|
|
1489
1534
|
$("pair-side").textContent = nothingYet ? "Your pairing code" : "Pairing code";
|
|
1490
1535
|
if (devices === 0)
|
|
1491
1536
|
$("d-msg").textContent = nothingYet
|
|
@@ -1738,8 +1783,16 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
1738
1783
|
const host = $("a-packs");
|
|
1739
1784
|
while (host.firstChild) host.removeChild(host.firstChild);
|
|
1740
1785
|
for (const pack of PACKAGES.filter((p) => p.managed)) {
|
|
1741
|
-
const chip = document.createElement("span");
|
|
1786
|
+
const chip = document.createElement(pack.id === current ? "span" : "a");
|
|
1742
1787
|
chip.className = "packchip" + (pack.id === current ? " now" : "");
|
|
1788
|
+
if (pack.id !== current) {
|
|
1789
|
+
// It looked like a picker and did nothing when clicked. Changing a
|
|
1790
|
+
// package is handled by a person today, so the chip starts that
|
|
1791
|
+
// message rather than pretending to be a checkout.
|
|
1792
|
+
chip.href = "mailto:hello@spala.ai?subject=" + encodeURIComponent(`Move me to ${pack.name}`)
|
|
1793
|
+
+ "&body=" + encodeURIComponent(`Please move my account to ${pack.name}${pack.managed ? ` ($${pack.monthly} a month)` : ""}.`);
|
|
1794
|
+
chip.title = `Ask us to move you to ${pack.name}`;
|
|
1795
|
+
}
|
|
1743
1796
|
const name = document.createElement("b"); name.textContent = pack.name;
|
|
1744
1797
|
const price = document.createElement("span"); price.textContent = `$${pack.monthly}/mo`;
|
|
1745
1798
|
chip.append(name, price);
|
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/doctor.js
CHANGED
|
@@ -169,6 +169,44 @@ export async function runDoctor(dataDir, env = process.env) {
|
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
171
|
catch { /* coverage is advisory; a broken sidecar must not fail doctor */ }
|
|
172
|
+
// A routing rule can pin a project to a destination, and the account panel
|
|
173
|
+
// cannot check the name: project names never reach the cloud. So a typo is
|
|
174
|
+
// saved happily and silently routes nothing, forever. This machine knows the
|
|
175
|
+
// real names, so it is the one that can say.
|
|
176
|
+
try {
|
|
177
|
+
const { resolveTargets } = await import("./storage-targets.js");
|
|
178
|
+
const targets = await resolveTargets(dataDir);
|
|
179
|
+
const pinned = [...new Set(targets.flatMap((target) => target.projects ?? []))];
|
|
180
|
+
if (pinned.length > 0) {
|
|
181
|
+
const { listArchives } = await import("./vault.js");
|
|
182
|
+
const { claudeProjectFromPath, codexProjectFromRollout } = await import("./adapters.js");
|
|
183
|
+
const records = await listArchives(dataDir).catch(() => []);
|
|
184
|
+
const known = new Set();
|
|
185
|
+
for (const record of records) {
|
|
186
|
+
try {
|
|
187
|
+
const name = record.source.agent === "claude"
|
|
188
|
+
? claudeProjectFromPath(record.source.path).project
|
|
189
|
+
: record.source.agent === "codex"
|
|
190
|
+
? (await codexProjectFromRollout(record.source.path, record.createdAt)).project
|
|
191
|
+
: null;
|
|
192
|
+
if (name)
|
|
193
|
+
known.add(name);
|
|
194
|
+
}
|
|
195
|
+
catch { /* a project we cannot read is not evidence of a bad pin */ }
|
|
196
|
+
}
|
|
197
|
+
const unmatched = pinned.filter((name) => !known.has(name));
|
|
198
|
+
if (unmatched.length === 0) {
|
|
199
|
+
checks.push({ name: "routing-pins", status: "pass", detail: `${pinned.length} pinned project${pinned.length === 1 ? "" : "s"} match sessions on this machine.` });
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
checks.push({
|
|
203
|
+
name: "routing-pins", status: "warn",
|
|
204
|
+
detail: `${unmatched.length === pinned.length ? "No" : unmatched.length} pinned project${unmatched.length === 1 ? "" : "s"} match anything sealed here: ${unmatched.slice(0, 3).join(", ")}. Those rules route nothing. Run \`sealkeep projects\` for the names this vault actually knows${known.size ? `, such as ${[...known].slice(0, 2).join(", ")}` : ""}.`
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch { /* routing rules are advisory here; never fail doctor over them */ }
|
|
172
210
|
// Where the daemon actually looks: an explicit env var, or the phrase the
|
|
173
211
|
// wizard put in this machine's keystore. This used to test only the env var,
|
|
174
212
|
// so a machine set up correctly through the window — phrase in the keystore,
|
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">
|
package/web/setup.html
CHANGED
|
@@ -267,12 +267,12 @@
|
|
|
267
267
|
<p>It unlocks with your password on this machine. If you ever lose the password, the phrase below is the only other way in — which is a good reason to spend a minute on it now.</p>
|
|
268
268
|
</div>
|
|
269
269
|
<details class="panel pad explain" id="phrase-fold">
|
|
270
|
-
<summary>
|
|
270
|
+
<summary>Show the 24 words that survive this machine</summary>
|
|
271
271
|
<p id="fold-note"></p>
|
|
272
272
|
<ol class="phrase" id="fold-phrase-words"></ol>
|
|
273
273
|
</details>
|
|
274
274
|
<div class="wizfoot">
|
|
275
|
-
<p class="note">
|
|
275
|
+
<p class="note warn"><b>Your password opens this vault on this machine only.</b> It is kept in this machine's keystore — it does not travel. If this machine is lost or wiped, the 24 words above are the only thing that opens the archives, and nobody, including us, can recreate them. Take them now, or accept that this vault dies with this machine.</p>
|
|
276
276
|
<div class="actions"><button type="button" class="action primary" id="finish-password">Finish setup</button></div>
|
|
277
277
|
</div>
|
|
278
278
|
</div>
|
package/web/style.css
CHANGED
|
@@ -674,6 +674,16 @@ nav.steps .step.locked.current:hover { background: var(--ink-2); color: #fff; }
|
|
|
674
674
|
.callout h3 { margin: 0; }
|
|
675
675
|
.callout h3 + p { margin-top: -6px; }
|
|
676
676
|
.callout.warn { border-left: 3px solid var(--alert); }
|
|
677
|
+
/* A note that carries a consequence reads like one. Used where setup asks
|
|
678
|
+
whether to keep the 24 words: the copy there used to call skipping them
|
|
679
|
+
"fine", which is true until the machine is lost and then permanently false. */
|
|
680
|
+
.note.warn {
|
|
681
|
+
border-left: 3px solid var(--alert);
|
|
682
|
+
padding: 10px 12px;
|
|
683
|
+
background: color-mix(in srgb, var(--alert) 7%, transparent);
|
|
684
|
+
color: var(--ink);
|
|
685
|
+
}
|
|
686
|
+
.note.warn b { color: var(--alert); }
|
|
677
687
|
.callout.warn p { color: var(--text); }
|
|
678
688
|
ul.plain { margin: 0; padding-left: 19px; display: grid; gap: 8px; color: var(--text-2); max-width: 72ch; }
|
|
679
689
|
ul.plain b { color: var(--text); }
|