@rebasepro/client 0.17.3-canary.gdd23447 → 0.18.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.
Files changed (63) hide show
  1. package/README.md +4 -0
  2. package/dist/auth.d.ts +80 -0
  3. package/dist/functions.d.ts +6 -1
  4. package/dist/index.d.ts +8 -0
  5. package/dist/index.es.js +471 -94
  6. package/dist/index.es.js.map +1 -1
  7. package/dist/offline-connectivity.d.ts +12 -1
  8. package/dist/offline.d.ts +23 -1
  9. package/dist/query-contract.types.d.ts +30 -0
  10. package/dist/realtime-channel.d.ts +29 -1
  11. package/dist/sdk_query_builder.d.ts +21 -2
  12. package/dist/transport.d.ts +24 -0
  13. package/package.json +28 -15
  14. package/src/admin.ts +0 -90
  15. package/src/anonymous-client-guard.test.ts +0 -190
  16. package/src/api-keys.ts +0 -87
  17. package/src/auth-listener-errors.test.ts +0 -57
  18. package/src/auth-refresh-overflow.test.ts +0 -89
  19. package/src/auth.ts +0 -982
  20. package/src/backups.ts +0 -40
  21. package/src/client-close.test.ts +0 -80
  22. package/src/collection-listen-meta.test.ts +0 -105
  23. package/src/collection-observe.test.ts +0 -138
  24. package/src/collection.test.ts +0 -293
  25. package/src/collection.ts +0 -525
  26. package/src/cron.test.ts +0 -164
  27. package/src/cron.ts +0 -62
  28. package/src/data-proxy.test.ts +0 -183
  29. package/src/errors.ts +0 -9
  30. package/src/functions.ts +0 -82
  31. package/src/index.ts +0 -639
  32. package/src/like-pattern-redos.test.ts +0 -61
  33. package/src/offline-codec.ts +0 -79
  34. package/src/offline-connectivity.test.ts +0 -191
  35. package/src/offline-connectivity.ts +0 -255
  36. package/src/offline-idb-store.test.ts +0 -340
  37. package/src/offline-integration.test.ts +0 -180
  38. package/src/offline-query.test.ts +0 -431
  39. package/src/offline-query.ts +0 -529
  40. package/src/offline-store.ts +0 -357
  41. package/src/offline-sync-engine.test.ts +0 -857
  42. package/src/offline.test.ts +0 -897
  43. package/src/offline.ts +0 -1928
  44. package/src/query-contract.types.ts +0 -206
  45. package/src/query_builder.ts +0 -1
  46. package/src/realtime-channel.test.ts +0 -542
  47. package/src/realtime-channel.ts +0 -539
  48. package/src/realtime-concurrent-subscribe.test.ts +0 -102
  49. package/src/realtime-error-surfacing.test.ts +0 -105
  50. package/src/realtime-optout.test.ts +0 -279
  51. package/src/realtime-row-identity.test.ts +0 -254
  52. package/src/realtime-subscription-key.test.ts +0 -92
  53. package/src/reviver.ts +0 -39
  54. package/src/sdk_query_builder.ts +0 -206
  55. package/src/storage-key-encoding.test.ts +0 -65
  56. package/src/storage-registry.ts +0 -102
  57. package/src/storage.ts +0 -253
  58. package/src/transport-baseurl.test.ts +0 -101
  59. package/src/transport.ts +0 -505
  60. package/src/vector-search-listen.test.ts +0 -42
  61. package/src/vector-search-query.test.ts +0 -55
  62. package/src/websocket-url.test.ts +0 -97
  63. package/src/websocket.ts +0 -1837
package/src/storage.ts DELETED
@@ -1,253 +0,0 @@
1
- import { StorageSource, UploadFileProps, UploadFileResult, DownloadConfig, StorageListResult, DownloadMetadata, PUBLIC_STORAGE_PREFIX, isPublicStoragePath } from "@rebasepro/types";
2
- import { Transport } from "./transport";
3
-
4
- /**
5
- * Create a StorageSource that talks to the Rebase backend REST API.
6
- *
7
- * @param transport - HTTP transport instance
8
- * @param storageId - Optional storage-source key for multi-backend routing.
9
- * When set, it is forwarded to the server so the correct
10
- * `StorageController` is resolved from the registry.
11
- */
12
- /**
13
- * A storage key, encoded for a URL path.
14
- *
15
- * Every call below interpolated the key raw, and the server decodes what it
16
- * receives — so three ordinary filenames did three different wrong things:
17
- *
18
- * `Invoice #12.pdf` the `#` began the fragment, the server saw
19
- * `default/Invoice ` and looked up a key that is not the
20
- * file — and a scoped `?token=` after it was swallowed too
21
- * `100% done.png` `decodeURIComponent` threw URIError → 500
22
- * `a%2Fb.png` decoded to `a/b.png`, silently resolving a DIFFERENT
23
- * object
24
- *
25
- * Per SEGMENT, because `/` is the key's own separator and must survive; every
26
- * other character is encoded, including a literal `%`. Verified to round-trip
27
- * through the server's decode for all of the above, plus `+`, accented
28
- * characters and a trailing `%`.
29
- */
30
- export function encodeStorageKey(key: string): string {
31
- return key.split("/").map(encodeURIComponent).join("/");
32
- }
33
-
34
- export function createStorage(transport: Transport, storageId?: string): StorageSource {
35
- const urlsCache = new Map<string, { config: DownloadConfig; expiresAt?: number }>();
36
-
37
- /**
38
- * Base for URLs the *browser* will fetch on its own (file downloads,
39
- * previews). API requests keep going to `baseUrl`; see
40
- * {@link RebaseClientConfig.storageUrlOrigin} for why these can differ.
41
- */
42
- const fileUrlBase = (): string =>
43
- `${transport.storageUrlOrigin ?? transport.baseUrl}${transport.apiPath}`;
44
-
45
- /** Append ?storageId=... to a path when multi-backend routing is active. */
46
- const withStorageId = (path: string): string => {
47
- if (!storageId) return path;
48
- const sep = path.includes("?") ? "&" : "?";
49
- return `${path}${sep}storageId=${encodeURIComponent(storageId)}`;
50
- };
51
-
52
- async function putObject({
53
- file,
54
- key,
55
- metadata,
56
- bucket,
57
- public: isPublic
58
- }: UploadFileProps): Promise<UploadFileResult> {
59
- const formData = new FormData();
60
- formData.append("file", file);
61
-
62
- // Public objects live under the public prefix so they can be served
63
- // token-less via a stable, permanent URL. Normalize the key here so the
64
- // stored path is self-describing (no server round-trip needed to know
65
- // it's public).
66
- let effectiveKey = key;
67
- if (isPublic && effectiveKey && !isPublicStoragePath(effectiveKey)) {
68
- effectiveKey = `${PUBLIC_STORAGE_PREFIX}${effectiveKey.replace(/^\/+/, "")}`;
69
- }
70
-
71
- if (effectiveKey) formData.append("key", effectiveKey);
72
- if (bucket) formData.append("bucket", bucket);
73
- if (storageId) formData.append("storageId", storageId);
74
-
75
- if (metadata) {
76
- for (const [key, value] of Object.entries(metadata)) {
77
- if (value !== undefined && value !== null) {
78
- formData.append(
79
- `metadata_${key}`,
80
- typeof value === "string" ? value : JSON.stringify(value)
81
- );
82
- }
83
- }
84
- }
85
-
86
- const result = await transport.request<{ data: UploadFileResult }>(withStorageId("/storage/upload"), {
87
- method: "POST",
88
- body: formData,
89
- headers: {}
90
- });
91
-
92
- return result.data;
93
- }
94
-
95
- async function getSignedUrl(
96
- keyOrUrl: string,
97
- bucket?: string
98
- ): Promise<DownloadConfig> {
99
- const cacheKey = bucket ? `${bucket}/${keyOrUrl}` : keyOrUrl;
100
- const cachedEntry = urlsCache.get(cacheKey);
101
- if (cachedEntry) {
102
- if (!cachedEntry.expiresAt || cachedEntry.expiresAt > Date.now()) {
103
- return cachedEntry.config;
104
- }
105
- urlsCache.delete(cacheKey);
106
- }
107
-
108
- let filePath = keyOrUrl;
109
-
110
- if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://") || filePath.startsWith("gs://"))) {
111
- filePath = filePath.substring(filePath.indexOf("://") + 3);
112
- }
113
-
114
- if (bucket && filePath && !filePath.startsWith(bucket)) {
115
- filePath = `${bucket}/${filePath}`;
116
- }
117
-
118
- if (!filePath || filePath.trim() === "" || filePath === "/") {
119
- return { url: null, fileNotFound: true };
120
- }
121
-
122
- // ── Public objects ────────────────────────────────────────────────
123
- // A public file (under the public prefix) is served token-less via a
124
- // stable, permanent, CDN-cacheable URL. No metadata round-trip and no
125
- // token are needed — build the URL directly and cache it forever.
126
- if (isPublicStoragePath(filePath)) {
127
- const publicConfig: DownloadConfig = {
128
- url: withStorageId(`${fileUrlBase()}/storage/file/${encodeStorageKey(filePath)}`)
129
- };
130
- urlsCache.set(cacheKey, { config: publicConfig }); // no expiry
131
- return publicConfig;
132
- }
133
-
134
- try {
135
- const result = await transport.request<{ data: DownloadMetadata }>(withStorageId(`/storage/metadata/${encodeStorageKey(filePath)}`));
136
-
137
- // Public object (server-confirmed): token-less permanent URL.
138
- if (result.data.public) {
139
- const publicConfig: DownloadConfig = {
140
- url: withStorageId(`${fileUrlBase()}/storage/file/${encodeStorageKey(filePath)}`),
141
- metadata: result.data
142
- };
143
- urlsCache.set(cacheKey, { config: publicConfig }); // no expiry
144
- return publicConfig;
145
- }
146
-
147
- // Private object: use the short-lived, file-scoped download token
148
- // minted by the server. We deliberately do NOT fall back to the
149
- // caller's access token — a URL must never carry a full-privilege
150
- // credential. If no scoped token is present the URL fails closed.
151
- const scopedToken = result.data.token;
152
- const tokenQuery = scopedToken ? `?token=${scopedToken}` : "";
153
-
154
- const downloadConfig: DownloadConfig = {
155
- // `withStorageId` picks `?` or `&` based on whether the token
156
- // query is already present, so the URL stays valid even when
157
- // there is no token.
158
- url: withStorageId(`${fileUrlBase()}/storage/file/${encodeStorageKey(filePath)}${tokenQuery}`),
159
- metadata: result.data
160
- };
161
-
162
- const expiresAt = result.data.tokenExpiresIn
163
- ? Date.now() + (result.data.tokenExpiresIn - 10) * 1000 // subtract 10s buffer
164
- : undefined;
165
-
166
- urlsCache.set(cacheKey, { config: downloadConfig, expiresAt });
167
- return downloadConfig;
168
- } catch (e: unknown) {
169
- if (e instanceof Error && "status" in e && (e as { status: number }).status === 404) {
170
- return { url: null, fileNotFound: true };
171
- }
172
- throw e;
173
- }
174
- }
175
-
176
- async function getObject(
177
- key: string,
178
- bucket?: string
179
- ): Promise<File | null> {
180
- const downloadConfig = await getSignedUrl(key, bucket);
181
- if (downloadConfig.fileNotFound || !downloadConfig.url) {
182
- return null;
183
- }
184
-
185
- // Fetch using the signed URL directly. Since the scoped token is in the ?token= query param,
186
- // we explicitly omit any Authorization headers to prevent passing full access tokens to file serving routes.
187
- const response = await transport.fetchFn(downloadConfig.url, {
188
- headers: {}
189
- });
190
-
191
- if (response.status === 404) return null;
192
- if (!response.ok) throw new Error("Failed to get file");
193
-
194
- const blob = await response.blob();
195
- const fileName = (bucket ? `${bucket}/${key}` : key).split("/").pop() || "file";
196
- return new File([blob], fileName, { type: blob.type });
197
- }
198
-
199
- async function deleteObject(
200
- key: string,
201
- bucket?: string
202
- ): Promise<void> {
203
- let filePath = key;
204
-
205
- if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://") || filePath.startsWith("gs://"))) {
206
- filePath = filePath.substring(filePath.indexOf("://") + 3);
207
- }
208
-
209
- if (bucket && filePath && !filePath.startsWith(bucket)) {
210
- filePath = `${bucket}/${filePath}`;
211
- }
212
-
213
- if (!filePath || filePath.trim() === "" || filePath === "/") {
214
- return;
215
- }
216
-
217
- try {
218
- await transport.request(withStorageId(`/storage/file/${encodeStorageKey(filePath)}`), { method: "DELETE" });
219
- } catch (e: unknown) {
220
- if (!(e instanceof Error && "status" in e && (e as { status: number }).status === 404)) throw e;
221
- }
222
-
223
- urlsCache.delete(bucket ? `${bucket}/${key}` : key);
224
- }
225
-
226
- async function listObjects(
227
- prefix: string,
228
- options?: {
229
- bucket?: string;
230
- maxResults?: number;
231
- pageToken?: string;
232
- }
233
- ): Promise<StorageListResult> {
234
- const params = new URLSearchParams();
235
- if (prefix) params.set("prefix", prefix);
236
- if (options?.bucket) params.set("bucket", options.bucket);
237
- if (options?.maxResults) params.set("maxResults", String(options.maxResults));
238
- if (options?.pageToken) params.set("pageToken", options.pageToken);
239
-
240
- if (storageId) params.set("storageId", storageId);
241
-
242
- const result = await transport.request<{ data: StorageListResult }>(`/storage/list?${params.toString()}`);
243
- return result.data;
244
- }
245
-
246
- return {
247
- putObject,
248
- getSignedUrl,
249
- getObject,
250
- deleteObject,
251
- listObjects
252
- };
253
- }
@@ -1,101 +0,0 @@
1
- import { describe, it, expect, afterEach, jest } from "@jest/globals";
2
- import { createTransport } from "./transport";
3
-
4
- /**
5
- * `baseUrl` is optional because the common production shape is a Rebase backend
6
- * serving its own SPA: the API is the page's origin. These pin the resolution,
7
- * because getting it wrong pushes apps into baking an absolute host — which
8
- * breaks the moment a custom domain points at the same app, and which CORS
9
- * cannot repair (a SameSite=Lax auth cookie is not sent cross-site either).
10
- */
11
- const setWindow = (origin?: string) => {
12
- if (origin === undefined) { delete (globalThis as never as { window?: unknown }).window; return; }
13
- (globalThis as never as { window: unknown }).window = { location: { origin, href: origin + "/" } };
14
- };
15
-
16
- afterEach(() => setWindow(undefined));
17
-
18
- describe("transport baseUrl resolution", () => {
19
- it("uses the page origin when unset in a browser", () => {
20
- setWindow("https://dadaki.com");
21
- expect(createTransport({}).baseUrl).toBe("https://dadaki.com");
22
- });
23
-
24
- it("gives callers something they can build an absolute URL from", () => {
25
- setWindow("https://dadaki.com");
26
- const t = createTransport({});
27
- // The empty-string version of this threw, which is what drove apps to
28
- // bake a hostname into the bundle.
29
- expect(() => new URL(`${t.baseUrl}/api/functions/doc-content/get`)).not.toThrow();
30
- });
31
-
32
- it("follows the page, so a second hostname on the same app just works", () => {
33
- setWindow("https://dadaki.rebase.website");
34
- expect(createTransport({}).baseUrl).toBe("https://dadaki.rebase.website");
35
- setWindow("https://dadaki.com");
36
- expect(createTransport({}).baseUrl).toBe("https://dadaki.com");
37
- });
38
-
39
- it("still honours an explicit baseUrl, which is what dev needs", () => {
40
- setWindow("https://dadaki.com");
41
- expect(createTransport({ baseUrl: "http://localhost:3001" }).baseUrl).toBe("http://localhost:3001");
42
- });
43
-
44
- it("strips a trailing slash so joins do not double up", () => {
45
- setWindow(undefined);
46
- expect(createTransport({ baseUrl: "https://api.example.com/" }).baseUrl).toBe("https://api.example.com");
47
- });
48
-
49
- it("stays relative off-browser, where there is no origin to borrow", () => {
50
- setWindow(undefined);
51
- expect(createTransport({}).baseUrl).toBe("");
52
- });
53
- });
54
-
55
- describe("baseUrl that already contains the apiPath", () => {
56
- /**
57
- * The docblock on `baseUrl` says this "silently builds `/api/api/…` and
58
- * every request 404s" — a failure mode understood well enough to be written
59
- * down, and still left to be discovered at runtime. This package's own
60
- * tests configured it that way a dozen times over, which is about as clear
61
- * a signal as a trap gets.
62
- */
63
- it("warns rather than silently building /api/api", () => {
64
- const warn = jest.spyOn(console, "warn").mockImplementation(() => {});
65
- createTransport({ baseUrl: "http://localhost:3000/api" });
66
-
67
- expect(warn).toHaveBeenCalledWith(expect.stringContaining("/api/api"));
68
- warn.mockRestore();
69
- });
70
-
71
- it("warns for a custom apiPath too, and tolerates a trailing slash", () => {
72
- const warn = jest.spyOn(console, "warn").mockImplementation(() => {});
73
- createTransport({ baseUrl: "https://api.example.com/v2/", apiPath: "/v2" });
74
-
75
- expect(warn).toHaveBeenCalledTimes(1);
76
- warn.mockRestore();
77
- });
78
-
79
- it("says nothing about a baseUrl that merely ends in a similar word", () => {
80
- const warn = jest.spyOn(console, "warn").mockImplementation(() => {});
81
- createTransport({ baseUrl: "https://rapid.example.com" });
82
- createTransport({ baseUrl: "https://example.com/myapi" });
83
- createTransport({ baseUrl: "https://example.com" });
84
-
85
- expect(warn).not.toHaveBeenCalled();
86
- warn.mockRestore();
87
- });
88
- });
89
-
90
- describe("storageUrlOrigin that already contains the apiPath", () => {
91
- it("warns, because storage composes it the same way baseUrl is composed", () => {
92
- const warn = jest.spyOn(console, "warn").mockImplementation(() => {});
93
- createTransport({
94
- baseUrl: "https://example.com",
95
- storageUrlOrigin: "https://files.example.com/api"
96
- });
97
-
98
- expect(warn).toHaveBeenCalledWith(expect.stringContaining("storageUrlOrigin"));
99
- warn.mockRestore();
100
- });
101
- });