@zoreal/oauth2-js 0.1.19 → 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 +233 -80
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -2
- package/dist/index.d.ts +13 -2
- package/dist/index.js +232 -80
- 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;
|
|
@@ -244,6 +245,76 @@ function titleFor(t, intent) {
|
|
|
244
245
|
return t.title;
|
|
245
246
|
}
|
|
246
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
|
+
|
|
247
318
|
// src/i18n.ts
|
|
248
319
|
var en = {
|
|
249
320
|
title: "Scan to sign in",
|
|
@@ -1087,8 +1158,8 @@ function interpolate(template, time) {
|
|
|
1087
1158
|
}
|
|
1088
1159
|
|
|
1089
1160
|
// src/styles.ts
|
|
1090
|
-
var
|
|
1091
|
-
var cx = (name) => `${
|
|
1161
|
+
var PREFIX2 = "zrl";
|
|
1162
|
+
var cx = (name) => `${PREFIX2}-${name}`;
|
|
1092
1163
|
var STYLE_ELEMENT_ID = "zoreal-pairing-styles";
|
|
1093
1164
|
var LIGHT = `
|
|
1094
1165
|
--zrl-scrim: rgba(16, 18, 27, 0.45);
|
|
@@ -1152,13 +1223,13 @@ var DARK = `
|
|
|
1152
1223
|
--zrl-glow-opacity: 0.85;
|
|
1153
1224
|
`;
|
|
1154
1225
|
var CSS = `
|
|
1155
|
-
.${
|
|
1156
|
-
.${
|
|
1226
|
+
.${PREFIX2}-root { ${LIGHT} }
|
|
1227
|
+
.${PREFIX2}-root[data-theme="dark"] { ${DARK} }
|
|
1157
1228
|
@media (prefers-color-scheme: dark) {
|
|
1158
|
-
.${
|
|
1229
|
+
.${PREFIX2}-root[data-theme="auto"] { ${DARK} }
|
|
1159
1230
|
}
|
|
1160
1231
|
|
|
1161
|
-
.${
|
|
1232
|
+
.${PREFIX2}-scrim {
|
|
1162
1233
|
position: fixed;
|
|
1163
1234
|
inset: 0;
|
|
1164
1235
|
z-index: 2147483000;
|
|
@@ -1170,10 +1241,10 @@ var CSS = `
|
|
|
1170
1241
|
backdrop-filter: blur(3px);
|
|
1171
1242
|
-webkit-backdrop-filter: blur(3px);
|
|
1172
1243
|
font-family: inherit;
|
|
1173
|
-
animation: ${
|
|
1244
|
+
animation: ${PREFIX2}-fade 200ms ease-out both;
|
|
1174
1245
|
}
|
|
1175
1246
|
|
|
1176
|
-
.${
|
|
1247
|
+
.${PREFIX2}-card {
|
|
1177
1248
|
position: relative;
|
|
1178
1249
|
box-sizing: border-box;
|
|
1179
1250
|
width: 100%;
|
|
@@ -1185,14 +1256,14 @@ var CSS = `
|
|
|
1185
1256
|
outline: 1px solid var(--zrl-ring);
|
|
1186
1257
|
outline-offset: -1px;
|
|
1187
1258
|
text-align: center;
|
|
1188
|
-
animation: ${
|
|
1259
|
+
animation: ${PREFIX2}-rise 300ms cubic-bezier(0.23, 1, 0.32, 1) both;
|
|
1189
1260
|
}
|
|
1190
1261
|
|
|
1191
|
-
.${
|
|
1262
|
+
.${PREFIX2}-body { padding: 28px 24px 20px; }
|
|
1192
1263
|
|
|
1193
|
-
.${
|
|
1264
|
+
.${PREFIX2}-lockup { display: block; margin: 0 auto; color: var(--zrl-ink); }
|
|
1194
1265
|
|
|
1195
|
-
.${
|
|
1266
|
+
.${PREFIX2}-title {
|
|
1196
1267
|
margin: 18px 0 0;
|
|
1197
1268
|
font-size: 18px;
|
|
1198
1269
|
font-weight: 600;
|
|
@@ -1201,7 +1272,7 @@ var CSS = `
|
|
|
1201
1272
|
color: var(--zrl-ink);
|
|
1202
1273
|
}
|
|
1203
1274
|
|
|
1204
|
-
.${
|
|
1275
|
+
.${PREFIX2}-body-text {
|
|
1205
1276
|
margin: 6px auto 0;
|
|
1206
1277
|
max-width: 30ch;
|
|
1207
1278
|
font-size: 14px;
|
|
@@ -1209,7 +1280,7 @@ var CSS = `
|
|
|
1209
1280
|
color: var(--zrl-ink-soft);
|
|
1210
1281
|
}
|
|
1211
1282
|
|
|
1212
|
-
.${
|
|
1283
|
+
.${PREFIX2}-qr-well {
|
|
1213
1284
|
position: relative;
|
|
1214
1285
|
display: grid;
|
|
1215
1286
|
place-items: center;
|
|
@@ -1248,9 +1319,9 @@ var CSS = `
|
|
|
1248
1319
|
All three share one containing block, the well's padding box, so the two
|
|
1249
1320
|
comets ride the same path; the spent badge is a later sibling and paints
|
|
1250
1321
|
above them. */
|
|
1251
|
-
.${
|
|
1252
|
-
.${
|
|
1253
|
-
.${
|
|
1322
|
+
.${PREFIX2}-qr-beam,
|
|
1323
|
+
.${PREFIX2}-qr-beam-glow,
|
|
1324
|
+
.${PREFIX2}-qr-beam-glow-band {
|
|
1254
1325
|
position: absolute;
|
|
1255
1326
|
inset: calc(0px - var(--zrl-beam-line));
|
|
1256
1327
|
border: var(--zrl-beam-line) solid transparent;
|
|
@@ -1263,7 +1334,7 @@ var CSS = `
|
|
|
1263
1334
|
mask-clip: padding-box, border-box;
|
|
1264
1335
|
mask-composite: intersect;
|
|
1265
1336
|
}
|
|
1266
|
-
.${
|
|
1337
|
+
.${PREFIX2}-qr-beam-glow {
|
|
1267
1338
|
inset: calc(0px - var(--zrl-glow-reach));
|
|
1268
1339
|
border-width: var(--zrl-glow-reach);
|
|
1269
1340
|
border-radius: calc(var(--zrl-radius) - 1px + var(--zrl-glow-reach));
|
|
@@ -1271,7 +1342,7 @@ var CSS = `
|
|
|
1271
1342
|
opacity: var(--zrl-glow-opacity);
|
|
1272
1343
|
will-change: filter;
|
|
1273
1344
|
}
|
|
1274
|
-
.${
|
|
1345
|
+
.${PREFIX2}-qr-beam-glow-band {
|
|
1275
1346
|
inset: calc(0px - var(--zrl-glow-core));
|
|
1276
1347
|
border-width: var(--zrl-glow-core);
|
|
1277
1348
|
border-radius: calc(var(--zrl-radius) - 1px + var(--zrl-glow-core));
|
|
@@ -1282,8 +1353,8 @@ var CSS = `
|
|
|
1282
1353
|
border reads as the well's own line with a light passing over it; shown
|
|
1283
1354
|
once the light has stopped. Every path below ends here, which is what
|
|
1284
1355
|
makes them look the same at rest. */
|
|
1285
|
-
.${
|
|
1286
|
-
.${
|
|
1356
|
+
.${PREFIX2}-qr-beam::before,
|
|
1357
|
+
.${PREFIX2}-qr-beam-glow-band::before {
|
|
1287
1358
|
content: '';
|
|
1288
1359
|
position: absolute;
|
|
1289
1360
|
inset: -50%;
|
|
@@ -1296,8 +1367,8 @@ var CSS = `
|
|
|
1296
1367
|
whole. A transform animation runs on the compositor, so the light keeps
|
|
1297
1368
|
moving while the page is busy; animating the gradient angle instead
|
|
1298
1369
|
repaints every frame on the main thread and stutters. */
|
|
1299
|
-
.${
|
|
1300
|
-
.${
|
|
1370
|
+
.${PREFIX2}-qr-beam::after,
|
|
1371
|
+
.${PREFIX2}-qr-beam-glow-band::after {
|
|
1301
1372
|
content: '';
|
|
1302
1373
|
position: absolute;
|
|
1303
1374
|
inset: -50%;
|
|
@@ -1308,7 +1379,7 @@ var CSS = `
|
|
|
1308
1379
|
var(--zrl-beam-head) 348deg,
|
|
1309
1380
|
transparent 356deg 360deg
|
|
1310
1381
|
);
|
|
1311
|
-
animation: ${
|
|
1382
|
+
animation: ${PREFIX2}-orbit var(--zrl-beam-time) linear infinite;
|
|
1312
1383
|
will-change: transform;
|
|
1313
1384
|
transition: opacity 400ms cubic-bezier(0.23, 1, 0.32, 1);
|
|
1314
1385
|
}
|
|
@@ -1316,15 +1387,15 @@ var CSS = `
|
|
|
1316
1387
|
/* Spent: the light stops where it is and fades, and the edge settles to the
|
|
1317
1388
|
dim glow. Paused rather than removed, so it does not jump back to its start
|
|
1318
1389
|
on the way out. */
|
|
1319
|
-
.${
|
|
1320
|
-
.${
|
|
1390
|
+
.${PREFIX2}-qr-well[data-spent="true"] .${PREFIX2}-qr-beam::after,
|
|
1391
|
+
.${PREFIX2}-qr-well[data-spent="true"] .${PREFIX2}-qr-beam-glow-band::after {
|
|
1321
1392
|
animation-play-state: paused;
|
|
1322
1393
|
opacity: 0;
|
|
1323
1394
|
}
|
|
1324
|
-
.${
|
|
1325
|
-
.${
|
|
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; }
|
|
1326
1397
|
|
|
1327
|
-
.${
|
|
1398
|
+
.${PREFIX2}-qr {
|
|
1328
1399
|
display: block;
|
|
1329
1400
|
width: 100%;
|
|
1330
1401
|
height: 100%;
|
|
@@ -1339,17 +1410,17 @@ var CSS = `
|
|
|
1339
1410
|
/* Once the code is claimed the QR is spent. Blurring it out rather than
|
|
1340
1411
|
swapping it keeps one object on screen through the state change, so the eye
|
|
1341
1412
|
reads a transformation instead of two things trading places. */
|
|
1342
|
-
.${
|
|
1413
|
+
.${PREFIX2}-qr[data-spent="true"] { opacity: 0.2; filter: var(--zrl-qr-spent-filter); transform: scale(0.96); }
|
|
1343
1414
|
|
|
1344
|
-
.${
|
|
1415
|
+
.${PREFIX2}-qr-overlay {
|
|
1345
1416
|
position: absolute;
|
|
1346
1417
|
inset: 0;
|
|
1347
1418
|
display: grid;
|
|
1348
1419
|
place-items: center;
|
|
1349
|
-
animation: ${
|
|
1420
|
+
animation: ${PREFIX2}-fade 200ms ease-out both;
|
|
1350
1421
|
}
|
|
1351
1422
|
|
|
1352
|
-
.${
|
|
1423
|
+
.${PREFIX2}-qr-badge {
|
|
1353
1424
|
display: grid;
|
|
1354
1425
|
place-items: center;
|
|
1355
1426
|
width: 56px;
|
|
@@ -1359,7 +1430,7 @@ var CSS = `
|
|
|
1359
1430
|
color: var(--zrl-accent-ink);
|
|
1360
1431
|
}
|
|
1361
1432
|
|
|
1362
|
-
.${
|
|
1433
|
+
.${PREFIX2}-status {
|
|
1363
1434
|
display: flex;
|
|
1364
1435
|
align-items: center;
|
|
1365
1436
|
justify-content: center;
|
|
@@ -1370,8 +1441,8 @@ var CSS = `
|
|
|
1370
1441
|
color: var(--zrl-ink);
|
|
1371
1442
|
}
|
|
1372
1443
|
|
|
1373
|
-
.${
|
|
1374
|
-
.${
|
|
1444
|
+
.${PREFIX2}-dot { position: relative; display: grid; place-items: center; width: 8px; height: 8px; }
|
|
1445
|
+
.${PREFIX2}-dot i {
|
|
1375
1446
|
position: absolute;
|
|
1376
1447
|
width: 8px;
|
|
1377
1448
|
height: 8px;
|
|
@@ -1379,25 +1450,25 @@ var CSS = `
|
|
|
1379
1450
|
background: var(--zrl-accent);
|
|
1380
1451
|
font-style: normal;
|
|
1381
1452
|
}
|
|
1382
|
-
.${
|
|
1453
|
+
.${PREFIX2}-dot i:first-child { animation: ${PREFIX2}-ping 1.8s cubic-bezier(0.23, 1, 0.32, 1) infinite; }
|
|
1383
1454
|
|
|
1384
|
-
.${
|
|
1455
|
+
.${PREFIX2}-timer {
|
|
1385
1456
|
margin: 4px 0 0;
|
|
1386
1457
|
font-size: 12px;
|
|
1387
1458
|
font-variant-numeric: tabular-nums;
|
|
1388
1459
|
color: var(--zrl-ink-mute);
|
|
1389
1460
|
transition: color 200ms ease-out;
|
|
1390
1461
|
}
|
|
1391
|
-
.${
|
|
1462
|
+
.${PREFIX2}-timer[data-urgent="true"] { color: var(--zrl-urgent); }
|
|
1392
1463
|
|
|
1393
|
-
.${
|
|
1464
|
+
.${PREFIX2}-help {
|
|
1394
1465
|
padding: 14px 24px;
|
|
1395
1466
|
border-top: 1px solid var(--zrl-line-soft);
|
|
1396
1467
|
background: var(--zrl-surface-sunken);
|
|
1397
1468
|
border-radius: 0;
|
|
1398
1469
|
}
|
|
1399
|
-
.${
|
|
1400
|
-
.${
|
|
1470
|
+
.${PREFIX2}-help-title { margin: 0; font-size: 12px; font-weight: 600; color: var(--zrl-ink); }
|
|
1471
|
+
.${PREFIX2}-help-body {
|
|
1401
1472
|
margin: 4px auto 0;
|
|
1402
1473
|
max-width: 34ch;
|
|
1403
1474
|
font-size: 12px;
|
|
@@ -1405,9 +1476,9 @@ var CSS = `
|
|
|
1405
1476
|
color: var(--zrl-ink-soft);
|
|
1406
1477
|
}
|
|
1407
1478
|
|
|
1408
|
-
.${
|
|
1479
|
+
.${PREFIX2}-footer { padding: 12px; border-top: 1px solid var(--zrl-line-soft); }
|
|
1409
1480
|
|
|
1410
|
-
.${
|
|
1481
|
+
.${PREFIX2}-cancel {
|
|
1411
1482
|
display: block;
|
|
1412
1483
|
width: 100%;
|
|
1413
1484
|
padding: 10px;
|
|
@@ -1421,10 +1492,10 @@ var CSS = `
|
|
|
1421
1492
|
cursor: pointer;
|
|
1422
1493
|
transition: background-color 150ms ease-out, color 150ms ease-out, transform 150ms ease-out;
|
|
1423
1494
|
}
|
|
1424
|
-
.${
|
|
1425
|
-
.${
|
|
1495
|
+
.${PREFIX2}-cancel:hover { background: var(--zrl-surface-sunken); color: var(--zrl-ink); }
|
|
1496
|
+
.${PREFIX2}-cancel:active { transform: scale(0.99); }
|
|
1426
1497
|
|
|
1427
|
-
.${
|
|
1498
|
+
.${PREFIX2}-secured {
|
|
1428
1499
|
display: flex;
|
|
1429
1500
|
align-items: center;
|
|
1430
1501
|
justify-content: center;
|
|
@@ -1436,9 +1507,9 @@ var CSS = `
|
|
|
1436
1507
|
border-radius: 6px;
|
|
1437
1508
|
transition: color 150ms ease-out;
|
|
1438
1509
|
}
|
|
1439
|
-
.${
|
|
1510
|
+
.${PREFIX2}-secured:hover { color: var(--zrl-ink); }
|
|
1440
1511
|
|
|
1441
|
-
.${
|
|
1512
|
+
.${PREFIX2}-close {
|
|
1442
1513
|
position: absolute;
|
|
1443
1514
|
top: 12px;
|
|
1444
1515
|
inset-inline-end: 12px;
|
|
@@ -1454,25 +1525,25 @@ var CSS = `
|
|
|
1454
1525
|
cursor: pointer;
|
|
1455
1526
|
transition: background-color 150ms ease-out, color 150ms ease-out, transform 150ms ease-out;
|
|
1456
1527
|
}
|
|
1457
|
-
.${
|
|
1458
|
-
.${
|
|
1528
|
+
.${PREFIX2}-close:hover { background: var(--zrl-surface-sunken); color: var(--zrl-ink); }
|
|
1529
|
+
.${PREFIX2}-close:active { transform: scale(0.95); }
|
|
1459
1530
|
|
|
1460
|
-
.${
|
|
1531
|
+
.${PREFIX2}-card :focus-visible {
|
|
1461
1532
|
outline: 2px solid var(--zrl-accent);
|
|
1462
1533
|
outline-offset: 2px;
|
|
1463
1534
|
}
|
|
1464
1535
|
|
|
1465
|
-
@keyframes ${
|
|
1466
|
-
@keyframes ${
|
|
1536
|
+
@keyframes ${PREFIX2}-fade { from { opacity: 0 } to { opacity: 1 } }
|
|
1537
|
+
@keyframes ${PREFIX2}-rise {
|
|
1467
1538
|
from { opacity: 0; transform: translateY(10px) scale(0.98) }
|
|
1468
1539
|
to { opacity: 1; transform: none }
|
|
1469
1540
|
}
|
|
1470
|
-
@keyframes ${
|
|
1541
|
+
@keyframes ${PREFIX2}-ping {
|
|
1471
1542
|
0% { transform: scale(1); opacity: 0.5 }
|
|
1472
1543
|
70%, 100% { transform: scale(2.6); opacity: 0 }
|
|
1473
1544
|
}
|
|
1474
1545
|
|
|
1475
|
-
@keyframes ${
|
|
1546
|
+
@keyframes ${PREFIX2}-orbit { to { transform: rotate(360deg) } }
|
|
1476
1547
|
/* THE BUSY RING. The light of the QR well, around any control that is
|
|
1477
1548
|
waiting on the provider: the button on a phone between the tap and the
|
|
1478
1549
|
hand-over to the app. The well's sweep is a cone from the centre, which is
|
|
@@ -1489,13 +1560,13 @@ var CSS = `
|
|
|
1489
1560
|
straight fade from the head to nothing three tenths of the way back; the
|
|
1490
1561
|
component sets each layer's length, offset and opacity. Shown only while
|
|
1491
1562
|
busy. */
|
|
1492
|
-
.${
|
|
1563
|
+
.${PREFIX2}-ring {
|
|
1493
1564
|
position: relative;
|
|
1494
1565
|
display: inline-flex;
|
|
1495
1566
|
vertical-align: middle;
|
|
1496
1567
|
--zrl-beam-time: 4s;
|
|
1497
1568
|
}
|
|
1498
|
-
.${
|
|
1569
|
+
.${PREFIX2}-ring-svg {
|
|
1499
1570
|
position: absolute;
|
|
1500
1571
|
inset: -4px;
|
|
1501
1572
|
width: calc(100% + 8px);
|
|
@@ -1505,8 +1576,8 @@ var CSS = `
|
|
|
1505
1576
|
opacity: 0;
|
|
1506
1577
|
transition: opacity 200ms ease-out;
|
|
1507
1578
|
}
|
|
1508
|
-
.${
|
|
1509
|
-
.${
|
|
1579
|
+
.${PREFIX2}-ring[data-busy="true"] > .${PREFIX2}-ring-svg { opacity: 1; }
|
|
1580
|
+
.${PREFIX2}-ring-svg rect {
|
|
1510
1581
|
--zrl-l: var(--zrl-ring-len, 600px);
|
|
1511
1582
|
x: 2px;
|
|
1512
1583
|
y: 2px;
|
|
@@ -1517,37 +1588,37 @@ var CSS = `
|
|
|
1517
1588
|
stroke-width: var(--zrl-beam-line);
|
|
1518
1589
|
stroke-linecap: round;
|
|
1519
1590
|
stroke-dashoffset: var(--zrl-s, 0px);
|
|
1520
|
-
animation: ${
|
|
1591
|
+
animation: ${PREFIX2}-dash var(--zrl-beam-time) linear infinite;
|
|
1521
1592
|
}
|
|
1522
|
-
.${
|
|
1523
|
-
.${
|
|
1593
|
+
.${PREFIX2}-ring-head { stroke: var(--zrl-beam-head); }
|
|
1594
|
+
.${PREFIX2}-ring-halo {
|
|
1524
1595
|
stroke-width: calc(var(--zrl-glow-core) * 2 + var(--zrl-beam-line));
|
|
1525
1596
|
filter: blur(var(--zrl-glow-blur));
|
|
1526
1597
|
}
|
|
1527
|
-
@keyframes ${
|
|
1598
|
+
@keyframes ${PREFIX2}-dash {
|
|
1528
1599
|
from { stroke-dashoffset: var(--zrl-s, 0px); }
|
|
1529
1600
|
to { stroke-dashoffset: calc(var(--zrl-s, 0px) - var(--zrl-l)); }
|
|
1530
1601
|
}
|
|
1531
1602
|
@media (prefers-reduced-motion: reduce) {
|
|
1532
|
-
.${
|
|
1533
|
-
.${
|
|
1603
|
+
.${PREFIX2}-ring-svg rect { animation: none; stroke-dasharray: none; opacity: 0.45; }
|
|
1604
|
+
.${PREFIX2}-ring-halo, .${PREFIX2}-ring-head { display: none; }
|
|
1534
1605
|
}
|
|
1535
1606
|
|
|
1536
1607
|
|
|
1537
1608
|
@media (prefers-reduced-motion: reduce) {
|
|
1538
|
-
.${
|
|
1539
|
-
.${
|
|
1540
|
-
.${
|
|
1541
|
-
.${
|
|
1542
|
-
.${
|
|
1543
|
-
.${
|
|
1544
|
-
.${
|
|
1545
|
-
.${
|
|
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 }
|
|
1546
1617
|
/* No travelling light; the edge keeps its dim static glow instead. */
|
|
1547
|
-
.${
|
|
1548
|
-
.${
|
|
1549
|
-
.${
|
|
1550
|
-
.${
|
|
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 }
|
|
1551
1622
|
}
|
|
1552
1623
|
`;
|
|
1553
1624
|
function ensureStyles() {
|
|
@@ -1952,8 +2023,23 @@ function startLogin(options) {
|
|
|
1952
2023
|
try {
|
|
1953
2024
|
let code;
|
|
1954
2025
|
let selectBy = "device";
|
|
2026
|
+
let returnId = null;
|
|
1955
2027
|
if (useAppLink && typeof window !== "undefined") {
|
|
1956
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;
|
|
1957
2043
|
const startUrl = sameDeviceStartUrl(issuer, {
|
|
1958
2044
|
client_id: options.clientId,
|
|
1959
2045
|
scope: options.scope ?? "openid",
|
|
@@ -1966,7 +2052,8 @@ function startLogin(options) {
|
|
|
1966
2052
|
prompt: options.prompt,
|
|
1967
2053
|
locale: options.locale,
|
|
1968
2054
|
request_id: requestId,
|
|
1969
|
-
origin: window.location.origin
|
|
2055
|
+
origin: window.location.origin,
|
|
2056
|
+
return_to: returnToUrl()
|
|
1970
2057
|
});
|
|
1971
2058
|
selectBy = "app_link";
|
|
1972
2059
|
surface.requestId = requestId;
|
|
@@ -1988,6 +2075,9 @@ function startLogin(options) {
|
|
|
1988
2075
|
controller.signal,
|
|
1989
2076
|
{ tolerateUnknownUntil: Date.now() + 15e3 }
|
|
1990
2077
|
);
|
|
2078
|
+
if (isReturnDone(requestId)) {
|
|
2079
|
+
throw new DOMException("aborted", "AbortError");
|
|
2080
|
+
}
|
|
1991
2081
|
} else {
|
|
1992
2082
|
const started = await startPairing(
|
|
1993
2083
|
issuer,
|
|
@@ -2088,6 +2178,7 @@ function startLogin(options) {
|
|
|
2088
2178
|
}
|
|
2089
2179
|
teardown();
|
|
2090
2180
|
}
|
|
2181
|
+
if (returnId) markReturnDone(returnId);
|
|
2091
2182
|
if (flow === "auth-code") {
|
|
2092
2183
|
const response2 = {
|
|
2093
2184
|
code,
|
|
@@ -2141,6 +2232,67 @@ function startLogin(options) {
|
|
|
2141
2232
|
}
|
|
2142
2233
|
};
|
|
2143
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
|
+
}
|
|
2144
2296
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2145
2297
|
0 && (module.exports = {
|
|
2146
2298
|
DEFAULT_ISSUER,
|
|
@@ -2161,6 +2313,7 @@ function startLogin(options) {
|
|
|
2161
2313
|
mountPairingModal,
|
|
2162
2314
|
pollUntilApproved,
|
|
2163
2315
|
resolveIntent,
|
|
2316
|
+
resumeLogin,
|
|
2164
2317
|
startLogin,
|
|
2165
2318
|
startPairing,
|
|
2166
2319
|
unsafeClaims
|