@structured-id/ui-core 1.1.0 → 1.3.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 +118 -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"
@@ -73,6 +124,8 @@
73
124
  :disable="loading"
74
125
  :label="identifierLabel"
75
126
  :lazy-rules="false"
127
+ :min-username-length="usernameMinLength"
128
+ data-test="principal"
76
129
  @update:principal-type="principalType = $event"
77
130
  />
78
131
 
@@ -87,6 +140,7 @@
87
140
  :disable="loading"
88
141
  :lazy-rules="false"
89
142
  :rules="passwordRules"
143
+ data-test="password"
90
144
  @update:model-value="onPasswordInput"
91
145
  >
92
146
  <template v-slot:prepend>
@@ -114,6 +168,7 @@
114
168
  (v: string) => !!v || passwordRequiredText,
115
169
  (v: string) => v === password || passwordsMismatchText,
116
170
  ]"
171
+ data-test="confirm-password"
117
172
  >
118
173
  <template v-slot:prepend>
119
174
  <q-icon name="sym_o_lock" />
@@ -130,8 +185,10 @@
130
185
  !identifier ||
131
186
  !password ||
132
187
  (showConfirmPassword && !confirmPassword) ||
133
- (inviteRequired && !inviteCode)
188
+ (claimRequired && !claimToken.trim()) ||
189
+ (inviteRequired && !claimRequired && !inviteCode)
134
190
  "
191
+ data-test="submit"
135
192
  />
136
193
  </q-form>
137
194
 
@@ -159,7 +216,21 @@ interface ValidatableInput {
159
216
  validate: () => boolean | Promise<boolean>;
160
217
  }
161
218
  import type { PrincipalType } from "../../quasar";
162
- 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
+ }
163
234
 
164
235
  void SidPrincipalInput;
165
236
 
@@ -205,16 +276,34 @@ const props = withDefaults(
205
276
  inviteRequired?: boolean;
206
277
  /** Pre-filled invite code (e.g. from URL) */
207
278
  initialInviteCode?: string;
208
- /** 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. */
209
294
  registerFn: (
210
295
  identifier: string,
211
296
  password: string,
212
- inviteCode?: string,
297
+ extras: RegistrationExtras,
298
+ onProgress?: (p: RegistrationProgress) => void,
213
299
  ) => Promise<void>;
214
300
  /** Min password length (default 12 — NIST SP 800-63B 4th draft baseline). */
215
301
  passwordMinLength?: number;
216
302
  /** Show confirm-password field with match validation. Default true. */
217
303
  showConfirmPassword?: boolean;
304
+ /** Min username length. Registration default 6 (premium feature gating);
305
+ * login flow can pass 3 since short usernames are valid post-registration. */
306
+ usernameMinLength?: number;
218
307
  }>(),
219
308
  {
220
309
  title: "Create account",
@@ -239,7 +328,14 @@ const props = withDefaults(
239
328
  loginUrl: undefined,
240
329
  inviteRequired: false,
241
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.",
242
337
  showConfirmPassword: true,
338
+ usernameMinLength: 6,
243
339
  },
244
340
  );
245
341
 
@@ -256,6 +352,7 @@ const showPassword = ref(false);
256
352
  const loading = ref(false);
257
353
  const error = ref<string | null>(null);
258
354
  const success = ref(false);
355
+ const progress = ref<RegistrationProgress | null>(null);
259
356
 
260
357
  const passwordRef = ref<ValidatableInput | null>(null);
261
358
  const confirmRef = ref<ValidatableInput | null>(null);
@@ -284,10 +381,14 @@ function onPasswordInput() {
284
381
 
285
382
  const inviteCode = ref(props.initialInviteCode);
286
383
  const inviteFromUrl = ref(!!props.initialInviteCode);
384
+ // While unclaimed the claim token is the only admission, whatever the mode.
287
385
  const showInviteField = computed(
288
- () => props.inviteRequired || !!inviteCode.value,
386
+ () => !props.claimRequired && (props.inviteRequired || !!inviteCode.value),
289
387
  );
290
388
 
389
+ const claimToken = ref("");
390
+ const showClaimToken = ref(false);
391
+
291
392
  async function onSubmit() {
292
393
  if (props.showConfirmPassword && password.value !== confirmPassword.value) {
293
394
  error.value = props.passwordsMismatchText;
@@ -298,12 +399,16 @@ async function onSubmit() {
298
399
  error.value = null;
299
400
 
300
401
  try {
301
- const normalized = normalizePrincipal(identifier.value);
302
- await props.registerFn(
303
- normalized.value,
304
- password.value,
305
- inviteCode.value || undefined,
402
+ const normalized = normalizePrincipal(
403
+ identifier.value,
404
+ props.usernameMinLength,
306
405
  );
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
+ });
307
412
  success.value = true;
308
413
  emit("success", {
309
414
  identifier: normalized.value,
@@ -312,10 +417,11 @@ async function onSubmit() {
312
417
  } catch (e) {
313
418
  success.value = false;
314
419
  const err = e instanceof Error ? e : new Error("Registration failed");
315
- error.value = err.message;
420
+ error.value = refusalMessage(e, "Registration failed");
316
421
  emit("error", err);
317
422
  } finally {
318
423
  loading.value = false;
424
+ progress.value = null;
319
425
  }
320
426
  }
321
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