better-auth-offline 0.0.0 → 0.0.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Roman Sirokov <roman@mrlightful.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,248 @@
1
+ # better-auth-offline
2
+
3
+ [![NPM Version](https://img.shields.io/npm/v/better-auth-offline)](https://www.npmjs.com/package/better-auth-offline)
4
+ [![NPM Downloads](https://img.shields.io/npm/dm/better-auth-offline)](https://www.npmjs.com/package/better-auth-offline)
5
+
6
+ Offline-first plugin for [better-auth](https://github.com/better-auth/better-auth). Transparently caches auth API responses so your app keeps working when the network doesn't.
7
+
8
+ ## Why offline-first?
9
+
10
+ The offline-first community is growing — and for good reason. Users expect apps to work everywhere: on flaky Wi-Fi, in airplane mode, in rural areas with spotty coverage, and on mobile devices that constantly switch between networks. PWAs, local-first architectures, and edge computing are making offline-capable apps the norm, not the exception.
11
+
12
+ Authentication is often the first thing that breaks when the network drops. A user who was just signed in gets thrown to a login screen — or worse, a blank page. **better-auth-offline** fixes that. It's a drop-in plugin that caches auth responses so your users stay authenticated and your UI stays functional, even offline.
13
+
14
+ No changes to your existing code. Just add the plugin.
15
+
16
+ ## Features
17
+
18
+ - **Network-first with cache fallback** — always tries the network first, serves cached data when offline
19
+ - **Drop-in integration** — one line to add, zero changes to your existing auth code
20
+ - **Automatic cache invalidation** — clears cache on sign-in/sign-out to prevent cross-user data leaks
21
+ - **Configurable allowlist** — choose which endpoints to cache, extend or replace the defaults
22
+ - **IndexedDB storage** — persistent cache that survives page refreshes, with a pluggable storage adapter interface
23
+ - **Online status tracking** — reactive `useOnlineStatus()` hook for your UI
24
+ - **SSR safe** — works in server-side rendering environments without errors
25
+
26
+ ## Quick Start
27
+
28
+ Install the plugin:
29
+
30
+ ```bash
31
+ npm install better-auth-offline
32
+ ```
33
+
34
+ Add it to your auth client:
35
+
36
+ ```ts
37
+ import { createAuthClient } from "better-auth/react";
38
+ import { offlinePlugin } from "better-auth-offline";
39
+
40
+ const authClient = createAuthClient({
41
+ plugins: [offlinePlugin()],
42
+ });
43
+ ```
44
+
45
+ That's it. Your auth API responses are now cached and served when offline.
46
+
47
+ ## How It Works
48
+
49
+ 1. Your app makes a GET request to an allowlisted auth endpoint (e.g. `/api/auth/get-session`)
50
+ 2. The plugin lets the request go to the network as normal
51
+ 3. On success, the JSON response is cached in IndexedDB (fire-and-forget — doesn't slow down the response)
52
+ 4. If the network request fails (no connectivity), the plugin serves the cached response instead
53
+ 5. Cached responses include an `X-Offline-Cache: true` header so you can detect them if needed
54
+
55
+ Only GET requests to allowlisted paths are cached. Mutations (sign-in, sign-out, etc.) always go to the network.
56
+
57
+ ## Configuration
58
+
59
+ ### Default mode — extend or exclude paths
60
+
61
+ By default, the plugin caches a curated set of common auth endpoints. You can extend or trim this list:
62
+
63
+ ```ts
64
+ offlinePlugin({
65
+ // Add your custom endpoints to the default list
66
+ includePaths: ["/my-custom-endpoint", "/user/preferences"],
67
+
68
+ // Remove endpoints you don't need cached
69
+ excludePaths: ["/admin/list-users"],
70
+ })
71
+ ```
72
+
73
+ ### Custom mode — full control
74
+
75
+ Replace the default allowlist entirely:
76
+
77
+ ```ts
78
+ offlinePlugin({
79
+ mode: "custom",
80
+ allowlist: [
81
+ "/get-session",
82
+ "/list-accounts",
83
+ "/my-app/specific-endpoint",
84
+ ],
85
+ })
86
+ ```
87
+
88
+ ### Custom storage adapter
89
+
90
+ Swap out IndexedDB for any storage backend:
91
+
92
+ ```ts
93
+ import { offlinePlugin } from "better-auth-offline";
94
+
95
+ offlinePlugin({
96
+ storage: myCustomAdapter,
97
+ })
98
+ ```
99
+
100
+ See [Custom Storage Adapters](#custom-storage-adapters) for the interface.
101
+
102
+ ## Default Allowlist
103
+
104
+ These endpoints are cached by default (in `mode: "default"`):
105
+
106
+ | Category | Endpoints |
107
+ |----------|-----------|
108
+ | **Core** | `/get-session`, `/list-sessions`, `/list-accounts`, `/account-info` |
109
+ | **Organization** | `/organization/list`, `/organization/get-active-member`, `/organization/get-active-member-role`, `/organization/get-full-organization`, `/organization/list-members`, `/organization/list-teams`, `/organization/list-invitations` |
110
+ | **Admin** | `/admin/list-users` |
111
+ | **Multi-session** | `/multi-session/list-device-sessions` |
112
+ | **Passkey** | `/passkey/list-user-passkeys` |
113
+ | **API Key** | `/api-key/get`, `/api-key/list` |
114
+
115
+ Path matching uses suffix matching, so these work regardless of your `baseURL` or path prefix configuration.
116
+
117
+ ## Online Status
118
+
119
+ The plugin exposes a reactive online/offline status hook:
120
+
121
+ ```tsx
122
+ function MyComponent() {
123
+ const onlineStatus = authClient.useOnlineStatus();
124
+
125
+ return (
126
+ <div>
127
+ {onlineStatus ? "Online" : "Offline"}
128
+ </div>
129
+ );
130
+ }
131
+ ```
132
+
133
+ This tracks the browser's `navigator.onLine` property and listens for `online`/`offline` events. In SSR environments, it defaults to `true`.
134
+
135
+ ## Cache Management
136
+
137
+ ### Automatic invalidation
138
+
139
+ The cache is automatically cleared when a user signs in or signs out. This prevents stale data from one user being served to another.
140
+
141
+ ### Manual cache clearing
142
+
143
+ ```ts
144
+ await authClient.clearCache();
145
+ ```
146
+
147
+ ### Detecting cached responses
148
+
149
+ Responses served from cache include the header `X-Offline-Cache: true`.
150
+
151
+ ## Custom Storage Adapters
152
+
153
+ The plugin uses IndexedDB by default, but you can provide any storage backend that implements the `StorageAdapter` interface:
154
+
155
+ ```ts
156
+ import type { StorageAdapter } from "better-auth-offline";
157
+
158
+ const myAdapter: StorageAdapter = {
159
+ async get(key: string): Promise<unknown | null> {
160
+ // Return cached value or null
161
+ },
162
+ async set(key: string, value: unknown): Promise<void> {
163
+ // Store the value
164
+ },
165
+ async delete(key: string): Promise<void> {
166
+ // Remove a single entry
167
+ },
168
+ async clear(): Promise<void> {
169
+ // Remove all entries
170
+ },
171
+ };
172
+
173
+ offlinePlugin({ storage: myAdapter });
174
+ ```
175
+
176
+ The built-in IndexedDB adapter can also be customized with a different database name:
177
+
178
+ ```ts
179
+ import { createIndexedDBAdapter } from "better-auth-offline";
180
+
181
+ offlinePlugin({
182
+ storage: createIndexedDBAdapter("my-custom-db-name"),
183
+ });
184
+ ```
185
+
186
+ ## API Reference
187
+
188
+ ### Exports from `better-auth-offline`
189
+
190
+ | Export | Type | Description |
191
+ |--------|------|-------------|
192
+ | `offlinePlugin` | `(options?: OfflinePluginOptions) => BetterAuthClientPlugin` | Main plugin factory |
193
+ | `createIndexedDBAdapter` | `(dbName?: string) => StorageAdapter` | Creates an IndexedDB storage adapter |
194
+ | `createOnlineStatusAtom` | `() => Atom<boolean>` | Creates a nanostores atom tracking online status |
195
+ | `StorageAdapter` | Type | Interface for custom storage backends |
196
+ | `OfflinePluginOptions` | Type | Plugin configuration options |
197
+ | `CacheEntry` | Type | Shape of cached entries (`{ data: unknown, cachedAt: number }`) |
198
+
199
+ ### Exports from `better-auth-offline/adapters`
200
+
201
+ | Export | Type | Description |
202
+ |--------|------|-------------|
203
+ | `createIndexedDBAdapter` | `(dbName?: string) => StorageAdapter` | IndexedDB storage adapter (alternative import path) |
204
+
205
+ ## Example App
206
+
207
+ The `example/` directory contains a full Next.js app demonstrating the plugin in action:
208
+
209
+ - Sign in with demo credentials (`demo@example.com` / `demo`)
210
+ - See session data cached and served offline
211
+ - Toggle offline mode in DevTools to test
212
+ - Inspect the IndexedDB cache directly
213
+
214
+ Run it:
215
+
216
+ ```bash
217
+ cd example
218
+ bun install
219
+ bun dev
220
+ ```
221
+
222
+ ## Roadmap
223
+
224
+ - **Prewarming / Eager Data Loading** — Mechanism to eagerly fetch and cache endpoints on app initialization, so the cache is warm before going offline. Critical for apps that go offline unpredictably (mobile, field workers).
225
+
226
+ ## Contributing
227
+
228
+ Contributions are welcome! This project uses:
229
+
230
+ - **bun** as the package manager
231
+ - **vitest** for testing
232
+ - **tsup** for building
233
+ - **turbo** for monorepo orchestration
234
+
235
+ ```bash
236
+ # Install dependencies
237
+ bun install
238
+
239
+ # Run tests
240
+ cd better-auth-offline && bun test
241
+
242
+ # Build
243
+ cd better-auth-offline && bun run build
244
+ ```
245
+
246
+ ## License
247
+
248
+ MIT
@@ -28,7 +28,9 @@ var STORE_NAME = "cache";
28
28
  function createIndexedDBAdapter(dbName = DEFAULT_DB_NAME) {
29
29
  let dbPromise = null;
30
30
  function openDB() {
31
- if (dbPromise) return dbPromise;
31
+ if (dbPromise) {
32
+ return dbPromise;
33
+ }
32
34
  dbPromise = new Promise((resolve, reject) => {
33
35
  const request = indexedDB.open(dbName, 1);
34
36
  request.onupgradeneeded = () => {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/adapters/indexeddb.ts"],"sourcesContent":["import type { StorageAdapter, CacheEntry } from \"../types.js\";\n\nconst DEFAULT_DB_NAME = \"better-auth-offline\";\nconst STORE_NAME = \"cache\";\n\n/**\n * Creates an IndexedDB-backed storage adapter.\n *\n * @param dbName - Name of the IndexedDB database. Defaults to \"better-auth-offline\".\n */\nexport function createIndexedDBAdapter(\n dbName: string = DEFAULT_DB_NAME,\n): StorageAdapter {\n let dbPromise: Promise<IDBDatabase> | null = null;\n\n function openDB(): Promise<IDBDatabase> {\n if (dbPromise) return dbPromise;\n\n dbPromise = new Promise<IDBDatabase>((resolve, reject) => {\n const request = indexedDB.open(dbName, 1);\n\n request.onupgradeneeded = () => {\n const db = request.result;\n if (!db.objectStoreNames.contains(STORE_NAME)) {\n db.createObjectStore(STORE_NAME);\n }\n };\n\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => {\n dbPromise = null;\n reject(request.error);\n };\n });\n\n return dbPromise;\n }\n\n return {\n async get(key: string): Promise<CacheEntry | null> {\n try {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE_NAME, \"readonly\");\n const store = tx.objectStore(STORE_NAME);\n const request = store.get(key);\n request.onsuccess = () => resolve(request.result ?? null);\n request.onerror = () => reject(request.error);\n });\n } catch {\n return null;\n }\n },\n\n async set(key: string, value: unknown): Promise<void> {\n try {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE_NAME, \"readwrite\");\n const store = tx.objectStore(STORE_NAME);\n const request = store.put(value, key);\n request.onsuccess = () => resolve();\n request.onerror = () => reject(request.error);\n });\n } catch {\n // Fire-and-forget: swallow errors for cache writes\n }\n },\n\n async delete(key: string): Promise<void> {\n try {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE_NAME, \"readwrite\");\n const store = tx.objectStore(STORE_NAME);\n const request = store.delete(key);\n request.onsuccess = () => resolve();\n request.onerror = () => reject(request.error);\n });\n } catch {\n // Swallow errors\n }\n },\n\n async clear(): Promise<void> {\n try {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE_NAME, \"readwrite\");\n const store = tx.objectStore(STORE_NAME);\n const request = store.clear();\n request.onsuccess = () => resolve();\n request.onerror = () => reject(request.error);\n });\n } catch {\n // Swallow errors\n }\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAEA,IAAM,kBAAkB;AACxB,IAAM,aAAa;AAOZ,SAAS,uBACd,SAAiB,iBACD;AAChB,MAAI,YAAyC;AAE7C,WAAS,SAA+B;AACtC,QAAI,UAAW,QAAO;AAEtB,gBAAY,IAAI,QAAqB,CAAC,SAAS,WAAW;AACxD,YAAM,UAAU,UAAU,KAAK,QAAQ,CAAC;AAExC,cAAQ,kBAAkB,MAAM;AAC9B,cAAM,KAAK,QAAQ;AACnB,YAAI,CAAC,GAAG,iBAAiB,SAAS,UAAU,GAAG;AAC7C,aAAG,kBAAkB,UAAU;AAAA,QACjC;AAAA,MACF;AAEA,cAAQ,YAAY,MAAM,QAAQ,QAAQ,MAAM;AAChD,cAAQ,UAAU,MAAM;AACtB,oBAAY;AACZ,eAAO,QAAQ,KAAK;AAAA,MACtB;AAAA,IACF,CAAC;AAED,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,MAAM,IAAI,KAAyC;AACjD,UAAI;AACF,cAAM,KAAK,MAAM,OAAO;AACxB,eAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,gBAAM,KAAK,GAAG,YAAY,YAAY,UAAU;AAChD,gBAAM,QAAQ,GAAG,YAAY,UAAU;AACvC,gBAAM,UAAU,MAAM,IAAI,GAAG;AAC7B,kBAAQ,YAAY,MAAM,QAAQ,QAAQ,UAAU,IAAI;AACxD,kBAAQ,UAAU,MAAM,OAAO,QAAQ,KAAK;AAAA,QAC9C,CAAC;AAAA,MACH,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF;AAAA,IAEA,MAAM,IAAI,KAAa,OAA+B;AACpD,UAAI;AACF,cAAM,KAAK,MAAM,OAAO;AACxB,eAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,gBAAM,KAAK,GAAG,YAAY,YAAY,WAAW;AACjD,gBAAM,QAAQ,GAAG,YAAY,UAAU;AACvC,gBAAM,UAAU,MAAM,IAAI,OAAO,GAAG;AACpC,kBAAQ,YAAY,MAAM,QAAQ;AAClC,kBAAQ,UAAU,MAAM,OAAO,QAAQ,KAAK;AAAA,QAC9C,CAAC;AAAA,MACH,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,IAEA,MAAM,OAAO,KAA4B;AACvC,UAAI;AACF,cAAM,KAAK,MAAM,OAAO;AACxB,eAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,gBAAM,KAAK,GAAG,YAAY,YAAY,WAAW;AACjD,gBAAM,QAAQ,GAAG,YAAY,UAAU;AACvC,gBAAM,UAAU,MAAM,OAAO,GAAG;AAChC,kBAAQ,YAAY,MAAM,QAAQ;AAClC,kBAAQ,UAAU,MAAM,OAAO,QAAQ,KAAK;AAAA,QAC9C,CAAC;AAAA,MACH,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,IAEA,MAAM,QAAuB;AAC3B,UAAI;AACF,cAAM,KAAK,MAAM,OAAO;AACxB,eAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,gBAAM,KAAK,GAAG,YAAY,YAAY,WAAW;AACjD,gBAAM,QAAQ,GAAG,YAAY,UAAU;AACvC,gBAAM,UAAU,MAAM,MAAM;AAC5B,kBAAQ,YAAY,MAAM,QAAQ;AAClC,kBAAQ,UAAU,MAAM,OAAO,QAAQ,KAAK;AAAA,QAC9C,CAAC;AAAA,MACH,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/adapters/indexeddb.ts"],"sourcesContent":["import type { CacheEntry, StorageAdapter } from \"../types.js\";\n\nconst DEFAULT_DB_NAME = \"better-auth-offline\";\nconst STORE_NAME = \"cache\";\n\n/**\n * Creates an IndexedDB-backed storage adapter.\n *\n * @param dbName - Name of the IndexedDB database. Defaults to \"better-auth-offline\".\n */\nexport function createIndexedDBAdapter(\n dbName: string = DEFAULT_DB_NAME\n): StorageAdapter {\n let dbPromise: Promise<IDBDatabase> | null = null;\n\n function openDB(): Promise<IDBDatabase> {\n if (dbPromise) {\n return dbPromise;\n }\n\n dbPromise = new Promise<IDBDatabase>((resolve, reject) => {\n const request = indexedDB.open(dbName, 1);\n\n request.onupgradeneeded = () => {\n const db = request.result;\n if (!db.objectStoreNames.contains(STORE_NAME)) {\n db.createObjectStore(STORE_NAME);\n }\n };\n\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => {\n dbPromise = null;\n reject(request.error);\n };\n });\n\n return dbPromise;\n }\n\n return {\n async get(key: string): Promise<CacheEntry | null> {\n try {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE_NAME, \"readonly\");\n const store = tx.objectStore(STORE_NAME);\n const request = store.get(key);\n request.onsuccess = () => resolve(request.result ?? null);\n request.onerror = () => reject(request.error);\n });\n } catch {\n return null;\n }\n },\n\n async set(key: string, value: unknown): Promise<void> {\n try {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE_NAME, \"readwrite\");\n const store = tx.objectStore(STORE_NAME);\n const request = store.put(value, key);\n request.onsuccess = () => resolve();\n request.onerror = () => reject(request.error);\n });\n } catch {\n // Fire-and-forget: swallow errors for cache writes\n }\n },\n\n async delete(key: string): Promise<void> {\n try {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE_NAME, \"readwrite\");\n const store = tx.objectStore(STORE_NAME);\n const request = store.delete(key);\n request.onsuccess = () => resolve();\n request.onerror = () => reject(request.error);\n });\n } catch {\n // Swallow errors\n }\n },\n\n async clear(): Promise<void> {\n try {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE_NAME, \"readwrite\");\n const store = tx.objectStore(STORE_NAME);\n const request = store.clear();\n request.onsuccess = () => resolve();\n request.onerror = () => reject(request.error);\n });\n } catch {\n // Swallow errors\n }\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAEA,IAAM,kBAAkB;AACxB,IAAM,aAAa;AAOZ,SAAS,uBACd,SAAiB,iBACD;AAChB,MAAI,YAAyC;AAE7C,WAAS,SAA+B;AACtC,QAAI,WAAW;AACb,aAAO;AAAA,IACT;AAEA,gBAAY,IAAI,QAAqB,CAAC,SAAS,WAAW;AACxD,YAAM,UAAU,UAAU,KAAK,QAAQ,CAAC;AAExC,cAAQ,kBAAkB,MAAM;AAC9B,cAAM,KAAK,QAAQ;AACnB,YAAI,CAAC,GAAG,iBAAiB,SAAS,UAAU,GAAG;AAC7C,aAAG,kBAAkB,UAAU;AAAA,QACjC;AAAA,MACF;AAEA,cAAQ,YAAY,MAAM,QAAQ,QAAQ,MAAM;AAChD,cAAQ,UAAU,MAAM;AACtB,oBAAY;AACZ,eAAO,QAAQ,KAAK;AAAA,MACtB;AAAA,IACF,CAAC;AAED,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,MAAM,IAAI,KAAyC;AACjD,UAAI;AACF,cAAM,KAAK,MAAM,OAAO;AACxB,eAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,gBAAM,KAAK,GAAG,YAAY,YAAY,UAAU;AAChD,gBAAM,QAAQ,GAAG,YAAY,UAAU;AACvC,gBAAM,UAAU,MAAM,IAAI,GAAG;AAC7B,kBAAQ,YAAY,MAAM,QAAQ,QAAQ,UAAU,IAAI;AACxD,kBAAQ,UAAU,MAAM,OAAO,QAAQ,KAAK;AAAA,QAC9C,CAAC;AAAA,MACH,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF;AAAA,IAEA,MAAM,IAAI,KAAa,OAA+B;AACpD,UAAI;AACF,cAAM,KAAK,MAAM,OAAO;AACxB,eAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,gBAAM,KAAK,GAAG,YAAY,YAAY,WAAW;AACjD,gBAAM,QAAQ,GAAG,YAAY,UAAU;AACvC,gBAAM,UAAU,MAAM,IAAI,OAAO,GAAG;AACpC,kBAAQ,YAAY,MAAM,QAAQ;AAClC,kBAAQ,UAAU,MAAM,OAAO,QAAQ,KAAK;AAAA,QAC9C,CAAC;AAAA,MACH,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,IAEA,MAAM,OAAO,KAA4B;AACvC,UAAI;AACF,cAAM,KAAK,MAAM,OAAO;AACxB,eAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,gBAAM,KAAK,GAAG,YAAY,YAAY,WAAW;AACjD,gBAAM,QAAQ,GAAG,YAAY,UAAU;AACvC,gBAAM,UAAU,MAAM,OAAO,GAAG;AAChC,kBAAQ,YAAY,MAAM,QAAQ;AAClC,kBAAQ,UAAU,MAAM,OAAO,QAAQ,KAAK;AAAA,QAC9C,CAAC;AAAA,MACH,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,IAEA,MAAM,QAAuB;AAC3B,UAAI;AACF,cAAM,KAAK,MAAM,OAAO;AACxB,eAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,gBAAM,KAAK,GAAG,YAAY,YAAY,WAAW;AACjD,gBAAM,QAAQ,GAAG,YAAY,UAAU;AACvC,gBAAM,UAAU,MAAM,MAAM;AAC5B,kBAAQ,YAAY,MAAM,QAAQ;AAClC,kBAAQ,UAAU,MAAM,OAAO,QAAQ,KAAK;AAAA,QAC9C,CAAC;AAAA,MACH,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
@@ -1 +1 @@
1
- export { c as createIndexedDBAdapter } from '../indexeddb-CkUWH5Sl.cjs';
1
+ export { c as createIndexedDBAdapter } from '../indexeddb-B7LON8ue.cjs';
@@ -1 +1 @@
1
- export { c as createIndexedDBAdapter } from '../indexeddb-CkUWH5Sl.js';
1
+ export { c as createIndexedDBAdapter } from '../indexeddb-B7LON8ue.js';
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createIndexedDBAdapter
3
- } from "../chunk-JPTDSCSW.js";
3
+ } from "../chunk-NFJRQCGL.js";
4
4
  export {
5
5
  createIndexedDBAdapter
6
6
  };
@@ -4,7 +4,9 @@ var STORE_NAME = "cache";
4
4
  function createIndexedDBAdapter(dbName = DEFAULT_DB_NAME) {
5
5
  let dbPromise = null;
6
6
  function openDB() {
7
- if (dbPromise) return dbPromise;
7
+ if (dbPromise) {
8
+ return dbPromise;
9
+ }
8
10
  dbPromise = new Promise((resolve, reject) => {
9
11
  const request = indexedDB.open(dbName, 1);
10
12
  request.onupgradeneeded = () => {
@@ -81,4 +83,4 @@ function createIndexedDBAdapter(dbName = DEFAULT_DB_NAME) {
81
83
  export {
82
84
  createIndexedDBAdapter
83
85
  };
84
- //# sourceMappingURL=chunk-JPTDSCSW.js.map
86
+ //# sourceMappingURL=chunk-NFJRQCGL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/adapters/indexeddb.ts"],"sourcesContent":["import type { CacheEntry, StorageAdapter } from \"../types.js\";\n\nconst DEFAULT_DB_NAME = \"better-auth-offline\";\nconst STORE_NAME = \"cache\";\n\n/**\n * Creates an IndexedDB-backed storage adapter.\n *\n * @param dbName - Name of the IndexedDB database. Defaults to \"better-auth-offline\".\n */\nexport function createIndexedDBAdapter(\n dbName: string = DEFAULT_DB_NAME\n): StorageAdapter {\n let dbPromise: Promise<IDBDatabase> | null = null;\n\n function openDB(): Promise<IDBDatabase> {\n if (dbPromise) {\n return dbPromise;\n }\n\n dbPromise = new Promise<IDBDatabase>((resolve, reject) => {\n const request = indexedDB.open(dbName, 1);\n\n request.onupgradeneeded = () => {\n const db = request.result;\n if (!db.objectStoreNames.contains(STORE_NAME)) {\n db.createObjectStore(STORE_NAME);\n }\n };\n\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => {\n dbPromise = null;\n reject(request.error);\n };\n });\n\n return dbPromise;\n }\n\n return {\n async get(key: string): Promise<CacheEntry | null> {\n try {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE_NAME, \"readonly\");\n const store = tx.objectStore(STORE_NAME);\n const request = store.get(key);\n request.onsuccess = () => resolve(request.result ?? null);\n request.onerror = () => reject(request.error);\n });\n } catch {\n return null;\n }\n },\n\n async set(key: string, value: unknown): Promise<void> {\n try {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE_NAME, \"readwrite\");\n const store = tx.objectStore(STORE_NAME);\n const request = store.put(value, key);\n request.onsuccess = () => resolve();\n request.onerror = () => reject(request.error);\n });\n } catch {\n // Fire-and-forget: swallow errors for cache writes\n }\n },\n\n async delete(key: string): Promise<void> {\n try {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE_NAME, \"readwrite\");\n const store = tx.objectStore(STORE_NAME);\n const request = store.delete(key);\n request.onsuccess = () => resolve();\n request.onerror = () => reject(request.error);\n });\n } catch {\n // Swallow errors\n }\n },\n\n async clear(): Promise<void> {\n try {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE_NAME, \"readwrite\");\n const store = tx.objectStore(STORE_NAME);\n const request = store.clear();\n request.onsuccess = () => resolve();\n request.onerror = () => reject(request.error);\n });\n } catch {\n // Swallow errors\n }\n },\n };\n}\n"],"mappings":";AAEA,IAAM,kBAAkB;AACxB,IAAM,aAAa;AAOZ,SAAS,uBACd,SAAiB,iBACD;AAChB,MAAI,YAAyC;AAE7C,WAAS,SAA+B;AACtC,QAAI,WAAW;AACb,aAAO;AAAA,IACT;AAEA,gBAAY,IAAI,QAAqB,CAAC,SAAS,WAAW;AACxD,YAAM,UAAU,UAAU,KAAK,QAAQ,CAAC;AAExC,cAAQ,kBAAkB,MAAM;AAC9B,cAAM,KAAK,QAAQ;AACnB,YAAI,CAAC,GAAG,iBAAiB,SAAS,UAAU,GAAG;AAC7C,aAAG,kBAAkB,UAAU;AAAA,QACjC;AAAA,MACF;AAEA,cAAQ,YAAY,MAAM,QAAQ,QAAQ,MAAM;AAChD,cAAQ,UAAU,MAAM;AACtB,oBAAY;AACZ,eAAO,QAAQ,KAAK;AAAA,MACtB;AAAA,IACF,CAAC;AAED,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,MAAM,IAAI,KAAyC;AACjD,UAAI;AACF,cAAM,KAAK,MAAM,OAAO;AACxB,eAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,gBAAM,KAAK,GAAG,YAAY,YAAY,UAAU;AAChD,gBAAM,QAAQ,GAAG,YAAY,UAAU;AACvC,gBAAM,UAAU,MAAM,IAAI,GAAG;AAC7B,kBAAQ,YAAY,MAAM,QAAQ,QAAQ,UAAU,IAAI;AACxD,kBAAQ,UAAU,MAAM,OAAO,QAAQ,KAAK;AAAA,QAC9C,CAAC;AAAA,MACH,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF;AAAA,IAEA,MAAM,IAAI,KAAa,OAA+B;AACpD,UAAI;AACF,cAAM,KAAK,MAAM,OAAO;AACxB,eAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,gBAAM,KAAK,GAAG,YAAY,YAAY,WAAW;AACjD,gBAAM,QAAQ,GAAG,YAAY,UAAU;AACvC,gBAAM,UAAU,MAAM,IAAI,OAAO,GAAG;AACpC,kBAAQ,YAAY,MAAM,QAAQ;AAClC,kBAAQ,UAAU,MAAM,OAAO,QAAQ,KAAK;AAAA,QAC9C,CAAC;AAAA,MACH,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,IAEA,MAAM,OAAO,KAA4B;AACvC,UAAI;AACF,cAAM,KAAK,MAAM,OAAO;AACxB,eAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,gBAAM,KAAK,GAAG,YAAY,YAAY,WAAW;AACjD,gBAAM,QAAQ,GAAG,YAAY,UAAU;AACvC,gBAAM,UAAU,MAAM,OAAO,GAAG;AAChC,kBAAQ,YAAY,MAAM,QAAQ;AAClC,kBAAQ,UAAU,MAAM,OAAO,QAAQ,KAAK;AAAA,QAC9C,CAAC;AAAA,MACH,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,IAEA,MAAM,QAAuB;AAC3B,UAAI;AACF,cAAM,KAAK,MAAM,OAAO;AACxB,eAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,gBAAM,KAAK,GAAG,YAAY,YAAY,WAAW;AACjD,gBAAM,QAAQ,GAAG,YAAY,UAAU;AACvC,gBAAM,UAAU,MAAM,MAAM;AAC5B,kBAAQ,YAAY,MAAM,QAAQ;AAClC,kBAAQ,UAAU,MAAM,OAAO,QAAQ,KAAK;AAAA,QAC9C,CAAC;AAAA,MACH,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
package/dist/index.cjs CHANGED
@@ -26,6 +26,88 @@ __export(src_exports, {
26
26
  });
27
27
  module.exports = __toCommonJS(src_exports);
28
28
 
29
+ // src/adapters/indexeddb.ts
30
+ var DEFAULT_DB_NAME = "better-auth-offline";
31
+ var STORE_NAME = "cache";
32
+ function createIndexedDBAdapter(dbName = DEFAULT_DB_NAME) {
33
+ let dbPromise = null;
34
+ function openDB() {
35
+ if (dbPromise) {
36
+ return dbPromise;
37
+ }
38
+ dbPromise = new Promise((resolve, reject) => {
39
+ const request = indexedDB.open(dbName, 1);
40
+ request.onupgradeneeded = () => {
41
+ const db = request.result;
42
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
43
+ db.createObjectStore(STORE_NAME);
44
+ }
45
+ };
46
+ request.onsuccess = () => resolve(request.result);
47
+ request.onerror = () => {
48
+ dbPromise = null;
49
+ reject(request.error);
50
+ };
51
+ });
52
+ return dbPromise;
53
+ }
54
+ return {
55
+ async get(key) {
56
+ try {
57
+ const db = await openDB();
58
+ return new Promise((resolve, reject) => {
59
+ const tx = db.transaction(STORE_NAME, "readonly");
60
+ const store = tx.objectStore(STORE_NAME);
61
+ const request = store.get(key);
62
+ request.onsuccess = () => resolve(request.result ?? null);
63
+ request.onerror = () => reject(request.error);
64
+ });
65
+ } catch {
66
+ return null;
67
+ }
68
+ },
69
+ async set(key, value) {
70
+ try {
71
+ const db = await openDB();
72
+ return new Promise((resolve, reject) => {
73
+ const tx = db.transaction(STORE_NAME, "readwrite");
74
+ const store = tx.objectStore(STORE_NAME);
75
+ const request = store.put(value, key);
76
+ request.onsuccess = () => resolve();
77
+ request.onerror = () => reject(request.error);
78
+ });
79
+ } catch {
80
+ }
81
+ },
82
+ async delete(key) {
83
+ try {
84
+ const db = await openDB();
85
+ return new Promise((resolve, reject) => {
86
+ const tx = db.transaction(STORE_NAME, "readwrite");
87
+ const store = tx.objectStore(STORE_NAME);
88
+ const request = store.delete(key);
89
+ request.onsuccess = () => resolve();
90
+ request.onerror = () => reject(request.error);
91
+ });
92
+ } catch {
93
+ }
94
+ },
95
+ async clear() {
96
+ try {
97
+ const db = await openDB();
98
+ return new Promise((resolve, reject) => {
99
+ const tx = db.transaction(STORE_NAME, "readwrite");
100
+ const store = tx.objectStore(STORE_NAME);
101
+ const request = store.clear();
102
+ request.onsuccess = () => resolve();
103
+ request.onerror = () => reject(request.error);
104
+ });
105
+ } catch {
106
+ }
107
+ }
108
+ };
109
+ }
110
+
29
111
  // src/allowlist.ts
30
112
  var DEFAULT_ALLOWLIST = [
31
113
  // Core
@@ -61,7 +143,7 @@ function getAllowlist(options) {
61
143
  }
62
144
  function isAllowlisted(path, allowlist) {
63
145
  return allowlist.some(
64
- (allowed) => path === allowed || path.endsWith(allowed) || path.includes(allowed + "/")
146
+ (allowed) => path === allowed || path.endsWith(allowed) || path.includes(`${allowed}/`)
65
147
  );
66
148
  }
67
149
 
@@ -69,7 +151,9 @@ function isAllowlisted(path, allowlist) {
69
151
  function extractPath(urlOrPath) {
70
152
  try {
71
153
  const url = typeof urlOrPath === "string" && urlOrPath.startsWith("http") ? new URL(urlOrPath) : null;
72
- if (url) return url.pathname;
154
+ if (url) {
155
+ return url.pathname;
156
+ }
73
157
  } catch {
74
158
  }
75
159
  const str = typeof urlOrPath === "string" ? urlOrPath : urlOrPath.pathname;
@@ -77,8 +161,12 @@ function extractPath(urlOrPath) {
77
161
  return qIndex >= 0 ? str.slice(0, qIndex) : str;
78
162
  }
79
163
  function isNetworkError(error) {
80
- if (error instanceof TypeError) return true;
81
- if (error instanceof DOMException && error.name === "AbortError") return true;
164
+ if (error instanceof TypeError) {
165
+ return true;
166
+ }
167
+ if (error instanceof DOMException && error.name === "AbortError") {
168
+ return true;
169
+ }
82
170
  return false;
83
171
  }
84
172
  function createOfflineFetchPlugin(storage, options) {
@@ -146,86 +234,6 @@ function createOnlineStatusAtom() {
146
234
  return isOnline;
147
235
  }
148
236
 
149
- // src/adapters/indexeddb.ts
150
- var DEFAULT_DB_NAME = "better-auth-offline";
151
- var STORE_NAME = "cache";
152
- function createIndexedDBAdapter(dbName = DEFAULT_DB_NAME) {
153
- let dbPromise = null;
154
- function openDB() {
155
- if (dbPromise) return dbPromise;
156
- dbPromise = new Promise((resolve, reject) => {
157
- const request = indexedDB.open(dbName, 1);
158
- request.onupgradeneeded = () => {
159
- const db = request.result;
160
- if (!db.objectStoreNames.contains(STORE_NAME)) {
161
- db.createObjectStore(STORE_NAME);
162
- }
163
- };
164
- request.onsuccess = () => resolve(request.result);
165
- request.onerror = () => {
166
- dbPromise = null;
167
- reject(request.error);
168
- };
169
- });
170
- return dbPromise;
171
- }
172
- return {
173
- async get(key) {
174
- try {
175
- const db = await openDB();
176
- return new Promise((resolve, reject) => {
177
- const tx = db.transaction(STORE_NAME, "readonly");
178
- const store = tx.objectStore(STORE_NAME);
179
- const request = store.get(key);
180
- request.onsuccess = () => resolve(request.result ?? null);
181
- request.onerror = () => reject(request.error);
182
- });
183
- } catch {
184
- return null;
185
- }
186
- },
187
- async set(key, value) {
188
- try {
189
- const db = await openDB();
190
- return new Promise((resolve, reject) => {
191
- const tx = db.transaction(STORE_NAME, "readwrite");
192
- const store = tx.objectStore(STORE_NAME);
193
- const request = store.put(value, key);
194
- request.onsuccess = () => resolve();
195
- request.onerror = () => reject(request.error);
196
- });
197
- } catch {
198
- }
199
- },
200
- async delete(key) {
201
- try {
202
- const db = await openDB();
203
- return new Promise((resolve, reject) => {
204
- const tx = db.transaction(STORE_NAME, "readwrite");
205
- const store = tx.objectStore(STORE_NAME);
206
- const request = store.delete(key);
207
- request.onsuccess = () => resolve();
208
- request.onerror = () => reject(request.error);
209
- });
210
- } catch {
211
- }
212
- },
213
- async clear() {
214
- try {
215
- const db = await openDB();
216
- return new Promise((resolve, reject) => {
217
- const tx = db.transaction(STORE_NAME, "readwrite");
218
- const store = tx.objectStore(STORE_NAME);
219
- const request = store.clear();
220
- request.onsuccess = () => resolve();
221
- request.onerror = () => reject(request.error);
222
- });
223
- } catch {
224
- }
225
- }
226
- };
227
- }
228
-
229
237
  // src/index.ts
230
238
  var SIGN_OUT_PATHS = ["/sign-out", "/signout", "/logout"];
231
239
  var SIGN_IN_PATHS = ["/sign-in", "/signin", "/login"];
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/allowlist.ts","../src/fetch-plugin.ts","../src/online-status.ts","../src/adapters/indexeddb.ts"],"sourcesContent":["import type { BetterAuthClientPlugin } from \"better-auth/client\";\nimport type { OfflinePluginOptions, StorageAdapter } from \"./types.js\";\nimport { createOfflineFetchPlugin } from \"./fetch-plugin.js\";\nimport { createOnlineStatusAtom } from \"./online-status.js\";\nimport { createIndexedDBAdapter } from \"./adapters/indexeddb.js\";\n\n// Re-export types for consumers\nexport type { StorageAdapter, OfflinePluginOptions, CacheEntry } from \"./types.js\";\nexport { createIndexedDBAdapter } from \"./adapters/indexeddb.js\";\nexport { createOnlineStatusAtom } from \"./online-status.js\";\n\nconst SIGN_OUT_PATHS = [\"/sign-out\", \"/signout\", \"/logout\"];\nconst SIGN_IN_PATHS = [\"/sign-in\", \"/signin\", \"/login\"];\n\nfunction isAuthChangePath(path: string): boolean {\n return (\n SIGN_OUT_PATHS.some((p) => path.includes(p)) ||\n SIGN_IN_PATHS.some((p) => path.includes(p))\n );\n}\n\n/**\n * better-auth offline plugin.\n *\n * Transparently caches GET API responses and serves them when offline.\n * Drop-in: no consumer code changes required.\n *\n * @example\n * ```ts\n * import { createAuthClient } from \"better-auth/client\";\n * import { offlinePlugin } from \"better-auth-offline\";\n *\n * const authClient = createAuthClient({\n * plugins: [offlinePlugin()],\n * });\n * ```\n */\nexport function offlinePlugin(\n options: OfflinePluginOptions = {},\n): BetterAuthClientPlugin {\n const storage: StorageAdapter =\n options.storage ?? createIndexedDBAdapter();\n\n return {\n id: \"better-auth-offline\",\n\n getAtoms() {\n return {\n onlineStatus: createOnlineStatusAtom(),\n };\n },\n\n fetchPlugins: [createOfflineFetchPlugin(storage, options)],\n\n atomListeners: [\n {\n matcher(path: string) {\n return isAuthChangePath(path);\n },\n signal: \"$sessionSignal\",\n callback() {\n // Clear cache on sign-out / sign-in to prevent cross-user data leaks\n storage.clear();\n },\n },\n ],\n\n getActions() {\n return {\n clearCache: () => storage.clear(),\n };\n },\n };\n}\n","import type { OfflinePluginOptions } from \"./types.js\";\n\n/**\n * Default paths that are cached for offline access.\n * Only GET requests to these paths will be cached.\n */\nconst DEFAULT_ALLOWLIST = [\n // Core\n \"/get-session\",\n \"/list-sessions\",\n \"/list-accounts\",\n \"/account-info\",\n // Organization\n \"/organization/list\",\n \"/organization/get-active-member\",\n \"/organization/get-active-member-role\",\n \"/organization/get-full-organization\",\n \"/organization/list-members\",\n \"/organization/list-teams\",\n \"/organization/list-invitations\",\n // Admin\n \"/admin/list-users\",\n // Multi-session\n \"/multi-session/list-device-sessions\",\n // Passkey\n \"/passkey/list-user-passkeys\",\n // API Key\n \"/api-key/get\",\n \"/api-key/list\",\n];\n\n/**\n * Get the effective allowlist based on plugin options.\n */\nexport function getAllowlist(options: OfflinePluginOptions): string[] {\n if (options.mode === \"custom\") {\n return options.allowlist;\n }\n const exclude = new Set(options.excludePaths ?? []);\n const base = exclude.size > 0\n ? DEFAULT_ALLOWLIST.filter((p) => !exclude.has(p))\n : DEFAULT_ALLOWLIST;\n return [...base, ...(options.includePaths ?? [])];\n}\n\n/**\n * Check if a path should be cached.\n * Uses suffix matching to handle configurable base path prefixes.\n */\nexport function isAllowlisted(\n path: string,\n allowlist: string[],\n): boolean {\n return allowlist.some(\n (allowed) =>\n path === allowed ||\n path.endsWith(allowed) ||\n path.includes(allowed + \"/\"),\n );\n}\n","import type { BetterFetchPlugin } from \"@better-fetch/fetch\";\nimport type { StorageAdapter, CacheEntry, OfflinePluginOptions } from \"./types.js\";\nimport { getAllowlist, isAllowlisted } from \"./allowlist.js\";\n\n/**\n * Extracts the pathname from a URL string, stripping query params.\n * Used as the cache key.\n */\nexport function extractPath(urlOrPath: string | URL): string {\n try {\n const url = typeof urlOrPath === \"string\" && urlOrPath.startsWith(\"http\")\n ? new URL(urlOrPath)\n : null;\n if (url) return url.pathname;\n } catch {\n // Not a valid URL, treat as path\n }\n // Strip query string from path\n const str = typeof urlOrPath === \"string\" ? urlOrPath : urlOrPath.pathname;\n const qIndex = str.indexOf(\"?\");\n return qIndex >= 0 ? str.slice(0, qIndex) : str;\n}\n\n/**\n * Checks if a network error is a connectivity failure (not an HTTP error).\n */\nfunction isNetworkError(error: unknown): boolean {\n if (error instanceof TypeError) return true;\n if (error instanceof DOMException && error.name === \"AbortError\") return true;\n return false;\n}\n\n/**\n * Creates the BetterFetchPlugin that provides offline caching.\n *\n * Strategy: Network-First with Cache Fallback\n * - Uses `init` to inject a custom fetch that wraps the real fetch\n * - On successful GET: caches the response body (fire-and-forget)\n * - On network error for GET: serves cached response if available\n * - Only allowlisted GET paths are cached; everything else passes through\n */\nexport function createOfflineFetchPlugin(\n storage: StorageAdapter,\n options: OfflinePluginOptions,\n): BetterFetchPlugin {\n const allowlist = getAllowlist(options);\n\n return {\n id: \"better-auth-offline\",\n name: \"better-auth-offline\",\n\n init(url, fetchOptions) {\n const originalFetch = fetchOptions?.customFetchImpl ?? globalThis.fetch;\n const method = (fetchOptions?.method ?? \"GET\").toUpperCase();\n const path = extractPath(url);\n const shouldCache = method === \"GET\" && isAllowlisted(path, allowlist);\n\n const wrappedFetch: typeof globalThis.fetch = async (input, init) => {\n if (!shouldCache) {\n return originalFetch(input, init);\n }\n\n try {\n const response = await originalFetch(input, init);\n\n // Cache successful GET responses (fire-and-forget)\n if (response.ok) {\n // Clone before reading body so the original response remains usable\n const clone = response.clone();\n clone.json().then((data) => {\n const entry: CacheEntry = { data, cachedAt: Date.now() };\n storage.set(path, entry);\n }).catch(() => {\n // Response wasn't JSON or read failed — skip caching\n });\n }\n\n return response;\n } catch (error) {\n // Network error — try serving from cache\n if (isNetworkError(error)) {\n const cached = await storage.get(path) as CacheEntry | null;\n if (cached) {\n return new Response(JSON.stringify(cached.data), {\n status: 200,\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-Offline-Cache\": \"true\",\n },\n });\n }\n }\n // No cache hit or not a network error — rethrow\n throw error;\n }\n };\n\n // Mutate the original options object directly rather than spreading\n // into a new one. @better-fetch/fetch's initializePlugins passes the\n // same `options` reference to every plugin's init(). If we return a\n // new object, a later plugin (e.g. applySchemaPlugin) that returns\n // the original reference will overwrite our customFetchImpl, silently\n // disabling the offline cache. By mutating in place, the wrapping\n // survives regardless of plugin ordering.\n if (fetchOptions) {\n fetchOptions.customFetchImpl = wrappedFetch;\n }\n\n return {\n url,\n options: fetchOptions ?? { customFetchImpl: wrappedFetch },\n };\n },\n };\n}\n","import { atom } from \"nanostores\";\n\n/**\n * Creates a reactive atom that tracks the browser's online/offline status.\n * Defaults to `true` in non-browser environments (SSR).\n */\nexport function createOnlineStatusAtom() {\n const isOnline = atom<boolean>(\n typeof navigator !== \"undefined\" && typeof navigator.onLine === \"boolean\"\n ? navigator.onLine\n : true,\n );\n\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"online\", () => isOnline.set(true));\n window.addEventListener(\"offline\", () => isOnline.set(false));\n }\n\n return isOnline;\n}\n","import type { StorageAdapter, CacheEntry } from \"../types.js\";\n\nconst DEFAULT_DB_NAME = \"better-auth-offline\";\nconst STORE_NAME = \"cache\";\n\n/**\n * Creates an IndexedDB-backed storage adapter.\n *\n * @param dbName - Name of the IndexedDB database. Defaults to \"better-auth-offline\".\n */\nexport function createIndexedDBAdapter(\n dbName: string = DEFAULT_DB_NAME,\n): StorageAdapter {\n let dbPromise: Promise<IDBDatabase> | null = null;\n\n function openDB(): Promise<IDBDatabase> {\n if (dbPromise) return dbPromise;\n\n dbPromise = new Promise<IDBDatabase>((resolve, reject) => {\n const request = indexedDB.open(dbName, 1);\n\n request.onupgradeneeded = () => {\n const db = request.result;\n if (!db.objectStoreNames.contains(STORE_NAME)) {\n db.createObjectStore(STORE_NAME);\n }\n };\n\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => {\n dbPromise = null;\n reject(request.error);\n };\n });\n\n return dbPromise;\n }\n\n return {\n async get(key: string): Promise<CacheEntry | null> {\n try {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE_NAME, \"readonly\");\n const store = tx.objectStore(STORE_NAME);\n const request = store.get(key);\n request.onsuccess = () => resolve(request.result ?? null);\n request.onerror = () => reject(request.error);\n });\n } catch {\n return null;\n }\n },\n\n async set(key: string, value: unknown): Promise<void> {\n try {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE_NAME, \"readwrite\");\n const store = tx.objectStore(STORE_NAME);\n const request = store.put(value, key);\n request.onsuccess = () => resolve();\n request.onerror = () => reject(request.error);\n });\n } catch {\n // Fire-and-forget: swallow errors for cache writes\n }\n },\n\n async delete(key: string): Promise<void> {\n try {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE_NAME, \"readwrite\");\n const store = tx.objectStore(STORE_NAME);\n const request = store.delete(key);\n request.onsuccess = () => resolve();\n request.onerror = () => reject(request.error);\n });\n } catch {\n // Swallow errors\n }\n },\n\n async clear(): Promise<void> {\n try {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE_NAME, \"readwrite\");\n const store = tx.objectStore(STORE_NAME);\n const request = store.clear();\n request.onsuccess = () => resolve();\n request.onerror = () => reject(request.error);\n });\n } catch {\n // Swallow errors\n }\n },\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/adapters/indexeddb.ts","../src/allowlist.ts","../src/fetch-plugin.ts","../src/online-status.ts"],"sourcesContent":["import type { BetterAuthClientPlugin } from \"better-auth/client\";\nimport { createIndexedDBAdapter } from \"./adapters/indexeddb.js\";\nimport { createOfflineFetchPlugin } from \"./fetch-plugin.js\";\nimport { createOnlineStatusAtom } from \"./online-status.js\";\nimport type { OfflinePluginOptions, StorageAdapter } from \"./types.js\";\n\nexport { createIndexedDBAdapter } from \"./adapters/indexeddb.js\";\nexport { createOnlineStatusAtom } from \"./online-status.js\";\n// Re-export types for consumers\nexport type {\n CacheEntry,\n OfflinePluginOptions,\n StorageAdapter,\n} from \"./types.js\";\n\nconst SIGN_OUT_PATHS = [\"/sign-out\", \"/signout\", \"/logout\"];\nconst SIGN_IN_PATHS = [\"/sign-in\", \"/signin\", \"/login\"];\n\nfunction isAuthChangePath(path: string): boolean {\n return (\n SIGN_OUT_PATHS.some((p) => path.includes(p)) ||\n SIGN_IN_PATHS.some((p) => path.includes(p))\n );\n}\n\n/**\n * better-auth offline plugin.\n *\n * Transparently caches GET API responses and serves them when offline.\n * Drop-in: no consumer code changes required.\n *\n * @example\n * ```ts\n * import { createAuthClient } from \"better-auth/client\";\n * import { offlinePlugin } from \"better-auth-offline\";\n *\n * const authClient = createAuthClient({\n * plugins: [offlinePlugin()],\n * });\n * ```\n */\nexport function offlinePlugin(\n options: OfflinePluginOptions = {}\n): BetterAuthClientPlugin {\n const storage: StorageAdapter = options.storage ?? createIndexedDBAdapter();\n\n return {\n id: \"better-auth-offline\",\n\n getAtoms() {\n return {\n onlineStatus: createOnlineStatusAtom(),\n };\n },\n\n fetchPlugins: [createOfflineFetchPlugin(storage, options)],\n\n atomListeners: [\n {\n matcher(path: string) {\n return isAuthChangePath(path);\n },\n signal: \"$sessionSignal\",\n callback() {\n // Clear cache on sign-out / sign-in to prevent cross-user data leaks\n storage.clear();\n },\n },\n ],\n\n getActions() {\n return {\n clearCache: () => storage.clear(),\n };\n },\n };\n}\n","import type { CacheEntry, StorageAdapter } from \"../types.js\";\n\nconst DEFAULT_DB_NAME = \"better-auth-offline\";\nconst STORE_NAME = \"cache\";\n\n/**\n * Creates an IndexedDB-backed storage adapter.\n *\n * @param dbName - Name of the IndexedDB database. Defaults to \"better-auth-offline\".\n */\nexport function createIndexedDBAdapter(\n dbName: string = DEFAULT_DB_NAME\n): StorageAdapter {\n let dbPromise: Promise<IDBDatabase> | null = null;\n\n function openDB(): Promise<IDBDatabase> {\n if (dbPromise) {\n return dbPromise;\n }\n\n dbPromise = new Promise<IDBDatabase>((resolve, reject) => {\n const request = indexedDB.open(dbName, 1);\n\n request.onupgradeneeded = () => {\n const db = request.result;\n if (!db.objectStoreNames.contains(STORE_NAME)) {\n db.createObjectStore(STORE_NAME);\n }\n };\n\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => {\n dbPromise = null;\n reject(request.error);\n };\n });\n\n return dbPromise;\n }\n\n return {\n async get(key: string): Promise<CacheEntry | null> {\n try {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE_NAME, \"readonly\");\n const store = tx.objectStore(STORE_NAME);\n const request = store.get(key);\n request.onsuccess = () => resolve(request.result ?? null);\n request.onerror = () => reject(request.error);\n });\n } catch {\n return null;\n }\n },\n\n async set(key: string, value: unknown): Promise<void> {\n try {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE_NAME, \"readwrite\");\n const store = tx.objectStore(STORE_NAME);\n const request = store.put(value, key);\n request.onsuccess = () => resolve();\n request.onerror = () => reject(request.error);\n });\n } catch {\n // Fire-and-forget: swallow errors for cache writes\n }\n },\n\n async delete(key: string): Promise<void> {\n try {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE_NAME, \"readwrite\");\n const store = tx.objectStore(STORE_NAME);\n const request = store.delete(key);\n request.onsuccess = () => resolve();\n request.onerror = () => reject(request.error);\n });\n } catch {\n // Swallow errors\n }\n },\n\n async clear(): Promise<void> {\n try {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE_NAME, \"readwrite\");\n const store = tx.objectStore(STORE_NAME);\n const request = store.clear();\n request.onsuccess = () => resolve();\n request.onerror = () => reject(request.error);\n });\n } catch {\n // Swallow errors\n }\n },\n };\n}\n","import type { OfflinePluginOptions } from \"./types.js\";\n\n/**\n * Default paths that are cached for offline access.\n * Only GET requests to these paths will be cached.\n */\nconst DEFAULT_ALLOWLIST = [\n // Core\n \"/get-session\",\n \"/list-sessions\",\n \"/list-accounts\",\n \"/account-info\",\n // Organization\n \"/organization/list\",\n \"/organization/get-active-member\",\n \"/organization/get-active-member-role\",\n \"/organization/get-full-organization\",\n \"/organization/list-members\",\n \"/organization/list-teams\",\n \"/organization/list-invitations\",\n // Admin\n \"/admin/list-users\",\n // Multi-session\n \"/multi-session/list-device-sessions\",\n // Passkey\n \"/passkey/list-user-passkeys\",\n // API Key\n \"/api-key/get\",\n \"/api-key/list\",\n];\n\n/**\n * Get the effective allowlist based on plugin options.\n */\nexport function getAllowlist(options: OfflinePluginOptions): string[] {\n if (options.mode === \"custom\") {\n return options.allowlist;\n }\n const exclude = new Set(options.excludePaths ?? []);\n const base =\n exclude.size > 0\n ? DEFAULT_ALLOWLIST.filter((p) => !exclude.has(p))\n : DEFAULT_ALLOWLIST;\n return [...base, ...(options.includePaths ?? [])];\n}\n\n/**\n * Check if a path should be cached.\n * Uses suffix matching to handle configurable base path prefixes.\n */\nexport function isAllowlisted(path: string, allowlist: string[]): boolean {\n return allowlist.some(\n (allowed) =>\n path === allowed || path.endsWith(allowed) || path.includes(`${allowed}/`)\n );\n}\n","import type { BetterFetchPlugin } from \"@better-fetch/fetch\";\nimport { getAllowlist, isAllowlisted } from \"./allowlist.js\";\nimport type {\n CacheEntry,\n OfflinePluginOptions,\n StorageAdapter,\n} from \"./types.js\";\n\n/**\n * Extracts the pathname from a URL string, stripping query params.\n * Used as the cache key.\n */\nexport function extractPath(urlOrPath: string | URL): string {\n try {\n const url =\n typeof urlOrPath === \"string\" && urlOrPath.startsWith(\"http\")\n ? new URL(urlOrPath)\n : null;\n if (url) {\n return url.pathname;\n }\n } catch {\n // Not a valid URL, treat as path\n }\n // Strip query string from path\n const str = typeof urlOrPath === \"string\" ? urlOrPath : urlOrPath.pathname;\n const qIndex = str.indexOf(\"?\");\n return qIndex >= 0 ? str.slice(0, qIndex) : str;\n}\n\n/**\n * Checks if a network error is a connectivity failure (not an HTTP error).\n */\nfunction isNetworkError(error: unknown): boolean {\n if (error instanceof TypeError) {\n return true;\n }\n if (error instanceof DOMException && error.name === \"AbortError\") {\n return true;\n }\n return false;\n}\n\n/**\n * Creates the BetterFetchPlugin that provides offline caching.\n *\n * Strategy: Network-First with Cache Fallback\n * - Uses `init` to inject a custom fetch that wraps the real fetch\n * - On successful GET: caches the response body (fire-and-forget)\n * - On network error for GET: serves cached response if available\n * - Only allowlisted GET paths are cached; everything else passes through\n */\nexport function createOfflineFetchPlugin(\n storage: StorageAdapter,\n options: OfflinePluginOptions\n): BetterFetchPlugin {\n const allowlist = getAllowlist(options);\n\n return {\n id: \"better-auth-offline\",\n name: \"better-auth-offline\",\n\n init(url, fetchOptions) {\n const originalFetch = fetchOptions?.customFetchImpl ?? globalThis.fetch;\n const method = (fetchOptions?.method ?? \"GET\").toUpperCase();\n const path = extractPath(url);\n const shouldCache = method === \"GET\" && isAllowlisted(path, allowlist);\n\n const wrappedFetch: typeof globalThis.fetch = async (input, init) => {\n if (!shouldCache) {\n return originalFetch(input, init);\n }\n\n try {\n const response = await originalFetch(input, init);\n\n // Cache successful GET responses (fire-and-forget)\n if (response.ok) {\n // Clone before reading body so the original response remains usable\n const clone = response.clone();\n clone\n .json()\n .then((data) => {\n const entry: CacheEntry = { data, cachedAt: Date.now() };\n storage.set(path, entry);\n })\n .catch(() => {\n // Response wasn't JSON or read failed — skip caching\n });\n }\n\n return response;\n } catch (error) {\n // Network error — try serving from cache\n if (isNetworkError(error)) {\n const cached = (await storage.get(path)) as CacheEntry | null;\n if (cached) {\n return new Response(JSON.stringify(cached.data), {\n status: 200,\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-Offline-Cache\": \"true\",\n },\n });\n }\n }\n // No cache hit or not a network error — rethrow\n throw error;\n }\n };\n\n // Mutate the original options object directly rather than spreading\n // into a new one. @better-fetch/fetch's initializePlugins passes the\n // same `options` reference to every plugin's init(). If we return a\n // new object, a later plugin (e.g. applySchemaPlugin) that returns\n // the original reference will overwrite our customFetchImpl, silently\n // disabling the offline cache. By mutating in place, the wrapping\n // survives regardless of plugin ordering.\n if (fetchOptions) {\n fetchOptions.customFetchImpl = wrappedFetch;\n }\n\n return {\n url,\n options: fetchOptions ?? { customFetchImpl: wrappedFetch },\n };\n },\n };\n}\n","import { atom } from \"nanostores\";\n\n/**\n * Creates a reactive atom that tracks the browser's online/offline status.\n * Defaults to `true` in non-browser environments (SSR).\n */\nexport function createOnlineStatusAtom() {\n const isOnline = atom<boolean>(\n typeof navigator !== \"undefined\" && typeof navigator.onLine === \"boolean\"\n ? navigator.onLine\n : true\n );\n\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"online\", () => isOnline.set(true));\n window.addEventListener(\"offline\", () => isOnline.set(false));\n }\n\n return isOnline;\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { BetterAuthClientPlugin } from 'better-auth/client';
2
- import { O as OfflinePluginOptions } from './indexeddb-CkUWH5Sl.cjs';
3
- export { C as CacheEntry, S as StorageAdapter, c as createIndexedDBAdapter } from './indexeddb-CkUWH5Sl.cjs';
2
+ import { O as OfflinePluginOptions } from './indexeddb-B7LON8ue.cjs';
3
+ export { C as CacheEntry, S as StorageAdapter, c as createIndexedDBAdapter } from './indexeddb-B7LON8ue.cjs';
4
4
  import * as nanostores from 'nanostores';
5
5
 
6
6
  /**
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { BetterAuthClientPlugin } from 'better-auth/client';
2
- import { O as OfflinePluginOptions } from './indexeddb-CkUWH5Sl.js';
3
- export { C as CacheEntry, S as StorageAdapter, c as createIndexedDBAdapter } from './indexeddb-CkUWH5Sl.js';
2
+ import { O as OfflinePluginOptions } from './indexeddb-B7LON8ue.js';
3
+ export { C as CacheEntry, S as StorageAdapter, c as createIndexedDBAdapter } from './indexeddb-B7LON8ue.js';
4
4
  import * as nanostores from 'nanostores';
5
5
 
6
6
  /**
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createIndexedDBAdapter
3
- } from "./chunk-JPTDSCSW.js";
3
+ } from "./chunk-NFJRQCGL.js";
4
4
 
5
5
  // src/allowlist.ts
6
6
  var DEFAULT_ALLOWLIST = [
@@ -37,7 +37,7 @@ function getAllowlist(options) {
37
37
  }
38
38
  function isAllowlisted(path, allowlist) {
39
39
  return allowlist.some(
40
- (allowed) => path === allowed || path.endsWith(allowed) || path.includes(allowed + "/")
40
+ (allowed) => path === allowed || path.endsWith(allowed) || path.includes(`${allowed}/`)
41
41
  );
42
42
  }
43
43
 
@@ -45,7 +45,9 @@ function isAllowlisted(path, allowlist) {
45
45
  function extractPath(urlOrPath) {
46
46
  try {
47
47
  const url = typeof urlOrPath === "string" && urlOrPath.startsWith("http") ? new URL(urlOrPath) : null;
48
- if (url) return url.pathname;
48
+ if (url) {
49
+ return url.pathname;
50
+ }
49
51
  } catch {
50
52
  }
51
53
  const str = typeof urlOrPath === "string" ? urlOrPath : urlOrPath.pathname;
@@ -53,8 +55,12 @@ function extractPath(urlOrPath) {
53
55
  return qIndex >= 0 ? str.slice(0, qIndex) : str;
54
56
  }
55
57
  function isNetworkError(error) {
56
- if (error instanceof TypeError) return true;
57
- if (error instanceof DOMException && error.name === "AbortError") return true;
58
+ if (error instanceof TypeError) {
59
+ return true;
60
+ }
61
+ if (error instanceof DOMException && error.name === "AbortError") {
62
+ return true;
63
+ }
58
64
  return false;
59
65
  }
60
66
  function createOfflineFetchPlugin(storage, options) {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/allowlist.ts","../src/fetch-plugin.ts","../src/online-status.ts","../src/index.ts"],"sourcesContent":["import type { OfflinePluginOptions } from \"./types.js\";\n\n/**\n * Default paths that are cached for offline access.\n * Only GET requests to these paths will be cached.\n */\nconst DEFAULT_ALLOWLIST = [\n // Core\n \"/get-session\",\n \"/list-sessions\",\n \"/list-accounts\",\n \"/account-info\",\n // Organization\n \"/organization/list\",\n \"/organization/get-active-member\",\n \"/organization/get-active-member-role\",\n \"/organization/get-full-organization\",\n \"/organization/list-members\",\n \"/organization/list-teams\",\n \"/organization/list-invitations\",\n // Admin\n \"/admin/list-users\",\n // Multi-session\n \"/multi-session/list-device-sessions\",\n // Passkey\n \"/passkey/list-user-passkeys\",\n // API Key\n \"/api-key/get\",\n \"/api-key/list\",\n];\n\n/**\n * Get the effective allowlist based on plugin options.\n */\nexport function getAllowlist(options: OfflinePluginOptions): string[] {\n if (options.mode === \"custom\") {\n return options.allowlist;\n }\n const exclude = new Set(options.excludePaths ?? []);\n const base = exclude.size > 0\n ? DEFAULT_ALLOWLIST.filter((p) => !exclude.has(p))\n : DEFAULT_ALLOWLIST;\n return [...base, ...(options.includePaths ?? [])];\n}\n\n/**\n * Check if a path should be cached.\n * Uses suffix matching to handle configurable base path prefixes.\n */\nexport function isAllowlisted(\n path: string,\n allowlist: string[],\n): boolean {\n return allowlist.some(\n (allowed) =>\n path === allowed ||\n path.endsWith(allowed) ||\n path.includes(allowed + \"/\"),\n );\n}\n","import type { BetterFetchPlugin } from \"@better-fetch/fetch\";\nimport type { StorageAdapter, CacheEntry, OfflinePluginOptions } from \"./types.js\";\nimport { getAllowlist, isAllowlisted } from \"./allowlist.js\";\n\n/**\n * Extracts the pathname from a URL string, stripping query params.\n * Used as the cache key.\n */\nexport function extractPath(urlOrPath: string | URL): string {\n try {\n const url = typeof urlOrPath === \"string\" && urlOrPath.startsWith(\"http\")\n ? new URL(urlOrPath)\n : null;\n if (url) return url.pathname;\n } catch {\n // Not a valid URL, treat as path\n }\n // Strip query string from path\n const str = typeof urlOrPath === \"string\" ? urlOrPath : urlOrPath.pathname;\n const qIndex = str.indexOf(\"?\");\n return qIndex >= 0 ? str.slice(0, qIndex) : str;\n}\n\n/**\n * Checks if a network error is a connectivity failure (not an HTTP error).\n */\nfunction isNetworkError(error: unknown): boolean {\n if (error instanceof TypeError) return true;\n if (error instanceof DOMException && error.name === \"AbortError\") return true;\n return false;\n}\n\n/**\n * Creates the BetterFetchPlugin that provides offline caching.\n *\n * Strategy: Network-First with Cache Fallback\n * - Uses `init` to inject a custom fetch that wraps the real fetch\n * - On successful GET: caches the response body (fire-and-forget)\n * - On network error for GET: serves cached response if available\n * - Only allowlisted GET paths are cached; everything else passes through\n */\nexport function createOfflineFetchPlugin(\n storage: StorageAdapter,\n options: OfflinePluginOptions,\n): BetterFetchPlugin {\n const allowlist = getAllowlist(options);\n\n return {\n id: \"better-auth-offline\",\n name: \"better-auth-offline\",\n\n init(url, fetchOptions) {\n const originalFetch = fetchOptions?.customFetchImpl ?? globalThis.fetch;\n const method = (fetchOptions?.method ?? \"GET\").toUpperCase();\n const path = extractPath(url);\n const shouldCache = method === \"GET\" && isAllowlisted(path, allowlist);\n\n const wrappedFetch: typeof globalThis.fetch = async (input, init) => {\n if (!shouldCache) {\n return originalFetch(input, init);\n }\n\n try {\n const response = await originalFetch(input, init);\n\n // Cache successful GET responses (fire-and-forget)\n if (response.ok) {\n // Clone before reading body so the original response remains usable\n const clone = response.clone();\n clone.json().then((data) => {\n const entry: CacheEntry = { data, cachedAt: Date.now() };\n storage.set(path, entry);\n }).catch(() => {\n // Response wasn't JSON or read failed — skip caching\n });\n }\n\n return response;\n } catch (error) {\n // Network error — try serving from cache\n if (isNetworkError(error)) {\n const cached = await storage.get(path) as CacheEntry | null;\n if (cached) {\n return new Response(JSON.stringify(cached.data), {\n status: 200,\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-Offline-Cache\": \"true\",\n },\n });\n }\n }\n // No cache hit or not a network error — rethrow\n throw error;\n }\n };\n\n // Mutate the original options object directly rather than spreading\n // into a new one. @better-fetch/fetch's initializePlugins passes the\n // same `options` reference to every plugin's init(). If we return a\n // new object, a later plugin (e.g. applySchemaPlugin) that returns\n // the original reference will overwrite our customFetchImpl, silently\n // disabling the offline cache. By mutating in place, the wrapping\n // survives regardless of plugin ordering.\n if (fetchOptions) {\n fetchOptions.customFetchImpl = wrappedFetch;\n }\n\n return {\n url,\n options: fetchOptions ?? { customFetchImpl: wrappedFetch },\n };\n },\n };\n}\n","import { atom } from \"nanostores\";\n\n/**\n * Creates a reactive atom that tracks the browser's online/offline status.\n * Defaults to `true` in non-browser environments (SSR).\n */\nexport function createOnlineStatusAtom() {\n const isOnline = atom<boolean>(\n typeof navigator !== \"undefined\" && typeof navigator.onLine === \"boolean\"\n ? navigator.onLine\n : true,\n );\n\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"online\", () => isOnline.set(true));\n window.addEventListener(\"offline\", () => isOnline.set(false));\n }\n\n return isOnline;\n}\n","import type { BetterAuthClientPlugin } from \"better-auth/client\";\nimport type { OfflinePluginOptions, StorageAdapter } from \"./types.js\";\nimport { createOfflineFetchPlugin } from \"./fetch-plugin.js\";\nimport { createOnlineStatusAtom } from \"./online-status.js\";\nimport { createIndexedDBAdapter } from \"./adapters/indexeddb.js\";\n\n// Re-export types for consumers\nexport type { StorageAdapter, OfflinePluginOptions, CacheEntry } from \"./types.js\";\nexport { createIndexedDBAdapter } from \"./adapters/indexeddb.js\";\nexport { createOnlineStatusAtom } from \"./online-status.js\";\n\nconst SIGN_OUT_PATHS = [\"/sign-out\", \"/signout\", \"/logout\"];\nconst SIGN_IN_PATHS = [\"/sign-in\", \"/signin\", \"/login\"];\n\nfunction isAuthChangePath(path: string): boolean {\n return (\n SIGN_OUT_PATHS.some((p) => path.includes(p)) ||\n SIGN_IN_PATHS.some((p) => path.includes(p))\n );\n}\n\n/**\n * better-auth offline plugin.\n *\n * Transparently caches GET API responses and serves them when offline.\n * Drop-in: no consumer code changes required.\n *\n * @example\n * ```ts\n * import { createAuthClient } from \"better-auth/client\";\n * import { offlinePlugin } from \"better-auth-offline\";\n *\n * const authClient = createAuthClient({\n * plugins: [offlinePlugin()],\n * });\n * ```\n */\nexport function offlinePlugin(\n options: OfflinePluginOptions = {},\n): BetterAuthClientPlugin {\n const storage: StorageAdapter =\n options.storage ?? createIndexedDBAdapter();\n\n return {\n id: \"better-auth-offline\",\n\n getAtoms() {\n return {\n onlineStatus: createOnlineStatusAtom(),\n };\n },\n\n fetchPlugins: [createOfflineFetchPlugin(storage, options)],\n\n atomListeners: [\n {\n matcher(path: string) {\n return isAuthChangePath(path);\n },\n signal: \"$sessionSignal\",\n callback() {\n // Clear cache on sign-out / sign-in to prevent cross-user data leaks\n storage.clear();\n },\n },\n ],\n\n getActions() {\n return {\n clearCache: () => storage.clear(),\n };\n },\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/allowlist.ts","../src/fetch-plugin.ts","../src/online-status.ts","../src/index.ts"],"sourcesContent":["import type { OfflinePluginOptions } from \"./types.js\";\n\n/**\n * Default paths that are cached for offline access.\n * Only GET requests to these paths will be cached.\n */\nconst DEFAULT_ALLOWLIST = [\n // Core\n \"/get-session\",\n \"/list-sessions\",\n \"/list-accounts\",\n \"/account-info\",\n // Organization\n \"/organization/list\",\n \"/organization/get-active-member\",\n \"/organization/get-active-member-role\",\n \"/organization/get-full-organization\",\n \"/organization/list-members\",\n \"/organization/list-teams\",\n \"/organization/list-invitations\",\n // Admin\n \"/admin/list-users\",\n // Multi-session\n \"/multi-session/list-device-sessions\",\n // Passkey\n \"/passkey/list-user-passkeys\",\n // API Key\n \"/api-key/get\",\n \"/api-key/list\",\n];\n\n/**\n * Get the effective allowlist based on plugin options.\n */\nexport function getAllowlist(options: OfflinePluginOptions): string[] {\n if (options.mode === \"custom\") {\n return options.allowlist;\n }\n const exclude = new Set(options.excludePaths ?? []);\n const base =\n exclude.size > 0\n ? DEFAULT_ALLOWLIST.filter((p) => !exclude.has(p))\n : DEFAULT_ALLOWLIST;\n return [...base, ...(options.includePaths ?? [])];\n}\n\n/**\n * Check if a path should be cached.\n * Uses suffix matching to handle configurable base path prefixes.\n */\nexport function isAllowlisted(path: string, allowlist: string[]): boolean {\n return allowlist.some(\n (allowed) =>\n path === allowed || path.endsWith(allowed) || path.includes(`${allowed}/`)\n );\n}\n","import type { BetterFetchPlugin } from \"@better-fetch/fetch\";\nimport { getAllowlist, isAllowlisted } from \"./allowlist.js\";\nimport type {\n CacheEntry,\n OfflinePluginOptions,\n StorageAdapter,\n} from \"./types.js\";\n\n/**\n * Extracts the pathname from a URL string, stripping query params.\n * Used as the cache key.\n */\nexport function extractPath(urlOrPath: string | URL): string {\n try {\n const url =\n typeof urlOrPath === \"string\" && urlOrPath.startsWith(\"http\")\n ? new URL(urlOrPath)\n : null;\n if (url) {\n return url.pathname;\n }\n } catch {\n // Not a valid URL, treat as path\n }\n // Strip query string from path\n const str = typeof urlOrPath === \"string\" ? urlOrPath : urlOrPath.pathname;\n const qIndex = str.indexOf(\"?\");\n return qIndex >= 0 ? str.slice(0, qIndex) : str;\n}\n\n/**\n * Checks if a network error is a connectivity failure (not an HTTP error).\n */\nfunction isNetworkError(error: unknown): boolean {\n if (error instanceof TypeError) {\n return true;\n }\n if (error instanceof DOMException && error.name === \"AbortError\") {\n return true;\n }\n return false;\n}\n\n/**\n * Creates the BetterFetchPlugin that provides offline caching.\n *\n * Strategy: Network-First with Cache Fallback\n * - Uses `init` to inject a custom fetch that wraps the real fetch\n * - On successful GET: caches the response body (fire-and-forget)\n * - On network error for GET: serves cached response if available\n * - Only allowlisted GET paths are cached; everything else passes through\n */\nexport function createOfflineFetchPlugin(\n storage: StorageAdapter,\n options: OfflinePluginOptions\n): BetterFetchPlugin {\n const allowlist = getAllowlist(options);\n\n return {\n id: \"better-auth-offline\",\n name: \"better-auth-offline\",\n\n init(url, fetchOptions) {\n const originalFetch = fetchOptions?.customFetchImpl ?? globalThis.fetch;\n const method = (fetchOptions?.method ?? \"GET\").toUpperCase();\n const path = extractPath(url);\n const shouldCache = method === \"GET\" && isAllowlisted(path, allowlist);\n\n const wrappedFetch: typeof globalThis.fetch = async (input, init) => {\n if (!shouldCache) {\n return originalFetch(input, init);\n }\n\n try {\n const response = await originalFetch(input, init);\n\n // Cache successful GET responses (fire-and-forget)\n if (response.ok) {\n // Clone before reading body so the original response remains usable\n const clone = response.clone();\n clone\n .json()\n .then((data) => {\n const entry: CacheEntry = { data, cachedAt: Date.now() };\n storage.set(path, entry);\n })\n .catch(() => {\n // Response wasn't JSON or read failed — skip caching\n });\n }\n\n return response;\n } catch (error) {\n // Network error — try serving from cache\n if (isNetworkError(error)) {\n const cached = (await storage.get(path)) as CacheEntry | null;\n if (cached) {\n return new Response(JSON.stringify(cached.data), {\n status: 200,\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-Offline-Cache\": \"true\",\n },\n });\n }\n }\n // No cache hit or not a network error — rethrow\n throw error;\n }\n };\n\n // Mutate the original options object directly rather than spreading\n // into a new one. @better-fetch/fetch's initializePlugins passes the\n // same `options` reference to every plugin's init(). If we return a\n // new object, a later plugin (e.g. applySchemaPlugin) that returns\n // the original reference will overwrite our customFetchImpl, silently\n // disabling the offline cache. By mutating in place, the wrapping\n // survives regardless of plugin ordering.\n if (fetchOptions) {\n fetchOptions.customFetchImpl = wrappedFetch;\n }\n\n return {\n url,\n options: fetchOptions ?? { customFetchImpl: wrappedFetch },\n };\n },\n };\n}\n","import { atom } from \"nanostores\";\n\n/**\n * Creates a reactive atom that tracks the browser's online/offline status.\n * Defaults to `true` in non-browser environments (SSR).\n */\nexport function createOnlineStatusAtom() {\n const isOnline = atom<boolean>(\n typeof navigator !== \"undefined\" && typeof navigator.onLine === \"boolean\"\n ? navigator.onLine\n : true\n );\n\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"online\", () => isOnline.set(true));\n window.addEventListener(\"offline\", () => isOnline.set(false));\n }\n\n return isOnline;\n}\n","import type { BetterAuthClientPlugin } from \"better-auth/client\";\nimport { createIndexedDBAdapter } from \"./adapters/indexeddb.js\";\nimport { createOfflineFetchPlugin } from \"./fetch-plugin.js\";\nimport { createOnlineStatusAtom } from \"./online-status.js\";\nimport type { OfflinePluginOptions, StorageAdapter } from \"./types.js\";\n\nexport { createIndexedDBAdapter } from \"./adapters/indexeddb.js\";\nexport { createOnlineStatusAtom } from \"./online-status.js\";\n// Re-export types for consumers\nexport type {\n CacheEntry,\n OfflinePluginOptions,\n StorageAdapter,\n} from \"./types.js\";\n\nconst SIGN_OUT_PATHS = [\"/sign-out\", \"/signout\", \"/logout\"];\nconst SIGN_IN_PATHS = [\"/sign-in\", \"/signin\", \"/login\"];\n\nfunction isAuthChangePath(path: string): boolean {\n return (\n SIGN_OUT_PATHS.some((p) => path.includes(p)) ||\n SIGN_IN_PATHS.some((p) => path.includes(p))\n );\n}\n\n/**\n * better-auth offline plugin.\n *\n * Transparently caches GET API responses and serves them when offline.\n * Drop-in: no consumer code changes required.\n *\n * @example\n * ```ts\n * import { createAuthClient } from \"better-auth/client\";\n * import { offlinePlugin } from \"better-auth-offline\";\n *\n * const authClient = createAuthClient({\n * plugins: [offlinePlugin()],\n * });\n * ```\n */\nexport function offlinePlugin(\n options: OfflinePluginOptions = {}\n): BetterAuthClientPlugin {\n const storage: StorageAdapter = options.storage ?? createIndexedDBAdapter();\n\n return {\n id: \"better-auth-offline\",\n\n getAtoms() {\n return {\n onlineStatus: createOnlineStatusAtom(),\n };\n },\n\n fetchPlugins: [createOfflineFetchPlugin(storage, options)],\n\n atomListeners: [\n {\n matcher(path: string) {\n return isAuthChangePath(path);\n },\n signal: \"$sessionSignal\",\n callback() {\n // Clear cache on sign-out / sign-in to prevent cross-user data leaks\n storage.clear();\n },\n },\n ],\n\n getActions() {\n return {\n clearCache: () => storage.clear(),\n };\n },\n };\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "better-auth-offline",
3
- "version": "0.0.0",
3
+ "version": "0.0.2",
4
4
  "description": "Caches better-auth API responses so your app keeps working when the network doesn't.",
5
5
  "author": {
6
6
  "name": "Roman Sirokov",
@@ -10,7 +10,7 @@
10
10
  "license": "MIT",
11
11
  "repository": {
12
12
  "type": "git",
13
- "url": "https://github.com/MrLightful/better-auth-offline.git"
13
+ "url": "git+https://github.com/MrLightful/better-auth-offline.git"
14
14
  },
15
15
  "homepage": "https://github.com/MrLightful/better-auth-offline",
16
16
  "bugs": {
@@ -46,15 +46,21 @@
46
46
  }
47
47
  },
48
48
  "files": [
49
- "dist"
49
+ "dist",
50
+ "README.md",
51
+ "LICENSE"
50
52
  ],
51
53
  "scripts": {
52
54
  "build": "bun run tsup",
53
55
  "dev": "bun run tsup --watch",
54
56
  "test": "bun run vitest run",
55
57
  "test:watch": "bun run vitest",
56
- "typecheck": "bun run tsc --noEmit"
58
+ "typecheck": "bun run tsc --noEmit",
59
+ "check": "ultracite check",
60
+ "fix": "ultracite fix",
61
+ "prepare": "husky"
57
62
  },
63
+ "packageManager": "bun@1.3.10",
58
64
  "peerDependencies": {
59
65
  "better-auth": ">=1.0.0"
60
66
  },
@@ -63,10 +69,20 @@
63
69
  },
64
70
  "devDependencies": {
65
71
  "@better-fetch/fetch": "^1.1.21",
72
+ "@biomejs/biome": "2.4.7",
73
+ "@changesets/cli": "^2.30.0",
66
74
  "better-auth": "^1.5.6",
67
75
  "fake-indexeddb": "^6.2.5",
76
+ "husky": "^9.1.7",
77
+ "lint-staged": "^16.4.0",
68
78
  "tsup": "^8.5.1",
69
79
  "typescript": "^5.9.3",
80
+ "ultracite": "7.3.2",
70
81
  "vitest": "^3.2.4"
82
+ },
83
+ "lint-staged": {
84
+ "*.{js,jsx,ts,tsx,json,jsonc,css,scss,md,mdx}": [
85
+ "bun x ultracite fix"
86
+ ]
71
87
  }
72
88
  }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/adapters/indexeddb.ts"],"sourcesContent":["import type { StorageAdapter, CacheEntry } from \"../types.js\";\n\nconst DEFAULT_DB_NAME = \"better-auth-offline\";\nconst STORE_NAME = \"cache\";\n\n/**\n * Creates an IndexedDB-backed storage adapter.\n *\n * @param dbName - Name of the IndexedDB database. Defaults to \"better-auth-offline\".\n */\nexport function createIndexedDBAdapter(\n dbName: string = DEFAULT_DB_NAME,\n): StorageAdapter {\n let dbPromise: Promise<IDBDatabase> | null = null;\n\n function openDB(): Promise<IDBDatabase> {\n if (dbPromise) return dbPromise;\n\n dbPromise = new Promise<IDBDatabase>((resolve, reject) => {\n const request = indexedDB.open(dbName, 1);\n\n request.onupgradeneeded = () => {\n const db = request.result;\n if (!db.objectStoreNames.contains(STORE_NAME)) {\n db.createObjectStore(STORE_NAME);\n }\n };\n\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => {\n dbPromise = null;\n reject(request.error);\n };\n });\n\n return dbPromise;\n }\n\n return {\n async get(key: string): Promise<CacheEntry | null> {\n try {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE_NAME, \"readonly\");\n const store = tx.objectStore(STORE_NAME);\n const request = store.get(key);\n request.onsuccess = () => resolve(request.result ?? null);\n request.onerror = () => reject(request.error);\n });\n } catch {\n return null;\n }\n },\n\n async set(key: string, value: unknown): Promise<void> {\n try {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE_NAME, \"readwrite\");\n const store = tx.objectStore(STORE_NAME);\n const request = store.put(value, key);\n request.onsuccess = () => resolve();\n request.onerror = () => reject(request.error);\n });\n } catch {\n // Fire-and-forget: swallow errors for cache writes\n }\n },\n\n async delete(key: string): Promise<void> {\n try {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE_NAME, \"readwrite\");\n const store = tx.objectStore(STORE_NAME);\n const request = store.delete(key);\n request.onsuccess = () => resolve();\n request.onerror = () => reject(request.error);\n });\n } catch {\n // Swallow errors\n }\n },\n\n async clear(): Promise<void> {\n try {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE_NAME, \"readwrite\");\n const store = tx.objectStore(STORE_NAME);\n const request = store.clear();\n request.onsuccess = () => resolve();\n request.onerror = () => reject(request.error);\n });\n } catch {\n // Swallow errors\n }\n },\n };\n}\n"],"mappings":";AAEA,IAAM,kBAAkB;AACxB,IAAM,aAAa;AAOZ,SAAS,uBACd,SAAiB,iBACD;AAChB,MAAI,YAAyC;AAE7C,WAAS,SAA+B;AACtC,QAAI,UAAW,QAAO;AAEtB,gBAAY,IAAI,QAAqB,CAAC,SAAS,WAAW;AACxD,YAAM,UAAU,UAAU,KAAK,QAAQ,CAAC;AAExC,cAAQ,kBAAkB,MAAM;AAC9B,cAAM,KAAK,QAAQ;AACnB,YAAI,CAAC,GAAG,iBAAiB,SAAS,UAAU,GAAG;AAC7C,aAAG,kBAAkB,UAAU;AAAA,QACjC;AAAA,MACF;AAEA,cAAQ,YAAY,MAAM,QAAQ,QAAQ,MAAM;AAChD,cAAQ,UAAU,MAAM;AACtB,oBAAY;AACZ,eAAO,QAAQ,KAAK;AAAA,MACtB;AAAA,IACF,CAAC;AAED,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,MAAM,IAAI,KAAyC;AACjD,UAAI;AACF,cAAM,KAAK,MAAM,OAAO;AACxB,eAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,gBAAM,KAAK,GAAG,YAAY,YAAY,UAAU;AAChD,gBAAM,QAAQ,GAAG,YAAY,UAAU;AACvC,gBAAM,UAAU,MAAM,IAAI,GAAG;AAC7B,kBAAQ,YAAY,MAAM,QAAQ,QAAQ,UAAU,IAAI;AACxD,kBAAQ,UAAU,MAAM,OAAO,QAAQ,KAAK;AAAA,QAC9C,CAAC;AAAA,MACH,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF;AAAA,IAEA,MAAM,IAAI,KAAa,OAA+B;AACpD,UAAI;AACF,cAAM,KAAK,MAAM,OAAO;AACxB,eAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,gBAAM,KAAK,GAAG,YAAY,YAAY,WAAW;AACjD,gBAAM,QAAQ,GAAG,YAAY,UAAU;AACvC,gBAAM,UAAU,MAAM,IAAI,OAAO,GAAG;AACpC,kBAAQ,YAAY,MAAM,QAAQ;AAClC,kBAAQ,UAAU,MAAM,OAAO,QAAQ,KAAK;AAAA,QAC9C,CAAC;AAAA,MACH,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,IAEA,MAAM,OAAO,KAA4B;AACvC,UAAI;AACF,cAAM,KAAK,MAAM,OAAO;AACxB,eAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,gBAAM,KAAK,GAAG,YAAY,YAAY,WAAW;AACjD,gBAAM,QAAQ,GAAG,YAAY,UAAU;AACvC,gBAAM,UAAU,MAAM,OAAO,GAAG;AAChC,kBAAQ,YAAY,MAAM,QAAQ;AAClC,kBAAQ,UAAU,MAAM,OAAO,QAAQ,KAAK;AAAA,QAC9C,CAAC;AAAA,MACH,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,IAEA,MAAM,QAAuB;AAC3B,UAAI;AACF,cAAM,KAAK,MAAM,OAAO;AACxB,eAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,gBAAM,KAAK,GAAG,YAAY,YAAY,WAAW;AACjD,gBAAM,QAAQ,GAAG,YAAY,UAAU;AACvC,gBAAM,UAAU,MAAM,MAAM;AAC5B,kBAAQ,YAAY,MAAM,QAAQ;AAClC,kBAAQ,UAAU,MAAM,OAAO,QAAQ,KAAK;AAAA,QAC9C,CAAC;AAAA,MACH,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
@@ -1,8 +1,8 @@
1
1
  interface StorageAdapter {
2
+ clear(): Promise<void>;
3
+ delete(key: string): Promise<void>;
2
4
  get(key: string): Promise<unknown | null>;
3
5
  set(key: string, value: unknown): Promise<void>;
4
- delete(key: string): Promise<void>;
5
- clear(): Promise<void>;
6
6
  }
7
7
  interface BaseOptions {
8
8
  /**
@@ -11,30 +11,30 @@ interface BaseOptions {
11
11
  storage?: StorageAdapter;
12
12
  }
13
13
  interface DefaultAllowlistOptions extends BaseOptions {
14
- mode?: "default";
15
- /**
16
- * Additional paths to cache (extends the default allowlist).
17
- */
18
- includePaths?: string[];
19
14
  /**
20
15
  * Paths to remove from the default allowlist.
21
16
  */
22
17
  excludePaths?: string[];
18
+ /**
19
+ * Additional paths to cache (extends the default allowlist).
20
+ */
21
+ includePaths?: string[];
22
+ mode?: "default";
23
23
  }
24
24
  interface CustomAllowlistOptions extends BaseOptions {
25
- mode: "custom";
26
25
  /**
27
26
  * Only these paths are cached (default allowlist is ignored).
28
27
  */
29
28
  allowlist: string[];
29
+ mode: "custom";
30
30
  }
31
31
  type OfflinePluginOptions = DefaultAllowlistOptions | CustomAllowlistOptions;
32
32
  /**
33
33
  * Shape of a cached response entry.
34
34
  */
35
35
  interface CacheEntry {
36
- data: unknown;
37
36
  cachedAt: number;
37
+ data: unknown;
38
38
  }
39
39
 
40
40
  /**
@@ -1,8 +1,8 @@
1
1
  interface StorageAdapter {
2
+ clear(): Promise<void>;
3
+ delete(key: string): Promise<void>;
2
4
  get(key: string): Promise<unknown | null>;
3
5
  set(key: string, value: unknown): Promise<void>;
4
- delete(key: string): Promise<void>;
5
- clear(): Promise<void>;
6
6
  }
7
7
  interface BaseOptions {
8
8
  /**
@@ -11,30 +11,30 @@ interface BaseOptions {
11
11
  storage?: StorageAdapter;
12
12
  }
13
13
  interface DefaultAllowlistOptions extends BaseOptions {
14
- mode?: "default";
15
- /**
16
- * Additional paths to cache (extends the default allowlist).
17
- */
18
- includePaths?: string[];
19
14
  /**
20
15
  * Paths to remove from the default allowlist.
21
16
  */
22
17
  excludePaths?: string[];
18
+ /**
19
+ * Additional paths to cache (extends the default allowlist).
20
+ */
21
+ includePaths?: string[];
22
+ mode?: "default";
23
23
  }
24
24
  interface CustomAllowlistOptions extends BaseOptions {
25
- mode: "custom";
26
25
  /**
27
26
  * Only these paths are cached (default allowlist is ignored).
28
27
  */
29
28
  allowlist: string[];
29
+ mode: "custom";
30
30
  }
31
31
  type OfflinePluginOptions = DefaultAllowlistOptions | CustomAllowlistOptions;
32
32
  /**
33
33
  * Shape of a cached response entry.
34
34
  */
35
35
  interface CacheEntry {
36
- data: unknown;
37
36
  cachedAt: number;
37
+ data: unknown;
38
38
  }
39
39
 
40
40
  /**