@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.
@@ -0,0 +1,493 @@
1
+ /**
2
+ * Wire shapes shared across the client. These mirror what the kividb services
3
+ * actually return — see apps/auth, apps/data-api, apps/realtime. Keep them in
4
+ * sync with the server protocol, not with what we wish it were.
5
+ */
6
+ /** A signed-in session as returned by the auth service token endpoints. */
7
+ interface Session {
8
+ access_token: string;
9
+ token_type: "bearer";
10
+ /** Lifetime of the access token in seconds (currently 3600). */
11
+ expires_in: number;
12
+ /** Opaque, rotates on every refresh. No expiry until revoked. */
13
+ refresh_token: string;
14
+ user: User;
15
+ }
16
+ /** The public projection of a user the auth service is willing to expose. */
17
+ interface User {
18
+ id: string;
19
+ email: string | null;
20
+ email_confirmed_at: string | null;
21
+ last_sign_in_at: string | null;
22
+ user_metadata: Record<string, unknown>;
23
+ created_at: string;
24
+ }
25
+ type OAuthProvider = "github" | "google";
26
+ type Row = Record<string, unknown>;
27
+ /**
28
+ * Every public method resolves to this instead of throwing, so callers can
29
+ * destructure `{ data, error }` the way they would with supabase-js. Network
30
+ * faults and non-2xx responses both land in `error`.
31
+ */
32
+ interface KividbResponse<T> {
33
+ data: T | null;
34
+ error: KividbError | null;
35
+ }
36
+ /** Normalized error. `status` is 0 for transport failures (never reached the service). */
37
+ interface KividbError {
38
+ message: string;
39
+ status: number;
40
+ /** Extra fields the service attached, e.g. the OAuth `reason` code. */
41
+ details?: Record<string, unknown>;
42
+ }
43
+ /** Persistence hook so a session survives reloads. Defaults to in-memory only. */
44
+ interface SessionStore {
45
+ get(): Session | null | Promise<Session | null>;
46
+ set(session: Session | null): void | Promise<void>;
47
+ }
48
+ interface ClientOptions {
49
+ /**
50
+ * Override the project slug. By default it is read from the anon key's `ref`
51
+ * claim, which is correct for normal keys — only set this for unusual setups.
52
+ */
53
+ project?: string;
54
+ /**
55
+ * Per-service base URL overrides. Normally you don't set these: the three
56
+ * are derived from the single `url` passed to createClient (gateway model,
57
+ * `{url}/auth`, `{url}/rest`, `{url}/realtime`). Override one when hitting a
58
+ * service directly — e.g. local dev with no gateway: `rest: "http://localhost:4001"`.
59
+ */
60
+ rest?: string;
61
+ auth?: string;
62
+ realtime?: string;
63
+ storage?: string;
64
+ /** Where to persist the session. In-memory if omitted. */
65
+ store?: SessionStore;
66
+ /** Automatically refresh the access token shortly before it expires. Default true. */
67
+ autoRefresh?: boolean;
68
+ }
69
+
70
+ type AuthEvent = "SIGNED_IN" | "SIGNED_OUT" | "TOKEN_REFRESHED";
71
+ type AuthListener = (event: AuthEvent, session: Session | null) => void;
72
+ /**
73
+ * Single source of truth for "who is signed in right now". The auth client
74
+ * writes to it; the REST and realtime clients read from it to pick the bearer
75
+ * token. Keeping it separate avoids a circular dependency between those three.
76
+ */
77
+ declare class SessionManager {
78
+ private readonly store?;
79
+ private current;
80
+ private readonly listeners;
81
+ constructor(store?: SessionStore | undefined);
82
+ /** Load any persisted session on startup. Safe to call more than once. */
83
+ hydrate(): Promise<Session | null>;
84
+ /**
85
+ * Synchronous hydrate for stores that read synchronously (cookies,
86
+ * localStorage). SSR needs the session in memory before the first request in
87
+ * the same tick — an awaited hydrate would be too late. No-ops for async stores.
88
+ */
89
+ hydrateSync(): void;
90
+ get(): Session | null;
91
+ /** The token data requests should send: the user's, or null to fall back to the anon key. */
92
+ accessToken(): string | null;
93
+ set(session: Session | null, event: AuthEvent): Promise<void>;
94
+ onChange(listener: AuthListener): () => void;
95
+ }
96
+
97
+ /**
98
+ * Wraps the auth service (apps/auth). Every route is scoped by project slug:
99
+ * `{auth}/{project}/...`. Methods that mint a session also store it via the
100
+ * SessionManager, so a successful sign-in immediately upgrades subsequent data
101
+ * requests from the anon key to the user's token.
102
+ */
103
+ declare class AuthClient {
104
+ private readonly base;
105
+ private readonly project;
106
+ private readonly session;
107
+ private readonly autoRefresh;
108
+ constructor(base: string, project: string, session: SessionManager, autoRefresh?: boolean);
109
+ private url;
110
+ /** Register an email/password user. Returns a session only if autoconfirm is on server-side. */
111
+ signUp(params: {
112
+ email: string;
113
+ password: string;
114
+ data?: Record<string, unknown>;
115
+ }): Promise<KividbResponse<Session | {
116
+ user: User;
117
+ confirmation_sent: true;
118
+ }>>;
119
+ signInWithPassword(params: {
120
+ email: string;
121
+ password: string;
122
+ }): Promise<KividbResponse<Session>>;
123
+ /** Passwordless — triggers a magic-link email. The link resolves via `verifyOtp`. */
124
+ signInWithOtp(params: {
125
+ email: string;
126
+ }): Promise<KividbResponse<{
127
+ message: string;
128
+ }>>;
129
+ /**
130
+ * Build the URL the browser should navigate to in order to start an OAuth
131
+ * flow. We return the URL rather than redirecting so the caller controls
132
+ * navigation (and so this works outside a browser). `redirectTo` is where the
133
+ * provider bounces back with a one-time `?code=` to feed to `exchangeCodeForSession`.
134
+ */
135
+ getOAuthUrl(params: {
136
+ provider: OAuthProvider;
137
+ redirectTo: string;
138
+ /** Pass a signed-in user's access token to LINK this provider instead of logging in. */
139
+ linkToken?: string;
140
+ }): string;
141
+ /** Trade the one-time `?code=` from an OAuth redirect for a real session. */
142
+ exchangeCodeForSession(code: string): Promise<KividbResponse<Session>>;
143
+ /** Rotate the refresh token for a fresh access token. Defaults to the stored session's. */
144
+ refreshSession(refreshToken?: string): Promise<KividbResponse<Session>>;
145
+ signOut(): Promise<KividbResponse<null>>;
146
+ /** The currently stored session (in-memory / from the store). No network call. */
147
+ getSession(): Session | null;
148
+ /**
149
+ * Fetch the live user record for the signed-in session. This is also the
150
+ * canonical server-side "is this token still valid?" check (there is no local
151
+ * JWKS verification yet — see README). On a 401 it transparently rotates the
152
+ * refresh token once and retries, which is what middleware leans on to keep a
153
+ * session alive across requests.
154
+ */
155
+ getUser(): Promise<KividbResponse<User>>;
156
+ updateUser(params: {
157
+ password?: string;
158
+ data?: Record<string, unknown>;
159
+ }): Promise<KividbResponse<User>>;
160
+ resetPasswordForEmail(email: string): Promise<KividbResponse<{
161
+ message: string;
162
+ }>>;
163
+ /** Consume an emailed one-time token (confirmation / recovery / magiclink) for a session. */
164
+ verifyOtp(params: {
165
+ type: "confirmation" | "recovery" | "magiclink";
166
+ token: string;
167
+ }): Promise<KividbResponse<Session>>;
168
+ /** Subscribe to sign-in / sign-out / refresh transitions. Returns an unsubscribe fn. */
169
+ onAuthStateChange(listener: AuthListener): () => void;
170
+ private token;
171
+ }
172
+
173
+ /**
174
+ * Cookie plumbing for SSR. The auth service returns tokens in the response
175
+ * body (it never sets cookies itself), so the SDK is responsible for putting
176
+ * the session somewhere both the server and the browser can read it. A cookie
177
+ * is that shared place: the browser client writes it, the server client reads
178
+ * it on the next request.
179
+ *
180
+ * The adapter shape mirrors @supabase/ssr so the same Next.js wiring (middleware
181
+ * request/response cookies, next/headers in RSC, route handlers) drops in.
182
+ */
183
+ interface CookieOptions {
184
+ path?: string;
185
+ domain?: string;
186
+ maxAge?: number;
187
+ expires?: Date;
188
+ secure?: boolean;
189
+ httpOnly?: boolean;
190
+ sameSite?: "lax" | "strict" | "none";
191
+ }
192
+ interface CookieMethods {
193
+ getAll(): {
194
+ name: string;
195
+ value: string;
196
+ }[];
197
+ setAll(cookies: {
198
+ name: string;
199
+ value: string;
200
+ options?: CookieOptions;
201
+ }[]): void;
202
+ }
203
+ /** Cookie name carrying the JSON session for a given project. */
204
+ declare function sessionCookieName(project: string): string;
205
+
206
+ interface Context {
207
+ rest: string;
208
+ project: string;
209
+ anonKey: string;
210
+ session: SessionManager;
211
+ }
212
+ /** Entry point returned by `client.from(table)`. Picks the HTTP verb. */
213
+ declare class FromBuilder {
214
+ private readonly ctx;
215
+ private readonly table;
216
+ constructor(ctx: Context, table: string);
217
+ select(columns?: string): FilterBuilder;
218
+ insert(values: Row | Row[]): FilterBuilder;
219
+ update(values: Row): FilterBuilder;
220
+ delete(): FilterBuilder;
221
+ }
222
+ /**
223
+ * Chainable filters that resolve to `{ data, error }` when awaited (it's a
224
+ * thenable, like supabase-js). The data-api always RETURNs the affected rows,
225
+ * so insert/update/delete come back with their rows too — no extra `.select()`
226
+ * needed. Filters apply to GET/PATCH/DELETE; they are ignored by the server on
227
+ * POST.
228
+ */
229
+ declare class FilterBuilder implements PromiseLike<KividbResponse<Row[]>> {
230
+ private readonly ctx;
231
+ private readonly table;
232
+ private readonly method;
233
+ private readonly reserved;
234
+ private readonly body?;
235
+ private readonly filters;
236
+ private readonly orders;
237
+ private limitValue?;
238
+ private offsetValue?;
239
+ constructor(ctx: Context, table: string, method: string, reserved: Record<string, string>, body?: (Row | Row[]) | undefined);
240
+ eq(column: string, value: unknown): this;
241
+ neq(column: string, value: unknown): this;
242
+ gt(column: string, value: unknown): this;
243
+ gte(column: string, value: unknown): this;
244
+ lt(column: string, value: unknown): this;
245
+ lte(column: string, value: unknown): this;
246
+ like(column: string, pattern: string): this;
247
+ ilike(column: string, pattern: string): this;
248
+ /** Only `null`, `true`, `false` are accepted by the server for `is`. */
249
+ is(column: string, value: null | boolean): this;
250
+ order(column: string, opts?: {
251
+ ascending?: boolean;
252
+ }): this;
253
+ limit(count: number): this;
254
+ offset(count: number): this;
255
+ /** Inclusive range helper, like supabase-js. `range(0, 9)` → first 10 rows. */
256
+ range(from: number, to: number): this;
257
+ private filter;
258
+ private buildUrl;
259
+ private execute;
260
+ then<TResult1 = KividbResponse<Row[]>, TResult2 = never>(onfulfilled?: ((value: KividbResponse<Row[]>) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null): PromiseLike<TResult1 | TResult2>;
261
+ }
262
+
263
+ /**
264
+ * Realtime client for apps/realtime. One WebSocket per project carries many
265
+ * topics (channels) multiplexed over it — matching the server, which keys all
266
+ * routing off the per-message `topic`. Auth rides on the connection URL as
267
+ * `?token=`, so a channel cannot outlive a token change without reconnecting.
268
+ */
269
+ interface PostgresChange {
270
+ eventType: "INSERT" | "UPDATE" | "DELETE";
271
+ schema: string;
272
+ table: string;
273
+ new?: Record<string, unknown>;
274
+ old?: Record<string, unknown>;
275
+ }
276
+ type PostgresEvent = "INSERT" | "UPDATE" | "DELETE" | "*";
277
+ type BroadcastHandler = (msg: {
278
+ event: string;
279
+ payload: unknown;
280
+ }) => void;
281
+ type PresenceHandler = (state: Record<string, unknown>) => void;
282
+ type PostgresHandler = (change: PostgresChange) => void;
283
+ interface BroadcastBinding {
284
+ kind: "broadcast";
285
+ event: string;
286
+ cb: BroadcastHandler;
287
+ }
288
+ interface PresenceBinding {
289
+ kind: "presence";
290
+ cb: PresenceHandler;
291
+ }
292
+ interface PostgresBinding {
293
+ kind: "postgres_changes";
294
+ table: string;
295
+ event: PostgresEvent;
296
+ cb: PostgresHandler;
297
+ }
298
+ type Binding = BroadcastBinding | PresenceBinding | PostgresBinding;
299
+ declare class RealtimeChannel {
300
+ readonly topic: string;
301
+ private readonly client;
302
+ /** @internal */ readonly bindings: Binding[];
303
+ private subscribed;
304
+ constructor(topic: string, client: RealtimeClient);
305
+ /** Listen for a custom broadcast event. Pass `event: "*"` for every event. */
306
+ on(type: "broadcast", filter: {
307
+ event: string;
308
+ }, cb: BroadcastHandler): this;
309
+ /** Listen for presence sync (the full tracked-state map). */
310
+ on(type: "presence", filter: {
311
+ event: "sync";
312
+ }, cb: PresenceHandler): this;
313
+ /** Listen for row changes on a table. RLS is enforced server-side per row. */
314
+ on(type: "postgres_changes", filter: {
315
+ table: string;
316
+ event?: PostgresEvent;
317
+ }, cb: PostgresHandler): this;
318
+ /** Open the connection (if needed) and register this channel's topic. */
319
+ subscribe(): this;
320
+ /** Send a broadcast to every other subscriber on this topic (no self-echo). */
321
+ send(message: {
322
+ event: string;
323
+ payload: unknown;
324
+ }): void;
325
+ /** Advertise presence state for this client on the topic. */
326
+ track(payload?: Record<string, unknown>): void;
327
+ untrack(): void;
328
+ unsubscribe(): void;
329
+ /** @internal The subscribe frame the server expects for this channel. */
330
+ _subscribeFrame(): {
331
+ type: "subscribe";
332
+ topic: string;
333
+ presence: boolean;
334
+ postgres_changes: {
335
+ table: string;
336
+ event: PostgresEvent;
337
+ }[];
338
+ };
339
+ /** @internal */ get isSubscribed(): boolean;
340
+ }
341
+ declare class RealtimeClient {
342
+ private readonly base;
343
+ private readonly project;
344
+ private readonly anonKey;
345
+ private readonly session;
346
+ private socket;
347
+ private readonly channels;
348
+ private readonly outbox;
349
+ private reconnectAttempts;
350
+ private closedByUser;
351
+ constructor(base: string, project: string, anonKey: string, session: SessionManager);
352
+ channel(topic: string): RealtimeChannel;
353
+ /** Tear everything down. Call on app unmount / sign-out. */
354
+ disconnect(): void;
355
+ /** @internal */
356
+ _attach(channel: RealtimeChannel): void;
357
+ /** @internal */
358
+ _detach(channel: RealtimeChannel): void;
359
+ /** @internal Queues until the socket is open, then flushes in order. */
360
+ _send(frame: unknown): void;
361
+ private connect;
362
+ private scheduleReconnect;
363
+ private dispatch;
364
+ }
365
+
366
+ /**
367
+ * Object storage client — `kdb.storage.from(bucket)`. Mirrors supabase-js's
368
+ * storage surface. Requests carry the signed-in user's token when there is one,
369
+ * falling back to the anon key, so bucket RLS applies exactly as it does to data
370
+ * requests. Public and render URLs are built locally (no round-trip) for use in
371
+ * `<img>`/`<a>` tags.
372
+ */
373
+ interface StorageObjectInfo {
374
+ id: string;
375
+ name: string;
376
+ size: number | null;
377
+ mimeType: string | null;
378
+ checksum: string | null;
379
+ metadata: Record<string, unknown>;
380
+ version: number;
381
+ createdAt: string;
382
+ updatedAt: string;
383
+ }
384
+ type UploadBody = Blob | ArrayBuffer | ArrayBufferView | string;
385
+ interface UploadOptions {
386
+ /** Overrides the content type (defaults to a Blob's own type, else octet-stream). */
387
+ contentType?: string;
388
+ }
389
+ interface TransformOptions {
390
+ width?: number;
391
+ height?: number;
392
+ resize?: "cover" | "contain" | "fill" | "inside" | "outside";
393
+ format?: "webp" | "jpeg" | "png" | "avif";
394
+ quality?: number;
395
+ }
396
+ interface SignedUrl {
397
+ url: string;
398
+ token: string;
399
+ exp: number;
400
+ expiresAt: string;
401
+ }
402
+ interface Ctx {
403
+ storage: string;
404
+ project: string;
405
+ anonKey: string;
406
+ session: SessionManager;
407
+ }
408
+ declare class BucketApi {
409
+ private readonly ctx;
410
+ private readonly bucket;
411
+ constructor(ctx: Ctx, bucket: string);
412
+ private token;
413
+ private objectUrl;
414
+ /** Upload (or overwrite) an object. */
415
+ upload(path: string, body: UploadBody, options?: UploadOptions): Promise<KividbResponse<StorageObjectInfo>>;
416
+ /** Download an object's bytes. */
417
+ download(path: string): Promise<KividbResponse<Blob>>;
418
+ /** Delete one or more objects. */
419
+ remove(paths: string[]): Promise<KividbResponse<{
420
+ removed: number;
421
+ }>>;
422
+ /** List objects in the bucket, optionally under a path prefix. */
423
+ list(prefix?: string): Promise<KividbResponse<StorageObjectInfo[]>>;
424
+ /** Mint a time-boxed signed URL for a private object (default 1 hour). */
425
+ createSignedUrl(path: string, expiresIn?: number): Promise<KividbResponse<SignedUrl>>;
426
+ /** The token-less public URL for an object in a public bucket. */
427
+ getPublicUrl(path: string): string;
428
+ /** A transform (render) URL — resize/reformat an image on the fly. */
429
+ getRenderUrl(path: string, transform?: TransformOptions): string;
430
+ }
431
+ declare class StorageClient {
432
+ private readonly ctx;
433
+ constructor(ctx: Ctx);
434
+ /** Operate on a bucket. */
435
+ from(bucket: string): BucketApi;
436
+ }
437
+
438
+ /**
439
+ * Read the project slug out of an anon key. The key is a kividb JWT whose `ref`
440
+ * claim is the project it's scoped to, so the SDK can recover the slug without a
441
+ * separate config value. This is an UNVERIFIED decode — we only read a public
442
+ * claim to know which project paths to build; the server still verifies the
443
+ * signature on every request.
444
+ */
445
+ declare function projectFromAnonKey(anonKey: string): string;
446
+
447
+ /**
448
+ * The kividb client. One per (url, anon key) pair.
449
+ *
450
+ * const kdb = createClient("https://database.akivi.se", anonKey);
451
+ * const { data } = await kdb.from("posts").select("*").eq("published", true);
452
+ * await kdb.auth.signInWithPassword({ email, password });
453
+ * kdb.channel("room:1").on("broadcast", { event: "msg" }, console.log).subscribe();
454
+ *
455
+ * `url` is the gateway origin fronting all services. The project slug is read
456
+ * from the anon key's `ref` claim (override with `options.project`). `anonKey`
457
+ * is the project's long-lived anon JWT — data requests use it until a user signs
458
+ * in, after which they automatically switch to that user's access token.
459
+ */
460
+ declare class KividbClient {
461
+ readonly url: string;
462
+ private readonly anonKey;
463
+ readonly auth: AuthClient;
464
+ readonly realtime: RealtimeClient;
465
+ readonly storage: StorageClient;
466
+ readonly project: string;
467
+ private readonly endpoints;
468
+ private readonly session;
469
+ constructor(url: string, anonKey: string, options?: ClientOptions);
470
+ /** Start a query against a table. */
471
+ from(table: string): FromBuilder;
472
+ /** Open (or reuse) a realtime channel for a topic. */
473
+ channel(topic: string): RealtimeChannel;
474
+ }
475
+ declare function createClient(url: string, anonKey: string, options?: ClientOptions): KividbClient;
476
+ /**
477
+ * Browser client for SSR setups: persists the session to `document.cookie` so
478
+ * the server can read it on the next request. Use this in client components.
479
+ */
480
+ declare function createBrowserClient(url: string, anonKey: string, options?: Omit<ClientOptions, "store">): KividbClient;
481
+ /**
482
+ * Server client for SSR setups: reads (and writes) the session through a cookie
483
+ * adapter you supply — Next.js `cookies()` in RSC, or the request/response
484
+ * cookies in middleware and route handlers. Mirrors @supabase/ssr's wiring.
485
+ *
486
+ * There is no local token verification yet, so `auth.getUser()` is a network
487
+ * call to the auth service — that is the canonical server-side auth check.
488
+ */
489
+ declare function createServerClient(url: string, anonKey: string, options: Omit<ClientOptions, "store"> & {
490
+ cookies: CookieMethods;
491
+ }): KividbClient;
492
+
493
+ export { AuthClient, type AuthEvent, type AuthListener, type ClientOptions, type CookieMethods, type CookieOptions, FilterBuilder, FromBuilder, KividbClient, type KividbError, type KividbResponse, type OAuthProvider, type PostgresChange, RealtimeChannel, RealtimeClient, type Row, type Session, type SessionStore, type SignedUrl, StorageClient, type StorageObjectInfo, type TransformOptions, type UploadBody, type UploadOptions, type User, createBrowserClient, createClient, createServerClient, projectFromAnonKey, sessionCookieName };