@ibgib/web-gib 0.0.62 → 0.0.64

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 (98) hide show
  1. package/README.md +4 -0
  2. package/dist/AUTO-GENERATED-version.d.mts +1 -1
  3. package/dist/AUTO-GENERATED-version.mjs +1 -1
  4. package/dist/helpers.web.d.mts +17 -2
  5. package/dist/helpers.web.d.mts.map +1 -1
  6. package/dist/helpers.web.mjs +67 -2
  7. package/dist/helpers.web.mjs.map +1 -1
  8. package/dist/helpers.web.respec.mjs +37 -10
  9. package/dist/helpers.web.respec.mjs.map +1 -1
  10. package/dist/identity/passkey-helper.d.mts +7 -2
  11. package/dist/identity/passkey-helper.d.mts.map +1 -1
  12. package/dist/identity/passkey-helper.mjs +33 -12
  13. package/dist/identity/passkey-helper.mjs.map +1 -1
  14. package/dist/index.d.mts +3 -0
  15. package/dist/index.d.mts.map +1 -1
  16. package/dist/index.mjs +4 -0
  17. package/dist/index.mjs.map +1 -1
  18. package/dist/ui/component/identity/keystone-creator/keystone-creator.css +26 -0
  19. package/dist/ui/component/identity/keystone-creator/keystone-creator.d.mts.map +1 -1
  20. package/dist/ui/component/identity/keystone-creator/keystone-creator.html +18 -1
  21. package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs +10 -2
  22. package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs.map +1 -1
  23. package/dist/ui/component/identity/keystone-details/keystone-details.d.mts +12 -0
  24. package/dist/ui/component/identity/keystone-details/keystone-details.d.mts.map +1 -1
  25. package/dist/ui/component/identity/keystone-details/keystone-details.html +8 -0
  26. package/dist/ui/component/identity/keystone-details/keystone-details.mjs +419 -305
  27. package/dist/ui/component/identity/keystone-details/keystone-details.mjs.map +1 -1
  28. package/dist/ui/ui-helpers.d.mts.map +1 -1
  29. package/dist/ui/ui-helpers.mjs +7 -2
  30. package/dist/ui/ui-helpers.mjs.map +1 -1
  31. package/dist/witness/space/web-in-memory-workspace-projection-space/web-in-memory-workspace-projection-space-constants.d.mts +7 -0
  32. package/dist/witness/space/web-in-memory-workspace-projection-space/web-in-memory-workspace-projection-space-constants.d.mts.map +1 -0
  33. package/dist/witness/space/web-in-memory-workspace-projection-space/web-in-memory-workspace-projection-space-constants.mjs +7 -0
  34. package/dist/witness/space/web-in-memory-workspace-projection-space/web-in-memory-workspace-projection-space-constants.mjs.map +1 -0
  35. package/dist/witness/space/web-in-memory-workspace-projection-space/web-in-memory-workspace-projection-space-types.d.mts +24 -0
  36. package/dist/witness/space/web-in-memory-workspace-projection-space/web-in-memory-workspace-projection-space-types.d.mts.map +1 -0
  37. package/dist/witness/space/web-in-memory-workspace-projection-space/web-in-memory-workspace-projection-space-types.mjs +23 -0
  38. package/dist/witness/space/web-in-memory-workspace-projection-space/web-in-memory-workspace-projection-space-types.mjs.map +1 -0
  39. package/dist/witness/space/web-in-memory-workspace-projection-space/web-in-memory-workspace-projection-space-v1.d.mts +85 -0
  40. package/dist/witness/space/web-in-memory-workspace-projection-space/web-in-memory-workspace-projection-space-v1.d.mts.map +1 -0
  41. package/dist/witness/space/web-in-memory-workspace-projection-space/web-in-memory-workspace-projection-space-v1.mjs +343 -0
  42. package/dist/witness/space/web-in-memory-workspace-projection-space/web-in-memory-workspace-projection-space-v1.mjs.map +1 -0
  43. package/dist/witness/space/web-in-memory-workspace-projection-space/web-in-memory-workspace-projection-space-v1.respec.d.mts +8 -0
  44. package/dist/witness/space/web-in-memory-workspace-projection-space/web-in-memory-workspace-projection-space-v1.respec.d.mts.map +1 -0
  45. package/dist/witness/space/web-in-memory-workspace-projection-space/web-in-memory-workspace-projection-space-v1.respec.mjs +249 -0
  46. package/dist/witness/space/web-in-memory-workspace-projection-space/web-in-memory-workspace-projection-space-v1.respec.mjs.map +1 -0
  47. package/package.json +2 -2
  48. package/src/AUTO-GENERATED-version.mts +1 -1
  49. package/src/helpers.mts +1 -1
  50. package/src/helpers.web.mts +95 -5
  51. package/src/helpers.web.respec.mts +41 -10
  52. package/src/identity/passkey-helper.mts +43 -10
  53. package/src/index.mts +5 -0
  54. package/src/ui/component/identity/keystone-creator/keystone-creator.css +26 -0
  55. package/src/ui/component/identity/keystone-creator/keystone-creator.html +18 -1
  56. package/src/ui/component/identity/keystone-creator/keystone-creator.mts +12 -2
  57. package/src/ui/component/identity/keystone-details/keystone-details.html +8 -0
  58. package/src/ui/component/identity/keystone-details/keystone-details.mts +474 -339
  59. package/src/ui/ui-helpers.mts +6 -2
  60. package/src/witness/space/web-in-memory-workspace-projection-space/web-in-memory-workspace-projection-space-constants.mts +8 -0
  61. package/src/witness/space/web-in-memory-workspace-projection-space/web-in-memory-workspace-projection-space-types.mts +53 -0
  62. package/src/witness/space/web-in-memory-workspace-projection-space/web-in-memory-workspace-projection-space-v1.mts +424 -0
  63. package/src/witness/space/web-in-memory-workspace-projection-space/web-in-memory-workspace-projection-space-v1.respec.mts +304 -0
  64. package/tools/auto-generated-agent-skills/skills/ibgib-create-app/SKILL.md +65 -39
  65. package/tools/auto-generated-agent-skills/skills/ibgib-create-client-app/SKILL.md +51 -0
  66. package/tools/auto-generated-agent-skills/skills/{ibgib-create-app-core → ibgib-create-client-app-core}/SKILL.md +26 -22
  67. package/tools/auto-generated-agent-skills/skills/{ibgib-create-app-witness → ibgib-create-client-app-witness}/SKILL.md +11 -7
  68. package/tools/auto-generated-agent-skills/skills/ibgib-create-client-identity/SKILL.md +78 -0
  69. package/tools/auto-generated-agent-skills/skills/ibgib-create-client-shell/SKILL.md +42 -0
  70. package/tools/auto-generated-agent-skills/skills/ibgib-create-server-app/SKILL.md +50 -0
  71. package/tools/auto-generated-agent-skills/skills/ibgib-create-server-app/templates/Dockerfile.template +14 -0
  72. package/tools/auto-generated-agent-skills/skills/ibgib-create-server-app/templates/docker-compose.yml.template +33 -0
  73. package/tools/auto-generated-agent-skills/skills/ibgib-create-server-app/templates/server.mts.template +97 -0
  74. package/tools/auto-generated-agent-skills/skills/ibgib-create-server-app/templates/tsconfig.server.json.template +29 -0
  75. package/tools/auto-generated-agent-skills/skills/ibgib-implementation-plan/SKILL.md +4 -1
  76. package/src/identity/custodian-delegate-helper.mts.bak +0 -7
  77. package/src/identity/sso/sso-config-helper.mts.bak +0 -52
  78. package/src/identity/sso/sso-custodian-service.mts.bak +0 -566
  79. package/src/identity/sso/sso-custodian-service.respec.mts.bak +0 -298
  80. package/tools/auto-generated-agent-skills/skills/ibgib-create-shell/SKILL.md +0 -40
  81. /package/tools/auto-generated-agent-skills/skills/{ibgib-create-app-core → ibgib-create-client-app-core}/generate-id.js +0 -0
  82. /package/tools/auto-generated-agent-skills/skills/{ibgib-create-app-core → ibgib-create-client-app-core}/generate-version-file.js +0 -0
  83. /package/tools/auto-generated-agent-skills/skills/{ibgib-create-app-core → ibgib-create-client-app-core}/templates/bootstrap.mts.template +0 -0
  84. /package/tools/auto-generated-agent-skills/skills/{ibgib-create-app-core → ibgib-create-client-app-core}/templates/constants.mts.template +0 -0
  85. /package/tools/auto-generated-agent-skills/skills/{ibgib-create-app-core → ibgib-create-client-app-core}/templates/declarations.d.ts.template +0 -0
  86. /package/tools/auto-generated-agent-skills/skills/{ibgib-create-app-core → ibgib-create-client-app-core}/templates/helpers.web.mts.template +0 -0
  87. /package/tools/auto-generated-agent-skills/skills/{ibgib-create-app-core → ibgib-create-client-app-core}/templates/index.html.template +0 -0
  88. /package/tools/auto-generated-agent-skills/skills/{ibgib-create-app-core → ibgib-create-client-app-core}/templates/index.mts.template +0 -0
  89. /package/tools/auto-generated-agent-skills/skills/{ibgib-create-app-core → ibgib-create-client-app-core}/templates/script.mts.template +0 -0
  90. /package/tools/auto-generated-agent-skills/skills/{ibgib-create-app-core → ibgib-create-client-app-core}/templates/types.mts.template +0 -0
  91. /package/tools/auto-generated-agent-skills/skills/{ibgib-create-app-witness → ibgib-create-client-app-witness}/templates/api/function-infos.web.mts.template +0 -0
  92. /package/tools/auto-generated-agent-skills/skills/{ibgib-create-app-witness → ibgib-create-client-app-witness}/templates/witness/app/app-v1.mts.template +0 -0
  93. /package/tools/auto-generated-agent-skills/skills/{ibgib-create-app-witness → ibgib-create-client-app-witness}/templates/witness/app/constants.mts.template +0 -0
  94. /package/tools/auto-generated-agent-skills/skills/{ibgib-create-app-witness → ibgib-create-client-app-witness}/templates/witness/app/helper.mts.template +0 -0
  95. /package/tools/auto-generated-agent-skills/skills/{ibgib-create-app-witness → ibgib-create-client-app-witness}/templates/witness/app/types.mts.template +0 -0
  96. /package/tools/auto-generated-agent-skills/skills/{ibgib-create-shell → ibgib-create-client-shell}/templates/ui/shell/shell-constants.mts.template +0 -0
  97. /package/tools/auto-generated-agent-skills/skills/{ibgib-create-shell → ibgib-create-client-shell}/templates/ui/shell/shell-service.mts.template +0 -0
  98. /package/tools/auto-generated-agent-skills/skills/{ibgib-create-shell → ibgib-create-client-shell}/templates/ui/shell/shell-types.mts.template +0 -0
@@ -2,7 +2,7 @@ import thisCss from "./keystone-details.css";
2
2
  import thisHtml from "./keystone-details.html";
3
3
  import commonComponentStylesCss from "../../common-component-styles.css";
4
4
 
5
- import { extractErrorMsg, pretty, getSaferSubstring, HashAlgorithm } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs";
5
+ import { extractErrorMsg, pretty, getSaferSubstring, HashAlgorithm, delay } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs";
6
6
  import { IbGibAddr } from "@ibgib/ts-gib/dist/types.mjs";
7
7
  import { IbGib_V1 } from "@ibgib/ts-gib/dist/V1/types.mjs";
8
8
  import { getIbAndGib, getIbGibAddr } from "@ibgib/ts-gib/dist/helper.mjs";
@@ -24,7 +24,7 @@ import { getTjpAddr, toDto, getTimelinesGroupedByTjp } from "@ibgib/core-gib/dis
24
24
  import { parseSpaceIb } from "@ibgib/core-gib/dist/witness/space/space-helper.mjs";
25
25
  import { executeSsoOAuthPopup } from "../../../../identity/sso-popup-helper.mjs";
26
26
  import { getComponentCtorArg } from "../../../../app-bootstrap/init-orchestration.mjs";
27
- import { promptForSecret } from "../../../../helpers.web.mjs";
27
+ import { promptForSecret, promptForAuthoritySecret } from "../../../../helpers.web.mjs";
28
28
  import { isSsoProviderLinked, getKeystoneCapabilities, isDelegateExpired, deriveDelegateSecret, createSyncDelegateKeystone, isCustodialDelegate } from "@ibgib/core-gib/dist/keystone/delegate/delegate-helpers.mjs";
29
29
  import { generatePoolChallenges } from "@ibgib/core-gib/dist/keystone/keystone-helpers.mjs";
30
30
  import { POOL_ID_MANAGE, KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT, KEYSTONE_VERB_MANAGE } from "@ibgib/core-gib/dist/keystone/keystone-constants.mjs";
@@ -99,6 +99,7 @@ interface KeystoneDetailsElements extends ElementsBase {
99
99
  passkeysCardEl: HTMLDivElement;
100
100
  passkeysListEl: HTMLTableSectionElement;
101
101
  btnAddPasskeyEl: HTMLButtonElement;
102
+ selectPasskeyAttachmentEl?: HTMLSelectElement;
102
103
  passkeyStatusMsgEl: HTMLDivElement;
103
104
  historicalWarningBannerEl: HTMLDivElement;
104
105
  containerEl: HTMLDivElement;
@@ -260,6 +261,7 @@ export class KeystoneDetailsComponentInstance
260
261
  this.elements.passkeysCardEl = shadow.getElementById('passkeys-card') as HTMLDivElement;
261
262
  this.elements.passkeysListEl = shadow.getElementById('passkeys-list') as HTMLTableSectionElement;
262
263
  this.elements.btnAddPasskeyEl = shadow.getElementById('btn-add-passkey') as HTMLButtonElement;
264
+ this.elements.selectPasskeyAttachmentEl = shadow.getElementById('select-passkey-attachment') as HTMLSelectElement;
263
265
  this.elements.passkeyStatusMsgEl = shadow.getElementById('passkey-status-msg') as HTMLDivElement;
264
266
  this.elements.containerEl = shadow.getElementById('container') as HTMLDivElement;
265
267
  this.elements.historicalWarningBannerEl = shadow.getElementById('historical-warning-banner') as HTMLDivElement;
@@ -577,18 +579,16 @@ export class KeystoneDetailsComponentInstance
577
579
  private updatePasswordCardView(): void {
578
580
  const caps = !!this.ibGib ?
579
581
  getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
580
- false;
582
+ undefined;
581
583
  const cardTitleEl = this.elements?.changePasswordCardEl?.querySelector('h3');
582
584
  const cardDescEl = this.elements?.changePasswordCardEl?.querySelector('.description');
583
585
  const currentPasswordGroup = this.elements?.inputCurrentPasswordEl?.closest('.form-group') as HTMLElement | null;
584
586
  const btnChangePassword = this.elements?.btnChangePasswordEl;
585
587
 
586
- const isCustodialOnly = !!caps ?
587
- caps.isCustodial && !caps.isSovereign :
588
- false;
589
- if (isCustodialOnly) {
588
+ const hasPassword = !!caps?.hasPassword;
589
+ if (!hasPassword) {
590
590
  if (cardTitleEl) cardTitleEl.textContent = "Add Master Password";
591
- if (cardDescEl) cardDescEl.textContent = "Establish sovereign ownership by adding a master password. This enables direct, independent cryptographic management without relying solely on custodian authentication.";
591
+ if (cardDescEl) cardDescEl.textContent = "Establish a master password for this identity. This enables sovereign password authentication alongside your passkeys and linked providers.";
592
592
  if (currentPasswordGroup) currentPasswordGroup.style.display = 'none';
593
593
  if (btnChangePassword) btnChangePassword.textContent = "Add Master Password";
594
594
  } else {
@@ -1091,12 +1091,17 @@ export class KeystoneDetailsComponentInstance
1091
1091
  const lc = `${this.lc}[${this.handleRevokeDelegate.name}]`;
1092
1092
  try {
1093
1093
  let masterSecret: string;
1094
+ let matchedPoolId: string;
1094
1095
  try {
1095
- masterSecret = await promptForSecret({
1096
+ const authResult = await promptForAuthoritySecret({
1097
+ keystone: this.ibGib,
1098
+ aggregatedDetails: this.aggregatedDetails,
1099
+ title: 'Revoke Delegate',
1096
1100
  msg: `Enter Master Password or touch security key to revoke delegate '${deviceName}':`,
1097
- confirm: false,
1098
- passkeys: this.aggregatedDetails?.passkeys,
1101
+ verb: KEYSTONE_VERB_MANAGE
1099
1102
  });
1103
+ masterSecret = authResult.secret;
1104
+ matchedPoolId = authResult.poolId;
1100
1105
  } catch (err) {
1101
1106
  // User cancelled prompt
1102
1107
  return;
@@ -1112,29 +1117,6 @@ export class KeystoneDetailsComponentInstance
1112
1117
 
1113
1118
  const keystoneSvc = new KeystoneService_V1();
1114
1119
 
1115
- // Resolve which manage pool matches the provided masterSecret
1116
- let matchedPoolId: string | undefined;
1117
- const candidatePools = this.ibGib!.data?.challengePools.filter(p =>
1118
- p.id === 'manage' || p.id.startsWith('passkey-manage-') || (p.config.allowedVerbs && p.config.allowedVerbs.includes(KEYSTONE_VERB_MANAGE))
1119
- ) || [];
1120
-
1121
- for (const pool of candidatePools) {
1122
- const ok = await keystoneSvc.verifySigningSecret({
1123
- keystoneIbGib: this.ibGib!,
1124
- signingSecret: masterSecret,
1125
- poolId: pool.id
1126
- });
1127
- if (ok) {
1128
- matchedPoolId = pool.id;
1129
- break;
1130
- }
1131
- }
1132
-
1133
- if (!matchedPoolId) {
1134
- this.setSyncStatus("Failed to authorize revocation (invalid secret/passkey).", "error");
1135
- return;
1136
- }
1137
-
1138
1120
  // Unregister delegate from parent keystone
1139
1121
  const evolvedParent = await keystoneSvc.unregisterDelegate({
1140
1122
  parentKeystone: this.ibGib!,
@@ -1299,10 +1281,18 @@ export class KeystoneDetailsComponentInstance
1299
1281
  const rawSalt = crypto.getRandomValues(new Uint8Array(32));
1300
1282
  const saltHex = bufferToHex(rawSalt.buffer as ArrayBuffer);
1301
1283
 
1284
+ const attachmentVal = this.elements?.selectPasskeyAttachmentEl?.value;
1285
+ const authenticatorAttachment: AuthenticatorAttachment | undefined =
1286
+ (attachmentVal === 'cross-platform' || attachmentVal === 'platform')
1287
+ ? attachmentVal
1288
+ : undefined;
1289
+
1302
1290
  const regResult = await registerPasskeyCredential({
1303
1291
  username,
1304
1292
  displayName: username,
1305
- salt: saltHex
1293
+ salt: saltHex,
1294
+ userVerification: 'preferred',
1295
+ authenticatorAttachment,
1306
1296
  });
1307
1297
  const { credentialId, transports } = regResult;
1308
1298
 
@@ -1311,7 +1301,8 @@ export class KeystoneDetailsComponentInstance
1311
1301
  const prfResult = await evaluatePasskeyPrf({
1312
1302
  credentialId,
1313
1303
  salt: saltHex,
1314
- transports
1304
+ transports,
1305
+ userVerification: 'preferred'
1315
1306
  });
1316
1307
  passkeySecret = prfResult.passkeySecret;
1317
1308
  }
@@ -1338,6 +1329,36 @@ export class KeystoneDetailsComponentInstance
1338
1329
  const keystoneService = new KeystoneService_V1();
1339
1330
  let evolvedKeystone: KeystoneIbGib_V1;
1340
1331
 
1332
+ const poolConfig: KeystonePoolConfig_HashV1 = {
1333
+ id: poolId,
1334
+ type: KeystoneChallengeType.hash_reveal_v1,
1335
+ salt: poolId,
1336
+ algo: HashAlgorithm.sha_256,
1337
+ rounds: 1,
1338
+ allowedVerbs: [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
1339
+ behavior: {
1340
+ size: 10,
1341
+ replenish: KeystoneReplenishStrategy.topUp,
1342
+ selectSequentially: 1,
1343
+ selectRandomly: 0,
1344
+ targetBindingCount: 0,
1345
+ }
1346
+ };
1347
+
1348
+ const challenges = await generatePoolChallenges({
1349
+ config: poolConfig,
1350
+ masterSecret: passkeySecret
1351
+ });
1352
+
1353
+ const newPool: KeystoneChallengePool = {
1354
+ id: poolId,
1355
+ config: poolConfig,
1356
+ challenges
1357
+ };
1358
+
1359
+ let authSecret: string | undefined;
1360
+ let signingPoolId: string | undefined;
1361
+
1341
1362
  if (isCustodialOnly) {
1342
1363
  // Elevate custodial account with sovereign passkey
1343
1364
  this.showBusy({
@@ -1347,37 +1368,13 @@ export class KeystoneDetailsComponentInstance
1347
1368
  animationEmoji: '⚡'
1348
1369
  });
1349
1370
 
1350
- // Generate new manage pool configuration using passkeySecret
1351
- const poolConfig: KeystonePoolConfig_HashV1 = {
1352
- id: poolId,
1353
- type: KeystoneChallengeType.hash_reveal_v1,
1354
- salt: poolId,
1355
- algo: HashAlgorithm.sha_256,
1356
- rounds: 1,
1357
- allowedVerbs: [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
1358
- behavior: {
1359
- size: 10,
1360
- replenish: KeystoneReplenishStrategy.topUp,
1361
- selectSequentially: 1,
1362
- selectRandomly: 0,
1363
- targetBindingCount: 0,
1364
- }
1365
- };
1366
-
1367
- const challenges = await generatePoolChallenges({
1368
- config: poolConfig,
1369
- masterSecret: passkeySecret
1370
- });
1371
-
1372
- const newPool: KeystoneChallengePool = {
1373
- id: poolId,
1374
- config: poolConfig,
1375
- challenges
1376
- };
1371
+ authSecret = passkeySecret;
1372
+ signingPoolId = poolId;
1377
1373
 
1378
1374
  evolvedKeystone = await keystoneService.addPools({
1379
1375
  latestKeystone: this.ibGib,
1380
- masterSecret: passkeySecret,
1376
+ masterSecret: authSecret,
1377
+ signingPoolId,
1381
1378
  newPools: [newPool],
1382
1379
  frameDetails: {
1383
1380
  passkeys: {
@@ -1392,39 +1389,18 @@ export class KeystoneDetailsComponentInstance
1392
1389
  });
1393
1390
  } else {
1394
1391
  // Secondary passkey registration on existing sovereign identity:
1395
- // Prompt user to authorize with current master password
1396
- let authSecret: string | undefined;
1397
- let isCorrect = false;
1398
- let tries = 0;
1399
- const maxTries = 5;
1400
-
1401
- while (tries < maxTries && !isCorrect) {
1402
- try {
1403
- const promptMsg = tries === 0
1404
- ? 'Enter your current Master Secret (Password) or touch security key to authorize adding this passkey:'
1405
- : `Incorrect password. Please try again (${maxTries - tries} tries remaining):`;
1406
- authSecret = await promptForSecret({
1407
- msg: promptMsg,
1408
- confirm: false,
1409
- passkeys: this.aggregatedDetails?.passkeys,
1410
- });
1411
- } catch (err) {
1412
- authSecret = undefined;
1413
- break;
1414
- }
1415
-
1416
- if (!authSecret) break;
1417
-
1418
- // Verify secret against manage/passkey pools
1419
- isCorrect = await keystoneService.verifySigningSecret({
1420
- keystoneIbGib: this.ibGib,
1421
- signingSecret: authSecret
1392
+ // Prompt user to authorize with current master password or passkey
1393
+ try {
1394
+ const authResult = await promptForAuthoritySecret({
1395
+ keystone: this.ibGib,
1396
+ aggregatedDetails: this.aggregatedDetails,
1397
+ title: 'Authorize New Passkey',
1398
+ msg: 'Enter Master Password or touch security key to authorize adding this passkey:',
1399
+ verb: KEYSTONE_VERB_MANAGE
1422
1400
  });
1423
-
1424
- if (!isCorrect) tries++;
1425
- }
1426
-
1427
- if (!isCorrect || !authSecret) {
1401
+ authSecret = authResult.secret;
1402
+ signingPoolId = authResult.poolId;
1403
+ } catch (err) {
1428
1404
  this.setPasskeyStatus("Registration cancelled (authorization required).", "error");
1429
1405
  return;
1430
1406
  }
@@ -1436,36 +1412,10 @@ export class KeystoneDetailsComponentInstance
1436
1412
  animationEmoji: '🛡️'
1437
1413
  });
1438
1414
 
1439
- const poolConfig: KeystonePoolConfig_HashV1 = {
1440
- id: poolId,
1441
- type: KeystoneChallengeType.hash_reveal_v1,
1442
- salt: poolId,
1443
- algo: HashAlgorithm.sha_256,
1444
- rounds: 1,
1445
- allowedVerbs: [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
1446
- behavior: {
1447
- size: 10,
1448
- replenish: KeystoneReplenishStrategy.topUp,
1449
- selectSequentially: 1,
1450
- selectRandomly: 0,
1451
- targetBindingCount: 0,
1452
- }
1453
- };
1454
-
1455
- const challenges = await generatePoolChallenges({
1456
- config: poolConfig,
1457
- masterSecret: passkeySecret
1458
- });
1459
-
1460
- const newPool: KeystoneChallengePool = {
1461
- id: poolId,
1462
- config: poolConfig,
1463
- challenges
1464
- };
1465
-
1466
1415
  evolvedKeystone = await keystoneService.addPools({
1467
1416
  latestKeystone: this.ibGib,
1468
1417
  masterSecret: authSecret,
1418
+ signingPoolId,
1469
1419
  newPools: [newPool],
1470
1420
  frameDetails: {
1471
1421
  passkeys: {
@@ -1478,25 +1428,75 @@ export class KeystoneDetailsComponentInstance
1478
1428
  metaspace,
1479
1429
  space
1480
1430
  });
1431
+
1432
+ // Sync pool addition frame to server
1433
+ const primaryTjpAddr = getTjpAddr({ ibGib: evolvedKeystone, defaultIfNone: 'incomingAddr' })!;
1434
+ const resSyncPool = await getApiBridge().putEvolveKeystone({
1435
+ domainAddr: primaryTjpAddr,
1436
+ keystoneIbGib: evolvedKeystone,
1437
+ relatedKeystoneIbGibs: []
1438
+ });
1439
+ if (!resSyncPool.success) {
1440
+ throw new Error(`Server synchronization of passkey pool failed: ${resSyncPool.message}`);
1441
+ }
1481
1442
  }
1482
1443
 
1483
- // Sync evolved keystone to server
1444
+ // Construct outward device sync delegate for this passkey
1445
+ this.showBusy({
1446
+ isBusy: true,
1447
+ title: 'Creating Sync Delegate...',
1448
+ msg: 'Constructing outward passkey device sync delegate...',
1449
+ animationEmoji: '📱'
1450
+ });
1451
+
1452
+ const primaryTjpAddr = getTjpAddr({ ibGib: evolvedKeystone, defaultIfNone: 'incomingAddr' })!;
1453
+ const { delegateKeystone, deviceTag } = await createPasskeyDeviceSyncDelegate({
1454
+ username,
1455
+ primaryTjpAddr,
1456
+ passkeySecret,
1457
+ transports,
1458
+ metaspace,
1459
+ space
1460
+ });
1461
+
1462
+ // Register outward sync delegate on primary keystone
1463
+ this.showBusy({
1464
+ isBusy: true,
1465
+ title: 'Registering Delegate...',
1466
+ msg: 'Registering outward passkey delegate on primary identity...',
1467
+ animationEmoji: '📝'
1468
+ });
1469
+
1470
+ const delegateSigningPoolId = isCustodialOnly ? poolId : signingPoolId!;
1471
+ const delegateAuthSecret = isCustodialOnly ? passkeySecret : authSecret!;
1472
+
1473
+ evolvedKeystone = await keystoneService.registerDelegate({
1474
+ parentKeystone: evolvedKeystone,
1475
+ delegateKeystone,
1476
+ masterSecret: delegateAuthSecret,
1477
+ signingPoolId: delegateSigningPoolId,
1478
+ metaspace,
1479
+ space,
1480
+ allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
1481
+ deviceName: deviceTag
1482
+ });
1483
+
1484
+ // Sync evolved keystone and delegate to server
1484
1485
  this.showBusy({
1485
1486
  isBusy: true,
1486
1487
  title: 'Syncing to Server...',
1487
- msg: 'Publishing evolved identity keystone to server...',
1488
+ msg: 'Publishing evolved identity keystone and delegate to server...',
1488
1489
  animationEmoji: '⚡'
1489
1490
  });
1490
1491
 
1491
- const primaryTjpAddr = getTjpAddr({ ibGib: evolvedKeystone, defaultIfNone: 'incomingAddr' })!;
1492
1492
  const resSync = await getApiBridge().putEvolveKeystone({
1493
1493
  domainAddr: primaryTjpAddr,
1494
1494
  keystoneIbGib: evolvedKeystone,
1495
- relatedKeystoneIbGibs: []
1495
+ relatedKeystoneIbGibs: [delegateKeystone]
1496
1496
  });
1497
1497
 
1498
1498
  if (!resSync.success) {
1499
- console.warn(`${lc} Server sync note: ${resSync.message}`);
1499
+ throw new Error(`Server synchronization of delegate failed: ${resSync.message}`);
1500
1500
  }
1501
1501
 
1502
1502
  // Update Special Index
@@ -1505,7 +1505,7 @@ export class KeystoneDetailsComponentInstance
1505
1505
  rel8nInfos: [
1506
1506
  {
1507
1507
  rel8nName: "keystone",
1508
- ibGibs: [toDto({ ibGib: evolvedKeystone })]
1508
+ ibGibs: [toDto({ ibGib: evolvedKeystone }), toDto({ ibGib: delegateKeystone })]
1509
1509
  }
1510
1510
  ],
1511
1511
  metaspace,
@@ -1535,36 +1535,19 @@ export class KeystoneDetailsComponentInstance
1535
1535
  try {
1536
1536
  const keystoneService = new KeystoneService_V1();
1537
1537
  let masterSecret: string | undefined;
1538
- let isCorrect = false;
1539
- let tries = 0;
1540
- const maxTries = 5;
1538
+ let signingPoolId: string | undefined;
1541
1539
 
1542
- while (tries < maxTries && !isCorrect) {
1543
- try {
1544
- const promptMsg = tries === 0
1545
- ? `Are you sure you want to remove "${passkeyName}"? Enter your Master Secret (Password) or touch security key to authorize:`
1546
- : `Incorrect password. Please try again (${maxTries - tries} tries remaining):`;
1547
- masterSecret = await promptForSecret({
1548
- msg: promptMsg,
1549
- confirm: false,
1550
- passkeys: this.aggregatedDetails?.passkeys,
1551
- });
1552
- } catch (err) {
1553
- masterSecret = undefined;
1554
- break;
1555
- }
1556
-
1557
- if (!masterSecret) break;
1558
-
1559
- isCorrect = await keystoneService.verifySigningSecret({
1560
- keystoneIbGib: this.ibGib,
1561
- signingSecret: masterSecret
1540
+ try {
1541
+ const authResult = await promptForAuthoritySecret({
1542
+ keystone: this.ibGib,
1543
+ aggregatedDetails: this.aggregatedDetails,
1544
+ title: 'Authorize Removal',
1545
+ msg: `Are you sure you want to remove "${passkeyName}"? Enter your Master Secret (Password) or touch security key to authorize:`,
1546
+ verb: KEYSTONE_VERB_MANAGE
1562
1547
  });
1563
-
1564
- if (!isCorrect) tries++;
1565
- }
1566
-
1567
- if (!isCorrect || !masterSecret) {
1548
+ masterSecret = authResult.secret;
1549
+ signingPoolId = authResult.poolId;
1550
+ } catch (err) {
1568
1551
  this.setPasskeyStatus("Removal cancelled or unauthorized.", "error");
1569
1552
  return;
1570
1553
  }
@@ -1590,7 +1573,8 @@ export class KeystoneDetailsComponentInstance
1590
1573
  if (poolExists) {
1591
1574
  evolvedKeystone = await keystoneService.removePools({
1592
1575
  latestKeystone: this.ibGib,
1593
- masterSecret,
1576
+ masterSecret: masterSecret!,
1577
+ signingPoolId: signingPoolId!,
1594
1578
  poolIds: [poolIdToRemove],
1595
1579
  frameDetails: {
1596
1580
  passkeys: {
@@ -1604,11 +1588,12 @@ export class KeystoneDetailsComponentInstance
1604
1588
  space
1605
1589
  });
1606
1590
  } else {
1591
+ // If pool doesn't exist, just update frameDetails
1607
1592
  evolvedKeystone = await keystoneService.sign({
1608
1593
  latestKeystone: this.ibGib,
1609
- masterSecret,
1594
+ masterSecret: masterSecret!,
1595
+ poolId: signingPoolId,
1610
1596
  claim: {
1611
- verb: POOL_ID_MANAGE,
1612
1597
  target: getIbGibAddr({ ibGib: this.ibGib })
1613
1598
  },
1614
1599
  frameDetails: {
@@ -1783,48 +1768,23 @@ export class KeystoneDetailsComponentInstance
1783
1768
 
1784
1769
  const custodianPool = linkData.custodianPool;
1785
1770
 
1786
- // 2. Prompt user for master secret and verify it
1787
- this.showBusy({ isBusy: false });
1771
+ // 2. Prompt user for authorization secret
1788
1772
  const keystoneService = new KeystoneService_V1();
1789
1773
  let masterSecret: string | undefined;
1790
- let isCorrect = false;
1791
- let tries = 0;
1792
- const maxTries = 8;
1793
-
1794
- while (tries < maxTries && !isCorrect) {
1795
- try {
1796
- const promptMsg = tries === 0
1797
- ? "Link successful! Enter your local Master Secret (Password) to sign and register the custodian pool"
1798
- : `Incorrect password. Please try again (${maxTries - tries} tries remaining):`;
1799
- masterSecret = await promptForSecret({
1800
- msg: promptMsg,
1801
- confirm: false
1802
- });
1803
- } catch (error) {
1804
- // cancelled
1805
- if (logalot) { console.log(`${lc}[promptForSecret] user cancelled (I: 911ff1fb0234949228481968e9087626)`); }
1806
- masterSecret = undefined;
1807
- break;
1808
- }
1809
-
1810
- if (!masterSecret) {
1811
- break;
1812
- }
1774
+ let signingPoolId: string | undefined;
1813
1775
 
1814
- // Verify secret using verifySigningSecret (requires manage poolId)
1815
- isCorrect = await keystoneService.verifySigningSecret({
1816
- keystoneIbGib: this.ibGib!,
1817
- signingSecret: masterSecret,
1818
- poolId: 'manage'
1776
+ try {
1777
+ const authResult = await promptForAuthoritySecret({
1778
+ keystone: this.ibGib,
1779
+ aggregatedDetails: this.aggregatedDetails,
1780
+ title: `Link ${providerName}`,
1781
+ msg: `Enter Master Password or touch security key to authorize linking ${providerName}:`,
1782
+ verb: KEYSTONE_VERB_MANAGE
1819
1783
  });
1820
-
1821
- if (!isCorrect) {
1822
- tries++;
1823
- }
1824
- }
1825
-
1826
- if (!isCorrect) {
1827
- this.setSSOStatus("Link cancelled or failed (incorrect password).", "error");
1784
+ masterSecret = authResult.secret;
1785
+ signingPoolId = authResult.poolId;
1786
+ } catch (error) {
1787
+ this.setSSOStatus("Link cancelled or failed.", "error");
1828
1788
  return;
1829
1789
  }
1830
1790
 
@@ -1845,6 +1805,7 @@ export class KeystoneDetailsComponentInstance
1845
1805
  const evolvedParent = await keystoneService.addPools({
1846
1806
  latestKeystone: this.ibGib!,
1847
1807
  masterSecret: masterSecret!,
1808
+ signingPoolId: signingPoolId!,
1848
1809
  newPools: [custodianPool],
1849
1810
  metaspace,
1850
1811
  space
@@ -1875,7 +1836,7 @@ export class KeystoneDetailsComponentInstance
1875
1836
  parentKeystone: evolvedParent,
1876
1837
  delegateKeystone,
1877
1838
  masterSecret: masterSecret!,
1878
- signingPoolId: 'manage',
1839
+ signingPoolId: signingPoolId!,
1879
1840
  metaspace,
1880
1841
  space,
1881
1842
  allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
@@ -1949,45 +1910,23 @@ export class KeystoneDetailsComponentInstance
1949
1910
  this.setSSOStatus(`Unlinking ${providerName}...`, "info");
1950
1911
 
1951
1912
  try {
1952
- // 1. Prompt user for password
1913
+ // 1. Prompt user for authorization secret
1953
1914
  const keystoneService = new KeystoneService_V1();
1954
1915
  let masterSecret: string | undefined;
1955
- let isCorrect = false;
1956
- let tries = 0;
1957
- const maxTries = 8;
1958
-
1959
- while (tries < maxTries && !isCorrect) {
1960
- try {
1961
- const promptMsg = tries === 0
1962
- ? `Are you sure you want to unlink your ${providerName} account? If so, enter your local Master Secret (Password) or touch security key to authorize. Otherwise, click Cancel.`
1963
- : `Incorrect password. Please try again (${maxTries - tries} tries remaining):`;
1964
- masterSecret = await promptForSecret({
1965
- msg: promptMsg,
1966
- confirm: false,
1967
- passkeys: this.aggregatedDetails?.passkeys,
1968
- });
1969
- } catch (error) {
1970
- masterSecret = undefined;
1971
- break;
1972
- }
1973
-
1974
- if (!masterSecret) {
1975
- break;
1976
- }
1916
+ let signingPoolId: string | undefined;
1977
1917
 
1978
- // Verify secret using verifySigningSecret
1979
- isCorrect = await keystoneService.verifySigningSecret({
1980
- keystoneIbGib: this.ibGib!,
1981
- signingSecret: masterSecret
1918
+ try {
1919
+ const authResult = await promptForAuthoritySecret({
1920
+ keystone: this.ibGib,
1921
+ aggregatedDetails: this.aggregatedDetails,
1922
+ title: `Unlink ${providerName}`,
1923
+ msg: `Are you sure you want to unlink your ${providerName} account? If so, enter your Master Password or touch security key to authorize.`,
1924
+ verb: KEYSTONE_VERB_MANAGE
1982
1925
  });
1983
-
1984
- if (!isCorrect) {
1985
- tries++;
1986
- }
1987
- }
1988
-
1989
- if (!isCorrect) {
1990
- this.setSSOStatus("Unlink cancelled or failed (incorrect password).", "error");
1926
+ masterSecret = authResult.secret;
1927
+ signingPoolId = authResult.poolId;
1928
+ } catch (error) {
1929
+ this.setSSOStatus("Unlink cancelled or failed.", "error");
1991
1930
  return;
1992
1931
  }
1993
1932
 
@@ -2009,6 +1948,7 @@ export class KeystoneDetailsComponentInstance
2009
1948
  let evolvedParent = await keystoneService.removePools({
2010
1949
  latestKeystone: this.ibGib!,
2011
1950
  masterSecret: masterSecret!,
1951
+ signingPoolId: signingPoolId!,
2012
1952
  poolIds: [targetPoolId],
2013
1953
  metaspace,
2014
1954
  space
@@ -2293,113 +2233,14 @@ export class KeystoneDetailsComponentInstance
2293
2233
  await metaspace.registerNewIbGib({ ibGib: evolvedParent, space });
2294
2234
  }
2295
2235
 
2296
- // 1. Create a password-based sync delegate for this device
2297
- this.showBusy({
2298
- isBusy: true,
2299
- title: 'Creating Sovereign Sync Delegate...',
2300
- msg: 'Deriving local device sync delegate keys from Master Password...',
2301
- animationEmoji: '🔑'
2302
- });
2303
-
2304
- const rawUsername = this.aggregatedDetails?.username
2305
- || this.ibGib?.data?.username
2306
- || this.aggregatedDetails?.emails?.[0]?.split('@')[0]
2307
- || this.ibGib?.data?.emails?.[0]?.split('@')[0]
2308
- || 'user';
2309
-
2310
- const { delegateKeystone: userSyncDelegate, deviceTag } = await createSovereignDeviceSyncDelegate({
2311
- username: rawUsername,
2236
+ await this.registerAndSyncSovereignPasswordDelegate({
2237
+ evolvedParent,
2238
+ newPassword,
2312
2239
  primaryTjpAddr,
2313
- masterSecret: newPassword,
2314
- metaspace,
2315
- space,
2316
- });
2317
-
2318
- // 2. Register the new user sync delegate on the evolved parent keystone
2319
- this.showBusy({
2320
- isBusy: true,
2321
- title: 'Registering Sovereign Delegate...',
2322
- msg: 'Registering user sync delegate on primary keystone...',
2323
- animationEmoji: '📝'
2324
- });
2325
-
2326
- const keystoneService = new KeystoneService_V1();
2327
- const parentWithUserDelegate = await keystoneService.registerDelegate({
2328
- parentKeystone: evolvedParent,
2329
- delegateKeystone: userSyncDelegate,
2330
- masterSecret: newPassword,
2331
- signingPoolId: 'manage',
2332
- metaspace,
2333
- space,
2334
- allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
2335
- deviceName: deviceTag
2336
- });
2337
-
2338
- // 3. Post evolved keystone with new delegate to server
2339
- this.showBusy({
2340
- isBusy: true,
2341
- title: 'Syncing to Server...',
2342
- msg: 'Publishing updated domain identity and sovereign delegate to server...',
2343
- animationEmoji: '⚡'
2344
- });
2345
-
2346
- const resSync = await getApiBridge().putEvolveKeystone({
2347
- domainAddr: primaryTjpAddr,
2348
- keystoneIbGib: parentWithUserDelegate,
2349
- relatedKeystoneIbGibs: [userSyncDelegate]
2350
- });
2351
- if (!resSync.success) {
2352
- console.warn(`${lc} Warning syncing user delegate to server: ${resSync.message}`);
2353
- }
2354
-
2355
- // 4. Save parentWithUserDelegate and userSyncDelegate to local space & metaspace
2356
- await metaspace.put({ ibGibs: [parentWithUserDelegate, userSyncDelegate], space });
2357
- await metaspace.registerNewIbGib({ ibGib: parentWithUserDelegate, space });
2358
- await metaspace.registerNewIbGib({ ibGib: userSyncDelegate, space });
2359
-
2360
- await updateSpecialIndex({
2361
- type: "keystones",
2362
- rel8nInfos: [
2363
- {
2364
- rel8nName: "keystone",
2365
- ibGibs: [toDto({ ibGib: parentWithUserDelegate }), toDto({ ibGib: userSyncDelegate })]
2366
- }
2367
- ],
2368
2240
  metaspace,
2369
2241
  space
2370
2242
  });
2371
2243
 
2372
- const evolvedAddr = getIbGibAddr({ ibGib: parentWithUserDelegate });
2373
-
2374
- // Dispatch EVENT_IBGIB_IDENTITY_CHANGED
2375
- window.dispatchEvent(new CustomEvent(EVENT_IBGIB_IDENTITY_CHANGED, {
2376
- detail: { activeIdentityAddr: evolvedAddr },
2377
- bubbles: true,
2378
- composed: true
2379
- }));
2380
-
2381
- this.setChangePasswordStatus("Successfully added Master Password! Identity is now sovereign.", "success");
2382
-
2383
- // Reset OTP section and button text
2384
- if (this.elements?.sectionAddPasswordOtpEl) {
2385
- this.elements.sectionAddPasswordOtpEl.classList.add('hidden');
2386
- if (this.elements.inputAddPasswordOtpEl) {
2387
- this.elements.inputAddPasswordOtpEl.value = '';
2388
- }
2389
- }
2390
- if (this.elements?.btnChangePasswordEl) {
2391
- this.elements.btnChangePasswordEl.textContent = "Update Password";
2392
- }
2393
-
2394
- // Refresh the view
2395
- this.ibGibAddr = evolvedAddr;
2396
- this.staticIbGib = parentWithUserDelegate;
2397
- await this.loadIbGib({ getLatest: true });
2398
-
2399
- // Clear inputs
2400
- this.elements!.inputNewPasswordEl.value = '';
2401
- this.elements!.inputConfirmPasswordEl.value = '';
2402
-
2403
2244
  } catch (error) {
2404
2245
  console.error(`${lc} Elevation failed: ${extractErrorMsg(error)}`);
2405
2246
  this.setChangePasswordStatus(`Failed to add master password: ${extractErrorMsg(error)}`, "error");
@@ -2460,6 +2301,295 @@ export class KeystoneDetailsComponentInstance
2460
2301
  }
2461
2302
  }
2462
2303
 
2304
+ /**
2305
+ * Common DRY post-elevation workflow: creates and registers the password-based
2306
+ * device sync delegate, synchronizes the evolved identity with the delegate to the server,
2307
+ * updates the local space and special index, and dispatches the identity change event.
2308
+ */
2309
+ private async registerAndSyncSovereignPasswordDelegate({
2310
+ evolvedParent,
2311
+ newPassword,
2312
+ primaryTjpAddr,
2313
+ metaspace,
2314
+ space,
2315
+ }: {
2316
+ evolvedParent: KeystoneIbGib_V1;
2317
+ newPassword: string;
2318
+ primaryTjpAddr: string;
2319
+ metaspace: any;
2320
+ space: any;
2321
+ }): Promise<KeystoneIbGib_V1> {
2322
+ const lc = `${this.lc}[${this.registerAndSyncSovereignPasswordDelegate.name}]`;
2323
+
2324
+ // 1. Create a password-based sync delegate for this device
2325
+ this.showBusy({
2326
+ isBusy: true,
2327
+ title: 'Creating Sovereign Sync Delegate...',
2328
+ msg: 'Deriving local device sync delegate keys from Master Password...',
2329
+ animationEmoji: '🔑'
2330
+ });
2331
+ await delay(50);
2332
+
2333
+ const rawUsername = this.aggregatedDetails?.username
2334
+ || this.ibGib?.data?.username
2335
+ || this.aggregatedDetails?.emails?.[0]?.split('@')[0]
2336
+ || this.ibGib?.data?.emails?.[0]?.split('@')[0]
2337
+ || 'user';
2338
+
2339
+ const { delegateKeystone: userSyncDelegate, deviceTag } = await createSovereignDeviceSyncDelegate({
2340
+ username: rawUsername,
2341
+ primaryTjpAddr,
2342
+ masterSecret: newPassword,
2343
+ metaspace,
2344
+ space,
2345
+ });
2346
+
2347
+ // 2. Register the new user sync delegate on the evolved parent keystone
2348
+ this.showBusy({
2349
+ isBusy: true,
2350
+ title: 'Registering Sovereign Delegate...',
2351
+ msg: 'Registering user sync delegate on primary keystone...',
2352
+ animationEmoji: '📝'
2353
+ });
2354
+ await delay(50);
2355
+
2356
+ const keystoneService = new KeystoneService_V1();
2357
+ const parentWithUserDelegate = await keystoneService.registerDelegate({
2358
+ parentKeystone: evolvedParent,
2359
+ delegateKeystone: userSyncDelegate,
2360
+ masterSecret: newPassword,
2361
+ signingPoolId: POOL_ID_MANAGE,
2362
+ metaspace,
2363
+ space,
2364
+ allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
2365
+ deviceName: deviceTag
2366
+ });
2367
+
2368
+ // 3. Post evolved keystone with new delegate to server
2369
+ this.showBusy({
2370
+ isBusy: true,
2371
+ title: 'Syncing to Server...',
2372
+ msg: 'Publishing updated domain identity and sovereign delegate to server...',
2373
+ animationEmoji: '⚡'
2374
+ });
2375
+
2376
+ const resSync = await getApiBridge().putEvolveKeystone({
2377
+ domainAddr: primaryTjpAddr,
2378
+ keystoneIbGib: parentWithUserDelegate,
2379
+ relatedKeystoneIbGibs: [userSyncDelegate]
2380
+ });
2381
+ if (!resSync.success) {
2382
+ throw new Error(`Server sync of password delegate failed: ${resSync.message}`);
2383
+ }
2384
+
2385
+ // 4. Save parentWithUserDelegate and userSyncDelegate to local space & metaspace
2386
+ await metaspace.put({ ibGibs: [parentWithUserDelegate, userSyncDelegate], space });
2387
+ await metaspace.registerNewIbGib({ ibGib: parentWithUserDelegate, space });
2388
+ await metaspace.registerNewIbGib({ ibGib: userSyncDelegate, space });
2389
+
2390
+ await updateSpecialIndex({
2391
+ type: "keystones",
2392
+ rel8nInfos: [
2393
+ {
2394
+ rel8nName: "keystone",
2395
+ ibGibs: [toDto({ ibGib: parentWithUserDelegate }), toDto({ ibGib: userSyncDelegate })]
2396
+ }
2397
+ ],
2398
+ metaspace,
2399
+ space
2400
+ });
2401
+
2402
+ const evolvedAddr = getIbGibAddr({ ibGib: parentWithUserDelegate });
2403
+
2404
+ // Dispatch EVENT_IBGIB_IDENTITY_CHANGED
2405
+ window.dispatchEvent(new CustomEvent(EVENT_IBGIB_IDENTITY_CHANGED, {
2406
+ detail: { activeIdentityAddr: evolvedAddr },
2407
+ bubbles: true,
2408
+ composed: true
2409
+ }));
2410
+
2411
+ this.setChangePasswordStatus("Successfully added Master Password! Identity is now sovereign.", "success");
2412
+
2413
+ // Reset OTP section and button text
2414
+ if (this.elements?.sectionAddPasswordOtpEl) {
2415
+ this.elements.sectionAddPasswordOtpEl.classList.add('hidden');
2416
+ if (this.elements.inputAddPasswordOtpEl) {
2417
+ this.elements.inputAddPasswordOtpEl.value = '';
2418
+ }
2419
+ }
2420
+ if (this.elements?.btnChangePasswordEl) {
2421
+ this.elements.btnChangePasswordEl.textContent = "Update Password";
2422
+ }
2423
+
2424
+ // Refresh the view
2425
+ this.ibGibAddr = evolvedAddr;
2426
+ this.staticIbGib = parentWithUserDelegate;
2427
+ await this.loadIbGib({ getLatest: true });
2428
+
2429
+ // Clear inputs
2430
+ this.elements!.inputNewPasswordEl.value = '';
2431
+ this.elements!.inputConfirmPasswordEl.value = '';
2432
+
2433
+ return parentWithUserDelegate;
2434
+ }
2435
+
2436
+ /**
2437
+ * Handles adding a master password to a sovereign identity that was created with passkeys only.
2438
+ * Authorizes via passkey biometric/hardware assertion and cryptographically adds the manage pool.
2439
+ */
2440
+ protected async handleAddPasswordToPasskeyIdentity(): Promise<void> {
2441
+ const lc = `${this.lc}[${this.handleAddPasswordToPasskeyIdentity.name}]`;
2442
+ this.setChangePasswordStatus("Validating entries...", "info");
2443
+
2444
+ try {
2445
+ const newPassword = this.elements!.inputNewPasswordEl.value;
2446
+ const confirmPassword = this.elements!.inputConfirmPasswordEl.value;
2447
+
2448
+ if (!newPassword) {
2449
+ this.setChangePasswordStatus("Master password is required.", "error");
2450
+ return;
2451
+ }
2452
+ if (newPassword !== confirmPassword) {
2453
+ this.setChangePasswordStatus("Master password and confirmation do not match.", "error");
2454
+ return;
2455
+ }
2456
+
2457
+ const passkeysMap = this.aggregatedDetails?.passkeys
2458
+ || (this.ibGib?.data as any)?.frameDetails?.passkeys
2459
+ || (this.ibGib?.data as any)?.passkeys
2460
+ || {};
2461
+ const passkeysList: KeystonePasskeyEntry[] = Array.isArray(passkeysMap)
2462
+ ? passkeysMap
2463
+ : Object.values(passkeysMap);
2464
+
2465
+ const passkeyPool = this.ibGib?.data?.challengePools?.find(p => p.id.startsWith('passkey-manage-'));
2466
+ if (!passkeyPool) {
2467
+ this.setChangePasswordStatus("No active passkey manage pool found on this identity.", "error");
2468
+ return;
2469
+ }
2470
+
2471
+ let passkeySecret: string | undefined;
2472
+ let signingPoolId: string | undefined;
2473
+
2474
+ try {
2475
+ const authResult = await promptForAuthoritySecret({
2476
+ keystone: this.ibGib,
2477
+ aggregatedDetails: this.aggregatedDetails,
2478
+ title: 'Authorize Master Password',
2479
+ msg: 'Please touch your fingerprint sensor, Face ID, or security key to authorize adding a Master Password...',
2480
+ verb: KEYSTONE_VERB_MANAGE
2481
+ });
2482
+ passkeySecret = authResult.secret;
2483
+ signingPoolId = authResult.poolId;
2484
+ } catch (err) {
2485
+ this.setChangePasswordStatus("Authorization cancelled.", "error");
2486
+ return;
2487
+ }
2488
+
2489
+ this.showBusy({
2490
+ isBusy: true,
2491
+ title: 'Adding Master Password...',
2492
+ msg: 'Compiling cryptographic master password pool and evolving identity...',
2493
+ animationEmoji: '🔑'
2494
+ });
2495
+ await delay(50);
2496
+ this.setChangePasswordStatus("Generating sovereign master password challenge pool...", "info");
2497
+
2498
+ const metaspace = await getGlobalMetaspace_waitIfNeeded();
2499
+ if (!metaspace) throw new Error("Global metaspace not available");
2500
+ const space = await metaspace.getLocalUserSpace({ lock: false });
2501
+ if (!space) throw new Error("Local user space not available");
2502
+
2503
+ const poolConfig: KeystonePoolConfig_HashV1 = {
2504
+ id: POOL_ID_MANAGE,
2505
+ type: KeystoneChallengeType.hash_reveal_v1,
2506
+ salt: POOL_ID_MANAGE,
2507
+ algo: HashAlgorithm.sha_256,
2508
+ rounds: 1,
2509
+ allowedVerbs: [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
2510
+ behavior: {
2511
+ size: 10,
2512
+ replenish: KeystoneReplenishStrategy.topUp,
2513
+ selectSequentially: 1,
2514
+ selectRandomly: 0,
2515
+ targetBindingCount: 0,
2516
+ }
2517
+ };
2518
+
2519
+ const newChallenges = await generatePoolChallenges({
2520
+ config: poolConfig,
2521
+ masterSecret: newPassword,
2522
+ });
2523
+
2524
+ const newManagePool = {
2525
+ id: POOL_ID_MANAGE,
2526
+ config: poolConfig,
2527
+ challenges: newChallenges,
2528
+ };
2529
+
2530
+ const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
2531
+ || (this.ibGibAddr ? (() => {
2532
+ const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
2533
+ const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
2534
+ return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
2535
+ })() : undefined)
2536
+ || getIbGibAddr({ ibGib: this.ibGib! });
2537
+
2538
+ const keystoneService = new KeystoneService_V1();
2539
+ const evolvedParent = await keystoneService.sign({
2540
+ latestKeystone: this.ibGib!,
2541
+ claim: {
2542
+ target: primaryTjpAddr,
2543
+ verb: KEYSTONE_VERB_MANAGE
2544
+ },
2545
+ masterSecret: passkeySecret,
2546
+ poolId: signingPoolId,
2547
+ dataToPatch: {
2548
+ challengePools: [...(this.ibGib!.data!.challengePools || []), newManagePool]
2549
+ },
2550
+ metaspace,
2551
+ space
2552
+ });
2553
+
2554
+ // Sync pool addition to server
2555
+ this.showBusy({
2556
+ isBusy: true,
2557
+ title: 'Syncing to Server...',
2558
+ msg: 'Publishing updated identity to server...',
2559
+ animationEmoji: '⚡'
2560
+ });
2561
+
2562
+ const resSync = await getApiBridge().putEvolveKeystone({
2563
+ domainAddr: primaryTjpAddr,
2564
+ keystoneIbGib: evolvedParent,
2565
+ relatedKeystoneIbGibs: []
2566
+ });
2567
+
2568
+ if (!resSync.success) {
2569
+ throw new Error(`Server sync failed: ${resSync.message}`);
2570
+ }
2571
+
2572
+ // Save locally
2573
+ await metaspace.put({ ibGibs: [evolvedParent], space });
2574
+ await metaspace.registerNewIbGib({ ibGib: evolvedParent, space });
2575
+
2576
+ // Delegate registration & final sync via DRY helper
2577
+ await this.registerAndSyncSovereignPasswordDelegate({
2578
+ evolvedParent,
2579
+ newPassword,
2580
+ primaryTjpAddr,
2581
+ metaspace,
2582
+ space
2583
+ });
2584
+
2585
+ } catch (error) {
2586
+ console.error(`${lc} Failed to add password to passkey identity: ${extractErrorMsg(error)}`, error);
2587
+ this.setChangePasswordStatus(`Failed to add master password: ${extractErrorMsg(error)}`, "error");
2588
+ } finally {
2589
+ this.showBusy({ isBusy: false });
2590
+ }
2591
+ }
2592
+
2463
2593
  protected async handleChangePassword(): Promise<void> {
2464
2594
  const lc = `${this.lc}[${this.handleChangePassword.name}]`;
2465
2595
 
@@ -2467,9 +2597,14 @@ export class KeystoneDetailsComponentInstance
2467
2597
  getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
2468
2598
  undefined;
2469
2599
 
2470
- const isCustodialOnly = !!caps ? caps.isCustodial && !caps.isSovereign : false;
2471
- if (isCustodialOnly) {
2472
- await this.handleAddMasterPassword();
2600
+ const hasPassword = !!caps?.hasPassword;
2601
+ if (!hasPassword) {
2602
+ const isCustodialOnly = !!caps ? caps.isCustodial && !caps.isSovereign : false;
2603
+ if (isCustodialOnly) {
2604
+ await this.handleAddMasterPassword();
2605
+ } else {
2606
+ await this.handleAddPasswordToPasskeyIdentity();
2607
+ }
2473
2608
  return;
2474
2609
  }
2475
2610