@ibgib/core-gib 0.1.8 → 0.1.10

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 (80) hide show
  1. package/dist/agent-helpers.d.mts +45 -0
  2. package/dist/agent-helpers.d.mts.map +1 -0
  3. package/dist/agent-helpers.mjs +36 -0
  4. package/dist/agent-helpers.mjs.map +1 -0
  5. package/dist/keystone/keystone-config-builder.respec.d.mts +2 -0
  6. package/dist/keystone/keystone-config-builder.respec.d.mts.map +1 -0
  7. package/dist/keystone/keystone-config-builder.respec.mjs +34 -0
  8. package/dist/keystone/keystone-config-builder.respec.mjs.map +1 -0
  9. package/dist/keystone/keystone-constants.d.mts +2 -0
  10. package/dist/keystone/keystone-constants.d.mts.map +1 -1
  11. package/dist/keystone/keystone-constants.mjs +2 -0
  12. package/dist/keystone/keystone-constants.mjs.map +1 -1
  13. package/dist/keystone/keystone-helpers.d.mts +54 -1
  14. package/dist/keystone/keystone-helpers.d.mts.map +1 -1
  15. package/dist/keystone/keystone-helpers.mjs +185 -1
  16. package/dist/keystone/keystone-helpers.mjs.map +1 -1
  17. package/dist/keystone/keystone-service-v1.d.mts +49 -16
  18. package/dist/keystone/keystone-service-v1.d.mts.map +1 -1
  19. package/dist/keystone/keystone-service-v1.mjs +151 -328
  20. package/dist/keystone/keystone-service-v1.mjs.map +1 -1
  21. package/dist/keystone/keystone-service-v1.respec.mjs +401 -20
  22. package/dist/keystone/keystone-service-v1.respec.mjs.map +1 -1
  23. package/dist/keystone/keystone-types.d.mts +22 -0
  24. package/dist/keystone/keystone-types.d.mts.map +1 -1
  25. package/dist/sync/sync-constants.d.mts +17 -0
  26. package/dist/sync/sync-constants.d.mts.map +1 -0
  27. package/dist/sync/sync-constants.mjs +16 -0
  28. package/dist/sync/sync-constants.mjs.map +1 -0
  29. package/dist/sync/sync-helpers.d.mts +15 -0
  30. package/dist/sync/sync-helpers.d.mts.map +1 -0
  31. package/dist/sync/sync-helpers.mjs +46 -0
  32. package/dist/sync/sync-helpers.mjs.map +1 -0
  33. package/dist/sync/sync-local-spaces.respec.d.mts +2 -0
  34. package/dist/sync/sync-local-spaces.respec.d.mts.map +1 -0
  35. package/dist/sync/sync-local-spaces.respec.mjs +159 -0
  36. package/dist/sync/sync-local-spaces.respec.mjs.map +1 -0
  37. package/dist/sync/sync-saga-coordinator.d.mts +118 -0
  38. package/dist/sync/sync-saga-coordinator.d.mts.map +1 -0
  39. package/dist/sync/sync-saga-coordinator.mjs +399 -0
  40. package/dist/sync/sync-saga-coordinator.mjs.map +1 -0
  41. package/dist/sync/sync-saga-coordinator.respec.d.mts +2 -0
  42. package/dist/sync/sync-saga-coordinator.respec.d.mts.map +1 -0
  43. package/dist/sync/sync-saga-coordinator.respec.mjs +40 -0
  44. package/dist/sync/sync-saga-coordinator.respec.mjs.map +1 -0
  45. package/dist/sync/sync-types.d.mts +103 -0
  46. package/dist/sync/sync-types.d.mts.map +1 -0
  47. package/dist/sync/sync-types.mjs +2 -0
  48. package/dist/sync/sync-types.mjs.map +1 -0
  49. package/dist/test/mock-space.d.mts +39 -0
  50. package/dist/test/mock-space.d.mts.map +1 -0
  51. package/dist/test/mock-space.mjs +79 -0
  52. package/dist/test/mock-space.mjs.map +1 -0
  53. package/dist/witness/space/inner-space/inner-space-v1.respec.mjs +163 -201
  54. package/dist/witness/space/inner-space/inner-space-v1.respec.mjs.map +1 -1
  55. package/dist/witness/space/space-helper.d.mts.map +1 -1
  56. package/dist/witness/space/space-helper.mjs +43 -4
  57. package/dist/witness/space/space-helper.mjs.map +1 -1
  58. package/dist/witness/space/space-helper.respec.d.mts +2 -0
  59. package/dist/witness/space/space-helper.respec.d.mts.map +1 -0
  60. package/dist/witness/space/space-helper.respec.mjs +30 -0
  61. package/dist/witness/space/space-helper.respec.mjs.map +1 -0
  62. package/package.json +2 -2
  63. package/src/agent-helpers.mts +58 -0
  64. package/src/keystone/keystone-config-builder.respec.mts +49 -0
  65. package/src/keystone/keystone-constants.mts +2 -0
  66. package/src/keystone/keystone-helpers.mts +211 -2
  67. package/src/keystone/keystone-service-v1.mts +183 -367
  68. package/src/keystone/keystone-service-v1.respec.mts +484 -21
  69. package/src/keystone/keystone-types.mts +24 -0
  70. package/src/sync/sync-constants.mts +24 -0
  71. package/src/sync/sync-helpers.mts +59 -0
  72. package/src/sync/sync-local-spaces.respec.mts +200 -0
  73. package/src/sync/sync-saga-coordinator.mts +477 -0
  74. package/src/sync/sync-saga-coordinator.respec.mts +52 -0
  75. package/src/sync/sync-types.mts +120 -0
  76. package/src/test/mock-space.mts +85 -0
  77. package/src/witness/space/inner-space/inner-space-v1.respec.mts +181 -228
  78. package/src/witness/space/space-helper.mts +42 -4
  79. package/src/witness/space/space-helper.respec.mts +42 -0
  80. package/tmp.md +11 -0
@@ -2,19 +2,17 @@ import {
2
2
  respecfully, iReckon, ifWe, firstOfAll, firstOfEach, lastOfAll, lastOfEach, respecfullyDear, ifWeMight
3
3
  } from '@ibgib/helper-gib/dist/respec-gib/respec-gib.mjs';
4
4
  const maam = `[${import.meta.url}]`, sir = maam;
5
- // import { extractErrorMsg } from '@ibgib/helper-gib/dist/helpers/utils-helper.mjs';
5
+ import { clone, hash } from '@ibgib/helper-gib/dist/helpers/utils-helper.mjs';
6
6
  import { IbGib_V1 } from '@ibgib/ts-gib/dist/V1/types.mjs';
7
7
  import { getIbGibAddr } from '@ibgib/ts-gib/dist/helper.mjs';
8
- // import { MetaspaceService, } from '@ibgib/core-gib/dist/witness/space/metaspace/metaspace-types.mjs';
9
- // import { Metaspace_Innerspace } from '@ibgib/core-gib/dist/witness/space/metaspace/metaspace-innerspace/metaspace-innerspace.mjs';
10
- // import { IbGibSpaceAny } from '@ibgib/core-gib/dist/witness/space/space-base-v1.mjs';
11
8
 
12
9
  import { GLOBAL_LOG_A_LOT } from '../core-constants.mjs';
13
10
  import { KeystoneStrategyFactory } from './strategy/keystone-strategy-factory.mjs';
14
- import { KeystoneClaim, KeystoneIbGib_V1, KeystonePoolConfig_HashV1 } from './keystone-types.mjs';
11
+ import { KeystoneChallengePool, KeystoneClaim, KeystoneIbGib_V1, KeystonePoolConfig_HashV1 } from './keystone-types.mjs';
15
12
  import { createRevocationPoolConfig, createStandardPoolConfig } from './keystone-config-builder.mjs';
16
- import { POOL_ID_DEFAULT, POOL_ID_REVOKE, KEYSTONE_VERB_REVOKE } from './keystone-constants.mjs';
13
+ import { POOL_ID_DEFAULT, POOL_ID_REVOKE, KEYSTONE_VERB_REVOKE, KEYSTONE_VERB_MANAGE } from './keystone-constants.mjs';
17
14
  import { KeystoneService_V1 } from './keystone-service-v1.mjs';
15
+ import { addToBindingMap } from './keystone-helpers.mjs';
18
16
 
19
17
  const logalot = GLOBAL_LOG_A_LOT;
20
18
 
@@ -202,7 +200,7 @@ await respecfully(sir, 'Suite A: Strategy Vectors (HashRevealV1)', async () => {
202
200
 
203
201
  await respecfully(sir, 'Derivation Logic', async () => {
204
202
 
205
- await ifWeMight(sir, 'derivePoolSecret with same inputs returns same output', async () => {
203
+ await ifWe(sir, 'derivePoolSecret with same inputs returns same output', async () => {
206
204
  const strategy = KeystoneStrategyFactory.create({ config });
207
205
 
208
206
  const secretA = await strategy.derivePoolSecret({ masterSecret });
@@ -212,7 +210,7 @@ await respecfully(sir, 'Suite A: Strategy Vectors (HashRevealV1)', async () => {
212
210
  iReckon(sir, secretA).asTo('secret length').isGonnaBeTruthy();
213
211
  });
214
212
 
215
- await ifWeMight(sir, 'derivePoolSecret with different master secret returns different output', async () => {
213
+ await ifWe(sir, 'derivePoolSecret with different master secret returns different output', async () => {
216
214
  const strategy = KeystoneStrategyFactory.create({ config });
217
215
 
218
216
  const secretA = await strategy.derivePoolSecret({ masterSecret });
@@ -221,7 +219,7 @@ await respecfully(sir, 'Suite A: Strategy Vectors (HashRevealV1)', async () => {
221
219
  iReckon(sir, secretA).asTo('secrets differ').not.willEqual(secretB);
222
220
  });
223
221
 
224
- await ifWeMight(sir, 'derivePoolSecret with different salt returns different output', async () => {
222
+ await ifWe(sir, 'derivePoolSecret with different salt returns different output', async () => {
225
223
  // Modify salt in a copy of config
226
224
  const configB = { ...config, salt: "OtherPool" };
227
225
  const strategyA = KeystoneStrategyFactory.create({ config });
@@ -236,7 +234,7 @@ await respecfully(sir, 'Suite A: Strategy Vectors (HashRevealV1)', async () => {
236
234
 
237
235
  await respecfully(sir, 'Challenge/Solution Logic', async () => {
238
236
 
239
- await ifWeMight(sir, 'generateSolution -> generateChallenge -> validateSolution loop works', async () => {
237
+ await ifWe(sir, 'generateSolution -> generateChallenge -> validateSolution loop works', async () => {
240
238
  const strategy = KeystoneStrategyFactory.create({ config });
241
239
  const poolSecret = await strategy.derivePoolSecret({ masterSecret });
242
240
  const challengeId = "a3ff7843552870fc28bef2b"; // arbitrary random challengeId
@@ -255,7 +253,7 @@ await respecfully(sir, 'Suite A: Strategy Vectors (HashRevealV1)', async () => {
255
253
  iReckon(sir, isValid).asTo('valid pair should pass').isGonnaBeTrue();
256
254
  });
257
255
 
258
- await ifWeMight(sir, 'validateSolution fails for mismatched values', async () => {
256
+ await ifWe(sir, 'validateSolution fails for mismatched values', async () => {
259
257
  const strategy = KeystoneStrategyFactory.create({ config });
260
258
  const poolSecret = await strategy.derivePoolSecret({ masterSecret });
261
259
  const challengeId = "8c994f3ed598f150e25513"; // arbitrary random challengeId
@@ -271,7 +269,7 @@ await respecfully(sir, 'Suite A: Strategy Vectors (HashRevealV1)', async () => {
271
269
  iReckon(sir, isValid).asTo('tampered solution should fail').isGonnaBeFalse();
272
270
  });
273
271
 
274
- await ifWeMight(sir, 'validateSolution fails for mismatched challenge hashes', async () => {
272
+ await ifWe(sir, 'validateSolution fails for mismatched challenge hashes', async () => {
275
273
  const strategy = KeystoneStrategyFactory.create({ config });
276
274
  const poolSecret = await strategy.derivePoolSecret({ masterSecret });
277
275
 
@@ -312,7 +310,7 @@ await respecfully(sir, 'Suite B: Service Lifecycle', async () => {
312
310
  });
313
311
 
314
312
  await respecfully(sir, 'Genesis', async () => {
315
- await ifWeMight(sir, 'creates a valid genesis frame and persists it', async () => {
313
+ await ifWe(sir, 'creates a valid genesis frame and persists it', async () => {
316
314
  const config = createStandardPoolConfig(POOL_ID_DEFAULT);
317
315
 
318
316
  genesisKeystone = await service.genesis({
@@ -340,7 +338,7 @@ await respecfully(sir, 'Suite B: Service Lifecycle', async () => {
340
338
  });
341
339
 
342
340
  await respecfully(sir, 'Signing (Evolution)', async () => {
343
- await ifWeMight(sir, 'evolves the keystone with a valid proof', async () => {
341
+ await ifWe(sir, 'evolves the keystone with a valid proof', async () => {
344
342
  const claim: Partial<KeystoneClaim> = {
345
343
  target: "comment 123^gib",
346
344
  verb: "post"
@@ -370,7 +368,7 @@ await respecfully(sir, 'Suite B: Service Lifecycle', async () => {
370
368
  });
371
369
 
372
370
  await respecfully(sir, 'Validation', async () => {
373
- await ifWeMight(sir, 'validates the genesis->signed transition', async () => {
371
+ await ifWe(sir, 'validates the genesis->signed transition', async () => {
374
372
  const errors = await service.validate({
375
373
  prevIbGib: genesisKeystone,
376
374
  currentIbGib: signedKeystone,
@@ -413,7 +411,7 @@ await respecfully(sir, 'Suite C: Security Vectors', async () => {
413
411
  });
414
412
 
415
413
  await respecfully(sir, 'Wrong Secret (Forgery)', async () => {
416
- await ifWeMight(sir, 'prevents creation of forged frames', async () => {
414
+ await ifWe(sir, 'prevents creation of forged frames', async () => {
417
415
  const claim: Partial<KeystoneClaim> = { target: "comment 123^gib", verb: "post" };
418
416
 
419
417
  let errorCaught = false;
@@ -442,7 +440,7 @@ await respecfully(sir, 'Suite C: Security Vectors', async () => {
442
440
  });
443
441
 
444
442
  await respecfully(sir, 'Policy Violation (Restricted Verbs)', async () => {
445
- await ifWeMight(sir, 'throws error if signing forbidden verb with restricted pool', async () => {
443
+ await ifWe(sir, 'throws error if signing forbidden verb with restricted pool', async () => {
446
444
  // Create a specific restricted pool config manually
447
445
  const restrictedPoolId = "read_only_pool";
448
446
  const restrictedConfig = createStandardPoolConfig(restrictedPoolId);
@@ -483,7 +481,7 @@ await respecfully(sir, 'Suite C: Security Vectors', async () => {
483
481
  // SUITE D: REVOCATION
484
482
  // ===========================================================================
485
483
 
486
- await respecfullyDear(sir, 'Suite D: Revocation', async () => {
484
+ await respecfully(sir, 'Suite D: Revocation', async () => {
487
485
 
488
486
  const service = new KeystoneService_V1();
489
487
  const masterSecret = "AliceSecret_RevokeTest";
@@ -511,7 +509,7 @@ await respecfullyDear(sir, 'Suite D: Revocation', async () => {
511
509
  await respecfully(sir, 'Revoke Lifecycle', async () => {
512
510
  let revokedKeystone: KeystoneIbGib_V1;
513
511
 
514
- await ifWeMight(sir, 'successfully creates a revocation frame', async () => {
512
+ await ifWe(sir, 'successfully creates a revocation frame', async () => {
515
513
  revokedKeystone = await service.revoke({
516
514
  latestKeystone: genesisKeystone,
517
515
  masterSecret,
@@ -529,7 +527,7 @@ await respecfullyDear(sir, 'Suite D: Revocation', async () => {
529
527
  iReckon(sir, data.revocationInfo!.proof.claim.verb).willEqual(KEYSTONE_VERB_REVOKE);
530
528
  });
531
529
 
532
- await ifWeMight(sir, 'validates the revocation frame', async () => {
530
+ await ifWe(sir, 'validates the revocation frame', async () => {
533
531
  const errors = await service.validate({
534
532
  prevIbGib: genesisKeystone,
535
533
  currentIbGib: revokedKeystone!,
@@ -540,7 +538,7 @@ await respecfullyDear(sir, 'Suite D: Revocation', async () => {
540
538
  iReckon(sir, errors.length).asTo('no validation errors').willEqual(0);
541
539
  });
542
540
 
543
- await ifWeMight(sir, 'consumed the revocation pool (Scorched Earth)', async () => {
541
+ await ifWe(sir, 'consumed the revocation pool (Scorched Earth)', async () => {
544
542
  const data = revokedKeystone!.data!;
545
543
  const revokePool = data.challengePools.find(p => p.id === POOL_ID_REVOKE);
546
544
 
@@ -553,3 +551,468 @@ await respecfullyDear(sir, 'Suite D: Revocation', async () => {
553
551
  });
554
552
  });
555
553
  });
554
+
555
+ // ===========================================================================
556
+ // SUITE E: STRUCTURAL EVOLUTION (addPools)
557
+ // ===========================================================================
558
+
559
+ await respecfully(sir, 'Suite E: Structural Evolution (addPools)', async () => {
560
+
561
+ const service = new KeystoneService_V1();
562
+ const aliceSecret = "Alice_Master_Key";
563
+ const bobSecret = "Bob_Foreign_Key";
564
+
565
+ let mockSpace: MockIbGibSpace;
566
+ let mockMetaspace: any;
567
+ let aliceKeystone: KeystoneIbGib_V1;
568
+
569
+ // Helper to generate a "Foreign" pool (e.g. from Bob)
570
+ const createForeignPool = async (id: string, verbs: string[] = []): Promise<KeystoneChallengePool> => {
571
+ const config = createStandardPoolConfig(id);
572
+ config.allowedVerbs = verbs;
573
+
574
+ // We use the factory manually here to simulate Bob doing this offline
575
+ const strategy = KeystoneStrategyFactory.create({ config });
576
+ const poolSecret = await strategy.derivePoolSecret({ masterSecret: bobSecret });
577
+ const challenges: { [id: string]: any } = {};
578
+ const bindingMap: { [char: string]: string[] } = {};
579
+
580
+ for (let i = 0; i < 10; i++) {
581
+ // Manually replicate ID gen for test
582
+ const raw = await hash({ s: `${config.salt}${Date.now()}${i}` });
583
+ const challengeId = raw.substring(0, 16);
584
+
585
+ const solution = await strategy.generateSolution({
586
+ poolSecret, poolId: config.salt, challengeId,
587
+ });
588
+ const challenge = await strategy.generateChallenge({ solution });
589
+ challenges[challengeId] = challenge;
590
+ addToBindingMap(bindingMap, challengeId);
591
+ }
592
+
593
+ return {
594
+ id,
595
+ config,
596
+ challenges,
597
+ bindingMap,
598
+ isForeign: true,
599
+ metadata: { owner: 'Bob' }
600
+ };
601
+ };
602
+
603
+ firstOfAll(sir, async () => {
604
+ mockSpace = new MockIbGibSpace();
605
+ mockMetaspace = new MockMetaspaceService(mockSpace);
606
+
607
+ // Alice Genesis: Standard pool (allows all verbs, including 'manage')
608
+ const config = createStandardPoolConfig(POOL_ID_DEFAULT);
609
+ aliceKeystone = await service.genesis({
610
+ masterSecret: aliceSecret,
611
+ configs: [config],
612
+ metaspace: mockMetaspace,
613
+ space: mockSpace as any,
614
+ });
615
+ });
616
+
617
+ await respecfully(sir, 'Happy Path', async () => {
618
+ await ifWe(sir, 'authorizes and adds a foreign pool', async () => {
619
+ const bobPool = await createForeignPool("pool_bob", ["post"]);
620
+
621
+ const updatedKeystone = await service.addPools({
622
+ latestKeystone: aliceKeystone,
623
+ masterSecret: aliceSecret,
624
+ newPools: [bobPool],
625
+ metaspace: mockMetaspace,
626
+ space: mockSpace as any,
627
+ });
628
+
629
+ // 1. Verify new state
630
+ iReckon(sir, updatedKeystone).isGonnaBeTruthy();
631
+ const pools = updatedKeystone.data!.challengePools;
632
+ iReckon(sir, pools.length).asTo('pool count increased').willEqual(2);
633
+
634
+ const foundBob = pools.find(p => p.id === "pool_bob");
635
+ iReckon(sir, foundBob).asTo('bob pool exists').isGonnaBeTruthy();
636
+ iReckon(sir, foundBob!.isForeign).asTo('isForeign flag').isGonnaBeTrue();
637
+
638
+ // 2. Verify Proof
639
+ const proof = updatedKeystone.data!.proofs[0];
640
+ iReckon(sir, proof.claim.verb).asTo('proof verb').willEqual("manage");
641
+ // Alice signed this using HER pool (default), not Bob's
642
+ iReckon(sir, proof.solutions[0].poolId).willEqual(POOL_ID_DEFAULT);
643
+
644
+ // 3. Verify Validity
645
+ const errors = await service.validate({
646
+ prevIbGib: aliceKeystone,
647
+ currentIbGib: updatedKeystone,
648
+ });
649
+ iReckon(sir, errors.length).asTo('validation passed').willEqual(0);
650
+
651
+ // Update local ref for next tests
652
+ aliceKeystone = updatedKeystone;
653
+ });
654
+ });
655
+
656
+ await respecfully(sir, 'Permissions & Logic', async () => {
657
+ await ifWe(sir, 'fails if no pool allows "manage" verb', async () => {
658
+ // 1. Create a restricted keystone
659
+ const restrictedConfig = createStandardPoolConfig("read_only");
660
+ restrictedConfig.allowedVerbs = ['read']; // No 'manage'
661
+
662
+ const restrictedKeystone = await service.genesis({
663
+ masterSecret: aliceSecret,
664
+ configs: [restrictedConfig],
665
+ metaspace: mockMetaspace,
666
+ space: mockSpace as any,
667
+ });
668
+
669
+ const newPool = await createForeignPool("pool_test");
670
+ let errorCaught = false;
671
+
672
+ try {
673
+ await service.addPools({
674
+ latestKeystone: restrictedKeystone,
675
+ masterSecret: aliceSecret,
676
+ newPools: [newPool],
677
+ metaspace: mockMetaspace,
678
+ space: mockSpace as any,
679
+ });
680
+ } catch (e: any) {
681
+ errorCaught = true;
682
+ // Should fail in resolveTargetPool or addPools logic
683
+ // console.log("Caught expected error:", e.message);
684
+ }
685
+
686
+ iReckon(sir, errorCaught).asTo('permission denied').isGonnaBeTrue();
687
+ });
688
+
689
+ await ifWe(sir, 'fails on ID collision', async () => {
690
+ // Try to add "pool_bob" again (it was added in Happy Path)
691
+ const duplicatePool = await createForeignPool("pool_bob");
692
+
693
+ let errorCaught = false;
694
+ try {
695
+ await service.addPools({
696
+ latestKeystone: aliceKeystone, // This already has pool_bob
697
+ masterSecret: aliceSecret,
698
+ newPools: [duplicatePool],
699
+ metaspace: mockMetaspace,
700
+ space: mockSpace as any,
701
+ });
702
+ } catch (e: any) {
703
+ errorCaught = true;
704
+ iReckon(sir, e.message).includes("ID collision");
705
+ }
706
+
707
+ iReckon(sir, errorCaught).asTo('collision detected').isGonnaBeTrue();
708
+ });
709
+ });
710
+ });
711
+
712
+ // ===========================================================================
713
+ // SUITE E: STRUCTURAL EVOLUTION (addPools)
714
+ // ===========================================================================
715
+
716
+ await respecfully(sir, 'Suite E: Structural Evolution (addPools)', async () => {
717
+
718
+ const service = new KeystoneService_V1();
719
+ const aliceSecret = "Alice_Master_Key";
720
+ const bobSecret = "Bob_Foreign_Key";
721
+
722
+ let mockSpace: MockIbGibSpace;
723
+ let mockMetaspace: any;
724
+ let aliceKeystone: KeystoneIbGib_V1;
725
+
726
+ // Helper to simulate Bob creating a pool "offline" to give to Alice
727
+ const createForeignPool = async (id: string, verbs: string[] = []): Promise<KeystoneChallengePool> => {
728
+ const config = createStandardPoolConfig(id);
729
+ config.allowedVerbs = verbs;
730
+
731
+ // We use the factory manually here to simulate Bob doing this on his own machine
732
+ const strategy = KeystoneStrategyFactory.create({ config });
733
+ const poolSecret = await strategy.derivePoolSecret({ masterSecret: bobSecret });
734
+ const challenges: { [id: string]: any } = {};
735
+ const bindingMap: { [char: string]: string[] } = {};
736
+
737
+ // Generate a small set of challenges
738
+ for (let i = 0; i < 10; i++) {
739
+ const raw = await hash({ s: `${config.salt}${Date.now()}${i}` });
740
+ const challengeId = raw.substring(0, 16);
741
+
742
+ const solution = await strategy.generateSolution({
743
+ poolSecret, poolId: config.salt, challengeId,
744
+ });
745
+ const challenge = await strategy.generateChallenge({ solution });
746
+ challenges[challengeId] = challenge;
747
+ addToBindingMap(bindingMap, challengeId);
748
+ }
749
+
750
+ return {
751
+ id,
752
+ config,
753
+ challenges,
754
+ bindingMap,
755
+ isForeign: true,
756
+ metadata: { owner: 'Bob', role: 'Delegate' }
757
+ };
758
+ };
759
+
760
+ firstOfAll(sir, async () => {
761
+ mockSpace = new MockIbGibSpace();
762
+ mockMetaspace = new MockMetaspaceService(mockSpace);
763
+
764
+ // Alice Genesis: Standard pool (allows all verbs, including 'manage')
765
+ const config = createStandardPoolConfig(POOL_ID_DEFAULT);
766
+ aliceKeystone = await service.genesis({
767
+ masterSecret: aliceSecret,
768
+ configs: [config],
769
+ metaspace: mockMetaspace,
770
+ space: mockSpace as any,
771
+ });
772
+ });
773
+
774
+ await respecfully(sir, 'Happy Path', async () => {
775
+ await ifWe(sir, 'authorizes and adds a foreign pool', async () => {
776
+ const bobPool = await createForeignPool("pool_bob", ["post"]);
777
+
778
+ const updatedKeystone = await service.addPools({
779
+ latestKeystone: aliceKeystone,
780
+ masterSecret: aliceSecret,
781
+ newPools: [bobPool],
782
+ metaspace: mockMetaspace,
783
+ space: mockSpace as any,
784
+ });
785
+
786
+ // 1. Verify new state
787
+ iReckon(sir, updatedKeystone).isGonnaBeTruthy();
788
+ const pools = updatedKeystone.data!.challengePools;
789
+ iReckon(sir, pools.length).asTo('pool count increased').willEqual(2);
790
+
791
+ const foundBob = pools.find(p => p.id === "pool_bob");
792
+ iReckon(sir, foundBob).asTo('bob pool exists').isGonnaBeTruthy();
793
+ iReckon(sir, foundBob!.isForeign).asTo('isForeign flag').isGonnaBeTrue();
794
+
795
+ // 2. Verify Proof
796
+ const proof = updatedKeystone.data!.proofs[0];
797
+ iReckon(sir, proof.claim.verb).asTo('proof verb').willEqual(KEYSTONE_VERB_MANAGE);
798
+ // Alice signed this using HER pool (default), not Bob's
799
+ iReckon(sir, proof.solutions[0].poolId).willEqual(POOL_ID_DEFAULT);
800
+
801
+ // 3. Verify Validity
802
+ const errors = await service.validate({
803
+ prevIbGib: aliceKeystone,
804
+ currentIbGib: updatedKeystone,
805
+ });
806
+ iReckon(sir, errors.length).asTo('validation passed').willEqual(0);
807
+
808
+ // Update local ref for next tests
809
+ aliceKeystone = updatedKeystone;
810
+ });
811
+ });
812
+
813
+ await respecfully(sir, 'Permissions & Logic', async () => {
814
+ await ifWe(sir, 'fails if no pool allows "manage" verb', async () => {
815
+ // 1. Create a restricted keystone (read-only)
816
+ const restrictedConfig = createStandardPoolConfig("read_only");
817
+ restrictedConfig.allowedVerbs = ['read']; // No 'manage'
818
+
819
+ const restrictedKeystone = await service.genesis({
820
+ masterSecret: aliceSecret,
821
+ configs: [restrictedConfig],
822
+ metaspace: mockMetaspace,
823
+ space: mockSpace as any,
824
+ });
825
+
826
+ const newPool = await createForeignPool("pool_test");
827
+ let errorCaught = false;
828
+
829
+ try {
830
+ await service.addPools({
831
+ latestKeystone: restrictedKeystone,
832
+ masterSecret: aliceSecret,
833
+ newPools: [newPool],
834
+ metaspace: mockMetaspace,
835
+ space: mockSpace as any,
836
+ });
837
+ } catch (e: any) {
838
+ errorCaught = true;
839
+ // Optional: Check error message
840
+ // iReckon(sir, e.message).includes("No local pool found with 'manage'");
841
+ }
842
+
843
+ iReckon(sir, errorCaught).asTo('permission denied').isGonnaBeTrue();
844
+ });
845
+
846
+ await ifWe(sir, 'fails on ID collision', async () => {
847
+ // Try to add "pool_bob" again (it was added in Happy Path)
848
+ const duplicatePool = await createForeignPool("pool_bob");
849
+
850
+ let errorCaught = false;
851
+ try {
852
+ await service.addPools({
853
+ latestKeystone: aliceKeystone, // This already has pool_bob
854
+ masterSecret: aliceSecret,
855
+ newPools: [duplicatePool],
856
+ metaspace: mockMetaspace,
857
+ space: mockSpace as any,
858
+ });
859
+ } catch (e: any) {
860
+ errorCaught = true;
861
+ iReckon(sir, e.message).includes("collision");
862
+ }
863
+
864
+ iReckon(sir, errorCaught).asTo('collision detected').isGonnaBeTrue();
865
+ });
866
+ });
867
+ });
868
+
869
+ // ===========================================================================
870
+ // SUITE F: DEEP INSPECTION (Granularity & Serialization)
871
+ // ===========================================================================
872
+
873
+ await respecfully(sir, 'Suite F: Deep Inspection', async () => {
874
+
875
+ const service = new KeystoneService_V1();
876
+ const aliceSecret = "Alice_Deep_Inspect";
877
+ const salt = "granularity_pool";
878
+
879
+ let mockSpace: MockIbGibSpace;
880
+ let mockMetaspace: any;
881
+ let genesisKeystone: KeystoneIbGib_V1;
882
+
883
+ let signedKeystone: KeystoneIbGib_V1;
884
+
885
+ // We use a specific hybrid config to test exact selection logic
886
+ const hybridConfig = createStandardPoolConfig(salt) as KeystonePoolConfig_HashV1;
887
+ // 2 FIFO + 2 Random = 4 Total per sign
888
+ hybridConfig.behavior.selectSequentially = 2;
889
+ hybridConfig.behavior.selectRandomly = 2;
890
+ hybridConfig.behavior.size = 20; // Small enough to track, large enough to be random
891
+
892
+ firstOfAll(sir, async () => {
893
+ mockSpace = new MockIbGibSpace();
894
+ mockMetaspace = new MockMetaspaceService(mockSpace);
895
+
896
+ genesisKeystone = await service.genesis({
897
+ masterSecret: aliceSecret,
898
+ configs: [hybridConfig],
899
+ metaspace: mockMetaspace,
900
+ space: mockSpace as any,
901
+ });
902
+ });
903
+
904
+ await respecfully(sir, 'Proof Granularity & Math', async () => {
905
+
906
+ await ifWe(sir, 'generates exactly the expected number of solutions', async () => {
907
+ signedKeystone = await service.sign({
908
+ latestKeystone: genesisKeystone,
909
+ masterSecret: aliceSecret,
910
+ claim: { verb: "post", target: "data^gib" },
911
+ metaspace: mockMetaspace,
912
+ space: mockSpace as any,
913
+ });
914
+
915
+ const proofs = signedKeystone.data!.proofs;
916
+ iReckon(sir, proofs.length).asTo('proof count').willEqual(1);
917
+
918
+ const solutions = proofs[0].solutions;
919
+ // 2 Sequential + 2 Random = 4
920
+ iReckon(sir, solutions.length).asTo('solution count').willEqual(4);
921
+ });
922
+
923
+ await ifWe(sir, 'verifies the math manually (White-box Crypto Check)', async () => {
924
+ const proof = signedKeystone.data!.proofs[0];
925
+ const poolSnapshot = genesisKeystone.data!.challengePools.find(p => p.id === salt)!;
926
+
927
+ // We iterate every solution in the proof and MANUALLY verify the hash relationship
928
+ // bypassing the Service's validation logic to ensure the raw math holds up.
929
+
930
+ for (const solution of proof.solutions) {
931
+ // 1. Find the challenge in the *Previous* frame (Genesis)
932
+ const challenge = poolSnapshot.challenges[solution.challengeId];
933
+
934
+ if (!challenge) {
935
+ throw new Error(`Test Failure: Solution references ID ${solution.challengeId} which was not in Genesis pool.`);
936
+ }
937
+
938
+ // 2. Re-implement HashReveal V1 verification logic locally in the test
939
+ // Hash(Salt + Value + Salt)
940
+ // Note: rounds=1 in standard config
941
+ const indexSalt = solution.challengeId;
942
+ const calculatedHash = await hash({
943
+ s: `${indexSalt}${solution.value}${indexSalt}`,
944
+ algorithm: 'SHA-256'
945
+ });
946
+
947
+ // 3. Assert
948
+ iReckon(sir, calculatedHash).asTo(`Manual hash verification for ${solution.challengeId}`).willEqual(challenge.hash);
949
+ }
950
+ });
951
+
952
+ await ifWe(sir, 'verifies FIFO logic (Deterministic Selection)', async () => {
953
+ const proof = signedKeystone.data!.proofs[0];
954
+ const poolSnapshot = genesisKeystone.data!.challengePools.find(p => p.id === salt)!;
955
+
956
+ // The first N keys in the pool should be the FIFO targets.
957
+ // Assumption: Object.keys returns insertion order (Standard in modern JS engines)
958
+ const allIds = Object.keys(poolSnapshot.challenges);
959
+ const expectedFifoIds = allIds.slice(0, 2);
960
+
961
+ const solvedIds = proof.solutions.map(s => s.challengeId);
962
+
963
+ // Check that our solution list *includes* the expected FIFO IDs
964
+ const hasFirst = solvedIds.includes(expectedFifoIds[0]);
965
+ const hasSecond = solvedIds.includes(expectedFifoIds[1]);
966
+
967
+ iReckon(sir, hasFirst).asTo(`Solution includes 1st FIFO ID (${expectedFifoIds[0]})`).isGonnaBeTrue();
968
+ iReckon(sir, hasSecond).asTo(`Solution includes 2nd FIFO ID (${expectedFifoIds[1]})`).isGonnaBeTrue();
969
+ });
970
+ });
971
+
972
+ await respecfully(sir, 'DTO & Serialization', async () => {
973
+
974
+ await ifWe(sir, 'survives a clone/JSON-cycle without corruption', async () => {
975
+ // 1. Create a DTO (simulate network transmission/storage)
976
+ // 'clone' does a JSON stringify/parse under the hood (usually) or structured clone.
977
+ const dto = clone(signedKeystone);
978
+
979
+ // 2. Structural checks
980
+ iReckon(sir, dto).asTo('dto exists').isGonnaBeTruthy();
981
+ iReckon(sir, dto.data).asTo('dto data').isGonnaBeTruthy();
982
+ iReckon(sir, dto.data!.proofs).asTo('dto proofs').isGonnaBeTruthy();
983
+
984
+ // 3. Functional check: Can the service validate this DTO?
985
+ // This ensures no prototypes or hidden properties were lost that the service depends on.
986
+ const errors = await service.validate({
987
+ prevIbGib: genesisKeystone,
988
+ currentIbGib: dto, // Passing the DTO, not the original object
989
+ });
990
+
991
+ iReckon(sir, errors.length).asTo('DTO validation errors').willEqual(0);
992
+ });
993
+
994
+ await ifWe(sir, 'ensures data contains no functions or circular refs', async () => {
995
+ // A crude but effective test: ensure JSON.stringify doesn't throw
996
+ // and the result is equal to the object (if we parsed it back).
997
+
998
+ const jsonStr = JSON.stringify(signedKeystone);
999
+ const parsed = JSON.parse(jsonStr);
1000
+
1001
+ // Compare specific deep fields
1002
+ const originalSolution = signedKeystone.data!.proofs[0].solutions[0].value;
1003
+ const parsedSolution = parsed.data.proofs[0].solutions[0].value;
1004
+
1005
+ iReckon(sir, parsedSolution).asTo('deep property survives stringify').willEqual(originalSolution);
1006
+
1007
+ // Ensure no extra properties were lost
1008
+ // FIX: JSON.stringify removes keys with 'undefined' values.
1009
+ // We must filter the original keys to match this behavior for a fair comparison.
1010
+ const origKeys = Object.keys(signedKeystone.data!)
1011
+ .filter(k => (signedKeystone.data as any)[k] !== undefined);
1012
+
1013
+ const parsedKeys = Object.keys(parsed.data);
1014
+ iReckon(sir, parsedKeys.length).asTo('key count matches').willEqual(origKeys.length);
1015
+ });
1016
+
1017
+ });
1018
+ });
@@ -217,6 +217,30 @@ export interface KeystoneChallengePool {
217
217
  * Note: A single ID may appear in multiple buckets (Coverage Strategy).
218
218
  */
219
219
  bindingMap: { [hexChar: string]: string[] };
220
+
221
+ /**
222
+ * If true, this pool's secrets are NOT derived from the Keystone's
223
+ * primary Master Secret. They are held by an external entity.
224
+ *
225
+ * ## intent
226
+ *
227
+ * The driving use case for this is signing in with a server "super node"
228
+ * and giving that node the ability to sign on behalf of the user. This is a
229
+ * common pattern in SSO-type workflows.
230
+ */
231
+ isForeign?: boolean;
232
+
233
+ /**
234
+ * Arbitrary metadata for the wallet/user to identify the pool.
235
+ * e.g. { delegate: "PrimaryServer", purpose: "SSO" }
236
+ *
237
+ * ## intent
238
+ *
239
+ * The driving use case for this is signing in with a server "super node"
240
+ * and giving that node the ability to sign on behalf of the user. This is a
241
+ * common pattern in SSO-type workflows.
242
+ */
243
+ metadata?: any;
220
244
  }
221
245
 
222
246
  /**
@@ -0,0 +1,24 @@
1
+ export const SYNC_ATOM = "sync";
2
+
3
+ /**
4
+ * Protocol version string for V1.
5
+ */
6
+ export const SYNC_PROTOCOL_V1 = "sync 1.0.0";
7
+
8
+ export const SYNC_STAGE_INIT = "init";
9
+ export const SYNC_STAGE_REQUEST = "request";
10
+ export const SYNC_STAGE_DELTA = "delta";
11
+ export const SYNC_STAGE_COMMIT = "commit";
12
+
13
+ export type SyncStage =
14
+ | typeof SYNC_STAGE_INIT
15
+ | typeof SYNC_STAGE_REQUEST
16
+ | typeof SYNC_STAGE_DELTA
17
+ | typeof SYNC_STAGE_COMMIT;
18
+
19
+ export const SyncStage = {
20
+ init: SYNC_STAGE_INIT,
21
+ request: SYNC_STAGE_REQUEST,
22
+ delta: SYNC_STAGE_DELTA,
23
+ commit: SYNC_STAGE_COMMIT,
24
+ } as const;