@pylonsync/react 0.5.0 → 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 +3 -3
- package/src/index.ts +51 -0
- package/src/storageMigration.test.ts +76 -0
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.5.
|
|
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.
|
|
18
|
-
"@pylonsync/sync": "0.5.
|
|
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
|
@@ -236,7 +236,58 @@ 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.
|
|
245
|
+
if (config.appName !== "default" && prev === "default") {
|
|
246
|
+
adoptLegacyKeys();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Session slots whose legacy (default-keyspace) value is adopted into the
|
|
252
|
+
* app namespace on first `configureClient({ appName })`. */
|
|
253
|
+
const MIGRATED_SLOTS = ["token", "userId"] as const;
|
|
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
|
+
|
|
276
|
+
function adoptLegacyKeys(): void {
|
|
277
|
+
try {
|
|
278
|
+
if (anotherAppNamespaced()) return; // legacy token is likely foreign — skip
|
|
279
|
+
for (const slot of MIGRATED_SLOTS) {
|
|
280
|
+
const nsKey = storageKey(slot); // pylon:<app>:<slot>
|
|
281
|
+
const legacyKey = `pylon_${slot}`;
|
|
282
|
+
if (nsKey === legacyKey) continue;
|
|
283
|
+
const existing = _storage.get(nsKey);
|
|
284
|
+
if (existing != null && existing !== "") continue; // already namespaced
|
|
285
|
+
const legacy = _storage.get(legacyKey);
|
|
286
|
+
if (legacy != null && legacy !== "") _storage.set(nsKey, legacy);
|
|
287
|
+
}
|
|
288
|
+
} catch {
|
|
289
|
+
// Storage can be absent (SSR/node) or throw (Safari private mode) —
|
|
290
|
+
// never let a migration attempt break client configuration.
|
|
240
291
|
}
|
|
241
292
|
}
|
|
242
293
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// A fresh app defaulted `_appName` to "default", so `storageKey("token")`
|
|
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. 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.
|
|
8
|
+
//
|
|
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.
|
|
12
|
+
|
|
13
|
+
import { beforeEach, expect, test } from "bun:test";
|
|
14
|
+
|
|
15
|
+
import { configureClient, setReactStorage, storageKey } from "./index";
|
|
16
|
+
import type { Storage as PylonStorage } from "@pylonsync/sync";
|
|
17
|
+
|
|
18
|
+
function memStorage(): PylonStorage & { map: Map<string, string> } {
|
|
19
|
+
const map = new Map<string, string>();
|
|
20
|
+
return {
|
|
21
|
+
map,
|
|
22
|
+
get: (k: string) => map.get(k) ?? null,
|
|
23
|
+
set: (k: string, v: string) => void map.set(k, v),
|
|
24
|
+
remove: (k: string) => void map.delete(k),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
configureClient({ appName: "default" }); // reset the module-global _appName
|
|
30
|
+
if (typeof localStorage !== "undefined") localStorage.clear();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("adopts a legacy pylon_token into the app namespace, non-destructively", () => {
|
|
34
|
+
const store = memStorage();
|
|
35
|
+
setReactStorage(store);
|
|
36
|
+
store.set("pylon_token", "tok_legacy");
|
|
37
|
+
store.set("pylon_userId", "user_1");
|
|
38
|
+
|
|
39
|
+
configureClient({ appName: "revtrail" });
|
|
40
|
+
|
|
41
|
+
// Session preserved under the new namespaced keys…
|
|
42
|
+
expect(store.get("pylon:revtrail:token")).toBe("tok_legacy");
|
|
43
|
+
expect(store.get("pylon:revtrail:userId")).toBe("user_1");
|
|
44
|
+
// …and COPIED, not moved — a framework rollback still finds the old key.
|
|
45
|
+
expect(store.get("pylon_token")).toBe("tok_legacy");
|
|
46
|
+
// storageKey now resolves to the namespace.
|
|
47
|
+
expect(storageKey("token")).toBe("pylon:revtrail:token");
|
|
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
|
+
});
|