@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
@@ -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,13 +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 } from "@ibgib/core-gib/dist/keystone/strategy/hash-reveal-v1/hash-reveal-v1.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 { deriveLocalCustodianDelegateSecret } from "../../../../identity/custodian-delegate-helper.mjs";
16
+ import { createSovereignDeviceSyncDelegate, createOAuth2DeviceSyncDelegate, createEmailDeviceSyncDelegate } from "../../../../identity/device-delegate-helper.mjs";
18
17
  import { executeSsoOAuthPopup } from "../../../../identity/sso-popup-helper.mjs";
18
+ import { generateDefaultDeviceTag } from "../../../../util/device-helper.mjs";
19
+ import { registerPasskeyCredential, evaluatePasskeyPrf, createPasskeyDeviceSyncDelegate, bufferToHex } from "../../../../identity/passkey-helper.mjs";
19
20
  import { EVENT_IBGIB_IDENTITY_REQUEST_CHANGE, EVENT_IBGIB_UI_MESSAGE } from "../../../ui-constants.mjs";
20
21
  import { alertUser, shadowRoot_getElementById } from "../../../../helpers.web.mjs";
21
22
  export const KEYSTONE_CREATOR_COMPONENT_NAME = 'ibgib-keystone-creator';
@@ -50,12 +51,43 @@ export class KeystoneCreatorComponentMeta extends IbGibDynamicComponentMetaBase
50
51
  export class KeystoneCreatorComponentInstance extends IbGibFormInstanceBase {
51
52
  lc = `[KeystoneCreatorComponentInstance]`;
52
53
  hasExistingIdentities = false;
54
+ resendCooldownInterval = null;
53
55
  constructor() {
54
56
  super();
55
57
  }
56
58
  async initialize(opts) {
57
59
  await super.initialize(opts);
58
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
+ }
59
91
  async created() {
60
92
  this.elements = {};
61
93
  this.elements.containerEl = this.shadowRoot.getElementById('container');
@@ -68,6 +100,12 @@ export class KeystoneCreatorComponentInstance extends IbGibFormInstanceBase {
68
100
  this.elements.inputEmail = this.shadowRoot.getElementById('input-email');
69
101
  this.elements.inputSecret = this.shadowRoot.getElementById('input-secret');
70
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');
71
109
  this.elements.statusArea = this.shadowRoot.getElementById('status-area');
72
110
  this.elements.statusText = this.shadowRoot.getElementById('status-text');
73
111
  // ElementBase requires contentEl
@@ -98,46 +136,46 @@ export class KeystoneCreatorComponentInstance extends IbGibFormInstanceBase {
98
136
  console.warn(`${lc} Error checking existing identities: ${extractErrorMsg(error)}`);
99
137
  this.hasExistingIdentities = false;
100
138
  }
101
- const checkbox = this.elements.checkboxMakeActive;
102
- if (checkbox) {
103
- if (!this.hasExistingIdentities) {
104
- checkbox.checked = true;
105
- checkbox.disabled = true;
106
- checkbox.title = "At least one active identity is required.";
107
- }
108
- else {
109
- checkbox.disabled = false;
110
- 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
+ }
111
151
  }
112
152
  }
113
153
  }
114
- async disconnected() {
115
- // cleanup if needed
116
- }
117
154
  initHandlers() {
118
- // Mode Tab Switching (Quick vs Password-based)
155
+ // Mode Tab Switching (Quick vs Passkey vs Password-based)
119
156
  const tabQuick = this.shadowRoot?.getElementById('tab-quick');
157
+ const tabPasskey = this.shadowRoot?.getElementById('tab-passkey');
120
158
  const tabPassword = this.shadowRoot?.getElementById('tab-password');
121
159
  const panelQuick = this.shadowRoot?.getElementById('panel-quick');
160
+ const panelPasskey = this.shadowRoot?.getElementById('panel-passkey');
122
161
  const panelPassword = this.shadowRoot?.getElementById('panel-password');
123
162
  const subtitleQuick = this.shadowRoot?.getElementById('subtitle-quick');
163
+ const subtitlePasskey = this.shadowRoot?.getElementById('subtitle-passkey');
124
164
  const subtitlePassword = this.shadowRoot?.getElementById('subtitle-password');
125
- tabQuick?.addEventListener('click', () => {
126
- tabQuick.classList.add('active');
127
- tabPassword?.classList.remove('active');
128
- panelQuick?.classList.remove('hidden');
129
- panelPassword?.classList.add('hidden');
130
- subtitleQuick?.classList.remove('hidden');
131
- subtitlePassword?.classList.add('hidden');
132
- });
133
- tabPassword?.addEventListener('click', () => {
134
- tabPassword.classList.add('active');
135
- tabQuick?.classList.remove('active');
136
- panelPassword?.classList.remove('hidden');
137
- panelQuick?.classList.add('hidden');
138
- subtitlePassword?.classList.remove('hidden');
139
- subtitleQuick?.classList.add('hidden');
140
- });
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'));
141
179
  const btnGoogleSso = this.shadowRoot?.getElementById('btn-google-sso');
142
180
  btnGoogleSso?.addEventListener('click', () => this.handleSsoClick('google'));
143
181
  const btnGithubSso = this.shadowRoot?.getElementById('btn-github-sso');
@@ -157,8 +195,11 @@ export class KeystoneCreatorComponentInstance extends IbGibFormInstanceBase {
157
195
  const secQuickOtp = this.shadowRoot?.getElementById('section-quick-otp');
158
196
  const inputQuickOtp = this.shadowRoot?.getElementById('input-quick-otp');
159
197
  const btnQuickVerifyCode = this.shadowRoot?.getElementById('btn-quick-verify-code');
198
+ const btnQuickResendCode = this.shadowRoot?.getElementById('btn-quick-resend-code');
160
199
  btnQuickSendCode?.addEventListener('click', () => this.handleQuickSendCode());
161
200
  btnQuickVerifyCode?.addEventListener('click', () => this.handleQuickVerifyCode());
201
+ btnQuickResendCode?.addEventListener('click', () => this.handleQuickResendCode());
202
+ this.elements.btnGeneratePasskey?.addEventListener('click', () => this.handleGeneratePasskey());
162
203
  this.elements.btnGenerateEl.addEventListener('click', () => this.handleGenerate());
163
204
  this.shadowRoot.getElementById('btn-switch-sign-in')?.addEventListener('click', (e) => {
164
205
  e.preventDefault();
@@ -169,6 +210,15 @@ export class KeystoneCreatorComponentInstance extends IbGibFormInstanceBase {
169
210
  }));
170
211
  });
171
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
+ });
172
222
  this.elements.inputUsername?.addEventListener('input', () => {
173
223
  this.elements.inputUsername.classList.remove('invalid');
174
224
  });
@@ -235,37 +285,21 @@ export class KeystoneCreatorComponentInstance extends IbGibFormInstanceBase {
235
285
  msg: 'Generating cryptographic sync delegate keys...',
236
286
  animationEmoji: '🔑'
237
287
  });
238
- const delegateSecret = await deriveLocalCustodianDelegateSecret({ primaryTjpAddr });
239
- const email = resLogin.userInfo?.email || '';
240
- const username = email ? email.split('@')[0] : 'user';
241
- const custodianSyncProfileName = process.env.KEYSTONE_PROFILE_CUSTODIAN_SYNC;
242
- if (!custodianSyncProfileName || !custodianSyncProfileName.trim()) {
243
- 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)');
244
- }
245
- const delegateBuilder = KeystoneProfileBuilder.buildKeystone(custodianSyncProfileName.trim())
246
- .withUsername(`${username}-sync`)
247
- .withDescription(`Sync Delegate for ${username}`)
248
- .withDetails({
249
- client: 'space-gib-web',
250
- role: 'sync-delegate',
251
- primaryTjpAddr,
252
- custodianReqs,
253
- });
254
- const configs = await delegateBuilder.compileConfigs();
255
288
  const metaspace = await getGlobalMetaspace_waitIfNeeded();
256
289
  const space = await metaspace.getLocalUserSpace({ lock: false });
257
290
  if (!space) {
258
291
  this.setStatus('Could not access local user space.', 'error');
259
292
  return;
260
293
  }
261
- const keystoneService = new KeystoneService_V1();
262
- const delegateKeystone = await keystoneService.genesis({
263
- masterSecret: delegateSecret,
264
- configs,
294
+ const email = resLogin.userInfo?.email || '';
295
+ const username = email ? email.split('@')[0] : 'user';
296
+ const { delegateKeystone, delegateSecret } = await createOAuth2DeviceSyncDelegate({
297
+ providerId,
298
+ username,
299
+ primaryTjpAddr,
300
+ custodianReqs,
265
301
  metaspace,
266
302
  space,
267
- frameDetails: delegateBuilder.getFrameDetails(),
268
- isPrimary: false
269
303
  });
270
304
  this.setStatus('Registering device delegate with custodian server...', 'info');
271
305
  this.showBusy({
@@ -322,6 +356,7 @@ export class KeystoneCreatorComponentInstance extends IbGibFormInstanceBase {
322
356
  if (resCode.success) {
323
357
  secQuickOtp?.classList.remove('hidden');
324
358
  this.setStatus("✉️ 6-digit verification code sent to your email!", "success");
359
+ this.startResendCooldownTimer({ btnId: 'btn-quick-resend-code', durationSecs: 30 });
325
360
  }
326
361
  else {
327
362
  this.setStatus(resCode.message || "Failed to dispatch verification code.", "error");
@@ -339,6 +374,52 @@ export class KeystoneCreatorComponentInstance extends IbGibFormInstanceBase {
339
374
  }
340
375
  }
341
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
+ }
342
423
  /**
343
424
  * Triggered by the "Verify Code" button.
344
425
  * Submits 6-digit OTP code to custodian server, builds local sync delegate keystone,
@@ -382,36 +463,18 @@ export class KeystoneCreatorComponentInstance extends IbGibFormInstanceBase {
382
463
  msg: 'Generating cryptographic sync delegate keys...',
383
464
  animationEmoji: '🔑'
384
465
  });
385
- const delegateSecret = await deriveLocalCustodianDelegateSecret({ primaryTjpAddr });
386
- const username = email ? email.split('@')[0] : 'user';
387
- const custodianSyncProfileName = process.env.KEYSTONE_PROFILE_CUSTODIAN_SYNC;
388
- if (!custodianSyncProfileName || !custodianSyncProfileName.trim()) {
389
- 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)');
390
- }
391
- const delegateBuilder = KeystoneProfileBuilder.buildKeystone(custodianSyncProfileName.trim())
392
- .withUsername(`${username}-sync`)
393
- .withDescription(`Sync Delegate for ${username}`)
394
- .withDetails({
395
- client: 'space-gib-web',
396
- role: 'sync-delegate',
397
- primaryTjpAddr,
398
- custodianReqs,
399
- });
400
- const configs = await delegateBuilder.compileConfigs();
401
466
  const metaspace = await getGlobalMetaspace_waitIfNeeded();
402
467
  const space = await metaspace.getLocalUserSpace({ lock: false });
403
468
  if (!space) {
404
469
  this.setStatus("Failed to access local user space.", "error");
405
470
  return;
406
471
  }
407
- const keystoneService = new KeystoneService_V1();
408
- const delegateKeystone = await keystoneService.genesis({
409
- masterSecret: delegateSecret,
410
- configs,
472
+ const { delegateKeystone, delegateSecret } = await createEmailDeviceSyncDelegate({
473
+ email,
474
+ primaryTjpAddr,
475
+ custodianReqs,
411
476
  metaspace,
412
477
  space,
413
- frameDetails: delegateBuilder.getFrameDetails(),
414
- isPrimary: false
415
478
  });
416
479
  // Register delegate with server
417
480
  this.setStatus("Registering device delegate with custodian server...", "info");
@@ -449,6 +512,358 @@ export class KeystoneCreatorComponentInstance extends IbGibFormInstanceBase {
449
512
  }
450
513
  }
451
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 rawSalt = crypto.getRandomValues(new Uint8Array(32));
595
+ const saltHex = bufferToHex(rawSalt.buffer);
596
+ const regResult = await registerPasskeyCredential({
597
+ username,
598
+ displayName: username,
599
+ salt: saltHex
600
+ });
601
+ const { credentialId, transports } = regResult;
602
+ let passkeySecret = regResult.passkeySecret;
603
+ if (!passkeySecret) {
604
+ const prfResult = await evaluatePasskeyPrf({
605
+ credentialId,
606
+ salt: saltHex,
607
+ transports
608
+ });
609
+ passkeySecret = prfResult.passkeySecret;
610
+ }
611
+ const passkeyName = (inputName?.value || '').trim() || generateDefaultDeviceTag();
612
+ const passkeyTag = getSaferSubstring({ text: passkeyName, length: 12, keepLiterals: ['-'] }) || 'key';
613
+ const poolId = `passkey-manage-${passkeyTag}`;
614
+ const passkeyEntry = {
615
+ credentialId,
616
+ poolId,
617
+ name: passkeyName,
618
+ salt: saltHex,
619
+ createdUTC: new Date().toISOString(),
620
+ ...(transports && transports.length > 0 ? { transports } : {})
621
+ };
622
+ this.showBusy({
623
+ isBusy: true,
624
+ title: 'Generating Identity...',
625
+ msg: 'Compiling sovereign passkey challenge pools...',
626
+ animationEmoji: '⚙️'
627
+ });
628
+ // 2. Prepare Sovereign Keystone Configs
629
+ const sovereignPrimaryProfileName = process.env.KEYSTONE_PROFILE_SOVEREIGN_PRIMARY;
630
+ if (!sovereignPrimaryProfileName || !sovereignPrimaryProfileName.trim()) {
631
+ throw new Error('KEYSTONE_PROFILE_SOVEREIGN_PRIMARY environment variable is missing.');
632
+ }
633
+ const builder = KeystoneProfileBuilder.buildKeystone(sovereignPrimaryProfileName.trim())
634
+ .withUsername(username)
635
+ .withDescription(description);
636
+ if (email) {
637
+ builder.withEmails([email]);
638
+ }
639
+ builder.withDetails({
640
+ client: 'space-gib-web',
641
+ role: 'domain-identity',
642
+ passkeys: {
643
+ [credentialId]: passkeyEntry
644
+ }
645
+ });
646
+ const configs = await builder.compileConfigs();
647
+ // Re-label manage pool to passkey pool id
648
+ for (const cfg of configs) {
649
+ if (cfg.id === 'manage') {
650
+ cfg.id = poolId;
651
+ }
652
+ }
653
+ // 3. Prepare Metaspace & Local Space
654
+ const metaspace = await getGlobalMetaspace_waitIfNeeded();
655
+ const space = await metaspace.getLocalUserSpace({ lock: false });
656
+ if (!space) {
657
+ this.setStatus("Failed to access local user space.", "error");
658
+ return;
659
+ }
660
+ // 4. Perform Local Genesis
661
+ this.showBusy({
662
+ isBusy: true,
663
+ title: 'Generating Identity...',
664
+ msg: 'Generating cryptographic domain identity keys...',
665
+ animationEmoji: '🔑'
666
+ });
667
+ const keystoneService = new KeystoneService_V1();
668
+ const keystoneIbGib = await keystoneService.genesis({
669
+ masterSecret: passkeySecret,
670
+ emails: email ? [email] : undefined,
671
+ credentialIds: [credentialId],
672
+ configs,
673
+ metaspace,
674
+ space,
675
+ frameDetails: builder.getFrameDetails(),
676
+ isPrimary: true
677
+ });
678
+ const primaryTjpAddr = getTjpAddr({ ibGib: keystoneIbGib, defaultIfNone: 'incomingAddr' });
679
+ console.log(`${lc} passkey local genesis complete: ${primaryTjpAddr}`);
680
+ await delay(humanEyeDelay);
681
+ this.setStatus("Local genesis complete. Creating passkey sync delegate...", "info");
682
+ // 5. Create Passkey Device Sync Delegate
683
+ this.showBusy({
684
+ isBusy: true,
685
+ title: 'Creating Sync Delegate...',
686
+ msg: 'Generating cryptographic sync delegate keys...',
687
+ animationEmoji: '🔑'
688
+ });
689
+ const { delegateKeystone: delegateKeystoneIbGib, deviceTag, delegateSecret } = await createPasskeyDeviceSyncDelegate({
690
+ username,
691
+ primaryTjpAddr,
692
+ passkeySecret,
693
+ transports,
694
+ metaspace,
695
+ space,
696
+ });
697
+ // 6. Register Delegate locally on primary keystone
698
+ const evolvedParentKeystoneIbGib = await keystoneService.registerDelegate({
699
+ parentKeystone: keystoneIbGib,
700
+ delegateKeystone: delegateKeystoneIbGib,
701
+ masterSecret: passkeySecret,
702
+ signingPoolId: poolId,
703
+ allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
704
+ deviceName: deviceTag,
705
+ metaspace,
706
+ space,
707
+ });
708
+ // 7. Post to Server & Handle Verification
709
+ this.setStatus("Syncing genesis keystone to server...", "info");
710
+ this.showBusy({
711
+ isBusy: true,
712
+ title: 'Syncing to Server...',
713
+ msg: 'Staging pending genesis keystone on server...',
714
+ animationEmoji: '🔄'
715
+ });
716
+ const resSync = await getApiBridge().postGenesisKeystone(keystoneIbGib);
717
+ if (!resSync.success) {
718
+ const msg = `Sync failed: ${resSync.message}`;
719
+ console.error(`${lc} ${msg}`);
720
+ this.setStatus(msg, "error");
721
+ this.showBusy({ isBusy: false });
722
+ if (btn) {
723
+ btn.disabled = false;
724
+ btn.textContent = '🛡️ Create Identity with Passkey / Biometrics';
725
+ }
726
+ return;
727
+ }
728
+ const isPending = resSync.pending || resSync.statusCode === 202;
729
+ if (isPending) {
730
+ console.log(`${lc} Genesis keystone quarantined in pending staging area. Prompting for 6-digit verification code.`);
731
+ this.showBusy({ isBusy: false });
732
+ this.setFormDisabled(true);
733
+ if (!this.shadowRoot) {
734
+ throw new Error(`(UNEXPECTED) this.shadowRoot falsy? (E: 6d316f5a2bf8e6a0286306468d35c826)`);
735
+ }
736
+ const secVerify = shadowRoot_getElementById(this.shadowRoot, 'section-code-verification');
737
+ const inputVerifyCode = shadowRoot_getElementById(this.shadowRoot, 'input-verify-code');
738
+ const btnSubmitCode = shadowRoot_getElementById(this.shadowRoot, 'btn-submit-code');
739
+ const btnResendCode = shadowRoot_getElementById(this.shadowRoot, 'btn-resend-code');
740
+ secVerify.classList.remove('hidden');
741
+ secVerify.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
742
+ this.setStatus("✉️ Verification code sent to your email. Enter 6-digit code below:", "info");
743
+ this.startResendCooldownTimer({ btnId: 'btn-resend-code', durationSecs: 30 });
744
+ const onResendCode = async () => {
745
+ btnResendCode.disabled = true;
746
+ btnResendCode.textContent = "Sending...";
747
+ this.setStatus("Resending verification code to your email...", "info");
748
+ try {
749
+ const resResend = await getApiBridge().postVerificationCode({ email, primaryTjpAddr });
750
+ if (resResend.success) {
751
+ this.setStatus("✉️ Fresh 6-digit verification code sent to your email!", "success");
752
+ this.startResendCooldownTimer({ btnId: 'btn-resend-code', durationSecs: 30 });
753
+ inputVerifyCode.value = '';
754
+ inputVerifyCode.focus();
755
+ }
756
+ else {
757
+ this.setStatus(resResend.message || "Failed to resend verification code.", "error");
758
+ btnResendCode.disabled = false;
759
+ btnResendCode.textContent = "Resend Code";
760
+ }
761
+ }
762
+ catch (error) {
763
+ const emsg = extractErrorMsg(error);
764
+ console.error(`${lc} ${emsg}`);
765
+ this.setStatus(`Failed to resend verification code: ${emsg}`, "error");
766
+ btnResendCode.disabled = false;
767
+ btnResendCode.textContent = "Resend Code";
768
+ }
769
+ };
770
+ btnResendCode.addEventListener('click', onResendCode);
771
+ await new Promise((resolve) => {
772
+ const onSubmitCode = async () => {
773
+ const code = inputVerifyCode.value.trim();
774
+ if (!code || code.length !== 6 || !/^\d{6}$/.test(code)) {
775
+ this.setStatus("Please enter a valid 6-digit numeric verification code (e.g. 482901).", "error");
776
+ return;
777
+ }
778
+ btnSubmitCode.disabled = true;
779
+ btnSubmitCode.textContent = "Verifying...";
780
+ this.showBusy({
781
+ isBusy: true,
782
+ title: 'Verifying & Activating...',
783
+ msg: 'Great! Juust a moment...\n(Validating code, promoting server domain space, other security stuff...)',
784
+ animationEmoji: '🔐'
785
+ });
786
+ const resVerify = await getApiBridge().postVerifyCode({ email, primaryTjpAddr, code });
787
+ if (!resVerify.success) {
788
+ this.showBusy({ isBusy: false });
789
+ this.setStatus(resVerify.message || "Incorrect verification code.", "error");
790
+ btnSubmitCode.disabled = false;
791
+ btnSubmitCode.textContent = "Verify Code";
792
+ return;
793
+ }
794
+ console.log(`${lc} Code verified & server domain promoted! Syncing delegate...`);
795
+ this.showBusy({
796
+ isBusy: true,
797
+ title: 'Syncing Delegate...',
798
+ msg: 'Posting evolved domain identity and sync delegate to server...',
799
+ animationEmoji: '⚡'
800
+ });
801
+ btnSubmitCode.removeEventListener('click', onSubmitCode);
802
+ secVerify.classList.add('hidden');
803
+ this.setFormDisabled(false);
804
+ // Now that server domain space is promoted, post evolved keystone + delegate
805
+ const resEvolve = await getApiBridge().putEvolveKeystone({
806
+ domainAddr: primaryTjpAddr,
807
+ keystoneIbGib: evolvedParentKeystoneIbGib,
808
+ relatedKeystoneIbGibs: [delegateKeystoneIbGib]
809
+ });
810
+ if (!resEvolve.success) {
811
+ console.warn(`${lc} Delegate sync warning after code verification: ${resEvolve.message}`);
812
+ }
813
+ resolve();
814
+ };
815
+ btnSubmitCode.addEventListener('click', onSubmitCode);
816
+ });
817
+ }
818
+ else {
819
+ // Post evolved keystone + delegate keystone immediately
820
+ this.setStatus("Syncing delegate registration to server...", "info");
821
+ this.showBusy({
822
+ isBusy: true,
823
+ title: 'Syncing Delegate...',
824
+ msg: 'Posting evolved domain identity and sync delegate to server...',
825
+ animationEmoji: '🔄'
826
+ });
827
+ const resEvolve = await getApiBridge().putEvolveKeystone({
828
+ domainAddr: primaryTjpAddr,
829
+ keystoneIbGib: evolvedParentKeystoneIbGib,
830
+ relatedKeystoneIbGibs: [delegateKeystoneIbGib]
831
+ });
832
+ if (!resEvolve.success) {
833
+ const msg = `Delegate registration sync failed: ${resEvolve.message}`;
834
+ console.error(`${lc} ${msg}`);
835
+ this.setStatus(msg, "error");
836
+ this.showBusy({ isBusy: false });
837
+ if (btn) {
838
+ btn.disabled = false;
839
+ btn.textContent = '🛡️ Create Identity with Passkey / Biometrics';
840
+ }
841
+ return;
842
+ }
843
+ }
844
+ // 8. Finalize Onboarding
845
+ await this.finalizeIdentityOnboarding({
846
+ primaryKeystone: evolvedParentKeystoneIbGib,
847
+ delegateKeystone: delegateKeystoneIbGib,
848
+ primaryTjpAddr,
849
+ masterSecret: passkeySecret,
850
+ delegateSecret,
851
+ makeActive: checkboxActive?.checked ?? true
852
+ });
853
+ }
854
+ catch (error) {
855
+ const emsg = extractErrorMsg(error);
856
+ console.error(`${lc} ${emsg}`, error);
857
+ this.setStatus(`Error: ${emsg}`, "error");
858
+ }
859
+ finally {
860
+ this.showBusy({ isBusy: false });
861
+ if (btn) {
862
+ btn.disabled = false;
863
+ btn.textContent = '🛡️ Create Identity with Passkey / Biometrics';
864
+ }
865
+ }
866
+ }
452
867
  /**
453
868
  * Triggered by the "Generate" button.
454
869
  * Orchestrates: Secret -> Local Genesis -> Graph Collection -> Server Sync.
@@ -577,26 +992,12 @@ export class KeystoneCreatorComponentInstance extends IbGibFormInstanceBase {
577
992
  msg: 'Generating cryptographic sync delegate keys...',
578
993
  animationEmoji: '🔑'
579
994
  });
580
- const delegateSecret = await deriveDelegateSecret({ masterSecret });
581
- const sovereignSyncProfileName = process.env.KEYSTONE_PROFILE_SOVEREIGN_SYNC;
582
- if (!sovereignSyncProfileName || !sovereignSyncProfileName.trim()) {
583
- 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)');
584
- }
585
- const delegateBuilder = KeystoneProfileBuilder.buildKeystone(sovereignSyncProfileName.trim())
586
- .withUsername(`${username}-sync`)
587
- .withDescription(`Sync Delegate for ${username}`)
588
- .withDetails({
589
- client: 'space-gib-web',
590
- role: 'sync-delegate',
591
- primaryTjpAddr: primaryTjpAddr,
592
- });
593
- const delegateConfigs = await delegateBuilder.compileConfigs();
594
- const delegateKeystoneIbGib = await keystoneService.genesis({
595
- masterSecret: delegateSecret,
596
- configs: delegateConfigs,
995
+ const { delegateKeystone: delegateKeystoneIbGib, deviceTag, delegateSecret } = await createSovereignDeviceSyncDelegate({
996
+ username,
997
+ primaryTjpAddr,
998
+ masterSecret,
597
999
  metaspace,
598
1000
  space,
599
- frameDetails: delegateBuilder.getFrameDetails()
600
1001
  });
601
1002
  const delegateAddr = getIbGibAddr({ ibGib: delegateKeystoneIbGib });
602
1003
  console.log(`${lc} sync delegate genesis complete: ${delegateAddr}`);
@@ -613,9 +1014,11 @@ export class KeystoneCreatorComponentInstance extends IbGibFormInstanceBase {
613
1014
  parentKeystone: keystoneIbGib,
614
1015
  delegateKeystone: delegateKeystoneIbGib,
615
1016
  masterSecret,
1017
+ signingPoolId: 'manage',
616
1018
  metaspace,
617
1019
  space,
618
- allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT]
1020
+ allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
1021
+ deviceName: deviceTag,
619
1022
  });
620
1023
  const evolvedParentAddr = getIbGibAddr({ ibGib: evolvedParentKeystoneIbGib });
621
1024
  console.log(`${lc} delegate registered. evolved parent address: ${evolvedParentAddr}`);
@@ -652,19 +1055,32 @@ export class KeystoneCreatorComponentInstance extends IbGibFormInstanceBase {
652
1055
  secVerify.classList.remove('hidden');
653
1056
  secVerify.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
654
1057
  this.setStatus("✉️ Verification code sent to your email. Enter 6-digit code below:", "info");
1058
+ this.startResendCooldownTimer({ btnId: 'btn-resend-code', durationSecs: 30 });
655
1059
  const onResendCode = async () => {
656
1060
  btnResendCode.disabled = true;
657
1061
  btnResendCode.textContent = "Sending...";
658
1062
  this.setStatus("Resending verification code to your email...", "info");
659
- const resResend = await getApiBridge().postVerificationCode({ email, primaryTjpAddr: primaryTjpAddr });
660
- if (resResend.success) {
661
- this.setStatus("✉️ New verification code sent to your email!", "success");
1063
+ try {
1064
+ const resResend = await getApiBridge().postVerificationCode({ email, primaryTjpAddr: primaryTjpAddr });
1065
+ if (resResend.success) {
1066
+ this.setStatus("✉️ Fresh 6-digit verification code sent to your email!", "success");
1067
+ this.startResendCooldownTimer({ btnId: 'btn-resend-code', durationSecs: 30 });
1068
+ inputVerifyCode.value = '';
1069
+ inputVerifyCode.focus();
1070
+ }
1071
+ else {
1072
+ this.setStatus(resResend.message || "Failed to resend verification code.", "error");
1073
+ btnResendCode.disabled = false;
1074
+ btnResendCode.textContent = "Resend Code";
1075
+ }
662
1076
  }
663
- else {
664
- this.setStatus(resResend.message || "Failed to resend verification code.", "error");
1077
+ catch (error) {
1078
+ const emsg = extractErrorMsg(error);
1079
+ console.error(`${lc} ${emsg}`);
1080
+ this.setStatus(`Failed to resend verification code: ${emsg}`, "error");
1081
+ btnResendCode.disabled = false;
1082
+ btnResendCode.textContent = "Resend Code";
665
1083
  }
666
- btnResendCode.disabled = false;
667
- btnResendCode.textContent = "Resend Code";
668
1084
  };
669
1085
  btnResendCode.addEventListener('click', onResendCode);
670
1086
  await new Promise((resolve, reject) => {
@@ -864,6 +1280,10 @@ export class KeystoneCreatorComponentInstance extends IbGibFormInstanceBase {
864
1280
  }
865
1281
  this.setStatus(successMsg, "success");
866
1282
  console.log(`${lc} ✓ ${successMsg}`);
1283
+ // 3.5 Sanitize secret input fields from DOM
1284
+ if (this.elements?.inputSecret) {
1285
+ this.elements.inputSecret.value = '';
1286
+ }
867
1287
  // 4. Dispatch UI navigation message to close creator and open identity manager
868
1288
  window.dispatchEvent(new CustomEvent(EVENT_IBGIB_UI_MESSAGE, {
869
1289
  detail: { action: 'show-identity-manager', addr: primaryTjpAddr },