@atlasauth/expo 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 ADDED
@@ -0,0 +1,143 @@
1
+ # @atlasauth/expo
2
+
3
+ The official Atlas SDK for **Expo** — the Expo-native peer of
4
+ [`@atlasauth/react`](../react) and the Expo sibling of
5
+ [`@atlasauth/react-native`](../react-native). It wraps the React provider and
6
+ re-exports the same headless hooks and control components (`useAuth`, `useUser`,
7
+ `useSession`, `SignedIn`, `SignedOut`, `<Protect>`), so auth reads identically to
8
+ the web — swapping only the one thing Expo must change: **how the session is
9
+ carried.**
10
+
11
+ The web SDK keeps the session in an HttpOnly cookie. Expo / React Native has no
12
+ cookie jar, so `@atlasauth/expo` carries the session token in a **`tokenCache`**
13
+ backed by [`expo-secure-store`](https://docs.expo.dev/versions/latest/sdk/securestore/)
14
+ (hardware-backed Keychain / Keystore) and sends it as a bearer header.
15
+
16
+ ## Install
17
+
18
+ ```sh
19
+ npx expo install @atlasauth/expo expo-secure-store
20
+ # plus, as you use them:
21
+ # expo-web-browser expo-auth-session (OAuth)
22
+ ```
23
+
24
+ `react` and `react-native` come from your Expo app. `expo-secure-store`,
25
+ `expo-web-browser` and `expo-auth-session` are all **optional peers** — install
26
+ only what you use.
27
+
28
+ ## Token cache
29
+
30
+ ```ts
31
+ // lib/tokenCache.ts
32
+ import * as SecureStore from 'expo-secure-store';
33
+ import { createSecureStoreTokenCache } from '@atlasauth/expo';
34
+
35
+ export const tokenCache = createSecureStoreTokenCache(SecureStore);
36
+ ```
37
+
38
+ The package ships a non-persistent in-memory cache so it runs out of the box in
39
+ Expo Go and tests, but **production apps must pass a persistent cache** — the
40
+ secure-store one above.
41
+
42
+ ## Provider (Expo Router)
43
+
44
+ Mount it once at the app root:
45
+
46
+ ```tsx
47
+ // app/_layout.tsx
48
+ import { Slot } from 'expo-router';
49
+ import { AtlasProvider } from '@atlasauth/expo';
50
+ import { tokenCache } from '../lib/tokenCache';
51
+
52
+ export default function RootLayout() {
53
+ return (
54
+ <AtlasProvider
55
+ publishableKey="pk_live_…"
56
+ frontendApi="https://fapi.your-instance.atlasauth.net"
57
+ tokenCache={tokenCache}
58
+ >
59
+ <Slot />
60
+ </AtlasProvider>
61
+ );
62
+ }
63
+ ```
64
+
65
+ `frontendApi` is required — the publishable key encodes the instance id but not a
66
+ host. Use `https://atlasauth.net` before you verify a custom domain, your custom
67
+ domain after.
68
+
69
+ ## Reading auth
70
+
71
+ ```tsx
72
+ import { Text } from 'react-native';
73
+ import { useAuth, useUser, SignedIn, SignedOut } from '@atlasauth/expo';
74
+
75
+ function Screen() {
76
+ const { isLoaded, isSignedIn, signOut } = useAuth();
77
+ const { user } = useUser();
78
+ if (!isLoaded) return <Text>Loading…</Text>;
79
+ return (
80
+ <>
81
+ <SignedIn>
82
+ <Text>Hello {user?.first_name}</Text>
83
+ </SignedIn>
84
+ <SignedOut>
85
+ <Text>Please sign in</Text>
86
+ </SignedOut>
87
+ </>
88
+ );
89
+ }
90
+ ```
91
+
92
+ `getToken()` from `useAuth()` returns the stored session JWT for your own API
93
+ calls; `getProviderToken(provider)` returns the user's own live token at a
94
+ connected social provider.
95
+
96
+ > **Headless only on native.** The hooks and the conditional-render controls
97
+ > (`SignedIn` / `SignedOut` / `Protect` / `AtlasLoading` / `AtlasLoaded`) render
98
+ > no host element, so they run under React Native. The prebuilt web UI
99
+ > components (`<SignIn>`, `<UserButton>`, …) render DOM and are **web-only** —
100
+ > build those screens with React Native primitives and these hooks.
101
+
102
+ ## OAuth with expo-web-browser
103
+
104
+ The provider exposes the same flow helpers as the web SDK; open the authorization
105
+ URL with `expo-web-browser` and let the provider complete the redirect. See
106
+ [`@atlasauth/react-native`](../react-native#oauth-with-expo-auth-session) for the
107
+ full pattern — it is identical here.
108
+
109
+ ## Native session (first-party OAuth → session)
110
+
111
+ A first-party app that already holds an Atlas OAuth access token can trade it for
112
+ a cookie-free session with `createNativeSessionManager` — it exchanges once, then
113
+ auto-refreshes the short-lived JWT and persists each rotated refresh token into
114
+ the secure store:
115
+
116
+ ```ts
117
+ import {
118
+ createNativeSessionManager,
119
+ exchangeForSession,
120
+ tokenStorageFor,
121
+ } from '@atlasauth/expo';
122
+ import { tokenCache } from '../lib/tokenCache';
123
+
124
+ const mgr = await createNativeSessionManager({
125
+ frontendApi: 'https://fapi.your-instance.atlasauth.net',
126
+ publishableKey: 'pk_…',
127
+ // a slot DEDICATED to the native session, separate from the provider's.
128
+ storage: tokenStorageFor(tokenCache, 'atlas.native-session'),
129
+ });
130
+
131
+ const session = await exchangeForSession({ baseUrl, clientId, accessToken });
132
+ if (session) mgr.setSession(session);
133
+
134
+ const headers = await mgr.authHeaders(); // { Authorization, 'x-publishable-key' }
135
+ ```
136
+
137
+ ## Companion packages
138
+
139
+ | Package | What it adds |
140
+ | --- | --- |
141
+ | [`@atlasauth/expo-passkeys`](../expo-passkeys) | Passkey register / sign-in via `react-native-passkey` |
142
+ | [`@atlasauth/expo-biometrics`](../expo-biometrics) | Gate a stored session behind Face ID / Touch ID |
143
+ | [`@atlasauth/expo-google-signin`](../expo-google-signin) | Native Google sign-in → Atlas id_token sign-in |
@@ -0,0 +1,35 @@
1
+ import { type ReactElement } from 'react';
2
+ import { type AtlasProviderProps as ReactAtlasProviderProps } from '@atlasauth/react';
3
+ import { type TokenCache } from './token-cache';
4
+ /**
5
+ * `<AtlasProvider>` — the Expo peer of the web provider.
6
+ *
7
+ * It does NOT re-implement the auth state machine: it wraps `@atlasauth/react`'s
8
+ * provider (so every hook and control component in this package is the exact web
9
+ * one) and swaps one thing — the transport. The web provider carries the session
10
+ * in an HttpOnly cookie; Expo has no cookie jar, so this provider injects a
11
+ * {@link createCacheFetch} that bearer-authenticates from, and persists rotated
12
+ * tokens into, an `expo-secure-store`-backed {@link TokenCache}.
13
+ *
14
+ * Expo Router friendly: mount it once at the app root (e.g. in `app/_layout.tsx`)
15
+ * and read auth anywhere with `useAuth()` / `useUser()` / `<SignedIn>` — the
16
+ * token cache, not a cookie, is what carries the session across a cold start.
17
+ */
18
+ /**
19
+ * Props mirror `@atlasauth/react`'s provider (publishableKey, frontendApi,
20
+ * appearance, localization, oauthMode, onSignedIn) and add the two Expo knobs.
21
+ * `fetchImpl` is replaced by the cache-backed fetch, so it is not forwardable.
22
+ */
23
+ export interface AtlasProviderProps extends Omit<ReactAtlasProviderProps, 'fetchImpl'> {
24
+ /**
25
+ * Where the session token is kept. Defaults to a NON-persistent in-memory
26
+ * cache — pass `createSecureStoreTokenCache(SecureStore)` in production so the
27
+ * session survives an app restart.
28
+ */
29
+ tokenCache?: TokenCache;
30
+ /** The cache key the session token is stored under. */
31
+ tokenKey?: string;
32
+ /** Injectable for tests; defaults to the platform `fetch`. */
33
+ fetchImpl?: typeof fetch;
34
+ }
35
+ export declare function AtlasProvider(props: AtlasProviderProps): ReactElement;
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AtlasProvider = AtlasProvider;
4
+ const react_1 = require("react");
5
+ const react_2 = require("@atlasauth/react");
6
+ const cache_fetch_1 = require("./cache-fetch");
7
+ const token_cache_1 = require("./token-cache");
8
+ function AtlasProvider(props) {
9
+ const { tokenCache, tokenKey = token_cache_1.DEFAULT_SESSION_KEY, fetchImpl, ...rest } = props;
10
+ // Built exactly once — the initializer runs on first render only, so the cache
11
+ // (and the default in-memory one, when no adapter is passed) survives
12
+ // re-renders rather than being re-created and losing the session every render.
13
+ const [cacheFetch] = (0, react_1.useState)(() => (0, cache_fetch_1.createCacheFetch)({
14
+ cache: tokenCache ?? (0, token_cache_1.createInMemoryTokenCache)(),
15
+ key: tokenKey,
16
+ fetchImpl: fetchImpl ?? fetch,
17
+ }));
18
+ return (0, react_1.createElement)(react_2.AtlasProvider, { ...rest, fetchImpl: cacheFetch });
19
+ }
20
+ //# sourceMappingURL=AtlasProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AtlasProvider.js","sourceRoot":"","sources":["../src/AtlasProvider.tsx"],"names":[],"mappings":";;AA6CA,sCAeC;AA5DD,iCAAmE;AACnE,4CAG0B;AAC1B,+CAAiD;AACjD,+CAIuB;AAmCvB,SAAgB,aAAa,CAAC,KAAyB;IACrD,MAAM,EAAE,UAAU,EAAE,QAAQ,GAAG,iCAAmB,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,GAAG,KAAK,CAAC;IAEjF,+EAA+E;IAC/E,sEAAsE;IACtE,+EAA+E;IAC/E,MAAM,CAAC,UAAU,CAAC,GAAG,IAAA,gBAAQ,EAAC,GAAG,EAAE,CACjC,IAAA,8BAAgB,EAAC;QACf,KAAK,EAAE,UAAU,IAAI,IAAA,sCAAwB,GAAE;QAC/C,GAAG,EAAE,QAAQ;QACb,SAAS,EAAE,SAAS,IAAI,KAAK;KAC9B,CAAC,CACH,CAAC;IAEF,OAAO,IAAA,qBAAa,EAAC,qBAAkB,EAAE,EAAE,GAAG,IAAI,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC,CAAC;AAC/E,CAAC"}
@@ -0,0 +1,27 @@
1
+ import type { TokenCache } from './token-cache';
2
+ /**
3
+ * The bridge that makes `@atlasauth/react`'s cookie-based provider work on Expo.
4
+ *
5
+ * The web provider authenticates with an HttpOnly cookie (`credentials:
6
+ * 'include'`) and reads the rotated `jwt` out of each `/v1/client*` response into
7
+ * an in-memory cache. React Native has no cookie jar, so neither half survives on
8
+ * its own:
9
+ *
10
+ * 1. REQUEST — attach the session token from the {@link TokenCache} as an
11
+ * `Authorization: Bearer` header, so an authenticated call is recognised
12
+ * without a cookie. The token is read FRESH on every call (never captured),
13
+ * so a refresh or sign-out between requests is always reflected.
14
+ * 2. RESPONSE — when a JSON body carries a fresh `jwt`, persist it back to the
15
+ * cache so the rotated session survives an app restart. The web provider
16
+ * also keeps that jwt in memory for the tab's lifetime; this adds the
17
+ * durable copy native needs. Reading the body is done on a `clone()` so the
18
+ * provider still gets to consume the original.
19
+ *
20
+ * The persist is fire-and-forget: a secure-store write failure must not break a
21
+ * live, already-parsed response.
22
+ */
23
+ export declare function createCacheFetch(opts: {
24
+ cache: TokenCache;
25
+ key: string;
26
+ fetchImpl: typeof fetch;
27
+ }): typeof fetch;
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createCacheFetch = createCacheFetch;
4
+ /**
5
+ * The bridge that makes `@atlasauth/react`'s cookie-based provider work on Expo.
6
+ *
7
+ * The web provider authenticates with an HttpOnly cookie (`credentials:
8
+ * 'include'`) and reads the rotated `jwt` out of each `/v1/client*` response into
9
+ * an in-memory cache. React Native has no cookie jar, so neither half survives on
10
+ * its own:
11
+ *
12
+ * 1. REQUEST — attach the session token from the {@link TokenCache} as an
13
+ * `Authorization: Bearer` header, so an authenticated call is recognised
14
+ * without a cookie. The token is read FRESH on every call (never captured),
15
+ * so a refresh or sign-out between requests is always reflected.
16
+ * 2. RESPONSE — when a JSON body carries a fresh `jwt`, persist it back to the
17
+ * cache so the rotated session survives an app restart. The web provider
18
+ * also keeps that jwt in memory for the tab's lifetime; this adds the
19
+ * durable copy native needs. Reading the body is done on a `clone()` so the
20
+ * provider still gets to consume the original.
21
+ *
22
+ * The persist is fire-and-forget: a secure-store write failure must not break a
23
+ * live, already-parsed response.
24
+ */
25
+ function createCacheFetch(opts) {
26
+ const { cache, key, fetchImpl } = opts;
27
+ return async (input, init) => {
28
+ const token = (await cache.getToken(key)) ?? null;
29
+ const headers = new Headers(init?.headers);
30
+ if (token)
31
+ headers.set('authorization', `Bearer ${token}`);
32
+ const response = await fetchImpl(input, { ...init, headers });
33
+ // Capture a rotated session token for persistence, without consuming the
34
+ // body the caller still needs.
35
+ const contentType = response.headers.get('content-type') ?? '';
36
+ if (response.ok && contentType.includes('application/json')) {
37
+ void (async () => {
38
+ try {
39
+ const body = (await response.clone().json());
40
+ if (typeof body.jwt === 'string' && body.jwt) {
41
+ await cache.saveToken(key, body.jwt);
42
+ }
43
+ }
44
+ catch {
45
+ // Not JSON, an empty body, or a secure-store write failure — the live
46
+ // response is unaffected.
47
+ }
48
+ })();
49
+ }
50
+ return response;
51
+ };
52
+ }
53
+ //# sourceMappingURL=cache-fetch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-fetch.js","sourceRoot":"","sources":["../src/cache-fetch.ts"],"names":[],"mappings":";;AAuBA,4CAiCC;AAtDD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,SAAgB,gBAAgB,CAAC,IAIhC;IACC,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;IAEvC,OAAO,KAAK,EAAE,KAAwB,EAAE,IAAkB,EAAqB,EAAE;QAC/E,MAAM,KAAK,GAAG,CAAC,MAAM,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC;QAClD,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC3C,IAAI,KAAK;YAAE,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,UAAU,KAAK,EAAE,CAAC,CAAC;QAE3D,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,KAAK,EAAE,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QAE9D,yEAAyE;QACzE,+BAA+B;QAC/B,MAAM,WAAW,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC;QAC/D,IAAI,QAAQ,CAAC,EAAE,IAAI,WAAW,CAAC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,CAAC;YAC5D,KAAK,CAAC,KAAK,IAAI,EAAE;gBACf,IAAI,CAAC;oBACH,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,CAAsB,CAAC;oBAClE,IAAI,OAAO,IAAI,CAAC,GAAG,KAAK,QAAQ,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;wBAC7C,MAAM,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;oBACvC,CAAC;gBACH,CAAC;gBAAC,MAAM,CAAC;oBACP,sEAAsE;oBACtE,0BAA0B;gBAC5B,CAAC;YACH,CAAC,CAAC,EAAE,CAAC;QACP,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,5 @@
1
+ export * from './token-cache';
2
+ export * from './cache-fetch';
3
+ export * from './AtlasProvider';
4
+ export * from './native-session';
5
+ export { useAuth, useUser, useSession, useSessionList, useOrganization, SignedIn, SignedOut, Protect, AtlasLoading, AtlasLoaded, } from '@atlasauth/react';
package/dist/index.js ADDED
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.AtlasLoaded = exports.AtlasLoading = exports.Protect = exports.SignedOut = exports.SignedIn = exports.useOrganization = exports.useSessionList = exports.useSession = exports.useUser = exports.useAuth = void 0;
18
+ // Expo provider (wraps @atlasauth/react, swaps the cookie transport for an
19
+ // expo-secure-store-backed token cache) + the token cache itself.
20
+ __exportStar(require("./token-cache"), exports);
21
+ __exportStar(require("./cache-fetch"), exports);
22
+ __exportStar(require("./AtlasProvider"), exports);
23
+ // Native session (first-party OAuth → cookie-free session with auto-refresh).
24
+ __exportStar(require("./native-session"), exports);
25
+ /*
26
+ * Re-export the NATIVE-SAFE React surface from @atlasauth/react, so an Expo app
27
+ * reads auth with the very same hooks and control components a web app does.
28
+ *
29
+ * Deliberately a NAMED re-export, not `export *`: the hooks and the
30
+ * conditional-render controls (`SignedIn` / `SignedOut` / `Protect`) decide
31
+ * whether their children appear and render no host element, so they run fine
32
+ * under React Native. The prebuilt UI components (`<SignIn>`, `<UserButton>`, …)
33
+ * render DOM (`<div>`, `<input>`, `<style>`) and are web-only — build those
34
+ * screens with React Native primitives and these headless hooks instead.
35
+ */
36
+ var react_1 = require("@atlasauth/react");
37
+ Object.defineProperty(exports, "useAuth", { enumerable: true, get: function () { return react_1.useAuth; } });
38
+ Object.defineProperty(exports, "useUser", { enumerable: true, get: function () { return react_1.useUser; } });
39
+ Object.defineProperty(exports, "useSession", { enumerable: true, get: function () { return react_1.useSession; } });
40
+ Object.defineProperty(exports, "useSessionList", { enumerable: true, get: function () { return react_1.useSessionList; } });
41
+ Object.defineProperty(exports, "useOrganization", { enumerable: true, get: function () { return react_1.useOrganization; } });
42
+ Object.defineProperty(exports, "SignedIn", { enumerable: true, get: function () { return react_1.SignedIn; } });
43
+ Object.defineProperty(exports, "SignedOut", { enumerable: true, get: function () { return react_1.SignedOut; } });
44
+ Object.defineProperty(exports, "Protect", { enumerable: true, get: function () { return react_1.Protect; } });
45
+ Object.defineProperty(exports, "AtlasLoading", { enumerable: true, get: function () { return react_1.AtlasLoading; } });
46
+ Object.defineProperty(exports, "AtlasLoaded", { enumerable: true, get: function () { return react_1.AtlasLoaded; } });
47
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,2EAA2E;AAC3E,kEAAkE;AAClE,gDAA8B;AAC9B,gDAA8B;AAC9B,kDAAgC;AAEhC,8EAA8E;AAC9E,mDAAiC;AAEjC;;;;;;;;;;GAUG;AACH,0CAW0B;AAVxB,gGAAA,OAAO,OAAA;AACP,gGAAA,OAAO,OAAA;AACP,mGAAA,UAAU,OAAA;AACV,uGAAA,cAAc,OAAA;AACd,wGAAA,eAAe,OAAA;AACf,iGAAA,QAAQ,OAAA;AACR,kGAAA,SAAS,OAAA;AACT,gGAAA,OAAO,OAAA;AACP,qGAAA,YAAY,OAAA;AACZ,oGAAA,WAAW,OAAA"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Native session for Expo / React Native.
3
+ *
4
+ * A thin wiring of the framework-agnostic {@link NativeSessionManager} from
5
+ * `@atlasauth/js` to this package's {@link TokenStorage} secure-store adapter.
6
+ * The core manager owns the exchange / refresh / rotation logic; this file's only
7
+ * job is to PERSIST each rotated session into `expo-secure-store` (or whatever
8
+ * adapter the app wired) and to rehydrate the manager from it on boot.
9
+ *
10
+ * The whole session is stored as one JSON string under the adapter's single
11
+ * slot — the refresh token (which rotates and must survive a restart) alongside
12
+ * the session id the refresh call needs and the current JWT. Use a storage slot
13
+ * DEDICATED to the native session (its own key), separate from the one you pass
14
+ * to `AtlasProvider`, since each slot holds one value. `tokenStorageFor(cache,
15
+ * 'atlas.native-session')` builds such a slot from the same token cache.
16
+ */
17
+ import { NativeSessionManager } from '@atlasauth/js';
18
+ import { type TokenStorage } from './token-cache';
19
+ export interface NativeSessionConfig {
20
+ /** The instance's FAPI origin, e.g. `https://fapi.acme.atlasauth.net`. */
21
+ frontendApi: string;
22
+ /** The publishable key — sent on the refresh call and in `authHeaders()`. */
23
+ publishableKey: string;
24
+ /** The first-party OAuth client id (for a later in-manager exchange). */
25
+ clientId?: string;
26
+ /**
27
+ * Where the rotated session is persisted. Defaults to a NON-persistent
28
+ * in-memory store — ship an `expo-secure-store` adapter (see {@link
29
+ * TokenStorage}) in production, dedicated to the native session.
30
+ */
31
+ storage?: TokenStorage;
32
+ /** Injectable for tests; defaults to the platform `fetch`. */
33
+ fetchImpl?: typeof fetch;
34
+ /** Injectable clock, epoch ms. Defaults to `Date.now`. */
35
+ now?: () => number;
36
+ }
37
+ /**
38
+ * Build a {@link NativeSessionManager} wired to an Expo secure store.
39
+ *
40
+ * Async because the secure store is async: it reads whatever session the adapter
41
+ * holds and seeds the manager with it, so a returning user is signed in before
42
+ * the first request. Thereafter every rotation is written straight back to the
43
+ * adapter via the manager's `persist` hook.
44
+ */
45
+ export declare function createNativeSessionManager(config: NativeSessionConfig): Promise<NativeSessionManager>;
46
+ /** Clear the persisted native session (sign-out). */
47
+ export declare function clearNativeSession(storage: TokenStorage): Promise<void>;
48
+ export { NativeSessionManager, type NativeSession } from '@atlasauth/js';
49
+ export { exchangeForSession, refreshNativeSession } from '@atlasauth/js';
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ /**
3
+ * Native session for Expo / React Native.
4
+ *
5
+ * A thin wiring of the framework-agnostic {@link NativeSessionManager} from
6
+ * `@atlasauth/js` to this package's {@link TokenStorage} secure-store adapter.
7
+ * The core manager owns the exchange / refresh / rotation logic; this file's only
8
+ * job is to PERSIST each rotated session into `expo-secure-store` (or whatever
9
+ * adapter the app wired) and to rehydrate the manager from it on boot.
10
+ *
11
+ * The whole session is stored as one JSON string under the adapter's single
12
+ * slot — the refresh token (which rotates and must survive a restart) alongside
13
+ * the session id the refresh call needs and the current JWT. Use a storage slot
14
+ * DEDICATED to the native session (its own key), separate from the one you pass
15
+ * to `AtlasProvider`, since each slot holds one value. `tokenStorageFor(cache,
16
+ * 'atlas.native-session')` builds such a slot from the same token cache.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.refreshNativeSession = exports.exchangeForSession = exports.NativeSessionManager = void 0;
20
+ exports.createNativeSessionManager = createNativeSessionManager;
21
+ exports.clearNativeSession = clearNativeSession;
22
+ const js_1 = require("@atlasauth/js");
23
+ const token_cache_1 = require("./token-cache");
24
+ /**
25
+ * Build a {@link NativeSessionManager} wired to an Expo secure store.
26
+ *
27
+ * Async because the secure store is async: it reads whatever session the adapter
28
+ * holds and seeds the manager with it, so a returning user is signed in before
29
+ * the first request. Thereafter every rotation is written straight back to the
30
+ * adapter via the manager's `persist` hook.
31
+ */
32
+ async function createNativeSessionManager(config) {
33
+ const storage = config.storage ?? (0, token_cache_1.createInMemoryStorage)();
34
+ const initial = (0, js_1.parseNativeSession)(await storage.getToken()) ?? undefined;
35
+ return new js_1.NativeSessionManager({
36
+ baseUrl: config.frontendApi,
37
+ publishableKey: config.publishableKey,
38
+ clientId: config.clientId,
39
+ initial,
40
+ fetchImpl: config.fetchImpl,
41
+ now: config.now,
42
+ // The manager calls this on every set/rotate; persist the fresh session so
43
+ // the rotated refresh token survives an app restart. Fire-and-forget — the
44
+ // adapter may be async, and a write failure must not break the live session.
45
+ persist: (session) => {
46
+ void Promise.resolve(storage.setToken((0, js_1.serializeNativeSession)(session))).catch(() => undefined);
47
+ },
48
+ });
49
+ }
50
+ /** Clear the persisted native session (sign-out). */
51
+ async function clearNativeSession(storage) {
52
+ await storage.clearToken();
53
+ }
54
+ var js_2 = require("@atlasauth/js");
55
+ Object.defineProperty(exports, "NativeSessionManager", { enumerable: true, get: function () { return js_2.NativeSessionManager; } });
56
+ var js_3 = require("@atlasauth/js");
57
+ Object.defineProperty(exports, "exchangeForSession", { enumerable: true, get: function () { return js_3.exchangeForSession; } });
58
+ Object.defineProperty(exports, "refreshNativeSession", { enumerable: true, get: function () { return js_3.refreshNativeSession; } });
59
+ //# sourceMappingURL=native-session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native-session.js","sourceRoot":"","sources":["../src/native-session.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;;;AAqCH,gEAsBC;AAGD,gDAEC;AA9DD,sCAKuB;AACvB,+CAAyE;AAqBzE;;;;;;;GAOG;AACI,KAAK,UAAU,0BAA0B,CAC9C,MAA2B;IAE3B,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,IAAI,IAAA,mCAAqB,GAAE,CAAC;IAC1D,MAAM,OAAO,GAAG,IAAA,uBAAkB,EAAC,MAAM,OAAO,CAAC,QAAQ,EAAE,CAAC,IAAI,SAAS,CAAC;IAE1E,OAAO,IAAI,yBAAoB,CAAC;QAC9B,OAAO,EAAE,MAAM,CAAC,WAAW;QAC3B,cAAc,EAAE,MAAM,CAAC,cAAc;QACrC,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,OAAO;QACP,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,GAAG,EAAE,MAAM,CAAC,GAAG;QACf,2EAA2E;QAC3E,2EAA2E;QAC3E,6EAA6E;QAC7E,OAAO,EAAE,CAAC,OAAsB,EAAE,EAAE;YAClC,KAAK,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAA,2BAAsB,EAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAC3E,GAAG,EAAE,CAAC,SAAS,CAChB,CAAC;QACJ,CAAC;KACF,CAAC,CAAC;AACL,CAAC;AAED,qDAAqD;AAC9C,KAAK,UAAU,kBAAkB,CAAC,OAAqB;IAC5D,MAAM,OAAO,CAAC,UAAU,EAAE,CAAC;AAC7B,CAAC;AAED,oCAAyE;AAAhE,0GAAA,oBAAoB,OAAA;AAC7B,oCAAyE;AAAhE,wGAAA,kBAAkB,OAAA;AAAE,0GAAA,oBAAoB,OAAA"}
@@ -0,0 +1,89 @@
1
+ /**
2
+ * The Expo session token cache.
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. Expo / React Native has neither a cookie
6
+ * jar nor `localStorage`, so the session token has to live somewhere the app
7
+ * chooses — and where it lives is a security decision this SDK must not make for
8
+ * the app. A token in AsyncStorage is readable by any code in the process; a
9
+ * token in `expo-secure-store` is hardware-backed (Keychain / Keystore).
10
+ *
11
+ * So the SDK depends only on the small {@link TokenCache} interface below and
12
+ * ships an in-memory default plus an `expo-secure-store` adapter. The
13
+ * `expo-secure-store` module is an OPTIONAL peer, so its surface is declared
14
+ * structurally ({@link SecureStore}) and the app passes the real module in — the
15
+ * package compiles, typechecks and tests without `expo-secure-store` installed.
16
+ */
17
+ /**
18
+ * A key→token cache, the shape `<AtlasProvider tokenCache>` and the native
19
+ * session wiring both consume. Mirrors Clerk's Expo `tokenCache` contract
20
+ * (`getToken` / `saveToken` / `clearToken`). Every method may be sync or async,
21
+ * so the same adapter fits an async `expo-secure-store` and a synchronous MMKV
22
+ * store alike.
23
+ */
24
+ export interface TokenCache {
25
+ /** The token stored under `key`, or null/undefined when absent. */
26
+ getToken(key: string): string | null | undefined | Promise<string | null | undefined>;
27
+ /** Persist `value` under `key`. Called on every successful sign-in / refresh. */
28
+ saveToken(key: string, value: string): void | Promise<void>;
29
+ /** Drop whatever is stored under `key` (sign-out, or a rejected session). */
30
+ clearToken(key: string): void | Promise<void>;
31
+ }
32
+ /**
33
+ * The slice of `expo-secure-store` this package uses, declared structurally so
34
+ * the package needs no dependency on it. The real module (and a fake in tests)
35
+ * is structurally compatible.
36
+ *
37
+ * Every method is optional-arg-compatible with `expo-secure-store`'s
38
+ * `SecureStoreOptions` (keychain accessibility, service, etc.), which the app
39
+ * may pass through via {@link SecureStoreTokenCacheOptions.secureStoreOptions}.
40
+ */
41
+ export interface SecureStore {
42
+ getItemAsync(key: string, options?: unknown): Promise<string | null>;
43
+ setItemAsync(key: string, value: string, options?: unknown): Promise<void>;
44
+ deleteItemAsync(key: string, options?: unknown): Promise<void>;
45
+ }
46
+ export interface SecureStoreTokenCacheOptions {
47
+ /**
48
+ * Pass-through `SecureStoreOptions` (e.g. `{ keychainAccessible:
49
+ * SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY }`). Forwarded verbatim to every
50
+ * `expo-secure-store` call.
51
+ */
52
+ secureStoreOptions?: unknown;
53
+ }
54
+ /**
55
+ * Build a {@link TokenCache} backed by `expo-secure-store` (hardware-backed
56
+ * Keychain / Keystore). The app imports the module and passes it in — the SDK
57
+ * never imports `expo-secure-store` itself, so it stays an optional peer:
58
+ *
59
+ * ```ts
60
+ * import * as SecureStore from 'expo-secure-store';
61
+ * import { createSecureStoreTokenCache } from '@atlasauth/expo';
62
+ *
63
+ * export const tokenCache = createSecureStoreTokenCache(SecureStore);
64
+ * ```
65
+ */
66
+ export declare function createSecureStoreTokenCache(store: SecureStore, options?: SecureStoreTokenCacheOptions): TokenCache;
67
+ /**
68
+ * A process-memory token cache. The zero-config default so the SDK runs out of
69
+ * the box in tests, prototypes and Expo Go — but it does NOT survive an app
70
+ * restart, so ship {@link createSecureStoreTokenCache} in production.
71
+ */
72
+ export declare function createInMemoryTokenCache(): TokenCache;
73
+ /**
74
+ * Adapt a single-slot {@link TokenCache} (plus a fixed key) to the three-method
75
+ * {@link TokenStorage} the native-session manager wants. The native session is
76
+ * kept under its OWN key, separate from the `AtlasProvider` session, because the
77
+ * adapter holds one value per slot.
78
+ */
79
+ export interface TokenStorage {
80
+ getToken(): string | null | Promise<string | null>;
81
+ setToken(token: string): void | Promise<void>;
82
+ clearToken(): void | Promise<void>;
83
+ }
84
+ /** View a {@link TokenCache} + one key as a single-slot {@link TokenStorage}. */
85
+ export declare function tokenStorageFor(cache: TokenCache, key: string): TokenStorage;
86
+ /** A process-memory {@link TokenStorage}. Non-persistent — see note above. */
87
+ export declare function createInMemoryStorage(): TokenStorage;
88
+ /** The default cache key the `<AtlasProvider>` session is stored under. */
89
+ export declare const DEFAULT_SESSION_KEY = "atlas.session";
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ /**
3
+ * The Expo session token cache.
4
+ *
5
+ * The web SDK keeps its short-lived JWT in memory and lets the browser carry the
6
+ * refresh token in an HttpOnly cookie. Expo / React Native has neither a cookie
7
+ * jar nor `localStorage`, so the session token has to live somewhere the app
8
+ * chooses — and where it lives is a security decision this SDK must not make for
9
+ * the app. A token in AsyncStorage is readable by any code in the process; a
10
+ * token in `expo-secure-store` is hardware-backed (Keychain / Keystore).
11
+ *
12
+ * So the SDK depends only on the small {@link TokenCache} interface below and
13
+ * ships an in-memory default plus an `expo-secure-store` adapter. The
14
+ * `expo-secure-store` module is an OPTIONAL peer, so its surface is declared
15
+ * structurally ({@link SecureStore}) and the app passes the real module in — the
16
+ * package compiles, typechecks and tests without `expo-secure-store` installed.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.DEFAULT_SESSION_KEY = void 0;
20
+ exports.createSecureStoreTokenCache = createSecureStoreTokenCache;
21
+ exports.createInMemoryTokenCache = createInMemoryTokenCache;
22
+ exports.tokenStorageFor = tokenStorageFor;
23
+ exports.createInMemoryStorage = createInMemoryStorage;
24
+ /**
25
+ * Build a {@link TokenCache} backed by `expo-secure-store` (hardware-backed
26
+ * Keychain / Keystore). The app imports the module and passes it in — the SDK
27
+ * never imports `expo-secure-store` itself, so it stays an optional peer:
28
+ *
29
+ * ```ts
30
+ * import * as SecureStore from 'expo-secure-store';
31
+ * import { createSecureStoreTokenCache } from '@atlasauth/expo';
32
+ *
33
+ * export const tokenCache = createSecureStoreTokenCache(SecureStore);
34
+ * ```
35
+ */
36
+ function createSecureStoreTokenCache(store, options = {}) {
37
+ const opts = options.secureStoreOptions;
38
+ return {
39
+ getToken: (key) => store.getItemAsync(key, opts),
40
+ saveToken: (key, value) => store.setItemAsync(key, value, opts),
41
+ clearToken: (key) => store.deleteItemAsync(key, opts),
42
+ };
43
+ }
44
+ /**
45
+ * A process-memory token cache. The zero-config default so the SDK runs out of
46
+ * the box in tests, prototypes and Expo Go — but it does NOT survive an app
47
+ * restart, so ship {@link createSecureStoreTokenCache} in production.
48
+ */
49
+ function createInMemoryTokenCache() {
50
+ const map = new Map();
51
+ return {
52
+ getToken: (key) => map.get(key) ?? null,
53
+ saveToken: (key, value) => {
54
+ map.set(key, value);
55
+ },
56
+ clearToken: (key) => {
57
+ map.delete(key);
58
+ },
59
+ };
60
+ }
61
+ /** View a {@link TokenCache} + one key as a single-slot {@link TokenStorage}. */
62
+ function tokenStorageFor(cache, key) {
63
+ return {
64
+ getToken: async () => (await cache.getToken(key)) ?? null,
65
+ setToken: (token) => cache.saveToken(key, token),
66
+ clearToken: () => cache.clearToken(key),
67
+ };
68
+ }
69
+ /** A process-memory {@link TokenStorage}. Non-persistent — see note above. */
70
+ function createInMemoryStorage() {
71
+ let token = null;
72
+ return {
73
+ getToken: () => token,
74
+ setToken: (next) => {
75
+ token = next;
76
+ },
77
+ clearToken: () => {
78
+ token = null;
79
+ },
80
+ };
81
+ }
82
+ /** The default cache key the `<AtlasProvider>` session is stored under. */
83
+ exports.DEFAULT_SESSION_KEY = 'atlas.session';
84
+ //# sourceMappingURL=token-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-cache.js","sourceRoot":"","sources":["../src/token-cache.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;;;AAsDH,kEAUC;AAOD,4DAWC;AAeD,0CAMC;AAGD,sDAWC;AA3ED;;;;;;;;;;;GAWG;AACH,SAAgB,2BAA2B,CACzC,KAAkB,EAClB,UAAwC,EAAE;IAE1C,MAAM,IAAI,GAAG,OAAO,CAAC,kBAAkB,CAAC;IACxC,OAAO;QACL,QAAQ,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,KAAK,CAAC,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC;QAChD,SAAS,EAAE,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,YAAY,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC;QAC/D,UAAU,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,KAAK,CAAC,eAAe,CAAC,GAAG,EAAE,IAAI,CAAC;KACtD,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,SAAgB,wBAAwB;IACtC,MAAM,GAAG,GAAG,IAAI,GAAG,EAAkB,CAAC;IACtC,OAAO;QACL,QAAQ,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI;QACvC,SAAS,EAAE,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE;YACxB,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACtB,CAAC;QACD,UAAU,EAAE,CAAC,GAAG,EAAE,EAAE;YAClB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAClB,CAAC;KACF,CAAC;AACJ,CAAC;AAcD,iFAAiF;AACjF,SAAgB,eAAe,CAAC,KAAiB,EAAE,GAAW;IAC5D,OAAO;QACL,QAAQ,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,IAAI;QACzD,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE,KAAK,CAAC;QAChD,UAAU,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC;KACxC,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,SAAgB,qBAAqB;IACnC,IAAI,KAAK,GAAkB,IAAI,CAAC;IAChC,OAAO;QACL,QAAQ,EAAE,GAAG,EAAE,CAAC,KAAK;QACrB,QAAQ,EAAE,CAAC,IAAY,EAAE,EAAE;YACzB,KAAK,GAAG,IAAI,CAAC;QACf,CAAC;QACD,UAAU,EAAE,GAAG,EAAE;YACf,KAAK,GAAG,IAAI,CAAC;QACf,CAAC;KACF,CAAC;AACJ,CAAC;AAED,2EAA2E;AAC9D,QAAA,mBAAmB,GAAG,eAAe,CAAC"}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@atlasauth/expo",
3
+ "version": "0.1.0",
4
+ "main": "./dist/index.js",
5
+ "types": "./dist/index.d.ts",
6
+ "dependencies": {
7
+ "@atlasauth/js": "0.2.0",
8
+ "@atlasauth/react": "0.1.30"
9
+ },
10
+ "peerDependencies": {
11
+ "react": ">=18",
12
+ "react-native": ">=0.73",
13
+ "expo-secure-store": ">=12",
14
+ "expo-web-browser": ">=12",
15
+ "expo-auth-session": ">=5"
16
+ },
17
+ "peerDependenciesMeta": {
18
+ "react": {
19
+ "optional": true
20
+ },
21
+ "react-native": {
22
+ "optional": true
23
+ },
24
+ "expo-secure-store": {
25
+ "optional": true
26
+ },
27
+ "expo-web-browser": {
28
+ "optional": true
29
+ },
30
+ "expo-auth-session": {
31
+ "optional": true
32
+ }
33
+ },
34
+ "devDependencies": {
35
+ "@types/react": "^18.3.12",
36
+ "react": "^18.3.1",
37
+ "typescript": "^5.6.3",
38
+ "vitest": "^2.1.4"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "files": [
44
+ "dist",
45
+ "README.md"
46
+ ],
47
+ "scripts": {
48
+ "build": "tsc -p tsconfig.build.json",
49
+ "typecheck": "tsc -p tsconfig.json --noEmit",
50
+ "test": "vitest run"
51
+ }
52
+ }