@ibgib/web-gib 0.0.62 → 0.0.63

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 (38) 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.web.d.mts +17 -2
  4. package/dist/helpers.web.d.mts.map +1 -1
  5. package/dist/helpers.web.mjs +67 -2
  6. package/dist/helpers.web.mjs.map +1 -1
  7. package/dist/identity/passkey-helper.d.mts +7 -2
  8. package/dist/identity/passkey-helper.d.mts.map +1 -1
  9. package/dist/identity/passkey-helper.mjs +33 -12
  10. package/dist/identity/passkey-helper.mjs.map +1 -1
  11. package/dist/ui/component/identity/keystone-creator/keystone-creator.css +26 -0
  12. package/dist/ui/component/identity/keystone-creator/keystone-creator.d.mts.map +1 -1
  13. package/dist/ui/component/identity/keystone-creator/keystone-creator.html +18 -1
  14. package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs +10 -2
  15. package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs.map +1 -1
  16. package/dist/ui/component/identity/keystone-details/keystone-details.d.mts +12 -0
  17. package/dist/ui/component/identity/keystone-details/keystone-details.d.mts.map +1 -1
  18. package/dist/ui/component/identity/keystone-details/keystone-details.html +8 -0
  19. package/dist/ui/component/identity/keystone-details/keystone-details.mjs +419 -305
  20. package/dist/ui/component/identity/keystone-details/keystone-details.mjs.map +1 -1
  21. package/dist/ui/ui-helpers.d.mts.map +1 -1
  22. package/dist/ui/ui-helpers.mjs +7 -2
  23. package/dist/ui/ui-helpers.mjs.map +1 -1
  24. package/package.json +2 -2
  25. package/src/AUTO-GENERATED-version.mts +1 -1
  26. package/src/helpers.web.mts +95 -5
  27. package/src/identity/passkey-helper.mts +43 -10
  28. package/src/ui/component/identity/keystone-creator/keystone-creator.css +26 -0
  29. package/src/ui/component/identity/keystone-creator/keystone-creator.html +18 -1
  30. package/src/ui/component/identity/keystone-creator/keystone-creator.mts +12 -2
  31. package/src/ui/component/identity/keystone-details/keystone-details.html +8 -0
  32. package/src/ui/component/identity/keystone-details/keystone-details.mts +474 -339
  33. package/src/ui/ui-helpers.mts +6 -2
  34. package/tools/auto-generated-agent-skills/skills/ibgib-create-app-server/SKILL.md +50 -0
  35. package/src/identity/custodian-delegate-helper.mts.bak +0 -7
  36. package/src/identity/sso/sso-config-helper.mts.bak +0 -52
  37. package/src/identity/sso/sso-custodian-service.mts.bak +0 -566
  38. package/src/identity/sso/sso-custodian-service.respec.mts.bak +0 -298
@@ -427,9 +427,13 @@ export async function promptForText({
427
427
  import { evaluatePasskeyPrf, isPasskeyPrfSupported } from "./identity/passkey-helper.mjs";
428
428
  import { KeystonePasskeyEntry } from "@ibgib/core-gib/dist/keystone/keystone-types.mjs";
429
429
 
430
+ export interface AuthoritySecretResult {
431
+ secret: string;
432
+ poolId: string;
433
+ }
434
+
430
435
  /**
431
- * not implemented atow (12/2024). I think we'll have perhaps a modal dialog
432
- * with a known name and get at it that way.
436
+ * Prompts the user for a secret, supporting both master password and passkey/hardware authenticators.
433
437
  */
434
438
  export async function promptForSecret({
435
439
  msg,
@@ -444,7 +448,7 @@ export async function promptForSecret({
444
448
  try {
445
449
  if (logalot) { console.log(`${lc} starting... (I: 795a4ce348ddb9a5119770f4c572ac24)`); }
446
450
 
447
- let result: string | undefined = undefined;
451
+ let result: any = undefined;
448
452
 
449
453
  let firstTry = true;
450
454
  let attempts = 0;
@@ -458,6 +462,7 @@ export async function promptForSecret({
458
462
  credentialId: passkeyEntry.credentialId,
459
463
  salt: passkeyEntry.salt,
460
464
  transports: passkeyEntry.transports,
465
+ userVerification: 'preferred'
461
466
  });
462
467
  return passkeySecret;
463
468
  }
@@ -481,9 +486,13 @@ export async function promptForSecret({
481
486
  });
482
487
  if (result === undefined) { throw new Error(`user cancelled (E: 91519d04378e84106f3df8eb36d99725)`); }
483
488
 
489
+ if (typeof result === 'object' && result?.secret !== undefined) {
490
+ result = result.secret;
491
+ }
492
+
484
493
  if (confirm) {
485
494
  await delay(250);
486
- let result2 = await showFullscreenDialog({
495
+ let result2: any = await showFullscreenDialog({
487
496
  title: 'Shh...',
488
497
  msg: 'Confirm...',
489
498
  prompt: true,
@@ -492,6 +501,9 @@ export async function promptForSecret({
492
501
  defaultValue: undefined,
493
502
  });
494
503
  if (result2 === undefined) { result = undefined; }
504
+ if (typeof result2 === 'object' && result2?.secret !== undefined) {
505
+ result2 = result2.secret;
506
+ }
495
507
  if (result !== result2) {
496
508
  result = undefined;
497
509
  firstTry = false;
@@ -499,7 +511,6 @@ export async function promptForSecret({
499
511
  }
500
512
  }
501
513
 
502
-
503
514
  } while (result === undefined);
504
515
 
505
516
  return result;
@@ -511,6 +522,85 @@ export async function promptForSecret({
511
522
  }
512
523
  }
513
524
 
525
+ /**
526
+ * Unified authority prompt that inspects the candidate pools on a keystone and prompts the user
527
+ * to authorize via either Master Password or a specific Passkey.
528
+ * Returns both the verified secret and the exact signingPoolId to use for keystone operations.
529
+ */
530
+ export async function promptForAuthoritySecret({
531
+ keystone,
532
+ aggregatedDetails,
533
+ title,
534
+ msg,
535
+ verb = 'manage',
536
+ }: {
537
+ keystone: any;
538
+ aggregatedDetails?: any;
539
+ title?: string;
540
+ msg?: string;
541
+ verb?: string;
542
+ }): Promise<AuthoritySecretResult> {
543
+ const lc = `[${promptForAuthoritySecret.name}]`;
544
+ try {
545
+ const passkeysMap = aggregatedDetails?.passkeys
546
+ || (keystone.data as any)?.frameDetails?.passkeys
547
+ || (keystone.data as any)?.passkeys
548
+ || {};
549
+
550
+ const prfSupported = await isPasskeyPrfSupported().catch(() => false);
551
+ const onPasskeyAuth = (prfSupported && passkeysMap && Object.keys(passkeysMap).length > 0)
552
+ ? async (passkeyEntry?: any) => {
553
+ if (!passkeyEntry) return undefined;
554
+ const { passkeySecret } = await evaluatePasskeyPrf({
555
+ credentialId: passkeyEntry.credentialId,
556
+ salt: passkeyEntry.salt,
557
+ transports: passkeyEntry.transports,
558
+ userVerification: 'preferred'
559
+ });
560
+ return passkeySecret;
561
+ }
562
+ : undefined;
563
+
564
+ const candidatePools = (keystone.data?.challengePools || []).filter((p: any) =>
565
+ p.id === 'manage' ||
566
+ p.id.startsWith('passkey-manage-') ||
567
+ (p.config?.allowedVerbs && verb && p.config.allowedVerbs.includes(verb))
568
+ );
569
+
570
+ const hasPasswordPool = candidatePools.some((p: any) => p.id === 'manage');
571
+
572
+ const dialogResult: any = await showFullscreenDialog({
573
+ title: title || 'Authorize Action',
574
+ msg: msg || 'Authorize this action with your master password or passkey:',
575
+ prompt: true,
576
+ showInput: hasPasswordPool,
577
+ isPassword: true,
578
+ defaultValue: undefined,
579
+ passkeys: passkeysMap,
580
+ onPasskeyAuth,
581
+ });
582
+
583
+ if (!dialogResult) {
584
+ throw new Error("Action cancelled by user.");
585
+ }
586
+
587
+ if (typeof dialogResult === 'object' && dialogResult.secret !== undefined) {
588
+ return {
589
+ secret: dialogResult.secret,
590
+ poolId: dialogResult.poolId || 'manage'
591
+ };
592
+ }
593
+
594
+ return {
595
+ secret: String(dialogResult),
596
+ poolId: 'manage'
597
+ };
598
+ } catch (error) {
599
+ console.error(`${lc} ${extractErrorMsg(error)}`);
600
+ throw error;
601
+ }
602
+ }
603
+
514
604
  /**
515
605
  * prompt specifically looking to confirm something. if "cancelled", returns
516
606
  * false.
@@ -91,12 +91,18 @@ export async function registerPasskeyCredential({
91
91
  rpName = 'ibgib',
92
92
  rpId,
93
93
  salt,
94
+ userVerification = 'preferred',
95
+ authenticatorAttachment,
96
+ hints = ['security-key', 'client-device', 'hybrid'],
94
97
  }: {
95
98
  username: string;
96
99
  displayName?: string;
97
100
  rpName?: string;
98
101
  rpId?: string;
99
102
  salt?: string | Uint8Array;
103
+ userVerification?: UserVerificationRequirement;
104
+ authenticatorAttachment?: AuthenticatorAttachment;
105
+ hints?: ('security-key' | 'client-device' | 'hybrid')[];
100
106
  }): Promise<{
101
107
  credentialId: string;
102
108
  rawId: ArrayBuffer;
@@ -137,8 +143,10 @@ export async function registerPasskeyCredential({
137
143
  ],
138
144
  authenticatorSelection: {
139
145
  residentKey: 'required',
140
- userVerification: 'required'
146
+ userVerification,
147
+ ...(authenticatorAttachment ? { authenticatorAttachment } : {})
141
148
  },
149
+ ...(hints && hints.length > 0 ? { hints } : {}),
142
150
  extensions: {
143
151
  prf: prfExtension
144
152
  } as any
@@ -184,11 +192,15 @@ export async function evaluatePasskeyPrf({
184
192
  credentialId,
185
193
  transports,
186
194
  rpId,
195
+ userVerification = 'preferred',
196
+ hints = ['security-key', 'client-device', 'hybrid'],
187
197
  }: {
188
198
  salt: string | Uint8Array;
189
199
  credentialId?: string;
190
200
  transports?: AuthenticatorTransport[];
191
201
  rpId?: string;
202
+ userVerification?: UserVerificationRequirement;
203
+ hints?: ('security-key' | 'client-device' | 'hybrid')[];
192
204
  }): Promise<{
193
205
  passkeySecret: string;
194
206
  credentialId: string;
@@ -203,6 +215,7 @@ export async function evaluatePasskeyPrf({
203
215
  const challenge = crypto.getRandomValues(new Uint8Array(32));
204
216
  const saltBuffer = normalizeSaltTo32Bytes(salt);
205
217
 
218
+ let credentialIdBase64Url: string | undefined = undefined;
206
219
  let allowCredentials: PublicKeyCredentialDescriptor[] | undefined = undefined;
207
220
  if (credentialId) {
208
221
  const rawBytes = hexToUint8Array(credentialId);
@@ -211,6 +224,24 @@ export async function evaluatePasskeyPrf({
211
224
  type: 'public-key' as const,
212
225
  ...(transports && transports.length > 0 ? { transports } : {})
213
226
  }];
227
+ let binary = '';
228
+ for (let i = 0; i < rawBytes.byteLength; i++) {
229
+ binary += String.fromCharCode(rawBytes[i]);
230
+ }
231
+ credentialIdBase64Url = btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
232
+ }
233
+
234
+ const prfExtension: any = {
235
+ eval: {
236
+ first: saltBuffer
237
+ }
238
+ };
239
+ if (credentialIdBase64Url) {
240
+ prfExtension.evalByCredential = {
241
+ [credentialIdBase64Url]: {
242
+ first: saltBuffer
243
+ }
244
+ };
214
245
  }
215
246
 
216
247
  const assertion = await navigator.credentials.get({
@@ -218,13 +249,10 @@ export async function evaluatePasskeyPrf({
218
249
  challenge,
219
250
  ...(effectiveRpId ? { rpId: effectiveRpId } : {}),
220
251
  allowCredentials,
221
- userVerification: 'required',
252
+ userVerification,
253
+ ...(hints && hints.length > 0 ? { hints } : {}),
222
254
  extensions: {
223
- prf: {
224
- eval: {
225
- first: saltBuffer
226
- }
227
- }
255
+ prf: prfExtension
228
256
  } as any
229
257
  }
230
258
  }) as PublicKeyCredential;
@@ -235,11 +263,16 @@ export async function evaluatePasskeyPrf({
235
263
 
236
264
  const resolvedCredentialId = bufferToHex(assertion.rawId);
237
265
  const extResults: any = assertion.getClientExtensionResults?.() || {};
238
- console.log(`${lc} WebAuthn assertion clientExtensionResults:`, extResults);
239
- const prfResult = extResults.prf?.results?.first;
266
+ console.log(`${lc} WebAuthn assertion clientExtensionResults:`, JSON.stringify(extResults), extResults);
267
+
268
+ let prfResult: ArrayBuffer | undefined = extResults.prf?.results?.first;
269
+ if (!prfResult && credentialIdBase64Url && extResults.prf?.resultsByCredential?.[credentialIdBase64Url]?.first) {
270
+ prfResult = extResults.prf.resultsByCredential[credentialIdBase64Url].first;
271
+ console.log(`${lc} Extracted PRF result from resultsByCredential!`);
272
+ }
240
273
 
241
274
  if (!prfResult) {
242
- console.error(`${lc} Extension results did not contain PRF evaluation:`, extResults);
275
+ console.error(`${lc} Extension results did not contain PRF evaluation:`, JSON.stringify(extResults), extResults);
243
276
  throw new Error('Authenticator did not return a PRF evaluation. PRF extension may not be supported on this authenticator.');
244
277
  }
245
278
 
@@ -199,4 +199,30 @@ h2 {
199
199
  input::placeholder {
200
200
  color: rgba(255, 255, 255, 0.35);
201
201
  opacity: 1;
202
+ }
203
+
204
+ .public-directory-notice {
205
+ padding: 0.75rem 1rem;
206
+ border-radius: 8px;
207
+ background: rgba(137, 180, 250, 0.08);
208
+ border: 1px solid rgba(137, 180, 250, 0.25);
209
+ display: flex;
210
+ gap: 0.6rem;
211
+ align-items: flex-start;
212
+ text-align: left;
213
+ }
214
+
215
+ .notice-icon {
216
+ font-size: 1.1rem;
217
+ line-height: 1.2;
218
+ }
219
+
220
+ .notice-body {
221
+ font-size: 0.82rem;
222
+ color: var(--clr-text, #cdd6f4);
223
+ line-height: 1.45;
224
+ }
225
+
226
+ .notice-body strong {
227
+ color: #89b4fa;
202
228
  }
@@ -80,6 +80,15 @@
80
80
  <input type="text" id="input-passkey-name" placeholder="e.g. MacBook Touch ID, YubiKey 5Ci, Android Phone..." class="text-input">
81
81
  </div>
82
82
 
83
+ <div class="input-group">
84
+ <label for="select-passkey-attachment">Authenticator Device Target</label>
85
+ <select id="select-passkey-attachment" class="text-input" style="cursor: pointer;">
86
+ <option value="any" selected>Any / Smart Chooser (All Options, No Forced Windows PIN)</option>
87
+ <option value="cross-platform">Hardware Security Key (USB / NFC YubiKey Directly)</option>
88
+ <option value="platform">Built-in Biometrics (Windows Hello, Touch ID, Fingerprint)</option>
89
+ </select>
90
+ </div>
91
+
83
92
  <div class="checkbox-group" style="margin-top: 0.5rem; margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem;">
84
93
  <input type="checkbox" id="checkbox-passkey-make-active" checked style="cursor: pointer; width: 1rem; height: 1rem;">
85
94
  <label for="checkbox-passkey-make-active" style="cursor: pointer; font-size: 0.9rem; user-select: none;">Make this the active identity</label>
@@ -150,13 +159,21 @@
150
159
  <p class="warning">⚠️ Store this address safely. It is your root identity for this space.</p>
151
160
  </div>
152
161
 
162
+ <!-- Public Directory Notice Callout -->
163
+ <div class="public-directory-notice">
164
+ <span class="notice-icon">ℹ️</span>
165
+ <div class="notice-body">
166
+ <strong>Public Directory Notice:</strong> Your username and registered email address are associated with your public identity keystone and can be queried on the network (similar to a public PGP key or public DNS record).
167
+ </div>
168
+ </div>
169
+
153
170
  <div class="switch-workflow" style="margin-top: 1.2rem; text-align: center; font-size: 0.9rem; color: #a6adc8;">
154
171
  Already have a device paired? <button id="btn-switch-sign-in" class="link-btn" style="background: none; border: none; color: #89b4fa; font-weight: 600; cursor: pointer; text-decoration: underline; padding: 0; font-size: 0.9rem;">Pair New Device</button>
155
172
  </div>
156
173
 
157
174
  <!-- Privacy Footer Note -->
158
175
  <div class="privacy-footer" style="margin-top: 1rem; text-align: center; font-size: 0.78rem; color: #a6adc8; line-height: 1.4;">
159
- ℹ️ Primary email addresses and identity keystones form part of the public graph for account discovery. Business data requires authorized sync delegates. Learn more in our <a id="link-privacy-policy" href="#privacy" style="color: #89b4fa; text-decoration: underline;">Privacy Policy</a>.
176
+ Identity keystones form part of the public graph for account discovery. Learn more in our <a id="link-privacy-policy" href="/privacy" target="_blank" rel="noopener" style="color: #89b4fa; text-decoration: underline;">Privacy Policy</a>.
160
177
  </div>
161
178
  </div>
162
179
  </div>
@@ -696,12 +696,21 @@ export class KeystoneCreatorComponentInstance
696
696
  animationEmoji: '🛡️'
697
697
  });
698
698
 
699
+ const selectAttachment = this.shadowRoot?.querySelector('#select-passkey-attachment') as HTMLSelectElement | null;
700
+ const attachmentVal = selectAttachment?.value;
701
+ const authenticatorAttachment: AuthenticatorAttachment | undefined =
702
+ (attachmentVal === 'cross-platform' || attachmentVal === 'platform')
703
+ ? attachmentVal
704
+ : undefined;
705
+
699
706
  const rawSalt = crypto.getRandomValues(new Uint8Array(32));
700
707
  const saltHex = bufferToHex(rawSalt.buffer as ArrayBuffer);
701
708
  const regResult = await registerPasskeyCredential({
702
709
  username,
703
710
  displayName: username,
704
- salt: saltHex
711
+ salt: saltHex,
712
+ authenticatorAttachment,
713
+ userVerification: 'preferred'
705
714
  });
706
715
  const { credentialId, transports } = regResult;
707
716
 
@@ -710,7 +719,8 @@ export class KeystoneCreatorComponentInstance
710
719
  const prfResult = await evaluatePasskeyPrf({
711
720
  credentialId,
712
721
  salt: saltHex,
713
- transports
722
+ transports,
723
+ userVerification: 'preferred'
714
724
  });
715
725
  passkeySecret = prfResult.passkeySecret;
716
726
  }
@@ -97,6 +97,14 @@
97
97
  </tbody>
98
98
  </table>
99
99
  </div>
100
+ <div class="form-group" style="margin-top: 1rem; max-width: 360px;">
101
+ <label for="select-passkey-attachment" style="font-size: 0.85rem; color: var(--clr-text-secondary, #b3b3b3);">Authenticator Device Target</label>
102
+ <select id="select-passkey-attachment" class="form-control" style="width: 100%; padding: 0.4rem 0.6rem; margin-top: 0.25rem; border-radius: 4px; background: rgba(0,0,0,0.3); color: inherit; border: 1px solid rgba(255,255,255,0.2);">
103
+ <option value="">Any (Default / Auto-Detect)</option>
104
+ <option value="cross-platform">🔑 Hardware Security Key (USB / NFC)</option>
105
+ <option value="platform">💻 Built-in Biometrics (Fingerprint / Touch ID)</option>
106
+ </select>
107
+ </div>
100
108
  <div class="action-row" style="margin-top: 0.5rem;">
101
109
  <button id="btn-add-passkey" class="action-btn">
102
110
  🛡️ Register New Passkey / Security Key