backlex 0.1.0 → 0.1.2

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.d.ts CHANGED
@@ -1,83 +1,7 @@
1
- import { L as ListQuery, k as ListResponse, C as Condition, f as DurationParts, R as RelativeNow, A as AggregateQuery, a as AggregateRow, S as SearchQuery, m as SearchResponse, I as ImportSummary, h as ItemQuery, i as ItemResponse, d as BatchResponse, c as BatchOperation, g as ItemEvent, l as ResumableUploadResult, D as DeviceToken, P as PhoneNumber, j as JobStatus, J as Job, F as FlagState } from './types-CbcbXGiA.js';
2
- export { B as BacklexError, U as Upload, n as UploadStatus } from './types-CbcbXGiA.js';
1
+ import { L as ListQuery, m as ListResponse, C as Condition, h as DurationParts, R as RelativeNow, A as AggregateQuery, a as AggregateRow, S as SearchQuery, o as SearchResponse, I as ImportSummary, j as ItemQuery, k as ItemResponse, d as BatchResponse, f as BulkUpdateResponse, c as BatchOperation, i as ItemEvent, n as ResumableUploadResult, D as DeviceToken, P as PhoneNumber, l as JobStatus, J as Job, F as FlagState } from './types-DiJCXOy-.js';
2
+ export { B as BacklexError, U as Upload, p as UploadStatus } from './types-DiJCXOy-.js';
3
3
  export { VerifyWebhookOptions, verifyWebhook } from './webhook.js';
4
4
 
5
- /**
6
- * Offline-first sync for a single collection.
7
- *
8
- * Pulls the server changefeed (`GET /api/items/:slug/changes`) into a local
9
- * store, keeps it live over SSE, and lets the app read/write while offline:
10
- * writes apply optimistically to the local store and queue, then flush through
11
- * the batch endpoint on reconnect. Conflicts resolve last-write-wins by
12
- * `updated_at`, with locally-queued (not-yet-flushed) writes held until they
13
- * land.
14
- *
15
- * The store is pluggable — `memoryStore()` works anywhere; `indexedDbStore()`
16
- * persists across reloads in the browser.
17
- */
18
- type QueuedOp = {
19
- kind: "create";
20
- tempId: string;
21
- data: Record<string, unknown>;
22
- } | {
23
- kind: "update";
24
- id: string;
25
- data: Record<string, unknown>;
26
- } | {
27
- kind: "delete";
28
- id: string;
29
- };
30
- interface SyncStore {
31
- get(id: string): Promise<Record<string, unknown> | undefined>;
32
- set(id: string, row: Record<string, unknown>): Promise<void>;
33
- remove(id: string): Promise<void>;
34
- all(): Promise<Record<string, unknown>[]>;
35
- getMeta(key: string): Promise<string | null>;
36
- setMeta(key: string, value: string): Promise<void>;
37
- queueGet(): Promise<QueuedOp[]>;
38
- queueSet(ops: QueuedOp[]): Promise<void>;
39
- }
40
- /** In-memory store — non-persistent; the default and the testing baseline. */
41
- declare const memoryStore: () => SyncStore;
42
- /** IndexedDB-backed store (browser). One object store for rows + one for meta;
43
- * the write queue lives under a meta key. Falls back to throwing if IndexedDB
44
- * is unavailable — use `memoryStore()` outside the browser. */
45
- declare const indexedDbStore: (opts: {
46
- collection: string;
47
- dbName?: string;
48
- }) => SyncStore;
49
- /** The subset of the backlex client `createSync` needs. The full client satisfies it. */
50
- interface SyncClientLike {
51
- request: <T>(method: string, path: string, body?: unknown) => Promise<T>;
52
- subscribe: (channel: string, onEvent: (e: {
53
- event: "created" | "updated" | "deleted";
54
- data: Record<string, unknown>;
55
- }) => void, onError?: (err: unknown) => void) => () => void;
56
- }
57
- interface SyncOptions {
58
- collection: string;
59
- store?: SyncStore;
60
- /** Primary-key field. Default `id`. */
61
- pk?: string;
62
- /** Rows per changefeed page. Default 200. */
63
- pageSize?: number;
64
- /** Called after the local store changes (pull / live / local write). */
65
- onChange?: () => void;
66
- }
67
- declare const createSync: (client: SyncClientLike, options: SyncOptions) => {
68
- pull: () => Promise<number>;
69
- flush: () => Promise<void>;
70
- live: () => (() => void);
71
- start: () => Promise<void>;
72
- stop: () => void;
73
- getAll: () => Promise<Record<string, unknown>[]>;
74
- get: (id: string) => Promise<Record<string, unknown> | undefined>;
75
- create: (data: Record<string, unknown>) => Promise<string>;
76
- update: (id: string, data: Record<string, unknown>) => Promise<void>;
77
- remove: (id: string) => Promise<void>;
78
- store: SyncStore;
79
- };
80
-
81
5
  /**
82
6
  * Type-safe fluent query builder — a Drizzle/Supabase-style ergonomics layer
83
7
  * that COMPILES to the canonical JSON `Condition` / `ListQuery` the REST API
@@ -104,25 +28,46 @@ declare const createSync: (client: SyncClientLike, options: SyncOptions) => {
104
28
  type FieldKey<T> = (keyof T & string) | (string & {});
105
29
  /** `field` ascending, or `-field` descending. */
106
30
  type SortKey<T> = FieldKey<T> | `-${string}`;
31
+ /** Fluent condition factory passed to `QueryBuilder.where(...)`. Each method
32
+ * returns a canonical {@link Condition}; multiple are combined with `and`/`or`. */
107
33
  interface FilterBuilder<T> {
34
+ /** `field = value`. */
108
35
  eq(field: FieldKey<T>, value: unknown): Condition;
36
+ /** `field != value`. */
109
37
  neq(field: FieldKey<T>, value: unknown): Condition;
38
+ /** `field > value`. */
110
39
  gt(field: FieldKey<T>, value: unknown): Condition;
40
+ /** `field >= value`. */
111
41
  gte(field: FieldKey<T>, value: unknown): Condition;
42
+ /** `field < value`. */
112
43
  lt(field: FieldKey<T>, value: unknown): Condition;
44
+ /** `field <= value`. */
113
45
  lte(field: FieldKey<T>, value: unknown): Condition;
46
+ /** `field IN (values)`. */
114
47
  in(field: FieldKey<T>, values: unknown[]): Condition;
48
+ /** `field NOT IN (values)`. */
115
49
  nin(field: FieldKey<T>, values: unknown[]): Condition;
50
+ /** `field BETWEEN lo AND hi` (inclusive). */
116
51
  between(field: FieldKey<T>, lo: unknown, hi: unknown): Condition;
52
+ /** `field IS NULL` (or `IS NOT NULL` when `isNull` is false). */
117
53
  isNull(field: FieldKey<T>, isNull?: boolean): Condition;
54
+ /** `field` is NULL or empty string. */
118
55
  empty(field: FieldKey<T>): Condition;
56
+ /** `field` is neither NULL nor empty string. */
119
57
  nempty(field: FieldKey<T>): Condition;
58
+ /** `field` contains the substring `value`. */
120
59
  contains(field: FieldKey<T>, value: string): Condition;
60
+ /** Case-insensitive {@link contains}. */
121
61
  icontains(field: FieldKey<T>, value: string): Condition;
62
+ /** `field` starts with `value`. */
122
63
  startsWith(field: FieldKey<T>, value: string): Condition;
64
+ /** `field` ends with `value`. */
123
65
  endsWith(field: FieldKey<T>, value: string): Condition;
66
+ /** Logical AND of the given conditions. */
124
67
  and(...conds: Condition[]): Condition;
68
+ /** Logical OR of the given conditions. */
125
69
  or(...conds: Condition[]): Condition;
70
+ /** Logical NOT of a condition. */
126
71
  not(cond: Condition): Condition;
127
72
  /** Traverse a relation: keys produced by `build` are prefixed with `head.`. */
128
73
  rel<R = Record<string, unknown>>(head: FieldKey<T>, build: (f: FilterBuilder<R>) => Condition): Condition;
@@ -132,6 +77,8 @@ interface FilterBuilder<T> {
132
77
  sub?: DurationParts;
133
78
  }): RelativeNow;
134
79
  }
80
+ /** Fluent, type-safe query builder returned by `from(slug).query()`. Chain
81
+ * `.where`/`.select`/`.orderBy`/… and finish with `.list()` (or `.toQuery()`). */
135
82
  declare class QueryBuilder<T extends Record<string, unknown>> {
136
83
  private readonly listFn;
137
84
  private _filter?;
@@ -144,10 +91,13 @@ declare class QueryBuilder<T extends Record<string, unknown>> {
144
91
  private _locale?;
145
92
  private _q?;
146
93
  constructor(listFn: (q: ListQuery) => Promise<ListResponse<T>>);
94
+ /** Build the filter fluently with a {@link FilterBuilder}. */
147
95
  where(build: (f: FilterBuilder<T>) => Condition): this;
148
96
  /** Replace the filter with a raw canonical condition (escape hatch). */
149
97
  filter(cond: Condition): this;
98
+ /** Project only these fields (column allow-list). */
150
99
  select(...fields: FieldKey<T>[]): this;
100
+ /** Sort by one or more keys (`"-field"` for descending). */
151
101
  orderBy(...sorts: SortKey<T>[]): this;
152
102
  /** Inline single-hop relations (replaces each FK with the related object). */
153
103
  expand(...rels: FieldKey<T>[]): this;
@@ -155,14 +105,128 @@ declare class QueryBuilder<T extends Record<string, unknown>> {
155
105
  locale(loc: string): this;
156
106
  /** Free-text search across readable text fields. */
157
107
  search(text: string): this;
108
+ /** Max rows to return. */
158
109
  limit(n: number): this;
110
+ /** Number of rows to skip (paging). */
159
111
  offset(n: number): this;
112
+ /** Ask the server for a count alongside the page (`filter_count` / `total_count`). */
160
113
  withMeta(m: "filter_count" | "total_count" | "*"): this;
161
114
  /** Assemble the plain `ListQuery` — the canonical JSON the REST API takes. */
162
115
  toQuery(): ListQuery;
116
+ /** Run the query and return the page of rows. */
163
117
  list(): Promise<ListResponse<T>>;
164
118
  }
165
119
 
120
+ /**
121
+ * Offline-first sync for a single collection.
122
+ *
123
+ * Pulls the server changefeed (`GET /api/items/:slug/changes`) into a local
124
+ * store, keeps it live over SSE, and lets the app read/write while offline:
125
+ * writes apply optimistically to the local store and queue, then flush through
126
+ * the batch endpoint on reconnect. Conflicts resolve last-write-wins by
127
+ * `updated_at`, with locally-queued (not-yet-flushed) writes held until they
128
+ * land.
129
+ *
130
+ * The store is pluggable — `memoryStore()` works anywhere; `indexedDbStore()`
131
+ * persists across reloads in the browser.
132
+ */
133
+ /** A locally-queued offline write awaiting flush to the server. */
134
+ type QueuedOp = {
135
+ kind: "create";
136
+ tempId: string;
137
+ data: Record<string, unknown>;
138
+ } | {
139
+ kind: "update";
140
+ id: string;
141
+ data: Record<string, unknown>;
142
+ } | {
143
+ kind: "delete";
144
+ id: string;
145
+ };
146
+ /** Pluggable persistence for the local sync store (rows + meta + write queue). */
147
+ interface SyncStore {
148
+ get(id: string): Promise<Record<string, unknown> | undefined>;
149
+ set(id: string, row: Record<string, unknown>): Promise<void>;
150
+ remove(id: string): Promise<void>;
151
+ all(): Promise<Record<string, unknown>[]>;
152
+ getMeta(key: string): Promise<string | null>;
153
+ setMeta(key: string, value: string): Promise<void>;
154
+ queueGet(): Promise<QueuedOp[]>;
155
+ queueSet(ops: QueuedOp[]): Promise<void>;
156
+ }
157
+ /** In-memory store — non-persistent; the default and the testing baseline. */
158
+ declare const memoryStore: () => SyncStore;
159
+ /** IndexedDB-backed store (browser). One object store for rows + one for meta;
160
+ * the write queue lives under a meta key. Falls back to throwing if IndexedDB
161
+ * is unavailable — use `memoryStore()` outside the browser. */
162
+ declare const indexedDbStore: (opts: {
163
+ collection: string;
164
+ dbName?: string;
165
+ }) => SyncStore;
166
+ /** The subset of the backlex client `createSync` needs. The full client satisfies it. */
167
+ interface SyncClientLike {
168
+ request: <T>(method: string, path: string, body?: unknown) => Promise<T>;
169
+ subscribe: (channel: string, onEvent: (e: {
170
+ event: "created" | "updated" | "deleted";
171
+ data: Record<string, unknown>;
172
+ }) => void, onError?: (err: unknown) => void) => () => void;
173
+ }
174
+ /** Options for `client.sync(...)` / `createSync(...)`. */
175
+ interface SyncOptions {
176
+ collection: string;
177
+ store?: SyncStore;
178
+ /** Primary-key field. Default `id`. */
179
+ pk?: string;
180
+ /** Rows per changefeed page. Default 200. */
181
+ pageSize?: number;
182
+ /** Called after the local store changes (pull / live / local write). */
183
+ onChange?: () => void;
184
+ }
185
+ /** Live, offline-first controller for one collection, returned by `createSync`. */
186
+ interface SyncController {
187
+ /** Drain the changefeed from the saved cursor to head; returns rows applied. */
188
+ pull(): Promise<number>;
189
+ /** Flush queued offline writes through the batch endpoint. */
190
+ flush(): Promise<void>;
191
+ /** Subscribe to live SSE updates; returns an unsubscribe function. */
192
+ live(): () => void;
193
+ /** Pull, go live, and re-sync whenever connectivity returns. */
194
+ start(): Promise<void>;
195
+ /** Stop live updates and remove the online listener. */
196
+ stop(): void;
197
+ /** Every row currently in the local store. */
198
+ getAll(): Promise<Record<string, unknown>[]>;
199
+ /** One row by id from the local store. */
200
+ get(id: string): Promise<Record<string, unknown> | undefined>;
201
+ /** Optimistically create a row locally + queue the write; returns the temp id. */
202
+ create(data: Record<string, unknown>): Promise<string>;
203
+ /** Optimistically update a row locally + queue the write. */
204
+ update(id: string, data: Record<string, unknown>): Promise<void>;
205
+ /** Optimistically remove a row locally + queue the delete. */
206
+ remove(id: string): Promise<void>;
207
+ /** The underlying pluggable local store. */
208
+ store: SyncStore;
209
+ }
210
+ declare const createSync: (client: SyncClientLike, options: SyncOptions) => SyncController;
211
+
212
+ /**
213
+ * @module
214
+ *
215
+ * The backlex client — a typed `fetch` wrapper over the backlex API. Create one
216
+ * with {@link createClient}, then use `.from(slug)` for typed collection CRUD,
217
+ * `.auth` for sign-in/up, `.subscribe` for realtime (SSE), plus `.storage`,
218
+ * `.jobs`, `.flags`, and offline-first `.sync`.
219
+ *
220
+ * ```ts
221
+ * import { createClient } from "backlex";
222
+ *
223
+ * const backlex = createClient({ url: "https://api.your.app", workspace: "acme" });
224
+ * await backlex.auth.signIn({ email, password });
225
+ * const { data } = await backlex.from("todos").list({ sort: ["-created_at"] });
226
+ * const off = backlex.subscribe("items:todos", (e) => console.log(e.event, e.data));
227
+ * ```
228
+ */
229
+
166
230
  interface ClientOptions {
167
231
  url: string;
168
232
  /** Static API key (`pak_...`) for server-to-server calls. Browser apps
@@ -191,16 +255,19 @@ interface ClientOptions {
191
255
  /** Optional fetch override (testing / Node polyfill). */
192
256
  fetch?: typeof fetch;
193
257
  }
258
+ /** A signed-in user as returned by the auth surface. */
194
259
  interface AuthUser {
195
260
  id: string;
196
261
  email: string;
197
262
  name?: string | null;
198
263
  image?: string | null;
199
264
  }
265
+ /** Result of a sign-in / sign-up — the user and (app mode) the session token. */
200
266
  interface AuthResult {
201
267
  user: AuthUser;
202
268
  token?: string;
203
269
  }
270
+ /** One active session (device/login) of the signed-in user. */
204
271
  interface AuthSession {
205
272
  id: string;
206
273
  token: string;
@@ -211,12 +278,14 @@ interface AuthSession {
211
278
  createdAt: string;
212
279
  updatedAt: string;
213
280
  }
281
+ /** A public sign-in provider as advertised by `auth.providers()`. */
214
282
  interface PublicProvider {
215
283
  id: string;
216
284
  kind: "credential" | "magic-link" | "email-otp" | "passkey" | "social";
217
285
  label: string;
218
286
  enabled: boolean;
219
287
  }
288
+ /** Public description of a workspace's auth surface (provider list + policy). */
220
289
  interface AuthSurface {
221
290
  tenantId: string | null;
222
291
  providers: PublicProvider[];
@@ -261,6 +330,7 @@ interface CollectionClient<T extends Record<string, unknown>> {
261
330
  deleteMany(ids: string[], opts?: {
262
331
  atomic?: boolean;
263
332
  }): Promise<BatchResponse<T>>;
333
+ bulkUpdate(keys: string[], data: Partial<T>): Promise<BulkUpdateResponse>;
264
334
  batch(operations: BatchOperation<T>[], opts?: {
265
335
  atomic?: boolean;
266
336
  }): Promise<BatchResponse<T>>;
@@ -268,278 +338,317 @@ interface CollectionClient<T extends Record<string, unknown>> {
268
338
  unpublish(id: string): Promise<ItemResponse<T>>;
269
339
  schedulePublish(id: string, at: Date | string | null): Promise<ItemResponse<T>>;
270
340
  }
271
- declare const createClient: (opts: ClientOptions) => {
272
- from: <T extends Record<string, unknown>>(slug: string) => CollectionClient<T>;
273
- subscribe: <T = Record<string, unknown>>(channel: string, onEvent: (e: ItemEvent<T>) => void, onError?: (err: unknown) => void) => (() => void);
274
- auth: {
275
- /** Email + password sign-up. In app mode this creates a *workspace* end-
276
- * user (in `app_users`), not a control-plane account. */
277
- signUp: (input: {
278
- email: string;
279
- password: string;
280
- name?: string;
281
- }) => Promise<AuthResult>;
282
- /** Email + password sign-in. */
283
- signIn: (input: {
284
- email: string;
285
- password: string;
286
- }) => Promise<AuthResult>;
287
- /**
288
- * Begin an OAuth sign-in. Returns `{ url }` — the provider's authorize
289
- * page — which a browser app should navigate to (`location.href = url`).
290
- * `provider` must be one of the ids returned by `auth.providers()`.
291
- */
292
- signInSocial: (provider: string, input?: {
293
- callbackURL?: string;
294
- errorCallbackURL?: string;
295
- }) => Promise<{
296
- url: string;
297
- redirect: boolean;
298
- }>;
299
- /** Send a one-time sign-in link by email (requires the `magic` provider
300
- * to be enabled for the workspace). */
301
- signInMagicLink: (input: {
302
- email: string;
303
- callbackURL?: string;
304
- }) => Promise<{
305
- status: boolean;
306
- }>;
307
- /** Email a one-time numeric code (requires the `email-otp` provider). `type`
308
- * defaults to `"sign-in"`; use `"email-verification"` / `"forget-password"`
309
- * for those flows. Complete a sign-in with `signInEmailOTP`. */
310
- sendVerificationOTP: (input: {
311
- email: string;
312
- type?: "sign-in" | "email-verification" | "forget-password";
313
- }) => Promise<{
314
- success: boolean;
315
- }>;
316
- /** Complete an email-OTP sign-in with the code from `sendVerificationOTP`. In
317
- * app mode the returned session token is captured and replayed as a bearer. */
318
- signInEmailOTP: (input: {
319
- email: string;
320
- otp: string;
321
- }) => Promise<AuthResult>;
322
- /** Send a password-reset email. `redirectTo` is the link the email points at. */
323
- requestPasswordReset: (input: {
324
- email: string;
325
- redirectTo?: string;
326
- }) => Promise<{
327
- status: boolean;
328
- }>;
329
- /** Complete a reset with the token from the email and a new password. */
330
- resetPassword: (input: {
331
- newPassword: string;
332
- token: string;
333
- }) => Promise<{
334
- status: boolean;
335
- }>;
336
- /** Mint a fresh short-lived access JWT from the stored session token (app
337
- * mode). The SDK's own requests keep using the session token; use this when a
338
- * downstream service needs a proper access token. */
339
- refresh: () => Promise<{
340
- accessToken: string;
341
- refreshToken: string;
342
- expiresIn: number;
343
- tokenType: string;
344
- }>;
345
- /** Change the signed-in user's password (requires the current password). */
346
- changePassword: (input: {
347
- newPassword: string;
348
- currentPassword: string;
349
- revokeOtherSessions?: boolean;
350
- }) => Promise<Record<string, unknown>>;
351
- /** Update the signed-in user's profile (e.g. `{ name, image }`). */
352
- updateUser: (attributes: Record<string, unknown>) => Promise<Record<string, unknown>>;
353
- /** Send an email-verification link to the signed-in (or named) user. */
354
- sendVerificationEmail: (input: {
355
- email: string;
356
- callbackURL?: string;
357
- }) => Promise<{
358
- status: boolean;
359
- }>;
360
- signOut: () => Promise<{
361
- success: boolean;
362
- }>;
363
- /** Current session, or `{ user: null }`. */
364
- getSession: () => Promise<{
365
- user: AuthUser | null;
366
- } & Record<string, unknown>>;
367
- /** List the signed-in user's active sessions (one row per device/login). */
368
- listSessions: () => Promise<AuthSession[]>;
369
- /** Revoke one session by its `token` (from `listSessions`). */
370
- revokeSession: (input: {
371
- token: string;
372
- }) => Promise<{
373
- status: boolean;
374
- }>;
375
- /** Revoke every session **except** the current one (sign out other devices). */
376
- revokeOtherSessions: () => Promise<{
377
- status: boolean;
378
- }>;
379
- /** Revoke **all** sessions, including the current one. */
380
- revokeSessions: () => Promise<{
381
- status: boolean;
382
- }>;
383
- /** Public description of this workspace's auth surface (provider list +
384
- * policy flags) — what a sign-in screen needs to render. No secrets. */
385
- providers: () => Promise<AuthSurface>;
386
- /** The current workspace session token (app mode) — persist this across
387
- * reloads and pass it back via `createClient({ token })`. */
388
- getToken: () => string | null;
389
- /** Restore a workspace session token (app mode). */
390
- setToken: (token: string | null) => void;
391
- };
392
- storage: {
393
- list: (prefix?: string) => Promise<{
394
- data: {
395
- key: string;
396
- size: number;
397
- contentType?: string;
398
- ownerId: string | null;
399
- uploadedAt: string;
400
- }[];
401
- }>;
402
- put: (key: string, body: BodyInit, contentType?: string, folderId?: string) => Promise<any>;
403
- download: (key: string) => Promise<Response>;
404
- delete: (key: string) => Promise<{
405
- ok: boolean;
406
- }>;
407
- /**
408
- * Resumable upload (TUS 1.0.0). Splits `data` into chunks and PATCHes them
409
- * to `/api/uploads`, resuming from the server's committed offset after a
410
- * transient failure. `data` may be a `Blob`/`File`, `ArrayBuffer`, or
411
- * `Uint8Array`. Returns the final key + the TUS session `location` (persist
412
- * it to resume across page reloads via `resumeUpload`). The standard TUS
413
- * protocol means Uppy / tus-js-client can also target `/api/uploads`.
414
- */
415
- uploadResumable: (input: {
341
+ /** Auth surface for a workspace's end-users (and the admin pool). See `createClient`. */
342
+ interface AuthClient {
343
+ /** Email + password sign-up (app mode a workspace end-user). */
344
+ signUp(input: {
345
+ email: string;
346
+ password: string;
347
+ name?: string;
348
+ }): Promise<AuthResult>;
349
+ /** Email + password sign-in. */
350
+ signIn(input: {
351
+ email: string;
352
+ password: string;
353
+ }): Promise<AuthResult>;
354
+ /** Begin an OAuth sign-in; returns the provider authorize `url` to navigate to. */
355
+ signInSocial(provider: string, input?: {
356
+ callbackURL?: string;
357
+ errorCallbackURL?: string;
358
+ }): Promise<{
359
+ url: string;
360
+ redirect: boolean;
361
+ }>;
362
+ /** Send a one-time sign-in link by email (magic-link provider). */
363
+ signInMagicLink(input: {
364
+ email: string;
365
+ callbackURL?: string;
366
+ }): Promise<{
367
+ status: boolean;
368
+ }>;
369
+ /** Email a one-time numeric code (email-otp provider). */
370
+ sendVerificationOTP(input: {
371
+ email: string;
372
+ type?: "sign-in" | "email-verification" | "forget-password";
373
+ }): Promise<{
374
+ success: boolean;
375
+ }>;
376
+ /** Complete an email-OTP sign-in with the emailed code. */
377
+ signInEmailOTP(input: {
378
+ email: string;
379
+ otp: string;
380
+ }): Promise<AuthResult>;
381
+ /** Send a password-reset email. */
382
+ requestPasswordReset(input: {
383
+ email: string;
384
+ redirectTo?: string;
385
+ }): Promise<{
386
+ status: boolean;
387
+ }>;
388
+ /** Complete a reset with the emailed token and a new password. */
389
+ resetPassword(input: {
390
+ newPassword: string;
391
+ token: string;
392
+ }): Promise<{
393
+ status: boolean;
394
+ }>;
395
+ /** Mint a fresh short-lived access JWT from the stored session token (app mode). */
396
+ refresh(): Promise<{
397
+ accessToken: string;
398
+ refreshToken: string;
399
+ expiresIn: number;
400
+ tokenType: string;
401
+ }>;
402
+ /** Change the signed-in user's password. */
403
+ changePassword(input: {
404
+ newPassword: string;
405
+ currentPassword: string;
406
+ revokeOtherSessions?: boolean;
407
+ }): Promise<Record<string, unknown>>;
408
+ /** Update the signed-in user's profile (e.g. `{ name, image }`). */
409
+ updateUser(attributes: Record<string, unknown>): Promise<Record<string, unknown>>;
410
+ /** Send an email-verification link to the signed-in (or named) user. */
411
+ sendVerificationEmail(input: {
412
+ email: string;
413
+ callbackURL?: string;
414
+ }): Promise<{
415
+ status: boolean;
416
+ }>;
417
+ /** Sign out the current session. */
418
+ signOut(): Promise<{
419
+ success: boolean;
420
+ }>;
421
+ /** Current session, or `{ user: null }`. */
422
+ getSession(): Promise<{
423
+ user: AuthUser | null;
424
+ } & Record<string, unknown>>;
425
+ /** List the signed-in user's active sessions. */
426
+ listSessions(): Promise<AuthSession[]>;
427
+ /** Revoke one session by its token. */
428
+ revokeSession(input: {
429
+ token: string;
430
+ }): Promise<{
431
+ status: boolean;
432
+ }>;
433
+ /** Revoke every session except the current one. */
434
+ revokeOtherSessions(): Promise<{
435
+ status: boolean;
436
+ }>;
437
+ /** Revoke all sessions, including the current one. */
438
+ revokeSessions(): Promise<{
439
+ status: boolean;
440
+ }>;
441
+ /** Public description of this workspace's auth surface (providers + policy). */
442
+ providers(): Promise<AuthSurface>;
443
+ /** The current workspace session token (app mode) — persist across reloads. */
444
+ getToken(): string | null;
445
+ /** Restore a workspace session token (app mode). */
446
+ setToken(token: string | null): void;
447
+ }
448
+ /** File storage + resumable (TUS) uploads. See `createClient`. */
449
+ interface StorageClient {
450
+ /** List stored objects, optionally under a key prefix. */
451
+ list(prefix?: string): Promise<{
452
+ data: {
416
453
  key: string;
417
- data: Blob | ArrayBuffer | Uint8Array;
454
+ size: number;
418
455
  contentType?: string;
419
- folderId?: string;
420
- /** Bytes per PATCH. Default 8 MiB (object stores need ≥5 MiB non-final parts). */
421
- chunkSize?: number;
422
- onProgress?: (sent: number, total: number) => void;
423
- signal?: AbortSignal;
424
- }) => Promise<ResumableUploadResult>;
425
- /** Resume a previously-started resumable upload at the server's offset. */
426
- resumeUpload: (location: string, data: Blob | ArrayBuffer | Uint8Array, opts2?: {
427
- chunkSize?: number;
428
- onProgress?: (sent: number, total: number) => void;
429
- signal?: AbortSignal;
430
- }) => Promise<void>;
431
- };
432
- messaging: {
433
- /** Register (or refresh) the current user's push device. Re-registering the
434
- * same token reactivates it and updates last-seen, so call this on every
435
- * app launch. `web-push` requires `keys` (the VAPID subscription keys). */
436
- registerDevice: (input: {
437
- platform: "fcm" | "apns" | "web-push";
438
- token: string;
439
- keys?: {
440
- p256dh: string;
441
- auth: string;
442
- };
443
- deviceName?: string;
444
- }) => Promise<{
445
- data: {
446
- id: string;
447
- };
448
- }>;
449
- /** Remove one of the caller's registered devices by id. */
450
- unregister: (id: string) => Promise<{
451
- ok: boolean;
452
- }>;
453
- /** List the caller's registered devices. */
454
- listDevices: () => Promise<{
455
- data: DeviceToken[];
456
- }>;
457
- /** Register (or refresh) the current user's phone number for SMS. Number
458
- * must be E.164 (e.g. "+14155552671"). Re-registering reactivates it. */
459
- registerPhone: (input: {
460
- phoneNumber: string;
461
- }) => Promise<{
462
- data: {
463
- id: string;
464
- };
465
- }>;
466
- /** Remove one of the caller's registered phone numbers by id. */
467
- unregisterPhone: (id: string) => Promise<{
468
- ok: boolean;
469
- }>;
470
- /** List the caller's registered phone numbers. */
471
- listPhones: () => Promise<{
472
- data: PhoneNumber[];
473
- }>;
474
- };
475
- jobs: {
476
- /** Enqueue a durable background job. `type` is `function` (run a named
477
- * function with `payload.name` + `payload.input`) or `webhook.deliver`.
478
- * Jobs retry with backoff and dead-letter after `maxAttempts`. Pass
479
- * `runAt` (ISO string) to schedule for later. Admin-scoped. */
480
- enqueue: (input: {
481
- type: "function" | "webhook.deliver";
482
- payload?: Record<string, unknown>;
483
- queue?: string;
484
- runAt?: string;
485
- maxAttempts?: number;
486
- priority?: number;
487
- }) => Promise<{
456
+ ownerId: string | null;
457
+ uploadedAt: string;
458
+ }[];
459
+ }>;
460
+ /** Upload an object in one request. */
461
+ put(key: string, body: BodyInit, contentType?: string, folderId?: string): Promise<unknown>;
462
+ /** Download an object; returns the raw `Response`. */
463
+ download(key: string): Promise<Response>;
464
+ /** Delete an object by key. */
465
+ delete(key: string): Promise<{
466
+ ok: boolean;
467
+ }>;
468
+ /** Resumable upload (TUS 1.0.0) that resumes after a transient failure. */
469
+ uploadResumable(input: {
470
+ key: string;
471
+ data: Blob | ArrayBuffer | Uint8Array;
472
+ contentType?: string;
473
+ folderId?: string;
474
+ chunkSize?: number;
475
+ onProgress?: (sent: number, total: number) => void;
476
+ signal?: AbortSignal;
477
+ }): Promise<ResumableUploadResult>;
478
+ /** Resume a previously-started resumable upload at the server's offset. */
479
+ resumeUpload(location: string, data: Blob | ArrayBuffer | Uint8Array, opts2?: {
480
+ chunkSize?: number;
481
+ onProgress?: (sent: number, total: number) => void;
482
+ signal?: AbortSignal;
483
+ }): Promise<void>;
484
+ }
485
+ /** Push + SMS device registration for the current user. See `createClient`. */
486
+ interface MessagingClient {
487
+ /** Register (or refresh) the current user's push device. */
488
+ registerDevice(input: {
489
+ platform: "fcm" | "apns" | "web-push";
490
+ token: string;
491
+ keys?: {
492
+ p256dh: string;
493
+ auth: string;
494
+ };
495
+ deviceName?: string;
496
+ }): Promise<{
497
+ data: {
498
+ id: string;
499
+ };
500
+ }>;
501
+ /** Remove one of the caller's registered devices by id. */
502
+ unregister(id: string): Promise<{
503
+ ok: boolean;
504
+ }>;
505
+ /** List the caller's registered devices. */
506
+ listDevices(): Promise<{
507
+ data: DeviceToken[];
508
+ }>;
509
+ /** Register (or refresh) the caller's E.164 phone number for SMS. */
510
+ registerPhone(input: {
511
+ phoneNumber: string;
512
+ }): Promise<{
513
+ data: {
488
514
  id: string;
489
- }>;
490
- /** List jobs (newest first), optionally filtered by queue/status. */
491
- list: (q?: {
492
- queue?: string;
493
- status?: JobStatus;
494
- limit?: number;
495
- }) => Promise<{
496
- jobs: Job[];
497
- }>;
498
- /** Fetch a single job by id. */
499
- get: (id: string) => Promise<Job>;
500
- /** Requeue a failed / dead-lettered / cancelled job to run again. */
501
- retry: (id: string) => Promise<{
502
- ok: boolean;
503
- }>;
504
- /** Cancel a pending job. */
505
- cancel: (id: string) => Promise<{
506
- ok: boolean;
507
- }>;
508
- /** Delete a job row. */
509
- remove: (id: string) => Promise<{
510
- ok: boolean;
511
- }>;
512
- };
513
- flags: {
514
- /** Fetch + cache the evaluated flag map. */
515
- all: () => Promise<Record<string, FlagState>>;
516
- /** Resolved value for a flag (remote config payload), or `undefined`. Uses
517
- * the cache if `all()` was already called this session; pass
518
- * `{ refresh: true }` to force a re-fetch. */
519
- get: (key: string, opts?: {
520
- refresh?: boolean;
521
- }) => Promise<unknown>;
522
- /** Whether a flag is on for the caller. */
523
- isEnabled: (key: string, opts?: {
524
- refresh?: boolean;
525
- }) => Promise<boolean>;
526
- };
527
- sync: (options: SyncOptions) => {
528
- pull: () => Promise<number>;
529
- flush: () => Promise<void>;
530
- live: () => (() => void);
531
- start: () => Promise<void>;
532
- stop: () => void;
533
- getAll: () => Promise<Record<string, unknown>[]>;
534
- get: (id: string) => Promise<Record<string, unknown> | undefined>;
535
- create: (data: Record<string, unknown>) => Promise<string>;
536
- update: (id: string, data: Record<string, unknown>) => Promise<void>;
537
- remove: (id: string) => Promise<void>;
538
- store: SyncStore;
539
- };
515
+ };
516
+ }>;
517
+ /** Remove one of the caller's registered phone numbers by id. */
518
+ unregisterPhone(id: string): Promise<{
519
+ ok: boolean;
520
+ }>;
521
+ /** List the caller's registered phone numbers. */
522
+ listPhones(): Promise<{
523
+ data: PhoneNumber[];
524
+ }>;
525
+ }
526
+ /** Durable background job queue (admin-scoped). See `createClient`. */
527
+ interface JobsClient {
528
+ /** Enqueue a durable background job. */
529
+ enqueue(input: {
530
+ type: "function" | "webhook.deliver";
531
+ payload?: Record<string, unknown>;
532
+ queue?: string;
533
+ runAt?: string;
534
+ maxAttempts?: number;
535
+ priority?: number;
536
+ }): Promise<{
537
+ id: string;
538
+ }>;
539
+ /** List jobs (newest first), optionally filtered by queue/status. */
540
+ list(q?: {
541
+ queue?: string;
542
+ status?: JobStatus;
543
+ limit?: number;
544
+ }): Promise<{
545
+ jobs: Job[];
546
+ }>;
547
+ /** Fetch a single job by id. */
548
+ get(id: string): Promise<Job>;
549
+ /** Requeue a failed / dead-lettered / cancelled job. */
550
+ retry(id: string): Promise<{
551
+ ok: boolean;
552
+ }>;
553
+ /** Cancel a pending job. */
554
+ cancel(id: string): Promise<{
555
+ ok: boolean;
556
+ }>;
557
+ /** Delete a job row. */
558
+ remove(id: string): Promise<{
559
+ ok: boolean;
560
+ }>;
561
+ }
562
+ /** Feature flags / remote config evaluated for the current caller. See `createClient`. */
563
+ interface FlagsClient {
564
+ /** Fetch + cache the evaluated flag map. */
565
+ all(): Promise<Record<string, FlagState>>;
566
+ /** Resolved value (remote config payload) for a flag, or `undefined`. */
567
+ get(key: string, opts?: {
568
+ refresh?: boolean;
569
+ }): Promise<unknown>;
570
+ /** Whether a flag is on for the caller. */
571
+ isEnabled(key: string, opts?: {
572
+ refresh?: boolean;
573
+ }): Promise<boolean>;
574
+ }
575
+ /** A visual workflow (flow) row. `operations` is the serialized op DSL the
576
+ * builder compiles; `layout` is a purely-presentational graph snapshot. */
577
+ interface Flow {
578
+ id: string;
579
+ tenantId?: string | null;
580
+ name: string;
581
+ trigger: string;
582
+ operations: unknown[];
583
+ layout?: unknown;
584
+ active: boolean;
585
+ }
586
+ /** Create/update payload for a flow. `operations` must be non-empty on create;
587
+ * `update` accepts any subset. */
588
+ interface FlowInput {
589
+ name: string;
590
+ trigger: string;
591
+ operations: unknown[];
592
+ layout?: unknown;
593
+ active?: boolean;
594
+ }
595
+ /** Outcome of a manual flow run. `ok: false` means the run halted on an
596
+ * unhandled op error; `error` carries the first failure message. */
597
+ interface FlowRunResult {
598
+ ok: boolean;
599
+ error?: string;
600
+ }
601
+ /** Visual workflows (admin-scoped). Mirrors `/api/flows`. See `createClient`. */
602
+ interface FlowsClient {
603
+ /** List every flow in the active workspace. */
604
+ list(): Promise<{
605
+ data: Flow[];
606
+ }>;
607
+ /** Fetch a single flow's full definition by id. */
608
+ get(id: string): Promise<{
609
+ data: Flow;
610
+ }>;
611
+ /** Create a flow scoped to the active workspace. */
612
+ create(input: FlowInput): Promise<{
613
+ data: Flow;
614
+ }>;
615
+ /** Partial update of a flow by id. */
616
+ update(id: string, patch: Partial<FlowInput>): Promise<{
617
+ ok: boolean;
618
+ }>;
619
+ /** Delete a flow by id. */
620
+ delete(id: string): Promise<{
621
+ ok: boolean;
622
+ }>;
623
+ /** Synchronously run a flow with an arbitrary `input` trigger payload. */
624
+ run(id: string, input?: Record<string, unknown>): Promise<FlowRunResult>;
625
+ }
626
+ /** The backlex client returned by `createClient` — data, auth, storage, realtime, and more. */
627
+ interface BacklexClient {
628
+ /** Typed data API for one collection by slug. */
629
+ from<T extends Record<string, unknown>>(slug: string): CollectionClient<T>;
630
+ /** Subscribe to a realtime channel (SSE); returns an unsubscribe function. */
631
+ subscribe<T = Record<string, unknown>>(channel: string, onEvent: (e: ItemEvent<T>) => void, onError?: (err: unknown) => void): () => void;
632
+ /** Auth surface (sign-in/up, sessions, tokens). */
633
+ auth: AuthClient;
634
+ /** File storage + resumable uploads. */
635
+ storage: StorageClient;
636
+ /** Push + SMS device registration. */
637
+ messaging: MessagingClient;
638
+ /** Durable background job queue. */
639
+ jobs: JobsClient;
640
+ /** Visual workflows (flows). */
641
+ flows: FlowsClient;
642
+ /** Feature flags / remote config. */
643
+ flags: FlagsClient;
644
+ /** Offline-first sync controller for one collection. */
645
+ sync(options: SyncOptions): SyncController;
540
646
  /** Raw escape hatch — issues a request with auth headers applied. */
541
- request: <T>(method: string, path: string, body?: unknown, extraHeaders?: Record<string, string>) => Promise<T>;
542
- };
647
+ request<T>(method: string, path: string, body?: unknown, extraHeaders?: Record<string, string>): Promise<T>;
648
+ }
649
+ /** Create a backlex client. In app mode (`workspace` set) auth + data scope to
650
+ * one workspace and the session token is captured + replayed as a bearer. */
651
+ declare const createClient: (opts: ClientOptions) => BacklexClient;
543
652
  /** A registry mapping each collection slug to its row type — the shape
544
653
  * `backlex gen-types --sdk` emits as `Collections`. */
545
654
  type CollectionsMap = Record<string, Record<string, unknown>>;
@@ -564,6 +673,4 @@ type TypedClient<R extends CollectionsMap> = BacklexClient & {
564
673
  */
565
674
  declare const typedCollections: <R extends CollectionsMap>(client: BacklexClient) => TypedClient<R>;
566
675
 
567
- type BacklexClient = ReturnType<typeof createClient>;
568
-
569
- export { AggregateQuery, AggregateRow, type BacklexClient, BatchOperation, BatchResponse, type ClientOptions, type CollectionClient, type CollectionsMap, DeviceToken, type FieldKey, type FilterBuilder, FlagState, ImportSummary, ItemEvent, ItemQuery, ItemResponse, Job, JobStatus, ListQuery, ListResponse, PhoneNumber, QueryBuilder, type QueuedOp, ResumableUploadResult, SearchQuery, SearchResponse, type SortKey, type SyncOptions, type SyncStore, type TypedClient, type TypedCollections, createClient, createSync, indexedDbStore, memoryStore, typedCollections };
676
+ export { AggregateQuery, AggregateRow, type AuthClient, type AuthResult, type AuthSession, type AuthSurface, type AuthUser, type BacklexClient, BatchOperation, BatchResponse, BulkUpdateResponse, type ClientOptions, type CollectionClient, type CollectionsMap, DeviceToken, type FieldKey, type FilterBuilder, FlagState, type FlagsClient, type Flow, type FlowInput, type FlowRunResult, type FlowsClient, ImportSummary, ItemEvent, ItemQuery, ItemResponse, Job, JobStatus, type JobsClient, ListQuery, ListResponse, type MessagingClient, PhoneNumber, type PublicProvider, QueryBuilder, type QueuedOp, ResumableUploadResult, SearchQuery, SearchResponse, type SortKey, type StorageClient, type SyncOptions, type SyncStore, type TypedClient, type TypedCollections, createClient, createSync, indexedDbStore, memoryStore, typedCollections };
package/dist/index.js CHANGED
@@ -111,6 +111,7 @@ var QueryBuilder = class {
111
111
  _meta;
112
112
  _locale;
113
113
  _q;
114
+ /** Build the filter fluently with a {@link FilterBuilder}. */
114
115
  where(build) {
115
116
  this._filter = normalizeCondition(build(makeFilterBuilder()));
116
117
  return this;
@@ -120,10 +121,12 @@ var QueryBuilder = class {
120
121
  this._filter = normalizeCondition(cond);
121
122
  return this;
122
123
  }
124
+ /** Project only these fields (column allow-list). */
123
125
  select(...fields) {
124
126
  this._fields.push(...fields);
125
127
  return this;
126
128
  }
129
+ /** Sort by one or more keys (`"-field"` for descending). */
127
130
  orderBy(...sorts) {
128
131
  this._sort.push(...sorts);
129
132
  return this;
@@ -143,14 +146,17 @@ var QueryBuilder = class {
143
146
  this._q = text;
144
147
  return this;
145
148
  }
149
+ /** Max rows to return. */
146
150
  limit(n) {
147
151
  this._limit = n;
148
152
  return this;
149
153
  }
154
+ /** Number of rows to skip (paging). */
150
155
  offset(n) {
151
156
  this._offset = n;
152
157
  return this;
153
158
  }
159
+ /** Ask the server for a count alongside the page (`filter_count` / `total_count`). */
154
160
  withMeta(m) {
155
161
  this._meta = m;
156
162
  return this;
@@ -169,6 +175,7 @@ var QueryBuilder = class {
169
175
  if (this._q) q.q = this._q;
170
176
  return q;
171
177
  }
178
+ /** Run the query and return the page of rows. */
172
179
  list() {
173
180
  return this.listFn(this.toQuery());
174
181
  }
@@ -449,7 +456,11 @@ var createClient = (opts) => {
449
456
  const tenantHeader = () => opts.tenant ? { "x-backlex-tenant": opts.tenant } : {};
450
457
  const request = async (method, path, body, extraHeaders) => {
451
458
  const headers = {
452
- "content-type": "application/json",
459
+ // Only advertise a JSON body when we actually send one. A bodyless POST
460
+ // (publish, unpublish, restore, fts-reindex, …) that still carried
461
+ // `content-type: application/json` made the server's body validator try to
462
+ // parse an empty body and fail with "Malformed JSON in request body".
463
+ ...body !== void 0 ? { "content-type": "application/json" } : {},
453
464
  ...authHeader(),
454
465
  ...tenantHeader(),
455
466
  ...extraHeaders ?? {}
@@ -532,6 +543,14 @@ var createClient = (opts) => {
532
543
  operations: ids.map((id) => ({ op: "delete", id })),
533
544
  atomic: opts2?.atomic
534
545
  }),
546
+ /** Set the SAME fields on many ids at once (one shared patch). Only the
547
+ * named fields change per row; partial-success (a key the caller can't
548
+ * write is reported `NOT_FOUND`). Differs from `updateMany`, which sends
549
+ * a distinct patch per id. */
550
+ bulkUpdate: (keys, data) => request("POST", `/api/items/${slug}/bulk-update`, {
551
+ keys,
552
+ data
553
+ }),
535
554
  /** Mixed create/update/delete in one request. `atomic` = all-or-nothing. */
536
555
  batch: (operations, opts2) => request("POST", `/api/items/${slug}/batch`, {
537
556
  operations,
@@ -811,6 +830,20 @@ var createClient = (opts) => {
811
830
  /** Delete a job row. */
812
831
  remove: (id) => request("DELETE", `/api/jobs/${encodeURIComponent(id)}`)
813
832
  };
833
+ const flows = {
834
+ /** List every flow in the active workspace. */
835
+ list: () => request("GET", "/api/flows"),
836
+ /** Fetch a single flow's full definition by id. */
837
+ get: (id) => request("GET", `/api/flows/${encodeURIComponent(id)}`),
838
+ /** Create a flow scoped to the active workspace. */
839
+ create: (input) => request("POST", "/api/flows", input),
840
+ /** Partial update of a flow by id. */
841
+ update: (id, patch) => request("PATCH", `/api/flows/${encodeURIComponent(id)}`, patch),
842
+ /** Delete a flow by id. */
843
+ delete: (id) => request("DELETE", `/api/flows/${encodeURIComponent(id)}`),
844
+ /** Run a flow synchronously with an arbitrary `input` trigger payload. */
845
+ run: (id, input) => request("POST", `/api/flows/${encodeURIComponent(id)}/run`, input ?? {})
846
+ };
814
847
  let flagsCache = null;
815
848
  const fetchFlags = async () => {
816
849
  const res = await request("GET", "/api/flags");
@@ -841,6 +874,7 @@ var createClient = (opts) => {
841
874
  storage,
842
875
  messaging,
843
876
  jobs,
877
+ flows,
844
878
  flags,
845
879
  sync,
846
880
  /** Raw escape hatch — issues a request with auth headers applied. */
@@ -52,6 +52,15 @@ type Condition = {
52
52
  [field: string]: ComparisonObj;
53
53
  };
54
54
 
55
+ /**
56
+ * @module
57
+ *
58
+ * Wire types for the backlex client — the request/response shapes for list,
59
+ * item, query, search, aggregate, batch, device tokens, jobs, uploads, and
60
+ * feature flags, plus the {@link BacklexError} thrown on a failed request.
61
+ */
62
+
63
+ /** Response from `from(slug).list(...)` — a page of rows + paging metadata. */
55
64
  interface ListResponse<T> {
56
65
  data: T[];
57
66
  limit: number;
@@ -61,9 +70,11 @@ interface ListResponse<T> {
61
70
  total_count?: number;
62
71
  };
63
72
  }
73
+ /** Response wrapping a single row (`one` / `create` / `update`). */
64
74
  interface ItemResponse<T> {
65
75
  data: T;
66
76
  }
77
+ /** Query params for `from(slug).list(...)` — filter, sort, projection, paging. */
67
78
  interface ListQuery {
68
79
  filter?: Condition;
69
80
  sort?: string | string[];
@@ -137,10 +148,12 @@ interface ImportSummary {
137
148
  error: string;
138
149
  }[];
139
150
  }
151
+ /** A realtime event delivered to `subscribe(...)`. */
140
152
  interface ItemEvent<T = Record<string, unknown>> {
141
153
  event: "created" | "updated" | "deleted";
142
154
  data: T;
143
155
  }
156
+ /** One operation in a `batch(...)` request. */
144
157
  type BatchOperation<T = Record<string, unknown>> = {
145
158
  op: "create";
146
159
  data: Partial<T>;
@@ -152,6 +165,7 @@ type BatchOperation<T = Record<string, unknown>> = {
152
165
  op: "delete";
153
166
  id: string;
154
167
  };
168
+ /** Per-row outcome inside a {@link BatchResponse}. */
155
169
  interface BatchRowResult<T = Record<string, unknown>> {
156
170
  index: number;
157
171
  op: "create" | "update" | "delete";
@@ -163,6 +177,7 @@ interface BatchRowResult<T = Record<string, unknown>> {
163
177
  message: string;
164
178
  };
165
179
  }
180
+ /** Response from a bulk `createMany`/`updateMany`/`deleteMany`/`batch` call. */
166
181
  interface BatchResponse<T = Record<string, unknown>> {
167
182
  data: {
168
183
  atomic: boolean;
@@ -172,6 +187,26 @@ interface BatchResponse<T = Record<string, unknown>> {
172
187
  results: BatchRowResult<T>[];
173
188
  };
174
189
  }
190
+ /** Per-key outcome inside a {@link BulkUpdateResponse}. */
191
+ interface BulkUpdateRowResult {
192
+ id: string;
193
+ ok: boolean;
194
+ error?: {
195
+ code: string;
196
+ message: string;
197
+ };
198
+ }
199
+ /** Response from a `bulkUpdate(keys, data)` call — one shared patch over many
200
+ * ids, partial-success (a key the caller can't write is `NOT_FOUND`). */
201
+ interface BulkUpdateResponse {
202
+ data: {
203
+ total: number;
204
+ updated: number;
205
+ failed: number;
206
+ results: BulkUpdateRowResult[];
207
+ };
208
+ }
209
+ /** A registered push device (from `messaging.listDevices()`). */
175
210
  interface DeviceToken {
176
211
  id: string;
177
212
  platform: "fcm" | "apns" | "web-push";
@@ -181,6 +216,7 @@ interface DeviceToken {
181
216
  createdAt: string | number;
182
217
  lastSeenAt: string | number | null;
183
218
  }
219
+ /** A registered SMS phone number (from `messaging.listPhones()`). */
184
220
  interface PhoneNumber {
185
221
  id: string;
186
222
  phoneNumber: string;
@@ -188,7 +224,9 @@ interface PhoneNumber {
188
224
  createdAt: string | number;
189
225
  lastSeenAt: string | number | null;
190
226
  }
227
+ /** Lifecycle state of a durable {@link Job}. */
191
228
  type JobStatus = "pending" | "active" | "succeeded" | "failed" | "dead_letter" | "cancelled";
229
+ /** A durable background job row (from `jobs.get`/`jobs.list`). */
192
230
  interface Job {
193
231
  id: string;
194
232
  tenantId: string | null;
@@ -205,6 +243,7 @@ interface Job {
205
243
  createdAt: string | number;
206
244
  completedAt: string | number | null;
207
245
  }
246
+ /** Status of a resumable {@link Upload} session. */
208
247
  type UploadStatus = "pending" | "completed" | "aborted";
209
248
  /** A resumable (TUS) upload session, as returned by the management API. */
210
249
  interface Upload {
@@ -230,11 +269,14 @@ interface FlagState {
230
269
  enabled: boolean;
231
270
  value: unknown;
232
271
  }
272
+ /** The error envelope returned by the API on a failed request. */
233
273
  interface ApiError {
234
274
  code: string;
235
275
  message: string;
236
276
  details?: unknown;
237
277
  }
278
+ /** Thrown by every client method on a non-2xx response — carries the HTTP
279
+ * `status`, the API error `code`, the `message`, and any `details`. */
238
280
  declare class BacklexError extends Error {
239
281
  readonly code: string;
240
282
  readonly status: number;
@@ -244,4 +286,4 @@ declare class BacklexError extends Error {
244
286
  } | undefined);
245
287
  }
246
288
 
247
- export { type AggregateQuery as A, BacklexError as B, type Condition as C, type DeviceToken as D, type FlagState as F, type ImportSummary as I, type Job as J, type ListQuery as L, type PhoneNumber as P, type RelativeNow as R, type SearchQuery as S, type Upload as U, type AggregateRow as a, type ApiError as b, type BatchOperation as c, type BatchResponse as d, type BatchRowResult as e, type DurationParts as f, type ItemEvent as g, type ItemQuery as h, type ItemResponse as i, type JobStatus as j, type ListResponse as k, type ResumableUploadResult as l, type SearchResponse as m, type UploadStatus as n };
289
+ export { type AggregateQuery as A, BacklexError as B, type Condition as C, type DeviceToken as D, type FlagState as F, type ImportSummary as I, type Job as J, type ListQuery as L, type PhoneNumber as P, type RelativeNow as R, type SearchQuery as S, type Upload as U, type AggregateRow as a, type ApiError as b, type BatchOperation as c, type BatchResponse as d, type BatchRowResult as e, type BulkUpdateResponse as f, type BulkUpdateRowResult as g, type DurationParts as h, type ItemEvent as i, type ItemQuery as j, type ItemResponse as k, type JobStatus as l, type ListResponse as m, type ResumableUploadResult as n, type SearchResponse as o, type UploadStatus as p };
package/dist/types.d.ts CHANGED
@@ -1 +1 @@
1
- export { A as AggregateQuery, a as AggregateRow, b as ApiError, B as BacklexError, c as BatchOperation, d as BatchResponse, e as BatchRowResult, D as DeviceToken, F as FlagState, I as ImportSummary, g as ItemEvent, h as ItemQuery, i as ItemResponse, J as Job, j as JobStatus, L as ListQuery, k as ListResponse, P as PhoneNumber, l as ResumableUploadResult, S as SearchQuery, m as SearchResponse, U as Upload, n as UploadStatus } from './types-CbcbXGiA.js';
1
+ export { A as AggregateQuery, a as AggregateRow, b as ApiError, B as BacklexError, c as BatchOperation, d as BatchResponse, e as BatchRowResult, f as BulkUpdateResponse, g as BulkUpdateRowResult, D as DeviceToken, F as FlagState, I as ImportSummary, i as ItemEvent, j as ItemQuery, k as ItemResponse, J as Job, l as JobStatus, L as ListQuery, m as ListResponse, P as PhoneNumber, n as ResumableUploadResult, S as SearchQuery, o as SearchResponse, U as Upload, p as UploadStatus } from './types-DiJCXOy-.js';
package/dist/webhook.d.ts CHANGED
@@ -1,3 +1,16 @@
1
+ /**
2
+ * @module
3
+ *
4
+ * Receiver-side helper for verifying inbound backlex webhook signatures with
5
+ * {@link verifyWebhook}. Runs anywhere Web Crypto is available (Workers,
6
+ * Node 18+, Bun, Deno). Supports the V2 (timestamped, replay-protected) and
7
+ * legacy signature schemes.
8
+ *
9
+ * ```ts
10
+ * import { verifyWebhook } from "backlex/webhook";
11
+ * const ok = await verifyWebhook({ secret, body, signature, timestamp });
12
+ * ```
13
+ */
1
14
  interface VerifyWebhookOptions {
2
15
  /** The hook's signing secret (the `secret` you configured on the webhook). */
3
16
  secret: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "backlex",
3
- "version": "0.1.0",
4
- "description": "Typed TypeScript client for the backlex API — auth, collection CRUD, query builder, realtime, storage, jobs, flags, and offline sync. Zero dependencies. On npm as compiled ESM + types; on JSR (@backlex/backlex) as TypeScript source.",
3
+ "version": "0.1.2",
4
+ "description": "Typed TypeScript client for the backlex API — auth, collection CRUD, query builder, realtime, storage, jobs, flows, flags, and offline sync. Zero dependencies. On npm as compiled ESM + types; on JSR (@backlex/backlex) as TypeScript source.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
7
7
  "homepage": "https://backlex.com",