@kividb/client 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/dist/index.cjs ADDED
@@ -0,0 +1,830 @@
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
+ AuthClient: () => AuthClient,
24
+ FilterBuilder: () => FilterBuilder,
25
+ FromBuilder: () => FromBuilder,
26
+ KividbClient: () => KividbClient,
27
+ RealtimeChannel: () => RealtimeChannel,
28
+ RealtimeClient: () => RealtimeClient,
29
+ StorageClient: () => StorageClient,
30
+ createBrowserClient: () => createBrowserClient,
31
+ createClient: () => createClient,
32
+ createServerClient: () => createServerClient,
33
+ projectFromAnonKey: () => projectFromAnonKey,
34
+ sessionCookieName: () => sessionCookieName
35
+ });
36
+ module.exports = __toCommonJS(index_exports);
37
+
38
+ // src/http.ts
39
+ async function request(url, init = {}) {
40
+ const { token, headers, ...rest } = init;
41
+ const merged = new Headers(headers);
42
+ if (token) merged.set("Authorization", `Bearer ${token}`);
43
+ if (rest.body && !merged.has("Content-Type")) {
44
+ merged.set("Content-Type", "application/json");
45
+ }
46
+ let res;
47
+ try {
48
+ res = await fetch(url, { ...rest, headers: merged });
49
+ } catch (cause) {
50
+ const message = cause instanceof Error ? cause.message : "Network request failed.";
51
+ return { data: null, error: { message, status: 0 } };
52
+ }
53
+ if (res.status === 204) return { data: null, error: null };
54
+ const text = await res.text();
55
+ const body = text ? safeJson(text) : null;
56
+ if (!res.ok) {
57
+ const { error: message, ...details } = body ?? {};
58
+ return {
59
+ data: null,
60
+ error: {
61
+ message: message ?? res.statusText ?? "Request failed.",
62
+ status: res.status,
63
+ details: Object.keys(details).length ? details : void 0
64
+ }
65
+ };
66
+ }
67
+ return { data: body, error: null };
68
+ }
69
+ function safeJson(text) {
70
+ try {
71
+ return JSON.parse(text);
72
+ } catch {
73
+ return text;
74
+ }
75
+ }
76
+
77
+ // src/auth.ts
78
+ var AuthClient = class {
79
+ constructor(base, project, session, autoRefresh = true) {
80
+ this.base = base;
81
+ this.project = project;
82
+ this.session = session;
83
+ this.autoRefresh = autoRefresh;
84
+ }
85
+ base;
86
+ project;
87
+ session;
88
+ autoRefresh;
89
+ url(path) {
90
+ return `${this.base}/${this.project}${path}`;
91
+ }
92
+ /** Register an email/password user. Returns a session only if autoconfirm is on server-side. */
93
+ async signUp(params) {
94
+ const res = await request(
95
+ this.url("/signup"),
96
+ { method: "POST", body: JSON.stringify(params) }
97
+ );
98
+ if (res.data && "access_token" in res.data) {
99
+ await this.session.set(res.data, "SIGNED_IN");
100
+ }
101
+ return res;
102
+ }
103
+ async signInWithPassword(params) {
104
+ return this.token("password", params);
105
+ }
106
+ /** Passwordless — triggers a magic-link email. The link resolves via `verifyOtp`. */
107
+ signInWithOtp(params) {
108
+ return request(this.url("/magiclink"), {
109
+ method: "POST",
110
+ body: JSON.stringify(params)
111
+ });
112
+ }
113
+ /**
114
+ * Build the URL the browser should navigate to in order to start an OAuth
115
+ * flow. We return the URL rather than redirecting so the caller controls
116
+ * navigation (and so this works outside a browser). `redirectTo` is where the
117
+ * provider bounces back with a one-time `?code=` to feed to `exchangeCodeForSession`.
118
+ */
119
+ getOAuthUrl(params) {
120
+ const q = new URLSearchParams({
121
+ provider: params.provider,
122
+ redirect_to: params.redirectTo
123
+ });
124
+ if (params.linkToken) q.set("link_token", params.linkToken);
125
+ return `${this.url("/authorize")}?${q.toString()}`;
126
+ }
127
+ /** Trade the one-time `?code=` from an OAuth redirect for a real session. */
128
+ exchangeCodeForSession(code) {
129
+ return this.token("oauth_code", { code });
130
+ }
131
+ /** Rotate the refresh token for a fresh access token. Defaults to the stored session's. */
132
+ async refreshSession(refreshToken) {
133
+ const token = refreshToken ?? this.session.get()?.refresh_token;
134
+ if (!token) {
135
+ return { data: null, error: { message: "No refresh token available.", status: 400 } };
136
+ }
137
+ const res = await this.token("refresh_token", { refresh_token: token });
138
+ if (res.data) await this.session.set(res.data, "TOKEN_REFRESHED");
139
+ return res;
140
+ }
141
+ async signOut() {
142
+ const refresh = this.session.get()?.refresh_token;
143
+ const res = await request(this.url("/logout"), {
144
+ method: "POST",
145
+ body: JSON.stringify(refresh ? { refresh_token: refresh } : {})
146
+ });
147
+ await this.session.set(null, "SIGNED_OUT");
148
+ return res;
149
+ }
150
+ /** The currently stored session (in-memory / from the store). No network call. */
151
+ getSession() {
152
+ return this.session.get();
153
+ }
154
+ /**
155
+ * Fetch the live user record for the signed-in session. This is also the
156
+ * canonical server-side "is this token still valid?" check (there is no local
157
+ * JWKS verification yet — see README). On a 401 it transparently rotates the
158
+ * refresh token once and retries, which is what middleware leans on to keep a
159
+ * session alive across requests.
160
+ */
161
+ async getUser() {
162
+ let res = await request(this.url("/user"), { token: this.session.accessToken() });
163
+ if (this.autoRefresh && res.error?.status === 401 && this.session.get()?.refresh_token) {
164
+ const refreshed = await this.refreshSession();
165
+ if (refreshed.data) {
166
+ res = await request(this.url("/user"), { token: this.session.accessToken() });
167
+ }
168
+ }
169
+ return res;
170
+ }
171
+ updateUser(params) {
172
+ return request(this.url("/user"), {
173
+ method: "PUT",
174
+ token: this.session.accessToken(),
175
+ body: JSON.stringify(params)
176
+ });
177
+ }
178
+ resetPasswordForEmail(email) {
179
+ return request(this.url("/recover"), {
180
+ method: "POST",
181
+ body: JSON.stringify({ email })
182
+ });
183
+ }
184
+ /** Consume an emailed one-time token (confirmation / recovery / magiclink) for a session. */
185
+ async verifyOtp(params) {
186
+ const res = await request(this.url("/verify"), {
187
+ method: "POST",
188
+ body: JSON.stringify(params)
189
+ });
190
+ if (res.data) await this.session.set(res.data, "SIGNED_IN");
191
+ return res;
192
+ }
193
+ /** Subscribe to sign-in / sign-out / refresh transitions. Returns an unsubscribe fn. */
194
+ onAuthStateChange(listener) {
195
+ return this.session.onChange(listener);
196
+ }
197
+ async token(grantType, body) {
198
+ const res = await request(
199
+ `${this.url("/token")}?grant_type=${grantType}`,
200
+ { method: "POST", body: JSON.stringify(body) }
201
+ );
202
+ if (res.data && grantType !== "refresh_token") {
203
+ await this.session.set(res.data, "SIGNED_IN");
204
+ }
205
+ return res;
206
+ }
207
+ };
208
+
209
+ // src/cookies.ts
210
+ function sessionCookieName(project) {
211
+ return `kdb-${project}-auth`;
212
+ }
213
+ function cookieSessionStore(project, cookies) {
214
+ const name = sessionCookieName(project);
215
+ return {
216
+ get() {
217
+ const raw = cookies.getAll().find((c) => c.name === name)?.value;
218
+ if (!raw) return null;
219
+ try {
220
+ return JSON.parse(decodeURIComponent(raw));
221
+ } catch {
222
+ return null;
223
+ }
224
+ },
225
+ set(session) {
226
+ if (!session) {
227
+ cookies.setAll([{ name, value: "", options: { path: "/", maxAge: 0 } }]);
228
+ return;
229
+ }
230
+ cookies.setAll([
231
+ {
232
+ name,
233
+ value: encodeURIComponent(JSON.stringify(session)),
234
+ options: {
235
+ path: "/",
236
+ sameSite: "lax",
237
+ // ~1 year; the access token inside still expires in 1h and is
238
+ // refreshed via the rotating refresh token.
239
+ maxAge: 60 * 60 * 24 * 365
240
+ }
241
+ }
242
+ ]);
243
+ }
244
+ };
245
+ }
246
+ function documentCookieMethods() {
247
+ return {
248
+ getAll() {
249
+ if (typeof document === "undefined" || !document.cookie) return [];
250
+ return document.cookie.split("; ").map((pair) => {
251
+ const eq = pair.indexOf("=");
252
+ return { name: pair.slice(0, eq), value: pair.slice(eq + 1) };
253
+ });
254
+ },
255
+ setAll(toSet) {
256
+ if (typeof document === "undefined") return;
257
+ for (const { name, value, options } of toSet) {
258
+ document.cookie = serializeCookie(name, value, options);
259
+ }
260
+ }
261
+ };
262
+ }
263
+ function serializeCookie(name, value, options = {}) {
264
+ const parts = [`${name}=${value}`];
265
+ parts.push(`Path=${options.path ?? "/"}`);
266
+ if (options.maxAge !== void 0) parts.push(`Max-Age=${options.maxAge}`);
267
+ if (options.domain) parts.push(`Domain=${options.domain}`);
268
+ if (options.expires) parts.push(`Expires=${options.expires.toUTCString()}`);
269
+ if (options.sameSite) parts.push(`SameSite=${capitalize(options.sameSite)}`);
270
+ if (options.secure) parts.push("Secure");
271
+ return parts.join("; ");
272
+ }
273
+ function capitalize(s) {
274
+ return s.charAt(0).toUpperCase() + s.slice(1);
275
+ }
276
+
277
+ // src/jwt.ts
278
+ function projectFromAnonKey(anonKey) {
279
+ const parts = anonKey.split(".");
280
+ if (parts.length < 2) {
281
+ throw new Error("Invalid kividb key: expected a JWT (header.payload.signature).");
282
+ }
283
+ let payload;
284
+ try {
285
+ payload = JSON.parse(base64UrlDecode(parts[1]));
286
+ } catch {
287
+ throw new Error("Invalid kividb key: could not decode the JWT payload.");
288
+ }
289
+ if (typeof payload.ref !== "string" || !payload.ref) {
290
+ throw new Error("kividb key has no `ref` claim \u2014 cannot determine the project slug.");
291
+ }
292
+ return payload.ref;
293
+ }
294
+ function base64UrlDecode(segment) {
295
+ const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
296
+ const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
297
+ if (typeof atob !== "function") {
298
+ throw new Error("No base64 decoder (atob) available in this runtime.");
299
+ }
300
+ const binary = atob(padded);
301
+ const bytes = Uint8Array.from(binary, (ch) => ch.charCodeAt(0));
302
+ return new TextDecoder().decode(bytes);
303
+ }
304
+
305
+ // src/postgrest.ts
306
+ var FromBuilder = class {
307
+ constructor(ctx, table) {
308
+ this.ctx = ctx;
309
+ this.table = table;
310
+ }
311
+ ctx;
312
+ table;
313
+ select(columns = "*") {
314
+ return new FilterBuilder(this.ctx, this.table, "GET", { select: columns });
315
+ }
316
+ insert(values) {
317
+ return new FilterBuilder(this.ctx, this.table, "POST", {}, values);
318
+ }
319
+ update(values) {
320
+ return new FilterBuilder(this.ctx, this.table, "PATCH", {}, values);
321
+ }
322
+ delete() {
323
+ return new FilterBuilder(this.ctx, this.table, "DELETE", {});
324
+ }
325
+ };
326
+ var FilterBuilder = class {
327
+ constructor(ctx, table, method, reserved, body) {
328
+ this.ctx = ctx;
329
+ this.table = table;
330
+ this.method = method;
331
+ this.reserved = reserved;
332
+ this.body = body;
333
+ }
334
+ ctx;
335
+ table;
336
+ method;
337
+ reserved;
338
+ body;
339
+ filters = [];
340
+ orders = [];
341
+ limitValue;
342
+ offsetValue;
343
+ eq(column, value) {
344
+ return this.filter(column, "eq", value);
345
+ }
346
+ neq(column, value) {
347
+ return this.filter(column, "neq", value);
348
+ }
349
+ gt(column, value) {
350
+ return this.filter(column, "gt", value);
351
+ }
352
+ gte(column, value) {
353
+ return this.filter(column, "gte", value);
354
+ }
355
+ lt(column, value) {
356
+ return this.filter(column, "lt", value);
357
+ }
358
+ lte(column, value) {
359
+ return this.filter(column, "lte", value);
360
+ }
361
+ like(column, pattern) {
362
+ return this.filter(column, "like", pattern);
363
+ }
364
+ ilike(column, pattern) {
365
+ return this.filter(column, "ilike", pattern);
366
+ }
367
+ /** Only `null`, `true`, `false` are accepted by the server for `is`. */
368
+ is(column, value) {
369
+ return this.filter(column, "is", value === null ? "null" : String(value));
370
+ }
371
+ order(column, opts = {}) {
372
+ this.orders.push(`${column}.${opts.ascending === false ? "desc" : "asc"}`);
373
+ return this;
374
+ }
375
+ limit(count) {
376
+ this.limitValue = count;
377
+ return this;
378
+ }
379
+ offset(count) {
380
+ this.offsetValue = count;
381
+ return this;
382
+ }
383
+ /** Inclusive range helper, like supabase-js. `range(0, 9)` → first 10 rows. */
384
+ range(from, to) {
385
+ this.offsetValue = from;
386
+ this.limitValue = to - from + 1;
387
+ return this;
388
+ }
389
+ filter(column, op, value) {
390
+ this.filters.push([column, `${op}.${String(value)}`]);
391
+ return this;
392
+ }
393
+ buildUrl() {
394
+ const params = new URLSearchParams();
395
+ for (const [key, value] of Object.entries(this.reserved)) params.set(key, value);
396
+ for (const [column, expr] of this.filters) params.append(column, expr);
397
+ if (this.orders.length) params.set("order", this.orders.join(","));
398
+ if (this.limitValue !== void 0) params.set("limit", String(this.limitValue));
399
+ if (this.offsetValue !== void 0) params.set("offset", String(this.offsetValue));
400
+ const query = params.toString();
401
+ const path = `${this.ctx.rest}/${this.ctx.project}/${this.table}`;
402
+ return query ? `${path}?${query}` : path;
403
+ }
404
+ execute() {
405
+ const token = this.ctx.session.accessToken() ?? this.ctx.anonKey;
406
+ return request(this.buildUrl(), {
407
+ method: this.method,
408
+ token,
409
+ body: this.body !== void 0 ? JSON.stringify(this.body) : void 0
410
+ });
411
+ }
412
+ then(onfulfilled, onrejected) {
413
+ return this.execute().then(onfulfilled, onrejected);
414
+ }
415
+ };
416
+
417
+ // src/realtime.ts
418
+ var RealtimeChannel = class {
419
+ constructor(topic, client) {
420
+ this.topic = topic;
421
+ this.client = client;
422
+ }
423
+ topic;
424
+ client;
425
+ /** @internal */
426
+ bindings = [];
427
+ subscribed = false;
428
+ on(type, filter, cb) {
429
+ if (type === "broadcast") {
430
+ this.bindings.push({ kind: "broadcast", event: String(filter.event), cb });
431
+ } else if (type === "presence") {
432
+ this.bindings.push({ kind: "presence", cb });
433
+ } else if (type === "postgres_changes") {
434
+ this.bindings.push({
435
+ kind: "postgres_changes",
436
+ table: String(filter.table),
437
+ event: filter.event ?? "*",
438
+ cb
439
+ });
440
+ }
441
+ return this;
442
+ }
443
+ /** Open the connection (if needed) and register this channel's topic. */
444
+ subscribe() {
445
+ this.subscribed = true;
446
+ this.client._attach(this);
447
+ return this;
448
+ }
449
+ /** Send a broadcast to every other subscriber on this topic (no self-echo). */
450
+ send(message) {
451
+ this.client._send({ type: "broadcast", topic: this.topic, event: message.event, payload: message.payload });
452
+ }
453
+ /** Advertise presence state for this client on the topic. */
454
+ track(payload = {}) {
455
+ this.client._send({ type: "presence", event: "track", topic: this.topic, payload });
456
+ }
457
+ untrack() {
458
+ this.client._send({ type: "presence", event: "untrack", topic: this.topic });
459
+ }
460
+ unsubscribe() {
461
+ this.subscribed = false;
462
+ this.client._send({ type: "unsubscribe", topic: this.topic });
463
+ this.client._detach(this);
464
+ }
465
+ /** @internal The subscribe frame the server expects for this channel. */
466
+ _subscribeFrame() {
467
+ const changes = this.bindings.filter((b) => b.kind === "postgres_changes").map((b) => ({ table: b.table, event: b.event }));
468
+ return {
469
+ type: "subscribe",
470
+ topic: this.topic,
471
+ presence: this.bindings.some((b) => b.kind === "presence"),
472
+ postgres_changes: changes
473
+ };
474
+ }
475
+ /** @internal */
476
+ get isSubscribed() {
477
+ return this.subscribed;
478
+ }
479
+ };
480
+ var RealtimeClient = class {
481
+ constructor(base, project, anonKey, session) {
482
+ this.base = base;
483
+ this.project = project;
484
+ this.anonKey = anonKey;
485
+ this.session = session;
486
+ }
487
+ base;
488
+ project;
489
+ anonKey;
490
+ session;
491
+ socket = null;
492
+ channels = /* @__PURE__ */ new Map();
493
+ outbox = [];
494
+ reconnectAttempts = 0;
495
+ closedByUser = false;
496
+ channel(topic) {
497
+ const existing = this.channels.get(topic);
498
+ if (existing) return existing;
499
+ const channel = new RealtimeChannel(topic, this);
500
+ this.channels.set(topic, channel);
501
+ return channel;
502
+ }
503
+ /** Tear everything down. Call on app unmount / sign-out. */
504
+ disconnect() {
505
+ this.closedByUser = true;
506
+ this.socket?.close();
507
+ this.socket = null;
508
+ this.channels.clear();
509
+ }
510
+ /** @internal */
511
+ _attach(channel) {
512
+ this.channels.set(channel.topic, channel);
513
+ if (this.socket?.readyState === WebSocket.OPEN) {
514
+ this._send(channel._subscribeFrame());
515
+ } else {
516
+ this.connect();
517
+ }
518
+ }
519
+ /** @internal */
520
+ _detach(channel) {
521
+ this.channels.delete(channel.topic);
522
+ }
523
+ /** @internal Queues until the socket is open, then flushes in order. */
524
+ _send(frame) {
525
+ if (this.socket?.readyState === WebSocket.OPEN) {
526
+ this.socket.send(JSON.stringify(frame));
527
+ } else {
528
+ this.outbox.push(frame);
529
+ this.connect();
530
+ }
531
+ }
532
+ connect() {
533
+ if (this.socket && this.socket.readyState <= WebSocket.OPEN) return;
534
+ this.closedByUser = false;
535
+ const token = this.session.accessToken() ?? this.anonKey;
536
+ const url = `${this.base}/${this.project}?token=${encodeURIComponent(token)}`;
537
+ const socket = new WebSocket(url);
538
+ this.socket = socket;
539
+ socket.onopen = () => {
540
+ this.reconnectAttempts = 0;
541
+ for (const channel of this.channels.values()) {
542
+ if (channel.isSubscribed) socket.send(JSON.stringify(channel._subscribeFrame()));
543
+ }
544
+ while (this.outbox.length) socket.send(JSON.stringify(this.outbox.shift()));
545
+ };
546
+ socket.onmessage = (ev) => this.dispatch(ev.data);
547
+ socket.onclose = () => {
548
+ this.socket = null;
549
+ if (!this.closedByUser && this.channels.size) this.scheduleReconnect();
550
+ };
551
+ socket.onerror = () => socket.close();
552
+ }
553
+ scheduleReconnect() {
554
+ const delay = Math.min(1e3 * 2 ** this.reconnectAttempts, 1e4);
555
+ this.reconnectAttempts += 1;
556
+ setTimeout(() => {
557
+ if (!this.closedByUser) this.connect();
558
+ }, delay);
559
+ }
560
+ dispatch(raw) {
561
+ let msg;
562
+ try {
563
+ msg = JSON.parse(String(raw));
564
+ } catch {
565
+ return;
566
+ }
567
+ const topic = msg.topic;
568
+ const channel = topic ? this.channels.get(topic) : void 0;
569
+ if (!channel) return;
570
+ for (const binding of channel.bindings) {
571
+ if (msg.type === "broadcast" && binding.kind === "broadcast") {
572
+ if (binding.event === "*" || binding.event === msg.event) {
573
+ binding.cb({ event: msg.event, payload: msg.payload });
574
+ }
575
+ } else if (msg.type === "presence" && binding.kind === "presence") {
576
+ binding.cb(msg.state ?? {});
577
+ } else if (msg.type === "postgres_changes" && binding.kind === "postgres_changes") {
578
+ const change = msg.payload;
579
+ const tableMatch = binding.table === change.table;
580
+ const eventMatch = binding.event === "*" || binding.event === change.eventType;
581
+ if (tableMatch && eventMatch) binding.cb(change);
582
+ }
583
+ }
584
+ }
585
+ };
586
+
587
+ // src/session.ts
588
+ var SessionManager = class {
589
+ constructor(store) {
590
+ this.store = store;
591
+ }
592
+ store;
593
+ current = null;
594
+ listeners = /* @__PURE__ */ new Set();
595
+ /** Load any persisted session on startup. Safe to call more than once. */
596
+ async hydrate() {
597
+ if (this.current) return this.current;
598
+ if (this.store) {
599
+ this.current = await this.store.get() ?? null;
600
+ }
601
+ return this.current;
602
+ }
603
+ /**
604
+ * Synchronous hydrate for stores that read synchronously (cookies,
605
+ * localStorage). SSR needs the session in memory before the first request in
606
+ * the same tick — an awaited hydrate would be too late. No-ops for async stores.
607
+ */
608
+ hydrateSync() {
609
+ if (this.current || !this.store) return;
610
+ const maybe = this.store.get();
611
+ if (maybe && typeof maybe.then === "function") return;
612
+ this.current = maybe ?? null;
613
+ }
614
+ get() {
615
+ return this.current;
616
+ }
617
+ /** The token data requests should send: the user's, or null to fall back to the anon key. */
618
+ accessToken() {
619
+ return this.current?.access_token ?? null;
620
+ }
621
+ async set(session, event) {
622
+ this.current = session;
623
+ await this.store?.set(session);
624
+ for (const listener of this.listeners) listener(event, session);
625
+ }
626
+ onChange(listener) {
627
+ this.listeners.add(listener);
628
+ return () => this.listeners.delete(listener);
629
+ }
630
+ };
631
+
632
+ // src/storage.ts
633
+ function encodePath(path) {
634
+ return path.replace(/^\/+/, "").split("/").map(encodeURIComponent).join("/");
635
+ }
636
+ var BucketApi = class {
637
+ constructor(ctx, bucket) {
638
+ this.ctx = ctx;
639
+ this.bucket = bucket;
640
+ }
641
+ ctx;
642
+ bucket;
643
+ token() {
644
+ return this.ctx.session.accessToken() ?? this.ctx.anonKey;
645
+ }
646
+ objectUrl(path) {
647
+ return `${this.ctx.storage}/${this.ctx.project}/object/${encodeURIComponent(this.bucket)}/${encodePath(path)}`;
648
+ }
649
+ /** Upload (or overwrite) an object. */
650
+ upload(path, body, options = {}) {
651
+ const contentType = options.contentType ?? (typeof Blob !== "undefined" && body instanceof Blob && body.type ? body.type : "application/octet-stream");
652
+ return request(this.objectUrl(path), {
653
+ method: "PUT",
654
+ token: this.token(),
655
+ headers: { "Content-Type": contentType },
656
+ body
657
+ });
658
+ }
659
+ /** Download an object's bytes. */
660
+ async download(path) {
661
+ try {
662
+ const res = await fetch(this.objectUrl(path), {
663
+ headers: { Authorization: `Bearer ${this.token()}` }
664
+ });
665
+ if (!res.ok) {
666
+ const body = await res.json().catch(() => null);
667
+ return {
668
+ data: null,
669
+ error: { message: body?.error ?? res.statusText, status: res.status }
670
+ };
671
+ }
672
+ return { data: await res.blob(), error: null };
673
+ } catch (cause) {
674
+ const message = cause instanceof Error ? cause.message : "Network request failed.";
675
+ return { data: null, error: { message, status: 0 } };
676
+ }
677
+ }
678
+ /** Delete one or more objects. */
679
+ async remove(paths) {
680
+ let removed = 0;
681
+ for (const path of paths) {
682
+ const res = await request(this.objectUrl(path), {
683
+ method: "DELETE",
684
+ token: this.token()
685
+ });
686
+ if (res.error) return { data: null, error: res.error };
687
+ removed++;
688
+ }
689
+ return { data: { removed }, error: null };
690
+ }
691
+ /** List objects in the bucket, optionally under a path prefix. */
692
+ list(prefix) {
693
+ const qs = prefix ? `?prefix=${encodeURIComponent(prefix)}` : "";
694
+ return request(
695
+ `${this.ctx.storage}/${this.ctx.project}/list/${encodeURIComponent(this.bucket)}${qs}`,
696
+ { token: this.token() }
697
+ );
698
+ }
699
+ /** Mint a time-boxed signed URL for a private object (default 1 hour). */
700
+ createSignedUrl(path, expiresIn = 3600) {
701
+ return request(
702
+ `${this.ctx.storage}/${this.ctx.project}/sign/${encodeURIComponent(this.bucket)}/${encodePath(path)}`,
703
+ {
704
+ method: "POST",
705
+ token: this.token(),
706
+ body: JSON.stringify({ expiresIn })
707
+ }
708
+ );
709
+ }
710
+ /** The token-less public URL for an object in a public bucket. */
711
+ getPublicUrl(path) {
712
+ return `${this.ctx.storage}/${this.ctx.project}/public/${encodeURIComponent(this.bucket)}/${encodePath(path)}`;
713
+ }
714
+ /** A transform (render) URL — resize/reformat an image on the fly. */
715
+ getRenderUrl(path, transform = {}) {
716
+ const params = new URLSearchParams();
717
+ if (transform.width) params.set("width", String(transform.width));
718
+ if (transform.height) params.set("height", String(transform.height));
719
+ if (transform.resize) params.set("resize", transform.resize);
720
+ if (transform.format) params.set("format", transform.format);
721
+ if (transform.quality) params.set("quality", String(transform.quality));
722
+ const qs = params.toString();
723
+ return `${this.ctx.storage}/${this.ctx.project}/render/${encodeURIComponent(this.bucket)}/${encodePath(path)}${qs ? `?${qs}` : ""}`;
724
+ }
725
+ };
726
+ var StorageClient = class {
727
+ constructor(ctx) {
728
+ this.ctx = ctx;
729
+ }
730
+ ctx;
731
+ /** Operate on a bucket. */
732
+ from(bucket) {
733
+ return new BucketApi(this.ctx, bucket);
734
+ }
735
+ };
736
+
737
+ // src/index.ts
738
+ function resolveEndpoints(url, options) {
739
+ const base = url.replace(/\/+$/, "");
740
+ const wsBase = base.replace(/^http/i, "ws");
741
+ return {
742
+ rest: options.rest ?? `${base}/rest`,
743
+ auth: options.auth ?? `${base}/auth`,
744
+ realtime: options.realtime ?? `${wsBase}/realtime`,
745
+ storage: options.storage ?? `${base}/storage`
746
+ };
747
+ }
748
+ var KividbClient = class {
749
+ constructor(url, anonKey, options = {}) {
750
+ this.url = url;
751
+ this.anonKey = anonKey;
752
+ this.project = options.project ?? projectFromAnonKey(anonKey);
753
+ this.endpoints = resolveEndpoints(url, options);
754
+ this.session = new SessionManager(options.store);
755
+ this.auth = new AuthClient(
756
+ this.endpoints.auth,
757
+ this.project,
758
+ this.session,
759
+ options.autoRefresh ?? true
760
+ );
761
+ this.realtime = new RealtimeClient(
762
+ this.endpoints.realtime,
763
+ this.project,
764
+ anonKey,
765
+ this.session
766
+ );
767
+ this.storage = new StorageClient({
768
+ storage: this.endpoints.storage,
769
+ project: this.project,
770
+ anonKey,
771
+ session: this.session
772
+ });
773
+ this.session.hydrateSync();
774
+ void this.session.hydrate();
775
+ }
776
+ url;
777
+ anonKey;
778
+ auth;
779
+ realtime;
780
+ storage;
781
+ project;
782
+ endpoints;
783
+ session;
784
+ /** Start a query against a table. */
785
+ from(table) {
786
+ return new FromBuilder(
787
+ { rest: this.endpoints.rest, project: this.project, anonKey: this.anonKey, session: this.session },
788
+ table
789
+ );
790
+ }
791
+ /** Open (or reuse) a realtime channel for a topic. */
792
+ channel(topic) {
793
+ return this.realtime.channel(topic);
794
+ }
795
+ };
796
+ function createClient(url, anonKey, options) {
797
+ return new KividbClient(url, anonKey, options);
798
+ }
799
+ function createBrowserClient(url, anonKey, options = {}) {
800
+ const project = options.project ?? projectFromAnonKey(anonKey);
801
+ return new KividbClient(url, anonKey, {
802
+ ...options,
803
+ project,
804
+ store: cookieSessionStore(project, documentCookieMethods())
805
+ });
806
+ }
807
+ function createServerClient(url, anonKey, options) {
808
+ const { cookies, ...rest } = options;
809
+ const project = rest.project ?? projectFromAnonKey(anonKey);
810
+ return new KividbClient(url, anonKey, {
811
+ ...rest,
812
+ project,
813
+ store: cookieSessionStore(project, cookies)
814
+ });
815
+ }
816
+ // Annotate the CommonJS export names for ESM import in node:
817
+ 0 && (module.exports = {
818
+ AuthClient,
819
+ FilterBuilder,
820
+ FromBuilder,
821
+ KividbClient,
822
+ RealtimeChannel,
823
+ RealtimeClient,
824
+ StorageClient,
825
+ createBrowserClient,
826
+ createClient,
827
+ createServerClient,
828
+ projectFromAnonKey,
829
+ sessionCookieName
830
+ });