@ibgib/web-gib 0.0.59 → 0.0.61

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