@rebasepro/types 0.13.0 → 0.13.1-canary.g06dbe5b

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,231 @@
1
+ /**
2
+ * Opt-in full-text search configuration.
3
+ *
4
+ * ## Why this is opt-in
5
+ *
6
+ * Without a `search` block, `.search()` behaves exactly as it always has: an
7
+ * `ILIKE '%term%'` OR-ed across the collection's top-level, non-enum `string`
8
+ * properties. That default is unchanged and will stay unchanged — declaring
9
+ * this block is the only way to get anything else.
10
+ *
11
+ * The default has three limits that no amount of tuning inside it can fix:
12
+ * it cannot reach inside `map` (JSONB) or `array` properties, it has no notion
13
+ * of relevance, and a leading `%` means it can never use an index. Collections
14
+ * that outgrow those limits declare what they want searched; collections that
15
+ * have not are left completely alone.
16
+ *
17
+ * ## What declaring it does
18
+ *
19
+ * One `tsvector` column, `GENERATED ALWAYS AS … STORED`, plus one GIN index on
20
+ * it. Postgres recomputes the column on every write of a source field, so it
21
+ * cannot drift from the row, and refuses any attempt to write it directly.
22
+ * `.search()` then compiles to `@@ websearch_to_tsquery(…)` against that
23
+ * column, which stems, drops stopwords, AND-es the terms, and ranks.
24
+ *
25
+ * These are stated consequences, not hidden ones: the column and the index
26
+ * appear in generated DDL, in `schema.generated.ts`, and in `rebase db push`
27
+ * output like any other declared object.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * const talents: PostgresCollectionConfig = {
32
+ * slug: "talents",
33
+ * table: "talents",
34
+ * properties: { … },
35
+ * search: {
36
+ * language: "spanish",
37
+ * unaccent: true,
38
+ * fields: [
39
+ * { path: "full_name", weight: "A" },
40
+ * "location",
41
+ * "questionnaire.certifications" // into the JSONB
42
+ * ]
43
+ * }
44
+ * };
45
+ * ```
46
+ *
47
+ * @group Search
48
+ */
49
+ export interface SearchConfig {
50
+ /**
51
+ * The fields to index, in the author's own words. Nothing is inferred: a
52
+ * field is searched if and only if it is named here.
53
+ *
54
+ * A bare string is shorthand for `{ path, weight: "B" }`.
55
+ *
56
+ * A path may address:
57
+ * - a top-level `string` property — `"full_name"`
58
+ * - a `string[]` property — `"tags"` (every element is indexed)
59
+ * - a path into a `map` property — `"questionnaire.certifications"`,
60
+ * which indexes every string found at or below that point, including
61
+ * nested objects and arrays of strings. JSON *keys* are never indexed,
62
+ * only values.
63
+ *
64
+ * A path that does not resolve to one of those is a boot-time error, not
65
+ * a silent omission — a search field you believe is live and is not is the
66
+ * failure this whole block exists to prevent.
67
+ */
68
+ fields: readonly (string | SearchField)[];
69
+ /**
70
+ * The Postgres text search configuration, which decides stemming and
71
+ * stopwords. `"spanish"` stems `auditores` to `auditor` and drops `de`;
72
+ * `"simple"` does neither.
73
+ *
74
+ * Defaults to `"simple"`, which is the only choice that is never wrong:
75
+ * a stemmer applied to the wrong language silently mangles lexemes. Set it
76
+ * to your content's language to get stemming.
77
+ *
78
+ * @default "simple"
79
+ */
80
+ language?: string;
81
+ /**
82
+ * Fold accents before indexing, so `auditoria` matches `auditoría`.
83
+ *
84
+ * This is not cosmetic in accented languages. Postgres stems the two
85
+ * spellings to *different* lexemes — `to_tsvector('spanish', 'auditoría')`
86
+ * yields `auditor` while `'auditoria'` yields `auditori` — so without this
87
+ * a query typed without accents misses the rows that carry them, which is
88
+ * most queries most users type.
89
+ *
90
+ * Requires the `unaccent` extension. Boot fails with an explicit message if
91
+ * it is not installed and cannot be created, rather than quietly indexing
92
+ * accented text as-is.
93
+ *
94
+ * @default false
95
+ */
96
+ unaccent?: boolean;
97
+ /**
98
+ * Name of the generated column holding the `tsvector`.
99
+ *
100
+ * Only change this if `search_vector` collides with a column you already
101
+ * have. It is part of your schema once created: renaming it later is a
102
+ * column drop and recreate, which rewrites the table.
103
+ *
104
+ * @default "search_vector"
105
+ */
106
+ column?: string;
107
+ /**
108
+ * Also match on trigram similarity, so near-misses and typos still rank —
109
+ * `iso14000` reaching `ISO 14001`, which no amount of stemming will do
110
+ * because they are simply different lexemes.
111
+ *
112
+ * Adds a second generated `text` column and a GIN trigram index alongside
113
+ * the `tsvector`, and requires the `pg_trgm` extension. Costs write time
114
+ * and disk; buys the single most common class of failed search.
115
+ *
116
+ * Also changes what `_score` means: the trigram similarity is added to
117
+ * `ts_rank`. It has to be. A typo matches nothing on the exact path, so
118
+ * every row this finds has a `ts_rank` of zero — ranking by that alone
119
+ * would order the results arbitrarily, which is the failure `fuzzy` exists
120
+ * to fix.
121
+ *
122
+ * @default false
123
+ */
124
+ fuzzy?: boolean;
125
+ /**
126
+ * Similarity floor for {@link SearchConfig.fuzzy}, between 0 and 1. A row
127
+ * whose trigram similarity to the query falls below this never matches on
128
+ * the fuzzy path (it can still match on the exact one).
129
+ *
130
+ * Lower admits more typos and more noise. Ignored unless `fuzzy` is set.
131
+ *
132
+ * @default 0.3
133
+ */
134
+ fuzzyThreshold?: number;
135
+ }
136
+ /**
137
+ * One indexed field, with the weight it carries in the ranking.
138
+ *
139
+ * @group Search
140
+ */
141
+ export interface SearchField {
142
+ /**
143
+ * Property name, or dotted path into a `map` property.
144
+ * @see SearchConfig.fields
145
+ */
146
+ path: string;
147
+ /**
148
+ * Postgres weight class. `ts_rank` scores an `A` hit far above a `D` hit,
149
+ * which is how a name outranks a passing mention in a long description.
150
+ *
151
+ * The four classes are Postgres's own and there are exactly four.
152
+ *
153
+ * @default "B"
154
+ */
155
+ weight?: SearchWeight;
156
+ }
157
+ /**
158
+ * Postgres tsvector weight classes, strongest to weakest.
159
+ *
160
+ * @group Search
161
+ */
162
+ export type SearchWeight = "A" | "B" | "C" | "D";
163
+ /** The column name used when {@link SearchConfig.column} is not given. */
164
+ export declare const DEFAULT_SEARCH_COLUMN = "search_vector";
165
+ /** The text search configuration used when {@link SearchConfig.language} is not given. */
166
+ export declare const DEFAULT_SEARCH_LANGUAGE = "simple";
167
+ /** The weight a field carries when it does not name one. */
168
+ export declare const DEFAULT_SEARCH_WEIGHT: SearchWeight;
169
+ /** The similarity floor used when {@link SearchConfig.fuzzyThreshold} is not given. */
170
+ export declare const DEFAULT_FUZZY_THRESHOLD = 0.3;
171
+ /**
172
+ * Sort keys a query computes rather than reads from a column.
173
+ *
174
+ * `orderBy` is otherwise typed against the row — `keyof M` — which is exactly
175
+ * right for a column and exactly wrong for relevance: `_score` is produced by
176
+ * the query, so it appears in no generated row type and a project with a
177
+ * generated SDK could not name it. The runtime accepted it, the docs told
178
+ * people to use it, and the types rejected it.
179
+ *
180
+ * Kept as a named union rather than a loose `string` so the other half of the
181
+ * guarantee survives: a typo'd column is still a compile error, and remains a
182
+ * 400 at runtime rather than a silently unsorted list.
183
+ *
184
+ * `_distance` is deliberately not here. A vector search orders by distance on
185
+ * its own and overrides `orderBy` outright, so naming it would imply a choice
186
+ * the caller does not have.
187
+ *
188
+ * @group Search
189
+ */
190
+ export type ComputedSortField = typeof RELEVANCE_SORT_FIELD;
191
+ /**
192
+ * The relevance sort key. Valid only on a collection that declares a
193
+ * {@link SearchConfig} *and* on a query that carries a search string; anywhere
194
+ * else it is an unknown field and the request is refused.
195
+ */
196
+ export declare const RELEVANCE_SORT_FIELD = "_score";
197
+ /**
198
+ * One field that matched, and the text around the hit.
199
+ *
200
+ * Returned per row as `_matches` when a query asks for it — see the `explain`
201
+ * option on `.search()`. Answers the question a ranked list otherwise leaves
202
+ * open: *why is this row here?* A candidate surfacing for "iso 14001" because
203
+ * of a certification is a different result from one surfacing because the
204
+ * string appears in a paragraph about something else, and the score alone
205
+ * cannot tell them apart.
206
+ *
207
+ * @group Search
208
+ */
209
+ export interface SearchMatch {
210
+ /**
211
+ * The declared field path that matched, exactly as written in
212
+ * {@link SearchConfig.fields} — e.g. `"questionnaire.certifications"`.
213
+ * Map it to a label for display; the path is stable, a label is yours.
214
+ */
215
+ field: string;
216
+ /**
217
+ * The matching text, with each hit wrapped in `<mark>…</mark>`.
218
+ *
219
+ * Built by Postgres's `ts_headline` over the same normalized text that was
220
+ * indexed. With {@link SearchConfig.unaccent} on that means the snippet
221
+ * reads with accents folded — `Auditoria` rather than `Auditoría`. That is
222
+ * deliberate: `ts_headline` over the *original* text cannot find a hit the
223
+ * unaccented query produced, so it returns the text with nothing marked at
224
+ * all. A readable snippet that highlights beats a prettier one that
225
+ * silently does not.
226
+ *
227
+ * Contains markup by construction. Render it as HTML or strip the tags —
228
+ * do not display it raw, and do not trust it as plain text.
229
+ */
230
+ snippet: string;
231
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/types",
3
3
  "type": "module",
4
- "version": "0.13.0",
4
+ "version": "0.13.1-canary.g06dbe5b",
5
5
  "description": "Rebase type definitions — shared interfaces and controller types",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/rebaseco"
@@ -42,7 +42,7 @@
42
42
  "@types/node": "^26.1.2",
43
43
  "@types/object-hash": "^3.0.6",
44
44
  "@types/react-measure": "^2.0.12",
45
- "hono": "^4.12.27",
45
+ "hono": "^4.13.0",
46
46
  "jest": "^30.4.2",
47
47
  "ts-jest": "^29.4.12",
48
48
  "typescript": "^6.0.3",
@@ -1,3 +1,4 @@
1
+ import type { DataDriver } from "./controllers/data_driver";
1
2
  import type { StorageSource } from "./controllers/storage";
2
3
  import type { RebaseClient } from "./controllers/client";
3
4
  import type { RebaseSdkData } from "./controllers/data";
@@ -21,7 +22,16 @@ export type RebaseCallContext<USER extends User = User> = {
21
22
  * The Rebase client instance.
22
23
  * Available in all entity callbacks (beforeSave, afterSave, afterRead,
23
24
  * beforeDelete, afterDelete) and in CollectionActionsProps via context.
24
- * Use it to call backend functions, access data, storage, etc.
25
+ * Use it to call backend functions, access storage, send email, etc.
26
+ *
27
+ * ⚠️ **Not the same trust level as {@link data}.** Server-side this is the
28
+ * app singleton, so `client.dataAsAdmin` is **always** the admin-scoped
29
+ * plane — scoped as `{ uid: "service", roles: ["admin"] }`, so policies are
30
+ * evaluated against that identity rather than skipped — while {@link data},
31
+ * one property over, follows whoever triggered the callback. On a user
32
+ * request, reaching for `context.client.dataAsAdmin` silently escalates a
33
+ * user-scoped operation to admin. For queries in a callback use
34
+ * {@link data}; come here for functions, storage and email.
25
35
  *
26
36
  * @example
27
37
  * // In a beforeSave callback:
@@ -38,12 +48,66 @@ export type RebaseCallContext<USER extends User = User> = {
38
48
  * Unified data access — `context.data.products.create(...)`.
39
49
  * Access any collection as a dynamic property.
40
50
  *
41
- * Returns flat rows (`{ id, ...columns }`), identical to the frontend SDK
42
- * client — so `context.data` in a backend callback and `client.data` in the
43
- * frontend behave the same way (`row.title`, never `row.values.title`).
51
+ * **Inherits the privilege of whatever triggered the callback.** This is not
52
+ * a fixed trust level, and it is the one thing to know about this accessor:
53
+ *
54
+ * - Triggered by a **user request** (REST, realtime, an admin-panel edit):
55
+ * user-scoped. The callback runs on the RLS-bound transaction opened for
56
+ * that request, so policies apply to reads *and* writes — a callback
57
+ * cannot see a row its caller could not.
58
+ * - Triggered by **`rebase.dataAsAdmin` or a cron** (the same singleton):
59
+ * admin-scoped, not unscoped. That driver is scoped as
60
+ * `{ uid: "service", roles: ["admin"] }`, so the callback still runs on an
61
+ * RLS-bound transaction — policies are evaluated against that identity.
62
+ * - Triggered by **the base driver** (auth flows, migrations): unscoped, on
63
+ * the owner connection, bypassing RLS.
64
+ *
65
+ * So a callback that reads a sibling row will find it when an admin task
66
+ * saves and may find nothing when an end user saves — without an error,
67
+ * because RLS filters rather than raises. Write callbacks that tolerate
68
+ * that, or reach for {@link client}`.dataAsAdmin` deliberately when the
69
+ * callback genuinely has to see what an admin may see. Note what that does
70
+ * *not* buy you: `policy.serverContext()` (`auth.uid() IS NULL`) is false
71
+ * for the service identity, so a collection whose only rule is
72
+ * `serverContext()` stays closed to it.
73
+ *
74
+ * Verified end-to-end against Postgres rather than asserted — see
75
+ * `"scopes context.data to the caller when a callback runs on a user
76
+ * request"` in `server-postgres`' `rls-enforcement` e2e suite. The
77
+ * documentation previously claimed the opposite (that callbacks always have
78
+ * full access), which is the unsafe direction to be wrong in.
79
+ *
80
+ * Returns flat rows (`{ id, ...columns }`), identical in *shape* to the
81
+ * frontend SDK client — so `context.data` in a backend callback and
82
+ * `client.data` in the frontend are accessed the same way (`row.title`,
83
+ * never `row.values.title`). Shape only: privilege differs as above.
44
84
  */
45
85
  data: RebaseSdkData;
46
86
 
87
+ /**
88
+ * The driver executing the operation this callback is attached to.
89
+ *
90
+ * Present server-side only. Declared here because it is already public in
91
+ * practice — the backend has always passed it, and the callbacks guide
92
+ * documented `context.driver.withAuth(user)` in all six locales. The
93
+ * contract simply did not name it, so `buildCallContext` was cast through
94
+ * `as unknown as RebaseCallContext` and nothing about the object was
95
+ * type-checked at all.
96
+ *
97
+ * The guide no longer recommends `withAuth` — {@link data} is already
98
+ * user-scoped on a user request, so the manual re-scoping it described was
99
+ * answering a problem that did not exist. The field stays declared rather
100
+ * than removed: it is on the runtime object, dropping it would break anyone
101
+ * who found it, and a named optional is better than a silent extra.
102
+ *
103
+ * `withAuth` is not on {@link DataDriver} because not every engine supports
104
+ * RLS scoping; it is narrowed here, and left optional so a driver without it
105
+ * is a compile-time absence rather than a runtime surprise.
106
+ */
107
+ driver?: DataDriver & {
108
+ withAuth?(user: { uid: string; roles?: string[] }): Promise<DataDriver>;
109
+ };
110
+
47
111
  /**
48
112
  * Used storage implementation
49
113
  */
@@ -267,18 +267,31 @@ export interface RebaseClient<DB = unknown> {
267
267
  data: RebaseSdkData<DB>;
268
268
 
269
269
  /**
270
- * Admin-scoped, **RLS-bypassing** data accessor.
270
+ * Admin-scoped data accessor — **not** an RLS bypass.
271
271
  *
272
272
  * Present on the **server** singleton only (see {@link RebaseServerClient}).
273
- * It runs with `{ uid: "service", roles: ["admin"] }` — every read and write
274
- * bypasses row-level-security policies. This is the correct tool for trusted
275
- * background work (cron jobs, migrations, service-to-service tasks).
273
+ * It runs as the service identity `{ uid: "service", roles: ["admin"] }`,
274
+ * and the driver is scoped with `withAuth()` at boot, so every read and
275
+ * write runs in a transaction that has switched to the restricted
276
+ * `rebase_user` role with `app.uid = 'service'`: policies are evaluated,
277
+ * against that identity. This is the correct tool for trusted background
278
+ * work (cron jobs, migrations, service-to-service tasks).
279
+ *
280
+ * Two consequences the name does not suggest:
281
+ *
282
+ * - `policy.serverContext()` compiles to `auth.uid() IS NULL` and is
283
+ * therefore **false** here. A collection with `disableDefaultPolicies:
284
+ * true` whose only rule is `serverContext()` refuses these writes
285
+ * (`42501`) and returns zero rows — HTTP 200, empty — for these reads.
286
+ * - Its reach equals an `admin`-roled application user's reach. It is not a
287
+ * private channel. The true bypass is {@link sql}, which runs on the
288
+ * owner connection and never goes through `withAuth`.
276
289
  *
277
290
  * ⚠️ **Do NOT use it to serve user-facing data.** Inside a request handler,
278
291
  * user-scoped queries must go through the request-scoped driver
279
- * (`c.var.driver`), which carries the caller's identity so RLS applies.
280
- * Reaching for `dataAsAdmin` (or its alias {@link data}) in a request handler
281
- * silently exposes every row to every caller.
292
+ * (`c.var.driver`), which carries the caller's identity. Reaching for
293
+ * `dataAsAdmin` (or its alias {@link data}) in a request handler serves
294
+ * every caller whatever an admin may see.
282
295
  *
283
296
  * Undefined in the browser SDK.
284
297
  */
@@ -362,7 +375,22 @@ export interface RebaseClient<DB = unknown> {
362
375
  /** Resolve the current auth token */
363
376
  resolveToken?(): Promise<string | null>;
364
377
 
365
- /** Make a raw HTTP call to the backend */
378
+ /**
379
+ * POST to an arbitrary path on the backend — the escape hatch, not the way
380
+ * to call a function.
381
+ *
382
+ * For a custom function use {@link functions}`.invoke(name, payload)`: it
383
+ * targets `/functions/<name>`, takes a method and sub-path, and returns the
384
+ * response body as sent. This posts wherever you point it and **unwraps**:
385
+ * it returns `res.data` when the response has a `data` property and the
386
+ * whole envelope otherwise — so an endpoint that legitimately answers
387
+ * `{ data: null }` hands back the envelope rather than `null`. Two ways to
388
+ * reach a function with two different response contracts is a trap; this is
389
+ * the one that exists for paths `invoke` cannot express.
390
+ *
391
+ * @internal Prefer `functions.invoke()`. Kept public because a backend can
392
+ * mount routes outside `/functions`, and nothing else reaches those.
393
+ */
366
394
  call?<T = unknown>(endpoint: string, payload?: unknown): Promise<T>;
367
395
 
368
396
  /**
@@ -382,17 +410,20 @@ export interface RebaseClient<DB = unknown> {
382
410
  * the admin-scoped {@link dataAsAdmin} accessor, raw {@link sql}, and the
383
411
  * {@link email} service are all present (non-optional).
384
412
  *
385
- * **Trust levels.** {@link dataAsAdmin} is the admin-scoped, **RLS-bypassing**
386
- * driver, and it is the only name for it here — the `data` alias that used to
387
- * sit beside it is deliberately `Omit`ted from {@link RebaseClient} so the
388
- * privilege has to be spelled out at every call site. For user-scoped queries
389
- * inside a request handler use the request-scoped driver (`c.var.driver`)
390
- * instead — never `dataAsAdmin`.
413
+ * **Trust levels.** {@link dataAsAdmin} is the admin-scoped driver — scoped as
414
+ * `{ uid: "service", roles: ["admin"] }`, so policies are still evaluated
415
+ * against that identity rather than skipped — and it is the only name for it
416
+ * here: the `data` alias that used to sit beside it is deliberately `Omit`ted
417
+ * from {@link RebaseClient} so the privilege has to be spelled out at every
418
+ * call site. {@link sql} is the unconditional bypass: raw SQL on the owner
419
+ * connection, no policies. For user-scoped queries inside a request handler use
420
+ * the request-scoped driver (`c.var.driver`) instead — never `dataAsAdmin`.
391
421
  */
392
422
  export interface RebaseServerClient<DB = unknown> extends Omit<RebaseClient<DB>, "data"> {
393
423
  /**
394
- * Admin-scoped, **RLS-bypassing** data accessor. Always present server-side.
395
- * See {@link RebaseClient.dataAsAdmin} for the full safety contract.
424
+ * Admin-scoped data accessor (RLS is evaluated as the service identity, not
425
+ * skipped). Always present server-side. See {@link RebaseClient.dataAsAdmin}
426
+ * for the full safety contract.
396
427
  */
397
428
  dataAsAdmin: RebaseSdkData<DB>;
398
429
 
@@ -410,85 +441,6 @@ export interface RebaseServerClient<DB = unknown> extends Omit<RebaseClient<DB>,
410
441
  sql(query: string, options?: { database?: string; role?: string; params?: unknown[] }): Promise<Record<string, unknown>[]>;
411
442
  }
412
443
 
413
- // ─── RebaseBrowserClient ─────────────────────────────────────────────────────
414
-
415
- /**
416
- * The browser-side Rebase surface — the shape produced by
417
- * `createRebaseClient()` in `@rebasepro/client`.
418
- *
419
- * Its {@link data} accessor is **user-scoped**: every call carries the signed-in
420
- * user's token, so backend RLS policies apply. It deliberately omits the
421
- * server-only members — there is no `sql`, no `email`, and no
422
- * `dataAsAdmin`, so the RLS-bypassing accessor can never be reached from
423
- * browser code.
424
- */
425
- export interface RebaseBrowserClient<DB = unknown> {
426
- /** User-scoped data access layer (carries the signed-in user's token). */
427
- data: RebaseSdkData<DB>;
428
-
429
- /** Unified Authentication layer */
430
- auth: AuthClient;
431
-
432
- /** Unified Storage layer (default storage source, backward-compatible) */
433
- storage?: StorageSource;
434
-
435
- /** Registry of all named storage sources for multi-backend support */
436
- storageRegistry?: StorageSourceRegistry;
437
-
438
- /** Build a server-backed {@link StorageSource} for a named storage source. */
439
- createStorageSource?(storageId: string): StorageSource;
440
-
441
- /** Discover the storage sources declared on the backend. */
442
- fetchStorageSources?(): Promise<StorageSourceDefinition[]>;
443
-
444
- /** Admin API for user management */
445
- admin?: AdminAPI;
446
-
447
- /** Cron job management API */
448
- cron?: CronAPI;
449
-
450
- /** Database backup management API */
451
- backups?: BackupsAPI;
452
-
453
- /** Custom backend functions API */
454
- functions?: FunctionsAPI;
455
-
456
- /** Service API keys management API */
457
- apiKeys?: ApiKeysAPI;
458
-
459
- /** Base HTTP URL of the backend server */
460
- baseUrl?: string;
461
-
462
- /**
463
- * The path every API route is mounted under, appended to {@link baseUrl}.
464
- *
465
- * `"/api"` unless the backend was configured with a different `basePath`
466
- * and the client told to match. Exposed because code that builds a URL by
467
- * hand — rather than going through the client's own methods — otherwise has
468
- * to guess, and guessing `/api` is wrong for exactly the projects that set
469
- * the option.
470
- */
471
- apiPath?: string;
472
-
473
- /** WebSocket client for realtime subscriptions */
474
- ws?: RebaseWebSocket;
475
-
476
- /** Set the auth token for subsequent requests */
477
- setToken?(token: string | null): void;
478
-
479
- /** Set a function that lazily resolves the auth token */
480
- setAuthTokenGetter?(getter: () => Promise<string | null>): void;
481
-
482
- /** Set handler called when a request returns 401 */
483
- setOnUnauthorized?(handler: () => Promise<boolean>): void;
484
-
485
- /** Resolve the current auth token */
486
- resolveToken?(): Promise<string | null>;
487
-
488
- /** Make a raw HTTP call to the backend */
489
- call?<T = unknown>(endpoint: string, payload?: unknown): Promise<T>;
490
- }
491
-
492
444
  /**
493
445
  * Client-side registry for managing multiple storage sources.
494
446
  *