@lunora/react-native 1.0.0-alpha.90 → 1.0.0-alpha.91
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
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
The same live hooks you use on the web — `useQuery`, `useMutation`,
|
|
14
14
|
`useSubscription`, `useAuth`, `usePresence`, … — running on your phone, plus the
|
|
15
15
|
two seams a native app needs that a browser gives you for free: a durable offline
|
|
16
|
-
queue backed by `AsyncStorage`, and credentialed requests (
|
|
17
|
-
|
|
16
|
+
queue backed by `AsyncStorage`, and credentialed requests (the session is a
|
|
17
|
+
bearer, attached explicitly).
|
|
18
18
|
|
|
19
19
|
This package **re-exports the whole `@lunora/react` surface** (see
|
|
20
20
|
[below](#re-exported-lunorareact-surface)), so you import your hooks and
|
|
@@ -90,10 +90,9 @@ other clients write, and `send` is optimistic and offline-safe.
|
|
|
90
90
|
|
|
91
91
|
## Authentication (better-auth + Expo)
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
CSRF guard rejects on an `Origin`-less native request (see [Why a bearer token](#why-a-bearer-token)).
|
|
93
|
+
The session is sent as a **bearer** token: the HTTP RPC carries it in the
|
|
94
|
+
`Authorization` header and the live socket carries it in the `?token=` query
|
|
95
|
+
param (see [Why a bearer token](#why-a-bearer-token)).
|
|
97
96
|
|
|
98
97
|
```tsx
|
|
99
98
|
// auth.ts
|
|
@@ -182,11 +181,22 @@ import { expo } from "@better-auth/expo";
|
|
|
182
181
|
|
|
183
182
|
Lunora's runtime enables a CSRF Origin-check by default — it rejects any
|
|
184
183
|
state-changing HTTP request or WebSocket upgrade that carries a `Cookie` but no
|
|
185
|
-
trusted `Origin`. React Native sends no `Origin`, so a cookie-based credential
|
|
186
|
-
|
|
187
|
-
|
|
184
|
+
trusted `Origin`. React Native sends no `Origin`, so a cookie-based credential is
|
|
185
|
+
**rejected** once signed in. A bearer token is the credential instead, and it
|
|
186
|
+
works identically on `react-native-web` (the browser lets you set
|
|
188
187
|
`Authorization`, and the token rides `?token=` on the socket).
|
|
189
188
|
|
|
189
|
+
Using a bearer does **not** by itself guarantee the absence of a `Cookie` header.
|
|
190
|
+
React Native has a real cookie jar — `fetch` is backed by the platform HTTP stack
|
|
191
|
+
(`NSURLSession` / `OkHttp`) and its shared cookie store — so the `Set-Cookie` a
|
|
192
|
+
better-auth sign-in returns is kept and re-attached to later requests, and the
|
|
193
|
+
guard then 403s every state-changing RPC with `FORBIDDEN_ORIGIN`. It is
|
|
194
|
+
intermittent (it depends on whether the jar holds the cookie that launch), and
|
|
195
|
+
`security.csrf.trustedOrigins` cannot fix it: the trust list is only consulted
|
|
196
|
+
for an `Origin` that was actually received, and a missing one is rejected
|
|
197
|
+
outright. `createLunoraClient` closes this by sending `credentials: "omit"` on
|
|
198
|
+
every request; if you pass your own `fetch`, wrap it in `withoutAmbientCookies`.
|
|
199
|
+
|
|
190
200
|
### TanStack Query focus / online managers
|
|
191
201
|
|
|
192
202
|
React Native doesn't fire the browser's `focus` / `online` events, so Query
|
package/dist/index.d.mts
CHANGED
|
@@ -3,15 +3,15 @@ export * from '@lunora/react';
|
|
|
3
3
|
/**
|
|
4
4
|
* A `() => headers` factory the React Native client threads onto every HTTP RPC
|
|
5
5
|
* request *and* the WebSocket upgrade — a generic escape hatch for attaching a
|
|
6
|
-
* **custom** credential header (an API-gateway key, a proxy token, …)
|
|
7
|
-
*
|
|
8
|
-
* (or an empty object) when there's nothing to attach.
|
|
6
|
+
* **custom** credential header (an API-gateway key, a proxy token, …). Return
|
|
7
|
+
* `undefined` (or an empty object) when there's nothing to attach.
|
|
9
8
|
*
|
|
10
9
|
* For better-auth Expo sessions, prefer a **bearer** token instead: read it with
|
|
11
10
|
* `@lunora/react-native/auth`'s `expoBearerToken` and feed it to
|
|
12
|
-
* `client.setAuthToken` / `setWsToken` (see the package README).
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* `client.setAuthToken` / `setWsToken` (see the package README). Either way the
|
|
12
|
+
* client sends `credentials: "omit"`, so the platform cookie jar never attaches a
|
|
13
|
+
* session cookie the runtime's CSRF guard would reject on an `Origin`-less native
|
|
14
|
+
* request.
|
|
15
15
|
* @experimental
|
|
16
16
|
*/
|
|
17
17
|
type AuthHeadersFactory = () => Record<string, string> | undefined;
|
|
@@ -59,6 +59,51 @@ interface CreateLunoraClientOptions extends LunoraClientOptions {
|
|
|
59
59
|
*/
|
|
60
60
|
storage?: AsyncStorageLike;
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Wrap a `fetch` so no request carries the platform's ambient cookie credential.
|
|
64
|
+
*
|
|
65
|
+
* React Native **does** have a cookie jar — its `fetch` is backed by the
|
|
66
|
+
* platform HTTP stack (`NSURLSession` / `OkHttp`), which owns a shared,
|
|
67
|
+
* persistent cookie store — so a `Set-Cookie` from a better-auth sign-in is kept
|
|
68
|
+
* and re-attached to later requests automatically. The package's own docs used
|
|
69
|
+
* to assert the opposite, and the bearer design leaned on it: "there is no jar,
|
|
70
|
+
* therefore no `Cookie` header, therefore the runtime's CSRF guard never sees
|
|
71
|
+
* one".
|
|
72
|
+
*
|
|
73
|
+
* It does see one. The guard rejects an unsafe, cookie-bearing request whose
|
|
74
|
+
* `Origin` is missing or untrusted, and a native request sends no `Origin` — so
|
|
75
|
+
* every state-changing RPC 403s with `FORBIDDEN_ORIGIN` for as long as the jar
|
|
76
|
+
* holds that cookie. `security.csrf.trustedOrigins` cannot fix it: the trust list
|
|
77
|
+
* is only consulted for an `Origin` that was actually received, and a missing one
|
|
78
|
+
* is rejected outright. It is also intermittent — it depends on whether the jar
|
|
79
|
+
* happens to hold the session cookie that launch — which is what let it hide.
|
|
80
|
+
*
|
|
81
|
+
* `credentials: "omit"` is the fix, and it costs nothing on native: the session
|
|
82
|
+
* is a bearer there (`setAuthToken` / `setWsToken`), so the cookie was never the
|
|
83
|
+
* credential — only an accident of transport riding along. Wrapped INNERMOST by
|
|
84
|
+
* {@link createLunoraClient}, so it runs after every other layer and also
|
|
85
|
+
* overrides the `credentials: "include"` `@lunora/client` sends on its
|
|
86
|
+
* `get-session` probe.
|
|
87
|
+
*
|
|
88
|
+
* **Scope.** This covers `fetch`, which is the whole HTTP RPC/REST/storage
|
|
89
|
+
* surface. It does NOT cover the WebSocket upgrade — React Native attaches the
|
|
90
|
+
* same jar's cookie to the handshake and exposes no per-socket opt-out — nor
|
|
91
|
+
* `httpStream` imported standalone from `@lunora/client`, which resolves
|
|
92
|
+
* `globalThis.fetch` itself. Neither is a live 403 today: React Native sends an
|
|
93
|
+
* `Origin` equal to the server's on a WS handshake, and better-auth's `bearer`
|
|
94
|
+
* plugin overwrites the session cookie with the bearer rather than deferring to
|
|
95
|
+
* it. Both are noted so the guarantee is not read wider than it is.
|
|
96
|
+
*
|
|
97
|
+
* {@link createLunoraClient} applies it only on native — see
|
|
98
|
+
* {@link isNativeRuntime} — and only to the global `fetch`. Under
|
|
99
|
+
* `react-native-web` the jar is the browser's, `Origin` IS sent, the CSRF guard
|
|
100
|
+
* never fires, and a cookie session is a legitimate setup that this would
|
|
101
|
+
* silently sign out (`getCurrentUser` deliberately sends `credentials:
|
|
102
|
+
* "include"`). A caller-supplied `fetch` is never wrapped either, so a
|
|
103
|
+
* cookie-forwarding SSR transport keeps its credentials; apply this yourself if
|
|
104
|
+
* you want it.
|
|
105
|
+
*/
|
|
106
|
+
declare const withoutAmbientCookies: (fetchImpl: typeof fetch) => typeof fetch;
|
|
62
107
|
/**
|
|
63
108
|
* Construct a `LunoraClient` tuned for React Native / Expo — a thin wrapper over
|
|
64
109
|
* `new LunoraClient(options)` that fills in the three things a browser gets for
|
|
@@ -75,17 +120,21 @@ interface CreateLunoraClientOptions extends LunoraClientOptions {
|
|
|
75
120
|
* after a restart while the socket reconnects — mirroring the browser's
|
|
76
121
|
* IndexedDB-backed default.
|
|
77
122
|
*
|
|
78
|
-
* Third, credentialed requests:
|
|
79
|
-
*
|
|
80
|
-
*
|
|
123
|
+
* Third, credentialed requests: the session is a bearer on this platform, so the
|
|
124
|
+
* returned `fetch` is wrapped in {@link withoutAmbientCookies} (React Native's
|
|
125
|
+
* cookie jar is real, and a stray session cookie 403s every state-changing RPC —
|
|
126
|
+
* see there), and passing `getAuthHeaders` additionally rides its headers on both
|
|
127
|
+
* the HTTP RPC path and the WebSocket upgrade.
|
|
81
128
|
*
|
|
82
129
|
* Everything on `LunoraClientOptions` is still accepted and passed through; an
|
|
83
|
-
* explicit `persistence`, `queryCache`, `fetch`, or `WebSocket`
|
|
84
|
-
*
|
|
130
|
+
* explicit `persistence`, `queryCache`, `fetch`, or `WebSocket` replaces the
|
|
131
|
+
* transport derived from `storage`. A `getAuthHeaders` factory is layered over a
|
|
132
|
+
* caller-supplied `fetch` rather than ignored — the previous behaviour dropped
|
|
133
|
+
* the credential on HTTP while still injecting it on the WS upgrade. `persistence`
|
|
85
134
|
* and `queryCache` override independently — `storage` backs both, so opting out
|
|
86
135
|
* of one leaves the other wired. See the package README
|
|
87
136
|
* for a full setup example.
|
|
88
137
|
* @experimental
|
|
89
138
|
*/
|
|
90
139
|
declare const createLunoraClient: (options: CreateLunoraClientOptions) => LunoraClient;
|
|
91
|
-
export { type AuthHeadersFactory, type CreateLunoraClientOptions, createLunoraClient };
|
|
140
|
+
export { type AuthHeadersFactory, type CreateLunoraClientOptions, createLunoraClient, withoutAmbientCookies };
|
package/dist/index.d.ts
CHANGED
|
@@ -3,15 +3,15 @@ export * from '@lunora/react';
|
|
|
3
3
|
/**
|
|
4
4
|
* A `() => headers` factory the React Native client threads onto every HTTP RPC
|
|
5
5
|
* request *and* the WebSocket upgrade — a generic escape hatch for attaching a
|
|
6
|
-
* **custom** credential header (an API-gateway key, a proxy token, …)
|
|
7
|
-
*
|
|
8
|
-
* (or an empty object) when there's nothing to attach.
|
|
6
|
+
* **custom** credential header (an API-gateway key, a proxy token, …). Return
|
|
7
|
+
* `undefined` (or an empty object) when there's nothing to attach.
|
|
9
8
|
*
|
|
10
9
|
* For better-auth Expo sessions, prefer a **bearer** token instead: read it with
|
|
11
10
|
* `@lunora/react-native/auth`'s `expoBearerToken` and feed it to
|
|
12
|
-
* `client.setAuthToken` / `setWsToken` (see the package README).
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* `client.setAuthToken` / `setWsToken` (see the package README). Either way the
|
|
12
|
+
* client sends `credentials: "omit"`, so the platform cookie jar never attaches a
|
|
13
|
+
* session cookie the runtime's CSRF guard would reject on an `Origin`-less native
|
|
14
|
+
* request.
|
|
15
15
|
* @experimental
|
|
16
16
|
*/
|
|
17
17
|
type AuthHeadersFactory = () => Record<string, string> | undefined;
|
|
@@ -59,6 +59,51 @@ interface CreateLunoraClientOptions extends LunoraClientOptions {
|
|
|
59
59
|
*/
|
|
60
60
|
storage?: AsyncStorageLike;
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Wrap a `fetch` so no request carries the platform's ambient cookie credential.
|
|
64
|
+
*
|
|
65
|
+
* React Native **does** have a cookie jar — its `fetch` is backed by the
|
|
66
|
+
* platform HTTP stack (`NSURLSession` / `OkHttp`), which owns a shared,
|
|
67
|
+
* persistent cookie store — so a `Set-Cookie` from a better-auth sign-in is kept
|
|
68
|
+
* and re-attached to later requests automatically. The package's own docs used
|
|
69
|
+
* to assert the opposite, and the bearer design leaned on it: "there is no jar,
|
|
70
|
+
* therefore no `Cookie` header, therefore the runtime's CSRF guard never sees
|
|
71
|
+
* one".
|
|
72
|
+
*
|
|
73
|
+
* It does see one. The guard rejects an unsafe, cookie-bearing request whose
|
|
74
|
+
* `Origin` is missing or untrusted, and a native request sends no `Origin` — so
|
|
75
|
+
* every state-changing RPC 403s with `FORBIDDEN_ORIGIN` for as long as the jar
|
|
76
|
+
* holds that cookie. `security.csrf.trustedOrigins` cannot fix it: the trust list
|
|
77
|
+
* is only consulted for an `Origin` that was actually received, and a missing one
|
|
78
|
+
* is rejected outright. It is also intermittent — it depends on whether the jar
|
|
79
|
+
* happens to hold the session cookie that launch — which is what let it hide.
|
|
80
|
+
*
|
|
81
|
+
* `credentials: "omit"` is the fix, and it costs nothing on native: the session
|
|
82
|
+
* is a bearer there (`setAuthToken` / `setWsToken`), so the cookie was never the
|
|
83
|
+
* credential — only an accident of transport riding along. Wrapped INNERMOST by
|
|
84
|
+
* {@link createLunoraClient}, so it runs after every other layer and also
|
|
85
|
+
* overrides the `credentials: "include"` `@lunora/client` sends on its
|
|
86
|
+
* `get-session` probe.
|
|
87
|
+
*
|
|
88
|
+
* **Scope.** This covers `fetch`, which is the whole HTTP RPC/REST/storage
|
|
89
|
+
* surface. It does NOT cover the WebSocket upgrade — React Native attaches the
|
|
90
|
+
* same jar's cookie to the handshake and exposes no per-socket opt-out — nor
|
|
91
|
+
* `httpStream` imported standalone from `@lunora/client`, which resolves
|
|
92
|
+
* `globalThis.fetch` itself. Neither is a live 403 today: React Native sends an
|
|
93
|
+
* `Origin` equal to the server's on a WS handshake, and better-auth's `bearer`
|
|
94
|
+
* plugin overwrites the session cookie with the bearer rather than deferring to
|
|
95
|
+
* it. Both are noted so the guarantee is not read wider than it is.
|
|
96
|
+
*
|
|
97
|
+
* {@link createLunoraClient} applies it only on native — see
|
|
98
|
+
* {@link isNativeRuntime} — and only to the global `fetch`. Under
|
|
99
|
+
* `react-native-web` the jar is the browser's, `Origin` IS sent, the CSRF guard
|
|
100
|
+
* never fires, and a cookie session is a legitimate setup that this would
|
|
101
|
+
* silently sign out (`getCurrentUser` deliberately sends `credentials:
|
|
102
|
+
* "include"`). A caller-supplied `fetch` is never wrapped either, so a
|
|
103
|
+
* cookie-forwarding SSR transport keeps its credentials; apply this yourself if
|
|
104
|
+
* you want it.
|
|
105
|
+
*/
|
|
106
|
+
declare const withoutAmbientCookies: (fetchImpl: typeof fetch) => typeof fetch;
|
|
62
107
|
/**
|
|
63
108
|
* Construct a `LunoraClient` tuned for React Native / Expo — a thin wrapper over
|
|
64
109
|
* `new LunoraClient(options)` that fills in the three things a browser gets for
|
|
@@ -75,17 +120,21 @@ interface CreateLunoraClientOptions extends LunoraClientOptions {
|
|
|
75
120
|
* after a restart while the socket reconnects — mirroring the browser's
|
|
76
121
|
* IndexedDB-backed default.
|
|
77
122
|
*
|
|
78
|
-
* Third, credentialed requests:
|
|
79
|
-
*
|
|
80
|
-
*
|
|
123
|
+
* Third, credentialed requests: the session is a bearer on this platform, so the
|
|
124
|
+
* returned `fetch` is wrapped in {@link withoutAmbientCookies} (React Native's
|
|
125
|
+
* cookie jar is real, and a stray session cookie 403s every state-changing RPC —
|
|
126
|
+
* see there), and passing `getAuthHeaders` additionally rides its headers on both
|
|
127
|
+
* the HTTP RPC path and the WebSocket upgrade.
|
|
81
128
|
*
|
|
82
129
|
* Everything on `LunoraClientOptions` is still accepted and passed through; an
|
|
83
|
-
* explicit `persistence`, `queryCache`, `fetch`, or `WebSocket`
|
|
84
|
-
*
|
|
130
|
+
* explicit `persistence`, `queryCache`, `fetch`, or `WebSocket` replaces the
|
|
131
|
+
* transport derived from `storage`. A `getAuthHeaders` factory is layered over a
|
|
132
|
+
* caller-supplied `fetch` rather than ignored — the previous behaviour dropped
|
|
133
|
+
* the credential on HTTP while still injecting it on the WS upgrade. `persistence`
|
|
85
134
|
* and `queryCache` override independently — `storage` backs both, so opting out
|
|
86
135
|
* of one leaves the other wired. See the package README
|
|
87
136
|
* for a full setup example.
|
|
88
137
|
* @experimental
|
|
89
138
|
*/
|
|
90
139
|
declare const createLunoraClient: (options: CreateLunoraClientOptions) => LunoraClient;
|
|
91
|
-
export { type AuthHeadersFactory, type CreateLunoraClientOptions, createLunoraClient };
|
|
140
|
+
export { type AuthHeadersFactory, type CreateLunoraClientOptions, createLunoraClient, withoutAmbientCookies };
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createLunoraClient as
|
|
1
|
+
import{createLunoraClient as t,withoutAmbientCookies as r}from"./packem_shared/createLunoraClient-WOgHmG35.mjs";export*from"@lunora/react";export{t as createLunoraClient,r as withoutAmbientCookies};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraClient as d,createAsyncStorageQueryCache as h,createAsyncStoragePersistence as i}from"@lunora/client";const u=()=>typeof document>"u",f=s=>(t,o)=>s(t,{...o,credentials:"omit"}),b=(s,t)=>(o,e)=>{const r=t();if(!r)return s(o,e);const c=new Headers(r);return e?.headers&&new Headers(e.headers).forEach((n,a)=>{c.set(a,n)}),s(o,{...e,headers:c})},S=(s,t)=>class extends s{constructor(e,r){const c=t();super(e,r,c?{headers:c}:void 0)}},y=s=>{const{getAuthHeaders:t,storage:o,...e}=s,r=e.fetch===void 0&&typeof fetch=="function"?fetch.bind(globalThis):void 0,c=e.fetch??(r&&u()?f(r):r),n=t&&c?b(c,t):c,a=t&&e.WebSocket===void 0&&typeof WebSocket=="function"?S(WebSocket,t):e.WebSocket;return new d({...e,persistence:e.persistence??(o?i({storage:o}):void 0),queryCache:e.queryCache??(o?h({storage:o}):void 0),fetch:n,WebSocket:a})};export{y as createLunoraClient,b as withAuthHeaders,S as withAuthWebSocket,f as withoutAmbientCookies};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/react-native",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.91",
|
|
4
4
|
"description": "React Native / Expo integration for Lunora: an AsyncStorage-backed client factory, the useQuery/useMutation/useSubscription hooks, and a one-call better-auth Expo client",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"access": "public"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@lunora/client": "1.0.0-alpha.
|
|
54
|
-
"@lunora/react": "1.0.0-alpha.
|
|
53
|
+
"@lunora/client": "1.0.0-alpha.114",
|
|
54
|
+
"@lunora/react": "1.0.0-alpha.119"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"@better-auth/expo": "^1.7.1",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{LunoraClient as h,createAsyncStorageQueryCache as d,createAsyncStoragePersistence as u}from"@lunora/client";const f=(r,t)=>(c,e)=>{const s=t();if(!s)return r(c,e);const o=new Headers(s);return e?.headers&&new Headers(e.headers).forEach((n,a)=>{o.set(a,n)}),r(c,{...e,headers:o})},i=(r,t)=>class extends r{constructor(e,s){const o=t();super(e,s,o?{headers:o}:void 0)}},k=r=>{const{getAuthHeaders:t,storage:c,...e}=r,s=t&&e.fetch===void 0&&typeof fetch=="function"?f(fetch.bind(globalThis),t):e.fetch,o=t&&e.WebSocket===void 0&&typeof WebSocket=="function"?i(WebSocket,t):e.WebSocket;return new h({...e,persistence:e.persistence??(c?u({storage:c}):void 0),queryCache:e.queryCache??(c?d({storage:c}):void 0),fetch:s,WebSocket:o})};export{k as createLunoraClient,f as withAuthHeaders,i as withAuthWebSocket};
|