@ibgib/web-gib 0.0.59 → 0.0.60

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 (53) hide show
  1. package/dist/AUTO-GENERATED-version.d.mts +1 -1
  2. package/dist/AUTO-GENERATED-version.mjs +1 -1
  3. package/dist/identity/custodian-delegate-helper.d.mts.map +1 -1
  4. package/dist/identity/custodian-delegate-helper.mjs +20 -3
  5. package/dist/identity/custodian-delegate-helper.mjs.map +1 -1
  6. package/dist/identity/identity-constants.mjs +2 -2
  7. package/dist/identity/identity-constants.mjs.map +1 -1
  8. package/dist/identity/identity-types.d.mts +4 -4
  9. package/dist/identity/identity-types.d.mts.map +1 -1
  10. package/dist/ui/component/identity/add-device/add-device.css +93 -17
  11. package/dist/ui/component/identity/add-device/add-device.d.mts +24 -7
  12. package/dist/ui/component/identity/add-device/add-device.d.mts.map +1 -1
  13. package/dist/ui/component/identity/add-device/add-device.html +97 -23
  14. package/dist/ui/component/identity/add-device/add-device.mjs +490 -186
  15. package/dist/ui/component/identity/add-device/add-device.mjs.map +1 -1
  16. package/dist/ui/component/identity/keystone-creator/keystone-creator.d.mts.map +1 -1
  17. package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs +7 -4
  18. package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs.map +1 -1
  19. package/dist/ui/component/identity/keystone-details/keystone-details.css +1 -1
  20. package/dist/ui/component/identity/keystone-details/keystone-details.d.mts +8 -8
  21. package/dist/ui/component/identity/keystone-details/keystone-details.d.mts.map +1 -1
  22. package/dist/ui/component/identity/keystone-details/keystone-details.html +29 -20
  23. package/dist/ui/component/identity/keystone-details/keystone-details.mjs +51 -51
  24. package/dist/ui/component/identity/keystone-details/keystone-details.mjs.map +1 -1
  25. package/dist/ui/component/identity/remove-device/remove-device.d.mts +1 -1
  26. package/dist/ui/component/identity/remove-device/remove-device.d.mts.map +1 -1
  27. package/dist/ui/component/identity/remove-device/remove-device.html +3 -3
  28. package/dist/ui/component/identity/remove-device/remove-device.mjs +7 -7
  29. package/dist/ui/component/identity/remove-device/remove-device.mjs.map +1 -1
  30. package/dist/util/device-helper.d.mts +30 -0
  31. package/dist/util/device-helper.d.mts.map +1 -0
  32. package/dist/util/device-helper.mjs +90 -0
  33. package/dist/util/device-helper.mjs.map +1 -0
  34. package/dist/util/device-helper.respec.d.mts +2 -0
  35. package/dist/util/device-helper.respec.d.mts.map +1 -0
  36. package/dist/util/device-helper.respec.mjs +83 -0
  37. package/dist/util/device-helper.respec.mjs.map +1 -0
  38. package/package.json +3 -3
  39. package/src/AUTO-GENERATED-version.mts +1 -1
  40. package/src/identity/custodian-delegate-helper.mts +28 -3
  41. package/src/identity/identity-constants.mts +2 -2
  42. package/src/identity/identity-types.mts +4 -4
  43. package/src/ui/component/identity/add-device/add-device.css +93 -17
  44. package/src/ui/component/identity/add-device/add-device.html +97 -23
  45. package/src/ui/component/identity/add-device/add-device.mts +577 -194
  46. package/src/ui/component/identity/keystone-creator/keystone-creator.mts +7 -4
  47. package/src/ui/component/identity/keystone-details/keystone-details.css +1 -1
  48. package/src/ui/component/identity/keystone-details/keystone-details.html +29 -20
  49. package/src/ui/component/identity/keystone-details/keystone-details.mts +57 -57
  50. package/src/ui/component/identity/remove-device/remove-device.html +3 -3
  51. package/src/ui/component/identity/remove-device/remove-device.mts +8 -8
  52. package/src/util/device-helper.mts +104 -0
  53. package/src/util/device-helper.respec.mts +90 -0
@@ -13,6 +13,8 @@ import { KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT, KEYSTONE_USERNAME_REGEXP } f
13
13
  import { deriveDelegateSecret } from "@ibgib/core-gib/dist/keystone/strategy/hash-reveal-v1/hash-reveal-v1.mjs";
14
14
  import { getTimelinesGroupedByTjp, getTjpAddr, toDto } from "@ibgib/core-gib/dist/common/other/ibgib-helper.mjs";
15
15
  import { updateSpecialIndex } from "@ibgib/core-gib/dist/timeline/timeline-api.mjs";
16
+ import { validateEmail } from "@ibgib/core-gib/dist/keystone/keystone-helpers.mjs";
17
+ import { FlatIbGibGraph } from "@ibgib/core-gib/dist/common/other/graph-types.mjs";
16
18
 
17
19
  import { getApiBridge } from "../../../../api/ibgib-api-bridge.mjs";
18
20
  import { getGlobalMetaspace_waitIfNeeded } from "../../../../helpers.mjs";
@@ -25,6 +27,9 @@ import { getComponentCtorArg } from "../../../../app-bootstrap/init-orchestratio
25
27
  import {
26
28
  EVENT_IBGIB_IDENTITY_REQUEST_CHANGE, EVENT_IBGIB_UI_MESSAGE
27
29
  } from "../../../ui-constants.mjs";
30
+ import { generateDefaultDeviceTag } from "../../../../util/device-helper.mjs";
31
+ import { executeSsoOAuthPopup } from "../../../../identity/sso-popup-helper.mjs";
32
+ import { deriveLocalCustodianDelegateSecret } from "../../../../identity/custodian-delegate-helper.mjs";
28
33
 
29
34
  export const ADD_DEVICE_COMPONENT_NAME = 'ibgib-add-device';
30
35
 
@@ -65,11 +70,20 @@ export class AddDeviceComponentMeta extends IbGibDynamicComponentMetaBase {
65
70
  interface AddDeviceElements extends ElementsBase {
66
71
  containerEl: HTMLDivElement;
67
72
  titleEl: HTMLHeadingElement;
73
+ subtitleEl: HTMLParagraphElement;
74
+ surfaceEntryEl: HTMLDivElement;
75
+ btnGoogleSso: HTMLButtonElement;
76
+ btnGithubSso: HTMLButtonElement;
68
77
  inputIdentifier: HTMLInputElement;
78
+ btnContinueEmail: HTMLButtonElement;
79
+ sectionCodeVerification: HTMLDivElement;
80
+ inputCode: HTMLInputElement;
81
+ btnVerifyCode: HTMLButtonElement;
82
+ sectionSovereignPassword: HTMLDivElement;
83
+ inputPassword: HTMLInputElement;
84
+ btnAuthorizePassword: HTMLButtonElement;
69
85
  inputDeviceTag: HTMLInputElement;
70
- inputPassphrase: HTMLInputElement;
71
86
  checkboxMakeActive: HTMLInputElement;
72
- btnAddDeviceEl: HTMLButtonElement;
73
87
  statusArea: HTMLDivElement;
74
88
  statusText: HTMLParagraphElement;
75
89
  resultAreaEl: HTMLDivElement;
@@ -85,6 +99,12 @@ export class AddDeviceComponentInstance
85
99
 
86
100
  protected lc: string = `[AddDeviceComponentInstance]`;
87
101
 
102
+ // Staged state across progressive surfaces
103
+ private _stagedEmail?: string;
104
+ private _stagedPrimaryTjpAddr?: string;
105
+ private _stagedPrimaryKeystone?: KeystoneIbGib_V1;
106
+ private _stagedPrimaryGraph?: FlatIbGibGraph;
107
+
88
108
  constructor() {
89
109
  super();
90
110
  }
@@ -94,21 +114,37 @@ export class AddDeviceComponentInstance
94
114
  }
95
115
 
96
116
  override async created(): Promise<void> {
97
- this.elements = {} as AddDeviceElements;
98
-
99
- this.elements.containerEl = this.shadowRoot!.getElementById('container') as HTMLDivElement;
100
- this.elements.titleEl = this.shadowRoot!.getElementById('title') as HTMLHeadingElement;
101
- this.elements.inputIdentifier = this.shadowRoot!.getElementById('input-identifier') as HTMLInputElement;
102
- this.elements.inputDeviceTag = (this.shadowRoot!.getElementById('input-device-tag') || this.shadowRoot!.getElementById('input-device-name')) as HTMLInputElement;
103
- this.elements.inputPassphrase = this.shadowRoot!.getElementById('input-passphrase') as HTMLInputElement;
104
- this.elements.checkboxMakeActive = this.shadowRoot!.getElementById('checkbox-make-active') as HTMLInputElement;
105
- this.elements.btnAddDeviceEl = this.shadowRoot!.getElementById('btn-add-device') as HTMLButtonElement;
106
- this.elements.statusArea = this.shadowRoot!.getElementById('status-area') as HTMLDivElement;
107
- this.elements.statusText = this.shadowRoot!.getElementById('status-text') as HTMLParagraphElement;
108
- this.elements.resultAreaEl = this.shadowRoot!.getElementById('result-area') as HTMLDivElement;
109
- this.elements.delegateAddrEl = this.shadowRoot!.getElementById('delegate-addr') as HTMLElement;
110
-
111
- this.elements.contentEl = this.elements.containerEl;
117
+ const shadow = this.shadowRoot!;
118
+ const elements: AddDeviceElements = {} as AddDeviceElements;
119
+
120
+ elements.containerEl = shadow.getElementById('container') as HTMLDivElement;
121
+ elements.titleEl = shadow.getElementById('title') as HTMLHeadingElement;
122
+ elements.subtitleEl = shadow.getElementById('subtitle') as HTMLParagraphElement;
123
+ elements.surfaceEntryEl = shadow.getElementById('surface-entry') as HTMLDivElement;
124
+ elements.btnGoogleSso = shadow.getElementById('btn-google-sso') as HTMLButtonElement;
125
+ elements.btnGithubSso = shadow.getElementById('btn-github-sso') as HTMLButtonElement;
126
+ elements.inputIdentifier = shadow.getElementById('input-identifier') as HTMLInputElement;
127
+ elements.btnContinueEmail = shadow.getElementById('btn-continue-email') as HTMLButtonElement;
128
+ elements.sectionCodeVerification = shadow.getElementById('section-code-verification') as HTMLDivElement;
129
+ elements.inputCode = shadow.getElementById('input-code') as HTMLInputElement;
130
+ elements.btnVerifyCode = shadow.getElementById('btn-verify-code') as HTMLButtonElement;
131
+ elements.sectionSovereignPassword = shadow.getElementById('section-sovereign-password') as HTMLDivElement;
132
+ elements.inputPassword = shadow.getElementById('input-password') as HTMLInputElement;
133
+ elements.btnAuthorizePassword = shadow.getElementById('btn-authorize-password') as HTMLButtonElement;
134
+ elements.inputDeviceTag = shadow.getElementById('input-device-tag') as HTMLInputElement;
135
+ elements.checkboxMakeActive = shadow.getElementById('checkbox-make-active') as HTMLInputElement;
136
+ elements.statusArea = shadow.getElementById('status-area') as HTMLDivElement;
137
+ elements.statusText = shadow.getElementById('status-text') as HTMLParagraphElement;
138
+ elements.resultAreaEl = shadow.getElementById('result-area') as HTMLDivElement;
139
+ elements.delegateAddrEl = shadow.getElementById('delegate-addr') as HTMLElement;
140
+ elements.contentEl = elements.containerEl;
141
+
142
+ this.elements = elements;
143
+
144
+ // Auto-generate clean, friendly default device tag
145
+ if (elements.inputDeviceTag && !elements.inputDeviceTag.value) {
146
+ elements.inputDeviceTag.value = generateDefaultDeviceTag();
147
+ }
112
148
 
113
149
  this.initHandlers();
114
150
  await this.renderUI();
@@ -119,7 +155,12 @@ export class AddDeviceComponentInstance
119
155
  }
120
156
 
121
157
  private initHandlers() {
122
- this.elements!.btnAddDeviceEl.addEventListener('click', () => this.handleAddDevice());
158
+ const elements = this.elements!;
159
+ elements.btnGoogleSso?.addEventListener('click', () => this.handleSsoClick('google'));
160
+ elements.btnGithubSso?.addEventListener('click', () => this.handleSsoClick('github'));
161
+ elements.btnContinueEmail?.addEventListener('click', () => this.handleContinueEmail());
162
+ elements.btnVerifyCode?.addEventListener('click', () => this.handleVerifyCode());
163
+ elements.btnAuthorizePassword?.addEventListener('click', () => this.handleAuthorizePassword());
123
164
 
124
165
  this.shadowRoot!.getElementById('btn-switch-create-account')?.addEventListener('click', (e) => {
125
166
  e.preventDefault();
@@ -130,158 +171,432 @@ export class AddDeviceComponentInstance
130
171
  }));
131
172
  });
132
173
 
133
- this.elements!.inputIdentifier.addEventListener('input', () => {
134
- this.elements!.inputIdentifier.classList.remove('invalid');
174
+ elements.inputIdentifier?.addEventListener('input', () => {
175
+ elements.inputIdentifier.classList.remove('invalid');
176
+ });
177
+ elements.inputCode?.addEventListener('input', () => {
178
+ elements.inputCode.classList.remove('invalid');
135
179
  });
136
- this.elements!.inputDeviceTag.addEventListener('input', () => {
137
- this.elements!.inputDeviceTag.classList.remove('invalid');
180
+ elements.inputPassword?.addEventListener('input', () => {
181
+ elements.inputPassword.classList.remove('invalid');
138
182
  });
139
- this.elements!.inputPassphrase.addEventListener('input', () => {
140
- this.elements!.inputPassphrase.classList.remove('invalid');
183
+ elements.inputDeviceTag?.addEventListener('input', () => {
184
+ elements.inputDeviceTag.classList.remove('invalid');
141
185
  });
142
186
  }
143
187
 
144
- private setButtonLoading(isLoading: boolean) {
145
- if (this.elements?.btnAddDeviceEl) {
146
- this.elements.btnAddDeviceEl.disabled = isLoading;
147
- this.elements.btnAddDeviceEl.textContent = isLoading ? 'Pairing...' : 'Pair & Add Device';
188
+ private getDeviceTag(): string {
189
+ const elements = this.elements!;
190
+ const raw = elements.inputDeviceTag?.value?.trim();
191
+ if (raw && KEYSTONE_USERNAME_REGEXP.test(raw)) {
192
+ return raw;
148
193
  }
194
+ const generated = generateDefaultDeviceTag();
195
+ if (elements.inputDeviceTag) {
196
+ elements.inputDeviceTag.value = generated;
197
+ }
198
+ return generated;
149
199
  }
150
200
 
151
- /**
152
- * Executes the Phase 1 Add-Device pairing orchestration flow.
153
- */
154
- protected async handleAddDevice() {
155
- const lc = `${this.lc}[${this.handleAddDevice.name}]`;
201
+ // =========================================================================
202
+ // Flow 1: OAuth2 SSO Sign In (Google / GitHub)
203
+ // =========================================================================
204
+
205
+ protected async handleSsoClick(providerId: 'google' | 'github') {
206
+ const lc = `${this.lc}[${this.handleSsoClick.name}]`;
207
+ const providerName = providerId === 'google' ? 'Google' : 'GitHub';
208
+
209
+ this.setStatus(`Opening ${providerName} sign-in window...`, 'info');
210
+ this.showBusy({
211
+ isBusy: true,
212
+ title: 'Sign In via SSO',
213
+ msg: `Opening ${providerName} authorization window...\nPlease complete sign-in in the popup window.`,
214
+ animationEmoji: '🌐'
215
+ });
216
+
156
217
  try {
157
- const humanEyeDelay = 500;
158
- this.setStatus("Initializing device pairing...", "info");
159
-
160
- const identifier = this.elements!.inputIdentifier.value.trim();
161
- this.elements!.inputIdentifier.value = identifier;
162
- if (!identifier) {
163
- this.elements!.inputIdentifier.classList.add('invalid');
164
- this.setStatus("Please enter a registered email.", "error");
218
+ const resPopup = await executeSsoOAuthPopup({ providerId });
219
+ if (!resPopup.success || !resPopup.code) {
220
+ this.setStatus(resPopup.message || `${providerName} sign-in cancelled.`, 'error');
165
221
  return;
166
222
  }
167
223
 
168
- const deviceTag = this.elements!.inputDeviceTag.value.trim();
169
- this.elements!.inputDeviceTag.value = deviceTag;
170
- if (!deviceTag || !KEYSTONE_USERNAME_REGEXP.test(deviceTag)) {
171
- this.elements!.inputDeviceTag.classList.add('invalid');
172
- this.setStatus("Please enter a valid Device Name (e.g. 'work-laptop').", "error");
224
+ this.setStatus(`Validating ${providerName} authorization...`, 'info');
225
+ this.showBusy({
226
+ isBusy: true,
227
+ title: 'Exchanging SSO Code...',
228
+ msg: `Validating ${providerName} authorization with server...`,
229
+ animationEmoji: '🔐'
230
+ });
231
+
232
+ const apiBridge = getApiBridge();
233
+ const resLogin = await apiBridge.postSsoLogin({
234
+ code: resPopup.code,
235
+ providerId,
236
+ redirectUri: resPopup.redirectUri!
237
+ });
238
+
239
+ if (!resLogin.success || !resLogin.primaryTjpAddr || !resLogin.custodianReqs) {
240
+ this.setStatus(resLogin.message || `${providerName} login failed.`, 'error');
173
241
  return;
174
242
  }
175
243
 
176
- const masterSecret = this.elements!.inputPassphrase.value.trim();
177
- this.elements!.inputPassphrase.value = masterSecret;
178
- if (!masterSecret) {
179
- this.elements!.inputPassphrase.classList.add('invalid');
180
- this.setStatus("Please enter your master passphrase.", "error");
244
+ const { primaryTjpAddr, custodianReqs, primaryKeystone } = resLogin;
245
+ const email = resLogin.userInfo?.email || '';
246
+ const username = email ? email.split('@')[0] : 'user';
247
+
248
+ await this.completeCustodialDelegateOnboarding({
249
+ primaryTjpAddr,
250
+ custodianReqs,
251
+ fallbackPrimaryKeystone: primaryKeystone,
252
+ fallbackPrimaryGraph: resLogin.primaryGraph,
253
+ email,
254
+ username,
255
+ });
256
+
257
+ } catch (error) {
258
+ const emsg = extractErrorMsg(error);
259
+ console.error(`${lc} SSO login error: ${emsg}`, error);
260
+ this.setStatus(`Error: ${emsg}`, 'error');
261
+ } finally {
262
+ this.showBusy({ isBusy: false });
263
+ }
264
+ }
265
+
266
+ // =========================================================================
267
+ // Flow 2: Email Sign In (2FA OTP Gate)
268
+ // =========================================================================
269
+
270
+ protected async handleContinueEmail() {
271
+ const lc = `${this.lc}[${this.handleContinueEmail.name}]`;
272
+ const elements = this.elements!;
273
+ const email = elements.inputIdentifier?.value?.trim();
274
+ if (!email || !validateEmail(email)) {
275
+ elements.inputIdentifier.classList.add('invalid');
276
+ this.setStatus('Please enter a valid email address.', 'error');
277
+ return;
278
+ }
279
+
280
+ elements.btnContinueEmail.disabled = true;
281
+ elements.btnContinueEmail.textContent = 'Checking...';
282
+ this.setStatus('Checking identity on server...', 'info');
283
+ this.showBusy({
284
+ isBusy: true,
285
+ title: 'Locating Identity...',
286
+ msg: 'Locating account on server...',
287
+ animationEmoji: '🔍'
288
+ });
289
+
290
+ try {
291
+ // 1. Look up primary identity keystone for this email
292
+ const resLookup = await getApiBridge().lookupPrimaryKeystone({ identifier: email });
293
+ if (!resLookup.success || !resLookup.tjpAddr) {
294
+ this.setStatus('No account found for this email address. Please click "Create Account" below.', 'error');
181
295
  return;
182
296
  }
183
297
 
184
- this.setButtonLoading(true);
298
+ this._stagedEmail = email;
299
+ this._stagedPrimaryTjpAddr = resLookup.tjpAddr;
300
+
301
+ // 2. Dispatch 6-digit verification code to email
302
+ this.setStatus('Dispatching 6-digit verification code...', 'info');
185
303
  this.showBusy({
186
304
  isBusy: true,
187
- title: 'Locating Identity...',
188
- msg: 'Searching for primary identity...',
189
- animationEmoji: '🔍'
305
+ title: 'Sending Code...',
306
+ msg: 'Dispatching 6-digit verification code to your email...',
307
+ animationEmoji: '✉️'
190
308
  });
191
309
 
192
- const metaspace = await getGlobalMetaspace_waitIfNeeded();
193
- const space = await metaspace.getLocalUserSpace({});
194
- if (!space) { throw new Error("No default space found in metaspace."); }
310
+ const resCode = await getApiBridge().postVerificationCode({
311
+ email,
312
+ primaryTjpAddr: this._stagedPrimaryTjpAddr
313
+ });
314
+ if (!resCode.success) {
315
+ this.setStatus(resCode.message || 'Failed to dispatch verification code.', 'error');
316
+ return;
317
+ }
195
318
 
319
+ // 3. Transition UI to 6-digit PIN input prompt
320
+ elements.sectionCodeVerification.classList.remove('hidden');
321
+ elements.sectionCodeVerification.scrollIntoView({ behavior: 'smooth' });
322
+ this.setStatus(`Verification code sent to ${email}! Enter the 6-digit code above to continue.`, 'info');
323
+ elements.inputCode?.focus();
196
324
 
197
- // 1a. First attempt to locate primary identity keystone locally in space
198
- const keystonesIndex = await metaspace.getSpecialIbGib({
199
- type: "keystones",
200
- space,
201
- initialize: true,
325
+ } catch (error) {
326
+ const emsg = extractErrorMsg(error);
327
+ console.error(`${lc} ${emsg}`, error);
328
+ this.setStatus(`Error: ${emsg}`, 'error');
329
+ } finally {
330
+ elements.btnContinueEmail.disabled = false;
331
+ elements.btnContinueEmail.textContent = 'Continue with Email';
332
+ this.showBusy({ isBusy: false });
333
+ }
334
+ }
335
+
336
+ protected async handleVerifyCode() {
337
+ const lc = `${this.lc}[${this.handleVerifyCode.name}]`;
338
+ const elements = this.elements!;
339
+ const code = elements.inputCode?.value?.trim();
340
+ const email = this._stagedEmail;
341
+ const primaryTjpAddr = this._stagedPrimaryTjpAddr;
342
+
343
+ if (!email || !primaryTjpAddr) {
344
+ this.setStatus('Please enter your email and request a code first.', 'error');
345
+ return;
346
+ }
347
+
348
+ if (!code || code.length !== 6) {
349
+ elements.inputCode.classList.add('invalid');
350
+ this.setStatus('Please enter a 6-digit verification code.', 'error');
351
+ return;
352
+ }
353
+
354
+ elements.btnVerifyCode.disabled = true;
355
+ elements.btnVerifyCode.textContent = 'Verifying...';
356
+ this.setStatus('Verifying 6-digit code...', 'info');
357
+ this.showBusy({
358
+ isBusy: true,
359
+ title: 'Verifying Code...',
360
+ msg: 'Validating verification code with server...',
361
+ animationEmoji: '🔐'
362
+ });
363
+
364
+ try {
365
+ // First, attempt Custodian Genesis / Sign-In resolution
366
+ const resGenesis = await getApiBridge().postCustodianGenesis({ email, code });
367
+
368
+ if (resGenesis.success && resGenesis.primaryTjpAddr && resGenesis.custodianReqs) {
369
+ // -------------------------------------------------------------
370
+ // Path A: Custodial Account (100% Passwordless)
371
+ // -------------------------------------------------------------
372
+ const { primaryTjpAddr, custodianReqs, primaryKeystone, primaryGraph } = resGenesis;
373
+ const username = email ? email.split('@')[0] : 'user';
374
+
375
+ await this.completeCustodialDelegateOnboarding({
376
+ primaryTjpAddr,
377
+ custodianReqs,
378
+ fallbackPrimaryKeystone: primaryKeystone,
379
+ fallbackPrimaryGraph: primaryGraph,
380
+ email,
381
+ username,
382
+ });
383
+ return;
384
+ }
385
+
386
+ // -----------------------------------------------------------------
387
+ // Path B: Sovereign Account (Fetch Verified Graph, Reveal Password Prompt)
388
+ // -----------------------------------------------------------------
389
+ const resVerify = await getApiBridge().postVerifyCode({ email, primaryTjpAddr, code });
390
+ if (!resVerify.success) {
391
+ this.setStatus(resVerify.message || 'Verification failed. Please check the code.', 'error');
392
+ return;
393
+ }
394
+
395
+ this.setStatus('Retrieving identity graph from server...', 'info');
396
+ this.showBusy({
397
+ isBusy: true,
398
+ title: 'Loading Identity Graph...',
399
+ msg: 'Fetching cryptographic identity timeline from server...',
400
+ animationEmoji: '📥'
202
401
  });
203
- const registeredAddrs: string[] = keystonesIndex?.rel8ns?.keystone ?? [];
204
-
205
- let primaryKeystone: KeystoneIbGib_V1 | undefined;
206
- for (const addr of registeredAddrs) {
207
- const res = await metaspace.get({ addrs: [addr], space });
208
- const found = res?.ibGibs?.[0] as KeystoneIbGib_V1 | undefined;
209
- if (found && found.data) {
210
- const username = found.data.frameDetails?.username ?? '';
211
- const emails = found.data.emails ?? found.data.frameDetails?.emails ?? [];
212
- if (username.toLowerCase() === identifier.toLowerCase() || emails.some((e: string) => e.toLowerCase() === identifier.toLowerCase())) {
213
- primaryKeystone = found;
214
- break;
215
- }
216
- }
402
+
403
+ const resGraph = await getApiBridge().getKeystoneGraph(primaryTjpAddr);
404
+ if (!resGraph.success || !resGraph.graph) {
405
+ this.setStatus('Failed to retrieve identity graph from server.', 'error');
406
+ return;
217
407
  }
218
408
 
219
- // 1b. If not found locally, query server identity index
220
- if (!primaryKeystone) {
221
- this.setStatus("Locating identity on server...", "info");
222
- const resLookup = await getApiBridge().lookupPrimaryKeystone({ identifier });
223
- if (resLookup.success && resLookup.primaryKeystoneIbGib) {
224
- const fetchedKeystone = resLookup.primaryKeystoneIbGib as KeystoneIbGib_V1;
225
- const domainAddr = resLookup.tjpAddr || getIbGibAddr({ ibGib: fetchedKeystone });
226
-
227
- // Fetch full domain graph from server so local space has all historical keystone frames
228
- const resGraph = await getApiBridge().getKeystoneGraph(domainAddr);
229
- if (resGraph.success && resGraph.graph) {
230
- const graphIbGibs = Object.values(resGraph.graph);
231
- await metaspace.put({ ibGibs: graphIbGibs, space });
232
-
233
- // Order timelines by TJP and register each frame in ascending order
234
- const timelinesMap = getTimelinesGroupedByTjp({ ibGibs: graphIbGibs });
235
- for (const tjpKey of Object.keys(timelinesMap)) {
236
- const timeline = timelinesMap[tjpKey];
237
- for (const frame of timeline) {
238
- await metaspace.registerNewIbGib({ ibGib: frame, space });
239
- }
240
- }
241
-
242
- // Set primaryKeystone to the latest frame in the domain timeline
243
- const domainTimeline = timelinesMap[domainAddr] || timelinesMap[Object.keys(timelinesMap)[0]];
244
- if (domainTimeline && domainTimeline.length > 0) {
245
- primaryKeystone = domainTimeline[domainTimeline.length - 1] as KeystoneIbGib_V1;
246
- } else {
247
- primaryKeystone = fetchedKeystone;
248
- }
249
- } else {
250
- await metaspace.put({ ibGibs: [fetchedKeystone], space });
251
- await metaspace.registerNewIbGib({ ibGib: fetchedKeystone, space });
252
- primaryKeystone = fetchedKeystone;
409
+ this._stagedPrimaryGraph = resGraph.graph;
410
+ const metaspace = await getGlobalMetaspace_waitIfNeeded();
411
+ const space = await metaspace.getLocalUserSpace({});
412
+ if (space) {
413
+ const graphIbGibs = Object.values(resGraph.graph);
414
+ await metaspace.put({ ibGibs: graphIbGibs, space });
415
+ const timelinesMap = getTimelinesGroupedByTjp({ ibGibs: graphIbGibs });
416
+ for (const tjpKey of Object.keys(timelinesMap)) {
417
+ for (const frame of timelinesMap[tjpKey]) {
418
+ await metaspace.registerNewIbGib({ ibGib: frame, space });
253
419
  }
254
420
  }
421
+ const domainTimeline = timelinesMap[primaryTjpAddr] || timelinesMap[Object.keys(timelinesMap)[0]];
422
+ if (domainTimeline && domainTimeline.length > 0) {
423
+ this._stagedPrimaryKeystone = domainTimeline[domainTimeline.length - 1] as KeystoneIbGib_V1;
424
+ }
255
425
  }
256
426
 
257
- if (!primaryKeystone) {
258
- const msg = `No registered identity found for '${identifier}'. Please check your username/email or create a new account.`;
259
- console.error(`${lc} ${msg}`);
260
- this.setStatus(msg, "error");
261
- this.showBusy({ isBusy: false });
262
- this.setButtonLoading(false);
427
+ // Hide OTP section, reveal Sovereign Master Password prompt
428
+ elements.sectionCodeVerification.classList.add('hidden');
429
+ elements.sectionSovereignPassword.classList.remove('hidden');
430
+ elements.sectionSovereignPassword.scrollIntoView({ behavior: 'smooth' });
431
+ this.setStatus('2FA verified! Please enter your master password to authorize this device.', 'info');
432
+ elements.inputPassword?.focus();
433
+
434
+ } catch (error) {
435
+ const emsg = extractErrorMsg(error);
436
+ console.error(`${lc} ${emsg}`, error);
437
+ this.setStatus(`Error: ${emsg}`, 'error');
438
+ } finally {
439
+ elements.btnVerifyCode.disabled = false;
440
+ elements.btnVerifyCode.textContent = 'Verify Code';
441
+ this.showBusy({ isBusy: false });
442
+ }
443
+ }
444
+
445
+ // =========================================================================
446
+ // DRY Custodial Delegation Helper (Shared by SSO & Email OTP)
447
+ // =========================================================================
448
+
449
+ private async completeCustodialDelegateOnboarding({
450
+ primaryTjpAddr,
451
+ custodianReqs,
452
+ fallbackPrimaryKeystone,
453
+ fallbackPrimaryGraph,
454
+ email,
455
+ username,
456
+ }: {
457
+ primaryTjpAddr: string;
458
+ custodianReqs: any;
459
+ fallbackPrimaryKeystone?: any;
460
+ fallbackPrimaryGraph?: FlatIbGibGraph;
461
+ email: string;
462
+ username?: string;
463
+ }): Promise<void> {
464
+ const lc = `${this.lc}[${this.completeCustodialDelegateOnboarding.name}]`;
465
+ const deviceTag = this.getDeviceTag();
466
+
467
+ this.setStatus('Deriving local device delegate keys...', 'info');
468
+ this.showBusy({
469
+ isBusy: true,
470
+ title: 'Creating Sync Delegate...',
471
+ msg: `Generating cryptographic sync delegate keys for ${deviceTag}...`,
472
+ animationEmoji: '🔑'
473
+ });
474
+
475
+ const delegateSecret = await deriveLocalCustodianDelegateSecret({ primaryTjpAddr });
476
+ const userAlias = username || (email ? email.split('@')[0] : 'user');
477
+ const custodianSyncProfileName = process.env.KEYSTONE_PROFILE_CUSTODIAN_SYNC;
478
+ if (!custodianSyncProfileName || !custodianSyncProfileName.trim()) {
479
+ 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)');
480
+ }
481
+
482
+ const delegateBuilder = KeystoneProfileBuilder.buildKeystone(custodianSyncProfileName.trim() as any)
483
+ .withUsername(`${userAlias}-sync-${deviceTag}`)
484
+ .withDescription(`Sync Delegate for ${userAlias} on ${deviceTag}`)
485
+ .withDetails({
486
+ client: 'space-gib-web',
487
+ role: 'sync-delegate',
488
+ deviceTag,
489
+ primaryTjpAddr,
490
+ custodianReqs,
491
+ });
492
+
493
+ const configs = await delegateBuilder.compileConfigs();
494
+ const metaspace = await getGlobalMetaspace_waitIfNeeded();
495
+ const space = await metaspace.getLocalUserSpace({ lock: false });
496
+ if (!space) {
497
+ this.setStatus('Could not access local user space.', 'error');
498
+ return;
499
+ }
500
+
501
+ const keystoneService = new KeystoneService_V1();
502
+ const delegateKeystone = await keystoneService.genesis({
503
+ masterSecret: delegateSecret,
504
+ configs,
505
+ metaspace,
506
+ space,
507
+ frameDetails: delegateBuilder.getFrameDetails(),
508
+ isPrimary: false
509
+ });
510
+
511
+ this.setStatus('Registering device delegate with custodian server...', 'info');
512
+ this.showBusy({
513
+ isBusy: true,
514
+ title: 'Registering Delegate...',
515
+ msg: 'Registering local delegate with custodian server...',
516
+ animationEmoji: '🔄'
517
+ });
518
+
519
+ const resRegister = await getApiBridge().postCustodianRegisterDelegate({ delegateKeystone });
520
+ if (!resRegister.success) {
521
+ this.setStatus(resRegister.message || 'Custodian delegate registration failed.', 'error');
522
+ return;
523
+ }
524
+
525
+ // Finalize onboarding into local space & special index
526
+ await this.finalizeDeviceOnboarding({
527
+ primaryKeystone: resRegister.evolvedPrimaryKeystone || fallbackPrimaryKeystone,
528
+ primaryGraph: resRegister.primaryGraph || fallbackPrimaryGraph,
529
+ delegateKeystone,
530
+ primaryTjpAddr,
531
+ delegateSecret,
532
+ makeActive: this.elements?.checkboxMakeActive?.checked ?? true
533
+ });
534
+ }
535
+
536
+ // =========================================================================
537
+ // Flow 3: Sovereign Master Password Unlock & Local Evolution
538
+ // =========================================================================
539
+
540
+ protected async handleAuthorizePassword() {
541
+ const lc = `${this.lc}[${this.handleAuthorizePassword.name}]`;
542
+ const elements = this.elements!;
543
+ const masterSecret = elements.inputPassword?.value?.trim();
544
+ const primaryKeystone = this._stagedPrimaryKeystone;
545
+ const primaryTjpAddr = this._stagedPrimaryTjpAddr;
546
+
547
+ if (!masterSecret) {
548
+ elements.inputPassword.classList.add('invalid');
549
+ this.setStatus('Please enter your master password.', 'error');
550
+ return;
551
+ }
552
+
553
+ if (!primaryKeystone || !primaryTjpAddr) {
554
+ this.setStatus('Missing staged primary identity context. Please restart sign-in.', 'error');
555
+ return;
556
+ }
557
+
558
+ elements.btnAuthorizePassword.disabled = true;
559
+ elements.btnAuthorizePassword.textContent = 'Authorizing...';
560
+ this.setStatus('Verifying master password locally...', 'info');
561
+ this.showBusy({
562
+ isBusy: true,
563
+ title: 'Verifying Password...',
564
+ msg: 'Verifying cryptographic password locally in browser...',
565
+ animationEmoji: '🔐'
566
+ });
567
+
568
+ try {
569
+ const keystoneService = new KeystoneService_V1();
570
+ const isValid = await keystoneService.verifySigningSecret({
571
+ keystoneIbGib: primaryKeystone,
572
+ signingSecret: masterSecret,
573
+ poolId: 'manage'
574
+ });
575
+
576
+ if (!isValid) {
577
+ elements.inputPassword.classList.add('invalid');
578
+ this.setStatus('Incorrect master password. Please try again.', 'error');
263
579
  return;
264
580
  }
265
581
 
266
- const primaryUsername = primaryKeystone.data?.frameDetails?.username ?? 'user';
582
+ const deviceTag = this.getDeviceTag();
583
+ const primaryUsername = primaryKeystone.data?.frameDetails?.username || 'user';
267
584
 
268
- // 2. Generate local device delegate keystone
269
- this.setStatus("Generating local device delegate...", "info");
585
+ this.setStatus(`Generating local sync delegate for ${deviceTag}...`, 'info');
270
586
  this.showBusy({
271
587
  isBusy: true,
272
588
  title: 'Generating Keys...',
273
- msg: 'Generating cryptographic delegate keys for this device...',
589
+ msg: `Generating cryptographic delegate keys for ${deviceTag}...`,
274
590
  animationEmoji: '🔑'
275
591
  });
276
592
 
277
- const primaryTjpAddr = getTjpAddr({ ibGib: primaryKeystone, defaultIfNone: 'incomingAddr' });
278
- if (!primaryTjpAddr) { throw new Error(`(UNEXPECTED) primaryTjpAddr falsy? couldn't get tjpAddr from primaryKeystone? (E: bc8cc8678e1ce4194ab02646160fde26)`); }
279
593
  const delegateSecret = await deriveDelegateSecret({ masterSecret });
280
- const syncProfileName = process.env.KEYSTONE_PROFILE_SOVEREIGN_SYNC;
281
- if (!syncProfileName || !syncProfileName.trim()) {
594
+ const sovereignSyncProfileName = process.env.KEYSTONE_PROFILE_SOVEREIGN_SYNC;
595
+ if (!sovereignSyncProfileName || !sovereignSyncProfileName.trim()) {
282
596
  throw new Error('KEYSTONE_PROFILE_SOVEREIGN_SYNC environment variable is missing. Expected valid profile name like "sync-profile.dev" or "sync-profile.prod". (E: 8f9e0a1b2c3d4e5f67890123456789ab)');
283
597
  }
284
- const delegateBuilder = KeystoneProfileBuilder.buildKeystone(syncProfileName.trim() as any)
598
+
599
+ const delegateBuilder = KeystoneProfileBuilder.buildKeystone(sovereignSyncProfileName.trim() as any)
285
600
  .withUsername(`${primaryUsername}-sync-${deviceTag}`)
286
601
  .withDescription(`Sync Delegate for ${primaryUsername} on ${deviceTag}`)
287
602
  .withDetails({
@@ -290,113 +605,181 @@ export class AddDeviceComponentInstance
290
605
  deviceTag,
291
606
  primaryTjpAddr,
292
607
  });
608
+
293
609
  const delegateConfigs = await delegateBuilder.compileConfigs();
610
+ const metaspace = await getGlobalMetaspace_waitIfNeeded();
611
+ const space = await metaspace.getLocalUserSpace({});
612
+ if (!space) { throw new Error('No default space found in metaspace.'); }
294
613
 
295
- const keystoneService = new KeystoneService_V1();
296
614
  const delegateKeystone = await keystoneService.genesis({
297
615
  masterSecret: delegateSecret,
298
616
  configs: delegateConfigs,
299
617
  metaspace,
300
618
  space,
301
- frameDetails: delegateBuilder.getFrameDetails()
619
+ frameDetails: delegateBuilder.getFrameDetails(),
620
+ isPrimary: false
302
621
  });
303
622
 
304
- const delegateAddr = getIbGibAddr({ ibGib: delegateKeystone });
305
- console.log(`${lc} Local device delegate generated: ${delegateAddr}`);
306
- await delay(humanEyeDelay);
307
-
308
- // 3. Evolve parent primary keystone to register delegate
309
- this.setStatus("Registering device on primary identity...", "info");
623
+ this.setStatus('Evolving primary identity to register delegate...', 'info');
310
624
  this.showBusy({
311
625
  isBusy: true,
312
626
  title: 'Registering Device...',
313
- msg: 'Updating primary identity with new device delegate...',
314
- animationEmoji: '📝'
627
+ msg: 'Evolving primary identity to register new device delegate...',
628
+ animationEmoji: '⚙️'
315
629
  });
316
630
 
317
- const evolvedPrimaryKeystone = await keystoneService.registerDelegate({
631
+ const evolvedParentKeystone = await keystoneService.registerDelegate({
318
632
  parentKeystone: primaryKeystone,
319
633
  delegateKeystone,
320
634
  masterSecret,
635
+ allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
321
636
  metaspace,
322
637
  space,
323
- allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
324
- deviceName: deviceTag,
638
+ deviceName: deviceTag
325
639
  });
326
640
 
327
- const evolvedPrimaryAddr = getIbGibAddr({ ibGib: evolvedPrimaryKeystone });
328
- console.log(`${lc} Primary identity evolved with delegate: ${evolvedPrimaryAddr}`);
329
- await delay(humanEyeDelay);
330
-
331
- // 4. Post evolved primary keystone + delegate to server
332
- this.setStatus("Syncing device pairing to server...", "info");
641
+ this.setStatus('Publishing evolved identity to server...', 'info');
333
642
  this.showBusy({
334
643
  isBusy: true,
335
644
  title: 'Syncing to Server...',
336
- msg: 'Posting device pairing to server sync node...',
337
- animationEmoji: '🔄'
645
+ msg: 'Publishing evolved identity and delegate to server...',
646
+ animationEmoji: ''
338
647
  });
339
648
 
340
- // const domainAddr = primaryTjpAddr ? getIbGibAddr({ ibGib: primaryKeystone }) : getIbGibAddr({ ibGib: primaryKeystone });
341
-
342
649
  const resEvolve = await getApiBridge().putEvolveKeystone({
343
650
  domainAddr: primaryTjpAddr,
344
- keystoneIbGib: evolvedPrimaryKeystone,
651
+ keystoneIbGib: evolvedParentKeystone,
345
652
  relatedKeystoneIbGibs: [delegateKeystone]
346
653
  });
347
-
348
654
  if (!resEvolve.success) {
349
- const msg = `Server rejected device pairing: ${resEvolve.message}`;
350
- console.error(`${lc} ${msg}`);
351
- this.setStatus(msg, "error");
352
- this.showBusy({ isBusy: false });
353
- this.setButtonLoading(false);
354
- return;
655
+ console.warn(`${lc} Warning syncing delegate evolution to server: ${resEvolve.message}`);
355
656
  }
356
657
 
357
- // 5. Update local keystones index
358
- this.showBusy({
359
- isBusy: true,
360
- title: 'Updating Local Indexes...',
361
- msg: 'Registering new device in local index...',
362
- animationEmoji: '💾'
658
+ await this.finalizeDeviceOnboarding({
659
+ primaryKeystone: evolvedParentKeystone,
660
+ primaryGraph: this._stagedPrimaryGraph,
661
+ delegateKeystone,
662
+ primaryTjpAddr,
663
+ masterSecret,
664
+ delegateSecret,
665
+ makeActive: elements.checkboxMakeActive?.checked ?? true
363
666
  });
364
667
 
365
- await updateSpecialIndex({
366
- type: "keystones",
367
- rel8nInfos: [
368
- {
369
- rel8nName: "keystone",
370
- ibGibs: [
371
- toDto({ ibGib: evolvedPrimaryKeystone }),
372
- toDto({ ibGib: delegateKeystone })
373
- ],
374
- },
375
- ],
376
- metaspace,
377
- space,
378
- });
668
+ } catch (error) {
669
+ const emsg = extractErrorMsg(error);
670
+ console.error(`${lc} ${emsg}`, error);
671
+ this.setStatus(`Error: ${emsg}`, 'error');
672
+ } finally {
673
+ elements.btnAuthorizePassword.disabled = false;
674
+ elements.btnAuthorizePassword.textContent = 'Authorize & Pair Device';
675
+ this.showBusy({ isBusy: false });
676
+ }
677
+ }
379
678
 
380
- // 6. Set as active identity if checked
381
- if (this.elements!.checkboxMakeActive.checked) {
382
- window.dispatchEvent(new CustomEvent(EVENT_IBGIB_IDENTITY_REQUEST_CHANGE, {
383
- detail: { activeIdentityAddr: evolvedPrimaryAddr },
384
- bubbles: true,
385
- composed: true
386
- }));
679
+ // =========================================================================
680
+ // Finalize Device Onboarding & Navigation
681
+ // =========================================================================
682
+
683
+ private async finalizeDeviceOnboarding({
684
+ primaryKeystone,
685
+ primaryGraph,
686
+ delegateKeystone,
687
+ primaryTjpAddr,
688
+ masterSecret,
689
+ delegateSecret,
690
+ makeActive = true,
691
+ }: {
692
+ primaryKeystone?: any;
693
+ primaryGraph?: FlatIbGibGraph;
694
+ delegateKeystone: any;
695
+ primaryTjpAddr: string;
696
+ masterSecret?: string;
697
+ delegateSecret?: string;
698
+ makeActive?: boolean;
699
+ }): Promise<void> {
700
+ const lc = `${this.lc}[${this.finalizeDeviceOnboarding.name}]`;
701
+ const humanEyeDelay = 500;
702
+
703
+ const metaspace = await getGlobalMetaspace_waitIfNeeded();
704
+ if (!metaspace) { throw new Error('Metaspace not found'); }
705
+ const space = await metaspace.getLocalUserSpace({});
706
+ if (!space) { throw new Error('Local user space not found'); }
707
+
708
+ // 1. Put primary frames and delegate into local space
709
+ if (primaryGraph) {
710
+ const graphIbGibs = Object.values(primaryGraph) as IbGib_V1[];
711
+ await metaspace.put({ ibGibs: graphIbGibs, space });
712
+ const timelinesMap = getTimelinesGroupedByTjp({ ibGibs: graphIbGibs });
713
+ for (const tjpKey of Object.keys(timelinesMap)) {
714
+ for (const frame of timelinesMap[tjpKey]) {
715
+ await metaspace.registerNewIbGib({ ibGib: frame, space });
716
+ }
387
717
  }
718
+ } else if (primaryKeystone) {
719
+ await metaspace.put({ ibGibs: [primaryKeystone], space });
720
+ await metaspace.registerNewIbGib({ ibGib: primaryKeystone, space });
721
+ }
388
722
 
389
- this.showBusy({ isBusy: false });
390
- this.setButtonLoading(false);
391
- this.elements!.delegateAddrEl.innerText = delegateAddr;
392
- this.elements!.resultAreaEl.classList.remove('collapsed');
393
- this.setStatus("Device successfully paired!", "success");
723
+ await metaspace.put({ ibGibs: [delegateKeystone], space });
724
+ await metaspace.registerNewIbGib({ ibGib: delegateKeystone, space });
394
725
 
395
- } catch (error) {
396
- console.error(`${lc} ${extractErrorMsg(error)}`, error);
397
- this.setStatus(`Pairing failed: ${extractErrorMsg(error)}`, "error");
398
- this.showBusy({ isBusy: false });
399
- this.setButtonLoading(false);
726
+ // 2. Register in keystones special index
727
+ const ibGibsToRegister: IbGib_V1[] = [toDto({ ibGib: delegateKeystone })!];
728
+ if (primaryKeystone) {
729
+ ibGibsToRegister.unshift(toDto({ ibGib: primaryKeystone })!);
400
730
  }
731
+
732
+ await updateSpecialIndex({
733
+ type: 'keystones',
734
+ rel8nInfos: [{ rel8nName: 'keystone', ibGibs: ibGibsToRegister }],
735
+ metaspace,
736
+ space
737
+ });
738
+
739
+ // 3. Make active if checked
740
+ if (makeActive) {
741
+ window.dispatchEvent(new CustomEvent(EVENT_IBGIB_IDENTITY_REQUEST_CHANGE, {
742
+ detail: { activeIdentityAddr: primaryTjpAddr },
743
+ bubbles: true,
744
+ composed: true
745
+ }));
746
+ }
747
+
748
+ // 4. Update UI results
749
+ const delegateAddr = getIbGibAddr({ ibGib: delegateKeystone });
750
+ if (this.elements?.delegateAddrEl) {
751
+ this.elements.delegateAddrEl.textContent = delegateAddr;
752
+ }
753
+ if (this.elements?.resultAreaEl) {
754
+ this.elements.resultAreaEl.classList.remove('collapsed');
755
+ }
756
+
757
+ const successMsg = 'Device paired and identity activated successfully!';
758
+ this.setStatus(successMsg, 'success');
759
+ this.showBusy({
760
+ isBusy: true,
761
+ title: 'Success!',
762
+ msg: successMsg,
763
+ animationEmoji: '✅'
764
+ });
765
+
766
+ await delay(humanEyeDelay);
767
+
768
+ // 5. Navigate to identity manager
769
+ window.dispatchEvent(new CustomEvent(EVENT_IBGIB_UI_MESSAGE, {
770
+ detail: { action: 'show-identity-manager', addr: primaryTjpAddr },
771
+ bubbles: true,
772
+ composed: true
773
+ }));
774
+
775
+ this.showBusy({ isBusy: false });
776
+ }
777
+
778
+ protected override async renderUI(): Promise<void> {
779
+ // No complex rendering needed
780
+ }
781
+
782
+ override async handleContextUpdated(): Promise<void> {
783
+ await this.renderUI();
401
784
  }
402
785
  }