@ibgib/core-gib 0.1.61 → 0.1.62

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.
@@ -10,10 +10,11 @@ import { GLOBAL_LOG_A_LOT } from '../core-constants.mjs';
10
10
  import { KeystoneStrategyFactory } from './strategy/keystone-strategy-factory.mjs';
11
11
  import { KeystoneChallengePool, KeystoneClaim, KeystoneIbGib_V1, KeystonePoolConfig_HashV1 } from './keystone-types.mjs';
12
12
  import { createRevocationPoolConfig, createStandardPoolConfig } from './keystone-config-builder.mjs';
13
- import { POOL_ID_DEFAULT, POOL_ID_REVOKE, KEYSTONE_VERB_REVOKE, KEYSTONE_VERB_MANAGE } from './keystone-constants.mjs';
14
- import { KeystoneService_V1 } from './keystone-service-v1.mjs';
15
- import { selectChallengeIds, validateChallengePool } from './keystone-helpers.mjs';
13
+ import { POOL_ID_DEFAULT, POOL_ID_REVOKE, KEYSTONE_VERB_REVOKE, KEYSTONE_VERB_MANAGE, POOL_ID_MANAGE } from './keystone-constants.mjs';
14
+ import { KeystoneService_V1, } from './keystone-service-v1.mjs';
15
+ import { selectChallengeIds, validateChallengePool, validateKeystoneIb, validateKeystoneTransition, validateGenesisKeystone } from './keystone-helpers.mjs';
16
16
  import { KeystoneProfileBuilder } from './policy/keystone-profile-builder.mjs';
17
+ import { getTjpAddr } from '../common/other/ibgib-helper.mjs';
17
18
 
18
19
  const logalot = GLOBAL_LOG_A_LOT;
19
20
 
@@ -37,16 +38,55 @@ class MockIbGibSpace {
37
38
  return data ? JSON.parse(JSON.stringify(data)) : null;
38
39
  }
39
40
 
41
+ async argy({ argData, ibGibs }: { argData: any; ibGibs?: IbGib_V1[] }): Promise<any> {
42
+ return {
43
+ ib: 'arg^gib',
44
+ gib: 'arg_gib',
45
+ data: argData,
46
+ ibGibs,
47
+ };
48
+ }
49
+
40
50
  async witness(arg: any): Promise<any> {
41
51
  const cmd = arg.data?.cmd;
52
+ const cmdModifiers = arg.data?.cmdModifiers || [];
42
53
  if (cmd === 'get') {
43
54
  const addrs = arg.data.ibGibAddrs || [];
44
- const ibGibs: IbGib_V1[] = [];
45
- for (const addr of addrs) {
46
- const x = await this.get({ addr });
47
- if (x) { ibGibs.push(x); }
55
+ if (cmdModifiers.includes('latest') && cmdModifiers.includes('addrs')) {
56
+ const latestAddrsMap: { [addr: string]: string } = {};
57
+ for (const queryAddr of addrs) {
58
+ const queryTjpGib = queryAddr.includes('.') ? queryAddr.split('.')[1] : queryAddr.split('^')[1];
59
+ let latestAddr = queryAddr;
60
+ let maxN = -1;
61
+ for (const [addr, ibGib] of this.store.entries()) {
62
+ const tjpGib = addr.includes('.') ? addr.split('.')[1] : addr.split('^')[1];
63
+ if (tjpGib === queryTjpGib) {
64
+ const n = ibGib.data?.n ?? 0;
65
+ if (n > maxN) {
66
+ maxN = n;
67
+ latestAddr = addr;
68
+ }
69
+ }
70
+ }
71
+ latestAddrsMap[queryAddr] = latestAddr;
72
+ }
73
+ return {
74
+ data: {
75
+ success: true,
76
+ latestAddrsMap,
77
+ }
78
+ };
79
+ } else {
80
+ const ibGibs: IbGib_V1[] = [];
81
+ for (const addr of addrs) {
82
+ const x = await this.get({ addr });
83
+ if (x) { ibGibs.push(x); }
84
+ }
85
+ return {
86
+ data: { success: true },
87
+ ibGibs,
88
+ };
48
89
  }
49
- return { ibGibs };
50
90
  }
51
91
  return undefined;
52
92
  }
@@ -81,6 +121,15 @@ class MockMetaspaceService {
81
121
  return target.put(args);
82
122
  }
83
123
 
124
+ async get(args: { addr: string, space?: any }): Promise<{ success: boolean, ibGibs?: IbGib_V1[] }> {
125
+ const target = args.space || this.space;
126
+ const x = await target.get({ addr: args.addr });
127
+ return {
128
+ success: !!x,
129
+ ibGibs: x ? [x] : [],
130
+ };
131
+ }
132
+
84
133
  /**
85
134
  * Tracks the latest version of an ibGib timeline.
86
135
  */
@@ -1160,4 +1209,330 @@ await respecfully(sir, 'Suite F: Deep Inspection', async () => {
1160
1209
  });
1161
1210
 
1162
1211
  });
1212
+ });
1213
+
1214
+
1215
+ // ===========================================================================
1216
+ // SUITE E: DELEGATION (Register/Unregister)
1217
+ // ===========================================================================
1218
+
1219
+ await respecfully(sir, 'Suite E: Delegation (Register/Unregister)', async () => {
1220
+
1221
+ const service = new KeystoneService_V1();
1222
+ const parentSecret = "ParentMasterSecret_123";
1223
+ const delegateSecret = "DelegateMasterSecret_456";
1224
+
1225
+ let mockSpace: MockIbGibSpace;
1226
+ let mockMetaspace: any;
1227
+
1228
+ let parentKeystone: KeystoneIbGib_V1;
1229
+ let delegateKeystone: KeystoneIbGib_V1;
1230
+
1231
+ firstOfEach(sir, async () => {
1232
+ mockSpace = new MockIbGibSpace();
1233
+ mockMetaspace = new MockMetaspaceService(mockSpace);
1234
+
1235
+ const salt = (await getUUID()).substring(0, 16);
1236
+ const defaultConfig = createStandardPoolConfig({ id: POOL_ID_DEFAULT, salt });
1237
+ const manageConfig = createStandardPoolConfig({ id: POOL_ID_MANAGE, salt: salt + "_manage" });
1238
+ manageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE];
1239
+
1240
+ // 1. Create Parent Keystone (with manage pool)
1241
+ parentKeystone = await service.genesis({
1242
+ masterSecret: parentSecret,
1243
+ configs: [defaultConfig, manageConfig],
1244
+ metaspace: mockMetaspace,
1245
+ space: mockSpace as any,
1246
+ });
1247
+
1248
+ // 2. Create Delegate Keystone
1249
+ const delegateConfig = createStandardPoolConfig({ id: POOL_ID_DEFAULT, salt: salt + "_delegate" });
1250
+ delegateKeystone = await service.genesis({
1251
+ masterSecret: delegateSecret,
1252
+ configs: [delegateConfig],
1253
+ metaspace: mockMetaspace,
1254
+ space: mockSpace as any,
1255
+ });
1256
+ });
1257
+
1258
+ await ifWeMight(sir, 'registers a delegate keystone and evolves parent using manage pool', async () => {
1259
+ // Register the delegate
1260
+ const evolvedParent = await service.registerDelegate({
1261
+ parentKeystone,
1262
+ delegateKeystone,
1263
+ masterSecret: parentSecret,
1264
+ metaspace: mockMetaspace,
1265
+ space: mockSpace as any,
1266
+ });
1267
+
1268
+ iReckon(sir, evolvedParent).asTo('evolved parent exists').isGonnaBeTruthy();
1269
+ iReckon(sir, evolvedParent.data?.delegates).asTo('delegates map exists').isGonnaBeTruthy();
1270
+
1271
+ const delegateTjpAddr = getTjpAddr({ ibGib: delegateKeystone });
1272
+ if (!delegateTjpAddr) { throw new Error(`(UNEXPECTED) delegateTjpAddr falsy? (E: 312e986ce4b2ed6c084c03e8f45fa826)`); }
1273
+ const delegateInfo = evolvedParent.data?.delegates?.[delegateTjpAddr];
1274
+ iReckon(sir, delegateInfo).asTo('delegate info registered').isGonnaBeTruthy();
1275
+ iReckon(sir, delegateInfo?.delegateTjpAddr).asTo('delegate tjp matches').willEqual(delegateTjpAddr);
1276
+ });
1277
+
1278
+ await ifWeMight(sir, 'gets correct delegate status via parent TJP address', async () => {
1279
+ const parentTjpAddr = getTjpAddr({ ibGib: parentKeystone });
1280
+ if (!parentTjpAddr) { throw new Error(`(UNEXPECTED) parentTjpAddr falsy? (E: b7ce2835ccd951d8c99257328be3b826)`); }
1281
+ const delegateTjpAddr = getTjpAddr({ ibGib: delegateKeystone });
1282
+ if (!delegateTjpAddr) { throw new Error(`(UNEXPECTED) delegateTjpAddr falsy? (E: 3e4878ea19dd8034e827466a8bf09526)`); }
1283
+
1284
+ // Before registration
1285
+ const statusBefore = await service.getDelegateStatus({
1286
+ parentTjpAddr: parentTjpAddr,
1287
+ delegateTjpAddr: delegateTjpAddr,
1288
+ metaspace: mockMetaspace,
1289
+ space: mockSpace as any,
1290
+ });
1291
+ iReckon(sir, statusBefore.registered).asTo('initially unregistered').isGonnaBeFalse();
1292
+
1293
+ // Register
1294
+ const evolvedParent = await service.registerDelegate({
1295
+ parentKeystone,
1296
+ delegateKeystone,
1297
+ masterSecret: parentSecret,
1298
+ metaspace: mockMetaspace,
1299
+ space: mockSpace as any,
1300
+ });
1301
+
1302
+ // After registration
1303
+ const statusAfter = await service.getDelegateStatus({
1304
+ parentTjpAddr: parentTjpAddr,
1305
+ delegateTjpAddr: delegateTjpAddr,
1306
+ metaspace: mockMetaspace,
1307
+ space: mockSpace as any,
1308
+ });
1309
+ iReckon(sir, statusAfter.registered).asTo('registered status').isGonnaBeTrue();
1310
+ iReckon(sir, statusAfter.delegateInfo?.delegateTjpAddr).asTo('status tjp matches').willEqual(delegateTjpAddr);
1311
+ });
1312
+
1313
+ await ifWeMight(sir, 'unregisters a registered delegate keystone', async () => {
1314
+ // Register first
1315
+ const evolvedParent = await service.registerDelegate({
1316
+ parentKeystone,
1317
+ delegateKeystone,
1318
+ masterSecret: parentSecret,
1319
+ metaspace: mockMetaspace,
1320
+ space: mockSpace as any,
1321
+ });
1322
+
1323
+ const delegateTjpAddr = getTjpAddr({ ibGib: delegateKeystone });
1324
+ if (!delegateTjpAddr) { throw new Error(`(UNEXPECTED) delegateTjpAddr falsy? (E: 486b486a8c984a52b8d2b22418c20d26)`); }
1325
+
1326
+ // Unregister
1327
+ const finalParent = await service.unregisterDelegate({
1328
+ parentKeystone: evolvedParent,
1329
+ delegateTjpAddr,
1330
+ masterSecret: parentSecret,
1331
+ metaspace: mockMetaspace,
1332
+ space: mockSpace as any,
1333
+ });
1334
+
1335
+ iReckon(sir, finalParent).asTo('final evolved parent exists').isGonnaBeTruthy();
1336
+ const info = finalParent.data?.delegates?.[delegateTjpAddr];
1337
+ iReckon(sir, info).asTo('delegate info removed').isGonnaBeUndefined();
1338
+ });
1339
+
1340
+ await ifWeMight(sir, 'throws if parent does not have a manage pool', async () => {
1341
+ // Create a parent WITHOUT a manage pool
1342
+ const salt = (await getUUID()).substring(0, 16);
1343
+ const defaultConfig = createStandardPoolConfig({ id: POOL_ID_DEFAULT, salt });
1344
+ const restrictedParent = await service.genesis({
1345
+ masterSecret: parentSecret,
1346
+ configs: [defaultConfig],
1347
+ metaspace: mockMetaspace,
1348
+ space: mockSpace as any,
1349
+ });
1350
+
1351
+ let errorCaught = false;
1352
+ try {
1353
+ await service.registerDelegate({
1354
+ parentKeystone: restrictedParent,
1355
+ delegateKeystone,
1356
+ masterSecret: parentSecret,
1357
+ metaspace: mockMetaspace,
1358
+ space: mockSpace as any,
1359
+ });
1360
+ } catch (e: any) {
1361
+ errorCaught = true;
1362
+ iReckon(sir, e.message).includes('Manage pool is required');
1363
+ }
1364
+ iReckon(sir, errorCaught).asTo('delegation rejected').isGonnaBeTrue();
1365
+ });
1366
+ });
1367
+
1368
+ await respecfully(sir, 'Suite G: Delegate & IB Validation', async () => {
1369
+ const service = new KeystoneService_V1();
1370
+ const parentSecret = "ParentMasterSecret_SuiteG";
1371
+ const delegateSecret = "DelegateMasterSecret_SuiteG";
1372
+
1373
+ let mockSpace: MockIbGibSpace;
1374
+ let mockMetaspace: any;
1375
+
1376
+ let parentKeystone: KeystoneIbGib_V1;
1377
+ let delegateKeystone: KeystoneIbGib_V1;
1378
+
1379
+ firstOfEach(sir, async () => {
1380
+ mockSpace = new MockIbGibSpace();
1381
+ mockMetaspace = new MockMetaspaceService(mockSpace);
1382
+
1383
+ const salt1 = (await getUUID()).substring(0, 16);
1384
+ const salt2 = (await getUUID()).substring(0, 16);
1385
+
1386
+ const manageConfig = createStandardPoolConfig({ id: POOL_ID_MANAGE, salt: salt2 });
1387
+ manageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE];
1388
+
1389
+ const configs = [
1390
+ createStandardPoolConfig({ id: POOL_ID_DEFAULT, salt: salt1 }),
1391
+ manageConfig
1392
+ ];
1393
+
1394
+ parentKeystone = await service.genesis({
1395
+ masterSecret: parentSecret,
1396
+ configs,
1397
+ metaspace: mockMetaspace,
1398
+ space: mockSpace as any,
1399
+ });
1400
+
1401
+ delegateKeystone = await service.genesis({
1402
+ masterSecret: delegateSecret,
1403
+ configs: [createStandardPoolConfig({ id: POOL_ID_DEFAULT, salt: salt1 })],
1404
+ metaspace: mockMetaspace,
1405
+ space: mockSpace as any,
1406
+ });
1407
+ });
1408
+
1409
+ await ifWeMight(sir, 'validateKeystoneIb returns true for valid keystone ib and false for others', async () => {
1410
+ iReckon(sir, await validateKeystoneIb({ ib: 'keystone' })).isGonnaBeTrue();
1411
+ iReckon(sir, await validateKeystoneIb({ ib: 'keystone user info' })).isGonnaBeTrue();
1412
+ iReckon(sir, await validateKeystoneIb({ ib: 'invalidAtom' })).isGonnaBeFalse();
1413
+ iReckon(sir, await validateKeystoneIb({ ib: 'not_keystone sub_atom' })).isGonnaBeFalse();
1414
+ });
1415
+
1416
+ await ifWeMight(sir, 'validateGenesisKeystone fails validation if ib is invalid', async () => {
1417
+ const invalidGenesis = {
1418
+ ...parentKeystone,
1419
+ ib: 'not_a_keystone^' + parentKeystone.gib,
1420
+ };
1421
+ const errors = await validateGenesisKeystone({ keystoneIbGib: invalidGenesis });
1422
+ iReckon(sir, errors.length > 0).isGonnaBeTrue();
1423
+ iReckon(sir, errors[0]).includes('invalid keystone ib');
1424
+ });
1425
+
1426
+ await ifWeMight(sir, 'validateKeystoneTransition passes for valid delegate registration', async () => {
1427
+ const evolvedParent = await service.registerDelegate({
1428
+ parentKeystone,
1429
+ delegateKeystone,
1430
+ masterSecret: parentSecret,
1431
+ metaspace: mockMetaspace,
1432
+ space: mockSpace as any,
1433
+ });
1434
+
1435
+ const errors = await validateKeystoneTransition({
1436
+ currentIbGib: evolvedParent,
1437
+ prevIbGib: parentKeystone,
1438
+ });
1439
+ iReckon(sir, errors.length).willEqual(0);
1440
+ });
1441
+
1442
+ await ifWeMight(sir, 'validateKeystoneTransition fails if delegates map is mutated without manage proof', async () => {
1443
+ const evolvedParent = await service.registerDelegate({
1444
+ parentKeystone,
1445
+ delegateKeystone,
1446
+ masterSecret: parentSecret,
1447
+ metaspace: mockMetaspace,
1448
+ space: mockSpace as any,
1449
+ });
1450
+
1451
+ const tamperedParent = JSON.parse(JSON.stringify(evolvedParent)) as KeystoneIbGib_V1;
1452
+
1453
+ // Remove the manage verb proof by converting it to sign
1454
+ for (const proof of tamperedParent.data.proofs) {
1455
+ if (proof.claim) {
1456
+ proof.claim.verb = 'sign';
1457
+ }
1458
+ }
1459
+
1460
+ const errors = await validateKeystoneTransition({
1461
+ currentIbGib: tamperedParent,
1462
+ prevIbGib: parentKeystone,
1463
+ });
1464
+ iReckon(sir, errors.length > 0).isGonnaBeTrue();
1465
+ iReckon(sir, errors.some(e => e.includes('Policy Violation: Delegates map was mutated'))).isGonnaBeTrue();
1466
+ });
1467
+
1468
+ await ifWeMight(sir, 'validateKeystoneTransition fails if delegate key does not match delegateTjpAddr', async () => {
1469
+ const evolvedParent = await service.registerDelegate({
1470
+ parentKeystone,
1471
+ delegateKeystone,
1472
+ masterSecret: parentSecret,
1473
+ metaspace: mockMetaspace,
1474
+ space: mockSpace as any,
1475
+ });
1476
+
1477
+ const tamperedParent = JSON.parse(JSON.stringify(evolvedParent)) as KeystoneIbGib_V1;
1478
+ const delegateTjpAddr = getTjpAddr({ ibGib: delegateKeystone });
1479
+ if (!delegateTjpAddr) { throw new Error(`delegateTjpAddr falsy?`); }
1480
+
1481
+ const info = tamperedParent.data.delegates![delegateTjpAddr];
1482
+ delete tamperedParent.data.delegates![delegateTjpAddr];
1483
+ tamperedParent.data.delegates!['mismatched_key'] = info;
1484
+
1485
+ const errors = await validateKeystoneTransition({
1486
+ currentIbGib: tamperedParent,
1487
+ prevIbGib: parentKeystone,
1488
+ });
1489
+ iReckon(sir, errors.length > 0).isGonnaBeTrue();
1490
+ iReckon(sir, errors.some(e => e.includes('does not match delegateTjpAddr'))).isGonnaBeTrue();
1491
+ });
1492
+
1493
+ await ifWeMight(sir, 'validateKeystoneTransition fails if delegate addresses have invalid keystone ib', async () => {
1494
+ const evolvedParent = await service.registerDelegate({
1495
+ parentKeystone,
1496
+ delegateKeystone,
1497
+ masterSecret: parentSecret,
1498
+ metaspace: mockMetaspace,
1499
+ space: mockSpace as any,
1500
+ });
1501
+
1502
+ const tamperedParent = JSON.parse(JSON.stringify(evolvedParent)) as KeystoneIbGib_V1;
1503
+ const delegateTjpAddr = getTjpAddr({ ibGib: delegateKeystone });
1504
+ if (!delegateTjpAddr) { throw new Error(`delegateTjpAddr falsy?`); }
1505
+
1506
+ tamperedParent.data.delegates![delegateTjpAddr].delegateAddr = 'invalid_ib^12345';
1507
+
1508
+ const errors = await validateKeystoneTransition({
1509
+ currentIbGib: tamperedParent,
1510
+ prevIbGib: parentKeystone,
1511
+ });
1512
+ iReckon(sir, errors.length > 0).isGonnaBeTrue();
1513
+ iReckon(sir, errors.some(e => e.includes('not a valid keystone address'))).isGonnaBeTrue();
1514
+ });
1515
+
1516
+ await ifWeMight(sir, 'validateKeystoneTransition fails if thisAddr is not in the parent timeline history', async () => {
1517
+ const evolvedParent = await service.registerDelegate({
1518
+ parentKeystone,
1519
+ delegateKeystone,
1520
+ masterSecret: parentSecret,
1521
+ metaspace: mockMetaspace,
1522
+ space: mockSpace as any,
1523
+ });
1524
+
1525
+ const tamperedParent = JSON.parse(JSON.stringify(evolvedParent)) as KeystoneIbGib_V1;
1526
+ const delegateTjpAddr = getTjpAddr({ ibGib: delegateKeystone });
1527
+ if (!delegateTjpAddr) { throw new Error(`delegateTjpAddr falsy?`); }
1528
+
1529
+ tamperedParent.data.delegates![delegateTjpAddr].thisAddr = 'keystone^randomgib';
1530
+
1531
+ const errors = await validateKeystoneTransition({
1532
+ currentIbGib: tamperedParent,
1533
+ prevIbGib: parentKeystone,
1534
+ });
1535
+ iReckon(sir, errors.length > 0).isGonnaBeTrue();
1536
+ iReckon(sir, errors.some(e => e.includes('not a valid historical address in the parent keystone timeline'))).isGonnaBeTrue();
1537
+ });
1163
1538
  });
@@ -314,6 +314,50 @@ export interface KeystoneRevocationInfo {
314
314
  proof: KeystoneProof;
315
315
  }
316
316
 
317
+ /**
318
+ * Shape of info we collect about delegated keystones. We can check
319
+ * against this info later when validating any identity delegation requests.
320
+ */
321
+ export interface DelegateKeystoneInfo {
322
+ /**
323
+ * The Timeline Junction Point (TJP) address of the delegate keystone.
324
+ * Serves as its immutable, unique identifier.
325
+ */
326
+ delegateTjpAddr: IbGibAddr;
327
+ /**
328
+ * concrete addr of **DELEGATE** keystone **at the time when the
329
+ * delegate was added.**
330
+ *
331
+ * So if we have a delegate keystone at D3, then this value would be the
332
+ * addr of that D3 keystone. This will have both a `punctiliarHash` and a
333
+ * `tjpGib` in its `GibInfo`.
334
+ */
335
+ delegateAddr: IbGibAddr;
336
+ /**
337
+ * concrete keystone addr of **THIS** parent keystone **at the time
338
+ * when the delegate was added.**
339
+ *
340
+ * So if we have a delegate keystone at D3, then this value would be the
341
+ * addr of that D3 keystone. This will have both a `punctiliarHash` and a
342
+ * `tjpGib` in its `GibInfo`.
343
+ */
344
+ thisAddr: IbGibAddr;
345
+ }
346
+
347
+ /**
348
+ * Status response when checking if a delegate keystone is registered.
349
+ */
350
+ export interface DelegateKeystoneStatus {
351
+ /**
352
+ * True if the delegate is currently registered on the parent.
353
+ */
354
+ registered: boolean;
355
+ /**
356
+ * Truthy only if registered is true.
357
+ */
358
+ delegateInfo?: DelegateKeystoneInfo;
359
+ }
360
+
317
361
  // ===========================================================================
318
362
  // TOP LEVEL IBGIB DATA
319
363
  // ===========================================================================
@@ -351,6 +395,12 @@ export interface KeystoneData_V1 extends IbGibData_V1 {
351
395
  * Acts as a snapshot to avoid walking the entire timeline.
352
396
  */
353
397
  checkpointDetails?: any;
398
+
399
+ /**
400
+ * Map of delegated keystones soft-linked to this parent keystone.
401
+ * Keyed by the delegate keystone's tjp address.
402
+ */
403
+ delegates?: { [delegateTjpAddr: IbGibAddr]: DelegateKeystoneInfo };
354
404
  }
355
405
 
356
406
  export interface KeystoneRel8ns_V1 extends IbGibRel8ns_V1 {