@structured-id/ui-core 1.2.0 → 2.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 (101) hide show
  1. package/README.md +9 -5
  2. package/dist/browser-vp/index.d.cts +0 -11
  3. package/dist/browser-vp/index.d.ts +0 -11
  4. package/dist/index.cjs +13721 -9514
  5. package/dist/index.d.cts +4362 -2375
  6. package/dist/index.d.ts +4362 -2375
  7. package/dist/index.js +13937 -9730
  8. package/package.json +3 -3
  9. package/src/browser-vp/crypto.ts +0 -1
  10. package/src/browser-vp/index.ts +0 -2
  11. package/src/browser-vp/prf.ts +0 -2
  12. package/src/browser-vp/storage.ts +0 -2
  13. package/src/browser-vp/types.ts +0 -2
  14. package/src/browser-vp/vp.ts +0 -2
  15. package/src/composables/index.ts +6 -1
  16. package/src/composables/useAccountTransport.test.ts +121 -0
  17. package/src/composables/useAccountTransport.ts +69 -0
  18. package/src/composables/useBffAuth.ts +44 -34
  19. package/src/generated/google/api/httpbody.ts +159 -0
  20. package/src/generated/google/protobuf/any.ts +326 -0
  21. package/src/generated/google/rpc/error_details.ts +1386 -0
  22. package/src/generated/google/rpc/status.ts +127 -0
  23. package/src/generated/sid/v1/account/account.ts +1 -1
  24. package/src/generated/sid/v1/admin/admin.client.ts +15 -12
  25. package/src/generated/sid/v1/admin/admin.ts +24 -89
  26. package/src/generated/sid/v1/admin/enrollment.client.ts +24 -12
  27. package/src/generated/sid/v1/admin/enrollment.ts +65 -3
  28. package/src/generated/sid/v1/admin/events.client.ts +8 -8
  29. package/src/generated/sid/v1/admin/flow.ts +1 -1
  30. package/src/generated/sid/v1/admin/keys.ts +1 -1
  31. package/src/generated/sid/v1/admin/notifications.client.ts +12 -6
  32. package/src/generated/sid/v1/admin/notifications.ts +11 -70
  33. package/src/generated/sid/v1/admin/operations.ts +20 -35
  34. package/src/generated/sid/v1/admin/organizations.client.ts +2 -2
  35. package/src/generated/sid/v1/admin/organizations.ts +15 -32
  36. package/src/generated/sid/v1/admin/provisioning.client.ts +232 -0
  37. package/src/generated/sid/v1/admin/provisioning.ts +1383 -0
  38. package/src/generated/sid/v1/admin/scopes.ts +1 -1
  39. package/src/generated/sid/v1/admin/security.client.ts +22 -24
  40. package/src/generated/sid/v1/admin/security.ts +13 -22
  41. package/src/generated/sid/v1/admin/settings.client.ts +29 -18
  42. package/src/generated/sid/v1/admin/settings.ts +2 -82
  43. package/src/generated/sid/v1/attestation/attestation.client.ts +6 -6
  44. package/src/generated/sid/v1/attestation/attestation.ts +8 -8
  45. package/src/generated/sid/v1/authn/auth.client.ts +101 -58
  46. package/src/generated/sid/v1/authn/auth.ts +842 -414
  47. package/src/generated/sid/v1/authn/flows.ts +0 -12
  48. package/src/generated/sid/v1/authn/issuer.client.ts +489 -0
  49. package/src/generated/sid/v1/authn/issuer.ts +1600 -0
  50. package/src/generated/sid/v1/authn/password_history.client.ts +57 -0
  51. package/src/generated/sid/v1/authn/password_history.ts +578 -0
  52. package/src/generated/sid/v1/authn/system_integration.client.ts +73 -0
  53. package/src/generated/sid/v1/authn/system_integration.ts +218 -0
  54. package/src/generated/sid/v1/authz/authz.ts +1498 -317
  55. package/src/generated/sid/v1/authz/forward_auth.client.ts +97 -0
  56. package/src/generated/sid/v1/authz/forward_auth.ts +79 -0
  57. package/src/generated/sid/v1/authz/governance.client.ts +2 -2
  58. package/src/generated/sid/v1/authz/governance.ts +61 -4
  59. package/src/generated/sid/v1/common/errors.ts +532 -6
  60. package/src/generated/sid/v1/events/event_stream.client.ts +2 -2
  61. package/src/generated/sid/v1/events/event_stream.ts +2 -1
  62. package/src/generated/sid/v1/identity/identity.ts +98 -58
  63. package/src/generated/sid/v1/ids/ids.ts +395 -0
  64. package/src/generated/sid/v1/machine/machine_user.ts +0 -36
  65. package/src/generated/sid/v1/machine/pat.client.ts +2 -19
  66. package/src/generated/sid/v1/machine/pat.ts +1 -186
  67. package/src/generated/sid/v1/projects/projects.client.ts +137 -19
  68. package/src/generated/sid/v1/projects/projects.ts +2105 -405
  69. package/src/generated/sid/v1/scim/scim.client.ts +248 -2
  70. package/src/generated/sid/v1/scim/scim.ts +153 -14
  71. package/src/generated/sid/v1/test/test.ts +1 -1
  72. package/src/i18n/index.ts +0 -11
  73. package/src/index.ts +36 -2
  74. package/src/package-exports.test.ts +18 -0
  75. package/src/profile/components/SidLoginForm.vue +5 -2
  76. package/src/profile/components/SidRegistrationForm.test.ts +58 -1
  77. package/src/profile/components/SidRegistrationForm.vue +110 -12
  78. package/src/profile/components/index.ts +4 -0
  79. package/src/profile/composables/index.ts +2 -1
  80. package/src/profile/composables/useAccountApi.ts +20 -7
  81. package/src/profile/composables/useAuth.test.ts +453 -0
  82. package/src/profile/composables/useAuth.ts +414 -0
  83. package/src/profile/composables/useAuthApi.opaque.test.ts +18 -0
  84. package/src/profile/composables/useAuthApi.ts +188 -20
  85. package/src/profile/composables/useEnrollmentApi.test.ts +33 -0
  86. package/src/profile/composables/useEnrollmentApi.ts +26 -0
  87. package/src/profile/composables/useIdentityApi.ts +17 -7
  88. package/src/profile/composables/useLoginSuccess.test.ts +97 -3
  89. package/src/profile/composables/useLoginSuccess.ts +80 -22
  90. package/src/profile/composables/useSecurityLevel.test.ts +10 -3
  91. package/src/profile/composables/useSecurityLevel.ts +12 -3
  92. package/src/quasar/SidPrincipalInput.vue +4 -7
  93. package/src/utils/normalizePrincipal.test.ts +19 -5
  94. package/src/utils/normalizePrincipal.ts +7 -9
  95. package/src/utils/rpcRefusal.test.ts +151 -0
  96. package/src/utils/rpcRefusal.ts +252 -0
  97. package/src/utils/validators.test.ts +9 -0
  98. package/src/utils/validators.ts +14 -2
  99. package/src/composables/useAuth.ts +0 -40
  100. package/src/profile/composables/useOpaqueAuth.test.ts +0 -290
  101. package/src/profile/composables/useOpaqueAuth.ts +0 -191
@@ -0,0 +1,414 @@
1
+ /**
2
+ * The password ceremonies (shared between all profile UIs): registration,
3
+ * sign-in, password change and reset.
4
+ *
5
+ * A password is installed (a first password, a change or a reset) in one
6
+ * server-side operation. The preparing RPC returns the operation's history
7
+ * context; the client blinds its history input for the server's evaluator,
8
+ * proves policy and history over the same password, bound to the operation
9
+ * and to its OPAQUE request, and sends the proof with the final record.
10
+ * Sign-in runs on the same Pallas OPAQUE client, since the server registers
11
+ * on Pallas.
12
+ *
13
+ * The client-side crypto is injected as {@link ZkppClientApi} (implemented by
14
+ * `@structured-id/opaque-zkpp`), keeping the WASM dependency at the app layer.
15
+ * gRPC calls go through useAuthApi wrappers.
16
+ */
17
+ import {
18
+ completePasswordReset,
19
+ evaluatePasswordHistory,
20
+ executePasswordReset,
21
+ opaqueLoginFinish,
22
+ opaqueLoginStart,
23
+ opaqueZkppRegistrationFinish,
24
+ opaqueZkppRegistrationStart,
25
+ passwordChangeChallenge,
26
+ passwordChangeExecute,
27
+ passwordChangeFinish,
28
+ type CompletePasswordResetResponse,
29
+ type PasswordHistoryContext,
30
+ type PasswordHistoryEvaluation,
31
+ type PasswordRegistrationProof,
32
+ } from "./useAuthApi";
33
+
34
+ // ── Types ──
35
+
36
+ /** A sign-in's session, as the server issued it. */
37
+ export interface LoginResult {
38
+ sessionId: string;
39
+ accessToken: string;
40
+ expiresIn: number;
41
+ }
42
+
43
+ /** A registration's profile and its password credential. */
44
+ export interface RegisterResult {
45
+ profileId: string;
46
+ credentialId: string;
47
+ }
48
+
49
+ /** Progress of a password operation, for a determinate gauge (0..1). */
50
+ export interface PasswordOperationProgress {
51
+ fraction: number;
52
+ label: string;
53
+ }
54
+
55
+ /** The operation's history context as the ZKPP client takes it. */
56
+ export interface ZkppHistoryContext {
57
+ /** The operation, 16 bytes; every later step carries it. */
58
+ operationId: Uint8Array;
59
+ ownerDomain: Uint8Array;
60
+ domains: { comparisonDomain: Uint8Array; evaluatorPublicKey: Uint8Array }[];
61
+ policyVersion: number;
62
+ }
63
+
64
+ /** The client's blinded history input; `blind` stays with the client. */
65
+ export interface ZkppHistoryRequest {
66
+ blind: Uint8Array;
67
+ blinded: Uint8Array;
68
+ }
69
+
70
+ /** The evaluator's answer for one comparison domain. */
71
+ export interface ZkppHistoryEvaluation {
72
+ evaluatedElement: Uint8Array;
73
+ proof: { challenge: Uint8Array; response: Uint8Array };
74
+ }
75
+
76
+ /** The OPAQUE registration start: the request sent, the state kept local. */
77
+ export interface ZkppRegistrationStart {
78
+ request: Uint8Array;
79
+ state: string;
80
+ }
81
+
82
+ /** A registration proof: Halo2 bytes and its public instances. */
83
+ export interface ZkppProof {
84
+ proof: Uint8Array;
85
+ instances: Uint8Array[];
86
+ }
87
+
88
+ /**
89
+ * Interface for the ZKPP client-side crypto.
90
+ * Implemented by @structured-id/opaque ZkppClient.
91
+ */
92
+ export interface ZkppClientApi {
93
+ /** True once the client answers no more calls (a worker it runs on stopped). */
94
+ readonly stopped: boolean;
95
+ registrationStart(password: string): Promise<ZkppRegistrationStart>;
96
+ /** `null` for a password the circuit cannot hold: it installs unproven. */
97
+ historyRequest(
98
+ password: string,
99
+ ownerDomain: Uint8Array,
100
+ ): Promise<ZkppHistoryRequest | null>;
101
+ prove(
102
+ password: string,
103
+ start: ZkppRegistrationStart,
104
+ opts: {
105
+ context: ZkppHistoryContext;
106
+ history: {
107
+ request: ZkppHistoryRequest;
108
+ evaluations: ZkppHistoryEvaluation[];
109
+ } | null;
110
+ onProgress?: (p: PasswordOperationProgress) => void;
111
+ },
112
+ ): Promise<ZkppProof | null>;
113
+ registrationFinish(
114
+ password: string,
115
+ state: string,
116
+ response: Uint8Array,
117
+ ): Promise<Uint8Array>;
118
+ loginStart(password: string): Promise<{ request: Uint8Array; state: string }>;
119
+ loginFinish(
120
+ password: string,
121
+ state: string,
122
+ response: Uint8Array,
123
+ ): Promise<Uint8Array>;
124
+ }
125
+
126
+ /**
127
+ * Loads the client on first use. Rejects where no client can run (no
128
+ * threaded WebAssembly: `ZkppUnavailableError` of the package).
129
+ */
130
+ export type ZkppClientLoader = () => Promise<ZkppClientApi>;
131
+
132
+ /** What the finish of an operation sends. */
133
+ interface Installed {
134
+ operationId: Uint8Array;
135
+ record: Uint8Array;
136
+ proof?: PasswordRegistrationProof;
137
+ }
138
+
139
+ /** The preparing RPC's answer to the OPAQUE request: the operation and the response. */
140
+ interface Exchange {
141
+ context: PasswordHistoryContext | undefined;
142
+ registrationResponse: Uint8Array;
143
+ }
144
+
145
+ // ── Wire conversions ──
146
+
147
+ /** The operation's context; a server that prepared none cannot take a password. */
148
+ function historyContext(
149
+ wire: PasswordHistoryContext | undefined,
150
+ ): ZkppHistoryContext {
151
+ const operationId = wire?.operationId?.value;
152
+ if (!wire || !operationId || operationId.length === 0) {
153
+ throw new Error("The server did not prepare a password operation");
154
+ }
155
+ return {
156
+ operationId,
157
+ ownerDomain: wire.ownerDomain,
158
+ domains: wire.domains.map((d) => ({
159
+ comparisonDomain: d.comparisonDomain,
160
+ evaluatorPublicKey: d.evaluatorPublicKey,
161
+ })),
162
+ policyVersion: wire.policyVersion,
163
+ };
164
+ }
165
+
166
+ /** The evaluator's answers with their proofs; an answer without one proves nothing. */
167
+ function historyEvaluations(
168
+ answers: PasswordHistoryEvaluation[],
169
+ ): ZkppHistoryEvaluation[] {
170
+ return answers.map((answer) => {
171
+ if (!answer.proof) {
172
+ throw new Error("The history evaluator answered without a proof");
173
+ }
174
+ return {
175
+ evaluatedElement: answer.evaluatedElement,
176
+ proof: {
177
+ challenge: answer.proof.challenge,
178
+ response: answer.proof.response,
179
+ },
180
+ };
181
+ });
182
+ }
183
+
184
+ function wireProof(
185
+ proof: ZkppProof | null,
186
+ ): PasswordRegistrationProof | undefined {
187
+ return proof
188
+ ? { zkppProof: proof.proof, instances: proof.instances }
189
+ : undefined;
190
+ }
191
+
192
+ /**
193
+ * Create the password ceremonies with an injected client loader; an app
194
+ * wraps the result in its `useAuth()`.
195
+ *
196
+ * @example
197
+ * ```ts
198
+ * import { loadZkppClient } from "@structured-id/opaque";
199
+ * import { createAuth } from "@structured-id/ui-core/profile";
200
+ *
201
+ * const { register, login, prewarm } = createAuth(() => loadZkppClient());
202
+ * void prewarm(); // when the page opens
203
+ * ```
204
+ */
205
+ export function createAuth(loadClient: ZkppClientLoader) {
206
+ let client: Promise<ZkppClientApi> | undefined;
207
+ let loaded: ZkppClientApi | undefined;
208
+
209
+ /**
210
+ * The client, loaded once and kept for each next operation; a failed load
211
+ * is retried next time, and a client that stopped answering is replaced.
212
+ */
213
+ function zkpp(): Promise<ZkppClientApi> {
214
+ if (loaded?.stopped) {
215
+ client = undefined;
216
+ loaded = undefined;
217
+ }
218
+ client ??= loadClient().then(
219
+ (c) => (loaded = c),
220
+ (e: unknown) => {
221
+ client = undefined;
222
+ throw e;
223
+ },
224
+ );
225
+ return client;
226
+ }
227
+
228
+ /**
229
+ * The steps every operation shares: the OPAQUE start, `exchange` (the RPC
230
+ * that answers the request and names the operation), the history
231
+ * evaluation, the proof and the record.
232
+ */
233
+ async function install(
234
+ password: string,
235
+ exchange: (request: Uint8Array) => Promise<Exchange>,
236
+ onProgress?: (p: PasswordOperationProgress) => void,
237
+ ): Promise<Installed> {
238
+ const c = await zkpp();
239
+ onProgress?.({ fraction: 0, label: "Preparing" });
240
+ const start = await c.registrationStart(password);
241
+ const { context: wire, registrationResponse } = await exchange(
242
+ start.request,
243
+ );
244
+ const context = historyContext(wire);
245
+ const request = await c.historyRequest(password, context.ownerDomain);
246
+ const history = request
247
+ ? {
248
+ request,
249
+ evaluations: historyEvaluations(
250
+ await evaluatePasswordHistory(context.operationId, request.blinded),
251
+ ),
252
+ }
253
+ : null;
254
+ const proof = await c.prove(password, start, {
255
+ context,
256
+ history,
257
+ onProgress,
258
+ });
259
+ const record = await c.registrationFinish(
260
+ password,
261
+ start.state,
262
+ registrationResponse,
263
+ );
264
+ onProgress?.({ fraction: 1, label: "Done" });
265
+ return {
266
+ operationId: context.operationId,
267
+ record,
268
+ proof: wireProof(proof),
269
+ };
270
+ }
271
+
272
+ /**
273
+ * Register `identifier` with `password`: the start carries the OPAQUE
274
+ * request and returns the operation, the finish carries the record with
275
+ * the proof. `claimToken` claims an installation that has no administrator.
276
+ */
277
+ async function register(
278
+ identifier: string,
279
+ password: string,
280
+ claimToken?: string,
281
+ onProgress?: (p: PasswordOperationProgress) => void,
282
+ ): Promise<RegisterResult> {
283
+ const done = await install(
284
+ password,
285
+ async (request) => {
286
+ const started = await opaqueZkppRegistrationStart(
287
+ identifier,
288
+ request,
289
+ claimToken,
290
+ );
291
+ return {
292
+ context: started.history,
293
+ registrationResponse: started.registrationResponse,
294
+ };
295
+ },
296
+ onProgress,
297
+ );
298
+ const finished = await opaqueZkppRegistrationFinish(
299
+ done.operationId,
300
+ done.record,
301
+ done.proof,
302
+ );
303
+ return {
304
+ profileId: finished.profileId,
305
+ credentialId: finished.credentialId,
306
+ };
307
+ }
308
+
309
+ /**
310
+ * Replace the signed-in user's password `credentialId` with `newPassword`:
311
+ * the challenge names the operation, the execute step answers the OPAQUE
312
+ * request, the finish carries the record with the proof.
313
+ */
314
+ async function changePassword(
315
+ credentialId: string,
316
+ newPassword: string,
317
+ onProgress?: (p: PasswordOperationProgress) => void,
318
+ ): Promise<void> {
319
+ const challenge = await passwordChangeChallenge(credentialId);
320
+ const context = historyContext(challenge.history);
321
+ const done = await install(
322
+ newPassword,
323
+ async (request) => ({
324
+ context: challenge.history,
325
+ registrationResponse: (
326
+ await passwordChangeExecute(
327
+ context.operationId,
328
+ credentialId,
329
+ request,
330
+ )
331
+ ).registrationResponse,
332
+ }),
333
+ onProgress,
334
+ );
335
+ await passwordChangeFinish(
336
+ done.operationId,
337
+ credentialId,
338
+ done.record,
339
+ done.proof,
340
+ );
341
+ }
342
+
343
+ /**
344
+ * Replace the password under a verified reset: `context` is the operation
345
+ * the verification prepared (`VerifyPasswordResetResponse.history`).
346
+ */
347
+ async function resetPassword(
348
+ resetSessionId: string,
349
+ context: PasswordHistoryContext | undefined,
350
+ newPassword: string,
351
+ onProgress?: (p: PasswordOperationProgress) => void,
352
+ ): Promise<CompletePasswordResetResponse> {
353
+ const prepared = historyContext(context);
354
+ const done = await install(
355
+ newPassword,
356
+ async (request) => ({
357
+ context,
358
+ registrationResponse: (
359
+ await executePasswordReset(prepared.operationId, request)
360
+ ).registrationResponse,
361
+ }),
362
+ onProgress,
363
+ );
364
+ return completePasswordReset(
365
+ resetSessionId,
366
+ done.operationId,
367
+ done.record,
368
+ done.proof,
369
+ );
370
+ }
371
+
372
+ /**
373
+ * Sign in with a Pallas OPAQUE credential (2 round-trips via gRPC-web).
374
+ */
375
+ async function login(
376
+ identifier: string,
377
+ password: string,
378
+ onStep?: (step: number) => void,
379
+ ): Promise<LoginResult> {
380
+ const c = await zkpp();
381
+ onStep?.(1);
382
+ const start = await c.loginStart(password);
383
+ onStep?.(2);
384
+ const started = await opaqueLoginStart(identifier, start.request);
385
+ onStep?.(3);
386
+ const finalization = await c.loginFinish(
387
+ password,
388
+ start.state,
389
+ started.credentialResponse,
390
+ );
391
+ const finished = await opaqueLoginFinish(
392
+ identifier,
393
+ finalization,
394
+ started.serverLoginState,
395
+ );
396
+ return {
397
+ sessionId: finished.sessionId,
398
+ accessToken: finished.accessToken,
399
+ expiresIn: Number(finished.expiresIn),
400
+ };
401
+ }
402
+
403
+ /**
404
+ * Load the client now, when the page opens, so the kernel's download and
405
+ * compilation are done before the user submits a password. Resolves when
406
+ * loading ends either way: a failed load is retried, and reported, by the
407
+ * next operation.
408
+ */
409
+ async function prewarm(): Promise<void> {
410
+ await zkpp().catch(() => undefined);
411
+ }
412
+
413
+ return { register, changePassword, resetPassword, login, prewarm };
414
+ }
@@ -103,6 +103,24 @@ describe("OPAQUE gRPC wrappers", () => {
103
103
  });
104
104
  expect(result).toBe(expected);
105
105
  });
106
+
107
+ // An unclaimed installation accepts only a registration carrying the
108
+ // claim token; the wrapper must put it on the wire unchanged.
109
+ it("sends the instance claim token when given", async () => {
110
+ mockOpaqueRegistrationStart.mockResolvedValue({ response: {} });
111
+
112
+ await opaqueRegistrationStart(
113
+ "owner@test.com",
114
+ new Uint8Array([1]),
115
+ "sidclaim_abc",
116
+ );
117
+
118
+ expect(mockOpaqueRegistrationStart).toHaveBeenCalledWith({
119
+ principal: "owner@test.com",
120
+ registrationRequest: new Uint8Array([1]),
121
+ claimToken: "sidclaim_abc",
122
+ });
123
+ });
106
124
  });
107
125
 
108
126
  describe("opaqueRegistrationFinish", () => {