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