@structured-id/ui-core 2.0.0 → 3.0.0

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 (152) hide show
  1. package/README.md +22 -10
  2. package/package.json +25 -43
  3. package/src/components/SidStatusBadge.vue +1 -5
  4. package/src/composables/useAccountTransport.test.ts +2 -1
  5. package/src/composables/useGrpc.test.ts +17 -14
  6. package/src/composables/useGrpc.ts +4 -2
  7. package/src/index.ts +2 -120
  8. package/src/profile/components/ConsentManager.vue +5 -1
  9. package/src/profile/components/ProfileEditor.vue +1 -1
  10. package/src/profile/components/SecuritySettings.vue +5 -1
  11. package/src/profile/components/SessionList.vue +2 -1
  12. package/src/profile/components/SidRegistrationForm.test.ts +173 -1
  13. package/src/profile/components/SidRegistrationForm.vue +88 -32
  14. package/src/profile/components/index.ts +1 -4
  15. package/src/profile/composables/index.ts +1 -0
  16. package/src/profile/composables/operationPlan.test.ts +155 -0
  17. package/src/profile/composables/operationPlan.ts +218 -0
  18. package/src/profile/composables/useAccountApi.ts +4 -4
  19. package/src/profile/composables/useAuth.test.ts +312 -31
  20. package/src/profile/composables/useAuth.ts +176 -30
  21. package/src/profile/composables/useAuthApi.opaque.test.ts +13 -6
  22. package/src/profile/composables/useAuthApi.ts +40 -18
  23. package/src/profile/composables/useCurrentPasswordRequirement.test.ts +196 -0
  24. package/src/profile/composables/useCurrentPasswordRequirement.ts +95 -0
  25. package/src/profile/composables/useEnrollmentApi.test.ts +7 -3
  26. package/src/profile/composables/useEnrollmentApi.ts +3 -5
  27. package/src/profile/composables/useIdentityApi.ts +4 -4
  28. package/src/profile/composables/useSecurityLevel.test.ts +1 -1
  29. package/src/profile/composables/useSecurityLevel.ts +1 -1
  30. package/src/quasar/SidPasswordCeremony.test.ts +253 -0
  31. package/src/quasar/SidPasswordCeremony.vue +456 -0
  32. package/src/quasar/index.ts +2 -0
  33. package/src/utils/rpcRefusal.test.ts +4 -4
  34. package/src/utils/rpcRefusal.ts +8 -4
  35. package/dist/browser-vp/index.cjs +0 -349
  36. package/dist/browser-vp/index.d.cts +0 -276
  37. package/dist/browser-vp/index.d.ts +0 -276
  38. package/dist/browser-vp/index.js +0 -349
  39. package/dist/chunk-C3Q766CY.cjs +0 -125
  40. package/dist/chunk-J5YXRXB5.js +0 -125
  41. package/dist/index.cjs +0 -23581
  42. package/dist/index.d.cts +0 -11539
  43. package/dist/index.d.ts +0 -11539
  44. package/dist/index.js +0 -23581
  45. package/dist/storage-4CB2JTLW.cjs +0 -22
  46. package/dist/storage-THRWREZP.js +0 -22
  47. package/src/browser-vp/crypto.ts +0 -253
  48. package/src/browser-vp/index.test.ts +0 -10
  49. package/src/browser-vp/index.ts +0 -116
  50. package/src/browser-vp/prf.ts +0 -179
  51. package/src/browser-vp/storage.test.ts +0 -9
  52. package/src/browser-vp/storage.ts +0 -163
  53. package/src/browser-vp/types.ts +0 -95
  54. package/src/browser-vp/vp.test.ts +0 -43
  55. package/src/browser-vp/vp.ts +0 -111
  56. package/src/generated/google/api/http.ts +0 -687
  57. package/src/generated/google/api/httpbody.ts +0 -159
  58. package/src/generated/google/protobuf/any.ts +0 -326
  59. package/src/generated/google/protobuf/descriptor.ts +0 -4865
  60. package/src/generated/google/protobuf/duration.ts +0 -230
  61. package/src/generated/google/protobuf/empty.ts +0 -94
  62. package/src/generated/google/protobuf/struct.ts +0 -484
  63. package/src/generated/google/protobuf/timestamp.ts +0 -287
  64. package/src/generated/google/rpc/error_details.ts +0 -1386
  65. package/src/generated/google/rpc/status.ts +0 -127
  66. package/src/generated/sid/v1/account/account.client.ts +0 -527
  67. package/src/generated/sid/v1/account/account.ts +0 -3261
  68. package/src/generated/sid/v1/admin/admin.client.ts +0 -139
  69. package/src/generated/sid/v1/admin/admin.ts +0 -781
  70. package/src/generated/sid/v1/admin/admin_roles.client.ts +0 -181
  71. package/src/generated/sid/v1/admin/admin_roles.ts +0 -1606
  72. package/src/generated/sid/v1/admin/analytics.client.ts +0 -106
  73. package/src/generated/sid/v1/admin/analytics.ts +0 -1017
  74. package/src/generated/sid/v1/admin/branding.client.ts +0 -166
  75. package/src/generated/sid/v1/admin/branding.ts +0 -1906
  76. package/src/generated/sid/v1/admin/enrollment.client.ts +0 -177
  77. package/src/generated/sid/v1/admin/enrollment.ts +0 -1766
  78. package/src/generated/sid/v1/admin/events.client.ts +0 -195
  79. package/src/generated/sid/v1/admin/events.ts +0 -1900
  80. package/src/generated/sid/v1/admin/flow.client.ts +0 -183
  81. package/src/generated/sid/v1/admin/flow.ts +0 -1552
  82. package/src/generated/sid/v1/admin/keys.client.ts +0 -107
  83. package/src/generated/sid/v1/admin/keys.ts +0 -931
  84. package/src/generated/sid/v1/admin/login_experience.client.ts +0 -160
  85. package/src/generated/sid/v1/admin/login_experience.ts +0 -1061
  86. package/src/generated/sid/v1/admin/migration.client.ts +0 -127
  87. package/src/generated/sid/v1/admin/migration.ts +0 -1948
  88. package/src/generated/sid/v1/admin/notifications.client.ts +0 -134
  89. package/src/generated/sid/v1/admin/notifications.ts +0 -1273
  90. package/src/generated/sid/v1/admin/operations.client.ts +0 -241
  91. package/src/generated/sid/v1/admin/operations.ts +0 -2633
  92. package/src/generated/sid/v1/admin/organizations.client.ts +0 -57
  93. package/src/generated/sid/v1/admin/organizations.ts +0 -332
  94. package/src/generated/sid/v1/admin/provisioning.client.ts +0 -232
  95. package/src/generated/sid/v1/admin/provisioning.ts +0 -1383
  96. package/src/generated/sid/v1/admin/scopes.client.ts +0 -171
  97. package/src/generated/sid/v1/admin/scopes.ts +0 -1324
  98. package/src/generated/sid/v1/admin/security.client.ts +0 -410
  99. package/src/generated/sid/v1/admin/security.ts +0 -4077
  100. package/src/generated/sid/v1/admin/sessions.client.ts +0 -114
  101. package/src/generated/sid/v1/admin/sessions.ts +0 -923
  102. package/src/generated/sid/v1/admin/settings.client.ts +0 -351
  103. package/src/generated/sid/v1/admin/settings.ts +0 -2458
  104. package/src/generated/sid/v1/admin/user_profile.client.ts +0 -57
  105. package/src/generated/sid/v1/admin/user_profile.ts +0 -730
  106. package/src/generated/sid/v1/admin/webhooks.client.ts +0 -269
  107. package/src/generated/sid/v1/admin/webhooks.ts +0 -2804
  108. package/src/generated/sid/v1/attestation/attestation.client.ts +0 -115
  109. package/src/generated/sid/v1/attestation/attestation.ts +0 -813
  110. package/src/generated/sid/v1/authn/auth.client.ts +0 -943
  111. package/src/generated/sid/v1/authn/auth.ts +0 -9719
  112. package/src/generated/sid/v1/authn/flows.client.ts +0 -413
  113. package/src/generated/sid/v1/authn/flows.ts +0 -3205
  114. package/src/generated/sid/v1/authn/issuer.client.ts +0 -489
  115. package/src/generated/sid/v1/authn/issuer.ts +0 -1600
  116. package/src/generated/sid/v1/authn/password_history.client.ts +0 -57
  117. package/src/generated/sid/v1/authn/password_history.ts +0 -578
  118. package/src/generated/sid/v1/authn/system_integration.client.ts +0 -73
  119. package/src/generated/sid/v1/authn/system_integration.ts +0 -218
  120. package/src/generated/sid/v1/authz/authz.client.ts +0 -459
  121. package/src/generated/sid/v1/authz/authz.ts +0 -6524
  122. package/src/generated/sid/v1/authz/forward_auth.client.ts +0 -97
  123. package/src/generated/sid/v1/authz/forward_auth.ts +0 -79
  124. package/src/generated/sid/v1/authz/governance.client.ts +0 -102
  125. package/src/generated/sid/v1/authz/governance.ts +0 -875
  126. package/src/generated/sid/v1/common/errors.ts +0 -710
  127. package/src/generated/sid/v1/common/pagination.ts +0 -152
  128. package/src/generated/sid/v1/common/transport.ts +0 -227
  129. package/src/generated/sid/v1/common/types.ts +0 -151
  130. package/src/generated/sid/v1/events/event_stream.client.ts +0 -67
  131. package/src/generated/sid/v1/events/event_stream.ts +0 -375
  132. package/src/generated/sid/v1/federation/upstream.client.ts +0 -142
  133. package/src/generated/sid/v1/federation/upstream.ts +0 -1736
  134. package/src/generated/sid/v1/identity/identity.client.ts +0 -467
  135. package/src/generated/sid/v1/identity/identity.ts +0 -5676
  136. package/src/generated/sid/v1/ids/ids.ts +0 -395
  137. package/src/generated/sid/v1/machine/machine_user.client.ts +0 -219
  138. package/src/generated/sid/v1/machine/machine_user.ts +0 -2364
  139. package/src/generated/sid/v1/machine/pat.client.ts +0 -130
  140. package/src/generated/sid/v1/machine/pat.ts +0 -1083
  141. package/src/generated/sid/v1/projects/projects.client.ts +0 -404
  142. package/src/generated/sid/v1/projects/projects.ts +0 -4446
  143. package/src/generated/sid/v1/scim/scim.client.ts +0 -520
  144. package/src/generated/sid/v1/scim/scim.ts +0 -3182
  145. package/src/generated/sid/v1/test/test.client.ts +0 -232
  146. package/src/generated/sid/v1/test/test.ts +0 -1136
  147. package/src/transport/adaptive.test.ts +0 -728
  148. package/src/transport/adaptive.ts +0 -308
  149. package/src/transport/framing.test.ts +0 -227
  150. package/src/transport/framing.ts +0 -98
  151. package/src/transport/webtransport.test.ts +0 -728
  152. package/src/transport/webtransport.ts +0 -323
@@ -1,22 +0,0 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true});
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
- var _chunkC3Q766CYcjs = require('./chunk-C3Q766CY.cjs');
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
- exports.clearAll = _chunkC3Q766CYcjs.clearAll; exports.deleteBinding = _chunkC3Q766CYcjs.deleteBinding; exports.deleteProfileState = _chunkC3Q766CYcjs.deleteProfileState; exports.isIndexedDbAvailable = _chunkC3Q766CYcjs.isIndexedDbAvailable; exports.listBindings = _chunkC3Q766CYcjs.listBindings; exports.loadBinding = _chunkC3Q766CYcjs.loadBinding; exports.loadProfileState = _chunkC3Q766CYcjs.loadProfileState; exports.saveBinding = _chunkC3Q766CYcjs.saveBinding; exports.saveProfileState = _chunkC3Q766CYcjs.saveProfileState;
@@ -1,22 +0,0 @@
1
- import {
2
- clearAll,
3
- deleteBinding,
4
- deleteProfileState,
5
- isIndexedDbAvailable,
6
- listBindings,
7
- loadBinding,
8
- loadProfileState,
9
- saveBinding,
10
- saveProfileState
11
- } from "./chunk-J5YXRXB5.js";
12
- export {
13
- clearAll,
14
- deleteBinding,
15
- deleteProfileState,
16
- isIndexedDbAvailable,
17
- listBindings,
18
- loadBinding,
19
- loadProfileState,
20
- saveBinding,
21
- saveProfileState
22
- };
@@ -1,253 +0,0 @@
1
- /**
2
- * Cryptographic primitives for Browser VP.
3
- *
4
- * All operations use Web Crypto API. Keys are non-extractable.
5
- */
6
-
7
- import type { DerivedKeys } from "./types";
8
-
9
- const PRF_SALT = "sid-ppk-v1";
10
- const SIGNING_INFO = "signing-key";
11
- const DATA_KEY_INFO = "data-encryption-key";
12
-
13
- const encoder = new TextEncoder();
14
-
15
- /**
16
- * Derive signing key (ECDSA P-256) and data key (AES-256-GCM)
17
- * from a PRF output using HKDF-SHA256.
18
- *
19
- * Both keys are deterministic from the same PRF seed.
20
- * Same passkey on any device → same keys.
21
- */
22
- export async function deriveKeysFromPrf(
23
- prfOutput: ArrayBuffer,
24
- ): Promise<DerivedKeys> {
25
- // Import PRF output as HKDF key material
26
- const baseKey = await crypto.subtle.importKey(
27
- "raw",
28
- prfOutput,
29
- "HKDF",
30
- false,
31
- ["deriveBits"],
32
- );
33
-
34
- const salt = encoder.encode(PRF_SALT);
35
-
36
- // Derive 32 bytes for ECDSA signing key seed
37
- const signingBits = await crypto.subtle.deriveBits(
38
- { name: "HKDF", hash: "SHA-256", salt, info: encoder.encode(SIGNING_INFO) },
39
- baseKey,
40
- 256,
41
- );
42
-
43
- // Derive 32 bytes for AES-256-GCM data key
44
- const dataKeyBits = await crypto.subtle.deriveBits(
45
- {
46
- name: "HKDF",
47
- hash: "SHA-256",
48
- salt,
49
- info: encoder.encode(DATA_KEY_INFO),
50
- },
51
- baseKey,
52
- 256,
53
- );
54
-
55
- // Import signing seed as ECDSA P-256 private key
56
- // WebCrypto doesn't support importing raw seed → we use PKCS8
57
- // Instead, derive via HKDF into a JWK-compatible format
58
- const signingKey = await importEcdsaFromSeed(signingBits);
59
-
60
- // Import data key as AES-256-GCM
61
- const dataKey = await crypto.subtle.importKey(
62
- "raw",
63
- dataKeyBits,
64
- { name: "AES-GCM", length: 256 },
65
- false,
66
- ["encrypt", "decrypt"],
67
- );
68
-
69
- return { signingKey, dataKey };
70
- }
71
-
72
- /**
73
- * Generate a random ECDSA P-256 key pair (Strategy B).
74
- * The private key is non-extractable.
75
- */
76
- export async function generateDeviceKey(): Promise<CryptoKeyPair> {
77
- return crypto.subtle.generateKey(
78
- { name: "ECDSA", namedCurve: "P-256" },
79
- false, // non-extractable
80
- ["sign", "verify"],
81
- );
82
- }
83
-
84
- /**
85
- * Generate a random AES-256-GCM data key (Strategy B).
86
- */
87
- export async function generateDataKey(): Promise<CryptoKey> {
88
- return crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, false, [
89
- "encrypt",
90
- "decrypt",
91
- ]);
92
- }
93
-
94
- /**
95
- * Wrap a CryptoKey with AES-KW for upload to SID (Strategy B).
96
- * Returns the wrapped key bytes.
97
- */
98
- export async function wrapKey(
99
- keyToWrap: CryptoKey,
100
- wrappingKey: CryptoKey,
101
- ): Promise<ArrayBuffer> {
102
- return crypto.subtle.wrapKey("jwk", keyToWrap, wrappingKey, "AES-KW");
103
- }
104
-
105
- /**
106
- * Unwrap a CryptoKey from SID (Strategy B, new browser).
107
- * Returns non-extractable ECDSA P-256 key pair.
108
- */
109
- export async function unwrapSigningKey(
110
- wrappedKey: ArrayBuffer,
111
- wrappingKey: CryptoKey,
112
- ): Promise<CryptoKey> {
113
- return crypto.subtle.unwrapKey(
114
- "jwk",
115
- wrappedKey,
116
- wrappingKey,
117
- "AES-KW",
118
- { name: "ECDSA", namedCurve: "P-256" },
119
- false, // non-extractable
120
- ["sign"],
121
- );
122
- }
123
-
124
- /**
125
- * Derive AES-256-KW wrapping key from OPAQUE session export key (Strategy B).
126
- */
127
- export async function deriveWrappingKey(
128
- sessionExportKey: ArrayBuffer,
129
- ): Promise<CryptoKey> {
130
- const baseKey = await crypto.subtle.importKey(
131
- "raw",
132
- sessionExportKey,
133
- "HKDF",
134
- false,
135
- ["deriveKey"],
136
- );
137
-
138
- return crypto.subtle.deriveKey(
139
- {
140
- name: "HKDF",
141
- hash: "SHA-256",
142
- salt: encoder.encode("sid-device-key-wrap"),
143
- info: encoder.encode("wrapping-key"),
144
- },
145
- baseKey,
146
- { name: "AES-KW", length: 256 },
147
- false,
148
- ["wrapKey", "unwrapKey"],
149
- );
150
- }
151
-
152
- /**
153
- * Encrypt data with AES-256-GCM.
154
- * Returns IV (12 bytes) prepended to ciphertext.
155
- */
156
- export async function encrypt(
157
- data: ArrayBuffer,
158
- key: CryptoKey,
159
- ): Promise<ArrayBuffer> {
160
- const iv = crypto.getRandomValues(new Uint8Array(12));
161
- const ciphertext = await crypto.subtle.encrypt(
162
- { name: "AES-GCM", iv },
163
- key,
164
- data,
165
- );
166
-
167
- // Prepend IV to ciphertext
168
- const result = new Uint8Array(iv.length + ciphertext.byteLength);
169
- result.set(iv, 0);
170
- result.set(new Uint8Array(ciphertext), iv.length);
171
- return result.buffer;
172
- }
173
-
174
- /**
175
- * Decrypt data encrypted with `encrypt()`.
176
- * Expects IV (12 bytes) prepended to ciphertext.
177
- */
178
- export async function decrypt(
179
- data: ArrayBuffer,
180
- key: CryptoKey,
181
- ): Promise<ArrayBuffer> {
182
- const bytes = new Uint8Array(data);
183
- const iv = bytes.slice(0, 12);
184
- const ciphertext = bytes.slice(12);
185
-
186
- return crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ciphertext);
187
- }
188
-
189
- /**
190
- * Sign data with ECDSA P-256 (SHA-256).
191
- */
192
- export async function sign(
193
- data: ArrayBuffer,
194
- privateKey: CryptoKey,
195
- ): Promise<ArrayBuffer> {
196
- return crypto.subtle.sign(
197
- { name: "ECDSA", hash: "SHA-256" },
198
- privateKey,
199
- data,
200
- );
201
- }
202
-
203
- /**
204
- * Export public key as raw bytes (for CSR / certificate request).
205
- */
206
- export async function exportPublicKey(
207
- keyPair: CryptoKeyPair,
208
- ): Promise<ArrayBuffer> {
209
- return crypto.subtle.exportKey("raw", keyPair.publicKey);
210
- }
211
-
212
- /**
213
- * Compute SHA-256 hash (for authenticator change detection).
214
- */
215
- export async function sha256(data: ArrayBuffer): Promise<string> {
216
- const hash = await crypto.subtle.digest("SHA-256", data);
217
- return Array.from(new Uint8Array(hash))
218
- .map((b) => b.toString(16).padStart(2, "0"))
219
- .join("");
220
- }
221
-
222
- // ── Internal helpers ──
223
-
224
- /**
225
- * Import a 32-byte seed as ECDSA P-256 key pair.
226
- *
227
- * WebCrypto doesn't support direct seed import for ECDSA.
228
- * We use the seed as HKDF input to derive a proper PKCS8 key.
229
- * This is deterministic: same seed → same key pair.
230
- */
231
- async function importEcdsaFromSeed(_seed: ArrayBuffer): Promise<CryptoKeyPair> {
232
- // Use seed to deterministically generate key via HKDF → generateKey
233
- // Since WebCrypto generateKey is non-deterministic, we instead:
234
- // 1. Import seed as HMAC key (deterministic)
235
- // 2. Sign a fixed message to get deterministic bytes
236
- // 3. Use those bytes as JWK `d` parameter
237
- //
238
- // Note: This is a simplification. Production implementation
239
- // should use a proper deterministic ECDSA keygen (RFC 6979-style).
240
- // For now, we generate a random key pair — deterministic derivation
241
- // requires either a WASM implementation or a JS bigint EC library.
242
- //
243
- // TODO: Replace with deterministic keygen from sid-wasm when available.
244
- // The PRF output is still the entropy source, ensuring same passkey
245
- // produces same seed. Full deterministic EC keygen will be added
246
- // when sid-wasm exposes the derivation function.
247
-
248
- return crypto.subtle.generateKey(
249
- { name: "ECDSA", namedCurve: "P-256" },
250
- false,
251
- ["sign", "verify"],
252
- );
253
- }
@@ -1,10 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { detectStrategy } from "./index";
3
-
4
- describe("detectStrategy", () => {
5
- it("returns null when window is undefined (Node.js)", async () => {
6
- // Node.js has no window global
7
- const result = await detectStrategy();
8
- expect(result).toBeNull();
9
- });
10
- });
@@ -1,116 +0,0 @@
1
- /**
2
- * Browser VP — Zero-Load Auth in Browser.
3
- *
4
- * Public API for browser-based Verifiable Presentation creation.
5
- * Automatically selects the best strategy based on browser capabilities:
6
- *
7
- * - **Strategy A (PRF):** Passkey PRF extension → deterministic keys.
8
- * Cross-device via passkey sync. ~90% browser coverage (2026).
9
- *
10
- * - **Strategy B (device-key):** Random WebCrypto key, wrapped for sync.
11
- * Each browser = separate device. Fallback for non-PRF browsers.
12
- *
13
- * Reference: Prudnikov, D. (2026). "Client-Side Verifiable Presentation System
14
- * with Passkey-Derived Persistent Keys and Pairwise Service Bindings."
15
- * doi:10.5281/zenodo.19387768 — https://doi.org/10.5281/zenodo.19387768
16
- */
17
-
18
- // Types
19
- export type {
20
- VpStrategy,
21
- ProfileState,
22
- BindingEntry,
23
- DerivedKeys,
24
- VerifiablePresentation,
25
- VpRequest,
26
- BrowserVpContext,
27
- } from "./types";
28
-
29
- // PRF extension
30
- export { hasPrfSupport, evaluatePrf, createPasskeyWithPrf } from "./prf";
31
-
32
- // Cryptographic primitives
33
- export {
34
- deriveKeysFromPrf,
35
- generateDeviceKey,
36
- generateDataKey,
37
- wrapKey,
38
- unwrapSigningKey,
39
- deriveWrappingKey,
40
- encrypt,
41
- decrypt,
42
- sign,
43
- exportPublicKey,
44
- sha256,
45
- } from "./crypto";
46
-
47
- // IndexedDB storage
48
- export {
49
- saveProfileState,
50
- loadProfileState,
51
- deleteProfileState,
52
- saveBinding,
53
- loadBinding,
54
- listBindings,
55
- deleteBinding,
56
- clearAll,
57
- isIndexedDbAvailable,
58
- } from "./storage";
59
-
60
- // VP creation
61
- export { createVp, needsRenewal, getExpiringSoon } from "./vp";
62
-
63
- // ── Strategy selection ──
64
-
65
- import { hasPrfSupport } from "./prf";
66
- import { isIndexedDbAvailable } from "./storage";
67
- import type { VpStrategy, BrowserVpContext } from "./types";
68
-
69
- /**
70
- * Detect the best VP strategy for this browser.
71
- *
72
- * Returns "prf" if WebAuthn PRF extension is supported,
73
- * "device-key" otherwise. Returns null if Browser VP is
74
- * not possible (no IndexedDB, no WebCrypto).
75
- */
76
- export async function detectStrategy(): Promise<VpStrategy | null> {
77
- // Prerequisites
78
- if (typeof window === "undefined") return null;
79
- if (!isIndexedDbAvailable()) return null;
80
- if (!window.crypto?.subtle) return null;
81
-
82
- // Try PRF first (primary strategy)
83
- if (await hasPrfSupport()) {
84
- return "prf";
85
- }
86
-
87
- // Fallback to device key
88
- return "device-key";
89
- }
90
-
91
- /**
92
- * Initialize Browser VP for a profile.
93
- *
94
- * This is the main entry point. It:
95
- * 1. Detects the best strategy
96
- * 2. Checks for existing state in IndexedDB
97
- * 3. Returns a context object indicating readiness
98
- *
99
- * If not ready, the caller should trigger setup flow
100
- * (passkey creation for PRF, or key generation for device-key).
101
- */
102
- export async function initBrowserVp(
103
- profileId: string,
104
- ): Promise<BrowserVpContext | null> {
105
- const strategy = await detectStrategy();
106
- if (!strategy) return null;
107
-
108
- const { loadProfileState } = await import("./storage");
109
- const state = await loadProfileState(profileId);
110
-
111
- return {
112
- strategy,
113
- profileId,
114
- ready: state != null && state.strategy === strategy,
115
- };
116
- }
@@ -1,179 +0,0 @@
1
- /**
2
- * WebAuthn PRF extension support (Strategy A).
3
- *
4
- * PRF (Pseudo-Random Function) extension allows deriving
5
- * deterministic secrets from a passkey. Same passkey on any
6
- * synced device produces the same PRF output → same keys.
7
- */
8
-
9
- const PRF_EVAL_LABEL = "sid-ppk-v1";
10
- const PRF_PROBE_LABEL = "sid-probe";
11
- const encoder = new TextEncoder();
12
-
13
- /**
14
- * Detect whether the browser supports WebAuthn PRF extension.
15
- *
16
- * Attempts a PRF evaluation on an existing passkey. Returns false
17
- * if PRF is unsupported — no error thrown.
18
- *
19
- * Browser support (2026): Chrome 116+, Edge 116+, Safari 17.4+,
20
- * Chrome Android, Safari iOS 17.4+. ~90% coverage.
21
- */
22
- export async function hasPrfSupport(): Promise<boolean> {
23
- try {
24
- // Check if PublicKeyCredential and extensions are available
25
- if (typeof window === "undefined" || !window.PublicKeyCredential) {
26
- return false;
27
- }
28
-
29
- // Check if PRF extension is supported via static method (if available)
30
- if (typeof PublicKeyCredential.getClientCapabilities === "function") {
31
- try {
32
- const caps = await PublicKeyCredential.getClientCapabilities();
33
- if (caps && "prf" in caps) {
34
- return Boolean(caps.prf);
35
- }
36
- } catch {
37
- // Fall through to probe-based detection
38
- }
39
- }
40
-
41
- // Probe-based detection: try to use PRF in a get() call
42
- // This requires an existing passkey for this origin
43
- const result = await navigator.credentials.get({
44
- publicKey: {
45
- challenge: crypto.getRandomValues(new Uint8Array(32)),
46
- rpId: window.location.hostname,
47
- userVerification: "discouraged",
48
- extensions: {
49
- prf: {
50
- eval: {
51
- first: encoder.encode(PRF_PROBE_LABEL),
52
- },
53
- },
54
- } as AuthenticationExtensionsClientInputs,
55
- },
56
- });
57
-
58
- if (!result) return false;
59
-
60
- const ext = (
61
- result as PublicKeyCredential
62
- ).getClientExtensionResults() as Record<string, unknown>;
63
- const prfResults = ext?.prf as
64
- | { results?: { first?: ArrayBuffer } }
65
- | undefined;
66
-
67
- return prfResults?.results?.first != null;
68
- } catch {
69
- return false;
70
- }
71
- }
72
-
73
- /**
74
- * Evaluate PRF extension during passkey authentication.
75
- *
76
- * Returns the raw PRF output (32 bytes) which serves as the
77
- * entropy source for key derivation.
78
- *
79
- * @param credentialId - Optional: specific credential to use.
80
- * @returns PRF output bytes, or null if PRF evaluation failed.
81
- */
82
- export async function evaluatePrf(
83
- credentialId?: ArrayBuffer,
84
- ): Promise<ArrayBuffer | null> {
85
- try {
86
- const allowCredentials: PublicKeyCredentialDescriptor[] | undefined =
87
- credentialId ? [{ type: "public-key", id: credentialId }] : undefined;
88
-
89
- const result = await navigator.credentials.get({
90
- publicKey: {
91
- challenge: crypto.getRandomValues(new Uint8Array(32)),
92
- rpId: window.location.hostname,
93
- userVerification: "required",
94
- allowCredentials,
95
- extensions: {
96
- prf: {
97
- eval: {
98
- first: encoder.encode(PRF_EVAL_LABEL),
99
- },
100
- },
101
- } as AuthenticationExtensionsClientInputs,
102
- },
103
- });
104
-
105
- if (!result) return null;
106
-
107
- const ext = (
108
- result as PublicKeyCredential
109
- ).getClientExtensionResults() as Record<string, unknown>;
110
- const prfResults = ext?.prf as
111
- | { results?: { first?: ArrayBuffer } }
112
- | undefined;
113
-
114
- return prfResults?.results?.first ?? null;
115
- } catch {
116
- return null;
117
- }
118
- }
119
-
120
- /**
121
- * Create a passkey with PRF extension (first-time setup).
122
- *
123
- * Returns both the credential and the initial PRF output.
124
- */
125
- export async function createPasskeyWithPrf(
126
- userId: ArrayBuffer,
127
- userName: string,
128
- rpName: string,
129
- ): Promise<{ credential: PublicKeyCredential; prfOutput: ArrayBuffer } | null> {
130
- try {
131
- const result = await navigator.credentials.create({
132
- publicKey: {
133
- challenge: crypto.getRandomValues(new Uint8Array(32)),
134
- rp: {
135
- name: rpName,
136
- id: window.location.hostname,
137
- },
138
- user: {
139
- id: userId,
140
- name: userName,
141
- displayName: userName,
142
- },
143
- pubKeyCredParams: [
144
- { alg: -7, type: "public-key" }, // ES256
145
- { alg: -257, type: "public-key" }, // RS256 fallback
146
- ],
147
- authenticatorSelection: {
148
- residentKey: "required",
149
- userVerification: "required",
150
- },
151
- extensions: {
152
- prf: {
153
- eval: {
154
- first: encoder.encode(PRF_EVAL_LABEL),
155
- },
156
- },
157
- } as AuthenticationExtensionsClientInputs,
158
- },
159
- });
160
-
161
- if (!result) return null;
162
-
163
- const credential = result as PublicKeyCredential;
164
- const ext = credential.getClientExtensionResults() as Record<
165
- string,
166
- unknown
167
- >;
168
- const prfResults = ext?.prf as
169
- | { results?: { first?: ArrayBuffer } }
170
- | undefined;
171
- const prfOutput = prfResults?.results?.first;
172
-
173
- if (!prfOutput) return null;
174
-
175
- return { credential, prfOutput };
176
- } catch {
177
- return null;
178
- }
179
- }
@@ -1,9 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { isIndexedDbAvailable } from "./storage";
3
-
4
- describe("isIndexedDbAvailable", () => {
5
- it("returns false in Node.js environment (no indexedDB global)", () => {
6
- // Node.js doesn't have indexedDB
7
- expect(isIndexedDbAvailable()).toBe(false);
8
- });
9
- });