@zoreal/oauth2-js 0.1.18 → 0.1.20
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 +1 -1
- package/dist/index.cjs +258 -85
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -7
- package/dist/index.d.ts +13 -7
- package/dist/index.js +257 -85
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -140,7 +140,7 @@ is nothing to configure and nothing to draw.
|
|
|
140
140
|
|
|
141
141
|
| | |
|
|
142
142
|
| --- | --- |
|
|
143
|
-
| **Mobile** | No QR and no modal. The tap itself is a navigation: `startLogin` sends the tab to the provider's `/pair/start` with the pairing's parameters, synchronously, and the provider answers with a redirect to the pairing's universal link, which the ZOREAL ID app claims while the page stays put and polls. A browser hands a link to an app only inside a navigation the person began, which is why nothing is fetched first. With no app installed the same redirect lands on the page that installs it. Call `startLogin` from the click handler itself, disable the button, and show it working until the promise settles. Force one or the other with `display: 'qr'` / `'link'`. The choice is made before the pairing is created, because the provider binds the surface there: a link-mode pairing is claimed only by the app that opened that exact link, and has no QR at all. |
|
|
143
|
+
| **Mobile** | No QR and no modal. The tap itself is a navigation: `startLogin` sends the tab to the provider's `/pair/start` with the pairing's parameters, synchronously, and the provider answers with a redirect to the pairing's universal link, which the ZOREAL ID app claims while the page stays put and polls. A browser hands a link to an app only inside a navigation the person began, which is why nothing is fetched first. With no app installed the same redirect lands on the page that installs it. Call `startLogin` from the click handler itself, disable the button, and show it working until the promise settles. Once the holder has approved, the app reopens your page with the pairing named in the fragment; call `resumeLogin({ clientId })` on every page load where `startLogin` can be called, and it finishes the sign-in there, resolving the way `startLogin` would have, or answers `null` at once when the page load is not a return. The tab that was left behind keeps polling and stands down when the returned page finishes first. Force one or the other with `display: 'qr'` / `'link'`. The choice is made before the pairing is created, because the provider binds the surface there: a link-mode pairing is claimed only by the app that opened that exact link, and has no QR at all. |
|
|
144
144
|
| **Live status** | Copy and title follow the pairing: waiting for a scan, then waiting for approval once the code is claimed (the spent QR blurs out behind a phone glyph). |
|
|
145
145
|
| **Title** | Says what the scan is for, inferred from the request: "Scan to sign in" for `openid`, `email` and `profile.name`; "Scan to verify your identity" once a document attribute such as `zoreal.age` or `profile.birthdate` is requested; "Scan to prove you are a real human" for `openid` alone with `acr_values: 'zoreal.live'`. Override with `intent`, one of `'sign-in'`, `'identify'`, `'presence'`, when the scope does not say. |
|
|
146
146
|
| **Countdown** | Counts down to expiry, turning amber under 20s. Reads the clock each tick rather than decrementing, so a backgrounded tab comes back honest. |
|
package/dist/index.cjs
CHANGED
|
@@ -38,6 +38,7 @@ __export(index_exports, {
|
|
|
38
38
|
mountPairingModal: () => mountPairingModal,
|
|
39
39
|
pollUntilApproved: () => pollUntilApproved,
|
|
40
40
|
resolveIntent: () => resolveIntent,
|
|
41
|
+
resumeLogin: () => resumeLogin,
|
|
41
42
|
startLogin: () => startLogin,
|
|
42
43
|
startPairing: () => startPairing,
|
|
43
44
|
unsafeClaims: () => unsafeClaims
|
|
@@ -60,7 +61,7 @@ function unsafeClaims(idToken) {
|
|
|
60
61
|
|
|
61
62
|
// src/wire.ts
|
|
62
63
|
var WIRE_VERSION = 1;
|
|
63
|
-
var SDK_VERSION = "0.1.
|
|
64
|
+
var SDK_VERSION = "0.1.20";
|
|
64
65
|
var SDK_NAME = "@zoreal/oauth2-js";
|
|
65
66
|
var DEFAULT_ISSUER = "https://id.zoreal.com";
|
|
66
67
|
var POLL_INTERVAL_MS = 2e3;
|
|
@@ -138,11 +139,30 @@ var sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
|
138
139
|
}, ms);
|
|
139
140
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
140
141
|
});
|
|
142
|
+
var SETTLE_MS = 1500;
|
|
143
|
+
var NETWORK_FAILURES_TOLERATED = 4;
|
|
141
144
|
async function pollUntilApproved(issuer, requestId, onState, signal, options = {}) {
|
|
145
|
+
const settlingUntil = options.tolerateUnknownUntil ?? 0;
|
|
146
|
+
let networkFailures = 0;
|
|
147
|
+
let last = { status: "pending" };
|
|
148
|
+
if (settlingUntil > Date.now()) await sleep(SETTLE_MS, signal);
|
|
142
149
|
for (; ; ) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
150
|
+
let response;
|
|
151
|
+
try {
|
|
152
|
+
response = await fetch(`${issuer}/pair/${encodeURIComponent(requestId)}/status`, {
|
|
153
|
+
signal
|
|
154
|
+
});
|
|
155
|
+
networkFailures = 0;
|
|
156
|
+
} catch (e) {
|
|
157
|
+
if (e instanceof DOMException && e.name === "AbortError") throw e;
|
|
158
|
+
networkFailures += 1;
|
|
159
|
+
if (settlingUntil > Date.now() || networkFailures <= NETWORK_FAILURES_TOLERATED) {
|
|
160
|
+
onState?.(last);
|
|
161
|
+
await sleep(POLL_INTERVAL_MS, signal);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
throw e;
|
|
165
|
+
}
|
|
146
166
|
const body = await parseJson(response);
|
|
147
167
|
if (response.status === 404 && (options.tolerateUnknownUntil ?? 0) > Date.now()) {
|
|
148
168
|
onState?.({ status: "pending" });
|
|
@@ -155,11 +175,12 @@ async function pollUntilApproved(issuer, requestId, onState, signal, options = {
|
|
|
155
175
|
body.error_description ?? `Pairing status failed (${response.status})`
|
|
156
176
|
);
|
|
157
177
|
}
|
|
158
|
-
|
|
178
|
+
last = {
|
|
159
179
|
status: body.status,
|
|
160
180
|
expiresIn: body.expires_in,
|
|
161
181
|
enrolmentDeadline: body.enrolment_deadline
|
|
162
|
-
}
|
|
182
|
+
};
|
|
183
|
+
onState?.(last);
|
|
163
184
|
switch (body.status) {
|
|
164
185
|
case "approved":
|
|
165
186
|
if (!body.code) {
|
|
@@ -224,6 +245,76 @@ function titleFor(t, intent) {
|
|
|
224
245
|
return t.title;
|
|
225
246
|
}
|
|
226
247
|
|
|
248
|
+
// src/return.ts
|
|
249
|
+
var PREFIX = "zoreal:oauth2:return:";
|
|
250
|
+
var DONE = "zoreal:oauth2:done:";
|
|
251
|
+
var MAX_AGE_MS = 10 * 60 * 1e3;
|
|
252
|
+
function storage() {
|
|
253
|
+
try {
|
|
254
|
+
return typeof localStorage === "undefined" ? null : localStorage;
|
|
255
|
+
} catch {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function saveReturnFlow(flow) {
|
|
260
|
+
try {
|
|
261
|
+
storage()?.setItem(PREFIX + flow.requestId, JSON.stringify(flow));
|
|
262
|
+
} catch {
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function peekReturnFlow(requestId) {
|
|
266
|
+
const store = storage();
|
|
267
|
+
if (!store) return null;
|
|
268
|
+
const raw = store.getItem(PREFIX + requestId);
|
|
269
|
+
if (!raw) return null;
|
|
270
|
+
try {
|
|
271
|
+
const flow = JSON.parse(raw);
|
|
272
|
+
if (flow.v !== 1 || flow.requestId !== requestId) return null;
|
|
273
|
+
if (Date.now() - flow.createdAt > MAX_AGE_MS) return null;
|
|
274
|
+
return flow;
|
|
275
|
+
} catch {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function forgetReturnFlow(requestId) {
|
|
280
|
+
try {
|
|
281
|
+
storage()?.removeItem(PREFIX + requestId);
|
|
282
|
+
} catch {
|
|
283
|
+
}
|
|
284
|
+
if (pending === requestId) pending = null;
|
|
285
|
+
}
|
|
286
|
+
function markReturnDone(requestId) {
|
|
287
|
+
try {
|
|
288
|
+
const store = storage();
|
|
289
|
+
store?.setItem(DONE + requestId, String(Date.now()));
|
|
290
|
+
store?.removeItem(PREFIX + requestId);
|
|
291
|
+
} catch {
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function isReturnDone(requestId) {
|
|
295
|
+
return storage()?.getItem(DONE + requestId) !== null && storage()?.getItem(DONE + requestId) !== void 0;
|
|
296
|
+
}
|
|
297
|
+
function returnToUrl() {
|
|
298
|
+
if (typeof window === "undefined") return void 0;
|
|
299
|
+
const { href } = window.location;
|
|
300
|
+
const hash = href.indexOf("#");
|
|
301
|
+
return hash === -1 ? href : href.slice(0, hash);
|
|
302
|
+
}
|
|
303
|
+
var RETURN_MARK = /(?:^|[#&])zoreal_return=([A-Za-z0-9]{32})(?:&|$)/;
|
|
304
|
+
var pending = null;
|
|
305
|
+
function pendingReturnId() {
|
|
306
|
+
if (pending) return pending;
|
|
307
|
+
if (typeof window === "undefined") return null;
|
|
308
|
+
const match = RETURN_MARK.exec(window.location.hash);
|
|
309
|
+
if (!match) return null;
|
|
310
|
+
pending = match[1];
|
|
311
|
+
try {
|
|
312
|
+
window.history.replaceState(window.history.state, "", returnToUrl());
|
|
313
|
+
} catch {
|
|
314
|
+
}
|
|
315
|
+
return pending;
|
|
316
|
+
}
|
|
317
|
+
|
|
227
318
|
// src/i18n.ts
|
|
228
319
|
var en = {
|
|
229
320
|
title: "Scan to sign in",
|
|
@@ -1067,8 +1158,8 @@ function interpolate(template, time) {
|
|
|
1067
1158
|
}
|
|
1068
1159
|
|
|
1069
1160
|
// src/styles.ts
|
|
1070
|
-
var
|
|
1071
|
-
var cx = (name) => `${
|
|
1161
|
+
var PREFIX2 = "zrl";
|
|
1162
|
+
var cx = (name) => `${PREFIX2}-${name}`;
|
|
1072
1163
|
var STYLE_ELEMENT_ID = "zoreal-pairing-styles";
|
|
1073
1164
|
var LIGHT = `
|
|
1074
1165
|
--zrl-scrim: rgba(16, 18, 27, 0.45);
|
|
@@ -1132,13 +1223,13 @@ var DARK = `
|
|
|
1132
1223
|
--zrl-glow-opacity: 0.85;
|
|
1133
1224
|
`;
|
|
1134
1225
|
var CSS = `
|
|
1135
|
-
.${
|
|
1136
|
-
.${
|
|
1226
|
+
.${PREFIX2}-root { ${LIGHT} }
|
|
1227
|
+
.${PREFIX2}-root[data-theme="dark"] { ${DARK} }
|
|
1137
1228
|
@media (prefers-color-scheme: dark) {
|
|
1138
|
-
.${
|
|
1229
|
+
.${PREFIX2}-root[data-theme="auto"] { ${DARK} }
|
|
1139
1230
|
}
|
|
1140
1231
|
|
|
1141
|
-
.${
|
|
1232
|
+
.${PREFIX2}-scrim {
|
|
1142
1233
|
position: fixed;
|
|
1143
1234
|
inset: 0;
|
|
1144
1235
|
z-index: 2147483000;
|
|
@@ -1150,10 +1241,10 @@ var CSS = `
|
|
|
1150
1241
|
backdrop-filter: blur(3px);
|
|
1151
1242
|
-webkit-backdrop-filter: blur(3px);
|
|
1152
1243
|
font-family: inherit;
|
|
1153
|
-
animation: ${
|
|
1244
|
+
animation: ${PREFIX2}-fade 200ms ease-out both;
|
|
1154
1245
|
}
|
|
1155
1246
|
|
|
1156
|
-
.${
|
|
1247
|
+
.${PREFIX2}-card {
|
|
1157
1248
|
position: relative;
|
|
1158
1249
|
box-sizing: border-box;
|
|
1159
1250
|
width: 100%;
|
|
@@ -1165,14 +1256,14 @@ var CSS = `
|
|
|
1165
1256
|
outline: 1px solid var(--zrl-ring);
|
|
1166
1257
|
outline-offset: -1px;
|
|
1167
1258
|
text-align: center;
|
|
1168
|
-
animation: ${
|
|
1259
|
+
animation: ${PREFIX2}-rise 300ms cubic-bezier(0.23, 1, 0.32, 1) both;
|
|
1169
1260
|
}
|
|
1170
1261
|
|
|
1171
|
-
.${
|
|
1262
|
+
.${PREFIX2}-body { padding: 28px 24px 20px; }
|
|
1172
1263
|
|
|
1173
|
-
.${
|
|
1264
|
+
.${PREFIX2}-lockup { display: block; margin: 0 auto; color: var(--zrl-ink); }
|
|
1174
1265
|
|
|
1175
|
-
.${
|
|
1266
|
+
.${PREFIX2}-title {
|
|
1176
1267
|
margin: 18px 0 0;
|
|
1177
1268
|
font-size: 18px;
|
|
1178
1269
|
font-weight: 600;
|
|
@@ -1181,7 +1272,7 @@ var CSS = `
|
|
|
1181
1272
|
color: var(--zrl-ink);
|
|
1182
1273
|
}
|
|
1183
1274
|
|
|
1184
|
-
.${
|
|
1275
|
+
.${PREFIX2}-body-text {
|
|
1185
1276
|
margin: 6px auto 0;
|
|
1186
1277
|
max-width: 30ch;
|
|
1187
1278
|
font-size: 14px;
|
|
@@ -1189,7 +1280,7 @@ var CSS = `
|
|
|
1189
1280
|
color: var(--zrl-ink-soft);
|
|
1190
1281
|
}
|
|
1191
1282
|
|
|
1192
|
-
.${
|
|
1283
|
+
.${PREFIX2}-qr-well {
|
|
1193
1284
|
position: relative;
|
|
1194
1285
|
display: grid;
|
|
1195
1286
|
place-items: center;
|
|
@@ -1228,9 +1319,9 @@ var CSS = `
|
|
|
1228
1319
|
All three share one containing block, the well's padding box, so the two
|
|
1229
1320
|
comets ride the same path; the spent badge is a later sibling and paints
|
|
1230
1321
|
above them. */
|
|
1231
|
-
.${
|
|
1232
|
-
.${
|
|
1233
|
-
.${
|
|
1322
|
+
.${PREFIX2}-qr-beam,
|
|
1323
|
+
.${PREFIX2}-qr-beam-glow,
|
|
1324
|
+
.${PREFIX2}-qr-beam-glow-band {
|
|
1234
1325
|
position: absolute;
|
|
1235
1326
|
inset: calc(0px - var(--zrl-beam-line));
|
|
1236
1327
|
border: var(--zrl-beam-line) solid transparent;
|
|
@@ -1243,7 +1334,7 @@ var CSS = `
|
|
|
1243
1334
|
mask-clip: padding-box, border-box;
|
|
1244
1335
|
mask-composite: intersect;
|
|
1245
1336
|
}
|
|
1246
|
-
.${
|
|
1337
|
+
.${PREFIX2}-qr-beam-glow {
|
|
1247
1338
|
inset: calc(0px - var(--zrl-glow-reach));
|
|
1248
1339
|
border-width: var(--zrl-glow-reach);
|
|
1249
1340
|
border-radius: calc(var(--zrl-radius) - 1px + var(--zrl-glow-reach));
|
|
@@ -1251,7 +1342,7 @@ var CSS = `
|
|
|
1251
1342
|
opacity: var(--zrl-glow-opacity);
|
|
1252
1343
|
will-change: filter;
|
|
1253
1344
|
}
|
|
1254
|
-
.${
|
|
1345
|
+
.${PREFIX2}-qr-beam-glow-band {
|
|
1255
1346
|
inset: calc(0px - var(--zrl-glow-core));
|
|
1256
1347
|
border-width: var(--zrl-glow-core);
|
|
1257
1348
|
border-radius: calc(var(--zrl-radius) - 1px + var(--zrl-glow-core));
|
|
@@ -1262,8 +1353,8 @@ var CSS = `
|
|
|
1262
1353
|
border reads as the well's own line with a light passing over it; shown
|
|
1263
1354
|
once the light has stopped. Every path below ends here, which is what
|
|
1264
1355
|
makes them look the same at rest. */
|
|
1265
|
-
.${
|
|
1266
|
-
.${
|
|
1356
|
+
.${PREFIX2}-qr-beam::before,
|
|
1357
|
+
.${PREFIX2}-qr-beam-glow-band::before {
|
|
1267
1358
|
content: '';
|
|
1268
1359
|
position: absolute;
|
|
1269
1360
|
inset: -50%;
|
|
@@ -1276,8 +1367,8 @@ var CSS = `
|
|
|
1276
1367
|
whole. A transform animation runs on the compositor, so the light keeps
|
|
1277
1368
|
moving while the page is busy; animating the gradient angle instead
|
|
1278
1369
|
repaints every frame on the main thread and stutters. */
|
|
1279
|
-
.${
|
|
1280
|
-
.${
|
|
1370
|
+
.${PREFIX2}-qr-beam::after,
|
|
1371
|
+
.${PREFIX2}-qr-beam-glow-band::after {
|
|
1281
1372
|
content: '';
|
|
1282
1373
|
position: absolute;
|
|
1283
1374
|
inset: -50%;
|
|
@@ -1288,7 +1379,7 @@ var CSS = `
|
|
|
1288
1379
|
var(--zrl-beam-head) 348deg,
|
|
1289
1380
|
transparent 356deg 360deg
|
|
1290
1381
|
);
|
|
1291
|
-
animation: ${
|
|
1382
|
+
animation: ${PREFIX2}-orbit var(--zrl-beam-time) linear infinite;
|
|
1292
1383
|
will-change: transform;
|
|
1293
1384
|
transition: opacity 400ms cubic-bezier(0.23, 1, 0.32, 1);
|
|
1294
1385
|
}
|
|
@@ -1296,15 +1387,15 @@ var CSS = `
|
|
|
1296
1387
|
/* Spent: the light stops where it is and fades, and the edge settles to the
|
|
1297
1388
|
dim glow. Paused rather than removed, so it does not jump back to its start
|
|
1298
1389
|
on the way out. */
|
|
1299
|
-
.${
|
|
1300
|
-
.${
|
|
1390
|
+
.${PREFIX2}-qr-well[data-spent="true"] .${PREFIX2}-qr-beam::after,
|
|
1391
|
+
.${PREFIX2}-qr-well[data-spent="true"] .${PREFIX2}-qr-beam-glow-band::after {
|
|
1301
1392
|
animation-play-state: paused;
|
|
1302
1393
|
opacity: 0;
|
|
1303
1394
|
}
|
|
1304
|
-
.${
|
|
1305
|
-
.${
|
|
1395
|
+
.${PREFIX2}-qr-well[data-spent="true"] .${PREFIX2}-qr-beam::before { opacity: 0.55; }
|
|
1396
|
+
.${PREFIX2}-qr-well[data-spent="true"] .${PREFIX2}-qr-beam-glow-band::before { opacity: 0.7; }
|
|
1306
1397
|
|
|
1307
|
-
.${
|
|
1398
|
+
.${PREFIX2}-qr {
|
|
1308
1399
|
display: block;
|
|
1309
1400
|
width: 100%;
|
|
1310
1401
|
height: 100%;
|
|
@@ -1319,17 +1410,17 @@ var CSS = `
|
|
|
1319
1410
|
/* Once the code is claimed the QR is spent. Blurring it out rather than
|
|
1320
1411
|
swapping it keeps one object on screen through the state change, so the eye
|
|
1321
1412
|
reads a transformation instead of two things trading places. */
|
|
1322
|
-
.${
|
|
1413
|
+
.${PREFIX2}-qr[data-spent="true"] { opacity: 0.2; filter: var(--zrl-qr-spent-filter); transform: scale(0.96); }
|
|
1323
1414
|
|
|
1324
|
-
.${
|
|
1415
|
+
.${PREFIX2}-qr-overlay {
|
|
1325
1416
|
position: absolute;
|
|
1326
1417
|
inset: 0;
|
|
1327
1418
|
display: grid;
|
|
1328
1419
|
place-items: center;
|
|
1329
|
-
animation: ${
|
|
1420
|
+
animation: ${PREFIX2}-fade 200ms ease-out both;
|
|
1330
1421
|
}
|
|
1331
1422
|
|
|
1332
|
-
.${
|
|
1423
|
+
.${PREFIX2}-qr-badge {
|
|
1333
1424
|
display: grid;
|
|
1334
1425
|
place-items: center;
|
|
1335
1426
|
width: 56px;
|
|
@@ -1339,7 +1430,7 @@ var CSS = `
|
|
|
1339
1430
|
color: var(--zrl-accent-ink);
|
|
1340
1431
|
}
|
|
1341
1432
|
|
|
1342
|
-
.${
|
|
1433
|
+
.${PREFIX2}-status {
|
|
1343
1434
|
display: flex;
|
|
1344
1435
|
align-items: center;
|
|
1345
1436
|
justify-content: center;
|
|
@@ -1350,8 +1441,8 @@ var CSS = `
|
|
|
1350
1441
|
color: var(--zrl-ink);
|
|
1351
1442
|
}
|
|
1352
1443
|
|
|
1353
|
-
.${
|
|
1354
|
-
.${
|
|
1444
|
+
.${PREFIX2}-dot { position: relative; display: grid; place-items: center; width: 8px; height: 8px; }
|
|
1445
|
+
.${PREFIX2}-dot i {
|
|
1355
1446
|
position: absolute;
|
|
1356
1447
|
width: 8px;
|
|
1357
1448
|
height: 8px;
|
|
@@ -1359,25 +1450,25 @@ var CSS = `
|
|
|
1359
1450
|
background: var(--zrl-accent);
|
|
1360
1451
|
font-style: normal;
|
|
1361
1452
|
}
|
|
1362
|
-
.${
|
|
1453
|
+
.${PREFIX2}-dot i:first-child { animation: ${PREFIX2}-ping 1.8s cubic-bezier(0.23, 1, 0.32, 1) infinite; }
|
|
1363
1454
|
|
|
1364
|
-
.${
|
|
1455
|
+
.${PREFIX2}-timer {
|
|
1365
1456
|
margin: 4px 0 0;
|
|
1366
1457
|
font-size: 12px;
|
|
1367
1458
|
font-variant-numeric: tabular-nums;
|
|
1368
1459
|
color: var(--zrl-ink-mute);
|
|
1369
1460
|
transition: color 200ms ease-out;
|
|
1370
1461
|
}
|
|
1371
|
-
.${
|
|
1462
|
+
.${PREFIX2}-timer[data-urgent="true"] { color: var(--zrl-urgent); }
|
|
1372
1463
|
|
|
1373
|
-
.${
|
|
1464
|
+
.${PREFIX2}-help {
|
|
1374
1465
|
padding: 14px 24px;
|
|
1375
1466
|
border-top: 1px solid var(--zrl-line-soft);
|
|
1376
1467
|
background: var(--zrl-surface-sunken);
|
|
1377
1468
|
border-radius: 0;
|
|
1378
1469
|
}
|
|
1379
|
-
.${
|
|
1380
|
-
.${
|
|
1470
|
+
.${PREFIX2}-help-title { margin: 0; font-size: 12px; font-weight: 600; color: var(--zrl-ink); }
|
|
1471
|
+
.${PREFIX2}-help-body {
|
|
1381
1472
|
margin: 4px auto 0;
|
|
1382
1473
|
max-width: 34ch;
|
|
1383
1474
|
font-size: 12px;
|
|
@@ -1385,9 +1476,9 @@ var CSS = `
|
|
|
1385
1476
|
color: var(--zrl-ink-soft);
|
|
1386
1477
|
}
|
|
1387
1478
|
|
|
1388
|
-
.${
|
|
1479
|
+
.${PREFIX2}-footer { padding: 12px; border-top: 1px solid var(--zrl-line-soft); }
|
|
1389
1480
|
|
|
1390
|
-
.${
|
|
1481
|
+
.${PREFIX2}-cancel {
|
|
1391
1482
|
display: block;
|
|
1392
1483
|
width: 100%;
|
|
1393
1484
|
padding: 10px;
|
|
@@ -1401,10 +1492,10 @@ var CSS = `
|
|
|
1401
1492
|
cursor: pointer;
|
|
1402
1493
|
transition: background-color 150ms ease-out, color 150ms ease-out, transform 150ms ease-out;
|
|
1403
1494
|
}
|
|
1404
|
-
.${
|
|
1405
|
-
.${
|
|
1495
|
+
.${PREFIX2}-cancel:hover { background: var(--zrl-surface-sunken); color: var(--zrl-ink); }
|
|
1496
|
+
.${PREFIX2}-cancel:active { transform: scale(0.99); }
|
|
1406
1497
|
|
|
1407
|
-
.${
|
|
1498
|
+
.${PREFIX2}-secured {
|
|
1408
1499
|
display: flex;
|
|
1409
1500
|
align-items: center;
|
|
1410
1501
|
justify-content: center;
|
|
@@ -1416,9 +1507,9 @@ var CSS = `
|
|
|
1416
1507
|
border-radius: 6px;
|
|
1417
1508
|
transition: color 150ms ease-out;
|
|
1418
1509
|
}
|
|
1419
|
-
.${
|
|
1510
|
+
.${PREFIX2}-secured:hover { color: var(--zrl-ink); }
|
|
1420
1511
|
|
|
1421
|
-
.${
|
|
1512
|
+
.${PREFIX2}-close {
|
|
1422
1513
|
position: absolute;
|
|
1423
1514
|
top: 12px;
|
|
1424
1515
|
inset-inline-end: 12px;
|
|
@@ -1434,25 +1525,25 @@ var CSS = `
|
|
|
1434
1525
|
cursor: pointer;
|
|
1435
1526
|
transition: background-color 150ms ease-out, color 150ms ease-out, transform 150ms ease-out;
|
|
1436
1527
|
}
|
|
1437
|
-
.${
|
|
1438
|
-
.${
|
|
1528
|
+
.${PREFIX2}-close:hover { background: var(--zrl-surface-sunken); color: var(--zrl-ink); }
|
|
1529
|
+
.${PREFIX2}-close:active { transform: scale(0.95); }
|
|
1439
1530
|
|
|
1440
|
-
.${
|
|
1531
|
+
.${PREFIX2}-card :focus-visible {
|
|
1441
1532
|
outline: 2px solid var(--zrl-accent);
|
|
1442
1533
|
outline-offset: 2px;
|
|
1443
1534
|
}
|
|
1444
1535
|
|
|
1445
|
-
@keyframes ${
|
|
1446
|
-
@keyframes ${
|
|
1536
|
+
@keyframes ${PREFIX2}-fade { from { opacity: 0 } to { opacity: 1 } }
|
|
1537
|
+
@keyframes ${PREFIX2}-rise {
|
|
1447
1538
|
from { opacity: 0; transform: translateY(10px) scale(0.98) }
|
|
1448
1539
|
to { opacity: 1; transform: none }
|
|
1449
1540
|
}
|
|
1450
|
-
@keyframes ${
|
|
1541
|
+
@keyframes ${PREFIX2}-ping {
|
|
1451
1542
|
0% { transform: scale(1); opacity: 0.5 }
|
|
1452
1543
|
70%, 100% { transform: scale(2.6); opacity: 0 }
|
|
1453
1544
|
}
|
|
1454
1545
|
|
|
1455
|
-
@keyframes ${
|
|
1546
|
+
@keyframes ${PREFIX2}-orbit { to { transform: rotate(360deg) } }
|
|
1456
1547
|
/* THE BUSY RING. The light of the QR well, around any control that is
|
|
1457
1548
|
waiting on the provider: the button on a phone between the tap and the
|
|
1458
1549
|
hand-over to the app. The well's sweep is a cone from the centre, which is
|
|
@@ -1469,13 +1560,13 @@ var CSS = `
|
|
|
1469
1560
|
straight fade from the head to nothing three tenths of the way back; the
|
|
1470
1561
|
component sets each layer's length, offset and opacity. Shown only while
|
|
1471
1562
|
busy. */
|
|
1472
|
-
.${
|
|
1563
|
+
.${PREFIX2}-ring {
|
|
1473
1564
|
position: relative;
|
|
1474
1565
|
display: inline-flex;
|
|
1475
1566
|
vertical-align: middle;
|
|
1476
1567
|
--zrl-beam-time: 4s;
|
|
1477
1568
|
}
|
|
1478
|
-
.${
|
|
1569
|
+
.${PREFIX2}-ring-svg {
|
|
1479
1570
|
position: absolute;
|
|
1480
1571
|
inset: -4px;
|
|
1481
1572
|
width: calc(100% + 8px);
|
|
@@ -1485,8 +1576,8 @@ var CSS = `
|
|
|
1485
1576
|
opacity: 0;
|
|
1486
1577
|
transition: opacity 200ms ease-out;
|
|
1487
1578
|
}
|
|
1488
|
-
.${
|
|
1489
|
-
.${
|
|
1579
|
+
.${PREFIX2}-ring[data-busy="true"] > .${PREFIX2}-ring-svg { opacity: 1; }
|
|
1580
|
+
.${PREFIX2}-ring-svg rect {
|
|
1490
1581
|
--zrl-l: var(--zrl-ring-len, 600px);
|
|
1491
1582
|
x: 2px;
|
|
1492
1583
|
y: 2px;
|
|
@@ -1497,37 +1588,37 @@ var CSS = `
|
|
|
1497
1588
|
stroke-width: var(--zrl-beam-line);
|
|
1498
1589
|
stroke-linecap: round;
|
|
1499
1590
|
stroke-dashoffset: var(--zrl-s, 0px);
|
|
1500
|
-
animation: ${
|
|
1591
|
+
animation: ${PREFIX2}-dash var(--zrl-beam-time) linear infinite;
|
|
1501
1592
|
}
|
|
1502
|
-
.${
|
|
1503
|
-
.${
|
|
1593
|
+
.${PREFIX2}-ring-head { stroke: var(--zrl-beam-head); }
|
|
1594
|
+
.${PREFIX2}-ring-halo {
|
|
1504
1595
|
stroke-width: calc(var(--zrl-glow-core) * 2 + var(--zrl-beam-line));
|
|
1505
1596
|
filter: blur(var(--zrl-glow-blur));
|
|
1506
1597
|
}
|
|
1507
|
-
@keyframes ${
|
|
1598
|
+
@keyframes ${PREFIX2}-dash {
|
|
1508
1599
|
from { stroke-dashoffset: var(--zrl-s, 0px); }
|
|
1509
1600
|
to { stroke-dashoffset: calc(var(--zrl-s, 0px) - var(--zrl-l)); }
|
|
1510
1601
|
}
|
|
1511
1602
|
@media (prefers-reduced-motion: reduce) {
|
|
1512
|
-
.${
|
|
1513
|
-
.${
|
|
1603
|
+
.${PREFIX2}-ring-svg rect { animation: none; stroke-dasharray: none; opacity: 0.45; }
|
|
1604
|
+
.${PREFIX2}-ring-halo, .${PREFIX2}-ring-head { display: none; }
|
|
1514
1605
|
}
|
|
1515
1606
|
|
|
1516
1607
|
|
|
1517
1608
|
@media (prefers-reduced-motion: reduce) {
|
|
1518
|
-
.${
|
|
1519
|
-
.${
|
|
1520
|
-
.${
|
|
1521
|
-
.${
|
|
1522
|
-
.${
|
|
1523
|
-
.${
|
|
1524
|
-
.${
|
|
1525
|
-
.${
|
|
1609
|
+
.${PREFIX2}-scrim,
|
|
1610
|
+
.${PREFIX2}-card,
|
|
1611
|
+
.${PREFIX2}-qr-overlay { animation: none }
|
|
1612
|
+
.${PREFIX2}-dot i:first-child { animation: none; opacity: 0.35 }
|
|
1613
|
+
.${PREFIX2}-qr,
|
|
1614
|
+
.${PREFIX2}-cancel,
|
|
1615
|
+
.${PREFIX2}-close,
|
|
1616
|
+
.${PREFIX2}-timer { transition: none }
|
|
1526
1617
|
/* No travelling light; the edge keeps its dim static glow instead. */
|
|
1527
|
-
.${
|
|
1528
|
-
.${
|
|
1529
|
-
.${
|
|
1530
|
-
.${
|
|
1618
|
+
.${PREFIX2}-qr-beam::after,
|
|
1619
|
+
.${PREFIX2}-qr-beam-glow-band::after { animation: none; opacity: 0 }
|
|
1620
|
+
.${PREFIX2}-qr-beam::before { opacity: 0.55 }
|
|
1621
|
+
.${PREFIX2}-qr-beam-glow-band::before { opacity: 0.7 }
|
|
1531
1622
|
}
|
|
1532
1623
|
`;
|
|
1533
1624
|
function ensureStyles() {
|
|
@@ -1932,8 +2023,23 @@ function startLogin(options) {
|
|
|
1932
2023
|
try {
|
|
1933
2024
|
let code;
|
|
1934
2025
|
let selectBy = "device";
|
|
2026
|
+
let returnId = null;
|
|
1935
2027
|
if (useAppLink && typeof window !== "undefined") {
|
|
1936
2028
|
const requestId = generateRequestId();
|
|
2029
|
+
saveReturnFlow({
|
|
2030
|
+
v: 1,
|
|
2031
|
+
issuer,
|
|
2032
|
+
clientId: options.clientId,
|
|
2033
|
+
flow,
|
|
2034
|
+
verifier,
|
|
2035
|
+
nonce,
|
|
2036
|
+
state,
|
|
2037
|
+
scope: options.scope ?? "openid",
|
|
2038
|
+
appState: options.app_state,
|
|
2039
|
+
requestId,
|
|
2040
|
+
createdAt: Date.now()
|
|
2041
|
+
});
|
|
2042
|
+
returnId = requestId;
|
|
1937
2043
|
const startUrl = sameDeviceStartUrl(issuer, {
|
|
1938
2044
|
client_id: options.clientId,
|
|
1939
2045
|
scope: options.scope ?? "openid",
|
|
@@ -1946,7 +2052,8 @@ function startLogin(options) {
|
|
|
1946
2052
|
prompt: options.prompt,
|
|
1947
2053
|
locale: options.locale,
|
|
1948
2054
|
request_id: requestId,
|
|
1949
|
-
origin: window.location.origin
|
|
2055
|
+
origin: window.location.origin,
|
|
2056
|
+
return_to: returnToUrl()
|
|
1950
2057
|
});
|
|
1951
2058
|
selectBy = "app_link";
|
|
1952
2059
|
surface.requestId = requestId;
|
|
@@ -1968,6 +2075,9 @@ function startLogin(options) {
|
|
|
1968
2075
|
controller.signal,
|
|
1969
2076
|
{ tolerateUnknownUntil: Date.now() + 15e3 }
|
|
1970
2077
|
);
|
|
2078
|
+
if (isReturnDone(requestId)) {
|
|
2079
|
+
throw new DOMException("aborted", "AbortError");
|
|
2080
|
+
}
|
|
1971
2081
|
} else {
|
|
1972
2082
|
const started = await startPairing(
|
|
1973
2083
|
issuer,
|
|
@@ -2068,6 +2178,7 @@ function startLogin(options) {
|
|
|
2068
2178
|
}
|
|
2069
2179
|
teardown();
|
|
2070
2180
|
}
|
|
2181
|
+
if (returnId) markReturnDone(returnId);
|
|
2071
2182
|
if (flow === "auth-code") {
|
|
2072
2183
|
const response2 = {
|
|
2073
2184
|
code,
|
|
@@ -2121,6 +2232,67 @@ function startLogin(options) {
|
|
|
2121
2232
|
}
|
|
2122
2233
|
};
|
|
2123
2234
|
}
|
|
2235
|
+
function resumeLogin(options) {
|
|
2236
|
+
const id = pendingReturnId();
|
|
2237
|
+
if (!id) return null;
|
|
2238
|
+
const saved = peekReturnFlow(id);
|
|
2239
|
+
if (!saved || saved.clientId !== options.clientId) return null;
|
|
2240
|
+
forgetReturnFlow(id);
|
|
2241
|
+
const issuer = options.issuer ?? saved.issuer;
|
|
2242
|
+
const controller = new AbortController();
|
|
2243
|
+
const promise = (async () => {
|
|
2244
|
+
const code = await pollUntilApproved(
|
|
2245
|
+
issuer,
|
|
2246
|
+
id,
|
|
2247
|
+
(s) => options.onState?.({ ...s, appLink: true }),
|
|
2248
|
+
controller.signal,
|
|
2249
|
+
{ tolerateUnknownUntil: Date.now() + 5e3 }
|
|
2250
|
+
);
|
|
2251
|
+
let response;
|
|
2252
|
+
if (saved.flow === "auth-code") {
|
|
2253
|
+
response = {
|
|
2254
|
+
code,
|
|
2255
|
+
scope: saved.scope,
|
|
2256
|
+
app_state: saved.appState,
|
|
2257
|
+
code_verifier: saved.verifier,
|
|
2258
|
+
nonce: saved.nonce
|
|
2259
|
+
};
|
|
2260
|
+
} else {
|
|
2261
|
+
const tokens = await exchangeCode(issuer, {
|
|
2262
|
+
code,
|
|
2263
|
+
code_verifier: saved.verifier,
|
|
2264
|
+
client_id: options.clientId
|
|
2265
|
+
});
|
|
2266
|
+
const claims = unsafeClaims(tokens.id_token);
|
|
2267
|
+
response = {
|
|
2268
|
+
credential: tokens.id_token,
|
|
2269
|
+
clientId: options.clientId,
|
|
2270
|
+
select_by: "app_link",
|
|
2271
|
+
acr: claims.acr ?? "zoreal.device"
|
|
2272
|
+
};
|
|
2273
|
+
}
|
|
2274
|
+
markReturnDone(id);
|
|
2275
|
+
return response;
|
|
2276
|
+
})();
|
|
2277
|
+
promise.catch(() => {
|
|
2278
|
+
});
|
|
2279
|
+
return {
|
|
2280
|
+
promise,
|
|
2281
|
+
cancel: () => controller.abort(),
|
|
2282
|
+
get requestId() {
|
|
2283
|
+
return id;
|
|
2284
|
+
},
|
|
2285
|
+
get pairUrl() {
|
|
2286
|
+
return void 0;
|
|
2287
|
+
},
|
|
2288
|
+
get qrUrl() {
|
|
2289
|
+
return void 0;
|
|
2290
|
+
},
|
|
2291
|
+
get appLink() {
|
|
2292
|
+
return true;
|
|
2293
|
+
}
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2124
2296
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2125
2297
|
0 && (module.exports = {
|
|
2126
2298
|
DEFAULT_ISSUER,
|
|
@@ -2141,6 +2313,7 @@ function startLogin(options) {
|
|
|
2141
2313
|
mountPairingModal,
|
|
2142
2314
|
pollUntilApproved,
|
|
2143
2315
|
resolveIntent,
|
|
2316
|
+
resumeLogin,
|
|
2144
2317
|
startLogin,
|
|
2145
2318
|
startPairing,
|
|
2146
2319
|
unsafeClaims
|