@pylonsync/react 0.5.0 → 0.5.2

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.0",
6
+ "version": "0.5.2",
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.0",
18
- "@pylonsync/sync": "0.5.0"
17
+ "@pylonsync/sdk": "0.5.2",
18
+ "@pylonsync/sync": "0.5.2"
19
19
  },
20
20
  "peerDependencies": {
21
21
  "react": ">=19.0.0"
package/src/index.ts CHANGED
@@ -236,7 +236,37 @@ export function configureClient(config: AgentDBClientConfig): void {
236
236
  maybeWarnDowngrade(config.baseUrl);
237
237
  }
238
238
  if (config.appName) {
239
+ const prev = _appName;
239
240
  _appName = config.appName;
241
+ // An app moving off the default (unprefixed `pylon_token`) keyspace onto
242
+ // its own `pylon:<app>:token` namespace keeps its session: copy the legacy
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).
246
+ if (config.appName !== "default" && prev === "default") {
247
+ adoptLegacyKeys();
248
+ }
249
+ }
250
+ }
251
+
252
+ /** Session slots whose legacy (default-keyspace) value is adopted into the
253
+ * app namespace on first `configureClient({ appName })`. */
254
+ const MIGRATED_SLOTS = ["token", "userId"] as const;
255
+
256
+ function adoptLegacyKeys(): void {
257
+ try {
258
+ for (const slot of MIGRATED_SLOTS) {
259
+ const nsKey = storageKey(slot); // pylon:<app>:<slot>
260
+ const legacyKey = `pylon_${slot}`;
261
+ if (nsKey === legacyKey) continue;
262
+ const existing = _storage.get(nsKey);
263
+ if (existing != null && existing !== "") continue; // already namespaced
264
+ const legacy = _storage.get(legacyKey);
265
+ if (legacy != null && legacy !== "") _storage.set(nsKey, legacy);
266
+ }
267
+ } catch {
268
+ // Storage can be absent (SSR/node) or throw (Safari private mode) —
269
+ // never let a migration attempt break client configuration.
240
270
  }
241
271
  }
242
272
 
@@ -0,0 +1,42 @@
1
+ // Regression: a fresh app defaulted `_appName` to "default", so `storageKey`
2
+ // returned the unprefixed `pylon_token`. Two apps on one origin (every app on
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.
7
+ //
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.
11
+
12
+ import { expect, test } from "bun:test";
13
+
14
+ import { configureClient, setReactStorage, storageKey } from "./index";
15
+ import type { Storage as PylonStorage } from "@pylonsync/sync";
16
+
17
+ function memStorage(): PylonStorage & { map: Map<string, string> } {
18
+ const map = new Map<string, string>();
19
+ return {
20
+ map,
21
+ get: (k: string) => map.get(k) ?? null,
22
+ set: (k: string, v: string) => void map.set(k, v),
23
+ remove: (k: string) => void map.delete(k),
24
+ };
25
+ }
26
+
27
+ test("adopts a legacy pylon_token into the app namespace, non-destructively", () => {
28
+ const store = memStorage();
29
+ setReactStorage(store);
30
+ store.set("pylon_token", "tok_legacy");
31
+ store.set("pylon_userId", "user_1");
32
+
33
+ configureClient({ appName: "revtrail" });
34
+
35
+ // Session preserved under the new namespaced keys…
36
+ expect(store.get("pylon:revtrail:token")).toBe("tok_legacy");
37
+ expect(store.get("pylon:revtrail:userId")).toBe("user_1");
38
+ // …and COPIED, not moved — a framework rollback still finds the old key.
39
+ expect(store.get("pylon_token")).toBe("tok_legacy");
40
+ // storageKey now resolves to the namespace.
41
+ expect(storageKey("token")).toBe("pylon:revtrail:token");
42
+ });
@@ -0,0 +1,30 @@
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
+ });