@ibgib/web-gib 0.0.61 → 0.0.63

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 (66) 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.web.d.mts +20 -3
  4. package/dist/helpers.web.d.mts.map +1 -1
  5. package/dist/helpers.web.mjs +84 -3
  6. package/dist/helpers.web.mjs.map +1 -1
  7. package/dist/identity/device-delegate-helper.d.mts +48 -0
  8. package/dist/identity/device-delegate-helper.d.mts.map +1 -1
  9. package/dist/identity/device-delegate-helper.mjs +86 -2
  10. package/dist/identity/device-delegate-helper.mjs.map +1 -1
  11. package/dist/identity/device-delegate.respec.mjs +6 -6
  12. package/dist/identity/device-delegate.respec.mjs.map +1 -1
  13. package/dist/identity/passkey-helper.d.mts +77 -0
  14. package/dist/identity/passkey-helper.d.mts.map +1 -0
  15. package/dist/identity/passkey-helper.mjs +258 -0
  16. package/dist/identity/passkey-helper.mjs.map +1 -0
  17. package/dist/identity/passkey.respec.d.mts +2 -0
  18. package/dist/identity/passkey.respec.d.mts.map +1 -0
  19. package/dist/identity/passkey.respec.mjs +169 -0
  20. package/dist/identity/passkey.respec.mjs.map +1 -0
  21. package/dist/index.d.mts +1 -0
  22. package/dist/index.d.mts.map +1 -1
  23. package/dist/index.mjs +1 -0
  24. package/dist/index.mjs.map +1 -1
  25. package/dist/ui/component/identity/add-device/add-device.d.mts.map +1 -1
  26. package/dist/ui/component/identity/add-device/add-device.mjs +1 -0
  27. package/dist/ui/component/identity/add-device/add-device.mjs.map +1 -1
  28. package/dist/ui/component/identity/keystone-creator/keystone-creator.css +26 -0
  29. package/dist/ui/component/identity/keystone-creator/keystone-creator.d.mts +18 -1
  30. package/dist/ui/component/identity/keystone-creator/keystone-creator.d.mts.map +1 -1
  31. package/dist/ui/component/identity/keystone-creator/keystone-creator.html +60 -4
  32. package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs +518 -74
  33. package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs.map +1 -1
  34. package/dist/ui/component/identity/keystone-details/keystone-details.d.mts +28 -1
  35. package/dist/ui/component/identity/keystone-details/keystone-details.d.mts.map +1 -1
  36. package/dist/ui/component/identity/keystone-details/keystone-details.html +46 -0
  37. package/dist/ui/component/identity/keystone-details/keystone-details.mjs +1146 -208
  38. package/dist/ui/component/identity/keystone-details/keystone-details.mjs.map +1 -1
  39. package/dist/ui/component/identity/remove-device/remove-device.d.mts.map +1 -1
  40. package/dist/ui/component/identity/remove-device/remove-device.mjs +1 -0
  41. package/dist/ui/component/identity/remove-device/remove-device.mjs.map +1 -1
  42. package/dist/ui/ui-helpers.d.mts +3 -0
  43. package/dist/ui/ui-helpers.d.mts.map +1 -1
  44. package/dist/ui/ui-helpers.mjs +116 -7
  45. package/dist/ui/ui-helpers.mjs.map +1 -1
  46. package/package.json +2 -2
  47. package/src/AUTO-GENERATED-version.mts +1 -1
  48. package/src/helpers.web.mts +117 -7
  49. package/src/identity/device-delegate-helper.mts +135 -2
  50. package/src/identity/device-delegate.respec.mts +6 -6
  51. package/src/identity/passkey-helper.mts +337 -0
  52. package/src/identity/passkey.respec.mts +194 -0
  53. package/src/index.mts +1 -0
  54. package/src/ui/component/identity/add-device/add-device.mts +1 -0
  55. package/src/ui/component/identity/keystone-creator/keystone-creator.css +26 -0
  56. package/src/ui/component/identity/keystone-creator/keystone-creator.html +60 -4
  57. package/src/ui/component/identity/keystone-creator/keystone-creator.mts +561 -77
  58. package/src/ui/component/identity/keystone-details/keystone-details.html +46 -0
  59. package/src/ui/component/identity/keystone-details/keystone-details.mts +1239 -224
  60. package/src/ui/component/identity/remove-device/remove-device.mts +1 -0
  61. package/src/ui/ui-helpers.mts +123 -6
  62. package/tools/auto-generated-agent-skills/skills/ibgib-create-app-server/SKILL.md +50 -0
  63. package/src/identity/custodian-delegate-helper.mts.bak +0 -7
  64. package/src/identity/sso/sso-config-helper.mts.bak +0 -52
  65. package/src/identity/sso/sso-custodian-service.mts.bak +0 -566
  66. package/src/identity/sso/sso-custodian-service.respec.mts.bak +0 -298
@@ -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";
@@ -24,9 +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 { getOrCreateDeviceDelegateSecret, setDeviceDelegateSecret } from "../../../../identity/device-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
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";
30
32
  import { EVENT_IBGIB_IDENTITY_REQUEST_CHANGE, EVENT_IBGIB_UI_MESSAGE } from "../../../ui-constants.mjs";
31
33
  import { alertUser, shadowRoot_getElementById } from "../../../../helpers.web.mjs";
32
34
 
@@ -81,6 +83,12 @@ interface KeystoneCreatorElements extends ElementsBase {
81
83
  inputEmail: HTMLInputElement;
82
84
  inputSecret: HTMLInputElement;
83
85
  checkboxMakeActive: HTMLInputElement;
86
+ inputPasskeyUsername: HTMLInputElement;
87
+ inputPasskeyDescription: HTMLInputElement;
88
+ inputPasskeyEmail: HTMLInputElement;
89
+ inputPasskeyName: HTMLInputElement;
90
+ checkboxPasskeyMakeActive: HTMLInputElement;
91
+ btnGeneratePasskey: HTMLButtonElement;
84
92
  statusArea: HTMLDivElement;
85
93
  statusText: HTMLParagraphElement;
86
94
  }
@@ -94,6 +102,7 @@ export class KeystoneCreatorComponentInstance
94
102
 
95
103
  protected lc: string = `[KeystoneCreatorComponentInstance]`;
96
104
  private hasExistingIdentities = false;
105
+ private resendCooldownInterval: any = null;
97
106
 
98
107
  constructor() {
99
108
  super();
@@ -103,6 +112,36 @@ export class KeystoneCreatorComponentInstance
103
112
  await super.initialize(opts);
104
113
  }
105
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
+
106
145
  override async created(): Promise<void> {
107
146
  this.elements = {} as KeystoneCreatorElements;
108
147
 
@@ -116,6 +155,12 @@ export class KeystoneCreatorComponentInstance
116
155
  this.elements.inputEmail = this.shadowRoot!.getElementById('input-email') as HTMLInputElement;
117
156
  this.elements.inputSecret = this.shadowRoot!.getElementById('input-secret') as HTMLInputElement;
118
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;
119
164
  this.elements.statusArea = this.shadowRoot!.getElementById('status-area') as HTMLDivElement;
120
165
  this.elements.statusText = this.shadowRoot!.getElementById('status-text') as HTMLParagraphElement;
121
166
 
@@ -150,49 +195,50 @@ export class KeystoneCreatorComponentInstance
150
195
  this.hasExistingIdentities = false;
151
196
  }
152
197
 
153
- const checkbox = this.elements!.checkboxMakeActive;
154
- if (checkbox) {
155
- if (!this.hasExistingIdentities) {
156
- checkbox.checked = true;
157
- checkbox.disabled = true;
158
- checkbox.title = "At least one active identity is required.";
159
- } else {
160
- checkbox.disabled = false;
161
- 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
+ }
162
209
  }
163
210
  }
164
211
  }
165
212
 
166
- override async disconnected(): Promise<void> {
167
- // cleanup if needed
168
- }
169
-
170
213
  private initHandlers() {
171
- // Mode Tab Switching (Quick vs Password-based)
214
+ // Mode Tab Switching (Quick vs Passkey vs Password-based)
172
215
  const tabQuick = this.shadowRoot?.getElementById('tab-quick') as HTMLButtonElement | null;
216
+ const tabPasskey = this.shadowRoot?.getElementById('tab-passkey') as HTMLButtonElement | null;
173
217
  const tabPassword = this.shadowRoot?.getElementById('tab-password') as HTMLButtonElement | null;
174
218
  const panelQuick = this.shadowRoot?.getElementById('panel-quick') as HTMLDivElement | null;
219
+ const panelPasskey = this.shadowRoot?.getElementById('panel-passkey') as HTMLDivElement | null;
175
220
  const panelPassword = this.shadowRoot?.getElementById('panel-password') as HTMLDivElement | null;
176
221
  const subtitleQuick = this.shadowRoot?.getElementById('subtitle-quick') as HTMLParagraphElement | null;
222
+ const subtitlePasskey = this.shadowRoot?.getElementById('subtitle-passkey') as HTMLParagraphElement | null;
177
223
  const subtitlePassword = this.shadowRoot?.getElementById('subtitle-password') as HTMLParagraphElement | null;
178
224
 
179
- tabQuick?.addEventListener('click', () => {
180
- tabQuick.classList.add('active');
181
- tabPassword?.classList.remove('active');
182
- panelQuick?.classList.remove('hidden');
183
- panelPassword?.classList.add('hidden');
184
- subtitleQuick?.classList.remove('hidden');
185
- subtitlePassword?.classList.add('hidden');
186
- });
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');
187
229
 
188
- tabPassword?.addEventListener('click', () => {
189
- tabPassword.classList.add('active');
190
- tabQuick?.classList.remove('active');
191
- panelPassword?.classList.remove('hidden');
192
- panelQuick?.classList.add('hidden');
193
- subtitlePassword?.classList.remove('hidden');
194
- subtitleQuick?.classList.add('hidden');
195
- });
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'));
196
242
 
197
243
  const btnGoogleSso = this.shadowRoot?.getElementById('btn-google-sso') as HTMLButtonElement | null;
198
244
  btnGoogleSso?.addEventListener('click', () => this.handleSsoClick('google'));
@@ -216,11 +262,13 @@ export class KeystoneCreatorComponentInstance
216
262
  const secQuickOtp = this.shadowRoot?.getElementById('section-quick-otp') as HTMLDivElement | null;
217
263
  const inputQuickOtp = this.shadowRoot?.getElementById('input-quick-otp') as HTMLInputElement | null;
218
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;
219
266
 
220
267
  btnQuickSendCode?.addEventListener('click', () => this.handleQuickSendCode());
221
-
222
268
  btnQuickVerifyCode?.addEventListener('click', () => this.handleQuickVerifyCode());
269
+ btnQuickResendCode?.addEventListener('click', () => this.handleQuickResendCode());
223
270
 
271
+ this.elements!.btnGeneratePasskey?.addEventListener('click', () => this.handleGeneratePasskey());
224
272
  this.elements!.btnGenerateEl.addEventListener('click', () => this.handleGenerate());
225
273
 
226
274
  this.shadowRoot!.getElementById('btn-switch-sign-in')?.addEventListener('click', (e) => {
@@ -233,6 +281,16 @@ export class KeystoneCreatorComponentInstance
233
281
  });
234
282
 
235
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
+
236
294
  this.elements!.inputUsername?.addEventListener('input', () => {
237
295
  this.elements!.inputUsername.classList.remove('invalid');
238
296
  });
@@ -315,22 +373,14 @@ export class KeystoneCreatorComponentInstance
315
373
  return;
316
374
  }
317
375
 
318
- const delegateSecret = await getOrCreateDeviceDelegateSecret({ primaryTjpAddr });
319
376
  const email = resLogin.userInfo?.email || '';
320
377
  const username = email ? email.split('@')[0] : 'user';
321
- const custodianSyncProfileName = process.env.KEYSTONE_PROFILE_CUSTODIAN_SYNC;
322
- if (!custodianSyncProfileName || !custodianSyncProfileName.trim()) {
323
- 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)');
324
- }
325
378
 
326
- const delegateKeystone = await createSyncDelegateKeystone({
327
- profileName: custodianSyncProfileName.trim() as any,
379
+ const { delegateKeystone, delegateSecret } = await createOAuth2DeviceSyncDelegate({
380
+ providerId,
328
381
  username,
329
- deviceTag: generateDefaultDeviceTag(),
330
382
  primaryTjpAddr,
331
- delegateSecret,
332
383
  custodianReqs,
333
- additionalFrameDetails: { client: 'space-gib-web' },
334
384
  metaspace,
335
385
  space,
336
386
  });
@@ -395,6 +445,7 @@ export class KeystoneCreatorComponentInstance
395
445
  if (resCode.success) {
396
446
  secQuickOtp?.classList.remove('hidden');
397
447
  this.setStatus("✉️ 6-digit verification code sent to your email!", "success");
448
+ this.startResendCooldownTimer({ btnId: 'btn-quick-resend-code', durationSecs: 30 });
398
449
  } else {
399
450
  this.setStatus(resCode.message || "Failed to dispatch verification code.", "error");
400
451
  }
@@ -410,6 +461,54 @@ export class KeystoneCreatorComponentInstance
410
461
  }
411
462
  }
412
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
+
413
512
  /**
414
513
  * Triggered by the "Verify Code" button.
415
514
  * Submits 6-digit OTP code to custodian server, builds local sync delegate keystone,
@@ -466,21 +565,10 @@ export class KeystoneCreatorComponentInstance
466
565
  return;
467
566
  }
468
567
 
469
- const delegateSecret = await getOrCreateDeviceDelegateSecret({ primaryTjpAddr });
470
- const username = email ? email.split('@')[0] : 'user';
471
- const custodianSyncProfileName = process.env.KEYSTONE_PROFILE_CUSTODIAN_SYNC;
472
- if (!custodianSyncProfileName || !custodianSyncProfileName.trim()) {
473
- 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)');
474
- }
475
-
476
- const delegateKeystone = await createSyncDelegateKeystone({
477
- profileName: custodianSyncProfileName.trim() as any,
478
- username,
479
- deviceTag: generateDefaultDeviceTag(),
568
+ const { delegateKeystone, delegateSecret } = await createEmailDeviceSyncDelegate({
569
+ email,
480
570
  primaryTjpAddr,
481
- delegateSecret,
482
571
  custodianReqs,
483
- additionalFrameDetails: { client: 'space-gib-web' },
484
572
  metaspace,
485
573
  space,
486
574
  });
@@ -523,6 +611,399 @@ export class KeystoneCreatorComponentInstance
523
611
  }
524
612
  }
525
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 selectAttachment = this.shadowRoot?.querySelector('#select-passkey-attachment') as HTMLSelectElement | null;
700
+ const attachmentVal = selectAttachment?.value;
701
+ const authenticatorAttachment: AuthenticatorAttachment | undefined =
702
+ (attachmentVal === 'cross-platform' || attachmentVal === 'platform')
703
+ ? attachmentVal
704
+ : undefined;
705
+
706
+ const rawSalt = crypto.getRandomValues(new Uint8Array(32));
707
+ const saltHex = bufferToHex(rawSalt.buffer as ArrayBuffer);
708
+ const regResult = await registerPasskeyCredential({
709
+ username,
710
+ displayName: username,
711
+ salt: saltHex,
712
+ authenticatorAttachment,
713
+ userVerification: 'preferred'
714
+ });
715
+ const { credentialId, transports } = regResult;
716
+
717
+ let passkeySecret = regResult.passkeySecret;
718
+ if (!passkeySecret) {
719
+ const prfResult = await evaluatePasskeyPrf({
720
+ credentialId,
721
+ salt: saltHex,
722
+ transports,
723
+ userVerification: 'preferred'
724
+ });
725
+ passkeySecret = prfResult.passkeySecret;
726
+ }
727
+
728
+ const passkeyName = (inputName?.value || '').trim() || generateDefaultDeviceTag();
729
+ const passkeyTag = getSaferSubstring({ text: passkeyName, length: 12, keepLiterals: ['-'] }) || 'key';
730
+ const poolId = `passkey-manage-${passkeyTag}`;
731
+
732
+ const passkeyEntry: KeystonePasskeyEntry = {
733
+ credentialId,
734
+ poolId,
735
+ name: passkeyName,
736
+ salt: saltHex,
737
+ createdUTC: new Date().toISOString(),
738
+ ...(transports && transports.length > 0 ? { transports } : {})
739
+ };
740
+
741
+ this.showBusy({
742
+ isBusy: true,
743
+ title: 'Generating Identity...',
744
+ msg: 'Compiling sovereign passkey challenge pools...',
745
+ animationEmoji: '⚙️'
746
+ });
747
+
748
+ // 2. Prepare Sovereign Keystone Configs
749
+ const sovereignPrimaryProfileName = process.env.KEYSTONE_PROFILE_SOVEREIGN_PRIMARY;
750
+ if (!sovereignPrimaryProfileName || !sovereignPrimaryProfileName.trim()) {
751
+ throw new Error('KEYSTONE_PROFILE_SOVEREIGN_PRIMARY environment variable is missing.');
752
+ }
753
+
754
+ const builder = KeystoneProfileBuilder.buildKeystone(sovereignPrimaryProfileName.trim() as any)
755
+ .withUsername(username)
756
+ .withDescription(description);
757
+ if (email) {
758
+ builder.withEmails([email]);
759
+ }
760
+ builder.withDetails({
761
+ client: 'space-gib-web',
762
+ role: 'domain-identity',
763
+ passkeys: {
764
+ [credentialId]: passkeyEntry
765
+ }
766
+ });
767
+
768
+ const configs = await builder.compileConfigs();
769
+ // Re-label manage pool to passkey pool id
770
+ for (const cfg of configs) {
771
+ if (cfg.id === 'manage') {
772
+ cfg.id = poolId;
773
+ }
774
+ }
775
+
776
+ // 3. Prepare Metaspace & Local Space
777
+ const metaspace = await getGlobalMetaspace_waitIfNeeded();
778
+ const space = await metaspace.getLocalUserSpace({ lock: false });
779
+ if (!space) {
780
+ this.setStatus("Failed to access local user space.", "error");
781
+ return;
782
+ }
783
+
784
+ // 4. Perform Local Genesis
785
+ this.showBusy({
786
+ isBusy: true,
787
+ title: 'Generating Identity...',
788
+ msg: 'Generating cryptographic domain identity keys...',
789
+ animationEmoji: '🔑'
790
+ });
791
+
792
+ const keystoneService = new KeystoneService_V1();
793
+ const keystoneIbGib = await keystoneService.genesis({
794
+ masterSecret: passkeySecret,
795
+ emails: email ? [email] : undefined,
796
+ credentialIds: [credentialId],
797
+ configs,
798
+ metaspace,
799
+ space,
800
+ frameDetails: builder.getFrameDetails(),
801
+ isPrimary: true
802
+ });
803
+
804
+ const primaryTjpAddr = getTjpAddr({ ibGib: keystoneIbGib, defaultIfNone: 'incomingAddr' })!;
805
+ console.log(`${lc} passkey local genesis complete: ${primaryTjpAddr}`);
806
+ await delay(humanEyeDelay);
807
+ this.setStatus("Local genesis complete. Creating passkey sync delegate...", "info");
808
+
809
+ // 5. Create Passkey Device Sync Delegate
810
+ this.showBusy({
811
+ isBusy: true,
812
+ title: 'Creating Sync Delegate...',
813
+ msg: 'Generating cryptographic sync delegate keys...',
814
+ animationEmoji: '🔑'
815
+ });
816
+
817
+ const { delegateKeystone: delegateKeystoneIbGib, deviceTag, delegateSecret } = await createPasskeyDeviceSyncDelegate({
818
+ username,
819
+ primaryTjpAddr,
820
+ passkeySecret,
821
+ transports,
822
+ metaspace,
823
+ space,
824
+ });
825
+
826
+ // 6. Register Delegate locally on primary keystone
827
+ const evolvedParentKeystoneIbGib = await keystoneService.registerDelegate({
828
+ parentKeystone: keystoneIbGib,
829
+ delegateKeystone: delegateKeystoneIbGib,
830
+ masterSecret: passkeySecret,
831
+ signingPoolId: poolId,
832
+ allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
833
+ deviceName: deviceTag,
834
+ metaspace,
835
+ space,
836
+ });
837
+
838
+ // 7. Post to Server & Handle Verification
839
+ this.setStatus("Syncing genesis keystone to server...", "info");
840
+ this.showBusy({
841
+ isBusy: true,
842
+ title: 'Syncing to Server...',
843
+ msg: 'Staging pending genesis keystone on server...',
844
+ animationEmoji: '🔄'
845
+ });
846
+
847
+ const resSync = await getApiBridge().postGenesisKeystone(keystoneIbGib);
848
+ if (!resSync.success) {
849
+ const msg = `Sync failed: ${resSync.message}`;
850
+ console.error(`${lc} ${msg}`);
851
+ this.setStatus(msg, "error");
852
+ this.showBusy({ isBusy: false });
853
+ if (btn) {
854
+ btn.disabled = false;
855
+ btn.textContent = '🛡️ Create Identity with Passkey / Biometrics';
856
+ }
857
+ return;
858
+ }
859
+
860
+ const isPending = (resSync as any).pending || (resSync as any).statusCode === 202;
861
+
862
+ if (isPending) {
863
+ console.log(`${lc} Genesis keystone quarantined in pending staging area. Prompting for 6-digit verification code.`);
864
+ this.showBusy({ isBusy: false });
865
+ this.setFormDisabled(true);
866
+
867
+ if (!this.shadowRoot) { throw new Error(`(UNEXPECTED) this.shadowRoot falsy? (E: 6d316f5a2bf8e6a0286306468d35c826)`); }
868
+ const secVerify = shadowRoot_getElementById<HTMLDivElement>(this.shadowRoot, 'section-code-verification');
869
+ const inputVerifyCode = shadowRoot_getElementById<HTMLInputElement>(this.shadowRoot, 'input-verify-code');
870
+ const btnSubmitCode = shadowRoot_getElementById<HTMLButtonElement>(this.shadowRoot, 'btn-submit-code');
871
+ const btnResendCode = shadowRoot_getElementById<HTMLButtonElement>(this.shadowRoot, 'btn-resend-code');
872
+
873
+ secVerify.classList.remove('hidden');
874
+ secVerify.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
875
+ this.setStatus("✉️ Verification code sent to your email. Enter 6-digit code below:", "info");
876
+ this.startResendCooldownTimer({ btnId: 'btn-resend-code', durationSecs: 30 });
877
+
878
+ const onResendCode = async () => {
879
+ btnResendCode.disabled = true;
880
+ btnResendCode.textContent = "Sending...";
881
+ this.setStatus("Resending verification code to your email...", "info");
882
+ try {
883
+ const resResend = await getApiBridge().postVerificationCode({ email, primaryTjpAddr });
884
+ if (resResend.success) {
885
+ this.setStatus("✉️ Fresh 6-digit verification code sent to your email!", "success");
886
+ this.startResendCooldownTimer({ btnId: 'btn-resend-code', durationSecs: 30 });
887
+ inputVerifyCode.value = '';
888
+ inputVerifyCode.focus();
889
+ } else {
890
+ this.setStatus(resResend.message || "Failed to resend verification code.", "error");
891
+ btnResendCode.disabled = false;
892
+ btnResendCode.textContent = "Resend Code";
893
+ }
894
+ } catch (error) {
895
+ const emsg = extractErrorMsg(error);
896
+ console.error(`${lc} ${emsg}`);
897
+ this.setStatus(`Failed to resend verification code: ${emsg}`, "error");
898
+ btnResendCode.disabled = false;
899
+ btnResendCode.textContent = "Resend Code";
900
+ }
901
+ };
902
+ btnResendCode.addEventListener('click', onResendCode);
903
+
904
+ await new Promise<void>((resolve) => {
905
+ const onSubmitCode = async () => {
906
+ const code = inputVerifyCode.value.trim();
907
+ if (!code || code.length !== 6 || !/^\d{6}$/.test(code)) {
908
+ this.setStatus("Please enter a valid 6-digit numeric verification code (e.g. 482901).", "error");
909
+ return;
910
+ }
911
+
912
+ btnSubmitCode.disabled = true;
913
+ btnSubmitCode.textContent = "Verifying...";
914
+ this.showBusy({
915
+ isBusy: true,
916
+ title: 'Verifying & Activating...',
917
+ msg: 'Great! Juust a moment...\n(Validating code, promoting server domain space, other security stuff...)',
918
+ animationEmoji: '🔐'
919
+ });
920
+
921
+ const resVerify = await getApiBridge().postVerifyCode({ email, primaryTjpAddr, code });
922
+ if (!resVerify.success) {
923
+ this.showBusy({ isBusy: false });
924
+ this.setStatus(resVerify.message || "Incorrect verification code.", "error");
925
+ btnSubmitCode.disabled = false;
926
+ btnSubmitCode.textContent = "Verify Code";
927
+ return;
928
+ }
929
+
930
+ console.log(`${lc} Code verified & server domain promoted! Syncing delegate...`);
931
+ this.showBusy({
932
+ isBusy: true,
933
+ title: 'Syncing Delegate...',
934
+ msg: 'Posting evolved domain identity and sync delegate to server...',
935
+ animationEmoji: '⚡'
936
+ });
937
+
938
+ btnSubmitCode.removeEventListener('click', onSubmitCode);
939
+ secVerify.classList.add('hidden');
940
+ this.setFormDisabled(false);
941
+
942
+ // Now that server domain space is promoted, post evolved keystone + delegate
943
+ const resEvolve = await getApiBridge().putEvolveKeystone({
944
+ domainAddr: primaryTjpAddr,
945
+ keystoneIbGib: evolvedParentKeystoneIbGib,
946
+ relatedKeystoneIbGibs: [delegateKeystoneIbGib]
947
+ });
948
+ if (!resEvolve.success) {
949
+ console.warn(`${lc} Delegate sync warning after code verification: ${resEvolve.message}`);
950
+ }
951
+
952
+ resolve();
953
+ };
954
+
955
+ btnSubmitCode.addEventListener('click', onSubmitCode);
956
+ });
957
+ } else {
958
+ // Post evolved keystone + delegate keystone immediately
959
+ this.setStatus("Syncing delegate registration to server...", "info");
960
+ this.showBusy({
961
+ isBusy: true,
962
+ title: 'Syncing Delegate...',
963
+ msg: 'Posting evolved domain identity and sync delegate to server...',
964
+ animationEmoji: '🔄'
965
+ });
966
+ const resEvolve = await getApiBridge().putEvolveKeystone({
967
+ domainAddr: primaryTjpAddr,
968
+ keystoneIbGib: evolvedParentKeystoneIbGib,
969
+ relatedKeystoneIbGibs: [delegateKeystoneIbGib]
970
+ });
971
+ if (!resEvolve.success) {
972
+ const msg = `Delegate registration sync failed: ${resEvolve.message}`;
973
+ console.error(`${lc} ${msg}`);
974
+ this.setStatus(msg, "error");
975
+ this.showBusy({ isBusy: false });
976
+ if (btn) {
977
+ btn.disabled = false;
978
+ btn.textContent = '🛡️ Create Identity with Passkey / Biometrics';
979
+ }
980
+ return;
981
+ }
982
+ }
983
+
984
+ // 8. Finalize Onboarding
985
+ await this.finalizeIdentityOnboarding({
986
+ primaryKeystone: evolvedParentKeystoneIbGib,
987
+ delegateKeystone: delegateKeystoneIbGib,
988
+ primaryTjpAddr,
989
+ masterSecret: passkeySecret,
990
+ delegateSecret,
991
+ makeActive: checkboxActive?.checked ?? true
992
+ });
993
+
994
+ } catch (error) {
995
+ const emsg = extractErrorMsg(error);
996
+ console.error(`${lc} ${emsg}`, error);
997
+ this.setStatus(`Error: ${emsg}`, "error");
998
+ } finally {
999
+ this.showBusy({ isBusy: false });
1000
+ if (btn) {
1001
+ btn.disabled = false;
1002
+ btn.textContent = '🛡️ Create Identity with Passkey / Biometrics';
1003
+ }
1004
+ }
1005
+ }
1006
+
526
1007
  /**
527
1008
  * Triggered by the "Generate" button.
528
1009
  * Orchestrates: Secret -> Local Genesis -> Graph Collection -> Server Sync.
@@ -666,21 +1147,10 @@ export class KeystoneCreatorComponentInstance
666
1147
  animationEmoji: '🔑'
667
1148
  });
668
1149
 
669
- const delegateSecret = await deriveDelegateSecret({ masterSecret });
670
- await setDeviceDelegateSecret({ primaryTjpAddr, delegateSecret });
671
-
672
- const sovereignSyncProfileName = process.env.KEYSTONE_PROFILE_SOVEREIGN_SYNC;
673
- if (!sovereignSyncProfileName || !sovereignSyncProfileName.trim()) {
674
- 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)');
675
- }
676
-
677
- const delegateKeystoneIbGib = await createSyncDelegateKeystone({
678
- profileName: sovereignSyncProfileName.trim() as any,
1150
+ const { delegateKeystone: delegateKeystoneIbGib, deviceTag, delegateSecret } = await createSovereignDeviceSyncDelegate({
679
1151
  username,
680
- deviceTag: generateDefaultDeviceTag(),
681
1152
  primaryTjpAddr,
682
- delegateSecret,
683
- additionalFrameDetails: { client: 'space-gib-web' },
1153
+ masterSecret,
684
1154
  metaspace,
685
1155
  space,
686
1156
  });
@@ -701,9 +1171,11 @@ export class KeystoneCreatorComponentInstance
701
1171
  parentKeystone: keystoneIbGib,
702
1172
  delegateKeystone: delegateKeystoneIbGib,
703
1173
  masterSecret,
1174
+ signingPoolId: 'manage',
704
1175
  metaspace,
705
1176
  space,
706
- allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT]
1177
+ allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
1178
+ deviceName: deviceTag,
707
1179
  });
708
1180
  const evolvedParentAddr = getIbGibAddr({ ibGib: evolvedParentKeystoneIbGib });
709
1181
  console.log(`${lc} delegate registered. evolved parent address: ${evolvedParentAddr}`);
@@ -746,19 +1218,31 @@ export class KeystoneCreatorComponentInstance
746
1218
  secVerify.classList.remove('hidden');
747
1219
  secVerify.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
748
1220
  this.setStatus("✉️ Verification code sent to your email. Enter 6-digit code below:", "info");
1221
+ this.startResendCooldownTimer({ btnId: 'btn-resend-code', durationSecs: 30 });
749
1222
 
750
1223
  const onResendCode = async () => {
751
1224
  btnResendCode.disabled = true;
752
1225
  btnResendCode.textContent = "Sending...";
753
1226
  this.setStatus("Resending verification code to your email...", "info");
754
- const resResend = await getApiBridge().postVerificationCode({ email, primaryTjpAddr: primaryTjpAddr });
755
- if (resResend.success) {
756
- this.setStatus("✉️ New verification code sent to your email!", "success");
757
- } else {
758
- this.setStatus(resResend.message || "Failed to resend verification code.", "error");
1227
+ try {
1228
+ const resResend = await getApiBridge().postVerificationCode({ email, primaryTjpAddr: primaryTjpAddr });
1229
+ if (resResend.success) {
1230
+ this.setStatus("✉️ Fresh 6-digit verification code sent to your email!", "success");
1231
+ this.startResendCooldownTimer({ btnId: 'btn-resend-code', durationSecs: 30 });
1232
+ inputVerifyCode.value = '';
1233
+ inputVerifyCode.focus();
1234
+ } else {
1235
+ this.setStatus(resResend.message || "Failed to resend verification code.", "error");
1236
+ btnResendCode.disabled = false;
1237
+ btnResendCode.textContent = "Resend Code";
1238
+ }
1239
+ } catch (error) {
1240
+ const emsg = extractErrorMsg(error);
1241
+ console.error(`${lc} ${emsg}`);
1242
+ this.setStatus(`Failed to resend verification code: ${emsg}`, "error");
1243
+ btnResendCode.disabled = false;
1244
+ btnResendCode.textContent = "Resend Code";
759
1245
  }
760
- btnResendCode.disabled = false;
761
- btnResendCode.textContent = "Resend Code";
762
1246
  };
763
1247
  btnResendCode.addEventListener('click', onResendCode);
764
1248