@private.me/xcontinuity 2.1.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/AGENTS.md +123 -0
- package/LICENSE.md +26 -0
- package/MIGRATING.md +77 -0
- package/README.md +601 -0
- package/dist/adjudicator.d.ts +75 -0
- package/dist/adjudicator.js +184 -0
- package/dist/cascade.d.ts +157 -0
- package/dist/cascade.js +323 -0
- package/dist/chronicle.d.ts +76 -0
- package/dist/chronicle.js +173 -0
- package/dist/cjs/adjudicator.js +189 -0
- package/dist/cjs/cascade.js +328 -0
- package/dist/cjs/chronicle.js +178 -0
- package/dist/cjs/enforcement.js +108 -0
- package/dist/cjs/errors.js +72 -0
- package/dist/cjs/index.js +108 -0
- package/dist/cjs/memory-runtime.js +129 -0
- package/dist/cjs/memory-session.js +134 -0
- package/dist/cjs/mission.js +178 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/provenance.js +192 -0
- package/dist/cjs/ratification.js +322 -0
- package/dist/cjs/reverse-xorida.js +506 -0
- package/dist/cjs/session.js +273 -0
- package/dist/cjs/state-serializer.js +300 -0
- package/dist/cjs/store-memory.js +33 -0
- package/dist/cjs/trust.js +133 -0
- package/dist/cjs/types.js +59 -0
- package/dist/enforcement.d.ts +40 -0
- package/dist/enforcement.js +104 -0
- package/dist/errors.d.ts +25 -0
- package/dist/errors.js +68 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +43 -0
- package/dist/memory-runtime.d.ts +36 -0
- package/dist/memory-runtime.js +125 -0
- package/dist/memory-session.d.ts +38 -0
- package/dist/memory-session.js +97 -0
- package/dist/mission.d.ts +68 -0
- package/dist/mission.js +172 -0
- package/dist/provenance.d.ts +54 -0
- package/dist/provenance.js +182 -0
- package/dist/ratification.d.ts +113 -0
- package/dist/ratification.js +317 -0
- package/dist/reverse-xorida.d.ts +174 -0
- package/dist/reverse-xorida.js +490 -0
- package/dist/session.d.ts +102 -0
- package/dist/session.js +269 -0
- package/dist/state-serializer.d.ts +37 -0
- package/dist/state-serializer.js +294 -0
- package/dist/store-memory.d.ts +18 -0
- package/dist/store-memory.js +29 -0
- package/dist/trust.d.ts +76 -0
- package/dist/trust.js +121 -0
- package/dist/types.d.ts +320 -0
- package/dist/types.js +56 -0
- package/llms.txt +43 -0
- package/package.json +125 -0
- package/share1.dat +0 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @private.me/xcontinuity — Reverse-XorIDA Core Crypto Layer
|
|
3
|
+
*
|
|
4
|
+
* The novel primitive: exploits XorIDA's linearity over GF(2) to perform
|
|
5
|
+
* incremental state updates at O(delta) cost instead of re-splitting entire state.
|
|
6
|
+
*
|
|
7
|
+
* Key insight: split(A XOR B) = split(A) XOR split(B)
|
|
8
|
+
*
|
|
9
|
+
* Therefore: newShares = oldShares XOR split(oldPadded XOR newPadded)
|
|
10
|
+
*
|
|
11
|
+
* HMAC must be recomputed fresh for the new state (not derived from delta).
|
|
12
|
+
*/
|
|
13
|
+
import { ok, err } from '@private.me/shared';
|
|
14
|
+
import { splitXorIDA, reconstructXorIDA, nextOddPrime, pkcs7Pad, pkcs7Unpad, generateHMAC, verifyHMAC, } from '@private.me/crypto';
|
|
15
|
+
import { DEFAULT_SPLIT_CONFIG } from './types.js';
|
|
16
|
+
import { continuityError } from './errors.js';
|
|
17
|
+
/**
|
|
18
|
+
* Pad data for XorIDA splitting.
|
|
19
|
+
* Block size = nextOddPrime(n) - 1.
|
|
20
|
+
*/
|
|
21
|
+
export function padForSplit(data, n) {
|
|
22
|
+
const p = nextOddPrime(n);
|
|
23
|
+
const blockSize = p - 1;
|
|
24
|
+
return pkcs7Pad(data, blockSize);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Split a state snapshot into XorIDA threshold shares.
|
|
28
|
+
*
|
|
29
|
+
* Pipeline: pad -> HMAC -> split -> package as StateShare[]
|
|
30
|
+
*
|
|
31
|
+
* @param snapshot - Serialized state with checksum
|
|
32
|
+
* @param config - Split configuration (n, k)
|
|
33
|
+
* @returns SplitState containing all shares
|
|
34
|
+
*/
|
|
35
|
+
export async function splitState(snapshot, config = DEFAULT_SPLIT_CONFIG) {
|
|
36
|
+
try {
|
|
37
|
+
const { n, k } = config;
|
|
38
|
+
if (n < 2 || k < 2 || k > n) {
|
|
39
|
+
return err(continuityError('SPLIT_FAILED', `Invalid config: n=${n}, k=${k}`));
|
|
40
|
+
}
|
|
41
|
+
const padded = padForSplit(snapshot.serializedBytes, n);
|
|
42
|
+
const { key: hmacKey, signature: hmacSignature } = await generateHMAC(padded);
|
|
43
|
+
const rawShares = splitXorIDA(padded, n, k);
|
|
44
|
+
const shares = rawShares.map((data, index) => ({
|
|
45
|
+
stateId: snapshot.stateId,
|
|
46
|
+
index,
|
|
47
|
+
n,
|
|
48
|
+
k,
|
|
49
|
+
data,
|
|
50
|
+
hmacKey,
|
|
51
|
+
hmacSignature,
|
|
52
|
+
}));
|
|
53
|
+
return ok({
|
|
54
|
+
stateId: snapshot.stateId,
|
|
55
|
+
metadata: snapshot.metadata,
|
|
56
|
+
shares,
|
|
57
|
+
n,
|
|
58
|
+
k,
|
|
59
|
+
paddedLength: padded.length,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
const msg = e instanceof Error ? e.message : 'Split failed';
|
|
64
|
+
return err(continuityError('SPLIT_FAILED', msg));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Reconstruct a state snapshot from threshold shares.
|
|
69
|
+
*
|
|
70
|
+
* Pipeline: validate -> reconstruct -> verify HMAC -> unpad -> return snapshot bytes
|
|
71
|
+
*
|
|
72
|
+
* @param shares - Array of k or more StateShares (uses first k)
|
|
73
|
+
* @returns Reconstructed StateSnapshot serialized bytes and checksum
|
|
74
|
+
*/
|
|
75
|
+
export async function reconstructState(shares) {
|
|
76
|
+
try {
|
|
77
|
+
if (shares.length === 0) {
|
|
78
|
+
return err(continuityError('INSUFFICIENT_SHARES', 'No shares provided'));
|
|
79
|
+
}
|
|
80
|
+
const first = shares[0];
|
|
81
|
+
const k = first.k;
|
|
82
|
+
const n = first.n;
|
|
83
|
+
if (shares.length < k) {
|
|
84
|
+
return err(continuityError('INSUFFICIENT_SHARES', `Need ${k} shares, got ${shares.length}`));
|
|
85
|
+
}
|
|
86
|
+
// Verify all shares have same stateId
|
|
87
|
+
const stateId = first.stateId;
|
|
88
|
+
for (const share of shares) {
|
|
89
|
+
if (share.stateId !== stateId) {
|
|
90
|
+
return err(continuityError('INVALID_SHARES', 'Share stateId mismatch'));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Take first k shares
|
|
94
|
+
const subset = shares.slice(0, k);
|
|
95
|
+
const shareData = subset.map(s => s.data);
|
|
96
|
+
const indices = subset.map(s => s.index);
|
|
97
|
+
const reconstructed = reconstructXorIDA(shareData, indices, n, k);
|
|
98
|
+
// Verify HMAC using the key from the first share
|
|
99
|
+
const hmacValid = await verifyHMAC(first.hmacKey, reconstructed, first.hmacSignature);
|
|
100
|
+
if (!hmacValid) {
|
|
101
|
+
return err(continuityError('HMAC_FAILURE', 'HMAC verification failed after reconstruction'));
|
|
102
|
+
}
|
|
103
|
+
// Unpad
|
|
104
|
+
const p = nextOddPrime(n);
|
|
105
|
+
const blockSize = p - 1;
|
|
106
|
+
const unpadResult = pkcs7Unpad(reconstructed, blockSize);
|
|
107
|
+
if (!unpadResult.ok) {
|
|
108
|
+
return err(continuityError('PADDING_ERROR', 'Failed to remove padding after reconstruction'));
|
|
109
|
+
}
|
|
110
|
+
return ok({ serializedBytes: unpadResult.value, stateId });
|
|
111
|
+
}
|
|
112
|
+
catch (e) {
|
|
113
|
+
const msg = e instanceof Error ? e.message : 'Reconstruction failed';
|
|
114
|
+
return err(continuityError('RECONSTRUCT_FAILED', msg));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Compute the byte-level delta between two padded state arrays.
|
|
119
|
+
* Both arrays must be the same length.
|
|
120
|
+
*
|
|
121
|
+
* delta[i] = oldPadded[i] XOR newPadded[i]
|
|
122
|
+
*/
|
|
123
|
+
export function computeDelta(oldPadded, newPadded, fromStateId, toStateId, fromChecksum, toChecksum) {
|
|
124
|
+
if (oldPadded.length !== newPadded.length) {
|
|
125
|
+
return err(continuityError('DELTA_SIZE_MISMATCH', `Padded sizes differ: ${oldPadded.length} vs ${newPadded.length}`));
|
|
126
|
+
}
|
|
127
|
+
const deltaBytes = new Uint8Array(oldPadded.length);
|
|
128
|
+
for (let i = 0; i < oldPadded.length; i++) {
|
|
129
|
+
deltaBytes[i] = oldPadded[i] ^ newPadded[i];
|
|
130
|
+
}
|
|
131
|
+
return ok({ fromStateId, toStateId, deltaBytes, fromChecksum, toChecksum });
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Apply delta shares to old split state via XOR.
|
|
135
|
+
*
|
|
136
|
+
* newShares[i].data = oldShares[i].data XOR deltaShares[i].data
|
|
137
|
+
*
|
|
138
|
+
* @param oldShares - Original shares
|
|
139
|
+
* @param deltaShares - Delta shares (from splitting the delta)
|
|
140
|
+
* @param newStateId - State ID for the new split state
|
|
141
|
+
* @param newMetadata - Metadata for the new state
|
|
142
|
+
* @param newHmacKey - Fresh HMAC key for the new state
|
|
143
|
+
* @param newHmacSignature - Fresh HMAC signature for the new padded state
|
|
144
|
+
*/
|
|
145
|
+
export function applyDeltaShares(oldShares, deltaShares, newStateId, newHmacKey, newHmacSignature) {
|
|
146
|
+
if (oldShares.length !== deltaShares.length) {
|
|
147
|
+
return err(continuityError('INVALID_SHARES', `Share count mismatch: ${oldShares.length} vs ${deltaShares.length}`));
|
|
148
|
+
}
|
|
149
|
+
const newShares = [];
|
|
150
|
+
for (let i = 0; i < oldShares.length; i++) {
|
|
151
|
+
const oldShare = oldShares[i];
|
|
152
|
+
const deltaData = deltaShares[i];
|
|
153
|
+
if (oldShare.data.length !== deltaData.length) {
|
|
154
|
+
return err(continuityError('DELTA_SIZE_MISMATCH', `Share ${i} size mismatch: ${oldShare.data.length} vs ${deltaData.length}`));
|
|
155
|
+
}
|
|
156
|
+
const newData = new Uint8Array(oldShare.data.length);
|
|
157
|
+
for (let j = 0; j < oldShare.data.length; j++) {
|
|
158
|
+
newData[j] = oldShare.data[j] ^ deltaData[j];
|
|
159
|
+
}
|
|
160
|
+
newShares.push({
|
|
161
|
+
stateId: newStateId,
|
|
162
|
+
index: oldShare.index,
|
|
163
|
+
n: oldShare.n,
|
|
164
|
+
k: oldShare.k,
|
|
165
|
+
data: newData,
|
|
166
|
+
hmacKey: newHmacKey,
|
|
167
|
+
hmacSignature: newHmacSignature,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return ok(newShares);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Perform an incremental state update using Reverse-XorIDA.
|
|
174
|
+
*
|
|
175
|
+
* Instead of re-splitting the entire new state:
|
|
176
|
+
* 1. Pad both old and new to the same block-aligned size
|
|
177
|
+
* 2. Compute delta = oldPadded XOR newPadded
|
|
178
|
+
* 3. Split the delta
|
|
179
|
+
* 4. XOR each old share with the corresponding delta share
|
|
180
|
+
* 5. Recompute HMAC fresh for the new padded state
|
|
181
|
+
*
|
|
182
|
+
* Falls back to fresh split when sizes differ significantly
|
|
183
|
+
* (incremental update only works when padded lengths match).
|
|
184
|
+
*
|
|
185
|
+
* @param oldSplitState - Previous split state
|
|
186
|
+
* @param oldSnapshot - Previous state snapshot
|
|
187
|
+
* @param newSnapshot - New state snapshot
|
|
188
|
+
* @returns Updated SplitState with new shares
|
|
189
|
+
*/
|
|
190
|
+
export async function incrementalUpdate(oldSplitState, oldSnapshot, newSnapshot) {
|
|
191
|
+
try {
|
|
192
|
+
const { n, k } = oldSplitState;
|
|
193
|
+
// Pad both to the same block boundary
|
|
194
|
+
const oldPadded = padForSplit(oldSnapshot.serializedBytes, n);
|
|
195
|
+
const newPadded = padForSplit(newSnapshot.serializedBytes, n);
|
|
196
|
+
// If padded lengths differ, fall back to fresh split
|
|
197
|
+
if (oldPadded.length !== newPadded.length) {
|
|
198
|
+
return splitState(newSnapshot, { n, k });
|
|
199
|
+
}
|
|
200
|
+
// Compute delta
|
|
201
|
+
const deltaBytes = new Uint8Array(oldPadded.length);
|
|
202
|
+
for (let i = 0; i < oldPadded.length; i++) {
|
|
203
|
+
deltaBytes[i] = oldPadded[i] ^ newPadded[i];
|
|
204
|
+
}
|
|
205
|
+
// Split the delta
|
|
206
|
+
const deltaShares = splitXorIDA(deltaBytes, n, k);
|
|
207
|
+
// Fresh HMAC for the new padded state
|
|
208
|
+
const { key: newHmacKey, signature: newHmacSignature } = await generateHMAC(newPadded);
|
|
209
|
+
// XOR old shares with delta shares
|
|
210
|
+
const applyResult = applyDeltaShares(oldSplitState.shares, deltaShares, newSnapshot.stateId, newHmacKey, newHmacSignature);
|
|
211
|
+
if (!applyResult.ok)
|
|
212
|
+
return applyResult;
|
|
213
|
+
return ok({
|
|
214
|
+
stateId: newSnapshot.stateId,
|
|
215
|
+
metadata: newSnapshot.metadata,
|
|
216
|
+
shares: applyResult.value,
|
|
217
|
+
n,
|
|
218
|
+
k,
|
|
219
|
+
paddedLength: newPadded.length,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
catch (e) {
|
|
223
|
+
const msg = e instanceof Error ? e.message : 'Incremental update failed';
|
|
224
|
+
return err(continuityError('SPLIT_FAILED', msg));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/* ══════════════════════════════════════════════════════════════════
|
|
228
|
+
* Algebraic Extensions (v2.0.0)
|
|
229
|
+
*
|
|
230
|
+
* All extensions exploit XorIDA's linearity over GF(2):
|
|
231
|
+
* split(A ⊕ B) = split(A) ⊕ split(B)
|
|
232
|
+
*
|
|
233
|
+
* This property enables O(δ) operations on encrypted shares
|
|
234
|
+
* without ever reconstructing the plaintext.
|
|
235
|
+
* ══════════════════════════════════════════════════════════════════ */
|
|
236
|
+
/**
|
|
237
|
+
* Extension 1: Undo / Time-Travel
|
|
238
|
+
*
|
|
239
|
+
* Reverse a delta by re-applying it (XOR is its own inverse in GF(2)).
|
|
240
|
+
* Given shares at state B and the delta A→B, produces shares at state A.
|
|
241
|
+
*
|
|
242
|
+
* Math: shares_A = shares_B ⊕ deltaShares(A→B)
|
|
243
|
+
* because: (A ⊕ Δ) ⊕ Δ = A (XOR self-inverse)
|
|
244
|
+
*/
|
|
245
|
+
export function undoDelta(currentShares, deltaShares, previousStateId, hmacKey, hmacSignature) {
|
|
246
|
+
// Undo is identical to apply — XOR is self-inverse
|
|
247
|
+
return applyDeltaShares(currentShares, deltaShares, previousStateId, hmacKey, hmacSignature);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Extension 2: Branch
|
|
251
|
+
*
|
|
252
|
+
* Fork a split state into an independent branch by copying shares.
|
|
253
|
+
* The branch can diverge independently from the original.
|
|
254
|
+
*
|
|
255
|
+
* @param source - The split state to branch from
|
|
256
|
+
* @param branchStateId - New stateId for the branch
|
|
257
|
+
* @returns Independent copy of the split state
|
|
258
|
+
*/
|
|
259
|
+
export function branchState(source, branchStateId) {
|
|
260
|
+
const branchedShares = source.shares.map(share => ({
|
|
261
|
+
stateId: branchStateId,
|
|
262
|
+
index: share.index,
|
|
263
|
+
n: share.n,
|
|
264
|
+
k: share.k,
|
|
265
|
+
data: new Uint8Array(share.data),
|
|
266
|
+
hmacKey: new Uint8Array(share.hmacKey),
|
|
267
|
+
hmacSignature: new Uint8Array(share.hmacSignature),
|
|
268
|
+
}));
|
|
269
|
+
return {
|
|
270
|
+
stateId: branchStateId,
|
|
271
|
+
metadata: source.metadata,
|
|
272
|
+
shares: branchedShares,
|
|
273
|
+
n: source.n,
|
|
274
|
+
k: source.k,
|
|
275
|
+
paddedLength: source.paddedLength,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Extension 3: Delta Squashing
|
|
280
|
+
*
|
|
281
|
+
* Combine multiple sequential deltas into a single delta.
|
|
282
|
+
* Exploits GF(2) associativity: Δ_total = Δ₁ ⊕ Δ₂ ⊕ ... ⊕ Δₙ
|
|
283
|
+
*
|
|
284
|
+
* @param deltas - Ordered array of deltas to squash (must form a chain)
|
|
285
|
+
* @returns Single squashed delta from first.fromStateId to last.toStateId
|
|
286
|
+
*/
|
|
287
|
+
export function squashDeltas(deltas) {
|
|
288
|
+
if (deltas.length === 0) {
|
|
289
|
+
return err(continuityError('DELTA_SIZE_MISMATCH', 'No deltas to squash'));
|
|
290
|
+
}
|
|
291
|
+
if (deltas.length === 1) {
|
|
292
|
+
return ok(deltas[0]);
|
|
293
|
+
}
|
|
294
|
+
const first = deltas[0];
|
|
295
|
+
const last = deltas[deltas.length - 1];
|
|
296
|
+
// Verify chain continuity
|
|
297
|
+
for (let i = 1; i < deltas.length; i++) {
|
|
298
|
+
if (deltas[i].fromStateId !== deltas[i - 1].toStateId) {
|
|
299
|
+
return err(continuityError('DELTA_SIZE_MISMATCH', `Delta chain broken at index ${i}: expected from=${deltas[i - 1].toStateId}, got from=${deltas[i].fromStateId}`));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// Verify all deltas have the same length
|
|
303
|
+
const len = first.deltaBytes.length;
|
|
304
|
+
for (const d of deltas) {
|
|
305
|
+
if (d.deltaBytes.length !== len) {
|
|
306
|
+
return err(continuityError('DELTA_SIZE_MISMATCH', `Delta size mismatch: expected ${len}, got ${d.deltaBytes.length}`));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// XOR all deltas together
|
|
310
|
+
const squashed = new Uint8Array(len);
|
|
311
|
+
for (const d of deltas) {
|
|
312
|
+
for (let i = 0; i < len; i++) {
|
|
313
|
+
squashed[i] ^= d.deltaBytes[i];
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return ok({
|
|
317
|
+
fromStateId: first.fromStateId,
|
|
318
|
+
toStateId: last.toStateId,
|
|
319
|
+
deltaBytes: squashed,
|
|
320
|
+
fromChecksum: first.fromChecksum,
|
|
321
|
+
toChecksum: last.toChecksum,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Extension 4: Blind Update
|
|
326
|
+
*
|
|
327
|
+
* Apply a delta to shares without seeing the plaintext state.
|
|
328
|
+
* The share holder never reconstructs the full state.
|
|
329
|
+
*
|
|
330
|
+
* Math: newShare_i = oldShare_i ⊕ deltaShare_i
|
|
331
|
+
*
|
|
332
|
+
* @param share - A single share to update
|
|
333
|
+
* @param deltaShareData - The corresponding delta share data
|
|
334
|
+
* @param newStateId - New state identifier
|
|
335
|
+
* @param newHmacKey - Fresh HMAC key
|
|
336
|
+
* @param newHmacSignature - Fresh HMAC signature
|
|
337
|
+
*/
|
|
338
|
+
export function blindUpdateShare(share, deltaShareData, newStateId, newHmacKey, newHmacSignature) {
|
|
339
|
+
if (share.data.length !== deltaShareData.length) {
|
|
340
|
+
return err(continuityError('DELTA_SIZE_MISMATCH', `Share data length ${share.data.length} != delta length ${deltaShareData.length}`));
|
|
341
|
+
}
|
|
342
|
+
const newData = new Uint8Array(share.data.length);
|
|
343
|
+
for (let i = 0; i < share.data.length; i++) {
|
|
344
|
+
newData[i] = share.data[i] ^ deltaShareData[i];
|
|
345
|
+
}
|
|
346
|
+
return ok({
|
|
347
|
+
stateId: newStateId,
|
|
348
|
+
index: share.index,
|
|
349
|
+
n: share.n,
|
|
350
|
+
k: share.k,
|
|
351
|
+
data: newData,
|
|
352
|
+
hmacKey: newHmacKey,
|
|
353
|
+
hmacSignature: newHmacSignature,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Extension 5: Proactive Share Refresh
|
|
358
|
+
*
|
|
359
|
+
* Re-randomize shares while preserving reconstruction correctness.
|
|
360
|
+
* Generates a random mask, splits it, XORs with existing shares.
|
|
361
|
+
* The mask XORs to zero upon reconstruction, so the plaintext is unchanged.
|
|
362
|
+
*
|
|
363
|
+
* @param splitState - Current split state
|
|
364
|
+
* @returns Refreshed split state with re-randomized shares (same plaintext)
|
|
365
|
+
*/
|
|
366
|
+
export async function refreshShares(state) {
|
|
367
|
+
try {
|
|
368
|
+
const { n, k, paddedLength } = state;
|
|
369
|
+
// Generate random mask of the same padded length
|
|
370
|
+
const mask = new Uint8Array(paddedLength);
|
|
371
|
+
crypto.getRandomValues(mask);
|
|
372
|
+
// Split the mask
|
|
373
|
+
const maskShares = splitXorIDA(mask, n, k);
|
|
374
|
+
// XOR each share with its mask share
|
|
375
|
+
const refreshedShares = [];
|
|
376
|
+
for (let i = 0; i < state.shares.length; i++) {
|
|
377
|
+
const share = state.shares[i];
|
|
378
|
+
const maskData = maskShares[i];
|
|
379
|
+
const newData = new Uint8Array(share.data.length);
|
|
380
|
+
for (let j = 0; j < share.data.length; j++) {
|
|
381
|
+
newData[j] = share.data[j] ^ maskData[j];
|
|
382
|
+
}
|
|
383
|
+
refreshedShares.push({
|
|
384
|
+
...share,
|
|
385
|
+
data: newData,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
// Recompute HMAC for the refreshed state
|
|
389
|
+
// Note: the plaintext doesn't change, so we reconstruct and verify
|
|
390
|
+
const refreshedData = refreshedShares.map(s => s.data);
|
|
391
|
+
const indices = refreshedShares.map(s => s.index);
|
|
392
|
+
const reconstructed = reconstructXorIDA(refreshedData.slice(0, k), indices.slice(0, k), n, k);
|
|
393
|
+
const { key: newHmacKey, signature: newHmacSignature } = await generateHMAC(reconstructed);
|
|
394
|
+
const finalShares = refreshedShares.map(s => ({
|
|
395
|
+
...s,
|
|
396
|
+
hmacKey: newHmacKey,
|
|
397
|
+
hmacSignature: newHmacSignature,
|
|
398
|
+
}));
|
|
399
|
+
return ok({
|
|
400
|
+
...state,
|
|
401
|
+
shares: finalShares,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
catch (e) {
|
|
405
|
+
return err(continuityError('SPLIT_FAILED', `Share refresh failed: ${e instanceof Error ? e.message : String(e)}`));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Extension 6: Blind Equality Check
|
|
410
|
+
*
|
|
411
|
+
* Compare two split states for equality without reconstructing plaintext.
|
|
412
|
+
* Computes delta between corresponding shares — if all delta bytes are zero,
|
|
413
|
+
* the states are equal.
|
|
414
|
+
*
|
|
415
|
+
* @param a - First split state
|
|
416
|
+
* @param b - Second split state
|
|
417
|
+
* @returns true if both states encode the same plaintext
|
|
418
|
+
*/
|
|
419
|
+
export function blindEqual(a, b) {
|
|
420
|
+
if (a.n !== b.n || a.k !== b.k) {
|
|
421
|
+
return err(continuityError('INVALID_SHARES', 'Split configs differ'));
|
|
422
|
+
}
|
|
423
|
+
if (a.shares.length !== b.shares.length) {
|
|
424
|
+
return err(continuityError('INVALID_SHARES', 'Share count differs'));
|
|
425
|
+
}
|
|
426
|
+
// Check if all corresponding share data XOR to zero
|
|
427
|
+
for (let i = 0; i < a.shares.length; i++) {
|
|
428
|
+
const aShare = a.shares[i];
|
|
429
|
+
const bShare = b.shares[i];
|
|
430
|
+
if (aShare.data.length !== bShare.data.length) {
|
|
431
|
+
return ok(false);
|
|
432
|
+
}
|
|
433
|
+
for (let j = 0; j < aShare.data.length; j++) {
|
|
434
|
+
if ((aShare.data[j] ^ bShare.data[j]) !== 0) {
|
|
435
|
+
return ok(false);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return ok(true);
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Extension 7: Network-Coded Sync
|
|
443
|
+
*
|
|
444
|
+
* Combine multiple shares during transit using XOR for bandwidth efficiency.
|
|
445
|
+
* The receiver can extract individual shares if they hold the complementary shares.
|
|
446
|
+
*
|
|
447
|
+
* coded = share_i ⊕ share_j
|
|
448
|
+
* share_i = coded ⊕ share_j (if receiver already holds share_j)
|
|
449
|
+
*
|
|
450
|
+
* @param shares - Array of shares to combine
|
|
451
|
+
* @returns Single coded block (XOR of all input shares)
|
|
452
|
+
*/
|
|
453
|
+
export function networkCodeShares(shares) {
|
|
454
|
+
if (shares.length === 0) {
|
|
455
|
+
return err(continuityError('INSUFFICIENT_SHARES', 'No shares to code'));
|
|
456
|
+
}
|
|
457
|
+
const len = shares[0].data.length;
|
|
458
|
+
for (const s of shares) {
|
|
459
|
+
if (s.data.length !== len) {
|
|
460
|
+
return err(continuityError('DELTA_SIZE_MISMATCH', 'Share data lengths differ'));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
const coded = new Uint8Array(len);
|
|
464
|
+
for (const s of shares) {
|
|
465
|
+
for (let i = 0; i < len; i++) {
|
|
466
|
+
coded[i] ^= s.data[i];
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return ok(coded);
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Decode a network-coded block using a held share.
|
|
473
|
+
*
|
|
474
|
+
* If coded = share_i ⊕ share_j, and you hold share_j:
|
|
475
|
+
* share_i = coded ⊕ share_j
|
|
476
|
+
*
|
|
477
|
+
* @param codedBlock - The network-coded block
|
|
478
|
+
* @param heldShare - The share the receiver already holds
|
|
479
|
+
* @returns The extracted share data
|
|
480
|
+
*/
|
|
481
|
+
export function networkDecodeShare(codedBlock, heldShare) {
|
|
482
|
+
if (codedBlock.length !== heldShare.data.length) {
|
|
483
|
+
return err(continuityError('DELTA_SIZE_MISMATCH', 'Coded block and share lengths differ'));
|
|
484
|
+
}
|
|
485
|
+
const decoded = new Uint8Array(codedBlock.length);
|
|
486
|
+
for (let i = 0; i < codedBlock.length; i++) {
|
|
487
|
+
decoded[i] = codedBlock[i] ^ heldShare.data[i];
|
|
488
|
+
}
|
|
489
|
+
return ok(decoded);
|
|
490
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @private.me/xcontinuity — Session Manager
|
|
3
|
+
*
|
|
4
|
+
* Stateful controller combining all layers:
|
|
5
|
+
* - RuntimeMemory for ephemeral in-process state
|
|
6
|
+
* - SessionMemory for XorIDA-persisted state
|
|
7
|
+
* - Chronicle for ordered state history
|
|
8
|
+
*
|
|
9
|
+
* Status transitions: active -> suspended -> active (resume), active -> closed (terminal).
|
|
10
|
+
*/
|
|
11
|
+
import type { Result } from '@private.me/shared';
|
|
12
|
+
import type { AgentState, StateSnapshot, ContinuitySession, SessionConfig } from './types.js';
|
|
13
|
+
import type { ContinuityError } from './errors.js';
|
|
14
|
+
import { Chronicle } from './chronicle.js';
|
|
15
|
+
import type { TrustStore } from './ratification.js';
|
|
16
|
+
import type { MissionGuard } from './mission.js';
|
|
17
|
+
import type { EnforcementLoop } from './enforcement.js';
|
|
18
|
+
/** Extended session config with optional trust substrate. */
|
|
19
|
+
export interface TrustSessionConfig extends SessionConfig {
|
|
20
|
+
/** Optional TrustStore for trust-annotated state management. */
|
|
21
|
+
readonly trustStore?: TrustStore;
|
|
22
|
+
/** Optional MissionGuard for constraint checking on state writes. */
|
|
23
|
+
readonly missionGuard?: MissionGuard;
|
|
24
|
+
/** Optional EnforcementLoop for violation tracking. */
|
|
25
|
+
readonly enforcementLoop?: EnforcementLoop;
|
|
26
|
+
}
|
|
27
|
+
export declare class SessionManager {
|
|
28
|
+
private sessionRecord;
|
|
29
|
+
private state;
|
|
30
|
+
private readonly memory;
|
|
31
|
+
private readonly chronicle;
|
|
32
|
+
private readonly trustStore?;
|
|
33
|
+
private readonly missionGuard?;
|
|
34
|
+
private readonly enforcementLoop?;
|
|
35
|
+
private sequenceCounter;
|
|
36
|
+
private constructor();
|
|
37
|
+
/**
|
|
38
|
+
* Create a new continuity session.
|
|
39
|
+
*
|
|
40
|
+
* @param config - Session configuration (agentId, store, optional splitConfig)
|
|
41
|
+
* @returns SessionManager instance
|
|
42
|
+
*/
|
|
43
|
+
static create(config: SessionConfig | TrustSessionConfig): SessionManager;
|
|
44
|
+
/** Get the current session record. */
|
|
45
|
+
get session(): ContinuitySession;
|
|
46
|
+
/** Get a copy of the current agent state. */
|
|
47
|
+
get currentState(): AgentState;
|
|
48
|
+
/**
|
|
49
|
+
* Merge a partial update into the current state.
|
|
50
|
+
* Only works when session is active.
|
|
51
|
+
*/
|
|
52
|
+
updateState(patch: AgentState): Result<void, ContinuityError>;
|
|
53
|
+
/**
|
|
54
|
+
* Replace the entire current state.
|
|
55
|
+
* Only works when session is active.
|
|
56
|
+
*/
|
|
57
|
+
setState(state: AgentState): Result<void, ContinuityError>;
|
|
58
|
+
/**
|
|
59
|
+
* Persist current state as a XorIDA-split snapshot.
|
|
60
|
+
* Adds a chronicle entry for the snapshot.
|
|
61
|
+
*
|
|
62
|
+
* @param description - Optional human-readable description
|
|
63
|
+
* @param tags - Optional tags for filtering
|
|
64
|
+
* @returns StateSnapshot with stateId for later retrieval
|
|
65
|
+
*/
|
|
66
|
+
snapshot(description?: string, tags?: string[]): Promise<Result<StateSnapshot, ContinuityError>>;
|
|
67
|
+
/**
|
|
68
|
+
* Restore state from a specific snapshot.
|
|
69
|
+
* Sets the current state to the reconstructed state.
|
|
70
|
+
*
|
|
71
|
+
* @param stateId - The state ID to restore
|
|
72
|
+
* @returns Reconstructed AgentState
|
|
73
|
+
*/
|
|
74
|
+
restore(stateId: string): Promise<Result<AgentState, ContinuityError>>;
|
|
75
|
+
/**
|
|
76
|
+
* Restore the latest snapshot.
|
|
77
|
+
* Convenience method for restoring the most recent state.
|
|
78
|
+
*/
|
|
79
|
+
restoreLatest(): Promise<Result<AgentState, ContinuityError>>;
|
|
80
|
+
/**
|
|
81
|
+
* Suspend the session. Persists current state before suspending.
|
|
82
|
+
* Can be resumed later.
|
|
83
|
+
*/
|
|
84
|
+
suspend(): Promise<Result<void, ContinuityError>>;
|
|
85
|
+
/**
|
|
86
|
+
* Resume a suspended session. Restores the latest snapshot.
|
|
87
|
+
*/
|
|
88
|
+
resume(): Promise<Result<AgentState, ContinuityError>>;
|
|
89
|
+
/**
|
|
90
|
+
* Close the session permanently. No further operations allowed.
|
|
91
|
+
*/
|
|
92
|
+
close(): Result<void, ContinuityError>;
|
|
93
|
+
/** Get the chronicle for state history navigation. */
|
|
94
|
+
getChronicle(): Chronicle;
|
|
95
|
+
/** Get the trust store (if configured). */
|
|
96
|
+
getTrustStore(): TrustStore | undefined;
|
|
97
|
+
/** Get the mission guard (if configured). */
|
|
98
|
+
getMissionGuard(): MissionGuard | undefined;
|
|
99
|
+
/** Get the enforcement loop (if configured). */
|
|
100
|
+
getEnforcementLoop(): EnforcementLoop | undefined;
|
|
101
|
+
private updateTimestamp;
|
|
102
|
+
}
|