@ibgib/core-gib 0.1.64 → 0.1.66

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 (32) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/keystone/keystone-helpers.d.mts +8 -0
  3. package/dist/keystone/keystone-helpers.d.mts.map +1 -1
  4. package/dist/keystone/keystone-helpers.mjs +74 -4
  5. package/dist/keystone/keystone-helpers.mjs.map +1 -1
  6. package/dist/keystone/keystone-service-v1.d.mts +85 -18
  7. package/dist/keystone/keystone-service-v1.d.mts.map +1 -1
  8. package/dist/keystone/keystone-service-v1.mjs +178 -22
  9. package/dist/keystone/keystone-service-v1.mjs.map +1 -1
  10. package/dist/keystone/keystone-service-v1.respec.mjs +780 -340
  11. package/dist/keystone/keystone-service-v1.respec.mjs.map +1 -1
  12. package/dist/keystone/keystone-types.d.mts +15 -2
  13. package/dist/keystone/keystone-types.d.mts.map +1 -1
  14. package/dist/keystone/keystone-types.mjs +2 -0
  15. package/dist/keystone/keystone-types.mjs.map +1 -1
  16. package/dist/keystone/policy/profiles/profile-domain.json +2 -46
  17. package/dist/keystone/policy/profiles/profile-sync.json +2 -16
  18. package/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-receiver/sync-websocket-peer-helpers.d.mts +27 -4
  19. package/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-receiver/sync-websocket-peer-helpers.d.mts.map +1 -1
  20. package/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-receiver/sync-websocket-peer-helpers.mjs +230 -135
  21. package/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-receiver/sync-websocket-peer-helpers.mjs.map +1 -1
  22. package/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-sender/sync-peer-websocket-sender-v1.mjs +1 -1
  23. package/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-sender/sync-peer-websocket-sender-v1.mjs.map +1 -1
  24. package/package.json +1 -1
  25. package/src/keystone/keystone-helpers.mts +89 -4
  26. package/src/keystone/keystone-service-v1.mts +262 -34
  27. package/src/keystone/keystone-service-v1.respec.mts +792 -376
  28. package/src/keystone/keystone-types.mts +16 -1
  29. package/src/keystone/policy/profiles/profile-domain.json +2 -46
  30. package/src/keystone/policy/profiles/profile-sync.json +2 -16
  31. package/src/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-receiver/sync-websocket-peer-helpers.mts +294 -131
  32. package/src/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-sender/sync-peer-websocket-sender-v1.mts +1 -1
@@ -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,13 +24,14 @@ 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';
31
31
  import { MetaspaceService } from '../witness/space/metaspace/metaspace-types.mjs';
32
32
  import { FlatIbGibGraph } from '../common/other/graph-types.mjs';
33
33
  import { getTjpAddr } from '../common/other/ibgib-helper.mjs';
34
+ import { deriveDelegateSecret } from './strategy/hash-reveal-v1/hash-reveal-v1.mjs';
34
35
 
35
36
  const logalot = GLOBAL_LOG_A_LOT;
36
37
 
@@ -796,6 +797,128 @@ export class KeystoneService_V1 {
796
797
  }
797
798
  }
798
799
 
800
+ /**
801
+ * Structural evolution: Removes challenge pools from the keystone.
802
+ *
803
+ * Use Case: Unlinking a custodian or delegate SSO pool.
804
+ *
805
+ * Requires the Master Secret to authorize the change via a pool containing
806
+ * the 'manage' verb (the administrative manage pool).
807
+ */
808
+ async removePools({
809
+ latestKeystone,
810
+ masterSecret,
811
+ poolIds,
812
+ metaspace,
813
+ space,
814
+ }: {
815
+ latestKeystone: KeystoneIbGib_V1;
816
+ /**
817
+ * Alice's Master Secret.
818
+ * Required to solve challenges from the Admin/Manage pool to authorize this change.
819
+ */
820
+ masterSecret: string;
821
+ /**
822
+ * The IDs of the pools to remove.
823
+ */
824
+ poolIds: string[];
825
+ metaspace: MetaspaceService;
826
+ space: IbGibSpaceAny;
827
+ }): Promise<KeystoneIbGib_V1> {
828
+ const lc = `${this.lc}[${this.removePools.name}]`;
829
+ try {
830
+ if (logalot) { console.log(`${lc} starting...`); }
831
+
832
+ if (!latestKeystone.data) { throw new Error(`(UNEXPECTED) latestKeystone.data falsy? (E: 8334c8faed128166a999d428c7805b25)`); }
833
+ const prevData = latestKeystone.data;
834
+
835
+ if (prevData.revocationInfo) { throw new Error(`Keystone has been revoked. Cannot remove pools. (E: 9599f8f51c78d722252ddb2894fdbe25)`); }
836
+
837
+ if (poolIds.length === 0) { throw new Error(`No pool IDs provided to remove. (E: 7599f8f51c78d722252ddb2894fdbe25)`); }
838
+
839
+ // 1. Identify Authority (Resolve Admin Pool)
840
+ const adminPool = resolveTargetPool({
841
+ pools: prevData.challengePools,
842
+ verb: KEYSTONE_VERB_MANAGE,
843
+ });
844
+
845
+ if (logalot) { console.log(`${lc} Authorized via pool: ${adminPool.id}`); }
846
+
847
+ // 2. Construct the Management Claim with typed details
848
+ const target = getIbGibAddr({ ibGib: latestKeystone });
849
+ const claim: Partial<KeystoneClaim> = {
850
+ verb: KEYSTONE_VERB_MANAGE,
851
+ target,
852
+ details: {
853
+ type: KeystoneClaimType.remove_pool,
854
+ info: { remove: poolIds } as ClaimDetails_RemovePool
855
+ }
856
+ };
857
+
858
+ // 3. Calculate Costs
859
+ const idsToSolve = await selectChallengeIds({
860
+ pool: adminPool,
861
+ targetAddr: target
862
+ });
863
+
864
+ // 4. Pay the Cost (Solve & Replenish)
865
+ const { proof, nextPools: replenishedExistingPools } = await solveAndReplenish({
866
+ targetPoolId: adminPool.id,
867
+ prevPools: prevData.challengePools,
868
+ masterSecret,
869
+ challengeIds: idsToSolve,
870
+ claim,
871
+ });
872
+
873
+ // 5. Mutate Structure (Remove the pools)
874
+ const removeSet = new Set(poolIds);
875
+ const finalPools = replenishedExistingPools.filter(p => !removeSet.has(p.id));
876
+
877
+ // Verify that we actually removed the expected count of pools
878
+ if (finalPools.length !== replenishedExistingPools.length - poolIds.length) {
879
+ const missingIds = poolIds.filter(id => !replenishedExistingPools.some(p => p.id === id));
880
+ throw new Error(`Cannot remove pool. Pool IDs not found in keystone: ${missingIds.join(', ')} (E: 8a4c2b1d3e5f6a7b8c9d0e1f2a3b4c5e)`);
881
+ }
882
+
883
+ // 6. Construct New Data
884
+ const n = (prevData.n ?? 0) + 1;
885
+ let checkpointDetails: any | undefined;
886
+ if (n % KEYSTONE_CHECKPOINT_FREQUENCY === 0) {
887
+ const currentFrameView: KeystoneIbGib_V1 = {
888
+ ...latestKeystone,
889
+ data: { ...prevData, challengePools: finalPools }
890
+ };
891
+ checkpointDetails = await this.getAggregateDetails({
892
+ latestKeystone: currentFrameView,
893
+ metaspace,
894
+ space
895
+ });
896
+ }
897
+
898
+ const newData: KeystoneData_V1 = {
899
+ challengePools: finalPools,
900
+ proofs: [proof],
901
+ checkpointDetails,
902
+ ...(prevData.delegates ? { delegates: prevData.delegates } : {}),
903
+ };
904
+
905
+ // 7. Commit
906
+ const newKeystone = await evolvePersistAndRegisterKeystone({
907
+ prevIbGib: latestKeystone,
908
+ newData,
909
+ metaspace,
910
+ space
911
+ });
912
+
913
+ return newKeystone;
914
+ } catch (error) {
915
+ console.error(`${lc} ${extractErrorMsg(error)}`);
916
+ throw error;
917
+ } finally {
918
+ if (logalot) { console.log(`${lc} complete.`); }
919
+ }
920
+ }
921
+
799
922
  /**
800
923
  * Consolidates the state of a Keystone identity across its history.
801
924
  * Walks back until it hits a checkpoint or genesis.
@@ -938,19 +1061,19 @@ export class KeystoneService_V1 {
938
1061
  /**
939
1062
  * Replaces a challenge pool (such as manage or custodian-manage) with a new pool,
940
1063
  * authorizing the transition via the specified signing pool.
941
- *
1064
+ *
942
1065
  * @param latestKeystone The current tip frame of the keystone timeline.
943
1066
  * - Visibility: Public (Public Merkle frame, safe for logs/ledger).
944
1067
  * - Generator: Client/Server (Constructed locally and synced).
945
- *
1068
+ *
946
1069
  * @param newPool The fully constructed new pool to place in the next frame.
947
1070
  * - Visibility: Public (Public configuration and public challenges, safe for ledger).
948
1071
  * - Generator: Client/Server (Generated dynamically by the party proposing the update).
949
- *
1072
+ *
950
1073
  * @param signingSecret The master secret or derived KDF secret of the pool authorizing the change.
951
1074
  * - Visibility: Private (Confidential secret credential. Must never go over the wire).
952
1075
  * - Generator: Client/Server (Known only to the signing party).
953
- *
1076
+ *
954
1077
  * @param signingPoolId The ID of the pool used to authorize the transition (defaults to 'manage').
955
1078
  * - Visibility: Public (Non-sensitive configuration string).
956
1079
  * - Generator: Client/Server (From pool configuration).
@@ -1064,28 +1187,17 @@ export class KeystoneService_V1 {
1064
1187
  /**
1065
1188
  * Evolve the keystone by rotating the primary user manage pool (`manage`) to a new master secret (passphrase).
1066
1189
  * This creates a new frame signed by the specified `signingPoolId` using `signingSecret`.
1067
- *
1068
- * Supports recovery (signing via `custodian-manage` using the custodian secret) and normal rotation
1069
- * (signing via `manage` using the old password).
1070
- *
1071
- * @param latestKeystone The current tip frame of the keystone.
1072
- * - Visibility: Public (Public Merkle frame, safe for ledger).
1073
- * - Generator: Client/Server (Constructed locally and synced).
1074
- *
1075
- * @param newMasterSecret The new passphrase/master secret to set for the user's primary pools.
1076
- * - Visibility: Private (Confidential secret credential. Must never go over the wire).
1077
- * - Generator: Client (From user input in settings).
1078
- *
1079
- * @param signingSecret The secret of the pool authorizing this change (either old passphrase or derived custodian secret).
1080
- * - Visibility: Private (Confidential secret. Must never go over the wire).
1081
- * - Generator: Client/Server (Known only to the signing party).
1082
- *
1083
- * @param signingPoolId The ID of the pool used to authorize the rotation (defaults to 'manage').
1084
- * - Visibility: Public (Non-sensitive ID string).
1085
- * - Generator: Client/Server (From pool configuration).
1190
+ *
1191
+ * Supports recovery (signing via `custodian-manage` using the custodian secret) and normal rotation
1192
+ * (signing via `manage` using the old password). **I DONT THINK THIS IS POSSIBLE. WE WOULD NEED CUSTODIAN MANAGE POOLS ON ALL THE DELEGATES AS WELL**
1193
+ *
1194
+ * @param signingSecret
1195
+ *
1196
+ * @param signingPoolId
1086
1197
  */
1087
1198
  async changePassword({
1088
- latestKeystone,
1199
+ keystoneIbGib,
1200
+ keystoneAddr,
1089
1201
  newMasterSecret,
1090
1202
  signingSecret,
1091
1203
  signingPoolId = POOL_ID_MANAGE,
@@ -1094,21 +1206,82 @@ export class KeystoneService_V1 {
1094
1206
  metaspace,
1095
1207
  space,
1096
1208
  }: {
1097
- latestKeystone: KeystoneIbGib_V1;
1209
+ /**
1210
+ * The keystone IBGIB whose password we are changing. Does NOT have to
1211
+ * be the tip, as we will get the latest keystone in the implementation
1212
+ * of this function.
1213
+ * - Visibility: Public (Public Merkle frame, safe for ledger).
1214
+ * - Generator: Client/Server (Constructed locally and synced).
1215
+ *
1216
+ * NOTE: You can provide {@link keystoneIbGib} or {@link keystoneAddr},
1217
+ * but either way the latest keystone ibgib will be gotten first.
1218
+ */
1219
+ keystoneIbGib?: KeystoneIbGib_V1;
1220
+ /**
1221
+ * The keystone ADDRESS whose password we are changing. Does NOT have to
1222
+ * be the tip, as we will get the latest keystone in the implementation
1223
+ * of this function.
1224
+ * - Visibility: Public (Public Merkle frame, safe for ledger).
1225
+ * - Generator: Client/Server (Constructed locally and synced).
1226
+ *
1227
+ * NOTE: You can provide {@link keystoneIbGib} or {@link keystoneAddr},
1228
+ * but either way the latest keystone ibgib will be gotten first.
1229
+ */
1230
+ keystoneAddr?: IbGibAddr;
1231
+ /**
1232
+ * The new passphrase/master secret to set for the user's primary pools.
1233
+ * - Visibility: Private (Confidential secret credential. Must never go
1234
+ * over the wire).
1235
+ * - Generator: Client (From user input in settings).
1236
+ */
1098
1237
  newMasterSecret: string;
1238
+ /**
1239
+ * The secret of the pool authorizing this change (either old passphrase
1240
+ * or derived custodian secret).
1241
+ * - Visibility: Private (Confidential secret. Never over the wire).
1242
+ * - Generator: Client/Server (Known only to the signing party).
1243
+ */
1099
1244
  signingSecret: string;
1245
+ /**
1246
+ * The ID of the pool used to authorize the rotation. Defaults to
1247
+ * "manage" for user password changes, or if rotating custodian, should
1248
+ * be the custodian's corresponding manage pool.
1249
+ * - Visibility: Public (Non-sensitive ID string).
1250
+ * - Generator: Client/Server (From pool configuration).
1251
+ */
1100
1252
  signingPoolId?: string;
1253
+ /**
1254
+ * If provided, password will only be changed for these pools.
1255
+ * NOTE: **delegate passwords will NOT be changed if poolIds are
1256
+ * explicitly provided.**
1257
+ */
1101
1258
  poolIds?: string[];
1102
1259
  frameDetails?: any;
1103
1260
  metaspace: MetaspaceService;
1104
1261
  space: IbGibSpaceAny;
1105
- }): Promise<KeystoneIbGib_V1> {
1262
+ }): Promise<{ newKeystone: KeystoneIbGib_V1, newDelegates?: KeystoneIbGib_V1[] }> {
1106
1263
  const lc = `${this.lc}[${this.changePassword.name}]`;
1107
1264
  try {
1108
- if (logalot) { console.log(`${lc} starting...`); }
1109
-
1110
- if (!latestKeystone.data) { throw new Error(`latestKeystone.data falsy`); }
1111
- if (latestKeystone.data.revocationInfo) { throw new Error(`Keystone has been revoked. Cannot change password. (E: 8599f8f51c78d722252ddb2894fdbe25)`); }
1265
+ if (logalot) { console.log(`${lc} starting... (I: 9e3ff83ebd74511f9dad784a1020d926)`); }
1266
+
1267
+ if (!keystoneIbGib && !keystoneAddr) { throw new Error(`Either keystoneIbGib or keystoneAddr required. (E: d1b8c8cb75f2c022284e69b8c30af826)`); }
1268
+ if (keystoneIbGib && keystoneAddr) {
1269
+ // double check addr matches ibgib
1270
+ const checkAddr = getIbGibAddr({ ibGib: keystoneIbGib });
1271
+ if (checkAddr !== keystoneAddr) { throw new Error(`(UNEXPECTED) both keystoneIbGib (addr: ${checkAddr}) and keystoneAddr (${keystoneAddr}) provided, but the addrs are unequal? Note that only one is required for this function, but if both are provided the addrs must match exactly. (E: c5574f7e353a48b0bba94a1862af4e26)`); }
1272
+ }
1273
+
1274
+ keystoneAddr ??= getIbGibAddr({ ibGib: keystoneIbGib });
1275
+
1276
+ const latestKeystone = await this.getLatestKeystone({
1277
+ addr: keystoneAddr,
1278
+ metaspace,
1279
+ space,
1280
+ });
1281
+
1282
+ if (!latestKeystone.data) { throw new Error(`latestKeystone.data falsy (E: 22e498eb8308a33e68b812d8d3905826)`); }
1283
+ if (latestKeystone.data.revocationInfo) { throw new Error(`Keystone has been revoked. Cannot change password. (E: 726298617e8508b3c83b5e985a03e826)`); }
1284
+
1112
1285
 
1113
1286
  const prevPools = latestKeystone.data.challengePools;
1114
1287
  const signingPool = prevPools.find(p => p.id === signingPoolId);
@@ -1198,19 +1371,74 @@ export class KeystoneService_V1 {
1198
1371
  proofs: [proof],
1199
1372
  frameDetails,
1200
1373
  checkpointDetails,
1201
- ...(latestKeystone.data.delegates ? { delegates: latestKeystone.data.delegates } : {}),
1374
+ ...(latestKeystone.data.delegates ? { delegates: latestKeystone.data.delegates } : undefined),
1202
1375
  };
1203
1376
 
1204
1377
  // 8. Commit
1205
- return await evolvePersistAndRegisterKeystone({
1378
+ const newKeystoneIbGib = await evolvePersistAndRegisterKeystone({
1206
1379
  prevIbGib: latestKeystone,
1207
1380
  newData,
1208
1381
  metaspace,
1209
1382
  space
1210
1383
  });
1384
+
1385
+ // update delegates' passwords also, **BUT ONLY IF NOT DOING
1386
+ // SPECIFIC POOLS**.
1387
+ const newDelegates: KeystoneIbGib_V1[] = [];
1388
+ if ((poolIds ?? []).length === 0) {
1389
+ const delegateSigningSecret = await deriveDelegateSecret({ masterSecret: signingSecret });
1390
+ const delegateNewMasterSecret = await deriveDelegateSecret({ masterSecret: newMasterSecret });
1391
+ const delegateTjpAddrs = Object.keys(latestKeystone.data.delegates ?? {});
1392
+ for (const delegateTjpAddr of delegateTjpAddrs) {
1393
+ const { newKeystone: newDelegate, newDelegates: newDelegatesSecondOrder } = await this.changePassword({
1394
+ keystoneAddr: delegateTjpAddr,
1395
+ metaspace,
1396
+ space,
1397
+ newMasterSecret: delegateNewMasterSecret,
1398
+ signingSecret: delegateSigningSecret,
1399
+ signingPoolId,
1400
+ poolIds: undefined,
1401
+ frameDetails,
1402
+ });
1403
+ if (newDelegatesSecondOrder && newDelegatesSecondOrder.length > 0) {
1404
+ throw new Error(`(UNEXPECTED) Delegate keystone has its own delegate(s)? ATOW (07/20/2026) second order delegates are not supported. (E: de45a7b7d9b8f61f6e58158269acba26)`);
1405
+ }
1406
+ newDelegates.push(newDelegate);
1407
+ }
1408
+ }
1409
+
1410
+ return { newKeystone: newKeystoneIbGib, newDelegates: newDelegates.length > 0 ? newDelegates : undefined };
1211
1411
  } catch (error) {
1212
1412
  console.error(`${lc} ${extractErrorMsg(error)}`);
1213
1413
  throw error;
1414
+ } finally {
1415
+ if (logalot) { console.log(`${lc} complete.`); }
1416
+ }
1417
+ }
1418
+
1419
+ /**
1420
+ * Verifies if a candidate signing secret (passphrase) is correct for a specific challenge pool.
1421
+ * Does not mutate any state.
1422
+ */
1423
+ async verifySigningSecret({
1424
+ keystoneIbGib,
1425
+ signingSecret,
1426
+ poolId
1427
+ }: {
1428
+ keystoneIbGib: KeystoneIbGib_V1;
1429
+ signingSecret: string;
1430
+ poolId: string;
1431
+ }): Promise<boolean> {
1432
+ const lc = `${this.lc}[${this.verifySigningSecret.name}]`;
1433
+ try {
1434
+ if (!keystoneIbGib.data) return false;
1435
+ const pool = keystoneIbGib.data.challengePools.find(p => p.id === poolId);
1436
+ if (!pool) return false;
1437
+
1438
+ return await verifyPoolSecret({ pool, signingSecret });
1439
+ } catch (error) {
1440
+ console.warn(`${lc} Exception during verification: ${extractErrorMsg(error)}`);
1441
+ return false;
1214
1442
  }
1215
1443
  }
1216
1444
  }