@pylonsync/sync 0.3.139 → 0.3.142

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.ts +89 -0
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.3.139",
6
+ "version": "0.3.142",
7
7
  "type": "module",
8
8
  "main": "src/index.ts",
9
9
  "types": "src/index.ts",
package/src/index.ts CHANGED
@@ -1683,11 +1683,100 @@ export class SyncEngine {
1683
1683
  * mutates the server session (sign-in, sign-out, `/api/auth/select-org`)
1684
1684
  * so the cached session and React subscribers pick up the change without
1685
1685
  * waiting for the next pull.
1686
+ *
1687
+ * Most apps shouldn't need to call this directly — prefer the higher-
1688
+ * level helpers below (`selectOrg`, `clearOrg`, `signOut`) which do the
1689
+ * fetch + notify in one step, or the React `useSession()` hook which
1690
+ * exposes the same helpers bound to the in-scope engine. This is the
1691
+ * escape hatch for code that talks to /api/auth/* via its own client.
1686
1692
  */
1687
1693
  notifySessionChanged(): Promise<void> {
1688
1694
  return this.refreshResolvedSession();
1689
1695
  }
1690
1696
 
1697
+ /**
1698
+ * Switch the caller's active tenant (organization) and refresh the
1699
+ * resolved session in one shot. Membership is verified server-side
1700
+ * (POST /api/auth/select-org throws 403 if the user isn't a member
1701
+ * of the target org), and the engine's local replica resets so
1702
+ * `db.useQuery` stops returning the previous tenant's rows.
1703
+ *
1704
+ * Throws on any non-2xx response. The error carries the
1705
+ * server-issued JSON error body when available, so callers can
1706
+ * branch on `err.code === "NOT_A_MEMBER"` etc.
1707
+ */
1708
+ async selectOrg(orgId: string): Promise<void> {
1709
+ await this.authMutate("/api/auth/select-org", { orgId });
1710
+ await this.refreshResolvedSession();
1711
+ }
1712
+
1713
+ /**
1714
+ * Drop the caller's active tenant — back to the "no active org"
1715
+ * state typical of a login-lobby route. Refreshes the resolved
1716
+ * session so React subscribers re-render with `tenantId: null`.
1717
+ */
1718
+ async clearOrg(): Promise<void> {
1719
+ await this.authMutate("/api/auth/select-org", { orgId: null });
1720
+ await this.refreshResolvedSession();
1721
+ }
1722
+
1723
+ /**
1724
+ * Revoke the current session server-side (DELETE /api/auth/session)
1725
+ * and refresh — leaves the caller anonymous. Local sync stops on
1726
+ * the next pull cycle; replica content stays in IndexedDB so a
1727
+ * subsequent sign-in as the same user is instant.
1728
+ */
1729
+ async signOut(): Promise<void> {
1730
+ await this.authMutate("/api/auth/session", undefined, "DELETE");
1731
+ await this.refreshResolvedSession();
1732
+ }
1733
+
1734
+ /** Shared transport for the auth helpers above. Same bearer/cookie
1735
+ * policy as `request()` — keeps the auth flows on the same
1736
+ * authentication footing as data sync. */
1737
+ private async authMutate(
1738
+ path: string,
1739
+ body?: unknown,
1740
+ method = "POST",
1741
+ ): Promise<unknown> {
1742
+ const headers: Record<string, string> = {};
1743
+ if (body !== undefined) headers["Content-Type"] = "application/json";
1744
+ const token =
1745
+ this.config.token ??
1746
+ this.storage.get(this.tokenStorageKey()) ??
1747
+ undefined;
1748
+ if (token) headers["Authorization"] = `Bearer ${token}`;
1749
+ const res = await fetch(`${this.config.baseUrl}${path}`, {
1750
+ method,
1751
+ headers,
1752
+ credentials: "include",
1753
+ body: body !== undefined ? JSON.stringify(body) : undefined,
1754
+ });
1755
+ const text = await res.text();
1756
+ let parsed: unknown = null;
1757
+ if (text) {
1758
+ try {
1759
+ parsed = JSON.parse(text);
1760
+ } catch {
1761
+ // Server returned non-JSON (HTML error page from a proxy,
1762
+ // empty 204, etc.) — fall through; the !res.ok branch will
1763
+ // synthesise a useful Error from the status.
1764
+ }
1765
+ }
1766
+ if (!res.ok) {
1767
+ const err = new Error(
1768
+ (parsed as { error?: { message?: string } } | null)?.error?.message ??
1769
+ `${method} ${path} failed: ${res.status}`,
1770
+ ) as Error & { status?: number; code?: string };
1771
+ err.status = res.status;
1772
+ const code = (parsed as { error?: { code?: string } } | null)?.error
1773
+ ?.code;
1774
+ if (code) err.code = code;
1775
+ throw err;
1776
+ }
1777
+ return parsed;
1778
+ }
1779
+
1691
1780
  /**
1692
1781
  * In-flight push promise. Used as a mutex so a slow push can't be restarted
1693
1782
  * by the poll timer or a user mutation, which would resend the same batch