@pylonsync/react 0.5.2 → 0.5.3

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.5.2",
6
+ "version": "0.5.3",
7
7
  "type": "module",
8
8
  "main": "./src/index.ts",
9
9
  "types": "./dist/index.d.ts",
@@ -14,8 +14,8 @@
14
14
  "prepack": "bun run build"
15
15
  },
16
16
  "dependencies": {
17
- "@pylonsync/sdk": "0.5.2",
18
- "@pylonsync/sync": "0.5.2"
17
+ "@pylonsync/sdk": "0.5.3",
18
+ "@pylonsync/sync": "0.5.3"
19
19
  },
20
20
  "peerDependencies": {
21
21
  "react": ">=19.0.0"
package/src/index.ts CHANGED
@@ -241,8 +241,7 @@ export function configureClient(config: AgentDBClientConfig): void {
241
241
  // An app moving off the default (unprefixed `pylon_token`) keyspace onto
242
242
  // its own `pylon:<app>:token` namespace keeps its session: copy the legacy
243
243
  // keys up once. COPY, never delete — a framework rollback still finds the
244
- // old keys, and a shared-origin multi-app collision self-heals on next
245
- // login (each app rewrites its own namespace).
244
+ // old keys.
246
245
  if (config.appName !== "default" && prev === "default") {
247
246
  adoptLegacyKeys();
248
247
  }
@@ -253,8 +252,30 @@ export function configureClient(config: AgentDBClientConfig): void {
253
252
  * app namespace on first `configureClient({ appName })`. */
254
253
  const MIGRATED_SLOTS = ["token", "userId"] as const;
255
254
 
255
+ /**
256
+ * A `pylon:<other>:token` key already on this origin means another Pylon app
257
+ * has run here — so the unprefixed `pylon_token` cannot be assumed to be OURS.
258
+ * The legacy keyspace records no owner, and on a shared origin (every app on
259
+ * localhost:4321 in dev) that stranger's token is usually invalid for us.
260
+ * Adopt only when this looks like the origin's sole app; otherwise leave the
261
+ * slot empty and let a real sign-in fill it. Web-only — RN's per-app async
262
+ * adapters are already isolated, so the shared-origin hazard doesn't exist.
263
+ */
264
+ function anotherAppNamespaced(): boolean {
265
+ if (typeof localStorage === "undefined") return false;
266
+ const mine = storageKey("token");
267
+ for (let i = 0; i < localStorage.length; i++) {
268
+ const k = localStorage.key(i);
269
+ if (k && k.startsWith("pylon:") && k.endsWith(":token") && k !== mine) {
270
+ return true;
271
+ }
272
+ }
273
+ return false;
274
+ }
275
+
256
276
  function adoptLegacyKeys(): void {
257
277
  try {
278
+ if (anotherAppNamespaced()) return; // legacy token is likely foreign — skip
258
279
  for (const slot of MIGRATED_SLOTS) {
259
280
  const nsKey = storageKey(slot); // pylon:<app>:<slot>
260
281
  const legacyKey = `pylon_${slot}`;
@@ -1,15 +1,16 @@
1
- // Regression: a fresh app defaulted `_appName` to "default", so `storageKey`
1
+ // A fresh app defaulted `_appName` to "default", so `storageKey("token")`
2
2
  // returned the unprefixed `pylon_token`. Two apps on one origin (every app on
3
3
  // localhost:4321 in dev) then shared one token keyspace and clobbered each
4
- // other's sessions. The fix auto-namespaces per app; this pins that adopting a
5
- // namespace copies an existing default-keyspace session up (non-destructively),
6
- // so no one gets logged out on the switch.
4
+ // other's sessions. When an app adopts a namespace, `configureClient` copies a
5
+ // legacy default-keyspace session up so a single-app upgrade doesn't log anyone
6
+ // out — but ONLY when the legacy key is plausibly ours (no other app has
7
+ // namespaced here). These pin adopt / no-clobber / don't-import-a-stranger.
7
8
  //
8
- // `_appName` is module-global and only transitions off "default" once, so the
9
- // no-clobber branch lives in its own file (storageMigrationNoClobber.test.ts)
10
- // with a fresh transition.
9
+ // `_appName` is a module global that only transitions off "default" once per
10
+ // process, so `beforeEach` resets it (configureClient({ appName: "default" }))
11
+ // and clears the origin's localStorage that the shared-origin guard reads.
11
12
 
12
- import { expect, test } from "bun:test";
13
+ import { beforeEach, expect, test } from "bun:test";
13
14
 
14
15
  import { configureClient, setReactStorage, storageKey } from "./index";
15
16
  import type { Storage as PylonStorage } from "@pylonsync/sync";
@@ -24,6 +25,11 @@ function memStorage(): PylonStorage & { map: Map<string, string> } {
24
25
  };
25
26
  }
26
27
 
28
+ beforeEach(() => {
29
+ configureClient({ appName: "default" }); // reset the module-global _appName
30
+ if (typeof localStorage !== "undefined") localStorage.clear();
31
+ });
32
+
27
33
  test("adopts a legacy pylon_token into the app namespace, non-destructively", () => {
28
34
  const store = memStorage();
29
35
  setReactStorage(store);
@@ -40,3 +46,31 @@ test("adopts a legacy pylon_token into the app namespace, non-destructively", ()
40
46
  // storageKey now resolves to the namespace.
41
47
  expect(storageKey("token")).toBe("pylon:revtrail:token");
42
48
  });
49
+
50
+ test("does not clobber a token already in the app namespace", () => {
51
+ const store = memStorage();
52
+ setReactStorage(store);
53
+ store.set("pylon_token", "tok_legacy");
54
+ store.set("pylon:revtrail:token", "tok_current");
55
+
56
+ configureClient({ appName: "revtrail" });
57
+
58
+ expect(store.get("pylon:revtrail:token")).toBe("tok_current");
59
+ });
60
+
61
+ test("does not adopt a foreign pylon_token when another app is namespaced here", () => {
62
+ // Another Pylon app has already run on this origin (shared-origin repro from
63
+ // a 0.5.2 field report). The ownerless legacy key can't be assumed ours.
64
+ localStorage.setItem("pylon:otherapp:token", "tok_other_app");
65
+
66
+ const store = memStorage();
67
+ setReactStorage(store);
68
+ store.set("pylon_token", "tok_FOREIGN_from_another_app");
69
+
70
+ configureClient({ appName: "mastagents" });
71
+
72
+ // The stranger's token is NOT copied into our namespace…
73
+ expect(store.get("pylon:mastagents:token")).toBeNull();
74
+ // …and the legacy key is untouched (we never delete it either).
75
+ expect(store.get("pylon_token")).toBe("tok_FOREIGN_from_another_app");
76
+ });
@@ -1,30 +0,0 @@
1
- // The legacy-token adoption (see storageMigration.test.ts) must never overwrite
2
- // a token already living in the app namespace — that would replay a stale
3
- // default-keyspace session over the app's real one. Own file so `_appName`
4
- // starts at "default" and the single transition runs the guard.
5
-
6
- import { expect, test } from "bun:test";
7
-
8
- import { configureClient, setReactStorage } from "./index";
9
- import type { Storage as PylonStorage } from "@pylonsync/sync";
10
-
11
- function memStorage(): PylonStorage & { map: Map<string, string> } {
12
- const map = new Map<string, string>();
13
- return {
14
- map,
15
- get: (k: string) => map.get(k) ?? null,
16
- set: (k: string, v: string) => void map.set(k, v),
17
- remove: (k: string) => void map.delete(k),
18
- };
19
- }
20
-
21
- test("does not clobber a token already in the app namespace", () => {
22
- const store = memStorage();
23
- setReactStorage(store);
24
- store.set("pylon_token", "tok_legacy");
25
- store.set("pylon:revtrail:token", "tok_current");
26
-
27
- configureClient({ appName: "revtrail" });
28
-
29
- expect(store.get("pylon:revtrail:token")).toBe("tok_current");
30
- });