@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
@@ -1,7 +1,7 @@
1
1
  import thisCss from "./keystone-creator.css";
2
2
  import thisHtml from "./keystone-creator.html";
3
3
  import commonComponentStylesCss from "../../common-component-styles.css";
4
- import { delay, extractErrorMsg, } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs";
4
+ import { delay, extractErrorMsg, getSaferSubstring } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs";
5
5
  import { getIbGibAddr } from "@ibgib/ts-gib/dist/helper.mjs";
6
6
  import { KeystoneService_V1 } from "@ibgib/core-gib/dist/keystone/keystone-service-v1.mjs";
7
7
  import { KeystoneProfileBuilder } from "@ibgib/core-gib/dist/keystone/policy/keystone-profile-builder.mjs";
@@ -9,14 +9,14 @@ import { updateSpecialIndex, } from "@ibgib/core-gib/dist/timeline/timeline-api.
9
9
  import { toDto, getTjpAddr, getTimelinesGroupedByTjp } from "@ibgib/core-gib/dist/common/other/ibgib-helper.mjs";
10
10
  import { validateEmail } from "@ibgib/core-gib/dist/keystone/keystone-helpers.mjs";
11
11
  import { KEYSTONE_USERNAME_REGEXP, KEYSTONE_DESCRIPTION_REGEXP, KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT } from "@ibgib/core-gib/dist/keystone/keystone-constants.mjs";
12
- import { deriveDelegateSecret, createSyncDelegateKeystone } from "@ibgib/core-gib/dist/keystone/delegate/delegate-helpers.mjs";
13
12
  import { getGlobalMetaspace_waitIfNeeded, } from "../../../../helpers.mjs";
14
13
  import { IbGibDynamicComponentMetaBase, IbGibFormInstanceBase, } from "../../ibgib-dynamic-component-bases.mjs";
15
14
  import { getComponentCtorArg } from "../../../../app-bootstrap/init-orchestration.mjs";
16
15
  import { getApiBridge } from "../../../../api/ibgib-api-bridge.mjs";
17
- import { getOrCreateDeviceDelegateSecret, setDeviceDelegateSecret } from "../../../../identity/device-delegate-helper.mjs";
16
+ import { createSovereignDeviceSyncDelegate, createOAuth2DeviceSyncDelegate, createEmailDeviceSyncDelegate } from "../../../../identity/device-delegate-helper.mjs";
18
17
  import { executeSsoOAuthPopup } from "../../../../identity/sso-popup-helper.mjs";
19
18
  import { generateDefaultDeviceTag } from "../../../../util/device-helper.mjs";
19
+ import { registerPasskeyCredential, evaluatePasskeyPrf, createPasskeyDeviceSyncDelegate, bufferToHex } from "../../../../identity/passkey-helper.mjs";
20
20
  import { EVENT_IBGIB_IDENTITY_REQUEST_CHANGE, EVENT_IBGIB_UI_MESSAGE } from "../../../ui-constants.mjs";
21
21
  import { alertUser, shadowRoot_getElementById } from "../../../../helpers.web.mjs";
22
22
  export const KEYSTONE_CREATOR_COMPONENT_NAME = 'ibgib-keystone-creator';
@@ -51,12 +51,43 @@ export class KeystoneCreatorComponentMeta extends IbGibDynamicComponentMetaBase
51
51
  export class KeystoneCreatorComponentInstance extends IbGibFormInstanceBase {
52
52
  lc = `[KeystoneCreatorComponentInstance]`;
53
53
  hasExistingIdentities = false;
54
+ resendCooldownInterval = null;
54
55
  constructor() {
55
56
  super();
56
57
  }
57
58
  async initialize(opts) {
58
59
  await super.initialize(opts);
59
60
  }
61
+ async disconnected() {
62
+ if (this.resendCooldownInterval) {
63
+ clearInterval(this.resendCooldownInterval);
64
+ this.resendCooldownInterval = null;
65
+ }
66
+ }
67
+ startResendCooldownTimer({ btnId, durationSecs = 30 }) {
68
+ const btn = this.shadowRoot?.getElementById(btnId);
69
+ if (!btn)
70
+ return;
71
+ if (this.resendCooldownInterval) {
72
+ clearInterval(this.resendCooldownInterval);
73
+ this.resendCooldownInterval = null;
74
+ }
75
+ let remaining = durationSecs;
76
+ btn.disabled = true;
77
+ btn.textContent = `Resend Code (${remaining}s)`;
78
+ this.resendCooldownInterval = setInterval(() => {
79
+ remaining -= 1;
80
+ if (remaining <= 0) {
81
+ clearInterval(this.resendCooldownInterval);
82
+ this.resendCooldownInterval = null;
83
+ btn.disabled = false;
84
+ btn.textContent = "Resend Code";
85
+ }
86
+ else {
87
+ btn.textContent = `Resend Code (${remaining}s)`;
88
+ }
89
+ }, 1000);
90
+ }
60
91
  async created() {
61
92
  this.elements = {};
62
93
  this.elements.containerEl = this.shadowRoot.getElementById('container');
@@ -69,6 +100,12 @@ export class KeystoneCreatorComponentInstance extends IbGibFormInstanceBase {
69
100
  this.elements.inputEmail = this.shadowRoot.getElementById('input-email');
70
101
  this.elements.inputSecret = this.shadowRoot.getElementById('input-secret');
71
102
  this.elements.checkboxMakeActive = this.shadowRoot.getElementById('checkbox-make-active');
103
+ this.elements.inputPasskeyUsername = this.shadowRoot.getElementById('input-passkey-username');
104
+ this.elements.inputPasskeyDescription = this.shadowRoot.getElementById('input-passkey-description');
105
+ this.elements.inputPasskeyEmail = this.shadowRoot.getElementById('input-passkey-email');
106
+ this.elements.inputPasskeyName = this.shadowRoot.getElementById('input-passkey-name');
107
+ this.elements.checkboxPasskeyMakeActive = this.shadowRoot.getElementById('checkbox-passkey-make-active');
108
+ this.elements.btnGeneratePasskey = this.shadowRoot.getElementById('btn-generate-passkey');
72
109
  this.elements.statusArea = this.shadowRoot.getElementById('status-area');
73
110
  this.elements.statusText = this.shadowRoot.getElementById('status-text');
74
111
  // ElementBase requires contentEl
@@ -99,46 +136,46 @@ export class KeystoneCreatorComponentInstance extends IbGibFormInstanceBase {
99
136
  console.warn(`${lc} Error checking existing identities: ${extractErrorMsg(error)}`);
100
137
  this.hasExistingIdentities = false;
101
138
  }
102
- const checkbox = this.elements.checkboxMakeActive;
103
- if (checkbox) {
104
- if (!this.hasExistingIdentities) {
105
- checkbox.checked = true;
106
- checkbox.disabled = true;
107
- checkbox.title = "At least one active identity is required.";
108
- }
109
- else {
110
- checkbox.disabled = false;
111
- checkbox.title = "";
139
+ const checkboxes = [this.elements.checkboxMakeActive, this.elements.checkboxPasskeyMakeActive];
140
+ for (const checkbox of checkboxes) {
141
+ if (checkbox) {
142
+ if (!this.hasExistingIdentities) {
143
+ checkbox.checked = true;
144
+ checkbox.disabled = true;
145
+ checkbox.title = "At least one active identity is required.";
146
+ }
147
+ else {
148
+ checkbox.disabled = false;
149
+ checkbox.title = "";
150
+ }
112
151
  }
113
152
  }
114
153
  }
115
- async disconnected() {
116
- // cleanup if needed
117
- }
118
154
  initHandlers() {
119
- // Mode Tab Switching (Quick vs Password-based)
155
+ // Mode Tab Switching (Quick vs Passkey vs Password-based)
120
156
  const tabQuick = this.shadowRoot?.getElementById('tab-quick');
157
+ const tabPasskey = this.shadowRoot?.getElementById('tab-passkey');
121
158
  const tabPassword = this.shadowRoot?.getElementById('tab-password');
122
159
  const panelQuick = this.shadowRoot?.getElementById('panel-quick');
160
+ const panelPasskey = this.shadowRoot?.getElementById('panel-passkey');
123
161
  const panelPassword = this.shadowRoot?.getElementById('panel-password');
124
162
  const subtitleQuick = this.shadowRoot?.getElementById('subtitle-quick');
163
+ const subtitlePasskey = this.shadowRoot?.getElementById('subtitle-passkey');
125
164
  const subtitlePassword = this.shadowRoot?.getElementById('subtitle-password');
126
- tabQuick?.addEventListener('click', () => {
127
- tabQuick.classList.add('active');
128
- tabPassword?.classList.remove('active');
129
- panelQuick?.classList.remove('hidden');
130
- panelPassword?.classList.add('hidden');
131
- subtitleQuick?.classList.remove('hidden');
132
- subtitlePassword?.classList.add('hidden');
133
- });
134
- tabPassword?.addEventListener('click', () => {
135
- tabPassword.classList.add('active');
136
- tabQuick?.classList.remove('active');
137
- panelPassword?.classList.remove('hidden');
138
- panelQuick?.classList.add('hidden');
139
- subtitlePassword?.classList.remove('hidden');
140
- subtitleQuick?.classList.add('hidden');
141
- });
165
+ const switchTab = (mode) => {
166
+ tabQuick?.classList.toggle('active', mode === 'quick');
167
+ tabPasskey?.classList.toggle('active', mode === 'passkey');
168
+ tabPassword?.classList.toggle('active', mode === 'password');
169
+ panelQuick?.classList.toggle('hidden', mode !== 'quick');
170
+ panelPasskey?.classList.toggle('hidden', mode !== 'passkey');
171
+ panelPassword?.classList.toggle('hidden', mode !== 'password');
172
+ subtitleQuick?.classList.toggle('hidden', mode !== 'quick');
173
+ subtitlePasskey?.classList.toggle('hidden', mode !== 'passkey');
174
+ subtitlePassword?.classList.toggle('hidden', mode !== 'password');
175
+ };
176
+ tabQuick?.addEventListener('click', () => switchTab('quick'));
177
+ tabPasskey?.addEventListener('click', () => switchTab('passkey'));
178
+ tabPassword?.addEventListener('click', () => switchTab('password'));
142
179
  const btnGoogleSso = this.shadowRoot?.getElementById('btn-google-sso');
143
180
  btnGoogleSso?.addEventListener('click', () => this.handleSsoClick('google'));
144
181
  const btnGithubSso = this.shadowRoot?.getElementById('btn-github-sso');
@@ -158,8 +195,11 @@ export class KeystoneCreatorComponentInstance extends IbGibFormInstanceBase {
158
195
  const secQuickOtp = this.shadowRoot?.getElementById('section-quick-otp');
159
196
  const inputQuickOtp = this.shadowRoot?.getElementById('input-quick-otp');
160
197
  const btnQuickVerifyCode = this.shadowRoot?.getElementById('btn-quick-verify-code');
198
+ const btnQuickResendCode = this.shadowRoot?.getElementById('btn-quick-resend-code');
161
199
  btnQuickSendCode?.addEventListener('click', () => this.handleQuickSendCode());
162
200
  btnQuickVerifyCode?.addEventListener('click', () => this.handleQuickVerifyCode());
201
+ btnQuickResendCode?.addEventListener('click', () => this.handleQuickResendCode());
202
+ this.elements.btnGeneratePasskey?.addEventListener('click', () => this.handleGeneratePasskey());
163
203
  this.elements.btnGenerateEl.addEventListener('click', () => this.handleGenerate());
164
204
  this.shadowRoot.getElementById('btn-switch-sign-in')?.addEventListener('click', (e) => {
165
205
  e.preventDefault();
@@ -170,6 +210,15 @@ export class KeystoneCreatorComponentInstance extends IbGibFormInstanceBase {
170
210
  }));
171
211
  });
172
212
  // Clear invalid styling when user inputs text
213
+ this.elements.inputPasskeyUsername?.addEventListener('input', () => {
214
+ this.elements.inputPasskeyUsername.classList.remove('invalid');
215
+ });
216
+ this.elements.inputPasskeyDescription?.addEventListener('input', () => {
217
+ this.elements.inputPasskeyDescription.classList.remove('invalid');
218
+ });
219
+ this.elements.inputPasskeyEmail?.addEventListener('input', () => {
220
+ this.elements.inputPasskeyEmail.classList.remove('invalid');
221
+ });
173
222
  this.elements.inputUsername?.addEventListener('input', () => {
174
223
  this.elements.inputUsername.classList.remove('invalid');
175
224
  });
@@ -242,21 +291,13 @@ export class KeystoneCreatorComponentInstance extends IbGibFormInstanceBase {
242
291
  this.setStatus('Could not access local user space.', 'error');
243
292
  return;
244
293
  }
245
- const delegateSecret = await getOrCreateDeviceDelegateSecret({ primaryTjpAddr });
246
294
  const email = resLogin.userInfo?.email || '';
247
295
  const username = email ? email.split('@')[0] : 'user';
248
- const custodianSyncProfileName = process.env.KEYSTONE_PROFILE_CUSTODIAN_SYNC;
249
- if (!custodianSyncProfileName || !custodianSyncProfileName.trim()) {
250
- 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)');
251
- }
252
- const delegateKeystone = await createSyncDelegateKeystone({
253
- profileName: custodianSyncProfileName.trim(),
296
+ const { delegateKeystone, delegateSecret } = await createOAuth2DeviceSyncDelegate({
297
+ providerId,
254
298
  username,
255
- deviceTag: generateDefaultDeviceTag(),
256
299
  primaryTjpAddr,
257
- delegateSecret,
258
300
  custodianReqs,
259
- additionalFrameDetails: { client: 'space-gib-web' },
260
301
  metaspace,
261
302
  space,
262
303
  });
@@ -315,6 +356,7 @@ export class KeystoneCreatorComponentInstance extends IbGibFormInstanceBase {
315
356
  if (resCode.success) {
316
357
  secQuickOtp?.classList.remove('hidden');
317
358
  this.setStatus("✉️ 6-digit verification code sent to your email!", "success");
359
+ this.startResendCooldownTimer({ btnId: 'btn-quick-resend-code', durationSecs: 30 });
318
360
  }
319
361
  else {
320
362
  this.setStatus(resCode.message || "Failed to dispatch verification code.", "error");
@@ -332,6 +374,52 @@ export class KeystoneCreatorComponentInstance extends IbGibFormInstanceBase {
332
374
  }
333
375
  }
334
376
  }
377
+ /**
378
+ * Triggered by the "Resend Code" button in the quick email panel.
379
+ */
380
+ async handleQuickResendCode() {
381
+ const lc = `${this.lc}[${this.handleQuickResendCode.name}]`;
382
+ const btnQuickResendCode = this.shadowRoot?.getElementById('btn-quick-resend-code');
383
+ const inputQuickEmail = this.shadowRoot?.getElementById('input-quick-email');
384
+ const inputQuickOtp = this.shadowRoot?.getElementById('input-quick-otp');
385
+ const email = (inputQuickEmail?.value || '').trim();
386
+ if (!email || !validateEmail(email)) {
387
+ this.setStatus("Please enter a valid email address.", "error");
388
+ return;
389
+ }
390
+ if (btnQuickResendCode) {
391
+ btnQuickResendCode.disabled = true;
392
+ btnQuickResendCode.textContent = "Sending...";
393
+ }
394
+ this.setStatus("Resending verification code...", "info");
395
+ try {
396
+ const resCode = await getApiBridge().postVerificationCode({ email });
397
+ if (resCode.success) {
398
+ this.setStatus(`✉️ Fresh 6-digit verification code sent to ${email}!`, "success");
399
+ this.startResendCooldownTimer({ btnId: 'btn-quick-resend-code', durationSecs: 30 });
400
+ if (inputQuickOtp) {
401
+ inputQuickOtp.value = '';
402
+ inputQuickOtp.focus();
403
+ }
404
+ }
405
+ else {
406
+ this.setStatus(resCode.message || "Failed to resend verification code.", "error");
407
+ if (btnQuickResendCode) {
408
+ btnQuickResendCode.disabled = false;
409
+ btnQuickResendCode.textContent = "Resend Code";
410
+ }
411
+ }
412
+ }
413
+ catch (error) {
414
+ const emsg = extractErrorMsg(error);
415
+ console.error(`${lc} ${emsg}`);
416
+ this.setStatus(`Failed to resend verification code: ${emsg}`, "error");
417
+ if (btnQuickResendCode) {
418
+ btnQuickResendCode.disabled = false;
419
+ btnQuickResendCode.textContent = "Resend Code";
420
+ }
421
+ }
422
+ }
335
423
  /**
336
424
  * Triggered by the "Verify Code" button.
337
425
  * Submits 6-digit OTP code to custodian server, builds local sync delegate keystone,
@@ -381,20 +469,10 @@ export class KeystoneCreatorComponentInstance extends IbGibFormInstanceBase {
381
469
  this.setStatus("Failed to access local user space.", "error");
382
470
  return;
383
471
  }
384
- const delegateSecret = await getOrCreateDeviceDelegateSecret({ primaryTjpAddr });
385
- const username = email ? email.split('@')[0] : 'user';
386
- const custodianSyncProfileName = process.env.KEYSTONE_PROFILE_CUSTODIAN_SYNC;
387
- if (!custodianSyncProfileName || !custodianSyncProfileName.trim()) {
388
- 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)');
389
- }
390
- const delegateKeystone = await createSyncDelegateKeystone({
391
- profileName: custodianSyncProfileName.trim(),
392
- username,
393
- deviceTag: generateDefaultDeviceTag(),
472
+ const { delegateKeystone, delegateSecret } = await createEmailDeviceSyncDelegate({
473
+ email,
394
474
  primaryTjpAddr,
395
- delegateSecret,
396
475
  custodianReqs,
397
- additionalFrameDetails: { client: 'space-gib-web' },
398
476
  metaspace,
399
477
  space,
400
478
  });
@@ -434,6 +512,366 @@ export class KeystoneCreatorComponentInstance extends IbGibFormInstanceBase {
434
512
  }
435
513
  }
436
514
  }
515
+ /**
516
+ * Triggered by the "Create Identity with Passkey / Biometrics" button.
517
+ * Orchestrates: WebAuthn Credential -> PRF Secret Evaluation -> Local Passkey Genesis -> Graph Collection -> Server Sync.
518
+ */
519
+ async handleGeneratePasskey() {
520
+ const lc = `${this.lc}[${this.handleGeneratePasskey.name}]`;
521
+ const btn = this.shadowRoot?.getElementById('btn-generate-passkey');
522
+ try {
523
+ console.log(`${lc} starting passkey genesis orchestration...`);
524
+ const humanEyeDelay = 500;
525
+ this.setStatus("Initializing passkey genesis...", "info");
526
+ const inputUsername = this.elements.inputPasskeyUsername;
527
+ const inputDesc = this.elements.inputPasskeyDescription;
528
+ const inputEmail = this.elements.inputPasskeyEmail;
529
+ const inputName = this.elements.inputPasskeyName;
530
+ const checkboxActive = this.elements.checkboxPasskeyMakeActive;
531
+ const username = (inputUsername?.value || '').trim();
532
+ if (inputUsername)
533
+ inputUsername.value = username;
534
+ if (!this.validateInputPattern({
535
+ input: inputUsername,
536
+ pattern: KEYSTONE_USERNAME_REGEXP,
537
+ errorMessage: "Username must be 1-63 characters (alphanumeric, underscores, hyphens, dots).",
538
+ required: true
539
+ })) {
540
+ this.setStatus("Invalid username. Must be 1-63 characters (alphanumeric, _, -, .).", "error");
541
+ return;
542
+ }
543
+ const description = (inputDesc?.value || '').trim();
544
+ if (inputDesc)
545
+ inputDesc.value = description;
546
+ if (inputDesc && !this.validateInputPattern({
547
+ input: inputDesc,
548
+ pattern: KEYSTONE_DESCRIPTION_REGEXP,
549
+ errorMessage: "Description must be 0-128 characters.",
550
+ required: false
551
+ })) {
552
+ this.setStatus("Invalid description. Max 128 chars.", "error");
553
+ return;
554
+ }
555
+ const email = (inputEmail?.value || '').trim();
556
+ if (inputEmail)
557
+ inputEmail.value = email;
558
+ if (email && !validateEmail(email)) {
559
+ inputEmail?.classList.add('invalid');
560
+ this.setStatus("Invalid email address format.", "error");
561
+ return;
562
+ }
563
+ if (btn) {
564
+ btn.disabled = true;
565
+ btn.textContent = 'Awaiting Biometrics / Security Key...';
566
+ }
567
+ // Pre-check email availability on server if email provided
568
+ if (email) {
569
+ this.showBusy({
570
+ isBusy: true,
571
+ title: 'Checking Email Availability...',
572
+ msg: 'Checking server identity index...',
573
+ animationEmoji: '🔍'
574
+ });
575
+ const resCheck = await getApiBridge().checkEmailAvailability({ email });
576
+ if (!resCheck.available) {
577
+ const msg = resCheck.message || "Email/Identity is already registered.";
578
+ this.setStatus(msg, "error");
579
+ this.showBusy({ isBusy: false });
580
+ if (btn) {
581
+ btn.disabled = false;
582
+ btn.textContent = '🛡️ Create Identity with Passkey / Biometrics';
583
+ }
584
+ return;
585
+ }
586
+ }
587
+ // 1. WebAuthn Passkey Registration & PRF Secret Evaluation
588
+ this.showBusy({
589
+ isBusy: true,
590
+ title: 'Touch Biometrics or Security Key',
591
+ msg: 'Please touch your fingerprint sensor, Face ID, or insert your USB security key when prompted...',
592
+ animationEmoji: '🛡️'
593
+ });
594
+ const selectAttachment = this.shadowRoot?.querySelector('#select-passkey-attachment');
595
+ const attachmentVal = selectAttachment?.value;
596
+ const authenticatorAttachment = (attachmentVal === 'cross-platform' || attachmentVal === 'platform')
597
+ ? attachmentVal
598
+ : undefined;
599
+ const rawSalt = crypto.getRandomValues(new Uint8Array(32));
600
+ const saltHex = bufferToHex(rawSalt.buffer);
601
+ const regResult = await registerPasskeyCredential({
602
+ username,
603
+ displayName: username,
604
+ salt: saltHex,
605
+ authenticatorAttachment,
606
+ userVerification: 'preferred'
607
+ });
608
+ const { credentialId, transports } = regResult;
609
+ let passkeySecret = regResult.passkeySecret;
610
+ if (!passkeySecret) {
611
+ const prfResult = await evaluatePasskeyPrf({
612
+ credentialId,
613
+ salt: saltHex,
614
+ transports,
615
+ userVerification: 'preferred'
616
+ });
617
+ passkeySecret = prfResult.passkeySecret;
618
+ }
619
+ const passkeyName = (inputName?.value || '').trim() || generateDefaultDeviceTag();
620
+ const passkeyTag = getSaferSubstring({ text: passkeyName, length: 12, keepLiterals: ['-'] }) || 'key';
621
+ const poolId = `passkey-manage-${passkeyTag}`;
622
+ const passkeyEntry = {
623
+ credentialId,
624
+ poolId,
625
+ name: passkeyName,
626
+ salt: saltHex,
627
+ createdUTC: new Date().toISOString(),
628
+ ...(transports && transports.length > 0 ? { transports } : {})
629
+ };
630
+ this.showBusy({
631
+ isBusy: true,
632
+ title: 'Generating Identity...',
633
+ msg: 'Compiling sovereign passkey challenge pools...',
634
+ animationEmoji: '⚙️'
635
+ });
636
+ // 2. Prepare Sovereign Keystone Configs
637
+ const sovereignPrimaryProfileName = process.env.KEYSTONE_PROFILE_SOVEREIGN_PRIMARY;
638
+ if (!sovereignPrimaryProfileName || !sovereignPrimaryProfileName.trim()) {
639
+ throw new Error('KEYSTONE_PROFILE_SOVEREIGN_PRIMARY environment variable is missing.');
640
+ }
641
+ const builder = KeystoneProfileBuilder.buildKeystone(sovereignPrimaryProfileName.trim())
642
+ .withUsername(username)
643
+ .withDescription(description);
644
+ if (email) {
645
+ builder.withEmails([email]);
646
+ }
647
+ builder.withDetails({
648
+ client: 'space-gib-web',
649
+ role: 'domain-identity',
650
+ passkeys: {
651
+ [credentialId]: passkeyEntry
652
+ }
653
+ });
654
+ const configs = await builder.compileConfigs();
655
+ // Re-label manage pool to passkey pool id
656
+ for (const cfg of configs) {
657
+ if (cfg.id === 'manage') {
658
+ cfg.id = poolId;
659
+ }
660
+ }
661
+ // 3. Prepare Metaspace & Local Space
662
+ const metaspace = await getGlobalMetaspace_waitIfNeeded();
663
+ const space = await metaspace.getLocalUserSpace({ lock: false });
664
+ if (!space) {
665
+ this.setStatus("Failed to access local user space.", "error");
666
+ return;
667
+ }
668
+ // 4. Perform Local Genesis
669
+ this.showBusy({
670
+ isBusy: true,
671
+ title: 'Generating Identity...',
672
+ msg: 'Generating cryptographic domain identity keys...',
673
+ animationEmoji: '🔑'
674
+ });
675
+ const keystoneService = new KeystoneService_V1();
676
+ const keystoneIbGib = await keystoneService.genesis({
677
+ masterSecret: passkeySecret,
678
+ emails: email ? [email] : undefined,
679
+ credentialIds: [credentialId],
680
+ configs,
681
+ metaspace,
682
+ space,
683
+ frameDetails: builder.getFrameDetails(),
684
+ isPrimary: true
685
+ });
686
+ const primaryTjpAddr = getTjpAddr({ ibGib: keystoneIbGib, defaultIfNone: 'incomingAddr' });
687
+ console.log(`${lc} passkey local genesis complete: ${primaryTjpAddr}`);
688
+ await delay(humanEyeDelay);
689
+ this.setStatus("Local genesis complete. Creating passkey sync delegate...", "info");
690
+ // 5. Create Passkey Device Sync Delegate
691
+ this.showBusy({
692
+ isBusy: true,
693
+ title: 'Creating Sync Delegate...',
694
+ msg: 'Generating cryptographic sync delegate keys...',
695
+ animationEmoji: '🔑'
696
+ });
697
+ const { delegateKeystone: delegateKeystoneIbGib, deviceTag, delegateSecret } = await createPasskeyDeviceSyncDelegate({
698
+ username,
699
+ primaryTjpAddr,
700
+ passkeySecret,
701
+ transports,
702
+ metaspace,
703
+ space,
704
+ });
705
+ // 6. Register Delegate locally on primary keystone
706
+ const evolvedParentKeystoneIbGib = await keystoneService.registerDelegate({
707
+ parentKeystone: keystoneIbGib,
708
+ delegateKeystone: delegateKeystoneIbGib,
709
+ masterSecret: passkeySecret,
710
+ signingPoolId: poolId,
711
+ allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
712
+ deviceName: deviceTag,
713
+ metaspace,
714
+ space,
715
+ });
716
+ // 7. Post to Server & Handle Verification
717
+ this.setStatus("Syncing genesis keystone to server...", "info");
718
+ this.showBusy({
719
+ isBusy: true,
720
+ title: 'Syncing to Server...',
721
+ msg: 'Staging pending genesis keystone on server...',
722
+ animationEmoji: '🔄'
723
+ });
724
+ const resSync = await getApiBridge().postGenesisKeystone(keystoneIbGib);
725
+ if (!resSync.success) {
726
+ const msg = `Sync failed: ${resSync.message}`;
727
+ console.error(`${lc} ${msg}`);
728
+ this.setStatus(msg, "error");
729
+ this.showBusy({ isBusy: false });
730
+ if (btn) {
731
+ btn.disabled = false;
732
+ btn.textContent = '🛡️ Create Identity with Passkey / Biometrics';
733
+ }
734
+ return;
735
+ }
736
+ const isPending = resSync.pending || resSync.statusCode === 202;
737
+ if (isPending) {
738
+ console.log(`${lc} Genesis keystone quarantined in pending staging area. Prompting for 6-digit verification code.`);
739
+ this.showBusy({ isBusy: false });
740
+ this.setFormDisabled(true);
741
+ if (!this.shadowRoot) {
742
+ throw new Error(`(UNEXPECTED) this.shadowRoot falsy? (E: 6d316f5a2bf8e6a0286306468d35c826)`);
743
+ }
744
+ const secVerify = shadowRoot_getElementById(this.shadowRoot, 'section-code-verification');
745
+ const inputVerifyCode = shadowRoot_getElementById(this.shadowRoot, 'input-verify-code');
746
+ const btnSubmitCode = shadowRoot_getElementById(this.shadowRoot, 'btn-submit-code');
747
+ const btnResendCode = shadowRoot_getElementById(this.shadowRoot, 'btn-resend-code');
748
+ secVerify.classList.remove('hidden');
749
+ secVerify.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
750
+ this.setStatus("✉️ Verification code sent to your email. Enter 6-digit code below:", "info");
751
+ this.startResendCooldownTimer({ btnId: 'btn-resend-code', durationSecs: 30 });
752
+ const onResendCode = async () => {
753
+ btnResendCode.disabled = true;
754
+ btnResendCode.textContent = "Sending...";
755
+ this.setStatus("Resending verification code to your email...", "info");
756
+ try {
757
+ const resResend = await getApiBridge().postVerificationCode({ email, primaryTjpAddr });
758
+ if (resResend.success) {
759
+ this.setStatus("✉️ Fresh 6-digit verification code sent to your email!", "success");
760
+ this.startResendCooldownTimer({ btnId: 'btn-resend-code', durationSecs: 30 });
761
+ inputVerifyCode.value = '';
762
+ inputVerifyCode.focus();
763
+ }
764
+ else {
765
+ this.setStatus(resResend.message || "Failed to resend verification code.", "error");
766
+ btnResendCode.disabled = false;
767
+ btnResendCode.textContent = "Resend Code";
768
+ }
769
+ }
770
+ catch (error) {
771
+ const emsg = extractErrorMsg(error);
772
+ console.error(`${lc} ${emsg}`);
773
+ this.setStatus(`Failed to resend verification code: ${emsg}`, "error");
774
+ btnResendCode.disabled = false;
775
+ btnResendCode.textContent = "Resend Code";
776
+ }
777
+ };
778
+ btnResendCode.addEventListener('click', onResendCode);
779
+ await new Promise((resolve) => {
780
+ const onSubmitCode = async () => {
781
+ const code = inputVerifyCode.value.trim();
782
+ if (!code || code.length !== 6 || !/^\d{6}$/.test(code)) {
783
+ this.setStatus("Please enter a valid 6-digit numeric verification code (e.g. 482901).", "error");
784
+ return;
785
+ }
786
+ btnSubmitCode.disabled = true;
787
+ btnSubmitCode.textContent = "Verifying...";
788
+ this.showBusy({
789
+ isBusy: true,
790
+ title: 'Verifying & Activating...',
791
+ msg: 'Great! Juust a moment...\n(Validating code, promoting server domain space, other security stuff...)',
792
+ animationEmoji: '🔐'
793
+ });
794
+ const resVerify = await getApiBridge().postVerifyCode({ email, primaryTjpAddr, code });
795
+ if (!resVerify.success) {
796
+ this.showBusy({ isBusy: false });
797
+ this.setStatus(resVerify.message || "Incorrect verification code.", "error");
798
+ btnSubmitCode.disabled = false;
799
+ btnSubmitCode.textContent = "Verify Code";
800
+ return;
801
+ }
802
+ console.log(`${lc} Code verified & server domain promoted! Syncing delegate...`);
803
+ this.showBusy({
804
+ isBusy: true,
805
+ title: 'Syncing Delegate...',
806
+ msg: 'Posting evolved domain identity and sync delegate to server...',
807
+ animationEmoji: '⚡'
808
+ });
809
+ btnSubmitCode.removeEventListener('click', onSubmitCode);
810
+ secVerify.classList.add('hidden');
811
+ this.setFormDisabled(false);
812
+ // Now that server domain space is promoted, post evolved keystone + delegate
813
+ const resEvolve = await getApiBridge().putEvolveKeystone({
814
+ domainAddr: primaryTjpAddr,
815
+ keystoneIbGib: evolvedParentKeystoneIbGib,
816
+ relatedKeystoneIbGibs: [delegateKeystoneIbGib]
817
+ });
818
+ if (!resEvolve.success) {
819
+ console.warn(`${lc} Delegate sync warning after code verification: ${resEvolve.message}`);
820
+ }
821
+ resolve();
822
+ };
823
+ btnSubmitCode.addEventListener('click', onSubmitCode);
824
+ });
825
+ }
826
+ else {
827
+ // Post evolved keystone + delegate keystone immediately
828
+ this.setStatus("Syncing delegate registration to server...", "info");
829
+ this.showBusy({
830
+ isBusy: true,
831
+ title: 'Syncing Delegate...',
832
+ msg: 'Posting evolved domain identity and sync delegate to server...',
833
+ animationEmoji: '🔄'
834
+ });
835
+ const resEvolve = await getApiBridge().putEvolveKeystone({
836
+ domainAddr: primaryTjpAddr,
837
+ keystoneIbGib: evolvedParentKeystoneIbGib,
838
+ relatedKeystoneIbGibs: [delegateKeystoneIbGib]
839
+ });
840
+ if (!resEvolve.success) {
841
+ const msg = `Delegate registration sync failed: ${resEvolve.message}`;
842
+ console.error(`${lc} ${msg}`);
843
+ this.setStatus(msg, "error");
844
+ this.showBusy({ isBusy: false });
845
+ if (btn) {
846
+ btn.disabled = false;
847
+ btn.textContent = '🛡️ Create Identity with Passkey / Biometrics';
848
+ }
849
+ return;
850
+ }
851
+ }
852
+ // 8. Finalize Onboarding
853
+ await this.finalizeIdentityOnboarding({
854
+ primaryKeystone: evolvedParentKeystoneIbGib,
855
+ delegateKeystone: delegateKeystoneIbGib,
856
+ primaryTjpAddr,
857
+ masterSecret: passkeySecret,
858
+ delegateSecret,
859
+ makeActive: checkboxActive?.checked ?? true
860
+ });
861
+ }
862
+ catch (error) {
863
+ const emsg = extractErrorMsg(error);
864
+ console.error(`${lc} ${emsg}`, error);
865
+ this.setStatus(`Error: ${emsg}`, "error");
866
+ }
867
+ finally {
868
+ this.showBusy({ isBusy: false });
869
+ if (btn) {
870
+ btn.disabled = false;
871
+ btn.textContent = '🛡️ Create Identity with Passkey / Biometrics';
872
+ }
873
+ }
874
+ }
437
875
  /**
438
876
  * Triggered by the "Generate" button.
439
877
  * Orchestrates: Secret -> Local Genesis -> Graph Collection -> Server Sync.
@@ -562,19 +1000,10 @@ export class KeystoneCreatorComponentInstance extends IbGibFormInstanceBase {
562
1000
  msg: 'Generating cryptographic sync delegate keys...',
563
1001
  animationEmoji: '🔑'
564
1002
  });
565
- const delegateSecret = await deriveDelegateSecret({ masterSecret });
566
- await setDeviceDelegateSecret({ primaryTjpAddr, delegateSecret });
567
- const sovereignSyncProfileName = process.env.KEYSTONE_PROFILE_SOVEREIGN_SYNC;
568
- if (!sovereignSyncProfileName || !sovereignSyncProfileName.trim()) {
569
- 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)');
570
- }
571
- const delegateKeystoneIbGib = await createSyncDelegateKeystone({
572
- profileName: sovereignSyncProfileName.trim(),
1003
+ const { delegateKeystone: delegateKeystoneIbGib, deviceTag, delegateSecret } = await createSovereignDeviceSyncDelegate({
573
1004
  username,
574
- deviceTag: generateDefaultDeviceTag(),
575
1005
  primaryTjpAddr,
576
- delegateSecret,
577
- additionalFrameDetails: { client: 'space-gib-web' },
1006
+ masterSecret,
578
1007
  metaspace,
579
1008
  space,
580
1009
  });
@@ -593,9 +1022,11 @@ export class KeystoneCreatorComponentInstance extends IbGibFormInstanceBase {
593
1022
  parentKeystone: keystoneIbGib,
594
1023
  delegateKeystone: delegateKeystoneIbGib,
595
1024
  masterSecret,
1025
+ signingPoolId: 'manage',
596
1026
  metaspace,
597
1027
  space,
598
- allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT]
1028
+ allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
1029
+ deviceName: deviceTag,
599
1030
  });
600
1031
  const evolvedParentAddr = getIbGibAddr({ ibGib: evolvedParentKeystoneIbGib });
601
1032
  console.log(`${lc} delegate registered. evolved parent address: ${evolvedParentAddr}`);
@@ -632,19 +1063,32 @@ export class KeystoneCreatorComponentInstance extends IbGibFormInstanceBase {
632
1063
  secVerify.classList.remove('hidden');
633
1064
  secVerify.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
634
1065
  this.setStatus("✉️ Verification code sent to your email. Enter 6-digit code below:", "info");
1066
+ this.startResendCooldownTimer({ btnId: 'btn-resend-code', durationSecs: 30 });
635
1067
  const onResendCode = async () => {
636
1068
  btnResendCode.disabled = true;
637
1069
  btnResendCode.textContent = "Sending...";
638
1070
  this.setStatus("Resending verification code to your email...", "info");
639
- const resResend = await getApiBridge().postVerificationCode({ email, primaryTjpAddr: primaryTjpAddr });
640
- if (resResend.success) {
641
- this.setStatus("✉️ New verification code sent to your email!", "success");
1071
+ try {
1072
+ const resResend = await getApiBridge().postVerificationCode({ email, primaryTjpAddr: primaryTjpAddr });
1073
+ if (resResend.success) {
1074
+ this.setStatus("✉️ Fresh 6-digit verification code sent to your email!", "success");
1075
+ this.startResendCooldownTimer({ btnId: 'btn-resend-code', durationSecs: 30 });
1076
+ inputVerifyCode.value = '';
1077
+ inputVerifyCode.focus();
1078
+ }
1079
+ else {
1080
+ this.setStatus(resResend.message || "Failed to resend verification code.", "error");
1081
+ btnResendCode.disabled = false;
1082
+ btnResendCode.textContent = "Resend Code";
1083
+ }
642
1084
  }
643
- else {
644
- this.setStatus(resResend.message || "Failed to resend verification code.", "error");
1085
+ catch (error) {
1086
+ const emsg = extractErrorMsg(error);
1087
+ console.error(`${lc} ${emsg}`);
1088
+ this.setStatus(`Failed to resend verification code: ${emsg}`, "error");
1089
+ btnResendCode.disabled = false;
1090
+ btnResendCode.textContent = "Resend Code";
645
1091
  }
646
- btnResendCode.disabled = false;
647
- btnResendCode.textContent = "Resend Code";
648
1092
  };
649
1093
  btnResendCode.addEventListener('click', onResendCode);
650
1094
  await new Promise((resolve, reject) => {