@zoreal/oauth2-js 0.1.8 → 0.1.10
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 +40 -10
- package/dist/index.cjs +95 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +99 -16
- package/dist/index.d.ts +99 -16
- package/dist/index.js +94 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -31,11 +31,28 @@ interface PairingState {
|
|
|
31
31
|
/**
|
|
32
32
|
* The pairing link and its provider-served QR image. Present on every
|
|
33
33
|
* callback of a QR/link flow: the QR flow cannot complete unless SOMETHING
|
|
34
|
-
* renders
|
|
34
|
+
* renders the code. By default that something is this package's own modal;
|
|
35
|
+
* these fields are what you render from instead when you opt out with
|
|
36
|
+
* `pairingUI: 'none'`.
|
|
35
37
|
*/
|
|
36
38
|
pairUrl?: string;
|
|
37
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* The provider-served SVG of the code to scan. Put it in an <img>; do not
|
|
41
|
+
* draw your own. It CHANGES OVER TIME: while the pairing is pending a fresh
|
|
42
|
+
* URL arrives every `qrRefreshSeconds`, because the code on screen moves
|
|
43
|
+
* and the provider refuses a frame older than 30 seconds. That is what
|
|
44
|
+
* makes a screenshot of it useless to an attacker. Render the qrUrl you
|
|
45
|
+
* are given on every state; a UI that caches the first one shows a code
|
|
46
|
+
* that stops working.
|
|
47
|
+
*/
|
|
38
48
|
qrUrl?: string;
|
|
49
|
+
/**
|
|
50
|
+
* How often qrUrl changes, in seconds, while the pairing is pending.
|
|
51
|
+
* Present on a QR flow, absent on an app-link flow and on a provider that
|
|
52
|
+
* still serves a static code. Informational: the refresh itself is done for
|
|
53
|
+
* you and arrives through this callback.
|
|
54
|
+
*/
|
|
55
|
+
qrRefreshSeconds?: number;
|
|
39
56
|
/** True when the flow resolved to the app link (mobile) rather than a QR. */
|
|
40
57
|
appLink?: boolean;
|
|
41
58
|
/** Abandons this pairing: stops the poll. Wire it to your UI's cancel control. */
|
|
@@ -153,9 +170,13 @@ interface LoginHandle<T> {
|
|
|
153
170
|
cancel: () => void;
|
|
154
171
|
/** The pairing request id, once created. Undefined before, and for prompt=none immediate codes. */
|
|
155
172
|
readonly requestId: string | undefined;
|
|
156
|
-
/** The pairing URL, once created.
|
|
173
|
+
/** The pairing URL, once created. On an app-link flow it carries the start token; navigate to it verbatim. */
|
|
157
174
|
readonly pairUrl: string | undefined;
|
|
158
|
-
/**
|
|
175
|
+
/**
|
|
176
|
+
* The provider-served QR image, current frame. Changes every few seconds
|
|
177
|
+
* while the pairing is pending; the onState callback is where a UI should
|
|
178
|
+
* read it from, since that is the only place a new frame announces itself.
|
|
179
|
+
*/
|
|
159
180
|
readonly qrUrl: string | undefined;
|
|
160
181
|
/** True when display resolved to the app link rather than the QR. */
|
|
161
182
|
readonly appLink: boolean | undefined;
|
|
@@ -222,36 +243,86 @@ declare function mountPairingModal(state: PairingState, options: PairingModalOpt
|
|
|
222
243
|
* authorized JavaScript origins (the dashboard):
|
|
223
244
|
*
|
|
224
245
|
* POST /pair start a pairing request. Body carries the
|
|
225
|
-
* authorize parameters plus PKCE challenge
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
246
|
+
* authorize parameters plus PKCE challenge,
|
|
247
|
+
* and `display`: which pairing surface this
|
|
248
|
+
* package is about to show, 'qr' or 'link',
|
|
249
|
+
* decided before the request is made. Returns
|
|
250
|
+
* { request_id, pair_url, expires_in, display,
|
|
251
|
+
* qr_refresh_seconds } or, for prompt=none
|
|
252
|
+
* with a live consented session, { code }
|
|
253
|
+
* immediately. `display` is echoed as the
|
|
254
|
+
* provider bound it ('legacy' for a request
|
|
255
|
+
* that sent none, which gets the static code
|
|
256
|
+
* older versions of this package showed).
|
|
257
|
+
* `qr_refresh_seconds` comes with 'qr' and is
|
|
258
|
+
* how often to re-fetch the image; 3 today.
|
|
259
|
+
* A 'link' pairing's pair_url carries
|
|
260
|
+
* ?t=<start_token>: it can only be claimed by
|
|
261
|
+
* the app that opened that exact link, and
|
|
262
|
+
* the provider never renders a QR for it, so
|
|
263
|
+
* nobody can turn a same-device link into a
|
|
264
|
+
* static code to relay.
|
|
229
265
|
* GET /pair/:id/status poll: pending | claimed |
|
|
230
266
|
* approved (with code) | denied | expired |
|
|
231
267
|
* enrolling. Over-polling cancels the request
|
|
232
268
|
* rather than throttling it, so the cadence
|
|
233
269
|
* below is not a suggestion.
|
|
234
|
-
* GET /pair/:id/qr.svg the QR image
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
270
|
+
* GET /pair/:id/qr.svg the QR image, rendered by the provider so
|
|
271
|
+
* this package draws nothing and keeps zero
|
|
272
|
+
* dependencies. For a 'qr' pairing it encodes
|
|
273
|
+
* the CURRENT FRAME, the pairing URL with
|
|
274
|
+
* ?f=<time>.<hmac>: `time` is whole seconds
|
|
275
|
+
* since the pairing was created on the
|
|
276
|
+
* provider's clock, `hmac` is keyed with a
|
|
277
|
+
* secret only the provider holds. Served
|
|
278
|
+
* Cache-Control: no-store, only while the
|
|
279
|
+
* pairing is pending. The app sends the frame
|
|
280
|
+
* it scanned with its claim, and the provider
|
|
281
|
+
* refuses a frame older than 30 seconds, so a
|
|
282
|
+
* screenshot of the code is dead on arrival.
|
|
283
|
+
* This package re-fetches the image every
|
|
284
|
+
* `qr_refresh_seconds` with a cache-busting
|
|
285
|
+
* ?t=<Date.now()>. A 'link' pairing has no
|
|
286
|
+
* image (404).
|
|
238
287
|
* POST /token the code exchange. Browser-direct mode uses
|
|
239
288
|
* it directly with PKCE and no client secret;
|
|
240
289
|
* auth-code mode leaves it to the RP backend.
|
|
241
290
|
*/
|
|
242
291
|
declare const WIRE_VERSION = 1;
|
|
243
|
-
declare const SDK_VERSION = "0.1.
|
|
292
|
+
declare const SDK_VERSION = "0.1.10";
|
|
244
293
|
declare const SDK_NAME = "@zoreal/oauth2-js";
|
|
245
294
|
declare const DEFAULT_ISSUER = "https://id.zoreal.com";
|
|
246
295
|
/** Pending TTL is short. Poll gently; over-polling cancels the request. */
|
|
247
296
|
declare const POLL_INTERVAL_MS = 2000;
|
|
248
297
|
/** Enrolling extends the window well beyond a normal login; poll slower. */
|
|
249
298
|
declare const POLL_INTERVAL_ENROLLING_MS = 5000;
|
|
299
|
+
/**
|
|
300
|
+
* How often the QR image is re-fetched while a pairing is pending, when the
|
|
301
|
+
* provider does not say. The provider's own `qr_refresh_seconds` wins when
|
|
302
|
+
* present. Refreshing is what makes the code on screen move, and a frame the
|
|
303
|
+
* provider refuses after 30 seconds is what makes a screenshot of it useless.
|
|
304
|
+
*/
|
|
305
|
+
declare const DEFAULT_QR_REFRESH_SECONDS = 3;
|
|
306
|
+
/** The pairing surface this package will show, sent on POST /pair. */
|
|
307
|
+
type PairDisplay = 'qr' | 'link';
|
|
250
308
|
interface PairCreated {
|
|
251
309
|
request_id: string;
|
|
252
|
-
/**
|
|
310
|
+
/**
|
|
311
|
+
* https://zoreal.com/login/<request_id>. For a 'link' pairing the URL also
|
|
312
|
+
* carries ?t=<start_token>, and only the app that opens that exact link can
|
|
313
|
+
* claim it. Navigate to it verbatim.
|
|
314
|
+
*/
|
|
253
315
|
pair_url: string;
|
|
254
316
|
expires_in: number;
|
|
317
|
+
/**
|
|
318
|
+
* The surface the provider bound, echoed back. 'legacy' means the request
|
|
319
|
+
* sent no `display` (an older version of this package) and got the static
|
|
320
|
+
* code. Absent from a provider that predates the field, which also serves
|
|
321
|
+
* the static code.
|
|
322
|
+
*/
|
|
323
|
+
display?: PairDisplay | 'legacy';
|
|
324
|
+
/** 'qr' pairings only: how often to re-fetch qr.svg. Defaults to 3 when absent. */
|
|
325
|
+
qr_refresh_seconds?: number;
|
|
255
326
|
}
|
|
256
327
|
interface PairImmediate {
|
|
257
328
|
/** prompt=none resolved silently: consented sector, live session. */
|
|
@@ -263,7 +334,12 @@ interface PairStatusResponse {
|
|
|
263
334
|
code?: string;
|
|
264
335
|
expires_in?: number;
|
|
265
336
|
enrolment_deadline?: number;
|
|
266
|
-
/**
|
|
337
|
+
/**
|
|
338
|
+
* The provider's reason on denial or refusal. Surfaced verbatim, never
|
|
339
|
+
* rewritten: it is also how a site's own policy reaches the person, such
|
|
340
|
+
* as a sign-in refused because the phone that approved it was in a
|
|
341
|
+
* different country than the browser.
|
|
342
|
+
*/
|
|
267
343
|
error?: string;
|
|
268
344
|
error_description?: string;
|
|
269
345
|
}
|
|
@@ -307,6 +383,13 @@ interface StartPairingParams {
|
|
|
307
383
|
max_age?: number;
|
|
308
384
|
prompt?: string;
|
|
309
385
|
locale?: string;
|
|
386
|
+
/**
|
|
387
|
+
* Which surface the caller will show: 'qr' binds the pairing to moving QR
|
|
388
|
+
* frames, 'link' to a start token only the opened link carries. Decided
|
|
389
|
+
* before the request, because the provider binds it at creation and will
|
|
390
|
+
* not serve the other surface afterwards. Omitting it gets the static code.
|
|
391
|
+
*/
|
|
392
|
+
display?: PairDisplay;
|
|
310
393
|
}
|
|
311
394
|
declare function startPairing(issuer: string, params: StartPairingParams, signal?: AbortSignal): Promise<PairStartResponse>;
|
|
312
395
|
/**
|
|
@@ -351,4 +434,4 @@ declare function generateState(): string;
|
|
|
351
434
|
*/
|
|
352
435
|
declare function unsafeClaims(idToken: string): Record<string, unknown>;
|
|
353
436
|
|
|
354
|
-
export { type AcrValue, type AuthCodeLoginOptions, type BrowserDirectLoginOptions, DEFAULT_ISSUER, DEFAULT_PAIRING_TIMEOUT_MS, type ErrorCode, FlowAbandonedError, type LoginHandle, type NonOAuthError, OAuthFlowError, POLL_INTERVAL_ENROLLING_MS, POLL_INTERVAL_MS, type PairCreated, 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, startLogin, startPairing, unsafeClaims };
|
|
437
|
+
export { type AcrValue, type AuthCodeLoginOptions, type BrowserDirectLoginOptions, DEFAULT_ISSUER, DEFAULT_PAIRING_TIMEOUT_MS, DEFAULT_QR_REFRESH_SECONDS, type ErrorCode, FlowAbandonedError, type LoginHandle, 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, startLogin, startPairing, unsafeClaims };
|
package/dist/index.d.ts
CHANGED
|
@@ -31,11 +31,28 @@ interface PairingState {
|
|
|
31
31
|
/**
|
|
32
32
|
* The pairing link and its provider-served QR image. Present on every
|
|
33
33
|
* callback of a QR/link flow: the QR flow cannot complete unless SOMETHING
|
|
34
|
-
* renders
|
|
34
|
+
* renders the code. By default that something is this package's own modal;
|
|
35
|
+
* these fields are what you render from instead when you opt out with
|
|
36
|
+
* `pairingUI: 'none'`.
|
|
35
37
|
*/
|
|
36
38
|
pairUrl?: string;
|
|
37
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* The provider-served SVG of the code to scan. Put it in an <img>; do not
|
|
41
|
+
* draw your own. It CHANGES OVER TIME: while the pairing is pending a fresh
|
|
42
|
+
* URL arrives every `qrRefreshSeconds`, because the code on screen moves
|
|
43
|
+
* and the provider refuses a frame older than 30 seconds. That is what
|
|
44
|
+
* makes a screenshot of it useless to an attacker. Render the qrUrl you
|
|
45
|
+
* are given on every state; a UI that caches the first one shows a code
|
|
46
|
+
* that stops working.
|
|
47
|
+
*/
|
|
38
48
|
qrUrl?: string;
|
|
49
|
+
/**
|
|
50
|
+
* How often qrUrl changes, in seconds, while the pairing is pending.
|
|
51
|
+
* Present on a QR flow, absent on an app-link flow and on a provider that
|
|
52
|
+
* still serves a static code. Informational: the refresh itself is done for
|
|
53
|
+
* you and arrives through this callback.
|
|
54
|
+
*/
|
|
55
|
+
qrRefreshSeconds?: number;
|
|
39
56
|
/** True when the flow resolved to the app link (mobile) rather than a QR. */
|
|
40
57
|
appLink?: boolean;
|
|
41
58
|
/** Abandons this pairing: stops the poll. Wire it to your UI's cancel control. */
|
|
@@ -153,9 +170,13 @@ interface LoginHandle<T> {
|
|
|
153
170
|
cancel: () => void;
|
|
154
171
|
/** The pairing request id, once created. Undefined before, and for prompt=none immediate codes. */
|
|
155
172
|
readonly requestId: string | undefined;
|
|
156
|
-
/** The pairing URL, once created.
|
|
173
|
+
/** The pairing URL, once created. On an app-link flow it carries the start token; navigate to it verbatim. */
|
|
157
174
|
readonly pairUrl: string | undefined;
|
|
158
|
-
/**
|
|
175
|
+
/**
|
|
176
|
+
* The provider-served QR image, current frame. Changes every few seconds
|
|
177
|
+
* while the pairing is pending; the onState callback is where a UI should
|
|
178
|
+
* read it from, since that is the only place a new frame announces itself.
|
|
179
|
+
*/
|
|
159
180
|
readonly qrUrl: string | undefined;
|
|
160
181
|
/** True when display resolved to the app link rather than the QR. */
|
|
161
182
|
readonly appLink: boolean | undefined;
|
|
@@ -222,36 +243,86 @@ declare function mountPairingModal(state: PairingState, options: PairingModalOpt
|
|
|
222
243
|
* authorized JavaScript origins (the dashboard):
|
|
223
244
|
*
|
|
224
245
|
* POST /pair start a pairing request. Body carries the
|
|
225
|
-
* authorize parameters plus PKCE challenge
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
246
|
+
* authorize parameters plus PKCE challenge,
|
|
247
|
+
* and `display`: which pairing surface this
|
|
248
|
+
* package is about to show, 'qr' or 'link',
|
|
249
|
+
* decided before the request is made. Returns
|
|
250
|
+
* { request_id, pair_url, expires_in, display,
|
|
251
|
+
* qr_refresh_seconds } or, for prompt=none
|
|
252
|
+
* with a live consented session, { code }
|
|
253
|
+
* immediately. `display` is echoed as the
|
|
254
|
+
* provider bound it ('legacy' for a request
|
|
255
|
+
* that sent none, which gets the static code
|
|
256
|
+
* older versions of this package showed).
|
|
257
|
+
* `qr_refresh_seconds` comes with 'qr' and is
|
|
258
|
+
* how often to re-fetch the image; 3 today.
|
|
259
|
+
* A 'link' pairing's pair_url carries
|
|
260
|
+
* ?t=<start_token>: it can only be claimed by
|
|
261
|
+
* the app that opened that exact link, and
|
|
262
|
+
* the provider never renders a QR for it, so
|
|
263
|
+
* nobody can turn a same-device link into a
|
|
264
|
+
* static code to relay.
|
|
229
265
|
* GET /pair/:id/status poll: pending | claimed |
|
|
230
266
|
* approved (with code) | denied | expired |
|
|
231
267
|
* enrolling. Over-polling cancels the request
|
|
232
268
|
* rather than throttling it, so the cadence
|
|
233
269
|
* below is not a suggestion.
|
|
234
|
-
* GET /pair/:id/qr.svg the QR image
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
270
|
+
* GET /pair/:id/qr.svg the QR image, rendered by the provider so
|
|
271
|
+
* this package draws nothing and keeps zero
|
|
272
|
+
* dependencies. For a 'qr' pairing it encodes
|
|
273
|
+
* the CURRENT FRAME, the pairing URL with
|
|
274
|
+
* ?f=<time>.<hmac>: `time` is whole seconds
|
|
275
|
+
* since the pairing was created on the
|
|
276
|
+
* provider's clock, `hmac` is keyed with a
|
|
277
|
+
* secret only the provider holds. Served
|
|
278
|
+
* Cache-Control: no-store, only while the
|
|
279
|
+
* pairing is pending. The app sends the frame
|
|
280
|
+
* it scanned with its claim, and the provider
|
|
281
|
+
* refuses a frame older than 30 seconds, so a
|
|
282
|
+
* screenshot of the code is dead on arrival.
|
|
283
|
+
* This package re-fetches the image every
|
|
284
|
+
* `qr_refresh_seconds` with a cache-busting
|
|
285
|
+
* ?t=<Date.now()>. A 'link' pairing has no
|
|
286
|
+
* image (404).
|
|
238
287
|
* POST /token the code exchange. Browser-direct mode uses
|
|
239
288
|
* it directly with PKCE and no client secret;
|
|
240
289
|
* auth-code mode leaves it to the RP backend.
|
|
241
290
|
*/
|
|
242
291
|
declare const WIRE_VERSION = 1;
|
|
243
|
-
declare const SDK_VERSION = "0.1.
|
|
292
|
+
declare const SDK_VERSION = "0.1.10";
|
|
244
293
|
declare const SDK_NAME = "@zoreal/oauth2-js";
|
|
245
294
|
declare const DEFAULT_ISSUER = "https://id.zoreal.com";
|
|
246
295
|
/** Pending TTL is short. Poll gently; over-polling cancels the request. */
|
|
247
296
|
declare const POLL_INTERVAL_MS = 2000;
|
|
248
297
|
/** Enrolling extends the window well beyond a normal login; poll slower. */
|
|
249
298
|
declare const POLL_INTERVAL_ENROLLING_MS = 5000;
|
|
299
|
+
/**
|
|
300
|
+
* How often the QR image is re-fetched while a pairing is pending, when the
|
|
301
|
+
* provider does not say. The provider's own `qr_refresh_seconds` wins when
|
|
302
|
+
* present. Refreshing is what makes the code on screen move, and a frame the
|
|
303
|
+
* provider refuses after 30 seconds is what makes a screenshot of it useless.
|
|
304
|
+
*/
|
|
305
|
+
declare const DEFAULT_QR_REFRESH_SECONDS = 3;
|
|
306
|
+
/** The pairing surface this package will show, sent on POST /pair. */
|
|
307
|
+
type PairDisplay = 'qr' | 'link';
|
|
250
308
|
interface PairCreated {
|
|
251
309
|
request_id: string;
|
|
252
|
-
/**
|
|
310
|
+
/**
|
|
311
|
+
* https://zoreal.com/login/<request_id>. For a 'link' pairing the URL also
|
|
312
|
+
* carries ?t=<start_token>, and only the app that opens that exact link can
|
|
313
|
+
* claim it. Navigate to it verbatim.
|
|
314
|
+
*/
|
|
253
315
|
pair_url: string;
|
|
254
316
|
expires_in: number;
|
|
317
|
+
/**
|
|
318
|
+
* The surface the provider bound, echoed back. 'legacy' means the request
|
|
319
|
+
* sent no `display` (an older version of this package) and got the static
|
|
320
|
+
* code. Absent from a provider that predates the field, which also serves
|
|
321
|
+
* the static code.
|
|
322
|
+
*/
|
|
323
|
+
display?: PairDisplay | 'legacy';
|
|
324
|
+
/** 'qr' pairings only: how often to re-fetch qr.svg. Defaults to 3 when absent. */
|
|
325
|
+
qr_refresh_seconds?: number;
|
|
255
326
|
}
|
|
256
327
|
interface PairImmediate {
|
|
257
328
|
/** prompt=none resolved silently: consented sector, live session. */
|
|
@@ -263,7 +334,12 @@ interface PairStatusResponse {
|
|
|
263
334
|
code?: string;
|
|
264
335
|
expires_in?: number;
|
|
265
336
|
enrolment_deadline?: number;
|
|
266
|
-
/**
|
|
337
|
+
/**
|
|
338
|
+
* The provider's reason on denial or refusal. Surfaced verbatim, never
|
|
339
|
+
* rewritten: it is also how a site's own policy reaches the person, such
|
|
340
|
+
* as a sign-in refused because the phone that approved it was in a
|
|
341
|
+
* different country than the browser.
|
|
342
|
+
*/
|
|
267
343
|
error?: string;
|
|
268
344
|
error_description?: string;
|
|
269
345
|
}
|
|
@@ -307,6 +383,13 @@ interface StartPairingParams {
|
|
|
307
383
|
max_age?: number;
|
|
308
384
|
prompt?: string;
|
|
309
385
|
locale?: string;
|
|
386
|
+
/**
|
|
387
|
+
* Which surface the caller will show: 'qr' binds the pairing to moving QR
|
|
388
|
+
* frames, 'link' to a start token only the opened link carries. Decided
|
|
389
|
+
* before the request, because the provider binds it at creation and will
|
|
390
|
+
* not serve the other surface afterwards. Omitting it gets the static code.
|
|
391
|
+
*/
|
|
392
|
+
display?: PairDisplay;
|
|
310
393
|
}
|
|
311
394
|
declare function startPairing(issuer: string, params: StartPairingParams, signal?: AbortSignal): Promise<PairStartResponse>;
|
|
312
395
|
/**
|
|
@@ -351,4 +434,4 @@ declare function generateState(): string;
|
|
|
351
434
|
*/
|
|
352
435
|
declare function unsafeClaims(idToken: string): Record<string, unknown>;
|
|
353
436
|
|
|
354
|
-
export { type AcrValue, type AuthCodeLoginOptions, type BrowserDirectLoginOptions, DEFAULT_ISSUER, DEFAULT_PAIRING_TIMEOUT_MS, type ErrorCode, FlowAbandonedError, type LoginHandle, type NonOAuthError, OAuthFlowError, POLL_INTERVAL_ENROLLING_MS, POLL_INTERVAL_MS, type PairCreated, 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, startLogin, startPairing, unsafeClaims };
|
|
437
|
+
export { type AcrValue, type AuthCodeLoginOptions, type BrowserDirectLoginOptions, DEFAULT_ISSUER, DEFAULT_PAIRING_TIMEOUT_MS, DEFAULT_QR_REFRESH_SECONDS, type ErrorCode, FlowAbandonedError, type LoginHandle, 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, startLogin, startPairing, unsafeClaims };
|
package/dist/index.js
CHANGED
|
@@ -14,11 +14,12 @@ function unsafeClaims(idToken) {
|
|
|
14
14
|
|
|
15
15
|
// src/wire.ts
|
|
16
16
|
var WIRE_VERSION = 1;
|
|
17
|
-
var SDK_VERSION = "0.1.
|
|
17
|
+
var SDK_VERSION = "0.1.10";
|
|
18
18
|
var SDK_NAME = "@zoreal/oauth2-js";
|
|
19
19
|
var DEFAULT_ISSUER = "https://id.zoreal.com";
|
|
20
20
|
var POLL_INTERVAL_MS = 2e3;
|
|
21
21
|
var POLL_INTERVAL_ENROLLING_MS = 5e3;
|
|
22
|
+
var DEFAULT_QR_REFRESH_SECONDS = 3;
|
|
22
23
|
|
|
23
24
|
// src/pairing.ts
|
|
24
25
|
var OAuthFlowError = class extends Error {
|
|
@@ -67,11 +68,15 @@ var sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
|
67
68
|
reject(new DOMException("aborted", "AbortError"));
|
|
68
69
|
return;
|
|
69
70
|
}
|
|
70
|
-
const
|
|
71
|
-
signal?.addEventListener("abort", () => {
|
|
71
|
+
const onAbort = () => {
|
|
72
72
|
clearTimeout(t);
|
|
73
73
|
reject(new DOMException("aborted", "AbortError"));
|
|
74
|
-
}
|
|
74
|
+
};
|
|
75
|
+
const t = setTimeout(() => {
|
|
76
|
+
signal?.removeEventListener("abort", onAbort);
|
|
77
|
+
resolve();
|
|
78
|
+
}, ms);
|
|
79
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
75
80
|
});
|
|
76
81
|
async function pollUntilApproved(issuer, requestId, onState, signal) {
|
|
77
82
|
for (; ; ) {
|
|
@@ -1261,6 +1266,29 @@ function mountPairingModal(state, options) {
|
|
|
1261
1266
|
scrim.appendChild(card);
|
|
1262
1267
|
const serverMs = typeof state.expiresIn === "number" ? state.expiresIn * 1e3 : Infinity;
|
|
1263
1268
|
const deadline = Date.now() + Math.min(timeoutMs, serverMs);
|
|
1269
|
+
let shown = state.qrUrl;
|
|
1270
|
+
let loading = null;
|
|
1271
|
+
const spent = () => qr.dataset.spent === "true";
|
|
1272
|
+
const showFrame = (url) => {
|
|
1273
|
+
if (url === shown || spent()) return;
|
|
1274
|
+
if (!shown) {
|
|
1275
|
+
qr.src = url;
|
|
1276
|
+
shown = url;
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
const next = new Image();
|
|
1280
|
+
loading = next;
|
|
1281
|
+
next.onload = () => {
|
|
1282
|
+
if (loading !== next || spent()) return;
|
|
1283
|
+
loading = null;
|
|
1284
|
+
qr.src = url;
|
|
1285
|
+
shown = url;
|
|
1286
|
+
};
|
|
1287
|
+
next.onerror = () => {
|
|
1288
|
+
if (loading === next) loading = null;
|
|
1289
|
+
};
|
|
1290
|
+
next.src = url;
|
|
1291
|
+
};
|
|
1264
1292
|
const paint = (s) => {
|
|
1265
1293
|
const settled = s.status === "claimed" || s.status === "enrolling";
|
|
1266
1294
|
title.textContent = settled ? t.titleApprove : t.title;
|
|
@@ -1268,7 +1296,7 @@ function mountPairingModal(state, options) {
|
|
|
1268
1296
|
statusLabel.textContent = settled ? t.waitingApproval : t.waiting;
|
|
1269
1297
|
qr.dataset.spent = String(settled);
|
|
1270
1298
|
overlay.style.display = settled ? "" : "none";
|
|
1271
|
-
if (s.qrUrl
|
|
1299
|
+
if (s.qrUrl) showFrame(s.qrUrl);
|
|
1272
1300
|
};
|
|
1273
1301
|
const tick = () => {
|
|
1274
1302
|
const left = Math.max(0, Math.ceil((deadline - Date.now()) / 1e3));
|
|
@@ -1285,6 +1313,7 @@ function mountPairingModal(state, options) {
|
|
|
1285
1313
|
const close = () => {
|
|
1286
1314
|
if (closed) return;
|
|
1287
1315
|
closed = true;
|
|
1316
|
+
loading = null;
|
|
1288
1317
|
window.clearInterval(interval);
|
|
1289
1318
|
document.removeEventListener("keydown", onKey);
|
|
1290
1319
|
document.body.style.overflow = previousOverflow;
|
|
@@ -1334,10 +1363,15 @@ function startLogin(options) {
|
|
|
1334
1363
|
const flow = options.flow ?? "browser-direct";
|
|
1335
1364
|
const issuer = options.issuer ?? DEFAULT_ISSUER;
|
|
1336
1365
|
const controller = new AbortController();
|
|
1366
|
+
const useAppLink = options.display === "link" || options.display !== "qr" && isMobileUserAgent();
|
|
1337
1367
|
const surface = {};
|
|
1338
1368
|
const cancel = () => controller.abort();
|
|
1339
1369
|
let modal = null;
|
|
1340
|
-
|
|
1370
|
+
let stopRefresh = () => {
|
|
1371
|
+
};
|
|
1372
|
+
controller.signal.addEventListener("abort", () => stopRefresh());
|
|
1373
|
+
const teardown = () => {
|
|
1374
|
+
stopRefresh();
|
|
1341
1375
|
modal?.close();
|
|
1342
1376
|
modal = null;
|
|
1343
1377
|
};
|
|
@@ -1358,7 +1392,8 @@ function startLogin(options) {
|
|
|
1358
1392
|
acr_values: Array.isArray(options.acr_values) ? options.acr_values.join(" ") : options.acr_values,
|
|
1359
1393
|
max_age: options.max_age,
|
|
1360
1394
|
prompt: options.prompt,
|
|
1361
|
-
locale: options.locale
|
|
1395
|
+
locale: options.locale,
|
|
1396
|
+
display: useAppLink ? "link" : "qr"
|
|
1362
1397
|
},
|
|
1363
1398
|
controller.signal
|
|
1364
1399
|
);
|
|
@@ -1368,19 +1403,25 @@ function startLogin(options) {
|
|
|
1368
1403
|
code = started.code;
|
|
1369
1404
|
selectBy = "session";
|
|
1370
1405
|
} else {
|
|
1371
|
-
const useAppLink = options.display === "link" || options.display !== "qr" && isMobileUserAgent();
|
|
1372
1406
|
selectBy = useAppLink ? "app_link" : "qr";
|
|
1373
|
-
|
|
1407
|
+
const requestId = started.request_id;
|
|
1408
|
+
const qrBase = `${issuer}/pair/${encodeURIComponent(requestId)}/qr.svg`;
|
|
1409
|
+
const animated = !useAppLink && started.display !== "legacy";
|
|
1410
|
+
const qrRefreshSeconds = !animated ? void 0 : typeof started.qr_refresh_seconds === "number" && started.qr_refresh_seconds > 0 ? started.qr_refresh_seconds : DEFAULT_QR_REFRESH_SECONDS;
|
|
1411
|
+
surface.requestId = requestId;
|
|
1374
1412
|
surface.pairUrl = started.pair_url;
|
|
1375
|
-
surface.qrUrl =
|
|
1413
|
+
surface.qrUrl = qrBase;
|
|
1376
1414
|
surface.appLink = useAppLink;
|
|
1377
|
-
const
|
|
1415
|
+
const withSurface = (s) => ({
|
|
1416
|
+
...s,
|
|
1378
1417
|
pairUrl: surface.pairUrl,
|
|
1379
1418
|
qrUrl: surface.qrUrl,
|
|
1419
|
+
qrRefreshSeconds,
|
|
1380
1420
|
appLink: useAppLink,
|
|
1381
1421
|
cancel
|
|
1382
|
-
};
|
|
1383
|
-
|
|
1422
|
+
});
|
|
1423
|
+
let lastPolled = { status: "pending", expiresIn: started.expires_in };
|
|
1424
|
+
const initial = withSurface(lastPolled);
|
|
1384
1425
|
options.onState?.(initial);
|
|
1385
1426
|
if ((options.pairingUI ?? "modal") === "modal" && !useAppLink) {
|
|
1386
1427
|
modal = mountPairingModal(initial, {
|
|
@@ -1393,18 +1434,53 @@ function startLogin(options) {
|
|
|
1393
1434
|
if (useAppLink && typeof window !== "undefined") {
|
|
1394
1435
|
window.location.assign(started.pair_url);
|
|
1395
1436
|
}
|
|
1437
|
+
if (qrRefreshSeconds !== void 0) {
|
|
1438
|
+
const periodMs = qrRefreshSeconds * 1e3;
|
|
1439
|
+
let due = Date.now() + periodMs;
|
|
1440
|
+
let timer;
|
|
1441
|
+
let stopped = false;
|
|
1442
|
+
const emit = () => {
|
|
1443
|
+
timer = void 0;
|
|
1444
|
+
surface.qrUrl = `${qrBase}?t=${Date.now()}`;
|
|
1445
|
+
const next = withSurface(lastPolled);
|
|
1446
|
+
modal?.update(next);
|
|
1447
|
+
options.onState?.(next);
|
|
1448
|
+
if (stopped) return;
|
|
1449
|
+
due = Date.now() + periodMs;
|
|
1450
|
+
timer = setTimeout(emit, periodMs);
|
|
1451
|
+
};
|
|
1452
|
+
const onVisible = () => {
|
|
1453
|
+
if (document.visibilityState === "visible" && timer !== void 0 && Date.now() >= due) {
|
|
1454
|
+
clearTimeout(timer);
|
|
1455
|
+
emit();
|
|
1456
|
+
}
|
|
1457
|
+
};
|
|
1458
|
+
const hasDocument = typeof document !== "undefined";
|
|
1459
|
+
if (hasDocument) document.addEventListener("visibilitychange", onVisible);
|
|
1460
|
+
stopRefresh = () => {
|
|
1461
|
+
stopped = true;
|
|
1462
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1463
|
+
timer = void 0;
|
|
1464
|
+
if (hasDocument) document.removeEventListener("visibilitychange", onVisible);
|
|
1465
|
+
stopRefresh = () => {
|
|
1466
|
+
};
|
|
1467
|
+
};
|
|
1468
|
+
timer = setTimeout(emit, Math.max(0, due - Date.now()));
|
|
1469
|
+
}
|
|
1396
1470
|
code = await pollUntilApproved(
|
|
1397
1471
|
issuer,
|
|
1398
|
-
|
|
1472
|
+
requestId,
|
|
1399
1473
|
(s) => {
|
|
1400
|
-
|
|
1474
|
+
lastPolled = s;
|
|
1475
|
+
if (s.status !== "pending") stopRefresh();
|
|
1476
|
+
const next = withSurface(s);
|
|
1401
1477
|
modal?.update(next);
|
|
1402
1478
|
options.onState?.(next);
|
|
1403
1479
|
},
|
|
1404
1480
|
controller.signal
|
|
1405
1481
|
);
|
|
1406
1482
|
}
|
|
1407
|
-
|
|
1483
|
+
teardown();
|
|
1408
1484
|
if (flow === "auth-code") {
|
|
1409
1485
|
const response2 = {
|
|
1410
1486
|
code,
|
|
@@ -1429,7 +1505,7 @@ function startLogin(options) {
|
|
|
1429
1505
|
};
|
|
1430
1506
|
return response;
|
|
1431
1507
|
} catch (e) {
|
|
1432
|
-
|
|
1508
|
+
teardown();
|
|
1433
1509
|
if (e instanceof DOMException && e.name === "AbortError") throw e;
|
|
1434
1510
|
if (e instanceof OAuthFlowError || e instanceof FlowAbandonedError) throw e;
|
|
1435
1511
|
throw new FlowAbandonedError({
|
|
@@ -1461,6 +1537,7 @@ function startLogin(options) {
|
|
|
1461
1537
|
export {
|
|
1462
1538
|
DEFAULT_ISSUER,
|
|
1463
1539
|
DEFAULT_PAIRING_TIMEOUT_MS,
|
|
1540
|
+
DEFAULT_QR_REFRESH_SECONDS,
|
|
1464
1541
|
FlowAbandonedError,
|
|
1465
1542
|
OAuthFlowError,
|
|
1466
1543
|
POLL_INTERVAL_ENROLLING_MS,
|