@atlasauth/js 0.6.0 → 0.7.0
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/CHANGELOG.md +36 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/popup-channel.d.ts +95 -0
- package/dist/popup-channel.js +231 -0
- package/dist/popup-channel.js.map +1 -0
- package/dist/redirect.d.ts +45 -4
- package/dist/redirect.js +102 -16
- package/dist/redirect.js.map +1 -1
- package/dist/sign-out.d.ts +50 -9
- package/dist/sign-out.js +193 -30
- package/dist/sign-out.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# @atlasauth/js changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
- **Popup OAuth completes when the opener page sends `Cross-Origin-Opener-Policy: same-origin`.** That
|
|
5
|
+
header puts the provider popup in another browsing-context group: `popup.closed` reads `true` at once and
|
|
6
|
+
the popup has no `window.opener`, so the result could never be posted back and the flow settled as a
|
|
7
|
+
cancel while the popup stayed open. Now the popup lands back on your page with the result in the URL
|
|
8
|
+
fragment (`#atlas_popup_result=…`, never the query string), the SDK there hands it to the opener over
|
|
9
|
+
`BroadcastChannel('atlas:oauth:<nonce>')` (a `localStorage` `storage` event where BroadcastChannel is
|
|
10
|
+
missing) and closes the popup.
|
|
11
|
+
- **Changed `startOauthPopup` cancel semantics.** `popup.closed` turning true within 1.5 s of opening means
|
|
12
|
+
the opener link was severed, not that the user closed it: the flow keeps waiting, probing `GET /v1/client`
|
|
13
|
+
with back-off, for up to 5 minutes, and settles as `popup_closed` only on that timeout, an explicit error
|
|
14
|
+
result, `signal` aborting, or a newer popup flow starting. A close after 1.5 s is a cancel after one final
|
|
15
|
+
session probe (as before).
|
|
16
|
+
- **Changed `startOauthPopup` request.** It now sends `popup_nonce` and `opener_origin`
|
|
17
|
+
(`window.location.origin`) with `mode: 'popup'`, and opens the popup with the window name `atlas:oauth`.
|
|
18
|
+
- **Added** to `startOauthPopup`: `input.signal?: AbortSignal`, `handlers.onSevered?: () => void`,
|
|
19
|
+
`PopupStartResult.severed?: boolean`; new `OAuthPopupInput` type; `POPUP_SEVERED_WINDOW_MS` (1500) and
|
|
20
|
+
`POPUP_SEVERED_TIMEOUT_MS` (300000); `mapOAuthPopupPayload(data)` (the payload half of
|
|
21
|
+
`parseOAuthPopupMessage`).
|
|
22
|
+
- **Added popup channel helpers:** `completeOAuthPopupLanding({ win?, ackTimeoutMs? })` →
|
|
23
|
+
`PopupLanding | null` (`{ payload, delivered }`; reads and scrubs the fragment, relays the result and
|
|
24
|
+
closes once the opener acknowledges — call it on startup), `closeOAuthPopupAfterSignIn({ win?, ackTimeoutMs? })`
|
|
25
|
+
(a page that finished a sign-in inside the Atlas popup closes itself when an opener is waiting),
|
|
26
|
+
`listenForPopupResult(nonce, { onResult, onSignedIn? })`, `readPopupLanding(hash)`, `popupChannelName(nonce)`,
|
|
27
|
+
`createPopupNonce()`, `PopupRelayMessage`, and the constants `OAUTH_POPUP_WINDOW_NAME`
|
|
28
|
+
(`'atlas:oauth'`), `POPUP_RESULT_FRAGMENT` (`'atlas_popup_result'`), `POPUP_SIGNAL_CHANNEL`.
|
|
29
|
+
- **Sign-out tombstones are shared by every tab.** `SessionTombstones` moved from `sessionStorage` to
|
|
30
|
+
`localStorage`, so after a failed revoke a newly opened tab no longer re-adopts the session. Entries are
|
|
31
|
+
`{ id, at }`, at most 20, kept 30 days, pruned on every read, and keyed per Frontend API origin
|
|
32
|
+
(`atlas:signed-out-sessions:<origin>`). `sessionStorage` is used only when `localStorage` throws; tombstones
|
|
33
|
+
an earlier version kept in `sessionStorage` are carried over.
|
|
34
|
+
- **Changed** `new SessionTombstones(options?)`: accepts `{ frontendApi?, storageKey?, now? }`
|
|
35
|
+
(`SessionTombstonesOptions`); a bare storage-key string is still accepted. **Added** `remove(...ids)` (forget
|
|
36
|
+
a tombstone once a later revoke is confirmed) and `subscribe(listener)` (another tab's changes, via the
|
|
37
|
+
`storage` event). `has()`/`list()` re-read storage, so other tabs' changes are seen.
|
|
38
|
+
|
|
3
39
|
## 0.6.0
|
|
4
40
|
- **Per-flow captcha widget rendering**, shared by the prebuilt sign-in / sign-up UIs:
|
|
5
41
|
`renderCaptchaWidget(flow, container)` → `{ token: Promise<string>, remove() }`,
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -22,6 +22,8 @@ __exportStar(require("./token-cache"), exports);
|
|
|
22
22
|
__exportStar(require("./tab-election"), exports);
|
|
23
23
|
__exportStar(require("./attempt"), exports);
|
|
24
24
|
__exportStar(require("./redirect"), exports);
|
|
25
|
+
// §oauth popup mode: the same-origin hand-back for a popup whose opener was severed.
|
|
26
|
+
__exportStar(require("./popup-channel"), exports);
|
|
25
27
|
__exportStar(require("./fapi"), exports);
|
|
26
28
|
__exportStar(require("./native"), exports);
|
|
27
29
|
__exportStar(require("./native-session"), exports);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,6EAA6E;AAC7E,2CAAyB;AACzB,6CAA2B;AAC3B,gDAA8B;AAC9B,iDAA+B;AAC/B,4CAA0B;AAC1B,6CAA2B;AAC3B,yCAAuB;AACvB,2CAAyB;AACzB,mDAAiC;AACjC,4CAA0B;AAC1B,6CAA2B;AAC3B,yCAAuB;AACvB,8CAA4B;AAC5B,2CAAyB;AACzB,4DAA4D;AAC5D,6CAA2B;AAC3B,4CAA0B;AAC1B,mDAAiC;AACjC,kDAAgC;AAChC,sDAAsD;AACtD,wDAAsC;AACtC,+EAA+E;AAC/E,2EAA2E;AAC3E,4CAA0B;AAC1B,kFAAkF;AAClF,mDAAiC;AACjC,kFAAkF;AAClF,yEAAyE;AACzE,oEAAoE;AACpE,uDAAqC;AACrC,sDAAoC;AACpC,0EAA0E;AAC1E,4EAA4E;AAC5E,0CAa0B;AARxB,iGAAA,QAAQ,OAAA;AACR,sGAAA,aAAa,OAAA;AACb,0GAAA,iBAAiB,OAAA;AACjB,uGAAA,cAAc,OAAA;AACd,yGAAA,gBAAgB,OAAA;AAChB,+GAAA,sBAAsB,OAAA;AACtB,kGAAA,SAAS,OAAA;AACT,wGAAA,eAAe,OAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,6EAA6E;AAC7E,2CAAyB;AACzB,6CAA2B;AAC3B,gDAA8B;AAC9B,iDAA+B;AAC/B,4CAA0B;AAC1B,6CAA2B;AAC3B,qFAAqF;AACrF,kDAAgC;AAChC,yCAAuB;AACvB,2CAAyB;AACzB,mDAAiC;AACjC,4CAA0B;AAC1B,6CAA2B;AAC3B,yCAAuB;AACvB,8CAA4B;AAC5B,2CAAyB;AACzB,4DAA4D;AAC5D,6CAA2B;AAC3B,4CAA0B;AAC1B,mDAAiC;AACjC,kDAAgC;AAChC,sDAAsD;AACtD,wDAAsC;AACtC,+EAA+E;AAC/E,2EAA2E;AAC3E,4CAA0B;AAC1B,kFAAkF;AAClF,mDAAiC;AACjC,kFAAkF;AAClF,yEAAyE;AACzE,oEAAoE;AACpE,uDAAqC;AACrC,sDAAoC;AACpC,0EAA0E;AAC1E,4EAA4E;AAC5E,0CAa0B;AARxB,iGAAA,QAAQ,OAAA;AACR,sGAAA,aAAa,OAAA;AACb,0GAAA,iBAAiB,OAAA;AACjB,uGAAA,cAAc,OAAA;AACd,yGAAA,gBAAgB,OAAA;AAChB,+GAAA,sBAAsB,OAAA;AACtB,kGAAA,SAAS,OAAA;AACT,wGAAA,eAAe,OAAA"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §oauth popup mode — the same-origin hand-back for a popup whose
|
|
3
|
+
* `window.opener` was severed.
|
|
4
|
+
*
|
|
5
|
+
* When the page that opens the sign-in popup sends a restrictive
|
|
6
|
+
* Cross-Origin-Opener-Policy (`same-origin`), the browser puts the cross-origin
|
|
7
|
+
* popup in another browsing-context group: `popup.closed` reads `true` to the
|
|
8
|
+
* opener almost at once and `window.opener` is `null` inside the popup, so the
|
|
9
|
+
* Frontend API's `postMessage` can never arrive. The Frontend API then sends the
|
|
10
|
+
* popup back to the OPENER's own page with the result in the URL fragment
|
|
11
|
+
* (`#atlas_popup_result=…`, never the query). That page runs the SDK, which:
|
|
12
|
+
*
|
|
13
|
+
* 1. reads and scrubs the fragment ({@link completeOAuthPopupLanding}),
|
|
14
|
+
* 2. relays the result to the opener over
|
|
15
|
+
* `BroadcastChannel('atlas:oauth:<nonce>')` (a `localStorage` `storage`
|
|
16
|
+
* event where BroadcastChannel is missing) — both are same-origin, so
|
|
17
|
+
* they connect where `window.opener` cannot,
|
|
18
|
+
* 3. closes the popup once the opener acknowledges.
|
|
19
|
+
*
|
|
20
|
+
* Nothing here carries a token: the result is the same one-time ticket (or
|
|
21
|
+
* continuation) the redirect flow puts in its URL, and the opener exchanges it.
|
|
22
|
+
*/
|
|
23
|
+
/** The window name {@link startOauthPopup} opens the popup with. */
|
|
24
|
+
export declare const OAUTH_POPUP_WINDOW_NAME = "atlas:oauth";
|
|
25
|
+
/** The fragment key the Frontend API lands the popup result in. */
|
|
26
|
+
export declare const POPUP_RESULT_FRAGMENT = "atlas_popup_result";
|
|
27
|
+
/** The channel a page that finished sign-in inside the popup announces on. */
|
|
28
|
+
export declare const POPUP_SIGNAL_CHANNEL = "atlas:oauth";
|
|
29
|
+
/** The per-flow channel: `atlas:oauth:<nonce>`. */
|
|
30
|
+
export declare function popupChannelName(nonce: string): string;
|
|
31
|
+
/** A fresh base64url channel nonce (22 characters). */
|
|
32
|
+
export declare function createPopupNonce(): string;
|
|
33
|
+
/** The messages on the popup channels. */
|
|
34
|
+
export type PopupRelayMessage = {
|
|
35
|
+
type: 'atlas:oauth:result';
|
|
36
|
+
id: string;
|
|
37
|
+
result: Record<string, unknown>;
|
|
38
|
+
} | {
|
|
39
|
+
type: 'atlas:oauth:signed_in';
|
|
40
|
+
id: string;
|
|
41
|
+
} | {
|
|
42
|
+
type: 'atlas:oauth:ack';
|
|
43
|
+
id: string;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Opener side: listen on the flow's channel (and the shared signal channel) for
|
|
47
|
+
* the popup's result. Every message is acknowledged so the popup can close.
|
|
48
|
+
* Returns the stop function.
|
|
49
|
+
*/
|
|
50
|
+
export declare function listenForPopupResult(nonce: string, handlers: {
|
|
51
|
+
/** The landing page relayed the Frontend API's result payload. */
|
|
52
|
+
onResult: (payload: Record<string, unknown>) => void;
|
|
53
|
+
/** A page in the popup finished the sign-in itself (cookies are set). */
|
|
54
|
+
onSignedIn?: () => void;
|
|
55
|
+
}): () => void;
|
|
56
|
+
/** The popup result the Frontend API put in a URL fragment, or null. */
|
|
57
|
+
export declare function readPopupLanding(hash: string): {
|
|
58
|
+
nonce: string;
|
|
59
|
+
payload: Record<string, unknown>;
|
|
60
|
+
} | null;
|
|
61
|
+
/** What {@link completeOAuthPopupLanding} did with a landed popup result. */
|
|
62
|
+
export interface PopupLanding {
|
|
63
|
+
/** The Frontend API's result payload (`type`, `ticket`, `attemptId`, …). */
|
|
64
|
+
payload: Record<string, unknown>;
|
|
65
|
+
/**
|
|
66
|
+
* True when the opener acknowledged the result: it completes the sign-in and
|
|
67
|
+
* this window is being closed. False when nobody was listening (the opener tab
|
|
68
|
+
* was closed, or is on another origin) — the caller should complete the
|
|
69
|
+
* sign-in on this page instead, exactly like a redirect.
|
|
70
|
+
*/
|
|
71
|
+
delivered: boolean;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Popup side, run on startup: if this page is the landing of a severed popup
|
|
75
|
+
* (`#atlas_popup_result=…` in the URL — normally with `window.name ===
|
|
76
|
+
* 'atlas:oauth'`, though a browser may clear the name when the opener's COOP
|
|
77
|
+
* switched browsing-context groups, so the fragment alone is decisive), scrub
|
|
78
|
+
* the fragment, relay the result to the opener and close this window once the
|
|
79
|
+
* opener acknowledges. Returns null when this is not such a landing.
|
|
80
|
+
*/
|
|
81
|
+
export declare function completeOAuthPopupLanding(options?: {
|
|
82
|
+
win?: Window;
|
|
83
|
+
ackTimeoutMs?: number;
|
|
84
|
+
}): Promise<PopupLanding | null>;
|
|
85
|
+
/**
|
|
86
|
+
* Popup side, after a sign-in completed ON this page (the redirect flow ran in
|
|
87
|
+
* the popup — e.g. an SDK or provider path without the channel): when this is
|
|
88
|
+
* the Atlas popup window, tell the waiting opener and close. It closes only when
|
|
89
|
+
* an opener is there to take over (it acknowledged, or `window.opener` is live),
|
|
90
|
+
* never stranding a user whose opener tab is gone. Resolves whether it closed.
|
|
91
|
+
*/
|
|
92
|
+
export declare function closeOAuthPopupAfterSignIn(options?: {
|
|
93
|
+
win?: Window;
|
|
94
|
+
ackTimeoutMs?: number;
|
|
95
|
+
}): Promise<boolean>;
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* §oauth popup mode — the same-origin hand-back for a popup whose
|
|
4
|
+
* `window.opener` was severed.
|
|
5
|
+
*
|
|
6
|
+
* When the page that opens the sign-in popup sends a restrictive
|
|
7
|
+
* Cross-Origin-Opener-Policy (`same-origin`), the browser puts the cross-origin
|
|
8
|
+
* popup in another browsing-context group: `popup.closed` reads `true` to the
|
|
9
|
+
* opener almost at once and `window.opener` is `null` inside the popup, so the
|
|
10
|
+
* Frontend API's `postMessage` can never arrive. The Frontend API then sends the
|
|
11
|
+
* popup back to the OPENER's own page with the result in the URL fragment
|
|
12
|
+
* (`#atlas_popup_result=…`, never the query). That page runs the SDK, which:
|
|
13
|
+
*
|
|
14
|
+
* 1. reads and scrubs the fragment ({@link completeOAuthPopupLanding}),
|
|
15
|
+
* 2. relays the result to the opener over
|
|
16
|
+
* `BroadcastChannel('atlas:oauth:<nonce>')` (a `localStorage` `storage`
|
|
17
|
+
* event where BroadcastChannel is missing) — both are same-origin, so
|
|
18
|
+
* they connect where `window.opener` cannot,
|
|
19
|
+
* 3. closes the popup once the opener acknowledges.
|
|
20
|
+
*
|
|
21
|
+
* Nothing here carries a token: the result is the same one-time ticket (or
|
|
22
|
+
* continuation) the redirect flow puts in its URL, and the opener exchanges it.
|
|
23
|
+
*/
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.POPUP_SIGNAL_CHANNEL = exports.POPUP_RESULT_FRAGMENT = exports.OAUTH_POPUP_WINDOW_NAME = void 0;
|
|
26
|
+
exports.popupChannelName = popupChannelName;
|
|
27
|
+
exports.createPopupNonce = createPopupNonce;
|
|
28
|
+
exports.listenForPopupResult = listenForPopupResult;
|
|
29
|
+
exports.readPopupLanding = readPopupLanding;
|
|
30
|
+
exports.completeOAuthPopupLanding = completeOAuthPopupLanding;
|
|
31
|
+
exports.closeOAuthPopupAfterSignIn = closeOAuthPopupAfterSignIn;
|
|
32
|
+
const tab_election_1 = require("./tab-election");
|
|
33
|
+
/** The window name {@link startOauthPopup} opens the popup with. */
|
|
34
|
+
exports.OAUTH_POPUP_WINDOW_NAME = 'atlas:oauth';
|
|
35
|
+
/** The fragment key the Frontend API lands the popup result in. */
|
|
36
|
+
exports.POPUP_RESULT_FRAGMENT = 'atlas_popup_result';
|
|
37
|
+
/** The channel a page that finished sign-in inside the popup announces on. */
|
|
38
|
+
exports.POPUP_SIGNAL_CHANNEL = 'atlas:oauth';
|
|
39
|
+
/** The per-flow channel: `atlas:oauth:<nonce>`. */
|
|
40
|
+
function popupChannelName(nonce) {
|
|
41
|
+
return `${exports.POPUP_SIGNAL_CHANNEL}:${nonce}`;
|
|
42
|
+
}
|
|
43
|
+
/** A fresh base64url channel nonce (22 characters). */
|
|
44
|
+
function createPopupNonce() {
|
|
45
|
+
const bytes = new Uint8Array(16);
|
|
46
|
+
const c = globalThis.crypto;
|
|
47
|
+
if (c?.getRandomValues)
|
|
48
|
+
c.getRandomValues(bytes);
|
|
49
|
+
else
|
|
50
|
+
for (let i = 0; i < bytes.length; i++)
|
|
51
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
52
|
+
let bin = '';
|
|
53
|
+
for (const b of bytes)
|
|
54
|
+
bin += String.fromCharCode(b);
|
|
55
|
+
return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
56
|
+
}
|
|
57
|
+
function isRelayMessage(value) {
|
|
58
|
+
if (!value || typeof value !== 'object')
|
|
59
|
+
return false;
|
|
60
|
+
const m = value;
|
|
61
|
+
if (typeof m.id !== 'string')
|
|
62
|
+
return false;
|
|
63
|
+
if (m.type === 'atlas:oauth:result')
|
|
64
|
+
return !!m.result && typeof m.result === 'object';
|
|
65
|
+
return m.type === 'atlas:oauth:signed_in' || m.type === 'atlas:oauth:ack';
|
|
66
|
+
}
|
|
67
|
+
function openChannel(name) {
|
|
68
|
+
try {
|
|
69
|
+
return (0, tab_election_1.broadcastChannel)(name) ?? (0, tab_election_1.storageChannel)(name);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Post `message` on `name` and resolve whether anyone acknowledged it within
|
|
77
|
+
* `timeoutMs`.
|
|
78
|
+
*/
|
|
79
|
+
function relay(name, message, timeoutMs) {
|
|
80
|
+
const channel = openChannel(name);
|
|
81
|
+
if (!channel)
|
|
82
|
+
return Promise.resolve(false);
|
|
83
|
+
const id = createPopupNonce();
|
|
84
|
+
return new Promise((resolve) => {
|
|
85
|
+
let done = false;
|
|
86
|
+
const finish = (acked) => {
|
|
87
|
+
if (done)
|
|
88
|
+
return;
|
|
89
|
+
done = true;
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
unsubscribe();
|
|
92
|
+
channel.close();
|
|
93
|
+
resolve(acked);
|
|
94
|
+
};
|
|
95
|
+
const unsubscribe = channel.subscribe((m) => {
|
|
96
|
+
if (isRelayMessage(m) && m.type === 'atlas:oauth:ack' && m.id === id)
|
|
97
|
+
finish(true);
|
|
98
|
+
});
|
|
99
|
+
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
100
|
+
try {
|
|
101
|
+
channel.post({ ...message, id });
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
finish(false);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Opener side: listen on the flow's channel (and the shared signal channel) for
|
|
110
|
+
* the popup's result. Every message is acknowledged so the popup can close.
|
|
111
|
+
* Returns the stop function.
|
|
112
|
+
*/
|
|
113
|
+
function listenForPopupResult(nonce, handlers) {
|
|
114
|
+
const flow = openChannel(popupChannelName(nonce));
|
|
115
|
+
const signal = handlers.onSignedIn ? openChannel(exports.POPUP_SIGNAL_CHANNEL) : null;
|
|
116
|
+
const unsubscribers = [];
|
|
117
|
+
if (flow) {
|
|
118
|
+
unsubscribers.push(flow.subscribe((m) => {
|
|
119
|
+
if (!isRelayMessage(m) || m.type !== 'atlas:oauth:result')
|
|
120
|
+
return;
|
|
121
|
+
flow.post({ type: 'atlas:oauth:ack', id: m.id });
|
|
122
|
+
handlers.onResult(m.result);
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
if (signal) {
|
|
126
|
+
unsubscribers.push(signal.subscribe((m) => {
|
|
127
|
+
if (!isRelayMessage(m) || m.type !== 'atlas:oauth:signed_in')
|
|
128
|
+
return;
|
|
129
|
+
signal.post({ type: 'atlas:oauth:ack', id: m.id });
|
|
130
|
+
handlers.onSignedIn?.();
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
return () => {
|
|
134
|
+
for (const u of unsubscribers)
|
|
135
|
+
u();
|
|
136
|
+
flow?.close();
|
|
137
|
+
signal?.close();
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/** The popup result the Frontend API put in a URL fragment, or null. */
|
|
141
|
+
function readPopupLanding(hash) {
|
|
142
|
+
if (!hash || hash.length < 2)
|
|
143
|
+
return null;
|
|
144
|
+
const raw = new URLSearchParams(hash.replace(/^#/, '')).get(exports.POPUP_RESULT_FRAGMENT);
|
|
145
|
+
if (!raw)
|
|
146
|
+
return null;
|
|
147
|
+
try {
|
|
148
|
+
const b64 = raw.replace(/-/g, '+').replace(/_/g, '/');
|
|
149
|
+
const bin = atob(b64 + '='.repeat((4 - (b64.length % 4)) % 4));
|
|
150
|
+
const bytes = Uint8Array.from(bin, (ch) => ch.charCodeAt(0));
|
|
151
|
+
const payload = JSON.parse(new TextDecoder().decode(bytes));
|
|
152
|
+
if (!payload || typeof payload !== 'object')
|
|
153
|
+
return null;
|
|
154
|
+
const p = payload;
|
|
155
|
+
if (p.v !== 1 || typeof p.n !== 'string' || !p.n)
|
|
156
|
+
return null;
|
|
157
|
+
if (typeof p.type !== 'string' || !p.type.startsWith('atlas:oauth:'))
|
|
158
|
+
return null;
|
|
159
|
+
return { nonce: p.n, payload: p };
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Popup side, run on startup: if this page is the landing of a severed popup
|
|
167
|
+
* (`#atlas_popup_result=…` in the URL — normally with `window.name ===
|
|
168
|
+
* 'atlas:oauth'`, though a browser may clear the name when the opener's COOP
|
|
169
|
+
* switched browsing-context groups, so the fragment alone is decisive), scrub
|
|
170
|
+
* the fragment, relay the result to the opener and close this window once the
|
|
171
|
+
* opener acknowledges. Returns null when this is not such a landing.
|
|
172
|
+
*/
|
|
173
|
+
async function completeOAuthPopupLanding(options = {}) {
|
|
174
|
+
const win = options.win ?? (typeof window !== 'undefined' ? window : undefined);
|
|
175
|
+
if (!win)
|
|
176
|
+
return null;
|
|
177
|
+
const landing = readPopupLanding(win.location.hash);
|
|
178
|
+
if (!landing)
|
|
179
|
+
return null;
|
|
180
|
+
// Scrub first: a reload, the back button or a copied URL must never replay it.
|
|
181
|
+
try {
|
|
182
|
+
win.history.replaceState(win.history.state, '', win.location.href.split('#')[0]);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
/* a sandboxed history; the ticket is single-use regardless */
|
|
186
|
+
}
|
|
187
|
+
// We ARE the Atlas popup, whatever the browser did to the name.
|
|
188
|
+
try {
|
|
189
|
+
win.name = exports.OAUTH_POPUP_WINDOW_NAME;
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
/* ignore */
|
|
193
|
+
}
|
|
194
|
+
const delivered = await relay(popupChannelName(landing.nonce), { type: 'atlas:oauth:result', result: landing.payload }, options.ackTimeoutMs ?? 1_500);
|
|
195
|
+
if (delivered)
|
|
196
|
+
closeWindow(win);
|
|
197
|
+
return { payload: landing.payload, delivered };
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Popup side, after a sign-in completed ON this page (the redirect flow ran in
|
|
201
|
+
* the popup — e.g. an SDK or provider path without the channel): when this is
|
|
202
|
+
* the Atlas popup window, tell the waiting opener and close. It closes only when
|
|
203
|
+
* an opener is there to take over (it acknowledged, or `window.opener` is live),
|
|
204
|
+
* never stranding a user whose opener tab is gone. Resolves whether it closed.
|
|
205
|
+
*/
|
|
206
|
+
async function closeOAuthPopupAfterSignIn(options = {}) {
|
|
207
|
+
const win = options.win ?? (typeof window !== 'undefined' ? window : undefined);
|
|
208
|
+
if (!win || win.name !== exports.OAUTH_POPUP_WINDOW_NAME)
|
|
209
|
+
return false;
|
|
210
|
+
const acked = await relay(exports.POPUP_SIGNAL_CHANNEL, { type: 'atlas:oauth:signed_in' }, options.ackTimeoutMs ?? 1_500);
|
|
211
|
+
let hasOpener = false;
|
|
212
|
+
try {
|
|
213
|
+
hasOpener = !!win.opener && !win.opener.closed;
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
hasOpener = false;
|
|
217
|
+
}
|
|
218
|
+
if (!acked && !hasOpener)
|
|
219
|
+
return false;
|
|
220
|
+
closeWindow(win);
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
function closeWindow(win) {
|
|
224
|
+
try {
|
|
225
|
+
win.close();
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
/* the browser refused; nothing else to do */
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
//# sourceMappingURL=popup-channel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"popup-channel.js","sourceRoot":"","sources":["../src/popup-channel.ts"],"names":[],"mappings":"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"}
|
package/dist/redirect.d.ts
CHANGED
|
@@ -59,6 +59,12 @@ export interface PopupOutcome {
|
|
|
59
59
|
* `postMessage` on the page can never drive the flow.
|
|
60
60
|
*/
|
|
61
61
|
export declare function parseOAuthPopupMessage(eventOrigin: string, expectedOrigin: string, data: unknown): PopupOutcome | null;
|
|
62
|
+
/**
|
|
63
|
+
* Pure: map one of the Frontend API's `atlas:oauth:*` result payloads (from a
|
|
64
|
+
* `postMessage`, or relayed from a severed popup's landing page) to an outcome,
|
|
65
|
+
* or null when it is not one.
|
|
66
|
+
*/
|
|
67
|
+
export declare function mapOAuthPopupPayload(data: unknown): PopupOutcome | null;
|
|
62
68
|
export interface OAuthPopupHandlers {
|
|
63
69
|
onOpen?: () => void;
|
|
64
70
|
onComplete?: (r: {
|
|
@@ -68,6 +74,14 @@ export interface OAuthPopupHandlers {
|
|
|
68
74
|
onError?: (code: string) => void;
|
|
69
75
|
/** Fired when the popup couldn't open; the caller should navigate to the URL. */
|
|
70
76
|
onBlockedFallback?: (authorizationUrl: string) => void;
|
|
77
|
+
/**
|
|
78
|
+
* Fired when the popup's link to this page was severed right after opening
|
|
79
|
+
* (this page, or the provider, sends a Cross-Origin-Opener-Policy): the popup
|
|
80
|
+
* still works, but closing it can no longer be noticed, so the flow keeps
|
|
81
|
+
* waiting — up to 5 minutes — for its result. A good moment to show "Finish
|
|
82
|
+
* signing in in the popup window" with a cancel button that aborts `signal`.
|
|
83
|
+
*/
|
|
84
|
+
onSevered?: () => void;
|
|
71
85
|
}
|
|
72
86
|
export interface PopupStartResult extends PopupOutcome {
|
|
73
87
|
/** True when the popup was blocked — the caller falls back to a full redirect. */
|
|
@@ -81,18 +95,45 @@ export interface PopupStartResult extends PopupOutcome {
|
|
|
81
95
|
* cancel.
|
|
82
96
|
*/
|
|
83
97
|
sessionDetected?: boolean;
|
|
98
|
+
/** True when the popup's opener link was severed (see `onSevered`). */
|
|
99
|
+
severed?: boolean;
|
|
100
|
+
}
|
|
101
|
+
/** Input to {@link startOauthPopup}. */
|
|
102
|
+
export interface OAuthPopupInput {
|
|
103
|
+
provider: string;
|
|
104
|
+
/**
|
|
105
|
+
* Where the redirect flow would return to — normally the current page. A
|
|
106
|
+
* popup whose opener link is severed lands back here (result in the
|
|
107
|
+
* fragment), so it should be a page on THIS origin that runs the SDK.
|
|
108
|
+
*/
|
|
109
|
+
redirectUrl: string;
|
|
110
|
+
/** Abort to give up on the popup (settles as `popup_closed`). */
|
|
111
|
+
signal?: AbortSignal;
|
|
84
112
|
}
|
|
113
|
+
/** `popup.closed` this soon after opening means the opener link was severed, not a user close. */
|
|
114
|
+
export declare const POPUP_SEVERED_WINDOW_MS = 1500;
|
|
115
|
+
/** How long a severed popup is waited for before it counts as cancelled. */
|
|
116
|
+
export declare const POPUP_SEVERED_TIMEOUT_MS: number;
|
|
85
117
|
/**
|
|
86
118
|
* Start a POPUP OAuth sign-in (GSI-style). Opens the provider authorize URL in a
|
|
87
119
|
* popup and resolves when the callback posts back the one-time ticket (complete),
|
|
88
120
|
* an error, or a continuation. Falls back to a full redirect when the popup is
|
|
89
121
|
* blocked — resolves `{ blocked:true, authorizationUrl }` and the caller
|
|
90
122
|
* navigates there, so it is always safe to enable. Browser-only.
|
|
123
|
+
*
|
|
124
|
+
* The result arrives by `postMessage` from the Frontend API, or — when a
|
|
125
|
+
* Cross-Origin-Opener-Policy severed the popup from this page — relayed over
|
|
126
|
+
* `BroadcastChannel('atlas:oauth:<nonce>')` by the SDK on the page the popup
|
|
127
|
+
* lands on (see ./popup-channel). Cancel semantics:
|
|
128
|
+
* - the popup closes after the first {@link POPUP_SEVERED_WINDOW_MS}: the user
|
|
129
|
+
* closed it — one final session probe, then `popup_closed`;
|
|
130
|
+
* - `popup.closed` reads true within that window: the link was severed (the
|
|
131
|
+
* popup is very much open). Keep waiting for the relayed result, probing
|
|
132
|
+
* the session with back-off, and settle as `popup_closed` only after
|
|
133
|
+
* {@link POPUP_SEVERED_TIMEOUT_MS}, an explicit error/cancel result,
|
|
134
|
+
* `input.signal` aborting, or a newer popup flow starting.
|
|
91
135
|
*/
|
|
92
|
-
export declare function startOauthPopup(client: FapiClient, input:
|
|
93
|
-
provider: string;
|
|
94
|
-
redirectUrl: string;
|
|
95
|
-
}, handlers?: OAuthPopupHandlers): Promise<PopupStartResult>;
|
|
136
|
+
export declare function startOauthPopup(client: FapiClient, input: OAuthPopupInput, handlers?: OAuthPopupHandlers): Promise<PopupStartResult>;
|
|
96
137
|
/**
|
|
97
138
|
* Read Atlas's redirect params from a query string (`window.location.search`).
|
|
98
139
|
*
|
package/dist/redirect.js
CHANGED
|
@@ -15,12 +15,14 @@
|
|
|
15
15
|
* the React provider and any custom integration read the exact same contract.
|
|
16
16
|
*/
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
exports.REDIRECT_PARAMS = void 0;
|
|
18
|
+
exports.POPUP_SEVERED_TIMEOUT_MS = exports.POPUP_SEVERED_WINDOW_MS = exports.REDIRECT_PARAMS = void 0;
|
|
19
19
|
exports.startOauthRedirect = startOauthRedirect;
|
|
20
20
|
exports.parseOAuthPopupMessage = parseOAuthPopupMessage;
|
|
21
|
+
exports.mapOAuthPopupPayload = mapOAuthPopupPayload;
|
|
21
22
|
exports.startOauthPopup = startOauthPopup;
|
|
22
23
|
exports.readRedirectResult = readRedirectResult;
|
|
23
24
|
exports.stripRedirectParams = stripRedirectParams;
|
|
25
|
+
const popup_channel_1 = require("./popup-channel");
|
|
24
26
|
exports.REDIRECT_PARAMS = ['__atlas_attempt', '__atlas_ticket', '__atlas_status'];
|
|
25
27
|
/**
|
|
26
28
|
* Start a redirect-based OAuth sign-in for a provider that has no in-page native
|
|
@@ -48,6 +50,14 @@ async function startOauthRedirect(client, input) {
|
|
|
48
50
|
function parseOAuthPopupMessage(eventOrigin, expectedOrigin, data) {
|
|
49
51
|
if (eventOrigin !== expectedOrigin)
|
|
50
52
|
return null;
|
|
53
|
+
return mapOAuthPopupPayload(data);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Pure: map one of the Frontend API's `atlas:oauth:*` result payloads (from a
|
|
57
|
+
* `postMessage`, or relayed from a severed popup's landing page) to an outcome,
|
|
58
|
+
* or null when it is not one.
|
|
59
|
+
*/
|
|
60
|
+
function mapOAuthPopupPayload(data) {
|
|
51
61
|
if (!data || typeof data !== 'object')
|
|
52
62
|
return null;
|
|
53
63
|
const d = data;
|
|
@@ -67,6 +77,12 @@ function parseOAuthPopupMessage(eventOrigin, expectedOrigin, data) {
|
|
|
67
77
|
return null;
|
|
68
78
|
}
|
|
69
79
|
}
|
|
80
|
+
/** `popup.closed` this soon after opening means the opener link was severed, not a user close. */
|
|
81
|
+
exports.POPUP_SEVERED_WINDOW_MS = 1_500;
|
|
82
|
+
/** How long a severed popup is waited for before it counts as cancelled. */
|
|
83
|
+
exports.POPUP_SEVERED_TIMEOUT_MS = 5 * 60_000;
|
|
84
|
+
/** Back-off between session probes while a severed popup is pending. */
|
|
85
|
+
const SEVERED_PROBE_DELAYS_MS = [2_000, 3_000, 5_000, 8_000, 13_000, 21_000, 30_000];
|
|
70
86
|
function centeredPopupFeatures(w, h) {
|
|
71
87
|
const dualLeft = (window.screenLeft ?? window.screenX ?? 0);
|
|
72
88
|
const dualTop = (window.screenTop ?? window.screenY ?? 0);
|
|
@@ -76,18 +92,35 @@ function centeredPopupFeatures(w, h) {
|
|
|
76
92
|
const top = Math.max(0, dualTop + (vh - h) / 2);
|
|
77
93
|
return `popup,width=${w},height=${h},left=${Math.round(left)},top=${Math.round(top)}`;
|
|
78
94
|
}
|
|
95
|
+
/** The popup flow currently waiting in this page; a new one supersedes it. */
|
|
96
|
+
let cancelActivePopup = null;
|
|
79
97
|
/**
|
|
80
98
|
* Start a POPUP OAuth sign-in (GSI-style). Opens the provider authorize URL in a
|
|
81
99
|
* popup and resolves when the callback posts back the one-time ticket (complete),
|
|
82
100
|
* an error, or a continuation. Falls back to a full redirect when the popup is
|
|
83
101
|
* blocked — resolves `{ blocked:true, authorizationUrl }` and the caller
|
|
84
102
|
* navigates there, so it is always safe to enable. Browser-only.
|
|
103
|
+
*
|
|
104
|
+
* The result arrives by `postMessage` from the Frontend API, or — when a
|
|
105
|
+
* Cross-Origin-Opener-Policy severed the popup from this page — relayed over
|
|
106
|
+
* `BroadcastChannel('atlas:oauth:<nonce>')` by the SDK on the page the popup
|
|
107
|
+
* lands on (see ./popup-channel). Cancel semantics:
|
|
108
|
+
* - the popup closes after the first {@link POPUP_SEVERED_WINDOW_MS}: the user
|
|
109
|
+
* closed it — one final session probe, then `popup_closed`;
|
|
110
|
+
* - `popup.closed` reads true within that window: the link was severed (the
|
|
111
|
+
* popup is very much open). Keep waiting for the relayed result, probing
|
|
112
|
+
* the session with back-off, and settle as `popup_closed` only after
|
|
113
|
+
* {@link POPUP_SEVERED_TIMEOUT_MS}, an explicit error/cancel result,
|
|
114
|
+
* `input.signal` aborting, or a newer popup flow starting.
|
|
85
115
|
*/
|
|
86
116
|
async function startOauthPopup(client, input, handlers = {}) {
|
|
117
|
+
const nonce = (0, popup_channel_1.createPopupNonce)();
|
|
87
118
|
const res = await client.post('/v1/client/sign_ins/oauth', {
|
|
88
119
|
provider: input.provider,
|
|
89
120
|
redirect_url: input.redirectUrl,
|
|
90
121
|
mode: 'popup',
|
|
122
|
+
popup_nonce: nonce,
|
|
123
|
+
opener_origin: window.location.origin,
|
|
91
124
|
});
|
|
92
125
|
const authorizationUrl = res.ok ? res.data?.authorization_url : undefined;
|
|
93
126
|
if (!authorizationUrl)
|
|
@@ -95,7 +128,7 @@ async function startOauthPopup(client, input, handlers = {}) {
|
|
|
95
128
|
const expectedOrigin = client.frontendApiOrigin || window.location.origin;
|
|
96
129
|
let popup = null;
|
|
97
130
|
try {
|
|
98
|
-
popup = window.open(authorizationUrl,
|
|
131
|
+
popup = window.open(authorizationUrl, popup_channel_1.OAUTH_POPUP_WINDOW_NAME, centeredPopupFeatures(500, 650));
|
|
99
132
|
}
|
|
100
133
|
catch {
|
|
101
134
|
popup = null;
|
|
@@ -104,15 +137,28 @@ async function startOauthPopup(client, input, handlers = {}) {
|
|
|
104
137
|
handlers.onBlockedFallback?.(authorizationUrl);
|
|
105
138
|
return { blocked: true, authorizationUrl };
|
|
106
139
|
}
|
|
140
|
+
const openedAt = Date.now();
|
|
107
141
|
handlers.onOpen?.();
|
|
142
|
+
// Only one popup flow at a time: starting another cancels the one waiting.
|
|
143
|
+
cancelActivePopup?.();
|
|
108
144
|
return await new Promise((resolve) => {
|
|
109
145
|
let done = false;
|
|
146
|
+
let severed = false;
|
|
147
|
+
let probeTimer;
|
|
148
|
+
let deadline;
|
|
149
|
+
const cancel = () => settle({ error: 'popup_closed' });
|
|
110
150
|
const settle = (r) => {
|
|
111
151
|
if (done)
|
|
112
152
|
return;
|
|
113
153
|
done = true;
|
|
114
154
|
window.removeEventListener('message', onMessage);
|
|
115
155
|
clearInterval(timer);
|
|
156
|
+
clearTimeout(probeTimer);
|
|
157
|
+
clearTimeout(deadline);
|
|
158
|
+
stopListening();
|
|
159
|
+
input.signal?.removeEventListener('abort', cancel);
|
|
160
|
+
if (cancelActivePopup === cancel)
|
|
161
|
+
cancelActivePopup = null;
|
|
116
162
|
try {
|
|
117
163
|
if (popup && !popup.closed)
|
|
118
164
|
popup.close();
|
|
@@ -120,11 +166,10 @@ async function startOauthPopup(client, input, handlers = {}) {
|
|
|
120
166
|
catch {
|
|
121
167
|
/* cross-origin close can throw; ignore */
|
|
122
168
|
}
|
|
123
|
-
resolve(r);
|
|
169
|
+
resolve(severed ? { ...r, severed: true } : r);
|
|
124
170
|
};
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
if (!outcome)
|
|
171
|
+
const deliver = (outcome) => {
|
|
172
|
+
if (done)
|
|
128
173
|
return;
|
|
129
174
|
if (outcome.ticket)
|
|
130
175
|
handlers.onComplete?.({ ticket: outcome.ticket, attemptId: outcome.attemptId });
|
|
@@ -132,21 +177,62 @@ async function startOauthPopup(client, input, handlers = {}) {
|
|
|
132
177
|
handlers.onError?.(outcome.error);
|
|
133
178
|
settle(outcome);
|
|
134
179
|
};
|
|
180
|
+
const onMessage = (ev) => {
|
|
181
|
+
const outcome = parseOAuthPopupMessage(ev.origin, expectedOrigin, ev.data);
|
|
182
|
+
if (outcome)
|
|
183
|
+
deliver(outcome);
|
|
184
|
+
};
|
|
185
|
+
// A probe that finds a session settles; otherwise `onMiss` decides.
|
|
186
|
+
const probe = (onMiss) => {
|
|
187
|
+
if (done)
|
|
188
|
+
return;
|
|
189
|
+
void probeSession(client).then((signedIn) => {
|
|
190
|
+
if (done)
|
|
191
|
+
return;
|
|
192
|
+
if (signedIn)
|
|
193
|
+
settle({ error: 'popup_closed', sessionDetected: true });
|
|
194
|
+
else
|
|
195
|
+
onMiss();
|
|
196
|
+
});
|
|
197
|
+
};
|
|
198
|
+
const scheduleProbe = (attempt) => {
|
|
199
|
+
const delay = SEVERED_PROBE_DELAYS_MS[Math.min(attempt, SEVERED_PROBE_DELAYS_MS.length - 1)];
|
|
200
|
+
probeTimer = setTimeout(() => probe(() => scheduleProbe(attempt + 1)), delay);
|
|
201
|
+
};
|
|
202
|
+
const stopListening = (0, popup_channel_1.listenForPopupResult)(nonce, {
|
|
203
|
+
onResult: (payload) => {
|
|
204
|
+
const outcome = mapOAuthPopupPayload(payload);
|
|
205
|
+
if (outcome)
|
|
206
|
+
deliver(outcome);
|
|
207
|
+
},
|
|
208
|
+
// A page in the popup finished the sign-in itself: check now.
|
|
209
|
+
onSignedIn: () => probe(() => undefined),
|
|
210
|
+
});
|
|
135
211
|
// The user closing the popup without finishing is a cancellation, not a hang.
|
|
136
|
-
// But
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
// post back and finishes via the ordinary redirect instead, which sets the
|
|
140
|
-
// session cookies. So check the session once before calling it a cancel.
|
|
141
|
-
let probing = false;
|
|
212
|
+
// But `popup.closed` turning true right after opening is not a close: the
|
|
213
|
+
// opener link was severed (Cross-Origin-Opener-Policy) and the popup is
|
|
214
|
+
// still on screen. Then wait for the relayed result instead.
|
|
142
215
|
const timer = setInterval(() => {
|
|
143
|
-
if (!popup || !popup.closed ||
|
|
216
|
+
if (!popup || !popup.closed || done)
|
|
144
217
|
return;
|
|
145
|
-
probing = true;
|
|
146
218
|
clearInterval(timer);
|
|
147
|
-
|
|
148
|
-
|
|
219
|
+
if (Date.now() - openedAt <= exports.POPUP_SEVERED_WINDOW_MS) {
|
|
220
|
+
severed = true;
|
|
221
|
+
handlers.onSevered?.();
|
|
222
|
+
deadline = setTimeout(cancel, exports.POPUP_SEVERED_TIMEOUT_MS);
|
|
223
|
+
scheduleProbe(0);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
// A real close. It can still follow a sign-in the popup finished on its
|
|
227
|
+
// own (an opener the browser severed later, mid-flow), so check the
|
|
228
|
+
// session once before calling it a cancel.
|
|
229
|
+
probe(cancel);
|
|
230
|
+
}, 250);
|
|
149
231
|
window.addEventListener('message', onMessage);
|
|
232
|
+
input.signal?.addEventListener('abort', cancel);
|
|
233
|
+
if (input.signal?.aborted)
|
|
234
|
+
cancel();
|
|
235
|
+
cancelActivePopup = cancel;
|
|
150
236
|
});
|
|
151
237
|
}
|
|
152
238
|
/** One `GET /v1/client`: is there a signed-in session on this browser now? */
|
package/dist/redirect.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redirect.js","sourceRoot":"","sources":["../src/redirect.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;;
|
|
1
|
+
{"version":3,"file":"redirect.js","sourceRoot":"","sources":["../src/redirect.ts"],"names":[],"mappings":"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"}
|
package/dist/sign-out.d.ts
CHANGED
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
*
|
|
11
11
|
* These helpers close that gap: {@link revokeSession} retries once and reports
|
|
12
12
|
* what the server did (including the parked session it promoted, if any), and
|
|
13
|
-
* {@link SessionTombstones} remembers the ids of sessions this
|
|
14
|
-
* so a reload never re-adopts one, even when
|
|
15
|
-
* server.
|
|
13
|
+
* {@link SessionTombstones} remembers the ids of sessions this browser signed
|
|
14
|
+
* out of so a reload — or a tab opened later — never re-adopts one, even when
|
|
15
|
+
* the revoke never reached the server.
|
|
16
16
|
*/
|
|
17
17
|
/** What `signOut()` resolves with. */
|
|
18
18
|
export interface SignOutResult {
|
|
@@ -58,17 +58,58 @@ export interface RevokeSessionOutcome {
|
|
|
58
58
|
export declare function revokeSession(request: (path: string, init?: RequestInit) => Promise<Response>, sessionId: string, options?: {
|
|
59
59
|
retries?: number;
|
|
60
60
|
}): Promise<RevokeSessionOutcome>;
|
|
61
|
+
/** Options for {@link SessionTombstones}. */
|
|
62
|
+
export interface SessionTombstonesOptions {
|
|
63
|
+
/**
|
|
64
|
+
* The Frontend API URL or origin the sessions belong to. Tombstones are kept
|
|
65
|
+
* per Frontend API origin, so two Atlas instances used on one site never
|
|
66
|
+
* share (or clear) each other's.
|
|
67
|
+
*/
|
|
68
|
+
frontendApi?: string;
|
|
69
|
+
/** Override the storage key entirely (the Frontend API scope is then not appended). */
|
|
70
|
+
storageKey?: string;
|
|
71
|
+
/** Clock, for tests. */
|
|
72
|
+
now?: () => number;
|
|
73
|
+
}
|
|
61
74
|
/**
|
|
62
|
-
* The ids of sessions this
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
75
|
+
* The ids of sessions this browser signed out of, so a reload — or a tab opened
|
|
76
|
+
* later — never re-adopts one from a refresh cookie the revoke did not clear.
|
|
77
|
+
*
|
|
78
|
+
* Kept in `localStorage` (shared by every tab of the site and surviving a
|
|
79
|
+
* restart), at most 20 entries, each for 30 days, pruned whenever it is read,
|
|
80
|
+
* and keyed per Frontend API origin. Tabs see each other's changes on every
|
|
81
|
+
* read, and {@link SessionTombstones.subscribe} reports them as they happen
|
|
82
|
+
* (the `storage` event). `sessionStorage` is used only when `localStorage`
|
|
83
|
+
* throws (storage disabled, sandboxed frames); memory always backs both. A
|
|
84
|
+
* confirmed revoke may {@link SessionTombstones.remove} its tombstone. Never
|
|
85
|
+
* holds a token — a session id is not a credential.
|
|
66
86
|
*/
|
|
67
87
|
export declare class SessionTombstones {
|
|
88
|
+
private entries;
|
|
89
|
+
/** Ids no storage accepted: only memory remembers them, so a sync keeps them. */
|
|
90
|
+
private unsaved;
|
|
68
91
|
private readonly storageKey;
|
|
69
|
-
private readonly
|
|
70
|
-
constructor(
|
|
92
|
+
private readonly now;
|
|
93
|
+
constructor(options?: string | SessionTombstonesOptions);
|
|
71
94
|
has(sessionId: string | null | undefined): boolean;
|
|
72
95
|
add(...sessionIds: (string | null | undefined)[]): void;
|
|
96
|
+
/**
|
|
97
|
+
* Forget tombstones — called once a later revoke of the session is confirmed
|
|
98
|
+
* (the server has ended it, so there is nothing left to re-adopt).
|
|
99
|
+
*/
|
|
100
|
+
remove(...sessionIds: (string | null | undefined)[]): void;
|
|
73
101
|
list(): string[];
|
|
102
|
+
/**
|
|
103
|
+
* Call `listener` when another tab adds or removes a tombstone (the `storage`
|
|
104
|
+
* event). Returns the unsubscribe function; a no-op outside a browser.
|
|
105
|
+
*/
|
|
106
|
+
subscribe(listener: (ids: string[]) => void): () => void;
|
|
107
|
+
/**
|
|
108
|
+
* Re-read storage — the source of truth, so another tab's add or remove is
|
|
109
|
+
* seen here — keeping only what this tab could not store; then drop expired
|
|
110
|
+
* entries. With no readable storage, memory is all there is.
|
|
111
|
+
*/
|
|
112
|
+
private sync;
|
|
113
|
+
private prune;
|
|
114
|
+
private persist;
|
|
74
115
|
}
|
package/dist/sign-out.js
CHANGED
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
*
|
|
12
12
|
* These helpers close that gap: {@link revokeSession} retries once and reports
|
|
13
13
|
* what the server did (including the parked session it promoted, if any), and
|
|
14
|
-
* {@link SessionTombstones} remembers the ids of sessions this
|
|
15
|
-
* so a reload never re-adopts one, even when
|
|
16
|
-
* server.
|
|
14
|
+
* {@link SessionTombstones} remembers the ids of sessions this browser signed
|
|
15
|
+
* out of so a reload — or a tab opened later — never re-adopts one, even when
|
|
16
|
+
* the revoke never reached the server.
|
|
17
17
|
*/
|
|
18
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
19
|
exports.SessionTombstones = exports.SignOutError = void 0;
|
|
@@ -79,56 +79,219 @@ async function revokeSession(request, sessionId, options = {}) {
|
|
|
79
79
|
}
|
|
80
80
|
const TOMBSTONE_KEY = 'atlas:signed-out-sessions';
|
|
81
81
|
const TOMBSTONE_LIMIT = 20;
|
|
82
|
+
/** How long a tombstone is kept: longer than any refresh cookie it guards against. */
|
|
83
|
+
const TOMBSTONE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
82
84
|
/**
|
|
83
|
-
* The ids of sessions this
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
85
|
+
* The ids of sessions this browser signed out of, so a reload — or a tab opened
|
|
86
|
+
* later — never re-adopts one from a refresh cookie the revoke did not clear.
|
|
87
|
+
*
|
|
88
|
+
* Kept in `localStorage` (shared by every tab of the site and surviving a
|
|
89
|
+
* restart), at most 20 entries, each for 30 days, pruned whenever it is read,
|
|
90
|
+
* and keyed per Frontend API origin. Tabs see each other's changes on every
|
|
91
|
+
* read, and {@link SessionTombstones.subscribe} reports them as they happen
|
|
92
|
+
* (the `storage` event). `sessionStorage` is used only when `localStorage`
|
|
93
|
+
* throws (storage disabled, sandboxed frames); memory always backs both. A
|
|
94
|
+
* confirmed revoke may {@link SessionTombstones.remove} its tombstone. Never
|
|
95
|
+
* holds a token — a session id is not a credential.
|
|
87
96
|
*/
|
|
88
97
|
class SessionTombstones {
|
|
98
|
+
entries;
|
|
99
|
+
/** Ids no storage accepted: only memory remembers them, so a sync keeps them. */
|
|
100
|
+
unsaved = new Set();
|
|
89
101
|
storageKey;
|
|
90
|
-
|
|
91
|
-
constructor(
|
|
92
|
-
|
|
93
|
-
this.
|
|
102
|
+
now;
|
|
103
|
+
constructor(options = {}) {
|
|
104
|
+
const opts = typeof options === 'string' ? { storageKey: options } : options;
|
|
105
|
+
this.storageKey = opts.storageKey ?? scopedKey(opts.frontendApi);
|
|
106
|
+
this.now = opts.now ?? Date.now;
|
|
107
|
+
const stored = readStored(this.storageKey) ?? [];
|
|
108
|
+
const legacy = readLegacy(this.storageKey, this.now()).filter((l) => !stored.some((e) => e.id === l.id));
|
|
109
|
+
this.entries = this.prune([...stored, ...legacy]);
|
|
110
|
+
// Carry an older SDK's per-tab tombstones over into the shared store, once.
|
|
111
|
+
if (legacy.length) {
|
|
112
|
+
this.persist();
|
|
113
|
+
try {
|
|
114
|
+
storage('session')?.removeItem(TOMBSTONE_KEY);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
/* sessionStorage unavailable */
|
|
118
|
+
}
|
|
119
|
+
}
|
|
94
120
|
}
|
|
95
121
|
has(sessionId) {
|
|
96
|
-
|
|
122
|
+
if (typeof sessionId !== 'string' || !sessionId)
|
|
123
|
+
return false;
|
|
124
|
+
this.sync();
|
|
125
|
+
return this.entries.some((e) => e.id === sessionId);
|
|
97
126
|
}
|
|
98
127
|
add(...sessionIds) {
|
|
128
|
+
this.sync();
|
|
99
129
|
let changed = false;
|
|
100
130
|
for (const id of sessionIds) {
|
|
101
|
-
if (typeof id !== 'string' || !id || this.
|
|
131
|
+
if (typeof id !== 'string' || !id || this.entries.some((e) => e.id === id))
|
|
102
132
|
continue;
|
|
103
|
-
this.
|
|
133
|
+
this.entries.push({ id, at: this.now() });
|
|
104
134
|
changed = true;
|
|
105
135
|
}
|
|
106
|
-
if (
|
|
136
|
+
if (changed)
|
|
137
|
+
this.persist();
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Forget tombstones — called once a later revoke of the session is confirmed
|
|
141
|
+
* (the server has ended it, so there is nothing left to re-adopt).
|
|
142
|
+
*/
|
|
143
|
+
remove(...sessionIds) {
|
|
144
|
+
this.sync();
|
|
145
|
+
const before = this.entries.length;
|
|
146
|
+
this.entries = this.entries.filter((e) => !sessionIds.includes(e.id));
|
|
147
|
+
if (this.entries.length !== before)
|
|
148
|
+
this.persist();
|
|
149
|
+
}
|
|
150
|
+
list() {
|
|
151
|
+
this.sync();
|
|
152
|
+
return this.entries.map((e) => e.id);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Call `listener` when another tab adds or removes a tombstone (the `storage`
|
|
156
|
+
* event). Returns the unsubscribe function; a no-op outside a browser.
|
|
157
|
+
*/
|
|
158
|
+
subscribe(listener) {
|
|
159
|
+
const target = globalThis;
|
|
160
|
+
if (typeof target.addEventListener !== 'function')
|
|
161
|
+
return () => undefined;
|
|
162
|
+
const onStorage = (event) => {
|
|
163
|
+
if (event.key !== this.storageKey && event.key !== null)
|
|
164
|
+
return;
|
|
165
|
+
listener(this.list());
|
|
166
|
+
};
|
|
167
|
+
target.addEventListener('storage', onStorage);
|
|
168
|
+
return () => target.removeEventListener?.('storage', onStorage);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Re-read storage — the source of truth, so another tab's add or remove is
|
|
172
|
+
* seen here — keeping only what this tab could not store; then drop expired
|
|
173
|
+
* entries. With no readable storage, memory is all there is.
|
|
174
|
+
*/
|
|
175
|
+
sync() {
|
|
176
|
+
const stored = readStored(this.storageKey);
|
|
177
|
+
if (!stored) {
|
|
178
|
+
this.entries = this.prune(this.entries);
|
|
107
179
|
return;
|
|
108
|
-
while (this.ids.length > TOMBSTONE_LIMIT)
|
|
109
|
-
this.ids.shift();
|
|
110
|
-
try {
|
|
111
|
-
globalThis.sessionStorage?.setItem(this.storageKey, JSON.stringify(this.ids));
|
|
112
|
-
}
|
|
113
|
-
catch {
|
|
114
|
-
// Private mode / storage disabled: memory still protects this tab.
|
|
115
180
|
}
|
|
181
|
+
const known = new Set(stored.map((e) => e.id));
|
|
182
|
+
this.entries = this.prune([
|
|
183
|
+
...stored,
|
|
184
|
+
...this.entries.filter((e) => this.unsaved.has(e.id) && !known.has(e.id)),
|
|
185
|
+
]);
|
|
116
186
|
}
|
|
117
|
-
|
|
118
|
-
|
|
187
|
+
prune(entries) {
|
|
188
|
+
const cutoff = this.now() - TOMBSTONE_TTL_MS;
|
|
189
|
+
const seen = new Set();
|
|
190
|
+
const fresh = entries
|
|
191
|
+
.filter((e) => e.at > cutoff && !seen.has(e.id) && seen.add(e.id))
|
|
192
|
+
.sort((a, b) => a.at - b.at);
|
|
193
|
+
return fresh.slice(-TOMBSTONE_LIMIT);
|
|
194
|
+
}
|
|
195
|
+
persist() {
|
|
196
|
+
this.entries = this.prune(this.entries);
|
|
197
|
+
const raw = JSON.stringify(this.entries);
|
|
198
|
+
if (write('local', this.storageKey, raw)) {
|
|
199
|
+
// Anything parked in sessionStorage by an earlier failed write is now in localStorage.
|
|
200
|
+
try {
|
|
201
|
+
storage('session')?.removeItem(this.storageKey);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
/* nothing parked, or sessionStorage unavailable */
|
|
205
|
+
}
|
|
206
|
+
this.unsaved.clear();
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
// localStorage threw (disabled, sandboxed, full): fall back to this tab's
|
|
210
|
+
// sessionStorage, and memory still protects this tab if that throws too.
|
|
211
|
+
this.unsaved = write('session', this.storageKey, raw) ? new Set() : new Set(this.entries.map((e) => e.id));
|
|
119
212
|
}
|
|
120
213
|
}
|
|
121
214
|
exports.SessionTombstones = SessionTombstones;
|
|
122
|
-
|
|
215
|
+
/** `atlas:signed-out-sessions:<frontend api origin>`. */
|
|
216
|
+
function scopedKey(frontendApi) {
|
|
217
|
+
if (!frontendApi)
|
|
218
|
+
return TOMBSTONE_KEY;
|
|
219
|
+
try {
|
|
220
|
+
return `${TOMBSTONE_KEY}:${new URL(frontendApi).origin}`;
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return `${TOMBSTONE_KEY}:${frontendApi}`;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function storage(kind) {
|
|
227
|
+
// Reading `localStorage` itself throws in some sandboxed contexts.
|
|
228
|
+
return kind === 'local' ? globalThis.localStorage : globalThis.sessionStorage;
|
|
229
|
+
}
|
|
230
|
+
function write(kind, key, value) {
|
|
123
231
|
try {
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
232
|
+
const s = storage(kind);
|
|
233
|
+
if (!s)
|
|
234
|
+
return false;
|
|
235
|
+
s.setItem(key, value);
|
|
236
|
+
return true;
|
|
129
237
|
}
|
|
130
238
|
catch {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function parseEntries(raw, now) {
|
|
243
|
+
if (!raw)
|
|
131
244
|
return [];
|
|
245
|
+
try {
|
|
246
|
+
const parsed = JSON.parse(raw);
|
|
247
|
+
if (!Array.isArray(parsed))
|
|
248
|
+
return [];
|
|
249
|
+
const out = [];
|
|
250
|
+
for (const v of parsed) {
|
|
251
|
+
// Bare ids are the pre-TTL format: date them now.
|
|
252
|
+
if (typeof v === 'string' && v)
|
|
253
|
+
out.push({ id: v, at: now });
|
|
254
|
+
else if (v && typeof v === 'object') {
|
|
255
|
+
const { id, at } = v;
|
|
256
|
+
if (typeof id === 'string' && id && typeof at === 'number' && Number.isFinite(at))
|
|
257
|
+
out.push({ id, at });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return out;
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
return [];
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* localStorage (plus anything a failed write parked in sessionStorage), or
|
|
268
|
+
* sessionStorage alone when localStorage throws; null when neither is readable.
|
|
269
|
+
*/
|
|
270
|
+
function readStored(key) {
|
|
271
|
+
const now = Date.now();
|
|
272
|
+
try {
|
|
273
|
+
const s = storage('local');
|
|
274
|
+
if (s)
|
|
275
|
+
return [...parseEntries(s.getItem(key), now), ...(readSession(key, now) ?? [])];
|
|
132
276
|
}
|
|
277
|
+
catch {
|
|
278
|
+
/* fall through to sessionStorage */
|
|
279
|
+
}
|
|
280
|
+
return readSession(key, now);
|
|
281
|
+
}
|
|
282
|
+
function readSession(key, now) {
|
|
283
|
+
try {
|
|
284
|
+
const s = storage('session');
|
|
285
|
+
return s ? parseEntries(s.getItem(key), now) : null;
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/** Tombstones written by earlier SDK versions: per tab, unscoped, in sessionStorage. */
|
|
292
|
+
function readLegacy(key, now) {
|
|
293
|
+
if (key === TOMBSTONE_KEY)
|
|
294
|
+
return [];
|
|
295
|
+
return readSession(TOMBSTONE_KEY, now) ?? [];
|
|
133
296
|
}
|
|
134
297
|
//# sourceMappingURL=sign-out.js.map
|
package/dist/sign-out.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sign-out.js","sourceRoot":"","sources":["../src/sign-out.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;;;AAwDH,sCAgCC;AAvED,gFAAgF;AAChF,MAAa,YAAa,SAAQ,KAAK;IAC5B,IAAI,GAAG,wBAAiC,CAAC;IAClD,6CAA6C;IACpC,SAAS,CAAS;IAC3B,gFAAgF;IACvE,MAAM,CAAS;IAExB,YAAY,SAAiB,EAAE,MAAc;QAC3C,KAAK,CACH,MAAM,KAAK,CAAC;YACV,CAAC,CAAC,yFAAyF;YAC3F,CAAC,CAAC,mFAAmF,MAAM,IAAI,CAClG,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;QAC3B,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF;AAjBD,oCAiBC;AAaD;;;;;;;GAOG;AACI,KAAK,UAAU,aAAa,CACjC,OAAgE,EAChE,SAAiB,EACjB,UAAgC,EAAE;IAElC,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC;IAClD,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,OAAO,EAAE,OAAO,EAAE,EAAE,CAAC;QACpD,IAAI,QAAkB,CAAC;QACvB,IAAI,CAAC;YACH,QAAQ,GAAG,MAAM,OAAO,CAAC,uBAAuB,kBAAkB,CAAC,SAAS,CAAC,SAAS,EAAE;gBACtF,MAAM,EAAE,MAAM;aACf,CAAC,CAAC;QACL,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,GAAG,CAAC,CAAC;YACX,SAAS;QACX,CAAC;QACD,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC;QACzB,IAAI,QAAQ,CAAC,EAAE,EAAE,CAAC;YAChB,IAAI,eAAmC,CAAC;YACxC,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,CAA2C,CAAC;gBACvF,IAAI,OAAO,IAAI,EAAE,iBAAiB,KAAK,QAAQ;oBAAE,eAAe,GAAG,IAAI,CAAC,iBAAiB,CAAC;YAC5F,CAAC;YAAC,MAAM,CAAC;gBACP,0DAA0D;YAC5D,CAAC;YACD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;QAC9F,CAAC;QACD,IAAI,MAAM,KAAK,GAAG;YAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;QACrD,IAAI,MAAM,GAAG,GAAG,IAAI,MAAM,KAAK,GAAG;YAAE,MAAM;IAC5C,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;AACpC,CAAC;AAED,MAAM,aAAa,GAAG,2BAA2B,CAAC;AAClD,MAAM,eAAe,GAAG,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"sign-out.js","sourceRoot":"","sources":["../src/sign-out.ts"],"names":[],"mappings":"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"}
|