sealkeep 0.5.2 → 0.5.3
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 +10 -0
- package/dist/site/index.html +213 -174
- package/dist/site.zip +0 -0
- package/dist/src/chunk-store.js +6 -0
- package/dist/src/storage-targets.d.ts +3 -0
- package/dist/src/storage-targets.js +19 -0
- package/dist/src/types.d.ts +3 -0
- package/dist/src/upload.js +4 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
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.3 — 2026-08-19
|
|
7
|
+
|
|
8
|
+
The account now knows what every destination holds. Machines stamp
|
|
9
|
+
per-target usage (Drive, your bucket, cloud) into the routing rules they
|
|
10
|
+
already sync, so the panel's new headline reads like the truth:
|
|
11
|
+
"3.7 GB in Sealkeep cloud · 56 MB in Google Drive". The panel also got
|
|
12
|
+
its polish pass: status headline first, routing rules as three short
|
|
13
|
+
bullets, per-row "holds X", compact editors, your current package leads
|
|
14
|
+
with the tiers as chips, and Drive shows as "folder <name>".
|
|
15
|
+
|
|
6
16
|
## 0.5.2 — 2026-08-19
|
|
7
17
|
|
|
8
18
|
- "Manage in cloud" now opens sealkeep.spala.ai — the panel URL was still
|
package/dist/site/index.html
CHANGED
|
@@ -172,7 +172,7 @@
|
|
|
172
172
|
|
|
173
173
|
.ph{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem .9rem;background:var(--card);
|
|
174
174
|
border:1px solid var(--rule);border-radius:4px;padding:1.2rem 1.3rem;margin-bottom:1.3rem}
|
|
175
|
-
.ph .name{font-size:1.
|
|
175
|
+
.ph .name{font-size:1.15rem;font-weight:650;letter-spacing:-.02em}
|
|
176
176
|
.ph .who{font-family:var(--mono);font-size:.75rem;color:var(--soft);flex:1 1 100%}
|
|
177
177
|
/* The strip takes focus when the panel opens so a screen reader is told whose
|
|
178
178
|
account this is. It is not in the tab order, so nobody can arrive here by
|
|
@@ -193,11 +193,14 @@
|
|
|
193
193
|
.warncard{border-color:var(--warn)}
|
|
194
194
|
.warncard .eyebrow{color:var(--warn)}
|
|
195
195
|
|
|
196
|
-
.kv{display:grid;gap
|
|
196
|
+
.kv{display:grid;gap:.75rem;grid-template-columns:repeat(auto-fit,minmax(9rem,1fr))}
|
|
197
|
+
.kv>div{border:1px solid var(--rule);border-radius:4px;padding:.9rem 1rem}
|
|
198
|
+
.kv .cap{display:block;font-size:.78rem;color:var(--soft);margin-top:.25rem}
|
|
197
199
|
.kv .k{display:block;font-family:var(--mono);font-size:.72rem;color:var(--soft);margin-bottom:.2rem}
|
|
198
|
-
.kv b{font-family:var(--mono);font-size:
|
|
199
|
-
.
|
|
200
|
-
.bar
|
|
200
|
+
.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}
|
|
202
|
+
.bar{height:8px;background:var(--rule);border-radius:4px;margin:1.05rem 0 .4rem;overflow:hidden}
|
|
203
|
+
.bar i{display:block;height:100%;width:0;min-width:6px;background:var(--live);transition:width .5s ease}
|
|
201
204
|
|
|
202
205
|
table{width:100%;border-collapse:collapse;font-size:.9rem;min-width:31rem}
|
|
203
206
|
th{text-align:left;font-family:var(--mono);font-size:.72rem;color:var(--soft);font-weight:400;
|
|
@@ -205,7 +208,29 @@
|
|
|
205
208
|
td{padding:.68rem .65rem;border-bottom:1px solid var(--rule)}
|
|
206
209
|
tr:last-child td{border-bottom:0}
|
|
207
210
|
td code{font-size:.86em;color:var(--soft)}
|
|
208
|
-
.state{font-family:var(--mono);font-size:.
|
|
211
|
+
.state{font-family:var(--mono);font-size:.72rem;border:1px solid currentColor;border-radius:3px;padding:.18rem .55rem;white-space:nowrap}
|
|
212
|
+
.choice .t,.choice .d{font-family:var(--sans)}
|
|
213
|
+
.cellsub{display:block;font-family:var(--sans);font-size:.75rem;color:var(--soft);margin-top:.15rem}
|
|
214
|
+
.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:.28rem .8rem}
|
|
216
|
+
.acct-nav a:hover{color:var(--ink);border-color:var(--ink)}
|
|
217
|
+
.acct-hero{font-family:var(--mono);font-size:1.55rem;font-weight:700;letter-spacing:-.03em;margin:.9rem 0 0;line-height:1.3}
|
|
218
|
+
.ruleslist{margin:.3rem 0 .9rem;padding-left:1.1rem}
|
|
219
|
+
.ruleslist li{margin:.25rem 0;color:var(--soft)}
|
|
220
|
+
.ruleslist b{color:var(--ink);font-weight:600}
|
|
221
|
+
#tg-body input,#tg-body select{padding:.35rem .5rem;font-size:.85rem;border:1px solid var(--rule);border-radius:4px;background:transparent;color:var(--ink)}
|
|
222
|
+
#tg-body input[type=number]{width:4.2rem}
|
|
223
|
+
#tg-body input:focus,#tg-body select:focus{border-color:var(--live);outline:none}
|
|
224
|
+
#tg-body tr:hover td{background:color-mix(in srgb, var(--rule) 22%, transparent)}
|
|
225
|
+
#tg-save-msg{display:block;margin-top:.6rem}
|
|
226
|
+
.packrow{display:flex;gap:.6rem;flex-wrap:wrap;margin:.4rem 0 .9rem}
|
|
227
|
+
.packchip{display:flex;flex-direction:column;gap:.1rem;border:1px solid var(--rule);border-radius:6px;padding:.6rem .95rem;min-width:6.4rem}
|
|
228
|
+
.packchip b{font-family:var(--mono);font-size:.95rem}
|
|
229
|
+
.packchip span{font-size:.78rem;color:var(--soft)}
|
|
230
|
+
.packchip.now{border-color:var(--live)}
|
|
231
|
+
.packchip.now i{font-style:normal;font-family:var(--mono);font-size:.66rem;color:var(--live);letter-spacing:.03em}
|
|
232
|
+
#p-account .card h3{font-size:1.02rem;margin:.15rem 0 .5rem}
|
|
233
|
+
[id^="anchor-"]{scroll-margin-top:3.4rem}
|
|
209
234
|
.state.durable{color:var(--live)}
|
|
210
235
|
.state.pending{color:var(--soft);opacity:.75}
|
|
211
236
|
.del{background:none;color:var(--soft);border:1px solid var(--rule);padding:.3rem .7rem;
|
|
@@ -559,6 +584,16 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
559
584
|
<button class="chip" id="a-plan" type="button" title="See your plan">—</button>
|
|
560
585
|
<span class="who" id="a-who">—</span>
|
|
561
586
|
</div>
|
|
587
|
+
<p class="acct-hero" id="a-hero"></p>
|
|
588
|
+
<p class="msg" id="a-summary" style="margin:.4rem 0 0"></p>
|
|
589
|
+
<nav class="acct-nav" aria-label="Account sections">
|
|
590
|
+
<a href="#anchor-storage">Storage</a>
|
|
591
|
+
<a href="#anchor-routing">Routing</a>
|
|
592
|
+
<a href="#anchor-machines">Machines</a>
|
|
593
|
+
<a href="#anchor-archives" id="nav-archives">Archives</a>
|
|
594
|
+
<a href="#anchor-plan">Plan</a>
|
|
595
|
+
<a href="#anchor-recovery">Recovery</a>
|
|
596
|
+
</nav>
|
|
562
597
|
|
|
563
598
|
<div class="card next" id="a-firstrun" style="display:none">
|
|
564
599
|
<span class="eyebrow">Set up</span>
|
|
@@ -573,7 +608,7 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
573
608
|
<span class="saved" id="a-verify-msg" role="status" aria-live="polite"></span>
|
|
574
609
|
</div>
|
|
575
610
|
|
|
576
|
-
<div class="card">
|
|
611
|
+
<div class="card" id="anchor-storage">
|
|
577
612
|
<span class="eyebrow">Storage</span>
|
|
578
613
|
<div id="a-store-free" style="display:none">
|
|
579
614
|
<h3>You run the bucket</h3>
|
|
@@ -581,19 +616,83 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
581
616
|
<p class="empty">Your machine reports what it has stored. Run <code>sealkeep status</code> to see it.</p>
|
|
582
617
|
</div>
|
|
583
618
|
<div id="a-store-paid" style="display:none">
|
|
619
|
+
<h3>What you keep here</h3>
|
|
584
620
|
<div class="kv">
|
|
585
|
-
<div><span class="k">Stored</span><b id="a-used">—</b></div>
|
|
586
|
-
<div><span class="k">Included</span><b id="a-quota-b">—</b></div>
|
|
587
|
-
<div><span class="k">Archives</span><b id="a-count">—</b></div>
|
|
621
|
+
<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
|
+
<div><span class="k">Included</span><b id="a-quota-b">—</b><span class="cap">in your plan</span></div>
|
|
623
|
+
<div><span class="k">Archives</span><b id="a-count">—</b><span class="cap">chunk folders count as one</span></div>
|
|
624
|
+
<div><span class="k">Machines</span><b id="a-machines">—</b><span class="cap">signing uploads</span></div>
|
|
625
|
+
<div><span class="k">Destinations</span><b id="a-dest">—</b><span class="cap">where new seals go</span></div>
|
|
588
626
|
</div>
|
|
589
627
|
<div class="bar"><i id="a-bar"></i></div>
|
|
590
628
|
<p class="msg" id="a-quota"></p>
|
|
591
629
|
</div>
|
|
592
630
|
</div>
|
|
593
631
|
|
|
594
|
-
<div class="card" id="c-
|
|
632
|
+
<div class="card" id="c-targets"><span id="anchor-routing"></span>
|
|
633
|
+
<span class="eyebrow">Routing</span>
|
|
634
|
+
<h3>Where new archives go</h3>
|
|
635
|
+
<p>Rules live on your account — every machine of the vault follows the same ones.</p>
|
|
636
|
+
<ul class="ruleslist">
|
|
637
|
+
<li><b>Lowest priority number wins.</b></li>
|
|
638
|
+
<li><b>A pinned project always goes to its target.</b></li>
|
|
639
|
+
<li><b>Full or capped targets pass to the next</b> — Sealkeep cloud is the fallback.</li>
|
|
640
|
+
</ul>
|
|
641
|
+
<p class="msg" id="tg-msg">Loading…</p>
|
|
642
|
+
<div id="tg-wrap" style="display:none;overflow-x:auto">
|
|
643
|
+
<table>
|
|
644
|
+
<thead><tr><th>Target</th><th>Where</th><th>Priority</th><th>Cap (GB)</th><th>Only these projects</th><th></th></tr></thead>
|
|
645
|
+
<tbody id="tg-body"></tbody>
|
|
646
|
+
</table>
|
|
647
|
+
</div>
|
|
648
|
+
<div class="inline" style="margin-top:.9rem">
|
|
649
|
+
<button class="ghost" id="tg-add-drive" type="button">Add Google Drive</button>
|
|
650
|
+
<button class="ghost" id="tg-add-bucket" type="button">Add my own bucket</button>
|
|
651
|
+
<button class="primary" id="tg-save" type="button">Save routing</button>
|
|
652
|
+
<span class="saved" id="tg-save-msg" role="status" aria-live="polite"></span>
|
|
653
|
+
</div>
|
|
654
|
+
<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>
|
|
655
|
+
</div>
|
|
656
|
+
|
|
657
|
+
<div class="card" id="anchor-machines">
|
|
658
|
+
<span class="eyebrow">Machines</span>
|
|
659
|
+
<h3>Connected machines</h3>
|
|
660
|
+
<p>Each connected machine signs its own uploads. Revoking one stops it writing here; the archives it already stored are untouched.</p>
|
|
661
|
+
<ul class="lines" id="d-list"></ul>
|
|
662
|
+
<p class="empty" id="d-msg">Loading…</p>
|
|
663
|
+
|
|
664
|
+
<!-- One pairing block exists on the page. It leads the panel while no
|
|
665
|
+
machine is connected, and JS moves it back into this slot once one
|
|
666
|
+
is, so the same code is never printed in two places. -->
|
|
667
|
+
<div id="pair-home-more">
|
|
668
|
+
<div class="pair" id="pair">
|
|
669
|
+
<p class="side" id="pair-side" style="display:none">Pairing code</p>
|
|
670
|
+
<!-- Grouped and labelled, so the copy button is announced as
|
|
671
|
+
belonging to the code rather than as a bare "Copy". -->
|
|
672
|
+
<div class="pair-field" id="pair-shown" role="group" aria-labelledby="pair-side" style="display:none">
|
|
673
|
+
<code class="pair-code" id="pair-code"></code>
|
|
674
|
+
<button class="primary" id="pair-copy" type="button">Copy</button>
|
|
675
|
+
</div>
|
|
676
|
+
<p class="fine" id="pair-note"></p>
|
|
677
|
+
<div class="inline" id="pair-ask">
|
|
678
|
+
<button class="ghost" id="pair-go" type="button">Show a pairing code</button>
|
|
679
|
+
<span class="saved" id="pair-msg" role="status" aria-live="polite"></span>
|
|
680
|
+
</div>
|
|
681
|
+
<details class="alt" id="pair-alt" style="display:none">
|
|
682
|
+
<summary>Set the machine up from the terminal instead</summary>
|
|
683
|
+
<p class="empty">The same code, typed as one command. Needs Node 22 or newer.</p>
|
|
684
|
+
<div class="pair-field"><pre id="pair-cli"></pre>
|
|
685
|
+
<button class="ghost" id="pair-cli-copy" type="button">Copy</button>
|
|
686
|
+
</div>
|
|
687
|
+
</details>
|
|
688
|
+
</div>
|
|
689
|
+
</div>
|
|
690
|
+
</div>
|
|
691
|
+
|
|
692
|
+
<div class="card" id="anchor-archives" style="display:none">
|
|
595
693
|
<span class="eyebrow">Archives</span>
|
|
596
|
-
<
|
|
694
|
+
<h3>What is stored</h3>
|
|
695
|
+
<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>
|
|
597
696
|
<p class="msg" id="s-msg">Loading…</p>
|
|
598
697
|
<div id="s-wrap" style="display:none;overflow-x:auto">
|
|
599
698
|
<table>
|
|
@@ -604,16 +703,16 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
604
703
|
</div>
|
|
605
704
|
</div>
|
|
606
705
|
|
|
607
|
-
<div class="card" id="c-plan">
|
|
706
|
+
<div class="card" id="c-plan"><span id="anchor-plan"></span>
|
|
608
707
|
<span class="eyebrow">Plan</span>
|
|
609
708
|
<div id="a-plan-free" style="display:none">
|
|
610
709
|
<h3>Free — your bucket, your bill</h3>
|
|
611
710
|
<p>Unlimited archives with the same encryption. You supply the storage credentials and pay your provider directly.</p>
|
|
612
|
-
<p class="empty">Want us to run storage instead?
|
|
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>
|
|
613
712
|
</div>
|
|
614
713
|
<div id="a-plan-paid" style="display:none">
|
|
615
714
|
<h3>Managed storage</h3>
|
|
616
|
-
<p>We run the bucket and meter what you store. We still hold no key that opens any of it.</p>
|
|
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>
|
|
617
716
|
<div class="kv" style="margin-top:1rem">
|
|
618
717
|
<div><span class="k">Subscription</span><b id="a-sub-state">—</b></div>
|
|
619
718
|
<div><span class="k">Renews</span><b id="a-sub-end">—</b></div>
|
|
@@ -625,13 +724,16 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
625
724
|
|
|
626
725
|
<div class="card" id="a-pack-card">
|
|
627
726
|
<span class="eyebrow">Your package</span>
|
|
628
|
-
<
|
|
629
|
-
<
|
|
727
|
+
<h3 id="a-pack-current">—</h3>
|
|
728
|
+
<p class="msg" id="a-pack-context" style="margin:0 0 .8rem"></p>
|
|
729
|
+
<div class="packrow" id="a-packs"></div>
|
|
730
|
+
<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>
|
|
630
731
|
<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>
|
|
631
732
|
</div>
|
|
632
733
|
|
|
633
734
|
<div class="card">
|
|
634
735
|
<span class="eyebrow">What leaves this machine</span>
|
|
736
|
+
<h3>Your upload rules</h3>
|
|
635
737
|
<p>Everything you do not choose here stays local and is never uploaded.</p>
|
|
636
738
|
<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>
|
|
637
739
|
<div class="rules">
|
|
@@ -658,94 +760,7 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
658
760
|
</div>
|
|
659
761
|
</div>
|
|
660
762
|
|
|
661
|
-
<div class="card">
|
|
662
|
-
<span class="eyebrow">Where your archives live</span>
|
|
663
|
-
<p>Archives are encrypted on your machine before anything moves, on both options. This only decides who owns the bucket the ciphertext lands in.</p>
|
|
664
|
-
<div class="rules">
|
|
665
|
-
<label class="choice" data-on="yes">
|
|
666
|
-
<input type="radio" name="stor" value="managed" checked>
|
|
667
|
-
<span><span class="t">Let Sealkeep hold it</span>
|
|
668
|
-
<span class="d">Pick how many gigabytes and stop thinking about it. No bucket to create, no IAM policy, no access keys, no console. We choose the provider and absorb the storage bill inside your plan.</span></span>
|
|
669
|
-
</label>
|
|
670
|
-
<label class="choice" data-on="no">
|
|
671
|
-
<input type="radio" name="stor" value="byo">
|
|
672
|
-
<span><span class="t">Use my own bucket</span>
|
|
673
|
-
<span class="d">Your storage account, your bill, your retention rules. Sealkeep records the destination and nothing else — your access keys stay on your machine and are never sent here.</span>
|
|
674
|
-
<div class="row2" style="margin-top:.7rem">
|
|
675
|
-
<div><label for="st-prov">Provider</label>
|
|
676
|
-
<select id="st-prov">
|
|
677
|
-
<option value="r2">Cloudflare R2 — no egress fees</option>
|
|
678
|
-
<option value="s3">Amazon S3 or any S3-compatible</option>
|
|
679
|
-
<option value="gcs">Google Cloud Storage</option>
|
|
680
|
-
</select></div>
|
|
681
|
-
<div><label for="st-bucket">Bucket</label><input id="st-bucket" placeholder="my-sealkeep-archives"></div>
|
|
682
|
-
</div>
|
|
683
|
-
<div class="row2">
|
|
684
|
-
<div><label for="st-region">Region</label><input id="st-region" placeholder="auto"></div>
|
|
685
|
-
<div><label for="st-prefix">Prefix</label><input id="st-prefix" placeholder="sealkeep/"></div>
|
|
686
|
-
</div>
|
|
687
|
-
</span>
|
|
688
|
-
</label>
|
|
689
|
-
<button class="primary" id="st-save" type="button" style="margin-top:1.1rem">Save destination</button>
|
|
690
|
-
<span class="saved" id="st-msg" role="status" aria-live="polite"></span>
|
|
691
|
-
</div>
|
|
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>
|
|
693
|
-
</div>
|
|
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
|
-
|
|
714
|
-
<div class="card">
|
|
715
|
-
<span class="eyebrow">Machines</span>
|
|
716
|
-
<p>Each connected machine signs its own uploads. Revoking one stops it writing here; the archives it already stored are untouched.</p>
|
|
717
|
-
<ul class="lines" id="d-list"></ul>
|
|
718
|
-
<p class="empty" id="d-msg">Loading…</p>
|
|
719
|
-
|
|
720
|
-
<!-- One pairing block exists on the page. It leads the panel while no
|
|
721
|
-
machine is connected, and JS moves it back into this slot once one
|
|
722
|
-
is, so the same code is never printed in two places. -->
|
|
723
|
-
<div id="pair-home-more">
|
|
724
|
-
<div class="pair" id="pair">
|
|
725
|
-
<p class="side" id="pair-side" style="display:none">Pairing code</p>
|
|
726
|
-
<!-- Grouped and labelled, so the copy button is announced as
|
|
727
|
-
belonging to the code rather than as a bare "Copy". -->
|
|
728
|
-
<div class="pair-field" id="pair-shown" role="group" aria-labelledby="pair-side" style="display:none">
|
|
729
|
-
<code class="pair-code" id="pair-code"></code>
|
|
730
|
-
<button class="primary" id="pair-copy" type="button">Copy</button>
|
|
731
|
-
</div>
|
|
732
|
-
<p class="fine" id="pair-note"></p>
|
|
733
|
-
<div class="inline" id="pair-ask">
|
|
734
|
-
<button class="ghost" id="pair-go" type="button">Show a pairing code</button>
|
|
735
|
-
<span class="saved" id="pair-msg" role="status" aria-live="polite"></span>
|
|
736
|
-
</div>
|
|
737
|
-
<details class="alt" id="pair-alt" style="display:none">
|
|
738
|
-
<summary>Set the machine up from the terminal instead</summary>
|
|
739
|
-
<p class="empty">The same code, typed as one command. Needs Node 22 or newer.</p>
|
|
740
|
-
<div class="pair-field"><pre id="pair-cli"></pre>
|
|
741
|
-
<button class="ghost" id="pair-cli-copy" type="button">Copy</button>
|
|
742
|
-
</div>
|
|
743
|
-
</details>
|
|
744
|
-
</div>
|
|
745
|
-
</div>
|
|
746
|
-
</div>
|
|
747
|
-
|
|
748
|
-
<div class="card" id="c-recovery">
|
|
763
|
+
<div class="card" id="c-recovery"><span id="anchor-recovery"></span>
|
|
749
764
|
<span class="eyebrow">Recovery key</span>
|
|
750
765
|
<h3 id="e-head">Loading…</h3>
|
|
751
766
|
<p id="e-text"></p>
|
|
@@ -753,6 +768,7 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
753
768
|
|
|
754
769
|
<div class="card">
|
|
755
770
|
<span class="eyebrow">Activity</span>
|
|
771
|
+
<h3>What happened lately</h3>
|
|
756
772
|
<p>Every change made to this account, newest first.</p>
|
|
757
773
|
<ul class="lines" id="v-list"></ul>
|
|
758
774
|
<p class="empty" id="v-msg">Loading…</p>
|
|
@@ -760,6 +776,7 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
760
776
|
|
|
761
777
|
<div class="card">
|
|
762
778
|
<span class="eyebrow">Account</span>
|
|
779
|
+
<h3>Name and email</h3>
|
|
763
780
|
<label for="a-name">Account name</label>
|
|
764
781
|
<div class="inline">
|
|
765
782
|
<input id="a-name" maxlength="60" autocomplete="off">
|
|
@@ -994,7 +1011,7 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
994
1011
|
// destination arrives; the plan is only the opening guess.
|
|
995
1012
|
lastPaid = paid;
|
|
996
1013
|
applyStorageView();
|
|
997
|
-
on("
|
|
1014
|
+
on("anchor-archives", paid); on("nav-archives", paid);
|
|
998
1015
|
on("a-plan-free", !paid);
|
|
999
1016
|
on("a-plan-paid", paid);
|
|
1000
1017
|
|
|
@@ -1194,9 +1211,39 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
1194
1211
|
$("tg-msg").textContent = "";
|
|
1195
1212
|
for (const t of targetRows) {
|
|
1196
1213
|
const tr = document.createElement("tr");
|
|
1197
|
-
const name = document.createElement("td");
|
|
1214
|
+
const name = document.createElement("td");
|
|
1215
|
+
if (t.__editable) {
|
|
1216
|
+
const sel = document.createElement("select");
|
|
1217
|
+
for (const [v, l] of [["r2", "Cloudflare R2"], ["s3", "S3-compatible"], ["gcs", "Google Cloud"]]) {
|
|
1218
|
+
const o = document.createElement("option"); o.value = v; o.textContent = l; sel.append(o);
|
|
1219
|
+
}
|
|
1220
|
+
sel.value = t.provider;
|
|
1221
|
+
sel.setAttribute("aria-label", "Provider");
|
|
1222
|
+
sel.addEventListener("change", () => { t.provider = sel.value; });
|
|
1223
|
+
name.append(sel);
|
|
1224
|
+
} else name.textContent = providerLabel(t.provider);
|
|
1198
1225
|
const where = document.createElement("td");
|
|
1199
|
-
|
|
1226
|
+
if (t.__editable) {
|
|
1227
|
+
const b = document.createElement("input"); b.placeholder = "bucket"; b.style.width = "8rem"; b.value = t.bucket || "";
|
|
1228
|
+
b.setAttribute("aria-label", "Bucket name");
|
|
1229
|
+
b.addEventListener("input", () => { t.bucket = b.value.trim(); });
|
|
1230
|
+
const p = document.createElement("input"); p.placeholder = "prefix/"; p.style.width = "6rem"; p.value = t.prefix || "";
|
|
1231
|
+
p.setAttribute("aria-label", "Prefix");
|
|
1232
|
+
p.addEventListener("input", () => { t.prefix = p.value.trim() || undefined; });
|
|
1233
|
+
where.append(b, document.createTextNode(" "), p);
|
|
1234
|
+
} else {
|
|
1235
|
+
const wc = document.createElement("code");
|
|
1236
|
+
wc.textContent = t.provider === "vaultline" ? "managed"
|
|
1237
|
+
: t.provider === "gdrive" ? "folder " + (t.bucket || "Sealkeep")
|
|
1238
|
+
: [t.bucket, t.prefix].filter(Boolean).join("/") || "—";
|
|
1239
|
+
where.append(wc);
|
|
1240
|
+
if (t.usedBytes) {
|
|
1241
|
+
const held = document.createElement("span");
|
|
1242
|
+
held.className = "cellsub";
|
|
1243
|
+
held.textContent = "holds " + bytes(t.usedBytes);
|
|
1244
|
+
where.append(held);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1200
1247
|
const pr = document.createElement("td");
|
|
1201
1248
|
const prIn = document.createElement("input"); prIn.type = "number"; prIn.className = "n"; prIn.min = "0"; prIn.max = "99";
|
|
1202
1249
|
prIn.value = t.priority ?? (t.provider === "vaultline" ? 0 : 10);
|
|
@@ -1235,6 +1282,11 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
1235
1282
|
renderTargets();
|
|
1236
1283
|
} catch (error) { say("tg-msg", errText(error), "err"); }
|
|
1237
1284
|
}
|
|
1285
|
+
$("tg-add-bucket").addEventListener("click", () => {
|
|
1286
|
+
targetRows.push({ id: "byo-" + Math.random().toString(36).slice(2, 8), provider: "r2", priority: 5, __editable: true });
|
|
1287
|
+
renderTargets();
|
|
1288
|
+
say("tg-save-msg", "Fill bucket and prefix, save — then put the access keys on your machine: sealkeep storage credentials set");
|
|
1289
|
+
});
|
|
1238
1290
|
$("tg-add-drive").addEventListener("click", () => {
|
|
1239
1291
|
if (targetRows.some((t) => t.provider === "gdrive")) { say("tg-save-msg", "Drive is already in the rules."); return; }
|
|
1240
1292
|
targetRows.push({ id: "gdrive-" + Math.random().toString(36).slice(2, 8), provider: "gdrive", bucket: "Sealkeep", maxGb: 50, priority: 10 });
|
|
@@ -1246,7 +1298,8 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
1246
1298
|
if (!token) { say("tg-save-msg", "Sign in again to save.", "err"); return; }
|
|
1247
1299
|
$("tg-save").disabled = true;
|
|
1248
1300
|
try {
|
|
1249
|
-
|
|
1301
|
+
const clean = targetRows.map(({ __editable, ...t }) => t);
|
|
1302
|
+
await api("/v1/cloud/storage-targets", { method: "PUT", token, body: { targets_json: JSON.stringify(clean) } });
|
|
1250
1303
|
say("tg-save-msg", "Saved. Every machine follows these on its next seal.");
|
|
1251
1304
|
} catch (error) { say("tg-save-msg", errText(error), "err"); }
|
|
1252
1305
|
$("tg-save").disabled = false;
|
|
@@ -1275,13 +1328,21 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
1275
1328
|
const ref = document.createElement("td");
|
|
1276
1329
|
const code = document.createElement("code"); code.textContent = String(r.vault_ref).slice(0, 8); ref.append(code);
|
|
1277
1330
|
const size = document.createElement("td");
|
|
1278
|
-
|
|
1331
|
+
const sc = document.createElement("code");
|
|
1332
|
+
sc.textContent = bytes(r.bytes);
|
|
1333
|
+
size.append(sc);
|
|
1334
|
+
if (r.chunked) {
|
|
1335
|
+
const sub = document.createElement("span");
|
|
1336
|
+
sub.className = "cellsub";
|
|
1337
|
+
sub.textContent = r.chunkCount + " sealed chunks";
|
|
1338
|
+
size.append(sub);
|
|
1339
|
+
}
|
|
1279
1340
|
const st = document.createElement("td");
|
|
1280
1341
|
const s = document.createElement("span");
|
|
1281
1342
|
s.className = "state " + (r.state === "durable" ? "durable" : "pending");
|
|
1282
1343
|
s.textContent = r.state === "durable" ? "stored" : "incomplete";
|
|
1283
1344
|
st.append(s);
|
|
1284
|
-
const when = document.createElement("td"); when.textContent = ago(r.durable_at || r.created_at);
|
|
1345
|
+
const when = document.createElement("td"); when.className = "mono"; when.style.fontSize = ".78rem"; when.style.color = "var(--soft)"; when.textContent = ago(r.durable_at || r.created_at);
|
|
1285
1346
|
const act = document.createElement("td");
|
|
1286
1347
|
const btn = document.createElement("button");
|
|
1287
1348
|
btn.className = "del"; btn.type = "button"; btn.textContent = "Delete";
|
|
@@ -1311,11 +1372,13 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
1311
1372
|
body.append(tr);
|
|
1312
1373
|
}
|
|
1313
1374
|
|
|
1314
|
-
|
|
1375
|
+
// Counts describe archives, not stored objects — a chunk family is one.
|
|
1376
|
+
const folded = foldFamilies(rows);
|
|
1377
|
+
const stored = folded.filter((r) => r.state === "durable").length;
|
|
1315
1378
|
say("s-msg", "");
|
|
1316
|
-
$("a-count").textContent = String(
|
|
1317
|
-
$("s-foot").textContent = `${
|
|
1318
|
-
+ (stored <
|
|
1379
|
+
$("a-count").textContent = String(folded.length);
|
|
1380
|
+
$("s-foot").textContent = `${folded.length} archive${folded.length === 1 ? "" : "s"} · ${stored} fully stored`
|
|
1381
|
+
+ (stored < folded.length ? ` · ${folded.length - stored} incomplete, not billed` : "");
|
|
1319
1382
|
$("s-wrap").style.display = "";
|
|
1320
1383
|
} catch (error) { say("s-msg", errText(error), "err"); }
|
|
1321
1384
|
}
|
|
@@ -1328,14 +1391,28 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
1328
1391
|
renderAccount(user);
|
|
1329
1392
|
// Each section fails on its own. One dead panel must not blank the rest.
|
|
1330
1393
|
if (user.plan === "pro") { $("a-count").textContent = "—"; loadStorage(token); }
|
|
1331
|
-
loadTargets(token);
|
|
1394
|
+
await loadTargets(token);
|
|
1332
1395
|
loadAudit(token);
|
|
1333
|
-
loadStorage2(token);
|
|
1334
1396
|
const devices = await loadDevices(token);
|
|
1335
1397
|
|
|
1336
1398
|
// A brand-new account has exactly one useful next step. It leads the panel,
|
|
1337
1399
|
// and it leads with the code rather than with a terminal — a paying
|
|
1338
1400
|
// customer should not have to be a terminal user to finish setting up.
|
|
1401
|
+
// The glance line: what is connected, where seals go — from data already fetched.
|
|
1402
|
+
$("a-machines").textContent = String(devices);
|
|
1403
|
+
$("a-dest").textContent = String(Math.max(targetRows.length, 1));
|
|
1404
|
+
const destNames = (targetRows.length ? targetRows : [{ provider: "vaultline" }])
|
|
1405
|
+
.slice().sort((a, b) => (a.priority ?? 10) - (b.priority ?? 10))
|
|
1406
|
+
.map((t) => providerLabel(t.provider) + (t.maxGb ? " ≤" + t.maxGb + " GB" : ""));
|
|
1407
|
+
const driveUsed = targetRows.filter((t) => t.provider === "gdrive" && t.usedBytes)
|
|
1408
|
+
.reduce((n, t) => n + t.usedBytes, 0);
|
|
1409
|
+
const byoUsed = targetRows.filter((t) => t.provider !== "gdrive" && t.provider !== "vaultline" && t.usedBytes)
|
|
1410
|
+
.reduce((n, t) => n + t.usedBytes, 0);
|
|
1411
|
+
$("a-hero").textContent = bytes(Number(user.used_bytes) || 0) + " in Sealkeep cloud"
|
|
1412
|
+
+ (driveUsed ? " · " + bytes(driveUsed) + " in Google Drive" : "")
|
|
1413
|
+
+ (byoUsed ? " · " + bytes(byoUsed) + " in your bucket" : "");
|
|
1414
|
+
$("a-summary").textContent =
|
|
1415
|
+
devices + " machine" + (devices === 1 ? "" : "s") + " connected · new archives go to " + destNames.join(", then ");
|
|
1339
1416
|
const nothingYet = devices === 0 && Number(user.used_bytes) === 0;
|
|
1340
1417
|
on("a-firstrun", nothingYet);
|
|
1341
1418
|
$(nothingYet ? "pair-home-first" : "pair-home-more").append($("pair"));
|
|
@@ -1377,53 +1454,14 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
1377
1454
|
// half the bytes, and still no innerHTML anywhere on the page.
|
|
1378
1455
|
$("g-in").prepend($("g-up").querySelector("svg").cloneNode(true));
|
|
1379
1456
|
|
|
1380
|
-
|
|
1457
|
+
// The destination choice lives in Routing; this card only switches between
|
|
1458
|
+
// "we meter it" (paid) and "your machine reports it" (free).
|
|
1381
1459
|
let lastPaid = false;
|
|
1382
|
-
let lastStorageMode = null;
|
|
1383
1460
|
function applyStorageView() {
|
|
1384
|
-
|
|
1385
|
-
on("a-store-
|
|
1386
|
-
on("a-store-paid", managed);
|
|
1461
|
+
on("a-store-free", !lastPaid);
|
|
1462
|
+
on("a-store-paid", lastPaid);
|
|
1387
1463
|
}
|
|
1388
1464
|
|
|
1389
|
-
const paintStorage = () => {
|
|
1390
|
-
for (const input of storChoices) input.closest(".choice").dataset.on = input.checked ? "yes" : "no";
|
|
1391
|
-
};
|
|
1392
|
-
for (const input of storChoices) input.addEventListener("change", paintStorage);
|
|
1393
|
-
|
|
1394
|
-
async function loadStorage2(token) {
|
|
1395
|
-
try {
|
|
1396
|
-
const where = await api("/v1/cloud/storage", { token });
|
|
1397
|
-
const pick = storChoices.find((input) => input.value === where.mode) || storChoices[0];
|
|
1398
|
-
pick.checked = true;
|
|
1399
|
-
if (where.provider && where.provider !== "vaultline") $("st-prov").value = where.provider;
|
|
1400
|
-
$("st-bucket").value = where.bucket && where.mode === "byo" ? where.bucket : "";
|
|
1401
|
-
$("st-region").value = where.region || "";
|
|
1402
|
-
$("st-prefix").value = where.mode === "byo" ? (where.prefix || "") : "";
|
|
1403
|
-
lastStorageMode = where.mode;
|
|
1404
|
-
applyStorageView();
|
|
1405
|
-
paintStorage();
|
|
1406
|
-
} catch { /* the card keeps its defaults */ }
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
$("st-save").addEventListener("click", async () => {
|
|
1410
|
-
const token = sessionStorage.getItem(TOKEN);
|
|
1411
|
-
const mode = (storChoices.find((input) => input.checked) || {}).value || "managed";
|
|
1412
|
-
$("st-save").disabled = true;
|
|
1413
|
-
$("st-msg").textContent = "Saving";
|
|
1414
|
-
try {
|
|
1415
|
-
const saved = await api("/v1/cloud/storage", { method: "PUT", token, body: mode === "byo"
|
|
1416
|
-
? { mode, provider: $("st-prov").value, bucket: $("st-bucket").value.trim(), region: $("st-region").value.trim(), prefix: $("st-prefix").value.trim() }
|
|
1417
|
-
: { mode } });
|
|
1418
|
-
lastStorageMode = saved.mode;
|
|
1419
|
-
applyStorageView();
|
|
1420
|
-
$("st-msg").textContent = saved.mode === "byo"
|
|
1421
|
-
? "Saved. Run sealkeep storage credentials set on the machine that uploads — the keys stay there."
|
|
1422
|
-
: "Saved. Nothing else to configure.";
|
|
1423
|
-
} catch (error) { $("st-msg").textContent = errText(error); }
|
|
1424
|
-
finally { $("st-save").disabled = false; }
|
|
1425
|
-
});
|
|
1426
|
-
|
|
1427
1465
|
/* -------------------------------------------------------------- pairing
|
|
1428
1466
|
An account created through Google has no password anyone knows, so a
|
|
1429
1467
|
one-time code is the only way a machine can ever sign in. That makes this
|
|
@@ -1622,22 +1660,23 @@ sealkeep open 08424eb6… ./session.jsonl \
|
|
|
1622
1660
|
|
|
1623
1661
|
const packsFrom = (user) => {
|
|
1624
1662
|
const current = packageIdFor(user);
|
|
1663
|
+
const cur = PACKAGES.find((p) => p.id === current);
|
|
1664
|
+
$("a-pack-current").textContent = cur
|
|
1665
|
+
? (cur.managed ? `${cur.name} — $${cur.monthly} a month` : `${cur.name} — free`)
|
|
1666
|
+
: `Custom allocation`;
|
|
1667
|
+
$("a-pack-context").textContent = cur ? cur.blurb : "";
|
|
1625
1668
|
const host = $("a-packs");
|
|
1626
1669
|
while (host.firstChild) host.removeChild(host.firstChild);
|
|
1627
|
-
for (const pack of PACKAGES) {
|
|
1628
|
-
const
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
const
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
desc.textContent = pack.id === current ? `${pack.blurb} This is your package.` : pack.blurb;
|
|
1638
|
-
wrap.append(title, desc);
|
|
1639
|
-
row.append(wrap);
|
|
1640
|
-
host.append(row);
|
|
1670
|
+
for (const pack of PACKAGES.filter((p) => p.managed)) {
|
|
1671
|
+
const chip = document.createElement("span");
|
|
1672
|
+
chip.className = "packchip" + (pack.id === current ? " now" : "");
|
|
1673
|
+
const name = document.createElement("b"); name.textContent = pack.name;
|
|
1674
|
+
const price = document.createElement("span"); price.textContent = `$${pack.monthly}/mo`;
|
|
1675
|
+
chip.append(name, price);
|
|
1676
|
+
if (pack.id === current) {
|
|
1677
|
+
const tag = document.createElement("i"); tag.textContent = "your package"; chip.append(tag);
|
|
1678
|
+
}
|
|
1679
|
+
host.append(chip);
|
|
1641
1680
|
}
|
|
1642
1681
|
if (current === "custom") {
|
|
1643
1682
|
const gb = Math.round((Number(user.quota_bytes) || 0) / 1073741824);
|
package/dist/site.zip
CHANGED
|
Binary file
|
package/dist/src/chunk-store.js
CHANGED
|
@@ -407,6 +407,12 @@ async function runChunkSeal(ctx, args) {
|
|
|
407
407
|
indexed = true;
|
|
408
408
|
}
|
|
409
409
|
catch { /* the next `index build` re-indexes this archive */ }
|
|
410
|
+
// The account learns what each destination holds from the machines that
|
|
411
|
+
// write to them — refresh the stamps now that this archive is recorded.
|
|
412
|
+
{
|
|
413
|
+
const { pushTargetUsage } = await import("./storage-targets.js");
|
|
414
|
+
void pushTargetUsage(ctx.dataDir);
|
|
415
|
+
}
|
|
410
416
|
return { record, folder: args.folder, storedBytes: sealed.storedBytes, chunkCount: envelope.chunks.length, reusedChunks: args.reusedChunks, heldAtMostBytes, indexed };
|
|
411
417
|
}
|
|
412
418
|
/**
|
|
@@ -19,6 +19,9 @@ export type StorageTarget = StorageTargetConfig;
|
|
|
19
19
|
*/
|
|
20
20
|
export declare function resolveTargets(dataDir: string): Promise<StorageTarget[]>;
|
|
21
21
|
/** Pushes the rules to the account so every other machine syncs them. Advisory: offline edits still apply locally. */
|
|
22
|
+
/** Re-pushes the current rules with fresh usage stamps — machines are the only
|
|
23
|
+
* ones who can see Drive and BYO bytes, so the account learns them from here. */
|
|
24
|
+
export declare function pushTargetUsage(dataDir: string): Promise<void>;
|
|
22
25
|
export declare function pushTargetsToCloud(dataDir: string, targets: StorageTarget[]): Promise<boolean>;
|
|
23
26
|
/** Bytes each target currently holds, from the records: targetId when stamped, provider+bucket for older records. */
|
|
24
27
|
export declare function targetUsage(dataDir: string, targets: StorageTarget[]): Promise<Map<string, number>>;
|
|
@@ -59,8 +59,27 @@ async function pullTargetsFromCloud(dataDir) {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
/** Pushes the rules to the account so every other machine syncs them. Advisory: offline edits still apply locally. */
|
|
62
|
+
/** Re-pushes the current rules with fresh usage stamps — machines are the only
|
|
63
|
+
* ones who can see Drive and BYO bytes, so the account learns them from here. */
|
|
64
|
+
export async function pushTargetUsage(dataDir) {
|
|
65
|
+
try {
|
|
66
|
+
const targets = await resolveTargets(dataDir);
|
|
67
|
+
if (!targets.length)
|
|
68
|
+
return;
|
|
69
|
+
await pushTargetsToCloud(dataDir, targets);
|
|
70
|
+
}
|
|
71
|
+
catch { /* advisory */ }
|
|
72
|
+
}
|
|
62
73
|
export async function pushTargetsToCloud(dataDir, targets) {
|
|
63
74
|
try {
|
|
75
|
+
const usage = await targetUsage(dataDir, targets).catch(() => null);
|
|
76
|
+
if (usage) {
|
|
77
|
+
const now = new Date().toISOString();
|
|
78
|
+
targets = targets.map((t) => {
|
|
79
|
+
const used = usage.get(t.id);
|
|
80
|
+
return used && used > 0 ? { ...t, usedBytes: used, usedAt: now } : t;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
64
83
|
const { cloudToken, DEFAULT_CLOUD_URL } = await import("./cloud.js");
|
|
65
84
|
const token = await cloudToken(dataDir);
|
|
66
85
|
const base = (process.env.VAULTLINE_CLOUD_URL ?? DEFAULT_CLOUD_URL).replace(/\/+$/, "");
|
package/dist/src/types.d.ts
CHANGED
|
@@ -133,6 +133,9 @@ export type StorageTargetConfig = {
|
|
|
133
133
|
maxGb?: number;
|
|
134
134
|
projects?: string[];
|
|
135
135
|
priority?: number;
|
|
136
|
+
/** Stamped by machines when they push: bytes this target holds, per their records. */
|
|
137
|
+
usedBytes?: number;
|
|
138
|
+
usedAt?: string;
|
|
136
139
|
};
|
|
137
140
|
export declare function isV2(record: ArchiveRecord): record is ArchiveRecordV2;
|
|
138
141
|
export type VaultConfig = {
|
package/dist/src/upload.js
CHANGED
|
@@ -59,6 +59,10 @@ export async function uploadArchive(dataDir, archiveId, options = {}) {
|
|
|
59
59
|
const next = { ...record, remote };
|
|
60
60
|
await writeRecord(config.storage.root, next);
|
|
61
61
|
await recordAudit(dataDir, "upload.verify", "allowed", { archiveId, objectKey: lease.objectKey, bytes: ciphertext.length, provider: lease.provider });
|
|
62
|
+
{
|
|
63
|
+
const { pushTargetUsage } = await import("./storage-targets.js");
|
|
64
|
+
void pushTargetUsage(dataDir);
|
|
65
|
+
}
|
|
62
66
|
return { archiveId, objectKey: lease.objectKey, bytes: ciphertext.length, checksum: localChecksum, verified: true, provider: lease.provider };
|
|
63
67
|
}
|
|
64
68
|
/** Uploads every archive that has no verified remote object yet. */
|
package/package.json
CHANGED