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 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
+ });
@@ -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 };
@@ -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
+ }