@pylonsync/react 0.3.269 → 0.3.270

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/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.3.269",
6
+ "version": "0.3.270",
7
7
  "type": "module",
8
8
  "main": "src/index.ts",
9
9
  "types": "src/index.ts",
@@ -12,8 +12,8 @@
12
12
  "check": "tsc -p tsconfig.json --noEmit"
13
13
  },
14
14
  "dependencies": {
15
- "@pylonsync/sdk": "0.3.269",
16
- "@pylonsync/sync": "0.3.269"
15
+ "@pylonsync/sdk": "0.3.270",
16
+ "@pylonsync/sync": "0.3.270"
17
17
  },
18
18
  "peerDependencies": {
19
19
  "react": ">=19.0.0"
@@ -0,0 +1,44 @@
1
+ // Regression: `getBaseUrl()` is the origin every @pylonsync/client auth helper
2
+ // (createOrg, passwordRegister, createInvite, …) and the room/shard hooks fetch
3
+ // against. It used to return a static `http://localhost:4321` until an explicit
4
+ // `configureClient({ baseUrl })` — so a unified same-origin SSR app (which never
5
+ // calls configureClient) fired all auth/org calls at the dev port: broken on any
6
+ // non-4321 dev port AND in production. The fix defaults to the page origin in a
7
+ // browser. These pin that contract.
8
+
9
+ import { afterEach, describe, expect, test } from "bun:test";
10
+
11
+ import { configureClient, getBaseUrl } from "./index";
12
+
13
+ const realWindow = (globalThis as { window?: unknown }).window;
14
+ afterEach(() => {
15
+ (globalThis as { window?: unknown }).window = realWindow;
16
+ });
17
+
18
+ describe("getBaseUrl() — same-origin default for unified SSR apps", () => {
19
+ // Run order matters: these three execute before any configureClient, so the
20
+ // first two observe the un-configured resolution path.
21
+ test("SSR/node (no window) keeps the localhost dev default", () => {
22
+ (globalThis as { window?: unknown }).window = undefined;
23
+ expect(getBaseUrl()).toBe("http://localhost:4321");
24
+ });
25
+
26
+ test("browser, unconfigured → the page origin, not localhost:4321", () => {
27
+ (globalThis as { window?: unknown }).window = {
28
+ location: { origin: "https://app.example.com" },
29
+ };
30
+ expect(getBaseUrl()).toBe("https://app.example.com");
31
+ });
32
+
33
+ test("an explicit configureClient({ baseUrl }) wins over the origin default", () => {
34
+ (globalThis as { window?: unknown }).window = {
35
+ location: { origin: "https://app.example.com" },
36
+ };
37
+ configureClient({ baseUrl: "https://api.example.com" });
38
+ expect(getBaseUrl()).toBe("https://api.example.com");
39
+ // …and still wins when server-rendered (no window) — a separate-origin API
40
+ // deploy must not silently flip to the page origin.
41
+ (globalThis as { window?: unknown }).window = undefined;
42
+ expect(getBaseUrl()).toBe("https://api.example.com");
43
+ });
44
+ });
package/src/index.ts CHANGED
@@ -159,9 +159,29 @@ let _baseUrl = "http://localhost:4321";
159
159
  let _baseUrlConfigured = false;
160
160
  let _appName = "default";
161
161
 
162
- /** Current effective base URL. Used by hooks (useRoom, useShard) that share
163
- * the client config but don't have access to the module-private state. */
162
+ /** Current effective base URL. Used by hooks (useRoom, useShard) and the
163
+ * @pylonsync/client auth helpers (createOrg, passwordRegister, createInvite,
164
+ * …) that share the client config but don't have access to the module-private
165
+ * state.
166
+ *
167
+ * When NOT explicitly configured, default to the page origin in a browser
168
+ * instead of the `http://localhost:4321` dev constant. A unified SSR/embedded
169
+ * app serves its API same-origin, so the static default was a footgun: every
170
+ * auth/org call fired at `localhost:4321` — broken on any non-4321 dev port
171
+ * AND in production (it would hit the engineer's dev port, not the app's
172
+ * domain). `init()`/`createSyncEngine` already resolve `window.location.origin`
173
+ * for the sync engine; this brings the auth helpers to the same origin so the
174
+ * two never disagree. An explicit `configureClient({ baseUrl })` still wins
175
+ * (separate-origin API setups), and SSR/node (no `window`) keeps the dev
176
+ * default (server-side calls use same-process paths anyway). */
164
177
  export function getBaseUrl(): string {
178
+ if (
179
+ !_baseUrlConfigured &&
180
+ typeof window !== "undefined" &&
181
+ window.location?.origin
182
+ ) {
183
+ return window.location.origin;
184
+ }
165
185
  return _baseUrl;
166
186
  }
167
187
 
@@ -250,7 +270,7 @@ function assertBaseUrlSafeForEnv(): void {
250
270
  */
251
271
  function transportConfig(): import("@pylonsync/sync").TransportConfig {
252
272
  return {
253
- baseUrl: _baseUrl,
273
+ baseUrl: getBaseUrl(),
254
274
  getToken: () => currentAuthToken() ?? undefined,
255
275
  };
256
276
  }
@@ -328,7 +348,7 @@ export async function getAuthContext(
328
348
  token?: string
329
349
  ): Promise<{ user_id: string | null }> {
330
350
  return pylonFetch<{ user_id: string | null }>(
331
- { baseUrl: _baseUrl, token },
351
+ { baseUrl: getBaseUrl(), token },
332
352
  "/api/auth/me",
333
353
  );
334
354
  }
@@ -349,7 +369,7 @@ export async function refreshSession(
349
369
  token: string;
350
370
  user_id: string;
351
371
  expires_at: number;
352
- }>({ baseUrl: _baseUrl, token }, "/api/auth/refresh", { method: "POST" });
372
+ }>({ baseUrl: getBaseUrl(), token }, "/api/auth/refresh", { method: "POST" });
353
373
  } catch {
354
374
  return null;
355
375
  }
@@ -453,7 +473,7 @@ export async function callFn<T = unknown>(
453
473
  ): Promise<T> {
454
474
  return pylonFetch<T>(
455
475
  {
456
- baseUrl: _baseUrl,
476
+ baseUrl: getBaseUrl(),
457
477
  getToken: () => options.token ?? currentAuthToken() ?? undefined,
458
478
  },
459
479
  `/api/fn/${name}`,
@@ -481,7 +501,7 @@ export async function* streamFn(
481
501
  // transport.
482
502
  const res = await pylonFetchRaw(
483
503
  {
484
- baseUrl: _baseUrl,
504
+ baseUrl: getBaseUrl(),
485
505
  getToken: () => options.token ?? currentAuthToken() ?? undefined,
486
506
  },
487
507
  `/api/fn/${name}`,
@@ -605,7 +625,7 @@ export async function uploadFile(
605
625
 
606
626
  return pylonFetch<UploadedFile>(
607
627
  {
608
- baseUrl: _baseUrl,
628
+ baseUrl: getBaseUrl(),
609
629
  getToken: () => options.token ?? currentAuthToken() ?? undefined,
610
630
  },
611
631
  "/api/files/upload",
@@ -638,7 +658,7 @@ export async function uploadFileMultipart(
638
658
 
639
659
  return pylonFetch<UploadedFile>(
640
660
  {
641
- baseUrl: _baseUrl,
661
+ baseUrl: getBaseUrl(),
642
662
  getToken: () => options.token ?? currentAuthToken() ?? undefined,
643
663
  },
644
664
  "/api/files/upload",