@pylonsync/sync 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/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,17 @@ 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. Point at
737
+ * the real cause, once. A genuinely signed-out user has no stored token,
738
+ * so this stays quiet for them.
739
+ */
740
+ private warnForeignToken;
728
741
  /**
729
742
  * Apply a freshly-observed session through the resolver and act on
730
743
  * the verdict. Serialized via `sessionChain` so concurrent triggers
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",
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
- return this.storage.get(this.tokenStorageKey());
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,30 @@ 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. Point at
2400
+ * the real cause, once. A genuinely signed-out user has no stored token,
2401
+ * so this stays quiet for them.
2402
+ */
2403
+ private warnForeignToken(): void {
2404
+ if (this.warnedForeignToken) return;
2405
+ if (this.config.token) return; // explicit token, not a keyspace artifact
2406
+ if (!this.storage.get(this.tokenStorageKey())) return; // no token → normal signed-out
2407
+ this.warnedForeignToken = true;
2408
+ console.warn(
2409
+ '[pylon] a stored auth token was rejected (/api/auth/me resolved to an ' +
2410
+ 'anonymous identity). If more than one Pylon app runs on this origin, ' +
2411
+ 'they share the default "pylon_token" keyspace and clobber each ' +
2412
+ "other's sessions. Give each app a name: init({ appName }) / " +
2413
+ "configureClient({ appName }).",
2414
+ );
2415
+ }
2416
+
2375
2417
  /**
2376
2418
  * Apply a freshly-observed session through the resolver and act on
2377
2419
  * the verdict. Serialized via `sessionChain` so concurrent triggers