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