@run402/sdk 4.95.0 → 4.96.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/README.md +24 -24
- package/core-dist/keystore.d.ts +1 -1
- package/core-dist/keystore.js +1 -1
- package/dist/errors.d.ts +4 -4
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.d.ts +9 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -8
- package/dist/index.js.map +1 -1
- package/dist/namespaces/deploy.d.ts +5 -5
- package/dist/namespaces/deploy.d.ts.map +1 -1
- package/dist/namespaces/deploy.js +20 -20
- package/dist/namespaces/deploy.js.map +1 -1
- package/dist/namespaces/deploy.types.d.ts +17 -17
- package/dist/namespaces/deploy.types.d.ts.map +1 -1
- package/dist/namespaces/org.d.ts +2 -2
- package/dist/namespaces/org.js +2 -2
- package/dist/namespaces/org.types.d.ts +3 -3
- package/dist/namespaces/pay.js +1 -1
- package/dist/namespaces/{gitvault.d.ts → repos.d.ts} +436 -449
- package/dist/namespaces/repos.d.ts.map +1 -0
- package/dist/namespaces/{gitvault.js → repos.js} +451 -459
- package/dist/namespaces/repos.js.map +1 -0
- package/dist/namespaces/rooms.js +2 -2
- package/dist/namespaces/rooms.js.map +1 -1
- package/dist/namespaces/session.d.ts +2 -2
- package/dist/namespaces/session.d.ts.map +1 -1
- package/dist/namespaces/session.js +2 -2
- package/dist/namespaces/session.js.map +1 -1
- package/dist/namespaces/tier.d.ts +3 -3
- package/dist/namespaces/tier.d.ts.map +1 -1
- package/dist/namespaces/{gitvault.crypto.d.ts → vault.crypto.d.ts} +141 -141
- package/dist/namespaces/vault.crypto.d.ts.map +1 -0
- package/dist/namespaces/{gitvault.crypto.js → vault.crypto.js} +173 -173
- package/dist/namespaces/vault.crypto.js.map +1 -0
- package/dist/namespaces/{gitvault.types.d.ts → vault.types.d.ts} +136 -136
- package/dist/namespaces/vault.types.d.ts.map +1 -0
- package/dist/namespaces/{gitvault.types.js → vault.types.js} +4 -4
- package/dist/namespaces/vault.types.js.map +1 -0
- package/dist/node/bearer-redeem-key.d.ts +1 -1
- package/dist/node/bearer-redeem-key.js +5 -5
- package/dist/node/bearer-redeem-key.js.map +1 -1
- package/dist/node/http-dispatcher.js +5 -5
- package/dist/node/http-dispatcher.js.map +1 -1
- package/dist/node/index.d.ts +29 -29
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +39 -39
- package/dist/node/index.js.map +1 -1
- package/dist/node/{gitvault-address.d.ts → vault-address.d.ts} +42 -42
- package/dist/node/vault-address.d.ts.map +1 -0
- package/dist/node/{gitvault-address.js → vault-address.js} +43 -43
- package/dist/node/vault-address.js.map +1 -0
- package/dist/node/{gitvault-apply.d.ts → vault-apply.d.ts} +27 -27
- package/dist/node/vault-apply.d.ts.map +1 -0
- package/dist/node/{gitvault-apply.js → vault-apply.js} +38 -38
- package/dist/node/vault-apply.js.map +1 -0
- package/dist/node/vault-byo-config.d.ts +29 -0
- package/dist/node/vault-byo-config.d.ts.map +1 -0
- package/dist/node/{gitvault-byo-config.js → vault-byo-config.js} +18 -18
- package/dist/node/vault-byo-config.js.map +1 -0
- package/dist/node/vault-byo-probe.d.ts +20 -0
- package/dist/node/vault-byo-probe.d.ts.map +1 -0
- package/dist/node/vault-byo-probe.js +57 -0
- package/dist/node/vault-byo-probe.js.map +1 -0
- package/dist/node/{gitvault-creation-journal.d.ts → vault-creation-journal.d.ts} +69 -69
- package/dist/node/vault-creation-journal.d.ts.map +1 -0
- package/dist/node/{gitvault-creation-journal.js → vault-creation-journal.js} +54 -54
- package/dist/node/vault-creation-journal.js.map +1 -0
- package/dist/node/{gitvault-degraded-read.d.ts → vault-degraded-read.d.ts} +34 -34
- package/dist/node/vault-degraded-read.d.ts.map +1 -0
- package/dist/node/{gitvault-degraded-read.js → vault-degraded-read.js} +24 -24
- package/dist/node/vault-degraded-read.js.map +1 -0
- package/dist/node/{gitvault-deploy.d.ts → vault-deploy.d.ts} +67 -67
- package/dist/node/vault-deploy.d.ts.map +1 -0
- package/dist/node/{gitvault-deploy.js → vault-deploy.js} +27 -27
- package/dist/node/vault-deploy.js.map +1 -0
- package/dist/node/{gitvault-edge-fetch.d.ts → vault-edge-fetch.d.ts} +9 -9
- package/dist/node/vault-edge-fetch.d.ts.map +1 -0
- package/dist/node/{gitvault-edge-fetch.js → vault-edge-fetch.js} +11 -11
- package/dist/node/vault-edge-fetch.js.map +1 -0
- package/dist/node/{gitvault-handoff.d.ts → vault-handoff.d.ts} +9 -9
- package/dist/node/vault-handoff.d.ts.map +1 -0
- package/dist/node/{gitvault-handoff.js → vault-handoff.js} +24 -24
- package/dist/node/vault-handoff.js.map +1 -0
- package/dist/node/{gitvault-keystore.d.ts → vault-keystore.d.ts} +87 -77
- package/dist/node/vault-keystore.d.ts.map +1 -0
- package/dist/node/{gitvault-keystore.js → vault-keystore.js} +105 -68
- package/dist/node/vault-keystore.js.map +1 -0
- package/dist/node/{gitvault-member-bundle.d.ts → vault-member-bundle.d.ts} +17 -17
- package/dist/node/vault-member-bundle.d.ts.map +1 -0
- package/dist/node/{gitvault-member-bundle.js → vault-member-bundle.js} +11 -11
- package/dist/node/vault-member-bundle.js.map +1 -0
- package/dist/node/{gitvault-mirror-backend.d.ts → vault-mirror-backend.d.ts} +26 -26
- package/dist/node/vault-mirror-backend.d.ts.map +1 -0
- package/dist/node/{gitvault-mirror-backend.js → vault-mirror-backend.js} +25 -25
- package/dist/node/vault-mirror-backend.js.map +1 -0
- package/dist/node/{gitvault-mirror-config.d.ts → vault-mirror-config.d.ts} +23 -23
- package/dist/node/vault-mirror-config.d.ts.map +1 -0
- package/dist/node/{gitvault-mirror-config.js → vault-mirror-config.js} +16 -16
- package/dist/node/vault-mirror-config.js.map +1 -0
- package/dist/node/{gitvault-mirror.d.ts → vault-mirror.d.ts} +36 -36
- package/dist/node/vault-mirror.d.ts.map +1 -0
- package/dist/node/{gitvault-mirror.js → vault-mirror.js} +75 -75
- package/dist/node/vault-mirror.js.map +1 -0
- package/dist/node/{gitvault-native-crypto.d.ts → vault-native-crypto.d.ts} +10 -10
- package/dist/node/vault-native-crypto.d.ts.map +1 -0
- package/dist/node/{gitvault-native-crypto.js → vault-native-crypto.js} +16 -16
- package/dist/node/vault-native-crypto.js.map +1 -0
- package/dist/node/{gitvault-open-or-create.d.ts → vault-open-or-create.d.ts} +16 -16
- package/dist/node/vault-open-or-create.d.ts.map +1 -0
- package/dist/node/{gitvault-open-or-create.js → vault-open-or-create.js} +10 -10
- package/dist/node/vault-open-or-create.js.map +1 -0
- package/dist/node/{gitvault-prewarm.d.ts → vault-prewarm.d.ts} +13 -13
- package/dist/node/vault-prewarm.d.ts.map +1 -0
- package/dist/node/{gitvault-prewarm.js → vault-prewarm.js} +25 -25
- package/dist/node/vault-prewarm.js.map +1 -0
- package/dist/node/{gitvault-profile-scan.d.ts → vault-profile-scan.d.ts} +3 -3
- package/dist/node/vault-profile-scan.d.ts.map +1 -0
- package/dist/node/{gitvault-profile-scan.js → vault-profile-scan.js} +12 -12
- package/dist/node/vault-profile-scan.js.map +1 -0
- package/dist/node/{gitvault-prune.d.ts → vault-prune.d.ts} +61 -61
- package/dist/node/vault-prune.d.ts.map +1 -0
- package/dist/node/{gitvault-prune.js → vault-prune.js} +34 -34
- package/dist/node/vault-prune.js.map +1 -0
- package/dist/node/{gitvault-publication.d.ts → vault-publication.d.ts} +399 -399
- package/dist/node/vault-publication.d.ts.map +1 -0
- package/dist/node/{gitvault-publication.js → vault-publication.js} +584 -584
- package/dist/node/vault-publication.js.map +1 -0
- package/dist/node/{gitvault-push-to-create.d.ts → vault-push-to-create.d.ts} +16 -16
- package/dist/node/vault-push-to-create.d.ts.map +1 -0
- package/dist/node/{gitvault-push-to-create.js → vault-push-to-create.js} +9 -9
- package/dist/node/vault-push-to-create.js.map +1 -0
- package/dist/node/{gitvault-recover.d.ts → vault-recover.d.ts} +50 -50
- package/dist/node/vault-recover.d.ts.map +1 -0
- package/dist/node/{gitvault-recover.js → vault-recover.js} +84 -84
- package/dist/node/vault-recover.js.map +1 -0
- package/dist/node/{gitvault-restore.d.ts → vault-restore.d.ts} +8 -8
- package/dist/node/vault-restore.d.ts.map +1 -0
- package/dist/node/{gitvault-restore.js → vault-restore.js} +11 -11
- package/dist/node/vault-restore.js.map +1 -0
- package/dist/node/{gitvault-snapshot.d.ts → vault-snapshot.d.ts} +48 -48
- package/dist/node/vault-snapshot.d.ts.map +1 -0
- package/dist/node/{gitvault-snapshot.js → vault-snapshot.js} +61 -61
- package/dist/node/vault-snapshot.js.map +1 -0
- package/dist/node/{gitvault-writer-state.d.ts → vault-writer-state.d.ts} +8 -8
- package/dist/node/vault-writer-state.d.ts.map +1 -0
- package/dist/node/{gitvault-writer-state.js → vault-writer-state.js} +15 -15
- package/dist/node/vault-writer-state.js.map +1 -0
- package/dist/node/workflow-output.js +4 -4
- package/dist/node/workflow-output.js.map +1 -1
- package/package.json +1 -1
- package/dist/namespaces/gitvault.crypto.d.ts.map +0 -1
- package/dist/namespaces/gitvault.crypto.js.map +0 -1
- package/dist/namespaces/gitvault.d.ts.map +0 -1
- package/dist/namespaces/gitvault.js.map +0 -1
- package/dist/namespaces/gitvault.types.d.ts.map +0 -1
- package/dist/namespaces/gitvault.types.js.map +0 -1
- package/dist/node/gitvault-address.d.ts.map +0 -1
- package/dist/node/gitvault-address.js.map +0 -1
- package/dist/node/gitvault-apply.d.ts.map +0 -1
- package/dist/node/gitvault-apply.js.map +0 -1
- package/dist/node/gitvault-byo-config.d.ts +0 -29
- package/dist/node/gitvault-byo-config.d.ts.map +0 -1
- package/dist/node/gitvault-byo-config.js.map +0 -1
- package/dist/node/gitvault-byo-probe.d.ts +0 -20
- package/dist/node/gitvault-byo-probe.d.ts.map +0 -1
- package/dist/node/gitvault-byo-probe.js +0 -57
- package/dist/node/gitvault-byo-probe.js.map +0 -1
- package/dist/node/gitvault-creation-journal.d.ts.map +0 -1
- package/dist/node/gitvault-creation-journal.js.map +0 -1
- package/dist/node/gitvault-degraded-read.d.ts.map +0 -1
- package/dist/node/gitvault-degraded-read.js.map +0 -1
- package/dist/node/gitvault-deploy.d.ts.map +0 -1
- package/dist/node/gitvault-deploy.js.map +0 -1
- package/dist/node/gitvault-edge-fetch.d.ts.map +0 -1
- package/dist/node/gitvault-edge-fetch.js.map +0 -1
- package/dist/node/gitvault-handoff.d.ts.map +0 -1
- package/dist/node/gitvault-handoff.js.map +0 -1
- package/dist/node/gitvault-keystore.d.ts.map +0 -1
- package/dist/node/gitvault-keystore.js.map +0 -1
- package/dist/node/gitvault-member-bundle.d.ts.map +0 -1
- package/dist/node/gitvault-member-bundle.js.map +0 -1
- package/dist/node/gitvault-mirror-backend.d.ts.map +0 -1
- package/dist/node/gitvault-mirror-backend.js.map +0 -1
- package/dist/node/gitvault-mirror-config.d.ts.map +0 -1
- package/dist/node/gitvault-mirror-config.js.map +0 -1
- package/dist/node/gitvault-mirror.d.ts.map +0 -1
- package/dist/node/gitvault-mirror.js.map +0 -1
- package/dist/node/gitvault-native-crypto.d.ts.map +0 -1
- package/dist/node/gitvault-native-crypto.js.map +0 -1
- package/dist/node/gitvault-open-or-create.d.ts.map +0 -1
- package/dist/node/gitvault-open-or-create.js.map +0 -1
- package/dist/node/gitvault-prewarm.d.ts.map +0 -1
- package/dist/node/gitvault-prewarm.js.map +0 -1
- package/dist/node/gitvault-profile-scan.d.ts.map +0 -1
- package/dist/node/gitvault-profile-scan.js.map +0 -1
- package/dist/node/gitvault-prune.d.ts.map +0 -1
- package/dist/node/gitvault-prune.js.map +0 -1
- package/dist/node/gitvault-publication.d.ts.map +0 -1
- package/dist/node/gitvault-publication.js.map +0 -1
- package/dist/node/gitvault-push-to-create.d.ts.map +0 -1
- package/dist/node/gitvault-push-to-create.js.map +0 -1
- package/dist/node/gitvault-recover.d.ts.map +0 -1
- package/dist/node/gitvault-recover.js.map +0 -1
- package/dist/node/gitvault-restore.d.ts.map +0 -1
- package/dist/node/gitvault-restore.js.map +0 -1
- package/dist/node/gitvault-snapshot.d.ts.map +0 -1
- package/dist/node/gitvault-snapshot.js.map +0 -1
- package/dist/node/gitvault-writer-state.d.ts.map +0 -1
- package/dist/node/gitvault-writer-state.js.map +0 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* vault — `r402s/v0` crypto core (protocol rev 40, §1 + §2; task 5.1).
|
|
3
3
|
*
|
|
4
4
|
* Pure, isomorphic (WebCrypto + noble + `@hpke/core`), no I/O. Everything
|
|
5
5
|
* byte-level the protocol pins lives here ONCE and is replayed against the
|
|
6
|
-
* frozen vector set (`
|
|
6
|
+
* frozen vector set (`vault.crypto.test.ts`):
|
|
7
7
|
*
|
|
8
8
|
* - JCS (RFC 8785) for the no-JSON-numbers profile + the strict-parse profile
|
|
9
9
|
* - the two hash rules: stored bytes (JCS INCLUDING `signature`) and the
|
|
@@ -39,65 +39,65 @@ import { Chacha20Poly1305 } from "@hpke/chacha20poly1305";
|
|
|
39
39
|
import { LocalError } from "../errors.js";
|
|
40
40
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
41
41
|
/** Wire format tag (retained by decision D181 across the product rename). */
|
|
42
|
-
export const
|
|
42
|
+
export const VAULT_FORMAT = "r402s/v0";
|
|
43
43
|
/** The one V0 cryptographic suite. */
|
|
44
|
-
export const
|
|
44
|
+
export const VAULT_SUITE = "r402s-1";
|
|
45
45
|
/** Frame magic, bytes 0–5 of every encrypted object. */
|
|
46
|
-
export const
|
|
46
|
+
export const VAULT_FRAME_MAGIC = "R402S0";
|
|
47
47
|
/** Frame suite byte (byte 6) and its two-char AAD spelling. */
|
|
48
|
-
export const
|
|
49
|
-
export const
|
|
48
|
+
export const VAULT_FRAME_SUITE_BYTE = 0x01;
|
|
49
|
+
export const VAULT_FRAME_SUITE_ID = "01";
|
|
50
50
|
/** Protocol §2: max plaintext per encrypted object (256 MiB). */
|
|
51
|
-
export const
|
|
51
|
+
export const VAULT_MAX_PLAINTEXT_BYTES = 256 * 1024 * 1024;
|
|
52
52
|
/** The genesis generation / epoch constants (schema consts). */
|
|
53
|
-
export const
|
|
54
|
-
export const
|
|
53
|
+
export const VAULT_GENESIS_GENERATION = "0000000000000000";
|
|
54
|
+
export const VAULT_GENESIS_EPOCH = "0000000000000001";
|
|
55
55
|
/** HPKE `info` label for `key_envelope` (D188). */
|
|
56
|
-
export const
|
|
56
|
+
export const VAULT_ENVELOPE_INFO_LABEL = "r402s/v0/envelope";
|
|
57
57
|
/**
|
|
58
58
|
* The V0-A terminal-loss sentence — protocol §0 / client-surface spec. Doctor
|
|
59
59
|
* and status surfaces MUST state it verbatim while V0-A is current. Do not
|
|
60
60
|
* paraphrase; the wording is a reviewed product commitment.
|
|
61
61
|
*/
|
|
62
|
-
export const
|
|
62
|
+
export const VAULT_TERMINAL_LOSS_STATEMENT = "whole-machine or whole-keystore loss is terminal for vault history until human envelopes ship";
|
|
63
63
|
/**
|
|
64
64
|
* The protocol's durability sentence — the keystore-qualified half of the
|
|
65
65
|
* terminal-loss statement, reusable on its own. It is TRUE regardless of how
|
|
66
|
-
* many principals cover a vault (unlike {@link
|
|
66
|
+
* many principals cover a vault (unlike {@link VAULT_TERMINAL_LOSS_STATEMENT},
|
|
67
67
|
* which is specifically the single-principal V0-A claim), so a caller that has
|
|
68
68
|
* locally proven a vault carries >= 2 covering recipients prints this sentence
|
|
69
|
-
* instead of the terminal-loss one — see `
|
|
69
|
+
* instead of the terminal-loss one — see `Repos.status()`'s `covering_recipients`
|
|
70
70
|
* field. Never paraphrase; the wording is a reviewed product commitment.
|
|
71
71
|
*/
|
|
72
|
-
export const
|
|
72
|
+
export const VAULT_DURABILITY_STATEMENT = "The vault protects source history from host-side loss while a principal keystore survives.";
|
|
73
73
|
/** Protocol §0 sentence in full, as `doctor`/`status` print it. */
|
|
74
|
-
export const
|
|
75
|
-
`${
|
|
74
|
+
export const VAULT_TERMINAL_LOSS_DOCTOR_TEXT = "In V0-A, whole-machine or whole-keystore loss is terminal for vault history (VAULT_UNRECOVERABLE) until human envelopes ship. " +
|
|
75
|
+
`${VAULT_DURABILITY_STATEMENT} ` +
|
|
76
76
|
"The remaining paths are the platform's custodial restore of deployed artifacts (the deploy lane's CAS) and org/infra recovery; " +
|
|
77
77
|
"a recovery receipt authenticates a genesis — it cannot decrypt anything.";
|
|
78
78
|
/**
|
|
79
|
-
*
|
|
79
|
+
* vault-mirror-and-recover (design D8) — the two honesty statements. Both
|
|
80
80
|
* appear VERBATIM wherever mirror status or recovery success is shown (mirror
|
|
81
81
|
* `status`/`sync`, `recover`, keyless `mirror verify`), same voice and
|
|
82
|
-
* mechanism as {@link
|
|
82
|
+
* mechanism as {@link VAULT_TERMINAL_LOSS_STATEMENT} above. Never
|
|
83
83
|
* paraphrased — the §6.4 honesty posture is a reviewed product commitment.
|
|
84
84
|
*/
|
|
85
|
-
export const
|
|
85
|
+
export const VAULT_MIRROR_VALIDITY_NOT_FRESHNESS_STATEMENT = "this recovery proves validity, never freshness — a mirror (or the vault itself) can only tell you the newest generation it happens to hold, never that a newer one does not exist elsewhere";
|
|
86
86
|
/** The mirror does not change the V0 terminal-loss sentence: it is durability, not a second key. */
|
|
87
|
-
export const
|
|
87
|
+
export const VAULT_MIRROR_KEYSTORE_STILL_REQUIRED_STATEMENT = "a mirror without the principal keystore (or an equivalent key) recovers nothing — mirroring ciphertext does not create a second key, and the V0 terminal-loss sentence is unchanged";
|
|
88
88
|
/**
|
|
89
|
-
*
|
|
89
|
+
* vault-mirror-default — the recommended-default framing, stated the same
|
|
90
90
|
* way everywhere (the copy gate pins the doc surfaces to it). "Replicated"
|
|
91
91
|
* describes the platform's storage substrate (S3 multi-AZ redundancy), never
|
|
92
92
|
* a second run402 service; the customer-owned mirror is the only copy outside
|
|
93
93
|
* run402's custody, and — per the two honesty statements above — it proves
|
|
94
94
|
* validity, never freshness, and recovers nothing without the keystore.
|
|
95
95
|
*/
|
|
96
|
-
export const
|
|
96
|
+
export const VAULT_MIRROR_THREE_COPIES_STATEMENT = "the recommended shape is three copies: your working clone, the platform's replicated vault, and a mirror in storage you own";
|
|
97
97
|
/**
|
|
98
|
-
*
|
|
98
|
+
* vault-byo-primary-bucket (design D4) — the degraded-read mechanism
|
|
99
99
|
* sentence: the invariant part of the ONE stderr line a degraded chain/
|
|
100
|
-
* payload read prints (`
|
|
100
|
+
* payload read prints (`formatVaultDegradedReadNote`, `vault-node-
|
|
101
101
|
* degraded-read.ts`). Mechanism-only, per the copy rules — never a
|
|
102
102
|
* confidentiality claim: it names WHY (run402 is unreachable), restates
|
|
103
103
|
* validity-not-freshness in brief, and states that a write still needs the
|
|
@@ -107,84 +107,84 @@ export const GITVAULT_MIRROR_THREE_COPIES_STATEMENT = "the recommended shape is
|
|
|
107
107
|
* the constant. Never paraphrase — same reviewed-product-commitment voice
|
|
108
108
|
* as the other statements in this file.
|
|
109
109
|
*/
|
|
110
|
-
export const
|
|
110
|
+
export const VAULT_DEGRADED_READ_STATEMENT = "run402 is unreachable — this read is served from your mirror; it proves validity, not freshness; a later push still requires the gateway";
|
|
111
111
|
/**
|
|
112
|
-
*
|
|
112
|
+
* vault-mirror-default — the `vault_unmirrored` standing finding, worded to
|
|
113
113
|
* stay true in BOTH pre-clear states (no mirror configured at all, and a
|
|
114
114
|
* configured mirror with no successful write/sync yet — the finding clears on
|
|
115
115
|
* the first successful mirror write or sync, never on configuration alone).
|
|
116
116
|
* Informational, never blocking; computed client-side only, so the gateway
|
|
117
117
|
* never learns whether a mirror exists.
|
|
118
118
|
*/
|
|
119
|
-
export const
|
|
119
|
+
export const VAULT_UNMIRRORED_FINDING_STATEMENT = `this vault has no customer-held mirror copy yet — ${VAULT_MIRROR_THREE_COPIES_STATEMENT}; until a first mirror write or sync succeeds, the third copy does not exist`;
|
|
120
120
|
/**
|
|
121
|
-
*
|
|
121
|
+
* vault-mirror-default — the one-liner `repos create` prints beside the
|
|
122
122
|
* recovery receipt: the two things worth doing in the first minute, stated in
|
|
123
123
|
* the first minute. Deliberately custody-scoped ("stays in your custody"),
|
|
124
124
|
* never a recoverability claim — the terminal-loss statement printed beside it
|
|
125
125
|
* carries the key half.
|
|
126
126
|
*/
|
|
127
|
-
export const
|
|
127
|
+
export const VAULT_MIRROR_SETUP_HINT = "recommended: 'run402 repos mirror <destination>' (an s3:// bucket or a local directory) starts the customer-owned mirror — every later capture is mirrored to it automatically, and it is the copy that stays in your custody";
|
|
128
128
|
/**
|
|
129
|
-
*
|
|
129
|
+
* vault-byo-primary-bucket (proposal, the ratified D + chain-copy rung-3
|
|
130
130
|
* headline; design D10 — verified from network topology once the client's
|
|
131
131
|
* BYO write path ships, not by this sentence alone). Structurally scoped to
|
|
132
132
|
* `storage_profile:"byo"` ONLY — the sentence is FALSE for a managed vault
|
|
133
133
|
* (payload ciphertext IS client-written to run402's own bucket there), so
|
|
134
134
|
* every caller MUST gate it behind a BYO-profile check before printing it
|
|
135
|
-
* and MUST NEVER use it as a blanket claim describing
|
|
135
|
+
* and MUST NEVER use it as a blanket claim describing vault in general.
|
|
136
136
|
* INERT as of this fold (Phase 1, protocol coordination only) — nothing
|
|
137
137
|
* emits it yet; Phase 2/3 wire the allocation/doctor/`repos view` surfaces
|
|
138
138
|
* that print it once `storage_profile:"byo"` is a real, allocatable vault
|
|
139
139
|
* state. Never paraphrase — the wording is a reviewed product commitment.
|
|
140
140
|
*/
|
|
141
|
-
export const
|
|
141
|
+
export const VAULT_BYO_HEADLINE_STATEMENT = "your source ciphertext never touches our infrastructure — not even encrypted";
|
|
142
142
|
/**
|
|
143
|
-
*
|
|
143
|
+
* vault-byo-primary-bucket (design D7) — the allocation-time no-payload-copy
|
|
144
144
|
* disclosure a BYO `repos create` prints, unconditionally and independent of
|
|
145
145
|
* mirror status: unlike a managed vault (where the platform's own bucket is
|
|
146
146
|
* an extra copy beside the customer's clone), a BYO vault's primary bucket is
|
|
147
147
|
* the ONLY payload copy from the moment it is allocated — run402 holds the
|
|
148
148
|
* small signed chain only, never the payload. Distinct from
|
|
149
|
-
* {@link
|
|
149
|
+
* {@link VAULT_BYO_UNMIRRORED_REMEDY_STATEMENT} below, which is about a
|
|
150
150
|
* missing SECOND customer-held copy on top of this primary one. INERT as of
|
|
151
151
|
* this fold — Phase 2/3 wire the surfaces that print it. Never paraphrase.
|
|
152
152
|
*/
|
|
153
|
-
export const
|
|
153
|
+
export const VAULT_BYO_NO_PAYLOAD_COPY_STATEMENT = "run402 holds no payload copy of a BYO vault — only the small signed chain; your primary bucket is the sole copy of your source until you add a second customer-held location";
|
|
154
154
|
/**
|
|
155
|
-
*
|
|
156
|
-
* `vault_unmirrored` finding's remedy (
|
|
155
|
+
* vault-byo-primary-bucket (design D7) — the BYO variant of the
|
|
156
|
+
* `vault_unmirrored` finding's remedy (vault-mirror-default): for a
|
|
157
157
|
* managed vault the remedy is an ADDITIONAL mirror beside the platform's own
|
|
158
|
-
* replicated copy ({@link
|
|
158
|
+
* replicated copy ({@link VAULT_MIRROR_SETUP_HINT}); for a BYO vault the
|
|
159
159
|
* platform holds no payload copy to fall back on
|
|
160
|
-
* ({@link
|
|
160
|
+
* ({@link VAULT_BYO_NO_PAYLOAD_COPY_STATEMENT}), so the remedy names a
|
|
161
161
|
* SECOND customer-held location as the only additional copy available — the
|
|
162
162
|
* ordinary `repos mirror` machinery works unchanged against a different
|
|
163
163
|
* destination (design D7). The `vault_unmirrored` finding itself still
|
|
164
164
|
* applies to BYO vaults; only this remedy half is BYO-specific. INERT as of
|
|
165
165
|
* this fold — Phase 2/3 wire the surfaces that print it. Never paraphrase.
|
|
166
166
|
*/
|
|
167
|
-
export const
|
|
167
|
+
export const VAULT_BYO_UNMIRRORED_REMEDY_STATEMENT = "add a second customer-held location: 'run402 repos mirror <destination>' works unchanged against a different destination — for a BYO vault this is your only additional copy, since run402 holds no payload copy of its own";
|
|
168
168
|
const FRAME_NONCE_BYTES = 24;
|
|
169
169
|
const FRAME_HEADER_BYTES = 6 + 1 + FRAME_NONCE_BYTES;
|
|
170
170
|
const AEAD_TAG_BYTES = 16;
|
|
171
171
|
// ─── Scalar grammars (common.json) ───────────────────────────────────────────
|
|
172
|
-
export const
|
|
173
|
-
export const
|
|
174
|
-
export const
|
|
175
|
-
export const
|
|
176
|
-
export const
|
|
177
|
-
export const
|
|
178
|
-
export const
|
|
179
|
-
export const
|
|
180
|
-
export const
|
|
172
|
+
export const VAULT_HEX16_RE = /^[0-9a-f]{16}$/;
|
|
173
|
+
export const VAULT_HEX32_RE = /^[0-9a-f]{32}$/;
|
|
174
|
+
export const VAULT_SHA256_RE = /^[0-9a-f]{64}$/;
|
|
175
|
+
export const VAULT_OID40_RE = /^[0-9a-f]{40}$/;
|
|
176
|
+
export const VAULT_VK_RE = /^vk_[0-9a-f]{32}$/;
|
|
177
|
+
export const VAULT_EK_RE = /^ek_[0-9a-f]{32}$/;
|
|
178
|
+
export const VAULT_SRC_RE = /^src_[0-9a-f]{32}$/;
|
|
179
|
+
export const VAULT_SERVICE_KEY_ID_RE = /^sk_[0-9a-z-]{4,64}$/;
|
|
180
|
+
export const VAULT_SIZE_BYTES_RE = /^(0|[1-9][0-9]{0,14})$/;
|
|
181
181
|
/** 32 bytes base64url: 43 chars, final char in the 16-value padding class. */
|
|
182
|
-
export const
|
|
182
|
+
export const VAULT_B64U_32_RE = /^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/;
|
|
183
183
|
/** 64 bytes base64url: 86 chars, final char ∈ {A,Q,g,w}. */
|
|
184
|
-
export const
|
|
184
|
+
export const VAULT_B64U_64_RE = /^[A-Za-z0-9_-]{85}[AQgw]$/;
|
|
185
185
|
/** The HPKE-sealed K_repo: 48 bytes = exactly 64 base64url chars. */
|
|
186
|
-
export const
|
|
187
|
-
export const
|
|
186
|
+
export const VAULT_HPKE_CT_RE = /^[A-Za-z0-9_-]{64}$/;
|
|
187
|
+
export const VAULT_TIMESTAMP_RE = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]\.[0-9]{3}Z$/;
|
|
188
188
|
function fail(code, message, context, details) {
|
|
189
189
|
throw new LocalError(message, context, { code, details });
|
|
190
190
|
}
|
|
@@ -199,10 +199,10 @@ export function isCanonicalBase64url(value) {
|
|
|
199
199
|
return false;
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
|
-
/** Decode canonical base64url or throw (`
|
|
202
|
+
/** Decode canonical base64url or throw (`VAULT_NONCANONICAL_BASE64URL`). */
|
|
203
203
|
export function fromBase64url(value, field = "value") {
|
|
204
204
|
if (!isCanonicalBase64url(value)) {
|
|
205
|
-
fail("
|
|
205
|
+
fail("VAULT_NONCANONICAL_BASE64URL", `${field} is not canonical base64url`, "decoding r402s/v0 scalar", { field });
|
|
206
206
|
}
|
|
207
207
|
return base64urlnopad.decode(value);
|
|
208
208
|
}
|
|
@@ -210,8 +210,8 @@ export function toBase64url(bytes) {
|
|
|
210
210
|
return base64urlnopad.encode(bytes);
|
|
211
211
|
}
|
|
212
212
|
/** RFC 3339 UTC ms `Z` with semantic calendar validation (D187: `2026-02-31` and a non-leap Feb 29 are rejects). */
|
|
213
|
-
export function
|
|
214
|
-
if (!
|
|
213
|
+
export function isValidVaultTimestamp(value) {
|
|
214
|
+
if (!VAULT_TIMESTAMP_RE.test(value))
|
|
215
215
|
return false;
|
|
216
216
|
const year = Number(value.slice(0, 4));
|
|
217
217
|
const month = Number(value.slice(5, 7));
|
|
@@ -221,7 +221,7 @@ export function isValidGitvaultTimestamp(value) {
|
|
|
221
221
|
return day <= daysInMonth;
|
|
222
222
|
}
|
|
223
223
|
/** Format a Date as the protocol timestamp grammar (RFC 3339 UTC, milliseconds, `Z`). */
|
|
224
|
-
export function
|
|
224
|
+
export function formatVaultTimestamp(date = new Date()) {
|
|
225
225
|
return date.toISOString();
|
|
226
226
|
}
|
|
227
227
|
// ─── Encoding helpers ────────────────────────────────────────────────────────
|
|
@@ -242,11 +242,11 @@ let hashBackend = null;
|
|
|
242
242
|
* SDK-INTERNAL (underscore-exported so the byte-equality suite can swap
|
|
243
243
|
* backends). Installing `null` restores the `@noble` default.
|
|
244
244
|
*/
|
|
245
|
-
export function
|
|
245
|
+
export function _setVaultHashBackend(backend) {
|
|
246
246
|
hashBackend = backend;
|
|
247
247
|
}
|
|
248
248
|
/** SDK-INTERNAL: which hash backend is live. The isomorphic-entry test asserts `null`. */
|
|
249
|
-
export function
|
|
249
|
+
export function _vaultHashBackend() {
|
|
250
250
|
return hashBackend;
|
|
251
251
|
}
|
|
252
252
|
export function sha256Hex(bytes) {
|
|
@@ -261,7 +261,7 @@ export function randomBytes(length) {
|
|
|
261
261
|
return out;
|
|
262
262
|
}
|
|
263
263
|
/** A fresh `<prefix>_<32 lowercase hex>` id (protocol §1: ids are CSPRNG). */
|
|
264
|
-
export function
|
|
264
|
+
export function newVaultId(prefix) {
|
|
265
265
|
return `${prefix}_${bytesToHex(randomBytes(16))}`;
|
|
266
266
|
}
|
|
267
267
|
/** A fresh 32-lowercase-hex scalar (client_creation_id, capture_id, …). */
|
|
@@ -273,7 +273,7 @@ function assertProfileValue(value, path) {
|
|
|
273
273
|
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
274
274
|
return;
|
|
275
275
|
if (typeof value === "number" || typeof value === "bigint") {
|
|
276
|
-
fail("
|
|
276
|
+
fail("VAULT_JSON_NUMBER", `r402s/v0 profile forbids JSON numbers (at ${path})`, "canonicalizing r402s/v0 object", { path });
|
|
277
277
|
}
|
|
278
278
|
if (Array.isArray(value)) {
|
|
279
279
|
value.forEach((entry, i) => assertProfileValue(entry, `${path}[${i}]`));
|
|
@@ -282,13 +282,13 @@ function assertProfileValue(value, path) {
|
|
|
282
282
|
if (typeof value === "object") {
|
|
283
283
|
for (const [key, entry] of Object.entries(value)) {
|
|
284
284
|
if (entry === undefined) {
|
|
285
|
-
fail("
|
|
285
|
+
fail("VAULT_JSON_UNDEFINED", `undefined member at ${path}.${key}`, "canonicalizing r402s/v0 object", { path: `${path}.${key}` });
|
|
286
286
|
}
|
|
287
287
|
assertProfileValue(entry, `${path}.${key}`);
|
|
288
288
|
}
|
|
289
289
|
return;
|
|
290
290
|
}
|
|
291
|
-
fail("
|
|
291
|
+
fail("VAULT_JSON_UNSUPPORTED", `unsupported value at ${path}`, "canonicalizing r402s/v0 object", { path });
|
|
292
292
|
}
|
|
293
293
|
/** RFC 8785 canonical JSON (no-numbers profile) as UTF-8 bytes. */
|
|
294
294
|
export function jcs(value) {
|
|
@@ -305,14 +305,14 @@ function inspectStrict(node, path) {
|
|
|
305
305
|
case "Document":
|
|
306
306
|
return inspectStrict(node.body, path);
|
|
307
307
|
case "Number":
|
|
308
|
-
fail("
|
|
308
|
+
fail("VAULT_STRICT_PARSE", `JSON number at ${path}`, "strict-parsing r402s/v0 object", { reason: "json-number", path });
|
|
309
309
|
// eslint-disable-next-line no-fallthrough
|
|
310
310
|
case "Object": {
|
|
311
311
|
const seen = new Set();
|
|
312
312
|
for (const member of node.members) {
|
|
313
313
|
const name = member.name.type === "String" ? member.name.value : member.name.name;
|
|
314
314
|
if (seen.has(name)) {
|
|
315
|
-
fail("
|
|
315
|
+
fail("VAULT_STRICT_PARSE", `duplicate member ${name} at ${path}`, "strict-parsing r402s/v0 object", { reason: "duplicate-member", path: `${path}.${name}` });
|
|
316
316
|
}
|
|
317
317
|
seen.add(name);
|
|
318
318
|
inspectStrict(member.value, `${path}.${name}`);
|
|
@@ -332,24 +332,24 @@ function inspectStrict(node, path) {
|
|
|
332
332
|
* (member order, whitespace, escapes). Schema-awareness (unknown members,
|
|
333
333
|
* hex case, scalar lengths) is the caller's job.
|
|
334
334
|
*/
|
|
335
|
-
export function
|
|
335
|
+
export function parseVaultStrict(text) {
|
|
336
336
|
let ast;
|
|
337
337
|
try {
|
|
338
338
|
ast = parse(text, { mode: "json", allowTrailingCommas: false });
|
|
339
339
|
}
|
|
340
340
|
catch {
|
|
341
|
-
fail("
|
|
341
|
+
fail("VAULT_STRICT_PARSE", "invalid JSON", "strict-parsing r402s/v0 object", { reason: "invalid-json" });
|
|
342
342
|
}
|
|
343
343
|
inspectStrict(ast, "$");
|
|
344
344
|
const value = evaluate(ast);
|
|
345
345
|
if (jcsString(value) !== text) {
|
|
346
|
-
fail("
|
|
346
|
+
fail("VAULT_STRICT_PARSE", "text is not canonical JCS", "strict-parsing r402s/v0 object", { reason: "noncanonical-encoding" });
|
|
347
347
|
}
|
|
348
348
|
return value;
|
|
349
349
|
}
|
|
350
350
|
/** Classify a strict-parse failure (for vector replay + diagnostics). */
|
|
351
|
-
export function
|
|
352
|
-
if (error instanceof LocalError && error.code === "
|
|
351
|
+
export function vaultStrictParseReason(error) {
|
|
352
|
+
if (error instanceof LocalError && error.code === "VAULT_STRICT_PARSE") {
|
|
353
353
|
const details = error.details;
|
|
354
354
|
return details?.reason ?? null;
|
|
355
355
|
}
|
|
@@ -375,26 +375,26 @@ function withoutSignature(object) {
|
|
|
375
375
|
* (`sha256(JCS(descriptor minus signature))`) without reaching into this
|
|
376
376
|
* module's private helper twice under two different names.
|
|
377
377
|
*/
|
|
378
|
-
export function
|
|
378
|
+
export function vaultWithoutSignature(object) {
|
|
379
379
|
return withoutSignature(object);
|
|
380
380
|
}
|
|
381
381
|
/**
|
|
382
382
|
* Signature preimage = `"r402s/v0/" + domain + "\n" + JCS(object without its
|
|
383
|
-
* single top-level signature)`. `domain` is USUALLY a `
|
|
383
|
+
* single top-level signature)`. `domain` is USUALLY a `VaultObjectKind`
|
|
384
384
|
* (an actual `object_kind`-carrying signed object), but the type accepts any
|
|
385
|
-
* string:
|
|
385
|
+
* string: vault-multi-writer rev 47's `writer_acceptance.statement` reuses
|
|
386
386
|
* this exact byte-construction under the ad-hoc domain
|
|
387
387
|
* `"handoff-writer-accept/v1"` for a sub-object that is NOT itself a
|
|
388
|
-
* `
|
|
388
|
+
* `VaultSignedObject` (no `object_kind`/`signature` fields of its own —
|
|
389
389
|
* both signatures over it live on the PARENT `writer_acceptance`). Matches
|
|
390
390
|
* the gateway's own `signaturePreimage(objectKind: string, ...)`, which was
|
|
391
391
|
* never narrower than `string` to begin with.
|
|
392
392
|
*/
|
|
393
393
|
export function signaturePreimage(domain, objectWithoutSignature) {
|
|
394
394
|
if ("signature" in objectWithoutSignature) {
|
|
395
|
-
fail("
|
|
395
|
+
fail("VAULT_PREIMAGE_HAS_SIGNATURE", "preimage input must not carry a signature member", "building r402s/v0 signature preimage");
|
|
396
396
|
}
|
|
397
|
-
return concatBytes(utf8ToBytes(`${
|
|
397
|
+
return concatBytes(utf8ToBytes(`${VAULT_FORMAT}/${domain}\n`), jcs(objectWithoutSignature));
|
|
398
398
|
}
|
|
399
399
|
// ─── Ed25519 strict (RFC 8032, zip215:false) ─────────────────────────────────
|
|
400
400
|
export function ed25519PublicKey(seed) {
|
|
@@ -419,7 +419,7 @@ export function ed25519VerifyStrict(signature, message, publicKey) {
|
|
|
419
419
|
}
|
|
420
420
|
}
|
|
421
421
|
/** Sign an object under its `object_kind` domain; returns the complete signed object. */
|
|
422
|
-
export function
|
|
422
|
+
export function signVaultObject(objectWithoutSignature, seed) {
|
|
423
423
|
const preimage = signaturePreimage(objectWithoutSignature.object_kind, objectWithoutSignature);
|
|
424
424
|
return { ...objectWithoutSignature, signature: toBase64url(ed25519Sign(preimage, seed)) };
|
|
425
425
|
}
|
|
@@ -428,12 +428,12 @@ export function signGitvaultObject(objectWithoutSignature, seed) {
|
|
|
428
428
|
* checked FIRST (an 85-char signature or a 42-char key is a schema reject,
|
|
429
429
|
* never a decode-time surprise), then strict Ed25519 under the object's domain.
|
|
430
430
|
*/
|
|
431
|
-
export function
|
|
432
|
-
if (typeof object.signature !== "string" || !
|
|
431
|
+
export function verifyVaultObject(object, publicKey) {
|
|
432
|
+
if (typeof object.signature !== "string" || !VAULT_B64U_64_RE.test(object.signature) || !isCanonicalBase64url(object.signature))
|
|
433
433
|
return false;
|
|
434
434
|
let pub;
|
|
435
435
|
if (typeof publicKey === "string") {
|
|
436
|
-
if (!
|
|
436
|
+
if (!VAULT_B64U_32_RE.test(publicKey) || !isCanonicalBase64url(publicKey))
|
|
437
437
|
return false;
|
|
438
438
|
pub = fromBase64url(publicKey);
|
|
439
439
|
}
|
|
@@ -443,10 +443,10 @@ export function verifyGitvaultObject(object, publicKey) {
|
|
|
443
443
|
const preimage = signaturePreimage(object.object_kind, withoutSignature(object));
|
|
444
444
|
return ed25519VerifyStrict(fromBase64url(object.signature), preimage, pub);
|
|
445
445
|
}
|
|
446
|
-
/** Verify or throw `
|
|
447
|
-
export function
|
|
448
|
-
if (!
|
|
449
|
-
fail("
|
|
446
|
+
/** Verify or throw `VAULT_SIGNATURE_INVALID`. */
|
|
447
|
+
export function assertVaultSignature(object, publicKey, context = "verifying r402s/v0 signature") {
|
|
448
|
+
if (!verifyVaultObject(object, publicKey)) {
|
|
449
|
+
fail("VAULT_SIGNATURE_INVALID", `${object.object_kind} signature does not verify (strict RFC 8032)`, context, { object_kind: object.object_kind });
|
|
450
450
|
}
|
|
451
451
|
}
|
|
452
452
|
// ─── Key fingerprints + keypairs ─────────────────────────────────────────────
|
|
@@ -464,18 +464,18 @@ export function ekFingerprint(rawPublicKey) {
|
|
|
464
464
|
}
|
|
465
465
|
export function generateSigningKeypair(seed = randomBytes(32)) {
|
|
466
466
|
if (seed.length !== 32)
|
|
467
|
-
fail("
|
|
467
|
+
fail("VAULT_BAD_SEED", "Ed25519 seed must be 32 bytes", "generating signing keypair");
|
|
468
468
|
return { seed, public_key: ed25519PublicKey(seed) };
|
|
469
469
|
}
|
|
470
470
|
export function generateEncryptionKeypair(privateKey = randomBytes(32)) {
|
|
471
471
|
if (privateKey.length !== 32)
|
|
472
|
-
fail("
|
|
472
|
+
fail("VAULT_BAD_SEED", "X25519 private key must be 32 bytes", "generating encryption keypair");
|
|
473
473
|
return { private_key: privateKey, public_key: x25519.getPublicKey(privateKey) };
|
|
474
474
|
}
|
|
475
475
|
// ─── HKDF: k_obj / K_digest, keyed commitments ──────────────────────────────
|
|
476
476
|
/** `k_obj` info = lp("r402s/v0") ‖ lp(suite) ‖ lp(repo_id) ‖ lp(epoch) ‖ lp(object_kind) ‖ lp(object_id) ‖ lp("L=32"). */
|
|
477
477
|
export function objectKeyInfo(repoId, epoch, objectKind, objectId) {
|
|
478
|
-
return concatBytes(lp(
|
|
478
|
+
return concatBytes(lp(VAULT_FORMAT), lp(VAULT_SUITE), lp(repoId), lp(epoch), lp(objectKind), lp(objectId), lp("L=32"));
|
|
479
479
|
}
|
|
480
480
|
/** `k_obj = HKDF-SHA-256(ikm=K_repo, salt=∅, info, L=32)`. */
|
|
481
481
|
export function deriveObjectKey(kRepo, repoId, epoch, objectKind, objectId) {
|
|
@@ -483,7 +483,7 @@ export function deriveObjectKey(kRepo, repoId, epoch, objectKind, objectId) {
|
|
|
483
483
|
}
|
|
484
484
|
/** `K_digest` info = lp("r402s/v0") ‖ lp("r402s-1") ‖ lp(repo_id) ‖ lp(epoch) ‖ lp("digest") ‖ lp(label) ‖ lp("L=32"). */
|
|
485
485
|
export function digestKeyInfo(repoId, epoch, label) {
|
|
486
|
-
return concatBytes(lp(
|
|
486
|
+
return concatBytes(lp(VAULT_FORMAT), lp(VAULT_SUITE), lp(repoId), lp(epoch), lp("digest"), lp(label), lp("L=32"));
|
|
487
487
|
}
|
|
488
488
|
export function deriveDigestKey(kRepo, repoId, epoch, label) {
|
|
489
489
|
return deriveDigestKeyFrom(kRepo, repoId, epoch, label);
|
|
@@ -506,8 +506,8 @@ export function keyedCommitment(kDigest, content) {
|
|
|
506
506
|
export function objectsetContent(oids) {
|
|
507
507
|
const unique = [...new Set(oids)];
|
|
508
508
|
for (const oid of unique) {
|
|
509
|
-
if (!
|
|
510
|
-
fail("
|
|
509
|
+
if (!VAULT_OID40_RE.test(oid))
|
|
510
|
+
fail("VAULT_BAD_OID", `not a lowercase 40-hex oid: ${oid}`, "building objectset content");
|
|
511
511
|
}
|
|
512
512
|
return { oids: unique.sort() };
|
|
513
513
|
}
|
|
@@ -518,8 +518,8 @@ export function isCanonicalObjectset(content) {
|
|
|
518
518
|
}
|
|
519
519
|
/** The `"snapshot_oid"` content shape. */
|
|
520
520
|
export function snapshotOidContent(oid) {
|
|
521
|
-
if (!
|
|
522
|
-
fail("
|
|
521
|
+
if (!VAULT_OID40_RE.test(oid))
|
|
522
|
+
fail("VAULT_BAD_OID", `not a lowercase 40-hex oid: ${oid}`, "building snapshot_oid content");
|
|
523
523
|
return { oid, format: "sha1" };
|
|
524
524
|
}
|
|
525
525
|
// ─── Golden preimages (open-id / open-binding) ──────────────────────────────
|
|
@@ -534,7 +534,7 @@ export function openBindingPreimage(clientOpenId, baseHeadSha256, priorCheckpoin
|
|
|
534
534
|
// ─── AEAD frame (XChaCha20-Poly1305) ────────────────────────────────────────
|
|
535
535
|
/** The seven-member frame AAD, as the object JCS serializes. */
|
|
536
536
|
export function frameAad(repoId, objectKind, objectId, epoch) {
|
|
537
|
-
return { repo_id: repoId, object_kind: objectKind, object_id: objectId, epoch, suite:
|
|
537
|
+
return { repo_id: repoId, object_kind: objectKind, object_id: objectId, epoch, suite: VAULT_SUITE, magic: VAULT_FRAME_MAGIC, suite_id: VAULT_FRAME_SUITE_ID };
|
|
538
538
|
}
|
|
539
539
|
let aeadBackend = null;
|
|
540
540
|
/**
|
|
@@ -542,11 +542,11 @@ let aeadBackend = null;
|
|
|
542
542
|
* backends). Installing `null` restores the `@noble` default — which is what
|
|
543
543
|
* every runtime without a registration already has.
|
|
544
544
|
*/
|
|
545
|
-
export function
|
|
545
|
+
export function _setVaultAeadBackend(backend) {
|
|
546
546
|
aeadBackend = backend;
|
|
547
547
|
}
|
|
548
548
|
/** SDK-INTERNAL: which backend is live. The verifier-lineage test asserts `null`. */
|
|
549
|
-
export function
|
|
549
|
+
export function _vaultAeadBackend() {
|
|
550
550
|
return aeadBackend;
|
|
551
551
|
}
|
|
552
552
|
function aeadSeal(key32, nonce24, aad, plaintext) {
|
|
@@ -556,7 +556,7 @@ function aeadSeal(key32, nonce24, aad, plaintext) {
|
|
|
556
556
|
}
|
|
557
557
|
/**
|
|
558
558
|
* Returns the plaintext, or `null` for ANY failure the caller must render as
|
|
559
|
-
* `
|
|
559
|
+
* `VAULT_AEAD_AUTH_FAILURE` (D4). A backend that throws is treated as an
|
|
560
560
|
* auth failure rather than surfacing a second error shape from the same input
|
|
561
561
|
* space — the native path is an optimization, never a new failure mode.
|
|
562
562
|
*/
|
|
@@ -577,16 +577,16 @@ function aeadOpen(key32, nonce24, aad, ctAndTag) {
|
|
|
577
577
|
*/
|
|
578
578
|
export function sealFrame(input) {
|
|
579
579
|
if (input.k_obj.length !== 32)
|
|
580
|
-
fail("
|
|
581
|
-
if (input.plaintext.length >
|
|
580
|
+
fail("VAULT_BAD_KEY", "k_obj must be 32 bytes", "sealing r402s/v0 frame");
|
|
581
|
+
if (input.plaintext.length > VAULT_MAX_PLAINTEXT_BYTES) {
|
|
582
582
|
fail("GIT_OBJECT_TOO_LARGE", "plaintext exceeds the 256 MiB per-object cap", "sealing r402s/v0 frame", { size_bytes: String(input.plaintext.length) });
|
|
583
583
|
}
|
|
584
584
|
const nonce = input.nonce ?? randomBytes(FRAME_NONCE_BYTES);
|
|
585
585
|
if (nonce.length !== FRAME_NONCE_BYTES)
|
|
586
|
-
fail("
|
|
586
|
+
fail("VAULT_BAD_NONCE", "frame nonce must be 24 bytes", "sealing r402s/v0 frame");
|
|
587
587
|
const aad = jcs(frameAad(input.repo_id, input.object_kind, input.object_id, input.epoch));
|
|
588
588
|
const ct = aeadSeal(input.k_obj, nonce, aad, input.plaintext);
|
|
589
|
-
const frame = concatBytes(utf8ToBytes(
|
|
589
|
+
const frame = concatBytes(utf8ToBytes(VAULT_FRAME_MAGIC), new Uint8Array([VAULT_FRAME_SUITE_BYTE]), nonce, ct);
|
|
590
590
|
return { frame, ciphertext_sha256: sha256Hex(frame), size_bytes: String(frame.length), nonce_hex: bytesToHex(nonce) };
|
|
591
591
|
}
|
|
592
592
|
/**
|
|
@@ -597,14 +597,14 @@ export function sealFrame(input) {
|
|
|
597
597
|
export function openFrame(input) {
|
|
598
598
|
const { frame } = input;
|
|
599
599
|
if (input.expected_ciphertext_sha256 !== undefined && sha256Hex(frame) !== input.expected_ciphertext_sha256) {
|
|
600
|
-
fail("
|
|
600
|
+
fail("VAULT_RECEIPT_MISMATCH", "frame bytes do not hash to the receipt's ciphertext_sha256", "opening r402s/v0 frame", { object_id: input.object_id });
|
|
601
601
|
}
|
|
602
602
|
if (frame.length < FRAME_HEADER_BYTES + AEAD_TAG_BYTES) {
|
|
603
|
-
fail("
|
|
603
|
+
fail("VAULT_FRAME_INVALID", "frame shorter than header + tag", "opening r402s/v0 frame", { object_id: input.object_id });
|
|
604
604
|
}
|
|
605
605
|
const magic = new TextDecoder().decode(frame.subarray(0, 6));
|
|
606
|
-
if (magic !==
|
|
607
|
-
fail("
|
|
606
|
+
if (magic !== VAULT_FRAME_MAGIC || frame[6] !== VAULT_FRAME_SUITE_BYTE) {
|
|
607
|
+
fail("VAULT_FRAME_INVALID", "frame header magic/suite mismatch", "opening r402s/v0 frame", { object_id: input.object_id });
|
|
608
608
|
}
|
|
609
609
|
return openFrameWithAad(input.k_obj, frame, jcs(frameAad(input.repo_id, input.object_kind, input.object_id, input.epoch)), input.object_id);
|
|
610
610
|
}
|
|
@@ -616,20 +616,20 @@ export function openFrame(input) {
|
|
|
616
616
|
*/
|
|
617
617
|
export function openFrameWithAad(kObj, frame, aad, objectId = "?") {
|
|
618
618
|
if (frame.length < FRAME_HEADER_BYTES + AEAD_TAG_BYTES) {
|
|
619
|
-
fail("
|
|
619
|
+
fail("VAULT_FRAME_INVALID", "frame shorter than header + tag", "opening r402s/v0 frame", { object_id: objectId });
|
|
620
620
|
}
|
|
621
621
|
const nonce = frame.subarray(7, FRAME_HEADER_BYTES);
|
|
622
622
|
const ct = frame.subarray(FRAME_HEADER_BYTES);
|
|
623
623
|
const opened = aeadOpen(kObj, nonce, aad, ct);
|
|
624
624
|
if (opened === null) {
|
|
625
|
-
fail("
|
|
625
|
+
fail("VAULT_AEAD_AUTH_FAILURE", "frame failed AEAD authentication", "opening r402s/v0 frame", { object_id: objectId });
|
|
626
626
|
}
|
|
627
627
|
return opened;
|
|
628
628
|
}
|
|
629
629
|
// ─── HPKE (named implementation: @hpke/core + @hpke/chacha20poly1305) ───────
|
|
630
630
|
let hpkeSuite;
|
|
631
631
|
/** The one `r402s-1` HPKE suite: Base mode, DHKEM(X25519,HKDF-SHA256) 0x0020 / HKDF-SHA256 0x0001 / ChaCha20-Poly1305 0x0003. */
|
|
632
|
-
export function
|
|
632
|
+
export function vaultHpkeSuite() {
|
|
633
633
|
hpkeSuite ??= new CipherSuite({ kem: new DhkemX25519HkdfSha256(), kdf: new HkdfSha256(), aead: new Chacha20Poly1305() });
|
|
634
634
|
return hpkeSuite;
|
|
635
635
|
}
|
|
@@ -640,7 +640,7 @@ function toArrayBuffer(bytes) {
|
|
|
640
640
|
}
|
|
641
641
|
/** HPKE `info` (D188): lp("r402s/v0/envelope") ‖ lp("r402s-1") ‖ lp(lowerhex(SHA-256(recipient raw X25519 pubkey))) ‖ lp(created_by). */
|
|
642
642
|
export function envelopeInfo(recipientPublicKeyRaw, createdBy) {
|
|
643
|
-
return concatBytes(lp(
|
|
643
|
+
return concatBytes(lp(VAULT_ENVELOPE_INFO_LABEL), lp(VAULT_SUITE), lp(sha256Hex(recipientPublicKeyRaw)), lp(createdBy));
|
|
644
644
|
}
|
|
645
645
|
/**
|
|
646
646
|
* HPKE AAD (D188, discriminated by `rotation_id` presence — D203, rev 42):
|
|
@@ -660,31 +660,31 @@ export function envelopeAad(repoId, epoch, recipientFingerprint, rotationId) {
|
|
|
660
660
|
}
|
|
661
661
|
/** Single-shot HPKE seal (Base mode) returning `{enc, ct}`. */
|
|
662
662
|
export async function hpkeSeal(input) {
|
|
663
|
-
const suite =
|
|
663
|
+
const suite = vaultHpkeSuite();
|
|
664
664
|
const recipientPublicKey = await suite.kem.deserializePublicKey(toArrayBuffer(input.recipient_public_key));
|
|
665
665
|
const ekm = input.ikm_e ? await suite.kem.deriveKeyPair(toArrayBuffer(input.ikm_e)) : undefined;
|
|
666
666
|
const sender = await suite.createSenderContext({ recipientPublicKey, info: toArrayBuffer(input.info), ...(ekm ? { ekm } : {}) });
|
|
667
667
|
const ct = new Uint8Array(await sender.seal(toArrayBuffer(input.plaintext), toArrayBuffer(input.aad)));
|
|
668
668
|
return { enc: new Uint8Array(sender.enc), ct };
|
|
669
669
|
}
|
|
670
|
-
/** Single-shot HPKE open; any failure is `
|
|
670
|
+
/** Single-shot HPKE open; any failure is `VAULT_HPKE_OPEN_FAILED` (never garbage plaintext). */
|
|
671
671
|
export async function hpkeOpen(input) {
|
|
672
|
-
const suite =
|
|
672
|
+
const suite = vaultHpkeSuite();
|
|
673
673
|
try {
|
|
674
674
|
const recipientKey = await suite.kem.deserializePrivateKey(toArrayBuffer(input.recipient_private_key));
|
|
675
675
|
const recipient = await suite.createRecipientContext({ recipientKey, enc: toArrayBuffer(input.enc), info: toArrayBuffer(input.info) });
|
|
676
676
|
return new Uint8Array(await recipient.open(toArrayBuffer(input.ct), toArrayBuffer(input.aad)));
|
|
677
677
|
}
|
|
678
678
|
catch {
|
|
679
|
-
fail("
|
|
679
|
+
fail("VAULT_HPKE_OPEN_FAILED", "HPKE open failed — the recipient recovers no key", "opening r402s/v0 key envelope");
|
|
680
680
|
}
|
|
681
681
|
}
|
|
682
682
|
/** Seal + sign a `key_envelope` for one principal; returns the object with its stored-bytes identity + genesis receipt. */
|
|
683
683
|
export async function sealKeyEnvelope(input) {
|
|
684
684
|
if (input.k_repo.length !== 32)
|
|
685
|
-
fail("
|
|
686
|
-
if (!
|
|
687
|
-
fail("
|
|
685
|
+
fail("VAULT_BAD_KEY", "K_repo must be 32 bytes", "sealing r402s/v0 key envelope");
|
|
686
|
+
if (!isValidVaultTimestamp(input.created_at))
|
|
687
|
+
fail("VAULT_BAD_TIMESTAMP", "created_at is not an RFC 3339 UTC ms timestamp", "sealing r402s/v0 key envelope");
|
|
688
688
|
const createdBy = vkFingerprint(input.signer.public_key);
|
|
689
689
|
const recipientFingerprint = ekFingerprint(input.recipient_public_key);
|
|
690
690
|
const rotationId = input.rotation_id ?? null;
|
|
@@ -696,9 +696,9 @@ export async function sealKeyEnvelope(input) {
|
|
|
696
696
|
ikm_e: input.ikm_e,
|
|
697
697
|
});
|
|
698
698
|
const unsigned = {
|
|
699
|
-
format:
|
|
699
|
+
format: VAULT_FORMAT,
|
|
700
700
|
object_kind: "key_envelope",
|
|
701
|
-
suite:
|
|
701
|
+
suite: VAULT_SUITE,
|
|
702
702
|
repo_id: input.repo_id,
|
|
703
703
|
epoch: input.epoch,
|
|
704
704
|
recipient_kind: "principal",
|
|
@@ -709,7 +709,7 @@ export async function sealKeyEnvelope(input) {
|
|
|
709
709
|
created_at: input.created_at,
|
|
710
710
|
...(rotationId ? { rotation_id: rotationId } : {}),
|
|
711
711
|
};
|
|
712
|
-
const envelope =
|
|
712
|
+
const envelope = signVaultObject(unsigned, input.signer.seed);
|
|
713
713
|
const bytes = storedBytes(envelope);
|
|
714
714
|
const hash = sha256Hex(bytes);
|
|
715
715
|
return {
|
|
@@ -736,13 +736,13 @@ export async function sealKeyEnvelope(input) {
|
|
|
736
736
|
*/
|
|
737
737
|
export async function openKeyEnvelope(input) {
|
|
738
738
|
const env = input.envelope;
|
|
739
|
-
if (!
|
|
740
|
-
fail("
|
|
739
|
+
if (!VAULT_HPKE_CT_RE.test(env.ct) || !VAULT_B64U_32_RE.test(env.enc) || !isCanonicalBase64url(env.ct) || !isCanonicalBase64url(env.enc)) {
|
|
740
|
+
fail("VAULT_SCHEMA_REJECT", "key_envelope enc/ct are not the exact scalar lengths", "opening r402s/v0 key envelope");
|
|
741
741
|
}
|
|
742
|
-
|
|
742
|
+
assertVaultSignature(env, input.signer_public_key, "opening r402s/v0 key envelope");
|
|
743
743
|
const ours = ekFingerprint(input.recipient.public_key);
|
|
744
744
|
if (env.recipient_fingerprint !== ours || env.recipient_kind !== "principal") {
|
|
745
|
-
fail("
|
|
745
|
+
fail("VAULT_ENVELOPE_NOT_FOR_RECIPIENT", "key_envelope is addressed to another recipient", "opening r402s/v0 key envelope", { recipient_fingerprint: env.recipient_fingerprint });
|
|
746
746
|
}
|
|
747
747
|
const kRepo = await hpkeOpen({
|
|
748
748
|
recipient_private_key: input.recipient.private_key,
|
|
@@ -752,7 +752,7 @@ export async function openKeyEnvelope(input) {
|
|
|
752
752
|
ct: fromBase64url(env.ct, "ct"),
|
|
753
753
|
});
|
|
754
754
|
if (kRepo.length !== 32)
|
|
755
|
-
fail("
|
|
755
|
+
fail("VAULT_HPKE_OPEN_FAILED", "opened plaintext is not a 32-byte K_repo", "opening r402s/v0 key envelope");
|
|
756
756
|
return kRepo;
|
|
757
757
|
}
|
|
758
758
|
// ─── Epoch rotation — reader side (D193-D203, rev 42) ───────────────────────
|
|
@@ -769,7 +769,7 @@ export async function openKeyEnvelope(input) {
|
|
|
769
769
|
export function parseRotateEpochPayload(head) {
|
|
770
770
|
const t = head.transition;
|
|
771
771
|
if (!t || t.kind !== "rotate_epoch")
|
|
772
|
-
fail("
|
|
772
|
+
fail("VAULT_NOT_A_ROTATION", `head ${head.generation} does not carry a rotate_epoch transition`, "parsing rotate_epoch_payload");
|
|
773
773
|
if (t.payload_format !== "base64url-jcs")
|
|
774
774
|
fail("CHAIN_BROKEN", `head ${head.generation}: unsupported transition payload_format "${t.payload_format}"`, "parsing rotate_epoch_payload");
|
|
775
775
|
let raw;
|
|
@@ -783,7 +783,7 @@ export function parseRotateEpochPayload(head) {
|
|
|
783
783
|
fail("CHAIN_BROKEN", `head ${head.generation}: transition.payload_sha256 does not match the payload bytes`, "parsing rotate_epoch_payload");
|
|
784
784
|
let payload;
|
|
785
785
|
try {
|
|
786
|
-
payload =
|
|
786
|
+
payload = parseVaultStrict(new TextDecoder().decode(raw));
|
|
787
787
|
}
|
|
788
788
|
catch {
|
|
789
789
|
fail("CHAIN_BROKEN", `head ${head.generation}: transition.payload is not strict-parseable r402s/v0 JSON`, "parsing rotate_epoch_payload");
|
|
@@ -791,7 +791,7 @@ export function parseRotateEpochPayload(head) {
|
|
|
791
791
|
if (payload.new_epoch !== head.epoch) {
|
|
792
792
|
fail("CHAIN_BROKEN", `head ${head.generation}: rotate_epoch_payload.new_epoch (${payload.new_epoch}) does not equal the carrying head's own epoch (${head.epoch})`, "parsing rotate_epoch_payload");
|
|
793
793
|
}
|
|
794
|
-
if (!
|
|
794
|
+
if (!VAULT_SHA256_RE.test(payload.rotation_id))
|
|
795
795
|
fail("CHAIN_BROKEN", `head ${head.generation}: rotate_epoch_payload.rotation_id is not a 64-hex digest`, "parsing rotate_epoch_payload");
|
|
796
796
|
if (!Array.isArray(payload.envelopes))
|
|
797
797
|
fail("CHAIN_BROKEN", `head ${head.generation}: rotate_epoch_payload.envelopes is not an array`, "parsing rotate_epoch_payload");
|
|
@@ -799,21 +799,21 @@ export function parseRotateEpochPayload(head) {
|
|
|
799
799
|
}
|
|
800
800
|
/**
|
|
801
801
|
* Decode + validate one head's `transition.payload` as an
|
|
802
|
-
* `add_writer_key_payload` (
|
|
802
|
+
* `add_writer_key_payload` (vault-multi-writer rev 47, protocol §4.16):
|
|
803
803
|
* base64url-jcs decode, `payload_sha256` self-check, strict JSON parse, and
|
|
804
804
|
* the payload's OWN `repo_id` agreeing with the carrying head's — mirrors
|
|
805
805
|
* {@link parseRotateEpochPayload} exactly, `repo_id` in place of `epoch`
|
|
806
806
|
* (this transition never changes the epoch). Pure and keyless: the FULL
|
|
807
807
|
* cryptographic + structural validation (signatures, base/next writer-set
|
|
808
808
|
* commitments, handoff-grant binding) is `validateAddWriterKeyPayload`'s job
|
|
809
|
-
* (`../node/
|
|
809
|
+
* (`../node/vault-writer-state.js`) — this function only gets the bytes
|
|
810
810
|
* off the wire safely, so every reader can call it before any key material
|
|
811
811
|
* is needed.
|
|
812
812
|
*/
|
|
813
813
|
export function parseAddWriterKeyPayload(head) {
|
|
814
814
|
const t = head.transition;
|
|
815
815
|
if (!t || t.kind !== "add_writer_key")
|
|
816
|
-
fail("
|
|
816
|
+
fail("VAULT_NOT_A_WRITER_ADD", `head ${head.generation} does not carry an add_writer_key transition`, "parsing add_writer_key_payload");
|
|
817
817
|
if (t.payload_format !== "base64url-jcs")
|
|
818
818
|
fail("CHAIN_BROKEN", `head ${head.generation}: unsupported transition payload_format "${t.payload_format}"`, "parsing add_writer_key_payload");
|
|
819
819
|
let raw;
|
|
@@ -827,7 +827,7 @@ export function parseAddWriterKeyPayload(head) {
|
|
|
827
827
|
fail("CHAIN_BROKEN", `head ${head.generation}: transition.payload_sha256 does not match the payload bytes`, "parsing add_writer_key_payload");
|
|
828
828
|
let payload;
|
|
829
829
|
try {
|
|
830
|
-
payload =
|
|
830
|
+
payload = parseVaultStrict(new TextDecoder().decode(raw));
|
|
831
831
|
}
|
|
832
832
|
catch {
|
|
833
833
|
fail("CHAIN_BROKEN", `head ${head.generation}: transition.payload is not strict-parseable r402s/v0 JSON`, "parsing add_writer_key_payload");
|
|
@@ -844,29 +844,29 @@ export function parseAddWriterKeyPayload(head) {
|
|
|
844
844
|
* `own_fingerprint` and return the derived `K_e`, after verifying it
|
|
845
845
|
* reproduces the payload's `epoch_key_commitment` (D200's per-recipient
|
|
846
846
|
* self-check — never a global set-coherence proof, §14ao/§2). Fails CLOSED
|
|
847
|
-
* with `
|
|
847
|
+
* with `VAULT_EPOCH_NOT_OPENABLE` — never a bare `VAULT_AEAD_AUTH_FAILURE`
|
|
848
848
|
* — either when this recipient is not among the rotation's `envelopes[]` at
|
|
849
849
|
* all, or when the addressed envelope's stored bytes are absent or altered.
|
|
850
850
|
*
|
|
851
851
|
* Transport-agnostic by design (`get_envelope_bytes` + `envelope_path`
|
|
852
852
|
* callbacks): this is the ONE place a reader opens a rotation envelope, and
|
|
853
|
-
* both the live publication read path (`
|
|
853
|
+
* both the live publication read path (`Vault.verifyToNewest`, over
|
|
854
854
|
* the gateway transport) and the offline mirror recovery path
|
|
855
|
-
* (`
|
|
855
|
+
* (`recoverVaultMirror`, over a `VaultMirrorBackend`) call it —
|
|
856
856
|
* differing only in HOW they fetch bytes, never in the open/verify logic.
|
|
857
857
|
*/
|
|
858
858
|
export async function openEpochRotationForRecipient(input) {
|
|
859
859
|
const { repo_id, payload, own_fingerprint, own_encryption_keypair, writer_signing_public_key, get_envelope_bytes, envelope_path } = input;
|
|
860
860
|
const pair = payload.envelopes.find((p) => p.envelope.recipient_fingerprint === own_fingerprint);
|
|
861
861
|
if (!pair) {
|
|
862
|
-
fail("
|
|
862
|
+
fail("VAULT_EPOCH_NOT_OPENABLE", `this keystore (fingerprint ${own_fingerprint}) is not among epoch ${payload.new_epoch}'s included recipients (rotation ${payload.rotation_id}) — the vault rotated past an epoch this identity was never given a key for`, "opening an epoch-rotation key envelope", { epoch: payload.new_epoch, rotation_id: payload.rotation_id, recipient_fingerprint: own_fingerprint, reason: "not_included" });
|
|
863
863
|
}
|
|
864
864
|
const path = envelope_path(payload.new_epoch, own_fingerprint, payload.rotation_id);
|
|
865
865
|
const bytes = await get_envelope_bytes(path);
|
|
866
866
|
if (!bytes || sha256Hex(bytes) !== pair.envelope.stored_bytes_sha256) {
|
|
867
|
-
fail("
|
|
867
|
+
fail("VAULT_EPOCH_NOT_OPENABLE", `the rotation-attempt key_envelope for epoch ${payload.new_epoch} (rotation ${payload.rotation_id}) is absent or altered — this keystore (fingerprint ${own_fingerprint}) cannot open this epoch`, "opening an epoch-rotation key envelope", { epoch: payload.new_epoch, rotation_id: payload.rotation_id, recipient_fingerprint: own_fingerprint, reason: "envelope_absent_or_altered" });
|
|
868
868
|
}
|
|
869
|
-
const envelope =
|
|
869
|
+
const envelope = parseVaultStrict(new TextDecoder().decode(bytes));
|
|
870
870
|
const kE = await openKeyEnvelope({ envelope, recipient: own_encryption_keypair, signer_public_key: writer_signing_public_key });
|
|
871
871
|
const includedFingerprints = payload.envelopes.map((p) => p.envelope.recipient_fingerprint);
|
|
872
872
|
const recomputed = epochRotationKeyCommitment(kE, repo_id, payload.new_epoch, payload.rotation_id, includedFingerprints);
|
|
@@ -878,15 +878,15 @@ export async function openEpochRotationForRecipient(input) {
|
|
|
878
878
|
/** Build + sign a `vault_genesis` (generation 0, epoch 1, the creator's single envelope). */
|
|
879
879
|
export function buildVaultGenesis(input) {
|
|
880
880
|
const unsigned = {
|
|
881
|
-
format:
|
|
881
|
+
format: VAULT_FORMAT,
|
|
882
882
|
object_kind: "vault_genesis",
|
|
883
|
-
suite:
|
|
883
|
+
suite: VAULT_SUITE,
|
|
884
884
|
repo_id: input.repo_id,
|
|
885
885
|
org_id: input.org_id,
|
|
886
886
|
project_id: input.project_id,
|
|
887
887
|
allocation_nonce: input.allocation_nonce,
|
|
888
|
-
generation:
|
|
889
|
-
epoch:
|
|
888
|
+
generation: VAULT_GENESIS_GENERATION,
|
|
889
|
+
epoch: VAULT_GENESIS_EPOCH,
|
|
890
890
|
git_object_format: "sha1",
|
|
891
891
|
creator_signing_pubkey: toBase64url(input.creator_signing.public_key),
|
|
892
892
|
creator_encryption_pubkey: toBase64url(input.creator_encryption_public_key),
|
|
@@ -894,7 +894,7 @@ export function buildVaultGenesis(input) {
|
|
|
894
894
|
writer_key_id: vkFingerprint(input.creator_signing.public_key),
|
|
895
895
|
created_at: input.created_at,
|
|
896
896
|
};
|
|
897
|
-
return
|
|
897
|
+
return signVaultObject(unsigned, input.creator_signing.seed);
|
|
898
898
|
}
|
|
899
899
|
/**
|
|
900
900
|
* The genesis key bindings (round-10 A6): exactly one envelope at epoch 1 whose
|
|
@@ -908,7 +908,7 @@ export function checkGenesisKeyBindings(genesis, storedEnvelope) {
|
|
|
908
908
|
if (genesis.envelopes.length !== 1)
|
|
909
909
|
problems.push("envelope_count");
|
|
910
910
|
const receipt = genesis.envelopes[0];
|
|
911
|
-
if (receipt && receipt.epoch !==
|
|
911
|
+
if (receipt && receipt.epoch !== VAULT_GENESIS_EPOCH)
|
|
912
912
|
problems.push("envelope_epoch");
|
|
913
913
|
const expectedEk = ekFingerprint(fromBase64url(genesis.creator_encryption_pubkey, "creator_encryption_pubkey"));
|
|
914
914
|
if (receipt && receipt.recipient_fingerprint !== expectedEk)
|
|
@@ -936,9 +936,9 @@ export function checkGenesisKeyBindings(genesis, storedEnvelope) {
|
|
|
936
936
|
/** Build + sign the creator's `recovery_receipt` (emitted at ACTIVE; integrity data, not a secret). */
|
|
937
937
|
export function buildRecoveryReceipt(input) {
|
|
938
938
|
const unsigned = {
|
|
939
|
-
format:
|
|
939
|
+
format: VAULT_FORMAT,
|
|
940
940
|
object_kind: "recovery_receipt",
|
|
941
|
-
suite:
|
|
941
|
+
suite: VAULT_SUITE,
|
|
942
942
|
repo_id: input.repo_id,
|
|
943
943
|
org_id: input.org_id,
|
|
944
944
|
project_id: input.project_id,
|
|
@@ -946,7 +946,7 @@ export function buildRecoveryReceipt(input) {
|
|
|
946
946
|
creator_signing_fingerprint: vkFingerprint(input.creator_signing.public_key),
|
|
947
947
|
creator_encryption_fingerprint: ekFingerprint(input.creator_encryption_public_key),
|
|
948
948
|
};
|
|
949
|
-
return
|
|
949
|
+
return signVaultObject(unsigned, input.creator_signing.seed);
|
|
950
950
|
}
|
|
951
951
|
/**
|
|
952
952
|
* Check a recovery receipt against a genesis it claims to pin: the receipt's
|
|
@@ -956,7 +956,7 @@ export function buildRecoveryReceipt(input) {
|
|
|
956
956
|
*/
|
|
957
957
|
export function checkRecoveryReceipt(receipt, genesis) {
|
|
958
958
|
const problems = [];
|
|
959
|
-
if (!
|
|
959
|
+
if (!verifyVaultObject(receipt, genesis.creator_signing_pubkey))
|
|
960
960
|
problems.push("signature");
|
|
961
961
|
if (receipt.genesis_sha256 !== storedBytesSha256(genesis))
|
|
962
962
|
problems.push("genesis_sha256");
|
|
@@ -989,15 +989,15 @@ export function checkRecoveryReceipt(receipt, genesis) {
|
|
|
989
989
|
*/
|
|
990
990
|
export function checkAllocation(allocation, expected, servicePublicKey) {
|
|
991
991
|
const problems = [];
|
|
992
|
-
if (allocation.format !==
|
|
992
|
+
if (allocation.format !== VAULT_FORMAT || allocation.object_kind !== "allocation" || allocation.suite !== VAULT_SUITE)
|
|
993
993
|
problems.push("envelope");
|
|
994
|
-
if (!
|
|
994
|
+
if (!VAULT_SRC_RE.test(allocation.repo_id))
|
|
995
995
|
problems.push("repo_id");
|
|
996
|
-
if (!
|
|
996
|
+
if (!VAULT_HEX32_RE.test(allocation.allocation_nonce))
|
|
997
997
|
problems.push("allocation_nonce");
|
|
998
|
-
if (!
|
|
998
|
+
if (!VAULT_HEX16_RE.test(allocation.allocation_generation))
|
|
999
999
|
problems.push("allocation_generation");
|
|
1000
|
-
if (!
|
|
1000
|
+
if (!VAULT_SERVICE_KEY_ID_RE.test(allocation.service_key_id))
|
|
1001
1001
|
problems.push("service_key_id");
|
|
1002
1002
|
if (allocation.client_creation_id !== expected.client_creation_id)
|
|
1003
1003
|
problems.push("client_creation_id");
|
|
@@ -1009,19 +1009,19 @@ export function checkAllocation(allocation, expected, servicePublicKey) {
|
|
|
1009
1009
|
problems.push("org_id");
|
|
1010
1010
|
if (expected.project_id !== undefined && allocation.project_id !== expected.project_id)
|
|
1011
1011
|
problems.push("project_id");
|
|
1012
|
-
if (!
|
|
1012
|
+
if (!isValidVaultTimestamp(allocation.issued_at) || !isValidVaultTimestamp(allocation.created_at))
|
|
1013
1013
|
problems.push("timestamp");
|
|
1014
|
-
if (servicePublicKey !== undefined && !
|
|
1014
|
+
if (servicePublicKey !== undefined && !verifyVaultObject(allocation, servicePublicKey))
|
|
1015
1015
|
problems.push("signature");
|
|
1016
1016
|
return problems;
|
|
1017
1017
|
}
|
|
1018
|
-
// ─── Epoch rotation (D193-D203, rev 42, change
|
|
1018
|
+
// ─── Epoch rotation (D193-D203, rev 42, change vault-human-envelopes) ───
|
|
1019
1019
|
//
|
|
1020
|
-
// Pure, isomorphic math shared by the producer (Node-only,
|
|
1020
|
+
// Pure, isomorphic math shared by the producer (Node-only, vault-
|
|
1021
1021
|
// publication.ts's `rotateEpoch`, which has the live desired-recipient
|
|
1022
1022
|
// state + local keystore this module cannot see) and by vector generation.
|
|
1023
1023
|
// Every formula here MIRRORS the gateway's own pure functions in
|
|
1024
|
-
// `packages/gateway/src/services/
|
|
1024
|
+
// `packages/gateway/src/services/vault/epoch-rotation.ts` byte-for-byte
|
|
1025
1025
|
// (same field order is irrelevant — JCS sorts — but the SAME field SET and
|
|
1026
1026
|
// the SAME preimage construction is load-bearing: a drift here would build
|
|
1027
1027
|
// a descriptor/payload the gateway's own re-derivation refuses).
|
|
@@ -1066,7 +1066,7 @@ export function computeTargetPartitionDigest(input) {
|
|
|
1066
1066
|
* stripped either way, matching the gateway's `computeRotationId`).
|
|
1067
1067
|
*/
|
|
1068
1068
|
export function computeRotationId(descriptor) {
|
|
1069
|
-
return sha256Hex(jcs(
|
|
1069
|
+
return sha256Hex(jcs(vaultWithoutSignature(descriptor)));
|
|
1070
1070
|
}
|
|
1071
1071
|
/**
|
|
1072
1072
|
* D195's `attempt_key_commitment` preimage: the descriptor's OWN fields
|
|
@@ -1121,8 +1121,8 @@ export function epochRotationKeyCommitment(kE, repoId, newEpoch, rotationId, fin
|
|
|
1121
1121
|
* genesis support.
|
|
1122
1122
|
*/
|
|
1123
1123
|
export function genesisEpochKeyCommitment(k1, repoId, fingerprints) {
|
|
1124
|
-
const kDigest = deriveDigestKeyFrom(k1, repoId,
|
|
1125
|
-
return keyedCommitment(kDigest, { repo_id: repoId, generation:
|
|
1124
|
+
const kDigest = deriveDigestKeyFrom(k1, repoId, VAULT_GENESIS_EPOCH, "vault_genesis_epoch_key");
|
|
1125
|
+
return keyedCommitment(kDigest, { repo_id: repoId, generation: VAULT_GENESIS_GENERATION, epoch: VAULT_GENESIS_EPOCH, fingerprints: sortedDistinct(fingerprints) });
|
|
1126
1126
|
}
|
|
1127
1127
|
/**
|
|
1128
1128
|
* D195's producer obligation the round-3 review's own decisive witness
|
|
@@ -1139,11 +1139,11 @@ export function genesisEpochKeyCommitment(k1, repoId, fingerprints) {
|
|
|
1139
1139
|
*/
|
|
1140
1140
|
export function checkFreshEpochKeyAgainstPriorKeys(kE, priorKeys) {
|
|
1141
1141
|
if (kE.length !== 32)
|
|
1142
|
-
fail("
|
|
1142
|
+
fail("VAULT_BAD_KEY", "a freshly sampled K_e must be 32 bytes", "sampling a fresh epoch key");
|
|
1143
1143
|
const fresh = bytesToHex(kE);
|
|
1144
1144
|
for (const prior of priorKeys) {
|
|
1145
1145
|
if (bytesToHex(prior) === fresh) {
|
|
1146
|
-
fail("
|
|
1146
|
+
fail("VAULT_EPOCH_KEY_NOT_FRESH", "the freshly sampled K_e equals a prior epoch key already held locally — refusing to seal under it", "sampling a fresh epoch key");
|
|
1147
1147
|
}
|
|
1148
1148
|
}
|
|
1149
1149
|
}
|
|
@@ -1243,7 +1243,7 @@ export function pinManifestLedgerId(pinManifestVersion) {
|
|
|
1243
1243
|
}
|
|
1244
1244
|
// ─── Source-access wrapper custody — WrapperV1 / `swrap2` (DECRYPT SIDE) ─────
|
|
1245
1245
|
//
|
|
1246
|
-
//
|
|
1246
|
+
// vault-recovery-custody: a human member's X25519 private scalar is RANDOM
|
|
1247
1247
|
// and persisted only as sealed wrappers — each an XChaCha20-Poly1305 AEAD
|
|
1248
1248
|
// encryption of the 32-byte scalar under a KEK derived from a credential
|
|
1249
1249
|
// secret, bound to a canonical JCS context. This SDK implements only the
|
|
@@ -1253,7 +1253,7 @@ export function pinManifestLedgerId(pinManifestVersion) {
|
|
|
1253
1253
|
//
|
|
1254
1254
|
// Every constant below is pinned byte-for-byte against the canonical module
|
|
1255
1255
|
// (private repo `apps/git/public/lib/r402s-crypto.js`, "Source-access wrapper
|
|
1256
|
-
// custody" section, and
|
|
1256
|
+
// custody" section, and vault-recovery-custody/design.md D1 addendum). If
|
|
1257
1257
|
// they ever disagree, both are wrong and need reconciling together. The prior
|
|
1258
1258
|
// `swrap1_` format never protected a real member key and is NOT decodable —
|
|
1259
1259
|
// it fails by name.
|
|
@@ -1295,7 +1295,7 @@ export function buildSourceWrapperContext(fields) {
|
|
|
1295
1295
|
member_public_key_sha256: bytesToHex(sha256(fields.member_public_key)),
|
|
1296
1296
|
principal_id: String(fields.principal_id),
|
|
1297
1297
|
rp_id: String(fields.rp_id),
|
|
1298
|
-
suite:
|
|
1298
|
+
suite: VAULT_SUITE,
|
|
1299
1299
|
wrapper_id: String(fields.wrapper_id),
|
|
1300
1300
|
})));
|
|
1301
1301
|
}
|
|
@@ -1377,7 +1377,7 @@ export function normalizeSourceRecoveryCode(input) {
|
|
|
1377
1377
|
return core;
|
|
1378
1378
|
}
|
|
1379
1379
|
export { bytesToHex, hexToBytes };
|
|
1380
|
-
// ──
|
|
1380
|
+
// ── vault-agent-envelopes: keystore-key possession proof ──────────────────
|
|
1381
1381
|
/**
|
|
1382
1382
|
* The client half of the gateway's ECDH key-confirmation challenge for a
|
|
1383
1383
|
* `keystore_v1` enrollment (`POST /agent/v1/whoami/encryption-key` →
|
|
@@ -1390,16 +1390,16 @@ export { bytesToHex, hexToBytes };
|
|
|
1390
1390
|
* the prefix + JCS message are pinned here so the two lineages cannot drift.
|
|
1391
1391
|
* Client convention, not r402s/v0 wire protocol.
|
|
1392
1392
|
*/
|
|
1393
|
-
export const
|
|
1393
|
+
export const VAULT_KEYSTORE_POSSESSION_PREFIX = "r402s/v0/keystore-key-possession/v1\n";
|
|
1394
1394
|
export function computeKeystorePossessionProof(input) {
|
|
1395
1395
|
const shared = x25519.getSharedSecret(input.private_key, fromBase64url(input.epk_b64u, "activation.epk"));
|
|
1396
1396
|
// Sorted-key JSON.stringify IS RFC 8785 here: three plain string fields.
|
|
1397
1397
|
const jcs = JSON.stringify({ challenge_id: input.challenge_id, encryption_key_id: input.encryption_key_id, public_key: input.public_key_b64u });
|
|
1398
|
-
const message = concatBytes(utf8ToBytes(
|
|
1398
|
+
const message = concatBytes(utf8ToBytes(VAULT_KEYSTORE_POSSESSION_PREFIX), utf8ToBytes(jcs));
|
|
1399
1399
|
return toBase64url(hmac(sha256, shared, message));
|
|
1400
1400
|
}
|
|
1401
1401
|
/**
|
|
1402
|
-
*
|
|
1402
|
+
* vault-multi-writer rev 47 (design D9) — the writer-signing-key
|
|
1403
1403
|
* possession message. Byte-identical to the gateway's own
|
|
1404
1404
|
* `signingKeyPossessionMessage` (services/principal-encryption-keys.ts) —
|
|
1405
1405
|
* `JSON.stringify({domain, principal_id, signing_pubkey, encryption_pubkey})`
|
|
@@ -1414,13 +1414,13 @@ export function computeKeystorePossessionProof(input) {
|
|
|
1414
1414
|
* principal_id, signing_pubkey`) and silently produce a signature that
|
|
1415
1415
|
* never verifies.
|
|
1416
1416
|
*/
|
|
1417
|
-
export const
|
|
1417
|
+
export const VAULT_SIGNING_KEY_POSSESSION_DOMAIN = "r402s/v0/signing-key-possession/v1";
|
|
1418
1418
|
export function signingKeyPossessionMessage(fields) {
|
|
1419
|
-
const json = JSON.stringify({ domain:
|
|
1419
|
+
const json = JSON.stringify({ domain: VAULT_SIGNING_KEY_POSSESSION_DOMAIN, principal_id: fields.principal_id, signing_pubkey: fields.signing_pubkey, encryption_pubkey: fields.encryption_pubkey });
|
|
1420
1420
|
return utf8ToBytes(json);
|
|
1421
1421
|
}
|
|
1422
1422
|
/** Ed25519 signature over {@link signingKeyPossessionMessage} — proves possession of the signing half's private key over this specific (principal, signing key, encryption key) binding. */
|
|
1423
1423
|
export function computeSigningKeyPossessionSignature(input) {
|
|
1424
1424
|
return toBase64url(ed25519Sign(signingKeyPossessionMessage(input), input.signing_seed));
|
|
1425
1425
|
}
|
|
1426
|
-
//# sourceMappingURL=
|
|
1426
|
+
//# sourceMappingURL=vault.crypto.js.map
|