@structured-id/ui-core 0.1.5

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 (156) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +47 -0
  3. package/dist/browser-vp/index.cjs +349 -0
  4. package/dist/browser-vp/index.d.cts +287 -0
  5. package/dist/browser-vp/index.d.ts +287 -0
  6. package/dist/browser-vp/index.js +349 -0
  7. package/dist/chunk-C3Q766CY.cjs +125 -0
  8. package/dist/chunk-J5YXRXB5.js +125 -0
  9. package/dist/index.cjs +19360 -0
  10. package/dist/index.d.cts +9536 -0
  11. package/dist/index.d.ts +9536 -0
  12. package/dist/index.js +19360 -0
  13. package/dist/storage-4CB2JTLW.cjs +22 -0
  14. package/dist/storage-THRWREZP.js +22 -0
  15. package/package.json +93 -0
  16. package/src/browser-vp/crypto.ts +254 -0
  17. package/src/browser-vp/index.test.ts +10 -0
  18. package/src/browser-vp/index.ts +118 -0
  19. package/src/browser-vp/prf.ts +181 -0
  20. package/src/browser-vp/storage.test.ts +9 -0
  21. package/src/browser-vp/storage.ts +165 -0
  22. package/src/browser-vp/types.ts +97 -0
  23. package/src/browser-vp/vp.test.ts +43 -0
  24. package/src/browser-vp/vp.ts +113 -0
  25. package/src/components/SidAvatar.vue +106 -0
  26. package/src/components/SidConfirmDialog.vue +125 -0
  27. package/src/components/SidStatusBadge.vue +71 -0
  28. package/src/components/index.ts +5 -0
  29. package/src/composables/index.ts +10 -0
  30. package/src/composables/useAuth.ts +40 -0
  31. package/src/composables/useBffAuth.ts +147 -0
  32. package/src/composables/useGrpc.test.ts +196 -0
  33. package/src/composables/useGrpc.ts +120 -0
  34. package/src/composables/useSession.ts +80 -0
  35. package/src/generated/google/api/http.ts +687 -0
  36. package/src/generated/google/protobuf/descriptor.ts +4865 -0
  37. package/src/generated/google/protobuf/duration.ts +230 -0
  38. package/src/generated/google/protobuf/empty.ts +94 -0
  39. package/src/generated/google/protobuf/struct.ts +484 -0
  40. package/src/generated/google/protobuf/timestamp.ts +287 -0
  41. package/src/generated/sid/v1/account/account.client.ts +527 -0
  42. package/src/generated/sid/v1/account/account.ts +3261 -0
  43. package/src/generated/sid/v1/admin/admin.client.ts +136 -0
  44. package/src/generated/sid/v1/admin/admin.ts +846 -0
  45. package/src/generated/sid/v1/admin/admin_roles.client.ts +181 -0
  46. package/src/generated/sid/v1/admin/admin_roles.ts +1606 -0
  47. package/src/generated/sid/v1/admin/analytics.client.ts +106 -0
  48. package/src/generated/sid/v1/admin/analytics.ts +1017 -0
  49. package/src/generated/sid/v1/admin/branding.client.ts +166 -0
  50. package/src/generated/sid/v1/admin/branding.ts +1906 -0
  51. package/src/generated/sid/v1/admin/enrollment.client.ts +165 -0
  52. package/src/generated/sid/v1/admin/enrollment.ts +1704 -0
  53. package/src/generated/sid/v1/admin/events.client.ts +195 -0
  54. package/src/generated/sid/v1/admin/events.ts +1900 -0
  55. package/src/generated/sid/v1/admin/flow.client.ts +183 -0
  56. package/src/generated/sid/v1/admin/flow.ts +1552 -0
  57. package/src/generated/sid/v1/admin/keys.client.ts +107 -0
  58. package/src/generated/sid/v1/admin/keys.ts +931 -0
  59. package/src/generated/sid/v1/admin/login_experience.client.ts +160 -0
  60. package/src/generated/sid/v1/admin/login_experience.ts +1061 -0
  61. package/src/generated/sid/v1/admin/migration.client.ts +127 -0
  62. package/src/generated/sid/v1/admin/migration.ts +1948 -0
  63. package/src/generated/sid/v1/admin/notifications.client.ts +128 -0
  64. package/src/generated/sid/v1/admin/notifications.ts +1332 -0
  65. package/src/generated/sid/v1/admin/operations.client.ts +241 -0
  66. package/src/generated/sid/v1/admin/operations.ts +2648 -0
  67. package/src/generated/sid/v1/admin/organizations.client.ts +57 -0
  68. package/src/generated/sid/v1/admin/organizations.ts +349 -0
  69. package/src/generated/sid/v1/admin/scopes.client.ts +171 -0
  70. package/src/generated/sid/v1/admin/scopes.ts +1324 -0
  71. package/src/generated/sid/v1/admin/security.client.ts +412 -0
  72. package/src/generated/sid/v1/admin/security.ts +4086 -0
  73. package/src/generated/sid/v1/admin/sessions.client.ts +114 -0
  74. package/src/generated/sid/v1/admin/sessions.ts +923 -0
  75. package/src/generated/sid/v1/admin/settings.client.ts +340 -0
  76. package/src/generated/sid/v1/admin/settings.ts +2395 -0
  77. package/src/generated/sid/v1/admin/user_profile.client.ts +57 -0
  78. package/src/generated/sid/v1/admin/user_profile.ts +730 -0
  79. package/src/generated/sid/v1/admin/webhooks.client.ts +269 -0
  80. package/src/generated/sid/v1/admin/webhooks.ts +2804 -0
  81. package/src/generated/sid/v1/attestation/attestation.client.ts +115 -0
  82. package/src/generated/sid/v1/attestation/attestation.ts +813 -0
  83. package/src/generated/sid/v1/authn/auth.client.ts +900 -0
  84. package/src/generated/sid/v1/authn/auth.ts +9287 -0
  85. package/src/generated/sid/v1/authn/flows.client.ts +413 -0
  86. package/src/generated/sid/v1/authn/flows.ts +3217 -0
  87. package/src/generated/sid/v1/authz/authz.client.ts +459 -0
  88. package/src/generated/sid/v1/authz/authz.ts +5343 -0
  89. package/src/generated/sid/v1/authz/governance.client.ts +102 -0
  90. package/src/generated/sid/v1/authz/governance.ts +818 -0
  91. package/src/generated/sid/v1/common/errors.ts +184 -0
  92. package/src/generated/sid/v1/common/pagination.ts +152 -0
  93. package/src/generated/sid/v1/common/transport.ts +227 -0
  94. package/src/generated/sid/v1/common/types.ts +151 -0
  95. package/src/generated/sid/v1/events/event_stream.client.ts +67 -0
  96. package/src/generated/sid/v1/events/event_stream.ts +374 -0
  97. package/src/generated/sid/v1/federation/upstream.client.ts +142 -0
  98. package/src/generated/sid/v1/federation/upstream.ts +1736 -0
  99. package/src/generated/sid/v1/identity/identity.client.ts +467 -0
  100. package/src/generated/sid/v1/identity/identity.ts +5613 -0
  101. package/src/generated/sid/v1/machine/machine_user.client.ts +219 -0
  102. package/src/generated/sid/v1/machine/machine_user.ts +2400 -0
  103. package/src/generated/sid/v1/machine/pat.client.ts +147 -0
  104. package/src/generated/sid/v1/machine/pat.ts +1268 -0
  105. package/src/generated/sid/v1/projects/projects.client.ts +286 -0
  106. package/src/generated/sid/v1/projects/projects.ts +2714 -0
  107. package/src/generated/sid/v1/scim/scim.client.ts +274 -0
  108. package/src/generated/sid/v1/scim/scim.ts +3043 -0
  109. package/src/generated/sid/v1/test/test.client.ts +232 -0
  110. package/src/generated/sid/v1/test/test.ts +1136 -0
  111. package/src/i18n/index.ts +40 -0
  112. package/src/index.ts +157 -0
  113. package/src/profile/auth.ts +15 -0
  114. package/src/profile/components/ConsentManager.vue +88 -0
  115. package/src/profile/components/ProfileEditor.vue +146 -0
  116. package/src/profile/components/SecuritySettings.vue +170 -0
  117. package/src/profile/components/SessionList.vue +100 -0
  118. package/src/profile/components/SidLoginForm.test.ts +80 -0
  119. package/src/profile/components/SidLoginForm.vue +230 -0
  120. package/src/profile/components/SidMfaChallenge.test.ts +127 -0
  121. package/src/profile/components/SidMfaChallenge.vue +243 -0
  122. package/src/profile/components/SidRegistrationForm.test.ts +79 -0
  123. package/src/profile/components/SidRegistrationForm.vue +317 -0
  124. package/src/profile/components/index.ts +11 -0
  125. package/src/profile/composables/index.ts +6 -0
  126. package/src/profile/composables/useAccountApi.ts +203 -0
  127. package/src/profile/composables/useAuthApi.opaque.test.ts +140 -0
  128. package/src/profile/composables/useAuthApi.ts +617 -0
  129. package/src/profile/composables/useIdentityApi.ts +123 -0
  130. package/src/profile/composables/useLoginSuccess.test.ts +112 -0
  131. package/src/profile/composables/useLoginSuccess.ts +65 -0
  132. package/src/profile/composables/useOpaqueAuth.test.ts +290 -0
  133. package/src/profile/composables/useOpaqueAuth.ts +191 -0
  134. package/src/profile/composables/useSecurityLevel.test.ts +198 -0
  135. package/src/profile/composables/useSecurityLevel.ts +324 -0
  136. package/src/profile/index.ts +18 -0
  137. package/src/quasar/SidPrincipalInput.vue +209 -0
  138. package/src/quasar/index.ts +8 -0
  139. package/src/stores/auth.test.ts +83 -0
  140. package/src/stores/auth.ts +92 -0
  141. package/src/stores/index.ts +1 -0
  142. package/src/transport/adaptive.test.ts +728 -0
  143. package/src/transport/adaptive.ts +308 -0
  144. package/src/transport/framing.test.ts +227 -0
  145. package/src/transport/framing.ts +98 -0
  146. package/src/transport/webtransport.test.ts +728 -0
  147. package/src/transport/webtransport.ts +323 -0
  148. package/src/types/auth.ts +58 -0
  149. package/src/types/index.ts +1 -0
  150. package/src/utils/formatters.test.ts +26 -0
  151. package/src/utils/formatters.ts +69 -0
  152. package/src/utils/index.ts +16 -0
  153. package/src/utils/normalizePrincipal.test.ts +188 -0
  154. package/src/utils/normalizePrincipal.ts +159 -0
  155. package/src/utils/validators.test.ts +75 -0
  156. package/src/utils/validators.ts +36 -0
@@ -0,0 +1,243 @@
1
+ <template>
2
+ <q-card flat bordered>
3
+ <q-card-section>
4
+ <slot name="header">
5
+ <div class="text-h6">{{ title }}</div>
6
+ <div class="text-caption text-grey">{{ subtitle }}</div>
7
+ </slot>
8
+ </q-card-section>
9
+
10
+ <q-separator />
11
+
12
+ <q-card-section>
13
+ <q-banner
14
+ v-if="error"
15
+ class="bg-negative text-white q-mb-md"
16
+ rounded
17
+ dense
18
+ >
19
+ {{ error }}
20
+ </q-banner>
21
+
22
+ <!-- Method selection -->
23
+ <template v-if="phase === 'select'">
24
+ <div class="text-center q-mb-md">
25
+ <q-icon name="sym_o_security" size="48px" color="warning" />
26
+ <div class="text-body2 text-grey-7 q-mt-sm">
27
+ {{ reason || "You need to verify your identity before continuing" }}
28
+ </div>
29
+ </div>
30
+
31
+ <div class="text-subtitle2 q-mb-sm">Verify using:</div>
32
+
33
+ <q-list separator>
34
+ <q-item
35
+ v-for="method in methods"
36
+ :key="method.type"
37
+ clickable
38
+ v-ripple
39
+ @click="selectMethod(method.type)"
40
+ >
41
+ <q-item-section avatar>
42
+ <q-icon :name="method.icon" :color="method.color" />
43
+ </q-item-section>
44
+ <q-item-section>
45
+ <q-item-label>{{ method.label }}</q-item-label>
46
+ <q-item-label caption>{{ method.description }}</q-item-label>
47
+ </q-item-section>
48
+ <q-item-section side>
49
+ <q-badge
50
+ v-if="method.recommended"
51
+ color="positive"
52
+ label="Recommended"
53
+ />
54
+ <q-icon v-else name="sym_o_chevron_right" />
55
+ </q-item-section>
56
+ </q-item>
57
+ </q-list>
58
+ </template>
59
+
60
+ <!-- TOTP code entry -->
61
+ <template v-if="phase === 'totp'">
62
+ <q-form @submit.prevent="submitCode('totp')" class="q-gutter-md">
63
+ <q-input
64
+ v-model="totpCode"
65
+ label="Authentication code"
66
+ outlined
67
+ autofocus
68
+ mask="### ###"
69
+ unmasked-value
70
+ :disable="loading"
71
+ :rules="[
72
+ (v: string) => (!!v && v.length === 6) || 'Enter 6-digit code',
73
+ ]"
74
+ input-class="text-center text-h5 letter-spacing-wide"
75
+ inputmode="numeric"
76
+ autocomplete="one-time-code"
77
+ >
78
+ <template v-slot:prepend>
79
+ <q-icon name="sym_o_timer" color="primary" />
80
+ </template>
81
+ </q-input>
82
+
83
+ <q-btn
84
+ type="submit"
85
+ label="Verify"
86
+ color="primary"
87
+ class="full-width"
88
+ :loading="loading"
89
+ :disable="!totpCode || totpCode.length < 6"
90
+ />
91
+ </q-form>
92
+
93
+ <div class="text-caption text-grey-6 text-center q-mt-md">
94
+ <q-icon name="sym_o_info" size="xs" class="q-mr-xs" />
95
+ Open your authenticator app to find the code
96
+ </div>
97
+ </template>
98
+
99
+ <!-- Recovery code entry -->
100
+ <template v-if="phase === 'recovery'">
101
+ <q-form @submit.prevent="submitCode('recovery')" class="q-gutter-md">
102
+ <q-input
103
+ v-model="recoveryCode"
104
+ label="Recovery code"
105
+ outlined
106
+ autofocus
107
+ :disable="loading"
108
+ :rules="[(v: string) => !!v || 'Enter recovery code']"
109
+ input-class="text-center text-h6 font-monospace"
110
+ >
111
+ <template v-slot:prepend>
112
+ <q-icon name="sym_o_key" color="warning" />
113
+ </template>
114
+ </q-input>
115
+
116
+ <q-btn
117
+ type="submit"
118
+ label="Verify"
119
+ color="primary"
120
+ class="full-width"
121
+ :loading="loading"
122
+ :disable="!recoveryCode"
123
+ />
124
+ </q-form>
125
+
126
+ <div class="text-caption text-grey-6 text-center q-mt-md">
127
+ <q-icon name="sym_o_warning" size="xs" class="q-mr-xs" />
128
+ Recovery codes are single-use
129
+ </div>
130
+ </template>
131
+
132
+ <!-- WebAuthn (passkey) -->
133
+ <template v-if="phase === 'webauthn'">
134
+ <div class="text-center q-pa-lg">
135
+ <q-spinner-dots v-if="loading" size="48px" color="primary" />
136
+ <q-icon v-else name="sym_o_passkey" size="48px" color="primary" />
137
+ <div class="text-body2 text-grey-7 q-mt-md">
138
+ {{
139
+ loading
140
+ ? "Waiting for authenticator\u2026"
141
+ : "Use your passkey or security key to verify"
142
+ }}
143
+ </div>
144
+ </div>
145
+ </template>
146
+
147
+ <!-- Success -->
148
+ <template v-if="phase === 'success'">
149
+ <div class="text-center q-pa-lg">
150
+ <q-icon name="sym_o_check_circle" size="48px" color="positive" />
151
+ <div class="text-body1 text-positive q-mt-sm">Verified</div>
152
+ </div>
153
+ </template>
154
+
155
+ <slot name="below-challenge" :phase="phase" />
156
+ </q-card-section>
157
+
158
+ <slot name="footer" />
159
+ </q-card>
160
+ </template>
161
+
162
+ <script setup lang="ts">
163
+ import { ref } from "vue";
164
+
165
+ export interface MfaMethod {
166
+ type: "totp" | "webauthn" | "recovery";
167
+ icon: string;
168
+ label: string;
169
+ description: string;
170
+ color: string;
171
+ recommended?: boolean;
172
+ }
173
+
174
+ type Phase = "select" | "totp" | "webauthn" | "recovery" | "success";
175
+
176
+ withDefaults(
177
+ defineProps<{
178
+ title?: string;
179
+ subtitle?: string;
180
+ reason?: string;
181
+ methods: MfaMethod[];
182
+ }>(),
183
+ {
184
+ title: "Additional verification required",
185
+ subtitle: "This action requires a higher authentication level",
186
+ reason: "",
187
+ },
188
+ );
189
+
190
+ const emit = defineEmits<{
191
+ "select-method": [method: string];
192
+ "submit-totp": [code: string];
193
+ "submit-recovery": [code: string];
194
+ "submit-webauthn": [];
195
+ verified: [];
196
+ }>();
197
+
198
+ const phase = ref<Phase>("select");
199
+ const loading = ref(false);
200
+ const error = ref<string | null>(null);
201
+ const totpCode = ref("");
202
+ const recoveryCode = ref("");
203
+
204
+ function selectMethod(method: string) {
205
+ error.value = null;
206
+ phase.value = method as Phase;
207
+ emit("select-method", method);
208
+ }
209
+
210
+ function submitCode(method: "totp" | "recovery") {
211
+ if (method === "totp") {
212
+ emit("submit-totp", totpCode.value);
213
+ } else {
214
+ emit("submit-recovery", recoveryCode.value);
215
+ }
216
+ }
217
+
218
+ /** Called by parent to set loading state */
219
+ function setLoading(val: boolean) {
220
+ loading.value = val;
221
+ }
222
+
223
+ /** Called by parent to set error */
224
+ function setError(msg: string | null) {
225
+ error.value = msg;
226
+ }
227
+
228
+ /** Called by parent to show success */
229
+ function setSuccess() {
230
+ phase.value = "success";
231
+ emit("verified");
232
+ }
233
+
234
+ /** Called by parent to reset to method selection */
235
+ function reset() {
236
+ phase.value = "select";
237
+ error.value = null;
238
+ totpCode.value = "";
239
+ recoveryCode.value = "";
240
+ }
241
+
242
+ defineExpose({ setLoading, setError, setSuccess, reset, phase });
243
+ </script>
@@ -0,0 +1,79 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { mount } from "@vue/test-utils";
3
+ import SidRegistrationForm from "./SidRegistrationForm.vue";
4
+
5
+ const quasarStubs = {
6
+ "q-card": { template: "<div><slot /></div>" },
7
+ "q-card-section": { template: "<div><slot /></div>" },
8
+ "q-separator": { template: "<hr />" },
9
+ "q-banner": { template: "<div><slot /></div>" },
10
+ "q-icon": { template: "<span />" },
11
+ "q-chip": { template: "<span />" },
12
+ "q-form": {
13
+ template: "<form @submit.prevent=\"$emit('submit')\"><slot /></form>",
14
+ emits: ["submit"],
15
+ },
16
+ "q-input": {
17
+ props: ["modelValue"],
18
+ emits: ["update:modelValue"],
19
+ template:
20
+ '<input :value="modelValue" @input="$emit(\'update:modelValue\', $event.target.value)" />',
21
+ },
22
+ "q-btn": { template: '<button type="submit"><slot /></button>' },
23
+ "sid-principal-input": {
24
+ props: ["modelValue", "disable", "label"],
25
+ emits: ["update:modelValue", "update:principal-type"],
26
+ template:
27
+ '<input :value="modelValue" @input="$emit(\'update:modelValue\', $event.target.value)" />',
28
+ },
29
+ };
30
+
31
+ const registerFn = vi.fn().mockResolvedValue(undefined);
32
+
33
+ function mountForm(props = {}) {
34
+ return mount(SidRegistrationForm, {
35
+ props: { registerFn, ...props },
36
+ global: { stubs: quasarStubs },
37
+ });
38
+ }
39
+
40
+ describe("SidRegistrationForm", () => {
41
+ it("renders with default title", () => {
42
+ const w = mountForm();
43
+ expect(w.text()).toContain("Create account");
44
+ });
45
+
46
+ it("renders custom title", () => {
47
+ const w = mountForm({ title: "Join us" });
48
+ expect(w.text()).toContain("Join us");
49
+ });
50
+
51
+ it("shows invite warning when required and no code", () => {
52
+ const w = mountForm({ inviteRequired: true });
53
+ expect(w.text()).toContain("Registration requires an invite code");
54
+ });
55
+
56
+ it("hides invite field when not required and no code", () => {
57
+ const w = mountForm({ inviteRequired: false });
58
+ expect(w.text()).not.toContain("Invite code");
59
+ });
60
+
61
+ it("shows invite field when code provided", () => {
62
+ const w = mountForm({ initialInviteCode: "ABCD1234" });
63
+ // showInviteField computed = true when code exists → invite input rendered
64
+ // q-input stub renders as <input>, look for it in the rendered HTML
65
+ const inputs = w.findAll("input");
66
+ // At minimum: invite, principal, password, confirm = 4 inputs.
67
+ // (given_name dropped — handled via OrgClaimPolicy after first login.)
68
+ expect(inputs.length).toBeGreaterThanOrEqual(4);
69
+ });
70
+
71
+ it("renders footer slot", () => {
72
+ const w = mount(SidRegistrationForm, {
73
+ props: { registerFn },
74
+ global: { stubs: quasarStubs },
75
+ slots: { footer: "<div>Sign in instead</div>" },
76
+ });
77
+ expect(w.text()).toContain("Sign in instead");
78
+ });
79
+ });
@@ -0,0 +1,317 @@
1
+ <template>
2
+ <q-card flat bordered>
3
+ <q-card-section>
4
+ <slot name="header">
5
+ <div class="text-h6">{{ title }}</div>
6
+ <div class="text-caption text-grey">{{ subtitle }}</div>
7
+ </slot>
8
+ </q-card-section>
9
+
10
+ <q-separator />
11
+
12
+ <q-card-section>
13
+ <q-banner
14
+ v-if="inviteRequired && !inviteCode"
15
+ class="bg-warning text-dark q-mb-md"
16
+ rounded
17
+ dense
18
+ icon="sym_o_lock"
19
+ >
20
+ {{ inviteRequiredText }}
21
+ </q-banner>
22
+
23
+ <q-banner
24
+ v-if="error"
25
+ class="bg-negative text-white q-mb-md"
26
+ rounded
27
+ dense
28
+ >
29
+ {{ error }}
30
+ </q-banner>
31
+
32
+ <q-banner
33
+ v-if="success"
34
+ class="bg-positive text-white q-mb-md"
35
+ rounded
36
+ dense
37
+ >
38
+ {{ successText }}
39
+ </q-banner>
40
+
41
+ <q-form @submit.prevent="onSubmit" class="q-gutter-y-md">
42
+ <q-input
43
+ v-if="showInviteField"
44
+ v-model="inviteCode"
45
+ :label="inviteCodeLabel"
46
+ outlined
47
+ :disable="loading || inviteFromUrl"
48
+ maxlength="8"
49
+ mask="XXXXXXXX"
50
+ :rules="
51
+ inviteRequired ? [(v: string) => !!v || inviteCodeRequiredText] : []
52
+ "
53
+ :hint="inviteCodeHint"
54
+ >
55
+ <template v-slot:prepend>
56
+ <q-icon name="sym_o_vpn_key" />
57
+ </template>
58
+ <template v-slot:append>
59
+ <q-chip
60
+ v-if="inviteFromUrl"
61
+ color="positive"
62
+ text-color="white"
63
+ dense
64
+ size="sm"
65
+ icon="sym_o_link"
66
+ :label="inviteFromUrlLabel"
67
+ />
68
+ </template>
69
+ </q-input>
70
+
71
+ <sid-principal-input
72
+ v-model="identifier"
73
+ :disable="loading"
74
+ :label="identifierLabel"
75
+ :lazy-rules="false"
76
+ @update:principal-type="principalType = $event"
77
+ />
78
+
79
+ <slot name="extra-fields" />
80
+
81
+ <q-input
82
+ ref="passwordRef"
83
+ v-model="password"
84
+ :type="showPassword ? 'text' : 'password'"
85
+ :label="passwordLabel"
86
+ outlined
87
+ :disable="loading"
88
+ :lazy-rules="false"
89
+ :rules="passwordRules"
90
+ @update:model-value="onPasswordInput"
91
+ >
92
+ <template v-slot:prepend>
93
+ <q-icon name="sym_o_lock" />
94
+ </template>
95
+ <template v-slot:append>
96
+ <q-icon
97
+ :name="showPassword ? 'sym_o_visibility_off' : 'sym_o_visibility'"
98
+ class="cursor-pointer"
99
+ @click="showPassword = !showPassword"
100
+ />
101
+ </template>
102
+ </q-input>
103
+
104
+ <q-input
105
+ ref="confirmRef"
106
+ v-model="confirmPassword"
107
+ :type="showPassword ? 'text' : 'password'"
108
+ :label="confirmPasswordLabel"
109
+ outlined
110
+ :disable="loading"
111
+ :lazy-rules="false"
112
+ :rules="[
113
+ (v: string) => !!v || passwordRequiredText,
114
+ (v: string) => v === password || passwordsMismatchText,
115
+ ]"
116
+ >
117
+ <template v-slot:prepend>
118
+ <q-icon name="sym_o_lock" />
119
+ </template>
120
+ </q-input>
121
+
122
+ <q-btn
123
+ type="submit"
124
+ :label="submitLabel"
125
+ color="primary"
126
+ class="full-width"
127
+ :loading="loading"
128
+ :disable="
129
+ !identifier ||
130
+ !password ||
131
+ !confirmPassword ||
132
+ (inviteRequired && !inviteCode)
133
+ "
134
+ />
135
+ </q-form>
136
+
137
+ <slot name="links">
138
+ <div v-if="loginUrl" class="text-center q-mt-md text-body2">
139
+ {{ haveAccountText }}
140
+ <router-link :to="loginUrl" class="text-primary">
141
+ {{ loginLinkText }}
142
+ </router-link>
143
+ </div>
144
+ </slot>
145
+ </q-card-section>
146
+
147
+ <slot name="footer" />
148
+ </q-card>
149
+ </template>
150
+
151
+ <script setup lang="ts">
152
+ import { ref, computed, watch } from "vue";
153
+ import { SidPrincipalInput } from "../../quasar";
154
+
155
+ // Minimal QInput shape — we only use validate(). Avoids importing 'quasar' at
156
+ // type level (it's a peer dep, not present during ui-core typecheck).
157
+ interface ValidatableInput {
158
+ validate: () => boolean | Promise<boolean>;
159
+ }
160
+ import type { PrincipalType } from "../../quasar";
161
+ import { normalizePrincipal } from "../../index";
162
+
163
+ void SidPrincipalInput;
164
+
165
+ const props = withDefaults(
166
+ defineProps<{
167
+ /** Card title */
168
+ title?: string;
169
+ /** Card subtitle */
170
+ subtitle?: string;
171
+ /** Submit button label */
172
+ submitLabel?: string;
173
+ /** Identifier input label */
174
+ identifierLabel?: string;
175
+ /** Password input label */
176
+ passwordLabel?: string;
177
+ /** Confirm password input label */
178
+ confirmPasswordLabel?: string;
179
+ /** Password required validation text */
180
+ passwordRequiredText?: string;
181
+ /** Password min length validation text */
182
+ passwordMinLengthText?: string;
183
+ /** Passwords mismatch validation text */
184
+ passwordsMismatchText?: string;
185
+ /** Invite code input label */
186
+ inviteCodeLabel?: string;
187
+ /** Invite code required validation text */
188
+ inviteCodeRequiredText?: string;
189
+ /** Invite code hint text */
190
+ inviteCodeHint?: string;
191
+ /** Invite "from link" chip label */
192
+ inviteFromUrlLabel?: string;
193
+ /** Invite required banner text */
194
+ inviteRequiredText?: string;
195
+ /** Success banner text */
196
+ successText?: string;
197
+ /** "Already have an account?" text */
198
+ haveAccountText?: string;
199
+ /** "Sign in" link text */
200
+ loginLinkText?: string;
201
+ /** Route to login page (if set, shows login link) */
202
+ loginUrl?: string;
203
+ /** Whether invite code is required */
204
+ inviteRequired?: boolean;
205
+ /** Pre-filled invite code (e.g. from URL) */
206
+ initialInviteCode?: string;
207
+ /** External register function — receives (identifier, password, inviteCode?) */
208
+ registerFn: (
209
+ identifier: string,
210
+ password: string,
211
+ inviteCode?: string,
212
+ ) => Promise<void>;
213
+ /** Min password length (default 12 — NIST SP 800-63B 4th draft baseline). */
214
+ passwordMinLength?: number;
215
+ }>(),
216
+ {
217
+ title: "Create account",
218
+ subtitle: "Set up your StructuredID account",
219
+ submitLabel: "Create account",
220
+ identifierLabel: "Email, phone, or username",
221
+ passwordLabel: "Password",
222
+ confirmPasswordLabel: "Confirm password",
223
+ passwordRequiredText: "Required",
224
+ passwordMinLengthText: "At least 12 characters with mixed case + digit",
225
+ passwordMinLength: 12,
226
+ passwordsMismatchText: "Passwords do not match",
227
+ inviteCodeLabel: "Invite code",
228
+ inviteCodeRequiredText: "Invite code required",
229
+ inviteCodeHint: "8-character code from your administrator",
230
+ inviteFromUrlLabel: "From link",
231
+ inviteRequiredText:
232
+ "Registration requires an invite code. Contact your administrator for access.",
233
+ successText: "Account created. Signing you in\u2026",
234
+ haveAccountText: "Already have an account?",
235
+ loginLinkText: "Sign in",
236
+ loginUrl: undefined,
237
+ inviteRequired: false,
238
+ initialInviteCode: "",
239
+ },
240
+ );
241
+
242
+ const emit = defineEmits<{
243
+ success: [data: { identifier: string; principalType: string }];
244
+ error: [error: Error];
245
+ }>();
246
+
247
+ const identifier = ref("");
248
+ const principalType = ref<PrincipalType>("unknown");
249
+ const password = ref("");
250
+ const confirmPassword = ref("");
251
+ const showPassword = ref(false);
252
+ const loading = ref(false);
253
+ const error = ref<string | null>(null);
254
+ const success = ref(false);
255
+
256
+ const passwordRef = ref<ValidatableInput | null>(null);
257
+ const confirmRef = ref<ValidatableInput | null>(null);
258
+
259
+ // Password complexity (matches sid-pake-core ZKPP gadget A defaults).
260
+ // Server enforces via OPAQUE-ZKPP zero-knowledge proof; client also checks
261
+ // to give immediate UX feedback.
262
+ const passwordRules = computed(() => [
263
+ (v: string) => !!v || props.passwordRequiredText,
264
+ (v: string) =>
265
+ v.length >= props.passwordMinLength || props.passwordMinLengthText,
266
+ (v: string) => /[a-z]/.test(v) || props.passwordMinLengthText,
267
+ (v: string) => /[A-Z]/.test(v) || props.passwordMinLengthText,
268
+ (v: string) => /[0-9]/.test(v) || props.passwordMinLengthText,
269
+ ]);
270
+
271
+ // Re-validate confirm field whenever password changes (eager match feedback).
272
+ watch(password, () => {
273
+ if (confirmPassword.value) confirmRef.value?.validate();
274
+ });
275
+
276
+ function onPasswordInput() {
277
+ // Trigger downstream confirm re-validation immediately (don't wait for blur).
278
+ if (confirmPassword.value) confirmRef.value?.validate();
279
+ }
280
+
281
+ const inviteCode = ref(props.initialInviteCode);
282
+ const inviteFromUrl = ref(!!props.initialInviteCode);
283
+ const showInviteField = computed(
284
+ () => props.inviteRequired || !!inviteCode.value,
285
+ );
286
+
287
+ async function onSubmit() {
288
+ if (password.value !== confirmPassword.value) {
289
+ error.value = props.passwordsMismatchText;
290
+ return;
291
+ }
292
+
293
+ loading.value = true;
294
+ error.value = null;
295
+
296
+ try {
297
+ const normalized = normalizePrincipal(identifier.value);
298
+ await props.registerFn(
299
+ normalized.value,
300
+ password.value,
301
+ inviteCode.value || undefined,
302
+ );
303
+ success.value = true;
304
+ emit("success", {
305
+ identifier: normalized.value,
306
+ principalType: normalized.type,
307
+ });
308
+ } catch (e) {
309
+ success.value = false;
310
+ const err = e instanceof Error ? e : new Error("Registration failed");
311
+ error.value = err.message;
312
+ emit("error", err);
313
+ } finally {
314
+ loading.value = false;
315
+ }
316
+ }
317
+ </script>
@@ -0,0 +1,11 @@
1
+ // Slot-based auth forms
2
+ export { default as SidLoginForm } from "./SidLoginForm.vue";
3
+ export { default as SidRegistrationForm } from "./SidRegistrationForm.vue";
4
+ export { default as SidMfaChallenge } from "./SidMfaChallenge.vue";
5
+ export type { MfaMethod } from "./SidMfaChallenge.vue";
6
+
7
+ // Shared page components
8
+ export { default as SessionList } from "./SessionList.vue";
9
+ export { default as ProfileEditor } from "./ProfileEditor.vue";
10
+ export { default as ConsentManager } from "./ConsentManager.vue";
11
+ export { default as SecuritySettings } from "./SecuritySettings.vue";
@@ -0,0 +1,6 @@
1
+ export * from "./useAuthApi";
2
+ export * from "./useOpaqueAuth";
3
+ export * from "./useLoginSuccess";
4
+ export * from "./useAccountApi";
5
+ export * from "./useIdentityApi";
6
+ export * from "./useSecurityLevel";