@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 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.19";
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 PREFIX = "zrl";
1091
- var cx = (name) => `${PREFIX}-${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
- .${PREFIX}-root { ${LIGHT} }
1156
- .${PREFIX}-root[data-theme="dark"] { ${DARK} }
1226
+ .${PREFIX2}-root { ${LIGHT} }
1227
+ .${PREFIX2}-root[data-theme="dark"] { ${DARK} }
1157
1228
  @media (prefers-color-scheme: dark) {
1158
- .${PREFIX}-root[data-theme="auto"] { ${DARK} }
1229
+ .${PREFIX2}-root[data-theme="auto"] { ${DARK} }
1159
1230
  }
1160
1231
 
1161
- .${PREFIX}-scrim {
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: ${PREFIX}-fade 200ms ease-out both;
1244
+ animation: ${PREFIX2}-fade 200ms ease-out both;
1174
1245
  }
1175
1246
 
1176
- .${PREFIX}-card {
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: ${PREFIX}-rise 300ms cubic-bezier(0.23, 1, 0.32, 1) both;
1259
+ animation: ${PREFIX2}-rise 300ms cubic-bezier(0.23, 1, 0.32, 1) both;
1189
1260
  }
1190
1261
 
1191
- .${PREFIX}-body { padding: 28px 24px 20px; }
1262
+ .${PREFIX2}-body { padding: 28px 24px 20px; }
1192
1263
 
1193
- .${PREFIX}-lockup { display: block; margin: 0 auto; color: var(--zrl-ink); }
1264
+ .${PREFIX2}-lockup { display: block; margin: 0 auto; color: var(--zrl-ink); }
1194
1265
 
1195
- .${PREFIX}-title {
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
- .${PREFIX}-body-text {
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
- .${PREFIX}-qr-well {
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
- .${PREFIX}-qr-beam,
1252
- .${PREFIX}-qr-beam-glow,
1253
- .${PREFIX}-qr-beam-glow-band {
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
- .${PREFIX}-qr-beam-glow {
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
- .${PREFIX}-qr-beam-glow-band {
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
- .${PREFIX}-qr-beam::before,
1286
- .${PREFIX}-qr-beam-glow-band::before {
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
- .${PREFIX}-qr-beam::after,
1300
- .${PREFIX}-qr-beam-glow-band::after {
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: ${PREFIX}-orbit var(--zrl-beam-time) linear infinite;
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
- .${PREFIX}-qr-well[data-spent="true"] .${PREFIX}-qr-beam::after,
1320
- .${PREFIX}-qr-well[data-spent="true"] .${PREFIX}-qr-beam-glow-band::after {
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
- .${PREFIX}-qr-well[data-spent="true"] .${PREFIX}-qr-beam::before { opacity: 0.55; }
1325
- .${PREFIX}-qr-well[data-spent="true"] .${PREFIX}-qr-beam-glow-band::before { opacity: 0.7; }
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
- .${PREFIX}-qr {
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
- .${PREFIX}-qr[data-spent="true"] { opacity: 0.2; filter: var(--zrl-qr-spent-filter); transform: scale(0.96); }
1413
+ .${PREFIX2}-qr[data-spent="true"] { opacity: 0.2; filter: var(--zrl-qr-spent-filter); transform: scale(0.96); }
1343
1414
 
1344
- .${PREFIX}-qr-overlay {
1415
+ .${PREFIX2}-qr-overlay {
1345
1416
  position: absolute;
1346
1417
  inset: 0;
1347
1418
  display: grid;
1348
1419
  place-items: center;
1349
- animation: ${PREFIX}-fade 200ms ease-out both;
1420
+ animation: ${PREFIX2}-fade 200ms ease-out both;
1350
1421
  }
1351
1422
 
1352
- .${PREFIX}-qr-badge {
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
- .${PREFIX}-status {
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
- .${PREFIX}-dot { position: relative; display: grid; place-items: center; width: 8px; height: 8px; }
1374
- .${PREFIX}-dot i {
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
- .${PREFIX}-dot i:first-child { animation: ${PREFIX}-ping 1.8s cubic-bezier(0.23, 1, 0.32, 1) infinite; }
1453
+ .${PREFIX2}-dot i:first-child { animation: ${PREFIX2}-ping 1.8s cubic-bezier(0.23, 1, 0.32, 1) infinite; }
1383
1454
 
1384
- .${PREFIX}-timer {
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
- .${PREFIX}-timer[data-urgent="true"] { color: var(--zrl-urgent); }
1462
+ .${PREFIX2}-timer[data-urgent="true"] { color: var(--zrl-urgent); }
1392
1463
 
1393
- .${PREFIX}-help {
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
- .${PREFIX}-help-title { margin: 0; font-size: 12px; font-weight: 600; color: var(--zrl-ink); }
1400
- .${PREFIX}-help-body {
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
- .${PREFIX}-footer { padding: 12px; border-top: 1px solid var(--zrl-line-soft); }
1479
+ .${PREFIX2}-footer { padding: 12px; border-top: 1px solid var(--zrl-line-soft); }
1409
1480
 
1410
- .${PREFIX}-cancel {
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
- .${PREFIX}-cancel:hover { background: var(--zrl-surface-sunken); color: var(--zrl-ink); }
1425
- .${PREFIX}-cancel:active { transform: scale(0.99); }
1495
+ .${PREFIX2}-cancel:hover { background: var(--zrl-surface-sunken); color: var(--zrl-ink); }
1496
+ .${PREFIX2}-cancel:active { transform: scale(0.99); }
1426
1497
 
1427
- .${PREFIX}-secured {
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
- .${PREFIX}-secured:hover { color: var(--zrl-ink); }
1510
+ .${PREFIX2}-secured:hover { color: var(--zrl-ink); }
1440
1511
 
1441
- .${PREFIX}-close {
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
- .${PREFIX}-close:hover { background: var(--zrl-surface-sunken); color: var(--zrl-ink); }
1458
- .${PREFIX}-close:active { transform: scale(0.95); }
1528
+ .${PREFIX2}-close:hover { background: var(--zrl-surface-sunken); color: var(--zrl-ink); }
1529
+ .${PREFIX2}-close:active { transform: scale(0.95); }
1459
1530
 
1460
- .${PREFIX}-card :focus-visible {
1531
+ .${PREFIX2}-card :focus-visible {
1461
1532
  outline: 2px solid var(--zrl-accent);
1462
1533
  outline-offset: 2px;
1463
1534
  }
1464
1535
 
1465
- @keyframes ${PREFIX}-fade { from { opacity: 0 } to { opacity: 1 } }
1466
- @keyframes ${PREFIX}-rise {
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 ${PREFIX}-ping {
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 ${PREFIX}-orbit { to { transform: rotate(360deg) } }
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
- .${PREFIX}-ring {
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
- .${PREFIX}-ring-svg {
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
- .${PREFIX}-ring[data-busy="true"] > .${PREFIX}-ring-svg { opacity: 1; }
1509
- .${PREFIX}-ring-svg rect {
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: ${PREFIX}-dash var(--zrl-beam-time) linear infinite;
1591
+ animation: ${PREFIX2}-dash var(--zrl-beam-time) linear infinite;
1521
1592
  }
1522
- .${PREFIX}-ring-head { stroke: var(--zrl-beam-head); }
1523
- .${PREFIX}-ring-halo {
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 ${PREFIX}-dash {
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
- .${PREFIX}-ring-svg rect { animation: none; stroke-dasharray: none; opacity: 0.45; }
1533
- .${PREFIX}-ring-halo, .${PREFIX}-ring-head { display: none; }
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
- .${PREFIX}-scrim,
1539
- .${PREFIX}-card,
1540
- .${PREFIX}-qr-overlay { animation: none }
1541
- .${PREFIX}-dot i:first-child { animation: none; opacity: 0.35 }
1542
- .${PREFIX}-qr,
1543
- .${PREFIX}-cancel,
1544
- .${PREFIX}-close,
1545
- .${PREFIX}-timer { transition: none }
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
- .${PREFIX}-qr-beam::after,
1548
- .${PREFIX}-qr-beam-glow-band::after { animation: none; opacity: 0 }
1549
- .${PREFIX}-qr-beam::before { opacity: 0.55 }
1550
- .${PREFIX}-qr-beam-glow-band::before { opacity: 0.7 }
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