@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.
- package/dist/agent-helpers.d.mts +45 -0
- package/dist/agent-helpers.d.mts.map +1 -0
- package/dist/agent-helpers.mjs +36 -0
- package/dist/agent-helpers.mjs.map +1 -0
- package/dist/keystone/keystone-config-builder.respec.d.mts +2 -0
- package/dist/keystone/keystone-config-builder.respec.d.mts.map +1 -0
- package/dist/keystone/keystone-config-builder.respec.mjs +34 -0
- package/dist/keystone/keystone-config-builder.respec.mjs.map +1 -0
- package/dist/keystone/keystone-constants.d.mts +2 -0
- package/dist/keystone/keystone-constants.d.mts.map +1 -1
- package/dist/keystone/keystone-constants.mjs +2 -0
- package/dist/keystone/keystone-constants.mjs.map +1 -1
- package/dist/keystone/keystone-helpers.d.mts +54 -1
- package/dist/keystone/keystone-helpers.d.mts.map +1 -1
- package/dist/keystone/keystone-helpers.mjs +185 -1
- package/dist/keystone/keystone-helpers.mjs.map +1 -1
- package/dist/keystone/keystone-service-v1.d.mts +49 -16
- package/dist/keystone/keystone-service-v1.d.mts.map +1 -1
- package/dist/keystone/keystone-service-v1.mjs +151 -328
- package/dist/keystone/keystone-service-v1.mjs.map +1 -1
- package/dist/keystone/keystone-service-v1.respec.mjs +401 -20
- package/dist/keystone/keystone-service-v1.respec.mjs.map +1 -1
- package/dist/keystone/keystone-types.d.mts +22 -0
- package/dist/keystone/keystone-types.d.mts.map +1 -1
- package/dist/sync/sync-constants.d.mts +17 -0
- package/dist/sync/sync-constants.d.mts.map +1 -0
- package/dist/sync/sync-constants.mjs +16 -0
- package/dist/sync/sync-constants.mjs.map +1 -0
- package/dist/sync/sync-helpers.d.mts +15 -0
- package/dist/sync/sync-helpers.d.mts.map +1 -0
- package/dist/sync/sync-helpers.mjs +46 -0
- package/dist/sync/sync-helpers.mjs.map +1 -0
- package/dist/sync/sync-local-spaces.respec.d.mts +2 -0
- package/dist/sync/sync-local-spaces.respec.d.mts.map +1 -0
- package/dist/sync/sync-local-spaces.respec.mjs +159 -0
- package/dist/sync/sync-local-spaces.respec.mjs.map +1 -0
- package/dist/sync/sync-saga-coordinator.d.mts +118 -0
- package/dist/sync/sync-saga-coordinator.d.mts.map +1 -0
- package/dist/sync/sync-saga-coordinator.mjs +399 -0
- package/dist/sync/sync-saga-coordinator.mjs.map +1 -0
- package/dist/sync/sync-saga-coordinator.respec.d.mts +2 -0
- package/dist/sync/sync-saga-coordinator.respec.d.mts.map +1 -0
- package/dist/sync/sync-saga-coordinator.respec.mjs +40 -0
- package/dist/sync/sync-saga-coordinator.respec.mjs.map +1 -0
- package/dist/sync/sync-types.d.mts +103 -0
- package/dist/sync/sync-types.d.mts.map +1 -0
- package/dist/sync/sync-types.mjs +2 -0
- package/dist/sync/sync-types.mjs.map +1 -0
- package/dist/test/mock-space.d.mts +39 -0
- package/dist/test/mock-space.d.mts.map +1 -0
- package/dist/test/mock-space.mjs +79 -0
- package/dist/test/mock-space.mjs.map +1 -0
- package/dist/witness/space/inner-space/inner-space-v1.respec.mjs +163 -201
- package/dist/witness/space/inner-space/inner-space-v1.respec.mjs.map +1 -1
- package/dist/witness/space/space-helper.d.mts.map +1 -1
- package/dist/witness/space/space-helper.mjs +43 -4
- package/dist/witness/space/space-helper.mjs.map +1 -1
- package/dist/witness/space/space-helper.respec.d.mts +2 -0
- package/dist/witness/space/space-helper.respec.d.mts.map +1 -0
- package/dist/witness/space/space-helper.respec.mjs +30 -0
- package/dist/witness/space/space-helper.respec.mjs.map +1 -0
- package/package.json +2 -2
- package/src/agent-helpers.mts +58 -0
- package/src/keystone/keystone-config-builder.respec.mts +49 -0
- package/src/keystone/keystone-constants.mts +2 -0
- package/src/keystone/keystone-helpers.mts +211 -2
- package/src/keystone/keystone-service-v1.mts +183 -367
- package/src/keystone/keystone-service-v1.respec.mts +484 -21
- package/src/keystone/keystone-types.mts +24 -0
- package/src/sync/sync-constants.mts +24 -0
- package/src/sync/sync-helpers.mts +59 -0
- package/src/sync/sync-local-spaces.respec.mts +200 -0
- package/src/sync/sync-saga-coordinator.mts +477 -0
- package/src/sync/sync-saga-coordinator.respec.mts +52 -0
- package/src/sync/sync-types.mts +120 -0
- package/src/test/mock-space.mts +85 -0
- package/src/witness/space/inner-space/inner-space-v1.respec.mts +181 -228
- package/src/witness/space/space-helper.mts +42 -4
- package/src/witness/space/space-helper.respec.mts +42 -0
- 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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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;
|