@ibgib/web-gib 0.0.61 → 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 (66) 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 +20 -3
  4. package/dist/helpers.web.d.mts.map +1 -1
  5. package/dist/helpers.web.mjs +84 -3
  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 +77 -0
  14. package/dist/identity/passkey-helper.d.mts.map +1 -0
  15. package/dist/identity/passkey-helper.mjs +258 -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.css +26 -0
  29. package/dist/ui/component/identity/keystone-creator/keystone-creator.d.mts +18 -1
  30. package/dist/ui/component/identity/keystone-creator/keystone-creator.d.mts.map +1 -1
  31. package/dist/ui/component/identity/keystone-creator/keystone-creator.html +60 -4
  32. package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs +518 -74
  33. package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs.map +1 -1
  34. package/dist/ui/component/identity/keystone-details/keystone-details.d.mts +28 -1
  35. package/dist/ui/component/identity/keystone-details/keystone-details.d.mts.map +1 -1
  36. package/dist/ui/component/identity/keystone-details/keystone-details.html +46 -0
  37. package/dist/ui/component/identity/keystone-details/keystone-details.mjs +1146 -208
  38. package/dist/ui/component/identity/keystone-details/keystone-details.mjs.map +1 -1
  39. package/dist/ui/component/identity/remove-device/remove-device.d.mts.map +1 -1
  40. package/dist/ui/component/identity/remove-device/remove-device.mjs +1 -0
  41. package/dist/ui/component/identity/remove-device/remove-device.mjs.map +1 -1
  42. package/dist/ui/ui-helpers.d.mts +3 -0
  43. package/dist/ui/ui-helpers.d.mts.map +1 -1
  44. package/dist/ui/ui-helpers.mjs +116 -7
  45. package/dist/ui/ui-helpers.mjs.map +1 -1
  46. package/package.json +2 -2
  47. package/src/AUTO-GENERATED-version.mts +1 -1
  48. package/src/helpers.web.mts +117 -7
  49. package/src/identity/device-delegate-helper.mts +135 -2
  50. package/src/identity/device-delegate.respec.mts +6 -6
  51. package/src/identity/passkey-helper.mts +337 -0
  52. package/src/identity/passkey.respec.mts +194 -0
  53. package/src/index.mts +1 -0
  54. package/src/ui/component/identity/add-device/add-device.mts +1 -0
  55. package/src/ui/component/identity/keystone-creator/keystone-creator.css +26 -0
  56. package/src/ui/component/identity/keystone-creator/keystone-creator.html +60 -4
  57. package/src/ui/component/identity/keystone-creator/keystone-creator.mts +561 -77
  58. package/src/ui/component/identity/keystone-details/keystone-details.html +46 -0
  59. package/src/ui/component/identity/keystone-details/keystone-details.mts +1239 -224
  60. package/src/ui/component/identity/remove-device/remove-device.mts +1 -0
  61. package/src/ui/ui-helpers.mts +123 -6
  62. package/tools/auto-generated-agent-skills/skills/ibgib-create-app-server/SKILL.md +50 -0
  63. package/src/identity/custodian-delegate-helper.mts.bak +0 -7
  64. package/src/identity/sso/sso-config-helper.mts.bak +0 -52
  65. package/src/identity/sso/sso-custodian-service.mts.bak +0 -566
  66. package/src/identity/sso/sso-custodian-service.respec.mts.bak +0 -298
@@ -0,0 +1,337 @@
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
+ userVerification = 'preferred',
95
+ authenticatorAttachment,
96
+ hints = ['security-key', 'client-device', 'hybrid'],
97
+ }: {
98
+ username: string;
99
+ displayName?: string;
100
+ rpName?: string;
101
+ rpId?: string;
102
+ salt?: string | Uint8Array;
103
+ userVerification?: UserVerificationRequirement;
104
+ authenticatorAttachment?: AuthenticatorAttachment;
105
+ hints?: ('security-key' | 'client-device' | 'hybrid')[];
106
+ }): Promise<{
107
+ credentialId: string;
108
+ rawId: ArrayBuffer;
109
+ transports: AuthenticatorTransport[];
110
+ passkeySecret?: string;
111
+ }> {
112
+ const lc = `[registerPasskeyCredential]`;
113
+ try {
114
+ if (typeof window === 'undefined' || !navigator.credentials?.create) {
115
+ throw new Error('WebAuthn is not supported in this browser environment.');
116
+ }
117
+
118
+ const effectiveRpId = rpId || (typeof window !== 'undefined' ? window.location.hostname : undefined);
119
+ const challenge = crypto.getRandomValues(new Uint8Array(32));
120
+ const userId = new TextEncoder().encode(username.slice(0, 64));
121
+
122
+ const saltBuffer = salt ? normalizeSaltTo32Bytes(salt) : undefined;
123
+ const prfExtension = saltBuffer
124
+ ? { eval: { first: saltBuffer } }
125
+ : {};
126
+
127
+ const credential = await navigator.credentials.create({
128
+ publicKey: {
129
+ challenge,
130
+ rp: {
131
+ name: rpName,
132
+ ...(effectiveRpId ? { id: effectiveRpId } : {})
133
+ },
134
+ user: {
135
+ id: userId,
136
+ name: username,
137
+ displayName
138
+ },
139
+ pubKeyCredParams: [
140
+ { alg: -7, type: 'public-key' }, // ES256 (ECDSA P-256)
141
+ { alg: -257, type: 'public-key' }, // RS256 (RSA)
142
+ { alg: -8, type: 'public-key' }, // Ed25519
143
+ ],
144
+ authenticatorSelection: {
145
+ residentKey: 'required',
146
+ userVerification,
147
+ ...(authenticatorAttachment ? { authenticatorAttachment } : {})
148
+ },
149
+ ...(hints && hints.length > 0 ? { hints } : {}),
150
+ extensions: {
151
+ prf: prfExtension
152
+ } as any
153
+ }
154
+ }) as PublicKeyCredential;
155
+
156
+ if (!credential) {
157
+ throw new Error('Passkey creation was cancelled or failed.');
158
+ }
159
+
160
+ const credentialId = bufferToHex(credential.rawId);
161
+ const response: any = credential.response;
162
+ const transports: AuthenticatorTransport[] = (typeof response.getTransports === 'function')
163
+ ? response.getTransports()
164
+ : [];
165
+
166
+ const extResults: any = credential.getClientExtensionResults?.() || {};
167
+ console.log(`${lc} WebAuthn registration clientExtensionResults:`, extResults);
168
+
169
+ let passkeySecret: string | undefined;
170
+ if (extResults.prf?.results?.first) {
171
+ passkeySecret = bufferToHex(extResults.prf.results.first);
172
+ console.log(`${lc} Evaluated PRF directly during create!`);
173
+ }
174
+
175
+ return {
176
+ credentialId,
177
+ rawId: credential.rawId,
178
+ transports,
179
+ passkeySecret
180
+ };
181
+ } catch (error) {
182
+ console.error(`${lc} ${extractErrorMsg(error)}`);
183
+ throw error;
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Evaluates the PRF extension on a passkey assertion to derive a deterministic 256-bit symmetric secret.
189
+ */
190
+ export async function evaluatePasskeyPrf({
191
+ salt,
192
+ credentialId,
193
+ transports,
194
+ rpId,
195
+ userVerification = 'preferred',
196
+ hints = ['security-key', 'client-device', 'hybrid'],
197
+ }: {
198
+ salt: string | Uint8Array;
199
+ credentialId?: string;
200
+ transports?: AuthenticatorTransport[];
201
+ rpId?: string;
202
+ userVerification?: UserVerificationRequirement;
203
+ hints?: ('security-key' | 'client-device' | 'hybrid')[];
204
+ }): Promise<{
205
+ passkeySecret: string;
206
+ credentialId: string;
207
+ }> {
208
+ const lc = `[evaluatePasskeyPrf]`;
209
+ try {
210
+ if (typeof window === 'undefined' || !navigator.credentials?.get) {
211
+ throw new Error('WebAuthn is not supported in this browser environment.');
212
+ }
213
+
214
+ const effectiveRpId = rpId || (typeof window !== 'undefined' ? window.location.hostname : undefined);
215
+ const challenge = crypto.getRandomValues(new Uint8Array(32));
216
+ const saltBuffer = normalizeSaltTo32Bytes(salt);
217
+
218
+ let credentialIdBase64Url: string | undefined = undefined;
219
+ let allowCredentials: PublicKeyCredentialDescriptor[] | undefined = undefined;
220
+ if (credentialId) {
221
+ const rawBytes = hexToUint8Array(credentialId);
222
+ allowCredentials = [{
223
+ id: rawBytes.buffer as ArrayBuffer,
224
+ type: 'public-key' as const,
225
+ ...(transports && transports.length > 0 ? { transports } : {})
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
+ };
245
+ }
246
+
247
+ const assertion = await navigator.credentials.get({
248
+ publicKey: {
249
+ challenge,
250
+ ...(effectiveRpId ? { rpId: effectiveRpId } : {}),
251
+ allowCredentials,
252
+ userVerification,
253
+ ...(hints && hints.length > 0 ? { hints } : {}),
254
+ extensions: {
255
+ prf: prfExtension
256
+ } as any
257
+ }
258
+ }) as PublicKeyCredential;
259
+
260
+ if (!assertion) {
261
+ throw new Error('Passkey authentication was cancelled or failed.');
262
+ }
263
+
264
+ const resolvedCredentialId = bufferToHex(assertion.rawId);
265
+ const extResults: any = assertion.getClientExtensionResults?.() || {};
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
+ }
273
+
274
+ if (!prfResult) {
275
+ console.error(`${lc} Extension results did not contain PRF evaluation:`, JSON.stringify(extResults), extResults);
276
+ throw new Error('Authenticator did not return a PRF evaluation. PRF extension may not be supported on this authenticator.');
277
+ }
278
+
279
+ const passkeySecret = bufferToHex(prfResult);
280
+
281
+ return {
282
+ passkeySecret,
283
+ credentialId: resolvedCredentialId
284
+ };
285
+ } catch (error) {
286
+ console.error(`${lc} ${extractErrorMsg(error)}`);
287
+ throw error;
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Creates a Passkey sovereign sync delegate for this device.
293
+ * Standardized format: `passkey-${safeUsername}-${defaultDeviceTag}`
294
+ */
295
+ export async function createPasskeyDeviceSyncDelegate({
296
+ username,
297
+ primaryTjpAddr,
298
+ passkeySecret,
299
+ transports,
300
+ metaspace,
301
+ space,
302
+ }: {
303
+ username: string;
304
+ primaryTjpAddr: string;
305
+ passkeySecret: string;
306
+ transports?: AuthenticatorTransport[];
307
+ metaspace?: any;
308
+ space?: any;
309
+ }): Promise<{ delegateKeystone: any; deviceTag: string; delegateSecret: string }> {
310
+ const delegateSecret = await deriveDelegateSecret({ masterSecret: passkeySecret });
311
+ await setDeviceDelegateSecret({ primaryTjpAddr, delegateSecret });
312
+
313
+ const safeUser = getSaferSubstring({ text: username, length: 8, keepLiterals: ['-'] }) || 'user';
314
+ const defaultDeviceTag = generateDefaultDeviceTag();
315
+ const deviceTag = `passkey-${safeUser}-${defaultDeviceTag}`;
316
+ const sovereignSyncProfileName = process.env.KEYSTONE_PROFILE_SOVEREIGN_SYNC;
317
+ if (!sovereignSyncProfileName || !sovereignSyncProfileName.trim()) {
318
+ throw new Error('KEYSTONE_PROFILE_SOVEREIGN_SYNC environment variable is missing.');
319
+ }
320
+
321
+ const delegateKeystone = await createSyncDelegateKeystone({
322
+ profileName: sovereignSyncProfileName.trim() as any,
323
+ username,
324
+ deviceTag,
325
+ primaryTjpAddr,
326
+ delegateSecret,
327
+ additionalFrameDetails: {
328
+ client: 'space-gib-web',
329
+ role: 'passkey-sync',
330
+ ...(transports && transports.length > 0 ? { transports } : {})
331
+ },
332
+ metaspace,
333
+ space,
334
+ });
335
+
336
+ return { delegateKeystone, deviceTag, delegateSecret };
337
+ }
@@ -0,0 +1,194 @@
1
+ import { respecfully, ifWe, ifWeMight, iReckon } from '@ibgib/helper-gib/dist/respec-gib/respec-gib.mjs';
2
+ import { getIbGibGlobalThis_IbGib } from '../helpers.mjs';
3
+ import {
4
+ bufferToHex,
5
+ hexToUint8Array,
6
+ normalizeSaltTo32Bytes,
7
+ isPasskeyPrfSupported,
8
+ registerPasskeyCredential,
9
+ evaluatePasskeyPrf,
10
+ createPasskeyDeviceSyncDelegate
11
+ } from './passkey-helper.mjs';
12
+ import { getDeviceDelegateSecret } from './device-delegate-helper.mjs';
13
+
14
+ const sir = `[${import.meta.url}]`;
15
+
16
+ // Initialize global db & store names for tests
17
+ const globalThisIbGib = getIbGibGlobalThis_IbGib();
18
+ globalThisIbGib.dbName_hack = 'space_gib_db';
19
+ globalThisIbGib.storeName_hack = 'space_gib_store';
20
+
21
+ // Mock storage substrate for Node test environment
22
+ const inMemoryStore = new Map<string, string>();
23
+
24
+ if (typeof globalThis.indexedDB === 'undefined') {
25
+ (globalThis as any).indexedDB = {
26
+ open: (dbName: string) => {
27
+ const req: any = {};
28
+ setTimeout(() => {
29
+ const db = {
30
+ objectStoreNames: { contains: () => true },
31
+ transaction: (storeName: string) => ({
32
+ objectStore: (sName: string) => ({
33
+ get: (key: string) => {
34
+ const getReq: any = {};
35
+ setTimeout(() => {
36
+ const val = inMemoryStore.get(key);
37
+ getReq.result = val !== undefined ? { value: val } : undefined;
38
+ if (getReq.onsuccess) getReq.onsuccess({ target: getReq });
39
+ }, 0);
40
+ return getReq;
41
+ },
42
+ put: (datum: any) => {
43
+ const putReq: any = {};
44
+ setTimeout(() => {
45
+ inMemoryStore.set(datum.key, datum.value);
46
+ if (putReq.onsuccess) putReq.onsuccess({ target: putReq });
47
+ }, 0);
48
+ return putReq;
49
+ },
50
+ delete: (key: string) => {
51
+ const delReq: any = {};
52
+ setTimeout(() => {
53
+ inMemoryStore.delete(key);
54
+ if (delReq.onsuccess) delReq.onsuccess({ target: delReq });
55
+ }, 0);
56
+ return delReq;
57
+ }
58
+ })
59
+ }),
60
+ close: () => {}
61
+ };
62
+ req.result = db;
63
+ if (req.onsuccess) req.onsuccess({ target: req });
64
+ }, 0);
65
+ return req;
66
+ }
67
+ };
68
+ }
69
+
70
+ // Setup environment profile
71
+ process.env.KEYSTONE_PROFILE_SOVEREIGN_SYNC = 'sync-profile.dev';
72
+
73
+ class MockIbGibSpace {
74
+ store = new Map<string, any>();
75
+ ib: string = 'witness space mock-space mock_space_id user';
76
+
77
+ constructor(public name: string = "mock_space") { }
78
+
79
+ async put({ ibGib }: { ibGib: any }): Promise<void> {
80
+ const addr = `${ibGib.ib}^${ibGib.gib}`;
81
+ this.store.set(addr, JSON.parse(JSON.stringify(ibGib)));
82
+ }
83
+
84
+ async get({ addr }: { addr: string }): Promise<any | null> {
85
+ const data = this.store.get(addr);
86
+ return data ? JSON.parse(JSON.stringify(data)) : null;
87
+ }
88
+
89
+ async argy({ argData, ibGibs }: { argData: any; ibGibs?: any[] }): Promise<any> {
90
+ return {
91
+ ib: 'arg^gib',
92
+ gib: 'arg_gib',
93
+ data: argData,
94
+ ibGibs,
95
+ };
96
+ }
97
+
98
+ async witness(arg: any): Promise<any> {
99
+ return { data: { success: true } };
100
+ }
101
+ }
102
+
103
+ class MockMetaspaceService {
104
+ constructor(public space: MockIbGibSpace) { }
105
+
106
+ async getLocalUserSpace({ lock }: { lock?: boolean } = {}): Promise<MockIbGibSpace> {
107
+ return this.space;
108
+ }
109
+
110
+ async put(args: any): Promise<void> {
111
+ const target = args.space || this.space;
112
+ return target.put(args);
113
+ }
114
+
115
+ async registerNewIbGib({ ibGib }: { ibGib: any }): Promise<void> {
116
+ return this.put({ ibGib });
117
+ }
118
+
119
+ async get(args: { addr?: string; addrs?: string[]; space?: any }): Promise<{ success: boolean; ibGibs?: any[] }> {
120
+ const target = args.space || this.space;
121
+ if (args.addr) {
122
+ const x = await target.get({ addr: args.addr });
123
+ return { success: !!x, ibGibs: x ? [x] : [] };
124
+ }
125
+ return { success: true, ibGibs: [] };
126
+ }
127
+ }
128
+
129
+ const mockSpace = new MockIbGibSpace();
130
+ const mockMetaspace = new MockMetaspaceService(mockSpace);
131
+
132
+ await respecfully(sir, 'Passkey WebAuthn PRF Helper Service', async () => {
133
+
134
+ await ifWe(sir, 'bufferToHex and hexToUint8Array roundtrip correctly', async () => {
135
+ const originalBytes = new Uint8Array([0xde, 0xad, 0xbe, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]);
136
+ const hex = bufferToHex(originalBytes.buffer);
137
+ iReckon(sir, hex).willEqual('deadbeef0123456789abcdef');
138
+
139
+ const roundtripBytes = hexToUint8Array(hex);
140
+ iReckon(sir, roundtripBytes.length).willEqual(originalBytes.length);
141
+ for (let i = 0; i < originalBytes.length; i++) {
142
+ iReckon(sir, roundtripBytes[i]).willEqual(originalBytes[i]);
143
+ }
144
+ });
145
+
146
+ await ifWe(sir, 'normalizeSaltTo32Bytes normalizes various salt inputs to 32 bytes', async () => {
147
+ // 1. Hex string of 64 chars (32 bytes)
148
+ const hex64 = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
149
+ const normHex = normalizeSaltTo32Bytes(hex64);
150
+ iReckon(sir, normHex.length).willEqual(32);
151
+ iReckon(sir, bufferToHex(normHex.buffer)).willEqual(hex64);
152
+
153
+ // 2. Text string
154
+ const textSalt = 'ibgib_passkey_salt_domain_123';
155
+ const normText = normalizeSaltTo32Bytes(textSalt);
156
+ iReckon(sir, normText.length).willEqual(32);
157
+
158
+ // 3. Uint8Array
159
+ const rawBytes = new Uint8Array(32);
160
+ rawBytes.fill(0x42);
161
+ const normRaw = normalizeSaltTo32Bytes(rawBytes);
162
+ iReckon(sir, normRaw.length).willEqual(32);
163
+ iReckon(sir, normRaw[0]).willEqual(0x42);
164
+ });
165
+
166
+ await ifWe(sir, 'isPasskeyPrfSupported evaluates capabilities cleanly', async () => {
167
+ // In Node environment without window
168
+ const supported = await isPasskeyPrfSupported();
169
+ iReckon(sir, typeof supported).willEqual('boolean');
170
+ });
171
+
172
+ await ifWe(sir, 'createPasskeyDeviceSyncDelegate creates standard passkey sync delegate', async () => {
173
+ const username = 'alice';
174
+ const primaryTjpAddr = 'keystone^alice_primary_tjp.addr';
175
+ const passkeySecret = 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef';
176
+
177
+ const { delegateKeystone, deviceTag, delegateSecret } = await createPasskeyDeviceSyncDelegate({
178
+ username,
179
+ primaryTjpAddr,
180
+ passkeySecret,
181
+ transports: ['internal'],
182
+ metaspace: mockMetaspace as any,
183
+ space: mockSpace as any,
184
+ });
185
+
186
+ iReckon(sir, delegateKeystone).asTo('delegateKeystone is generated').isGonnaBeTruthy();
187
+ iReckon(sir, deviceTag.startsWith('passkey-alice-')).asTo('deviceTag follows passkey naming format').isGonnaBeTrue();
188
+ iReckon(sir, delegateSecret).asTo('delegateSecret is derived').isGonnaBeTruthy();
189
+
190
+ // Verify stored in IndexedDB
191
+ const storedSecret = await getDeviceDelegateSecret({ primaryTjpAddr });
192
+ iReckon(sir, storedSecret).willEqual(delegateSecret);
193
+ });
194
+ });
package/src/index.mts CHANGED
@@ -20,6 +20,7 @@ export * from "./api/ibgib-api-bridge.mjs";
20
20
  export * from "./identity/keystone-sync-service.mjs";
21
21
  export * from "./identity/device-delegate-helper.mjs";
22
22
  export * from "./identity/sso-popup-helper.mjs";
23
+ export * from "./identity/passkey-helper.mjs";
23
24
 
24
25
  // Identity UI components
25
26
  export * from "./ui/component/identity/index.mjs";
@@ -615,6 +615,7 @@ export class AddDeviceComponentInstance
615
615
  parentKeystone: primaryKeystone,
616
616
  delegateKeystone,
617
617
  masterSecret,
618
+ signingPoolId: 'manage',
618
619
  allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
619
620
  metaspace,
620
621
  space,
@@ -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
  }