sealkeep 0.5.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/ARCHITECTURE.md +201 -0
- package/CHANGELOG.md +218 -0
- package/CONTROL_PLANE.md +86 -0
- package/LICENSE +34 -0
- package/README.md +249 -0
- package/THIRD_PARTY.md +22 -0
- package/THREAT_MODEL.md +107 -0
- package/dist/packages/vaultline-crypto/src/aead.d.ts +12 -0
- package/dist/packages/vaultline-crypto/src/aead.js +24 -0
- package/dist/packages/vaultline-crypto/src/chunk-access.d.ts +39 -0
- package/dist/packages/vaultline-crypto/src/chunk-access.js +93 -0
- package/dist/packages/vaultline-crypto/src/envelope.d.ts +71 -0
- package/dist/packages/vaultline-crypto/src/envelope.js +188 -0
- package/dist/packages/vaultline-crypto/src/format.d.ts +106 -0
- package/dist/packages/vaultline-crypto/src/format.js +43 -0
- package/dist/packages/vaultline-crypto/src/index.d.ts +5 -0
- package/dist/packages/vaultline-crypto/src/index.js +5 -0
- package/dist/packages/vaultline-crypto/src/recipients.d.ts +42 -0
- package/dist/packages/vaultline-crypto/src/recipients.js +129 -0
- package/dist/packages/vaultline-crypto/src/sha256-stream.d.ts +41 -0
- package/dist/packages/vaultline-crypto/src/sha256-stream.js +206 -0
- package/dist/packages/vaultline-crypto/src/stream.d.ts +139 -0
- package/dist/packages/vaultline-crypto/src/stream.js +477 -0
- package/dist/site/index.html +1542 -0
- package/dist/site.zip +0 -0
- package/dist/src/activity.d.ts +22 -0
- package/dist/src/activity.js +52 -0
- package/dist/src/adapters.d.ts +212 -0
- package/dist/src/adapters.js +533 -0
- package/dist/src/audit.d.ts +24 -0
- package/dist/src/audit.js +41 -0
- package/dist/src/autopilot.d.ts +77 -0
- package/dist/src/autopilot.js +148 -0
- package/dist/src/bip39-wordlist.d.ts +15 -0
- package/dist/src/bip39-wordlist.js +272 -0
- package/dist/src/branding.d.ts +31 -0
- package/dist/src/branding.js +31 -0
- package/dist/src/chunk-store.d.ts +142 -0
- package/dist/src/chunk-store.js +502 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +2035 -0
- package/dist/src/cloud.d.ts +434 -0
- package/dist/src/cloud.js +851 -0
- package/dist/src/control-plane/auth.d.ts +62 -0
- package/dist/src/control-plane/auth.js +123 -0
- package/dist/src/control-plane/server.d.ts +31 -0
- package/dist/src/control-plane/server.js +263 -0
- package/dist/src/control-plane/store.d.ts +101 -0
- package/dist/src/control-plane/store.js +82 -0
- package/dist/src/control-plane-cli.d.ts +2 -0
- package/dist/src/control-plane-cli.js +37 -0
- package/dist/src/control-plane-server.d.ts +10 -0
- package/dist/src/control-plane-server.js +11 -0
- package/dist/src/control-plane.d.ts +78 -0
- package/dist/src/control-plane.js +61 -0
- package/dist/src/crypto.d.ts +56 -0
- package/dist/src/crypto.js +132 -0
- package/dist/src/daemon.d.ts +52 -0
- package/dist/src/daemon.js +142 -0
- package/dist/src/dashboard-cli.d.ts +2 -0
- package/dist/src/dashboard-cli.js +20 -0
- package/dist/src/disk.d.ts +110 -0
- package/dist/src/disk.js +169 -0
- package/dist/src/doctor.d.ts +11 -0
- package/dist/src/doctor.js +198 -0
- package/dist/src/enroll.d.ts +27 -0
- package/dist/src/enroll.js +136 -0
- package/dist/src/errors.d.ts +26 -0
- package/dist/src/errors.js +23 -0
- package/dist/src/heartbeat.d.ts +89 -0
- package/dist/src/heartbeat.js +120 -0
- package/dist/src/index-sync.d.ts +53 -0
- package/dist/src/index-sync.js +147 -0
- package/dist/src/leakscan.d.ts +48 -0
- package/dist/src/leakscan.js +222 -0
- package/dist/src/local-api.d.ts +132 -0
- package/dist/src/local-api.js +1757 -0
- package/dist/src/managed-chunks.d.ts +55 -0
- package/dist/src/managed-chunks.js +108 -0
- package/dist/src/mcp-install.d.ts +52 -0
- package/dist/src/mcp-install.js +140 -0
- package/dist/src/mcp.d.ts +1 -0
- package/dist/src/mcp.js +59 -0
- package/dist/src/migrate.d.ts +35 -0
- package/dist/src/migrate.js +88 -0
- package/dist/src/mnemonic.d.ts +60 -0
- package/dist/src/mnemonic.js +134 -0
- package/dist/src/net.d.ts +2 -0
- package/dist/src/net.js +16 -0
- package/dist/src/notify.d.ts +46 -0
- package/dist/src/notify.js +84 -0
- package/dist/src/offload.d.ts +117 -0
- package/dist/src/offload.js +331 -0
- package/dist/src/onboarding.d.ts +10 -0
- package/dist/src/onboarding.js +44 -0
- package/dist/src/packages.d.ts +126 -0
- package/dist/src/packages.js +114 -0
- package/dist/src/passkey.d.ts +26 -0
- package/dist/src/passkey.js +54 -0
- package/dist/src/password-lock.d.ts +19 -0
- package/dist/src/password-lock.js +156 -0
- package/dist/src/paths.d.ts +9 -0
- package/dist/src/paths.js +24 -0
- package/dist/src/providers/gcs.d.ts +133 -0
- package/dist/src/providers/gcs.js +235 -0
- package/dist/src/providers/gdrive.d.ts +156 -0
- package/dist/src/providers/gdrive.js +335 -0
- package/dist/src/providers/index.d.ts +45 -0
- package/dist/src/providers/index.js +74 -0
- package/dist/src/providers/s3.d.ts +174 -0
- package/dist/src/providers/s3.js +345 -0
- package/dist/src/providers/sigv4.d.ts +78 -0
- package/dist/src/providers/sigv4.js +112 -0
- package/dist/src/queue.d.ts +185 -0
- package/dist/src/queue.js +286 -0
- package/dist/src/recovery.d.ts +40 -0
- package/dist/src/recovery.js +132 -0
- package/dist/src/rehydrate.d.ts +43 -0
- package/dist/src/rehydrate.js +66 -0
- package/dist/src/restore.d.ts +34 -0
- package/dist/src/restore.js +80 -0
- package/dist/src/retention.d.ts +251 -0
- package/dist/src/retention.js +446 -0
- package/dist/src/rotate.d.ts +47 -0
- package/dist/src/rotate.js +95 -0
- package/dist/src/search.d.ts +147 -0
- package/dist/src/search.js +677 -0
- package/dist/src/secrets.d.ts +86 -0
- package/dist/src/secrets.js +220 -0
- package/dist/src/service.d.ts +73 -0
- package/dist/src/service.js +197 -0
- package/dist/src/share.d.ts +34 -0
- package/dist/src/share.js +68 -0
- package/dist/src/spool.d.ts +97 -0
- package/dist/src/spool.js +213 -0
- package/dist/src/start-tui.d.ts +17 -0
- package/dist/src/start-tui.js +113 -0
- package/dist/src/start.d.ts +75 -0
- package/dist/src/start.js +101 -0
- package/dist/src/storage-setup.d.ts +49 -0
- package/dist/src/storage-setup.js +222 -0
- package/dist/src/storage-targets.d.ts +40 -0
- package/dist/src/storage-targets.js +147 -0
- package/dist/src/stream-to-cloud.d.ts +76 -0
- package/dist/src/stream-to-cloud.js +820 -0
- package/dist/src/sync-rules.d.ts +85 -0
- package/dist/src/sync-rules.js +125 -0
- package/dist/src/trash.d.ts +15 -0
- package/dist/src/trash.js +63 -0
- package/dist/src/tui.d.ts +18 -0
- package/dist/src/tui.js +179 -0
- package/dist/src/types.d.ts +191 -0
- package/dist/src/types.js +3 -0
- package/dist/src/ui-server.d.ts +187 -0
- package/dist/src/ui-server.js +293 -0
- package/dist/src/ui.d.ts +41 -0
- package/dist/src/ui.js +102 -0
- package/dist/src/update.d.ts +30 -0
- package/dist/src/update.js +56 -0
- package/dist/src/upload.d.ts +46 -0
- package/dist/src/upload.js +80 -0
- package/dist/src/vault.d.ts +208 -0
- package/dist/src/vault.js +812 -0
- package/dist/src/watcher.d.ts +34 -0
- package/dist/src/watcher.js +121 -0
- package/dist/src/worker.d.ts +52 -0
- package/dist/src/worker.js +190 -0
- package/package.json +65 -0
- package/web/app.js +1372 -0
- package/web/index.html +476 -0
- package/web/rail.js +308 -0
- package/web/retention.html +17 -0
- package/web/rules-view.js +249 -0
- package/web/sessions-view.js +448 -0
- package/web/sessions.html +17 -0
- package/web/setup-api.js +181 -0
- package/web/setup-logic.js +394 -0
- package/web/setup.html +419 -0
- package/web/setup.js +697 -0
- package/web/style.css +990 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seal-time secret detection. Deterministic, offline, dependency-free.
|
|
3
|
+
*
|
|
4
|
+
* Agent transcripts are where pasted credentials go to be forgotten: an AWS
|
|
5
|
+
* key dropped into a prompt in March is still in the archive in November, and
|
|
6
|
+
* a sealed archive keeps it faithfully forever. Detection has to happen here,
|
|
7
|
+
* on this machine, at seal time — E2EE means nothing downstream (bucket,
|
|
8
|
+
* control plane, org dashboard) can ever look at the plaintext, so a scanner
|
|
9
|
+
* that lives anywhere else would need the very access the product promises
|
|
10
|
+
* away.
|
|
11
|
+
*
|
|
12
|
+
* This module is detection only, in the gitleaks/trufflehog family: fixed
|
|
13
|
+
* patterns plus a Shannon-entropy fallback. No AI, no network, no config —
|
|
14
|
+
* the same bytes always produce the same findings, which is what lets the
|
|
15
|
+
* exit code gate a script.
|
|
16
|
+
*
|
|
17
|
+
* Phase 2 — integration points, deliberately not implemented here:
|
|
18
|
+
*
|
|
19
|
+
* (a) Worker seal path: worker.ts calls scanFileStreaming(job.sourcePath)
|
|
20
|
+
* just before sealing and stores the findings — kind, severity, line,
|
|
21
|
+
* masked preview — as metadata beside the archive record, so "which
|
|
22
|
+
* archives hold live credentials?" is answerable without opening any
|
|
23
|
+
* of them.
|
|
24
|
+
* (b) Org rollup: the control plane receives kind/severity/counts only —
|
|
25
|
+
* never content, never lines, never previews. Because only this machine
|
|
26
|
+
* ever holds plaintext, an org can learn *that* a credential was sealed
|
|
27
|
+
* and *what sort*, but the credential itself cannot travel.
|
|
28
|
+
*/
|
|
29
|
+
import { createReadStream } from "node:fs";
|
|
30
|
+
import { readdir, stat } from "node:fs/promises";
|
|
31
|
+
import { join } from "node:path";
|
|
32
|
+
import { createInterface } from "node:readline";
|
|
33
|
+
import { fail } from "./errors.js";
|
|
34
|
+
/**
|
|
35
|
+
* First four characters, then only the length. A leak report that repeats the
|
|
36
|
+
* leak is a second leak: findings travel further than the transcript did —
|
|
37
|
+
* terminals, CI logs, screenshots, phase-2 metadata — so the preview carries
|
|
38
|
+
* just enough to locate the match and never enough to use it.
|
|
39
|
+
*/
|
|
40
|
+
export function maskSecret(secret) {
|
|
41
|
+
return `${secret.slice(0, 4)}…(${secret.length} chars)`;
|
|
42
|
+
}
|
|
43
|
+
/** Character-frequency Shannon entropy, in bits per character. */
|
|
44
|
+
export function shannonEntropy(value) {
|
|
45
|
+
const chars = [...value];
|
|
46
|
+
if (chars.length === 0)
|
|
47
|
+
return 0;
|
|
48
|
+
const counts = new Map();
|
|
49
|
+
for (const char of chars)
|
|
50
|
+
counts.set(char, (counts.get(char) ?? 0) + 1);
|
|
51
|
+
let entropy = 0;
|
|
52
|
+
for (const count of counts.values()) {
|
|
53
|
+
const p = count / chars.length;
|
|
54
|
+
entropy -= p * Math.log2(p);
|
|
55
|
+
}
|
|
56
|
+
return entropy;
|
|
57
|
+
}
|
|
58
|
+
/** A JWT is three base64url segments, but so is a lot of noise. The header must decode to JSON naming an alg. */
|
|
59
|
+
function plausibleJwtHeader(token) {
|
|
60
|
+
try {
|
|
61
|
+
const header = JSON.parse(Buffer.from(token.slice(0, token.indexOf(".")), "base64url").toString("utf8"));
|
|
62
|
+
return typeof header === "object" && header !== null && typeof header.alg === "string";
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Pattern detectors: shapes only a real credential takes. Every one is
|
|
70
|
+
* anchored on both sides (a 17th character disqualifies an AWS key id rather
|
|
71
|
+
* than truncating it) because precision is the whole game — false positives
|
|
72
|
+
* are how people come to disable scanners, and a disabled scanner catches
|
|
73
|
+
* nothing. A hit here is near-certain, so these report "high".
|
|
74
|
+
*
|
|
75
|
+
* Transcripts are jsonl, so quotes often arrive escaped (`key=\"value\"`);
|
|
76
|
+
* the assignment-context patterns accept the backslash.
|
|
77
|
+
*/
|
|
78
|
+
const PATTERN_DETECTORS = [
|
|
79
|
+
// AKIA (long-lived) / ASIA (temporary) + exactly 16 more upper-alnum.
|
|
80
|
+
{ kind: "aws-access-key-id", pattern: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/dg },
|
|
81
|
+
// The 40-char base64-ish value alone is far too common a shape, so it only
|
|
82
|
+
// counts inside an assignment whose name says "secret".
|
|
83
|
+
{
|
|
84
|
+
kind: "aws-secret-access-key",
|
|
85
|
+
pattern: /[A-Za-z0-9_.-]*secret[A-Za-z0-9_.-]*(?:\\?["'])?\s*[:=]\s*(?:\\?["'])?([A-Za-z0-9/+]{40})(?![A-Za-z0-9/+=])/dgi,
|
|
86
|
+
group: 1
|
|
87
|
+
},
|
|
88
|
+
{ kind: "github-token", pattern: /\b(?:(?:ghp|gho|ghs)_[A-Za-z0-9]{36,251}|github_pat_[A-Za-z0-9_]{22,255})\b/dg },
|
|
89
|
+
{ kind: "google-api-key", pattern: /(?<![0-9A-Za-z_-])AIza[0-9A-Za-z_-]{35}(?![0-9A-Za-z_-])/dg },
|
|
90
|
+
{ kind: "slack-token", pattern: /\bxox[bpars]-[A-Za-z0-9][A-Za-z0-9-]{9,}(?![A-Za-z0-9-])/dg },
|
|
91
|
+
// The armor header is proof enough; the body may be split across lines, but
|
|
92
|
+
// no innocent text starts a line of itself this way.
|
|
93
|
+
{ kind: "private-key", pattern: /-----BEGIN (?:(?:RSA|EC|OPENSSH|PGP|ENCRYPTED) )?PRIVATE KEY(?: BLOCK)?-----/dg },
|
|
94
|
+
{
|
|
95
|
+
kind: "jwt",
|
|
96
|
+
pattern: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{10,}(?![A-Za-z0-9_.-])/dg,
|
|
97
|
+
validate: plausibleJwtHeader
|
|
98
|
+
}
|
|
99
|
+
];
|
|
100
|
+
/** Tests derive their vectors from these rather than restating the numbers. */
|
|
101
|
+
export const GENERIC_MIN_LENGTH = 24;
|
|
102
|
+
export const GENERIC_ENTROPY_THRESHOLD = 4.2;
|
|
103
|
+
/**
|
|
104
|
+
* The generic fallback: something named like a credential, assigned a value
|
|
105
|
+
* long and dense enough that prose and identifiers cannot reach it. This is
|
|
106
|
+
* where scanners lose their users, so it is gated three ways — assignment
|
|
107
|
+
* context, length + entropy, and an allowlist of known-benign shapes — and
|
|
108
|
+
* even then it reports only "medium": pattern hits are certainties, entropy
|
|
109
|
+
* hits are suspicions, and the exit code (phase 1) and seal metadata
|
|
110
|
+
* (phase 2) treat them differently.
|
|
111
|
+
*/
|
|
112
|
+
const GENERIC_ASSIGNMENT = new RegExp(String.raw `[A-Za-z0-9_.-]*(?:key|token|secret|password|passwd|pwd)[A-Za-z0-9_.-]*(?:\\?["'])?\s*[:=]\s*(?:\\?["'])?([A-Za-z0-9+/=_-]{${GENERIC_MIN_LENGTH},})(?![A-Za-z0-9+/=_-])`, "dgi");
|
|
113
|
+
/**
|
|
114
|
+
* Benign shapes that clear the length bar and would otherwise page someone.
|
|
115
|
+
* Pure hex (and UUIDs) cannot actually exceed 4.2 bits/char, but relying on
|
|
116
|
+
* that emergent arithmetic would make the next threshold change a regression;
|
|
117
|
+
* the allowlist states the intent.
|
|
118
|
+
*/
|
|
119
|
+
const BENIGN_VALUE_SHAPES = [
|
|
120
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, // UUIDs: session ids, request ids, vault ids
|
|
121
|
+
/^[0-9a-f]{32,}$/i, // hex digests (sha256 and friends): integrity data, not credentials
|
|
122
|
+
/^AAAA/, // base64 of leading zero bytes: well-known placeholders and ssh key headers
|
|
123
|
+
/^(?:vlt|vault)_/i // vaultline's own refs (vlt_…, vault_ref ids), which are public by design
|
|
124
|
+
];
|
|
125
|
+
function scanLine(line, lineNumber) {
|
|
126
|
+
const findings = [];
|
|
127
|
+
const claimed = [];
|
|
128
|
+
for (const detector of PATTERN_DETECTORS) {
|
|
129
|
+
for (const match of line.matchAll(detector.pattern)) {
|
|
130
|
+
const span = match.indices?.[detector.group ?? 0];
|
|
131
|
+
if (!span)
|
|
132
|
+
continue;
|
|
133
|
+
const secret = line.slice(span[0], span[1]);
|
|
134
|
+
if (detector.validate && !detector.validate(secret))
|
|
135
|
+
continue;
|
|
136
|
+
claimed.push(span);
|
|
137
|
+
findings.push({ kind: detector.kind, line: lineNumber, column: span[0] + 1, preview: maskSecret(secret), severity: "high" });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
for (const match of line.matchAll(GENERIC_ASSIGNMENT)) {
|
|
141
|
+
const span = match.indices?.[1];
|
|
142
|
+
if (!span)
|
|
143
|
+
continue;
|
|
144
|
+
// A secret a precise detector already named is one finding, not two.
|
|
145
|
+
if (claimed.some(([start, end]) => span[0] < end && start < span[1]))
|
|
146
|
+
continue;
|
|
147
|
+
const value = line.slice(span[0], span[1]);
|
|
148
|
+
if (BENIGN_VALUE_SHAPES.some((shape) => shape.test(value)))
|
|
149
|
+
continue;
|
|
150
|
+
if (shannonEntropy(value) <= GENERIC_ENTROPY_THRESHOLD)
|
|
151
|
+
continue;
|
|
152
|
+
findings.push({ kind: "high-entropy-value", line: lineNumber, column: span[0] + 1, preview: maskSecret(value), severity: "medium" });
|
|
153
|
+
}
|
|
154
|
+
return findings.sort((a, b) => a.column - b.column || a.kind.localeCompare(b.kind));
|
|
155
|
+
}
|
|
156
|
+
export function scanText(text) {
|
|
157
|
+
return text.split(/\r?\n/).flatMap((line, index) => scanLine(line, index + 1));
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Line-by-line over a read stream: memory is bounded by the longest single
|
|
161
|
+
* line, never by the file, so a multi-GB jsonl scans in the same footprint as
|
|
162
|
+
* a small one. An unreadable input rejects — a scanner that swallows a read
|
|
163
|
+
* error would be reporting "clean" about bytes it never saw.
|
|
164
|
+
*/
|
|
165
|
+
export async function scanFileStreaming(path, onFinding) {
|
|
166
|
+
const input = createReadStream(path, { encoding: "utf8" });
|
|
167
|
+
const lines = createInterface({ input, crlfDelay: Infinity });
|
|
168
|
+
const findings = [];
|
|
169
|
+
let lineNumber = 0;
|
|
170
|
+
try {
|
|
171
|
+
for await (const line of lines) {
|
|
172
|
+
lineNumber += 1;
|
|
173
|
+
for (const finding of scanLine(line, lineNumber)) {
|
|
174
|
+
findings.push(finding);
|
|
175
|
+
onFinding?.(finding);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
lines.close();
|
|
181
|
+
input.close();
|
|
182
|
+
}
|
|
183
|
+
return findings;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Every *.jsonl under a directory, depth-first, sorted at each level so a
|
|
187
|
+
* report is comparable run to run. Symlinks are left alone: transcript trees
|
|
188
|
+
* do not need them and following them invites cycles.
|
|
189
|
+
*/
|
|
190
|
+
async function jsonlFiles(dir) {
|
|
191
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
192
|
+
const found = [];
|
|
193
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
194
|
+
const path = join(dir, entry.name);
|
|
195
|
+
if (entry.isDirectory())
|
|
196
|
+
found.push(...(await jsonlFiles(path)));
|
|
197
|
+
else if (entry.isFile() && entry.name.endsWith(".jsonl"))
|
|
198
|
+
found.push(path);
|
|
199
|
+
}
|
|
200
|
+
return found;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* A file is scanned as pointed at, whatever its name; a directory means every
|
|
204
|
+
* *.jsonl beneath it. Only high findings set the exit code — failing a script
|
|
205
|
+
* over a "medium" suspicion is exactly the cry-wolf behaviour the severity
|
|
206
|
+
* split exists to avoid.
|
|
207
|
+
*/
|
|
208
|
+
export async function scanPath(target, onFinding) {
|
|
209
|
+
const info = await stat(target).catch(() => fail("source_unreadable", `Cannot read ${target}`));
|
|
210
|
+
const files = info.isDirectory() ? await jsonlFiles(target) : [target];
|
|
211
|
+
const findings = [];
|
|
212
|
+
for (const file of files) {
|
|
213
|
+
await scanFileStreaming(file, (finding) => {
|
|
214
|
+
const placed = { ...finding, file };
|
|
215
|
+
findings.push(placed);
|
|
216
|
+
onFinding?.(placed);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
const high = findings.filter((finding) => finding.severity === "high").length;
|
|
220
|
+
const medium = findings.length - high;
|
|
221
|
+
return { files: files.length, findings, high, medium, exitCode: high > 0 ? 1 : 0 };
|
|
222
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { type Server } from "node:http";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { type AgentId } from "./adapters.js";
|
|
4
|
+
import { ArchiveQueue } from "./queue.js";
|
|
5
|
+
import { localApiTokenPath } from "./paths.js";
|
|
6
|
+
import { type ServiceOptions } from "./service.js";
|
|
7
|
+
declare const setupStorageSchema: z.ZodObject<{
|
|
8
|
+
provider: z.ZodEnum<{
|
|
9
|
+
s3: "s3";
|
|
10
|
+
r2: "r2";
|
|
11
|
+
b2: "b2";
|
|
12
|
+
gcs: "gcs";
|
|
13
|
+
gdrive: "gdrive";
|
|
14
|
+
}>;
|
|
15
|
+
bucket: z.ZodString;
|
|
16
|
+
prefix: z.ZodDefault<z.ZodString>;
|
|
17
|
+
region: z.ZodOptional<z.ZodString>;
|
|
18
|
+
endpoint: z.ZodOptional<z.ZodString>;
|
|
19
|
+
credentials: z.ZodUnion<readonly [z.ZodObject<{
|
|
20
|
+
accessKeyId: z.ZodString;
|
|
21
|
+
secretAccessKey: z.ZodString;
|
|
22
|
+
sessionToken: z.ZodOptional<z.ZodString>;
|
|
23
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
24
|
+
clientEmail: z.ZodString;
|
|
25
|
+
privateKey: z.ZodString;
|
|
26
|
+
}, z.core.$strict>]>;
|
|
27
|
+
confirm: z.ZodDefault<z.ZodBoolean>;
|
|
28
|
+
}, z.core.$strict>;
|
|
29
|
+
/**
|
|
30
|
+
* Proves a storage target works, before anyone trusts it with an archive.
|
|
31
|
+
*
|
|
32
|
+
* Credentials are the step of setup most likely to be wrong, and every way of
|
|
33
|
+
* being wrong used to surface at the same moment: hours later, in a background
|
|
34
|
+
* upload, as a failure nobody was watching. The bucket name has a typo, the key
|
|
35
|
+
* lacks PutObject, the region is the console's display name rather than its id,
|
|
36
|
+
* the B2 key is a native one rather than S3-compatible. All of that is knowable
|
|
37
|
+
* in a second while the person is still looking at the form.
|
|
38
|
+
*
|
|
39
|
+
* It writes a small object, reads it back, and compares the bytes, because
|
|
40
|
+
* anything less proves less: a successful PUT says nothing about whether GET is
|
|
41
|
+
* permitted, and this product's whole promise is that the bytes come back.
|
|
42
|
+
*
|
|
43
|
+
* Nothing is stored. The credential is used to build a client, held for the
|
|
44
|
+
* round trip, and dropped — the caller has not committed to this target yet,
|
|
45
|
+
* and a test that saved a broken configuration would be worse than no test.
|
|
46
|
+
*/
|
|
47
|
+
export declare function testStorageTarget(input: z.infer<typeof setupStorageSchema>): Promise<{
|
|
48
|
+
ok: boolean;
|
|
49
|
+
detail: string;
|
|
50
|
+
probeKey?: string;
|
|
51
|
+
}>;
|
|
52
|
+
export type SessionRow = {
|
|
53
|
+
agent: AgentId;
|
|
54
|
+
path: string;
|
|
55
|
+
bytes: number;
|
|
56
|
+
modifiedAt: string;
|
|
57
|
+
/** True when an archive already covers this file at exactly this length. */
|
|
58
|
+
archived: boolean;
|
|
59
|
+
/** True when that archive also has a verified copy in storage — off this machine, not just sealed on it. */
|
|
60
|
+
stored: boolean;
|
|
61
|
+
/** True while a queued or in-flight job exists for this file: it is being sealed and must not be selected again. */
|
|
62
|
+
queued: boolean;
|
|
63
|
+
/** Why a queued job is waiting instead of running — today, the disk guard's message. Null when it is genuinely being worked. A held row saying "sealing…" was a lie the queue knew better than. */
|
|
64
|
+
held: string | null;
|
|
65
|
+
/** Whole percentages of the source read so far while sealing, else null. The number a person actually watches on a 1.6 GB row. */
|
|
66
|
+
sealingPct: number | null;
|
|
67
|
+
/** The project this session ran in, when its metadata says — the Sessions page filters and labels on it. Null when nothing says. */
|
|
68
|
+
project: string | null;
|
|
69
|
+
/** The working directory behind `project`, best-effort decoded; a tooltip, never a filter. */
|
|
70
|
+
projectPath: string | null;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Every session on this machine, with the two facts a person needs to choose:
|
|
74
|
+
* how big it is, and when they last touched it.
|
|
75
|
+
*
|
|
76
|
+
* This exists because archiving everything automatically is the wrong default
|
|
77
|
+
* for the situation people are actually in — a nearly full disk, a handful of
|
|
78
|
+
* enormous transcripts, and no appetite for a background process deciding which
|
|
79
|
+
* ones to work through. Choosing is cheap when you can see sizes and dates;
|
|
80
|
+
* guessing is not.
|
|
81
|
+
*
|
|
82
|
+
* Already-archived sessions are marked rather than hidden, because "this one is
|
|
83
|
+
* done" is as useful as "this one is not", and hiding them would make the list
|
|
84
|
+
* shrink as you work and lose your place.
|
|
85
|
+
*/
|
|
86
|
+
export declare function listSessions(dataDir: string, queue?: ArchiveQueue): Promise<{
|
|
87
|
+
sessions: SessionRow[];
|
|
88
|
+
totalBytes: number;
|
|
89
|
+
unarchivedBytes: number;
|
|
90
|
+
}>;
|
|
91
|
+
export declare function ensureDraining(dataDir: string, queue: ArchiveQueue): Promise<void>;
|
|
92
|
+
export declare function archiveSelected(dataDir: string, paths: string[], queue: ArchiveQueue): Promise<{
|
|
93
|
+
requested: number;
|
|
94
|
+
queued: number;
|
|
95
|
+
bytes: number;
|
|
96
|
+
background: boolean;
|
|
97
|
+
results: {
|
|
98
|
+
path: string;
|
|
99
|
+
ok: boolean;
|
|
100
|
+
bytes?: number;
|
|
101
|
+
error?: string;
|
|
102
|
+
}[];
|
|
103
|
+
}>;
|
|
104
|
+
export { localApiTokenPath };
|
|
105
|
+
/** Mints the loopback token once and keeps it owner-readable. It is never logged or returned by an endpoint. */
|
|
106
|
+
export declare function localApiToken(dataDir: string): Promise<string>;
|
|
107
|
+
/** One row of GET /v1/cloud/copies: a copy held off this machine, wherever it is held. */
|
|
108
|
+
export type StoredCopy = {
|
|
109
|
+
ref: string;
|
|
110
|
+
bytes: number;
|
|
111
|
+
addedAt: string | null;
|
|
112
|
+
state: string;
|
|
113
|
+
/** True when this machine no longer holds the archive's bytes, so this stored copy is the archive. */
|
|
114
|
+
lastCopy: boolean;
|
|
115
|
+
};
|
|
116
|
+
/** Test seam: the setup endpoints act on the real machine, so tests need a home and a service manager of their own. */
|
|
117
|
+
export type SetupEnvironment = {
|
|
118
|
+
home?: string;
|
|
119
|
+
platform?: NodeJS.Platform;
|
|
120
|
+
exec?: ServiceOptions["exec"];
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* The loopback API shared by the CLI, dashboard, and MCP surfaces. It is the only
|
|
124
|
+
* place local surfaces read state from, so business logic is never duplicated.
|
|
125
|
+
*
|
|
126
|
+
* It deliberately cannot decrypt: no endpoint accepts a recovery phrase, and
|
|
127
|
+
* archive listings omit wrapped key material.
|
|
128
|
+
*/
|
|
129
|
+
export declare function createLocalApiServer(dataDir: string, token: string, options?: {
|
|
130
|
+
queue?: ArchiveQueue;
|
|
131
|
+
setup?: SetupEnvironment;
|
|
132
|
+
}): Server;
|