wire-mesh-core 1.42.0 → 1.44.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.
@@ -0,0 +1,149 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/domain/erasure-coding.ts
3
+ /**
4
+ * K-of-N erasure coding over GF(2^8) for wire-mesh#36's mailbox-opacity case (the shard half of #34's encrypt-then-shard design): a systematic Reed-Solomon construction where the first `dataShards` shards ARE the padded input split into equal runs, and the remaining shards are parity rows of a Vandermonde-style matrix. Any `dataShards` of `totalShards` reconstruct the input -- the property a single mailbox holder must NOT have (it holds one shard and never the epoch key either).
5
+ *
6
+ * Deliberately hand-written rather than a dependency, the same choice the ECIES construction made: the encode format must be byte-identical across this and the Rust implementation, so both pin the SAME vectors against the documented construction rather than trusting two third-party ports to agree. The math is linear algebra over GF(2^8) with the standard 0x11d polynomial (AES's field), not cryptography.
7
+ *
8
+ * The construction, pinned: pad the input with zero bytes to dataShards * ceil(len / dataShards); any shard index below dataShards is that shard's own equal run; parity shard j (0-based within the parity block) has, at each byte position, the GF dot product of the data runs at that position with the coefficient row (alpha^j, alpha^(j+1), ..., alpha^(j+dataShards-1)) where alpha = 0x02 (the field's generator). Decoding: build the dataShards x dataShards submatrix of the full encode matrix selected by the surviving shards' row indices, invert it over the field, and apply it to the surviving shards' bytes. The encode matrix's first dataShards rows are the identity (systematic), so the all-data-shards case needs no inversion at all.
9
+ */
10
+ /** The AES field's irreducible polynomial x^8+x^4+x^3+x+1, the conventional GF(2^8) choice. */
11
+ const GF_POLY = 285;
12
+ /** The bit that appears when a GF(2^8) value overflows a byte and needs the polynomial reduction. */
13
+ const GF_OVERFLOW_BIT = 256;
14
+ /** GF(2^8) caps total shards at 255 (a 256th row would repeat, breaking invertibility). */
15
+ const GF_MAX_SHARDS = 255;
16
+ const gfExp = Array.from({ length: 510 }, () => 0);
17
+ const gfLog = Array.from({ length: 256 }, () => 0);
18
+ {
19
+ let x = 1;
20
+ for (let power = 0; power < GF_MAX_SHARDS; power += 1) {
21
+ gfExp[power] = x;
22
+ gfLog[x] = power;
23
+ x <<= 1;
24
+ if (x & GF_OVERFLOW_BIT) x ^= GF_POLY;
25
+ }
26
+ for (let power = GF_MAX_SHARDS; power < 510; power += 1) gfExp[power] = gfExp[power - GF_MAX_SHARDS] ?? 0;
27
+ }
28
+ function gfMul(a, b) {
29
+ if (a === 0 || b === 0) return 0;
30
+ return gfExp[(gfLog[a] ?? 0) + (gfLog[b] ?? 0)] ?? 0;
31
+ }
32
+ function gfInv(a) {
33
+ return gfExp[GF_MAX_SHARDS - (gfLog[a] ?? 0)] ?? 1;
34
+ }
35
+ function validateConfig(config) {
36
+ const { dataShards, totalShards } = config;
37
+ if (!Number.isInteger(dataShards) || dataShards < 1) throw new Error(`dataShards must be a positive integer, got ${String(dataShards)}`);
38
+ if (!Number.isInteger(totalShards) || totalShards < dataShards) throw new Error(`totalShards must be an integer at least dataShards (${String(dataShards)}), got ${String(totalShards)}`);
39
+ if (totalShards > GF_MAX_SHARDS) throw new Error(`the GF(2^8) field supports at most ${String(GF_MAX_SHARDS)} shards, got ${String(totalShards)}`);
40
+ }
41
+ /** Row i of the encode matrix: identity rows for the data shards, alpha-power rows for parity. */
42
+ /**
43
+ * Row i of the encode matrix: identity rows for the data shards, Cauchy rows for parity. Cauchy, not Vandermonde, because only Cauchy guarantees EVERY square submatrix is invertible -- identity-plus-Vandermonde has singular K-subsets (found the hard way: parity-based reconstruction produced wrong bytes on some subsets). With parity row j carrying c_i = 1/(x_j XOR y_i) where x_j = row and y_i = column (all row/column values distinct across the whole matrix, no parity row index equals any data column index, so no denominator is zero), any K surviving rows reduce -- expanding along the identity rows -- to a square Cauchy submatrix, always invertible.
44
+ */
45
+ function encodeMatrixRow(row, dataShards) {
46
+ const coefficients = Array.from({ length: dataShards }, () => 0);
47
+ if (row < dataShards) {
48
+ coefficients[row] = 1;
49
+ return coefficients;
50
+ }
51
+ for (let column = 0; column < dataShards; column += 1) coefficients[column] = gfInv(row ^ column);
52
+ return coefficients;
53
+ }
54
+ /** Inverts a square matrix over GF(2^8) by Gauss-Jordan with an identity augmentation. */
55
+ function invertMatrix(matrix) {
56
+ const size = matrix.length;
57
+ const work = matrix.map((row, rowIndex) => {
58
+ const augmented = [...row, ...Array.from({ length: size }, () => 0)];
59
+ augmented[size + rowIndex] = 1;
60
+ return augmented;
61
+ });
62
+ for (let column = 0; column < size; column += 1) {
63
+ let pivotRow = -1;
64
+ for (let candidate = column; candidate < size; candidate += 1) if ((work[candidate]?.[column] ?? 0) !== 0) {
65
+ pivotRow = candidate;
66
+ break;
67
+ }
68
+ if (pivotRow === -1) throw new Error("matrix is singular over GF(2^8)");
69
+ if (pivotRow !== column) {
70
+ const swap = work[column] ?? [];
71
+ work[column] = work[pivotRow] ?? [];
72
+ work[pivotRow] = swap;
73
+ }
74
+ const pivotRowValues = work[column];
75
+ if (pivotRowValues === void 0) throw new Error("internal: pivot row vanished");
76
+ const pivotInverse = gfInv(pivotRowValues[column] ?? 1);
77
+ for (let c = 0; c < 2 * size; c += 1) pivotRowValues[c] = gfMul(pivotRowValues[c] ?? 0, pivotInverse);
78
+ for (let other = 0; other < size; other += 1) {
79
+ if (other === column) continue;
80
+ const factor = work[other]?.[column] ?? 0;
81
+ if (factor === 0) continue;
82
+ const target = work[other];
83
+ const source = work[column];
84
+ if (target === void 0 || source === void 0) throw new Error("internal: row vanished mid-inversion");
85
+ for (let c = 0; c < 2 * size; c += 1) target[c] = (target[c] ?? 0) ^ gfMul(factor, source[c] ?? 0);
86
+ }
87
+ }
88
+ return work.map((row) => row.slice(size));
89
+ }
90
+ async function encodeShards(data, config) {
91
+ validateConfig(config);
92
+ const { dataShards, totalShards } = config;
93
+ const shardLength = Math.ceil(data.length / dataShards);
94
+ const shards = [];
95
+ for (let i = 0; i < totalShards; i += 1) shards.push(new Uint8Array(new ArrayBuffer(shardLength)));
96
+ for (let i = 0; i < dataShards; i += 1) shards[i]?.set(data.subarray(i * shardLength, (i + 1) * shardLength));
97
+ for (let parityIndex = dataShards; parityIndex < totalShards; parityIndex += 1) {
98
+ const coefficients = encodeMatrixRow(parityIndex, dataShards);
99
+ for (let position = 0; position < shardLength; position += 1) {
100
+ let accumulated = 0;
101
+ for (let column = 0; column < dataShards; column += 1) accumulated ^= gfMul(coefficients[column] ?? 0, shards[column]?.[position] ?? 0);
102
+ const parityTarget = shards[parityIndex];
103
+ if (parityTarget !== void 0) parityTarget[position] = accumulated;
104
+ }
105
+ }
106
+ return Promise.resolve(shards);
107
+ }
108
+ async function decodeShards(presented, config, originalLength) {
109
+ validateConfig(config);
110
+ const { dataShards, totalShards } = config;
111
+ for (const entry of presented) if (entry.index < 0 || entry.index >= totalShards) throw new Error(`shard index ${String(entry.index)} is outside 0..${String(totalShards - 1)}`);
112
+ if (presented.length < dataShards) throw new Error(`need at least ${String(dataShards)} shards to reconstruct, got ${String(presented.length)}`);
113
+ const chosen = presented.slice(0, dataShards);
114
+ const firstChosen = chosen[0];
115
+ if (firstChosen === void 0) throw new Error("internal: chosen shard set is empty");
116
+ const shardLength = firstChosen.data.length;
117
+ const recovered = [];
118
+ if (chosen.every((entry) => entry.index < dataShards)) for (let dataIndex = 0; dataIndex < dataShards; dataIndex += 1) {
119
+ const entry = chosen.find((candidate) => candidate.index === dataIndex);
120
+ if (entry === void 0) throw new Error("internal: data shard missing after the all-present check");
121
+ recovered.push(entry.data);
122
+ }
123
+ else {
124
+ const inverse = invertMatrix(chosen.map((entry) => encodeMatrixRow(entry.index, dataShards)));
125
+ for (let dataIndex = 0; dataIndex < dataShards; dataIndex += 1) {
126
+ const row = new Uint8Array(shardLength);
127
+ for (let chosenIndex = 0; chosenIndex < dataShards; chosenIndex += 1) {
128
+ const coefficient = inverse[dataIndex]?.[chosenIndex] ?? 0;
129
+ if (coefficient === 0) continue;
130
+ for (let position = 0; position < shardLength; position += 1) row[position] = (row[position] ?? 0) ^ gfMul(coefficient, chosen[chosenIndex]?.data[position] ?? 0);
131
+ }
132
+ recovered.push(row);
133
+ }
134
+ }
135
+ const totalLength = dataShards * shardLength;
136
+ if (originalLength > totalLength) throw new Error(`originalLength ${String(originalLength)} exceeds the shards' capacity ${String(totalLength)} -- wrong manifest for these shards`);
137
+ const stitched = new Uint8Array(new ArrayBuffer(originalLength));
138
+ for (let position = 0; position < originalLength; position += 1) {
139
+ const shardIndex = Math.floor(position / shardLength);
140
+ const within = position % shardLength;
141
+ const byte = recovered[shardIndex]?.[within];
142
+ if (byte === void 0) throw new Error("shard position out of range while stitching");
143
+ stitched[position] = byte;
144
+ }
145
+ return Promise.resolve(stitched);
146
+ }
147
+ //#endregion
148
+ exports.decodeShards = decodeShards;
149
+ exports.encodeShards = encodeShards;
@@ -0,0 +1,27 @@
1
+ //#region src/domain/erasure-coding.d.ts
2
+ /**
3
+ * K-of-N erasure coding over GF(2^8) for wire-mesh#36's mailbox-opacity case (the shard half of #34's encrypt-then-shard design): a systematic Reed-Solomon construction where the first `dataShards` shards ARE the padded input split into equal runs, and the remaining shards are parity rows of a Vandermonde-style matrix. Any `dataShards` of `totalShards` reconstruct the input -- the property a single mailbox holder must NOT have (it holds one shard and never the epoch key either).
4
+ *
5
+ * Deliberately hand-written rather than a dependency, the same choice the ECIES construction made: the encode format must be byte-identical across this and the Rust implementation, so both pin the SAME vectors against the documented construction rather than trusting two third-party ports to agree. The math is linear algebra over GF(2^8) with the standard 0x11d polynomial (AES's field), not cryptography.
6
+ *
7
+ * The construction, pinned: pad the input with zero bytes to dataShards * ceil(len / dataShards); any shard index below dataShards is that shard's own equal run; parity shard j (0-based within the parity block) has, at each byte position, the GF dot product of the data runs at that position with the coefficient row (alpha^j, alpha^(j+1), ..., alpha^(j+dataShards-1)) where alpha = 0x02 (the field's generator). Decoding: build the dataShards x dataShards submatrix of the full encode matrix selected by the surviving shards' row indices, invert it over the field, and apply it to the surviving shards' bytes. The encode matrix's first dataShards rows are the identity (systematic), so the all-data-shards case needs no inversion at all.
8
+ */
9
+ export interface ShardConfig {
10
+ dataShards: number;
11
+ totalShards: number;
12
+ }
13
+ export declare function encodeShards(data: Uint8Array, config: Readonly<ShardConfig>): Promise<Uint8Array<ArrayBuffer>[]>;
14
+ /**
15
+ * Reconstructs the original bytes from any dataShards of the totalShards
16
+ * produced by encodeShards. originalLength is the manifest's own fact, not
17
+ * derivable from the shards (zero-padding is indistinguishable from a
18
+ * legitimate trailing zero byte), so the caller passes it -- #34's design
19
+ * puts the length in the shard manifest alongside the device/transfer-id list.
20
+ */
21
+ /** One surviving shard as presented for reconstruction: its ORIGINAL position among the encode output (the parity equation's row index), never its position in whichever array the caller collected survivors into -- confusing the two silently reconstructs garbage, which is exactly why the index is explicit rather than implied by array order. */
22
+ export interface PresentedShard {
23
+ index: number;
24
+ data: Uint8Array;
25
+ }
26
+ export declare function decodeShards(presented: readonly PresentedShard[], config: Readonly<ShardConfig>, originalLength: number): Promise<Uint8Array<ArrayBuffer>>;
27
+ //#endregion
@@ -0,0 +1,27 @@
1
+ //#region src/domain/erasure-coding.d.ts
2
+ /**
3
+ * K-of-N erasure coding over GF(2^8) for wire-mesh#36's mailbox-opacity case (the shard half of #34's encrypt-then-shard design): a systematic Reed-Solomon construction where the first `dataShards` shards ARE the padded input split into equal runs, and the remaining shards are parity rows of a Vandermonde-style matrix. Any `dataShards` of `totalShards` reconstruct the input -- the property a single mailbox holder must NOT have (it holds one shard and never the epoch key either).
4
+ *
5
+ * Deliberately hand-written rather than a dependency, the same choice the ECIES construction made: the encode format must be byte-identical across this and the Rust implementation, so both pin the SAME vectors against the documented construction rather than trusting two third-party ports to agree. The math is linear algebra over GF(2^8) with the standard 0x11d polynomial (AES's field), not cryptography.
6
+ *
7
+ * The construction, pinned: pad the input with zero bytes to dataShards * ceil(len / dataShards); any shard index below dataShards is that shard's own equal run; parity shard j (0-based within the parity block) has, at each byte position, the GF dot product of the data runs at that position with the coefficient row (alpha^j, alpha^(j+1), ..., alpha^(j+dataShards-1)) where alpha = 0x02 (the field's generator). Decoding: build the dataShards x dataShards submatrix of the full encode matrix selected by the surviving shards' row indices, invert it over the field, and apply it to the surviving shards' bytes. The encode matrix's first dataShards rows are the identity (systematic), so the all-data-shards case needs no inversion at all.
8
+ */
9
+ export interface ShardConfig {
10
+ dataShards: number;
11
+ totalShards: number;
12
+ }
13
+ export declare function encodeShards(data: Uint8Array, config: Readonly<ShardConfig>): Promise<Uint8Array<ArrayBuffer>[]>;
14
+ /**
15
+ * Reconstructs the original bytes from any dataShards of the totalShards
16
+ * produced by encodeShards. originalLength is the manifest's own fact, not
17
+ * derivable from the shards (zero-padding is indistinguishable from a
18
+ * legitimate trailing zero byte), so the caller passes it -- #34's design
19
+ * puts the length in the shard manifest alongside the device/transfer-id list.
20
+ */
21
+ /** One surviving shard as presented for reconstruction: its ORIGINAL position among the encode output (the parity equation's row index), never its position in whichever array the caller collected survivors into -- confusing the two silently reconstructs garbage, which is exactly why the index is explicit rather than implied by array order. */
22
+ export interface PresentedShard {
23
+ index: number;
24
+ data: Uint8Array;
25
+ }
26
+ export declare function decodeShards(presented: readonly PresentedShard[], config: Readonly<ShardConfig>, originalLength: number): Promise<Uint8Array<ArrayBuffer>>;
27
+ //#endregion
@@ -0,0 +1,147 @@
1
+ //#region src/domain/erasure-coding.ts
2
+ /**
3
+ * K-of-N erasure coding over GF(2^8) for wire-mesh#36's mailbox-opacity case (the shard half of #34's encrypt-then-shard design): a systematic Reed-Solomon construction where the first `dataShards` shards ARE the padded input split into equal runs, and the remaining shards are parity rows of a Vandermonde-style matrix. Any `dataShards` of `totalShards` reconstruct the input -- the property a single mailbox holder must NOT have (it holds one shard and never the epoch key either).
4
+ *
5
+ * Deliberately hand-written rather than a dependency, the same choice the ECIES construction made: the encode format must be byte-identical across this and the Rust implementation, so both pin the SAME vectors against the documented construction rather than trusting two third-party ports to agree. The math is linear algebra over GF(2^8) with the standard 0x11d polynomial (AES's field), not cryptography.
6
+ *
7
+ * The construction, pinned: pad the input with zero bytes to dataShards * ceil(len / dataShards); any shard index below dataShards is that shard's own equal run; parity shard j (0-based within the parity block) has, at each byte position, the GF dot product of the data runs at that position with the coefficient row (alpha^j, alpha^(j+1), ..., alpha^(j+dataShards-1)) where alpha = 0x02 (the field's generator). Decoding: build the dataShards x dataShards submatrix of the full encode matrix selected by the surviving shards' row indices, invert it over the field, and apply it to the surviving shards' bytes. The encode matrix's first dataShards rows are the identity (systematic), so the all-data-shards case needs no inversion at all.
8
+ */
9
+ /** The AES field's irreducible polynomial x^8+x^4+x^3+x+1, the conventional GF(2^8) choice. */
10
+ const GF_POLY = 285;
11
+ /** The bit that appears when a GF(2^8) value overflows a byte and needs the polynomial reduction. */
12
+ const GF_OVERFLOW_BIT = 256;
13
+ /** GF(2^8) caps total shards at 255 (a 256th row would repeat, breaking invertibility). */
14
+ const GF_MAX_SHARDS = 255;
15
+ const gfExp = Array.from({ length: 510 }, () => 0);
16
+ const gfLog = Array.from({ length: 256 }, () => 0);
17
+ {
18
+ let x = 1;
19
+ for (let power = 0; power < GF_MAX_SHARDS; power += 1) {
20
+ gfExp[power] = x;
21
+ gfLog[x] = power;
22
+ x <<= 1;
23
+ if (x & GF_OVERFLOW_BIT) x ^= GF_POLY;
24
+ }
25
+ for (let power = GF_MAX_SHARDS; power < 510; power += 1) gfExp[power] = gfExp[power - GF_MAX_SHARDS] ?? 0;
26
+ }
27
+ function gfMul(a, b) {
28
+ if (a === 0 || b === 0) return 0;
29
+ return gfExp[(gfLog[a] ?? 0) + (gfLog[b] ?? 0)] ?? 0;
30
+ }
31
+ function gfInv(a) {
32
+ return gfExp[GF_MAX_SHARDS - (gfLog[a] ?? 0)] ?? 1;
33
+ }
34
+ function validateConfig(config) {
35
+ const { dataShards, totalShards } = config;
36
+ if (!Number.isInteger(dataShards) || dataShards < 1) throw new Error(`dataShards must be a positive integer, got ${String(dataShards)}`);
37
+ if (!Number.isInteger(totalShards) || totalShards < dataShards) throw new Error(`totalShards must be an integer at least dataShards (${String(dataShards)}), got ${String(totalShards)}`);
38
+ if (totalShards > GF_MAX_SHARDS) throw new Error(`the GF(2^8) field supports at most ${String(GF_MAX_SHARDS)} shards, got ${String(totalShards)}`);
39
+ }
40
+ /** Row i of the encode matrix: identity rows for the data shards, alpha-power rows for parity. */
41
+ /**
42
+ * Row i of the encode matrix: identity rows for the data shards, Cauchy rows for parity. Cauchy, not Vandermonde, because only Cauchy guarantees EVERY square submatrix is invertible -- identity-plus-Vandermonde has singular K-subsets (found the hard way: parity-based reconstruction produced wrong bytes on some subsets). With parity row j carrying c_i = 1/(x_j XOR y_i) where x_j = row and y_i = column (all row/column values distinct across the whole matrix, no parity row index equals any data column index, so no denominator is zero), any K surviving rows reduce -- expanding along the identity rows -- to a square Cauchy submatrix, always invertible.
43
+ */
44
+ function encodeMatrixRow(row, dataShards) {
45
+ const coefficients = Array.from({ length: dataShards }, () => 0);
46
+ if (row < dataShards) {
47
+ coefficients[row] = 1;
48
+ return coefficients;
49
+ }
50
+ for (let column = 0; column < dataShards; column += 1) coefficients[column] = gfInv(row ^ column);
51
+ return coefficients;
52
+ }
53
+ /** Inverts a square matrix over GF(2^8) by Gauss-Jordan with an identity augmentation. */
54
+ function invertMatrix(matrix) {
55
+ const size = matrix.length;
56
+ const work = matrix.map((row, rowIndex) => {
57
+ const augmented = [...row, ...Array.from({ length: size }, () => 0)];
58
+ augmented[size + rowIndex] = 1;
59
+ return augmented;
60
+ });
61
+ for (let column = 0; column < size; column += 1) {
62
+ let pivotRow = -1;
63
+ for (let candidate = column; candidate < size; candidate += 1) if ((work[candidate]?.[column] ?? 0) !== 0) {
64
+ pivotRow = candidate;
65
+ break;
66
+ }
67
+ if (pivotRow === -1) throw new Error("matrix is singular over GF(2^8)");
68
+ if (pivotRow !== column) {
69
+ const swap = work[column] ?? [];
70
+ work[column] = work[pivotRow] ?? [];
71
+ work[pivotRow] = swap;
72
+ }
73
+ const pivotRowValues = work[column];
74
+ if (pivotRowValues === void 0) throw new Error("internal: pivot row vanished");
75
+ const pivotInverse = gfInv(pivotRowValues[column] ?? 1);
76
+ for (let c = 0; c < 2 * size; c += 1) pivotRowValues[c] = gfMul(pivotRowValues[c] ?? 0, pivotInverse);
77
+ for (let other = 0; other < size; other += 1) {
78
+ if (other === column) continue;
79
+ const factor = work[other]?.[column] ?? 0;
80
+ if (factor === 0) continue;
81
+ const target = work[other];
82
+ const source = work[column];
83
+ if (target === void 0 || source === void 0) throw new Error("internal: row vanished mid-inversion");
84
+ for (let c = 0; c < 2 * size; c += 1) target[c] = (target[c] ?? 0) ^ gfMul(factor, source[c] ?? 0);
85
+ }
86
+ }
87
+ return work.map((row) => row.slice(size));
88
+ }
89
+ async function encodeShards(data, config) {
90
+ validateConfig(config);
91
+ const { dataShards, totalShards } = config;
92
+ const shardLength = Math.ceil(data.length / dataShards);
93
+ const shards = [];
94
+ for (let i = 0; i < totalShards; i += 1) shards.push(new Uint8Array(new ArrayBuffer(shardLength)));
95
+ for (let i = 0; i < dataShards; i += 1) shards[i]?.set(data.subarray(i * shardLength, (i + 1) * shardLength));
96
+ for (let parityIndex = dataShards; parityIndex < totalShards; parityIndex += 1) {
97
+ const coefficients = encodeMatrixRow(parityIndex, dataShards);
98
+ for (let position = 0; position < shardLength; position += 1) {
99
+ let accumulated = 0;
100
+ for (let column = 0; column < dataShards; column += 1) accumulated ^= gfMul(coefficients[column] ?? 0, shards[column]?.[position] ?? 0);
101
+ const parityTarget = shards[parityIndex];
102
+ if (parityTarget !== void 0) parityTarget[position] = accumulated;
103
+ }
104
+ }
105
+ return Promise.resolve(shards);
106
+ }
107
+ async function decodeShards(presented, config, originalLength) {
108
+ validateConfig(config);
109
+ const { dataShards, totalShards } = config;
110
+ for (const entry of presented) if (entry.index < 0 || entry.index >= totalShards) throw new Error(`shard index ${String(entry.index)} is outside 0..${String(totalShards - 1)}`);
111
+ if (presented.length < dataShards) throw new Error(`need at least ${String(dataShards)} shards to reconstruct, got ${String(presented.length)}`);
112
+ const chosen = presented.slice(0, dataShards);
113
+ const firstChosen = chosen[0];
114
+ if (firstChosen === void 0) throw new Error("internal: chosen shard set is empty");
115
+ const shardLength = firstChosen.data.length;
116
+ const recovered = [];
117
+ if (chosen.every((entry) => entry.index < dataShards)) for (let dataIndex = 0; dataIndex < dataShards; dataIndex += 1) {
118
+ const entry = chosen.find((candidate) => candidate.index === dataIndex);
119
+ if (entry === void 0) throw new Error("internal: data shard missing after the all-present check");
120
+ recovered.push(entry.data);
121
+ }
122
+ else {
123
+ const inverse = invertMatrix(chosen.map((entry) => encodeMatrixRow(entry.index, dataShards)));
124
+ for (let dataIndex = 0; dataIndex < dataShards; dataIndex += 1) {
125
+ const row = new Uint8Array(shardLength);
126
+ for (let chosenIndex = 0; chosenIndex < dataShards; chosenIndex += 1) {
127
+ const coefficient = inverse[dataIndex]?.[chosenIndex] ?? 0;
128
+ if (coefficient === 0) continue;
129
+ for (let position = 0; position < shardLength; position += 1) row[position] = (row[position] ?? 0) ^ gfMul(coefficient, chosen[chosenIndex]?.data[position] ?? 0);
130
+ }
131
+ recovered.push(row);
132
+ }
133
+ }
134
+ const totalLength = dataShards * shardLength;
135
+ if (originalLength > totalLength) throw new Error(`originalLength ${String(originalLength)} exceeds the shards' capacity ${String(totalLength)} -- wrong manifest for these shards`);
136
+ const stitched = new Uint8Array(new ArrayBuffer(originalLength));
137
+ for (let position = 0; position < originalLength; position += 1) {
138
+ const shardIndex = Math.floor(position / shardLength);
139
+ const within = position % shardLength;
140
+ const byte = recovered[shardIndex]?.[within];
141
+ if (byte === void 0) throw new Error("shard position out of range while stitching");
142
+ stitched[position] = byte;
143
+ }
144
+ return Promise.resolve(stitched);
145
+ }
146
+ //#endregion
147
+ export { decodeShards, encodeShards };
@@ -0,0 +1,111 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_domain_erasure_coding = require("./erasure-coding.cjs");
3
+ let cbor2 = require("cbor2");
4
+ //#region src/domain/shard-manifest.ts
5
+ /**
6
+ * The shard manifest for wire-mesh#36's mailbox-opacity delivery (the composition half of #34's erasure-coded shard design): a small self-certifying-enough record published as an ordinary opaque core/data entry, naming which device holds which shard and how many are needed to reconstruct. The manifest deliberately carries NO key material and NO shard bytes -- a mailbox reading it learns only the distribution topology, and the encrypt-then- shard ordering means even a full set of shards without the epoch key is ciphertext.
7
+ *
8
+ * The entry encoding is plain canonical CDE CBOR (the same discipline every core/data application entry uses), with structural validation on both encode (self-check before publishing) and decode (a malformed or internally inconsistent manifest fails loudly rather than guiding a reader into reconstructing garbage).
9
+ */
10
+ /** The content-type a core/data entry carrying a shard manifest declares. */
11
+ const SHARD_MANIFEST_CONTENT_TYPE = "application/x-wire-mesh-shard-manifest";
12
+ function encodeShardManifest(manifest) {
13
+ validateManifest(manifest);
14
+ return new Uint8Array((0, cbor2.encode)({
15
+ "total-shards": manifest["total-shards"],
16
+ threshold: manifest.threshold,
17
+ "content-type": manifest["content-type"],
18
+ "original-length": manifest["original-length"],
19
+ shards: manifest.shards.map((shard) => ({
20
+ device: shard.device,
21
+ "transfer-id": shard["transfer-id"]
22
+ }))
23
+ }, cbor2.cdeEncodeOptions));
24
+ }
25
+ function decodeShardManifest(entry) {
26
+ const decoded = (0, cbor2.decode)(entry, cbor2.cdeDecodeOptions);
27
+ if (!isStringKeyedRecord(decoded)) throw new Error("shard manifest entry is not a CBOR map");
28
+ const totalShards = decoded["total-shards"];
29
+ const threshold = decoded.threshold;
30
+ const contentType = decoded["content-type"];
31
+ const originalLength = decoded["original-length"];
32
+ const shards = decoded.shards;
33
+ if (typeof totalShards !== "number" || typeof threshold !== "number") throw new Error("shard manifest is missing numeric total-shards/threshold");
34
+ if (typeof contentType !== "string" || typeof originalLength !== "number") throw new Error("shard manifest is missing content-type/original-length");
35
+ if (!Array.isArray(shards)) throw new Error("shard manifest is missing its shards array");
36
+ const manifest = {
37
+ "total-shards": totalShards,
38
+ threshold,
39
+ "content-type": contentType,
40
+ "original-length": originalLength,
41
+ shards: shards.map((raw) => {
42
+ if (!isStringKeyedRecord(raw)) throw new Error("shard manifest entry is malformed");
43
+ const device = raw.device;
44
+ const transferId = raw["transfer-id"];
45
+ if (!(device instanceof Uint8Array) || !(transferId instanceof Uint8Array)) throw new Error("shard manifest location is missing device/transfer-id bytes");
46
+ return {
47
+ device: Uint8Array.from(device),
48
+ "transfer-id": Uint8Array.from(transferId)
49
+ };
50
+ })
51
+ };
52
+ validateManifest(manifest);
53
+ return manifest;
54
+ }
55
+ /** The target for shard `index`, checked rather than asserted -- the length precondition above makes absence a caller bug worth a named error, not a silent undefined device. */
56
+ function targetFor(targets, index, shard) {
57
+ const target = targets[index];
58
+ if (target === void 0) throw new Error(`no target device for shard ${String(index)} (${String(shard.length)} bytes)`);
59
+ return target;
60
+ }
61
+ function isStringKeyedRecord(value) {
62
+ return typeof value === "object" && value !== null && !Array.isArray(value);
63
+ }
64
+ function validateManifest(manifest) {
65
+ if (manifest.shards.length !== manifest["total-shards"]) throw new Error(`shard manifest declares ${String(manifest["total-shards"])} shards but lists ${String(manifest.shards.length)}`);
66
+ if (manifest.threshold < 1 || manifest.threshold > manifest["total-shards"]) throw new Error(`shard manifest threshold ${String(manifest.threshold)} must be between 1 and ${String(manifest["total-shards"])}`);
67
+ }
68
+ /**
69
+ * The sender half of the composition: split `payload` (already encrypted, when the content is secret -- encrypt-then-shard, so a single holder has neither enough pieces nor the key) into shards and build the manifest naming each shard's home. The bulk transfers themselves are the caller's policy (which sessions, when) -- this module owns the bytes and the manifest, the same primitive-vs-policy split data-sync itself draws.
70
+ */
71
+ async function splitForShardedDelivery(payload, options) {
72
+ const { config, contentType, targets, nextTransferId } = options;
73
+ if (targets.length < config.totalShards) throw new Error(`every shard needs a home: ${String(config.totalShards)} shards but only ${String(targets.length)} targets`);
74
+ const shards = await require_domain_erasure_coding.encodeShards(payload, {
75
+ dataShards: config.dataShards,
76
+ totalShards: config.totalShards
77
+ });
78
+ const manifest = {
79
+ "total-shards": config.totalShards,
80
+ threshold: config.dataShards,
81
+ "content-type": contentType,
82
+ "original-length": payload.length,
83
+ shards: shards.map((shard, index) => ({
84
+ device: targetFor(targets, index, shard),
85
+ "transfer-id": nextTransferId(index)
86
+ }))
87
+ };
88
+ validateManifest(manifest);
89
+ return {
90
+ manifest,
91
+ shards,
92
+ manifestEntry: encodeShardManifest(manifest)
93
+ };
94
+ }
95
+ /**
96
+ * The reader half: any `threshold` shards plus the manifest reconstruct the payload -- and decrypt it, when a decryptor is given. Fails closed below the threshold (a mailbox holding fewer shards than the manifest names must not be able to read the content; that is the entire opacity property).
97
+ */
98
+ async function reconstructFromShards(fetched, manifest, options = {}) {
99
+ const reconstructed = await require_domain_erasure_coding.decodeShards(fetched, {
100
+ dataShards: manifest.threshold,
101
+ totalShards: manifest["total-shards"]
102
+ }, manifest["original-length"]);
103
+ if (options.decrypt === void 0) return reconstructed;
104
+ return options.decrypt(reconstructed);
105
+ }
106
+ //#endregion
107
+ exports.SHARD_MANIFEST_CONTENT_TYPE = SHARD_MANIFEST_CONTENT_TYPE;
108
+ exports.decodeShardManifest = decodeShardManifest;
109
+ exports.encodeShardManifest = encodeShardManifest;
110
+ exports.reconstructFromShards = reconstructFromShards;
111
+ exports.splitForShardedDelivery = splitForShardedDelivery;
@@ -0,0 +1,52 @@
1
+ import { D as DeviceId } from "../protocol-gbXtNeOc.cjs";
2
+ import { PresentedShard, ShardConfig } from "./erasure-coding.cjs";
3
+ //#region src/domain/shard-manifest.d.ts
4
+ /** The content-type a core/data entry carrying a shard manifest declares. */
5
+ export declare const SHARD_MANIFEST_CONTENT_TYPE = "application/x-wire-mesh-shard-manifest";
6
+ /** Where one shard lives: the device holding it, and the bulk transfer that delivered it. */
7
+ export interface ShardLocation {
8
+ device: DeviceId;
9
+ "transfer-id": Uint8Array;
10
+ }
11
+ /** The manifest itself: N shards, any `threshold` of which reconstruct `original-length` bytes. */
12
+ export interface ShardManifest {
13
+ "total-shards": number;
14
+ threshold: number;
15
+ /** The reconstructed content's own content-type (already suffixed if encrypted). */
16
+ "content-type": string;
17
+ /** The byte length the K shards reconstruct to -- the fact the shards cannot carry themselves (zero-padding is indistinguishable from a trailing zero byte). */
18
+ "original-length": number;
19
+ shards: ShardLocation[];
20
+ }
21
+ export declare function encodeShardManifest(manifest: Readonly<ShardManifest>): Uint8Array<ArrayBuffer>;
22
+ export declare function decodeShardManifest(entry: Uint8Array): ShardManifest;
23
+ /** How a split names each transfer: the caller owns transfer-id allocation, same convention mintCapabilityToken applies to token-ids. */
24
+ export type NextTransferId = (shardIndex: number) => Uint8Array;
25
+ export interface SplitForShardedDeliveryOptions {
26
+ config: Readonly<ShardConfig>;
27
+ /** The content's own content-type -- already +aes256gcm-suffixed when the payload is encrypted (the usual case: encrypt first, then shard). */
28
+ contentType: string;
29
+ /** One target device per shard, in shard order; every shard needs a home. */
30
+ targets: readonly DeviceId[];
31
+ nextTransferId: NextTransferId;
32
+ }
33
+ export interface ShardedDelivery {
34
+ manifest: ShardManifest;
35
+ /** Shard i goes to targets[i] under transfer-id nextTransferId(i); the manifest records both. */
36
+ shards: Uint8Array[];
37
+ /** The manifest itself, encoded as a publishable core/data entry. */
38
+ manifestEntry: Uint8Array<ArrayBuffer>;
39
+ }
40
+ /**
41
+ * The sender half of the composition: split `payload` (already encrypted, when the content is secret -- encrypt-then-shard, so a single holder has neither enough pieces nor the key) into shards and build the manifest naming each shard's home. The bulk transfers themselves are the caller's policy (which sessions, when) -- this module owns the bytes and the manifest, the same primitive-vs-policy split data-sync itself draws.
42
+ */
43
+ export declare function splitForShardedDelivery(payload: Uint8Array, options: Readonly<SplitForShardedDeliveryOptions>): Promise<ShardedDelivery>;
44
+ export interface ReconstructOptions {
45
+ /** The decryption step, when the reconstructed bytes are ciphertext (the usual case). Applied AFTER reconstruction, never before -- decrypt-then-shard would expose plaintext to every shard holder. */
46
+ decrypt?: (ciphertext: Uint8Array) => Promise<Uint8Array>;
47
+ }
48
+ /**
49
+ * The reader half: any `threshold` shards plus the manifest reconstruct the payload -- and decrypt it, when a decryptor is given. Fails closed below the threshold (a mailbox holding fewer shards than the manifest names must not be able to read the content; that is the entire opacity property).
50
+ */
51
+ export declare function reconstructFromShards(fetched: readonly PresentedShard[], manifest: Readonly<ShardManifest>, options?: Readonly<ReconstructOptions>): Promise<Uint8Array>;
52
+ //#endregion
@@ -0,0 +1,52 @@
1
+ import { D as DeviceId } from "../protocol-gbXtNeOc.mjs";
2
+ import { PresentedShard, ShardConfig } from "./erasure-coding.mjs";
3
+ //#region src/domain/shard-manifest.d.ts
4
+ /** The content-type a core/data entry carrying a shard manifest declares. */
5
+ export declare const SHARD_MANIFEST_CONTENT_TYPE = "application/x-wire-mesh-shard-manifest";
6
+ /** Where one shard lives: the device holding it, and the bulk transfer that delivered it. */
7
+ export interface ShardLocation {
8
+ device: DeviceId;
9
+ "transfer-id": Uint8Array;
10
+ }
11
+ /** The manifest itself: N shards, any `threshold` of which reconstruct `original-length` bytes. */
12
+ export interface ShardManifest {
13
+ "total-shards": number;
14
+ threshold: number;
15
+ /** The reconstructed content's own content-type (already suffixed if encrypted). */
16
+ "content-type": string;
17
+ /** The byte length the K shards reconstruct to -- the fact the shards cannot carry themselves (zero-padding is indistinguishable from a trailing zero byte). */
18
+ "original-length": number;
19
+ shards: ShardLocation[];
20
+ }
21
+ export declare function encodeShardManifest(manifest: Readonly<ShardManifest>): Uint8Array<ArrayBuffer>;
22
+ export declare function decodeShardManifest(entry: Uint8Array): ShardManifest;
23
+ /** How a split names each transfer: the caller owns transfer-id allocation, same convention mintCapabilityToken applies to token-ids. */
24
+ export type NextTransferId = (shardIndex: number) => Uint8Array;
25
+ export interface SplitForShardedDeliveryOptions {
26
+ config: Readonly<ShardConfig>;
27
+ /** The content's own content-type -- already +aes256gcm-suffixed when the payload is encrypted (the usual case: encrypt first, then shard). */
28
+ contentType: string;
29
+ /** One target device per shard, in shard order; every shard needs a home. */
30
+ targets: readonly DeviceId[];
31
+ nextTransferId: NextTransferId;
32
+ }
33
+ export interface ShardedDelivery {
34
+ manifest: ShardManifest;
35
+ /** Shard i goes to targets[i] under transfer-id nextTransferId(i); the manifest records both. */
36
+ shards: Uint8Array[];
37
+ /** The manifest itself, encoded as a publishable core/data entry. */
38
+ manifestEntry: Uint8Array<ArrayBuffer>;
39
+ }
40
+ /**
41
+ * The sender half of the composition: split `payload` (already encrypted, when the content is secret -- encrypt-then-shard, so a single holder has neither enough pieces nor the key) into shards and build the manifest naming each shard's home. The bulk transfers themselves are the caller's policy (which sessions, when) -- this module owns the bytes and the manifest, the same primitive-vs-policy split data-sync itself draws.
42
+ */
43
+ export declare function splitForShardedDelivery(payload: Uint8Array, options: Readonly<SplitForShardedDeliveryOptions>): Promise<ShardedDelivery>;
44
+ export interface ReconstructOptions {
45
+ /** The decryption step, when the reconstructed bytes are ciphertext (the usual case). Applied AFTER reconstruction, never before -- decrypt-then-shard would expose plaintext to every shard holder. */
46
+ decrypt?: (ciphertext: Uint8Array) => Promise<Uint8Array>;
47
+ }
48
+ /**
49
+ * The reader half: any `threshold` shards plus the manifest reconstruct the payload -- and decrypt it, when a decryptor is given. Fails closed below the threshold (a mailbox holding fewer shards than the manifest names must not be able to read the content; that is the entire opacity property).
50
+ */
51
+ export declare function reconstructFromShards(fetched: readonly PresentedShard[], manifest: Readonly<ShardManifest>, options?: Readonly<ReconstructOptions>): Promise<Uint8Array>;
52
+ //#endregion
@@ -0,0 +1,106 @@
1
+ import { decodeShards, encodeShards } from "./erasure-coding.mjs";
2
+ import { cdeDecodeOptions, cdeEncodeOptions, decode, encode } from "cbor2";
3
+ //#region src/domain/shard-manifest.ts
4
+ /**
5
+ * The shard manifest for wire-mesh#36's mailbox-opacity delivery (the composition half of #34's erasure-coded shard design): a small self-certifying-enough record published as an ordinary opaque core/data entry, naming which device holds which shard and how many are needed to reconstruct. The manifest deliberately carries NO key material and NO shard bytes -- a mailbox reading it learns only the distribution topology, and the encrypt-then- shard ordering means even a full set of shards without the epoch key is ciphertext.
6
+ *
7
+ * The entry encoding is plain canonical CDE CBOR (the same discipline every core/data application entry uses), with structural validation on both encode (self-check before publishing) and decode (a malformed or internally inconsistent manifest fails loudly rather than guiding a reader into reconstructing garbage).
8
+ */
9
+ /** The content-type a core/data entry carrying a shard manifest declares. */
10
+ const SHARD_MANIFEST_CONTENT_TYPE = "application/x-wire-mesh-shard-manifest";
11
+ function encodeShardManifest(manifest) {
12
+ validateManifest(manifest);
13
+ return new Uint8Array(encode({
14
+ "total-shards": manifest["total-shards"],
15
+ threshold: manifest.threshold,
16
+ "content-type": manifest["content-type"],
17
+ "original-length": manifest["original-length"],
18
+ shards: manifest.shards.map((shard) => ({
19
+ device: shard.device,
20
+ "transfer-id": shard["transfer-id"]
21
+ }))
22
+ }, cdeEncodeOptions));
23
+ }
24
+ function decodeShardManifest(entry) {
25
+ const decoded = decode(entry, cdeDecodeOptions);
26
+ if (!isStringKeyedRecord(decoded)) throw new Error("shard manifest entry is not a CBOR map");
27
+ const totalShards = decoded["total-shards"];
28
+ const threshold = decoded.threshold;
29
+ const contentType = decoded["content-type"];
30
+ const originalLength = decoded["original-length"];
31
+ const shards = decoded.shards;
32
+ if (typeof totalShards !== "number" || typeof threshold !== "number") throw new Error("shard manifest is missing numeric total-shards/threshold");
33
+ if (typeof contentType !== "string" || typeof originalLength !== "number") throw new Error("shard manifest is missing content-type/original-length");
34
+ if (!Array.isArray(shards)) throw new Error("shard manifest is missing its shards array");
35
+ const manifest = {
36
+ "total-shards": totalShards,
37
+ threshold,
38
+ "content-type": contentType,
39
+ "original-length": originalLength,
40
+ shards: shards.map((raw) => {
41
+ if (!isStringKeyedRecord(raw)) throw new Error("shard manifest entry is malformed");
42
+ const device = raw.device;
43
+ const transferId = raw["transfer-id"];
44
+ if (!(device instanceof Uint8Array) || !(transferId instanceof Uint8Array)) throw new Error("shard manifest location is missing device/transfer-id bytes");
45
+ return {
46
+ device: Uint8Array.from(device),
47
+ "transfer-id": Uint8Array.from(transferId)
48
+ };
49
+ })
50
+ };
51
+ validateManifest(manifest);
52
+ return manifest;
53
+ }
54
+ /** The target for shard `index`, checked rather than asserted -- the length precondition above makes absence a caller bug worth a named error, not a silent undefined device. */
55
+ function targetFor(targets, index, shard) {
56
+ const target = targets[index];
57
+ if (target === void 0) throw new Error(`no target device for shard ${String(index)} (${String(shard.length)} bytes)`);
58
+ return target;
59
+ }
60
+ function isStringKeyedRecord(value) {
61
+ return typeof value === "object" && value !== null && !Array.isArray(value);
62
+ }
63
+ function validateManifest(manifest) {
64
+ if (manifest.shards.length !== manifest["total-shards"]) throw new Error(`shard manifest declares ${String(manifest["total-shards"])} shards but lists ${String(manifest.shards.length)}`);
65
+ if (manifest.threshold < 1 || manifest.threshold > manifest["total-shards"]) throw new Error(`shard manifest threshold ${String(manifest.threshold)} must be between 1 and ${String(manifest["total-shards"])}`);
66
+ }
67
+ /**
68
+ * The sender half of the composition: split `payload` (already encrypted, when the content is secret -- encrypt-then-shard, so a single holder has neither enough pieces nor the key) into shards and build the manifest naming each shard's home. The bulk transfers themselves are the caller's policy (which sessions, when) -- this module owns the bytes and the manifest, the same primitive-vs-policy split data-sync itself draws.
69
+ */
70
+ async function splitForShardedDelivery(payload, options) {
71
+ const { config, contentType, targets, nextTransferId } = options;
72
+ if (targets.length < config.totalShards) throw new Error(`every shard needs a home: ${String(config.totalShards)} shards but only ${String(targets.length)} targets`);
73
+ const shards = await encodeShards(payload, {
74
+ dataShards: config.dataShards,
75
+ totalShards: config.totalShards
76
+ });
77
+ const manifest = {
78
+ "total-shards": config.totalShards,
79
+ threshold: config.dataShards,
80
+ "content-type": contentType,
81
+ "original-length": payload.length,
82
+ shards: shards.map((shard, index) => ({
83
+ device: targetFor(targets, index, shard),
84
+ "transfer-id": nextTransferId(index)
85
+ }))
86
+ };
87
+ validateManifest(manifest);
88
+ return {
89
+ manifest,
90
+ shards,
91
+ manifestEntry: encodeShardManifest(manifest)
92
+ };
93
+ }
94
+ /**
95
+ * The reader half: any `threshold` shards plus the manifest reconstruct the payload -- and decrypt it, when a decryptor is given. Fails closed below the threshold (a mailbox holding fewer shards than the manifest names must not be able to read the content; that is the entire opacity property).
96
+ */
97
+ async function reconstructFromShards(fetched, manifest, options = {}) {
98
+ const reconstructed = await decodeShards(fetched, {
99
+ dataShards: manifest.threshold,
100
+ totalShards: manifest["total-shards"]
101
+ }, manifest["original-length"]);
102
+ if (options.decrypt === void 0) return reconstructed;
103
+ return options.decrypt(reconstructed);
104
+ }
105
+ //#endregion
106
+ export { SHARD_MANIFEST_CONTENT_TYPE, decodeShardManifest, encodeShardManifest, reconstructFromShards, splitForShardedDelivery };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wire-mesh-core",
3
- "version": "1.42.0",
3
+ "version": "1.44.0",
4
4
  "type": "module",
5
5
  "packageManager": "pnpm@12.4.1+sha512.2e81e399d73fe8390dab25e06aa788ab7a5908248d2f5a370f82b481147a6a7a367bf8048f9a6fdb6460f21a66f0542dedb8b94ca2c8723596741920b1656d4c",
6
6
  "repository": {
@@ -110,6 +110,10 @@
110
110
  "import": "./dist/domain/device-id.mjs",
111
111
  "require": "./dist/domain/device-id.cjs"
112
112
  },
113
+ "./domain/erasure-coding": {
114
+ "import": "./dist/domain/erasure-coding.mjs",
115
+ "require": "./dist/domain/erasure-coding.cjs"
116
+ },
113
117
  "./domain/grant-candidates": {
114
118
  "import": "./dist/domain/grant-candidates.mjs",
115
119
  "require": "./dist/domain/grant-candidates.cjs"
@@ -158,6 +162,10 @@
158
162
  "import": "./dist/domain/room-token-verification.mjs",
159
163
  "require": "./dist/domain/room-token-verification.cjs"
160
164
  },
165
+ "./domain/shard-manifest": {
166
+ "import": "./dist/domain/shard-manifest.mjs",
167
+ "require": "./dist/domain/shard-manifest.cjs"
168
+ },
161
169
  "./domain/tokens": {
162
170
  "import": "./dist/domain/tokens.mjs",
163
171
  "require": "./dist/domain/tokens.cjs"