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 +21 -0
- package/README.md +248 -0
- package/dist/adapters/indexeddb.cjs +3 -1
- package/dist/adapters/indexeddb.cjs.map +1 -1
- package/dist/adapters/indexeddb.d.cts +1 -1
- package/dist/adapters/indexeddb.d.ts +1 -1
- package/dist/adapters/indexeddb.js +1 -1
- package/dist/{chunk-JPTDSCSW.js → chunk-NFJRQCGL.js} +4 -2
- package/dist/chunk-NFJRQCGL.js.map +1 -0
- package/dist/index.cjs +92 -84
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +11 -5
- package/dist/index.js.map +1 -1
- package/package.json +20 -4
- package/dist/chunk-JPTDSCSW.js.map +0 -1
- package/dist/{indexeddb-CkUWH5Sl.d.cts → indexeddb-B7LON8ue.d.cts} +9 -9
- package/dist/{indexeddb-CkUWH5Sl.d.ts → indexeddb-B7LON8ue.d.ts} +9 -9
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
|
+
[](https://www.npmjs.com/package/better-auth-offline)
|
|
4
|
+
[](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)
|
|
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 {
|
|
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-
|
|
1
|
+
export { c as createIndexedDBAdapter } from '../indexeddb-B7LON8ue.cjs';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { c as createIndexedDBAdapter } from '../indexeddb-
|
|
1
|
+
export { c as createIndexedDBAdapter } from '../indexeddb-B7LON8ue.js';
|
|
@@ -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)
|
|
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-
|
|
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)
|
|
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)
|
|
81
|
-
|
|
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"];
|
package/dist/index.cjs.map
CHANGED
|
@@ -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-
|
|
3
|
-
export { C as CacheEntry, S as StorageAdapter, c as createIndexedDBAdapter } from './indexeddb-
|
|
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-
|
|
3
|
-
export { C as CacheEntry, S as StorageAdapter, c as createIndexedDBAdapter } from './indexeddb-
|
|
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-
|
|
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)
|
|
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)
|
|
57
|
-
|
|
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.
|
|
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
|
/**
|