@structured-id/ui-core 2.0.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.
- package/README.md +22 -10
- package/package.json +25 -43
- package/src/components/SidStatusBadge.vue +1 -5
- package/src/composables/useAccountTransport.test.ts +2 -1
- package/src/composables/useGrpc.test.ts +17 -14
- package/src/composables/useGrpc.ts +4 -2
- package/src/index.ts +2 -120
- package/src/profile/components/ConsentManager.vue +5 -1
- package/src/profile/components/ProfileEditor.vue +1 -1
- package/src/profile/components/SecuritySettings.vue +5 -1
- package/src/profile/components/SessionList.vue +2 -1
- package/src/profile/components/SidRegistrationForm.test.ts +173 -1
- package/src/profile/components/SidRegistrationForm.vue +88 -32
- package/src/profile/components/index.ts +1 -4
- package/src/profile/composables/index.ts +1 -0
- package/src/profile/composables/operationPlan.test.ts +155 -0
- package/src/profile/composables/operationPlan.ts +218 -0
- package/src/profile/composables/useAccountApi.ts +4 -4
- package/src/profile/composables/useAuth.test.ts +312 -31
- package/src/profile/composables/useAuth.ts +176 -30
- package/src/profile/composables/useAuthApi.opaque.test.ts +13 -6
- package/src/profile/composables/useAuthApi.ts +40 -18
- package/src/profile/composables/useCurrentPasswordRequirement.test.ts +196 -0
- package/src/profile/composables/useCurrentPasswordRequirement.ts +95 -0
- package/src/profile/composables/useEnrollmentApi.test.ts +7 -3
- package/src/profile/composables/useEnrollmentApi.ts +3 -5
- package/src/profile/composables/useIdentityApi.ts +4 -4
- package/src/profile/composables/useSecurityLevel.test.ts +1 -1
- package/src/profile/composables/useSecurityLevel.ts +1 -1
- package/src/quasar/SidPasswordCeremony.test.ts +253 -0
- package/src/quasar/SidPasswordCeremony.vue +456 -0
- package/src/quasar/index.ts +2 -0
- package/src/utils/rpcRefusal.test.ts +4 -4
- package/src/utils/rpcRefusal.ts +8 -4
- package/dist/browser-vp/index.cjs +0 -349
- package/dist/browser-vp/index.d.cts +0 -276
- package/dist/browser-vp/index.d.ts +0 -276
- package/dist/browser-vp/index.js +0 -349
- package/dist/chunk-C3Q766CY.cjs +0 -125
- package/dist/chunk-J5YXRXB5.js +0 -125
- package/dist/index.cjs +0 -23581
- package/dist/index.d.cts +0 -11539
- package/dist/index.d.ts +0 -11539
- package/dist/index.js +0 -23581
- package/dist/storage-4CB2JTLW.cjs +0 -22
- package/dist/storage-THRWREZP.js +0 -22
- package/src/browser-vp/crypto.ts +0 -253
- package/src/browser-vp/index.test.ts +0 -10
- package/src/browser-vp/index.ts +0 -116
- package/src/browser-vp/prf.ts +0 -179
- package/src/browser-vp/storage.test.ts +0 -9
- package/src/browser-vp/storage.ts +0 -163
- package/src/browser-vp/types.ts +0 -95
- package/src/browser-vp/vp.test.ts +0 -43
- package/src/browser-vp/vp.ts +0 -111
- package/src/generated/google/api/http.ts +0 -687
- package/src/generated/google/api/httpbody.ts +0 -159
- package/src/generated/google/protobuf/any.ts +0 -326
- package/src/generated/google/protobuf/descriptor.ts +0 -4865
- package/src/generated/google/protobuf/duration.ts +0 -230
- package/src/generated/google/protobuf/empty.ts +0 -94
- package/src/generated/google/protobuf/struct.ts +0 -484
- package/src/generated/google/protobuf/timestamp.ts +0 -287
- package/src/generated/google/rpc/error_details.ts +0 -1386
- package/src/generated/google/rpc/status.ts +0 -127
- package/src/generated/sid/v1/account/account.client.ts +0 -527
- package/src/generated/sid/v1/account/account.ts +0 -3261
- package/src/generated/sid/v1/admin/admin.client.ts +0 -139
- package/src/generated/sid/v1/admin/admin.ts +0 -781
- package/src/generated/sid/v1/admin/admin_roles.client.ts +0 -181
- package/src/generated/sid/v1/admin/admin_roles.ts +0 -1606
- package/src/generated/sid/v1/admin/analytics.client.ts +0 -106
- package/src/generated/sid/v1/admin/analytics.ts +0 -1017
- package/src/generated/sid/v1/admin/branding.client.ts +0 -166
- package/src/generated/sid/v1/admin/branding.ts +0 -1906
- package/src/generated/sid/v1/admin/enrollment.client.ts +0 -177
- package/src/generated/sid/v1/admin/enrollment.ts +0 -1766
- package/src/generated/sid/v1/admin/events.client.ts +0 -195
- package/src/generated/sid/v1/admin/events.ts +0 -1900
- package/src/generated/sid/v1/admin/flow.client.ts +0 -183
- package/src/generated/sid/v1/admin/flow.ts +0 -1552
- package/src/generated/sid/v1/admin/keys.client.ts +0 -107
- package/src/generated/sid/v1/admin/keys.ts +0 -931
- package/src/generated/sid/v1/admin/login_experience.client.ts +0 -160
- package/src/generated/sid/v1/admin/login_experience.ts +0 -1061
- package/src/generated/sid/v1/admin/migration.client.ts +0 -127
- package/src/generated/sid/v1/admin/migration.ts +0 -1948
- package/src/generated/sid/v1/admin/notifications.client.ts +0 -134
- package/src/generated/sid/v1/admin/notifications.ts +0 -1273
- package/src/generated/sid/v1/admin/operations.client.ts +0 -241
- package/src/generated/sid/v1/admin/operations.ts +0 -2633
- package/src/generated/sid/v1/admin/organizations.client.ts +0 -57
- package/src/generated/sid/v1/admin/organizations.ts +0 -332
- package/src/generated/sid/v1/admin/provisioning.client.ts +0 -232
- package/src/generated/sid/v1/admin/provisioning.ts +0 -1383
- package/src/generated/sid/v1/admin/scopes.client.ts +0 -171
- package/src/generated/sid/v1/admin/scopes.ts +0 -1324
- package/src/generated/sid/v1/admin/security.client.ts +0 -410
- package/src/generated/sid/v1/admin/security.ts +0 -4077
- package/src/generated/sid/v1/admin/sessions.client.ts +0 -114
- package/src/generated/sid/v1/admin/sessions.ts +0 -923
- package/src/generated/sid/v1/admin/settings.client.ts +0 -351
- package/src/generated/sid/v1/admin/settings.ts +0 -2458
- package/src/generated/sid/v1/admin/user_profile.client.ts +0 -57
- package/src/generated/sid/v1/admin/user_profile.ts +0 -730
- package/src/generated/sid/v1/admin/webhooks.client.ts +0 -269
- package/src/generated/sid/v1/admin/webhooks.ts +0 -2804
- package/src/generated/sid/v1/attestation/attestation.client.ts +0 -115
- package/src/generated/sid/v1/attestation/attestation.ts +0 -813
- package/src/generated/sid/v1/authn/auth.client.ts +0 -943
- package/src/generated/sid/v1/authn/auth.ts +0 -9719
- package/src/generated/sid/v1/authn/flows.client.ts +0 -413
- package/src/generated/sid/v1/authn/flows.ts +0 -3205
- package/src/generated/sid/v1/authn/issuer.client.ts +0 -489
- package/src/generated/sid/v1/authn/issuer.ts +0 -1600
- package/src/generated/sid/v1/authn/password_history.client.ts +0 -57
- package/src/generated/sid/v1/authn/password_history.ts +0 -578
- package/src/generated/sid/v1/authn/system_integration.client.ts +0 -73
- package/src/generated/sid/v1/authn/system_integration.ts +0 -218
- package/src/generated/sid/v1/authz/authz.client.ts +0 -459
- package/src/generated/sid/v1/authz/authz.ts +0 -6524
- package/src/generated/sid/v1/authz/forward_auth.client.ts +0 -97
- package/src/generated/sid/v1/authz/forward_auth.ts +0 -79
- package/src/generated/sid/v1/authz/governance.client.ts +0 -102
- package/src/generated/sid/v1/authz/governance.ts +0 -875
- package/src/generated/sid/v1/common/errors.ts +0 -710
- package/src/generated/sid/v1/common/pagination.ts +0 -152
- package/src/generated/sid/v1/common/transport.ts +0 -227
- package/src/generated/sid/v1/common/types.ts +0 -151
- package/src/generated/sid/v1/events/event_stream.client.ts +0 -67
- package/src/generated/sid/v1/events/event_stream.ts +0 -375
- package/src/generated/sid/v1/federation/upstream.client.ts +0 -142
- package/src/generated/sid/v1/federation/upstream.ts +0 -1736
- package/src/generated/sid/v1/identity/identity.client.ts +0 -467
- package/src/generated/sid/v1/identity/identity.ts +0 -5676
- package/src/generated/sid/v1/ids/ids.ts +0 -395
- package/src/generated/sid/v1/machine/machine_user.client.ts +0 -219
- package/src/generated/sid/v1/machine/machine_user.ts +0 -2364
- package/src/generated/sid/v1/machine/pat.client.ts +0 -130
- package/src/generated/sid/v1/machine/pat.ts +0 -1083
- package/src/generated/sid/v1/projects/projects.client.ts +0 -404
- package/src/generated/sid/v1/projects/projects.ts +0 -4446
- package/src/generated/sid/v1/scim/scim.client.ts +0 -520
- package/src/generated/sid/v1/scim/scim.ts +0 -3182
- package/src/generated/sid/v1/test/test.client.ts +0 -232
- package/src/generated/sid/v1/test/test.ts +0 -1136
- package/src/transport/adaptive.test.ts +0 -728
- package/src/transport/adaptive.ts +0 -308
- package/src/transport/framing.test.ts +0 -227
- package/src/transport/framing.ts +0 -98
- package/src/transport/webtransport.test.ts +0 -728
- 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>
|
package/src/quasar/index.ts
CHANGED
|
@@ -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 "
|
|
5
|
-
import { Status } from "
|
|
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 "
|
|
10
|
-
import { ErrorReason } from "
|
|
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,
|
package/src/utils/rpcRefusal.ts
CHANGED
|
@@ -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 "
|
|
9
|
-
import { Status } from "
|
|
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 "
|
|
14
|
-
import { ErrorReason } from "
|
|
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)) {
|