@ibgib/web-gib 0.0.57 → 0.0.58

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 (41) hide show
  1. package/dist/AUTO-GENERATED-version.d.mts +1 -1
  2. package/dist/AUTO-GENERATED-version.mjs +1 -1
  3. package/dist/api/ibgib-api-bridge.d.mts +63 -1
  4. package/dist/api/ibgib-api-bridge.d.mts.map +1 -1
  5. package/dist/api/ibgib-api-bridge.mjs +135 -0
  6. package/dist/api/ibgib-api-bridge.mjs.map +1 -1
  7. package/dist/identity/custodian-delegate-helper.d.mts +22 -0
  8. package/dist/identity/custodian-delegate-helper.d.mts.map +1 -0
  9. package/dist/identity/custodian-delegate-helper.mjs +50 -0
  10. package/dist/identity/custodian-delegate-helper.mjs.map +1 -0
  11. package/dist/identity/sso-popup-helper.d.mts +33 -0
  12. package/dist/identity/sso-popup-helper.d.mts.map +1 -0
  13. package/dist/identity/sso-popup-helper.mjs +124 -0
  14. package/dist/identity/sso-popup-helper.mjs.map +1 -0
  15. package/dist/index.d.mts +2 -0
  16. package/dist/index.d.mts.map +1 -1
  17. package/dist/index.mjs +3 -0
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/ui/component/identity/identity-header/identity-header.d.mts.map +1 -1
  20. package/dist/ui/component/identity/identity-header/identity-header.mjs +2 -1
  21. package/dist/ui/component/identity/identity-header/identity-header.mjs.map +1 -1
  22. package/dist/ui/component/identity/keystone-creator/keystone-creator.css +109 -23
  23. package/dist/ui/component/identity/keystone-creator/keystone-creator.d.mts +22 -0
  24. package/dist/ui/component/identity/keystone-creator/keystone-creator.d.mts.map +1 -1
  25. package/dist/ui/component/identity/keystone-creator/keystone-creator.html +122 -60
  26. package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs +386 -63
  27. package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs.map +1 -1
  28. package/dist/ui/component/identity/keystone-details/keystone-details.d.mts.map +1 -1
  29. package/dist/ui/component/identity/keystone-details/keystone-details.mjs +10 -53
  30. package/dist/ui/component/identity/keystone-details/keystone-details.mjs.map +1 -1
  31. package/package.json +4 -4
  32. package/src/AUTO-GENERATED-version.mts +1 -1
  33. package/src/api/ibgib-api-bridge.mts +195 -1
  34. package/src/identity/custodian-delegate-helper.mts +61 -0
  35. package/src/identity/sso-popup-helper.mts +155 -0
  36. package/src/index.mts +3 -0
  37. package/src/ui/component/identity/identity-header/identity-header.mts +2 -1
  38. package/src/ui/component/identity/keystone-creator/keystone-creator.css +109 -23
  39. package/src/ui/component/identity/keystone-creator/keystone-creator.html +122 -60
  40. package/src/ui/component/identity/keystone-creator/keystone-creator.mts +441 -67
  41. package/src/ui/component/identity/keystone-details/keystone-details.mts +10 -62
@@ -11,6 +11,7 @@ import { KeystoneService_V1 } from "@ibgib/core-gib/dist/keystone/keystone-servi
11
11
  import { KeystoneProfileBuilder } from "@ibgib/core-gib/dist/keystone/policy/keystone-profile-builder.mjs";
12
12
  import { updateSpecialIndex, } from "@ibgib/core-gib/dist/timeline/timeline-api.mjs";
13
13
  import { toDto, getTjpAddr } from "@ibgib/core-gib/dist/common/other/ibgib-helper.mjs";
14
+ import { FlatIbGibGraph } from "@ibgib/core-gib/dist/common/other/graph-types.mjs";
14
15
  import { validateEmail } from "@ibgib/core-gib/dist/keystone/keystone-helpers.mjs";
15
16
  import { KEYSTONE_USERNAME_REGEXP, KEYSTONE_DESCRIPTION_REGEXP, KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT } from "@ibgib/core-gib/dist/keystone/keystone-constants.mjs";
16
17
  import { deriveDelegateSecret } from "@ibgib/core-gib/dist/keystone/strategy/hash-reveal-v1/hash-reveal-v1.mjs";
@@ -23,8 +24,10 @@ import {
23
24
  } from "../../component-types.mjs";
24
25
  import { getComponentCtorArg } from "../../../../app-bootstrap/init-orchestration.mjs";
25
26
  import { getApiBridge } from "../../../../api/ibgib-api-bridge.mjs";
27
+ import { deriveLocalCustodianDelegateSecret } from "../../../../identity/custodian-delegate-helper.mjs";
28
+ import { executeSsoOAuthPopup } from "../../../../identity/sso-popup-helper.mjs";
26
29
  import { EVENT_IBGIB_IDENTITY_REQUEST_CHANGE, EVENT_IBGIB_UI_MESSAGE } from "../../../ui-constants.mjs";
27
- import { shadowRoot_getElementById } from "../../../../helpers.web.mjs";
30
+ import { alertUser, shadowRoot_getElementById } from "../../../../helpers.web.mjs";
28
31
 
29
32
  export const KEYSTONE_CREATOR_COMPONENT_NAME = 'ibgib-keystone-creator';
30
33
 
@@ -164,6 +167,59 @@ export class KeystoneCreatorComponentInstance
164
167
  }
165
168
 
166
169
  private initHandlers() {
170
+ // Mode Tab Switching (Quick vs Password-based)
171
+ const tabQuick = this.shadowRoot?.getElementById('tab-quick') as HTMLButtonElement | null;
172
+ const tabPassword = this.shadowRoot?.getElementById('tab-password') as HTMLButtonElement | null;
173
+ const panelQuick = this.shadowRoot?.getElementById('panel-quick') as HTMLDivElement | null;
174
+ const panelPassword = this.shadowRoot?.getElementById('panel-password') as HTMLDivElement | null;
175
+ const subtitleQuick = this.shadowRoot?.getElementById('subtitle-quick') as HTMLParagraphElement | null;
176
+ const subtitlePassword = this.shadowRoot?.getElementById('subtitle-password') as HTMLParagraphElement | null;
177
+
178
+ tabQuick?.addEventListener('click', () => {
179
+ tabQuick.classList.add('active');
180
+ tabPassword?.classList.remove('active');
181
+ panelQuick?.classList.remove('hidden');
182
+ panelPassword?.classList.add('hidden');
183
+ subtitleQuick?.classList.remove('hidden');
184
+ subtitlePassword?.classList.add('hidden');
185
+ });
186
+
187
+ tabPassword?.addEventListener('click', () => {
188
+ tabPassword.classList.add('active');
189
+ tabQuick?.classList.remove('active');
190
+ panelPassword?.classList.remove('hidden');
191
+ panelQuick?.classList.add('hidden');
192
+ subtitlePassword?.classList.remove('hidden');
193
+ subtitleQuick?.classList.add('hidden');
194
+ });
195
+
196
+ const btnGoogleSso = this.shadowRoot?.getElementById('btn-google-sso') as HTMLButtonElement | null;
197
+ btnGoogleSso?.addEventListener('click', () => this.handleSsoClick('google'));
198
+
199
+ const btnGithubSso = this.shadowRoot?.getElementById('btn-github-sso') as HTMLButtonElement | null;
200
+ btnGithubSso?.addEventListener('click', () => this.handleSsoClick('github'));
201
+
202
+ // Email Sign-In Info Button
203
+ const btnEmailInfo = this.shadowRoot?.getElementById('btn-email-info') as HTMLButtonElement | null;
204
+ btnEmailInfo?.addEventListener('click', (e) => {
205
+ e.preventDefault();
206
+ alertUser({
207
+ title: 'Custom Email Sign-In',
208
+ msg: 'When you enter your email address, we dispatch a 6-digit one-time verification code to your inbox.\n\nEntering the code creates your cryptographic identity delegate on your device and signs you in seamlessly without needing a password!'
209
+ });
210
+ });
211
+
212
+ // Quick Email Verification Code Handlers
213
+ const btnQuickSendCode = this.shadowRoot?.getElementById('btn-quick-send-code') as HTMLButtonElement | null;
214
+ const inputQuickEmail = this.shadowRoot?.getElementById('input-quick-email') as HTMLInputElement | null;
215
+ const secQuickOtp = this.shadowRoot?.getElementById('section-quick-otp') as HTMLDivElement | null;
216
+ const inputQuickOtp = this.shadowRoot?.getElementById('input-quick-otp') as HTMLInputElement | null;
217
+ const btnQuickVerifyCode = this.shadowRoot?.getElementById('btn-quick-verify-code') as HTMLButtonElement | null;
218
+
219
+ btnQuickSendCode?.addEventListener('click', () => this.handleQuickSendCode());
220
+
221
+ btnQuickVerifyCode?.addEventListener('click', () => this.handleQuickVerifyCode());
222
+
167
223
  this.elements!.btnGenerateEl.addEventListener('click', () => this.handleGenerate());
168
224
 
169
225
  this.shadowRoot!.getElementById('btn-switch-sign-in')?.addEventListener('click', (e) => {
@@ -176,16 +232,16 @@ export class KeystoneCreatorComponentInstance
176
232
  });
177
233
 
178
234
  // Clear invalid styling when user inputs text
179
- this.elements!.inputUsername.addEventListener('input', () => {
235
+ this.elements!.inputUsername?.addEventListener('input', () => {
180
236
  this.elements!.inputUsername.classList.remove('invalid');
181
237
  });
182
- this.elements!.inputDescription.addEventListener('input', () => {
238
+ this.elements!.inputDescription?.addEventListener('input', () => {
183
239
  this.elements!.inputDescription.classList.remove('invalid');
184
240
  });
185
- this.elements!.inputEmail.addEventListener('input', () => {
241
+ this.elements!.inputEmail?.addEventListener('input', () => {
186
242
  this.elements!.inputEmail.classList.remove('invalid');
187
243
  });
188
- this.elements!.inputSecret.addEventListener('input', () => {
244
+ this.elements!.inputSecret?.addEventListener('input', () => {
189
245
  this.elements!.inputSecret.classList.remove('invalid');
190
246
  });
191
247
  }
@@ -198,6 +254,245 @@ export class KeystoneCreatorComponentInstance
198
254
  }
199
255
  }
200
256
 
257
+ /**
258
+ * Triggered by Google / GitHub SSO buttons.
259
+ * Opens OAuth popup, exchanges authorization code, builds local delegate keystone,
260
+ * registers delegate on server, and finalizes onboarding.
261
+ */
262
+ protected async handleSsoClick(providerId: 'google' | 'github'): Promise<void> {
263
+ const lc = `${this.lc}[${this.handleSsoClick.name}]`;
264
+ this.setStatus(`Opening ${providerId.toUpperCase()} SSO authorization window...`, 'info');
265
+ const resPopup = await executeSsoOAuthPopup({ providerId });
266
+ if (!resPopup.success || !resPopup.code) {
267
+ this.setStatus(resPopup.message || `${providerId.toUpperCase()} sign-in cancelled.`, 'error');
268
+ return;
269
+ }
270
+
271
+ this.setStatus(`Exchanging ${providerId.toUpperCase()} authorization code...`, 'info');
272
+ const apiBridge = getApiBridge();
273
+ const resLogin = await apiBridge.postSsoLogin({
274
+ code: resPopup.code,
275
+ providerId,
276
+ redirectUri: resPopup.redirectUri!
277
+ });
278
+
279
+ if (!resLogin.success || !resLogin.primaryTjpAddr || !resLogin.custodianReqs) {
280
+ this.setStatus(resLogin.message || `${providerId.toUpperCase()} login failed.`, 'error');
281
+ return;
282
+ }
283
+
284
+ const { primaryTjpAddr, custodianReqs } = resLogin;
285
+ this.setStatus('Deriving local device delegate keys...', 'info');
286
+ const delegateSecret = await deriveLocalCustodianDelegateSecret({ primaryTjpAddr });
287
+ const email = resLogin.userInfo?.email || '';
288
+ const username = email ? email.split('@')[0] : 'user';
289
+
290
+ const delegateBuilder = KeystoneProfileBuilder.buildKeystone('sync')
291
+ .withUsername(`${username}-sync`)
292
+ .withDescription(`Sync Delegate for ${username}`)
293
+ .withDetails({
294
+ client: 'space-gib-web',
295
+ role: 'sync-delegate',
296
+ primaryTjpAddr,
297
+ custodianReqs,
298
+ });
299
+
300
+ const configs = await delegateBuilder.compileConfigs();
301
+ const metaspace = await getGlobalMetaspace_waitIfNeeded();
302
+ const space = await metaspace.getLocalUserSpace({ lock: false });
303
+ if (!space) {
304
+ this.setStatus('Could not access local user space.', 'error');
305
+ return;
306
+ }
307
+
308
+ const keystoneService = new KeystoneService_V1();
309
+ const delegateKeystone = await keystoneService.genesis({
310
+ masterSecret: delegateSecret,
311
+ configs,
312
+ metaspace,
313
+ space,
314
+ frameDetails: delegateBuilder.getFrameDetails(),
315
+ isPrimary: false
316
+ });
317
+
318
+ this.setStatus('Registering device delegate with custodian server...', 'info');
319
+ const resRegister = await apiBridge.postCustodianRegisterDelegate({ delegateKeystone });
320
+
321
+ if (!resRegister.success) {
322
+ this.setStatus(resRegister.message || 'Custodian delegate registration failed.', 'error');
323
+ return;
324
+ }
325
+
326
+ // Finalize onboarding: update keystones index, activate identity, and navigate to identity manager
327
+ await this.finalizeIdentityOnboarding({
328
+ primaryKeystone: resRegister.evolvedPrimaryKeystone || resLogin.primaryKeystone,
329
+ primaryGraph: resRegister.primaryGraph || resLogin.primaryGraph,
330
+ delegateKeystone,
331
+ primaryTjpAddr,
332
+ delegateSecret,
333
+ makeActive: true
334
+ });
335
+ }
336
+
337
+ /**
338
+ * Triggered by the "Send 6-Digit Verification Code" button.
339
+ * Validates quick email input and dispatches a 6-digit OTP code to the user's inbox.
340
+ */
341
+ protected async handleQuickSendCode(): Promise<void> {
342
+ const lc = `${this.lc}[${this.handleQuickSendCode.name}]`;
343
+ const btnQuickSendCode = this.shadowRoot?.getElementById('btn-quick-send-code') as HTMLButtonElement | null;
344
+ const inputQuickEmail = this.shadowRoot?.getElementById('input-quick-email') as HTMLInputElement | null;
345
+ const secQuickOtp = this.shadowRoot?.getElementById('section-quick-otp') as HTMLDivElement | null;
346
+
347
+ const email = (inputQuickEmail?.value || '').trim();
348
+ if (!email || !validateEmail(email)) {
349
+ this.setStatus("Please enter a valid email address.", "error");
350
+ return;
351
+ }
352
+
353
+ if (btnQuickSendCode) {
354
+ btnQuickSendCode.disabled = true;
355
+ btnQuickSendCode.textContent = "Sending Verification Code...";
356
+ }
357
+ this.setStatus("Requesting verification code...", "info");
358
+
359
+ try {
360
+ const resCode = await getApiBridge().postVerificationCode({ email });
361
+ if (resCode.success) {
362
+ secQuickOtp?.classList.remove('hidden');
363
+ this.setStatus("✉️ 6-digit verification code sent to your email!", "success");
364
+ } else {
365
+ this.setStatus(resCode.message || "Failed to dispatch verification code.", "error");
366
+ }
367
+ } catch (error) {
368
+ const emsg = extractErrorMsg(error);
369
+ console.error(`${lc} ${emsg}`);
370
+ this.setStatus(`Failed to dispatch verification code: ${emsg}`, "error");
371
+ } finally {
372
+ if (btnQuickSendCode) {
373
+ btnQuickSendCode.disabled = false;
374
+ btnQuickSendCode.textContent = "Send 6-Digit Verification Code";
375
+ }
376
+ }
377
+ }
378
+
379
+ /**
380
+ * Triggered by the "Verify Code" button.
381
+ * Submits 6-digit OTP code to custodian server, builds local sync delegate keystone,
382
+ * registers delegate on server, and finalizes onboarding.
383
+ */
384
+ protected async handleQuickVerifyCode(): Promise<void> {
385
+ const lc = `${this.lc}[${this.handleQuickVerifyCode.name}]`;
386
+ const inputQuickEmail = this.shadowRoot?.getElementById('input-quick-email') as HTMLInputElement | null;
387
+ const inputQuickOtp = this.shadowRoot?.getElementById('input-quick-otp') as HTMLInputElement | null;
388
+ const btnQuickVerifyCode = this.shadowRoot?.getElementById('btn-quick-verify-code') as HTMLButtonElement | null;
389
+
390
+ const email = (inputQuickEmail?.value || '').trim();
391
+ const code = (inputQuickOtp?.value || '').trim();
392
+ if (!code || code.length !== 6 || !/^\d{6}$/.test(code)) {
393
+ this.setStatus("Please enter a valid 6-digit verification code.", "error");
394
+ return;
395
+ }
396
+
397
+ if (btnQuickVerifyCode) {
398
+ btnQuickVerifyCode.disabled = true;
399
+ btnQuickVerifyCode.textContent = "Verifying...";
400
+ }
401
+ this.setStatus("Verifying code & completing onboarding...", "info");
402
+ this.showBusy({
403
+ isBusy: true,
404
+ title: 'Verifying Code...',
405
+ msg: 'Great! Juust a moment...\n(Validating code, promoting server domain space, other security stuff...)',
406
+ animationEmoji: '🔐'
407
+ });
408
+
409
+ try {
410
+ const resGenesis = await getApiBridge().postCustodianGenesis({ email, code });
411
+ if (!resGenesis.success || !resGenesis.primaryTjpAddr || !resGenesis.custodianReqs) {
412
+ this.setStatus(resGenesis.message || "Custodian verification failed.", "error");
413
+ return;
414
+ }
415
+
416
+ const primaryTjpAddr = resGenesis.primaryTjpAddr;
417
+ const custodianReqs = resGenesis.custodianReqs;
418
+
419
+ // Construct local delegate keystone
420
+ this.setStatus("Constructing local sync delegate...", "info");
421
+ this.showBusy({
422
+ isBusy: true,
423
+ title: 'Creating Sync Delegate...',
424
+ msg: 'Generating cryptographic sync delegate keys...',
425
+ animationEmoji: '🔑'
426
+ });
427
+
428
+ const delegateSecret = await deriveLocalCustodianDelegateSecret({ primaryTjpAddr });
429
+ const username = email.split('@')[0];
430
+ const delegateBuilder = KeystoneProfileBuilder.buildKeystone('sync')
431
+ .withUsername(`${username}-sync`)
432
+ .withDescription(`Sync Delegate for ${username}`)
433
+ .withDetails({
434
+ client: 'space-gib-web',
435
+ role: 'sync-delegate',
436
+ primaryTjpAddr,
437
+ custodianReqs,
438
+ });
439
+
440
+ const configs = await delegateBuilder.compileConfigs();
441
+ const metaspace = await getGlobalMetaspace_waitIfNeeded();
442
+ const space = await metaspace.getLocalUserSpace({ lock: false });
443
+ if (!space) {
444
+ this.setStatus("Failed to access local user space.", "error");
445
+ return;
446
+ }
447
+
448
+ const keystoneService = new KeystoneService_V1();
449
+ const delegateKeystone = await keystoneService.genesis({
450
+ masterSecret: delegateSecret,
451
+ configs,
452
+ metaspace,
453
+ space,
454
+ frameDetails: delegateBuilder.getFrameDetails(),
455
+ isPrimary: false
456
+ });
457
+
458
+ // Register delegate with server
459
+ this.setStatus("Registering device delegate with custodian server...", "info");
460
+ this.showBusy({
461
+ isBusy: true,
462
+ title: 'Registering Delegate...',
463
+ msg: 'Registering local delegate with custodian server...',
464
+ animationEmoji: '📝'
465
+ });
466
+
467
+ const resRegister = await getApiBridge().postCustodianRegisterDelegate({ delegateKeystone });
468
+ if (!resRegister.success) {
469
+ this.setStatus(resRegister.message || "Failed to register delegate with custodian server.", "error");
470
+ return;
471
+ }
472
+
473
+ // Finalize onboarding: update keystones index, activate identity, and navigate to identity manager
474
+ await this.finalizeIdentityOnboarding({
475
+ primaryKeystone: resRegister.evolvedPrimaryKeystone || resGenesis.primaryKeystone,
476
+ primaryGraph: resRegister.primaryGraph || resGenesis.primaryGraph,
477
+ delegateKeystone,
478
+ primaryTjpAddr,
479
+ delegateSecret,
480
+ makeActive: true
481
+ });
482
+
483
+ } catch (error) {
484
+ const emsg = extractErrorMsg(error);
485
+ console.error(`${lc} ${emsg}`, error);
486
+ this.setStatus(`Error: ${emsg}`, "error");
487
+ } finally {
488
+ this.showBusy({ isBusy: false });
489
+ if (btnQuickVerifyCode) {
490
+ btnQuickVerifyCode.disabled = false;
491
+ btnQuickVerifyCode.textContent = "Verify Code";
492
+ }
493
+ }
494
+ }
495
+
201
496
  /**
202
497
  * Triggered by the "Generate" button.
203
498
  * Orchestrates: Secret -> Local Genesis -> Graph Collection -> Server Sync.
@@ -385,7 +680,7 @@ export class KeystoneCreatorComponentInstance
385
680
  this.showBusy({
386
681
  isBusy: true,
387
682
  title: 'Syncing to Server...',
388
- msg: 'Quarantining staging genesis keystone on server...',
683
+ msg: 'Staging pending genesis keystone on server...',
389
684
  animationEmoji: '🔄'
390
685
  });
391
686
  const resSync = await getApiBridge().postGenesisKeystone(keystoneIbGib);
@@ -510,69 +805,16 @@ export class KeystoneCreatorComponentInstance
510
805
  }
511
806
  }
512
807
 
513
- // 5. Register with the keystones special index
514
- this.showBusy({
515
- isBusy: true,
516
- title: 'Registering Identity...',
517
- msg: 'Server sync succeeded!\nUpdating local identity indexes and scope settings...',
518
- animationEmoji: '💾'
519
- });
520
- await delay(humanEyeDelay); // for human eyes, not needed for functionality
521
- await updateSpecialIndex({
522
- type: "keystones",
523
- rel8nInfos: [
524
- {
525
- rel8nName: "keystone",
526
- ibGibs: [
527
- toDto({ ibGib: evolvedParentKeystoneIbGib }),
528
- toDto({ ibGib: delegateKeystoneIbGib })
529
- ],
530
- },
531
- ],
532
- metaspace,
533
- space,
808
+ // 5. Finalize onboarding: update keystones index, activate identity, and navigate to identity manager
809
+ await this.finalizeIdentityOnboarding({
810
+ primaryKeystone: evolvedParentKeystoneIbGib,
811
+ delegateKeystone: delegateKeystoneIbGib,
812
+ primaryTjpAddr: evolvedParentAddr,
813
+ masterSecret,
814
+ delegateSecret,
815
+ makeActive: this.elements!.checkboxMakeActive?.checked ?? true
534
816
  });
535
817
 
536
- // Expose for dev tools
537
- (window as any).dev_domainI = evolvedParentKeystoneIbGib;
538
- (window as any).dev_domainIMasterSecret = masterSecret;
539
- (window as any).dev_syncDelegate = delegateKeystoneIbGib;
540
- (window as any).dev_syncDelegateSecret = delegateSecret;
541
-
542
- // 6. Request to make the new identity active if the checkbox is checked
543
- const makeActive = this.elements!.checkboxMakeActive?.checked;
544
- if (makeActive) {
545
- window.dispatchEvent(new CustomEvent(EVENT_IBGIB_IDENTITY_REQUEST_CHANGE, {
546
- detail: { activeIdentityAddr: evolvedParentAddr },
547
- bubbles: true,
548
- composed: true
549
- }));
550
- }
551
-
552
- // 7. Success
553
- const successMsg = "Identity successfully created and activated! Just a sec...";
554
- this.showBusy({
555
- isBusy: true,
556
- title: 'Success!',
557
- msg: successMsg,
558
- animationEmoji: '✅'
559
- });
560
- await delay(humanEyeDelay); // for human eyes, not needed for functionality, because we'll redirect
561
- await delay(humanEyeDelay); // for human eyes, not needed for functionality, because we'll redirect
562
- await delay(humanEyeDelay); // for human eyes, not needed for functionality, because we'll redirect
563
- await delay(humanEyeDelay); // for human eyes, not needed for functionality, because we'll redirect
564
- this.elements!.keystoneAddrEl.textContent = evolvedParentAddr;
565
- this.elements!.statusArea.classList.remove('hidden');
566
- this.setStatus(successMsg, "success");
567
- console.log(`${lc} ✓ ${successMsg} (I: 3396d8261978d7027895b71e27e9ab26)`);
568
-
569
- // Dispatch UI message to close keystone-creator and show identity-manager
570
- window.dispatchEvent(new CustomEvent(EVENT_IBGIB_UI_MESSAGE, {
571
- detail: { action: 'show-identity-manager', addr: evolvedParentAddr },
572
- bubbles: true,
573
- composed: true
574
- }));
575
-
576
818
  } catch (error) {
577
819
  const emsg = extractErrorMsg(error);
578
820
  console.error(`${lc} ${emsg}`, error);
@@ -586,6 +828,138 @@ export class KeystoneCreatorComponentInstance
586
828
  }
587
829
  }
588
830
 
831
+ /**
832
+ * DRY helper to register keystones in the local keystones special index,
833
+ * set the active identity, and navigate to the Identity Manager component.
834
+ */
835
+ private async finalizeIdentityOnboarding({
836
+ primaryKeystone,
837
+ primaryGraph,
838
+ delegateKeystone,
839
+ primaryTjpAddr,
840
+ masterSecret,
841
+ delegateSecret,
842
+ makeActive = true,
843
+ }: {
844
+ primaryKeystone?: IbGib_V1;
845
+ primaryGraph?: FlatIbGibGraph;
846
+ delegateKeystone: IbGib_V1;
847
+ primaryTjpAddr: string;
848
+ masterSecret?: string;
849
+ delegateSecret?: string;
850
+ makeActive?: boolean;
851
+ }): Promise<void> {
852
+ const lc = `${this.lc}[${this.finalizeIdentityOnboarding.name}]`;
853
+ const humanEyeDelay = 500;
854
+
855
+ const metaspace = await getGlobalMetaspace_waitIfNeeded();
856
+ if (!metaspace) {
857
+ throw new Error(`(UNEXPECTED) metaspace falsy? Expected active global metaspace instance. (E: 2e37a8b6fe987449c5d8693a7d4a2827)`);
858
+ }
859
+
860
+ const space = await metaspace.getLocalUserSpace({});
861
+ if (!space) {
862
+ throw new Error(`(UNEXPECTED) space falsy? Expected valid default local user space from metaspace. (E: 1e37a8b6fe987449c5d8693a7d4a2826)`);
863
+ }
864
+
865
+ // 0. Persist all frames of primary identity graph into local browser space
866
+ if (primaryGraph) {
867
+ const graphIbGibs = Object.values(primaryGraph) as IbGib_V1[];
868
+ for (const frame of graphIbGibs) {
869
+ await metaspace.put({ ibGibs: [frame], space });
870
+ await metaspace.registerNewIbGib({ ibGib: frame, space });
871
+ }
872
+ } else if (primaryKeystone) {
873
+ await metaspace.put({ ibGibs: [primaryKeystone], space });
874
+ await metaspace.registerNewIbGib({ ibGib: primaryKeystone, space });
875
+ }
876
+
877
+ // 0.5 Verify primary keystone exists in local browser space via getLatestKeystone
878
+ const keystoneService = new KeystoneService_V1();
879
+ try {
880
+ await keystoneService.getLatestKeystone({
881
+ addr: primaryTjpAddr,
882
+ metaspace,
883
+ space
884
+ });
885
+ } catch (error) {
886
+ const emsg = `Primary identity keystone ${primaryTjpAddr} not found in local browser space! Error: ${extractErrorMsg(error)}`;
887
+ console.error(`${lc} ${emsg} (E: a123456789abcdef0123456789c0001)`);
888
+ throw new Error(emsg);
889
+ }
890
+
891
+ // 1. Register with the local keystones special index
892
+ this.showBusy({
893
+ isBusy: true,
894
+ title: 'Registering Identity...',
895
+ msg: 'Updating local identity indexes and scope settings...',
896
+ animationEmoji: '💾'
897
+ });
898
+ const ibGibsToRegister: IbGib_V1[] = [toDto({ ibGib: delegateKeystone })];
899
+
900
+ if (!primaryKeystone && !primaryGraph) { throw new Error(`(UNEXPECTED) primaryKeystone and primaryGraph both falsy? We should have gotten this back from the server. (E: e34cd882ac66633c75c53318a3b28826)`); }
901
+ if (!delegateKeystone) { throw new Error(`(UNEXPECTED) delegateKeystone falsy? We should have made this locally and even registered it with the server. (E: 56b1c396269e15f008b7e298c71bc826)`); }
902
+
903
+ if (primaryKeystone) {
904
+ ibGibsToRegister.unshift(toDto({ ibGib: primaryKeystone }));
905
+ }
906
+ await updateSpecialIndex({
907
+ type: "keystones",
908
+ rel8nInfos: [
909
+ {
910
+ rel8nName: "keystone",
911
+ ibGibs: ibGibsToRegister,
912
+ },
913
+ ],
914
+ metaspace,
915
+ space,
916
+ });
917
+
918
+ // Expose for dev tools debugging if available
919
+ if (masterSecret) {
920
+ (window as any).dev_domainI = primaryKeystone;
921
+ (window as any).dev_domainIMasterSecret = masterSecret;
922
+ (window as any).dev_syncDelegate = delegateKeystone;
923
+ (window as any).dev_syncDelegateSecret = delegateSecret;
924
+ }
925
+
926
+ // 2. Request to make the new identity active
927
+ if (makeActive) {
928
+ window.dispatchEvent(new CustomEvent(EVENT_IBGIB_IDENTITY_REQUEST_CHANGE, {
929
+ detail: { activeIdentityAddr: primaryTjpAddr },
930
+ bubbles: true,
931
+ composed: true
932
+ }));
933
+ }
934
+
935
+ // 3. Display success status
936
+ const successMsg = "Identity successfully created and activated!";
937
+ this.showBusy({
938
+ isBusy: true,
939
+ title: 'Success!',
940
+ msg: successMsg,
941
+ animationEmoji: '✅'
942
+ });
943
+ await delay(humanEyeDelay);
944
+ if (this.elements?.keystoneAddrEl) {
945
+ this.elements.keystoneAddrEl.textContent = primaryTjpAddr;
946
+ }
947
+ if (this.elements?.statusArea) {
948
+ this.elements.statusArea.classList.remove('hidden');
949
+ }
950
+ this.setStatus(successMsg, "success");
951
+ console.log(`${lc} ✓ ${successMsg}`);
952
+
953
+ // 4. Dispatch UI navigation message to close creator and open identity manager
954
+ window.dispatchEvent(new CustomEvent(EVENT_IBGIB_UI_MESSAGE, {
955
+ detail: { action: 'show-identity-manager', addr: primaryTjpAddr },
956
+ bubbles: true,
957
+ composed: true
958
+ }));
959
+
960
+ this.showBusy({ isBusy: false });
961
+ }
962
+
589
963
  private setFormDisabled(disabled: boolean): void {
590
964
  const root = this.shadowRoot!;
591
965
  const inputIds = [
@@ -22,6 +22,7 @@ import { getKeystoneSyncService } from "../../../../identity/keystone-sync-servi
22
22
  import { updateSpecialIndex } from "@ibgib/core-gib/dist/timeline/timeline-api.mjs";
23
23
  import { getTjpAddr, toDto } from "@ibgib/core-gib/dist/common/other/ibgib-helper.mjs";
24
24
  import { parseSpaceIb } from "@ibgib/core-gib/dist/witness/space/space-helper.mjs";
25
+ import { executeSsoOAuthPopup } from "../../../../identity/sso-popup-helper.mjs";
25
26
 
26
27
  import { getComponentCtorArg } from "../../../../app-bootstrap/init-orchestration.mjs";
27
28
  import { promptForSecret } from "../../../../helpers.web.mjs";
@@ -403,7 +404,7 @@ export class KeystoneDetailsComponentInstance
403
404
 
404
405
  await this.updateFrameDetailsView();
405
406
  } catch (error) {
406
- console.error(`${lc} ${extractErrorMsg(error)}`);
407
+ console.error(`${lc} Registered identity keystone address ${this.ibGibAddr} not found in local browser space: ${extractErrorMsg(error)} (E: a123456789abcdef0123456789c0003)`);
407
408
  throw error;
408
409
  } finally {
409
410
  if (logalot) { console.log(`${lc} complete.`); }
@@ -1039,70 +1040,17 @@ export class KeystoneDetailsComponentInstance
1039
1040
  this.setSSOStatus(`Linking ${providerId}...`, "info");
1040
1041
 
1041
1042
  try {
1042
- // 1. Fetch public SSO configs from server
1043
- const res = await fetch('/api/identity/sso/config');
1044
- if (!res.ok) {
1045
- throw new Error(`Failed to fetch SSO config from server: ${res.statusText}`);
1046
- }
1047
- const data = await res.json();
1048
- if (!data.success || !data.providers) {
1049
- throw new Error(`SSO configuration fetch failed: ${data.message || 'unknown error'}`);
1050
- }
1051
-
1052
- const providerConfig = data.providers.find((p: any) => p.providerId === providerId);
1053
- if (!providerConfig) {
1054
- throw new Error(`SSO provider '${providerId}' is not configured on the server.`);
1055
- }
1056
-
1057
- // 2. Generate a secure, high-entropy user nonce
1058
- const userNonce = (Math.random().toString(36).substring(2) + Math.random().toString(36).substring(2)).substring(0, 32);
1059
-
1060
- // 3. Construct state payload
1061
- const stateObj = {
1043
+ const resPopup = await executeSsoOAuthPopup({
1062
1044
  providerId,
1063
- parentTjpAddr: this.ibGibAddr,
1064
- userNonce
1065
- };
1066
- const state = btoa(JSON.stringify(stateObj));
1067
-
1068
- // 4. Construct Auth URL
1069
- const authUrl = `${providerConfig.authUrl}?client_id=${providerConfig.clientId}&redirect_uri=${encodeURIComponent(providerConfig.redirectUri)}&response_type=code&scope=${encodeURIComponent(providerConfig.scope)}&state=${encodeURIComponent(state)}`;
1070
-
1071
- // 5. Open popup window
1072
- const popup = window.open(
1073
- authUrl,
1074
- 'sso-oauth-popup',
1075
- 'width=600,height=700,status=yes,toolbar=no,menubar=no,location=yes'
1076
- );
1077
- if (!popup) {
1078
- throw new Error("Popup blocked by browser. Please enable popups for this site.");
1079
- }
1045
+ parentTjpAddr: this.ibGibAddr
1046
+ });
1080
1047
 
1081
- // 6. Listen for callback from pop-up
1082
- let checkClosedInterval: any;
1083
- const messageListener = async (event: MessageEvent) => {
1084
- if (event.origin !== window.location.origin) return;
1085
- const callbackData = event.data;
1086
- if (callbackData && callbackData.type === 'sso-oauth-callback') {
1087
- window.removeEventListener('message', messageListener);
1088
- clearInterval(checkClosedInterval);
1089
- await this.handleSSOLinkCallback(callbackData.code, stateObj, providerConfig.redirectUri);
1090
- }
1091
- };
1092
- window.addEventListener('message', messageListener);
1093
-
1094
- // 7. Poll for manual closure of the popup window
1095
- checkClosedInterval = setInterval(() => {
1096
- if (popup.closed) {
1097
- clearInterval(checkClosedInterval);
1098
- window.removeEventListener('message', messageListener);
1099
- const statusEl = this.elements!.ssoStatusMsgEl;
1100
- if (statusEl.textContent && statusEl.textContent.startsWith('Linking')) {
1101
- this.setSSOStatus('Linking cancelled (window closed).', 'error');
1102
- }
1103
- }
1104
- }, 500);
1048
+ if (!resPopup.success || !resPopup.code) {
1049
+ this.setSSOStatus(resPopup.message || `Linking cancelled.`, "error");
1050
+ return;
1051
+ }
1105
1052
 
1053
+ await this.handleSSOLinkCallback(resPopup.code, resPopup.stateObj, resPopup.redirectUri!);
1106
1054
  } catch (error) {
1107
1055
  console.error(`${lc} SSO link initiation failed: ${extractErrorMsg(error)}`);
1108
1056
  this.setSSOStatus(`Link failed: ${extractErrorMsg(error)}`, "error");