@atlasauth/js 0.5.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 +188 -0
- package/dist/api-keys.d.ts +84 -0
- package/dist/api-keys.js +77 -0
- package/dist/api-keys.js.map +1 -0
- package/dist/captcha-controller.d.ts +95 -0
- package/dist/captcha-controller.js +205 -0
- package/dist/captcha-controller.js.map +1 -0
- package/dist/captcha-widget.d.ts +81 -0
- package/dist/captcha-widget.js +263 -0
- package/dist/captcha-widget.js.map +1 -0
- package/dist/config.d.ts +59 -0
- package/dist/config.js +146 -0
- package/dist/config.js.map +1 -0
- package/dist/frontchannel-logout.d.ts +7 -0
- package/dist/frontchannel-logout.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -1
- package/dist/instance-settings.d.ts +58 -0
- package/dist/instance-settings.js +74 -0
- package/dist/instance-settings.js.map +1 -0
- 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 +53 -4
- package/dist/redirect.js +114 -9
- package/dist/redirect.js.map +1 -1
- package/dist/sign-out.d.ts +115 -0
- package/dist/sign-out.js +297 -0
- package/dist/sign-out.js.map +1 -0
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -15,10 +15,15 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
exports.isPermissionKey = exports.isRoleKey = exports.PERMISSION_KEY_PATTERN = exports.ROLE_KEY_PATTERN = exports.ForbiddenError = exports.protectFromClaims = exports.hasFromClaims = exports.evaluate = void 0;
|
|
18
|
+
// Provider configuration checks (frontendApi) shared by every framework SDK.
|
|
19
|
+
__exportStar(require("./config"), exports);
|
|
20
|
+
__exportStar(require("./sign-out"), exports);
|
|
18
21
|
__exportStar(require("./token-cache"), exports);
|
|
19
22
|
__exportStar(require("./tab-election"), exports);
|
|
20
23
|
__exportStar(require("./attempt"), exports);
|
|
21
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);
|
|
22
27
|
__exportStar(require("./fapi"), exports);
|
|
23
28
|
__exportStar(require("./native"), exports);
|
|
24
29
|
__exportStar(require("./native-session"), exports);
|
|
@@ -27,6 +32,8 @@ __exportStar(require("./telegram"), exports);
|
|
|
27
32
|
__exportStar(require("./siwe"), exports);
|
|
28
33
|
__exportStar(require("./telemetry"), exports);
|
|
29
34
|
__exportStar(require("./reauth"), exports);
|
|
35
|
+
// End-user API key self-service (`/v1/client/me/api_keys`).
|
|
36
|
+
__exportStar(require("./api-keys"), exports);
|
|
30
37
|
__exportStar(require("./connect"), exports);
|
|
31
38
|
__exportStar(require("./password-reset"), exports);
|
|
32
39
|
__exportStar(require("./check-session"), exports);
|
|
@@ -35,6 +42,13 @@ __exportStar(require("./frontchannel-logout"), exports);
|
|
|
35
42
|
// §5 "captcha anywhere" — fetch the central widget config, render the provider
|
|
36
43
|
// widget, and verify a solved token, all without hardcoding provider keys.
|
|
37
44
|
__exportStar(require("./captcha"), exports);
|
|
45
|
+
// Per-flow captcha widget rendering shared by the prebuilt sign-in / sign-up UIs.
|
|
46
|
+
__exportStar(require("./captcha-widget"), exports);
|
|
47
|
+
// The framework-agnostic captcha lifecycle + sign-in / sign-up submit helpers the
|
|
48
|
+
// prebuilt forms of every framework SDK share, and the instance's public
|
|
49
|
+
// sign-in / sign-up settings (`/v1/appearance`) they are driven by.
|
|
50
|
+
__exportStar(require("./captcha-controller"), exports);
|
|
51
|
+
__exportStar(require("./instance-settings"), exports);
|
|
38
52
|
// Framework-agnostic authorization primitive — `has()`/`evaluate` for any
|
|
39
53
|
// non-React/Next consumer reading org_role/org_permissions off its session.
|
|
40
54
|
var authz_1 = require("@atlasauth/authz");
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,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,4CAA0B;AAC1B,mDAAiC;AACjC,kDAAgC;AAChC,sDAAsD;AACtD,wDAAsC;AACtC,+EAA+E;AAC/E,2EAA2E;AAC3E,4CAA0B;AAC1B,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,58 @@
|
|
|
1
|
+
import type { CaptchaFlowKey, FlowCaptcha } from './captcha-widget';
|
|
2
|
+
import type { FapiClient } from './fapi';
|
|
3
|
+
/**
|
|
4
|
+
* The instance's public sign-in / sign-up settings, read from
|
|
5
|
+
* `GET /v1/appearance`. Public data only — the captcha provider + site key +
|
|
6
|
+
* render mode per flow, the extra fields the create-account form collects, and
|
|
7
|
+
* the legal-consent policy. The prebuilt sign-in / sign-up components of every
|
|
8
|
+
* framework SDK read it so that turning a setting on in the dashboard (e.g.
|
|
9
|
+
* captcha on sign-up) takes effect with no code change.
|
|
10
|
+
*/
|
|
11
|
+
export interface InstanceSettings {
|
|
12
|
+
/** Per-flow captcha (`{ enabled: false }` when a flow draws nothing). */
|
|
13
|
+
captchaFlows: Partial<Record<CaptchaFlowKey, FlowCaptcha>>;
|
|
14
|
+
/** Extra fields the create-account form collects, in display order. */
|
|
15
|
+
signUpFields: PublicSignUpField[];
|
|
16
|
+
/** Terms / privacy links and whether agreement is required on sign-up. */
|
|
17
|
+
legal: {
|
|
18
|
+
termsUrl: string | null;
|
|
19
|
+
privacyUrl: string | null;
|
|
20
|
+
required: boolean;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** One configurable sign-up field, exactly as `/v1/appearance` resolves it. */
|
|
24
|
+
export interface PublicSignUpField {
|
|
25
|
+
key: string;
|
|
26
|
+
label: string;
|
|
27
|
+
type: 'text' | 'email' | 'tel' | 'url' | 'number' | 'date' | 'textarea' | 'checkbox' | 'select' | 'radio' | 'multiselect';
|
|
28
|
+
required: boolean;
|
|
29
|
+
placeholder: string | null;
|
|
30
|
+
options: string[];
|
|
31
|
+
builtin: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Read the settings out of a `/v1/appearance` body. Defensive: an older server
|
|
35
|
+
* (no `captchaFlows`) or a malformed body yields empty settings, so the
|
|
36
|
+
* components behave exactly as they did before these settings existed.
|
|
37
|
+
*/
|
|
38
|
+
export declare function instanceSettingsFrom(body: unknown): InstanceSettings;
|
|
39
|
+
/**
|
|
40
|
+
* Fetch the instance's public settings (`GET /v1/appearance`). Null when the
|
|
41
|
+
* server can't be reached or answers with an error — the components then fall
|
|
42
|
+
* back to their defaults, and the server still enforces every gate.
|
|
43
|
+
*/
|
|
44
|
+
export declare function fetchInstanceSettings(client: FapiClient): Promise<InstanceSettings | null>;
|
|
45
|
+
/**
|
|
46
|
+
* Split a create-account form's values into the `startSignUp` input: `email` and
|
|
47
|
+
* `password` are the credentials, everything else is a configured sign-up field
|
|
48
|
+
* (blank optional fields are dropped, so the server never sees empty strings).
|
|
49
|
+
* `consent` is the legal-consent tick (send `true` when no agreement is
|
|
50
|
+
* required — the server only enforces it when the instance opts in).
|
|
51
|
+
*/
|
|
52
|
+
export declare function toSignUpInput(values: Record<string, string>, consent: boolean, captchaToken?: string | null): {
|
|
53
|
+
email: string;
|
|
54
|
+
password: string;
|
|
55
|
+
fields: Record<string, string>;
|
|
56
|
+
consent: boolean;
|
|
57
|
+
captchaToken?: string;
|
|
58
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.instanceSettingsFrom = instanceSettingsFrom;
|
|
4
|
+
exports.fetchInstanceSettings = fetchInstanceSettings;
|
|
5
|
+
exports.toSignUpInput = toSignUpInput;
|
|
6
|
+
const NO_LEGAL = { termsUrl: null, privacyUrl: null, required: false };
|
|
7
|
+
const isRecord = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
8
|
+
/**
|
|
9
|
+
* Read the settings out of a `/v1/appearance` body. Defensive: an older server
|
|
10
|
+
* (no `captchaFlows`) or a malformed body yields empty settings, so the
|
|
11
|
+
* components behave exactly as they did before these settings existed.
|
|
12
|
+
*/
|
|
13
|
+
function instanceSettingsFrom(body) {
|
|
14
|
+
const root = isRecord(body) ? body : {};
|
|
15
|
+
const appearance = isRecord(root.appearance) ? root.appearance : {};
|
|
16
|
+
const captchaFlows = {};
|
|
17
|
+
if (isRecord(root.captchaFlows)) {
|
|
18
|
+
for (const key of ['signUp', 'signIn', 'guest', 'passwordReset']) {
|
|
19
|
+
const flow = root.captchaFlows[key];
|
|
20
|
+
if (!isRecord(flow))
|
|
21
|
+
continue;
|
|
22
|
+
captchaFlows[key] = {
|
|
23
|
+
enabled: flow.enabled === true,
|
|
24
|
+
...(typeof flow.provider === 'string' ? { provider: flow.provider } : {}),
|
|
25
|
+
...(typeof flow.siteKey === 'string' ? { siteKey: flow.siteKey } : {}),
|
|
26
|
+
...(flow.widget === 'invisible' || flow.widget === 'managed' || flow.widget === 'visible'
|
|
27
|
+
? { widget: flow.widget }
|
|
28
|
+
: {}),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const signUpFields = Array.isArray(appearance.signUpFields)
|
|
33
|
+
? appearance.signUpFields.filter((f) => isRecord(f) && typeof f.key === 'string' && typeof f.label === 'string')
|
|
34
|
+
: [];
|
|
35
|
+
const legalRaw = isRecord(appearance.legal) ? appearance.legal : null;
|
|
36
|
+
const legal = legalRaw
|
|
37
|
+
? {
|
|
38
|
+
termsUrl: typeof legalRaw.termsUrl === 'string' ? legalRaw.termsUrl : null,
|
|
39
|
+
privacyUrl: typeof legalRaw.privacyUrl === 'string' ? legalRaw.privacyUrl : null,
|
|
40
|
+
required: legalRaw.required === true,
|
|
41
|
+
}
|
|
42
|
+
: NO_LEGAL;
|
|
43
|
+
return { captchaFlows, signUpFields, legal };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Fetch the instance's public settings (`GET /v1/appearance`). Null when the
|
|
47
|
+
* server can't be reached or answers with an error — the components then fall
|
|
48
|
+
* back to their defaults, and the server still enforces every gate.
|
|
49
|
+
*/
|
|
50
|
+
async function fetchInstanceSettings(client) {
|
|
51
|
+
try {
|
|
52
|
+
const response = await client.get('/v1/appearance');
|
|
53
|
+
return response.ok ? instanceSettingsFrom(response.data) : null;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Split a create-account form's values into the `startSignUp` input: `email` and
|
|
61
|
+
* `password` are the credentials, everything else is a configured sign-up field
|
|
62
|
+
* (blank optional fields are dropped, so the server never sees empty strings).
|
|
63
|
+
* `consent` is the legal-consent tick (send `true` when no agreement is
|
|
64
|
+
* required — the server only enforces it when the instance opts in).
|
|
65
|
+
*/
|
|
66
|
+
function toSignUpInput(values, consent, captchaToken) {
|
|
67
|
+
const { email = '', password = '', ...rest } = values;
|
|
68
|
+
const fields = {};
|
|
69
|
+
for (const [key, value] of Object.entries(rest))
|
|
70
|
+
if (value)
|
|
71
|
+
fields[key] = value;
|
|
72
|
+
return { email, password, fields, consent, ...(captchaToken ? { captchaToken } : {}) };
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=instance-settings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instance-settings.js","sourceRoot":"","sources":["../src/instance-settings.ts"],"names":[],"mappings":"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"}
|
|
@@ -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,23 +74,66 @@ 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. */
|
|
74
88
|
blocked?: boolean;
|
|
75
89
|
authorizationUrl?: string;
|
|
90
|
+
/**
|
|
91
|
+
* Set (with `error: 'popup_closed'`) when the popup closed without posting a
|
|
92
|
+
* result but the Frontend API now reports a signed-in session — e.g. the
|
|
93
|
+
* browser severed `window.opener`, so the popup finished the sign-in itself.
|
|
94
|
+
* The caller should re-boot its session state rather than treat it as a
|
|
95
|
+
* cancel.
|
|
96
|
+
*/
|
|
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;
|
|
76
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;
|
|
77
117
|
/**
|
|
78
118
|
* Start a POPUP OAuth sign-in (GSI-style). Opens the provider authorize URL in a
|
|
79
119
|
* popup and resolves when the callback posts back the one-time ticket (complete),
|
|
80
120
|
* an error, or a continuation. Falls back to a full redirect when the popup is
|
|
81
121
|
* blocked — resolves `{ blocked:true, authorizationUrl }` and the caller
|
|
82
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.
|
|
83
135
|
*/
|
|
84
|
-
export declare function startOauthPopup(client: FapiClient, input:
|
|
85
|
-
provider: string;
|
|
86
|
-
redirectUrl: string;
|
|
87
|
-
}, handlers?: OAuthPopupHandlers): Promise<PopupStartResult>;
|
|
136
|
+
export declare function startOauthPopup(client: FapiClient, input: OAuthPopupInput, handlers?: OAuthPopupHandlers): Promise<PopupStartResult>;
|
|
88
137
|
/**
|
|
89
138
|
* Read Atlas's redirect params from a query string (`window.location.search`).
|
|
90
139
|
*
|