@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/dist/index.d.cts CHANGED
@@ -211,6 +211,17 @@ interface LoginHandle<T> {
211
211
 
212
212
  declare function startLogin(options: BrowserDirectLoginOptions): LoginHandle<ZorealCredentialResponse>;
213
213
  declare function startLogin(options: AuthCodeLoginOptions): LoginHandle<ZorealCodeResponse>;
214
+ /**
215
+ * Finishes a same-device sign-in on the page the ZOREAL ID app reopened.
216
+ *
217
+ * Call it on every page load where `startLogin` can be called. It answers
218
+ * null at once when this page load is not a return; otherwise it takes the
219
+ * flow saved before the tap, polls the pairing named in the fragment, and
220
+ * resolves the way `startLogin` would have: an ID token in browser-direct
221
+ * mode, the code and the verifier in auth-code mode. The fragment is removed
222
+ * from the address bar as it is read.
223
+ */
224
+ declare function resumeLogin(options: Pick<StartLoginOptions, 'clientId' | 'issuer' | 'onState'>): LoginHandle<ZorealCredentialResponse | ZorealCodeResponse> | null;
214
225
 
215
226
  /**
216
227
  * The pairing modal, in plain DOM.
@@ -310,7 +321,7 @@ declare function mountPairingModal(state: PairingState, options: PairingModalOpt
310
321
  * auth-code mode leaves it to the RP backend.
311
322
  */
312
323
  declare const WIRE_VERSION = 1;
313
- declare const SDK_VERSION = "0.1.19";
324
+ declare const SDK_VERSION = "0.1.20";
314
325
  declare const SDK_NAME = "@zoreal/oauth2-js";
315
326
  declare const DEFAULT_ISSUER = "https://id.zoreal.com";
316
327
  /** Pending TTL is short. Poll gently; over-polling cancels the request. */
@@ -466,4 +477,4 @@ declare function unsafeClaims(idToken: string): Record<string, unknown>;
466
477
  */
467
478
  declare function resolveIntent(intent: LoginIntent | undefined, scope: string | undefined, acrValues: string | readonly string[] | undefined): LoginIntent;
468
479
 
469
- export { type AcrValue, type AuthCodeLoginOptions, type BrowserDirectLoginOptions, DEFAULT_ISSUER, DEFAULT_PAIRING_TIMEOUT_MS, DEFAULT_QR_REFRESH_SECONDS, type ErrorCode, FlowAbandonedError, type LoginHandle, type LoginIntent, type NonOAuthError, OAuthFlowError, POLL_INTERVAL_ENROLLING_MS, POLL_INTERVAL_MS, type PairCreated, type PairDisplay, type PairImmediate, type PairStartResponse, type PairStatusResponse, type PairingModalHandle, type PairingModalOptions, type PairingState, type PairingUI, SDK_NAME, SDK_VERSION, type SelectBy, type StartLoginOptions, type StartPairingParams, type TokenResponse, WIRE_VERSION, type ZorealCodeResponse, type ZorealCredentialResponse, type ZorealTheme, challengeS256, exchangeCode, generateState, generateVerifier, isMobileUserAgent, mountPairingModal, pollUntilApproved, resolveIntent, startLogin, startPairing, unsafeClaims };
480
+ export { type AcrValue, type AuthCodeLoginOptions, type BrowserDirectLoginOptions, DEFAULT_ISSUER, DEFAULT_PAIRING_TIMEOUT_MS, DEFAULT_QR_REFRESH_SECONDS, type ErrorCode, FlowAbandonedError, type LoginHandle, type LoginIntent, type NonOAuthError, OAuthFlowError, POLL_INTERVAL_ENROLLING_MS, POLL_INTERVAL_MS, type PairCreated, type PairDisplay, type PairImmediate, type PairStartResponse, type PairStatusResponse, type PairingModalHandle, type PairingModalOptions, type PairingState, type PairingUI, SDK_NAME, SDK_VERSION, type SelectBy, type StartLoginOptions, type StartPairingParams, type TokenResponse, WIRE_VERSION, type ZorealCodeResponse, type ZorealCredentialResponse, type ZorealTheme, challengeS256, exchangeCode, generateState, generateVerifier, isMobileUserAgent, mountPairingModal, pollUntilApproved, resolveIntent, resumeLogin, startLogin, startPairing, unsafeClaims };
package/dist/index.d.ts CHANGED
@@ -211,6 +211,17 @@ interface LoginHandle<T> {
211
211
 
212
212
  declare function startLogin(options: BrowserDirectLoginOptions): LoginHandle<ZorealCredentialResponse>;
213
213
  declare function startLogin(options: AuthCodeLoginOptions): LoginHandle<ZorealCodeResponse>;
214
+ /**
215
+ * Finishes a same-device sign-in on the page the ZOREAL ID app reopened.
216
+ *
217
+ * Call it on every page load where `startLogin` can be called. It answers
218
+ * null at once when this page load is not a return; otherwise it takes the
219
+ * flow saved before the tap, polls the pairing named in the fragment, and
220
+ * resolves the way `startLogin` would have: an ID token in browser-direct
221
+ * mode, the code and the verifier in auth-code mode. The fragment is removed
222
+ * from the address bar as it is read.
223
+ */
224
+ declare function resumeLogin(options: Pick<StartLoginOptions, 'clientId' | 'issuer' | 'onState'>): LoginHandle<ZorealCredentialResponse | ZorealCodeResponse> | null;
214
225
 
215
226
  /**
216
227
  * The pairing modal, in plain DOM.
@@ -310,7 +321,7 @@ declare function mountPairingModal(state: PairingState, options: PairingModalOpt
310
321
  * auth-code mode leaves it to the RP backend.
311
322
  */
312
323
  declare const WIRE_VERSION = 1;
313
- declare const SDK_VERSION = "0.1.19";
324
+ declare const SDK_VERSION = "0.1.20";
314
325
  declare const SDK_NAME = "@zoreal/oauth2-js";
315
326
  declare const DEFAULT_ISSUER = "https://id.zoreal.com";
316
327
  /** Pending TTL is short. Poll gently; over-polling cancels the request. */
@@ -466,4 +477,4 @@ declare function unsafeClaims(idToken: string): Record<string, unknown>;
466
477
  */
467
478
  declare function resolveIntent(intent: LoginIntent | undefined, scope: string | undefined, acrValues: string | readonly string[] | undefined): LoginIntent;
468
479
 
469
- export { type AcrValue, type AuthCodeLoginOptions, type BrowserDirectLoginOptions, DEFAULT_ISSUER, DEFAULT_PAIRING_TIMEOUT_MS, DEFAULT_QR_REFRESH_SECONDS, type ErrorCode, FlowAbandonedError, type LoginHandle, type LoginIntent, type NonOAuthError, OAuthFlowError, POLL_INTERVAL_ENROLLING_MS, POLL_INTERVAL_MS, type PairCreated, type PairDisplay, type PairImmediate, type PairStartResponse, type PairStatusResponse, type PairingModalHandle, type PairingModalOptions, type PairingState, type PairingUI, SDK_NAME, SDK_VERSION, type SelectBy, type StartLoginOptions, type StartPairingParams, type TokenResponse, WIRE_VERSION, type ZorealCodeResponse, type ZorealCredentialResponse, type ZorealTheme, challengeS256, exchangeCode, generateState, generateVerifier, isMobileUserAgent, mountPairingModal, pollUntilApproved, resolveIntent, startLogin, startPairing, unsafeClaims };
480
+ export { type AcrValue, type AuthCodeLoginOptions, type BrowserDirectLoginOptions, DEFAULT_ISSUER, DEFAULT_PAIRING_TIMEOUT_MS, DEFAULT_QR_REFRESH_SECONDS, type ErrorCode, FlowAbandonedError, type LoginHandle, type LoginIntent, type NonOAuthError, OAuthFlowError, POLL_INTERVAL_ENROLLING_MS, POLL_INTERVAL_MS, type PairCreated, type PairDisplay, type PairImmediate, type PairStartResponse, type PairStatusResponse, type PairingModalHandle, type PairingModalOptions, type PairingState, type PairingUI, SDK_NAME, SDK_VERSION, type SelectBy, type StartLoginOptions, type StartPairingParams, type TokenResponse, WIRE_VERSION, type ZorealCodeResponse, type ZorealCredentialResponse, type ZorealTheme, challengeS256, exchangeCode, generateState, generateVerifier, isMobileUserAgent, mountPairingModal, pollUntilApproved, resolveIntent, resumeLogin, startLogin, startPairing, unsafeClaims };
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.19";
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;
@@ -198,6 +198,76 @@ function titleFor(t, intent) {
198
198
  return t.title;
199
199
  }
200
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
+
201
271
  // src/i18n.ts
202
272
  var en = {
203
273
  title: "Scan to sign in",
@@ -1041,8 +1111,8 @@ function interpolate(template, time) {
1041
1111
  }
1042
1112
 
1043
1113
  // src/styles.ts
1044
- var PREFIX = "zrl";
1045
- var cx = (name) => `${PREFIX}-${name}`;
1114
+ var PREFIX2 = "zrl";
1115
+ var cx = (name) => `${PREFIX2}-${name}`;
1046
1116
  var STYLE_ELEMENT_ID = "zoreal-pairing-styles";
1047
1117
  var LIGHT = `
1048
1118
  --zrl-scrim: rgba(16, 18, 27, 0.45);
@@ -1106,13 +1176,13 @@ var DARK = `
1106
1176
  --zrl-glow-opacity: 0.85;
1107
1177
  `;
1108
1178
  var CSS = `
1109
- .${PREFIX}-root { ${LIGHT} }
1110
- .${PREFIX}-root[data-theme="dark"] { ${DARK} }
1179
+ .${PREFIX2}-root { ${LIGHT} }
1180
+ .${PREFIX2}-root[data-theme="dark"] { ${DARK} }
1111
1181
  @media (prefers-color-scheme: dark) {
1112
- .${PREFIX}-root[data-theme="auto"] { ${DARK} }
1182
+ .${PREFIX2}-root[data-theme="auto"] { ${DARK} }
1113
1183
  }
1114
1184
 
1115
- .${PREFIX}-scrim {
1185
+ .${PREFIX2}-scrim {
1116
1186
  position: fixed;
1117
1187
  inset: 0;
1118
1188
  z-index: 2147483000;
@@ -1124,10 +1194,10 @@ var CSS = `
1124
1194
  backdrop-filter: blur(3px);
1125
1195
  -webkit-backdrop-filter: blur(3px);
1126
1196
  font-family: inherit;
1127
- animation: ${PREFIX}-fade 200ms ease-out both;
1197
+ animation: ${PREFIX2}-fade 200ms ease-out both;
1128
1198
  }
1129
1199
 
1130
- .${PREFIX}-card {
1200
+ .${PREFIX2}-card {
1131
1201
  position: relative;
1132
1202
  box-sizing: border-box;
1133
1203
  width: 100%;
@@ -1139,14 +1209,14 @@ var CSS = `
1139
1209
  outline: 1px solid var(--zrl-ring);
1140
1210
  outline-offset: -1px;
1141
1211
  text-align: center;
1142
- 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;
1143
1213
  }
1144
1214
 
1145
- .${PREFIX}-body { padding: 28px 24px 20px; }
1215
+ .${PREFIX2}-body { padding: 28px 24px 20px; }
1146
1216
 
1147
- .${PREFIX}-lockup { display: block; margin: 0 auto; color: var(--zrl-ink); }
1217
+ .${PREFIX2}-lockup { display: block; margin: 0 auto; color: var(--zrl-ink); }
1148
1218
 
1149
- .${PREFIX}-title {
1219
+ .${PREFIX2}-title {
1150
1220
  margin: 18px 0 0;
1151
1221
  font-size: 18px;
1152
1222
  font-weight: 600;
@@ -1155,7 +1225,7 @@ var CSS = `
1155
1225
  color: var(--zrl-ink);
1156
1226
  }
1157
1227
 
1158
- .${PREFIX}-body-text {
1228
+ .${PREFIX2}-body-text {
1159
1229
  margin: 6px auto 0;
1160
1230
  max-width: 30ch;
1161
1231
  font-size: 14px;
@@ -1163,7 +1233,7 @@ var CSS = `
1163
1233
  color: var(--zrl-ink-soft);
1164
1234
  }
1165
1235
 
1166
- .${PREFIX}-qr-well {
1236
+ .${PREFIX2}-qr-well {
1167
1237
  position: relative;
1168
1238
  display: grid;
1169
1239
  place-items: center;
@@ -1202,9 +1272,9 @@ var CSS = `
1202
1272
  All three share one containing block, the well's padding box, so the two
1203
1273
  comets ride the same path; the spent badge is a later sibling and paints
1204
1274
  above them. */
1205
- .${PREFIX}-qr-beam,
1206
- .${PREFIX}-qr-beam-glow,
1207
- .${PREFIX}-qr-beam-glow-band {
1275
+ .${PREFIX2}-qr-beam,
1276
+ .${PREFIX2}-qr-beam-glow,
1277
+ .${PREFIX2}-qr-beam-glow-band {
1208
1278
  position: absolute;
1209
1279
  inset: calc(0px - var(--zrl-beam-line));
1210
1280
  border: var(--zrl-beam-line) solid transparent;
@@ -1217,7 +1287,7 @@ var CSS = `
1217
1287
  mask-clip: padding-box, border-box;
1218
1288
  mask-composite: intersect;
1219
1289
  }
1220
- .${PREFIX}-qr-beam-glow {
1290
+ .${PREFIX2}-qr-beam-glow {
1221
1291
  inset: calc(0px - var(--zrl-glow-reach));
1222
1292
  border-width: var(--zrl-glow-reach);
1223
1293
  border-radius: calc(var(--zrl-radius) - 1px + var(--zrl-glow-reach));
@@ -1225,7 +1295,7 @@ var CSS = `
1225
1295
  opacity: var(--zrl-glow-opacity);
1226
1296
  will-change: filter;
1227
1297
  }
1228
- .${PREFIX}-qr-beam-glow-band {
1298
+ .${PREFIX2}-qr-beam-glow-band {
1229
1299
  inset: calc(0px - var(--zrl-glow-core));
1230
1300
  border-width: var(--zrl-glow-core);
1231
1301
  border-radius: calc(var(--zrl-radius) - 1px + var(--zrl-glow-core));
@@ -1236,8 +1306,8 @@ var CSS = `
1236
1306
  border reads as the well's own line with a light passing over it; shown
1237
1307
  once the light has stopped. Every path below ends here, which is what
1238
1308
  makes them look the same at rest. */
1239
- .${PREFIX}-qr-beam::before,
1240
- .${PREFIX}-qr-beam-glow-band::before {
1309
+ .${PREFIX2}-qr-beam::before,
1310
+ .${PREFIX2}-qr-beam-glow-band::before {
1241
1311
  content: '';
1242
1312
  position: absolute;
1243
1313
  inset: -50%;
@@ -1250,8 +1320,8 @@ var CSS = `
1250
1320
  whole. A transform animation runs on the compositor, so the light keeps
1251
1321
  moving while the page is busy; animating the gradient angle instead
1252
1322
  repaints every frame on the main thread and stutters. */
1253
- .${PREFIX}-qr-beam::after,
1254
- .${PREFIX}-qr-beam-glow-band::after {
1323
+ .${PREFIX2}-qr-beam::after,
1324
+ .${PREFIX2}-qr-beam-glow-band::after {
1255
1325
  content: '';
1256
1326
  position: absolute;
1257
1327
  inset: -50%;
@@ -1262,7 +1332,7 @@ var CSS = `
1262
1332
  var(--zrl-beam-head) 348deg,
1263
1333
  transparent 356deg 360deg
1264
1334
  );
1265
- animation: ${PREFIX}-orbit var(--zrl-beam-time) linear infinite;
1335
+ animation: ${PREFIX2}-orbit var(--zrl-beam-time) linear infinite;
1266
1336
  will-change: transform;
1267
1337
  transition: opacity 400ms cubic-bezier(0.23, 1, 0.32, 1);
1268
1338
  }
@@ -1270,15 +1340,15 @@ var CSS = `
1270
1340
  /* Spent: the light stops where it is and fades, and the edge settles to the
1271
1341
  dim glow. Paused rather than removed, so it does not jump back to its start
1272
1342
  on the way out. */
1273
- .${PREFIX}-qr-well[data-spent="true"] .${PREFIX}-qr-beam::after,
1274
- .${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 {
1275
1345
  animation-play-state: paused;
1276
1346
  opacity: 0;
1277
1347
  }
1278
- .${PREFIX}-qr-well[data-spent="true"] .${PREFIX}-qr-beam::before { opacity: 0.55; }
1279
- .${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; }
1280
1350
 
1281
- .${PREFIX}-qr {
1351
+ .${PREFIX2}-qr {
1282
1352
  display: block;
1283
1353
  width: 100%;
1284
1354
  height: 100%;
@@ -1293,17 +1363,17 @@ var CSS = `
1293
1363
  /* Once the code is claimed the QR is spent. Blurring it out rather than
1294
1364
  swapping it keeps one object on screen through the state change, so the eye
1295
1365
  reads a transformation instead of two things trading places. */
1296
- .${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); }
1297
1367
 
1298
- .${PREFIX}-qr-overlay {
1368
+ .${PREFIX2}-qr-overlay {
1299
1369
  position: absolute;
1300
1370
  inset: 0;
1301
1371
  display: grid;
1302
1372
  place-items: center;
1303
- animation: ${PREFIX}-fade 200ms ease-out both;
1373
+ animation: ${PREFIX2}-fade 200ms ease-out both;
1304
1374
  }
1305
1375
 
1306
- .${PREFIX}-qr-badge {
1376
+ .${PREFIX2}-qr-badge {
1307
1377
  display: grid;
1308
1378
  place-items: center;
1309
1379
  width: 56px;
@@ -1313,7 +1383,7 @@ var CSS = `
1313
1383
  color: var(--zrl-accent-ink);
1314
1384
  }
1315
1385
 
1316
- .${PREFIX}-status {
1386
+ .${PREFIX2}-status {
1317
1387
  display: flex;
1318
1388
  align-items: center;
1319
1389
  justify-content: center;
@@ -1324,8 +1394,8 @@ var CSS = `
1324
1394
  color: var(--zrl-ink);
1325
1395
  }
1326
1396
 
1327
- .${PREFIX}-dot { position: relative; display: grid; place-items: center; width: 8px; height: 8px; }
1328
- .${PREFIX}-dot i {
1397
+ .${PREFIX2}-dot { position: relative; display: grid; place-items: center; width: 8px; height: 8px; }
1398
+ .${PREFIX2}-dot i {
1329
1399
  position: absolute;
1330
1400
  width: 8px;
1331
1401
  height: 8px;
@@ -1333,25 +1403,25 @@ var CSS = `
1333
1403
  background: var(--zrl-accent);
1334
1404
  font-style: normal;
1335
1405
  }
1336
- .${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; }
1337
1407
 
1338
- .${PREFIX}-timer {
1408
+ .${PREFIX2}-timer {
1339
1409
  margin: 4px 0 0;
1340
1410
  font-size: 12px;
1341
1411
  font-variant-numeric: tabular-nums;
1342
1412
  color: var(--zrl-ink-mute);
1343
1413
  transition: color 200ms ease-out;
1344
1414
  }
1345
- .${PREFIX}-timer[data-urgent="true"] { color: var(--zrl-urgent); }
1415
+ .${PREFIX2}-timer[data-urgent="true"] { color: var(--zrl-urgent); }
1346
1416
 
1347
- .${PREFIX}-help {
1417
+ .${PREFIX2}-help {
1348
1418
  padding: 14px 24px;
1349
1419
  border-top: 1px solid var(--zrl-line-soft);
1350
1420
  background: var(--zrl-surface-sunken);
1351
1421
  border-radius: 0;
1352
1422
  }
1353
- .${PREFIX}-help-title { margin: 0; font-size: 12px; font-weight: 600; color: var(--zrl-ink); }
1354
- .${PREFIX}-help-body {
1423
+ .${PREFIX2}-help-title { margin: 0; font-size: 12px; font-weight: 600; color: var(--zrl-ink); }
1424
+ .${PREFIX2}-help-body {
1355
1425
  margin: 4px auto 0;
1356
1426
  max-width: 34ch;
1357
1427
  font-size: 12px;
@@ -1359,9 +1429,9 @@ var CSS = `
1359
1429
  color: var(--zrl-ink-soft);
1360
1430
  }
1361
1431
 
1362
- .${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); }
1363
1433
 
1364
- .${PREFIX}-cancel {
1434
+ .${PREFIX2}-cancel {
1365
1435
  display: block;
1366
1436
  width: 100%;
1367
1437
  padding: 10px;
@@ -1375,10 +1445,10 @@ var CSS = `
1375
1445
  cursor: pointer;
1376
1446
  transition: background-color 150ms ease-out, color 150ms ease-out, transform 150ms ease-out;
1377
1447
  }
1378
- .${PREFIX}-cancel:hover { background: var(--zrl-surface-sunken); color: var(--zrl-ink); }
1379
- .${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); }
1380
1450
 
1381
- .${PREFIX}-secured {
1451
+ .${PREFIX2}-secured {
1382
1452
  display: flex;
1383
1453
  align-items: center;
1384
1454
  justify-content: center;
@@ -1390,9 +1460,9 @@ var CSS = `
1390
1460
  border-radius: 6px;
1391
1461
  transition: color 150ms ease-out;
1392
1462
  }
1393
- .${PREFIX}-secured:hover { color: var(--zrl-ink); }
1463
+ .${PREFIX2}-secured:hover { color: var(--zrl-ink); }
1394
1464
 
1395
- .${PREFIX}-close {
1465
+ .${PREFIX2}-close {
1396
1466
  position: absolute;
1397
1467
  top: 12px;
1398
1468
  inset-inline-end: 12px;
@@ -1408,25 +1478,25 @@ var CSS = `
1408
1478
  cursor: pointer;
1409
1479
  transition: background-color 150ms ease-out, color 150ms ease-out, transform 150ms ease-out;
1410
1480
  }
1411
- .${PREFIX}-close:hover { background: var(--zrl-surface-sunken); color: var(--zrl-ink); }
1412
- .${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); }
1413
1483
 
1414
- .${PREFIX}-card :focus-visible {
1484
+ .${PREFIX2}-card :focus-visible {
1415
1485
  outline: 2px solid var(--zrl-accent);
1416
1486
  outline-offset: 2px;
1417
1487
  }
1418
1488
 
1419
- @keyframes ${PREFIX}-fade { from { opacity: 0 } to { opacity: 1 } }
1420
- @keyframes ${PREFIX}-rise {
1489
+ @keyframes ${PREFIX2}-fade { from { opacity: 0 } to { opacity: 1 } }
1490
+ @keyframes ${PREFIX2}-rise {
1421
1491
  from { opacity: 0; transform: translateY(10px) scale(0.98) }
1422
1492
  to { opacity: 1; transform: none }
1423
1493
  }
1424
- @keyframes ${PREFIX}-ping {
1494
+ @keyframes ${PREFIX2}-ping {
1425
1495
  0% { transform: scale(1); opacity: 0.5 }
1426
1496
  70%, 100% { transform: scale(2.6); opacity: 0 }
1427
1497
  }
1428
1498
 
1429
- @keyframes ${PREFIX}-orbit { to { transform: rotate(360deg) } }
1499
+ @keyframes ${PREFIX2}-orbit { to { transform: rotate(360deg) } }
1430
1500
  /* THE BUSY RING. The light of the QR well, around any control that is
1431
1501
  waiting on the provider: the button on a phone between the tap and the
1432
1502
  hand-over to the app. The well's sweep is a cone from the centre, which is
@@ -1443,13 +1513,13 @@ var CSS = `
1443
1513
  straight fade from the head to nothing three tenths of the way back; the
1444
1514
  component sets each layer's length, offset and opacity. Shown only while
1445
1515
  busy. */
1446
- .${PREFIX}-ring {
1516
+ .${PREFIX2}-ring {
1447
1517
  position: relative;
1448
1518
  display: inline-flex;
1449
1519
  vertical-align: middle;
1450
1520
  --zrl-beam-time: 4s;
1451
1521
  }
1452
- .${PREFIX}-ring-svg {
1522
+ .${PREFIX2}-ring-svg {
1453
1523
  position: absolute;
1454
1524
  inset: -4px;
1455
1525
  width: calc(100% + 8px);
@@ -1459,8 +1529,8 @@ var CSS = `
1459
1529
  opacity: 0;
1460
1530
  transition: opacity 200ms ease-out;
1461
1531
  }
1462
- .${PREFIX}-ring[data-busy="true"] > .${PREFIX}-ring-svg { opacity: 1; }
1463
- .${PREFIX}-ring-svg rect {
1532
+ .${PREFIX2}-ring[data-busy="true"] > .${PREFIX2}-ring-svg { opacity: 1; }
1533
+ .${PREFIX2}-ring-svg rect {
1464
1534
  --zrl-l: var(--zrl-ring-len, 600px);
1465
1535
  x: 2px;
1466
1536
  y: 2px;
@@ -1471,37 +1541,37 @@ var CSS = `
1471
1541
  stroke-width: var(--zrl-beam-line);
1472
1542
  stroke-linecap: round;
1473
1543
  stroke-dashoffset: var(--zrl-s, 0px);
1474
- animation: ${PREFIX}-dash var(--zrl-beam-time) linear infinite;
1544
+ animation: ${PREFIX2}-dash var(--zrl-beam-time) linear infinite;
1475
1545
  }
1476
- .${PREFIX}-ring-head { stroke: var(--zrl-beam-head); }
1477
- .${PREFIX}-ring-halo {
1546
+ .${PREFIX2}-ring-head { stroke: var(--zrl-beam-head); }
1547
+ .${PREFIX2}-ring-halo {
1478
1548
  stroke-width: calc(var(--zrl-glow-core) * 2 + var(--zrl-beam-line));
1479
1549
  filter: blur(var(--zrl-glow-blur));
1480
1550
  }
1481
- @keyframes ${PREFIX}-dash {
1551
+ @keyframes ${PREFIX2}-dash {
1482
1552
  from { stroke-dashoffset: var(--zrl-s, 0px); }
1483
1553
  to { stroke-dashoffset: calc(var(--zrl-s, 0px) - var(--zrl-l)); }
1484
1554
  }
1485
1555
  @media (prefers-reduced-motion: reduce) {
1486
- .${PREFIX}-ring-svg rect { animation: none; stroke-dasharray: none; opacity: 0.45; }
1487
- .${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; }
1488
1558
  }
1489
1559
 
1490
1560
 
1491
1561
  @media (prefers-reduced-motion: reduce) {
1492
- .${PREFIX}-scrim,
1493
- .${PREFIX}-card,
1494
- .${PREFIX}-qr-overlay { animation: none }
1495
- .${PREFIX}-dot i:first-child { animation: none; opacity: 0.35 }
1496
- .${PREFIX}-qr,
1497
- .${PREFIX}-cancel,
1498
- .${PREFIX}-close,
1499
- .${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 }
1500
1570
  /* No travelling light; the edge keeps its dim static glow instead. */
1501
- .${PREFIX}-qr-beam::after,
1502
- .${PREFIX}-qr-beam-glow-band::after { animation: none; opacity: 0 }
1503
- .${PREFIX}-qr-beam::before { opacity: 0.55 }
1504
- .${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 }
1505
1575
  }
1506
1576
  `;
1507
1577
  function ensureStyles() {
@@ -1906,8 +1976,23 @@ function startLogin(options) {
1906
1976
  try {
1907
1977
  let code;
1908
1978
  let selectBy = "device";
1979
+ let returnId = null;
1909
1980
  if (useAppLink && typeof window !== "undefined") {
1910
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;
1911
1996
  const startUrl = sameDeviceStartUrl(issuer, {
1912
1997
  client_id: options.clientId,
1913
1998
  scope: options.scope ?? "openid",
@@ -1920,7 +2005,8 @@ function startLogin(options) {
1920
2005
  prompt: options.prompt,
1921
2006
  locale: options.locale,
1922
2007
  request_id: requestId,
1923
- origin: window.location.origin
2008
+ origin: window.location.origin,
2009
+ return_to: returnToUrl()
1924
2010
  });
1925
2011
  selectBy = "app_link";
1926
2012
  surface.requestId = requestId;
@@ -1942,6 +2028,9 @@ function startLogin(options) {
1942
2028
  controller.signal,
1943
2029
  { tolerateUnknownUntil: Date.now() + 15e3 }
1944
2030
  );
2031
+ if (isReturnDone(requestId)) {
2032
+ throw new DOMException("aborted", "AbortError");
2033
+ }
1945
2034
  } else {
1946
2035
  const started = await startPairing(
1947
2036
  issuer,
@@ -2042,6 +2131,7 @@ function startLogin(options) {
2042
2131
  }
2043
2132
  teardown();
2044
2133
  }
2134
+ if (returnId) markReturnDone(returnId);
2045
2135
  if (flow === "auth-code") {
2046
2136
  const response2 = {
2047
2137
  code,
@@ -2095,6 +2185,67 @@ function startLogin(options) {
2095
2185
  }
2096
2186
  };
2097
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
+ }
2098
2249
  export {
2099
2250
  DEFAULT_ISSUER,
2100
2251
  DEFAULT_PAIRING_TIMEOUT_MS,
@@ -2114,6 +2265,7 @@ export {
2114
2265
  mountPairingModal,
2115
2266
  pollUntilApproved,
2116
2267
  resolveIntent,
2268
+ resumeLogin,
2117
2269
  startLogin,
2118
2270
  startPairing,
2119
2271
  unsafeClaims