@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.
- package/README.md +118 -0
- package/dist/index.d.ts +116 -0
- package/dist/index.js +155 -0
- package/dist/index.js.map +1 -0
- package/dist/native-session.d.ts +46 -0
- package/dist/native-session.js +56 -0
- package/dist/native-session.js.map +1 -0
- package/dist/pkce.d.ts +34 -0
- package/dist/pkce.js +50 -0
- package/dist/pkce.js.map +1 -0
- package/dist/plugins.d.ts +80 -0
- package/dist/plugins.js +102 -0
- package/dist/plugins.js.map +1 -0
- package/dist/sign-in.d.ts +153 -0
- package/dist/sign-in.js +251 -0
- package/dist/sign-in.js.map +1 -0
- package/dist/token-store.d.ts +63 -0
- package/dist/token-store.js +70 -0
- package/dist/token-store.js.map +1 -0
- package/package.json +56 -0
|
@@ -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;
|
package/dist/plugins.js
ADDED
|
@@ -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>;
|
package/dist/sign-in.js
ADDED
|
@@ -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;
|