better-auth-offline 0.0.0 → 0.0.1
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/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 +2 -2
- 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
|
@@ -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.1",
|
|
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": {
|
|
@@ -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
|
/**
|