@structured-id/ui-core 1.3.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
@@ -0,0 +1,456 @@
1
+ <template>
2
+ <div
3
+ class="sid-ceremony"
4
+ :class="[`sid-ceremony--${phase}`, { 'sid-ceremony--still': still }]"
5
+ data-test="password-ceremony"
6
+ >
7
+ <!-- The refusal's reason, where the form shows it once the fields are back. -->
8
+ <transition name="sid-ceremony-fade">
9
+ <q-banner
10
+ v-if="phase === 'unfolded'"
11
+ class="bg-negative text-white q-mb-md"
12
+ rounded
13
+ dense
14
+ data-test="ceremony-reason"
15
+ >
16
+ {{ refusedText }}
17
+ </q-banner>
18
+ </transition>
19
+
20
+ <!-- One row per password field, in the fields' places: they turn into
21
+ random digits, merge into one line, settle into dots as the
22
+ operation advances, and either close into the orb or unfold back
23
+ into the fields. Nothing of the password but its length is drawn. -->
24
+ <div
25
+ class="sid-ceremony__stage"
26
+ :style="{ height: `${stageHeight}px` }"
27
+ role="progressbar"
28
+ :aria-valuenow="Math.round(fraction * 100)"
29
+ aria-valuemin="0"
30
+ aria-valuemax="100"
31
+ :aria-label="label"
32
+ >
33
+ <div
34
+ v-for="row in rows"
35
+ :key="row"
36
+ class="sid-ceremony__row"
37
+ :style="[rowStyle(row), { '--sid-ceremony-glyphs': count }]"
38
+ aria-hidden="true"
39
+ >
40
+ <span
41
+ v-for="(glyph, i) in glyphs"
42
+ :key="i"
43
+ class="sid-ceremony__glyph"
44
+ :class="{ 'sid-ceremony__glyph--set': glyph === DOT }"
45
+ >{{ glyph }}</span
46
+ >
47
+ </div>
48
+
49
+ <svg
50
+ v-if="phase === 'orb' || phase === 'done'"
51
+ class="sid-ceremony__orb"
52
+ viewBox="0 0 64 64"
53
+ aria-hidden="true"
54
+ >
55
+ <circle cx="32" cy="32" r="28" />
56
+ </svg>
57
+ <q-icon
58
+ v-if="phase === 'done'"
59
+ :name="checked ? 'sym_o_check' : 'sym_o_lock'"
60
+ size="30px"
61
+ color="primary"
62
+ class="sid-ceremony__core"
63
+ :class="{ 'sid-ceremony__core--checked': checked }"
64
+ />
65
+ </div>
66
+
67
+ <div class="sid-ceremony__label text-body2 text-center" aria-live="polite">
68
+ <transition name="sid-ceremony-fade" mode="out-in">
69
+ <span :key="label">{{ label }}</span>
70
+ </transition>
71
+ </div>
72
+ </div>
73
+ </template>
74
+
75
+ <script setup lang="ts">
76
+ import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
77
+ import type {
78
+ PasswordOperationProgress,
79
+ PasswordOperationStep,
80
+ } from "../profile/composables/useAuth";
81
+
82
+ /** How the operation the ceremony shows ended, while it runs `running`. */
83
+ export type CeremonyOutcome = "running" | "accepted" | "refused";
84
+
85
+ /**
86
+ * What is on screen: the fields' rows of digits, the merged line, the orb
87
+ * closing over a success, the rows back in the fields after a refusal.
88
+ */
89
+ type Phase = "split" | "line" | "orb" | "done" | "unfolding" | "unfolded";
90
+
91
+ const props = withDefaults(
92
+ defineProps<{
93
+ /** The operation's latest report; null before its first. */
94
+ progress: PasswordOperationProgress | null;
95
+ outcome: CeremonyOutcome;
96
+ /** Length of the password, for the rows; capped for layout. */
97
+ length: number;
98
+ /** How many password fields the form shows (new and its confirmation: 2). */
99
+ fields?: number;
100
+ /** What the ceremony says once the server accepted the password. */
101
+ acceptedText?: string;
102
+ /** The refusal's reason, shown while the rows unfold. */
103
+ refusedText?: string;
104
+ /** What it says before the operation's first report. */
105
+ startText?: string;
106
+ /** A step shorter than this never shows its own label (no flicker). */
107
+ labelDelayMs?: number;
108
+ /** Distance between two fields' tops, px (outlined field 56 + gutter 16). */
109
+ fieldPitch?: number;
110
+ }>(),
111
+ {
112
+ fields: 2,
113
+ acceptedText: "Done",
114
+ refusedText: "Not accepted",
115
+ startText: "Protecting your password on this device",
116
+ labelDelayMs: 400,
117
+ fieldPitch: 72,
118
+ },
119
+ );
120
+
121
+ const emit = defineEmits<{
122
+ /** The closing animation finished: the form may move on or come back. */
123
+ settled: [outcome: Exclude<CeremonyOutcome, "running">];
124
+ }>();
125
+
126
+ const MAX_GLYPHS = 20;
127
+ const DIGITS = "0123456789";
128
+ const DOT = "●";
129
+ /** How long each choreography step takes, ms. */
130
+ const TIMING = {
131
+ split: 700,
132
+ merge: 600,
133
+ orb: 650,
134
+ lock: 450,
135
+ check: 700,
136
+ unfold: 650,
137
+ reason: 1600,
138
+ };
139
+
140
+ const still =
141
+ typeof window !== "undefined" &&
142
+ typeof window.matchMedia === "function" &&
143
+ window.matchMedia("(prefers-reduced-motion: reduce)").matches;
144
+
145
+ const rows = computed(() =>
146
+ Array.from({ length: Math.max(1, props.fields) }, (_, i) => i),
147
+ );
148
+ const stageHeight = computed(
149
+ () => 56 + (rows.value.length - 1) * props.fieldPitch,
150
+ );
151
+ const count = computed(() => Math.max(1, Math.min(props.length, MAX_GLYPHS)));
152
+ const glyphs = ref<string[]>(Array.from({ length: count.value }, () => DOT));
153
+
154
+ const phase = ref<Phase>("split");
155
+ const checked = ref(false);
156
+ const step = computed<PasswordOperationStep | undefined>(
157
+ () => props.progress?.step,
158
+ );
159
+ /**
160
+ * Where the line is, 0..1. Between reports it moves at the pace the report
161
+ * expects for the rest of its step, so it shows the time left rather than
162
+ * jumping at each report; it stops short of the step's end until the step
163
+ * reports done, and never moves back.
164
+ */
165
+ const fraction = ref(0);
166
+ // Each report re-aims the line from where it already is: toward the step's
167
+ // end over the time the report says is left. A report that lags behind the
168
+ // line (a prover still at its first stage) does not hold it back.
169
+ let anchor = 0;
170
+ // A monotonic clock: a change of the device's wall clock does not move the line.
171
+ let reportedAt = performance.now();
172
+ watch(
173
+ () => props.progress,
174
+ (p) => {
175
+ anchor = Math.max(fraction.value, p?.fraction ?? 0);
176
+ reportedAt = performance.now();
177
+ },
178
+ // A report present at mount counts like a later one.
179
+ { immediate: true },
180
+ );
181
+ function pace() {
182
+ if (props.outcome === "accepted") {
183
+ fraction.value = 1;
184
+ return;
185
+ }
186
+ const p = props.progress;
187
+ if (!p) return;
188
+ let target = anchor;
189
+ if (p.until !== undefined && p.until > anchor && p.remainingMs) {
190
+ const share = Math.min(
191
+ 0.95,
192
+ (performance.now() - reportedAt) / p.remainingMs,
193
+ );
194
+ target = anchor + (p.until - anchor) * share;
195
+ }
196
+ fraction.value = Math.max(fraction.value, target);
197
+ }
198
+
199
+ /** Where row `i` stands: its field's place, or the merged centre line. */
200
+ function rowStyle(i: number) {
201
+ const own = i * props.fieldPitch;
202
+ const centre = ((rows.value.length - 1) * props.fieldPitch) / 2;
203
+ const merged =
204
+ phase.value === "line" || phase.value === "orb" || phase.value === "done";
205
+ const top = merged ? centre : own;
206
+ return {
207
+ transform: `translateY(${top}px) scaleX(${phase.value === "orb" || phase.value === "done" ? 0.08 : 1})`,
208
+ opacity: merged && i > 0 ? 0 : phase.value === "done" ? 0 : 1,
209
+ };
210
+ }
211
+
212
+ // A step's label shows once the step has lasted `labelDelayMs`.
213
+ const visibleStep = ref<PasswordOperationStep | undefined>(undefined);
214
+ const visibleLabel = ref("");
215
+ let labelTimer: ReturnType<typeof setTimeout> | undefined;
216
+ watch(
217
+ step,
218
+ (now) => {
219
+ clearTimeout(labelTimer);
220
+ if (!now) return;
221
+ if (visibleStep.value === undefined) {
222
+ visibleStep.value = now;
223
+ return;
224
+ }
225
+ labelTimer = setTimeout(
226
+ () => (visibleStep.value = now),
227
+ props.labelDelayMs,
228
+ );
229
+ },
230
+ { immediate: true },
231
+ );
232
+ watch(
233
+ () => [visibleStep.value, props.progress?.label] as const,
234
+ ([shownStep, text]) => {
235
+ if (shownStep === step.value && text) visibleLabel.value = text;
236
+ },
237
+ { immediate: true },
238
+ );
239
+ const label = computed(() => {
240
+ if (phase.value === "done") return props.acceptedText;
241
+ if (phase.value === "unfolding" || phase.value === "unfolded") return "";
242
+ return visibleLabel.value || props.startText;
243
+ });
244
+
245
+ /**
246
+ * Turn the digits into dots left to right as the operation advances; the
247
+ * rest keep changing, a few at a time.
248
+ */
249
+ let ticks = 0;
250
+ function tick() {
251
+ pace();
252
+ const settledUpTo = Math.floor(fraction.value * count.value);
253
+ // The open digits change every fourth frame (about six times a second);
254
+ // the line itself follows the pace every frame.
255
+ const churning = !still && ticks++ % 4 === 0;
256
+ glyphs.value = glyphs.value.map((glyph, i) => {
257
+ if (i < settledUpTo) return DOT;
258
+ if (!churning) return still ? DOT : glyph;
259
+ // A calm churn: about a third of the open digits change at a time.
260
+ if (glyph !== DOT && Math.random() > 0.35) return glyph;
261
+ return DIGITS[Math.floor(Math.random() * DIGITS.length)];
262
+ });
263
+ }
264
+
265
+ const timers: ReturnType<typeof setTimeout>[] = [];
266
+ function after(ms: number, then: () => void) {
267
+ timers.push(setTimeout(then, still ? 0 : ms));
268
+ }
269
+
270
+ let churn: ReturnType<typeof setInterval> | undefined;
271
+ function stopChurn() {
272
+ clearInterval(churn);
273
+ churn = undefined;
274
+ }
275
+
276
+ /** The success: the line closes into the orb, the lock, then the check. */
277
+ function close() {
278
+ stopChurn();
279
+ fraction.value = 1;
280
+ glyphs.value = glyphs.value.map(() => DOT);
281
+ phase.value = "orb";
282
+ after(TIMING.orb, () => {
283
+ phase.value = "done";
284
+ after(TIMING.lock, () => {
285
+ checked.value = true;
286
+ after(TIMING.check, () => emit("settled", "accepted"));
287
+ });
288
+ });
289
+ }
290
+
291
+ /** The refusal: the line unfolds back into the fields, as dots, with the reason. */
292
+ function unfold() {
293
+ stopChurn();
294
+ glyphs.value = glyphs.value.map(() => DOT);
295
+ phase.value = "unfolding";
296
+ after(TIMING.unfold, () => {
297
+ phase.value = "unfolded";
298
+ after(TIMING.reason, () => emit("settled", "refused"));
299
+ });
300
+ }
301
+
302
+ /** The rows have merged; an outcome now runs at once. */
303
+ let merged = false;
304
+ let concluded = false;
305
+
306
+ /** Run the outcome once, after the rows have merged: the choreography is never cut. */
307
+ function conclude(outcome: CeremonyOutcome) {
308
+ if (outcome === "running" || !merged || concluded) return;
309
+ concluded = true;
310
+ if (outcome === "accepted") close();
311
+ else unfold();
312
+ }
313
+
314
+ watch(() => props.outcome, conclude);
315
+
316
+ onMounted(() => {
317
+ tick();
318
+ churn = setInterval(tick, 40);
319
+ after(TIMING.split, () => {
320
+ phase.value = "line";
321
+ after(TIMING.merge, () => {
322
+ merged = true;
323
+ // An outcome that arrived while the rows were merging runs now.
324
+ conclude(props.outcome);
325
+ });
326
+ });
327
+ });
328
+
329
+ onBeforeUnmount(() => {
330
+ stopChurn();
331
+ timers.forEach(clearTimeout);
332
+ clearTimeout(labelTimer);
333
+ });
334
+ </script>
335
+
336
+ <style scoped>
337
+ .sid-ceremony__stage {
338
+ position: relative;
339
+ /* The rows size their glyphs to the stage's width (cqi below). */
340
+ container-type: inline-size;
341
+ }
342
+ .sid-ceremony__row {
343
+ position: absolute;
344
+ inset: 0 0 auto 0;
345
+ height: 56px;
346
+ display: flex;
347
+ align-items: center;
348
+ justify-content: center;
349
+ border: 1px solid rgba(0, 0, 0, 0.24);
350
+ border-radius: 4px;
351
+ font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
352
+ /* Each glyph is 0.9em wide; a long password on a narrow card shrinks its
353
+ glyphs to fit the row rather than spill out of it. */
354
+ font-size: min(1.5rem, calc(96cqi / (var(--sid-ceremony-glyphs, 20) * 0.9)));
355
+ transform-origin: center;
356
+ transition:
357
+ transform 600ms cubic-bezier(0.65, 0, 0.35, 1),
358
+ opacity 400ms ease,
359
+ border-color 400ms ease;
360
+ }
361
+ /* While the operation runs the rows are a line, not fields. */
362
+ .sid-ceremony--line .sid-ceremony__row,
363
+ .sid-ceremony--orb .sid-ceremony__row,
364
+ .sid-ceremony--done .sid-ceremony__row {
365
+ border-color: transparent;
366
+ }
367
+ .sid-ceremony__glyph {
368
+ display: inline-block;
369
+ width: 0.9em;
370
+ text-align: center;
371
+ color: var(--q-primary);
372
+ opacity: 0.45;
373
+ transition:
374
+ opacity 250ms ease,
375
+ transform 250ms ease;
376
+ }
377
+ .sid-ceremony__glyph--set {
378
+ opacity: 1;
379
+ transform: scale(0.6);
380
+ }
381
+ .sid-ceremony__orb {
382
+ position: absolute;
383
+ top: 50%;
384
+ left: 50%;
385
+ width: 64px;
386
+ height: 64px;
387
+ margin: -32px 0 0 -32px;
388
+ }
389
+ .sid-ceremony__orb circle {
390
+ fill: none;
391
+ stroke: var(--q-primary);
392
+ stroke-width: 3;
393
+ stroke-linecap: round;
394
+ stroke-dasharray: 176;
395
+ stroke-dashoffset: 176;
396
+ transform: rotate(-90deg);
397
+ transform-origin: center;
398
+ animation: sid-ceremony-draw 600ms ease-out forwards;
399
+ }
400
+ @keyframes sid-ceremony-draw {
401
+ to {
402
+ stroke-dashoffset: 0;
403
+ }
404
+ }
405
+ .sid-ceremony__core {
406
+ position: absolute;
407
+ top: 50%;
408
+ left: 50%;
409
+ transform: translate(-50%, -50%) scale(1);
410
+ animation: sid-ceremony-pop 300ms cubic-bezier(0.2, 1.4, 0.4, 1);
411
+ }
412
+ .sid-ceremony__core--checked {
413
+ animation: sid-ceremony-pop 350ms cubic-bezier(0.2, 1.6, 0.4, 1);
414
+ }
415
+ @keyframes sid-ceremony-pop {
416
+ from {
417
+ transform: translate(-50%, -50%) scale(0.3);
418
+ opacity: 0;
419
+ }
420
+ }
421
+ .sid-ceremony__label {
422
+ min-height: 1.5em;
423
+ margin-top: 20px;
424
+ }
425
+ .sid-ceremony-fade-enter-active,
426
+ .sid-ceremony-fade-leave-active {
427
+ transition:
428
+ opacity 250ms ease,
429
+ transform 250ms ease;
430
+ }
431
+ .sid-ceremony-fade-enter-from {
432
+ opacity: 0;
433
+ transform: translateY(6px);
434
+ }
435
+ .sid-ceremony-fade-leave-to {
436
+ opacity: 0;
437
+ transform: translateY(-6px);
438
+ }
439
+ .sid-ceremony--still .sid-ceremony__row,
440
+ .sid-ceremony--still .sid-ceremony__glyph,
441
+ .sid-ceremony--still .sid-ceremony-fade-enter-active,
442
+ .sid-ceremony--still .sid-ceremony-fade-leave-active {
443
+ transition: none;
444
+ }
445
+ .sid-ceremony--still .sid-ceremony-fade-enter-from,
446
+ .sid-ceremony--still .sid-ceremony-fade-leave-to {
447
+ transform: none;
448
+ }
449
+ .sid-ceremony--still .sid-ceremony__orb circle {
450
+ animation: none;
451
+ stroke-dashoffset: 0;
452
+ }
453
+ .sid-ceremony--still .sid-ceremony__core {
454
+ animation: none;
455
+ }
456
+ </style>
@@ -6,3 +6,5 @@
6
6
  */
7
7
  export { default as SidPrincipalInput } from "./SidPrincipalInput.vue";
8
8
  export type { PrincipalType } from "./SidPrincipalInput.vue";
9
+ export { default as SidPasswordCeremony } from "./SidPasswordCeremony.vue";
10
+ export type { CeremonyOutcome } from "./SidPasswordCeremony.vue";
@@ -1,13 +1,13 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import { RpcError } from "@protobuf-ts/runtime-rpc";
3
3
  import { base64encode } from "@protobuf-ts/runtime";
4
- import { Any } from "../generated/google/protobuf/any";
5
- import { Status } from "../generated/google/rpc/status";
4
+ import { Any } from "@structured-id/proto/google/protobuf/any";
5
+ import { Status } from "@structured-id/proto/google/rpc/status";
6
6
  import {
7
7
  ErrorInfo,
8
8
  LocalizedMessage,
9
- } from "../generated/google/rpc/error_details";
10
- import { ErrorReason } from "../generated/sid/v1/common/errors";
9
+ } from "@structured-id/proto/google/rpc/error_details";
10
+ import { ErrorReason } from "@structured-id/proto/sid/v1/common/errors";
11
11
  import {
12
12
  REFUSAL_TEXTS,
13
13
  refusalMessage,
@@ -5,13 +5,13 @@
5
5
  */
6
6
  import { RpcError } from "@protobuf-ts/runtime-rpc";
7
7
  import { base64decode } from "@protobuf-ts/runtime";
8
- import { Any } from "../generated/google/protobuf/any";
9
- import { Status } from "../generated/google/rpc/status";
8
+ import { Any } from "@structured-id/proto/google/protobuf/any";
9
+ import { Status } from "@structured-id/proto/google/rpc/status";
10
10
  import {
11
11
  ErrorInfo,
12
12
  LocalizedMessage,
13
- } from "../generated/google/rpc/error_details";
14
- import { ErrorReason } from "../generated/sid/v1/common/errors";
13
+ } from "@structured-id/proto/google/rpc/error_details";
14
+ import { ErrorReason } from "@structured-id/proto/sid/v1/common/errors";
15
15
 
16
16
  export interface RpcRefusal {
17
17
  /** The gRPC status code's name (`FAILED_PRECONDITION`). */
@@ -22,6 +22,8 @@ export interface RpcRefusal {
22
22
  reason: string | null;
23
23
  /** SID's reason as the generated enum; null for another domain or a reason this client does not know. */
24
24
  reasonCode: ErrorReason | null;
25
+ /** The reason's context from `ErrorInfo.metadata` (`continuation`, …); empty without one. */
26
+ metadata: Record<string, string>;
25
27
  /** The status message, written for developers; gRPC percent-encodes it on the wire. */
26
28
  message: string;
27
29
  /** The server's text for people (`LocalizedMessage`), when it sent one. */
@@ -45,6 +47,7 @@ export function rpcRefusal(err: unknown): RpcRefusal | null {
45
47
  domain: null,
46
48
  reason: null,
47
49
  reasonCode: null,
50
+ metadata: {},
48
51
  message: decodeStatusMessage(err.message),
49
52
  localized: null,
50
53
  };
@@ -53,6 +56,7 @@ export function rpcRefusal(err: unknown): RpcRefusal | null {
53
56
  const info = Any.unpack(detail, ErrorInfo);
54
57
  refusal.domain = info.domain;
55
58
  refusal.reason = info.reason;
59
+ refusal.metadata = info.metadata;
56
60
  refusal.reasonCode =
57
61
  info.domain === SID_ERROR_DOMAIN ? sidReason(info.reason) : null;
58
62
  } else if (Any.contains(detail, LocalizedMessage)) {