@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.
@@ -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;
@@ -92,7 +92,8 @@ export interface TransportEnvironment {
92
92
  */
93
93
  export declare const ANONYMOUS_SERVER_CLIENT_WARNING: string;
94
94
  /**
95
- * Re-export from `@rebasepro/types` for backward compatibility.
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` /
@@ -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.0",
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.0",
33
- "@rebasepro/types": "0.16.0",
34
- "@rebasepro/utils": "0.16.0"
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
- export interface AuthConfig {
34
- needsSetup: boolean;
35
- registrationEnabled: boolean;
36
- emailServiceEnabled?: boolean;
37
- passwordReset?: boolean;
38
- emailVerification?: boolean;
39
- magicLink?: boolean;
40
- enabledProviders: string[];
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,
@@ -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
- * Still `PUT`, deliberately, even though the server now serves `PATCH`
186
- * on the same handler and `PATCH` is the honest verb for a merge.
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
- * The two are interchangeable server-side, so switching buys nothing at
189
- * runtime — and it costs compatibility in the direction that fails
190
- * quietly. A 0.14 client talking to a 0.13 server would send `PATCH` to
191
- * a route that does not exist and get a **404**, which is
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: "PUT",
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 `client` you were handed instead of building a new one: its data "
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-export from `@rebasepro/types` for backward compatibility.
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