@ibgib/core-gib 0.1.59 → 0.1.60

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 (42) hide show
  1. package/CHANGELOG.md +9 -1
  2. package/dist/sync/sync-peer/sync-peer-innerspace/sync-peer-innerspace-v1.mjs +1 -1
  3. package/dist/sync/sync-peer/sync-peer-innerspace/sync-peer-innerspace-v1.mjs.map +1 -1
  4. package/dist/sync/sync-peer/sync-peer-types.d.mts +12 -1
  5. package/dist/sync/sync-peer/sync-peer-types.d.mts.map +1 -1
  6. package/dist/sync/sync-peer/sync-peer-v1.d.mts +7 -0
  7. package/dist/sync/sync-peer/sync-peer-v1.d.mts.map +1 -1
  8. package/dist/sync/sync-peer/sync-peer-v1.mjs +43 -1
  9. package/dist/sync/sync-peer/sync-peer-v1.mjs.map +1 -1
  10. package/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-receiver/sync-peer-websocket-receiver-v1.d.mts +1 -0
  11. package/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-receiver/sync-peer-websocket-receiver-v1.d.mts.map +1 -1
  12. package/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-receiver/sync-peer-websocket-receiver-v1.mjs +15 -5
  13. package/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-receiver/sync-peer-websocket-receiver-v1.mjs.map +1 -1
  14. package/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-sender/sync-peer-websocket-sender-v1.d.mts +16 -0
  15. package/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-sender/sync-peer-websocket-sender-v1.d.mts.map +1 -1
  16. package/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-sender/sync-peer-websocket-sender-v1.mjs +223 -79
  17. package/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-sender/sync-peer-websocket-sender-v1.mjs.map +1 -1
  18. package/dist/sync/sync-saga-context/sync-saga-context-helpers.d.mts.map +1 -1
  19. package/dist/sync/sync-saga-context/sync-saga-context-helpers.mjs +41 -2
  20. package/dist/sync/sync-saga-context/sync-saga-context-helpers.mjs.map +1 -1
  21. package/dist/sync/sync-saga-context/sync-saga-context-types.d.mts +4 -0
  22. package/dist/sync/sync-saga-context/sync-saga-context-types.d.mts.map +1 -1
  23. package/dist/sync/sync-saga-coordinator.d.mts +6 -0
  24. package/dist/sync/sync-saga-coordinator.d.mts.map +1 -1
  25. package/dist/sync/sync-saga-coordinator.mjs +57 -1
  26. package/dist/sync/sync-saga-coordinator.mjs.map +1 -1
  27. package/dist/sync/sync-withid.pingpong.respec.mjs +68 -0
  28. package/dist/sync/sync-withid.pingpong.respec.mjs.map +1 -1
  29. package/package.json +1 -1
  30. package/src/sync/docs/security-3b.md +92 -0
  31. package/src/sync/docs/security.md +107 -39
  32. package/src/sync/sync-peer/sync-peer-innerspace/sync-peer-innerspace-v1.mts +1 -1
  33. package/src/sync/sync-peer/sync-peer-types.mts +11 -1
  34. package/src/sync/sync-peer/sync-peer-v1.mts +47 -1
  35. package/src/sync/sync-peer/sync-peer-websocket/README.md +42 -0
  36. package/src/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-receiver/sync-peer-websocket-receiver-v1.mts +14 -5
  37. package/src/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-sender/sync-peer-websocket-sender-v1.mts +242 -78
  38. package/src/sync/sync-saga-context/sync-saga-context-helpers.mts +46 -4
  39. package/src/sync/sync-saga-context/sync-saga-context-types.mts +5 -0
  40. package/src/sync/sync-saga-coordinator.mts +69 -1
  41. package/src/sync/sync-withid.pingpong.respec.mts +74 -1
  42. package/src/sync/docs/ping_pong_plan.md +0 -147
@@ -11,6 +11,7 @@ import { KeystoneStrategyFactory } from '../../../../keystone/strategy/keystone-
11
11
  import { deriveSessionSecret } from '../../../sync-helpers.mjs';
12
12
  import { SyncPeer_V1 } from '../../sync-peer-v1.mjs';
13
13
  import { SyncSagaContextIbGib_V1 } from '../../../sync-saga-context/sync-saga-context-types.mjs';
14
+ import { validateContextAndSagaFrame } from '../../../sync-saga-context/sync-saga-context-helpers.mjs';
14
15
  import { GLOBAL_LOG_A_LOT } from '../../../../core-constants.mjs';
15
16
  import {
16
17
  ConnectSyncPeerWebSocketSenderOpts,
@@ -23,7 +24,17 @@ import {
23
24
  SESSION_KEYSTONE_POLICY,
24
25
  getConnectChallenge
25
26
  } from '../sync-peer-websocket-receiver/sync-websocket-peer-helpers.mjs';
26
- import { SyncWebSocketMsgType } from '../sync-peer-websocket-constants.mjs';
27
+ import { SyncWebSocketMsgType, isSyncWebSocketMsgType, SYNC_WEB_SOCKET_MSG_TYPE_VALID_VALUES } from '../sync-peer-websocket-constants.mjs';
28
+ import { toDto } from '../../../../common/other/ibgib-helper.mjs';
29
+ import { putInSpace, registerNewIbGib } from '../../../../witness/space/space-helper.mjs';
30
+
31
+ /**
32
+ * helper in creating compile-time safety that we're handling all message types.
33
+ */
34
+ function assertUnreachable(x: never): never {
35
+ throw new Error(`Unhandled message type: ${x} (E: e928a3f82cd7469a98ef1bc248a3f826)`);
36
+ }
37
+
27
38
 
28
39
  const logalot = GLOBAL_LOG_A_LOT || true;
29
40
 
@@ -51,6 +62,7 @@ export class SyncPeerWebSocketSender_V1
51
62
  protected activeResolve?: (value: SyncSagaContextIbGib_V1 | undefined) => void;
52
63
  protected activeReject?: (reason: any) => void;
53
64
  protected pendingPayloadsToSend: IbGib_V1[] = [];
65
+ protected handshakeMessageListener?: (event: MessageEvent) => Promise<void>;
54
66
 
55
67
  constructor(
56
68
  initialData: SyncPeerWebSocketSenderData_V1,
@@ -188,63 +200,64 @@ export class SyncPeerWebSocketSender_V1
188
200
  if (logalot) { console.log(`${lc} WebSocket opened. Awaiting challenge...`); }
189
201
  });
190
202
 
191
- ws.addEventListener('message', async (ev) => {
203
+ this.handshakeMessageListener = async (ev) => {
192
204
  try {
193
205
  const msg = JSON.parse(ev.data);
194
- if (logalot) { console.log(`${lc} received frame: ${msg.type}`); }
195
-
196
- if (msg.type === SyncWebSocketMsgType.auth_challenge_init) {
197
- ws.send(JSON.stringify({
198
- type: SyncWebSocketMsgType.auth_init,
199
- sAddr: targetSAddr
200
- }));
201
- } else if (msg.type === SyncWebSocketMsgType.auth_challenge) {
202
- const { challengeUuid, demandedIds } = msg;
203
- if (logalot) { console.log(`${lc} solving demanded challenges: ${demandedIds.join(', ')}`); }
204
-
205
- const keystoneService = new KeystoneService_V1();
206
- const proofFrame = await keystoneService.sign({
207
- latestKeystone: sessionS,
208
- masterSecret: sessionSecret,
209
- poolId: SESSION_KEYSTONE_POLICY.CONNECT_POOL.ID,
210
- requiredChallengeIds: demandedIds,
211
- claim: {
212
- verb: SESSION_KEYSTONE_POLICY.CONNECT_POOL.VERB,
213
- target: challengeUuid
214
- },
215
- metaspace: localMetaspace,
216
- space: localSpace
217
- });
218
-
219
- ws.send(JSON.stringify({
220
- type: SyncWebSocketMsgType.auth_proof,
221
- proofFrame
222
- }));
223
- } else if (msg.type === SyncWebSocketMsgType.auth_ok) {
224
- if (logalot) { console.log(`${lc} WebSocket connect SUCCESS!`); }
225
- isResolved = true;
226
-
227
- // Setup persistent runtime listeners
228
- ws.removeEventListener('message', (() => { }) as any);
229
- ws.addEventListener('message', (event) => this.handleRuntimeMessage(event));
230
-
231
- resolve();
232
- } else if (msg.type === SyncWebSocketMsgType.auth_fail) {
233
- reject(new Error(`Connect failed: ${msg.message}`));
206
+ if (logalot) { console.log(`${lc} received handshake frame: ${msg.type}`); }
207
+
208
+ const msgType = msg.type;
209
+ if (!isSyncWebSocketMsgType(msgType)) {
210
+ const validTypes = SYNC_WEB_SOCKET_MSG_TYPE_VALID_VALUES.join(', ');
211
+ throw new Error(`Unknown message type '${msgType}' received during connection handshake. Valid types are: ${validTypes} (E: e983271bc84f46928e4695be2409826)`);
212
+ }
213
+
214
+ switch (msgType) {
215
+ case SyncWebSocketMsgType.auth_challenge_init:
216
+ await this.handleHandshakeAuthChallengeInit(ws, targetSAddr);
217
+ break;
218
+ case SyncWebSocketMsgType.auth_challenge:
219
+ await this.handleHandshakeAuthChallenge(ws, msg, sessionS, sessionSecret);
220
+ break;
221
+ case SyncWebSocketMsgType.auth_ok:
222
+ isResolved = true;
223
+ this.handleHandshakeAuthOk(ws, resolve);
224
+ break;
225
+ case SyncWebSocketMsgType.auth_fail:
226
+ this.handleHandshakeAuthFail(msg, reject);
227
+ break;
228
+ case SyncWebSocketMsgType.sync_error:
229
+ this.handleHandshakeSyncError(msg, reject);
230
+ break;
231
+ // Protocol violations / unexpected messages during connection handshake
232
+ case SyncWebSocketMsgType.auth_init:
233
+ case SyncWebSocketMsgType.auth_proof:
234
+ case SyncWebSocketMsgType.sync_frame:
235
+ case SyncWebSocketMsgType.sync_frame_response:
236
+ case SyncWebSocketMsgType.sync_frame_authenticated:
237
+ case SyncWebSocketMsgType.sync_frame_response_authenticated:
238
+ case SyncWebSocketMsgType.domain_payload:
239
+ throw new Error(`Unexpected message type '${msgType}' during connection handshake (E: e3f80c68ab2a46c2b1858c8a1e2f8926)`);
240
+ default:
241
+ assertUnreachable(msgType);
234
242
  }
235
243
  } catch (error) {
244
+ console.error(`${lc} ${extractErrorMsg(error)}`);
236
245
  reject(error);
237
246
  }
238
- });
247
+ };
248
+
249
+ ws.addEventListener('message', this.handshakeMessageListener);
239
250
 
240
251
  ws.addEventListener('close', (event) => {
241
252
  if (!isResolved) {
253
+ this.disconnect();
242
254
  reject(new Error(`WebSocket closed before connect completed (code: ${event.code})`));
243
255
  }
244
256
  });
245
257
 
246
258
  ws.addEventListener('error', (err) => {
247
259
  if (!isResolved) {
260
+ this.disconnect();
248
261
  reject(new Error(`WebSocket connection error`));
249
262
  }
250
263
  });
@@ -256,6 +269,36 @@ export class SyncPeerWebSocketSender_V1
256
269
  }
257
270
  }
258
271
 
272
+ /**
273
+ * Cleanly closes the socket and removes the handshake message listener.
274
+ *
275
+ * LOGS AND SWALLOWS ERRORS, DOES NOT THROW
276
+ */
277
+ public disconnect(): void {
278
+ const lc = `${this.lc}[${this.disconnect.name}]`;
279
+ try {
280
+ if (logalot) { console.log(`${lc} starting... (I: 35c4b85fca0c6e9d042f5ff87cde3826)`); }
281
+
282
+ if (this.ws) {
283
+ if (this.handshakeMessageListener) {
284
+ this.ws.removeEventListener('message', this.handshakeMessageListener);
285
+ this.handshakeMessageListener = undefined;
286
+ }
287
+ try {
288
+ this.ws.close();
289
+ } catch (error) {
290
+ console.error(`${lc} failed to close websocket: ${extractErrorMsg(error)}`);
291
+ }
292
+ this.ws = undefined;
293
+ }
294
+ } catch (error) {
295
+ console.error(`${lc} ${extractErrorMsg(error)}`);
296
+ // throw error; // disconnect does NOT rethrow
297
+ } finally {
298
+ if (logalot) { console.log(`${lc} complete.`); }
299
+ }
300
+ }
301
+
259
302
  /**
260
303
  * Handles synchronizing messages and evolved context frames during active transaction turns.
261
304
  */
@@ -265,49 +308,170 @@ export class SyncPeerWebSocketSender_V1
265
308
  const msg = JSON.parse(event.data);
266
309
  if (logalot) { console.log(`${lc} received runtime frame: ${msg.type}`); }
267
310
 
268
- if (msg.type === SyncWebSocketMsgType.sync_frame_response) {
269
- const responseContext = msg.context as SyncSagaContextIbGib_V1;
270
-
271
- // Validate and authenticate Bob's response context first
272
- await this.authenticateAndValidate({ context: responseContext });
273
-
274
- // If response has expected payloads, authorize Bob to stream them
275
- const expectedPayloadAddrs = responseContext.data?.['@payloadAddrsDomain'] || [];
276
- if (expectedPayloadAddrs.length > 0) {
277
- this.ws!.send(JSON.stringify({
278
- type: SyncWebSocketMsgType.sync_frame_response_authenticated,
279
- contextAddr: getIbGibAddr({ ibGib: responseContext })
280
- }));
281
- }
311
+ const msgType = msg.type;
312
+ if (!isSyncWebSocketMsgType(msgType)) {
313
+ const validTypes = SYNC_WEB_SOCKET_MSG_TYPE_VALID_VALUES.join(', ');
314
+ throw new Error(`Unknown message type '${msgType}' received during active sync saga loop. Valid types are: ${validTypes} (E: e23a8d10b74d47fb90518f8e3f4b826)`);
315
+ }
282
316
 
283
- if (this.activeResolve) {
284
- const resolve = this.activeResolve;
285
- this.activeResolve = undefined;
286
- this.activeReject = undefined;
287
- resolve(responseContext);
288
- }
289
- } else if (msg.type === SyncWebSocketMsgType.sync_frame_authenticated) {
290
- // Bob authenticated our context, stream buffered payloads
291
- const payloads = this.pendingPayloadsToSend || [];
292
- this.pendingPayloadsToSend = [];
293
- for (const ibGib of payloads) {
294
- this.ws!.send(JSON.stringify({
295
- type: SyncWebSocketMsgType.domain_payload,
296
- ibGib
297
- }));
298
- }
299
- } else if (msg.type === SyncWebSocketMsgType.domain_payload) {
300
- const payload = msg.ibGib as IbGib_V1;
301
- this.payloadIbGibsDomainReceived$.next(payload);
317
+ switch (msgType) {
318
+ case SyncWebSocketMsgType.sync_frame_response:
319
+ await this.handleRuntimeSyncFrameResponse(msg);
320
+ break;
321
+ case SyncWebSocketMsgType.sync_frame_authenticated:
322
+ this.handleRuntimeSyncFrameAuthenticated();
323
+ break;
324
+ case SyncWebSocketMsgType.domain_payload:
325
+ this.handleRuntimeDomainPayload(msg);
326
+ break;
327
+ case SyncWebSocketMsgType.sync_error:
328
+ this.handleRuntimeSyncError(msg);
329
+ break;
330
+ // Handshake messages / unexpected messages during active sync saga loop
331
+ case SyncWebSocketMsgType.auth_challenge_init:
332
+ case SyncWebSocketMsgType.auth_init:
333
+ case SyncWebSocketMsgType.auth_challenge:
334
+ case SyncWebSocketMsgType.auth_proof:
335
+ case SyncWebSocketMsgType.auth_ok:
336
+ case SyncWebSocketMsgType.auth_fail:
337
+ case SyncWebSocketMsgType.sync_frame:
338
+ case SyncWebSocketMsgType.sync_frame_response_authenticated:
339
+ throw new Error(`Unexpected message type '${msgType}' during active sync saga loop (E: e982b12cf92c448bbad0e84b7263c826)`);
340
+ default:
341
+ assertUnreachable(msgType);
302
342
  }
303
343
  } catch (error) {
304
- console.error(`${lc} failed parsing runtime frame: ${extractErrorMsg(error)}`);
344
+ console.error(`${lc} failed parsing/handling runtime frame: ${extractErrorMsg(error)}`);
345
+ this.disconnect();
305
346
  if (this.activeReject) {
306
- this.activeReject(error);
347
+ const reject = this.activeReject;
348
+ this.activeResolve = undefined;
349
+ this.activeReject = undefined;
350
+ reject(error);
307
351
  }
308
352
  }
309
353
  }
310
354
 
355
+ protected async handleHandshakeAuthChallengeInit(ws: WebSocket, targetSAddr: string): Promise<void> {
356
+ ws.send(JSON.stringify({
357
+ type: SyncWebSocketMsgType.auth_init,
358
+ sAddr: targetSAddr
359
+ }));
360
+ }
361
+
362
+ protected async handleHandshakeAuthChallenge(
363
+ ws: WebSocket,
364
+ msg: any,
365
+ sessionS: KeystoneIbGib_V1,
366
+ sessionSecret: string
367
+ ): Promise<void> {
368
+ const { challengeUuid, demandedIds } = msg;
369
+ if (logalot) { console.log(`${this.lc} solving demanded challenges: ${demandedIds.join(', ')}`); }
370
+
371
+ const proofFrame = await this.signContextConnect({
372
+ challengeUuid,
373
+ demandedIds
374
+ });
375
+
376
+ if (!proofFrame) {
377
+ throw new Error(`Failed to sign connect challenge proof (E: e9807f28ae9248bbadd0a7f1a8e9826)`);
378
+ }
379
+
380
+ ws.send(JSON.stringify({
381
+ type: SyncWebSocketMsgType.auth_proof,
382
+ proofFrame
383
+ }));
384
+ }
385
+
386
+ protected handleHandshakeAuthOk(ws: WebSocket, resolve: () => void): void {
387
+ if (logalot) { console.log(`${this.lc} WebSocket connect SUCCESS!`); }
388
+ if (this.handshakeMessageListener) {
389
+ ws.removeEventListener('message', this.handshakeMessageListener);
390
+ this.handshakeMessageListener = undefined;
391
+ }
392
+ ws.addEventListener('message', (event) => this.handleRuntimeMessage(event));
393
+ resolve();
394
+ }
395
+
396
+ protected handleHandshakeAuthFail(msg: any, reject: (err: Error) => void): void {
397
+ this.disconnect();
398
+ reject(new Error(`Handshake auth failed: ${msg.message || 'Unknown fail reason'} (E: f380b271a2be498db257bc8209fa8926)`));
399
+ }
400
+
401
+ protected handleHandshakeSyncError(msg: any, reject: (err: Error) => void): void {
402
+ this.disconnect();
403
+ reject(new Error(`Handshake sync error: ${msg.message || msg.error || 'Unknown sync error'} (E: f9208a01fe434cbbad83f210ea8f3426)`));
404
+ }
405
+
406
+ protected async handleRuntimeSyncFrameResponse(msg: any): Promise<void> {
407
+ const responseContext = msg.context as SyncSagaContextIbGib_V1;
408
+
409
+ const validationErrors = await validateContextAndSagaFrame({ context: responseContext });
410
+ if (validationErrors.length > 0) {
411
+ throw new Error(`Invalid response context received: ${validationErrors.join(', ')} (E: d7b5a283cf4c43ba8659c803800cf826)`);
412
+ }
413
+
414
+ // Put response control ibgibs into durable space immediately
415
+ const allControlIbGibs: IbGib_V1[] = [
416
+ toDto({ ibGib: responseContext }),
417
+ responseContext.sagaFrame,
418
+ responseContext.sagaFrameMsg!
419
+ ];
420
+ if (responseContext.signedSessionIdentity) {
421
+ allControlIbGibs.push(responseContext.signedSessionIdentity);
422
+ }
423
+
424
+ const { localSpace } = this.opts!;
425
+ for (const ibGib of allControlIbGibs) {
426
+ await putInSpace({ space: localSpace, ibGibs: [ibGib] });
427
+ await registerNewIbGib({ space: localSpace, ibGib });
428
+ }
429
+
430
+ const expectedPayloadAddrs = responseContext.data?.['@payloadAddrsDomain'] || [];
431
+ if (expectedPayloadAddrs.length > 0) {
432
+ this.ws!.send(JSON.stringify({
433
+ type: SyncWebSocketMsgType.sync_frame_response_authenticated,
434
+ contextAddr: getIbGibAddr({ ibGib: responseContext })
435
+ }));
436
+ }
437
+
438
+ if (this.activeResolve) {
439
+ const resolve = this.activeResolve;
440
+ this.activeResolve = undefined;
441
+ this.activeReject = undefined;
442
+ resolve(responseContext);
443
+ }
444
+ }
445
+
446
+ protected handleRuntimeSyncFrameAuthenticated(): void {
447
+ const payloads = this.pendingPayloadsToSend || [];
448
+ this.pendingPayloadsToSend = [];
449
+ for (const ibGib of payloads) {
450
+ this.ws!.send(JSON.stringify({
451
+ type: SyncWebSocketMsgType.domain_payload,
452
+ ibGib
453
+ }));
454
+ }
455
+ }
456
+
457
+ protected handleRuntimeDomainPayload(msg: any): void {
458
+ const payload = msg.ibGib as IbGib_V1;
459
+ this.payloadIbGibsDomainReceived$.next(payload);
460
+ }
461
+
462
+ protected handleRuntimeSyncError(msg: any): void {
463
+ const errorDetail = msg.message || msg.error || 'Unknown sync runtime error';
464
+ const error = new Error(`Sync runtime error from receiver: ${errorDetail} (E: e983bc1c828d447fa0581da2b8004f26)`);
465
+ if (this.activeReject) {
466
+ const reject = this.activeReject;
467
+ this.activeResolve = undefined;
468
+ this.activeReject = undefined;
469
+ reject(error);
470
+ } else {
471
+ throw error;
472
+ }
473
+ }
474
+
311
475
  /**
312
476
  * Serializes the transaction context and payload down the active WebSocket connection.
313
477
  */
@@ -2,14 +2,14 @@
2
2
  * @module sync saga context helpers
3
3
  */
4
4
 
5
- import { extractErrorMsg, } from '@ibgib/helper-gib/dist/helpers/utils-helper.mjs';
5
+ import { extractErrorMsg, pretty, } from '@ibgib/helper-gib/dist/helpers/utils-helper.mjs';
6
6
  import { getIbGibAddr, } from '@ibgib/ts-gib/dist/helper.mjs';
7
7
  import { Ib, } from '@ibgib/ts-gib/dist/types.mjs';
8
8
  import { validateIbGibIntrinsically } from '@ibgib/ts-gib/dist/V1/validate-helper.mjs';
9
9
 
10
10
  import { GLOBAL_LOG_A_LOT } from '../../core-constants.mjs';
11
11
  import { SYNC_SAGA_CONTEXT_ATOM } from './sync-saga-context-constants.mjs';
12
- import { SYNC_SAGA_PAYLOAD_ADDRS_DOMAIN } from '../sync-constants.mjs';
12
+ import { SYNC_SAGA_PAYLOAD_ADDRS_DOMAIN, SYNC_MSG_REL8N_NAME } from '../sync-constants.mjs';
13
13
  import {
14
14
  SyncSagaContextData_V1, SyncSagaContextIbGib_V1, SyncSagaContextIb_V1,
15
15
  } from './sync-saga-context-types.mjs';
@@ -17,9 +17,9 @@ import { IbGibSpaceAny } from '../../witness/space/space-base-v1.mjs';
17
17
  import { getFromSpace, getLatestAddrs, getTjpIbGib } from '../../witness/space/space-helper.mjs';
18
18
  import { SessionGenesisFrameDetails, } from '../sync-types.mjs';
19
19
  import { validateSyncSagaFrame } from '../sync-helpers.mjs';
20
- import { isIbGibWithAtom } from '../../common/other/ibgib-helper.mjs';
20
+ import { isIbGibWithAtom, toDto } from '../../common/other/ibgib-helper.mjs';
21
21
  import { KeystoneService_V1 } from '../../keystone/keystone-service-v1.mjs';
22
- import { KeystoneIbGib_V1 } from '../../keystone/keystone-types.mjs';
22
+ import { KeystoneData_V1, KeystoneIbGib_V1 } from '../../keystone/keystone-types.mjs';
23
23
  import { KEYSTONE_VERB_SYNC } from '../../keystone/keystone-constants.mjs';
24
24
 
25
25
  const logalot = GLOBAL_LOG_A_LOT;
@@ -150,6 +150,26 @@ export async function validateContextAndSagaFrame({
150
150
  errors.push(`context.sagaFrame is falsy. (E: b4edd88f4963f493789f83b29ba2df26)`);
151
151
  }
152
152
 
153
+ if (context.sagaFrameMsg) {
154
+ const sagaFrameMsgErrors =
155
+ await validateIbGibIntrinsically({ ibGib: context.sagaFrameMsg }) ?? [];
156
+ sagaFrameMsgErrors.forEach(x => errors.push(x));
157
+
158
+ if (context.sagaFrame) {
159
+ const expectedMsgAddr = context.sagaFrame.rel8ns?.[SYNC_MSG_REL8N_NAME]?.[0];
160
+ if (expectedMsgAddr) {
161
+ const actualMsgAddr = getIbGibAddr({ ibGib: context.sagaFrameMsg });
162
+ if (actualMsgAddr !== expectedMsgAddr) {
163
+ errors.push(`context.sagaFrameMsg address (${actualMsgAddr}) does not match the stone address referenced in sagaFrame relations (${expectedMsgAddr}). (E: a983b271fcae46bbad7e82098bc24826)`);
164
+ }
165
+ } else {
166
+ errors.push(`context.sagaFrame is missing the message stone relation '${SYNC_MSG_REL8N_NAME}'. (E: da872cf3a8d46dbbad89d0a68d712826)`);
167
+ }
168
+ }
169
+ } else {
170
+ errors.push(`context.sagaFrameMsg is falsy. (E: ed405a72ab0d8bbdca7b9605d8f9a26)`);
171
+ }
172
+
153
173
  // if this is already invalid, we could have intrinsic validation
154
174
  // errors, which are a non-starter.
155
175
  if (errors.length > 0) { return errors; /* <<<< returns early */ }
@@ -301,6 +321,28 @@ export async function authenticateContextIntrinsically({
301
321
  } else {
302
322
  // debugger; // in sync saga context auth, want to know if this hits...so far this does NOT hit
303
323
  errors.push(`context.rel8ns.sessionIdentity does not point to the most recent in the space (${space.ib}). (E: 2f8288f53c87b6aa47bd2178d9df0c26)`)
324
+
325
+ // #region debug error keystone
326
+ console.log(`context: ${pretty(toDto({ ibGib: context }))}`)
327
+ console.log(`prevSessionIdentityAddr (context.rel8ns.sessionIdentity): ${prevSessionIdentityAddr}`)
328
+ console.log(`prevSessionIdentityAddr_latest: ${prevSessionIdentityAddr_latest}`)
329
+ console.log(`currSessionIdentity (context.signedSessionIdentity): ${pretty(toDto({
330
+ ibGib: {
331
+ ib: currSessionIdentity.ib,
332
+ gib: currSessionIdentity.gib,
333
+ rel8ns: currSessionIdentity.rel8ns,
334
+ data: {
335
+ ...currSessionIdentity.data,
336
+ challengePools: currSessionIdentity.data.challengePools.map(p => {
337
+ return { ...p, challenges: {} }
338
+ }),
339
+ } satisfies Partial<KeystoneData_V1>,
340
+
341
+ }
342
+ }))}`)
343
+ console.log(`currSessionIdentityAddr: ${currSessionIdentityAddr}`)
344
+
345
+ // #endregion debug error keystone
304
346
  return errors; /* <<<< returns early */
305
347
  }
306
348
  }
@@ -79,4 +79,9 @@ export interface SyncSagaContextIbGib_V1 extends IbGib_V1<SyncSagaContextData_V1
79
79
  * Evolved session identity frame signed by Alice targeting this context.
80
80
  */
81
81
  signedSessionIdentity?: KeystoneIbGib_V1;
82
+
83
+ /**
84
+ * The actual message stone (the sync saga message containing state/stage).
85
+ */
86
+ sagaFrameMsg: IbGib_V1;
82
87
  }
@@ -32,7 +32,7 @@ import {
32
32
  HandleSagaResponseContextResult,
33
33
  SyncSagaFrameDependencyGraph,
34
34
  } from "./sync-types.mjs";
35
- import { getSyncSagaFrameOrigin, getFullSyncSagaHistory, getSyncIb, getTempSpaceName, isPastFrame, putInSpace_dnasThenNonDnas, validateFullSyncSagaHistory, getAllOrphanedAddresses, getFinalConflictsInfo } from "./sync-helpers.mjs";
35
+ import { getSyncSagaFrameOrigin, getFullSyncSagaHistory, getSyncIb, getTempSpaceName, isPastFrame, putInSpace_dnasThenNonDnas, validateFullSyncSagaHistory, getAllOrphanedAddresses, getFinalConflictsInfo, getSyncSagaFrameDependencyGraph } from "./sync-helpers.mjs";
36
36
  import { getDeltaDependencyGraph, getDependencyGraph, toFlatGraph } from "../common/other/graph-helper.mjs";
37
37
  import {
38
38
  SyncSagaMessageData_V1, SyncSagaMessageInitData_V1,
@@ -533,6 +533,16 @@ export class SyncSagaCoordinator {
533
533
  await validateContextAndSagaFrame({ context: responseCtx });
534
534
  if (contextAndSagaFrameValidationErrors.length > 0) { throw new Error(`contextAndSagaFrameValidationErrors: ${contextAndSagaFrameValidationErrors} (E: 6eebe8e7fa437c00a8cde3ada3c66826)`); }
535
535
 
536
+ // Turn-by-turn continuation & identity checks
537
+ const returnContextErrors = await this.validateReturnContext({
538
+ requestCtx,
539
+ responseCtx,
540
+ localSpace,
541
+ });
542
+ if (returnContextErrors.length > 0) {
543
+ throw new Error(`validateReturnContext errors: ${returnContextErrors.join(', ')} (E: cb8a023b9d0728cceb09fa3da0bb8226)`);
544
+ }
545
+
536
546
  // Extract expected domain addresses from response context
537
547
  const responsePayloadAddrsDomain = responseCtx.data[SYNC_SAGA_PAYLOAD_ADDRS_DOMAIN] as string[] || [];
538
548
 
@@ -738,6 +748,16 @@ export class SyncSagaCoordinator {
738
748
 
739
749
  // Attach actual ibgibs for transport
740
750
  contextIbGib.sagaFrame = sagaFrame;
751
+
752
+ const { msgStones } = await getSyncSagaFrameDependencyGraph({
753
+ sagaIbGib: sagaFrame,
754
+ localSpace: localSpace,
755
+ });
756
+ if (msgStones.length !== 1) {
757
+ throw new Error(`(UNEXPECTED) msgStones.length !== 1 inside createSyncSagaContext? (E: a98165cf46ab4e82b7bd5e45a273b826)`);
758
+ }
759
+ contextIbGib.sagaFrameMsg = msgStones[0];
760
+
741
761
  if (payloadIbGibsDomain && payloadIbGibsDomain.length > 0) {
742
762
  contextIbGib.payloadIbGibsDomain = payloadIbGibsDomain;
743
763
  }
@@ -942,6 +962,54 @@ export class SyncSagaCoordinator {
942
962
  }
943
963
  }
944
964
 
965
+ /**
966
+ * Validates that the return context received from a peer is a valid
967
+ * continuation of the outbound context and that the session identity
968
+ * is consistent.
969
+ */
970
+ private async validateReturnContext({
971
+ requestCtx,
972
+ responseCtx,
973
+ localSpace,
974
+ }: {
975
+ requestCtx: SyncSagaContextIbGib_V1,
976
+ responseCtx: SyncSagaContextIbGib_V1,
977
+ localSpace: IbGibSpaceAny,
978
+ }): Promise<string[]> {
979
+ const lc = `${this.lc}[${this.validateReturnContext.name}]`;
980
+ const errors: string[] = [];
981
+ try {
982
+ // 1. Verify saga frame continuation
983
+ if (requestCtx.sagaFrame && responseCtx.sagaFrame) {
984
+ const requestFrameAddr = getIbGibAddr({ ibGib: requestCtx.sagaFrame });
985
+ const responseFrameAddr = getIbGibAddr({ ibGib: responseCtx.sagaFrame });
986
+
987
+ const isContinuation = await isPastFrame({
988
+ olderAddr: requestFrameAddr,
989
+ newerAddr: responseFrameAddr,
990
+ space: localSpace,
991
+ });
992
+ if (!isContinuation) {
993
+ errors.push(`Response saga frame (${responseFrameAddr}) is not a valid continuation of request saga frame (${requestFrameAddr}). (E: 2c85e8d97318ff24ac8a02bd3a068226)`);
994
+ }
995
+ } else {
996
+ errors.push(`Missing sagaFrame on requestCtx or responseCtx. (E: b65c68ff891000ddca8d22384a088226)`);
997
+ }
998
+
999
+ // 2. Verify session identity has not changed
1000
+ const expectedSessionIdAddr = requestCtx.signedSessionIdentity ?
1001
+ getIbGibAddr({ ibGib: requestCtx.signedSessionIdentity }) :
1002
+ requestCtx.rel8ns?.sessionIdentity?.[0];
1003
+ const responseSessionIdAddr = responseCtx.rel8ns?.sessionIdentity?.[0];
1004
+ if (expectedSessionIdAddr !== responseSessionIdAddr) {
1005
+ errors.push(`Session identity mismatch. Expected ${expectedSessionIdAddr}, got ${responseSessionIdAddr}. (E: ab98716bca88d2243cc822187768226)`);
1006
+ }
1007
+ } catch (error) {
1008
+ errors.push(`Error during validateReturnContext: ${extractErrorMsg(error)} (E: da878e1239aa88ee27bdfca005c28226)`);
1009
+ }
1010
+ return errors;
1011
+ }
1012
+
945
1013
  /**
946
1014
  * Helper to poll for streaming domain payloads and put them in the
947
1015
  * local {@link tempSpace}.
@@ -34,7 +34,8 @@ import { IbGibAddr } from '@ibgib/ts-gib/dist/types.mjs';
34
34
  import { getIdentity_throwIfUndefined } from '../keystone/keystone-helpers.mjs';
35
35
  import { Factory_V1 } from '@ibgib/ts-gib/dist/V1/factory.mjs';
36
36
  import { IbGib_V1 } from '@ibgib/ts-gib/dist/V1/types.mjs';
37
- import { getDependencyGraph } from '../common/other/graph-helper.mjs';
37
+ import { getDependencyGraph, graphsAreEquivalent } from '../common/other/graph-helper.mjs';
38
+ import { getFromSpace } from '../witness/space/space-helper.mjs';
38
39
 
39
40
  const logalot = GLOBAL_LOG_A_LOT;
40
41
  const lc = sir;
@@ -158,4 +159,76 @@ await respecfully(sir, `Test Phase 3A: Ping Pong Sync with Identity`, async () =
158
159
  iReckon(sir, syncError).asTo(`syncError: ${errorMsg}`).isGonnaBeFalsy();
159
160
  });
160
161
 
162
+ await ifWeMight(sir, 'alpha dep graph matches on source and dest', async () => {
163
+ const resGetDest = await getFromSpace({ space: destSpace, addr: xStoneAddr });
164
+ iReckon(sir, resGetDest.success && resGetDest.ibGibs?.length === 1).asTo('xStone exists in destSpace').isGonnaBeTrue();
165
+ if (resGetDest.success && resGetDest.ibGibs?.[0]) {
166
+ const xStoneDest = resGetDest.ibGibs[0];
167
+ const depGraphSource = await getDependencyGraph({ ibGib: xStone, space: sourceSpace });
168
+ const depGraphDest = await getDependencyGraph({ ibGib: xStoneDest, space: destSpace });
169
+ const equal = graphsAreEquivalent({ graphA: depGraphSource, graphB: depGraphDest });
170
+ iReckon(sir, equal).asTo('graphs are equivalent').isGonnaBeTrue();
171
+ }
172
+ });
173
+
174
+ await ifWeMight(sir, 'sessionIdentity evolved the expected number of times', async () => {
175
+ const latestS = peer.currentSessionIdentity;
176
+ iReckon(sir, latestS).asTo('latest session identity exists on peer').isGonnaBeTruthy();
177
+ if (latestS) {
178
+ iReckon(sir, latestS.data?.n).asTo('session identity data.n').isGonnaBe(2);
179
+ }
180
+ });
181
+
182
+ await ifWeMight(sir, 'sender durable space has I (evolved frame)', async () => {
183
+ const latestSenderIdentityAddr = await metaspace.getLatestAddr({
184
+ addr: getIbGibAddr({ ibGib: senderIdentity }),
185
+ space: sourceSpace,
186
+ });
187
+ iReckon(sir, latestSenderIdentityAddr).asTo('latestSenderIdentityAddr exists').isGonnaBeTruthy();
188
+ if (latestSenderIdentityAddr) {
189
+ const resGetSourceI = await getFromSpace({ space: sourceSpace, addr: latestSenderIdentityAddr });
190
+ iReckon(sir, resGetSourceI.success && resGetSourceI.ibGibs?.length === 1).asTo('evolved I exists in sourceSpace').isGonnaBeTrue();
191
+ if (resGetSourceI.success && resGetSourceI.ibGibs?.[0]) {
192
+ const evolvedI = resGetSourceI.ibGibs[0];
193
+ iReckon(sir, evolvedI.data?.n).asTo('evolved I data.n').isGonnaBe(1);
194
+ }
195
+ }
196
+ });
197
+
198
+ await ifWeMight(sir, 'receiver durable space has I (evolved frame)', async () => {
199
+ const latestSenderIdentityAddr = await metaspace.getLatestAddr({
200
+ addr: getIbGibAddr({ ibGib: senderIdentity }),
201
+ space: sourceSpace,
202
+ });
203
+ iReckon(sir, latestSenderIdentityAddr).asTo('latestSenderIdentityAddr exists').isGonnaBeTruthy();
204
+ if (latestSenderIdentityAddr) {
205
+ const resGetDestI = await getFromSpace({ space: destSpace, addr: latestSenderIdentityAddr });
206
+ iReckon(sir, resGetDestI.success && resGetDestI.ibGibs?.length === 1).asTo('evolved I exists in destSpace').isGonnaBeTrue();
207
+ if (resGetDestI.success && resGetDestI.ibGibs?.[0]) {
208
+ const evolvedI = resGetDestI.ibGibs[0];
209
+ iReckon(sir, evolvedI.data?.n).asTo('evolved I data.n').isGonnaBe(1);
210
+ }
211
+ }
212
+ });
213
+
214
+ await ifWeMight(sir, 'sender durable space has full S dep graph', async () => {
215
+ const latestS = peer.currentSessionIdentity;
216
+ iReckon(sir, latestS).asTo('latestS exists').isGonnaBeTruthy();
217
+ if (latestS) {
218
+ const depGraphS = await getDependencyGraph({ ibGib: latestS, space: sourceSpace });
219
+ const addrs = Object.keys(depGraphS);
220
+ iReckon(sir, addrs.length > 2).asTo('number of nodes in S graph > 2').isGonnaBeTrue();
221
+ }
222
+ });
223
+
224
+ await ifWeMight(sir, 'receiver durable space has full S dep graph', async () => {
225
+ const latestS = peer.currentSessionIdentity;
226
+ iReckon(sir, latestS).asTo('latestS exists').isGonnaBeTruthy();
227
+ if (latestS) {
228
+ const depGraphS = await getDependencyGraph({ ibGib: latestS, space: destSpace });
229
+ const addrs = Object.keys(depGraphS);
230
+ iReckon(sir, addrs.length > 2).asTo('number of nodes in S graph in destSpace > 2').isGonnaBeTrue();
231
+ }
232
+ });
233
+
161
234
  });