run402 4.24.1 → 4.26.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 (115) hide show
  1. package/cli.mjs +6 -6
  2. package/git-remote-run402.mjs +328 -0
  3. package/lib/command-manifest.mjs +13 -1
  4. package/lib/credentials.mjs +4 -0
  5. package/lib/deploy-v2.mjs +78 -1
  6. package/lib/doctor.mjs +80 -1
  7. package/lib/domains.mjs +0 -14
  8. package/lib/gitvault.mjs +582 -0
  9. package/lib/init.mjs +153 -5
  10. package/lib/org-context.mjs +44 -1
  11. package/lib/pay.test.mjs +0 -4
  12. package/package.json +8 -2
  13. package/sdk/dist/index.d.ts +20 -3
  14. package/sdk/dist/index.d.ts.map +1 -1
  15. package/sdk/dist/index.js +22 -3
  16. package/sdk/dist/index.js.map +1 -1
  17. package/sdk/dist/namespaces/admin.d.ts +0 -5
  18. package/sdk/dist/namespaces/admin.d.ts.map +1 -1
  19. package/sdk/dist/namespaces/admin.js +0 -9
  20. package/sdk/dist/namespaces/admin.js.map +1 -1
  21. package/sdk/dist/namespaces/assets.d.ts +1 -20
  22. package/sdk/dist/namespaces/assets.d.ts.map +1 -1
  23. package/sdk/dist/namespaces/assets.js +0 -27
  24. package/sdk/dist/namespaces/assets.js.map +1 -1
  25. package/sdk/dist/namespaces/domains.d.ts +0 -19
  26. package/sdk/dist/namespaces/domains.d.ts.map +1 -1
  27. package/sdk/dist/namespaces/domains.js +0 -19
  28. package/sdk/dist/namespaces/domains.js.map +1 -1
  29. package/sdk/dist/namespaces/gitvault.crypto.d.ts +319 -0
  30. package/sdk/dist/namespaces/gitvault.crypto.d.ts.map +1 -0
  31. package/sdk/dist/namespaces/gitvault.crypto.js +690 -0
  32. package/sdk/dist/namespaces/gitvault.crypto.js.map +1 -0
  33. package/sdk/dist/namespaces/gitvault.d.ts +521 -0
  34. package/sdk/dist/namespaces/gitvault.d.ts.map +1 -0
  35. package/sdk/dist/namespaces/gitvault.js +786 -0
  36. package/sdk/dist/namespaces/gitvault.js.map +1 -0
  37. package/sdk/dist/namespaces/gitvault.types.d.ts +481 -0
  38. package/sdk/dist/namespaces/gitvault.types.d.ts.map +1 -0
  39. package/sdk/dist/namespaces/gitvault.types.js +19 -0
  40. package/sdk/dist/namespaces/gitvault.types.js.map +1 -0
  41. package/sdk/dist/namespaces/org.d.ts +1 -3
  42. package/sdk/dist/namespaces/org.d.ts.map +1 -1
  43. package/sdk/dist/namespaces/org.js +6 -10
  44. package/sdk/dist/namespaces/org.js.map +1 -1
  45. package/sdk/dist/namespaces/pay.d.ts +0 -8
  46. package/sdk/dist/namespaces/pay.d.ts.map +1 -1
  47. package/sdk/dist/namespaces/pay.js.map +1 -1
  48. package/sdk/dist/namespaces/projects.d.ts +2 -2
  49. package/sdk/dist/namespaces/projects.d.ts.map +1 -1
  50. package/sdk/dist/namespaces/projects.js +4 -12
  51. package/sdk/dist/namespaces/projects.js.map +1 -1
  52. package/sdk/dist/namespaces/secrets.d.ts +0 -2
  53. package/sdk/dist/namespaces/secrets.d.ts.map +1 -1
  54. package/sdk/dist/namespaces/secrets.js +3 -10
  55. package/sdk/dist/namespaces/secrets.js.map +1 -1
  56. package/sdk/dist/namespaces/subdomains.d.ts +0 -2
  57. package/sdk/dist/namespaces/subdomains.d.ts.map +1 -1
  58. package/sdk/dist/namespaces/subdomains.js +5 -16
  59. package/sdk/dist/namespaces/subdomains.js.map +1 -1
  60. package/sdk/dist/namespaces/transfers.d.ts +0 -2
  61. package/sdk/dist/namespaces/transfers.d.ts.map +1 -1
  62. package/sdk/dist/namespaces/transfers.js +8 -10
  63. package/sdk/dist/namespaces/transfers.js.map +1 -1
  64. package/sdk/dist/namespaces/wallets.d.ts +0 -12
  65. package/sdk/dist/namespaces/wallets.d.ts.map +1 -1
  66. package/sdk/dist/namespaces/wallets.js +0 -14
  67. package/sdk/dist/namespaces/wallets.js.map +1 -1
  68. package/sdk/dist/node/gitvault-creation-journal.d.ts +222 -0
  69. package/sdk/dist/node/gitvault-creation-journal.d.ts.map +1 -0
  70. package/sdk/dist/node/gitvault-creation-journal.js +365 -0
  71. package/sdk/dist/node/gitvault-creation-journal.js.map +1 -0
  72. package/sdk/dist/node/gitvault-deploy.d.ts +211 -0
  73. package/sdk/dist/node/gitvault-deploy.d.ts.map +1 -0
  74. package/sdk/dist/node/gitvault-deploy.js +292 -0
  75. package/sdk/dist/node/gitvault-deploy.js.map +1 -0
  76. package/sdk/dist/node/gitvault-keystore.d.ts +207 -0
  77. package/sdk/dist/node/gitvault-keystore.d.ts.map +1 -0
  78. package/sdk/dist/node/gitvault-keystore.js +529 -0
  79. package/sdk/dist/node/gitvault-keystore.js.map +1 -0
  80. package/sdk/dist/node/gitvault-prune.d.ts +341 -0
  81. package/sdk/dist/node/gitvault-prune.d.ts.map +1 -0
  82. package/sdk/dist/node/gitvault-prune.js +410 -0
  83. package/sdk/dist/node/gitvault-prune.js.map +1 -0
  84. package/sdk/dist/node/gitvault-publication.d.ts +730 -0
  85. package/sdk/dist/node/gitvault-publication.d.ts.map +1 -0
  86. package/sdk/dist/node/gitvault-publication.js +1475 -0
  87. package/sdk/dist/node/gitvault-publication.js.map +1 -0
  88. package/sdk/dist/node/gitvault-snapshot.d.ts +227 -0
  89. package/sdk/dist/node/gitvault-snapshot.d.ts.map +1 -0
  90. package/sdk/dist/node/gitvault-snapshot.js +556 -0
  91. package/sdk/dist/node/gitvault-snapshot.js.map +1 -0
  92. package/sdk/dist/node/index.d.ts +11 -0
  93. package/sdk/dist/node/index.d.ts.map +1 -1
  94. package/sdk/dist/node/index.js +9 -0
  95. package/sdk/dist/node/index.js.map +1 -1
  96. package/sdk/dist/node/paid-fetch.d.ts.map +1 -1
  97. package/sdk/dist/node/paid-fetch.js +0 -4
  98. package/sdk/dist/node/paid-fetch.js.map +1 -1
  99. package/sdk/dist/node/sites-node.d.ts +1 -8
  100. package/sdk/dist/node/sites-node.d.ts.map +1 -1
  101. package/sdk/dist/node/sites-node.js +1 -7
  102. package/sdk/dist/node/sites-node.js.map +1 -1
  103. package/sdk/dist/scoped.d.ts +4 -35
  104. package/sdk/dist/scoped.d.ts.map +1 -1
  105. package/sdk/dist/scoped.js +9 -74
  106. package/sdk/dist/scoped.js.map +1 -1
  107. package/lib/sender-domain.mjs +0 -36
  108. package/sdk/dist/deprecate.d.ts +0 -23
  109. package/sdk/dist/deprecate.d.ts.map +0 -1
  110. package/sdk/dist/deprecate.js +0 -42
  111. package/sdk/dist/deprecate.js.map +0 -1
  112. package/sdk/dist/namespaces/sender-domain.d.ts +0 -47
  113. package/sdk/dist/namespaces/sender-domain.d.ts.map +0 -1
  114. package/sdk/dist/namespaces/sender-domain.js +0 -45
  115. package/sdk/dist/namespaces/sender-domain.js.map +0 -1
@@ -0,0 +1,690 @@
1
+ /**
2
+ * gitvault — `r402s/v0` crypto core (protocol rev 40, §1 + §2; task 5.1).
3
+ *
4
+ * Pure, isomorphic (WebCrypto + noble + `@hpke/core`), no I/O. Everything
5
+ * byte-level the protocol pins lives here ONCE and is replayed against the
6
+ * frozen vector set (`gitvault.crypto.test.ts`):
7
+ *
8
+ * - JCS (RFC 8785) for the no-JSON-numbers profile + the strict-parse profile
9
+ * - the two hash rules: stored bytes (JCS INCLUDING `signature`) and the
10
+ * signature preimage `"r402s/v0/" + object_kind + "\n" + JCS(object minus signature)`
11
+ * - Ed25519 strict RFC 8032 — noble with `zip215: false` pinned; a
12
+ * ZIP215-only signature is REJECTED (vector `zip215-002`)
13
+ * - `lp` / `lp_opt` length prefixes + the open-id / open-binding preimages
14
+ * - HKDF-SHA-256 `k_obj` / `K_digest` derivations and HMAC keyed commitments
15
+ * - the XChaCha20-Poly1305 frame (`"R402S0"` ‖ suite byte ‖ 24-byte nonce ‖ ct‖tag)
16
+ * with its seven-member JCS AAD
17
+ * - the `key_envelope` HPKE seal/open (Base mode, DHKEM(X25519,HKDF-SHA256) /
18
+ * HKDF-SHA256 / ChaCha20-Poly1305) with the EXACT D188 `info` / AAD bytes
19
+ * - genesis key-binding + recovery-receipt checks
20
+ *
21
+ * Design rules carried from the protocol: HPKE is a NAMED implementation
22
+ * (`@hpke/core` + `@hpke/chacha20poly1305`), never assembled from primitives
23
+ * (D38); an envelope's signature is verified BEFORE any open; plain hashes of
24
+ * plaintext content never leave the client (every server-comparable digest is
25
+ * keyed); object ids are single-use key labels — callers MUST NOT re-encrypt
26
+ * under an id (retries read-and-compare).
27
+ */
28
+ import { parse, evaluate } from "@humanwhocodes/momoa";
29
+ import { ed25519, x25519 } from "@noble/curves/ed25519.js";
30
+ import { xchacha20poly1305 } from "@noble/ciphers/chacha.js";
31
+ import { sha256 } from "@noble/hashes/sha2.js";
32
+ import { hkdf } from "@noble/hashes/hkdf.js";
33
+ import { hmac } from "@noble/hashes/hmac.js";
34
+ import { bytesToHex, hexToBytes, utf8ToBytes, concatBytes } from "@noble/hashes/utils.js";
35
+ import { base64urlnopad } from "@scure/base";
36
+ import { canonicalize } from "json-canonicalize";
37
+ import { CipherSuite, DhkemX25519HkdfSha256, HkdfSha256 } from "@hpke/core";
38
+ import { Chacha20Poly1305 } from "@hpke/chacha20poly1305";
39
+ import { LocalError } from "../errors.js";
40
+ // ─── Constants ───────────────────────────────────────────────────────────────
41
+ /** Wire format tag (retained by decision D181 across the product rename). */
42
+ export const GITVAULT_FORMAT = "r402s/v0";
43
+ /** The one V0 cryptographic suite. */
44
+ export const GITVAULT_SUITE = "r402s-1";
45
+ /** Frame magic, bytes 0–5 of every encrypted object. */
46
+ export const GITVAULT_FRAME_MAGIC = "R402S0";
47
+ /** Frame suite byte (byte 6) and its two-char AAD spelling. */
48
+ export const GITVAULT_FRAME_SUITE_BYTE = 0x01;
49
+ export const GITVAULT_FRAME_SUITE_ID = "01";
50
+ /** Protocol §2: max plaintext per encrypted object (256 MiB). */
51
+ export const GITVAULT_MAX_PLAINTEXT_BYTES = 256 * 1024 * 1024;
52
+ /** The genesis generation / epoch constants (schema consts). */
53
+ export const GITVAULT_GENESIS_GENERATION = "0000000000000000";
54
+ export const GITVAULT_GENESIS_EPOCH = "0000000000000001";
55
+ /** HPKE `info` label for `key_envelope` (D188). */
56
+ export const GITVAULT_ENVELOPE_INFO_LABEL = "r402s/v0/envelope";
57
+ /**
58
+ * The V0-A terminal-loss sentence — protocol §0 / client-surface spec. Doctor
59
+ * and status surfaces MUST state it verbatim while V0-A is current. Do not
60
+ * paraphrase; the wording is a reviewed product commitment.
61
+ */
62
+ export const GITVAULT_TERMINAL_LOSS_STATEMENT = "whole-machine or whole-keystore loss is terminal for vault history until human envelopes ship";
63
+ /** Protocol §0 sentence in full, as `doctor`/`status` print it. */
64
+ export const GITVAULT_TERMINAL_LOSS_DOCTOR_TEXT = "In V0-A, whole-machine or whole-keystore loss is terminal for vault history (VAULT_UNRECOVERABLE) until human envelopes ship. " +
65
+ "The vault protects source history from host-side loss while a principal keystore survives. " +
66
+ "The remaining paths are the platform's custodial restore of deployed artifacts (the deploy lane's CAS) and org/infra recovery; " +
67
+ "a recovery receipt authenticates a genesis — it cannot decrypt anything.";
68
+ const FRAME_NONCE_BYTES = 24;
69
+ const FRAME_HEADER_BYTES = 6 + 1 + FRAME_NONCE_BYTES;
70
+ const AEAD_TAG_BYTES = 16;
71
+ // ─── Scalar grammars (common.json) ───────────────────────────────────────────
72
+ export const GITVAULT_HEX16_RE = /^[0-9a-f]{16}$/;
73
+ export const GITVAULT_HEX32_RE = /^[0-9a-f]{32}$/;
74
+ export const GITVAULT_SHA256_RE = /^[0-9a-f]{64}$/;
75
+ export const GITVAULT_OID40_RE = /^[0-9a-f]{40}$/;
76
+ export const GITVAULT_VK_RE = /^vk_[0-9a-f]{32}$/;
77
+ export const GITVAULT_EK_RE = /^ek_[0-9a-f]{32}$/;
78
+ export const GITVAULT_SRC_RE = /^src_[0-9a-f]{32}$/;
79
+ export const GITVAULT_SERVICE_KEY_ID_RE = /^sk_[0-9a-z-]{4,64}$/;
80
+ export const GITVAULT_SIZE_BYTES_RE = /^(0|[1-9][0-9]{0,14})$/;
81
+ /** 32 bytes base64url: 43 chars, final char in the 16-value padding class. */
82
+ export const GITVAULT_B64U_32_RE = /^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/;
83
+ /** 64 bytes base64url: 86 chars, final char ∈ {A,Q,g,w}. */
84
+ export const GITVAULT_B64U_64_RE = /^[A-Za-z0-9_-]{85}[AQgw]$/;
85
+ /** The HPKE-sealed K_repo: 48 bytes = exactly 64 base64url chars. */
86
+ export const GITVAULT_HPKE_CT_RE = /^[A-Za-z0-9_-]{64}$/;
87
+ export const GITVAULT_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$/;
88
+ function fail(code, message, context, details) {
89
+ throw new LocalError(message, context, { code, details });
90
+ }
91
+ /** Canonical base64url (no padding, length mod 4 ≠ 1, zero trailing bits): decode→encode round-trips. */
92
+ export function isCanonicalBase64url(value) {
93
+ if (!/^[A-Za-z0-9_-]*$/.test(value) || value.length % 4 === 1)
94
+ return false;
95
+ try {
96
+ return base64urlnopad.encode(base64urlnopad.decode(value)) === value;
97
+ }
98
+ catch {
99
+ return false;
100
+ }
101
+ }
102
+ /** Decode canonical base64url or throw (`GITVAULT_NONCANONICAL_BASE64URL`). */
103
+ export function fromBase64url(value, field = "value") {
104
+ if (!isCanonicalBase64url(value)) {
105
+ fail("GITVAULT_NONCANONICAL_BASE64URL", `${field} is not canonical base64url`, "decoding r402s/v0 scalar", { field });
106
+ }
107
+ return base64urlnopad.decode(value);
108
+ }
109
+ export function toBase64url(bytes) {
110
+ return base64urlnopad.encode(bytes);
111
+ }
112
+ /** RFC 3339 UTC ms `Z` with semantic calendar validation (D187: `2026-02-31` and a non-leap Feb 29 are rejects). */
113
+ export function isValidGitvaultTimestamp(value) {
114
+ if (!GITVAULT_TIMESTAMP_RE.test(value))
115
+ return false;
116
+ const year = Number(value.slice(0, 4));
117
+ const month = Number(value.slice(5, 7));
118
+ const day = Number(value.slice(8, 10));
119
+ const leap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
120
+ const daysInMonth = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1];
121
+ return day <= daysInMonth;
122
+ }
123
+ /** Format a Date as the protocol timestamp grammar (RFC 3339 UTC, milliseconds, `Z`). */
124
+ export function formatGitvaultTimestamp(date = new Date()) {
125
+ return date.toISOString();
126
+ }
127
+ // ─── Encoding helpers ────────────────────────────────────────────────────────
128
+ /** `lp(x)` = 4-byte big-endian UTF-8 byte length ‖ UTF-8 bytes (protocol §1). */
129
+ export function lp(value) {
130
+ const bytes = utf8ToBytes(value);
131
+ const out = new Uint8Array(4 + bytes.length);
132
+ new DataView(out.buffer).setUint32(0, bytes.length, false);
133
+ out.set(bytes, 4);
134
+ return out;
135
+ }
136
+ /** `lp_opt(null)` = `0x00`; `lp_opt(x)` = `0x01` ‖ `lp(x)`. */
137
+ export function lpOpt(value) {
138
+ return value === null ? new Uint8Array([0]) : concatBytes(new Uint8Array([1]), lp(value));
139
+ }
140
+ export function sha256Hex(bytes) {
141
+ return bytesToHex(sha256(bytes));
142
+ }
143
+ /** CSPRNG bytes from the platform WebCrypto (protocol §2: all keys/nonces/ephemerals from the OS CSPRNG). */
144
+ export function randomBytes(length) {
145
+ const out = new Uint8Array(length);
146
+ globalThis.crypto.getRandomValues(out);
147
+ return out;
148
+ }
149
+ /** A fresh `<prefix>_<32 lowercase hex>` id (protocol §1: ids are CSPRNG). */
150
+ export function newGitvaultId(prefix) {
151
+ return `${prefix}_${bytesToHex(randomBytes(16))}`;
152
+ }
153
+ /** A fresh 32-lowercase-hex scalar (client_creation_id, capture_id, …). */
154
+ export function newHex32() {
155
+ return bytesToHex(randomBytes(16));
156
+ }
157
+ // ─── JCS + strict parse ──────────────────────────────────────────────────────
158
+ function assertProfileValue(value, path) {
159
+ if (value === null || typeof value === "string" || typeof value === "boolean")
160
+ return;
161
+ if (typeof value === "number" || typeof value === "bigint") {
162
+ fail("GITVAULT_JSON_NUMBER", `r402s/v0 profile forbids JSON numbers (at ${path})`, "canonicalizing r402s/v0 object", { path });
163
+ }
164
+ if (Array.isArray(value)) {
165
+ value.forEach((entry, i) => assertProfileValue(entry, `${path}[${i}]`));
166
+ return;
167
+ }
168
+ if (typeof value === "object") {
169
+ for (const [key, entry] of Object.entries(value)) {
170
+ if (entry === undefined) {
171
+ fail("GITVAULT_JSON_UNDEFINED", `undefined member at ${path}.${key}`, "canonicalizing r402s/v0 object", { path: `${path}.${key}` });
172
+ }
173
+ assertProfileValue(entry, `${path}.${key}`);
174
+ }
175
+ return;
176
+ }
177
+ fail("GITVAULT_JSON_UNSUPPORTED", `unsupported value at ${path}`, "canonicalizing r402s/v0 object", { path });
178
+ }
179
+ /** RFC 8785 canonical JSON (no-numbers profile) as UTF-8 bytes. */
180
+ export function jcs(value) {
181
+ assertProfileValue(value, "$");
182
+ return utf8ToBytes(canonicalize(value));
183
+ }
184
+ /** RFC 8785 canonical JSON as a string. */
185
+ export function jcsString(value) {
186
+ assertProfileValue(value, "$");
187
+ return canonicalize(value);
188
+ }
189
+ function inspectStrict(node, path) {
190
+ switch (node.type) {
191
+ case "Document":
192
+ return inspectStrict(node.body, path);
193
+ case "Number":
194
+ fail("GITVAULT_STRICT_PARSE", `JSON number at ${path}`, "strict-parsing r402s/v0 object", { reason: "json-number", path });
195
+ // eslint-disable-next-line no-fallthrough
196
+ case "Object": {
197
+ const seen = new Set();
198
+ for (const member of node.members) {
199
+ const name = member.name.type === "String" ? member.name.value : member.name.name;
200
+ if (seen.has(name)) {
201
+ fail("GITVAULT_STRICT_PARSE", `duplicate member ${name} at ${path}`, "strict-parsing r402s/v0 object", { reason: "duplicate-member", path: `${path}.${name}` });
202
+ }
203
+ seen.add(name);
204
+ inspectStrict(member.value, `${path}.${name}`);
205
+ }
206
+ return;
207
+ }
208
+ case "Array":
209
+ node.elements.forEach((entry, i) => inspectStrict(entry.value, `${path}[${i}]`));
210
+ return;
211
+ default:
212
+ return;
213
+ }
214
+ }
215
+ /**
216
+ * The §1 strict-parse profile: reject ANY JSON number, duplicate members,
217
+ * invalid JSON, and any text that is not byte-for-byte the JCS of its value
218
+ * (member order, whitespace, escapes). Schema-awareness (unknown members,
219
+ * hex case, scalar lengths) is the caller's job.
220
+ */
221
+ export function parseGitvaultStrict(text) {
222
+ let ast;
223
+ try {
224
+ ast = parse(text, { mode: "json", allowTrailingCommas: false });
225
+ }
226
+ catch {
227
+ fail("GITVAULT_STRICT_PARSE", "invalid JSON", "strict-parsing r402s/v0 object", { reason: "invalid-json" });
228
+ }
229
+ inspectStrict(ast, "$");
230
+ const value = evaluate(ast);
231
+ if (jcsString(value) !== text) {
232
+ fail("GITVAULT_STRICT_PARSE", "text is not canonical JCS", "strict-parsing r402s/v0 object", { reason: "noncanonical-encoding" });
233
+ }
234
+ return value;
235
+ }
236
+ /** Classify a strict-parse failure (for vector replay + diagnostics). */
237
+ export function gitvaultStrictParseReason(error) {
238
+ if (error instanceof LocalError && error.code === "GITVAULT_STRICT_PARSE") {
239
+ const details = error.details;
240
+ return details?.reason ?? null;
241
+ }
242
+ return null;
243
+ }
244
+ // ─── Hash rules: stored bytes + signature preimage ───────────────────────────
245
+ /** Stored bytes of a signed object = JCS of the COMPLETE object including `signature`. */
246
+ export function storedBytes(object) {
247
+ return jcs(object);
248
+ }
249
+ /** SHA-256 over stored bytes — what every `*_sha256` naming a signed object means. */
250
+ export function storedBytesSha256(object) {
251
+ return sha256Hex(storedBytes(object));
252
+ }
253
+ function withoutSignature(object) {
254
+ const { signature: _signature, ...rest } = object;
255
+ void _signature;
256
+ return rest;
257
+ }
258
+ /** Signature preimage = `"r402s/v0/" + object_kind + "\n" + JCS(object without its single top-level signature)`. */
259
+ export function signaturePreimage(objectKind, objectWithoutSignature) {
260
+ if ("signature" in objectWithoutSignature) {
261
+ fail("GITVAULT_PREIMAGE_HAS_SIGNATURE", "preimage input must not carry a signature member", "building r402s/v0 signature preimage");
262
+ }
263
+ return concatBytes(utf8ToBytes(`${GITVAULT_FORMAT}/${objectKind}\n`), jcs(objectWithoutSignature));
264
+ }
265
+ // ─── Ed25519 strict (RFC 8032, zip215:false) ─────────────────────────────────
266
+ export function ed25519PublicKey(seed) {
267
+ return ed25519.getPublicKey(seed);
268
+ }
269
+ export function ed25519Sign(message, seed) {
270
+ return ed25519.sign(message, seed);
271
+ }
272
+ /**
273
+ * Strict RFC 8032 verification: `zip215: false` is PINNED. Non-canonical R or A
274
+ * encodings, non-canonical S, and wrong-length inputs are all `false` — never
275
+ * an exception a caller could mistake for a transport error.
276
+ */
277
+ export function ed25519VerifyStrict(signature, message, publicKey) {
278
+ if (signature.length !== 64 || publicKey.length !== 32)
279
+ return false;
280
+ try {
281
+ return ed25519.verify(signature, message, publicKey, { zip215: false });
282
+ }
283
+ catch {
284
+ return false;
285
+ }
286
+ }
287
+ /** Sign an object under its `object_kind` domain; returns the complete signed object. */
288
+ export function signGitvaultObject(objectWithoutSignature, seed) {
289
+ const preimage = signaturePreimage(objectWithoutSignature.object_kind, objectWithoutSignature);
290
+ return { ...objectWithoutSignature, signature: toBase64url(ed25519Sign(preimage, seed)) };
291
+ }
292
+ /**
293
+ * Verify a signed object against a raw public key. Exact scalar lengths are
294
+ * checked FIRST (an 85-char signature or a 42-char key is a schema reject,
295
+ * never a decode-time surprise), then strict Ed25519 under the object's domain.
296
+ */
297
+ export function verifyGitvaultObject(object, publicKey) {
298
+ if (typeof object.signature !== "string" || !GITVAULT_B64U_64_RE.test(object.signature) || !isCanonicalBase64url(object.signature))
299
+ return false;
300
+ let pub;
301
+ if (typeof publicKey === "string") {
302
+ if (!GITVAULT_B64U_32_RE.test(publicKey) || !isCanonicalBase64url(publicKey))
303
+ return false;
304
+ pub = fromBase64url(publicKey);
305
+ }
306
+ else {
307
+ pub = publicKey;
308
+ }
309
+ const preimage = signaturePreimage(object.object_kind, withoutSignature(object));
310
+ return ed25519VerifyStrict(fromBase64url(object.signature), preimage, pub);
311
+ }
312
+ /** Verify or throw `GITVAULT_SIGNATURE_INVALID`. */
313
+ export function assertGitvaultSignature(object, publicKey, context = "verifying r402s/v0 signature") {
314
+ if (!verifyGitvaultObject(object, publicKey)) {
315
+ fail("GITVAULT_SIGNATURE_INVALID", `${object.object_kind} signature does not verify (strict RFC 8032)`, context, { object_kind: object.object_kind });
316
+ }
317
+ }
318
+ // ─── Key fingerprints + keypairs ─────────────────────────────────────────────
319
+ /** First 16 bytes of SHA-256(raw pubkey), lowercase hex (32 chars). */
320
+ export function keyFingerprintHex(rawPublicKey) {
321
+ return sha256Hex(rawPublicKey).slice(0, 32);
322
+ }
323
+ /** `vk_` fingerprint of a raw Ed25519 public key. */
324
+ export function vkFingerprint(rawPublicKey) {
325
+ return `vk_${keyFingerprintHex(rawPublicKey)}`;
326
+ }
327
+ /** `ek_` fingerprint of a raw X25519 public key. */
328
+ export function ekFingerprint(rawPublicKey) {
329
+ return `ek_${keyFingerprintHex(rawPublicKey)}`;
330
+ }
331
+ export function generateSigningKeypair(seed = randomBytes(32)) {
332
+ if (seed.length !== 32)
333
+ fail("GITVAULT_BAD_SEED", "Ed25519 seed must be 32 bytes", "generating signing keypair");
334
+ return { seed, public_key: ed25519PublicKey(seed) };
335
+ }
336
+ export function generateEncryptionKeypair(privateKey = randomBytes(32)) {
337
+ if (privateKey.length !== 32)
338
+ fail("GITVAULT_BAD_SEED", "X25519 private key must be 32 bytes", "generating encryption keypair");
339
+ return { private_key: privateKey, public_key: x25519.getPublicKey(privateKey) };
340
+ }
341
+ // ─── HKDF: k_obj / K_digest, keyed commitments ──────────────────────────────
342
+ /** `k_obj` info = lp("r402s/v0") ‖ lp(suite) ‖ lp(repo_id) ‖ lp(epoch) ‖ lp(object_kind) ‖ lp(object_id) ‖ lp("L=32"). */
343
+ export function objectKeyInfo(repoId, epoch, objectKind, objectId) {
344
+ return concatBytes(lp(GITVAULT_FORMAT), lp(GITVAULT_SUITE), lp(repoId), lp(epoch), lp(objectKind), lp(objectId), lp("L=32"));
345
+ }
346
+ /** `k_obj = HKDF-SHA-256(ikm=K_repo, salt=∅, info, L=32)`. */
347
+ export function deriveObjectKey(kRepo, repoId, epoch, objectKind, objectId) {
348
+ return hkdf(sha256, kRepo, undefined, objectKeyInfo(repoId, epoch, objectKind, objectId), 32);
349
+ }
350
+ /** `K_digest` info = lp("r402s/v0") ‖ lp("r402s-1") ‖ lp(repo_id) ‖ lp(epoch) ‖ lp("digest") ‖ lp(label) ‖ lp("L=32"). */
351
+ export function digestKeyInfo(repoId, epoch, label) {
352
+ return concatBytes(lp(GITVAULT_FORMAT), lp(GITVAULT_SUITE), lp(repoId), lp(epoch), lp("digest"), lp(label), lp("L=32"));
353
+ }
354
+ export function deriveDigestKey(kRepo, repoId, epoch, label) {
355
+ return hkdf(sha256, kRepo, undefined, digestKeyInfo(repoId, epoch, label), 32);
356
+ }
357
+ /** Commitment = HMAC-SHA-256(K_digest(label), JCS(content)), lowercase hex. */
358
+ export function keyedCommitment(kDigest, content) {
359
+ return bytesToHex(hmac(sha256, kDigest, jcs(content)));
360
+ }
361
+ /** The `"objectset"` content shape: `{oids:[sorted unique lowercase 40-hex]}`. */
362
+ export function objectsetContent(oids) {
363
+ const unique = [...new Set(oids)];
364
+ for (const oid of unique) {
365
+ if (!GITVAULT_OID40_RE.test(oid))
366
+ fail("GITVAULT_BAD_OID", `not a lowercase 40-hex oid: ${oid}`, "building objectset content");
367
+ }
368
+ return { oids: unique.sort() };
369
+ }
370
+ /** True iff an `objectset` content is already in canonical (sorted-unique) order. */
371
+ export function isCanonicalObjectset(content) {
372
+ const canonical = objectsetContent(content.oids).oids;
373
+ return canonical.length === content.oids.length && canonical.every((oid, i) => oid === content.oids[i]);
374
+ }
375
+ /** The `"snapshot_oid"` content shape. */
376
+ export function snapshotOidContent(oid) {
377
+ if (!GITVAULT_OID40_RE.test(oid))
378
+ fail("GITVAULT_BAD_OID", `not a lowercase 40-hex oid: ${oid}`, "building snapshot_oid content");
379
+ return { oid, format: "sha1" };
380
+ }
381
+ // ─── Golden preimages (open-id / open-binding) ──────────────────────────────
382
+ /** `"r402s/v0/open-id" ‖ lp(org_id) ‖ lp(repo_id) ‖ lp(client_open_id)`. */
383
+ export function openIdPreimage(orgId, repoId, clientOpenId) {
384
+ return concatBytes(utf8ToBytes("r402s/v0/open-id"), lp(orgId), lp(repoId), lp(clientOpenId));
385
+ }
386
+ /** `"r402s/v0/open-binding" ‖ lp(client_open_id) ‖ lp(base_head_sha256) ‖ lp_opt(prior) ‖ lp(requested_r2_cap_size_bytes)`. */
387
+ export function openBindingPreimage(clientOpenId, baseHeadSha256, priorCheckpointClaimSetSha256, requestedR2CapSizeBytes) {
388
+ return concatBytes(utf8ToBytes("r402s/v0/open-binding"), lp(clientOpenId), lp(baseHeadSha256), lpOpt(priorCheckpointClaimSetSha256), lp(requestedR2CapSizeBytes));
389
+ }
390
+ // ─── AEAD frame (XChaCha20-Poly1305) ────────────────────────────────────────
391
+ /** The seven-member frame AAD, as the object JCS serializes. */
392
+ export function frameAad(repoId, objectKind, objectId, epoch) {
393
+ return { repo_id: repoId, object_kind: objectKind, object_id: objectId, epoch, suite: GITVAULT_SUITE, magic: GITVAULT_FRAME_MAGIC, suite_id: GITVAULT_FRAME_SUITE_ID };
394
+ }
395
+ /**
396
+ * Seal a plaintext into the §2 frame: `"R402S0"` ‖ `0x01` ‖ nonce(24) ‖ ct‖tag.
397
+ * `ciphertext_sha256` covers the COMPLETE frame. Callers own the single-use
398
+ * rule: one object id, one seal — never re-encrypt under an id.
399
+ */
400
+ export function sealFrame(input) {
401
+ if (input.k_obj.length !== 32)
402
+ fail("GITVAULT_BAD_KEY", "k_obj must be 32 bytes", "sealing r402s/v0 frame");
403
+ if (input.plaintext.length > GITVAULT_MAX_PLAINTEXT_BYTES) {
404
+ fail("GIT_OBJECT_TOO_LARGE", "plaintext exceeds the 256 MiB per-object cap", "sealing r402s/v0 frame", { size_bytes: String(input.plaintext.length) });
405
+ }
406
+ const nonce = input.nonce ?? randomBytes(FRAME_NONCE_BYTES);
407
+ if (nonce.length !== FRAME_NONCE_BYTES)
408
+ fail("GITVAULT_BAD_NONCE", "frame nonce must be 24 bytes", "sealing r402s/v0 frame");
409
+ const aad = jcs(frameAad(input.repo_id, input.object_kind, input.object_id, input.epoch));
410
+ const ct = xchacha20poly1305(input.k_obj, nonce, aad).encrypt(input.plaintext);
411
+ const frame = concatBytes(utf8ToBytes(GITVAULT_FRAME_MAGIC), new Uint8Array([GITVAULT_FRAME_SUITE_BYTE]), nonce, ct);
412
+ return { frame, ciphertext_sha256: sha256Hex(frame), size_bytes: String(frame.length), nonce_hex: bytesToHex(nonce) };
413
+ }
414
+ /**
415
+ * Open a §2 frame: header checked (magic, suite byte, length), receipt
416
+ * compared when supplied, then the AEAD opened under the seven-member AAD.
417
+ * No unverified streaming release — the whole plaintext or nothing.
418
+ */
419
+ export function openFrame(input) {
420
+ const { frame } = input;
421
+ if (input.expected_ciphertext_sha256 !== undefined && sha256Hex(frame) !== input.expected_ciphertext_sha256) {
422
+ fail("GITVAULT_RECEIPT_MISMATCH", "frame bytes do not hash to the receipt's ciphertext_sha256", "opening r402s/v0 frame", { object_id: input.object_id });
423
+ }
424
+ if (frame.length < FRAME_HEADER_BYTES + AEAD_TAG_BYTES) {
425
+ fail("GITVAULT_FRAME_INVALID", "frame shorter than header + tag", "opening r402s/v0 frame", { object_id: input.object_id });
426
+ }
427
+ const magic = new TextDecoder().decode(frame.subarray(0, 6));
428
+ if (magic !== GITVAULT_FRAME_MAGIC || frame[6] !== GITVAULT_FRAME_SUITE_BYTE) {
429
+ fail("GITVAULT_FRAME_INVALID", "frame header magic/suite mismatch", "opening r402s/v0 frame", { object_id: input.object_id });
430
+ }
431
+ return openFrameWithAad(input.k_obj, frame, jcs(frameAad(input.repo_id, input.object_kind, input.object_id, input.epoch)), input.object_id);
432
+ }
433
+ /**
434
+ * The AEAD half of {@link openFrame} with the AAD supplied verbatim — the
435
+ * vector replay uses it to prove every one of the seven AAD members is
436
+ * authenticated (including the constant `suite`/`magic`/`suite_id` members
437
+ * that {@link openFrame}'s typed input cannot vary). Header already checked.
438
+ */
439
+ export function openFrameWithAad(kObj, frame, aad, objectId = "?") {
440
+ if (frame.length < FRAME_HEADER_BYTES + AEAD_TAG_BYTES) {
441
+ fail("GITVAULT_FRAME_INVALID", "frame shorter than header + tag", "opening r402s/v0 frame", { object_id: objectId });
442
+ }
443
+ const nonce = frame.subarray(7, FRAME_HEADER_BYTES);
444
+ const ct = frame.subarray(FRAME_HEADER_BYTES);
445
+ try {
446
+ return xchacha20poly1305(kObj, nonce, aad).decrypt(ct);
447
+ }
448
+ catch {
449
+ fail("GITVAULT_AEAD_AUTH_FAILURE", "frame failed AEAD authentication", "opening r402s/v0 frame", { object_id: objectId });
450
+ }
451
+ }
452
+ // ─── HPKE (named implementation: @hpke/core + @hpke/chacha20poly1305) ───────
453
+ let hpkeSuite;
454
+ /** The one `r402s-1` HPKE suite: Base mode, DHKEM(X25519,HKDF-SHA256) 0x0020 / HKDF-SHA256 0x0001 / ChaCha20-Poly1305 0x0003. */
455
+ export function gitvaultHpkeSuite() {
456
+ hpkeSuite ??= new CipherSuite({ kem: new DhkemX25519HkdfSha256(), kdf: new HkdfSha256(), aead: new Chacha20Poly1305() });
457
+ return hpkeSuite;
458
+ }
459
+ function toArrayBuffer(bytes) {
460
+ const copy = new Uint8Array(bytes.length);
461
+ copy.set(bytes);
462
+ return copy.buffer;
463
+ }
464
+ /** HPKE `info` (D188): lp("r402s/v0/envelope") ‖ lp("r402s-1") ‖ lp(lowerhex(SHA-256(recipient raw X25519 pubkey))) ‖ lp(created_by). */
465
+ export function envelopeInfo(recipientPublicKeyRaw, createdBy) {
466
+ return concatBytes(lp(GITVAULT_ENVELOPE_INFO_LABEL), lp(GITVAULT_SUITE), lp(sha256Hex(recipientPublicKeyRaw)), lp(createdBy));
467
+ }
468
+ /** HPKE AAD (D188): JCS of exactly `{repo_id, epoch, recipient_kind:"principal", recipient_fingerprint}`. */
469
+ export function envelopeAad(repoId, epoch, recipientFingerprint) {
470
+ return { repo_id: repoId, epoch, recipient_kind: "principal", recipient_fingerprint: recipientFingerprint };
471
+ }
472
+ /** Single-shot HPKE seal (Base mode) returning `{enc, ct}`. */
473
+ export async function hpkeSeal(input) {
474
+ const suite = gitvaultHpkeSuite();
475
+ const recipientPublicKey = await suite.kem.deserializePublicKey(toArrayBuffer(input.recipient_public_key));
476
+ const ekm = input.ikm_e ? await suite.kem.deriveKeyPair(toArrayBuffer(input.ikm_e)) : undefined;
477
+ const sender = await suite.createSenderContext({ recipientPublicKey, info: toArrayBuffer(input.info), ...(ekm ? { ekm } : {}) });
478
+ const ct = new Uint8Array(await sender.seal(toArrayBuffer(input.plaintext), toArrayBuffer(input.aad)));
479
+ return { enc: new Uint8Array(sender.enc), ct };
480
+ }
481
+ /** Single-shot HPKE open; any failure is `GITVAULT_HPKE_OPEN_FAILED` (never garbage plaintext). */
482
+ export async function hpkeOpen(input) {
483
+ const suite = gitvaultHpkeSuite();
484
+ try {
485
+ const recipientKey = await suite.kem.deserializePrivateKey(toArrayBuffer(input.recipient_private_key));
486
+ const recipient = await suite.createRecipientContext({ recipientKey, enc: toArrayBuffer(input.enc), info: toArrayBuffer(input.info) });
487
+ return new Uint8Array(await recipient.open(toArrayBuffer(input.ct), toArrayBuffer(input.aad)));
488
+ }
489
+ catch {
490
+ fail("GITVAULT_HPKE_OPEN_FAILED", "HPKE open failed — the recipient recovers no key", "opening r402s/v0 key envelope");
491
+ }
492
+ }
493
+ /** Seal + sign a `key_envelope` for one principal; returns the object with its stored-bytes identity + genesis receipt. */
494
+ export async function sealKeyEnvelope(input) {
495
+ if (input.k_repo.length !== 32)
496
+ fail("GITVAULT_BAD_KEY", "K_repo must be 32 bytes", "sealing r402s/v0 key envelope");
497
+ if (!isValidGitvaultTimestamp(input.created_at))
498
+ fail("GITVAULT_BAD_TIMESTAMP", "created_at is not an RFC 3339 UTC ms timestamp", "sealing r402s/v0 key envelope");
499
+ const createdBy = vkFingerprint(input.signer.public_key);
500
+ const recipientFingerprint = ekFingerprint(input.recipient_public_key);
501
+ const { enc, ct } = await hpkeSeal({
502
+ recipient_public_key: input.recipient_public_key,
503
+ info: envelopeInfo(input.recipient_public_key, createdBy),
504
+ aad: jcs(envelopeAad(input.repo_id, input.epoch, recipientFingerprint)),
505
+ plaintext: input.k_repo,
506
+ ikm_e: input.ikm_e,
507
+ });
508
+ const unsigned = {
509
+ format: GITVAULT_FORMAT,
510
+ object_kind: "key_envelope",
511
+ suite: GITVAULT_SUITE,
512
+ repo_id: input.repo_id,
513
+ epoch: input.epoch,
514
+ recipient_kind: "principal",
515
+ recipient_fingerprint: recipientFingerprint,
516
+ enc: toBase64url(enc),
517
+ ct: toBase64url(ct),
518
+ created_by: createdBy,
519
+ created_at: input.created_at,
520
+ };
521
+ const envelope = signGitvaultObject(unsigned, input.signer.seed);
522
+ const bytes = storedBytes(envelope);
523
+ const hash = sha256Hex(bytes);
524
+ return {
525
+ envelope,
526
+ stored_bytes: bytes,
527
+ stored_bytes_sha256: hash,
528
+ size_bytes: String(bytes.length),
529
+ receipt: { object_kind: "key_envelope", epoch: input.epoch, recipient_fingerprint: recipientFingerprint, stored_bytes_sha256: hash, size_bytes: String(bytes.length) },
530
+ };
531
+ }
532
+ /**
533
+ * Open a `key_envelope`: exact-scalar checks → signature (strict, domain
534
+ * `key_envelope`) → path identity (recipient fingerprint = ours) → HPKE open
535
+ * with the D188 info/AAD. Returns the raw 32-byte K_repo.
536
+ */
537
+ export async function openKeyEnvelope(input) {
538
+ const env = input.envelope;
539
+ if (!GITVAULT_HPKE_CT_RE.test(env.ct) || !GITVAULT_B64U_32_RE.test(env.enc) || !isCanonicalBase64url(env.ct) || !isCanonicalBase64url(env.enc)) {
540
+ fail("GITVAULT_SCHEMA_REJECT", "key_envelope enc/ct are not the exact scalar lengths", "opening r402s/v0 key envelope");
541
+ }
542
+ assertGitvaultSignature(env, input.signer_public_key, "opening r402s/v0 key envelope");
543
+ const ours = ekFingerprint(input.recipient.public_key);
544
+ if (env.recipient_fingerprint !== ours || env.recipient_kind !== "principal") {
545
+ fail("GITVAULT_ENVELOPE_NOT_FOR_RECIPIENT", "key_envelope is addressed to another recipient", "opening r402s/v0 key envelope", { recipient_fingerprint: env.recipient_fingerprint });
546
+ }
547
+ const kRepo = await hpkeOpen({
548
+ recipient_private_key: input.recipient.private_key,
549
+ enc: fromBase64url(env.enc, "enc"),
550
+ info: envelopeInfo(input.recipient.public_key, env.created_by),
551
+ aad: jcs(envelopeAad(env.repo_id, env.epoch, env.recipient_fingerprint)),
552
+ ct: fromBase64url(env.ct, "ct"),
553
+ });
554
+ if (kRepo.length !== 32)
555
+ fail("GITVAULT_HPKE_OPEN_FAILED", "opened plaintext is not a 32-byte K_repo", "opening r402s/v0 key envelope");
556
+ return kRepo;
557
+ }
558
+ /** Build + sign a `vault_genesis` (generation 0, epoch 1, the creator's single envelope). */
559
+ export function buildVaultGenesis(input) {
560
+ const unsigned = {
561
+ format: GITVAULT_FORMAT,
562
+ object_kind: "vault_genesis",
563
+ suite: GITVAULT_SUITE,
564
+ repo_id: input.repo_id,
565
+ org_id: input.org_id,
566
+ project_id: input.project_id,
567
+ allocation_nonce: input.allocation_nonce,
568
+ generation: GITVAULT_GENESIS_GENERATION,
569
+ epoch: GITVAULT_GENESIS_EPOCH,
570
+ git_object_format: "sha1",
571
+ creator_signing_pubkey: toBase64url(input.creator_signing.public_key),
572
+ creator_encryption_pubkey: toBase64url(input.creator_encryption_public_key),
573
+ envelopes: [input.envelope_receipt],
574
+ writer_key_id: vkFingerprint(input.creator_signing.public_key),
575
+ created_at: input.created_at,
576
+ };
577
+ return signGitvaultObject(unsigned, input.creator_signing.seed);
578
+ }
579
+ /**
580
+ * The genesis key bindings (round-10 A6): exactly one envelope at epoch 1 whose
581
+ * `recipient_fingerprint` is the creator's `ek_`; `writer_key_id` is the
582
+ * creator's `vk_`; and, when the stored envelope is supplied, the receipt's
583
+ * hash/size name its stored bytes and its path identity + `created_by` equal
584
+ * the genesis values. Schema-valid-but-protocol-invalid objects surface here.
585
+ */
586
+ export function checkGenesisKeyBindings(genesis, storedEnvelope) {
587
+ const problems = [];
588
+ if (genesis.envelopes.length !== 1)
589
+ problems.push("envelope_count");
590
+ const receipt = genesis.envelopes[0];
591
+ if (receipt && receipt.epoch !== GITVAULT_GENESIS_EPOCH)
592
+ problems.push("envelope_epoch");
593
+ const expectedEk = ekFingerprint(fromBase64url(genesis.creator_encryption_pubkey, "creator_encryption_pubkey"));
594
+ if (receipt && receipt.recipient_fingerprint !== expectedEk)
595
+ problems.push("envelope_recipient_fingerprint");
596
+ const expectedVk = vkFingerprint(fromBase64url(genesis.creator_signing_pubkey, "creator_signing_pubkey"));
597
+ if (genesis.writer_key_id !== expectedVk)
598
+ problems.push("writer_key_id");
599
+ if (storedEnvelope && receipt) {
600
+ const bytes = storedBytes(storedEnvelope);
601
+ if (sha256Hex(bytes) !== receipt.stored_bytes_sha256)
602
+ problems.push("stored_envelope_hash");
603
+ if (String(bytes.length) !== receipt.size_bytes)
604
+ problems.push("stored_envelope_size");
605
+ if (storedEnvelope.repo_id !== genesis.repo_id)
606
+ problems.push("stored_envelope_repo_id");
607
+ if (storedEnvelope.epoch !== genesis.epoch)
608
+ problems.push("stored_envelope_epoch");
609
+ if (storedEnvelope.recipient_fingerprint !== receipt.recipient_fingerprint)
610
+ problems.push("stored_envelope_recipient_fingerprint");
611
+ if (storedEnvelope.created_by !== genesis.writer_key_id)
612
+ problems.push("stored_envelope_created_by");
613
+ }
614
+ return problems;
615
+ }
616
+ /** Build + sign the creator's `recovery_receipt` (emitted at ACTIVE; integrity data, not a secret). */
617
+ export function buildRecoveryReceipt(input) {
618
+ const unsigned = {
619
+ format: GITVAULT_FORMAT,
620
+ object_kind: "recovery_receipt",
621
+ suite: GITVAULT_SUITE,
622
+ repo_id: input.repo_id,
623
+ org_id: input.org_id,
624
+ project_id: input.project_id,
625
+ genesis_sha256: input.genesis_sha256,
626
+ creator_signing_fingerprint: vkFingerprint(input.creator_signing.public_key),
627
+ creator_encryption_fingerprint: ekFingerprint(input.creator_encryption_public_key),
628
+ };
629
+ return signGitvaultObject(unsigned, input.creator_signing.seed);
630
+ }
631
+ /**
632
+ * Check a recovery receipt against a genesis it claims to pin: the receipt's
633
+ * signature (under the genesis's creator key), the genesis stored-bytes hash,
634
+ * and every identity field. A substituted, internally consistent vault fails
635
+ * on `genesis_sha256` — exactly the fabricated-vault scenario.
636
+ */
637
+ export function checkRecoveryReceipt(receipt, genesis) {
638
+ const problems = [];
639
+ if (!verifyGitvaultObject(receipt, genesis.creator_signing_pubkey))
640
+ problems.push("signature");
641
+ if (receipt.genesis_sha256 !== storedBytesSha256(genesis))
642
+ problems.push("genesis_sha256");
643
+ if (receipt.repo_id !== genesis.repo_id)
644
+ problems.push("repo_id");
645
+ if (receipt.org_id !== genesis.org_id)
646
+ problems.push("org_id");
647
+ if (receipt.project_id !== genesis.project_id)
648
+ problems.push("project_id");
649
+ if (receipt.creator_signing_fingerprint !== genesis.writer_key_id)
650
+ problems.push("creator_signing_fingerprint");
651
+ const expectedEk = ekFingerprint(fromBase64url(genesis.creator_encryption_pubkey, "creator_encryption_pubkey"));
652
+ if (receipt.creator_encryption_fingerprint !== expectedEk)
653
+ problems.push("creator_encryption_fingerprint");
654
+ return problems;
655
+ }
656
+ /**
657
+ * Field-level checks on an `allocation` for THIS creation attempt (the
658
+ * control-plane signature itself is verified through the service-key registry,
659
+ * which task 5.4 wires; pass `service_public_key` once it is pinned).
660
+ */
661
+ export function checkAllocation(allocation, expected, servicePublicKey) {
662
+ const problems = [];
663
+ if (allocation.format !== GITVAULT_FORMAT || allocation.object_kind !== "allocation" || allocation.suite !== GITVAULT_SUITE)
664
+ problems.push("envelope");
665
+ if (!GITVAULT_SRC_RE.test(allocation.repo_id))
666
+ problems.push("repo_id");
667
+ if (!GITVAULT_HEX32_RE.test(allocation.allocation_nonce))
668
+ problems.push("allocation_nonce");
669
+ if (!GITVAULT_HEX16_RE.test(allocation.allocation_generation))
670
+ problems.push("allocation_generation");
671
+ if (!GITVAULT_SERVICE_KEY_ID_RE.test(allocation.service_key_id))
672
+ problems.push("service_key_id");
673
+ if (allocation.client_creation_id !== expected.client_creation_id)
674
+ problems.push("client_creation_id");
675
+ if (allocation.creator_signing_fingerprint !== expected.creator_signing_fingerprint)
676
+ problems.push("creator_signing_fingerprint");
677
+ if (allocation.creator_encryption_fingerprint !== expected.creator_encryption_fingerprint)
678
+ problems.push("creator_encryption_fingerprint");
679
+ if (allocation.org_id !== expected.org_id)
680
+ problems.push("org_id");
681
+ if (allocation.project_id !== expected.project_id)
682
+ problems.push("project_id");
683
+ if (!isValidGitvaultTimestamp(allocation.issued_at) || !isValidGitvaultTimestamp(allocation.created_at))
684
+ problems.push("timestamp");
685
+ if (servicePublicKey !== undefined && !verifyGitvaultObject(allocation, servicePublicKey))
686
+ problems.push("signature");
687
+ return problems;
688
+ }
689
+ export { bytesToHex, hexToBytes };
690
+ //# sourceMappingURL=gitvault.crypto.js.map