@dcimorra/authhub-sdk 1.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/dist/index.js ADDED
@@ -0,0 +1,792 @@
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
+ AuthHubClient: () => AuthHubClient,
24
+ AuthHubError: () => AuthHubError,
25
+ LocalStorageTokenStore: () => LocalStorageTokenStore,
26
+ MemoryTokenStore: () => MemoryTokenStore,
27
+ RealtimeClient: () => RealtimeClient,
28
+ TokenManager: () => TokenManager
29
+ });
30
+ module.exports = __toCommonJS(index_exports);
31
+
32
+ // src/errors.ts
33
+ var AuthHubError = class extends Error {
34
+ constructor(data) {
35
+ super(data.message);
36
+ this.name = "AuthHubError";
37
+ this.status = data.status;
38
+ this.code = data.code;
39
+ }
40
+ };
41
+
42
+ // src/token-store.ts
43
+ var ACCESS_KEY = "authhub_access_token";
44
+ var REFRESH_KEY = "authhub_refresh_token";
45
+ var EXPIRES_KEY = "authhub_expires_at";
46
+ var MemoryTokenStore = class {
47
+ constructor() {
48
+ this.store = /* @__PURE__ */ new Map();
49
+ }
50
+ get(key) {
51
+ return this.store.get(key) ?? null;
52
+ }
53
+ set(key, value) {
54
+ this.store.set(key, value);
55
+ }
56
+ remove(key) {
57
+ this.store.delete(key);
58
+ }
59
+ };
60
+ var LocalStorageTokenStore = class {
61
+ get(key) {
62
+ try {
63
+ return localStorage.getItem(key);
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+ set(key, value) {
69
+ try {
70
+ localStorage.setItem(key, value);
71
+ } catch {
72
+ }
73
+ }
74
+ remove(key) {
75
+ try {
76
+ localStorage.removeItem(key);
77
+ } catch {
78
+ }
79
+ }
80
+ };
81
+ var TokenManager = class {
82
+ constructor(store) {
83
+ this.store = store;
84
+ }
85
+ async getTokens() {
86
+ const accessToken = await this.store.get(ACCESS_KEY);
87
+ const refreshToken = await this.store.get(REFRESH_KEY);
88
+ const expiresAt = await this.store.get(EXPIRES_KEY);
89
+ if (!accessToken || !refreshToken) return null;
90
+ return {
91
+ accessToken,
92
+ refreshToken,
93
+ expiresAt: expiresAt ? parseInt(expiresAt, 10) : 0
94
+ };
95
+ }
96
+ async setTokens(tokens) {
97
+ await this.store.set(ACCESS_KEY, tokens.accessToken);
98
+ await this.store.set(REFRESH_KEY, tokens.refreshToken);
99
+ await this.store.set(EXPIRES_KEY, String(tokens.expiresAt));
100
+ }
101
+ async clear() {
102
+ await this.store.remove(ACCESS_KEY);
103
+ await this.store.remove(REFRESH_KEY);
104
+ await this.store.remove(EXPIRES_KEY);
105
+ }
106
+ async isExpired() {
107
+ const tokens = await this.getTokens();
108
+ if (!tokens) return true;
109
+ return Date.now() >= tokens.expiresAt - 3e4;
110
+ }
111
+ };
112
+
113
+ // src/client.ts
114
+ var isBrowser = typeof window !== "undefined" && typeof localStorage !== "undefined";
115
+ function resolveStore(option) {
116
+ if (!option) return isBrowser ? new LocalStorageTokenStore() : new MemoryTokenStore();
117
+ if (option === "localStorage") return new LocalStorageTokenStore();
118
+ if (option === "memory") return new MemoryTokenStore();
119
+ return option;
120
+ }
121
+ var AuthHubClient = class {
122
+ constructor(config) {
123
+ this.refreshPromise = null;
124
+ this.baseUrl = config.baseUrl.replace(/\/+$/, "");
125
+ this.projectId = config.projectId;
126
+ this.apiKey = config.apiKey;
127
+ this.autoRefresh = config.autoRefresh !== false;
128
+ this.onSessionExpired = config.onSessionExpired;
129
+ this.tokens = new TokenManager(resolveStore(config.tokenStore));
130
+ this.auth = new AuthModule(this);
131
+ this.session = new SessionModule(this);
132
+ this.db = new DbModule(this);
133
+ this.storage = new StorageModule(this);
134
+ this.oauth = new OAuthModule(this);
135
+ }
136
+ /**
137
+ * Create a Realtime client for subscribing to database changes via WebSocket.
138
+ *
139
+ * ```ts
140
+ * const realtime = hub.realtime("ws://localhost:4000");
141
+ * await realtime.connect(accessToken);
142
+ * realtime.on("table:users", (event, data) => console.log(event, data));
143
+ * ```
144
+ */
145
+ realtime(wsUrl) {
146
+ return new RealtimeClient({ url: wsUrl, projectId: this.projectId });
147
+ }
148
+ // ── Internal helpers (used by modules) ───────────────────────────────────
149
+ /** @internal */
150
+ _url(path) {
151
+ return `${this.baseUrl}/api/v1/${this.projectId}${path}`;
152
+ }
153
+ /** @internal — API key auth request */
154
+ async _apiRequest(path, options = {}) {
155
+ const headers = new Headers(options.headers);
156
+ headers.set("Authorization", `Bearer ${this.apiKey}`);
157
+ if (!headers.has("Content-Type") && !(options.body instanceof FormData)) {
158
+ headers.set("Content-Type", "application/json");
159
+ }
160
+ const res = await fetch(this._url(path), { ...options, headers });
161
+ return this._handleResponse(res);
162
+ }
163
+ /** @internal — Bearer JWT auth request (auto-refreshes if needed) */
164
+ async _authRequest(path, options = {}) {
165
+ const accessToken = await this._getValidAccessToken();
166
+ const headers = new Headers(options.headers);
167
+ headers.set("Authorization", `Bearer ${accessToken}`);
168
+ if (!headers.has("Content-Type")) {
169
+ headers.set("Content-Type", "application/json");
170
+ }
171
+ const res = await fetch(this._url(path), { ...options, headers });
172
+ if (res.status === 401 && this.autoRefresh) {
173
+ try {
174
+ await this._refreshTokens();
175
+ const newToken = await this._getValidAccessToken();
176
+ headers.set("Authorization", `Bearer ${newToken}`);
177
+ const retryRes = await fetch(this._url(path), { ...options, headers });
178
+ return this._handleResponse(retryRes);
179
+ } catch {
180
+ await this.tokens.clear();
181
+ this.onSessionExpired?.();
182
+ throw new AuthHubError({ message: "Session expired", status: 401 });
183
+ }
184
+ }
185
+ return this._handleResponse(res);
186
+ }
187
+ /** @internal */
188
+ async _handleResponse(res) {
189
+ if (!res.ok) {
190
+ let body;
191
+ try {
192
+ body = await res.json();
193
+ } catch {
194
+ throw new AuthHubError({ message: res.statusText, status: res.status });
195
+ }
196
+ const error = body.error;
197
+ const message = typeof error === "object" && error ? error.message : typeof error === "string" ? error : "Request failed";
198
+ const code = typeof error === "object" && error ? error.code : void 0;
199
+ throw new AuthHubError({ message, code, status: res.status });
200
+ }
201
+ const json = await res.json();
202
+ return json.data !== void 0 ? json.data : json;
203
+ }
204
+ /** @internal — Save tokens from an auth response */
205
+ async _saveAuthTokens(response) {
206
+ await this.tokens.setTokens({
207
+ accessToken: response.access_token,
208
+ refreshToken: response.refresh_token,
209
+ expiresAt: Date.now() + response.expires_in * 1e3
210
+ });
211
+ }
212
+ /** @internal — Get valid access token, refreshing if necessary */
213
+ async _getValidAccessToken() {
214
+ if (this.autoRefresh && await this.tokens.isExpired()) {
215
+ const pair2 = await this.tokens.getTokens();
216
+ if (pair2?.refreshToken) {
217
+ await this._refreshTokens();
218
+ }
219
+ }
220
+ const pair = await this.tokens.getTokens();
221
+ if (!pair) throw new AuthHubError({ message: "Not authenticated. Call auth.login() first.", status: 401 });
222
+ return pair.accessToken;
223
+ }
224
+ /** @internal — Refresh with dedup (prevents concurrent refresh calls) */
225
+ async _refreshTokens() {
226
+ if (this.refreshPromise) return this.refreshPromise;
227
+ this.refreshPromise = (async () => {
228
+ try {
229
+ const pair = await this.tokens.getTokens();
230
+ if (!pair?.refreshToken) {
231
+ throw new AuthHubError({ message: "No refresh token available", status: 401 });
232
+ }
233
+ return await this.auth.refreshToken(pair.refreshToken);
234
+ } finally {
235
+ this.refreshPromise = null;
236
+ }
237
+ })();
238
+ return this.refreshPromise;
239
+ }
240
+ /** Get the token manager (for advanced usage) */
241
+ getTokenManager() {
242
+ return this.tokens;
243
+ }
244
+ };
245
+ var AuthModule = class {
246
+ constructor(client) {
247
+ this.client = client;
248
+ }
249
+ /** Register a new user account */
250
+ async register(email, password, redirectTo) {
251
+ const body = { email, password };
252
+ if (redirectTo) body.redirect_to = redirectTo;
253
+ return this.client._apiRequest("/register", {
254
+ method: "POST",
255
+ body: JSON.stringify(body)
256
+ });
257
+ }
258
+ /** Login with email and password. Stores tokens automatically. */
259
+ async login(email, password) {
260
+ const response = await this.client._apiRequest("/login", {
261
+ method: "POST",
262
+ body: JSON.stringify({ email, password })
263
+ });
264
+ await this.client._saveAuthTokens(response);
265
+ return response;
266
+ }
267
+ /** Logout — revokes tokens on server and clears local storage */
268
+ async logout() {
269
+ try {
270
+ const pair = await this.client.getTokenManager().getTokens();
271
+ await this.client._authRequest("/logout", {
272
+ method: "POST",
273
+ body: JSON.stringify(pair?.refreshToken ? { refresh_token: pair.refreshToken } : {})
274
+ });
275
+ } catch {
276
+ }
277
+ await this.client.getTokenManager().clear();
278
+ }
279
+ /** Get current authenticated user profile */
280
+ async getUser() {
281
+ return this.client._authRequest("/user");
282
+ }
283
+ /** Change password for the authenticated user */
284
+ async changePassword(currentPassword, newPassword) {
285
+ return this.client._authRequest("/user", {
286
+ method: "PATCH",
287
+ body: JSON.stringify({ currentPassword, newPassword })
288
+ });
289
+ }
290
+ /** Request password recovery email */
291
+ async recover(email) {
292
+ return this.client._apiRequest("/recover", {
293
+ method: "POST",
294
+ body: JSON.stringify({ email })
295
+ });
296
+ }
297
+ /** Reset password using token from recovery email */
298
+ async resetPassword(token, newPassword) {
299
+ return this.client._apiRequest("/reset-password", {
300
+ method: "POST",
301
+ body: JSON.stringify({ token, newPassword })
302
+ });
303
+ }
304
+ /**
305
+ * Refresh tokens using a refresh token.
306
+ * Normally called automatically — use this for manual refresh.
307
+ */
308
+ async refreshToken(refreshToken) {
309
+ const response = await this.client._apiRequest("/refresh", {
310
+ method: "POST",
311
+ body: JSON.stringify({ refresh_token: refreshToken })
312
+ });
313
+ await this.client._saveAuthTokens(response);
314
+ return response;
315
+ }
316
+ /** Check if the user has a stored session (tokens exist and access token not expired) */
317
+ async isAuthenticated() {
318
+ const pair = await this.client.getTokenManager().getTokens();
319
+ return pair !== null && !await this.client.getTokenManager().isExpired();
320
+ }
321
+ /** Get the current access token (or null if not authenticated) */
322
+ async getAccessToken() {
323
+ const pair = await this.client.getTokenManager().getTokens();
324
+ return pair?.accessToken ?? null;
325
+ }
326
+ };
327
+ var SessionModule = class {
328
+ constructor(client) {
329
+ this.client = client;
330
+ }
331
+ /**
332
+ * Create a cookie-based session (login).
333
+ * Returns the user and Set-Cookie headers that must be forwarded to the browser.
334
+ *
335
+ * Usage in Next.js API route:
336
+ * ```ts
337
+ * const { user, setCookieHeaders } = await hub.session.login(email, password);
338
+ * const response = NextResponse.json({ success: true });
339
+ * for (const h of setCookieHeaders) response.headers.append("Set-Cookie", h);
340
+ * ```
341
+ */
342
+ async login(email, password) {
343
+ const res = await fetch(this.client._url("/session"), {
344
+ method: "POST",
345
+ headers: {
346
+ "Content-Type": "application/json",
347
+ Authorization: `Bearer ${this.client["apiKey"]}`
348
+ },
349
+ body: JSON.stringify({ email, password })
350
+ });
351
+ if (!res.ok) {
352
+ const body = await res.json().catch(() => ({}));
353
+ const error = body.error;
354
+ const message = typeof error === "string" ? error : "Login failed";
355
+ throw new AuthHubError({ message, status: res.status });
356
+ }
357
+ const data = await res.json();
358
+ const setCookieHeaders = res.headers.getSetCookie?.() ?? [];
359
+ return { user: data.user, setCookieHeaders };
360
+ }
361
+ /**
362
+ * Validate an existing session by forwarding browser cookies.
363
+ * Returns the user if valid, null if session is expired/invalid.
364
+ *
365
+ * Usage in Next.js:
366
+ * ```ts
367
+ * const cookieHeader = request.headers.get("cookie") || "";
368
+ * const user = await hub.session.validate(cookieHeader);
369
+ * ```
370
+ */
371
+ async validate(cookieHeader) {
372
+ try {
373
+ const res = await fetch(this.client._url("/session"), {
374
+ method: "GET",
375
+ headers: {
376
+ Authorization: `Bearer ${this.client["apiKey"]}`,
377
+ Cookie: cookieHeader
378
+ }
379
+ });
380
+ if (!res.ok) return null;
381
+ const data = await res.json();
382
+ return data.user ?? null;
383
+ } catch {
384
+ return null;
385
+ }
386
+ }
387
+ /**
388
+ * Destroy a cookie-based session (logout).
389
+ * Returns Set-Cookie headers that clear the session cookies in the browser.
390
+ *
391
+ * Usage in Next.js API route:
392
+ * ```ts
393
+ * const cookieHeader = request.headers.get("cookie") || "";
394
+ * const setCookieHeaders = await hub.session.logout(cookieHeader);
395
+ * const response = NextResponse.json({ success: true });
396
+ * for (const h of setCookieHeaders) response.headers.append("Set-Cookie", h);
397
+ * ```
398
+ */
399
+ async logout(cookieHeader) {
400
+ try {
401
+ const res = await fetch(this.client._url("/session"), {
402
+ method: "DELETE",
403
+ headers: {
404
+ Authorization: `Bearer ${this.client["apiKey"]}`,
405
+ Cookie: cookieHeader
406
+ }
407
+ });
408
+ return res.headers.getSetCookie?.() ?? [];
409
+ } catch {
410
+ return [];
411
+ }
412
+ }
413
+ /**
414
+ * Change password for a user identified by their session cookies.
415
+ * Extracts the access token from the cookie header and uses it for JWT auth.
416
+ */
417
+ async changePassword(cookieHeader, currentPassword, newPassword) {
418
+ const accessToken = cookieHeader.split(";").map((c) => c.trim()).find((c) => c.startsWith("ah_access_token="))?.split("=").slice(1).join("=");
419
+ if (!accessToken) {
420
+ throw new AuthHubError({ message: "No access token found in cookies", status: 401 });
421
+ }
422
+ const res = await fetch(this.client._url("/user"), {
423
+ method: "PATCH",
424
+ headers: {
425
+ "Content-Type": "application/json",
426
+ Authorization: `Bearer ${accessToken}`
427
+ },
428
+ body: JSON.stringify({ currentPassword, newPassword })
429
+ });
430
+ return this.client._handleResponse(res);
431
+ }
432
+ };
433
+ var DbModule = class {
434
+ constructor(client) {
435
+ this.client = client;
436
+ }
437
+ /** Insert a row into a table */
438
+ async create(options) {
439
+ return this.client._apiRequest("/db/create", {
440
+ method: "POST",
441
+ body: JSON.stringify(options)
442
+ });
443
+ }
444
+ /** Query rows from a table with filtering, selection, ordering, pagination */
445
+ async read(options) {
446
+ return this.client._apiRequest("/db/read", {
447
+ method: "POST",
448
+ body: JSON.stringify(options)
449
+ });
450
+ }
451
+ /** Update rows matching a where clause */
452
+ async update(options) {
453
+ return this.client._apiRequest("/db/update", {
454
+ method: "POST",
455
+ body: JSON.stringify(options)
456
+ });
457
+ }
458
+ /** Delete rows matching a where clause */
459
+ async delete(options) {
460
+ return this.client._apiRequest("/db/delete", {
461
+ method: "POST",
462
+ body: JSON.stringify(options)
463
+ });
464
+ }
465
+ // ── Convenience shortcuts ──────────────────────────────────────────────
466
+ /** Find one row by column value */
467
+ async findOne(table, where) {
468
+ const result = await this.read({ table, where, limit: 1 });
469
+ return result.rows[0] ?? null;
470
+ }
471
+ /** Find one row by id */
472
+ async findById(table, id) {
473
+ return this.findOne(table, { id });
474
+ }
475
+ /** Count rows matching a where clause */
476
+ async count(table, where) {
477
+ const result = await this.read({ table, where, select: ["id"], limit: 0 });
478
+ return result.count;
479
+ }
480
+ };
481
+ var StorageModule = class {
482
+ constructor(client) {
483
+ this.client = client;
484
+ }
485
+ /** Create a storage bucket */
486
+ async createBucket(options) {
487
+ return this.client._apiRequest("/storage/buckets", {
488
+ method: "POST",
489
+ body: JSON.stringify(options)
490
+ });
491
+ }
492
+ /** List all buckets */
493
+ async listBuckets() {
494
+ return this.client._apiRequest("/storage/buckets");
495
+ }
496
+ /** Delete a bucket and all its contents */
497
+ async deleteBucket(name) {
498
+ await this.client._apiRequest("/storage/buckets", {
499
+ method: "DELETE",
500
+ body: JSON.stringify({ name })
501
+ });
502
+ }
503
+ /** Upload a file to a bucket */
504
+ async upload(options) {
505
+ const formData = new FormData();
506
+ if (typeof Buffer !== "undefined" && Buffer.isBuffer(options.file)) {
507
+ const blob = new Blob([new Uint8Array(options.file)], { type: options.contentType || "application/octet-stream" });
508
+ formData.append("file", blob, options.key.split("/").pop() || "file");
509
+ } else {
510
+ formData.append("file", options.file);
511
+ }
512
+ formData.append("bucket", options.bucket);
513
+ formData.append("key", options.key);
514
+ const headers = new Headers();
515
+ headers.set("Authorization", `Bearer ${this.client["apiKey"]}`);
516
+ const res = await fetch(this.client._url("/storage/upload"), {
517
+ method: "POST",
518
+ headers,
519
+ body: formData
520
+ });
521
+ return this.client._handleResponse(res);
522
+ }
523
+ /** Download a file. Returns the raw Response for streaming support. */
524
+ async download(bucket, key) {
525
+ const headers = new Headers();
526
+ headers.set("Authorization", `Bearer ${this.client["apiKey"]}`);
527
+ const res = await fetch(this.client._url(`/storage/object/${bucket}/${key}`), { headers });
528
+ if (!res.ok) {
529
+ const body = await res.json().catch(() => ({}));
530
+ const error = body.error;
531
+ const message = typeof error === "object" && error ? error.message : "Download failed";
532
+ throw new AuthHubError({ message, status: res.status });
533
+ }
534
+ return res;
535
+ }
536
+ /** Download a file and return it as a Buffer (Node) or ArrayBuffer (browser) */
537
+ async downloadBuffer(bucket, key) {
538
+ const res = await this.download(bucket, key);
539
+ return res.arrayBuffer();
540
+ }
541
+ /** Get the public URL for a file (only works for public buckets) */
542
+ getPublicUrl(bucket, key) {
543
+ return this.client._url(`/storage/object/${bucket}/${key}`);
544
+ }
545
+ /** Delete a file from a bucket */
546
+ async deleteObject(bucket, key) {
547
+ await this.client._apiRequest(`/storage/object/${bucket}/${key}`, {
548
+ method: "DELETE"
549
+ });
550
+ }
551
+ /** List objects in a bucket */
552
+ async list(options) {
553
+ return this.client._apiRequest("/storage/list", {
554
+ method: "POST",
555
+ body: JSON.stringify(options)
556
+ });
557
+ }
558
+ };
559
+ var OAuthModule = class {
560
+ constructor(client) {
561
+ this.client = client;
562
+ }
563
+ /**
564
+ * Get the URL to redirect the user to for OAuth login.
565
+ *
566
+ * ```ts
567
+ * const url = hub.oauth.getAuthUrl("google", "/dashboard");
568
+ * window.location.href = url;
569
+ * ```
570
+ */
571
+ getAuthUrl(provider, redirectTo) {
572
+ const url = new URL(this.client._url(`/auth/${provider}`));
573
+ if (redirectTo) url.searchParams.set("redirect_to", redirectTo);
574
+ return url.toString();
575
+ }
576
+ /**
577
+ * Parse OAuth tokens from the URL hash fragment after a redirect callback.
578
+ * Returns null if no tokens are found in the hash.
579
+ *
580
+ * ```ts
581
+ * const result = hub.oauth.handleCallback();
582
+ * if (result) {
583
+ * console.log("Logged in via", result.provider, result.access_token);
584
+ * // Tokens are automatically stored
585
+ * }
586
+ * ```
587
+ */
588
+ async handleCallback(hash) {
589
+ const fragment = hash ?? (typeof window !== "undefined" ? window.location.hash : "");
590
+ if (!fragment || fragment.length < 2) return null;
591
+ const params = new URLSearchParams(fragment.slice(1));
592
+ const accessToken = params.get("access_token");
593
+ const refreshToken = params.get("refresh_token");
594
+ const expiresIn = params.get("expires_in");
595
+ const provider = params.get("provider");
596
+ const tokenType = params.get("token_type");
597
+ if (!accessToken || !refreshToken) return null;
598
+ const result = {
599
+ access_token: accessToken,
600
+ refresh_token: refreshToken,
601
+ token_type: tokenType || "bearer",
602
+ expires_in: expiresIn ? parseInt(expiresIn, 10) : 900,
603
+ provider: provider || "unknown"
604
+ };
605
+ await this.client._saveAuthTokens({
606
+ access_token: result.access_token,
607
+ refresh_token: result.refresh_token,
608
+ token_type: "bearer",
609
+ expires_in: result.expires_in,
610
+ user: {}
611
+ // user can be fetched separately via auth.getUser()
612
+ });
613
+ if (typeof window !== "undefined" && window.history?.replaceState) {
614
+ window.history.replaceState(null, "", window.location.pathname + window.location.search);
615
+ }
616
+ return result;
617
+ }
618
+ /**
619
+ * Convenience: redirect to OAuth provider login page.
620
+ * Only works in browser environments.
621
+ */
622
+ signInWithProvider(provider, redirectTo) {
623
+ if (typeof window === "undefined") {
624
+ throw new Error("signInWithProvider() is only available in browser environments");
625
+ }
626
+ window.location.href = this.getAuthUrl(provider, redirectTo);
627
+ }
628
+ };
629
+ var RealtimeClient = class {
630
+ constructor(config) {
631
+ this.ws = null;
632
+ this.listeners = /* @__PURE__ */ new Map();
633
+ this.connected = false;
634
+ this.authenticated = false;
635
+ this.config = config;
636
+ }
637
+ /**
638
+ * Connect to the realtime server and authenticate.
639
+ *
640
+ * ```ts
641
+ * const realtime = hub.realtime("ws://localhost:4000");
642
+ * await realtime.connect(accessToken);
643
+ * ```
644
+ */
645
+ connect(accessToken) {
646
+ return new Promise((resolve, reject) => {
647
+ const url = `${this.config.url}/realtime/v1/${this.config.projectId}`;
648
+ try {
649
+ this.ws = new WebSocket(url);
650
+ } catch (err) {
651
+ reject(err);
652
+ return;
653
+ }
654
+ const timeout = setTimeout(() => {
655
+ reject(new Error("Connection timeout"));
656
+ this.close();
657
+ }, 15e3);
658
+ this.ws.onopen = () => {
659
+ this.connected = true;
660
+ this.ws.send(JSON.stringify({ type: "auth", access_token: accessToken }));
661
+ };
662
+ this.ws.onmessage = (event) => {
663
+ let msg;
664
+ try {
665
+ msg = JSON.parse(typeof event.data === "string" ? event.data : String(event.data));
666
+ } catch {
667
+ return;
668
+ }
669
+ if (msg.type === "auth") {
670
+ if (msg.status === "ok") {
671
+ this.authenticated = true;
672
+ clearTimeout(timeout);
673
+ resolve();
674
+ } else {
675
+ clearTimeout(timeout);
676
+ reject(new Error(msg.message || "Authentication failed"));
677
+ }
678
+ return;
679
+ }
680
+ if (msg.type === "mutation" && msg.channel) {
681
+ const handlers = this.listeners.get(msg.channel);
682
+ if (handlers) {
683
+ for (const handler of handlers) {
684
+ try {
685
+ handler(msg.event || "unknown", msg.data || {});
686
+ } catch {
687
+ }
688
+ }
689
+ }
690
+ }
691
+ };
692
+ this.ws.onerror = (err) => {
693
+ clearTimeout(timeout);
694
+ if (!this.authenticated) {
695
+ reject(err);
696
+ }
697
+ };
698
+ this.ws.onclose = () => {
699
+ this.connected = false;
700
+ this.authenticated = false;
701
+ };
702
+ });
703
+ }
704
+ /**
705
+ * Subscribe to changes on a table.
706
+ *
707
+ * ```ts
708
+ * realtime.on("table:users", (event, data) => {
709
+ * console.log(event); // "INSERT", "UPDATE", "DELETE"
710
+ * console.log(data); // { table, operation, id, timestamp }
711
+ * });
712
+ * ```
713
+ */
714
+ on(channel, handler) {
715
+ if (!this.listeners.has(channel)) {
716
+ this.listeners.set(channel, /* @__PURE__ */ new Set());
717
+ }
718
+ this.listeners.get(channel).add(handler);
719
+ if (this.ws && this.authenticated) {
720
+ this.ws.send(JSON.stringify({ type: "subscribe", channel }));
721
+ }
722
+ return this;
723
+ }
724
+ /** Remove a specific handler from a channel */
725
+ off(channel, handler) {
726
+ const handlers = this.listeners.get(channel);
727
+ if (handlers) {
728
+ handlers.delete(handler);
729
+ if (handlers.size === 0) {
730
+ this.listeners.delete(channel);
731
+ if (this.ws && this.authenticated) {
732
+ this.ws.send(JSON.stringify({ type: "unsubscribe", channel }));
733
+ }
734
+ }
735
+ }
736
+ return this;
737
+ }
738
+ /**
739
+ * Subscribe to a table and send the subscribe message.
740
+ * Alias for `on()` that returns a Promise resolving when the server confirms.
741
+ */
742
+ subscribe(channel, handler) {
743
+ return new Promise((resolve, reject) => {
744
+ if (!this.ws || !this.authenticated) {
745
+ reject(new Error("Not connected. Call connect() first."));
746
+ return;
747
+ }
748
+ const onMessage = (event) => {
749
+ try {
750
+ const msg = JSON.parse(typeof event.data === "string" ? event.data : String(event.data));
751
+ if (msg.type === "subscribed" && msg.channel === channel) {
752
+ this.ws.removeEventListener("message", onMessage);
753
+ resolve();
754
+ }
755
+ } catch {
756
+ }
757
+ };
758
+ this.ws.addEventListener("message", onMessage);
759
+ this.on(channel, handler);
760
+ });
761
+ }
762
+ /** Unsubscribe from a channel (removes all handlers) */
763
+ unsubscribe(channel) {
764
+ this.listeners.delete(channel);
765
+ if (this.ws && this.authenticated) {
766
+ this.ws.send(JSON.stringify({ type: "unsubscribe", channel }));
767
+ }
768
+ }
769
+ /** Close the WebSocket connection */
770
+ close() {
771
+ if (this.ws) {
772
+ this.ws.close();
773
+ this.ws = null;
774
+ }
775
+ this.connected = false;
776
+ this.authenticated = false;
777
+ this.listeners.clear();
778
+ }
779
+ /** Check if connected and authenticated */
780
+ isConnected() {
781
+ return this.connected && this.authenticated;
782
+ }
783
+ };
784
+ // Annotate the CommonJS export names for ESM import in node:
785
+ 0 && (module.exports = {
786
+ AuthHubClient,
787
+ AuthHubError,
788
+ LocalStorageTokenStore,
789
+ MemoryTokenStore,
790
+ RealtimeClient,
791
+ TokenManager
792
+ });