vaporauth-js 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -0
- package/dist/index.cjs +476 -0
- package/dist/index.d.cts +88 -0
- package/dist/index.d.ts +88 -0
- package/dist/index.js +446 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# vaporauth-js
|
|
2
|
+
|
|
3
|
+
Supabase-style JavaScript client for VaporAuth backends.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i vaporauth-js
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { createVaporAuthClient } from 'vaporauth-js';
|
|
15
|
+
|
|
16
|
+
const auth = createVaporAuthClient({
|
|
17
|
+
apiUrl: 'https://api.example.com'
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const signIn = await auth.auth.signInWithPassword({
|
|
21
|
+
email: 'user@example.com',
|
|
22
|
+
password: 'secret'
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (signIn.error) {
|
|
26
|
+
console.error(signIn.error.message);
|
|
27
|
+
} else {
|
|
28
|
+
const { data } = await auth.auth.getUser();
|
|
29
|
+
console.log(data?.user);
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## API
|
|
34
|
+
|
|
35
|
+
- `createVaporAuthClient(options)`
|
|
36
|
+
- `auth.signInWithPassword({ email, password })`
|
|
37
|
+
- `auth.signOut({ scope?: 'global' | 'local' })`
|
|
38
|
+
- `auth.getSession()`
|
|
39
|
+
- `auth.getUser()`
|
|
40
|
+
- `auth.refreshSession(refreshToken?)`
|
|
41
|
+
- `auth.setSession(session | null)`
|
|
42
|
+
- `auth.fetch(input, init?)` (adds bearer token, refreshes once on `401`)
|
|
43
|
+
- `auth.onAuthStateChange((event, session) => void)`
|
|
44
|
+
|
|
45
|
+
## Notes
|
|
46
|
+
|
|
47
|
+
- Expects backend endpoints:
|
|
48
|
+
- `POST /auth/token`
|
|
49
|
+
- `GET /auth/me`
|
|
50
|
+
- `POST /auth/logout`
|
|
51
|
+
- Includes browser + memory + cookie storage adapters.
|
|
52
|
+
- Supports cross-tab sync (`BroadcastChannel` + storage events) and auto-refresh.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
browserStorage: () => browserStorage,
|
|
24
|
+
cookieStorage: () => cookieStorage,
|
|
25
|
+
createVaporAuthClient: () => createVaporAuthClient,
|
|
26
|
+
memoryStorage: () => memoryStorage
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
|
|
30
|
+
// src/adapters.ts
|
|
31
|
+
function browserStorage() {
|
|
32
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
33
|
+
return memoryStorage();
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
getItem: (key) => window.localStorage.getItem(key),
|
|
37
|
+
setItem: (key, value) => {
|
|
38
|
+
window.localStorage.setItem(key, value);
|
|
39
|
+
},
|
|
40
|
+
removeItem: (key) => {
|
|
41
|
+
window.localStorage.removeItem(key);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function memoryStorage(seed) {
|
|
46
|
+
const state = new Map(Object.entries(seed ?? {}));
|
|
47
|
+
return {
|
|
48
|
+
getItem: (key) => state.get(key) ?? null,
|
|
49
|
+
setItem: (key, value) => {
|
|
50
|
+
state.set(key, value);
|
|
51
|
+
},
|
|
52
|
+
removeItem: (key) => {
|
|
53
|
+
state.delete(key);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function cookieStorage(read, write) {
|
|
58
|
+
return {
|
|
59
|
+
getItem: () => read(),
|
|
60
|
+
setItem: (_, value) => {
|
|
61
|
+
write(value);
|
|
62
|
+
},
|
|
63
|
+
removeItem: () => {
|
|
64
|
+
write(null);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/index.ts
|
|
70
|
+
var DEFAULT_STORAGE_KEY = "vaporauth.session";
|
|
71
|
+
var DEFAULT_REFRESH_MARGIN_MS = 6e4;
|
|
72
|
+
var TAB_SYNC_CHANNEL = "vaporauth.sync";
|
|
73
|
+
function createVaporAuthClient(options) {
|
|
74
|
+
const state = new VaporAuthState(options);
|
|
75
|
+
return {
|
|
76
|
+
auth: {
|
|
77
|
+
signInWithPassword: (credentials) => state.signInWithPassword(credentials),
|
|
78
|
+
signOut: (signOutOptions) => state.signOut(signOutOptions),
|
|
79
|
+
getSession: () => state.getSession(),
|
|
80
|
+
getUser: () => state.getUser(),
|
|
81
|
+
refreshSession: (refreshToken) => state.refreshSession(refreshToken),
|
|
82
|
+
setSession: (session) => state.setSession(session),
|
|
83
|
+
fetch: (input, init) => state.authFetch(input, init),
|
|
84
|
+
onAuthStateChange: (callback) => state.onAuthStateChange(callback)
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
var VaporAuthState = class {
|
|
89
|
+
apiUrl;
|
|
90
|
+
fetcher;
|
|
91
|
+
storage;
|
|
92
|
+
storageKey;
|
|
93
|
+
persistSession;
|
|
94
|
+
autoRefreshToken;
|
|
95
|
+
refreshMarginMs;
|
|
96
|
+
headers;
|
|
97
|
+
multiTab;
|
|
98
|
+
session = null;
|
|
99
|
+
sessionLoaded = false;
|
|
100
|
+
listeners = /* @__PURE__ */ new Set();
|
|
101
|
+
refreshTimer = null;
|
|
102
|
+
inFlightRefresh = null;
|
|
103
|
+
channel = null;
|
|
104
|
+
instanceID = Math.random().toString(36).slice(2);
|
|
105
|
+
constructor(options) {
|
|
106
|
+
this.apiUrl = options.apiUrl.replace(/\/$/, "");
|
|
107
|
+
this.fetcher = options.fetch ?? globalThis.fetch;
|
|
108
|
+
this.storage = options.storage ?? browserStorage();
|
|
109
|
+
this.storageKey = options.storageKey ?? DEFAULT_STORAGE_KEY;
|
|
110
|
+
this.persistSession = options.persistSession ?? true;
|
|
111
|
+
this.autoRefreshToken = options.autoRefreshToken ?? true;
|
|
112
|
+
this.refreshMarginMs = options.refreshMarginMs ?? DEFAULT_REFRESH_MARGIN_MS;
|
|
113
|
+
this.headers = options.headers ?? {};
|
|
114
|
+
this.multiTab = options.multiTab ?? true;
|
|
115
|
+
this.setupBrowserLifecycle();
|
|
116
|
+
}
|
|
117
|
+
onAuthStateChange(callback) {
|
|
118
|
+
this.listeners.add(callback);
|
|
119
|
+
void this.ensureSessionLoaded().then(() => {
|
|
120
|
+
callback("INITIAL_SESSION", this.session);
|
|
121
|
+
});
|
|
122
|
+
return {
|
|
123
|
+
data: {
|
|
124
|
+
subscription: {
|
|
125
|
+
unsubscribe: () => {
|
|
126
|
+
this.listeners.delete(callback);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
async signInWithPassword(credentials) {
|
|
133
|
+
const response = await this.fetchJson("/auth/token", {
|
|
134
|
+
method: "POST",
|
|
135
|
+
body: JSON.stringify({
|
|
136
|
+
grant_type: "password",
|
|
137
|
+
email: credentials.email,
|
|
138
|
+
password: credentials.password
|
|
139
|
+
})
|
|
140
|
+
});
|
|
141
|
+
if (response.error || !response.data) {
|
|
142
|
+
return { data: null, error: response.error ?? makeError("Failed to sign in") };
|
|
143
|
+
}
|
|
144
|
+
const session = toSession(response.data);
|
|
145
|
+
await this.replaceSession(session, "SIGNED_IN", true);
|
|
146
|
+
return {
|
|
147
|
+
data: {
|
|
148
|
+
session,
|
|
149
|
+
user: session.user
|
|
150
|
+
},
|
|
151
|
+
error: null
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
async signOut(options = {}) {
|
|
155
|
+
await this.ensureSessionLoaded();
|
|
156
|
+
if (options.scope !== "local" && this.session?.refresh_token) {
|
|
157
|
+
await this.fetchJson("/auth/logout", {
|
|
158
|
+
method: "POST",
|
|
159
|
+
body: JSON.stringify({ refresh_token: this.session.refresh_token })
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
await this.replaceSession(null, "SIGNED_OUT", true);
|
|
163
|
+
return { data: null, error: null };
|
|
164
|
+
}
|
|
165
|
+
async getSession() {
|
|
166
|
+
await this.ensureSessionLoaded();
|
|
167
|
+
if (this.session && isExpired(this.session)) {
|
|
168
|
+
const refreshed = await this.refreshSession();
|
|
169
|
+
if (refreshed.error) {
|
|
170
|
+
return { data: { session: null }, error: null };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return { data: { session: this.session }, error: null };
|
|
174
|
+
}
|
|
175
|
+
async getUser() {
|
|
176
|
+
await this.ensureSessionLoaded();
|
|
177
|
+
if (!this.session?.access_token) {
|
|
178
|
+
return { data: null, error: makeError("No active session") };
|
|
179
|
+
}
|
|
180
|
+
const response = await this.fetchWithAuth("/auth/me", {
|
|
181
|
+
method: "GET"
|
|
182
|
+
});
|
|
183
|
+
if (response.error || !response.data) {
|
|
184
|
+
return { data: null, error: response.error ?? makeError("Failed to fetch user") };
|
|
185
|
+
}
|
|
186
|
+
this.session = {
|
|
187
|
+
...this.session,
|
|
188
|
+
user: response.data
|
|
189
|
+
};
|
|
190
|
+
await this.persistCurrentSession();
|
|
191
|
+
return { data: { user: response.data }, error: null };
|
|
192
|
+
}
|
|
193
|
+
async refreshSession(refreshToken) {
|
|
194
|
+
await this.ensureSessionLoaded();
|
|
195
|
+
if (this.inFlightRefresh) {
|
|
196
|
+
return this.inFlightRefresh;
|
|
197
|
+
}
|
|
198
|
+
const currentRefreshToken = refreshToken ?? this.session?.refresh_token;
|
|
199
|
+
if (!currentRefreshToken) {
|
|
200
|
+
return { data: null, error: makeError("No refresh token available") };
|
|
201
|
+
}
|
|
202
|
+
const refreshPromise = (async () => {
|
|
203
|
+
const response = await this.fetchJson("/auth/token", {
|
|
204
|
+
method: "POST",
|
|
205
|
+
body: JSON.stringify({
|
|
206
|
+
grant_type: "refresh_token",
|
|
207
|
+
refresh_token: currentRefreshToken
|
|
208
|
+
})
|
|
209
|
+
});
|
|
210
|
+
if (response.error || !response.data) {
|
|
211
|
+
await this.replaceSession(null, "SESSION_EXPIRED", true);
|
|
212
|
+
return { data: null, error: response.error ?? makeError("Failed to refresh session") };
|
|
213
|
+
}
|
|
214
|
+
const session = toSession(response.data);
|
|
215
|
+
await this.replaceSession(session, "TOKEN_REFRESHED", true);
|
|
216
|
+
return { data: { session }, error: null };
|
|
217
|
+
})();
|
|
218
|
+
this.inFlightRefresh = refreshPromise;
|
|
219
|
+
try {
|
|
220
|
+
return await refreshPromise;
|
|
221
|
+
} finally {
|
|
222
|
+
this.inFlightRefresh = null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
async setSession(session) {
|
|
226
|
+
await this.replaceSession(session, session ? "SIGNED_IN" : "SIGNED_OUT", true);
|
|
227
|
+
return { data: { session }, error: null };
|
|
228
|
+
}
|
|
229
|
+
async authFetch(input, init = {}) {
|
|
230
|
+
await this.ensureSessionLoaded();
|
|
231
|
+
const execute = (token) => {
|
|
232
|
+
const headers = new Headers(init.headers ?? void 0);
|
|
233
|
+
if (token) {
|
|
234
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
235
|
+
}
|
|
236
|
+
return this.fetcher(input, {
|
|
237
|
+
...init,
|
|
238
|
+
headers
|
|
239
|
+
});
|
|
240
|
+
};
|
|
241
|
+
const first = await execute(this.session?.access_token);
|
|
242
|
+
if (first.status !== 401) {
|
|
243
|
+
return first;
|
|
244
|
+
}
|
|
245
|
+
const refreshed = await this.refreshSession();
|
|
246
|
+
if (refreshed.error || !this.session?.access_token) {
|
|
247
|
+
return first;
|
|
248
|
+
}
|
|
249
|
+
return execute(this.session.access_token);
|
|
250
|
+
}
|
|
251
|
+
async fetchWithAuth(path, init) {
|
|
252
|
+
await this.ensureSessionLoaded();
|
|
253
|
+
const firstTry = await this.fetchJson(path, {
|
|
254
|
+
...init,
|
|
255
|
+
headers: {
|
|
256
|
+
...init.headers ?? {},
|
|
257
|
+
...this.session?.access_token ? {
|
|
258
|
+
Authorization: `Bearer ${this.session.access_token}`
|
|
259
|
+
} : {}
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
if (firstTry.error?.status !== 401) {
|
|
263
|
+
return firstTry;
|
|
264
|
+
}
|
|
265
|
+
const refreshed = await this.refreshSession();
|
|
266
|
+
if (refreshed.error || !this.session?.access_token) {
|
|
267
|
+
return firstTry;
|
|
268
|
+
}
|
|
269
|
+
return this.fetchJson(path, {
|
|
270
|
+
...init,
|
|
271
|
+
headers: {
|
|
272
|
+
...init.headers ?? {},
|
|
273
|
+
Authorization: `Bearer ${this.session.access_token}`
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
async fetchJson(path, init) {
|
|
278
|
+
try {
|
|
279
|
+
const response = await this.fetcher(`${this.apiUrl}${path}`, {
|
|
280
|
+
...init,
|
|
281
|
+
headers: {
|
|
282
|
+
"content-type": "application/json",
|
|
283
|
+
...this.headers,
|
|
284
|
+
...init.headers ?? {}
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
const isJson = response.headers.get("content-type")?.includes("application/json") ?? false;
|
|
288
|
+
const body = isJson ? await response.json() : null;
|
|
289
|
+
if (!response.ok) {
|
|
290
|
+
return {
|
|
291
|
+
data: null,
|
|
292
|
+
error: makeError(
|
|
293
|
+
parseErrorMessage(body) ?? `Request failed with status ${response.status}`,
|
|
294
|
+
response.status,
|
|
295
|
+
typeof body?.code === "string" ? body.code : void 0,
|
|
296
|
+
body
|
|
297
|
+
)
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
return { data: body, error: null };
|
|
301
|
+
} catch (error) {
|
|
302
|
+
return { data: null, error: makeError("Network error", void 0, void 0, error) };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
async ensureSessionLoaded() {
|
|
306
|
+
if (this.sessionLoaded) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
this.sessionLoaded = true;
|
|
310
|
+
if (!this.persistSession) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const raw = await this.storage.getItem(this.storageKey);
|
|
314
|
+
if (!raw) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
const parsed = JSON.parse(raw);
|
|
319
|
+
this.session = parsed;
|
|
320
|
+
this.scheduleRefresh();
|
|
321
|
+
} catch {
|
|
322
|
+
await this.storage.removeItem(this.storageKey);
|
|
323
|
+
this.session = null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
async replaceSession(session, event, broadcast) {
|
|
327
|
+
this.session = session;
|
|
328
|
+
await this.persistCurrentSession();
|
|
329
|
+
this.scheduleRefresh();
|
|
330
|
+
if (broadcast) {
|
|
331
|
+
this.broadcastSession();
|
|
332
|
+
}
|
|
333
|
+
this.emit(event);
|
|
334
|
+
}
|
|
335
|
+
async persistCurrentSession() {
|
|
336
|
+
if (!this.persistSession) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (this.session) {
|
|
340
|
+
await this.storage.setItem(this.storageKey, JSON.stringify(this.session));
|
|
341
|
+
} else {
|
|
342
|
+
await this.storage.removeItem(this.storageKey);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
scheduleRefresh() {
|
|
346
|
+
if (this.refreshTimer) {
|
|
347
|
+
clearTimeout(this.refreshTimer);
|
|
348
|
+
this.refreshTimer = null;
|
|
349
|
+
}
|
|
350
|
+
if (!this.autoRefreshToken || !this.session) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const refreshAtMs = this.session.expires_at * 1e3 - this.refreshMarginMs;
|
|
354
|
+
const delay = Math.max(0, refreshAtMs - Date.now());
|
|
355
|
+
this.refreshTimer = setTimeout(() => {
|
|
356
|
+
void this.refreshSession();
|
|
357
|
+
}, delay);
|
|
358
|
+
}
|
|
359
|
+
emit(event) {
|
|
360
|
+
for (const listener of this.listeners) {
|
|
361
|
+
listener(event, this.session);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
setupBrowserLifecycle() {
|
|
365
|
+
if (typeof window === "undefined") {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const triggerRefresh = () => {
|
|
369
|
+
if (!this.autoRefreshToken || !this.session) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const refreshAt = this.session.expires_at * 1e3 - this.refreshMarginMs;
|
|
373
|
+
if (Date.now() >= refreshAt) {
|
|
374
|
+
void this.refreshSession();
|
|
375
|
+
} else {
|
|
376
|
+
this.scheduleRefresh();
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
window.addEventListener("focus", triggerRefresh);
|
|
380
|
+
document.addEventListener("visibilitychange", () => {
|
|
381
|
+
if (!document.hidden) {
|
|
382
|
+
triggerRefresh();
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
if (!this.multiTab) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
window.addEventListener("storage", (event) => {
|
|
389
|
+
if (event.key !== this.storageKey) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
void this.syncFromStorage(event.newValue);
|
|
393
|
+
});
|
|
394
|
+
if (typeof BroadcastChannel !== "undefined") {
|
|
395
|
+
this.channel = new BroadcastChannel(TAB_SYNC_CHANNEL);
|
|
396
|
+
this.channel.addEventListener("message", (event) => {
|
|
397
|
+
const payload = event.data;
|
|
398
|
+
if (!payload || payload.instanceID === this.instanceID || payload.storageKey !== this.storageKey) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
void this.syncFromStorage(payload.rawSession ?? null);
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
async syncFromStorage(raw) {
|
|
406
|
+
let nextSession = null;
|
|
407
|
+
if (raw) {
|
|
408
|
+
try {
|
|
409
|
+
nextSession = JSON.parse(raw);
|
|
410
|
+
} catch {
|
|
411
|
+
nextSession = null;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
const currentRaw = this.session ? JSON.stringify(this.session) : null;
|
|
415
|
+
const nextRaw = nextSession ? JSON.stringify(nextSession) : null;
|
|
416
|
+
if (currentRaw === nextRaw) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const event = nextSession == null ? "SIGNED_OUT" : this.session == null ? "SIGNED_IN" : "TOKEN_REFRESHED";
|
|
420
|
+
await this.replaceSession(nextSession, event, false);
|
|
421
|
+
}
|
|
422
|
+
broadcastSession() {
|
|
423
|
+
if (!this.multiTab || !this.channel) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const rawSession = this.session ? JSON.stringify(this.session) : null;
|
|
427
|
+
this.channel.postMessage({
|
|
428
|
+
instanceID: this.instanceID,
|
|
429
|
+
storageKey: this.storageKey,
|
|
430
|
+
rawSession
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
function toSession(payload) {
|
|
435
|
+
return {
|
|
436
|
+
access_token: payload.access_token,
|
|
437
|
+
refresh_token: payload.refresh_token,
|
|
438
|
+
token_type: payload.token_type,
|
|
439
|
+
expires_in: payload.expires_in,
|
|
440
|
+
expires_at: Math.floor(Date.now() / 1e3) + payload.expires_in,
|
|
441
|
+
user: payload.user
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
function parseErrorMessage(body) {
|
|
445
|
+
if (!body || typeof body !== "object") {
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
const candidate = body;
|
|
449
|
+
const keys = ["error_description", "msg", "message", "error"];
|
|
450
|
+
for (const key of keys) {
|
|
451
|
+
const value = candidate[key];
|
|
452
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
453
|
+
return value;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
function makeError(message, status, code, cause) {
|
|
459
|
+
return {
|
|
460
|
+
name: "VaporAuthError",
|
|
461
|
+
message,
|
|
462
|
+
status,
|
|
463
|
+
code,
|
|
464
|
+
cause
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
function isExpired(session) {
|
|
468
|
+
return session.expires_at * 1e3 <= Date.now();
|
|
469
|
+
}
|
|
470
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
471
|
+
0 && (module.exports = {
|
|
472
|
+
browserStorage,
|
|
473
|
+
cookieStorage,
|
|
474
|
+
createVaporAuthClient,
|
|
475
|
+
memoryStorage
|
|
476
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
type VaporAuthUser = {
|
|
2
|
+
id: string;
|
|
3
|
+
email?: string | null;
|
|
4
|
+
role?: string | null;
|
|
5
|
+
aud?: string | null;
|
|
6
|
+
created_at?: string | null;
|
|
7
|
+
updated_at?: string | null;
|
|
8
|
+
};
|
|
9
|
+
type VaporAuthSession = {
|
|
10
|
+
access_token: string;
|
|
11
|
+
refresh_token: string;
|
|
12
|
+
token_type: string;
|
|
13
|
+
expires_in: number;
|
|
14
|
+
expires_at: number;
|
|
15
|
+
user?: VaporAuthUser;
|
|
16
|
+
};
|
|
17
|
+
type VaporAuthError = {
|
|
18
|
+
name: 'VaporAuthError';
|
|
19
|
+
message: string;
|
|
20
|
+
status?: number;
|
|
21
|
+
code?: string;
|
|
22
|
+
cause?: unknown;
|
|
23
|
+
};
|
|
24
|
+
type VaporAuthResponse<T> = Promise<{
|
|
25
|
+
data: T | null;
|
|
26
|
+
error: VaporAuthError | null;
|
|
27
|
+
}>;
|
|
28
|
+
type VaporAuthChangeEvent = 'INITIAL_SESSION' | 'SIGNED_IN' | 'SIGNED_OUT' | 'TOKEN_REFRESHED' | 'SESSION_EXPIRED';
|
|
29
|
+
type VaporAuthStorage = {
|
|
30
|
+
getItem: (key: string) => string | null | Promise<string | null>;
|
|
31
|
+
setItem: (key: string, value: string) => void | Promise<void>;
|
|
32
|
+
removeItem: (key: string) => void | Promise<void>;
|
|
33
|
+
};
|
|
34
|
+
type VaporAuthClientOptions = {
|
|
35
|
+
apiUrl: string;
|
|
36
|
+
fetch?: typeof fetch;
|
|
37
|
+
storage?: VaporAuthStorage;
|
|
38
|
+
storageKey?: string;
|
|
39
|
+
persistSession?: boolean;
|
|
40
|
+
autoRefreshToken?: boolean;
|
|
41
|
+
refreshMarginMs?: number;
|
|
42
|
+
headers?: Record<string, string>;
|
|
43
|
+
multiTab?: boolean;
|
|
44
|
+
};
|
|
45
|
+
type SignInWithPasswordCredentials = {
|
|
46
|
+
email: string;
|
|
47
|
+
password: string;
|
|
48
|
+
};
|
|
49
|
+
type OnAuthStateChangeCallback = (event: VaporAuthChangeEvent, session: VaporAuthSession | null) => void;
|
|
50
|
+
type VaporAuthClient = {
|
|
51
|
+
auth: {
|
|
52
|
+
signInWithPassword: (credentials: SignInWithPasswordCredentials) => VaporAuthResponse<{
|
|
53
|
+
session: VaporAuthSession;
|
|
54
|
+
user: VaporAuthUser | undefined;
|
|
55
|
+
}>;
|
|
56
|
+
signOut: (options?: {
|
|
57
|
+
scope?: 'global' | 'local';
|
|
58
|
+
}) => VaporAuthResponse<null>;
|
|
59
|
+
getSession: () => VaporAuthResponse<{
|
|
60
|
+
session: VaporAuthSession | null;
|
|
61
|
+
}>;
|
|
62
|
+
getUser: () => VaporAuthResponse<{
|
|
63
|
+
user: VaporAuthUser;
|
|
64
|
+
}>;
|
|
65
|
+
refreshSession: (refreshToken?: string) => VaporAuthResponse<{
|
|
66
|
+
session: VaporAuthSession;
|
|
67
|
+
}>;
|
|
68
|
+
setSession: (session: VaporAuthSession | null) => VaporAuthResponse<{
|
|
69
|
+
session: VaporAuthSession | null;
|
|
70
|
+
}>;
|
|
71
|
+
fetch: (input: string | URL, init?: RequestInit) => Promise<Response>;
|
|
72
|
+
onAuthStateChange: (callback: OnAuthStateChangeCallback) => {
|
|
73
|
+
data: {
|
|
74
|
+
subscription: {
|
|
75
|
+
unsubscribe: () => void;
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
declare function browserStorage(): VaporAuthStorage;
|
|
83
|
+
declare function memoryStorage(seed?: Record<string, string>): VaporAuthStorage;
|
|
84
|
+
declare function cookieStorage(read: () => string | null, write: (value: string | null) => void): VaporAuthStorage;
|
|
85
|
+
|
|
86
|
+
declare function createVaporAuthClient(options: VaporAuthClientOptions): VaporAuthClient;
|
|
87
|
+
|
|
88
|
+
export { type OnAuthStateChangeCallback, type SignInWithPasswordCredentials, type VaporAuthChangeEvent, type VaporAuthClient, type VaporAuthClientOptions, type VaporAuthError, type VaporAuthResponse, type VaporAuthSession, type VaporAuthStorage, type VaporAuthUser, browserStorage, cookieStorage, createVaporAuthClient, memoryStorage };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
type VaporAuthUser = {
|
|
2
|
+
id: string;
|
|
3
|
+
email?: string | null;
|
|
4
|
+
role?: string | null;
|
|
5
|
+
aud?: string | null;
|
|
6
|
+
created_at?: string | null;
|
|
7
|
+
updated_at?: string | null;
|
|
8
|
+
};
|
|
9
|
+
type VaporAuthSession = {
|
|
10
|
+
access_token: string;
|
|
11
|
+
refresh_token: string;
|
|
12
|
+
token_type: string;
|
|
13
|
+
expires_in: number;
|
|
14
|
+
expires_at: number;
|
|
15
|
+
user?: VaporAuthUser;
|
|
16
|
+
};
|
|
17
|
+
type VaporAuthError = {
|
|
18
|
+
name: 'VaporAuthError';
|
|
19
|
+
message: string;
|
|
20
|
+
status?: number;
|
|
21
|
+
code?: string;
|
|
22
|
+
cause?: unknown;
|
|
23
|
+
};
|
|
24
|
+
type VaporAuthResponse<T> = Promise<{
|
|
25
|
+
data: T | null;
|
|
26
|
+
error: VaporAuthError | null;
|
|
27
|
+
}>;
|
|
28
|
+
type VaporAuthChangeEvent = 'INITIAL_SESSION' | 'SIGNED_IN' | 'SIGNED_OUT' | 'TOKEN_REFRESHED' | 'SESSION_EXPIRED';
|
|
29
|
+
type VaporAuthStorage = {
|
|
30
|
+
getItem: (key: string) => string | null | Promise<string | null>;
|
|
31
|
+
setItem: (key: string, value: string) => void | Promise<void>;
|
|
32
|
+
removeItem: (key: string) => void | Promise<void>;
|
|
33
|
+
};
|
|
34
|
+
type VaporAuthClientOptions = {
|
|
35
|
+
apiUrl: string;
|
|
36
|
+
fetch?: typeof fetch;
|
|
37
|
+
storage?: VaporAuthStorage;
|
|
38
|
+
storageKey?: string;
|
|
39
|
+
persistSession?: boolean;
|
|
40
|
+
autoRefreshToken?: boolean;
|
|
41
|
+
refreshMarginMs?: number;
|
|
42
|
+
headers?: Record<string, string>;
|
|
43
|
+
multiTab?: boolean;
|
|
44
|
+
};
|
|
45
|
+
type SignInWithPasswordCredentials = {
|
|
46
|
+
email: string;
|
|
47
|
+
password: string;
|
|
48
|
+
};
|
|
49
|
+
type OnAuthStateChangeCallback = (event: VaporAuthChangeEvent, session: VaporAuthSession | null) => void;
|
|
50
|
+
type VaporAuthClient = {
|
|
51
|
+
auth: {
|
|
52
|
+
signInWithPassword: (credentials: SignInWithPasswordCredentials) => VaporAuthResponse<{
|
|
53
|
+
session: VaporAuthSession;
|
|
54
|
+
user: VaporAuthUser | undefined;
|
|
55
|
+
}>;
|
|
56
|
+
signOut: (options?: {
|
|
57
|
+
scope?: 'global' | 'local';
|
|
58
|
+
}) => VaporAuthResponse<null>;
|
|
59
|
+
getSession: () => VaporAuthResponse<{
|
|
60
|
+
session: VaporAuthSession | null;
|
|
61
|
+
}>;
|
|
62
|
+
getUser: () => VaporAuthResponse<{
|
|
63
|
+
user: VaporAuthUser;
|
|
64
|
+
}>;
|
|
65
|
+
refreshSession: (refreshToken?: string) => VaporAuthResponse<{
|
|
66
|
+
session: VaporAuthSession;
|
|
67
|
+
}>;
|
|
68
|
+
setSession: (session: VaporAuthSession | null) => VaporAuthResponse<{
|
|
69
|
+
session: VaporAuthSession | null;
|
|
70
|
+
}>;
|
|
71
|
+
fetch: (input: string | URL, init?: RequestInit) => Promise<Response>;
|
|
72
|
+
onAuthStateChange: (callback: OnAuthStateChangeCallback) => {
|
|
73
|
+
data: {
|
|
74
|
+
subscription: {
|
|
75
|
+
unsubscribe: () => void;
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
declare function browserStorage(): VaporAuthStorage;
|
|
83
|
+
declare function memoryStorage(seed?: Record<string, string>): VaporAuthStorage;
|
|
84
|
+
declare function cookieStorage(read: () => string | null, write: (value: string | null) => void): VaporAuthStorage;
|
|
85
|
+
|
|
86
|
+
declare function createVaporAuthClient(options: VaporAuthClientOptions): VaporAuthClient;
|
|
87
|
+
|
|
88
|
+
export { type OnAuthStateChangeCallback, type SignInWithPasswordCredentials, type VaporAuthChangeEvent, type VaporAuthClient, type VaporAuthClientOptions, type VaporAuthError, type VaporAuthResponse, type VaporAuthSession, type VaporAuthStorage, type VaporAuthUser, browserStorage, cookieStorage, createVaporAuthClient, memoryStorage };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
// src/adapters.ts
|
|
2
|
+
function browserStorage() {
|
|
3
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
4
|
+
return memoryStorage();
|
|
5
|
+
}
|
|
6
|
+
return {
|
|
7
|
+
getItem: (key) => window.localStorage.getItem(key),
|
|
8
|
+
setItem: (key, value) => {
|
|
9
|
+
window.localStorage.setItem(key, value);
|
|
10
|
+
},
|
|
11
|
+
removeItem: (key) => {
|
|
12
|
+
window.localStorage.removeItem(key);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function memoryStorage(seed) {
|
|
17
|
+
const state = new Map(Object.entries(seed ?? {}));
|
|
18
|
+
return {
|
|
19
|
+
getItem: (key) => state.get(key) ?? null,
|
|
20
|
+
setItem: (key, value) => {
|
|
21
|
+
state.set(key, value);
|
|
22
|
+
},
|
|
23
|
+
removeItem: (key) => {
|
|
24
|
+
state.delete(key);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function cookieStorage(read, write) {
|
|
29
|
+
return {
|
|
30
|
+
getItem: () => read(),
|
|
31
|
+
setItem: (_, value) => {
|
|
32
|
+
write(value);
|
|
33
|
+
},
|
|
34
|
+
removeItem: () => {
|
|
35
|
+
write(null);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/index.ts
|
|
41
|
+
var DEFAULT_STORAGE_KEY = "vaporauth.session";
|
|
42
|
+
var DEFAULT_REFRESH_MARGIN_MS = 6e4;
|
|
43
|
+
var TAB_SYNC_CHANNEL = "vaporauth.sync";
|
|
44
|
+
function createVaporAuthClient(options) {
|
|
45
|
+
const state = new VaporAuthState(options);
|
|
46
|
+
return {
|
|
47
|
+
auth: {
|
|
48
|
+
signInWithPassword: (credentials) => state.signInWithPassword(credentials),
|
|
49
|
+
signOut: (signOutOptions) => state.signOut(signOutOptions),
|
|
50
|
+
getSession: () => state.getSession(),
|
|
51
|
+
getUser: () => state.getUser(),
|
|
52
|
+
refreshSession: (refreshToken) => state.refreshSession(refreshToken),
|
|
53
|
+
setSession: (session) => state.setSession(session),
|
|
54
|
+
fetch: (input, init) => state.authFetch(input, init),
|
|
55
|
+
onAuthStateChange: (callback) => state.onAuthStateChange(callback)
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
var VaporAuthState = class {
|
|
60
|
+
apiUrl;
|
|
61
|
+
fetcher;
|
|
62
|
+
storage;
|
|
63
|
+
storageKey;
|
|
64
|
+
persistSession;
|
|
65
|
+
autoRefreshToken;
|
|
66
|
+
refreshMarginMs;
|
|
67
|
+
headers;
|
|
68
|
+
multiTab;
|
|
69
|
+
session = null;
|
|
70
|
+
sessionLoaded = false;
|
|
71
|
+
listeners = /* @__PURE__ */ new Set();
|
|
72
|
+
refreshTimer = null;
|
|
73
|
+
inFlightRefresh = null;
|
|
74
|
+
channel = null;
|
|
75
|
+
instanceID = Math.random().toString(36).slice(2);
|
|
76
|
+
constructor(options) {
|
|
77
|
+
this.apiUrl = options.apiUrl.replace(/\/$/, "");
|
|
78
|
+
this.fetcher = options.fetch ?? globalThis.fetch;
|
|
79
|
+
this.storage = options.storage ?? browserStorage();
|
|
80
|
+
this.storageKey = options.storageKey ?? DEFAULT_STORAGE_KEY;
|
|
81
|
+
this.persistSession = options.persistSession ?? true;
|
|
82
|
+
this.autoRefreshToken = options.autoRefreshToken ?? true;
|
|
83
|
+
this.refreshMarginMs = options.refreshMarginMs ?? DEFAULT_REFRESH_MARGIN_MS;
|
|
84
|
+
this.headers = options.headers ?? {};
|
|
85
|
+
this.multiTab = options.multiTab ?? true;
|
|
86
|
+
this.setupBrowserLifecycle();
|
|
87
|
+
}
|
|
88
|
+
onAuthStateChange(callback) {
|
|
89
|
+
this.listeners.add(callback);
|
|
90
|
+
void this.ensureSessionLoaded().then(() => {
|
|
91
|
+
callback("INITIAL_SESSION", this.session);
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
data: {
|
|
95
|
+
subscription: {
|
|
96
|
+
unsubscribe: () => {
|
|
97
|
+
this.listeners.delete(callback);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
async signInWithPassword(credentials) {
|
|
104
|
+
const response = await this.fetchJson("/auth/token", {
|
|
105
|
+
method: "POST",
|
|
106
|
+
body: JSON.stringify({
|
|
107
|
+
grant_type: "password",
|
|
108
|
+
email: credentials.email,
|
|
109
|
+
password: credentials.password
|
|
110
|
+
})
|
|
111
|
+
});
|
|
112
|
+
if (response.error || !response.data) {
|
|
113
|
+
return { data: null, error: response.error ?? makeError("Failed to sign in") };
|
|
114
|
+
}
|
|
115
|
+
const session = toSession(response.data);
|
|
116
|
+
await this.replaceSession(session, "SIGNED_IN", true);
|
|
117
|
+
return {
|
|
118
|
+
data: {
|
|
119
|
+
session,
|
|
120
|
+
user: session.user
|
|
121
|
+
},
|
|
122
|
+
error: null
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
async signOut(options = {}) {
|
|
126
|
+
await this.ensureSessionLoaded();
|
|
127
|
+
if (options.scope !== "local" && this.session?.refresh_token) {
|
|
128
|
+
await this.fetchJson("/auth/logout", {
|
|
129
|
+
method: "POST",
|
|
130
|
+
body: JSON.stringify({ refresh_token: this.session.refresh_token })
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
await this.replaceSession(null, "SIGNED_OUT", true);
|
|
134
|
+
return { data: null, error: null };
|
|
135
|
+
}
|
|
136
|
+
async getSession() {
|
|
137
|
+
await this.ensureSessionLoaded();
|
|
138
|
+
if (this.session && isExpired(this.session)) {
|
|
139
|
+
const refreshed = await this.refreshSession();
|
|
140
|
+
if (refreshed.error) {
|
|
141
|
+
return { data: { session: null }, error: null };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return { data: { session: this.session }, error: null };
|
|
145
|
+
}
|
|
146
|
+
async getUser() {
|
|
147
|
+
await this.ensureSessionLoaded();
|
|
148
|
+
if (!this.session?.access_token) {
|
|
149
|
+
return { data: null, error: makeError("No active session") };
|
|
150
|
+
}
|
|
151
|
+
const response = await this.fetchWithAuth("/auth/me", {
|
|
152
|
+
method: "GET"
|
|
153
|
+
});
|
|
154
|
+
if (response.error || !response.data) {
|
|
155
|
+
return { data: null, error: response.error ?? makeError("Failed to fetch user") };
|
|
156
|
+
}
|
|
157
|
+
this.session = {
|
|
158
|
+
...this.session,
|
|
159
|
+
user: response.data
|
|
160
|
+
};
|
|
161
|
+
await this.persistCurrentSession();
|
|
162
|
+
return { data: { user: response.data }, error: null };
|
|
163
|
+
}
|
|
164
|
+
async refreshSession(refreshToken) {
|
|
165
|
+
await this.ensureSessionLoaded();
|
|
166
|
+
if (this.inFlightRefresh) {
|
|
167
|
+
return this.inFlightRefresh;
|
|
168
|
+
}
|
|
169
|
+
const currentRefreshToken = refreshToken ?? this.session?.refresh_token;
|
|
170
|
+
if (!currentRefreshToken) {
|
|
171
|
+
return { data: null, error: makeError("No refresh token available") };
|
|
172
|
+
}
|
|
173
|
+
const refreshPromise = (async () => {
|
|
174
|
+
const response = await this.fetchJson("/auth/token", {
|
|
175
|
+
method: "POST",
|
|
176
|
+
body: JSON.stringify({
|
|
177
|
+
grant_type: "refresh_token",
|
|
178
|
+
refresh_token: currentRefreshToken
|
|
179
|
+
})
|
|
180
|
+
});
|
|
181
|
+
if (response.error || !response.data) {
|
|
182
|
+
await this.replaceSession(null, "SESSION_EXPIRED", true);
|
|
183
|
+
return { data: null, error: response.error ?? makeError("Failed to refresh session") };
|
|
184
|
+
}
|
|
185
|
+
const session = toSession(response.data);
|
|
186
|
+
await this.replaceSession(session, "TOKEN_REFRESHED", true);
|
|
187
|
+
return { data: { session }, error: null };
|
|
188
|
+
})();
|
|
189
|
+
this.inFlightRefresh = refreshPromise;
|
|
190
|
+
try {
|
|
191
|
+
return await refreshPromise;
|
|
192
|
+
} finally {
|
|
193
|
+
this.inFlightRefresh = null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
async setSession(session) {
|
|
197
|
+
await this.replaceSession(session, session ? "SIGNED_IN" : "SIGNED_OUT", true);
|
|
198
|
+
return { data: { session }, error: null };
|
|
199
|
+
}
|
|
200
|
+
async authFetch(input, init = {}) {
|
|
201
|
+
await this.ensureSessionLoaded();
|
|
202
|
+
const execute = (token) => {
|
|
203
|
+
const headers = new Headers(init.headers ?? void 0);
|
|
204
|
+
if (token) {
|
|
205
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
206
|
+
}
|
|
207
|
+
return this.fetcher(input, {
|
|
208
|
+
...init,
|
|
209
|
+
headers
|
|
210
|
+
});
|
|
211
|
+
};
|
|
212
|
+
const first = await execute(this.session?.access_token);
|
|
213
|
+
if (first.status !== 401) {
|
|
214
|
+
return first;
|
|
215
|
+
}
|
|
216
|
+
const refreshed = await this.refreshSession();
|
|
217
|
+
if (refreshed.error || !this.session?.access_token) {
|
|
218
|
+
return first;
|
|
219
|
+
}
|
|
220
|
+
return execute(this.session.access_token);
|
|
221
|
+
}
|
|
222
|
+
async fetchWithAuth(path, init) {
|
|
223
|
+
await this.ensureSessionLoaded();
|
|
224
|
+
const firstTry = await this.fetchJson(path, {
|
|
225
|
+
...init,
|
|
226
|
+
headers: {
|
|
227
|
+
...init.headers ?? {},
|
|
228
|
+
...this.session?.access_token ? {
|
|
229
|
+
Authorization: `Bearer ${this.session.access_token}`
|
|
230
|
+
} : {}
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
if (firstTry.error?.status !== 401) {
|
|
234
|
+
return firstTry;
|
|
235
|
+
}
|
|
236
|
+
const refreshed = await this.refreshSession();
|
|
237
|
+
if (refreshed.error || !this.session?.access_token) {
|
|
238
|
+
return firstTry;
|
|
239
|
+
}
|
|
240
|
+
return this.fetchJson(path, {
|
|
241
|
+
...init,
|
|
242
|
+
headers: {
|
|
243
|
+
...init.headers ?? {},
|
|
244
|
+
Authorization: `Bearer ${this.session.access_token}`
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
async fetchJson(path, init) {
|
|
249
|
+
try {
|
|
250
|
+
const response = await this.fetcher(`${this.apiUrl}${path}`, {
|
|
251
|
+
...init,
|
|
252
|
+
headers: {
|
|
253
|
+
"content-type": "application/json",
|
|
254
|
+
...this.headers,
|
|
255
|
+
...init.headers ?? {}
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
const isJson = response.headers.get("content-type")?.includes("application/json") ?? false;
|
|
259
|
+
const body = isJson ? await response.json() : null;
|
|
260
|
+
if (!response.ok) {
|
|
261
|
+
return {
|
|
262
|
+
data: null,
|
|
263
|
+
error: makeError(
|
|
264
|
+
parseErrorMessage(body) ?? `Request failed with status ${response.status}`,
|
|
265
|
+
response.status,
|
|
266
|
+
typeof body?.code === "string" ? body.code : void 0,
|
|
267
|
+
body
|
|
268
|
+
)
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
return { data: body, error: null };
|
|
272
|
+
} catch (error) {
|
|
273
|
+
return { data: null, error: makeError("Network error", void 0, void 0, error) };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
async ensureSessionLoaded() {
|
|
277
|
+
if (this.sessionLoaded) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
this.sessionLoaded = true;
|
|
281
|
+
if (!this.persistSession) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const raw = await this.storage.getItem(this.storageKey);
|
|
285
|
+
if (!raw) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
const parsed = JSON.parse(raw);
|
|
290
|
+
this.session = parsed;
|
|
291
|
+
this.scheduleRefresh();
|
|
292
|
+
} catch {
|
|
293
|
+
await this.storage.removeItem(this.storageKey);
|
|
294
|
+
this.session = null;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
async replaceSession(session, event, broadcast) {
|
|
298
|
+
this.session = session;
|
|
299
|
+
await this.persistCurrentSession();
|
|
300
|
+
this.scheduleRefresh();
|
|
301
|
+
if (broadcast) {
|
|
302
|
+
this.broadcastSession();
|
|
303
|
+
}
|
|
304
|
+
this.emit(event);
|
|
305
|
+
}
|
|
306
|
+
async persistCurrentSession() {
|
|
307
|
+
if (!this.persistSession) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (this.session) {
|
|
311
|
+
await this.storage.setItem(this.storageKey, JSON.stringify(this.session));
|
|
312
|
+
} else {
|
|
313
|
+
await this.storage.removeItem(this.storageKey);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
scheduleRefresh() {
|
|
317
|
+
if (this.refreshTimer) {
|
|
318
|
+
clearTimeout(this.refreshTimer);
|
|
319
|
+
this.refreshTimer = null;
|
|
320
|
+
}
|
|
321
|
+
if (!this.autoRefreshToken || !this.session) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const refreshAtMs = this.session.expires_at * 1e3 - this.refreshMarginMs;
|
|
325
|
+
const delay = Math.max(0, refreshAtMs - Date.now());
|
|
326
|
+
this.refreshTimer = setTimeout(() => {
|
|
327
|
+
void this.refreshSession();
|
|
328
|
+
}, delay);
|
|
329
|
+
}
|
|
330
|
+
emit(event) {
|
|
331
|
+
for (const listener of this.listeners) {
|
|
332
|
+
listener(event, this.session);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
setupBrowserLifecycle() {
|
|
336
|
+
if (typeof window === "undefined") {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const triggerRefresh = () => {
|
|
340
|
+
if (!this.autoRefreshToken || !this.session) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const refreshAt = this.session.expires_at * 1e3 - this.refreshMarginMs;
|
|
344
|
+
if (Date.now() >= refreshAt) {
|
|
345
|
+
void this.refreshSession();
|
|
346
|
+
} else {
|
|
347
|
+
this.scheduleRefresh();
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
window.addEventListener("focus", triggerRefresh);
|
|
351
|
+
document.addEventListener("visibilitychange", () => {
|
|
352
|
+
if (!document.hidden) {
|
|
353
|
+
triggerRefresh();
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
if (!this.multiTab) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
window.addEventListener("storage", (event) => {
|
|
360
|
+
if (event.key !== this.storageKey) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
void this.syncFromStorage(event.newValue);
|
|
364
|
+
});
|
|
365
|
+
if (typeof BroadcastChannel !== "undefined") {
|
|
366
|
+
this.channel = new BroadcastChannel(TAB_SYNC_CHANNEL);
|
|
367
|
+
this.channel.addEventListener("message", (event) => {
|
|
368
|
+
const payload = event.data;
|
|
369
|
+
if (!payload || payload.instanceID === this.instanceID || payload.storageKey !== this.storageKey) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
void this.syncFromStorage(payload.rawSession ?? null);
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
async syncFromStorage(raw) {
|
|
377
|
+
let nextSession = null;
|
|
378
|
+
if (raw) {
|
|
379
|
+
try {
|
|
380
|
+
nextSession = JSON.parse(raw);
|
|
381
|
+
} catch {
|
|
382
|
+
nextSession = null;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
const currentRaw = this.session ? JSON.stringify(this.session) : null;
|
|
386
|
+
const nextRaw = nextSession ? JSON.stringify(nextSession) : null;
|
|
387
|
+
if (currentRaw === nextRaw) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const event = nextSession == null ? "SIGNED_OUT" : this.session == null ? "SIGNED_IN" : "TOKEN_REFRESHED";
|
|
391
|
+
await this.replaceSession(nextSession, event, false);
|
|
392
|
+
}
|
|
393
|
+
broadcastSession() {
|
|
394
|
+
if (!this.multiTab || !this.channel) {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const rawSession = this.session ? JSON.stringify(this.session) : null;
|
|
398
|
+
this.channel.postMessage({
|
|
399
|
+
instanceID: this.instanceID,
|
|
400
|
+
storageKey: this.storageKey,
|
|
401
|
+
rawSession
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
function toSession(payload) {
|
|
406
|
+
return {
|
|
407
|
+
access_token: payload.access_token,
|
|
408
|
+
refresh_token: payload.refresh_token,
|
|
409
|
+
token_type: payload.token_type,
|
|
410
|
+
expires_in: payload.expires_in,
|
|
411
|
+
expires_at: Math.floor(Date.now() / 1e3) + payload.expires_in,
|
|
412
|
+
user: payload.user
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
function parseErrorMessage(body) {
|
|
416
|
+
if (!body || typeof body !== "object") {
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
const candidate = body;
|
|
420
|
+
const keys = ["error_description", "msg", "message", "error"];
|
|
421
|
+
for (const key of keys) {
|
|
422
|
+
const value = candidate[key];
|
|
423
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
424
|
+
return value;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
function makeError(message, status, code, cause) {
|
|
430
|
+
return {
|
|
431
|
+
name: "VaporAuthError",
|
|
432
|
+
message,
|
|
433
|
+
status,
|
|
434
|
+
code,
|
|
435
|
+
cause
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
function isExpired(session) {
|
|
439
|
+
return session.expires_at * 1e3 <= Date.now();
|
|
440
|
+
}
|
|
441
|
+
export {
|
|
442
|
+
browserStorage,
|
|
443
|
+
cookieStorage,
|
|
444
|
+
createVaporAuthClient,
|
|
445
|
+
memoryStorage
|
|
446
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vaporauth-js",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Supabase-style JavaScript client for VaporAuth backends",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.cjs",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"require": "./dist/index.cjs"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"scripts": {
|
|
22
|
+
"clean": "rm -rf dist",
|
|
23
|
+
"build": "npm run clean && tsup src/index.ts --format esm,cjs --dts",
|
|
24
|
+
"check": "tsc --noEmit",
|
|
25
|
+
"prepublishOnly": "npm run build"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"vapor",
|
|
29
|
+
"auth",
|
|
30
|
+
"supabase",
|
|
31
|
+
"jwt",
|
|
32
|
+
"typescript"
|
|
33
|
+
],
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"tsup": "^8.5.0",
|
|
36
|
+
"typescript": "^5.9.3"
|
|
37
|
+
}
|
|
38
|
+
}
|