@atlasauth/capacitor 0.1.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.
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The Capacitor plugin surfaces this package touches — declared STRUCTURALLY so
3
+ * the package carries no hard dependency on any `@capacitor/*` module.
4
+ *
5
+ * `@capacitor/core`, `@capacitor/browser`, `@capacitor/app` and
6
+ * `@capacitor/preferences` are OPTIONAL peers the consuming app installs. So
7
+ * instead of `import { Browser } from '@capacitor/browser'`, we declare the
8
+ * minimal slice of each plugin we call and load the real module LAZILY at
9
+ * runtime (see {@link loadCapacitorPlugins}). At build time there is no
10
+ * dependency on any Capacitor package; at runtime the app's installed plugins
11
+ * are structurally compatible, and a web target with a plugin absent degrades
12
+ * gracefully rather than throwing at import.
13
+ *
14
+ * Every loader is guarded: a missing plugin resolves to `null`, never a throw,
15
+ * so the package runs in a bare browser (no Capacitor runtime), in Node tests,
16
+ * and on a device alike.
17
+ */
18
+ /** The slice of `@capacitor/browser`'s `Browser` we use. */
19
+ export interface BrowserPlugin {
20
+ /** Open `url` in the system browser (SFSafariViewController / Custom Tabs). */
21
+ open(options: {
22
+ url: string;
23
+ }): Promise<void>;
24
+ /** Dismiss the in-app browser, when the platform supports it. */
25
+ close?(): Promise<void>;
26
+ }
27
+ /** A Capacitor listener handle. `addListener` may return it directly or a promise of it. */
28
+ export interface PluginListenerHandle {
29
+ remove(): Promise<void> | void;
30
+ }
31
+ /** The `appUrlOpen` event payload from `@capacitor/app`. */
32
+ export interface AppUrlOpenEvent {
33
+ /** The full deep-link URL that reopened the app, e.g. `com.acme.app://cb?code=…`. */
34
+ url: string;
35
+ }
36
+ /** The slice of `@capacitor/app`'s `App` we use. */
37
+ export interface AppPlugin {
38
+ addListener(eventName: 'appUrlOpen', listener: (event: AppUrlOpenEvent) => void): PluginListenerHandle | Promise<PluginListenerHandle>;
39
+ }
40
+ /** The slice of `@capacitor/preferences`'s `Preferences` we use. */
41
+ export interface PreferencesPlugin {
42
+ get(options: {
43
+ key: string;
44
+ }): Promise<{
45
+ value: string | null;
46
+ }>;
47
+ set(options: {
48
+ key: string;
49
+ value: string;
50
+ }): Promise<void>;
51
+ remove(options: {
52
+ key: string;
53
+ }): Promise<void>;
54
+ }
55
+ /** The slice of `@capacitor/core`'s `Capacitor` we use. */
56
+ export interface CapacitorGlobal {
57
+ /** True on iOS/Android, false on the web target. */
58
+ isNativePlatform(): boolean;
59
+ }
60
+ /** The Capacitor plugins a flow needs; any may be absent (web, tests). */
61
+ export interface CapacitorPlugins {
62
+ Browser: BrowserPlugin | null;
63
+ App: AppPlugin | null;
64
+ Preferences: PreferencesPlugin | null;
65
+ Capacitor: CapacitorGlobal | null;
66
+ }
67
+ /**
68
+ * Lazily load the four Capacitor plugins, each guarded. A plugin that is not
69
+ * installed (or whose import fails) comes back `null`; nothing throws. Capacitor
70
+ * plugins export a named binding (`{ Browser }`, `{ App }`, …), so we read that
71
+ * off the module.
72
+ */
73
+ export declare function loadCapacitorPlugins(): Promise<CapacitorPlugins>;
74
+ /**
75
+ * Decide whether this is a native target. Prefers Capacitor's own
76
+ * `isNativePlatform()`; falls back to "no `window` ⇒ not a browser ⇒ native-ish"
77
+ * only when Capacitor is absent. On a plain web page (window present, no
78
+ * Capacitor) it returns false, so the standard redirect flow is used.
79
+ */
80
+ export declare function isNativePlatform(plugins: Pick<CapacitorPlugins, 'Capacitor'>): boolean;
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ /**
3
+ * The Capacitor plugin surfaces this package touches — declared STRUCTURALLY so
4
+ * the package carries no hard dependency on any `@capacitor/*` module.
5
+ *
6
+ * `@capacitor/core`, `@capacitor/browser`, `@capacitor/app` and
7
+ * `@capacitor/preferences` are OPTIONAL peers the consuming app installs. So
8
+ * instead of `import { Browser } from '@capacitor/browser'`, we declare the
9
+ * minimal slice of each plugin we call and load the real module LAZILY at
10
+ * runtime (see {@link loadCapacitorPlugins}). At build time there is no
11
+ * dependency on any Capacitor package; at runtime the app's installed plugins
12
+ * are structurally compatible, and a web target with a plugin absent degrades
13
+ * gracefully rather than throwing at import.
14
+ *
15
+ * Every loader is guarded: a missing plugin resolves to `null`, never a throw,
16
+ * so the package runs in a bare browser (no Capacitor runtime), in Node tests,
17
+ * and on a device alike.
18
+ */
19
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ var desc = Object.getOwnPropertyDescriptor(m, k);
22
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
23
+ desc = { enumerable: true, get: function() { return m[k]; } };
24
+ }
25
+ Object.defineProperty(o, k2, desc);
26
+ }) : (function(o, m, k, k2) {
27
+ if (k2 === undefined) k2 = k;
28
+ o[k2] = m[k];
29
+ }));
30
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
31
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
32
+ }) : function(o, v) {
33
+ o["default"] = v;
34
+ });
35
+ var __importStar = (this && this.__importStar) || (function () {
36
+ var ownKeys = function(o) {
37
+ ownKeys = Object.getOwnPropertyNames || function (o) {
38
+ var ar = [];
39
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
40
+ return ar;
41
+ };
42
+ return ownKeys(o);
43
+ };
44
+ return function (mod) {
45
+ if (mod && mod.__esModule) return mod;
46
+ var result = {};
47
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
48
+ __setModuleDefault(result, mod);
49
+ return result;
50
+ };
51
+ })();
52
+ Object.defineProperty(exports, "__esModule", { value: true });
53
+ exports.loadCapacitorPlugins = loadCapacitorPlugins;
54
+ exports.isNativePlatform = isNativePlatform;
55
+ /**
56
+ * Dynamically import a module by specifier WITHOUT a static dependency on it.
57
+ *
58
+ * The specifier is a runtime value, so TypeScript does not resolve the module at
59
+ * build time (the package typechecks with no `@capacitor/*` installed) and a
60
+ * bundler can tree-shake/ignore it. A missing or broken module resolves to
61
+ * `null` rather than throwing — the caller decides how to degrade.
62
+ */
63
+ async function loadModule(specifier) {
64
+ try {
65
+ return (await Promise.resolve(`${specifier}`).then(s => __importStar(require(s))));
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
71
+ /**
72
+ * Lazily load the four Capacitor plugins, each guarded. A plugin that is not
73
+ * installed (or whose import fails) comes back `null`; nothing throws. Capacitor
74
+ * plugins export a named binding (`{ Browser }`, `{ App }`, …), so we read that
75
+ * off the module.
76
+ */
77
+ async function loadCapacitorPlugins() {
78
+ const [browserMod, appMod, prefsMod, coreMod] = await Promise.all([
79
+ loadModule('@capacitor/browser'),
80
+ loadModule('@capacitor/app'),
81
+ loadModule('@capacitor/preferences'),
82
+ loadModule('@capacitor/core'),
83
+ ]);
84
+ return {
85
+ Browser: browserMod?.Browser ?? null,
86
+ App: appMod?.App ?? null,
87
+ Preferences: prefsMod?.Preferences ?? null,
88
+ Capacitor: coreMod?.Capacitor ?? null,
89
+ };
90
+ }
91
+ /**
92
+ * Decide whether this is a native target. Prefers Capacitor's own
93
+ * `isNativePlatform()`; falls back to "no `window` ⇒ not a browser ⇒ native-ish"
94
+ * only when Capacitor is absent. On a plain web page (window present, no
95
+ * Capacitor) it returns false, so the standard redirect flow is used.
96
+ */
97
+ function isNativePlatform(plugins) {
98
+ if (plugins.Capacitor)
99
+ return plugins.Capacitor.isNativePlatform();
100
+ return typeof window === 'undefined';
101
+ }
102
+ //# sourceMappingURL=plugins.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugins.js","sourceRoot":"","sources":["../src/plugins.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;GAgBG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwEH,oDAaC;AAQD,4CAGC;AA9CD;;;;;;;GAOG;AACH,KAAK,UAAU,UAAU,CAAI,SAAiB;IAC5C,IAAI,CAAC;QACH,OAAO,CAAC,yBAA0D,SAAS,uCAAC,CAAM,CAAC;IACrF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACI,KAAK,UAAU,oBAAoB;IACxC,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QAChE,UAAU,CAA8B,oBAAoB,CAAC;QAC7D,UAAU,CAAsB,gBAAgB,CAAC;QACjD,UAAU,CAAsC,wBAAwB,CAAC;QACzE,UAAU,CAAkC,iBAAiB,CAAC;KAC/D,CAAC,CAAC;IACH,OAAO;QACL,OAAO,EAAE,UAAU,EAAE,OAAO,IAAI,IAAI;QACpC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,IAAI;QACxB,WAAW,EAAE,QAAQ,EAAE,WAAW,IAAI,IAAI;QAC1C,SAAS,EAAE,OAAO,EAAE,SAAS,IAAI,IAAI;KACtC,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,SAAgB,gBAAgB,CAAC,OAA4C;IAC3E,IAAI,OAAO,CAAC,SAAS;QAAE,OAAO,OAAO,CAAC,SAAS,CAAC,gBAAgB,EAAE,CAAC;IACnE,OAAO,OAAO,MAAM,KAAK,WAAW,CAAC;AACvC,CAAC"}
@@ -0,0 +1,153 @@
1
+ /**
2
+ * The native-session sign-in flow for Capacitor.
3
+ *
4
+ * The shape, end to end:
5
+ *
6
+ * 1. Mint a PKCE pair + `state`, build the first-party authorize URL.
7
+ * 2. Open it in the SYSTEM browser (`@capacitor/browser`) — never an in-app
8
+ * WebView, so the user authenticates in a trusted, cookie-bearing context.
9
+ * 3. The authorize response redirects to the app's custom-scheme `redirectUri`,
10
+ * which reopens the app; `@capacitor/app` fires `appUrlOpen` with that URL.
11
+ * 4. Pull the `code` off the callback URL (after checking `state`), redeem it
12
+ * for a first-party OAuth access token (PKCE `authorization_code` grant).
13
+ * 5. Trade that access token for a cookie-free Atlas SESSION via
14
+ * {@link exchangeForSession} from `@atlasauth/js`.
15
+ *
16
+ * On the WEB target there is no deep link, so the flow falls back to the STANDARD
17
+ * redirect: it stashes the PKCE verifier + state and navigates the page to the
18
+ * authorize URL. The app finishes on the next load with
19
+ * {@link completeRedirectSignIn}, reading the stashed verifier.
20
+ *
21
+ * Every network step returns `null` — never throws — on failure, so a failed
22
+ * sign-in is "re-run the flow", mirroring `@atlasauth/js`'s native-session.
23
+ */
24
+ import { type NativeSession } from '@atlasauth/js';
25
+ import { type PkcePair } from './pkce';
26
+ import { type AppPlugin, type BrowserPlugin, type CapacitorGlobal } from './plugins';
27
+ import type { TokenStore } from './token-store';
28
+ /** The default OAuth scopes requested for a first-party native sign-in. */
29
+ export declare const DEFAULT_SCOPE = "openid profile email offline_access";
30
+ /** The storage key the transient PKCE material is stashed under on the web path. */
31
+ export declare const PKCE_STASH_KEY = "atlas.capacitor.pkce";
32
+ export interface BuildAuthorizeUrlOptions {
33
+ /** The instance's FAPI origin, e.g. `https://fapi.acme.atlasauth.net`. */
34
+ frontendApi: string;
35
+ /** The first-party native OAuth client id. */
36
+ clientId: string;
37
+ /** The app's custom-scheme callback, e.g. `com.acme.app://auth`. */
38
+ redirectUri: string;
39
+ /** base64url(SHA-256(verifier)). */
40
+ codeChallenge: string;
41
+ /** CSRF `state` echoed back on the callback. */
42
+ state: string;
43
+ /** Space-delimited scopes. @default {@link DEFAULT_SCOPE} */
44
+ scope?: string;
45
+ }
46
+ /**
47
+ * Build the first-party PKCE authorize URL hit at `{frontendApi}/oauth2/authorize`.
48
+ * `response_type=code`, S256 challenge, space-delimited scopes.
49
+ */
50
+ export declare function buildAuthorizeUrl(options: BuildAuthorizeUrlOptions): string;
51
+ /** The outcome of reading a deep-link / redirect callback URL. */
52
+ export type AuthCallback = {
53
+ code: string;
54
+ error?: undefined;
55
+ } | {
56
+ code?: undefined;
57
+ error: string;
58
+ };
59
+ /**
60
+ * Read an authorization callback URL. Returns the `code` when `state` matches the
61
+ * value we sent, an `{ error }` when the server reported one (also state-checked),
62
+ * or `null` when the URL is not our callback (a foreign deep link, or a state
63
+ * mismatch) — the `appUrlOpen` listener fires for EVERY deep link, so a non-match
64
+ * must be ignored, not treated as a failure.
65
+ */
66
+ export declare function parseAuthCallback(rawUrl: string, expectedState: string): AuthCallback | null;
67
+ export interface RedeemAuthorizationCodeOptions {
68
+ frontendApi: string;
69
+ clientId: string;
70
+ redirectUri: string;
71
+ /** The `code` from the callback URL. */
72
+ code: string;
73
+ /** The PKCE verifier that matches the challenge sent on the authorize URL. */
74
+ codeVerifier: string;
75
+ fetchImpl?: typeof fetch;
76
+ }
77
+ /**
78
+ * Redeem an authorization code for a first-party OAuth access token via the PKCE
79
+ * `authorization_code` grant at `{frontendApi}/oauth2/token`. Returns the access
80
+ * token, or `null` — never throws — on a network failure, a non-2xx, or a body
81
+ * with no `access_token`.
82
+ */
83
+ export declare function redeemAuthorizationCode(options: RedeemAuthorizationCodeOptions): Promise<string | null>;
84
+ /**
85
+ * The flow tail shared by the native and web paths: redeem the code for an OAuth
86
+ * access token, then {@link exchangeForSession} it for an Atlas {@link
87
+ * NativeSession}. `null` at either step is `null` out — re-run the flow.
88
+ */
89
+ export declare function exchangeAuthCode(options: RedeemAuthorizationCodeOptions): Promise<NativeSession | null>;
90
+ export interface SignInWithBrowserOptions {
91
+ frontendApi: string;
92
+ clientId: string;
93
+ /** The app's custom-scheme callback, e.g. `com.acme.app://auth`. */
94
+ redirectUri: string;
95
+ scope?: string;
96
+ /** `@capacitor/browser`'s `Browser`. Required on native. */
97
+ browser: BrowserPlugin | null;
98
+ /** `@capacitor/app`'s `App`. Required on native. */
99
+ app: AppPlugin | null;
100
+ /** `@capacitor/core`'s `Capacitor`, to decide native vs web. */
101
+ capacitor: CapacitorGlobal | null;
102
+ /** Where the transient PKCE material is stashed for the WEB redirect path. */
103
+ pkceStore: TokenStore;
104
+ fetchImpl?: typeof fetch;
105
+ /** Navigate the page (web path). @default `window.location.assign` */
106
+ navigate?: (url: string) => void;
107
+ /** Injectable PKCE pair (tests). @default {@link createPkcePair} */
108
+ makePkce?: () => Promise<PkcePair>;
109
+ /** Injectable state (tests). @default {@link createState} */
110
+ makeState?: () => string;
111
+ }
112
+ /**
113
+ * The outcome of {@link signInWithBrowser}. On native it carries the completed
114
+ * session; on the web redirect path it carries `pending: true` (the page is
115
+ * navigating away — the app finishes with {@link completeRedirectSignIn} on the
116
+ * next load), never a session.
117
+ */
118
+ export type SignInOutcome = {
119
+ session: NativeSession;
120
+ pending?: false;
121
+ } | {
122
+ session: null;
123
+ pending: true;
124
+ } | {
125
+ session: null;
126
+ pending?: false;
127
+ };
128
+ /**
129
+ * Run the sign-in flow. On NATIVE: open the system browser, wait for the
130
+ * `appUrlOpen` deep link, complete the exchange, resolve the session. On WEB:
131
+ * stash the PKCE verifier + state and navigate to the authorize URL (standard
132
+ * redirect) — resolves `{ session: null, pending: true }`, and the app completes
133
+ * via {@link completeRedirectSignIn}.
134
+ */
135
+ export declare function signInWithBrowser(options: SignInWithBrowserOptions): Promise<SignInOutcome>;
136
+ export interface CompleteRedirectSignInOptions {
137
+ frontendApi: string;
138
+ clientId: string;
139
+ redirectUri: string;
140
+ /** The redirect URL the web app landed on (carries `code` + `state`). */
141
+ url: string;
142
+ /** The same store {@link signInWithBrowser} stashed the PKCE material in. */
143
+ pkceStore: TokenStore;
144
+ fetchImpl?: typeof fetch;
145
+ }
146
+ /**
147
+ * Finish the WEB redirect flow. Reads the stashed PKCE verifier + state, checks
148
+ * the callback URL against them, redeems + exchanges, and clears the stash.
149
+ * Returns `null` when there is nothing to complete (no stash, not our callback,
150
+ * or a failed exchange). Call it once on app load when the URL looks like a
151
+ * callback.
152
+ */
153
+ export declare function completeRedirectSignIn(options: CompleteRedirectSignInOptions): Promise<NativeSession | null>;
@@ -0,0 +1,251 @@
1
+ "use strict";
2
+ /**
3
+ * The native-session sign-in flow for Capacitor.
4
+ *
5
+ * The shape, end to end:
6
+ *
7
+ * 1. Mint a PKCE pair + `state`, build the first-party authorize URL.
8
+ * 2. Open it in the SYSTEM browser (`@capacitor/browser`) — never an in-app
9
+ * WebView, so the user authenticates in a trusted, cookie-bearing context.
10
+ * 3. The authorize response redirects to the app's custom-scheme `redirectUri`,
11
+ * which reopens the app; `@capacitor/app` fires `appUrlOpen` with that URL.
12
+ * 4. Pull the `code` off the callback URL (after checking `state`), redeem it
13
+ * for a first-party OAuth access token (PKCE `authorization_code` grant).
14
+ * 5. Trade that access token for a cookie-free Atlas SESSION via
15
+ * {@link exchangeForSession} from `@atlasauth/js`.
16
+ *
17
+ * On the WEB target there is no deep link, so the flow falls back to the STANDARD
18
+ * redirect: it stashes the PKCE verifier + state and navigates the page to the
19
+ * authorize URL. The app finishes on the next load with
20
+ * {@link completeRedirectSignIn}, reading the stashed verifier.
21
+ *
22
+ * Every network step returns `null` — never throws — on failure, so a failed
23
+ * sign-in is "re-run the flow", mirroring `@atlasauth/js`'s native-session.
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.PKCE_STASH_KEY = exports.DEFAULT_SCOPE = void 0;
27
+ exports.buildAuthorizeUrl = buildAuthorizeUrl;
28
+ exports.parseAuthCallback = parseAuthCallback;
29
+ exports.redeemAuthorizationCode = redeemAuthorizationCode;
30
+ exports.exchangeAuthCode = exchangeAuthCode;
31
+ exports.signInWithBrowser = signInWithBrowser;
32
+ exports.completeRedirectSignIn = completeRedirectSignIn;
33
+ const js_1 = require("@atlasauth/js");
34
+ const pkce_1 = require("./pkce");
35
+ const plugins_1 = require("./plugins");
36
+ /** The default OAuth scopes requested for a first-party native sign-in. */
37
+ exports.DEFAULT_SCOPE = 'openid profile email offline_access';
38
+ /** The storage key the transient PKCE material is stashed under on the web path. */
39
+ exports.PKCE_STASH_KEY = 'atlas.capacitor.pkce';
40
+ /**
41
+ * Build the first-party PKCE authorize URL hit at `{frontendApi}/oauth2/authorize`.
42
+ * `response_type=code`, S256 challenge, space-delimited scopes.
43
+ */
44
+ function buildAuthorizeUrl(options) {
45
+ const url = new URL('/oauth2/authorize', options.frontendApi);
46
+ url.searchParams.set('response_type', 'code');
47
+ url.searchParams.set('client_id', options.clientId);
48
+ url.searchParams.set('redirect_uri', options.redirectUri);
49
+ url.searchParams.set('scope', options.scope ?? exports.DEFAULT_SCOPE);
50
+ url.searchParams.set('state', options.state);
51
+ url.searchParams.set('code_challenge', options.codeChallenge);
52
+ url.searchParams.set('code_challenge_method', 'S256');
53
+ return url.toString();
54
+ }
55
+ /**
56
+ * Read an authorization callback URL. Returns the `code` when `state` matches the
57
+ * value we sent, an `{ error }` when the server reported one (also state-checked),
58
+ * or `null` when the URL is not our callback (a foreign deep link, or a state
59
+ * mismatch) — the `appUrlOpen` listener fires for EVERY deep link, so a non-match
60
+ * must be ignored, not treated as a failure.
61
+ */
62
+ function parseAuthCallback(rawUrl, expectedState) {
63
+ let url;
64
+ try {
65
+ url = new URL(rawUrl);
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ // A custom-scheme URL may carry its params in the query OR the fragment.
71
+ const params = url.searchParams;
72
+ const state = params.get('state');
73
+ if (state !== expectedState)
74
+ return null;
75
+ const error = params.get('error');
76
+ if (error)
77
+ return { error };
78
+ const code = params.get('code');
79
+ return code ? { code } : null;
80
+ }
81
+ /**
82
+ * Redeem an authorization code for a first-party OAuth access token via the PKCE
83
+ * `authorization_code` grant at `{frontendApi}/oauth2/token`. Returns the access
84
+ * token, or `null` — never throws — on a network failure, a non-2xx, or a body
85
+ * with no `access_token`.
86
+ */
87
+ async function redeemAuthorizationCode(options) {
88
+ const doFetch = options.fetchImpl ?? fetch;
89
+ const form = new URLSearchParams({
90
+ grant_type: 'authorization_code',
91
+ client_id: options.clientId,
92
+ redirect_uri: options.redirectUri,
93
+ code: options.code,
94
+ code_verifier: options.codeVerifier,
95
+ });
96
+ let response;
97
+ try {
98
+ response = await doFetch(`${options.frontendApi}/oauth2/token`, {
99
+ method: 'POST',
100
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
101
+ body: form.toString(),
102
+ });
103
+ }
104
+ catch {
105
+ return null;
106
+ }
107
+ if (!response.ok)
108
+ return null;
109
+ let body = null;
110
+ try {
111
+ body = (await response.json());
112
+ }
113
+ catch {
114
+ return null;
115
+ }
116
+ return body && typeof body.access_token === 'string' ? body.access_token : null;
117
+ }
118
+ /**
119
+ * The flow tail shared by the native and web paths: redeem the code for an OAuth
120
+ * access token, then {@link exchangeForSession} it for an Atlas {@link
121
+ * NativeSession}. `null` at either step is `null` out — re-run the flow.
122
+ */
123
+ async function exchangeAuthCode(options) {
124
+ const accessToken = await redeemAuthorizationCode(options);
125
+ if (!accessToken)
126
+ return null;
127
+ return (0, js_1.exchangeForSession)({
128
+ baseUrl: options.frontendApi,
129
+ clientId: options.clientId,
130
+ accessToken,
131
+ fetchImpl: options.fetchImpl,
132
+ });
133
+ }
134
+ /** Resolve a listener handle whether `addListener` returned it or a promise of it. */
135
+ async function resolveHandle(handle) {
136
+ return await Promise.resolve(handle);
137
+ }
138
+ /**
139
+ * Run the sign-in flow. On NATIVE: open the system browser, wait for the
140
+ * `appUrlOpen` deep link, complete the exchange, resolve the session. On WEB:
141
+ * stash the PKCE verifier + state and navigate to the authorize URL (standard
142
+ * redirect) — resolves `{ session: null, pending: true }`, and the app completes
143
+ * via {@link completeRedirectSignIn}.
144
+ */
145
+ async function signInWithBrowser(options) {
146
+ const makePkce = options.makePkce ?? pkce_1.createPkcePair;
147
+ const makeState = options.makeState ?? pkce_1.createState;
148
+ const pkce = await makePkce();
149
+ const state = makeState();
150
+ const authorizeUrl = buildAuthorizeUrl({
151
+ frontendApi: options.frontendApi,
152
+ clientId: options.clientId,
153
+ redirectUri: options.redirectUri,
154
+ codeChallenge: pkce.challenge,
155
+ state,
156
+ scope: options.scope,
157
+ });
158
+ const native = (0, plugins_1.isNativePlatform)({ Capacitor: options.capacitor });
159
+ if (!native) {
160
+ // Web: stash the verifier + state so the next page load can finish, then
161
+ // navigate. The page is about to unload, so there is no session to return.
162
+ await options.pkceStore.setToken(JSON.stringify({ verifier: pkce.verifier, state }));
163
+ const navigate = options.navigate ??
164
+ ((url) => {
165
+ if (typeof window !== 'undefined')
166
+ window.location.assign(url);
167
+ });
168
+ navigate(authorizeUrl);
169
+ return { session: null, pending: true };
170
+ }
171
+ if (!options.browser || !options.app) {
172
+ // Native target but a required plugin is missing — nothing to open.
173
+ return { session: null };
174
+ }
175
+ const browser = options.browser;
176
+ const app = options.app;
177
+ // Register the deep-link listener BEFORE opening the browser, so a very fast
178
+ // callback cannot race ahead of the handler.
179
+ const code = await new Promise((resolve) => {
180
+ let settled = false;
181
+ let handle = null;
182
+ const finish = (value) => {
183
+ if (settled)
184
+ return;
185
+ settled = true;
186
+ void handle?.remove();
187
+ void browser.close?.();
188
+ resolve(value);
189
+ };
190
+ void resolveHandle(app.addListener('appUrlOpen', (event) => {
191
+ const parsed = parseAuthCallback(event.url, state);
192
+ if (!parsed)
193
+ return; // not our callback — ignore foreign deep links.
194
+ finish(parsed.code ?? null);
195
+ })).then((h) => {
196
+ handle = h;
197
+ // If the flow already finished during handle setup, drop the listener now.
198
+ if (settled)
199
+ void h.remove();
200
+ });
201
+ browser.open({ url: authorizeUrl }).catch(() => finish(null));
202
+ });
203
+ if (!code)
204
+ return { session: null };
205
+ const session = await exchangeAuthCode({
206
+ frontendApi: options.frontendApi,
207
+ clientId: options.clientId,
208
+ redirectUri: options.redirectUri,
209
+ code,
210
+ codeVerifier: pkce.verifier,
211
+ fetchImpl: options.fetchImpl,
212
+ });
213
+ return session ? { session } : { session: null };
214
+ }
215
+ /**
216
+ * Finish the WEB redirect flow. Reads the stashed PKCE verifier + state, checks
217
+ * the callback URL against them, redeems + exchanges, and clears the stash.
218
+ * Returns `null` when there is nothing to complete (no stash, not our callback,
219
+ * or a failed exchange). Call it once on app load when the URL looks like a
220
+ * callback.
221
+ */
222
+ async function completeRedirectSignIn(options) {
223
+ const raw = await options.pkceStore.getToken();
224
+ if (!raw)
225
+ return null;
226
+ let stash = null;
227
+ try {
228
+ stash = JSON.parse(raw);
229
+ }
230
+ catch {
231
+ return null;
232
+ }
233
+ if (!stash || typeof stash.verifier !== 'string' || typeof stash.state !== 'string') {
234
+ return null;
235
+ }
236
+ const parsed = parseAuthCallback(options.url, stash.state);
237
+ if (!parsed || !parsed.code)
238
+ return null;
239
+ const session = await exchangeAuthCode({
240
+ frontendApi: options.frontendApi,
241
+ clientId: options.clientId,
242
+ redirectUri: options.redirectUri,
243
+ code: parsed.code,
244
+ codeVerifier: stash.verifier,
245
+ fetchImpl: options.fetchImpl,
246
+ });
247
+ // Single-use: clear the stash whether or not the exchange succeeded.
248
+ await options.pkceStore.clearToken();
249
+ return session;
250
+ }
251
+ //# sourceMappingURL=sign-in.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sign-in.js","sourceRoot":"","sources":["../src/sign-in.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The Capacitor session token store.
3
+ *
4
+ * The web SDK keeps its short-lived JWT in memory and lets the browser carry the
5
+ * refresh token in an HttpOnly cookie. A Capacitor app — a WebView with no cookie
6
+ * jar against the FAPI origin — has to put the session somewhere the app chooses,
7
+ * and WHERE it lives is a security decision this SDK must not make for the app.
8
+ *
9
+ * So the SDK depends only on the small {@link TokenStore} interface below (the
10
+ * same three-method shape the `@atlasauth/js` native-session manager persists
11
+ * through) and ships two adapters: an in-memory default and a
12
+ * `@capacitor/preferences` one. `@capacitor/preferences` is an OPTIONAL peer, so
13
+ * its surface is declared structurally ({@link PreferencesPlugin}) and the app
14
+ * passes the real plugin in — the package compiles, typechecks and tests without
15
+ * it installed.
16
+ *
17
+ * ⚠️ `@capacitor/preferences` is NOT encrypted at rest (UserDefaults /
18
+ * SharedPreferences). For production, back the store with a secure-storage
19
+ * community plugin — e.g. `@capacitor-community/secure-storage` or
20
+ * `capacitor-secure-storage-plugin` — behind this same {@link TokenStore}
21
+ * interface. See {@link createPreferencesTokenStore} for the drop-in shape.
22
+ */
23
+ import type { PreferencesPlugin } from './plugins';
24
+ /**
25
+ * A single-slot token store: read, write, clear one string value. This is the
26
+ * exact contract the `@atlasauth/js` {@link NativeSessionManager} persists the
27
+ * serialized session through (its `persist` hook writes, boot reads, sign-out
28
+ * clears). Every method may be sync or async, so an in-memory store and an async
29
+ * `@capacitor/preferences` store both fit.
30
+ */
31
+ export interface TokenStore {
32
+ /** The stored value, or null when absent. */
33
+ getToken(): string | null | Promise<string | null>;
34
+ /** Persist `value`. Called on every successful sign-in / refresh. */
35
+ setToken(value: string): void | Promise<void>;
36
+ /** Drop the stored value (sign-out, or a rejected session). */
37
+ clearToken(): void | Promise<void>;
38
+ }
39
+ /** The default key the serialized native session is stored under. */
40
+ export declare const DEFAULT_SESSION_KEY = "atlas.native_session";
41
+ /**
42
+ * Build a {@link TokenStore} backed by `@capacitor/preferences`, keyed by `key`.
43
+ * The app imports the plugin and passes it in — the SDK never imports
44
+ * `@capacitor/preferences` itself, so it stays an optional peer:
45
+ *
46
+ * ```ts
47
+ * import { Preferences } from '@capacitor/preferences';
48
+ * import { createPreferencesTokenStore } from '@atlasauth/capacitor';
49
+ *
50
+ * const store = createPreferencesTokenStore(Preferences);
51
+ * ```
52
+ *
53
+ * Preferences is plaintext at rest; swap a secure-storage plugin behind this same
54
+ * interface for production (see the module header).
55
+ */
56
+ export declare function createPreferencesTokenStore(preferences: PreferencesPlugin, key?: string): TokenStore;
57
+ /**
58
+ * A process-memory {@link TokenStore}. The zero-config default so the SDK runs
59
+ * out of the box in tests and prototypes — but it does NOT survive an app
60
+ * restart, so ship {@link createPreferencesTokenStore} (or a secure-storage
61
+ * adapter) in production.
62
+ */
63
+ export declare function createInMemoryTokenStore(): TokenStore;