@ibgib/web-gib 0.0.61 → 0.0.62

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 (59) 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 +3 -1
  4. package/dist/helpers.web.d.mts.map +1 -1
  5. package/dist/helpers.web.mjs +17 -1
  6. package/dist/helpers.web.mjs.map +1 -1
  7. package/dist/identity/device-delegate-helper.d.mts +48 -0
  8. package/dist/identity/device-delegate-helper.d.mts.map +1 -1
  9. package/dist/identity/device-delegate-helper.mjs +86 -2
  10. package/dist/identity/device-delegate-helper.mjs.map +1 -1
  11. package/dist/identity/device-delegate.respec.mjs +6 -6
  12. package/dist/identity/device-delegate.respec.mjs.map +1 -1
  13. package/dist/identity/passkey-helper.d.mts +72 -0
  14. package/dist/identity/passkey-helper.d.mts.map +1 -0
  15. package/dist/identity/passkey-helper.mjs +237 -0
  16. package/dist/identity/passkey-helper.mjs.map +1 -0
  17. package/dist/identity/passkey.respec.d.mts +2 -0
  18. package/dist/identity/passkey.respec.d.mts.map +1 -0
  19. package/dist/identity/passkey.respec.mjs +169 -0
  20. package/dist/identity/passkey.respec.mjs.map +1 -0
  21. package/dist/index.d.mts +1 -0
  22. package/dist/index.d.mts.map +1 -1
  23. package/dist/index.mjs +1 -0
  24. package/dist/index.mjs.map +1 -1
  25. package/dist/ui/component/identity/add-device/add-device.d.mts.map +1 -1
  26. package/dist/ui/component/identity/add-device/add-device.mjs +1 -0
  27. package/dist/ui/component/identity/add-device/add-device.mjs.map +1 -1
  28. package/dist/ui/component/identity/keystone-creator/keystone-creator.d.mts +18 -1
  29. package/dist/ui/component/identity/keystone-creator/keystone-creator.d.mts.map +1 -1
  30. package/dist/ui/component/identity/keystone-creator/keystone-creator.html +42 -3
  31. package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs +510 -74
  32. package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs.map +1 -1
  33. package/dist/ui/component/identity/keystone-details/keystone-details.d.mts +16 -1
  34. package/dist/ui/component/identity/keystone-details/keystone-details.d.mts.map +1 -1
  35. package/dist/ui/component/identity/keystone-details/keystone-details.html +38 -0
  36. package/dist/ui/component/identity/keystone-details/keystone-details.mjs +946 -122
  37. package/dist/ui/component/identity/keystone-details/keystone-details.mjs.map +1 -1
  38. package/dist/ui/component/identity/remove-device/remove-device.d.mts.map +1 -1
  39. package/dist/ui/component/identity/remove-device/remove-device.mjs +1 -0
  40. package/dist/ui/component/identity/remove-device/remove-device.mjs.map +1 -1
  41. package/dist/ui/ui-helpers.d.mts +3 -0
  42. package/dist/ui/ui-helpers.d.mts.map +1 -1
  43. package/dist/ui/ui-helpers.mjs +110 -6
  44. package/dist/ui/ui-helpers.mjs.map +1 -1
  45. package/package.json +2 -2
  46. package/src/AUTO-GENERATED-version.mts +1 -1
  47. package/src/helpers.web.mts +22 -2
  48. package/src/identity/device-delegate-helper.mts +135 -2
  49. package/src/identity/device-delegate.respec.mts +6 -6
  50. package/src/identity/passkey-helper.mts +304 -0
  51. package/src/identity/passkey.respec.mts +194 -0
  52. package/src/index.mts +1 -0
  53. package/src/ui/component/identity/add-device/add-device.mts +1 -0
  54. package/src/ui/component/identity/keystone-creator/keystone-creator.html +42 -3
  55. package/src/ui/component/identity/keystone-creator/keystone-creator.mts +551 -77
  56. package/src/ui/component/identity/keystone-details/keystone-details.html +38 -0
  57. package/src/ui/component/identity/keystone-details/keystone-details.mts +1011 -131
  58. package/src/ui/component/identity/remove-device/remove-device.mts +1 -0
  59. package/src/ui/ui-helpers.mts +118 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ibgib/web-gib",
3
- "version": "0.0.61",
3
+ "version": "0.0.62",
4
4
  "description": "Framework for creating agentic ibGib web apps. Contains plumbing for ibgib components, agentic framework (currently only Gemini implemented), web-based IndexedDB storage substrate, and more.",
5
5
  "funding": {
6
6
  "type": "individual",
@@ -29,7 +29,7 @@
29
29
  "license": "ISC",
30
30
  "dependencies": {
31
31
  "@google/genai": "^1.33.0",
32
- "@ibgib/core-gib": "^0.1.75",
32
+ "@ibgib/core-gib": "^0.1.76",
33
33
  "@ibgib/encrypt-gib": "^0.3.1",
34
34
  "@ibgib/helper-gib": "^0.0.37",
35
35
  "@ibgib/ts-gib": "^0.5.34"
@@ -11,4 +11,4 @@
11
11
  /**
12
12
  * this is the version of this package, auto-updated in the build process
13
13
  */
14
- export const AUTO_GENERATED_VERSION = '0.0.61';
14
+ export const AUTO_GENERATED_VERSION = '0.0.62';
@@ -424,6 +424,9 @@ export async function promptForText({
424
424
  }
425
425
  }
426
426
 
427
+ import { evaluatePasskeyPrf, isPasskeyPrfSupported } from "./identity/passkey-helper.mjs";
428
+ import { KeystonePasskeyEntry } from "@ibgib/core-gib/dist/keystone/keystone-types.mjs";
429
+
427
430
  /**
428
431
  * not implemented atow (12/2024). I think we'll have perhaps a modal dialog
429
432
  * with a known name and get at it that way.
@@ -431,9 +434,11 @@ export async function promptForText({
431
434
  export async function promptForSecret({
432
435
  msg,
433
436
  confirm,
437
+ passkeys,
434
438
  }: {
435
- msg?: string,
436
- confirm: boolean,
439
+ msg?: string;
440
+ confirm: boolean;
441
+ passkeys?: Record<string, KeystonePasskeyEntry>;
437
442
  }): Promise<string> {
438
443
  const lc = `[${promptForSecret.name}]`;
439
444
  try {
@@ -445,6 +450,19 @@ export async function promptForSecret({
445
450
  let attempts = 0;
446
451
  let maxAttempts = 5;
447
452
 
453
+ const prfSupported = await isPasskeyPrfSupported().catch(() => false);
454
+ const onPasskeyAuth = (prfSupported && passkeys && Object.keys(passkeys).length > 0)
455
+ ? async (passkeyEntry?: any) => {
456
+ if (!passkeyEntry) return undefined;
457
+ const { passkeySecret } = await evaluatePasskeyPrf({
458
+ credentialId: passkeyEntry.credentialId,
459
+ salt: passkeyEntry.salt,
460
+ transports: passkeyEntry.transports,
461
+ });
462
+ return passkeySecret;
463
+ }
464
+ : undefined;
465
+
448
466
  do {
449
467
  attempts++;
450
468
  if (attempts >= maxAttempts) {
@@ -458,6 +476,8 @@ export async function promptForSecret({
458
476
  showInput: true,
459
477
  isPassword: true,
460
478
  defaultValue: undefined,
479
+ passkeys,
480
+ onPasskeyAuth,
461
481
  });
462
482
  if (result === undefined) { throw new Error(`user cancelled (E: 91519d04378e84106f3df8eb36d99725)`); }
463
483
 
@@ -7,11 +7,14 @@
7
7
  * Applicable symmetrically to Sovereign, Custodial, and Hybrid identities.
8
8
  */
9
9
 
10
- import { extractErrorMsg } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs";
11
- import { deriveDeviceDelegateSecret } from "@ibgib/core-gib/dist/keystone/delegate/delegate-helpers.mjs";
10
+ import { extractErrorMsg, getSaferSubstring } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs";
11
+ import { deriveDeviceDelegateSecret, deriveDelegateSecret, createSyncDelegateKeystone } from "@ibgib/core-gib/dist/keystone/delegate/delegate-helpers.mjs";
12
+ import { generateDefaultDeviceTag } from "../util/device-helper.mjs";
12
13
  import { storageGet, storagePut, storageDelete } from "../storage/storage-helpers.web.mjs";
13
14
  import { getGlobalDbName, getGlobalStoreName } from "../helpers.web.mjs";
14
15
 
16
+ export { generateDefaultDeviceTag };
17
+
15
18
  export const DEVICE_DELEGATE_SECRET_PREFIX = 'ibgib_device_delegate_secret_';
16
19
 
17
20
  /**
@@ -133,3 +136,133 @@ export async function clearDeviceDelegateSecret({
133
136
  throw error;
134
137
  }
135
138
  }
139
+
140
+ /**
141
+ * Creates a Sovereign Password-based sync delegate for this device.
142
+ * Standardized format: `pwd-${safeUsername}-${defaultDeviceTag}`
143
+ */
144
+ export async function createSovereignDeviceSyncDelegate({
145
+ username,
146
+ primaryTjpAddr,
147
+ masterSecret,
148
+ metaspace,
149
+ space,
150
+ }: {
151
+ username: string;
152
+ primaryTjpAddr: string;
153
+ masterSecret: string;
154
+ metaspace?: any;
155
+ space?: any;
156
+ }): Promise<{ delegateKeystone: any; deviceTag: string; delegateSecret: string }> {
157
+ const delegateSecret = await deriveDelegateSecret({ masterSecret });
158
+ await setDeviceDelegateSecret({ primaryTjpAddr, delegateSecret });
159
+
160
+ const safeUser = getSaferSubstring({ text: username, length: 8, keepLiterals: ['-'] }) || 'user';
161
+ const defaultDeviceTag = generateDefaultDeviceTag();
162
+ const deviceTag = `pwd-${safeUser}-${defaultDeviceTag}`;
163
+ const sovereignSyncProfileName = process.env.KEYSTONE_PROFILE_SOVEREIGN_SYNC;
164
+ if (!sovereignSyncProfileName || !sovereignSyncProfileName.trim()) {
165
+ throw new Error('KEYSTONE_PROFILE_SOVEREIGN_SYNC environment variable is missing.');
166
+ }
167
+
168
+ const delegateKeystone = await createSyncDelegateKeystone({
169
+ profileName: sovereignSyncProfileName.trim() as any,
170
+ username,
171
+ deviceTag,
172
+ primaryTjpAddr,
173
+ delegateSecret,
174
+ additionalFrameDetails: { client: 'space-gib-web' },
175
+ metaspace,
176
+ space,
177
+ });
178
+
179
+ return { delegateKeystone, deviceTag, delegateSecret };
180
+ }
181
+
182
+ /**
183
+ * Creates an OAuth2 Custodial sync delegate for this device.
184
+ * Standardized format: `oauth2-${providerId}-${defaultDeviceTag}`
185
+ */
186
+ export async function createOAuth2DeviceSyncDelegate({
187
+ providerId,
188
+ username = 'user',
189
+ primaryTjpAddr,
190
+ custodianReqs,
191
+ metaspace,
192
+ space,
193
+ }: {
194
+ providerId: string;
195
+ username?: string;
196
+ primaryTjpAddr: string;
197
+ custodianReqs?: any;
198
+ metaspace?: any;
199
+ space?: any;
200
+ }): Promise<{ delegateKeystone: any; deviceTag: string; delegateSecret: string }> {
201
+ const delegateSecret = await getOrCreateDeviceDelegateSecret({ primaryTjpAddr });
202
+ const defaultDeviceTag = generateDefaultDeviceTag();
203
+ const deviceTag = `oauth2-${providerId}-${defaultDeviceTag}`;
204
+ const custodianSyncProfileName = process.env.KEYSTONE_PROFILE_CUSTODIAN_SYNC;
205
+ if (!custodianSyncProfileName || !custodianSyncProfileName.trim()) {
206
+ throw new Error('KEYSTONE_PROFILE_CUSTODIAN_SYNC environment variable is missing.');
207
+ }
208
+
209
+ const delegateKeystone = await createSyncDelegateKeystone({
210
+ profileName: custodianSyncProfileName.trim() as any,
211
+ username,
212
+ deviceTag,
213
+ primaryTjpAddr,
214
+ delegateSecret,
215
+ custodianReqs,
216
+ additionalFrameDetails: { client: 'space-gib-web' },
217
+ metaspace,
218
+ space,
219
+ });
220
+
221
+ return { delegateKeystone, deviceTag, delegateSecret };
222
+ }
223
+
224
+ /**
225
+ * Creates an Email Custodial sync delegate for this device.
226
+ * Standardized format: `email-${safeUser}@${safeDomain}-${defaultDeviceTag}`
227
+ */
228
+ export async function createEmailDeviceSyncDelegate({
229
+ email,
230
+ primaryTjpAddr,
231
+ custodianReqs,
232
+ metaspace,
233
+ space,
234
+ }: {
235
+ email: string;
236
+ primaryTjpAddr: string;
237
+ custodianReqs?: any;
238
+ metaspace?: any;
239
+ space?: any;
240
+ }): Promise<{ delegateKeystone: any; deviceTag: string; delegateSecret: string }> {
241
+ const delegateSecret = await getOrCreateDeviceDelegateSecret({ primaryTjpAddr });
242
+ const defaultDeviceTag = generateDefaultDeviceTag();
243
+ const [emailUser = '', emailDomain = ''] = (email || '').split('@');
244
+ const safeUser = getSaferSubstring({ text: emailUser, length: 8, keepLiterals: ['-'] }) || 'user';
245
+ const domainPart = emailDomain.split('.')[0] || '';
246
+ const safeDomain = domainPart ? getSaferSubstring({ text: domainPart, length: 8, keepLiterals: ['-'] }) : '';
247
+ const emailTag = safeDomain ? `${safeUser}@${safeDomain}` : safeUser;
248
+ const deviceTag = `email-${emailTag}-${defaultDeviceTag}`;
249
+ const username = safeUser;
250
+ const custodianSyncProfileName = process.env.KEYSTONE_PROFILE_CUSTODIAN_SYNC;
251
+ if (!custodianSyncProfileName || !custodianSyncProfileName.trim()) {
252
+ throw new Error('KEYSTONE_PROFILE_CUSTODIAN_SYNC environment variable is missing.');
253
+ }
254
+
255
+ const delegateKeystone = await createSyncDelegateKeystone({
256
+ profileName: custodianSyncProfileName.trim() as any,
257
+ username,
258
+ deviceTag,
259
+ primaryTjpAddr,
260
+ delegateSecret,
261
+ custodianReqs,
262
+ additionalFrameDetails: { client: 'space-gib-web' },
263
+ metaspace,
264
+ space,
265
+ });
266
+
267
+ return { delegateKeystone, deviceTag, delegateSecret };
268
+ }
@@ -1,4 +1,4 @@
1
- import { respecfully, ifWeMight, iReckon } from '@ibgib/helper-gib/dist/respec-gib/respec-gib.mjs';
1
+ import { respecfully, ifWe, ifWeMight, iReckon } from '@ibgib/helper-gib/dist/respec-gib/respec-gib.mjs';
2
2
  import { getIbGibGlobalThis_IbGib } from '../helpers.mjs';
3
3
  import {
4
4
  getDeviceDelegateSecretKey,
@@ -67,14 +67,14 @@ if (typeof globalThis.indexedDB === 'undefined') {
67
67
 
68
68
  await respecfully(sir, 'Device Delegate Storage & Secret Lifecycle', async () => {
69
69
 
70
- await ifWeMight(sir, 'getDeviceDelegateSecretKey constructs deterministic storage key', async () => {
70
+ await ifWe(sir, 'getDeviceDelegateSecretKey constructs deterministic storage key', async () => {
71
71
  const primaryTjpAddr = 'keystone^alice_tjp_12345';
72
72
  const key = getDeviceDelegateSecretKey({ primaryTjpAddr });
73
73
 
74
74
  iReckon(sir, key).willEqual(`${DEVICE_DELEGATE_SECRET_PREFIX}${primaryTjpAddr}`);
75
75
  });
76
76
 
77
- await ifWeMight(sir, 'setDeviceDelegateSecret and getDeviceDelegateSecret persist and retrieve secret', async () => {
77
+ await ifWe(sir, 'setDeviceDelegateSecret and getDeviceDelegateSecret persist and retrieve secret', async () => {
78
78
  const primaryTjpAddr = 'keystone^bob_tjp_999';
79
79
  const secret = 'super-secret-512-token-abc';
80
80
 
@@ -84,7 +84,7 @@ await respecfully(sir, 'Device Delegate Storage & Secret Lifecycle', async () =>
84
84
  iReckon(sir, retrieved).willEqual(secret);
85
85
  });
86
86
 
87
- await ifWeMight(sir, 'getOrCreateDeviceDelegateSecret generates high entropy token on first call and caches it', async () => {
87
+ await ifWe(sir, 'getOrCreateDeviceDelegateSecret generates high entropy token on first call and caches it', async () => {
88
88
  const primaryTjpAddr = 'keystone^charlie_tjp_888';
89
89
 
90
90
  const token1 = await getOrCreateDeviceDelegateSecret({ primaryTjpAddr, deviceTag: 'laptop' });
@@ -94,7 +94,7 @@ await respecfully(sir, 'Device Delegate Storage & Secret Lifecycle', async () =>
94
94
  iReckon(sir, token1).willEqual(token2);
95
95
  });
96
96
 
97
- await ifWeMight(sir, 'clearDeviceDelegateSecret removes stored secret', async () => {
97
+ await ifWe(sir, 'clearDeviceDelegateSecret removes stored secret', async () => {
98
98
  const primaryTjpAddr = 'keystone^david_tjp_777';
99
99
 
100
100
  await getOrCreateDeviceDelegateSecret({ primaryTjpAddr });
@@ -106,7 +106,7 @@ await respecfully(sir, 'Device Delegate Storage & Secret Lifecycle', async () =>
106
106
  iReckon(sir, afterClear).willEqual(undefined);
107
107
  });
108
108
 
109
- await ifWeMight(sir, 'fails fast when global db or store name is uninitialized', async () => {
109
+ await ifWe(sir, 'fails fast when global db or store name is uninitialized', async () => {
110
110
  const primaryTjpAddr = 'keystone^failfast_tjp_000';
111
111
  const savedDb = globalThisIbGib.dbName_hack;
112
112
  globalThisIbGib.dbName_hack = undefined as any;
@@ -0,0 +1,304 @@
1
+ /**
2
+ * @module identity/passkey-helper
3
+ *
4
+ * WebAuthn Level 3 PRF (Pseudo-Random Function) Passkey helper service.
5
+ * Derives deterministic 256-bit symmetric secrets from hardware biometrics
6
+ * (Touch ID, Windows Hello, Android Biometrics, YubiKey 5Ci) and creates
7
+ * passkey device sync delegates.
8
+ */
9
+
10
+ import { extractErrorMsg, getSaferSubstring } from '@ibgib/helper-gib/dist/helpers/utils-helper.mjs';
11
+ import { deriveDelegateSecret, createSyncDelegateKeystone } from '@ibgib/core-gib/dist/keystone/delegate/delegate-helpers.mjs';
12
+ import { AuthenticatorTransport, KeystonePasskeyEntry } from '@ibgib/core-gib/dist/keystone/keystone-types.mjs';
13
+ import { generateDefaultDeviceTag, setDeviceDelegateSecret } from './device-delegate-helper.mjs';
14
+
15
+ /**
16
+ * Checks if the browser and underlying platform authenticator support WebAuthn PRF.
17
+ */
18
+ export async function isPasskeyPrfSupported(): Promise<boolean> {
19
+ try {
20
+ if (typeof window === 'undefined' || !window.PublicKeyCredential) {
21
+ return false;
22
+ }
23
+ if (typeof (window.PublicKeyCredential as any).getClientCapabilities === 'function') {
24
+ const caps = await (window.PublicKeyCredential as any).getClientCapabilities();
25
+ if (caps && caps['prf']) {
26
+ return true;
27
+ }
28
+ }
29
+ // Fallback: check if PublicKeyCredential exists and platform authenticator is available
30
+ if (typeof PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable === 'function') {
31
+ const isPlatformAvailable = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
32
+ return isPlatformAvailable;
33
+ }
34
+ return false;
35
+ } catch {
36
+ return false;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Helper to convert an ArrayBuffer to a lowercase hex string.
42
+ */
43
+ export function bufferToHex(buffer: ArrayBuffer | ArrayBufferLike): string {
44
+ return Array.from(new Uint8Array(buffer as ArrayBuffer))
45
+ .map(b => b.toString(16).padStart(2, '0'))
46
+ .join('');
47
+ }
48
+
49
+ /**
50
+ * Helper to convert a hex string to a Uint8Array.
51
+ */
52
+ export function hexToUint8Array(hex: string): Uint8Array {
53
+ const cleanHex = hex.replace(/[^0-9a-fA-F]/g, '');
54
+ const bytes = new Uint8Array(Math.floor(cleanHex.length / 2));
55
+ for (let i = 0; i < bytes.length; i++) {
56
+ bytes[i] = parseInt(cleanHex.substr(i * 2, 2), 16);
57
+ }
58
+ return bytes;
59
+ }
60
+
61
+ /**
62
+ * Helper to normalize a salt string or buffer into a 32-byte Uint8Array for PRF evaluation.
63
+ */
64
+ export function normalizeSaltTo32Bytes(salt: string | Uint8Array): Uint8Array {
65
+ if (salt instanceof Uint8Array) {
66
+ if (salt.length === 32) return salt;
67
+ const result = new Uint8Array(32);
68
+ result.set(salt.subarray(0, 32));
69
+ return result;
70
+ }
71
+ // If hex string of length 64 (32 bytes)
72
+ if (/^[0-9a-fA-F]{64}$/.test(salt)) {
73
+ return hexToUint8Array(salt);
74
+ }
75
+ // Text string: encode and pad/slice to 32 bytes
76
+ const encoded = new TextEncoder().encode(salt);
77
+ const result = new Uint8Array(32);
78
+ result.set(encoded.subarray(0, 32));
79
+ return result;
80
+ }
81
+
82
+ /**
83
+ * Creates and registers a new WebAuthn resident passkey credential with PRF enabled.
84
+ */
85
+ /**
86
+ * Creates and registers a new WebAuthn resident passkey credential with PRF enabled.
87
+ */
88
+ export async function registerPasskeyCredential({
89
+ username,
90
+ displayName = username,
91
+ rpName = 'ibgib',
92
+ rpId,
93
+ salt,
94
+ }: {
95
+ username: string;
96
+ displayName?: string;
97
+ rpName?: string;
98
+ rpId?: string;
99
+ salt?: string | Uint8Array;
100
+ }): Promise<{
101
+ credentialId: string;
102
+ rawId: ArrayBuffer;
103
+ transports: AuthenticatorTransport[];
104
+ passkeySecret?: string;
105
+ }> {
106
+ const lc = `[registerPasskeyCredential]`;
107
+ try {
108
+ if (typeof window === 'undefined' || !navigator.credentials?.create) {
109
+ throw new Error('WebAuthn is not supported in this browser environment.');
110
+ }
111
+
112
+ const effectiveRpId = rpId || (typeof window !== 'undefined' ? window.location.hostname : undefined);
113
+ const challenge = crypto.getRandomValues(new Uint8Array(32));
114
+ const userId = new TextEncoder().encode(username.slice(0, 64));
115
+
116
+ const saltBuffer = salt ? normalizeSaltTo32Bytes(salt) : undefined;
117
+ const prfExtension = saltBuffer
118
+ ? { eval: { first: saltBuffer } }
119
+ : {};
120
+
121
+ const credential = await navigator.credentials.create({
122
+ publicKey: {
123
+ challenge,
124
+ rp: {
125
+ name: rpName,
126
+ ...(effectiveRpId ? { id: effectiveRpId } : {})
127
+ },
128
+ user: {
129
+ id: userId,
130
+ name: username,
131
+ displayName
132
+ },
133
+ pubKeyCredParams: [
134
+ { alg: -7, type: 'public-key' }, // ES256 (ECDSA P-256)
135
+ { alg: -257, type: 'public-key' }, // RS256 (RSA)
136
+ { alg: -8, type: 'public-key' }, // Ed25519
137
+ ],
138
+ authenticatorSelection: {
139
+ residentKey: 'required',
140
+ userVerification: 'required'
141
+ },
142
+ extensions: {
143
+ prf: prfExtension
144
+ } as any
145
+ }
146
+ }) as PublicKeyCredential;
147
+
148
+ if (!credential) {
149
+ throw new Error('Passkey creation was cancelled or failed.');
150
+ }
151
+
152
+ const credentialId = bufferToHex(credential.rawId);
153
+ const response: any = credential.response;
154
+ const transports: AuthenticatorTransport[] = (typeof response.getTransports === 'function')
155
+ ? response.getTransports()
156
+ : [];
157
+
158
+ const extResults: any = credential.getClientExtensionResults?.() || {};
159
+ console.log(`${lc} WebAuthn registration clientExtensionResults:`, extResults);
160
+
161
+ let passkeySecret: string | undefined;
162
+ if (extResults.prf?.results?.first) {
163
+ passkeySecret = bufferToHex(extResults.prf.results.first);
164
+ console.log(`${lc} Evaluated PRF directly during create!`);
165
+ }
166
+
167
+ return {
168
+ credentialId,
169
+ rawId: credential.rawId,
170
+ transports,
171
+ passkeySecret
172
+ };
173
+ } catch (error) {
174
+ console.error(`${lc} ${extractErrorMsg(error)}`);
175
+ throw error;
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Evaluates the PRF extension on a passkey assertion to derive a deterministic 256-bit symmetric secret.
181
+ */
182
+ export async function evaluatePasskeyPrf({
183
+ salt,
184
+ credentialId,
185
+ transports,
186
+ rpId,
187
+ }: {
188
+ salt: string | Uint8Array;
189
+ credentialId?: string;
190
+ transports?: AuthenticatorTransport[];
191
+ rpId?: string;
192
+ }): Promise<{
193
+ passkeySecret: string;
194
+ credentialId: string;
195
+ }> {
196
+ const lc = `[evaluatePasskeyPrf]`;
197
+ try {
198
+ if (typeof window === 'undefined' || !navigator.credentials?.get) {
199
+ throw new Error('WebAuthn is not supported in this browser environment.');
200
+ }
201
+
202
+ const effectiveRpId = rpId || (typeof window !== 'undefined' ? window.location.hostname : undefined);
203
+ const challenge = crypto.getRandomValues(new Uint8Array(32));
204
+ const saltBuffer = normalizeSaltTo32Bytes(salt);
205
+
206
+ let allowCredentials: PublicKeyCredentialDescriptor[] | undefined = undefined;
207
+ if (credentialId) {
208
+ const rawBytes = hexToUint8Array(credentialId);
209
+ allowCredentials = [{
210
+ id: rawBytes.buffer as ArrayBuffer,
211
+ type: 'public-key' as const,
212
+ ...(transports && transports.length > 0 ? { transports } : {})
213
+ }];
214
+ }
215
+
216
+ const assertion = await navigator.credentials.get({
217
+ publicKey: {
218
+ challenge,
219
+ ...(effectiveRpId ? { rpId: effectiveRpId } : {}),
220
+ allowCredentials,
221
+ userVerification: 'required',
222
+ extensions: {
223
+ prf: {
224
+ eval: {
225
+ first: saltBuffer
226
+ }
227
+ }
228
+ } as any
229
+ }
230
+ }) as PublicKeyCredential;
231
+
232
+ if (!assertion) {
233
+ throw new Error('Passkey authentication was cancelled or failed.');
234
+ }
235
+
236
+ const resolvedCredentialId = bufferToHex(assertion.rawId);
237
+ const extResults: any = assertion.getClientExtensionResults?.() || {};
238
+ console.log(`${lc} WebAuthn assertion clientExtensionResults:`, extResults);
239
+ const prfResult = extResults.prf?.results?.first;
240
+
241
+ if (!prfResult) {
242
+ console.error(`${lc} Extension results did not contain PRF evaluation:`, extResults);
243
+ throw new Error('Authenticator did not return a PRF evaluation. PRF extension may not be supported on this authenticator.');
244
+ }
245
+
246
+ const passkeySecret = bufferToHex(prfResult);
247
+
248
+ return {
249
+ passkeySecret,
250
+ credentialId: resolvedCredentialId
251
+ };
252
+ } catch (error) {
253
+ console.error(`${lc} ${extractErrorMsg(error)}`);
254
+ throw error;
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Creates a Passkey sovereign sync delegate for this device.
260
+ * Standardized format: `passkey-${safeUsername}-${defaultDeviceTag}`
261
+ */
262
+ export async function createPasskeyDeviceSyncDelegate({
263
+ username,
264
+ primaryTjpAddr,
265
+ passkeySecret,
266
+ transports,
267
+ metaspace,
268
+ space,
269
+ }: {
270
+ username: string;
271
+ primaryTjpAddr: string;
272
+ passkeySecret: string;
273
+ transports?: AuthenticatorTransport[];
274
+ metaspace?: any;
275
+ space?: any;
276
+ }): Promise<{ delegateKeystone: any; deviceTag: string; delegateSecret: string }> {
277
+ const delegateSecret = await deriveDelegateSecret({ masterSecret: passkeySecret });
278
+ await setDeviceDelegateSecret({ primaryTjpAddr, delegateSecret });
279
+
280
+ const safeUser = getSaferSubstring({ text: username, length: 8, keepLiterals: ['-'] }) || 'user';
281
+ const defaultDeviceTag = generateDefaultDeviceTag();
282
+ const deviceTag = `passkey-${safeUser}-${defaultDeviceTag}`;
283
+ const sovereignSyncProfileName = process.env.KEYSTONE_PROFILE_SOVEREIGN_SYNC;
284
+ if (!sovereignSyncProfileName || !sovereignSyncProfileName.trim()) {
285
+ throw new Error('KEYSTONE_PROFILE_SOVEREIGN_SYNC environment variable is missing.');
286
+ }
287
+
288
+ const delegateKeystone = await createSyncDelegateKeystone({
289
+ profileName: sovereignSyncProfileName.trim() as any,
290
+ username,
291
+ deviceTag,
292
+ primaryTjpAddr,
293
+ delegateSecret,
294
+ additionalFrameDetails: {
295
+ client: 'space-gib-web',
296
+ role: 'passkey-sync',
297
+ ...(transports && transports.length > 0 ? { transports } : {})
298
+ },
299
+ metaspace,
300
+ space,
301
+ });
302
+
303
+ return { delegateKeystone, deviceTag, delegateSecret };
304
+ }