vasabase-js 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +357 -0
  2. package/dist/index.js +1089 -0
  3. package/package.json +21 -0
package/dist/index.js ADDED
@@ -0,0 +1,1089 @@
1
+ export function createClient(url, key, options) {
2
+ return new VasabaseClient(url, key, options);
3
+ }
4
+ export class PostgrestError extends Error {
5
+ constructor(message, code, details) {
6
+ super(message);
7
+ this.name = 'PostgrestError';
8
+ this.code = code;
9
+ this.details = details;
10
+ }
11
+ }
12
+ export class AuthError extends Error {
13
+ constructor(message, code, details) {
14
+ super(message);
15
+ this.name = 'AuthError';
16
+ this.code = code;
17
+ this.details = details;
18
+ }
19
+ }
20
+ export class FunctionsError extends Error {
21
+ constructor(message, code) {
22
+ super(message);
23
+ this.name = 'FunctionsError';
24
+ this.code = code;
25
+ }
26
+ }
27
+ export class VasabaseClient {
28
+ constructor(url, key, options) {
29
+ this.url = url;
30
+ this.key = key;
31
+ this.projectRef = options?.projectRef || '';
32
+ this.auth = new AuthClient(`${url}/auth/v1`, key, this.projectRef);
33
+ const getAuthHeaders = () => {
34
+ const h = { apikey: this.key };
35
+ const tok = this.auth.currentSession?.access_token;
36
+ if (tok)
37
+ h['Authorization'] = `Bearer ${tok}`;
38
+ return h;
39
+ };
40
+ this.rest = new PostgrestClient(`${url}/rest/v1`, key, this.projectRef, getAuthHeaders);
41
+ this.storage = new StorageClient(`${url}/storage/v1`, key, this.projectRef, getAuthHeaders);
42
+ this.realtime = new RealtimeClient(url, key, this.projectRef);
43
+ this.functions = new FunctionsClient(`${url}/functions/v1`, key, this.projectRef, getAuthHeaders);
44
+ }
45
+ from(table) {
46
+ return this.rest.from(table);
47
+ }
48
+ }
49
+ class PostgrestClient {
50
+ constructor(url, key, projectRef, getAuthHeaders) {
51
+ this.url = url;
52
+ this.key = key;
53
+ this.projectRef = projectRef;
54
+ this.getAuthHeaders = getAuthHeaders;
55
+ }
56
+ from(table) {
57
+ return new PostgrestQueryBuilder(this.url, this.key, this.projectRef, this.getAuthHeaders, table);
58
+ }
59
+ }
60
+ class PostgrestQueryBuilder {
61
+ constructor(url, key, projectRef, getAuthHeaders, table, method = 'GET', query = {}, body = null, headers = {}, notPrefix = '') {
62
+ this.url = url;
63
+ this.key = key;
64
+ this.projectRef = projectRef;
65
+ this.getAuthHeaders = getAuthHeaders;
66
+ this.table = table;
67
+ this.method = method;
68
+ this.query = { ...query };
69
+ this.body = body;
70
+ this.headers = { ...headers };
71
+ this._notPrefix = notPrefix;
72
+ }
73
+ clone(overrides = {}) {
74
+ return new PostgrestQueryBuilder(this.url, this.key, this.projectRef, this.getAuthHeaders, this.table, overrides.method ?? this.method, overrides.query ?? { ...this.query }, overrides.body !== undefined ? overrides.body : this.body, overrides.headers ?? { ...this.headers }, overrides.notPrefix ?? this._notPrefix);
75
+ }
76
+ select(columns = '*') {
77
+ const q = { ...this.query, select: columns };
78
+ // If we're on a mutation method (POST/PATCH/DELETE), don't flip to GET;
79
+ // the execute() method will handle setting Prefer=return=representation
80
+ if (this.method === 'GET') {
81
+ return this.clone({ query: q });
82
+ }
83
+ return this.clone({ query: q });
84
+ }
85
+ insert(data) {
86
+ return this.clone({
87
+ method: 'POST',
88
+ body: data,
89
+ headers: { ...this.headers, Prefer: 'return=minimal' },
90
+ });
91
+ }
92
+ upsert(data, onConflict) {
93
+ const q = { ...this.query };
94
+ if (onConflict)
95
+ q['on_conflict'] = onConflict;
96
+ return this.clone({
97
+ method: 'POST',
98
+ body: data,
99
+ query: q,
100
+ headers: { ...this.headers, Prefer: 'return=minimal,resolution=merge-duplicates' },
101
+ });
102
+ }
103
+ update(data) {
104
+ return this.clone({
105
+ method: 'PATCH',
106
+ body: data,
107
+ headers: { ...this.headers, Prefer: 'return=minimal' },
108
+ });
109
+ }
110
+ delete() {
111
+ return this.clone({
112
+ method: 'DELETE',
113
+ headers: { ...this.headers, Prefer: 'return=minimal' },
114
+ });
115
+ }
116
+ eq(column, value) {
117
+ const q = { ...this.query, [column]: `${this._notPrefix}eq.${value}` };
118
+ return this.clone({ query: q });
119
+ }
120
+ neq(column, value) {
121
+ const q = { ...this.query, [column]: `${this._notPrefix}neq.${value}` };
122
+ return this.clone({ query: q });
123
+ }
124
+ gt(column, value) {
125
+ const q = { ...this.query, [column]: `${this._notPrefix}gt.${value}` };
126
+ return this.clone({ query: q });
127
+ }
128
+ gte(column, value) {
129
+ const q = { ...this.query, [column]: `${this._notPrefix}gte.${value}` };
130
+ return this.clone({ query: q });
131
+ }
132
+ lt(column, value) {
133
+ const q = { ...this.query, [column]: `${this._notPrefix}lt.${value}` };
134
+ return this.clone({ query: q });
135
+ }
136
+ lte(column, value) {
137
+ const q = { ...this.query, [column]: `${this._notPrefix}lte.${value}` };
138
+ return this.clone({ query: q });
139
+ }
140
+ like(column, pattern) {
141
+ const q = { ...this.query, [column]: `${this._notPrefix}like.${pattern}` };
142
+ return this.clone({ query: q });
143
+ }
144
+ ilike(column, pattern) {
145
+ const q = { ...this.query, [column]: `${this._notPrefix}ilike.${pattern}` };
146
+ return this.clone({ query: q });
147
+ }
148
+ is_(column, value) {
149
+ let suffix;
150
+ if (value === null)
151
+ suffix = 'null';
152
+ else if (value === true)
153
+ suffix = 'true';
154
+ else
155
+ suffix = 'false';
156
+ const q = { ...this.query, [column]: `${this._notPrefix}is.${suffix}` };
157
+ return this.clone({ query: q });
158
+ }
159
+ isNull(column) {
160
+ const q = { ...this.query, [column]: `${this._notPrefix}is.null` };
161
+ return this.clone({ query: q });
162
+ }
163
+ in_(column, values) {
164
+ const q = { ...this.query, [column]: `${this._notPrefix}in.(${values.join(',')})` };
165
+ return this.clone({ query: q });
166
+ }
167
+ contains(column, value) {
168
+ const q = { ...this.query, [column]: `${this._notPrefix}cs.${JSON.stringify(value)}` };
169
+ return this.clone({ query: q });
170
+ }
171
+ containedBy(column, value) {
172
+ const q = { ...this.query, [column]: `${this._notPrefix}cd.${JSON.stringify(value)}` };
173
+ return this.clone({ query: q });
174
+ }
175
+ match(column, query) {
176
+ const q = { ...this.query, [column]: `${this._notPrefix}match.${JSON.stringify(query)}` };
177
+ return this.clone({ query: q });
178
+ }
179
+ imatch(column, query) {
180
+ const q = { ...this.query, [column]: `${this._notPrefix}imatch.${JSON.stringify(query)}` };
181
+ return this.clone({ query: q });
182
+ }
183
+ or(filter) {
184
+ const existing = this.query['or'] || '';
185
+ const newOr = existing ? `(${existing}),${filter}` : filter;
186
+ const q = { ...this.query, or: newOr };
187
+ return this.clone({ query: q });
188
+ }
189
+ and(filter) {
190
+ const existing = this.query['and'] || '';
191
+ const newAnd = existing ? `(${existing}),${filter}` : filter;
192
+ const q = { ...this.query, and: newAnd };
193
+ return this.clone({ query: q });
194
+ }
195
+ not_() {
196
+ return this.clone({ notPrefix: 'not.' });
197
+ }
198
+ order(column, ascending = true) {
199
+ const q = { ...this.query, order: `${column}.${ascending ? 'asc' : 'desc'}` };
200
+ return this.clone({ query: q });
201
+ }
202
+ limit(n) {
203
+ const q = { ...this.query, limit: String(n) };
204
+ return this.clone({ query: q });
205
+ }
206
+ offset(n) {
207
+ const q = { ...this.query, offset: String(n) };
208
+ return this.clone({ query: q });
209
+ }
210
+ range(from, to) {
211
+ const h = { ...this.headers, Range: `${from}-${to}` };
212
+ return this.clone({ headers: h });
213
+ }
214
+ single() {
215
+ const h = { ...this.headers, Accept: 'application/vnd.pgrst.object+json' };
216
+ return this.clone({ headers: h });
217
+ }
218
+ maybeSingle() {
219
+ const h = { ...this.headers, Accept: 'application/vnd.pgrst.object+json' };
220
+ // maybeSingle converts 406 to null, handled in execute
221
+ return this.clone({ headers: h });
222
+ }
223
+ then(onFulfilled, onRejected) {
224
+ return this.execute().then(onFulfilled, onRejected);
225
+ }
226
+ catch(onRejected) {
227
+ return this.execute().catch(onRejected);
228
+ }
229
+ async execute() {
230
+ const params = new URLSearchParams();
231
+ for (const [k, v] of Object.entries(this.query)) {
232
+ params.set(k, v);
233
+ }
234
+ // Handle select on mutations -> Prefer return=representation
235
+ let headers = {
236
+ 'apikey': this.key,
237
+ ...this.getAuthHeaders(),
238
+ ...this.headers,
239
+ };
240
+ if ((this.method === 'POST' || this.method === 'PATCH' || this.method === 'DELETE') && this.query['select']) {
241
+ const existingPrefer = this.headers['Prefer'] || '';
242
+ const hasResolution = existingPrefer.includes('resolution=merge-duplicates');
243
+ headers['Prefer'] = hasResolution
244
+ ? 'return=representation,resolution=merge-duplicates'
245
+ : 'return=representation';
246
+ }
247
+ if (this.projectRef) {
248
+ headers['x-project-ref'] = this.projectRef;
249
+ }
250
+ if (this.method !== 'GET' && this.method !== 'DELETE' && this.body !== null) {
251
+ headers['Content-Type'] = 'application/json';
252
+ }
253
+ const url = `${this.url}/${this.table}?${params.toString()}`;
254
+ const fetchOpts = {
255
+ method: this.method,
256
+ headers,
257
+ };
258
+ if (this.method !== 'GET' && this.method !== 'DELETE' && this.body !== null) {
259
+ fetchOpts.body = JSON.stringify(this.body);
260
+ }
261
+ try {
262
+ const res = await fetch(url, fetchOpts);
263
+ // maybeSingle: if 406 Not Acceptable, return null data
264
+ if (this.headers['Accept'] === 'application/vnd.pgrst.object+json' &&
265
+ !Object.keys(this.headers).some(k => k === 'Prefer' && this.headers[k] === 'return=representation') &&
266
+ res.status === 406) {
267
+ return { data: null, error: null };
268
+ }
269
+ const text = await res.text();
270
+ let data = null;
271
+ try {
272
+ data = text ? JSON.parse(text) : null;
273
+ }
274
+ catch {
275
+ data = text;
276
+ }
277
+ if (!res.ok) {
278
+ const errMsg = typeof data?.message === 'string' ? data.message : 'Postgrest request failed';
279
+ const errCode = data?.code;
280
+ const errDetails = data?.details;
281
+ return { data: null, error: new PostgrestError(errMsg, errCode, errDetails) };
282
+ }
283
+ return { data, error: null };
284
+ }
285
+ catch (e) {
286
+ return { data: null, error: new PostgrestError(e.message || 'Network error') };
287
+ }
288
+ }
289
+ }
290
+ class AuthClient {
291
+ constructor(url, key, projectRef) {
292
+ this.currentSession = null;
293
+ this.currentUser = null;
294
+ this.listeners = [];
295
+ this.url = url;
296
+ this.key = key;
297
+ this.projectRef = projectRef;
298
+ }
299
+ onAuthStateChange(cb) {
300
+ this.listeners.push(cb);
301
+ return () => {
302
+ this.listeners = this.listeners.filter(l => l !== cb);
303
+ };
304
+ }
305
+ emit(event, session) {
306
+ this.currentSession = session;
307
+ this.currentUser = session?.user ?? null;
308
+ for (const cb of this.listeners) {
309
+ try {
310
+ cb(event, session);
311
+ }
312
+ catch { /* swallow */ }
313
+ }
314
+ }
315
+ getHeaders(token) {
316
+ const h = {
317
+ 'Content-Type': 'application/json',
318
+ 'apikey': this.key,
319
+ };
320
+ if (this.projectRef)
321
+ h['x-project-ref'] = this.projectRef;
322
+ if (token)
323
+ h['Authorization'] = `Bearer ${token}`;
324
+ return h;
325
+ }
326
+ async initialize() {
327
+ if (typeof localStorage === 'undefined')
328
+ return;
329
+ const stored = localStorage.getItem('vasabase_auth_token');
330
+ if (!stored)
331
+ return;
332
+ try {
333
+ const session = JSON.parse(stored);
334
+ this.emit('SIGNED_IN', session);
335
+ }
336
+ catch {
337
+ localStorage.removeItem('vasabase_auth_token');
338
+ }
339
+ }
340
+ persist(session) {
341
+ if (typeof localStorage === 'undefined')
342
+ return;
343
+ if (session) {
344
+ localStorage.setItem('vasabase_auth_token', JSON.stringify(session));
345
+ }
346
+ else {
347
+ localStorage.removeItem('vasabase_auth_token');
348
+ }
349
+ }
350
+ async signUp(body) {
351
+ const res = await fetch(`${this.url}/signup`, {
352
+ method: 'POST',
353
+ headers: this.getHeaders(),
354
+ body: JSON.stringify(body),
355
+ });
356
+ const data = await res.json().catch(() => null);
357
+ if (!res.ok) {
358
+ return { data: null, error: new AuthError(data?.message || 'Signup failed', data?.code, data?.details) };
359
+ }
360
+ const session = data;
361
+ this.persist(session);
362
+ this.emit('SIGNED_IN', session);
363
+ return { data: session, error: null };
364
+ }
365
+ async signInWithPassword(body) {
366
+ const res = await fetch(`${this.url}/token?grant_type=password`, {
367
+ method: 'POST',
368
+ headers: this.getHeaders(),
369
+ body: JSON.stringify(body),
370
+ });
371
+ const data = await res.json().catch(() => null);
372
+ if (!res.ok) {
373
+ return { data: null, error: new AuthError(data?.message || 'Sign-in failed', data?.code, data?.details) };
374
+ }
375
+ const session = data;
376
+ this.persist(session);
377
+ this.emit('SIGNED_IN', session);
378
+ return { data: session, error: null };
379
+ }
380
+ signInWithOAuth(options) {
381
+ const params = new URLSearchParams();
382
+ params.set('provider', options.provider);
383
+ if (options.redirectTo)
384
+ params.set('redirect_to', options.redirectTo);
385
+ if (options.codeChallenge)
386
+ params.set('code_challenge', options.codeChallenge);
387
+ return `${this.url}/authorize?${params.toString()}`;
388
+ }
389
+ async signInWithOtp(body) {
390
+ const res = await fetch(`${this.url}/otp`, {
391
+ method: 'POST',
392
+ headers: this.getHeaders(),
393
+ body: JSON.stringify(body),
394
+ });
395
+ const data = await res.json().catch(() => null);
396
+ if (!res.ok) {
397
+ return { error: new AuthError(data?.message || 'OTP send failed', data?.code, data?.details) };
398
+ }
399
+ return { error: null };
400
+ }
401
+ async signInWithIdToken(body) {
402
+ const res = await fetch(`${this.url}/token?grant_type=id_token`, {
403
+ method: 'POST',
404
+ headers: this.getHeaders(),
405
+ body: JSON.stringify({ provider: body.provider, id_token: body.idToken }),
406
+ });
407
+ const data = await res.json().catch(() => null);
408
+ if (!res.ok) {
409
+ return { data: null, error: new AuthError(data?.message || 'ID token sign-in failed', data?.code, data?.details) };
410
+ }
411
+ const session = data;
412
+ this.persist(session);
413
+ this.emit('SIGNED_IN', session);
414
+ return { data: session, error: null };
415
+ }
416
+ async signInAnonymously() {
417
+ const res = await fetch(`${this.url}/signup`, {
418
+ method: 'POST',
419
+ headers: this.getHeaders(),
420
+ body: JSON.stringify({}),
421
+ });
422
+ const data = await res.json().catch(() => null);
423
+ if (!res.ok) {
424
+ return { data: null, error: new AuthError(data?.message || 'Anonymous sign-in failed', data?.code, data?.details) };
425
+ }
426
+ const session = data;
427
+ this.persist(session);
428
+ this.emit('SIGNED_IN', session);
429
+ return { data: session, error: null };
430
+ }
431
+ async reauthenticate(body) {
432
+ const res = await fetch(`${this.url}/token?grant_type=password`, {
433
+ method: 'POST',
434
+ headers: this.getHeaders(),
435
+ body: JSON.stringify(body),
436
+ });
437
+ const data = await res.json().catch(() => null);
438
+ if (!res.ok) {
439
+ return { data: null, error: new AuthError(data?.message || 'Reauthentication failed', data?.code, data?.details) };
440
+ }
441
+ const session = data;
442
+ this.persist(session);
443
+ this.emit('TOKEN_REFRESHED', session);
444
+ return { data: session, error: null };
445
+ }
446
+ async signOut() {
447
+ const token = this.currentSession?.access_token;
448
+ const res = await fetch(`${this.url}/logout`, {
449
+ method: 'POST',
450
+ headers: this.getHeaders(token),
451
+ });
452
+ this.persist(null);
453
+ this.emit('SIGNED_OUT', null);
454
+ if (!res.ok) {
455
+ const data = await res.json().catch(() => null);
456
+ return { error: new AuthError(data?.message || 'Sign-out failed', data?.code, data?.details) };
457
+ }
458
+ return { error: null };
459
+ }
460
+ setAuth(token) {
461
+ const payload = parseJwt(token);
462
+ const session = {
463
+ access_token: token,
464
+ expires_in: payload.exp ? payload.exp - Math.floor(Date.now() / 1000) : 3600,
465
+ user: {
466
+ id: payload.sub || '',
467
+ email: payload.email || '',
468
+ user_metadata: payload.user_metadata,
469
+ },
470
+ };
471
+ this.persist(session);
472
+ this.emit('SIGNED_IN', session);
473
+ }
474
+ setSession(session) {
475
+ this.persist(session);
476
+ this.emit('SIGNED_IN', session);
477
+ }
478
+ async refreshSession() {
479
+ const refreshToken = this.currentSession?.refresh_token;
480
+ if (!refreshToken) {
481
+ return { data: null, error: new AuthError('No refresh token available') };
482
+ }
483
+ const res = await fetch(`${this.url}/token?grant_type=refresh_token`, {
484
+ method: 'POST',
485
+ headers: this.getHeaders(),
486
+ body: JSON.stringify({ refresh_token: refreshToken }),
487
+ });
488
+ const data = await res.json().catch(() => null);
489
+ if (!res.ok) {
490
+ return { data: null, error: new AuthError(data?.message || 'Refresh failed', data?.code, data?.details) };
491
+ }
492
+ const session = data;
493
+ this.persist(session);
494
+ this.emit('TOKEN_REFRESHED', session);
495
+ return { data: session, error: null };
496
+ }
497
+ async updateUser(attributes, options) {
498
+ const token = this.currentSession?.access_token;
499
+ const body = { ...attributes };
500
+ if (options?.emailRedirectTo)
501
+ body['email_redirect_to'] = options.emailRedirectTo;
502
+ const res = await fetch(`${this.url}/user`, {
503
+ method: 'PUT',
504
+ headers: this.getHeaders(token),
505
+ body: JSON.stringify(body),
506
+ });
507
+ const data = await res.json().catch(() => null);
508
+ if (!res.ok) {
509
+ return { data: null, error: new AuthError(data?.message || 'Update user failed', data?.code, data?.details) };
510
+ }
511
+ if (this.currentSession) {
512
+ this.currentSession.user = data;
513
+ this.currentUser = data;
514
+ this.persist(this.currentSession);
515
+ }
516
+ this.emit('USER_UPDATED', this.currentSession);
517
+ return { data: data, error: null };
518
+ }
519
+ async resetPasswordForEmail(email, options) {
520
+ const body = { email };
521
+ if (options?.redirectTo)
522
+ body['redirect_to'] = options.redirectTo;
523
+ const res = await fetch(`${this.url}/recover`, {
524
+ method: 'POST',
525
+ headers: this.getHeaders(),
526
+ body: JSON.stringify(body),
527
+ });
528
+ const data = await res.json().catch(() => null);
529
+ if (!res.ok) {
530
+ return { error: new AuthError(data?.message || 'Password reset failed', data?.code, data?.details) };
531
+ }
532
+ return { error: null };
533
+ }
534
+ async enroll(body) {
535
+ const token = this.currentSession?.access_token;
536
+ const res = await fetch(`${this.url}/factors`, {
537
+ method: 'POST',
538
+ headers: this.getHeaders(token),
539
+ body: JSON.stringify({ friendly_name: body.friendlyName }),
540
+ });
541
+ const data = await res.json().catch(() => null);
542
+ if (!res.ok) {
543
+ return { data: null, error: new AuthError(data?.message || 'MFA enroll failed', data?.code, data?.details) };
544
+ }
545
+ return { data: data, error: null };
546
+ }
547
+ async verifyFactor(body) {
548
+ const token = this.currentSession?.access_token;
549
+ const res = await fetch(`${this.url}/factors/${body.factorId}/verify`, {
550
+ method: 'POST',
551
+ headers: this.getHeaders(token),
552
+ body: JSON.stringify({ code: body.code, challenge_id: body.challengeId }),
553
+ });
554
+ const data = await res.json().catch(() => null);
555
+ if (!res.ok) {
556
+ return { data: null, error: new AuthError(data?.message || 'MFA verify failed', data?.code, data?.details) };
557
+ }
558
+ return { data, error: null };
559
+ }
560
+ async createChallenge(body) {
561
+ const token = this.currentSession?.access_token;
562
+ const res = await fetch(`${this.url}/factors/${body.factorId}/challenge`, {
563
+ method: 'POST',
564
+ headers: this.getHeaders(token),
565
+ });
566
+ const data = await res.json().catch(() => null);
567
+ if (!res.ok) {
568
+ return { data: null, error: new AuthError(data?.message || 'MFA challenge failed', data?.code, data?.details) };
569
+ }
570
+ return { data, error: null };
571
+ }
572
+ async verifyChallenge(body) {
573
+ const token = this.currentSession?.access_token;
574
+ const res = await fetch(`${this.url}/verify`, {
575
+ method: 'POST',
576
+ headers: this.getHeaders(token),
577
+ body: JSON.stringify({ challenge_id: body.challengeId, code: body.code }),
578
+ });
579
+ const data = await res.json().catch(() => null);
580
+ if (!res.ok) {
581
+ return { data: null, error: new AuthError(data?.message || 'MFA verify challenge failed', data?.code, data?.details) };
582
+ }
583
+ return { data, error: null };
584
+ }
585
+ async unenroll(body) {
586
+ const token = this.currentSession?.access_token;
587
+ const res = await fetch(`${this.url}/factors/${body.factorId}`, {
588
+ method: 'DELETE',
589
+ headers: this.getHeaders(token),
590
+ });
591
+ if (!res.ok) {
592
+ const data = await res.json().catch(() => null);
593
+ return { error: new AuthError(data?.message || 'MFA unenroll failed', data?.code, data?.details) };
594
+ }
595
+ return { error: null };
596
+ }
597
+ async listFactors() {
598
+ const token = this.currentSession?.access_token;
599
+ const res = await fetch(`${this.url}/factors`, {
600
+ method: 'GET',
601
+ headers: this.getHeaders(token),
602
+ });
603
+ const data = await res.json().catch(() => null);
604
+ if (!res.ok) {
605
+ return { data: null, error: new AuthError(data?.message || 'MFA list factors failed', data?.code, data?.details) };
606
+ }
607
+ return { data: data, error: null };
608
+ }
609
+ }
610
+ function parseJwt(token) {
611
+ try {
612
+ const base64Url = token.split('.')[1];
613
+ const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
614
+ const jsonPayload = decodeURIComponent(atob(base64)
615
+ .split('')
616
+ .map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
617
+ .join(''));
618
+ return JSON.parse(jsonPayload);
619
+ }
620
+ catch {
621
+ return {};
622
+ }
623
+ }
624
+ class StorageClient {
625
+ constructor(url, key, projectRef, getAuthHeaders) {
626
+ this.url = url;
627
+ this.key = key;
628
+ this.projectRef = projectRef;
629
+ this.getAuthHeaders = getAuthHeaders;
630
+ }
631
+ from(bucketId) {
632
+ return new StorageFileApi(this.url, this.key, this.projectRef, this.getAuthHeaders, bucketId);
633
+ }
634
+ async createBucket(id, options) {
635
+ const headers = {
636
+ 'Content-Type': 'application/json',
637
+ 'apikey': this.key,
638
+ ...this.getAuthHeaders(),
639
+ };
640
+ if (this.projectRef)
641
+ headers['x-project-ref'] = this.projectRef;
642
+ const res = await fetch(`${this.url}/bucket`, {
643
+ method: 'POST',
644
+ headers,
645
+ body: JSON.stringify({ id, ...options }),
646
+ });
647
+ const data = await res.json().catch(() => null);
648
+ if (!res.ok) {
649
+ return { data: null, error: new PostgrestError(data?.message || 'Create bucket failed') };
650
+ }
651
+ return { data, error: null };
652
+ }
653
+ async deleteBucket(id) {
654
+ const headers = {
655
+ 'apikey': this.key,
656
+ ...this.getAuthHeaders(),
657
+ };
658
+ if (this.projectRef)
659
+ headers['x-project-ref'] = this.projectRef;
660
+ const res = await fetch(`${this.url}/bucket/${id}`, {
661
+ method: 'DELETE',
662
+ headers,
663
+ });
664
+ const data = await res.json().catch(() => null);
665
+ if (!res.ok) {
666
+ return { data: null, error: new PostgrestError(data?.message || 'Delete bucket failed') };
667
+ }
668
+ return { data, error: null };
669
+ }
670
+ async listBuckets() {
671
+ const headers = {
672
+ 'apikey': this.key,
673
+ ...this.getAuthHeaders(),
674
+ };
675
+ if (this.projectRef)
676
+ headers['x-project-ref'] = this.projectRef;
677
+ const res = await fetch(`${this.url}/bucket`, { headers });
678
+ const data = await res.json().catch(() => null);
679
+ if (!res.ok) {
680
+ return { data: null, error: new PostgrestError(data?.message || 'List buckets failed') };
681
+ }
682
+ return { data, error: null };
683
+ }
684
+ }
685
+ class StorageFileApi {
686
+ constructor(url, key, projectRef, getAuthHeaders, bucketId) {
687
+ this.url = url;
688
+ this.key = key;
689
+ this.projectRef = projectRef;
690
+ this.getAuthHeaders = getAuthHeaders;
691
+ this.bucketId = bucketId;
692
+ }
693
+ getHeaders() {
694
+ const h = {
695
+ apikey: this.key,
696
+ ...this.getAuthHeaders(),
697
+ };
698
+ if (this.projectRef)
699
+ h['x-project-ref'] = this.projectRef;
700
+ return h;
701
+ }
702
+ async upload(path, file) {
703
+ const formData = new FormData();
704
+ formData.append('file', file);
705
+ const headers = this.getHeaders();
706
+ // Let browser set Content-Type for FormData
707
+ delete headers['Content-Type'];
708
+ const res = await fetch(`${this.url}/object/${this.bucketId}/${path}`, { method: 'POST', headers, body: formData });
709
+ const data = await res.json().catch(() => null);
710
+ if (!res.ok) {
711
+ return { data: null, error: new PostgrestError(data?.message || 'Upload failed') };
712
+ }
713
+ return { data, error: null };
714
+ }
715
+ async download(path) {
716
+ const res = await fetch(`${this.url}/object/${this.bucketId}/${path}`, { headers: this.getHeaders() });
717
+ if (!res.ok) {
718
+ const errData = await res.json().catch(() => ({}));
719
+ return { data: null, error: new PostgrestError(errData.message || 'Download failed') };
720
+ }
721
+ const blob = await res.blob();
722
+ return { data: blob, error: null };
723
+ }
724
+ async remove(paths) {
725
+ const headers = { ...this.getHeaders(), 'Content-Type': 'application/json' };
726
+ const res = await fetch(`${this.url}/object/${this.bucketId}`, {
727
+ method: 'DELETE',
728
+ headers,
729
+ body: JSON.stringify({ prefixes: paths }),
730
+ });
731
+ if (!res.ok) {
732
+ const errData = await res.json().catch(() => ({}));
733
+ return { error: new PostgrestError(errData.message || 'Remove failed') };
734
+ }
735
+ return { error: null };
736
+ }
737
+ async move(fromPath, toPath) {
738
+ const headers = { ...this.getHeaders(), 'Content-Type': 'application/json' };
739
+ const res = await fetch(`${this.url}/object/move`, {
740
+ method: 'POST',
741
+ headers,
742
+ body: JSON.stringify({
743
+ bucketId: this.bucketId,
744
+ sourceKey: fromPath,
745
+ destinationKey: toPath,
746
+ }),
747
+ });
748
+ if (!res.ok) {
749
+ const errData = await res.json().catch(() => ({}));
750
+ return { error: new PostgrestError(errData.message || 'Move failed') };
751
+ }
752
+ return { error: null };
753
+ }
754
+ async copy(fromPath, toPath) {
755
+ const headers = { ...this.getHeaders(), 'Content-Type': 'application/json' };
756
+ const res = await fetch(`${this.url}/object/copy`, {
757
+ method: 'POST',
758
+ headers,
759
+ body: JSON.stringify({
760
+ bucketId: this.bucketId,
761
+ sourceKey: fromPath,
762
+ destinationKey: toPath,
763
+ }),
764
+ });
765
+ if (!res.ok) {
766
+ const errData = await res.json().catch(() => ({}));
767
+ return { error: new PostgrestError(errData.message || 'Copy failed') };
768
+ }
769
+ return { error: null };
770
+ }
771
+ async createSignedUrl(path, expiresIn = 3600) {
772
+ const headers = { ...this.getHeaders(), 'Content-Type': 'application/json' };
773
+ const res = await fetch(`${this.url}/object/sign/${this.bucketId}/${path}`, {
774
+ method: 'POST',
775
+ headers,
776
+ body: JSON.stringify({ expiresIn }),
777
+ });
778
+ const data = await res.json().catch(() => null);
779
+ if (!res.ok) {
780
+ return { data: null, error: new PostgrestError(data?.message || 'Create signed URL failed') };
781
+ }
782
+ return { data: data?.signedURL || data?.signedUrl || data?.url || null, error: null };
783
+ }
784
+ getPublicUrl(path, download) {
785
+ let url = `${this.url}/object/public/${this.bucketId}/${path}`;
786
+ if (download)
787
+ url += '?download';
788
+ return url;
789
+ }
790
+ }
791
+ class RealtimeClient {
792
+ constructor(url, key, projectRef) {
793
+ this.ws = null;
794
+ this.channels = new Map();
795
+ this.reconnectTimer = null;
796
+ this.connected = false;
797
+ this.url = url;
798
+ this.key = key;
799
+ this.projectRef = projectRef;
800
+ }
801
+ connect() {
802
+ if (this.ws && this.ws.readyState === WebSocket.OPEN)
803
+ return;
804
+ const wsUrl = this.url.replace(/^http/, 'ws') +
805
+ `/realtime/v1?apikey=${encodeURIComponent(this.key)}` +
806
+ (this.projectRef ? `&project_ref=${encodeURIComponent(this.projectRef)}` : '');
807
+ this.ws = new WebSocket(wsUrl);
808
+ this.ws.onopen = () => {
809
+ this.connected = true;
810
+ // Re-subscribe channels
811
+ for (const ch of this.channels.values()) {
812
+ if (ch.subscribed)
813
+ ch.resubscribe();
814
+ }
815
+ };
816
+ this.ws.onmessage = (event) => {
817
+ try {
818
+ const msg = JSON.parse(event.data);
819
+ const topic = msg.topic || '';
820
+ if (topic.startsWith('realtime:')) {
821
+ const channelName = topic.replace('realtime:', '');
822
+ const ch = this.channels.get(channelName);
823
+ if (ch)
824
+ ch.handleMessage(msg);
825
+ }
826
+ }
827
+ catch { /* ignore */ }
828
+ };
829
+ this.ws.onclose = () => {
830
+ this.connected = false;
831
+ // Auto-reconnect
832
+ this.reconnectTimer = setTimeout(() => this.connect(), 5000);
833
+ };
834
+ this.ws.onerror = () => {
835
+ this.ws?.close();
836
+ };
837
+ }
838
+ disconnect() {
839
+ if (this.reconnectTimer)
840
+ clearTimeout(this.reconnectTimer);
841
+ for (const ch of this.channels.values()) {
842
+ ch.unsubscribe();
843
+ }
844
+ this.channels.clear();
845
+ this.ws?.close();
846
+ this.ws = null;
847
+ this.connected = false;
848
+ }
849
+ on(table) {
850
+ return {
851
+ subscribe: (cb) => {
852
+ const sub = new RealtimeSubscription(this, table, '*', cb);
853
+ sub.subscribe();
854
+ return sub;
855
+ },
856
+ };
857
+ }
858
+ channel(table, filter) {
859
+ const channelName = filter
860
+ ? `${table}:${filter.event}:${filter.schema || 'public'}:${filter.filter || ''}`
861
+ : `${table}:*`;
862
+ const existing = this.channels.get(channelName);
863
+ if (existing)
864
+ return existing;
865
+ const ch = new RealtimeChannel(this, channelName, table, filter);
866
+ this.channels.set(channelName, ch);
867
+ return ch;
868
+ }
869
+ send(payload) {
870
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
871
+ this.ws.send(JSON.stringify(payload));
872
+ }
873
+ }
874
+ get isConnected() {
875
+ return this.connected;
876
+ }
877
+ removeChannel(name) {
878
+ this.channels.delete(name);
879
+ }
880
+ }
881
+ class RealtimeChannel {
882
+ constructor(client, name, table, filter) {
883
+ this._subscribed = false;
884
+ this.cdcCallbacks = [];
885
+ this.presenceCallbacks = [];
886
+ this.broadcastCallbacks = [];
887
+ this.presenceState = {};
888
+ this.client = client;
889
+ this.name = name;
890
+ this.table = table;
891
+ this.filter = filter;
892
+ }
893
+ get subscribed() {
894
+ return this._subscribed;
895
+ }
896
+ subscribe() {
897
+ if (this._subscribed)
898
+ return this;
899
+ this._subscribed = true;
900
+ if (this.client.isConnected) {
901
+ this.resubscribe();
902
+ }
903
+ else {
904
+ this.client.connect();
905
+ }
906
+ return this;
907
+ }
908
+ resubscribe() {
909
+ // Send CDC subscription
910
+ if (this.cdcCallbacks.length > 0 || !this.filter) {
911
+ const event = this.filter?.event || '*';
912
+ const schema = this.filter?.schema || 'public';
913
+ const payload = {
914
+ event: 'phx_join',
915
+ topic: `realtime:${this.name}`,
916
+ payload: {
917
+ access_token: '',
918
+ config: {
919
+ broadcast: { self: false },
920
+ presence: { key: '' },
921
+ postgres_changes: [{
922
+ event,
923
+ schema,
924
+ table: this.table,
925
+ filter: this.filter?.filter || null,
926
+ }],
927
+ },
928
+ },
929
+ ref: this.name,
930
+ };
931
+ this.client.send(payload);
932
+ }
933
+ }
934
+ unsubscribe() {
935
+ this._subscribed = false;
936
+ if (this.client.isConnected) {
937
+ this.client.send({
938
+ event: 'phx_leave',
939
+ topic: `realtime:${this.name}`,
940
+ ref: this.name,
941
+ });
942
+ }
943
+ this.cdcCallbacks = [];
944
+ this.presenceCallbacks = [];
945
+ this.broadcastCallbacks = [];
946
+ this.client.removeChannel(this.name);
947
+ }
948
+ on(event, cb) {
949
+ this.cdcCallbacks.push(cb);
950
+ const sub = new RealtimeSubscription(this.client, this.table, event, cb);
951
+ if (this._subscribed) {
952
+ sub._setActive(true);
953
+ }
954
+ return sub;
955
+ }
956
+ onPresence(cb) {
957
+ this.presenceCallbacks.push(cb);
958
+ return () => {
959
+ this.presenceCallbacks = this.presenceCallbacks.filter(c => c !== cb);
960
+ };
961
+ }
962
+ onBroadcast(eventOrCb, cb) {
963
+ const callback = typeof eventOrCb === 'function' ? eventOrCb : cb;
964
+ this.broadcastCallbacks.push(callback);
965
+ return () => {
966
+ this.broadcastCallbacks = this.broadcastCallbacks.filter(c => c !== callback);
967
+ };
968
+ }
969
+ broadcast(event, payload) {
970
+ this.client.send({
971
+ event: 'broadcast',
972
+ topic: `realtime:${this.name}`,
973
+ payload: { event, payload: payload || {} },
974
+ ref: this.name,
975
+ });
976
+ }
977
+ track(userState) {
978
+ this.presenceState = userState;
979
+ this.client.send({
980
+ event: 'presence',
981
+ topic: `realtime:${this.name}`,
982
+ payload: userState,
983
+ ref: this.name,
984
+ });
985
+ }
986
+ get stream() {
987
+ return {
988
+ on: (event, cb) => {
989
+ this.broadcastCallbacks.push(cb);
990
+ return () => {
991
+ this.broadcastCallbacks = this.broadcastCallbacks.filter(c => c !== cb);
992
+ };
993
+ },
994
+ };
995
+ }
996
+ handleMessage(msg) {
997
+ const event = msg.event;
998
+ if (event === 'postgres_changes' || event === 'cdc') {
999
+ const payload = msg.payload;
1000
+ for (const cb of this.cdcCallbacks) {
1001
+ try {
1002
+ cb(payload);
1003
+ }
1004
+ catch { /* swallow */ }
1005
+ }
1006
+ }
1007
+ else if (event === 'presence_state' || event === 'presence_diff') {
1008
+ for (const cb of this.presenceCallbacks) {
1009
+ try {
1010
+ cb(msg.payload);
1011
+ }
1012
+ catch { /* swallow */ }
1013
+ }
1014
+ }
1015
+ else if (event === 'broadcast') {
1016
+ for (const cb of this.broadcastCallbacks) {
1017
+ try {
1018
+ cb(msg.payload);
1019
+ }
1020
+ catch { /* swallow */ }
1021
+ }
1022
+ }
1023
+ }
1024
+ }
1025
+ class RealtimeSubscription {
1026
+ constructor(client, table, event, callback) {
1027
+ this._active = false;
1028
+ this.client = client;
1029
+ this.table = table;
1030
+ this.event = event;
1031
+ this.callback = callback;
1032
+ }
1033
+ _setActive(active) {
1034
+ this._active = active;
1035
+ }
1036
+ subscribe() {
1037
+ const ch = this.client.channel(this.table, {
1038
+ event: this.event,
1039
+ });
1040
+ ch.on(this.event, this.callback);
1041
+ ch.subscribe();
1042
+ this._active = true;
1043
+ }
1044
+ unsubscribe() {
1045
+ this._active = false;
1046
+ }
1047
+ }
1048
+ class FunctionsClient {
1049
+ constructor(url, key, projectRef, getAuthHeaders) {
1050
+ this.url = url;
1051
+ this.key = key;
1052
+ this.projectRef = projectRef;
1053
+ this.getAuthHeaders = getAuthHeaders;
1054
+ }
1055
+ async invoke(functionName, options) {
1056
+ const headers = {
1057
+ 'apikey': this.key,
1058
+ ...this.getAuthHeaders(),
1059
+ };
1060
+ if (this.projectRef)
1061
+ headers['x-project-ref'] = this.projectRef;
1062
+ const fetchOpts = { method: 'POST', headers };
1063
+ if (options?.body !== undefined) {
1064
+ headers['Content-Type'] = 'application/json';
1065
+ fetchOpts.body = JSON.stringify(options.body);
1066
+ }
1067
+ try {
1068
+ const res = await fetch(`${this.url}/${functionName}`, fetchOpts);
1069
+ const text = await res.text();
1070
+ let data = null;
1071
+ try {
1072
+ data = text ? JSON.parse(text) : null;
1073
+ }
1074
+ catch {
1075
+ data = text;
1076
+ }
1077
+ if (!res.ok) {
1078
+ return {
1079
+ data: null,
1080
+ error: new FunctionsError(typeof data?.message === 'string' ? data.message : 'Function invocation failed', res.status),
1081
+ };
1082
+ }
1083
+ return { data, error: null };
1084
+ }
1085
+ catch (e) {
1086
+ return { data: null, error: new FunctionsError(e.message || 'Network error') };
1087
+ }
1088
+ }
1089
+ }