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.
Files changed (275) hide show
  1. package/ARCHITECTURE.md +163 -14
  2. package/CHANGELOG.md +244 -1
  3. package/CONTROL_PLANE.md +2 -2
  4. package/LICENSE +1 -1
  5. package/README.md +108 -22
  6. package/THIRD_PARTY.md +2 -2
  7. package/THREAT_MODEL.md +23 -4
  8. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/chunk-access.d.ts +26 -4
  9. package/dist/packages/sealkeep-crypto/src/chunk-access.js +219 -0
  10. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.d.ts +11 -1
  11. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.js +58 -21
  12. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.d.ts +2 -2
  13. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.d.ts +1 -0
  14. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.js +1 -0
  15. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.js +4 -4
  16. package/dist/packages/sealkeep-crypto/src/squeeze.d.ts +6 -0
  17. package/dist/packages/sealkeep-crypto/src/squeeze.js +39 -0
  18. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.d.ts +36 -3
  19. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.js +196 -37
  20. package/dist/site/index.html +1808 -1904
  21. package/dist/site/llms.txt +67 -0
  22. package/dist/site/trust/architecture-data-flow.html +53 -0
  23. package/dist/site/trust/audit-roadmap.html +37 -0
  24. package/dist/site/trust/deployment-responsibility.html +11 -0
  25. package/dist/site/trust/dpa-sample.html +30 -0
  26. package/dist/site/trust/release-provenance.html +21 -0
  27. package/dist/site/trust/subprocessors.html +15 -0
  28. package/dist/site/trust/threat-model.html +34 -0
  29. package/dist/site/trust/toms.html +41 -0
  30. package/dist/site/trust-document.css +32 -0
  31. package/dist/site/trust.html +73 -0
  32. package/dist/site/visual/assets/index-8Nxnkc7o.js +207 -0
  33. package/dist/site/visual/assets/index-oxLy1bpy.css +1 -0
  34. package/dist/site/visual/index.html +18 -0
  35. package/dist/site.zip +0 -0
  36. package/dist/src/activity.d.ts +9 -0
  37. package/dist/src/activity.js +90 -1
  38. package/dist/src/adapters.d.ts +175 -5
  39. package/dist/src/adapters.js +961 -73
  40. package/dist/src/agent-context.d.ts +135 -0
  41. package/dist/src/agent-context.js +1059 -0
  42. package/dist/src/archive-copies.d.ts +47 -0
  43. package/dist/src/archive-copies.js +179 -0
  44. package/dist/src/audit.d.ts +1 -1
  45. package/dist/src/audit.js +29 -4
  46. package/dist/src/autopilot.d.ts +52 -7
  47. package/dist/src/autopilot.js +143 -25
  48. package/dist/src/background-bandwidth.d.ts +46 -0
  49. package/dist/src/background-bandwidth.js +301 -0
  50. package/dist/src/background-cpu.d.ts +82 -0
  51. package/dist/src/background-cpu.js +212 -0
  52. package/dist/src/background-worker-error.d.ts +12 -0
  53. package/dist/src/background-worker-error.js +18 -0
  54. package/dist/src/branding.d.ts +24 -1
  55. package/dist/src/branding.js +31 -1
  56. package/dist/src/bridge.d.ts +233 -0
  57. package/dist/src/bridge.js +604 -0
  58. package/dist/src/byte-stream.d.ts +91 -0
  59. package/dist/src/byte-stream.js +385 -0
  60. package/dist/src/chunk-store.d.ts +41 -8
  61. package/dist/src/chunk-store.js +161 -65
  62. package/dist/src/cli.js +1746 -166
  63. package/dist/src/cloud.d.ts +841 -31
  64. package/dist/src/cloud.js +3196 -277
  65. package/dist/src/context-background.d.ts +37 -0
  66. package/dist/src/context-background.js +309 -0
  67. package/dist/src/context-drain-child.d.ts +1 -0
  68. package/dist/src/context-drain-child.js +98 -0
  69. package/dist/src/context-reader.d.ts +118 -0
  70. package/dist/src/context-reader.js +447 -0
  71. package/dist/src/control-plane/auth.d.ts +32 -4
  72. package/dist/src/control-plane/auth.js +85 -24
  73. package/dist/src/control-plane/server.js +19 -6
  74. package/dist/src/control-plane.d.ts +17 -1
  75. package/dist/src/control-plane.js +32 -6
  76. package/dist/src/crypto.d.ts +1 -1
  77. package/dist/src/crypto.js +5 -5
  78. package/dist/src/daemon-lease.d.ts +70 -0
  79. package/dist/src/daemon-lease.js +420 -0
  80. package/dist/src/daemon.d.ts +94 -1
  81. package/dist/src/daemon.js +1082 -105
  82. package/dist/src/darwin-service-policy.d.ts +41 -0
  83. package/dist/src/darwin-service-policy.js +60 -0
  84. package/dist/src/dashboard-cli.js +15 -15
  85. package/dist/src/device-authorization.d.ts +37 -0
  86. package/dist/src/device-authorization.js +199 -0
  87. package/dist/src/device-enrollment.d.ts +91 -0
  88. package/dist/src/device-enrollment.js +349 -0
  89. package/dist/src/disk.d.ts +17 -12
  90. package/dist/src/disk.js +43 -17
  91. package/dist/src/doctor.d.ts +35 -1
  92. package/dist/src/doctor.js +316 -41
  93. package/dist/src/durable-ticket-lock.d.ts +24 -0
  94. package/dist/src/durable-ticket-lock.js +232 -0
  95. package/dist/src/enroll.d.ts +1 -1
  96. package/dist/src/enroll.js +13 -7
  97. package/dist/src/env.d.ts +10 -1
  98. package/dist/src/env.js +11 -3
  99. package/dist/src/errors.d.ts +8 -8
  100. package/dist/src/errors.js +6 -6
  101. package/dist/src/flush.d.ts +12 -0
  102. package/dist/src/flush.js +37 -0
  103. package/dist/src/heartbeat.d.ts +86 -12
  104. package/dist/src/heartbeat.js +415 -29
  105. package/dist/src/index-background-watchdog.d.ts +1 -0
  106. package/dist/src/index-background-watchdog.js +94 -0
  107. package/dist/src/index-background-work.d.ts +21 -0
  108. package/dist/src/index-background-work.js +25 -0
  109. package/dist/src/index-background.d.ts +64 -0
  110. package/dist/src/index-background.js +394 -0
  111. package/dist/src/index-build-child.d.ts +1 -0
  112. package/dist/src/index-build-child.js +109 -0
  113. package/dist/src/index-manifest.d.ts +52 -0
  114. package/dist/src/index-manifest.js +444 -0
  115. package/dist/src/index-publication-proof.d.ts +84 -0
  116. package/dist/src/index-publication-proof.js +380 -0
  117. package/dist/src/index-publication-state.d.ts +149 -0
  118. package/dist/src/index-publication-state.js +696 -0
  119. package/dist/src/index-publication-verifier.d.ts +89 -0
  120. package/dist/src/index-publication-verifier.js +341 -0
  121. package/dist/src/index-publish.d.ts +62 -0
  122. package/dist/src/index-publish.js +540 -0
  123. package/dist/src/index-scratch-cleanup.d.ts +19 -0
  124. package/dist/src/index-scratch-cleanup.js +166 -0
  125. package/dist/src/index-segment-types.d.ts +132 -0
  126. package/dist/src/index-segment-types.js +21 -0
  127. package/dist/src/index-segments.d.ts +9 -0
  128. package/dist/src/index-segments.js +516 -0
  129. package/dist/src/index-store.d.ts +123 -0
  130. package/dist/src/index-store.js +495 -0
  131. package/dist/src/index-sync.d.ts +91 -15
  132. package/dist/src/index-sync.js +286 -53
  133. package/dist/src/index-upgrade-publication.d.ts +30 -0
  134. package/dist/src/index-upgrade-publication.js +179 -0
  135. package/dist/src/integration-manager.d.ts +32 -0
  136. package/dist/src/integration-manager.js +394 -0
  137. package/dist/src/leakscan.js +1 -1
  138. package/dist/src/local-api.d.ts +56 -15
  139. package/dist/src/local-api.js +4979 -541
  140. package/dist/src/machine-settings.d.ts +51 -0
  141. package/dist/src/machine-settings.js +166 -0
  142. package/dist/src/managed-chunks.d.ts +5 -2
  143. package/dist/src/managed-chunks.js +14 -14
  144. package/dist/src/mcp-install.d.ts +11 -9
  145. package/dist/src/mcp-install.js +73 -28
  146. package/dist/src/mcp-workspace.d.ts +18 -0
  147. package/dist/src/mcp-workspace.js +50 -0
  148. package/dist/src/mcp.js +294 -25
  149. package/dist/src/migrate.d.ts +1 -0
  150. package/dist/src/migrate.js +33 -5
  151. package/dist/src/notify.d.ts +1 -1
  152. package/dist/src/notify.js +5 -5
  153. package/dist/src/offload.d.ts +201 -14
  154. package/dist/src/offload.js +1848 -140
  155. package/dist/src/onboarding.d.ts +8 -1
  156. package/dist/src/onboarding.js +4 -4
  157. package/dist/src/packages.d.ts +2 -2
  158. package/dist/src/packages.js +10 -2
  159. package/dist/src/passkey.d.ts +0 -1
  160. package/dist/src/passkey.js +2 -7
  161. package/dist/src/password-lock.d.ts +2 -2
  162. package/dist/src/password-lock.js +6 -6
  163. package/dist/src/paths.d.ts +2 -0
  164. package/dist/src/paths.js +2 -0
  165. package/dist/src/presence.d.ts +86 -0
  166. package/dist/src/presence.js +240 -0
  167. package/dist/src/progress-deadline.d.ts +21 -0
  168. package/dist/src/progress-deadline.js +91 -0
  169. package/dist/src/project-repair.d.ts +55 -0
  170. package/dist/src/project-repair.js +131 -0
  171. package/dist/src/providers/gcs.d.ts +28 -7
  172. package/dist/src/providers/gcs.js +35 -24
  173. package/dist/src/providers/gdrive.d.ts +71 -8
  174. package/dist/src/providers/gdrive.js +223 -50
  175. package/dist/src/providers/index.d.ts +11 -3
  176. package/dist/src/providers/index.js +30 -10
  177. package/dist/src/providers/s3.d.ts +30 -8
  178. package/dist/src/providers/s3.js +41 -30
  179. package/dist/src/providers/safe-storage-fetch.d.ts +12 -0
  180. package/dist/src/providers/safe-storage-fetch.js +72 -0
  181. package/dist/src/queue.d.ts +136 -19
  182. package/dist/src/queue.js +862 -96
  183. package/dist/src/reclaim-transaction.d.ts +156 -0
  184. package/dist/src/reclaim-transaction.js +1027 -0
  185. package/dist/src/recovery-codes.d.ts +32 -0
  186. package/dist/src/recovery-codes.js +338 -0
  187. package/dist/src/recovery.js +12 -9
  188. package/dist/src/rehydrate.d.ts +25 -22
  189. package/dist/src/rehydrate.js +319 -23
  190. package/dist/src/restore.d.ts +57 -4
  191. package/dist/src/restore.js +272 -36
  192. package/dist/src/resume-stub.d.ts +92 -0
  193. package/dist/src/resume-stub.js +417 -0
  194. package/dist/src/retention.d.ts +98 -7
  195. package/dist/src/retention.js +1066 -62
  196. package/dist/src/rotate.js +3 -3
  197. package/dist/src/search.d.ts +566 -8
  198. package/dist/src/search.js +5897 -290
  199. package/dist/src/secrets.d.ts +51 -7
  200. package/dist/src/secrets.js +316 -24
  201. package/dist/src/service.d.ts +49 -11
  202. package/dist/src/service.js +776 -35
  203. package/dist/src/share.js +3 -3
  204. package/dist/src/shared-spaces.d.ts +98 -0
  205. package/dist/src/shared-spaces.js +214 -0
  206. package/dist/src/source-reader.d.ts +73 -0
  207. package/dist/src/source-reader.js +715 -0
  208. package/dist/src/spool.d.ts +1 -1
  209. package/dist/src/spool.js +1 -1
  210. package/dist/src/start-tui.js +2 -1
  211. package/dist/src/start.js +2 -2
  212. package/dist/src/storage-endpoint.d.ts +21 -0
  213. package/dist/src/storage-endpoint.js +122 -0
  214. package/dist/src/storage-setup.js +12 -12
  215. package/dist/src/storage-targets.d.ts +109 -6
  216. package/dist/src/storage-targets.js +975 -67
  217. package/dist/src/stream-to-cloud.d.ts +5 -1
  218. package/dist/src/stream-to-cloud.js +34 -14
  219. package/dist/src/sync-rules.d.ts +31 -6
  220. package/dist/src/sync-rules.js +153 -14
  221. package/dist/src/team-backfill-scheduling.d.ts +8 -0
  222. package/dist/src/team-backfill-scheduling.js +33 -0
  223. package/dist/src/team-backfill.d.ts +116 -0
  224. package/dist/src/team-backfill.js +1429 -0
  225. package/dist/src/team-index-cache.d.ts +16 -0
  226. package/dist/src/team-index-cache.js +152 -0
  227. package/dist/src/team-offboarding.d.ts +38 -0
  228. package/dist/src/team-offboarding.js +1043 -0
  229. package/dist/src/team-presence.d.ts +127 -0
  230. package/dist/src/team-presence.js +904 -0
  231. package/dist/src/team-publication-policy.d.ts +20 -0
  232. package/dist/src/team-publication-policy.js +140 -0
  233. package/dist/src/team-realtime.d.ts +68 -0
  234. package/dist/src/team-realtime.js +816 -0
  235. package/dist/src/team-source-facts-cache.d.ts +23 -0
  236. package/dist/src/team-source-facts-cache.js +255 -0
  237. package/dist/src/trash.d.ts +1 -1
  238. package/dist/src/trash.js +2 -2
  239. package/dist/src/tui.js +11 -12
  240. package/dist/src/types.d.ts +173 -7
  241. package/dist/src/types.js +20 -0
  242. package/dist/src/ui-server.d.ts +163 -35
  243. package/dist/src/ui-server.js +712 -72
  244. package/dist/src/ui.d.ts +1 -2
  245. package/dist/src/ui.js +1 -2
  246. package/dist/src/upload.d.ts +27 -0
  247. package/dist/src/upload.js +383 -43
  248. package/dist/src/vault.d.ts +235 -29
  249. package/dist/src/vault.js +1783 -190
  250. package/dist/src/watcher.d.ts +7 -1
  251. package/dist/src/watcher.js +198 -55
  252. package/dist/src/worker.d.ts +27 -3
  253. package/dist/src/worker.js +274 -55
  254. package/package.json +33 -12
  255. package/scripts/native-reboot-rehearsal.mjs +90 -0
  256. package/web/app.js +6032 -343
  257. package/web/bootstrap.js +17 -0
  258. package/web/index.html +255 -57
  259. package/web/rail.js +317 -40
  260. package/web/retention.html +2 -2
  261. package/web/rules-view.js +188 -16
  262. package/web/sessions-view.js +485 -62
  263. package/web/sessions.html +2 -2
  264. package/web/setup-api.js +152 -29
  265. package/web/setup-logic.js +68 -9
  266. package/web/setup.html +113 -44
  267. package/web/setup.js +604 -71
  268. package/web/style.css +513 -98
  269. package/dist/packages/vaultline-crypto/src/chunk-access.js +0 -93
  270. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.d.ts +0 -0
  271. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.js +0 -0
  272. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.js +0 -0
  273. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.d.ts +0 -0
  274. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.d.ts +0 -0
  275. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.js +0 -0
@@ -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/vaultline-crypto/src/index.js";
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/vaultline-crypto/src/index.js";
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");
@@ -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", "vaultline"),
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, isVaultlineError } from "./errors.js";
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 (${isVaultlineError(error) ? error.code : "unexpected error"}). Run \`sealkeep recovery seal\` once you are online.`;
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: "vaultline-policy.json", body: s3Policy(bucket, prefix) },
51
- commands: [`aws iam create-policy --policy-name ${policyName} \\\n --policy-document file://vaultline-policy.json`]
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 vaultline`,
59
- `aws iam attach-user-policy --user-name vaultline \\\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 vaultline`
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 = `vaultline@${project}.iam.gserviceaccount.com`;
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: "vaultline-binding.txt",
137
- body: `role: roles/storage.objectAdmin\nmember: serviceAccount:${account}\ncondition:\n title: vaultline-prefix-only\n expression: |\n resource.name.startsWith("projects/_/buckets/${bucket}/objects/${prefix}/")`
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=vaultline-prefix-only'`
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 vaultline-key.json \\\n --iam-account=${account} --project=${project}`]
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("./vaultline-key.json");process.stdout.write(JSON.stringify({clientEmail:k.client_email,privateKey:k.private_key}))' \\\n | sealkeep storage credentials set`,
153
- `rm vaultline-key.json # the key now lives in your keychain`,
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
- export declare function resolveTargets(dataDir: string): Promise<StorageTarget[]>;
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, from the records: targetId when stamped, provider+bucket for older records. */
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
- }): Promise<void>;
140
+ accountBaseline?: StorageTargetAccountBaseline | null;
141
+ }): Promise<{
142
+ localSaved: boolean;
143
+ accountSynced: boolean | null;
144
+ accountConflict?: boolean;
145
+ accountStale?: boolean;
146
+ }>;