sealkeep 0.8.1 → 0.11.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 +163 -14
- package/CHANGELOG.md +244 -1
- package/CONTROL_PLANE.md +2 -2
- package/LICENSE +1 -1
- package/README.md +108 -22
- package/THIRD_PARTY.md +2 -2
- package/THREAT_MODEL.md +23 -4
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/chunk-access.d.ts +26 -4
- package/dist/packages/sealkeep-crypto/src/chunk-access.js +219 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.d.ts +11 -1
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.js +58 -21
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.d.ts +2 -2
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.d.ts +1 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.js +1 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.js +4 -4
- package/dist/packages/sealkeep-crypto/src/squeeze.d.ts +6 -0
- package/dist/packages/sealkeep-crypto/src/squeeze.js +39 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.d.ts +36 -3
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.js +196 -37
- package/dist/site/index.html +1808 -1904
- package/dist/site/llms.txt +67 -0
- package/dist/site/trust/architecture-data-flow.html +53 -0
- package/dist/site/trust/audit-roadmap.html +37 -0
- package/dist/site/trust/deployment-responsibility.html +11 -0
- package/dist/site/trust/dpa-sample.html +30 -0
- package/dist/site/trust/release-provenance.html +21 -0
- package/dist/site/trust/subprocessors.html +15 -0
- package/dist/site/trust/threat-model.html +34 -0
- package/dist/site/trust/toms.html +41 -0
- package/dist/site/trust-document.css +32 -0
- package/dist/site/trust.html +73 -0
- package/dist/site/visual/assets/index-8Nxnkc7o.js +207 -0
- package/dist/site/visual/assets/index-oxLy1bpy.css +1 -0
- package/dist/site/visual/index.html +18 -0
- package/dist/site.zip +0 -0
- package/dist/src/activity.d.ts +9 -0
- package/dist/src/activity.js +90 -1
- package/dist/src/adapters.d.ts +175 -5
- package/dist/src/adapters.js +961 -73
- package/dist/src/agent-context.d.ts +135 -0
- package/dist/src/agent-context.js +1059 -0
- package/dist/src/archive-copies.d.ts +47 -0
- package/dist/src/archive-copies.js +179 -0
- package/dist/src/audit.d.ts +1 -1
- package/dist/src/audit.js +29 -4
- package/dist/src/autopilot.d.ts +52 -7
- package/dist/src/autopilot.js +143 -25
- package/dist/src/background-bandwidth.d.ts +46 -0
- package/dist/src/background-bandwidth.js +301 -0
- package/dist/src/background-cpu.d.ts +82 -0
- package/dist/src/background-cpu.js +212 -0
- package/dist/src/background-worker-error.d.ts +12 -0
- package/dist/src/background-worker-error.js +18 -0
- package/dist/src/branding.d.ts +24 -1
- package/dist/src/branding.js +31 -1
- package/dist/src/bridge.d.ts +233 -0
- package/dist/src/bridge.js +604 -0
- package/dist/src/byte-stream.d.ts +91 -0
- package/dist/src/byte-stream.js +385 -0
- package/dist/src/chunk-store.d.ts +41 -8
- package/dist/src/chunk-store.js +161 -65
- package/dist/src/cli.js +1746 -166
- package/dist/src/cloud.d.ts +841 -31
- package/dist/src/cloud.js +3196 -277
- package/dist/src/context-background.d.ts +37 -0
- package/dist/src/context-background.js +309 -0
- package/dist/src/context-drain-child.d.ts +1 -0
- package/dist/src/context-drain-child.js +98 -0
- package/dist/src/context-reader.d.ts +118 -0
- package/dist/src/context-reader.js +447 -0
- package/dist/src/control-plane/auth.d.ts +32 -4
- package/dist/src/control-plane/auth.js +85 -24
- package/dist/src/control-plane/server.js +19 -6
- package/dist/src/control-plane.d.ts +17 -1
- package/dist/src/control-plane.js +32 -6
- package/dist/src/crypto.d.ts +1 -1
- package/dist/src/crypto.js +5 -5
- package/dist/src/daemon-lease.d.ts +70 -0
- package/dist/src/daemon-lease.js +420 -0
- package/dist/src/daemon.d.ts +94 -1
- package/dist/src/daemon.js +1082 -105
- package/dist/src/darwin-service-policy.d.ts +41 -0
- package/dist/src/darwin-service-policy.js +60 -0
- package/dist/src/dashboard-cli.js +15 -15
- package/dist/src/device-authorization.d.ts +37 -0
- package/dist/src/device-authorization.js +199 -0
- package/dist/src/device-enrollment.d.ts +91 -0
- package/dist/src/device-enrollment.js +349 -0
- package/dist/src/disk.d.ts +17 -12
- package/dist/src/disk.js +43 -17
- package/dist/src/doctor.d.ts +35 -1
- package/dist/src/doctor.js +316 -41
- package/dist/src/durable-ticket-lock.d.ts +24 -0
- package/dist/src/durable-ticket-lock.js +232 -0
- package/dist/src/enroll.d.ts +1 -1
- package/dist/src/enroll.js +13 -7
- package/dist/src/env.d.ts +10 -1
- package/dist/src/env.js +11 -3
- package/dist/src/errors.d.ts +8 -8
- package/dist/src/errors.js +6 -6
- package/dist/src/flush.d.ts +12 -0
- package/dist/src/flush.js +37 -0
- package/dist/src/heartbeat.d.ts +86 -12
- package/dist/src/heartbeat.js +415 -29
- package/dist/src/index-background-watchdog.d.ts +1 -0
- package/dist/src/index-background-watchdog.js +94 -0
- package/dist/src/index-background-work.d.ts +21 -0
- package/dist/src/index-background-work.js +25 -0
- package/dist/src/index-background.d.ts +64 -0
- package/dist/src/index-background.js +394 -0
- package/dist/src/index-build-child.d.ts +1 -0
- package/dist/src/index-build-child.js +109 -0
- package/dist/src/index-manifest.d.ts +52 -0
- package/dist/src/index-manifest.js +444 -0
- package/dist/src/index-publication-proof.d.ts +84 -0
- package/dist/src/index-publication-proof.js +380 -0
- package/dist/src/index-publication-state.d.ts +149 -0
- package/dist/src/index-publication-state.js +696 -0
- package/dist/src/index-publication-verifier.d.ts +89 -0
- package/dist/src/index-publication-verifier.js +341 -0
- package/dist/src/index-publish.d.ts +62 -0
- package/dist/src/index-publish.js +540 -0
- package/dist/src/index-scratch-cleanup.d.ts +19 -0
- package/dist/src/index-scratch-cleanup.js +166 -0
- package/dist/src/index-segment-types.d.ts +132 -0
- package/dist/src/index-segment-types.js +21 -0
- package/dist/src/index-segments.d.ts +9 -0
- package/dist/src/index-segments.js +516 -0
- package/dist/src/index-store.d.ts +123 -0
- package/dist/src/index-store.js +495 -0
- package/dist/src/index-sync.d.ts +91 -15
- package/dist/src/index-sync.js +286 -53
- package/dist/src/index-upgrade-publication.d.ts +30 -0
- package/dist/src/index-upgrade-publication.js +179 -0
- package/dist/src/integration-manager.d.ts +32 -0
- package/dist/src/integration-manager.js +394 -0
- package/dist/src/leakscan.js +1 -1
- package/dist/src/local-api.d.ts +56 -15
- package/dist/src/local-api.js +4979 -541
- package/dist/src/machine-settings.d.ts +51 -0
- package/dist/src/machine-settings.js +166 -0
- package/dist/src/managed-chunks.d.ts +5 -2
- package/dist/src/managed-chunks.js +14 -14
- package/dist/src/mcp-install.d.ts +11 -9
- package/dist/src/mcp-install.js +73 -28
- package/dist/src/mcp-workspace.d.ts +18 -0
- package/dist/src/mcp-workspace.js +50 -0
- package/dist/src/mcp.js +294 -25
- package/dist/src/migrate.d.ts +1 -0
- package/dist/src/migrate.js +33 -5
- package/dist/src/notify.d.ts +1 -1
- package/dist/src/notify.js +5 -5
- package/dist/src/offload.d.ts +201 -14
- package/dist/src/offload.js +1848 -140
- package/dist/src/onboarding.d.ts +8 -1
- package/dist/src/onboarding.js +4 -4
- package/dist/src/packages.d.ts +2 -2
- package/dist/src/packages.js +10 -2
- package/dist/src/passkey.d.ts +0 -1
- package/dist/src/passkey.js +2 -7
- package/dist/src/password-lock.d.ts +2 -2
- package/dist/src/password-lock.js +6 -6
- package/dist/src/paths.d.ts +2 -0
- package/dist/src/paths.js +2 -0
- package/dist/src/presence.d.ts +86 -0
- package/dist/src/presence.js +240 -0
- package/dist/src/progress-deadline.d.ts +21 -0
- package/dist/src/progress-deadline.js +91 -0
- package/dist/src/project-repair.d.ts +55 -0
- package/dist/src/project-repair.js +131 -0
- package/dist/src/providers/gcs.d.ts +28 -7
- package/dist/src/providers/gcs.js +35 -24
- package/dist/src/providers/gdrive.d.ts +71 -8
- package/dist/src/providers/gdrive.js +223 -50
- package/dist/src/providers/index.d.ts +11 -3
- package/dist/src/providers/index.js +30 -10
- package/dist/src/providers/s3.d.ts +30 -8
- package/dist/src/providers/s3.js +41 -30
- package/dist/src/providers/safe-storage-fetch.d.ts +12 -0
- package/dist/src/providers/safe-storage-fetch.js +72 -0
- package/dist/src/queue.d.ts +136 -19
- package/dist/src/queue.js +862 -96
- package/dist/src/reclaim-transaction.d.ts +156 -0
- package/dist/src/reclaim-transaction.js +1027 -0
- package/dist/src/recovery-codes.d.ts +32 -0
- package/dist/src/recovery-codes.js +338 -0
- package/dist/src/recovery.js +12 -9
- package/dist/src/rehydrate.d.ts +25 -22
- package/dist/src/rehydrate.js +319 -23
- package/dist/src/restore.d.ts +57 -4
- package/dist/src/restore.js +272 -36
- package/dist/src/resume-stub.d.ts +92 -0
- package/dist/src/resume-stub.js +417 -0
- package/dist/src/retention.d.ts +98 -7
- package/dist/src/retention.js +1066 -62
- package/dist/src/rotate.js +3 -3
- package/dist/src/search.d.ts +566 -8
- package/dist/src/search.js +5897 -290
- package/dist/src/secrets.d.ts +51 -7
- package/dist/src/secrets.js +316 -24
- package/dist/src/service.d.ts +49 -11
- package/dist/src/service.js +776 -35
- package/dist/src/share.js +3 -3
- package/dist/src/shared-spaces.d.ts +98 -0
- package/dist/src/shared-spaces.js +214 -0
- package/dist/src/source-reader.d.ts +73 -0
- package/dist/src/source-reader.js +715 -0
- package/dist/src/spool.d.ts +1 -1
- package/dist/src/spool.js +1 -1
- package/dist/src/start-tui.js +2 -1
- package/dist/src/start.js +2 -2
- package/dist/src/storage-endpoint.d.ts +21 -0
- package/dist/src/storage-endpoint.js +122 -0
- package/dist/src/storage-setup.js +12 -12
- package/dist/src/storage-targets.d.ts +109 -6
- package/dist/src/storage-targets.js +975 -67
- package/dist/src/stream-to-cloud.d.ts +5 -1
- package/dist/src/stream-to-cloud.js +34 -14
- package/dist/src/sync-rules.d.ts +31 -6
- package/dist/src/sync-rules.js +153 -14
- package/dist/src/team-backfill-scheduling.d.ts +8 -0
- package/dist/src/team-backfill-scheduling.js +33 -0
- package/dist/src/team-backfill.d.ts +116 -0
- package/dist/src/team-backfill.js +1429 -0
- package/dist/src/team-index-cache.d.ts +16 -0
- package/dist/src/team-index-cache.js +152 -0
- package/dist/src/team-offboarding.d.ts +38 -0
- package/dist/src/team-offboarding.js +1043 -0
- package/dist/src/team-presence.d.ts +127 -0
- package/dist/src/team-presence.js +904 -0
- package/dist/src/team-publication-policy.d.ts +20 -0
- package/dist/src/team-publication-policy.js +140 -0
- package/dist/src/team-realtime.d.ts +68 -0
- package/dist/src/team-realtime.js +816 -0
- package/dist/src/team-source-facts-cache.d.ts +23 -0
- package/dist/src/team-source-facts-cache.js +255 -0
- package/dist/src/trash.d.ts +1 -1
- package/dist/src/trash.js +2 -2
- package/dist/src/tui.js +11 -12
- package/dist/src/types.d.ts +173 -7
- package/dist/src/types.js +20 -0
- package/dist/src/ui-server.d.ts +163 -35
- package/dist/src/ui-server.js +712 -72
- package/dist/src/ui.d.ts +1 -2
- package/dist/src/ui.js +1 -2
- package/dist/src/upload.d.ts +27 -0
- package/dist/src/upload.js +383 -43
- package/dist/src/vault.d.ts +235 -29
- package/dist/src/vault.js +1783 -190
- package/dist/src/watcher.d.ts +7 -1
- package/dist/src/watcher.js +198 -55
- package/dist/src/worker.d.ts +27 -3
- package/dist/src/worker.js +274 -55
- package/package.json +33 -12
- package/scripts/native-reboot-rehearsal.mjs +90 -0
- package/web/app.js +6032 -343
- package/web/bootstrap.js +17 -0
- package/web/index.html +255 -57
- package/web/rail.js +317 -40
- package/web/retention.html +2 -2
- package/web/rules-view.js +188 -16
- package/web/sessions-view.js +485 -62
- package/web/sessions.html +2 -2
- package/web/setup-api.js +152 -29
- package/web/setup-logic.js +68 -9
- package/web/setup.html +113 -44
- package/web/setup.js +604 -71
- package/web/style.css +513 -98
- package/dist/packages/vaultline-crypto/src/chunk-access.js +0 -93
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.d.ts +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.js +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.js +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.d.ts +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.d.ts +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.js +0 -0
package/dist/src/spool.d.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* trying to save. Rename is atomic on one filesystem, so a concurrent reader
|
|
21
21
|
* sees the old journal or the new one, never a mixture.
|
|
22
22
|
*/
|
|
23
|
-
import { type ChunkHeader, type Suite, type Unlock, type WrappedKey } from "../packages/
|
|
23
|
+
import { type ChunkHeader, type Suite, type Unlock, type WrappedKey } from "../packages/sealkeep-crypto/src/index.js";
|
|
24
24
|
export declare const SPOOL_VERSION = 1;
|
|
25
25
|
export type SpoolJournal = {
|
|
26
26
|
version: typeof SPOOL_VERSION;
|
package/dist/src/spool.js
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
import { mkdir, open, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
24
24
|
import { join, resolve } from "node:path";
|
|
25
25
|
import { randomUUID } from "node:crypto";
|
|
26
|
-
import { CryptoError, KEY_BYTES, unwrapArchiveKey, zeroize } from "../packages/
|
|
26
|
+
import { CryptoError, KEY_BYTES, unwrapArchiveKey, zeroize } from "../packages/sealkeep-crypto/src/index.js";
|
|
27
27
|
import { fail } from "./errors.js";
|
|
28
28
|
export const SPOOL_VERSION = 1;
|
|
29
29
|
const spoolDir = (dataDir) => join(dataDir, "spool");
|
package/dist/src/start-tui.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createInterface } from "node:readline/promises";
|
|
|
2
2
|
import { emitKeypressEvents } from "node:readline";
|
|
3
3
|
import { modeOptions } from "./start.js";
|
|
4
4
|
import { bold, dim, blue, green } from "./ui.js";
|
|
5
|
+
import { STORAGE_PREFIX_DEFAULT } from "./branding.js";
|
|
5
6
|
/**
|
|
6
7
|
* Interactive onboarding.
|
|
7
8
|
*
|
|
@@ -105,7 +106,7 @@ export async function onboard() {
|
|
|
105
106
|
return {
|
|
106
107
|
mode: "free", provider,
|
|
107
108
|
bucket: await ask("Bucket name"),
|
|
108
|
-
prefix: await ask("Prefix",
|
|
109
|
+
prefix: await ask("Prefix", STORAGE_PREFIX_DEFAULT),
|
|
109
110
|
project: provider === "gcs" ? await ask("Google Cloud project") : undefined,
|
|
110
111
|
region: provider === "s3" ? await ask("Region", "us-east-1") : undefined,
|
|
111
112
|
accountId: provider === "r2" ? await ask("Cloudflare account id") : undefined
|
package/dist/src/start.js
CHANGED
|
@@ -3,7 +3,7 @@ import { addRecipient, initialize } from "./vault.js";
|
|
|
3
3
|
import { recoveryKit } from "./recovery.js";
|
|
4
4
|
import { register, sealEscrow } from "./cloud.js";
|
|
5
5
|
import { setupPlan } from "./storage-setup.js";
|
|
6
|
-
import { fail,
|
|
6
|
+
import { fail, isSealkeepError } from "./errors.js";
|
|
7
7
|
/** An X25519 keypair for this machine. The private half never leaves it unsealed. */
|
|
8
8
|
export function generateDeviceKeys() {
|
|
9
9
|
const { publicKey, privateKey } = generateKeyPairSync("x25519");
|
|
@@ -65,7 +65,7 @@ export async function startPaid(dataDir, choice) {
|
|
|
65
65
|
// every archive, so the vault is usable — but say so plainly rather than
|
|
66
66
|
// printing a promise we did not keep.
|
|
67
67
|
sealed = false;
|
|
68
|
-
escrowNote = `The sealed copy of your key could not be stored (${
|
|
68
|
+
escrowNote = `The sealed copy of your key could not be stored (${isSealkeepError(error) ? error.code : "unexpected error"}). Run \`sealkeep recovery seal\` once you are online.`;
|
|
69
69
|
}
|
|
70
70
|
return {
|
|
71
71
|
mode: "paid", vaultId: config.vaultId, phrase, recoveryKit: recoveryKit(config), account, escrow,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { StorageTargetConfig } from "./types.js";
|
|
2
|
+
export type StorageAddress = {
|
|
3
|
+
address: string;
|
|
4
|
+
family: 4 | 6;
|
|
5
|
+
};
|
|
6
|
+
export type StorageAddressResolver = (hostname: string) => Promise<readonly StorageAddress[]>;
|
|
7
|
+
export declare const systemStorageAddressResolver: StorageAddressResolver;
|
|
8
|
+
/** A provider endpoint is an origin, never a URL carrying credentials or path state. */
|
|
9
|
+
export declare function parseStorageEndpoint(value: string, label?: string): URL;
|
|
10
|
+
export declare function isPublicStorageAddress(address: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Resolves and validates one complete DNS answer set. Callers must use the
|
|
13
|
+
* returned addresses for the connection whose lookup triggered this check;
|
|
14
|
+
* resolving once here and again later would recreate a DNS-rebinding window.
|
|
15
|
+
*/
|
|
16
|
+
export declare function allowedStorageAddresses(hostname: string, options?: {
|
|
17
|
+
allowPrivate?: boolean;
|
|
18
|
+
resolver?: StorageAddressResolver;
|
|
19
|
+
}): Promise<StorageAddress[]>;
|
|
20
|
+
/** Early UX validation. The transport repeats this policy inside its actual socket lookup. */
|
|
21
|
+
export declare function assertSafeStorageEndpoint(value: string, provider?: StorageTargetConfig["provider"], env?: NodeJS.ProcessEnv, resolver?: StorageAddressResolver): Promise<URL>;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { lookup } from "node:dns/promises";
|
|
2
|
+
import { isIP } from "node:net";
|
|
3
|
+
import { fail } from "./errors.js";
|
|
4
|
+
import { envFlag } from "./env.js";
|
|
5
|
+
export const systemStorageAddressResolver = async (hostname) => (await lookup(hostname, { all: true, verbatim: true })).map((answer) => ({
|
|
6
|
+
address: answer.address,
|
|
7
|
+
family: answer.family,
|
|
8
|
+
}));
|
|
9
|
+
function normalizedHostname(hostname) {
|
|
10
|
+
return hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
|
|
11
|
+
}
|
|
12
|
+
function isLoopbackEndpointHost(hostname) {
|
|
13
|
+
const host = normalizedHostname(hostname);
|
|
14
|
+
return host === "localhost" || host.endsWith(".localhost")
|
|
15
|
+
|| host === "::1" || host.startsWith("127.");
|
|
16
|
+
}
|
|
17
|
+
/** A provider endpoint is an origin, never a URL carrying credentials or path state. */
|
|
18
|
+
export function parseStorageEndpoint(value, label = "Storage endpoint") {
|
|
19
|
+
let parsed;
|
|
20
|
+
try {
|
|
21
|
+
parsed = new URL(value);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return fail("invalid_argument", `${label} is not a valid URL.`);
|
|
25
|
+
}
|
|
26
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
27
|
+
fail("invalid_argument", `${label} must use https (or http for a loopback test service).`);
|
|
28
|
+
}
|
|
29
|
+
if (parsed.username || parsed.password) {
|
|
30
|
+
fail("invalid_argument", `${label} must not contain credentials.`);
|
|
31
|
+
}
|
|
32
|
+
if (parsed.pathname !== "/" || parsed.search || parsed.hash || value.includes("?") || value.includes("#")) {
|
|
33
|
+
fail("invalid_argument", `${label} must be only an origin, without a path, query, or fragment.`);
|
|
34
|
+
}
|
|
35
|
+
if (parsed.protocol === "http:" && !isLoopbackEndpointHost(parsed.hostname)) {
|
|
36
|
+
fail("invalid_argument", `${label} must use https unless it is a loopback test service.`);
|
|
37
|
+
}
|
|
38
|
+
return parsed;
|
|
39
|
+
}
|
|
40
|
+
function publicIpv4(address) {
|
|
41
|
+
const parts = address.split(".").map(Number);
|
|
42
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255))
|
|
43
|
+
return false;
|
|
44
|
+
const [a, b, c] = parts;
|
|
45
|
+
if (a === 0 || a === 10 || a === 127 || a >= 224)
|
|
46
|
+
return false;
|
|
47
|
+
if (a === 100 && b >= 64 && b <= 127)
|
|
48
|
+
return false;
|
|
49
|
+
if (a === 169 && b === 254)
|
|
50
|
+
return false;
|
|
51
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
52
|
+
return false;
|
|
53
|
+
if (a === 192 && b === 0)
|
|
54
|
+
return false;
|
|
55
|
+
if (a === 192 && b === 168)
|
|
56
|
+
return false;
|
|
57
|
+
if (a === 198 && (b === 18 || b === 19))
|
|
58
|
+
return false;
|
|
59
|
+
if ((a === 192 && b === 0 && c === 2) || (a === 198 && b === 51 && c === 100) || (a === 203 && b === 0 && c === 113))
|
|
60
|
+
return false;
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
function publicIpv6(address) {
|
|
64
|
+
const lower = address.toLowerCase();
|
|
65
|
+
const mapped = /(?:^|:)ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(lower)?.[1];
|
|
66
|
+
if (mapped)
|
|
67
|
+
return publicIpv4(mapped);
|
|
68
|
+
// Restrict to global unicast, excluding documentation and transition ranges
|
|
69
|
+
// that could tunnel a request to an otherwise-blocked IPv4 destination.
|
|
70
|
+
const first = Number.parseInt(lower.split(":", 1)[0] || "0", 16);
|
|
71
|
+
if (!Number.isFinite(first) || first < 0x2000 || first > 0x3fff)
|
|
72
|
+
return false;
|
|
73
|
+
if (lower.startsWith("2001:db8:") || lower === "2001:db8::")
|
|
74
|
+
return false;
|
|
75
|
+
if (lower.startsWith("2001:0000:") || lower.startsWith("2001:0:"))
|
|
76
|
+
return false;
|
|
77
|
+
if (lower.startsWith("2002:"))
|
|
78
|
+
return false;
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
export function isPublicStorageAddress(address) {
|
|
82
|
+
const family = isIP(address);
|
|
83
|
+
return family === 4 ? publicIpv4(address) : family === 6 ? publicIpv6(address) : false;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Resolves and validates one complete DNS answer set. Callers must use the
|
|
87
|
+
* returned addresses for the connection whose lookup triggered this check;
|
|
88
|
+
* resolving once here and again later would recreate a DNS-rebinding window.
|
|
89
|
+
*/
|
|
90
|
+
export async function allowedStorageAddresses(hostname, options = {}) {
|
|
91
|
+
const host = normalizedHostname(hostname);
|
|
92
|
+
const literalFamily = isIP(host);
|
|
93
|
+
let answers;
|
|
94
|
+
try {
|
|
95
|
+
answers = literalFamily
|
|
96
|
+
? [{ address: host, family: literalFamily }]
|
|
97
|
+
: await (options.resolver ?? systemStorageAddressResolver)(host);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return fail("invalid_argument", `Storage endpoint ${host} could not be resolved safely.`);
|
|
101
|
+
}
|
|
102
|
+
const unique = [...new Map(answers.map((answer) => [`${answer.family}:${answer.address.toLowerCase()}`, answer])).values()];
|
|
103
|
+
if (unique.length === 0 || unique.some((answer) => isIP(answer.address) !== answer.family)) {
|
|
104
|
+
fail("invalid_argument", `Storage endpoint ${host} did not resolve to a usable IP address.`);
|
|
105
|
+
}
|
|
106
|
+
if (!options.allowPrivate && unique.some((answer) => !isPublicStorageAddress(answer.address))) {
|
|
107
|
+
fail("invalid_argument", `Storage endpoint ${host} resolves to a private, local, or reserved address. Set SEALKEEP_ALLOW_PRIVATE_STORAGE_ENDPOINT=1 only for a trusted local emulator.`);
|
|
108
|
+
}
|
|
109
|
+
return unique;
|
|
110
|
+
}
|
|
111
|
+
/** Early UX validation. The transport repeats this policy inside its actual socket lookup. */
|
|
112
|
+
export async function assertSafeStorageEndpoint(value, provider, env = process.env, resolver = systemStorageAddressResolver) {
|
|
113
|
+
const parsed = parseStorageEndpoint(value);
|
|
114
|
+
const allowPrivate = envFlag("ALLOW_PRIVATE_STORAGE_ENDPOINT", env);
|
|
115
|
+
const hostname = normalizedHostname(parsed.hostname);
|
|
116
|
+
if (provider === "r2" && !hostname.endsWith(".r2.cloudflarestorage.com") && !allowPrivate) {
|
|
117
|
+
fail("invalid_argument", "R2 endpoints must be an account origin ending in .r2.cloudflarestorage.com.");
|
|
118
|
+
}
|
|
119
|
+
if (!allowPrivate)
|
|
120
|
+
await allowedStorageAddresses(hostname, { resolver });
|
|
121
|
+
return parsed;
|
|
122
|
+
}
|
|
@@ -47,17 +47,17 @@ function s3Plan(input) {
|
|
|
47
47
|
title: "Create a least-privilege policy",
|
|
48
48
|
why: `This grants write and read under ${prefix}/ and nothing else — not the rest of the bucket, not other buckets.`,
|
|
49
49
|
link: "https://console.aws.amazon.com/iam/home#/policies$new?step=edit",
|
|
50
|
-
document: { name: "
|
|
51
|
-
commands: [`aws iam create-policy --policy-name ${policyName} \\\n --policy-document file://
|
|
50
|
+
document: { name: "sealkeep-policy.json", body: s3Policy(bucket, prefix) },
|
|
51
|
+
commands: [`aws iam create-policy --policy-name ${policyName} \\\n --policy-document file://sealkeep-policy.json`]
|
|
52
52
|
},
|
|
53
53
|
{
|
|
54
54
|
title: "Create a user and an access key",
|
|
55
55
|
why: "Sealkeep signs its own requests, so it needs a key rather than a role it cannot assume.",
|
|
56
56
|
link: "https://console.aws.amazon.com/iam/home#/users/create",
|
|
57
57
|
commands: [
|
|
58
|
-
`aws iam create-user --user-name
|
|
59
|
-
`aws iam attach-user-policy --user-name
|
|
60
|
-
`aws iam create-access-key --user-name
|
|
58
|
+
`aws iam create-user --user-name sealkeep`,
|
|
59
|
+
`aws iam attach-user-policy --user-name sealkeep \\\n --policy-arn arn:aws:iam::$(aws sts get-caller-identity --query Account --output text):policy/${policyName}`,
|
|
60
|
+
`aws iam create-access-key --user-name sealkeep`
|
|
61
61
|
]
|
|
62
62
|
}
|
|
63
63
|
],
|
|
@@ -113,7 +113,7 @@ function gcsPlan(input) {
|
|
|
113
113
|
const prefix = clean(input.prefix);
|
|
114
114
|
const project = requireValue(input.project, "--project", "every Google Cloud resource belongs to a project");
|
|
115
115
|
const location = input.region ?? "US";
|
|
116
|
-
const account = `
|
|
116
|
+
const account = `sealkeep@${project}.iam.gserviceaccount.com`;
|
|
117
117
|
return {
|
|
118
118
|
provider: "gcs", bucket, prefix,
|
|
119
119
|
steps: [
|
|
@@ -133,24 +133,24 @@ function gcsPlan(input) {
|
|
|
133
133
|
title: "Grant access to the prefix only",
|
|
134
134
|
why: `The IAM condition limits this account to objects under ${prefix}/, so it cannot read the rest of the bucket.`,
|
|
135
135
|
document: {
|
|
136
|
-
name: "
|
|
137
|
-
body: `role: roles/storage.objectAdmin\nmember: serviceAccount:${account}\ncondition:\n title:
|
|
136
|
+
name: "sealkeep-binding.txt",
|
|
137
|
+
body: `role: roles/storage.objectAdmin\nmember: serviceAccount:${account}\ncondition:\n title: sealkeep-prefix-only\n expression: |\n resource.name.startsWith("projects/_/buckets/${bucket}/objects/${prefix}/")`
|
|
138
138
|
},
|
|
139
139
|
commands: [
|
|
140
|
-
`gcloud storage buckets add-iam-policy-binding gs://${bucket} \\\n --member="serviceAccount:${account}" \\\n --role="roles/storage.objectAdmin" \\\n --condition='expression=resource.name.startsWith("projects/_/buckets/${bucket}/objects/${prefix}/"),title=
|
|
140
|
+
`gcloud storage buckets add-iam-policy-binding gs://${bucket} \\\n --member="serviceAccount:${account}" \\\n --role="roles/storage.objectAdmin" \\\n --condition='expression=resource.name.startsWith("projects/_/buckets/${bucket}/objects/${prefix}/"),title=sealkeep-prefix-only'`
|
|
141
141
|
]
|
|
142
142
|
},
|
|
143
143
|
{
|
|
144
144
|
title: "Create a key for the service account",
|
|
145
145
|
why: "Sealkeep needs the private key to sign upload URLs. It is stored in your OS keychain.",
|
|
146
146
|
link: `https://console.cloud.google.com/iam-admin/serviceaccounts?project=${project}`,
|
|
147
|
-
commands: [`gcloud iam service-accounts keys create
|
|
147
|
+
commands: [`gcloud iam service-accounts keys create sealkeep-key.json \\\n --iam-account=${account} --project=${project}`]
|
|
148
148
|
}
|
|
149
149
|
],
|
|
150
150
|
finish: [
|
|
151
151
|
`sealkeep storage configure --provider gcs --bucket ${bucket} --prefix ${prefix}`,
|
|
152
|
-
`node -e 'const k=require("./
|
|
153
|
-
`rm
|
|
152
|
+
`node -e 'const k=require("./sealkeep-key.json");process.stdout.write(JSON.stringify({clientEmail:k.client_email,privateKey:k.private_key}))' \\\n | sealkeep storage credentials set`,
|
|
153
|
+
`rm sealkeep-key.json # the key now lives in your keychain`,
|
|
154
154
|
`SEALKEEP_ENABLE_SIGNER=1 sealkeep upload --all`
|
|
155
155
|
],
|
|
156
156
|
notes: [
|
|
@@ -9,21 +9,116 @@
|
|
|
9
9
|
* bucket, so restore, search, and delete follow the record wherever it went.
|
|
10
10
|
* Routing decides only where NEW seals go.
|
|
11
11
|
*/
|
|
12
|
-
import type { StorageTargetConfig } from "./types.js";
|
|
12
|
+
import type { RemoteObject, StorageDestinationIdentity, StorageTargetConfig } from "./types.js";
|
|
13
|
+
import { type BackendName, type ProviderCredentialBinding, type ProviderCredentials } from "./secrets.js";
|
|
14
|
+
import type { EndpointOverrides } from "./providers/index.js";
|
|
15
|
+
export { assertSafeStorageEndpoint, parseStorageEndpoint } from "./storage-endpoint.js";
|
|
13
16
|
export type StorageTarget = StorageTargetConfig;
|
|
17
|
+
/** Exact account row which an editor actually rendered. `targetsJson` is kept
|
|
18
|
+
* byte-for-byte because the control plane compares it atomically; `updatedAt`
|
|
19
|
+
* is the row revision and also closes the otherwise possible A→B→A ABA gap.
|
|
20
|
+
* `accountScope` binds a browser/CLI draft to the login under which it loaded. */
|
|
21
|
+
export type StorageTargetAccountBaseline = {
|
|
22
|
+
targetsJson: string | null;
|
|
23
|
+
updatedAt: string | null;
|
|
24
|
+
accountScope: string | null;
|
|
25
|
+
};
|
|
26
|
+
export type StorageTargetEditSnapshot = {
|
|
27
|
+
targets: StorageTarget[];
|
|
28
|
+
/** Null means the account row could not be read. It never means “absent”. */
|
|
29
|
+
accountBaseline: StorageTargetAccountBaseline | null;
|
|
30
|
+
};
|
|
31
|
+
export type TargetUsagePushOptions = {
|
|
32
|
+
/** Cancels both account reads and the final account write. */
|
|
33
|
+
signal?: AbortSignal;
|
|
34
|
+
};
|
|
35
|
+
type TargetCloudPushOptions = TargetUsagePushOptions & {
|
|
36
|
+
renewCloudToken?: boolean;
|
|
37
|
+
claimMachineIdentity?: boolean;
|
|
38
|
+
/** Report evidence against the account's routing row without authoring it. */
|
|
39
|
+
advisoryUsageOnly?: boolean;
|
|
40
|
+
expectedAuthGeneration?: string | null;
|
|
41
|
+
/** Exact local routing state which authorised this account write. */
|
|
42
|
+
expectedRoutingFingerprint?: string;
|
|
43
|
+
/** Exact account row the person or telemetry operation edited. */
|
|
44
|
+
accountBaseline?: StorageTargetAccountBaseline;
|
|
45
|
+
};
|
|
46
|
+
export declare function storageTargetAccountBaselineIsCurrent(dataDir: string, baseline: StorageTargetAccountBaseline): Promise<boolean>;
|
|
47
|
+
/** Stable, serialisable identity used by both credential and copy bindings. */
|
|
48
|
+
export declare function storageDestinationIdentity(target: StorageTarget, effectiveEndpoint?: string | undefined, gdriveAccountId?: string | null): StorageDestinationIdentity;
|
|
49
|
+
/** Hash of a fixed-order tuple, never of user-controlled object key syntax. */
|
|
50
|
+
export declare function storageDestinationFingerprint(destination: StorageDestinationIdentity): string;
|
|
51
|
+
export declare function destinationIdentityForTarget(dataDir: string, target: StorageTarget, credentials?: ProviderCredentials): Promise<StorageDestinationIdentity>;
|
|
52
|
+
export declare function destinationBindingForTarget(dataDir: string, target: StorageTarget, verifiedAt?: string | null, credentials?: ProviderCredentials): Promise<ProviderCredentialBinding>;
|
|
53
|
+
export declare function storeProviderCredentialsForTarget(dataDir: string, target: StorageTarget, credentials: ProviderCredentials, preferred?: BackendName, verifiedAt?: string | null): Promise<{
|
|
54
|
+
backend: BackendName;
|
|
55
|
+
}>;
|
|
56
|
+
/** Provider host information is routing configuration, not a credential, so it
|
|
57
|
+
* travels with the account target and every machine signs against the same
|
|
58
|
+
* intended service. This also prevents a stale setup-wide endpoint from being
|
|
59
|
+
* accidentally reused for a different bucket target. */
|
|
60
|
+
export declare function endpointOverridesForTarget(target: StorageTarget): EndpointOverrides;
|
|
61
|
+
/**
|
|
62
|
+
* The setup wizard predates account-wide targets and kept its endpoint in
|
|
63
|
+
* setup.json. That value belongs to the one legacy `remoteStorage` target only:
|
|
64
|
+
* letting it bleed into a named target can sign an R2/S3 request for the right
|
|
65
|
+
* bucket against the wrong provider host. New targets carry their endpoint in
|
|
66
|
+
* routing metadata; only the synthesized `primary` target may use this fallback.
|
|
67
|
+
*/
|
|
68
|
+
export declare function endpointForTarget(dataDir: string, target: StorageTarget): Promise<string | undefined>;
|
|
69
|
+
/** Effective provider overrides, including the narrowly-scoped legacy fallback. */
|
|
70
|
+
export declare function endpointOverridesForResolvedTarget(dataDir: string, target: StorageTarget): Promise<EndpointOverrides>;
|
|
71
|
+
/**
|
|
72
|
+
* Provider credentials belong to a machine, while routing rules belong to the
|
|
73
|
+
* account. New installs address a credential by target id so two BYO targets
|
|
74
|
+
* cannot overwrite one another; the vault id remains a read fallback only for
|
|
75
|
+
* the synthesized primary target created by older releases.
|
|
76
|
+
*/
|
|
77
|
+
export declare function credentialConfigIdForTarget(dataDir: string, target: StorageTarget): Promise<string | null>;
|
|
78
|
+
/**
|
|
79
|
+
* Exact-copy routing guard. New records must match the immutable fingerprint.
|
|
80
|
+
* Legacy records are accepted only for globally addressed providers without a
|
|
81
|
+
* custom endpoint; R2/custom S3/Drive account identity is ambiguous and fails
|
|
82
|
+
* closed until a verified migration can stamp the copy.
|
|
83
|
+
*/
|
|
84
|
+
export declare function remoteCopyMatchesTarget(dataDir: string, target: StorageTarget, copy: Pick<RemoteObject, "provider" | "bucket" | "destination" | "destinationFingerprint">): Promise<boolean>;
|
|
85
|
+
/** Whether this particular machine can use an account-wide destination now. */
|
|
86
|
+
export declare function targetConnectedOnThisMachine(dataDir: string, target: StorageTarget, options?: {
|
|
87
|
+
renewCloudToken?: boolean;
|
|
88
|
+
signal?: AbortSignal;
|
|
89
|
+
}): Promise<boolean>;
|
|
90
|
+
export declare function targetConnectionEvidence(dataDir: string, target: StorageTarget, options?: {
|
|
91
|
+
renewCloudToken?: boolean;
|
|
92
|
+
signal?: AbortSignal;
|
|
93
|
+
}): Promise<{
|
|
94
|
+
connected: boolean;
|
|
95
|
+
status: "credentials_verified" | "credentials_saved" | "credentials_missing" | "account_session";
|
|
96
|
+
verifiedAt: string | null;
|
|
97
|
+
}>;
|
|
14
98
|
/**
|
|
15
99
|
* The effective target list. Explicit `storageTargets` in config wins;
|
|
16
100
|
* otherwise the single `remoteStorage` entry is the one target; otherwise a
|
|
17
101
|
* vault signed into the cloud account is managed-only — the same ladder the
|
|
18
102
|
* rest of the product climbs.
|
|
19
103
|
*/
|
|
20
|
-
|
|
104
|
+
type ResolveTargetsOptions = {
|
|
105
|
+
signal?: AbortSignal;
|
|
106
|
+
renewCloudToken?: boolean;
|
|
107
|
+
pullFromCloud?: boolean;
|
|
108
|
+
};
|
|
109
|
+
export declare function resolveTargets(dataDir: string, options?: ResolveTargetsOptions): Promise<StorageTarget[]>;
|
|
110
|
+
/** One coherent edit view: the effective rows and the exact account revision
|
|
111
|
+
* they came from. Callers must carry `accountBaseline` unchanged into
|
|
112
|
+
* `setStorageTargets`; a fresh read at save time would authorise a stale UI. */
|
|
113
|
+
export declare function storageTargetsForEditing(dataDir: string, options?: ResolveTargetsOptions): Promise<StorageTargetEditSnapshot>;
|
|
21
114
|
/** Pushes the rules to the account so every other machine syncs them. Advisory: offline edits still apply locally. */
|
|
22
115
|
/** Re-pushes the current rules with fresh usage stamps — machines are the only
|
|
23
116
|
* ones who can see Drive and BYO bytes, so the account learns them from here. */
|
|
24
|
-
export declare function pushTargetUsage(dataDir: string): Promise<void>;
|
|
25
|
-
export declare function pushTargetsToCloud(dataDir: string, targets: StorageTarget[]): Promise<boolean>;
|
|
26
|
-
/** Bytes each target currently holds
|
|
117
|
+
export declare function pushTargetUsage(dataDir: string, options?: TargetUsagePushOptions): Promise<void>;
|
|
118
|
+
export declare function pushTargetsToCloud(dataDir: string, targets: StorageTarget[], options?: TargetCloudPushOptions): Promise<boolean>;
|
|
119
|
+
/** Bytes each target currently holds. A mutable display id is never enough to
|
|
120
|
+
* attribute a copy: the record must still name the target's immutable physical
|
|
121
|
+
* destination (or pass the deliberately narrow legacy compatibility rule). */
|
|
27
122
|
export declare function targetUsage(dataDir: string, targets: StorageTarget[]): Promise<Map<string, number>>;
|
|
28
123
|
/**
|
|
29
124
|
* Where a new seal goes. Eligibility first (limit not blown, chunk-capable
|
|
@@ -33,11 +128,19 @@ export declare function targetUsage(dataDir: string, targets: StorageTarget[]):
|
|
|
33
128
|
*/
|
|
34
129
|
export declare function chooseTarget(dataDir: string, input: {
|
|
35
130
|
project?: string | null;
|
|
131
|
+
projectKey?: string | null;
|
|
36
132
|
bytes: number;
|
|
37
133
|
requireChunkCapable?: boolean;
|
|
134
|
+
requireManaged?: boolean;
|
|
38
135
|
targetId?: string;
|
|
39
136
|
}): Promise<StorageTarget>;
|
|
40
137
|
/** Persist an explicit target list. Empty array removes the setting (back to single remoteStorage / managed). */
|
|
41
138
|
export declare function setStorageTargets(dataDir: string, targets: StorageTarget[], options?: {
|
|
42
139
|
push?: boolean;
|
|
43
|
-
|
|
140
|
+
accountBaseline?: StorageTargetAccountBaseline | null;
|
|
141
|
+
}): Promise<{
|
|
142
|
+
localSaved: boolean;
|
|
143
|
+
accountSynced: boolean | null;
|
|
144
|
+
accountConflict?: boolean;
|
|
145
|
+
accountStale?: boolean;
|
|
146
|
+
}>;
|