@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/dist/index.js CHANGED
@@ -14,7 +14,7 @@ function unsafeClaims(idToken) {
14
14
 
15
15
  // src/wire.ts
16
16
  var WIRE_VERSION = 1;
17
- var SDK_VERSION = "0.1.18";
17
+ var SDK_VERSION = "0.1.20";
18
18
  var SDK_NAME = "@zoreal/oauth2-js";
19
19
  var DEFAULT_ISSUER = "https://id.zoreal.com";
20
20
  var POLL_INTERVAL_MS = 2e3;
@@ -92,11 +92,30 @@ var sleep = (ms, signal) => new Promise((resolve, reject) => {
92
92
  }, ms);
93
93
  signal?.addEventListener("abort", onAbort, { once: true });
94
94
  });
95
+ var SETTLE_MS = 1500;
96
+ var NETWORK_FAILURES_TOLERATED = 4;
95
97
  async function pollUntilApproved(issuer, requestId, onState, signal, options = {}) {
98
+ const settlingUntil = options.tolerateUnknownUntil ?? 0;
99
+ let networkFailures = 0;
100
+ let last = { status: "pending" };
101
+ if (settlingUntil > Date.now()) await sleep(SETTLE_MS, signal);
96
102
  for (; ; ) {
97
- const response = await fetch(`${issuer}/pair/${encodeURIComponent(requestId)}/status`, {
98
- signal
99
- });
103
+ let response;
104
+ try {
105
+ response = await fetch(`${issuer}/pair/${encodeURIComponent(requestId)}/status`, {
106
+ signal
107
+ });
108
+ networkFailures = 0;
109
+ } catch (e) {
110
+ if (e instanceof DOMException && e.name === "AbortError") throw e;
111
+ networkFailures += 1;
112
+ if (settlingUntil > Date.now() || networkFailures <= NETWORK_FAILURES_TOLERATED) {
113
+ onState?.(last);
114
+ await sleep(POLL_INTERVAL_MS, signal);
115
+ continue;
116
+ }
117
+ throw e;
118
+ }
100
119
  const body = await parseJson(response);
101
120
  if (response.status === 404 && (options.tolerateUnknownUntil ?? 0) > Date.now()) {
102
121
  onState?.({ status: "pending" });
@@ -109,11 +128,12 @@ async function pollUntilApproved(issuer, requestId, onState, signal, options = {
109
128
  body.error_description ?? `Pairing status failed (${response.status})`
110
129
  );
111
130
  }
112
- onState?.({
131
+ last = {
113
132
  status: body.status,
114
133
  expiresIn: body.expires_in,
115
134
  enrolmentDeadline: body.enrolment_deadline
116
- });
135
+ };
136
+ onState?.(last);
117
137
  switch (body.status) {
118
138
  case "approved":
119
139
  if (!body.code) {
@@ -178,6 +198,76 @@ function titleFor(t, intent) {
178
198
  return t.title;
179
199
  }
180
200
 
201
+ // src/return.ts
202
+ var PREFIX = "zoreal:oauth2:return:";
203
+ var DONE = "zoreal:oauth2:done:";
204
+ var MAX_AGE_MS = 10 * 60 * 1e3;
205
+ function storage() {
206
+ try {
207
+ return typeof localStorage === "undefined" ? null : localStorage;
208
+ } catch {
209
+ return null;
210
+ }
211
+ }
212
+ function saveReturnFlow(flow) {
213
+ try {
214
+ storage()?.setItem(PREFIX + flow.requestId, JSON.stringify(flow));
215
+ } catch {
216
+ }
217
+ }
218
+ function peekReturnFlow(requestId) {
219
+ const store = storage();
220
+ if (!store) return null;
221
+ const raw = store.getItem(PREFIX + requestId);
222
+ if (!raw) return null;
223
+ try {
224
+ const flow = JSON.parse(raw);
225
+ if (flow.v !== 1 || flow.requestId !== requestId) return null;
226
+ if (Date.now() - flow.createdAt > MAX_AGE_MS) return null;
227
+ return flow;
228
+ } catch {
229
+ return null;
230
+ }
231
+ }
232
+ function forgetReturnFlow(requestId) {
233
+ try {
234
+ storage()?.removeItem(PREFIX + requestId);
235
+ } catch {
236
+ }
237
+ if (pending === requestId) pending = null;
238
+ }
239
+ function markReturnDone(requestId) {
240
+ try {
241
+ const store = storage();
242
+ store?.setItem(DONE + requestId, String(Date.now()));
243
+ store?.removeItem(PREFIX + requestId);
244
+ } catch {
245
+ }
246
+ }
247
+ function isReturnDone(requestId) {
248
+ return storage()?.getItem(DONE + requestId) !== null && storage()?.getItem(DONE + requestId) !== void 0;
249
+ }
250
+ function returnToUrl() {
251
+ if (typeof window === "undefined") return void 0;
252
+ const { href } = window.location;
253
+ const hash = href.indexOf("#");
254
+ return hash === -1 ? href : href.slice(0, hash);
255
+ }
256
+ var RETURN_MARK = /(?:^|[#&])zoreal_return=([A-Za-z0-9]{32})(?:&|$)/;
257
+ var pending = null;
258
+ function pendingReturnId() {
259
+ if (pending) return pending;
260
+ if (typeof window === "undefined") return null;
261
+ const match = RETURN_MARK.exec(window.location.hash);
262
+ if (!match) return null;
263
+ pending = match[1];
264
+ try {
265
+ window.history.replaceState(window.history.state, "", returnToUrl());
266
+ } catch {
267
+ }
268
+ return pending;
269
+ }
270
+
181
271
  // src/i18n.ts
182
272
  var en = {
183
273
  title: "Scan to sign in",
@@ -1021,8 +1111,8 @@ function interpolate(template, time) {
1021
1111
  }
1022
1112
 
1023
1113
  // src/styles.ts
1024
- var PREFIX = "zrl";
1025
- var cx = (name) => `${PREFIX}-${name}`;
1114
+ var PREFIX2 = "zrl";
1115
+ var cx = (name) => `${PREFIX2}-${name}`;
1026
1116
  var STYLE_ELEMENT_ID = "zoreal-pairing-styles";
1027
1117
  var LIGHT = `
1028
1118
  --zrl-scrim: rgba(16, 18, 27, 0.45);
@@ -1086,13 +1176,13 @@ var DARK = `
1086
1176
  --zrl-glow-opacity: 0.85;
1087
1177
  `;
1088
1178
  var CSS = `
1089
- .${PREFIX}-root { ${LIGHT} }
1090
- .${PREFIX}-root[data-theme="dark"] { ${DARK} }
1179
+ .${PREFIX2}-root { ${LIGHT} }
1180
+ .${PREFIX2}-root[data-theme="dark"] { ${DARK} }
1091
1181
  @media (prefers-color-scheme: dark) {
1092
- .${PREFIX}-root[data-theme="auto"] { ${DARK} }
1182
+ .${PREFIX2}-root[data-theme="auto"] { ${DARK} }
1093
1183
  }
1094
1184
 
1095
- .${PREFIX}-scrim {
1185
+ .${PREFIX2}-scrim {
1096
1186
  position: fixed;
1097
1187
  inset: 0;
1098
1188
  z-index: 2147483000;
@@ -1104,10 +1194,10 @@ var CSS = `
1104
1194
  backdrop-filter: blur(3px);
1105
1195
  -webkit-backdrop-filter: blur(3px);
1106
1196
  font-family: inherit;
1107
- animation: ${PREFIX}-fade 200ms ease-out both;
1197
+ animation: ${PREFIX2}-fade 200ms ease-out both;
1108
1198
  }
1109
1199
 
1110
- .${PREFIX}-card {
1200
+ .${PREFIX2}-card {
1111
1201
  position: relative;
1112
1202
  box-sizing: border-box;
1113
1203
  width: 100%;
@@ -1119,14 +1209,14 @@ var CSS = `
1119
1209
  outline: 1px solid var(--zrl-ring);
1120
1210
  outline-offset: -1px;
1121
1211
  text-align: center;
1122
- animation: ${PREFIX}-rise 300ms cubic-bezier(0.23, 1, 0.32, 1) both;
1212
+ animation: ${PREFIX2}-rise 300ms cubic-bezier(0.23, 1, 0.32, 1) both;
1123
1213
  }
1124
1214
 
1125
- .${PREFIX}-body { padding: 28px 24px 20px; }
1215
+ .${PREFIX2}-body { padding: 28px 24px 20px; }
1126
1216
 
1127
- .${PREFIX}-lockup { display: block; margin: 0 auto; color: var(--zrl-ink); }
1217
+ .${PREFIX2}-lockup { display: block; margin: 0 auto; color: var(--zrl-ink); }
1128
1218
 
1129
- .${PREFIX}-title {
1219
+ .${PREFIX2}-title {
1130
1220
  margin: 18px 0 0;
1131
1221
  font-size: 18px;
1132
1222
  font-weight: 600;
@@ -1135,7 +1225,7 @@ var CSS = `
1135
1225
  color: var(--zrl-ink);
1136
1226
  }
1137
1227
 
1138
- .${PREFIX}-body-text {
1228
+ .${PREFIX2}-body-text {
1139
1229
  margin: 6px auto 0;
1140
1230
  max-width: 30ch;
1141
1231
  font-size: 14px;
@@ -1143,7 +1233,7 @@ var CSS = `
1143
1233
  color: var(--zrl-ink-soft);
1144
1234
  }
1145
1235
 
1146
- .${PREFIX}-qr-well {
1236
+ .${PREFIX2}-qr-well {
1147
1237
  position: relative;
1148
1238
  display: grid;
1149
1239
  place-items: center;
@@ -1182,9 +1272,9 @@ var CSS = `
1182
1272
  All three share one containing block, the well's padding box, so the two
1183
1273
  comets ride the same path; the spent badge is a later sibling and paints
1184
1274
  above them. */
1185
- .${PREFIX}-qr-beam,
1186
- .${PREFIX}-qr-beam-glow,
1187
- .${PREFIX}-qr-beam-glow-band {
1275
+ .${PREFIX2}-qr-beam,
1276
+ .${PREFIX2}-qr-beam-glow,
1277
+ .${PREFIX2}-qr-beam-glow-band {
1188
1278
  position: absolute;
1189
1279
  inset: calc(0px - var(--zrl-beam-line));
1190
1280
  border: var(--zrl-beam-line) solid transparent;
@@ -1197,7 +1287,7 @@ var CSS = `
1197
1287
  mask-clip: padding-box, border-box;
1198
1288
  mask-composite: intersect;
1199
1289
  }
1200
- .${PREFIX}-qr-beam-glow {
1290
+ .${PREFIX2}-qr-beam-glow {
1201
1291
  inset: calc(0px - var(--zrl-glow-reach));
1202
1292
  border-width: var(--zrl-glow-reach);
1203
1293
  border-radius: calc(var(--zrl-radius) - 1px + var(--zrl-glow-reach));
@@ -1205,7 +1295,7 @@ var CSS = `
1205
1295
  opacity: var(--zrl-glow-opacity);
1206
1296
  will-change: filter;
1207
1297
  }
1208
- .${PREFIX}-qr-beam-glow-band {
1298
+ .${PREFIX2}-qr-beam-glow-band {
1209
1299
  inset: calc(0px - var(--zrl-glow-core));
1210
1300
  border-width: var(--zrl-glow-core);
1211
1301
  border-radius: calc(var(--zrl-radius) - 1px + var(--zrl-glow-core));
@@ -1216,8 +1306,8 @@ var CSS = `
1216
1306
  border reads as the well's own line with a light passing over it; shown
1217
1307
  once the light has stopped. Every path below ends here, which is what
1218
1308
  makes them look the same at rest. */
1219
- .${PREFIX}-qr-beam::before,
1220
- .${PREFIX}-qr-beam-glow-band::before {
1309
+ .${PREFIX2}-qr-beam::before,
1310
+ .${PREFIX2}-qr-beam-glow-band::before {
1221
1311
  content: '';
1222
1312
  position: absolute;
1223
1313
  inset: -50%;
@@ -1230,8 +1320,8 @@ var CSS = `
1230
1320
  whole. A transform animation runs on the compositor, so the light keeps
1231
1321
  moving while the page is busy; animating the gradient angle instead
1232
1322
  repaints every frame on the main thread and stutters. */
1233
- .${PREFIX}-qr-beam::after,
1234
- .${PREFIX}-qr-beam-glow-band::after {
1323
+ .${PREFIX2}-qr-beam::after,
1324
+ .${PREFIX2}-qr-beam-glow-band::after {
1235
1325
  content: '';
1236
1326
  position: absolute;
1237
1327
  inset: -50%;
@@ -1242,7 +1332,7 @@ var CSS = `
1242
1332
  var(--zrl-beam-head) 348deg,
1243
1333
  transparent 356deg 360deg
1244
1334
  );
1245
- animation: ${PREFIX}-orbit var(--zrl-beam-time) linear infinite;
1335
+ animation: ${PREFIX2}-orbit var(--zrl-beam-time) linear infinite;
1246
1336
  will-change: transform;
1247
1337
  transition: opacity 400ms cubic-bezier(0.23, 1, 0.32, 1);
1248
1338
  }
@@ -1250,15 +1340,15 @@ var CSS = `
1250
1340
  /* Spent: the light stops where it is and fades, and the edge settles to the
1251
1341
  dim glow. Paused rather than removed, so it does not jump back to its start
1252
1342
  on the way out. */
1253
- .${PREFIX}-qr-well[data-spent="true"] .${PREFIX}-qr-beam::after,
1254
- .${PREFIX}-qr-well[data-spent="true"] .${PREFIX}-qr-beam-glow-band::after {
1343
+ .${PREFIX2}-qr-well[data-spent="true"] .${PREFIX2}-qr-beam::after,
1344
+ .${PREFIX2}-qr-well[data-spent="true"] .${PREFIX2}-qr-beam-glow-band::after {
1255
1345
  animation-play-state: paused;
1256
1346
  opacity: 0;
1257
1347
  }
1258
- .${PREFIX}-qr-well[data-spent="true"] .${PREFIX}-qr-beam::before { opacity: 0.55; }
1259
- .${PREFIX}-qr-well[data-spent="true"] .${PREFIX}-qr-beam-glow-band::before { opacity: 0.7; }
1348
+ .${PREFIX2}-qr-well[data-spent="true"] .${PREFIX2}-qr-beam::before { opacity: 0.55; }
1349
+ .${PREFIX2}-qr-well[data-spent="true"] .${PREFIX2}-qr-beam-glow-band::before { opacity: 0.7; }
1260
1350
 
1261
- .${PREFIX}-qr {
1351
+ .${PREFIX2}-qr {
1262
1352
  display: block;
1263
1353
  width: 100%;
1264
1354
  height: 100%;
@@ -1273,17 +1363,17 @@ var CSS = `
1273
1363
  /* Once the code is claimed the QR is spent. Blurring it out rather than
1274
1364
  swapping it keeps one object on screen through the state change, so the eye
1275
1365
  reads a transformation instead of two things trading places. */
1276
- .${PREFIX}-qr[data-spent="true"] { opacity: 0.2; filter: var(--zrl-qr-spent-filter); transform: scale(0.96); }
1366
+ .${PREFIX2}-qr[data-spent="true"] { opacity: 0.2; filter: var(--zrl-qr-spent-filter); transform: scale(0.96); }
1277
1367
 
1278
- .${PREFIX}-qr-overlay {
1368
+ .${PREFIX2}-qr-overlay {
1279
1369
  position: absolute;
1280
1370
  inset: 0;
1281
1371
  display: grid;
1282
1372
  place-items: center;
1283
- animation: ${PREFIX}-fade 200ms ease-out both;
1373
+ animation: ${PREFIX2}-fade 200ms ease-out both;
1284
1374
  }
1285
1375
 
1286
- .${PREFIX}-qr-badge {
1376
+ .${PREFIX2}-qr-badge {
1287
1377
  display: grid;
1288
1378
  place-items: center;
1289
1379
  width: 56px;
@@ -1293,7 +1383,7 @@ var CSS = `
1293
1383
  color: var(--zrl-accent-ink);
1294
1384
  }
1295
1385
 
1296
- .${PREFIX}-status {
1386
+ .${PREFIX2}-status {
1297
1387
  display: flex;
1298
1388
  align-items: center;
1299
1389
  justify-content: center;
@@ -1304,8 +1394,8 @@ var CSS = `
1304
1394
  color: var(--zrl-ink);
1305
1395
  }
1306
1396
 
1307
- .${PREFIX}-dot { position: relative; display: grid; place-items: center; width: 8px; height: 8px; }
1308
- .${PREFIX}-dot i {
1397
+ .${PREFIX2}-dot { position: relative; display: grid; place-items: center; width: 8px; height: 8px; }
1398
+ .${PREFIX2}-dot i {
1309
1399
  position: absolute;
1310
1400
  width: 8px;
1311
1401
  height: 8px;
@@ -1313,25 +1403,25 @@ var CSS = `
1313
1403
  background: var(--zrl-accent);
1314
1404
  font-style: normal;
1315
1405
  }
1316
- .${PREFIX}-dot i:first-child { animation: ${PREFIX}-ping 1.8s cubic-bezier(0.23, 1, 0.32, 1) infinite; }
1406
+ .${PREFIX2}-dot i:first-child { animation: ${PREFIX2}-ping 1.8s cubic-bezier(0.23, 1, 0.32, 1) infinite; }
1317
1407
 
1318
- .${PREFIX}-timer {
1408
+ .${PREFIX2}-timer {
1319
1409
  margin: 4px 0 0;
1320
1410
  font-size: 12px;
1321
1411
  font-variant-numeric: tabular-nums;
1322
1412
  color: var(--zrl-ink-mute);
1323
1413
  transition: color 200ms ease-out;
1324
1414
  }
1325
- .${PREFIX}-timer[data-urgent="true"] { color: var(--zrl-urgent); }
1415
+ .${PREFIX2}-timer[data-urgent="true"] { color: var(--zrl-urgent); }
1326
1416
 
1327
- .${PREFIX}-help {
1417
+ .${PREFIX2}-help {
1328
1418
  padding: 14px 24px;
1329
1419
  border-top: 1px solid var(--zrl-line-soft);
1330
1420
  background: var(--zrl-surface-sunken);
1331
1421
  border-radius: 0;
1332
1422
  }
1333
- .${PREFIX}-help-title { margin: 0; font-size: 12px; font-weight: 600; color: var(--zrl-ink); }
1334
- .${PREFIX}-help-body {
1423
+ .${PREFIX2}-help-title { margin: 0; font-size: 12px; font-weight: 600; color: var(--zrl-ink); }
1424
+ .${PREFIX2}-help-body {
1335
1425
  margin: 4px auto 0;
1336
1426
  max-width: 34ch;
1337
1427
  font-size: 12px;
@@ -1339,9 +1429,9 @@ var CSS = `
1339
1429
  color: var(--zrl-ink-soft);
1340
1430
  }
1341
1431
 
1342
- .${PREFIX}-footer { padding: 12px; border-top: 1px solid var(--zrl-line-soft); }
1432
+ .${PREFIX2}-footer { padding: 12px; border-top: 1px solid var(--zrl-line-soft); }
1343
1433
 
1344
- .${PREFIX}-cancel {
1434
+ .${PREFIX2}-cancel {
1345
1435
  display: block;
1346
1436
  width: 100%;
1347
1437
  padding: 10px;
@@ -1355,10 +1445,10 @@ var CSS = `
1355
1445
  cursor: pointer;
1356
1446
  transition: background-color 150ms ease-out, color 150ms ease-out, transform 150ms ease-out;
1357
1447
  }
1358
- .${PREFIX}-cancel:hover { background: var(--zrl-surface-sunken); color: var(--zrl-ink); }
1359
- .${PREFIX}-cancel:active { transform: scale(0.99); }
1448
+ .${PREFIX2}-cancel:hover { background: var(--zrl-surface-sunken); color: var(--zrl-ink); }
1449
+ .${PREFIX2}-cancel:active { transform: scale(0.99); }
1360
1450
 
1361
- .${PREFIX}-secured {
1451
+ .${PREFIX2}-secured {
1362
1452
  display: flex;
1363
1453
  align-items: center;
1364
1454
  justify-content: center;
@@ -1370,9 +1460,9 @@ var CSS = `
1370
1460
  border-radius: 6px;
1371
1461
  transition: color 150ms ease-out;
1372
1462
  }
1373
- .${PREFIX}-secured:hover { color: var(--zrl-ink); }
1463
+ .${PREFIX2}-secured:hover { color: var(--zrl-ink); }
1374
1464
 
1375
- .${PREFIX}-close {
1465
+ .${PREFIX2}-close {
1376
1466
  position: absolute;
1377
1467
  top: 12px;
1378
1468
  inset-inline-end: 12px;
@@ -1388,25 +1478,25 @@ var CSS = `
1388
1478
  cursor: pointer;
1389
1479
  transition: background-color 150ms ease-out, color 150ms ease-out, transform 150ms ease-out;
1390
1480
  }
1391
- .${PREFIX}-close:hover { background: var(--zrl-surface-sunken); color: var(--zrl-ink); }
1392
- .${PREFIX}-close:active { transform: scale(0.95); }
1481
+ .${PREFIX2}-close:hover { background: var(--zrl-surface-sunken); color: var(--zrl-ink); }
1482
+ .${PREFIX2}-close:active { transform: scale(0.95); }
1393
1483
 
1394
- .${PREFIX}-card :focus-visible {
1484
+ .${PREFIX2}-card :focus-visible {
1395
1485
  outline: 2px solid var(--zrl-accent);
1396
1486
  outline-offset: 2px;
1397
1487
  }
1398
1488
 
1399
- @keyframes ${PREFIX}-fade { from { opacity: 0 } to { opacity: 1 } }
1400
- @keyframes ${PREFIX}-rise {
1489
+ @keyframes ${PREFIX2}-fade { from { opacity: 0 } to { opacity: 1 } }
1490
+ @keyframes ${PREFIX2}-rise {
1401
1491
  from { opacity: 0; transform: translateY(10px) scale(0.98) }
1402
1492
  to { opacity: 1; transform: none }
1403
1493
  }
1404
- @keyframes ${PREFIX}-ping {
1494
+ @keyframes ${PREFIX2}-ping {
1405
1495
  0% { transform: scale(1); opacity: 0.5 }
1406
1496
  70%, 100% { transform: scale(2.6); opacity: 0 }
1407
1497
  }
1408
1498
 
1409
- @keyframes ${PREFIX}-orbit { to { transform: rotate(360deg) } }
1499
+ @keyframes ${PREFIX2}-orbit { to { transform: rotate(360deg) } }
1410
1500
  /* THE BUSY RING. The light of the QR well, around any control that is
1411
1501
  waiting on the provider: the button on a phone between the tap and the
1412
1502
  hand-over to the app. The well's sweep is a cone from the centre, which is
@@ -1423,13 +1513,13 @@ var CSS = `
1423
1513
  straight fade from the head to nothing three tenths of the way back; the
1424
1514
  component sets each layer's length, offset and opacity. Shown only while
1425
1515
  busy. */
1426
- .${PREFIX}-ring {
1516
+ .${PREFIX2}-ring {
1427
1517
  position: relative;
1428
1518
  display: inline-flex;
1429
1519
  vertical-align: middle;
1430
1520
  --zrl-beam-time: 4s;
1431
1521
  }
1432
- .${PREFIX}-ring-svg {
1522
+ .${PREFIX2}-ring-svg {
1433
1523
  position: absolute;
1434
1524
  inset: -4px;
1435
1525
  width: calc(100% + 8px);
@@ -1439,8 +1529,8 @@ var CSS = `
1439
1529
  opacity: 0;
1440
1530
  transition: opacity 200ms ease-out;
1441
1531
  }
1442
- .${PREFIX}-ring[data-busy="true"] > .${PREFIX}-ring-svg { opacity: 1; }
1443
- .${PREFIX}-ring-svg rect {
1532
+ .${PREFIX2}-ring[data-busy="true"] > .${PREFIX2}-ring-svg { opacity: 1; }
1533
+ .${PREFIX2}-ring-svg rect {
1444
1534
  --zrl-l: var(--zrl-ring-len, 600px);
1445
1535
  x: 2px;
1446
1536
  y: 2px;
@@ -1451,37 +1541,37 @@ var CSS = `
1451
1541
  stroke-width: var(--zrl-beam-line);
1452
1542
  stroke-linecap: round;
1453
1543
  stroke-dashoffset: var(--zrl-s, 0px);
1454
- animation: ${PREFIX}-dash var(--zrl-beam-time) linear infinite;
1544
+ animation: ${PREFIX2}-dash var(--zrl-beam-time) linear infinite;
1455
1545
  }
1456
- .${PREFIX}-ring-head { stroke: var(--zrl-beam-head); }
1457
- .${PREFIX}-ring-halo {
1546
+ .${PREFIX2}-ring-head { stroke: var(--zrl-beam-head); }
1547
+ .${PREFIX2}-ring-halo {
1458
1548
  stroke-width: calc(var(--zrl-glow-core) * 2 + var(--zrl-beam-line));
1459
1549
  filter: blur(var(--zrl-glow-blur));
1460
1550
  }
1461
- @keyframes ${PREFIX}-dash {
1551
+ @keyframes ${PREFIX2}-dash {
1462
1552
  from { stroke-dashoffset: var(--zrl-s, 0px); }
1463
1553
  to { stroke-dashoffset: calc(var(--zrl-s, 0px) - var(--zrl-l)); }
1464
1554
  }
1465
1555
  @media (prefers-reduced-motion: reduce) {
1466
- .${PREFIX}-ring-svg rect { animation: none; stroke-dasharray: none; opacity: 0.45; }
1467
- .${PREFIX}-ring-halo, .${PREFIX}-ring-head { display: none; }
1556
+ .${PREFIX2}-ring-svg rect { animation: none; stroke-dasharray: none; opacity: 0.45; }
1557
+ .${PREFIX2}-ring-halo, .${PREFIX2}-ring-head { display: none; }
1468
1558
  }
1469
1559
 
1470
1560
 
1471
1561
  @media (prefers-reduced-motion: reduce) {
1472
- .${PREFIX}-scrim,
1473
- .${PREFIX}-card,
1474
- .${PREFIX}-qr-overlay { animation: none }
1475
- .${PREFIX}-dot i:first-child { animation: none; opacity: 0.35 }
1476
- .${PREFIX}-qr,
1477
- .${PREFIX}-cancel,
1478
- .${PREFIX}-close,
1479
- .${PREFIX}-timer { transition: none }
1562
+ .${PREFIX2}-scrim,
1563
+ .${PREFIX2}-card,
1564
+ .${PREFIX2}-qr-overlay { animation: none }
1565
+ .${PREFIX2}-dot i:first-child { animation: none; opacity: 0.35 }
1566
+ .${PREFIX2}-qr,
1567
+ .${PREFIX2}-cancel,
1568
+ .${PREFIX2}-close,
1569
+ .${PREFIX2}-timer { transition: none }
1480
1570
  /* No travelling light; the edge keeps its dim static glow instead. */
1481
- .${PREFIX}-qr-beam::after,
1482
- .${PREFIX}-qr-beam-glow-band::after { animation: none; opacity: 0 }
1483
- .${PREFIX}-qr-beam::before { opacity: 0.55 }
1484
- .${PREFIX}-qr-beam-glow-band::before { opacity: 0.7 }
1571
+ .${PREFIX2}-qr-beam::after,
1572
+ .${PREFIX2}-qr-beam-glow-band::after { animation: none; opacity: 0 }
1573
+ .${PREFIX2}-qr-beam::before { opacity: 0.55 }
1574
+ .${PREFIX2}-qr-beam-glow-band::before { opacity: 0.7 }
1485
1575
  }
1486
1576
  `;
1487
1577
  function ensureStyles() {
@@ -1886,8 +1976,23 @@ function startLogin(options) {
1886
1976
  try {
1887
1977
  let code;
1888
1978
  let selectBy = "device";
1979
+ let returnId = null;
1889
1980
  if (useAppLink && typeof window !== "undefined") {
1890
1981
  const requestId = generateRequestId();
1982
+ saveReturnFlow({
1983
+ v: 1,
1984
+ issuer,
1985
+ clientId: options.clientId,
1986
+ flow,
1987
+ verifier,
1988
+ nonce,
1989
+ state,
1990
+ scope: options.scope ?? "openid",
1991
+ appState: options.app_state,
1992
+ requestId,
1993
+ createdAt: Date.now()
1994
+ });
1995
+ returnId = requestId;
1891
1996
  const startUrl = sameDeviceStartUrl(issuer, {
1892
1997
  client_id: options.clientId,
1893
1998
  scope: options.scope ?? "openid",
@@ -1900,7 +2005,8 @@ function startLogin(options) {
1900
2005
  prompt: options.prompt,
1901
2006
  locale: options.locale,
1902
2007
  request_id: requestId,
1903
- origin: window.location.origin
2008
+ origin: window.location.origin,
2009
+ return_to: returnToUrl()
1904
2010
  });
1905
2011
  selectBy = "app_link";
1906
2012
  surface.requestId = requestId;
@@ -1922,6 +2028,9 @@ function startLogin(options) {
1922
2028
  controller.signal,
1923
2029
  { tolerateUnknownUntil: Date.now() + 15e3 }
1924
2030
  );
2031
+ if (isReturnDone(requestId)) {
2032
+ throw new DOMException("aborted", "AbortError");
2033
+ }
1925
2034
  } else {
1926
2035
  const started = await startPairing(
1927
2036
  issuer,
@@ -2022,6 +2131,7 @@ function startLogin(options) {
2022
2131
  }
2023
2132
  teardown();
2024
2133
  }
2134
+ if (returnId) markReturnDone(returnId);
2025
2135
  if (flow === "auth-code") {
2026
2136
  const response2 = {
2027
2137
  code,
@@ -2075,6 +2185,67 @@ function startLogin(options) {
2075
2185
  }
2076
2186
  };
2077
2187
  }
2188
+ function resumeLogin(options) {
2189
+ const id = pendingReturnId();
2190
+ if (!id) return null;
2191
+ const saved = peekReturnFlow(id);
2192
+ if (!saved || saved.clientId !== options.clientId) return null;
2193
+ forgetReturnFlow(id);
2194
+ const issuer = options.issuer ?? saved.issuer;
2195
+ const controller = new AbortController();
2196
+ const promise = (async () => {
2197
+ const code = await pollUntilApproved(
2198
+ issuer,
2199
+ id,
2200
+ (s) => options.onState?.({ ...s, appLink: true }),
2201
+ controller.signal,
2202
+ { tolerateUnknownUntil: Date.now() + 5e3 }
2203
+ );
2204
+ let response;
2205
+ if (saved.flow === "auth-code") {
2206
+ response = {
2207
+ code,
2208
+ scope: saved.scope,
2209
+ app_state: saved.appState,
2210
+ code_verifier: saved.verifier,
2211
+ nonce: saved.nonce
2212
+ };
2213
+ } else {
2214
+ const tokens = await exchangeCode(issuer, {
2215
+ code,
2216
+ code_verifier: saved.verifier,
2217
+ client_id: options.clientId
2218
+ });
2219
+ const claims = unsafeClaims(tokens.id_token);
2220
+ response = {
2221
+ credential: tokens.id_token,
2222
+ clientId: options.clientId,
2223
+ select_by: "app_link",
2224
+ acr: claims.acr ?? "zoreal.device"
2225
+ };
2226
+ }
2227
+ markReturnDone(id);
2228
+ return response;
2229
+ })();
2230
+ promise.catch(() => {
2231
+ });
2232
+ return {
2233
+ promise,
2234
+ cancel: () => controller.abort(),
2235
+ get requestId() {
2236
+ return id;
2237
+ },
2238
+ get pairUrl() {
2239
+ return void 0;
2240
+ },
2241
+ get qrUrl() {
2242
+ return void 0;
2243
+ },
2244
+ get appLink() {
2245
+ return true;
2246
+ }
2247
+ };
2248
+ }
2078
2249
  export {
2079
2250
  DEFAULT_ISSUER,
2080
2251
  DEFAULT_PAIRING_TIMEOUT_MS,
@@ -2094,6 +2265,7 @@ export {
2094
2265
  mountPairingModal,
2095
2266
  pollUntilApproved,
2096
2267
  resolveIntent,
2268
+ resumeLogin,
2097
2269
  startLogin,
2098
2270
  startPairing,
2099
2271
  unsafeClaims