@ibgib/web-gib 0.0.60 → 0.0.62

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/dist/AUTO-GENERATED-version.d.mts +1 -1
  2. package/dist/AUTO-GENERATED-version.mjs +1 -1
  3. package/dist/helpers.d.mts.map +1 -1
  4. package/dist/helpers.mjs +1 -0
  5. package/dist/helpers.mjs.map +1 -1
  6. package/dist/helpers.web.d.mts +3 -1
  7. package/dist/helpers.web.d.mts.map +1 -1
  8. package/dist/helpers.web.mjs +17 -1
  9. package/dist/helpers.web.mjs.map +1 -1
  10. package/dist/identity/device-delegate-helper.d.mts +91 -0
  11. package/dist/identity/device-delegate-helper.d.mts.map +1 -0
  12. package/dist/identity/device-delegate-helper.mjs +190 -0
  13. package/dist/identity/device-delegate-helper.mjs.map +1 -0
  14. package/dist/identity/device-delegate.respec.d.mts +2 -0
  15. package/dist/identity/device-delegate.respec.d.mts.map +1 -0
  16. package/dist/identity/device-delegate.respec.mjs +106 -0
  17. package/dist/identity/device-delegate.respec.mjs.map +1 -0
  18. package/dist/identity/keystone-sync-service.d.mts +18 -0
  19. package/dist/identity/keystone-sync-service.d.mts.map +1 -1
  20. package/dist/identity/keystone-sync-service.mjs +77 -1
  21. package/dist/identity/keystone-sync-service.mjs.map +1 -1
  22. package/dist/identity/passkey-helper.d.mts +72 -0
  23. package/dist/identity/passkey-helper.d.mts.map +1 -0
  24. package/dist/identity/passkey-helper.mjs +237 -0
  25. package/dist/identity/passkey-helper.mjs.map +1 -0
  26. package/dist/identity/passkey.respec.d.mts +2 -0
  27. package/dist/identity/passkey.respec.d.mts.map +1 -0
  28. package/dist/identity/passkey.respec.mjs +169 -0
  29. package/dist/identity/passkey.respec.mjs.map +1 -0
  30. package/dist/index.d.mts +2 -1
  31. package/dist/index.d.mts.map +1 -1
  32. package/dist/index.mjs +2 -2
  33. package/dist/index.mjs.map +1 -1
  34. package/dist/storage/storage-helpers.web.d.mts +9 -0
  35. package/dist/storage/storage-helpers.web.d.mts.map +1 -1
  36. package/dist/storage/storage-helpers.web.mjs +42 -0
  37. package/dist/storage/storage-helpers.web.mjs.map +1 -1
  38. package/dist/ui/component/identity/add-device/add-device.d.mts.map +1 -1
  39. package/dist/ui/component/identity/add-device/add-device.mjs +31 -43
  40. package/dist/ui/component/identity/add-device/add-device.mjs.map +1 -1
  41. package/dist/ui/component/identity/keystone-creator/keystone-creator.d.mts +18 -1
  42. package/dist/ui/component/identity/keystone-creator/keystone-creator.d.mts.map +1 -1
  43. package/dist/ui/component/identity/keystone-creator/keystone-creator.html +42 -3
  44. package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs +524 -104
  45. package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs.map +1 -1
  46. package/dist/ui/component/identity/keystone-details/keystone-details.d.mts +19 -1
  47. package/dist/ui/component/identity/keystone-details/keystone-details.d.mts.map +1 -1
  48. package/dist/ui/component/identity/keystone-details/keystone-details.html +38 -0
  49. package/dist/ui/component/identity/keystone-details/keystone-details.mjs +1217 -132
  50. package/dist/ui/component/identity/keystone-details/keystone-details.mjs.map +1 -1
  51. package/dist/ui/component/identity/remove-device/remove-device.d.mts.map +1 -1
  52. package/dist/ui/component/identity/remove-device/remove-device.mjs +1 -0
  53. package/dist/ui/component/identity/remove-device/remove-device.mjs.map +1 -1
  54. package/dist/ui/ui-constants.d.mts +1 -0
  55. package/dist/ui/ui-constants.d.mts.map +1 -1
  56. package/dist/ui/ui-constants.mjs +1 -0
  57. package/dist/ui/ui-constants.mjs.map +1 -1
  58. package/dist/ui/ui-helpers.d.mts +3 -0
  59. package/dist/ui/ui-helpers.d.mts.map +1 -1
  60. package/dist/ui/ui-helpers.mjs +110 -6
  61. package/dist/ui/ui-helpers.mjs.map +1 -1
  62. package/dist/ui/ui-types.d.mts +5 -0
  63. package/dist/ui/ui-types.d.mts.map +1 -1
  64. package/package.json +2 -2
  65. package/src/AUTO-GENERATED-version.mts +1 -1
  66. package/src/helpers.mts +1 -0
  67. package/src/helpers.web.mts +22 -2
  68. package/src/identity/custodian-delegate-helper.mts.bak +7 -0
  69. package/src/identity/device-delegate-helper.mts +268 -0
  70. package/src/identity/device-delegate.respec.mts +125 -0
  71. package/src/identity/keystone-sync-service.mts +95 -2
  72. package/src/identity/passkey-helper.mts +304 -0
  73. package/src/identity/passkey.respec.mts +194 -0
  74. package/src/index.mts +2 -2
  75. package/src/storage/storage-helpers.web.mts +47 -0
  76. package/src/ui/component/identity/add-device/add-device.mts +37 -48
  77. package/src/ui/component/identity/keystone-creator/keystone-creator.html +42 -3
  78. package/src/ui/component/identity/keystone-creator/keystone-creator.mts +568 -111
  79. package/src/ui/component/identity/keystone-details/keystone-details.html +38 -0
  80. package/src/ui/component/identity/keystone-details/keystone-details.mts +1290 -142
  81. package/src/ui/component/identity/remove-device/remove-device.mts +1 -0
  82. package/src/ui/ui-constants.mts +1 -0
  83. package/src/ui/ui-helpers.mts +118 -5
  84. package/src/ui/ui-types.mts +6 -0
  85. package/dist/identity/custodian-delegate-helper.d.mts +0 -22
  86. package/dist/identity/custodian-delegate-helper.d.mts.map +0 -1
  87. package/dist/identity/custodian-delegate-helper.mjs +0 -67
  88. package/dist/identity/custodian-delegate-helper.mjs.map +0 -1
  89. package/src/identity/custodian-delegate-helper.mts +0 -86
@@ -2,7 +2,7 @@ import thisCss from "./keystone-creator.css";
2
2
  import thisHtml from "./keystone-creator.html";
3
3
  import commonComponentStylesCss from "../../common-component-styles.css";
4
4
 
5
- import { delay, extractErrorMsg, } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs";
5
+ import { delay, extractErrorMsg, getSaferSubstring } 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 { getIbGibAddr } from "@ibgib/ts-gib/dist/helper.mjs";
@@ -14,7 +14,7 @@ import { toDto, getTjpAddr, getTimelinesGroupedByTjp } from "@ibgib/core-gib/dis
14
14
  import { FlatIbGibGraph } from "@ibgib/core-gib/dist/common/other/graph-types.mjs";
15
15
  import { validateEmail } from "@ibgib/core-gib/dist/keystone/keystone-helpers.mjs";
16
16
  import { KEYSTONE_USERNAME_REGEXP, KEYSTONE_DESCRIPTION_REGEXP, KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT } from "@ibgib/core-gib/dist/keystone/keystone-constants.mjs";
17
- import { deriveDelegateSecret } from "@ibgib/core-gib/dist/keystone/strategy/hash-reveal-v1/hash-reveal-v1.mjs";
17
+ import { deriveDelegateSecret, createSyncDelegateKeystone } from "@ibgib/core-gib/dist/keystone/delegate/delegate-helpers.mjs";
18
18
 
19
19
  import { getGlobalMetaspace_waitIfNeeded, } from "../../../../helpers.mjs";
20
20
  import { IbGibDynamicComponentMetaBase, IbGibFormInstanceBase, } from "../../ibgib-dynamic-component-bases.mjs";
@@ -24,8 +24,11 @@ import {
24
24
  } from "../../component-types.mjs";
25
25
  import { getComponentCtorArg } from "../../../../app-bootstrap/init-orchestration.mjs";
26
26
  import { getApiBridge } from "../../../../api/ibgib-api-bridge.mjs";
27
- import { deriveLocalCustodianDelegateSecret } from "../../../../identity/custodian-delegate-helper.mjs";
27
+ import { getOrCreateDeviceDelegateSecret, setDeviceDelegateSecret, createSovereignDeviceSyncDelegate, createOAuth2DeviceSyncDelegate, createEmailDeviceSyncDelegate } from "../../../../identity/device-delegate-helper.mjs";
28
28
  import { executeSsoOAuthPopup } from "../../../../identity/sso-popup-helper.mjs";
29
+ import { generateDefaultDeviceTag } from "../../../../util/device-helper.mjs";
30
+ import { isPasskeyPrfSupported, registerPasskeyCredential, evaluatePasskeyPrf, createPasskeyDeviceSyncDelegate, bufferToHex } from "../../../../identity/passkey-helper.mjs";
31
+ import { KeystonePasskeyEntry } from "@ibgib/core-gib/dist/keystone/keystone-types.mjs";
29
32
  import { EVENT_IBGIB_IDENTITY_REQUEST_CHANGE, EVENT_IBGIB_UI_MESSAGE } from "../../../ui-constants.mjs";
30
33
  import { alertUser, shadowRoot_getElementById } from "../../../../helpers.web.mjs";
31
34
 
@@ -80,6 +83,12 @@ interface KeystoneCreatorElements extends ElementsBase {
80
83
  inputEmail: HTMLInputElement;
81
84
  inputSecret: HTMLInputElement;
82
85
  checkboxMakeActive: HTMLInputElement;
86
+ inputPasskeyUsername: HTMLInputElement;
87
+ inputPasskeyDescription: HTMLInputElement;
88
+ inputPasskeyEmail: HTMLInputElement;
89
+ inputPasskeyName: HTMLInputElement;
90
+ checkboxPasskeyMakeActive: HTMLInputElement;
91
+ btnGeneratePasskey: HTMLButtonElement;
83
92
  statusArea: HTMLDivElement;
84
93
  statusText: HTMLParagraphElement;
85
94
  }
@@ -93,6 +102,7 @@ export class KeystoneCreatorComponentInstance
93
102
 
94
103
  protected lc: string = `[KeystoneCreatorComponentInstance]`;
95
104
  private hasExistingIdentities = false;
105
+ private resendCooldownInterval: any = null;
96
106
 
97
107
  constructor() {
98
108
  super();
@@ -102,6 +112,36 @@ export class KeystoneCreatorComponentInstance
102
112
  await super.initialize(opts);
103
113
  }
104
114
 
115
+ override async disconnected(): Promise<void> {
116
+ if (this.resendCooldownInterval) {
117
+ clearInterval(this.resendCooldownInterval);
118
+ this.resendCooldownInterval = null;
119
+ }
120
+ }
121
+
122
+ private startResendCooldownTimer({ btnId, durationSecs = 30 }: { btnId: string; durationSecs?: number }): void {
123
+ const btn = this.shadowRoot?.getElementById(btnId) as HTMLButtonElement | null;
124
+ if (!btn) return;
125
+ if (this.resendCooldownInterval) {
126
+ clearInterval(this.resendCooldownInterval);
127
+ this.resendCooldownInterval = null;
128
+ }
129
+ let remaining = durationSecs;
130
+ btn.disabled = true;
131
+ btn.textContent = `Resend Code (${remaining}s)`;
132
+ this.resendCooldownInterval = setInterval(() => {
133
+ remaining -= 1;
134
+ if (remaining <= 0) {
135
+ clearInterval(this.resendCooldownInterval);
136
+ this.resendCooldownInterval = null;
137
+ btn.disabled = false;
138
+ btn.textContent = "Resend Code";
139
+ } else {
140
+ btn.textContent = `Resend Code (${remaining}s)`;
141
+ }
142
+ }, 1000);
143
+ }
144
+
105
145
  override async created(): Promise<void> {
106
146
  this.elements = {} as KeystoneCreatorElements;
107
147
 
@@ -115,6 +155,12 @@ export class KeystoneCreatorComponentInstance
115
155
  this.elements.inputEmail = this.shadowRoot!.getElementById('input-email') as HTMLInputElement;
116
156
  this.elements.inputSecret = this.shadowRoot!.getElementById('input-secret') as HTMLInputElement;
117
157
  this.elements.checkboxMakeActive = this.shadowRoot!.getElementById('checkbox-make-active') as HTMLInputElement;
158
+ this.elements.inputPasskeyUsername = this.shadowRoot!.getElementById('input-passkey-username') as HTMLInputElement;
159
+ this.elements.inputPasskeyDescription = this.shadowRoot!.getElementById('input-passkey-description') as HTMLInputElement;
160
+ this.elements.inputPasskeyEmail = this.shadowRoot!.getElementById('input-passkey-email') as HTMLInputElement;
161
+ this.elements.inputPasskeyName = this.shadowRoot!.getElementById('input-passkey-name') as HTMLInputElement;
162
+ this.elements.checkboxPasskeyMakeActive = this.shadowRoot!.getElementById('checkbox-passkey-make-active') as HTMLInputElement;
163
+ this.elements.btnGeneratePasskey = this.shadowRoot!.getElementById('btn-generate-passkey') as HTMLButtonElement;
118
164
  this.elements.statusArea = this.shadowRoot!.getElementById('status-area') as HTMLDivElement;
119
165
  this.elements.statusText = this.shadowRoot!.getElementById('status-text') as HTMLParagraphElement;
120
166
 
@@ -149,49 +195,50 @@ export class KeystoneCreatorComponentInstance
149
195
  this.hasExistingIdentities = false;
150
196
  }
151
197
 
152
- const checkbox = this.elements!.checkboxMakeActive;
153
- if (checkbox) {
154
- if (!this.hasExistingIdentities) {
155
- checkbox.checked = true;
156
- checkbox.disabled = true;
157
- checkbox.title = "At least one active identity is required.";
158
- } else {
159
- checkbox.disabled = false;
160
- checkbox.title = "";
198
+ const checkboxes = [this.elements!.checkboxMakeActive, this.elements!.checkboxPasskeyMakeActive];
199
+ for (const checkbox of checkboxes) {
200
+ if (checkbox) {
201
+ if (!this.hasExistingIdentities) {
202
+ checkbox.checked = true;
203
+ checkbox.disabled = true;
204
+ checkbox.title = "At least one active identity is required.";
205
+ } else {
206
+ checkbox.disabled = false;
207
+ checkbox.title = "";
208
+ }
161
209
  }
162
210
  }
163
211
  }
164
212
 
165
- override async disconnected(): Promise<void> {
166
- // cleanup if needed
167
- }
168
-
169
213
  private initHandlers() {
170
- // Mode Tab Switching (Quick vs Password-based)
214
+ // Mode Tab Switching (Quick vs Passkey vs Password-based)
171
215
  const tabQuick = this.shadowRoot?.getElementById('tab-quick') as HTMLButtonElement | null;
216
+ const tabPasskey = this.shadowRoot?.getElementById('tab-passkey') as HTMLButtonElement | null;
172
217
  const tabPassword = this.shadowRoot?.getElementById('tab-password') as HTMLButtonElement | null;
173
218
  const panelQuick = this.shadowRoot?.getElementById('panel-quick') as HTMLDivElement | null;
219
+ const panelPasskey = this.shadowRoot?.getElementById('panel-passkey') as HTMLDivElement | null;
174
220
  const panelPassword = this.shadowRoot?.getElementById('panel-password') as HTMLDivElement | null;
175
221
  const subtitleQuick = this.shadowRoot?.getElementById('subtitle-quick') as HTMLParagraphElement | null;
222
+ const subtitlePasskey = this.shadowRoot?.getElementById('subtitle-passkey') as HTMLParagraphElement | null;
176
223
  const subtitlePassword = this.shadowRoot?.getElementById('subtitle-password') as HTMLParagraphElement | null;
177
224
 
178
- tabQuick?.addEventListener('click', () => {
179
- tabQuick.classList.add('active');
180
- tabPassword?.classList.remove('active');
181
- panelQuick?.classList.remove('hidden');
182
- panelPassword?.classList.add('hidden');
183
- subtitleQuick?.classList.remove('hidden');
184
- subtitlePassword?.classList.add('hidden');
185
- });
225
+ const switchTab = (mode: 'quick' | 'passkey' | 'password') => {
226
+ tabQuick?.classList.toggle('active', mode === 'quick');
227
+ tabPasskey?.classList.toggle('active', mode === 'passkey');
228
+ tabPassword?.classList.toggle('active', mode === 'password');
186
229
 
187
- tabPassword?.addEventListener('click', () => {
188
- tabPassword.classList.add('active');
189
- tabQuick?.classList.remove('active');
190
- panelPassword?.classList.remove('hidden');
191
- panelQuick?.classList.add('hidden');
192
- subtitlePassword?.classList.remove('hidden');
193
- subtitleQuick?.classList.add('hidden');
194
- });
230
+ panelQuick?.classList.toggle('hidden', mode !== 'quick');
231
+ panelPasskey?.classList.toggle('hidden', mode !== 'passkey');
232
+ panelPassword?.classList.toggle('hidden', mode !== 'password');
233
+
234
+ subtitleQuick?.classList.toggle('hidden', mode !== 'quick');
235
+ subtitlePasskey?.classList.toggle('hidden', mode !== 'passkey');
236
+ subtitlePassword?.classList.toggle('hidden', mode !== 'password');
237
+ };
238
+
239
+ tabQuick?.addEventListener('click', () => switchTab('quick'));
240
+ tabPasskey?.addEventListener('click', () => switchTab('passkey'));
241
+ tabPassword?.addEventListener('click', () => switchTab('password'));
195
242
 
196
243
  const btnGoogleSso = this.shadowRoot?.getElementById('btn-google-sso') as HTMLButtonElement | null;
197
244
  btnGoogleSso?.addEventListener('click', () => this.handleSsoClick('google'));
@@ -215,11 +262,13 @@ export class KeystoneCreatorComponentInstance
215
262
  const secQuickOtp = this.shadowRoot?.getElementById('section-quick-otp') as HTMLDivElement | null;
216
263
  const inputQuickOtp = this.shadowRoot?.getElementById('input-quick-otp') as HTMLInputElement | null;
217
264
  const btnQuickVerifyCode = this.shadowRoot?.getElementById('btn-quick-verify-code') as HTMLButtonElement | null;
265
+ const btnQuickResendCode = this.shadowRoot?.getElementById('btn-quick-resend-code') as HTMLButtonElement | null;
218
266
 
219
267
  btnQuickSendCode?.addEventListener('click', () => this.handleQuickSendCode());
220
-
221
268
  btnQuickVerifyCode?.addEventListener('click', () => this.handleQuickVerifyCode());
269
+ btnQuickResendCode?.addEventListener('click', () => this.handleQuickResendCode());
222
270
 
271
+ this.elements!.btnGeneratePasskey?.addEventListener('click', () => this.handleGeneratePasskey());
223
272
  this.elements!.btnGenerateEl.addEventListener('click', () => this.handleGenerate());
224
273
 
225
274
  this.shadowRoot!.getElementById('btn-switch-sign-in')?.addEventListener('click', (e) => {
@@ -232,6 +281,16 @@ export class KeystoneCreatorComponentInstance
232
281
  });
233
282
 
234
283
  // Clear invalid styling when user inputs text
284
+ this.elements!.inputPasskeyUsername?.addEventListener('input', () => {
285
+ this.elements!.inputPasskeyUsername.classList.remove('invalid');
286
+ });
287
+ this.elements!.inputPasskeyDescription?.addEventListener('input', () => {
288
+ this.elements!.inputPasskeyDescription.classList.remove('invalid');
289
+ });
290
+ this.elements!.inputPasskeyEmail?.addEventListener('input', () => {
291
+ this.elements!.inputPasskeyEmail.classList.remove('invalid');
292
+ });
293
+
235
294
  this.elements!.inputUsername?.addEventListener('input', () => {
236
295
  this.elements!.inputUsername.classList.remove('invalid');
237
296
  });
@@ -307,25 +366,6 @@ export class KeystoneCreatorComponentInstance
307
366
  animationEmoji: '🔑'
308
367
  });
309
368
 
310
- const delegateSecret = await deriveLocalCustodianDelegateSecret({ primaryTjpAddr });
311
- const email = resLogin.userInfo?.email || '';
312
- const username = email ? email.split('@')[0] : 'user';
313
- const custodianSyncProfileName = process.env.KEYSTONE_PROFILE_CUSTODIAN_SYNC;
314
- if (!custodianSyncProfileName || !custodianSyncProfileName.trim()) {
315
- throw new Error('KEYSTONE_PROFILE_CUSTODIAN_SYNC environment variable is missing. Expected valid profile name like "sync-profile.dev" or "sync-profile.prod". (E: 4d5e6f7890123456789abcdef4)');
316
- }
317
-
318
- const delegateBuilder = KeystoneProfileBuilder.buildKeystone(custodianSyncProfileName.trim() as any)
319
- .withUsername(`${username}-sync`)
320
- .withDescription(`Sync Delegate for ${username}`)
321
- .withDetails({
322
- client: 'space-gib-web',
323
- role: 'sync-delegate',
324
- primaryTjpAddr,
325
- custodianReqs,
326
- });
327
-
328
- const configs = await delegateBuilder.compileConfigs();
329
369
  const metaspace = await getGlobalMetaspace_waitIfNeeded();
330
370
  const space = await metaspace.getLocalUserSpace({ lock: false });
331
371
  if (!space) {
@@ -333,14 +373,16 @@ export class KeystoneCreatorComponentInstance
333
373
  return;
334
374
  }
335
375
 
336
- const keystoneService = new KeystoneService_V1();
337
- const delegateKeystone = await keystoneService.genesis({
338
- masterSecret: delegateSecret,
339
- configs,
376
+ const email = resLogin.userInfo?.email || '';
377
+ const username = email ? email.split('@')[0] : 'user';
378
+
379
+ const { delegateKeystone, delegateSecret } = await createOAuth2DeviceSyncDelegate({
380
+ providerId,
381
+ username,
382
+ primaryTjpAddr,
383
+ custodianReqs,
340
384
  metaspace,
341
385
  space,
342
- frameDetails: delegateBuilder.getFrameDetails(),
343
- isPrimary: false
344
386
  });
345
387
 
346
388
  this.setStatus('Registering device delegate with custodian server...', 'info');
@@ -403,6 +445,7 @@ export class KeystoneCreatorComponentInstance
403
445
  if (resCode.success) {
404
446
  secQuickOtp?.classList.remove('hidden');
405
447
  this.setStatus("✉️ 6-digit verification code sent to your email!", "success");
448
+ this.startResendCooldownTimer({ btnId: 'btn-quick-resend-code', durationSecs: 30 });
406
449
  } else {
407
450
  this.setStatus(resCode.message || "Failed to dispatch verification code.", "error");
408
451
  }
@@ -418,6 +461,54 @@ export class KeystoneCreatorComponentInstance
418
461
  }
419
462
  }
420
463
 
464
+ /**
465
+ * Triggered by the "Resend Code" button in the quick email panel.
466
+ */
467
+ protected async handleQuickResendCode(): Promise<void> {
468
+ const lc = `${this.lc}[${this.handleQuickResendCode.name}]`;
469
+ const btnQuickResendCode = this.shadowRoot?.getElementById('btn-quick-resend-code') as HTMLButtonElement | null;
470
+ const inputQuickEmail = this.shadowRoot?.getElementById('input-quick-email') as HTMLInputElement | null;
471
+ const inputQuickOtp = this.shadowRoot?.getElementById('input-quick-otp') as HTMLInputElement | null;
472
+
473
+ const email = (inputQuickEmail?.value || '').trim();
474
+ if (!email || !validateEmail(email)) {
475
+ this.setStatus("Please enter a valid email address.", "error");
476
+ return;
477
+ }
478
+
479
+ if (btnQuickResendCode) {
480
+ btnQuickResendCode.disabled = true;
481
+ btnQuickResendCode.textContent = "Sending...";
482
+ }
483
+ this.setStatus("Resending verification code...", "info");
484
+
485
+ try {
486
+ const resCode = await getApiBridge().postVerificationCode({ email });
487
+ if (resCode.success) {
488
+ this.setStatus(`✉️ Fresh 6-digit verification code sent to ${email}!`, "success");
489
+ this.startResendCooldownTimer({ btnId: 'btn-quick-resend-code', durationSecs: 30 });
490
+ if (inputQuickOtp) {
491
+ inputQuickOtp.value = '';
492
+ inputQuickOtp.focus();
493
+ }
494
+ } else {
495
+ this.setStatus(resCode.message || "Failed to resend verification code.", "error");
496
+ if (btnQuickResendCode) {
497
+ btnQuickResendCode.disabled = false;
498
+ btnQuickResendCode.textContent = "Resend Code";
499
+ }
500
+ }
501
+ } catch (error) {
502
+ const emsg = extractErrorMsg(error);
503
+ console.error(`${lc} ${emsg}`);
504
+ this.setStatus(`Failed to resend verification code: ${emsg}`, "error");
505
+ if (btnQuickResendCode) {
506
+ btnQuickResendCode.disabled = false;
507
+ btnQuickResendCode.textContent = "Resend Code";
508
+ }
509
+ }
510
+ }
511
+
421
512
  /**
422
513
  * Triggered by the "Verify Code" button.
423
514
  * Submits 6-digit OTP code to custodian server, builds local sync delegate keystone,
@@ -467,24 +558,6 @@ export class KeystoneCreatorComponentInstance
467
558
  animationEmoji: '🔑'
468
559
  });
469
560
 
470
- const delegateSecret = await deriveLocalCustodianDelegateSecret({ primaryTjpAddr });
471
- const username = email ? email.split('@')[0] : 'user';
472
- const custodianSyncProfileName = process.env.KEYSTONE_PROFILE_CUSTODIAN_SYNC;
473
- if (!custodianSyncProfileName || !custodianSyncProfileName.trim()) {
474
- throw new Error('KEYSTONE_PROFILE_CUSTODIAN_SYNC environment variable is missing. Expected valid profile name like "sync-profile.dev" or "sync-profile.prod". (E: 4d5e6f7890123456789abcdef4)');
475
- }
476
-
477
- const delegateBuilder = KeystoneProfileBuilder.buildKeystone(custodianSyncProfileName.trim() as any)
478
- .withUsername(`${username}-sync`)
479
- .withDescription(`Sync Delegate for ${username}`)
480
- .withDetails({
481
- client: 'space-gib-web',
482
- role: 'sync-delegate',
483
- primaryTjpAddr,
484
- custodianReqs,
485
- });
486
-
487
- const configs = await delegateBuilder.compileConfigs();
488
561
  const metaspace = await getGlobalMetaspace_waitIfNeeded();
489
562
  const space = await metaspace.getLocalUserSpace({ lock: false });
490
563
  if (!space) {
@@ -492,14 +565,12 @@ export class KeystoneCreatorComponentInstance
492
565
  return;
493
566
  }
494
567
 
495
- const keystoneService = new KeystoneService_V1();
496
- const delegateKeystone = await keystoneService.genesis({
497
- masterSecret: delegateSecret,
498
- configs,
568
+ const { delegateKeystone, delegateSecret } = await createEmailDeviceSyncDelegate({
569
+ email,
570
+ primaryTjpAddr,
571
+ custodianReqs,
499
572
  metaspace,
500
573
  space,
501
- frameDetails: delegateBuilder.getFrameDetails(),
502
- isPrimary: false
503
574
  });
504
575
 
505
576
  // Register delegate with server
@@ -540,6 +611,389 @@ export class KeystoneCreatorComponentInstance
540
611
  }
541
612
  }
542
613
 
614
+ /**
615
+ * Triggered by the "Create Identity with Passkey / Biometrics" button.
616
+ * Orchestrates: WebAuthn Credential -> PRF Secret Evaluation -> Local Passkey Genesis -> Graph Collection -> Server Sync.
617
+ */
618
+ protected async handleGeneratePasskey() {
619
+ const lc = `${this.lc}[${this.handleGeneratePasskey.name}]`;
620
+ const btn = this.shadowRoot?.getElementById('btn-generate-passkey') as HTMLButtonElement | null;
621
+ try {
622
+ console.log(`${lc} starting passkey genesis orchestration...`);
623
+ const humanEyeDelay = 500;
624
+ this.setStatus("Initializing passkey genesis...", "info");
625
+
626
+ const inputUsername = this.elements!.inputPasskeyUsername;
627
+ const inputDesc = this.elements!.inputPasskeyDescription;
628
+ const inputEmail = this.elements!.inputPasskeyEmail;
629
+ const inputName = this.elements!.inputPasskeyName;
630
+ const checkboxActive = this.elements!.checkboxPasskeyMakeActive;
631
+
632
+ const username = (inputUsername?.value || '').trim();
633
+ if (inputUsername) inputUsername.value = username;
634
+
635
+ if (!this.validateInputPattern({
636
+ input: inputUsername!,
637
+ pattern: KEYSTONE_USERNAME_REGEXP,
638
+ errorMessage: "Username must be 1-63 characters (alphanumeric, underscores, hyphens, dots).",
639
+ required: true
640
+ })) {
641
+ this.setStatus("Invalid username. Must be 1-63 characters (alphanumeric, _, -, .).", "error");
642
+ return;
643
+ }
644
+
645
+ const description = (inputDesc?.value || '').trim();
646
+ if (inputDesc) inputDesc.value = description;
647
+ if (inputDesc && !this.validateInputPattern({
648
+ input: inputDesc,
649
+ pattern: KEYSTONE_DESCRIPTION_REGEXP,
650
+ errorMessage: "Description must be 0-128 characters.",
651
+ required: false
652
+ })) {
653
+ this.setStatus("Invalid description. Max 128 chars.", "error");
654
+ return;
655
+ }
656
+
657
+ const email = (inputEmail?.value || '').trim();
658
+ if (inputEmail) inputEmail.value = email;
659
+ if (email && !validateEmail(email)) {
660
+ inputEmail?.classList.add('invalid');
661
+ this.setStatus("Invalid email address format.", "error");
662
+ return;
663
+ }
664
+
665
+ if (btn) {
666
+ btn.disabled = true;
667
+ btn.textContent = 'Awaiting Biometrics / Security Key...';
668
+ }
669
+
670
+ // Pre-check email availability on server if email provided
671
+ if (email) {
672
+ this.showBusy({
673
+ isBusy: true,
674
+ title: 'Checking Email Availability...',
675
+ msg: 'Checking server identity index...',
676
+ animationEmoji: '🔍'
677
+ });
678
+ const resCheck = await getApiBridge().checkEmailAvailability({ email });
679
+ if (!resCheck.available) {
680
+ const msg = resCheck.message || "Email/Identity is already registered.";
681
+ this.setStatus(msg, "error");
682
+ this.showBusy({ isBusy: false });
683
+ if (btn) {
684
+ btn.disabled = false;
685
+ btn.textContent = '🛡️ Create Identity with Passkey / Biometrics';
686
+ }
687
+ return;
688
+ }
689
+ }
690
+
691
+ // 1. WebAuthn Passkey Registration & PRF Secret Evaluation
692
+ this.showBusy({
693
+ isBusy: true,
694
+ title: 'Touch Biometrics or Security Key',
695
+ msg: 'Please touch your fingerprint sensor, Face ID, or insert your USB security key when prompted...',
696
+ animationEmoji: '🛡️'
697
+ });
698
+
699
+ const rawSalt = crypto.getRandomValues(new Uint8Array(32));
700
+ const saltHex = bufferToHex(rawSalt.buffer as ArrayBuffer);
701
+ const regResult = await registerPasskeyCredential({
702
+ username,
703
+ displayName: username,
704
+ salt: saltHex
705
+ });
706
+ const { credentialId, transports } = regResult;
707
+
708
+ let passkeySecret = regResult.passkeySecret;
709
+ if (!passkeySecret) {
710
+ const prfResult = await evaluatePasskeyPrf({
711
+ credentialId,
712
+ salt: saltHex,
713
+ transports
714
+ });
715
+ passkeySecret = prfResult.passkeySecret;
716
+ }
717
+
718
+ const passkeyName = (inputName?.value || '').trim() || generateDefaultDeviceTag();
719
+ const passkeyTag = getSaferSubstring({ text: passkeyName, length: 12, keepLiterals: ['-'] }) || 'key';
720
+ const poolId = `passkey-manage-${passkeyTag}`;
721
+
722
+ const passkeyEntry: KeystonePasskeyEntry = {
723
+ credentialId,
724
+ poolId,
725
+ name: passkeyName,
726
+ salt: saltHex,
727
+ createdUTC: new Date().toISOString(),
728
+ ...(transports && transports.length > 0 ? { transports } : {})
729
+ };
730
+
731
+ this.showBusy({
732
+ isBusy: true,
733
+ title: 'Generating Identity...',
734
+ msg: 'Compiling sovereign passkey challenge pools...',
735
+ animationEmoji: '⚙️'
736
+ });
737
+
738
+ // 2. Prepare Sovereign Keystone Configs
739
+ const sovereignPrimaryProfileName = process.env.KEYSTONE_PROFILE_SOVEREIGN_PRIMARY;
740
+ if (!sovereignPrimaryProfileName || !sovereignPrimaryProfileName.trim()) {
741
+ throw new Error('KEYSTONE_PROFILE_SOVEREIGN_PRIMARY environment variable is missing.');
742
+ }
743
+
744
+ const builder = KeystoneProfileBuilder.buildKeystone(sovereignPrimaryProfileName.trim() as any)
745
+ .withUsername(username)
746
+ .withDescription(description);
747
+ if (email) {
748
+ builder.withEmails([email]);
749
+ }
750
+ builder.withDetails({
751
+ client: 'space-gib-web',
752
+ role: 'domain-identity',
753
+ passkeys: {
754
+ [credentialId]: passkeyEntry
755
+ }
756
+ });
757
+
758
+ const configs = await builder.compileConfigs();
759
+ // Re-label manage pool to passkey pool id
760
+ for (const cfg of configs) {
761
+ if (cfg.id === 'manage') {
762
+ cfg.id = poolId;
763
+ }
764
+ }
765
+
766
+ // 3. Prepare Metaspace & Local Space
767
+ const metaspace = await getGlobalMetaspace_waitIfNeeded();
768
+ const space = await metaspace.getLocalUserSpace({ lock: false });
769
+ if (!space) {
770
+ this.setStatus("Failed to access local user space.", "error");
771
+ return;
772
+ }
773
+
774
+ // 4. Perform Local Genesis
775
+ this.showBusy({
776
+ isBusy: true,
777
+ title: 'Generating Identity...',
778
+ msg: 'Generating cryptographic domain identity keys...',
779
+ animationEmoji: '🔑'
780
+ });
781
+
782
+ const keystoneService = new KeystoneService_V1();
783
+ const keystoneIbGib = await keystoneService.genesis({
784
+ masterSecret: passkeySecret,
785
+ emails: email ? [email] : undefined,
786
+ credentialIds: [credentialId],
787
+ configs,
788
+ metaspace,
789
+ space,
790
+ frameDetails: builder.getFrameDetails(),
791
+ isPrimary: true
792
+ });
793
+
794
+ const primaryTjpAddr = getTjpAddr({ ibGib: keystoneIbGib, defaultIfNone: 'incomingAddr' })!;
795
+ console.log(`${lc} passkey local genesis complete: ${primaryTjpAddr}`);
796
+ await delay(humanEyeDelay);
797
+ this.setStatus("Local genesis complete. Creating passkey sync delegate...", "info");
798
+
799
+ // 5. Create Passkey Device Sync Delegate
800
+ this.showBusy({
801
+ isBusy: true,
802
+ title: 'Creating Sync Delegate...',
803
+ msg: 'Generating cryptographic sync delegate keys...',
804
+ animationEmoji: '🔑'
805
+ });
806
+
807
+ const { delegateKeystone: delegateKeystoneIbGib, deviceTag, delegateSecret } = await createPasskeyDeviceSyncDelegate({
808
+ username,
809
+ primaryTjpAddr,
810
+ passkeySecret,
811
+ transports,
812
+ metaspace,
813
+ space,
814
+ });
815
+
816
+ // 6. Register Delegate locally on primary keystone
817
+ const evolvedParentKeystoneIbGib = await keystoneService.registerDelegate({
818
+ parentKeystone: keystoneIbGib,
819
+ delegateKeystone: delegateKeystoneIbGib,
820
+ masterSecret: passkeySecret,
821
+ signingPoolId: poolId,
822
+ allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
823
+ deviceName: deviceTag,
824
+ metaspace,
825
+ space,
826
+ });
827
+
828
+ // 7. Post to Server & Handle Verification
829
+ this.setStatus("Syncing genesis keystone to server...", "info");
830
+ this.showBusy({
831
+ isBusy: true,
832
+ title: 'Syncing to Server...',
833
+ msg: 'Staging pending genesis keystone on server...',
834
+ animationEmoji: '🔄'
835
+ });
836
+
837
+ const resSync = await getApiBridge().postGenesisKeystone(keystoneIbGib);
838
+ if (!resSync.success) {
839
+ const msg = `Sync failed: ${resSync.message}`;
840
+ console.error(`${lc} ${msg}`);
841
+ this.setStatus(msg, "error");
842
+ this.showBusy({ isBusy: false });
843
+ if (btn) {
844
+ btn.disabled = false;
845
+ btn.textContent = '🛡️ Create Identity with Passkey / Biometrics';
846
+ }
847
+ return;
848
+ }
849
+
850
+ const isPending = (resSync as any).pending || (resSync as any).statusCode === 202;
851
+
852
+ if (isPending) {
853
+ console.log(`${lc} Genesis keystone quarantined in pending staging area. Prompting for 6-digit verification code.`);
854
+ this.showBusy({ isBusy: false });
855
+ this.setFormDisabled(true);
856
+
857
+ if (!this.shadowRoot) { throw new Error(`(UNEXPECTED) this.shadowRoot falsy? (E: 6d316f5a2bf8e6a0286306468d35c826)`); }
858
+ const secVerify = shadowRoot_getElementById<HTMLDivElement>(this.shadowRoot, 'section-code-verification');
859
+ const inputVerifyCode = shadowRoot_getElementById<HTMLInputElement>(this.shadowRoot, 'input-verify-code');
860
+ const btnSubmitCode = shadowRoot_getElementById<HTMLButtonElement>(this.shadowRoot, 'btn-submit-code');
861
+ const btnResendCode = shadowRoot_getElementById<HTMLButtonElement>(this.shadowRoot, 'btn-resend-code');
862
+
863
+ secVerify.classList.remove('hidden');
864
+ secVerify.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
865
+ this.setStatus("✉️ Verification code sent to your email. Enter 6-digit code below:", "info");
866
+ this.startResendCooldownTimer({ btnId: 'btn-resend-code', durationSecs: 30 });
867
+
868
+ const onResendCode = async () => {
869
+ btnResendCode.disabled = true;
870
+ btnResendCode.textContent = "Sending...";
871
+ this.setStatus("Resending verification code to your email...", "info");
872
+ try {
873
+ const resResend = await getApiBridge().postVerificationCode({ email, primaryTjpAddr });
874
+ if (resResend.success) {
875
+ this.setStatus("✉️ Fresh 6-digit verification code sent to your email!", "success");
876
+ this.startResendCooldownTimer({ btnId: 'btn-resend-code', durationSecs: 30 });
877
+ inputVerifyCode.value = '';
878
+ inputVerifyCode.focus();
879
+ } else {
880
+ this.setStatus(resResend.message || "Failed to resend verification code.", "error");
881
+ btnResendCode.disabled = false;
882
+ btnResendCode.textContent = "Resend Code";
883
+ }
884
+ } catch (error) {
885
+ const emsg = extractErrorMsg(error);
886
+ console.error(`${lc} ${emsg}`);
887
+ this.setStatus(`Failed to resend verification code: ${emsg}`, "error");
888
+ btnResendCode.disabled = false;
889
+ btnResendCode.textContent = "Resend Code";
890
+ }
891
+ };
892
+ btnResendCode.addEventListener('click', onResendCode);
893
+
894
+ await new Promise<void>((resolve) => {
895
+ const onSubmitCode = async () => {
896
+ const code = inputVerifyCode.value.trim();
897
+ if (!code || code.length !== 6 || !/^\d{6}$/.test(code)) {
898
+ this.setStatus("Please enter a valid 6-digit numeric verification code (e.g. 482901).", "error");
899
+ return;
900
+ }
901
+
902
+ btnSubmitCode.disabled = true;
903
+ btnSubmitCode.textContent = "Verifying...";
904
+ this.showBusy({
905
+ isBusy: true,
906
+ title: 'Verifying & Activating...',
907
+ msg: 'Great! Juust a moment...\n(Validating code, promoting server domain space, other security stuff...)',
908
+ animationEmoji: '🔐'
909
+ });
910
+
911
+ const resVerify = await getApiBridge().postVerifyCode({ email, primaryTjpAddr, code });
912
+ if (!resVerify.success) {
913
+ this.showBusy({ isBusy: false });
914
+ this.setStatus(resVerify.message || "Incorrect verification code.", "error");
915
+ btnSubmitCode.disabled = false;
916
+ btnSubmitCode.textContent = "Verify Code";
917
+ return;
918
+ }
919
+
920
+ console.log(`${lc} Code verified & server domain promoted! Syncing delegate...`);
921
+ this.showBusy({
922
+ isBusy: true,
923
+ title: 'Syncing Delegate...',
924
+ msg: 'Posting evolved domain identity and sync delegate to server...',
925
+ animationEmoji: '⚡'
926
+ });
927
+
928
+ btnSubmitCode.removeEventListener('click', onSubmitCode);
929
+ secVerify.classList.add('hidden');
930
+ this.setFormDisabled(false);
931
+
932
+ // Now that server domain space is promoted, post evolved keystone + delegate
933
+ const resEvolve = await getApiBridge().putEvolveKeystone({
934
+ domainAddr: primaryTjpAddr,
935
+ keystoneIbGib: evolvedParentKeystoneIbGib,
936
+ relatedKeystoneIbGibs: [delegateKeystoneIbGib]
937
+ });
938
+ if (!resEvolve.success) {
939
+ console.warn(`${lc} Delegate sync warning after code verification: ${resEvolve.message}`);
940
+ }
941
+
942
+ resolve();
943
+ };
944
+
945
+ btnSubmitCode.addEventListener('click', onSubmitCode);
946
+ });
947
+ } else {
948
+ // Post evolved keystone + delegate keystone immediately
949
+ this.setStatus("Syncing delegate registration to server...", "info");
950
+ this.showBusy({
951
+ isBusy: true,
952
+ title: 'Syncing Delegate...',
953
+ msg: 'Posting evolved domain identity and sync delegate to server...',
954
+ animationEmoji: '🔄'
955
+ });
956
+ const resEvolve = await getApiBridge().putEvolveKeystone({
957
+ domainAddr: primaryTjpAddr,
958
+ keystoneIbGib: evolvedParentKeystoneIbGib,
959
+ relatedKeystoneIbGibs: [delegateKeystoneIbGib]
960
+ });
961
+ if (!resEvolve.success) {
962
+ const msg = `Delegate registration sync failed: ${resEvolve.message}`;
963
+ console.error(`${lc} ${msg}`);
964
+ this.setStatus(msg, "error");
965
+ this.showBusy({ isBusy: false });
966
+ if (btn) {
967
+ btn.disabled = false;
968
+ btn.textContent = '🛡️ Create Identity with Passkey / Biometrics';
969
+ }
970
+ return;
971
+ }
972
+ }
973
+
974
+ // 8. Finalize Onboarding
975
+ await this.finalizeIdentityOnboarding({
976
+ primaryKeystone: evolvedParentKeystoneIbGib,
977
+ delegateKeystone: delegateKeystoneIbGib,
978
+ primaryTjpAddr,
979
+ masterSecret: passkeySecret,
980
+ delegateSecret,
981
+ makeActive: checkboxActive?.checked ?? true
982
+ });
983
+
984
+ } catch (error) {
985
+ const emsg = extractErrorMsg(error);
986
+ console.error(`${lc} ${emsg}`, error);
987
+ this.setStatus(`Error: ${emsg}`, "error");
988
+ } finally {
989
+ this.showBusy({ isBusy: false });
990
+ if (btn) {
991
+ btn.disabled = false;
992
+ btn.textContent = '🛡️ Create Identity with Passkey / Biometrics';
993
+ }
994
+ }
995
+ }
996
+
543
997
  /**
544
998
  * Triggered by the "Generate" button.
545
999
  * Orchestrates: Secret -> Local Genesis -> Graph Collection -> Server Sync.
@@ -683,28 +1137,12 @@ export class KeystoneCreatorComponentInstance
683
1137
  animationEmoji: '🔑'
684
1138
  });
685
1139
 
686
- const delegateSecret = await deriveDelegateSecret({ masterSecret });
687
- const sovereignSyncProfileName = process.env.KEYSTONE_PROFILE_SOVEREIGN_SYNC;
688
- if (!sovereignSyncProfileName || !sovereignSyncProfileName.trim()) {
689
- throw new Error('KEYSTONE_PROFILE_SOVEREIGN_SYNC environment variable is missing. Expected valid profile name like "sync-profile.dev" or "sync-profile.prod". (E: 5e6f7890123456789abcdef5)');
690
- }
691
-
692
- const delegateBuilder = KeystoneProfileBuilder.buildKeystone(sovereignSyncProfileName.trim() as any)
693
- .withUsername(`${username}-sync`)
694
- .withDescription(`Sync Delegate for ${username}`)
695
- .withDetails({
696
- client: 'space-gib-web',
697
- role: 'sync-delegate',
698
- primaryTjpAddr: primaryTjpAddr,
699
- });
700
- const delegateConfigs = await delegateBuilder.compileConfigs();
701
-
702
- const delegateKeystoneIbGib = await keystoneService.genesis({
703
- masterSecret: delegateSecret,
704
- configs: delegateConfigs,
1140
+ const { delegateKeystone: delegateKeystoneIbGib, deviceTag, delegateSecret } = await createSovereignDeviceSyncDelegate({
1141
+ username,
1142
+ primaryTjpAddr,
1143
+ masterSecret,
705
1144
  metaspace,
706
1145
  space,
707
- frameDetails: delegateBuilder.getFrameDetails()
708
1146
  });
709
1147
 
710
1148
  const delegateAddr = getIbGibAddr({ ibGib: delegateKeystoneIbGib });
@@ -723,9 +1161,11 @@ export class KeystoneCreatorComponentInstance
723
1161
  parentKeystone: keystoneIbGib,
724
1162
  delegateKeystone: delegateKeystoneIbGib,
725
1163
  masterSecret,
1164
+ signingPoolId: 'manage',
726
1165
  metaspace,
727
1166
  space,
728
- allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT]
1167
+ allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
1168
+ deviceName: deviceTag,
729
1169
  });
730
1170
  const evolvedParentAddr = getIbGibAddr({ ibGib: evolvedParentKeystoneIbGib });
731
1171
  console.log(`${lc} delegate registered. evolved parent address: ${evolvedParentAddr}`);
@@ -768,19 +1208,31 @@ export class KeystoneCreatorComponentInstance
768
1208
  secVerify.classList.remove('hidden');
769
1209
  secVerify.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
770
1210
  this.setStatus("✉️ Verification code sent to your email. Enter 6-digit code below:", "info");
1211
+ this.startResendCooldownTimer({ btnId: 'btn-resend-code', durationSecs: 30 });
771
1212
 
772
1213
  const onResendCode = async () => {
773
1214
  btnResendCode.disabled = true;
774
1215
  btnResendCode.textContent = "Sending...";
775
1216
  this.setStatus("Resending verification code to your email...", "info");
776
- const resResend = await getApiBridge().postVerificationCode({ email, primaryTjpAddr: primaryTjpAddr });
777
- if (resResend.success) {
778
- this.setStatus("✉️ New verification code sent to your email!", "success");
779
- } else {
780
- this.setStatus(resResend.message || "Failed to resend verification code.", "error");
1217
+ try {
1218
+ const resResend = await getApiBridge().postVerificationCode({ email, primaryTjpAddr: primaryTjpAddr });
1219
+ if (resResend.success) {
1220
+ this.setStatus("✉️ Fresh 6-digit verification code sent to your email!", "success");
1221
+ this.startResendCooldownTimer({ btnId: 'btn-resend-code', durationSecs: 30 });
1222
+ inputVerifyCode.value = '';
1223
+ inputVerifyCode.focus();
1224
+ } else {
1225
+ this.setStatus(resResend.message || "Failed to resend verification code.", "error");
1226
+ btnResendCode.disabled = false;
1227
+ btnResendCode.textContent = "Resend Code";
1228
+ }
1229
+ } catch (error) {
1230
+ const emsg = extractErrorMsg(error);
1231
+ console.error(`${lc} ${emsg}`);
1232
+ this.setStatus(`Failed to resend verification code: ${emsg}`, "error");
1233
+ btnResendCode.disabled = false;
1234
+ btnResendCode.textContent = "Resend Code";
781
1235
  }
782
- btnResendCode.disabled = false;
783
- btnResendCode.textContent = "Resend Code";
784
1236
  };
785
1237
  btnResendCode.addEventListener('click', onResendCode);
786
1238
 
@@ -1009,6 +1461,11 @@ export class KeystoneCreatorComponentInstance
1009
1461
  this.setStatus(successMsg, "success");
1010
1462
  console.log(`${lc} ✓ ${successMsg}`);
1011
1463
 
1464
+ // 3.5 Sanitize secret input fields from DOM
1465
+ if (this.elements?.inputSecret) {
1466
+ this.elements.inputSecret.value = '';
1467
+ }
1468
+
1012
1469
  // 4. Dispatch UI navigation message to close creator and open identity manager
1013
1470
  window.dispatchEvent(new CustomEvent(EVENT_IBGIB_UI_MESSAGE, {
1014
1471
  detail: { action: 'show-identity-manager', addr: primaryTjpAddr },