@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/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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"}
@@ -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
  *