@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.
|
|
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
|
@@ -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
|
|
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
|
-
//
|
|
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.
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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
|
|
9
|
-
//
|
|
10
|
-
//
|
|
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
|
-
});
|