@ibgib/core-gib 0.1.64 → 0.1.65

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.
@@ -20,6 +20,7 @@ import {
20
20
  DelegateKeystoneInfo,
21
21
  ClaimDetails,
22
22
  ClaimDetails_AddPool,
23
+ ClaimDetails_RemovePool,
23
24
  ClaimDetails_ReplacePool,
24
25
  ClaimDetails_ChangePassword
25
26
  } from "./keystone-types.mjs";
@@ -468,6 +469,13 @@ export async function solveAndReplenish({
468
469
 
469
470
  const strategy = KeystoneStrategyFactory.create({ config: pool.config });
470
471
  const poolSecret = await strategy.derivePoolSecret({ masterSecret });
472
+
473
+ // Verify secret correctness before solving
474
+ const isSecretCorrect = await verifyPoolSecret({ pool, signingSecret: masterSecret });
475
+ if (!isSecretCorrect) {
476
+ throw new Error(`Crypto Violation: Invalid signing secret for pool: ${pool.id} (E: 8a4c2b1d3e5f6a7b8c9d0e1f2a3b4c5f)`);
477
+ }
478
+
471
479
  const solutions: KeystoneSolution[] = [];
472
480
 
473
481
  // 1. Solve
@@ -877,6 +885,8 @@ export async function validateKeystoneTransition({
877
885
  }
878
886
 
879
887
  // Claim details verification (transition validation)
888
+ const signingPoolIds = new Set(currData.proofs.map(p => p.solutions?.[0]?.poolId).filter(Boolean));
889
+
880
890
  for (const proof of currData.proofs) {
881
891
  const claim = proof.claim;
882
892
  if (claim?.details) {
@@ -918,11 +928,12 @@ export async function validateKeystoneTransition({
918
928
  errors.push(`Existing pool ${prevPool.id} was removed during add-pool transition. (E: 5fa2a8cb28c9b60e68994511a5825)`);
919
929
  continue;
920
930
  }
921
- // Verify configuration and challenges did not change
931
+ // Verify configuration did not change
922
932
  if (JSON.stringify(prevPool.config) !== JSON.stringify(currPool.config)) {
923
933
  errors.push(`Existing pool config ${prevPool.id} was mutated during add-pool transition. (E: 6fa2a8cb28c9b60e68994511a5825)`);
924
934
  }
925
- if (JSON.stringify(prevPool.challenges) !== JSON.stringify(currPool.challenges)) {
935
+ // Verify challenges did not change, unless this was the signing pool used to authorize the transition
936
+ if (!signingPoolIds.has(prevPool.id) && JSON.stringify(prevPool.challenges) !== JSON.stringify(currPool.challenges)) {
926
937
  errors.push(`Existing pool challenges ${prevPool.id} were mutated during add-pool transition. (E: 7fa2a8cb28c9b60e68994511a5825)`);
927
938
  }
928
939
  }
@@ -959,10 +970,52 @@ export async function validateKeystoneTransition({
959
970
  if (JSON.stringify(prevPool.config) !== JSON.stringify(currPool.config)) {
960
971
  errors.push(`Unrelated pool config ${prevPool.id} was mutated during replace-pool. (E: bfa2a8cb28c9b60e68994511a5825)`);
961
972
  }
962
- if (JSON.stringify(prevPool.challenges) !== JSON.stringify(currPool.challenges)) {
973
+ if (!signingPoolIds.has(prevPool.id) && JSON.stringify(prevPool.challenges) !== JSON.stringify(currPool.challenges)) {
963
974
  errors.push(`Unrelated pool challenges ${prevPool.id} were mutated during replace-pool. (E: cfa2a8cb28c9b60e68994511a5825)`);
964
975
  }
965
976
  }
977
+ } else if (type === 'remove-pool') {
978
+ const removeInfo = info as ClaimDetails_RemovePool;
979
+ if (!removeInfo || !Array.isArray(removeInfo.remove) || removeInfo.remove.length === 0) {
980
+ errors.push(`details type is '${type}' but payload is invalid. (E: b7a2a8cb28c9b60e68994511a5825)`);
981
+ continue;
982
+ }
983
+
984
+ const removeSet = new Set(removeInfo.remove);
985
+
986
+ // Verify that the set of pool IDs in current equals previous minus the removed pools
987
+ const prevIds = new Set(prevData.challengePools.map(p => p.id));
988
+ const currIds = new Set(currData.challengePools.map(p => p.id));
989
+
990
+ for (const id of removeInfo.remove) {
991
+ if (!prevIds.has(id)) {
992
+ errors.push(`ClaimDetails_RemovePool lists pool ${id} but it is missing in the previous keystone. (E: c7a2a8cb28c9b60e68994511a5825)`);
993
+ }
994
+ if (currIds.has(id)) {
995
+ errors.push(`ClaimDetails_RemovePool lists pool ${id} but it is still present in the evolved keystone. (E: d7a2a8cb28c9b60e68994511a5825)`);
996
+ }
997
+ }
998
+
999
+ // Verify size
1000
+ if (currData.challengePools.length !== prevData.challengePools.length - removeInfo.remove.length) {
1001
+ errors.push(`Structural mismatch: expected ${prevData.challengePools.length - removeInfo.remove.length} pools, found ${currData.challengePools.length}. (E: e7a2a8cb28c9b60e68994511a5825)`);
1002
+ }
1003
+
1004
+ // Verify all remaining pools are unmodified
1005
+ for (const prevPool of prevData.challengePools) {
1006
+ if (removeSet.has(prevPool.id)) continue;
1007
+ const currPool = currData.challengePools.find(p => p.id === prevPool.id);
1008
+ if (!currPool) {
1009
+ errors.push(`Pool ${prevPool.id} was unexpectedly removed. (E: f7a2a8cb28c9b60e68994511a5825)`);
1010
+ continue;
1011
+ }
1012
+ if (JSON.stringify(prevPool.config) !== JSON.stringify(currPool.config)) {
1013
+ errors.push(`Remaining pool config ${prevPool.id} was mutated during remove-pool. (E: 07a2a8cb28c9b60e68994511a5825)`);
1014
+ }
1015
+ if (!signingPoolIds.has(prevPool.id) && JSON.stringify(prevPool.challenges) !== JSON.stringify(currPool.challenges)) {
1016
+ errors.push(`Remaining pool challenges ${prevPool.id} were mutated during remove-pool. (E: 17a2a8cb28c9b60e68994511a5825)`);
1017
+ }
1018
+ }
966
1019
 
967
1020
  } else if (type === 'change-password') {
968
1021
  const changeInfo = info as ClaimDetails_ChangePassword;
@@ -1004,7 +1057,7 @@ export async function validateKeystoneTransition({
1004
1057
  if (JSON.stringify(prevPool.config) !== JSON.stringify(currPool.config)) {
1005
1058
  errors.push(`Unrelated pool config ${prevPool.id} was mutated during change-password. (E: 02b2a8cb28c9b60e68994511a5825)`);
1006
1059
  }
1007
- if (JSON.stringify(prevPool.challenges) !== JSON.stringify(currPool.challenges)) {
1060
+ if (!signingPoolIds.has(prevPool.id) && JSON.stringify(prevPool.challenges) !== JSON.stringify(currPool.challenges)) {
1008
1061
  errors.push(`Unrelated pool challenges ${prevPool.id} were mutated during change-password. (E: 03b2a8cb28c9b60e68994511a5825)`);
1009
1062
  }
1010
1063
  }
@@ -1488,4 +1541,36 @@ export async function rotatePoolChallenges({
1488
1541
  ...(prevPool.isForeign !== undefined ? { isForeign: prevPool.isForeign } : {}),
1489
1542
  ...(prevPool.metadata ? { metadata: prevPool.metadata } : {})
1490
1543
  };
1544
+ }
1545
+
1546
+ /**
1547
+ * Pure helper to verify if a candidate signing secret (passphrase) is correct for a specific challenge pool.
1548
+ * Does not mutate any state.
1549
+ */
1550
+ export async function verifyPoolSecret({
1551
+ pool,
1552
+ signingSecret
1553
+ }: {
1554
+ pool: KeystoneChallengePool;
1555
+ signingSecret: string;
1556
+ }): Promise<boolean> {
1557
+ const challengeIds = Object.keys(pool.challenges);
1558
+ if (challengeIds.length === 0) return false;
1559
+
1560
+ try {
1561
+ const firstId = challengeIds[0];
1562
+ const challenge = pool.challenges[firstId];
1563
+
1564
+ const strategy = KeystoneStrategyFactory.create({ config: pool.config });
1565
+ const poolSecret = await strategy.derivePoolSecret({ masterSecret: signingSecret });
1566
+ const solution = await strategy.generateSolution({
1567
+ poolSecret,
1568
+ poolId: pool.id,
1569
+ challengeId: firstId
1570
+ });
1571
+
1572
+ return await strategy.validateSolution({ solution, challenge });
1573
+ } catch {
1574
+ return false;
1575
+ }
1491
1576
  }
@@ -8,7 +8,7 @@ import {
8
8
  KeystoneData_V1, KeystoneIbGib_V1, KeystonePoolConfig, KeystoneClaim,
9
9
  KeystoneChallengePool, KeystoneReplenishStrategy, KeystoneRevocationInfo,
10
10
  DelegateKeystoneStatus, KeystoneClaimType,
11
- ClaimDetails_AddPool, ClaimDetails_ReplacePool, ClaimDetails_ChangePassword
11
+ ClaimDetails_AddPool, ClaimDetails_RemovePool, ClaimDetails_ReplacePool, ClaimDetails_ChangePassword
12
12
  } from './keystone-types.mjs';
13
13
  import { KeystoneStrategyFactory } from './strategy/keystone-strategy-factory.mjs';
14
14
  import { createStandardPoolConfig } from './keystone-config-builder.mjs';
@@ -24,7 +24,7 @@ import {
24
24
  generateOpaqueChallengeId, resolveTargetPool, selectChallengeIds,
25
25
  solveAndReplenish, validateKeystoneTransition,
26
26
  validateGenesisKeystone, validateKeystoneGraph,
27
- generatePoolChallenges, rotatePoolChallenges
27
+ generatePoolChallenges, rotatePoolChallenges, verifyPoolSecret
28
28
  } from './keystone-helpers.mjs';
29
29
  import { getLatestAddrs } from '../witness/space/space-helper.mjs';
30
30
  import { IbGibSpaceAny } from '../witness/space/space-base-v1.mjs';
@@ -796,6 +796,128 @@ export class KeystoneService_V1 {
796
796
  }
797
797
  }
798
798
 
799
+ /**
800
+ * Structural evolution: Removes challenge pools from the keystone.
801
+ *
802
+ * Use Case: Unlinking a custodian or delegate SSO pool.
803
+ *
804
+ * Requires the Master Secret to authorize the change via a pool containing
805
+ * the 'manage' verb (the administrative manage pool).
806
+ */
807
+ async removePools({
808
+ latestKeystone,
809
+ masterSecret,
810
+ poolIds,
811
+ metaspace,
812
+ space,
813
+ }: {
814
+ latestKeystone: KeystoneIbGib_V1;
815
+ /**
816
+ * Alice's Master Secret.
817
+ * Required to solve challenges from the Admin/Manage pool to authorize this change.
818
+ */
819
+ masterSecret: string;
820
+ /**
821
+ * The IDs of the pools to remove.
822
+ */
823
+ poolIds: string[];
824
+ metaspace: MetaspaceService;
825
+ space: IbGibSpaceAny;
826
+ }): Promise<KeystoneIbGib_V1> {
827
+ const lc = `${this.lc}[${this.removePools.name}]`;
828
+ try {
829
+ if (logalot) { console.log(`${lc} starting...`); }
830
+
831
+ if (!latestKeystone.data) { throw new Error(`(UNEXPECTED) latestKeystone.data falsy? (E: 8334c8faed128166a999d428c7805b25)`); }
832
+ const prevData = latestKeystone.data;
833
+
834
+ if (prevData.revocationInfo) { throw new Error(`Keystone has been revoked. Cannot remove pools. (E: 9599f8f51c78d722252ddb2894fdbe25)`); }
835
+
836
+ if (poolIds.length === 0) { throw new Error(`No pool IDs provided to remove. (E: 7599f8f51c78d722252ddb2894fdbe25)`); }
837
+
838
+ // 1. Identify Authority (Resolve Admin Pool)
839
+ const adminPool = resolveTargetPool({
840
+ pools: prevData.challengePools,
841
+ verb: KEYSTONE_VERB_MANAGE,
842
+ });
843
+
844
+ if (logalot) { console.log(`${lc} Authorized via pool: ${adminPool.id}`); }
845
+
846
+ // 2. Construct the Management Claim with typed details
847
+ const target = getIbGibAddr({ ibGib: latestKeystone });
848
+ const claim: Partial<KeystoneClaim> = {
849
+ verb: KEYSTONE_VERB_MANAGE,
850
+ target,
851
+ details: {
852
+ type: KeystoneClaimType.remove_pool,
853
+ info: { remove: poolIds } as ClaimDetails_RemovePool
854
+ }
855
+ };
856
+
857
+ // 3. Calculate Costs
858
+ const idsToSolve = await selectChallengeIds({
859
+ pool: adminPool,
860
+ targetAddr: target
861
+ });
862
+
863
+ // 4. Pay the Cost (Solve & Replenish)
864
+ const { proof, nextPools: replenishedExistingPools } = await solveAndReplenish({
865
+ targetPoolId: adminPool.id,
866
+ prevPools: prevData.challengePools,
867
+ masterSecret,
868
+ challengeIds: idsToSolve,
869
+ claim,
870
+ });
871
+
872
+ // 5. Mutate Structure (Remove the pools)
873
+ const removeSet = new Set(poolIds);
874
+ const finalPools = replenishedExistingPools.filter(p => !removeSet.has(p.id));
875
+
876
+ // Verify that we actually removed the expected count of pools
877
+ if (finalPools.length !== replenishedExistingPools.length - poolIds.length) {
878
+ const missingIds = poolIds.filter(id => !replenishedExistingPools.some(p => p.id === id));
879
+ throw new Error(`Cannot remove pool. Pool IDs not found in keystone: ${missingIds.join(', ')} (E: 8a4c2b1d3e5f6a7b8c9d0e1f2a3b4c5e)`);
880
+ }
881
+
882
+ // 6. Construct New Data
883
+ const n = (prevData.n ?? 0) + 1;
884
+ let checkpointDetails: any | undefined;
885
+ if (n % KEYSTONE_CHECKPOINT_FREQUENCY === 0) {
886
+ const currentFrameView: KeystoneIbGib_V1 = {
887
+ ...latestKeystone,
888
+ data: { ...prevData, challengePools: finalPools }
889
+ };
890
+ checkpointDetails = await this.getAggregateDetails({
891
+ latestKeystone: currentFrameView,
892
+ metaspace,
893
+ space
894
+ });
895
+ }
896
+
897
+ const newData: KeystoneData_V1 = {
898
+ challengePools: finalPools,
899
+ proofs: [proof],
900
+ checkpointDetails,
901
+ ...(prevData.delegates ? { delegates: prevData.delegates } : {}),
902
+ };
903
+
904
+ // 7. Commit
905
+ const newKeystone = await evolvePersistAndRegisterKeystone({
906
+ prevIbGib: latestKeystone,
907
+ newData,
908
+ metaspace,
909
+ space
910
+ });
911
+
912
+ return newKeystone;
913
+ } catch (error) {
914
+ console.error(`${lc} ${extractErrorMsg(error)}`);
915
+ throw error;
916
+ } finally {
917
+ if (logalot) { console.log(`${lc} complete.`); }
918
+ }
919
+ }
920
+
799
921
  /**
800
922
  * Consolidates the state of a Keystone identity across its history.
801
923
  * Walks back until it hits a checkpoint or genesis.
@@ -1213,4 +1335,30 @@ export class KeystoneService_V1 {
1213
1335
  throw error;
1214
1336
  }
1215
1337
  }
1338
+
1339
+ /**
1340
+ * Verifies if a candidate signing secret (passphrase) is correct for a specific challenge pool.
1341
+ * Does not mutate any state.
1342
+ */
1343
+ async verifySigningSecret({
1344
+ keystoneIbGib,
1345
+ signingSecret,
1346
+ poolId
1347
+ }: {
1348
+ keystoneIbGib: KeystoneIbGib_V1;
1349
+ signingSecret: string;
1350
+ poolId: string;
1351
+ }): Promise<boolean> {
1352
+ const lc = `${this.lc}[${this.verifySigningSecret.name}]`;
1353
+ try {
1354
+ if (!keystoneIbGib.data) return false;
1355
+ const pool = keystoneIbGib.data.challengePools.find(p => p.id === poolId);
1356
+ if (!pool) return false;
1357
+
1358
+ return await verifyPoolSecret({ pool, signingSecret });
1359
+ } catch (error) {
1360
+ console.warn(`${lc} Exception during verification: ${extractErrorMsg(error)}`);
1361
+ return false;
1362
+ }
1363
+ }
1216
1364
  }
@@ -1167,7 +1167,7 @@ await respecfully(sir, 'Suite F: Deep Inspection', async () => {
1167
1167
  .withDescription('Alice domain keystone');
1168
1168
 
1169
1169
  const configs = await builder.compileConfigs();
1170
- iReckon(sir, configs.length).asTo('number of pools').willEqual(4);
1170
+ iReckon(sir, configs.length).asTo('number of pools').willEqual(1);
1171
1171
 
1172
1172
  const connectPool = configs.find(c => c.id === 'connect');
1173
1173
  iReckon(sir, connectPool).asTo('connect pool omitted').isGonnaBeUndefined();
@@ -1964,6 +1964,61 @@ await respecfully(sir, 'Suite G: Combined Pools & Password Rotation', async () =
1964
1964
  currentIbGib: rotated
1965
1965
  });
1966
1966
  iReckon(sir, errors.length).willEqual(0);
1967
+
1968
+ // Verify we can sign using the rotated 'manage' pool with the new passphrase
1969
+ const manageClaim = { verb: KEYSTONE_VERB_MANAGE, target: "test_target_manage" };
1970
+ const signedWithNewManage = await service.sign({
1971
+ latestKeystone: rotated,
1972
+ masterSecret: newPassphrase,
1973
+ claim: manageClaim,
1974
+ poolId: POOL_ID_MANAGE,
1975
+ metaspace: mockMetaspace,
1976
+ space: mockSpace as any,
1977
+ });
1978
+ iReckon(sir, signedWithNewManage).isGonnaBeTruthy();
1979
+
1980
+ // Verify we can sign using the rotated 'secondary-native' pool with the new passphrase
1981
+ const syncClaim = { verb: KEYSTONE_VERB_SYNC, target: "test_target_sync" };
1982
+ const signedWithNewSync = await service.sign({
1983
+ latestKeystone: rotated,
1984
+ masterSecret: newPassphrase,
1985
+ claim: syncClaim,
1986
+ poolId: "secondary-native",
1987
+ metaspace: mockMetaspace,
1988
+ space: mockSpace as any,
1989
+ });
1990
+ iReckon(sir, signedWithNewSync).isGonnaBeTruthy();
1991
+
1992
+ // Verify signing with the OLD master secret fails on both pools
1993
+ let oldManageFailed = false;
1994
+ try {
1995
+ await service.sign({
1996
+ latestKeystone: rotated,
1997
+ masterSecret: userSecret,
1998
+ claim: manageClaim,
1999
+ poolId: POOL_ID_MANAGE,
2000
+ metaspace: mockMetaspace,
2001
+ space: mockSpace as any,
2002
+ });
2003
+ } catch {
2004
+ oldManageFailed = true;
2005
+ }
2006
+ iReckon(sir, oldManageFailed).isGonnaBeTrue();
2007
+
2008
+ let oldSyncFailed = false;
2009
+ try {
2010
+ await service.sign({
2011
+ latestKeystone: rotated,
2012
+ masterSecret: userSecret,
2013
+ claim: syncClaim,
2014
+ poolId: "secondary-native",
2015
+ metaspace: mockMetaspace,
2016
+ space: mockSpace as any,
2017
+ });
2018
+ } catch {
2019
+ oldSyncFailed = true;
2020
+ }
2021
+ iReckon(sir, oldSyncFailed).isGonnaBeTrue();
1967
2022
  });
1968
2023
 
1969
2024
  await ifWeMight(sir, 'fails validation if password rotation details or constraints are violated', async () => {
@@ -2046,4 +2101,130 @@ await respecfully(sir, 'Suite G: Combined Pools & Password Rotation', async () =
2046
2101
  const hasForeignViolation = errors.some(e => e.includes("Security Violation") && e.includes("foreign pool"));
2047
2102
  iReckon(sir, hasForeignViolation).isGonnaBeTrue();
2048
2103
  });
2104
+
2105
+ await ifWeMight(sir, 'verifies signing secrets correctly for a pool', async () => {
2106
+ const userSecret = "alice_secret_123";
2107
+ const builder = KeystoneProfileBuilder.buildKeystone('low')
2108
+ .withUsername('alice')
2109
+ .withEmail('alice@example.com')
2110
+ .withDescription('Alice test keystone');
2111
+
2112
+ const configs = await builder.compileConfigs();
2113
+ const keystone = await service.genesis({
2114
+ masterSecret: userSecret,
2115
+ configs,
2116
+ frameDetails: builder.getFrameDetails(),
2117
+ metaspace: mockMetaspace,
2118
+ space: mockSpace as any,
2119
+ });
2120
+
2121
+ // 1. Correct secret on manage pool
2122
+ const isCorrectManage = await service.verifySigningSecret({
2123
+ keystoneIbGib: keystone,
2124
+ signingSecret: userSecret,
2125
+ poolId: POOL_ID_MANAGE
2126
+ });
2127
+ iReckon(sir, isCorrectManage).asTo('correct manage secret').isGonnaBeTrue();
2128
+
2129
+ // 2. Incorrect secret on manage pool
2130
+ const isWrongManage = await service.verifySigningSecret({
2131
+ keystoneIbGib: keystone,
2132
+ signingSecret: "wrong_secret",
2133
+ poolId: POOL_ID_MANAGE
2134
+ });
2135
+ iReckon(sir, isWrongManage).asTo('incorrect manage secret').isGonnaBeFalse();
2136
+
2137
+ // 3. Correct secret on default/sign pool
2138
+ const isCorrectDefault = await service.verifySigningSecret({
2139
+ keystoneIbGib: keystone,
2140
+ signingSecret: userSecret,
2141
+ poolId: 'default'
2142
+ });
2143
+ iReckon(sir, isCorrectDefault).asTo('correct default secret').isGonnaBeTrue();
2144
+
2145
+ // 4. Invalid/non-existent pool ID
2146
+ const isInvalidPool = await service.verifySigningSecret({
2147
+ keystoneIbGib: keystone,
2148
+ signingSecret: userSecret,
2149
+ poolId: 'invalid_pool_id_123'
2150
+ });
2151
+ iReckon(sir, isInvalidPool).asTo('invalid pool id').isGonnaBeFalse();
2152
+ });
2153
+
2154
+ await ifWeMight(sir, 'removes pools successfully, validating transitions and auth', async () => {
2155
+ const userSecret = "alice_secret_123";
2156
+ const customPoolId = "custodian-manage-google";
2157
+ const configs = [
2158
+ createStandardPoolConfig({
2159
+ id: POOL_ID_MANAGE,
2160
+ salt: "manage_salt",
2161
+ }),
2162
+ createStandardPoolConfig({
2163
+ id: customPoolId,
2164
+ salt: "custodian_salt",
2165
+ })
2166
+ ];
2167
+
2168
+ const keystone = await service.genesis({
2169
+ masterSecret: userSecret,
2170
+ configs,
2171
+ metaspace: mockMetaspace,
2172
+ space: mockSpace as any,
2173
+ });
2174
+
2175
+ // 1. Success path: remove customPoolId using correct secret
2176
+ const evolvedKeystone = await service.removePools({
2177
+ latestKeystone: keystone,
2178
+ masterSecret: userSecret,
2179
+ poolIds: [customPoolId],
2180
+ metaspace: mockMetaspace,
2181
+ space: mockSpace as any,
2182
+ });
2183
+
2184
+ iReckon(sir, evolvedKeystone).asTo('evolved keystone exists').isGonnaBeTruthy();
2185
+ iReckon(sir, evolvedKeystone.data?.challengePools.length).asTo('pool count reduced').willEqual(1);
2186
+ iReckon(sir, evolvedKeystone.data?.challengePools[0].id).asTo('manage pool remains').willEqual(POOL_ID_MANAGE);
2187
+
2188
+ // Verify transition is fully valid
2189
+ const errors = await service.validate({
2190
+ prevIbGib: keystone,
2191
+ currentIbGib: evolvedKeystone
2192
+ });
2193
+ iReckon(sir, errors.length).asTo('transition validation succeeds').willEqual(0);
2194
+
2195
+ // 2. Sad path: invalid master secret
2196
+ let cryptoErrorCaught = false;
2197
+ try {
2198
+ await service.removePools({
2199
+ latestKeystone: keystone,
2200
+ masterSecret: "wrong_secret",
2201
+ poolIds: [customPoolId],
2202
+ metaspace: mockMetaspace,
2203
+ space: mockSpace as any,
2204
+ });
2205
+ } catch (e: any) {
2206
+ if (e.message.includes('Crypto Violation')) {
2207
+ cryptoErrorCaught = true;
2208
+ }
2209
+ }
2210
+ iReckon(sir, cryptoErrorCaught).asTo('forgery check prevents removal').isGonnaBeTrue();
2211
+
2212
+ // 3. Sad path: non-existent pool ID
2213
+ let notFoundErrorCaught = false;
2214
+ try {
2215
+ await service.removePools({
2216
+ latestKeystone: keystone,
2217
+ masterSecret: userSecret,
2218
+ poolIds: ["non-existent-pool"],
2219
+ metaspace: mockMetaspace,
2220
+ space: mockSpace as any,
2221
+ });
2222
+ } catch (e: any) {
2223
+ if (e.message.includes('not found in keystone')) {
2224
+ notFoundErrorCaught = true;
2225
+ }
2226
+ }
2227
+ iReckon(sir, notFoundErrorCaught).asTo('non-existent pool ID rejected').isGonnaBeTrue();
2228
+ });
2049
2229
  });
2230
+
@@ -258,17 +258,20 @@ export interface KeystoneChallengePool {
258
258
 
259
259
  // #region KeystoneClaimType enum
260
260
  export const KEYSTONE_CLAIM_TYPE_ADD_POOL = 'add-pool';
261
+ export const KEYSTONE_CLAIM_TYPE_REMOVE_POOL = 'remove-pool';
261
262
  export const KEYSTONE_CLAIM_TYPE_REPLACE_POOL = 'replace-pool';
262
263
  export const KEYSTONE_CLAIM_TYPE_CHANGE_PASSWORD = 'change-password';
263
264
 
264
265
  export type KeystoneClaimType =
265
266
  | typeof KEYSTONE_CLAIM_TYPE_ADD_POOL
267
+ | typeof KEYSTONE_CLAIM_TYPE_REMOVE_POOL
266
268
  | typeof KEYSTONE_CLAIM_TYPE_REPLACE_POOL
267
269
  | typeof KEYSTONE_CLAIM_TYPE_CHANGE_PASSWORD
268
270
  ;
269
271
 
270
272
  export const KeystoneClaimType = {
271
273
  add_pool: KEYSTONE_CLAIM_TYPE_ADD_POOL,
274
+ remove_pool: KEYSTONE_CLAIM_TYPE_REMOVE_POOL,
272
275
  replace_pool: KEYSTONE_CLAIM_TYPE_REPLACE_POOL,
273
276
  change_password: KEYSTONE_CLAIM_TYPE_CHANGE_PASSWORD,
274
277
  } satisfies { readonly [key: string]: KeystoneClaimType };
@@ -289,6 +292,10 @@ export interface ClaimDetails_AddPool {
289
292
  add: string[];
290
293
  }
291
294
 
295
+ export interface ClaimDetails_RemovePool {
296
+ remove: string[];
297
+ }
298
+
292
299
  export interface ClaimDetails_ReplacePool {
293
300
  replace: string;
294
301
  }
@@ -5,25 +5,11 @@
5
5
  "securityLevel": "high"
6
6
  },
7
7
  "pools": {
8
- "sync": {
9
- "id": "sync",
10
- "allowedVerbs": [
11
- "sync"
12
- ],
13
- "algo": "SHA-512",
14
- "rounds": 4,
15
- "behavior": {
16
- "size": 2000,
17
- "replenish": "top-up",
18
- "selectSequentially": 10,
19
- "selectRandomly": 10,
20
- "targetBindingCount": 16
21
- }
22
- },
23
8
  "manage": {
24
9
  "id": "manage",
25
10
  "allowedVerbs": [
26
- "manage"
11
+ "manage",
12
+ "revoke"
27
13
  ],
28
14
  "algo": "SHA-512",
29
15
  "rounds": 6,
@@ -34,36 +20,6 @@
34
20
  "selectRandomly": 12,
35
21
  "targetBindingCount": 16
36
22
  }
37
- },
38
- "revoke": {
39
- "id": "revoke",
40
- "allowedVerbs": [
41
- "revoke"
42
- ],
43
- "algo": "SHA-512",
44
- "rounds": 20,
45
- "behavior": {
46
- "size": 2000,
47
- "replenish": "delete-all",
48
- "selectSequentially": 50,
49
- "selectRandomly": 50,
50
- "targetBindingCount": 0
51
- }
52
- },
53
- "sign": {
54
- "id": "default",
55
- "allowedVerbs": [
56
- "sign"
57
- ],
58
- "algo": "SHA-512",
59
- "rounds": 4,
60
- "behavior": {
61
- "size": 2000,
62
- "replenish": "top-up",
63
- "selectSequentially": 10,
64
- "selectRandomly": 10,
65
- "targetBindingCount": 16
66
- }
67
23
  }
68
24
  }
69
25
  }
@@ -23,7 +23,8 @@
23
23
  "manage": {
24
24
  "id": "manage",
25
25
  "allowedVerbs": [
26
- "manage"
26
+ "manage",
27
+ "revoke"
27
28
  ],
28
29
  "algo": "SHA-256",
29
30
  "rounds": 2,
@@ -35,21 +36,6 @@
35
36
  "targetBindingCount": 8
36
37
  }
37
38
  },
38
- "revoke": {
39
- "id": "revoke",
40
- "allowedVerbs": [
41
- "revoke"
42
- ],
43
- "algo": "SHA-256",
44
- "rounds": 5,
45
- "behavior": {
46
- "size": 100,
47
- "replenish": "delete-all",
48
- "selectSequentially": 3,
49
- "selectRandomly": 3,
50
- "targetBindingCount": 0
51
- }
52
- },
53
39
  "connect": {
54
40
  "id": "connect",
55
41
  "allowedVerbs": [