@pylonsync/sync 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/dist/index.d.ts +18 -0
- package/package.json +1 -1
- package/src/index.ts +55 -1
package/dist/index.d.ts
CHANGED
|
@@ -265,6 +265,8 @@ export declare class SyncEngine {
|
|
|
265
265
|
* joiner whose orchestrator never fires onPromote (because it was
|
|
266
266
|
* never leader) silently run as a leader. */
|
|
267
267
|
private isMultiTabLeader;
|
|
268
|
+
/** One-shot guard for the cross-app keyspace-collision warning. */
|
|
269
|
+
private warnedForeignToken;
|
|
268
270
|
/**
|
|
269
271
|
* Serialized apply queue. Every change-event apply — from WS onmessage,
|
|
270
272
|
* pull(), or session-changed catchup — chains onto this promise so
|
|
@@ -725,6 +727,22 @@ export declare class SyncEngine {
|
|
|
725
727
|
* will retry. Errors must not abort bootstrap.
|
|
726
728
|
*/
|
|
727
729
|
private fetchSessionBootstrap;
|
|
730
|
+
/**
|
|
731
|
+
* A stored token that /api/auth/me resolves to no user is either expired
|
|
732
|
+
* or — the common footgun — a FOREIGN token: two Pylon apps sharing an
|
|
733
|
+
* origin (every app on localhost:4321 in dev) both read the default,
|
|
734
|
+
* unprefixed `pylon_token`, so one app sends the other's token and the
|
|
735
|
+
* server rejects it. The failure surfaces far away as "anonymous session"
|
|
736
|
+
* from createOrg, which names neither the token nor the keyspace.
|
|
737
|
+
*
|
|
738
|
+
* Heal it here instead of "on next login": clear the app's OWN namespaced
|
|
739
|
+
* token so the invalid value stops being sent and the app drops cleanly to
|
|
740
|
+
* signed-out (a real sign-in then writes a valid token into this namespace).
|
|
741
|
+
* Only the namespaced key — never the shared default `pylon_token`, which may
|
|
742
|
+
* belong to a different app on this origin. Warn once so the cause is named.
|
|
743
|
+
* A genuinely signed-out user has no stored token, so this stays quiet.
|
|
744
|
+
*/
|
|
745
|
+
private warnForeignToken;
|
|
728
746
|
/**
|
|
729
747
|
* Apply a freshly-observed session through the resolver and act on
|
|
730
748
|
* the verdict. Serialized via `sessionChain` so concurrent triggers
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -389,6 +389,8 @@ export class SyncEngine {
|
|
|
389
389
|
* joiner whose orchestrator never fires onPromote (because it was
|
|
390
390
|
* never leader) silently run as a leader. */
|
|
391
391
|
private isMultiTabLeader = false;
|
|
392
|
+
/** One-shot guard for the cross-app keyspace-collision warning. */
|
|
393
|
+
private warnedForeignToken = false;
|
|
392
394
|
|
|
393
395
|
/**
|
|
394
396
|
* Serialized apply queue. Every change-event apply — from WS onmessage,
|
|
@@ -1634,7 +1636,22 @@ export class SyncEngine {
|
|
|
1634
1636
|
/** Current auth token from config or the storage adapter. Null when neither has one. */
|
|
1635
1637
|
private currentToken(): string | null {
|
|
1636
1638
|
if (this.config.token) return this.config.token;
|
|
1637
|
-
|
|
1639
|
+
const key = this.tokenStorageKey();
|
|
1640
|
+
const token = this.storage.get(key);
|
|
1641
|
+
if (token) return token;
|
|
1642
|
+
// Non-destructive legacy adoption, mirroring React's configureClient:
|
|
1643
|
+
// an app that shipped on the default `pylon_token` keeps its session
|
|
1644
|
+
// when it adopts a namespace. On web this is a no-op (React already
|
|
1645
|
+
// copied it in shared localStorage); it matters for async/native
|
|
1646
|
+
// adapters the React layer doesn't touch. COPY, never delete.
|
|
1647
|
+
if (key !== "pylon_token") {
|
|
1648
|
+
const legacy = this.storage.get("pylon_token");
|
|
1649
|
+
if (legacy) {
|
|
1650
|
+
this.storage.set(key, legacy);
|
|
1651
|
+
return legacy;
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
return null;
|
|
1638
1655
|
}
|
|
1639
1656
|
|
|
1640
1657
|
/**
|
|
@@ -2358,6 +2375,7 @@ export class SyncEngine {
|
|
|
2358
2375
|
roles?: string[];
|
|
2359
2376
|
avatar_url?: string | null;
|
|
2360
2377
|
};
|
|
2378
|
+
if ((raw.user_id ?? null) === null) this.warnForeignToken();
|
|
2361
2379
|
return {
|
|
2362
2380
|
userId: raw.user_id ?? null,
|
|
2363
2381
|
tenantId: raw.tenant_id ?? null,
|
|
@@ -2372,6 +2390,42 @@ export class SyncEngine {
|
|
|
2372
2390
|
}
|
|
2373
2391
|
}
|
|
2374
2392
|
|
|
2393
|
+
/**
|
|
2394
|
+
* A stored token that /api/auth/me resolves to no user is either expired
|
|
2395
|
+
* or — the common footgun — a FOREIGN token: two Pylon apps sharing an
|
|
2396
|
+
* origin (every app on localhost:4321 in dev) both read the default,
|
|
2397
|
+
* unprefixed `pylon_token`, so one app sends the other's token and the
|
|
2398
|
+
* server rejects it. The failure surfaces far away as "anonymous session"
|
|
2399
|
+
* from createOrg, which names neither the token nor the keyspace.
|
|
2400
|
+
*
|
|
2401
|
+
* Heal it here instead of "on next login": clear the app's OWN namespaced
|
|
2402
|
+
* token so the invalid value stops being sent and the app drops cleanly to
|
|
2403
|
+
* signed-out (a real sign-in then writes a valid token into this namespace).
|
|
2404
|
+
* Only the namespaced key — never the shared default `pylon_token`, which may
|
|
2405
|
+
* belong to a different app on this origin. Warn once so the cause is named.
|
|
2406
|
+
* A genuinely signed-out user has no stored token, so this stays quiet.
|
|
2407
|
+
*/
|
|
2408
|
+
private warnForeignToken(): void {
|
|
2409
|
+
if (this.warnedForeignToken) return;
|
|
2410
|
+
if (this.config.token) return; // explicit token, not a keyspace artifact
|
|
2411
|
+
const key = this.tokenStorageKey();
|
|
2412
|
+
if (!this.storage.get(key)) return; // no token → normal signed-out
|
|
2413
|
+
this.warnedForeignToken = true;
|
|
2414
|
+
if (key !== "pylon_token") {
|
|
2415
|
+
// Self-heal: drop the rejected token from this app's namespace so the
|
|
2416
|
+
// next request doesn't resend it. Leaves the ambiguous default key alone.
|
|
2417
|
+
this.storage.remove(key);
|
|
2418
|
+
this.storage.remove(key.replace(/:token$/, ":userId"));
|
|
2419
|
+
}
|
|
2420
|
+
console.warn(
|
|
2421
|
+
'[pylon] a stored auth token was rejected (/api/auth/me resolved to an ' +
|
|
2422
|
+
'anonymous identity) and has been cleared from this app. If more than ' +
|
|
2423
|
+
'one Pylon app runs on this origin, they share the default ' +
|
|
2424
|
+
'"pylon_token" keyspace and clobber each other\'s sessions. Give each ' +
|
|
2425
|
+
"app a name: init({ appName }) / configureClient({ appName }).",
|
|
2426
|
+
);
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2375
2429
|
/**
|
|
2376
2430
|
* Apply a freshly-observed session through the resolver and act on
|
|
2377
2431
|
* the verdict. Serialized via `sessionChain` so concurrent triggers
|