@rebasepro/client 0.16.0 → 0.16.1-canary.g0d7af95
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/admin.d.ts +1 -1
- package/dist/api-keys.d.ts +1 -1
- package/dist/auth.d.ts +22 -12
- package/dist/backups.d.ts +1 -1
- package/dist/collection.d.ts +2 -2
- package/dist/cron.d.ts +1 -1
- package/dist/functions.d.ts +1 -1
- package/dist/index.d.ts +32 -32
- package/dist/index.es.js +81 -32
- package/dist/index.es.js.map +1 -1
- package/dist/offline-query.d.ts +1 -1
- package/dist/offline.d.ts +3 -3
- package/dist/storage-registry.d.ts +1 -1
- package/dist/storage.d.ts +20 -1
- package/dist/transport.d.ts +2 -1
- package/dist/websocket.d.ts +0 -1
- package/package.json +5 -5
- package/src/auth.ts +54 -10
- package/src/client-close.test.ts +1 -1
- package/src/collection.ts +17 -14
- package/src/storage-key-encoding.test.ts +65 -0
- package/src/storage.ts +27 -5
- package/src/transport.ts +3 -2
- package/src/vector-search-listen.test.ts +42 -0
- package/src/websocket.ts +0 -22
package/dist/offline-query.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { FilterValues, FindResult, LogicalCondition, FilterCondition, OrderBySpec, WhereFilterOp } from "@rebasepro/types";
|
|
2
|
-
import { FindParams } from "./transport";
|
|
2
|
+
import { FindParams } from "./transport.js";
|
|
3
3
|
/**
|
|
4
4
|
* The server's page size when the caller does not ask for one.
|
|
5
5
|
*
|
package/dist/offline.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SDKCollectionClient } from "@rebasepro/types";
|
|
2
|
-
import { CollectionClient } from "./collection";
|
|
3
|
-
import { OfflineStore, PendingMutation } from "./offline-store";
|
|
2
|
+
import { CollectionClient } from "./collection.js";
|
|
3
|
+
import { OfflineStore, PendingMutation } from "./offline-store.js";
|
|
4
4
|
/**
|
|
5
5
|
* The SDK's local-first sync engine.
|
|
6
6
|
*
|
|
@@ -95,7 +95,7 @@ export interface OfflineStatus {
|
|
|
95
95
|
/** The last replay rejection, if any. */
|
|
96
96
|
lastError?: string;
|
|
97
97
|
}
|
|
98
|
-
export type { LiveResult, ObserveOptions, RowSnapshotMeta } from "./collection";
|
|
98
|
+
export type { LiveResult, ObserveOptions, RowSnapshotMeta } from "./collection.js";
|
|
99
99
|
/** What `client.offline` exposes to the app. */
|
|
100
100
|
export interface OfflineApi {
|
|
101
101
|
/** Replay the queue now. Resolves with what was flushed and what remains. */
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* 3. Register "direct" sources manually (e.g. Firebase Storage hook)
|
|
12
12
|
*/
|
|
13
13
|
import type { StorageSource, StorageSourceRegistry, StorageSourceDefinition } from "@rebasepro/types";
|
|
14
|
-
import type { Transport } from "./transport";
|
|
14
|
+
import type { Transport } from "./transport.js";
|
|
15
15
|
/**
|
|
16
16
|
* Default implementation of the client-side `StorageSourceRegistry`.
|
|
17
17
|
*/
|
package/dist/storage.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { StorageSource } from "@rebasepro/types";
|
|
2
|
-
import { Transport } from "./transport";
|
|
2
|
+
import { Transport } from "./transport.js";
|
|
3
3
|
/**
|
|
4
4
|
* Create a StorageSource that talks to the Rebase backend REST API.
|
|
5
5
|
*
|
|
@@ -8,4 +8,23 @@ import { Transport } from "./transport";
|
|
|
8
8
|
* When set, it is forwarded to the server so the correct
|
|
9
9
|
* `StorageController` is resolved from the registry.
|
|
10
10
|
*/
|
|
11
|
+
/**
|
|
12
|
+
* A storage key, encoded for a URL path.
|
|
13
|
+
*
|
|
14
|
+
* Every call below interpolated the key raw, and the server decodes what it
|
|
15
|
+
* receives — so three ordinary filenames did three different wrong things:
|
|
16
|
+
*
|
|
17
|
+
* `Invoice #12.pdf` the `#` began the fragment, the server saw
|
|
18
|
+
* `default/Invoice ` and looked up a key that is not the
|
|
19
|
+
* file — and a scoped `?token=` after it was swallowed too
|
|
20
|
+
* `100% done.png` `decodeURIComponent` threw URIError → 500
|
|
21
|
+
* `a%2Fb.png` decoded to `a/b.png`, silently resolving a DIFFERENT
|
|
22
|
+
* object
|
|
23
|
+
*
|
|
24
|
+
* Per SEGMENT, because `/` is the key's own separator and must survive; every
|
|
25
|
+
* other character is encoded, including a literal `%`. Verified to round-trip
|
|
26
|
+
* through the server's decode for all of the above, plus `+`, accented
|
|
27
|
+
* characters and a trailing `%`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function encodeStorageKey(key: string): string;
|
|
11
30
|
export declare function createStorage(transport: Transport, storageId?: string): StorageSource;
|
package/dist/transport.d.ts
CHANGED
|
@@ -92,7 +92,8 @@ export interface TransportEnvironment {
|
|
|
92
92
|
*/
|
|
93
93
|
export declare const ANONYMOUS_SERVER_CLIENT_WARNING: string;
|
|
94
94
|
/**
|
|
95
|
-
* Re-
|
|
95
|
+
* Re-exported from `@rebasepro/types` so an SDK consumer can name the type of a
|
|
96
|
+
* call it is already making without a second dependency.
|
|
96
97
|
*
|
|
97
98
|
* Forwards the row type: without the parameter this alias flattened
|
|
98
99
|
* `FindParams<M>` back to its `Record<string, unknown>` default, and `where` /
|
package/dist/websocket.d.ts
CHANGED
|
@@ -23,7 +23,6 @@ export declare class RebaseWebSocketClient {
|
|
|
23
23
|
private websocketUrl;
|
|
24
24
|
private ws;
|
|
25
25
|
getAuthToken?: () => Promise<string | null>;
|
|
26
|
-
private subscriptions;
|
|
27
26
|
private listeners;
|
|
28
27
|
/** Channel-name → handlers, for broadcast and presence frames. */
|
|
29
28
|
private channelHandlers;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/client",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.16.
|
|
4
|
+
"version": "0.16.1-canary.g0d7af95",
|
|
5
5
|
"description": "HTTP SDK client for the Rebase custom backend",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/rebaseco"
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
"./package.json": "./package.json"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@rebasepro/common": "0.16.
|
|
33
|
-
"@rebasepro/types": "0.16.
|
|
34
|
-
"@rebasepro/utils": "0.16.
|
|
32
|
+
"@rebasepro/common": "0.16.1-canary.g0d7af95",
|
|
33
|
+
"@rebasepro/types": "0.16.1-canary.g0d7af95",
|
|
34
|
+
"@rebasepro/utils": "0.16.1-canary.g0d7af95"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@jest/globals": "^30.4.1",
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
},
|
|
79
79
|
"scripts": {
|
|
80
80
|
"watch": "vite build --watch",
|
|
81
|
-
"build": "vite build && tsc --emitDeclarationOnly -p tsconfig.prod.json && node ../../scripts/assert-build-output.mjs",
|
|
81
|
+
"build": "vite build && tsc --emitDeclarationOnly -p tsconfig.prod.json && node ../../tooling/scripts/add-dts-extensions.mjs dist && node ../../tooling/scripts/assert-build-output.mjs",
|
|
82
82
|
"test:lint": "eslint \"src/**\" --quiet",
|
|
83
83
|
"test": "jest --passWithNoTests --forceExit",
|
|
84
84
|
"clean": "rm -rf dist && find ./src -name '*.js' -type f | xargs rm -f"
|
package/src/auth.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RebaseApiError, Transport } from "./transport";
|
|
2
|
-
import type { AuthChangeEvent, RebaseSession, AuthTokens, DeviceSession, User } from "@rebasepro/types";
|
|
2
|
+
import type { AuthAdapterCapabilities, AuthChangeEvent, RebaseSession, AuthTokens, DeviceSession, User } from "@rebasepro/types";
|
|
3
3
|
|
|
4
4
|
// Re-export canonical types so `import { RebaseSession } from "@rebasepro/client"` keeps working
|
|
5
5
|
export type { RebaseSession, AuthTokens, AuthChangeEvent, DeviceSession } from "@rebasepro/types";
|
|
@@ -30,15 +30,15 @@ function mapRawUser(raw: Record<string, unknown>): User {
|
|
|
30
30
|
const EMPTY_USER: User = { uid: "", email: null, displayName: null, photoURL: null, providerId: "password", isAnonymous: false };
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
33
|
+
/**
|
|
34
|
+
* What `GET /auth/config` answers: the backend's auth capability document.
|
|
35
|
+
*
|
|
36
|
+
* Declared once, in `@rebasepro/types`, and re-exported here under the name the
|
|
37
|
+
* endpoint uses. The SDK used to carry its own near-copy of it, which listed an
|
|
38
|
+
* `emailServiceEnabled` flag no backend has ever sent and omitted half of what
|
|
39
|
+
* every backend does send.
|
|
40
|
+
*/
|
|
41
|
+
export type AuthConfig = AuthAdapterCapabilities;
|
|
42
42
|
|
|
43
43
|
export interface AuthStorage {
|
|
44
44
|
getItem: (key: string) => string | null;
|
|
@@ -746,6 +746,48 @@ accessToken: session.accessToken,
|
|
|
746
746
|
refreshToken: session.refreshToken };
|
|
747
747
|
}
|
|
748
748
|
|
|
749
|
+
/**
|
|
750
|
+
* Ask for a six-digit sign-in code by email.
|
|
751
|
+
*
|
|
752
|
+
* Answers the same thing whether or not the address has an account — do not
|
|
753
|
+
* use the result to tell a person whether they are registered, because it
|
|
754
|
+
* does not know.
|
|
755
|
+
*/
|
|
756
|
+
async function sendEmailOtp(email: string) {
|
|
757
|
+
const fetchFn = getFetch();
|
|
758
|
+
const res = await fetchFn(authUrl("/otp"), {
|
|
759
|
+
method: "POST",
|
|
760
|
+
headers: { "Content-Type": "application/json" },
|
|
761
|
+
body: JSON.stringify({ email })
|
|
762
|
+
});
|
|
763
|
+
const body = await res.json().catch(() => ({}));
|
|
764
|
+
if (!res.ok) throwApiError(res.status, body, res.statusText);
|
|
765
|
+
return body as { success: boolean; message: string; expiresInSeconds: number };
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Trade a code for a session.
|
|
770
|
+
*
|
|
771
|
+
* The address is sent with the code because the code is only valid for it:
|
|
772
|
+
* that is what keeps a six-digit guess a guess against one account rather
|
|
773
|
+
* than against every account at once.
|
|
774
|
+
*/
|
|
775
|
+
async function verifyEmailOtp(email: string, code: string) {
|
|
776
|
+
const fetchFn = getFetch();
|
|
777
|
+
const res = await fetchFn(authUrl("/otp/verify"), {
|
|
778
|
+
method: "POST",
|
|
779
|
+
headers: { "Content-Type": "application/json" },
|
|
780
|
+
body: JSON.stringify({ email, code }),
|
|
781
|
+
credentials: authFlowMode === "cookie" ? "include" : undefined
|
|
782
|
+
} as RequestInit);
|
|
783
|
+
const body = await res.json().catch(() => ({}));
|
|
784
|
+
if (!res.ok) throwApiError(res.status, body, res.statusText);
|
|
785
|
+
const session = handleAuthResponse(body, "SIGNED_IN");
|
|
786
|
+
return { user: session.user,
|
|
787
|
+
accessToken: session.accessToken,
|
|
788
|
+
refreshToken: session.refreshToken };
|
|
789
|
+
}
|
|
790
|
+
|
|
749
791
|
async function getSessions(): Promise<DeviceSession[]> {
|
|
750
792
|
const data = await transport.request<{ sessions: DeviceSession[] }>(authPath + "/sessions", { method: "GET" });
|
|
751
793
|
return data.sessions;
|
|
@@ -858,6 +900,8 @@ refreshToken: session.refreshToken };
|
|
|
858
900
|
verifyEmail,
|
|
859
901
|
sendMagicLink,
|
|
860
902
|
verifyMagicLink,
|
|
903
|
+
sendEmailOtp,
|
|
904
|
+
verifyEmailOtp,
|
|
861
905
|
getSessions,
|
|
862
906
|
revokeSession,
|
|
863
907
|
revokeAllSessions,
|
package/src/client-close.test.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { createRebaseClient } from "./index";
|
|
|
12
12
|
* an SSR request — released its socket, called `close()` exactly as documented,
|
|
13
13
|
* and still hung until the token would have expired.
|
|
14
14
|
*
|
|
15
|
-
* Found by `e2e/tests/client-sdk-e2e.ts`, which reported every check green and
|
|
15
|
+
* Found by `tests/e2e/tests/client-sdk-e2e.ts`, which reported every check green and
|
|
16
16
|
* then never exited. Pinned here rather than only there because that suite needs
|
|
17
17
|
* Docker and a real Postgres, and this costs nothing.
|
|
18
18
|
*
|
package/src/collection.ts
CHANGED
|
@@ -182,23 +182,17 @@ export function createCollectionClient<M extends Record<string, unknown> = Recor
|
|
|
182
182
|
},
|
|
183
183
|
|
|
184
184
|
/**
|
|
185
|
-
*
|
|
186
|
-
*
|
|
185
|
+
* `PATCH`, the verb for a merge — `update(id, data: Partial<M>)` sends
|
|
186
|
+
* only the keys the caller named and the server merges the rest.
|
|
187
187
|
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
* indistinguishable from "that row is gone". Every write would look like
|
|
193
|
-
* a missing record.
|
|
194
|
-
*
|
|
195
|
-
* `PATCH` is what the OpenAPI spec advertises, so anyone generating a
|
|
196
|
-
* client gets the correct verb; this stays on `PUT` until the oldest
|
|
197
|
-
* supported server is one that serves both.
|
|
188
|
+
* It was `PUT` for a while, on a route that served both so older
|
|
189
|
+
* clients kept working. The alias is gone: the OpenAPI spec, the SDK
|
|
190
|
+
* and the route now name one verb, and a spec-validating gateway in
|
|
191
|
+
* front of the API sees the operation the server actually implements.
|
|
198
192
|
*/
|
|
199
193
|
async update(id: string | number, data: Partial<M>) {
|
|
200
194
|
const raw = await transport.request<Record<string, unknown>>(`${basePath}/${encodeURIComponent(String(id))}`, {
|
|
201
|
-
method: "
|
|
195
|
+
method: "PATCH",
|
|
202
196
|
body: JSON.stringify(data)
|
|
203
197
|
});
|
|
204
198
|
return raw as M;
|
|
@@ -435,7 +429,16 @@ export function createCollectionClient<M extends Record<string, unknown> = Recor
|
|
|
435
429
|
// back in a different order from the same query fetched.
|
|
436
430
|
orderBy: normalizeOrderBy(params?.orderBy),
|
|
437
431
|
searchString: params?.searchString,
|
|
438
|
-
searchExplain: params?.searchExplain
|
|
432
|
+
searchExplain: params?.searchExplain,
|
|
433
|
+
// Forwarded so the SERVER can refuse it. `realtimeService`
|
|
434
|
+
// rejects a subscription carrying `vectorSearch` — a
|
|
435
|
+
// subscription is re-run on every matching write and
|
|
436
|
+
// nothing there computes distances — and the docs promise
|
|
437
|
+
// that refusal. Both producers hand-list their fields and
|
|
438
|
+
// both omitted this one, so the guard could not fire and
|
|
439
|
+
// `.vectorSearch(…).listen()` returned an ordinary
|
|
440
|
+
// `id DESC` listing with no `_distance` and no error.
|
|
441
|
+
vectorSearch: params?.vectorSearch
|
|
439
442
|
},
|
|
440
443
|
(incomingRows: Record<string, unknown>[]) => {
|
|
441
444
|
const currentUpdateId = ++lastUpdateId;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage keys in a URL path.
|
|
3
|
+
*
|
|
4
|
+
* Every call in `storage.ts` interpolated the key raw, and the server decodes
|
|
5
|
+
* what it receives. Three ordinary filenames did three different wrong things,
|
|
6
|
+
* all measured against a real Hono route before this was written:
|
|
7
|
+
*
|
|
8
|
+
* `Invoice #12.pdf` the `#` began the fragment, so the server saw
|
|
9
|
+
* `default/Invoice ` — a key that is not the file — and a
|
|
10
|
+
* scoped `?token=` after it was swallowed with it
|
|
11
|
+
* `100% done.png` `decodeURIComponent` threw URIError, answered 500
|
|
12
|
+
* `a%2Fb.png` decoded to `a/b.png`, silently resolving a DIFFERENT
|
|
13
|
+
* object
|
|
14
|
+
*
|
|
15
|
+
* The last is the one worth the test: no error, no log, the wrong bytes.
|
|
16
|
+
*/
|
|
17
|
+
import { encodeStorageKey } from "./storage";
|
|
18
|
+
|
|
19
|
+
describe("encodeStorageKey", () => {
|
|
20
|
+
it("leaves an ordinary key untouched", () => {
|
|
21
|
+
// The no-op property: every key that already worked still produces the
|
|
22
|
+
// same URL, so nothing cached or CDN-fronted moves.
|
|
23
|
+
expect(encodeStorageKey("default/photo.png")).toBe("default/photo.png");
|
|
24
|
+
expect(encodeStorageKey("public/a/b/c.jpg")).toBe("public/a/b/c.jpg");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("keeps the key's own separator", () => {
|
|
28
|
+
// `/` separates bucket from path and folder from file. Encoding it
|
|
29
|
+
// would make every existing key unresolvable.
|
|
30
|
+
expect(encodeStorageKey("a/b/c")).toBe("a/b/c");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it.each([
|
|
34
|
+
["default/Invoice #12.pdf", "default/Invoice%20%2312.pdf"],
|
|
35
|
+
["default/100% done.png", "default/100%25%20done.png"],
|
|
36
|
+
["default/a%2Fb.png", "default/a%252Fb.png"],
|
|
37
|
+
["default/a+b.png", "default/a%2Bb.png"],
|
|
38
|
+
["default/naïve café.png", "default/na%C3%AFve%20caf%C3%A9.png"]
|
|
39
|
+
])("encodes %s", (input, expected) => {
|
|
40
|
+
expect(encodeStorageKey(input)).toBe(expected);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("keeps a key containing an encoded slash distinct from a real one", () => {
|
|
44
|
+
// The silent one. `a%2Fb.png` is a legal object name and it is not
|
|
45
|
+
// `a/b.png`; before this they resolved to the same object.
|
|
46
|
+
expect(encodeStorageKey("default/a%2Fb.png"))
|
|
47
|
+
.not.toBe(encodeStorageKey("default/a/b.png"));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("survives a round trip through the server's decode", () => {
|
|
51
|
+
// The server runs `decodeURIComponent` over the whole path. Anything
|
|
52
|
+
// that does not come back unchanged is a key that resolves to the
|
|
53
|
+
// wrong object, or a 500.
|
|
54
|
+
for (const key of [
|
|
55
|
+
"default/photo.png",
|
|
56
|
+
"default/Invoice #12.pdf",
|
|
57
|
+
"default/100% done.png",
|
|
58
|
+
"default/a%2Fb.png",
|
|
59
|
+
"default/50%.png",
|
|
60
|
+
"default/naïve café.png"
|
|
61
|
+
]) {
|
|
62
|
+
expect(decodeURIComponent(encodeStorageKey(key))).toBe(key);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
package/src/storage.ts
CHANGED
|
@@ -9,6 +9,28 @@ import { Transport } from "./transport";
|
|
|
9
9
|
* When set, it is forwarded to the server so the correct
|
|
10
10
|
* `StorageController` is resolved from the registry.
|
|
11
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
|
+
|
|
12
34
|
export function createStorage(transport: Transport, storageId?: string): StorageSource {
|
|
13
35
|
const urlsCache = new Map<string, { config: DownloadConfig; expiresAt?: number }>();
|
|
14
36
|
|
|
@@ -103,19 +125,19 @@ export function createStorage(transport: Transport, storageId?: string): Storage
|
|
|
103
125
|
// token are needed — build the URL directly and cache it forever.
|
|
104
126
|
if (isPublicStoragePath(filePath)) {
|
|
105
127
|
const publicConfig: DownloadConfig = {
|
|
106
|
-
url: withStorageId(`${fileUrlBase()}/storage/file/${filePath}`)
|
|
128
|
+
url: withStorageId(`${fileUrlBase()}/storage/file/${encodeStorageKey(filePath)}`)
|
|
107
129
|
};
|
|
108
130
|
urlsCache.set(cacheKey, { config: publicConfig }); // no expiry
|
|
109
131
|
return publicConfig;
|
|
110
132
|
}
|
|
111
133
|
|
|
112
134
|
try {
|
|
113
|
-
const result = await transport.request<{ data: DownloadMetadata }>(withStorageId(`/storage/metadata/${filePath}`));
|
|
135
|
+
const result = await transport.request<{ data: DownloadMetadata }>(withStorageId(`/storage/metadata/${encodeStorageKey(filePath)}`));
|
|
114
136
|
|
|
115
137
|
// Public object (server-confirmed): token-less permanent URL.
|
|
116
138
|
if (result.data.public) {
|
|
117
139
|
const publicConfig: DownloadConfig = {
|
|
118
|
-
url: withStorageId(`${fileUrlBase()}/storage/file/${filePath}`),
|
|
140
|
+
url: withStorageId(`${fileUrlBase()}/storage/file/${encodeStorageKey(filePath)}`),
|
|
119
141
|
metadata: result.data
|
|
120
142
|
};
|
|
121
143
|
urlsCache.set(cacheKey, { config: publicConfig }); // no expiry
|
|
@@ -133,7 +155,7 @@ export function createStorage(transport: Transport, storageId?: string): Storage
|
|
|
133
155
|
// `withStorageId` picks `?` or `&` based on whether the token
|
|
134
156
|
// query is already present, so the URL stays valid even when
|
|
135
157
|
// there is no token.
|
|
136
|
-
url: withStorageId(`${fileUrlBase()}/storage/file/${filePath}${tokenQuery}`),
|
|
158
|
+
url: withStorageId(`${fileUrlBase()}/storage/file/${encodeStorageKey(filePath)}${tokenQuery}`),
|
|
137
159
|
metadata: result.data
|
|
138
160
|
};
|
|
139
161
|
|
|
@@ -193,7 +215,7 @@ export function createStorage(transport: Transport, storageId?: string): Storage
|
|
|
193
215
|
}
|
|
194
216
|
|
|
195
217
|
try {
|
|
196
|
-
await transport.request(withStorageId(`/storage/file/${filePath}`), { method: "DELETE" });
|
|
218
|
+
await transport.request(withStorageId(`/storage/file/${encodeStorageKey(filePath)}`), { method: "DELETE" });
|
|
197
219
|
} catch (e: unknown) {
|
|
198
220
|
if (!(e instanceof Error && "status" in e && (e as { status: number }).status === 404)) throw e;
|
|
199
221
|
}
|
package/src/transport.ts
CHANGED
|
@@ -119,12 +119,13 @@ export const ANONYMOUS_SERVER_CLIENT_WARNING =
|
|
|
119
119
|
"[rebase] This client was created outside a browser with no credential — no `token`, no auth token getter, "
|
|
120
120
|
+ "and no cookie auth flow — so every request runs as an anonymous caller. Row-level security will return only "
|
|
121
121
|
+ "publicly readable rows, which is usually nothing and occasionally the wrong thing. "
|
|
122
|
-
+ "Inside a cron or function handler, use the `
|
|
122
|
+
+ "Inside a cron or function handler, use the `rebase` you were handed instead of building a new one: its data "
|
|
123
123
|
+ "plane is already admin-scoped. In a standalone script or job, pass the service key as `token`. "
|
|
124
124
|
+ "If you really do want anonymous access, pass `anonymous: true` to silence this.";
|
|
125
125
|
|
|
126
126
|
/**
|
|
127
|
-
* Re-
|
|
127
|
+
* Re-exported from `@rebasepro/types` so an SDK consumer can name the type of a
|
|
128
|
+
* call it is already making without a second dependency.
|
|
128
129
|
*
|
|
129
130
|
* Forwards the row type: without the parameter this alias flattened
|
|
130
131
|
* `FindParams<M>` back to its `Record<string, unknown>` default, and `where` /
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `.vectorSearch(…).listen()` must reach the server's refusal.
|
|
3
|
+
*
|
|
4
|
+
* `realtimeService` rejects a subscription carrying `vectorSearch` — a
|
|
5
|
+
* subscription is re-run on every matching write and nothing there computes
|
|
6
|
+
* distances — and the documentation promises that refusal. Both producers of a
|
|
7
|
+
* subscription request hand-list their fields, and both omitted this one, so
|
|
8
|
+
* the guard could not fire: the call returned an ordinary `id DESC` listing,
|
|
9
|
+
* with no `_distance` and no error. Through `observe()` it was worse, because
|
|
10
|
+
* the correct initial snapshot was then overwritten by the wrong socket
|
|
11
|
+
* listing.
|
|
12
|
+
*
|
|
13
|
+
* Asserted on the request the client BUILDS rather than on a live socket: what
|
|
14
|
+
* was missing is a field, and the refusal it has to reach is tested where it
|
|
15
|
+
* lives.
|
|
16
|
+
*/
|
|
17
|
+
import { readFileSync } from "node:fs";
|
|
18
|
+
import { resolve } from "node:path";
|
|
19
|
+
|
|
20
|
+
const listenRequestBlock = (file: string, marker: string) => {
|
|
21
|
+
const source = readFileSync(resolve(__dirname, file), "utf-8");
|
|
22
|
+
const start = source.indexOf(marker);
|
|
23
|
+
expect(start).toBeGreaterThan(-1);
|
|
24
|
+
return source.slice(start, start + 3000);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
describe("a subscription request carries every field the server refuses on", () => {
|
|
28
|
+
it("the client's listenCollection forwards vectorSearch", () => {
|
|
29
|
+
const block = listenRequestBlock("./collection.ts", "ws.listenCollection(");
|
|
30
|
+
expect(block).toContain("vectorSearch: params?.vectorSearch");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("forwards it beside the fields it already forwarded", () => {
|
|
34
|
+
// Guards against the assertion above passing on a stray mention: the
|
|
35
|
+
// field has to be in the same object literal as the rest.
|
|
36
|
+
const block = listenRequestBlock("./collection.ts", "ws.listenCollection(");
|
|
37
|
+
const objectLiteral = block.slice(0, block.indexOf("},"));
|
|
38
|
+
for (const field of ["path:", "filter:", "orderBy:", "searchString:", "vectorSearch:"]) {
|
|
39
|
+
expect(objectLiteral).toContain(field);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
});
|
package/src/websocket.ts
CHANGED
|
@@ -86,11 +86,6 @@ export class RebaseWebSocketClient {
|
|
|
86
86
|
private websocketUrl: string;
|
|
87
87
|
private ws: WebSocket | null = null;
|
|
88
88
|
public getAuthToken?: () => Promise<string | null>;
|
|
89
|
-
private subscriptions = new Map<string, {
|
|
90
|
-
onUpdate: (data: WebSocketMessage) => void,
|
|
91
|
-
onError?: (error: Error) => void
|
|
92
|
-
}>();
|
|
93
|
-
|
|
94
89
|
private listeners = new Map<string, Set<(...args: unknown[]) => void>>();
|
|
95
90
|
|
|
96
91
|
/** Channel-name → handlers, for broadcast and presence frames. */
|
|
@@ -883,23 +878,6 @@ export class RebaseWebSocketClient {
|
|
|
883
878
|
}
|
|
884
879
|
}
|
|
885
880
|
|
|
886
|
-
// Legacy subscription handling (for backward compatibility)
|
|
887
|
-
if (subscriptionId && this.subscriptions.has(subscriptionId)) {
|
|
888
|
-
const callback = this.subscriptions.get(subscriptionId);
|
|
889
|
-
if (!callback) {
|
|
890
|
-
throw new Error(`Subscription callback not found for subscriptionId: ${subscriptionId}`);
|
|
891
|
-
}
|
|
892
|
-
if (message.type === "ERROR" || message.error) {
|
|
893
|
-
if (callback.onError) {
|
|
894
|
-
const { errorMessage, errorCode } = extractMessageError(message);
|
|
895
|
-
callback.onError(new RebaseApiError(errorMessage, { code: errorCode }));
|
|
896
|
-
}
|
|
897
|
-
} else {
|
|
898
|
-
callback.onUpdate(message);
|
|
899
|
-
}
|
|
900
|
-
return;
|
|
901
|
-
}
|
|
902
|
-
|
|
903
881
|
// An error that matched no waiter used to fall off the end of this
|
|
904
882
|
// method and disappear. Channel frames are the ones that always do:
|
|
905
883
|
// they are fire-and-forget by design, so no `pendingRequests` entry
|