@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
@@ -10,7 +10,9 @@ const quasarStubs = {
10
10
  "q-icon": { template: "<span />" },
11
11
  "q-chip": { template: "<span />" },
12
12
  "q-form": {
13
- template: "<form @submit.prevent=\"$emit('submit')\"><slot /></form>",
13
+ // QForm emits submit with the DOM event; the form's .prevent needs it.
14
+ template:
15
+ "<form @submit.prevent=\"$emit('submit', $event)\"><slot /></form>",
14
16
  emits: ["submit"],
15
17
  },
16
18
  "q-input": {
@@ -68,6 +70,61 @@ describe("SidRegistrationForm", () => {
68
70
  expect(inputs.length).toBeGreaterThanOrEqual(4);
69
71
  });
70
72
 
73
+ // While the installation has no administrator the claim token is the only
74
+ // admission: the form asks for it and never for an invite code.
75
+ it("asks for the claim token instead of an invite while unclaimed", () => {
76
+ const w = mountForm({ claimRequired: true, inviteRequired: true });
77
+ expect(w.text()).toContain("This installation has no administrator yet");
78
+ expect(w.text()).not.toContain("Registration requires an invite code");
79
+ // claim token, principal, password, confirm; no invite field
80
+ expect(w.findAll("input")).toHaveLength(4);
81
+ });
82
+
83
+ it("does not ask for the claim token once claimed", () => {
84
+ const w = mountForm({ claimRequired: false });
85
+ expect(w.text()).not.toContain(
86
+ "This installation has no administrator yet",
87
+ );
88
+ // principal, password, confirm
89
+ expect(w.findAll("input")).toHaveLength(3);
90
+ });
91
+
92
+ // The pasted token reaches registerFn trimmed, and no invite code with it.
93
+ it("submits the trimmed claim token", async () => {
94
+ registerFn.mockClear();
95
+ const w = mountForm({ claimRequired: true });
96
+ const [claim, principal, password, confirm] = w.findAll("input");
97
+ await claim.setValue(" sidclaim_abc \n");
98
+ await principal.setValue("owner@example.com");
99
+ await password.setValue("Secret-Pass-123");
100
+ await confirm.setValue("Secret-Pass-123");
101
+ await w.find("form").trigger("submit");
102
+
103
+ expect(registerFn).toHaveBeenCalledWith(
104
+ "owner@example.com",
105
+ "Secret-Pass-123",
106
+ { claimToken: "sidclaim_abc" },
107
+ expect.any(Function),
108
+ );
109
+ });
110
+
111
+ it("submits the invite code when the installation is claimed", async () => {
112
+ registerFn.mockClear();
113
+ const w = mountForm({ initialInviteCode: "ABCD1234" });
114
+ const [, principal, password, confirm] = w.findAll("input");
115
+ await principal.setValue("user@example.com");
116
+ await password.setValue("Secret-Pass-123");
117
+ await confirm.setValue("Secret-Pass-123");
118
+ await w.find("form").trigger("submit");
119
+
120
+ expect(registerFn).toHaveBeenCalledWith(
121
+ "user@example.com",
122
+ "Secret-Pass-123",
123
+ { inviteCode: "ABCD1234" },
124
+ expect.any(Function),
125
+ );
126
+ });
127
+
71
128
  it("renders footer slot", () => {
72
129
  const w = mount(SidRegistrationForm, {
73
130
  props: { registerFn },
@@ -11,7 +11,17 @@
11
11
 
12
12
  <q-card-section>
13
13
  <q-banner
14
- v-if="inviteRequired && !inviteCode"
14
+ v-if="claimRequired"
15
+ class="bg-info text-white q-mb-md"
16
+ rounded
17
+ dense
18
+ icon="sym_o_admin_panel_settings"
19
+ >
20
+ {{ claimRequiredText }}
21
+ </q-banner>
22
+
23
+ <q-banner
24
+ v-if="inviteRequired && !inviteCode && !claimRequired"
15
25
  class="bg-warning text-dark q-mb-md"
16
26
  rounded
17
27
  dense
@@ -38,7 +48,48 @@
38
48
  {{ successText }}
39
49
  </q-banner>
40
50
 
51
+ <slot name="progress" :progress="progress">
52
+ <div v-if="progress" class="q-mb-md">
53
+ <q-linear-progress
54
+ :value="progress.fraction"
55
+ color="primary"
56
+ class="q-mb-sm"
57
+ />
58
+ <div class="text-caption text-center text-grey-7">
59
+ <q-icon name="sym_o_lock" size="xs" class="q-mr-xs" />
60
+ {{ progress.label }}
61
+ </div>
62
+ </div>
63
+ </slot>
64
+
41
65
  <q-form @submit.prevent="onSubmit" class="q-gutter-y-md">
66
+ <q-input
67
+ v-if="claimRequired"
68
+ v-model="claimToken"
69
+ :type="showClaimToken ? 'text' : 'password'"
70
+ :label="claimTokenLabel"
71
+ :hint="claimTokenHint"
72
+ outlined
73
+ autocomplete="off"
74
+ spellcheck="false"
75
+ :disable="loading"
76
+ :rules="[(v: string) => !!v.trim() || claimTokenRequiredText]"
77
+ data-test="claim-token"
78
+ >
79
+ <template v-slot:prepend>
80
+ <q-icon name="sym_o_key" />
81
+ </template>
82
+ <template v-slot:append>
83
+ <q-icon
84
+ :name="
85
+ showClaimToken ? 'sym_o_visibility_off' : 'sym_o_visibility'
86
+ "
87
+ class="cursor-pointer"
88
+ @click="showClaimToken = !showClaimToken"
89
+ />
90
+ </template>
91
+ </q-input>
92
+
42
93
  <q-input
43
94
  v-if="showInviteField"
44
95
  v-model="inviteCode"
@@ -74,6 +125,7 @@
74
125
  :label="identifierLabel"
75
126
  :lazy-rules="false"
76
127
  :min-username-length="usernameMinLength"
128
+ data-test="principal"
77
129
  @update:principal-type="principalType = $event"
78
130
  />
79
131
 
@@ -88,6 +140,7 @@
88
140
  :disable="loading"
89
141
  :lazy-rules="false"
90
142
  :rules="passwordRules"
143
+ data-test="password"
91
144
  @update:model-value="onPasswordInput"
92
145
  >
93
146
  <template v-slot:prepend>
@@ -115,6 +168,7 @@
115
168
  (v: string) => !!v || passwordRequiredText,
116
169
  (v: string) => v === password || passwordsMismatchText,
117
170
  ]"
171
+ data-test="confirm-password"
118
172
  >
119
173
  <template v-slot:prepend>
120
174
  <q-icon name="sym_o_lock" />
@@ -131,8 +185,10 @@
131
185
  !identifier ||
132
186
  !password ||
133
187
  (showConfirmPassword && !confirmPassword) ||
134
- (inviteRequired && !inviteCode)
188
+ (claimRequired && !claimToken.trim()) ||
189
+ (inviteRequired && !claimRequired && !inviteCode)
135
190
  "
191
+ data-test="submit"
136
192
  />
137
193
  </q-form>
138
194
 
@@ -160,7 +216,21 @@ interface ValidatableInput {
160
216
  validate: () => boolean | Promise<boolean>;
161
217
  }
162
218
  import type { PrincipalType } from "../../quasar";
163
- import { normalizePrincipal } from "../../index";
219
+ import { normalizePrincipal, refusalMessage } from "../../index";
220
+
221
+ /** What the form collected besides the identifier and password. */
222
+ export interface RegistrationExtras {
223
+ /** Invite code, when the enrollment policy asks for one. */
224
+ inviteCode?: string;
225
+ /** Instance claim token from the service log, while the installation has no administrator. */
226
+ claimToken?: string;
227
+ }
228
+
229
+ /** Progress of the registration (the password proof takes seconds): 0..1 and a label. */
230
+ export interface RegistrationProgress {
231
+ fraction: number;
232
+ label: string;
233
+ }
164
234
 
165
235
  void SidPrincipalInput;
166
236
 
@@ -206,11 +276,26 @@ const props = withDefaults(
206
276
  inviteRequired?: boolean;
207
277
  /** Pre-filled invite code (e.g. from URL) */
208
278
  initialInviteCode?: string;
209
- /** External register function — receives (identifier, password, inviteCode?) */
279
+ /**
280
+ * The installation has no administrator yet (GetInstanceStatus). The form
281
+ * then asks for the claim token and the new account becomes administrator;
282
+ * the invite code is not asked for.
283
+ */
284
+ claimRequired?: boolean;
285
+ /** Claim token input label */
286
+ claimTokenLabel?: string;
287
+ /** Claim token hint text */
288
+ claimTokenHint?: string;
289
+ /** Claim token required validation text */
290
+ claimTokenRequiredText?: string;
291
+ /** Banner shown while the installation waits for its first administrator */
292
+ claimRequiredText?: string;
293
+ /** External register function; `onProgress` drives the progress gauge. */
210
294
  registerFn: (
211
295
  identifier: string,
212
296
  password: string,
213
- inviteCode?: string,
297
+ extras: RegistrationExtras,
298
+ onProgress?: (p: RegistrationProgress) => void,
214
299
  ) => Promise<void>;
215
300
  /** Min password length (default 12 — NIST SP 800-63B 4th draft baseline). */
216
301
  passwordMinLength?: number;
@@ -243,6 +328,12 @@ const props = withDefaults(
243
328
  loginUrl: undefined,
244
329
  inviteRequired: false,
245
330
  initialInviteCode: "",
331
+ claimRequired: false,
332
+ claimTokenLabel: "Claim token",
333
+ claimTokenHint: "The sidclaim_ token from the service log",
334
+ claimTokenRequiredText: "Claim token required",
335
+ claimRequiredText:
336
+ "This installation has no administrator yet. Enter the claim token from the service log; the account you create becomes its administrator.",
246
337
  showConfirmPassword: true,
247
338
  usernameMinLength: 6,
248
339
  },
@@ -261,6 +352,7 @@ const showPassword = ref(false);
261
352
  const loading = ref(false);
262
353
  const error = ref<string | null>(null);
263
354
  const success = ref(false);
355
+ const progress = ref<RegistrationProgress | null>(null);
264
356
 
265
357
  const passwordRef = ref<ValidatableInput | null>(null);
266
358
  const confirmRef = ref<ValidatableInput | null>(null);
@@ -289,10 +381,14 @@ function onPasswordInput() {
289
381
 
290
382
  const inviteCode = ref(props.initialInviteCode);
291
383
  const inviteFromUrl = ref(!!props.initialInviteCode);
384
+ // While unclaimed the claim token is the only admission, whatever the mode.
292
385
  const showInviteField = computed(
293
- () => props.inviteRequired || !!inviteCode.value,
386
+ () => !props.claimRequired && (props.inviteRequired || !!inviteCode.value),
294
387
  );
295
388
 
389
+ const claimToken = ref("");
390
+ const showClaimToken = ref(false);
391
+
296
392
  async function onSubmit() {
297
393
  if (props.showConfirmPassword && password.value !== confirmPassword.value) {
298
394
  error.value = props.passwordsMismatchText;
@@ -307,11 +403,12 @@ async function onSubmit() {
307
403
  identifier.value,
308
404
  props.usernameMinLength,
309
405
  );
310
- await props.registerFn(
311
- normalized.value,
312
- password.value,
313
- inviteCode.value || undefined,
314
- );
406
+ const extras: RegistrationExtras = props.claimRequired
407
+ ? { claimToken: claimToken.value.trim() }
408
+ : { inviteCode: inviteCode.value || undefined };
409
+ await props.registerFn(normalized.value, password.value, extras, (p) => {
410
+ progress.value = p;
411
+ });
315
412
  success.value = true;
316
413
  emit("success", {
317
414
  identifier: normalized.value,
@@ -320,10 +417,11 @@ async function onSubmit() {
320
417
  } catch (e) {
321
418
  success.value = false;
322
419
  const err = e instanceof Error ? e : new Error("Registration failed");
323
- error.value = err.message;
420
+ error.value = refusalMessage(e, "Registration failed");
324
421
  emit("error", err);
325
422
  } finally {
326
423
  loading.value = false;
424
+ progress.value = null;
327
425
  }
328
426
  }
329
427
  </script>
@@ -1,6 +1,10 @@
1
1
  // Slot-based auth forms
2
2
  export { default as SidLoginForm } from "./SidLoginForm.vue";
3
3
  export { default as SidRegistrationForm } from "./SidRegistrationForm.vue";
4
+ export type {
5
+ RegistrationExtras,
6
+ RegistrationProgress,
7
+ } from "./SidRegistrationForm.vue";
4
8
  export { default as SidMfaChallenge } from "./SidMfaChallenge.vue";
5
9
  export type { MfaMethod } from "./SidMfaChallenge.vue";
6
10
 
@@ -1,6 +1,7 @@
1
1
  export * from "./useAuthApi";
2
- export * from "./useOpaqueAuth";
2
+ export * from "./useAuth";
3
3
  export * from "./useLoginSuccess";
4
4
  export * from "./useAccountApi";
5
+ export * from "./useEnrollmentApi";
5
6
  export * from "./useIdentityApi";
6
7
  export * from "./useSecurityLevel";
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * Profile account API composable.
3
3
  *
4
- * Wraps AccountServiceClient proto calls with Bearer auth.
4
+ * The account API through the BFF (`getAccountTransport`), which adds the
5
+ * session's Bearer token.
5
6
  * Covers: consents, activity, linked accounts, app launcher, passwordless.
6
7
  */
7
8
  import {
8
- getTransport,
9
+ getAccountTransport,
9
10
  AccountServiceClient,
10
11
  type ConsentInfo,
11
12
  type ClaimConsent,
@@ -31,7 +32,7 @@ import {
31
32
  import { authMeta } from "../auth";
32
33
 
33
34
  function client(): AccountServiceClient {
34
- return new AccountServiceClient(getTransport());
35
+ return new AccountServiceClient(getAccountTransport());
35
36
  }
36
37
 
37
38
  // ── Consents ──
@@ -129,7 +130,10 @@ export async function reportSuspiciousEvent(
129
130
  export type { LinkedAccount, ListLinkedAccountsResponse };
130
131
 
131
132
  export async function listLinkedAccounts(): Promise<ListLinkedAccountsResponse> {
132
- const { response } = await client().listLinkedAccounts({}, { meta: authMeta() });
133
+ const { response } = await client().listLinkedAccounts(
134
+ {},
135
+ { meta: authMeta() },
136
+ );
133
137
  return response;
134
138
  }
135
139
 
@@ -164,12 +168,18 @@ export async function reauthenticateLink(
164
168
  export type { AvailableApp, PasswordlessStatus, DisablePasswordlessResponse };
165
169
 
166
170
  export async function listAvailableApps(): Promise<AvailableApp[]> {
167
- const { response } = await client().listAvailableApps({}, { meta: authMeta() });
171
+ const { response } = await client().listAvailableApps(
172
+ {},
173
+ { meta: authMeta() },
174
+ );
168
175
  return response.apps;
169
176
  }
170
177
 
171
178
  export async function launchApp(appId: string): Promise<LaunchAppResponse> {
172
- const { response } = await client().launchApp({ appId }, { meta: authMeta() });
179
+ const { response } = await client().launchApp(
180
+ { appId },
181
+ { meta: authMeta() },
182
+ );
173
183
  return response;
174
184
  }
175
185
 
@@ -188,7 +198,10 @@ export async function enablePasswordless(): Promise<void> {
188
198
  }
189
199
 
190
200
  export async function disablePasswordless(): Promise<DisablePasswordlessResponse> {
191
- const { response } = await client().disablePasswordless({}, { meta: authMeta() });
201
+ const { response } = await client().disablePasswordless(
202
+ {},
203
+ { meta: authMeta() },
204
+ );
192
205
  return response;
193
206
  }
194
207