@pylonsync/react 0.22.15 → 0.22.17

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
@@ -14,7 +14,7 @@ export type { ImageResponseOptions, ImageResponseFont, } from "./image-response"
14
14
  export type { PageProps, PageAuth, ServerData, SsrResponse, SsrCookieOptions, Metadata, GenerateMetadata, Sitemap, SitemapEntry, Robots, RobotsRule, LlmsTxt, LlmsSection, LlmsLink, RouteSegmentConfig, ErrorBoundaryProps, NotFoundProps, FormFields, FormDb, FormRequest, RouteHandler, RawRouteHandler, RawResponse, } from "./ssr";
15
15
  export { useRouter, useSearchParams, usePathname, useParams, useRouteSeed, useRouteData, redirect, notFound, NotFoundError, } from "./useRouter";
16
16
  export type { PylonRouter } from "./useRouter";
17
- import { type Storage as PylonStorage } from "@pylonsync/sync";
17
+ import { type Storage as PylonStorage, type TransportConfig } from "@pylonsync/sync";
18
18
  export { useQuery, useQueryOne, useReactiveQuery, useMutation, useInfiniteQuery, usePaginatedQuery, useEntityMutation, useAction, useQueryRaw, useQueryOneRaw, useLiveList, useLiveRow, useInsert, useUpdate, useDelete, useFn, useAggregate, useSearch, } from "./hooks";
19
19
  export type { QueryOptions, QueryFilter, IncludeSpec, UseQueryReturn, UseQueryOneReturn, UseReactiveQueryReturn, UseMutationReturn, UseInfiniteQueryReturn, UsePaginatedQueryReturn, PaginatedQueryStatus, UseFnReturn, AggregateSpec, UseAggregateReturn, SearchSpec, UseSearchReturn, } from "./hooks";
20
20
  export { useAgentRun } from "./useAgentRun";
@@ -74,6 +74,21 @@ export declare function getAppName(): string;
74
74
  */
75
75
  export declare function storageKey(slot: string): string;
76
76
  export declare function configureClient(config: AgentDBClientConfig): void;
77
+ /**
78
+ * Build the transport config for the React free helpers — base URL,
79
+ * token getter, and cookie credentials are all centralized in
80
+ * `pylonFetch`. Cookie-auth apps work because the transport always
81
+ * sets `credentials: "include"`; bearer-auth apps work because
82
+ * `getToken` returns the stored session token (or `explicitToken`).
83
+ *
84
+ * When the server answers that the token sent resolved to no one
85
+ * (`X-Pylon-Bearer-Rejected`), the stored token is cleared so later
86
+ * calls stop sending it and the session cookie applies alone. An
87
+ * `explicitToken` that is not the stored token is left to its caller.
88
+ *
89
+ * @internal Shared by the helpers in this package.
90
+ */
91
+ export declare function clientTransport(explicitToken?: string): TransportConfig;
77
92
  export declare function fetchList(entity: string): Promise<Record<string, unknown>[]>;
78
93
  export declare function fetchById(entity: string, id: string): Promise<Record<string, unknown> | null>;
79
94
  export declare function insert(entity: string, data: Record<string, unknown>): Promise<{
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.22.15",
6
+ "version": "0.22.17",
7
7
  "type": "module",
8
8
  "main": "./src/index.ts",
9
9
  "types": "./dist/index.d.ts",
@@ -14,9 +14,9 @@
14
14
  "prepack": "bun run build"
15
15
  },
16
16
  "dependencies": {
17
- "@pylonsync/realtime": "0.22.15",
18
- "@pylonsync/sdk": "0.22.15",
19
- "@pylonsync/sync": "0.22.15"
17
+ "@pylonsync/realtime": "0.22.17",
18
+ "@pylonsync/sdk": "0.22.17",
19
+ "@pylonsync/sync": "0.22.17"
20
20
  },
21
21
  "peerDependencies": {
22
22
  "react": ">=19.0.0"
@@ -6,7 +6,7 @@
6
6
  // behaved differently depending on which import you reached for.
7
7
 
8
8
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
9
- import { callFn } from "./index";
9
+ import { callFn, configureClient, getReactStorage, setReactStorage } from "./index";
10
10
  import { peekActiveEngine, setActiveEngine } from "./engine-registry";
11
11
 
12
12
  describe("callFn", () => {
@@ -74,6 +74,43 @@ describe("callFn", () => {
74
74
  expect(requests).toHaveLength(1);
75
75
  });
76
76
 
77
+ test("a stored token the server rejects is cleared and not sent again", async () => {
78
+ // The server answers `X-Pylon-Bearer-Rejected: 1` when the bearer
79
+ // resolved to no one; it used the session cookie instead. The next call
80
+ // must not resend the stale token.
81
+ const data = new Map<string, string>([
82
+ ["pylon:miles:token", "stale"],
83
+ ["pylon:miles:userId", "u_old"],
84
+ ]);
85
+ const prevStorage = getReactStorage();
86
+ setReactStorage({
87
+ get: (k) => data.get(k) ?? null,
88
+ set: (k, v) => void data.set(k, v),
89
+ remove: (k) => void data.delete(k),
90
+ });
91
+ configureClient({ appName: "miles" });
92
+ globalThis.fetch = (async (input: any, init: any = {}) => {
93
+ const headers = (init.headers ?? {}) as Record<string, string>;
94
+ requests.push({ url: String(input?.url ?? input), headers });
95
+ const reply: Record<string, string> = { "content-type": "application/json" };
96
+ if (headers.Authorization === "Bearer stale") reply["x-pylon-bearer-rejected"] = "1";
97
+ return new Response(JSON.stringify({ ok: true }), { status: 200, headers: reply });
98
+ }) as typeof fetch;
99
+ try {
100
+ await callFn("resetDemoDealership");
101
+ expect(data.has("pylon:miles:token")).toBe(false);
102
+ expect(data.has("pylon:miles:userId")).toBe(false);
103
+ await callFn("resetDemoDealership");
104
+ expect(requests.map((r) => r.headers.Authorization as string | undefined)).toEqual([
105
+ "Bearer stale",
106
+ undefined,
107
+ ]);
108
+ } finally {
109
+ setReactStorage(prevStorage);
110
+ configureClient({ appName: "default" });
111
+ }
112
+ });
113
+
77
114
  test("peekActiveEngine never constructs an engine", async () => {
78
115
  // The distinction from db.ts's getSync(), which deliberately does.
79
116
  expect(peekActiveEngine()).toBeNull();
package/src/hooks.ts CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { SyncEngine, generateId, pylonFetch, type Row } from "@pylonsync/sync";
4
4
  import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
5
- import { callFn, getBaseUrl, getReactStorage, storageKey } from "./index";
5
+ import { callFn, clientTransport } from "./index";
6
6
 
7
7
  // ---------------------------------------------------------------------------
8
8
  // Query shapes
@@ -1014,13 +1014,7 @@ export function useAggregate<Row = Record<string, unknown>>(
1014
1014
  setError(null);
1015
1015
  try {
1016
1016
  const json = await pylonFetch<{ rows?: Row[] }>(
1017
- {
1018
- baseUrl: getBaseUrl(),
1019
- getToken: () =>
1020
- (getReactStorage().get(storageKey("token")) ?? undefined) as
1021
- | string
1022
- | undefined,
1023
- },
1017
+ clientTransport(),
1024
1018
  `/api/aggregate/${entity}`,
1025
1019
  { method: "POST", body: specKey },
1026
1020
  );
@@ -1162,13 +1156,7 @@ export function useSearch<T = Row>(
1162
1156
  total?: number;
1163
1157
  tookMs?: number;
1164
1158
  }>(
1165
- {
1166
- baseUrl: getBaseUrl(),
1167
- getToken: () =>
1168
- (getReactStorage().get(storageKey("token")) ?? undefined) as
1169
- | string
1170
- | undefined,
1171
- },
1159
+ clientTransport(),
1172
1160
  `/api/search/${entity}`,
1173
1161
  {
1174
1162
  method: "POST",
package/src/index.ts CHANGED
@@ -70,11 +70,13 @@ export {
70
70
  export type { PylonRouter } from "./useRouter";
71
71
 
72
72
  import {
73
+ clearRejectedToken,
73
74
  defaultStorage,
74
75
  pylonFetch,
75
76
  pylonFetchRaw,
76
77
  PylonHttpError,
77
78
  type Storage as PylonStorage,
79
+ type TransportConfig,
78
80
  } from "@pylonsync/sync";
79
81
  import { peekActiveEngine } from "./engine-registry";
80
82
 
@@ -385,15 +387,29 @@ function assertBaseUrlSafeForEnv(): void {
385
387
  * token getter, and cookie credentials are all centralized in
386
388
  * `pylonFetch`. Cookie-auth apps work because the transport always
387
389
  * sets `credentials: "include"`; bearer-auth apps work because
388
- * `getToken` returns the cached session token.
390
+ * `getToken` returns the stored session token (or `explicitToken`).
391
+ *
392
+ * When the server answers that the token sent resolved to no one
393
+ * (`X-Pylon-Bearer-Rejected`), the stored token is cleared so later
394
+ * calls stop sending it and the session cookie applies alone. An
395
+ * `explicitToken` that is not the stored token is left to its caller.
396
+ *
397
+ * @internal Shared by the helpers in this package.
389
398
  */
390
- function transportConfig(): import("@pylonsync/sync").TransportConfig {
399
+ export function clientTransport(explicitToken?: string): TransportConfig {
391
400
  return {
392
401
  baseUrl: getBaseUrl(),
393
- getToken: () => currentAuthToken() ?? undefined,
402
+ getToken: () => explicitToken ?? currentAuthToken() ?? undefined,
403
+ onBearerRejected: (token) => {
404
+ clearRejectedToken(_storage, storageKey("token"), token);
405
+ },
394
406
  };
395
407
  }
396
408
 
409
+ function transportConfig(): TransportConfig {
410
+ return clientTransport();
411
+ }
412
+
397
413
  async function apiRequest(
398
414
  method: string,
399
415
  path: string,
@@ -610,10 +626,7 @@ export async function callFn<T = unknown>(
610
626
  return engine.fn<T>(name, args);
611
627
  }
612
628
  return pylonFetch<T>(
613
- {
614
- baseUrl: getBaseUrl(),
615
- getToken: () => options.token ?? currentAuthToken() ?? undefined,
616
- },
629
+ clientTransport(options.token),
617
630
  `/api/fn/${name}`,
618
631
  { method: "POST", json: args },
619
632
  );
@@ -773,10 +786,7 @@ export async function* streamFn(
773
786
  args: Record<string, unknown> = {},
774
787
  options: StreamFnOptions = {}
775
788
  ): AsyncGenerator<string, unknown, unknown> {
776
- const transport = {
777
- baseUrl: getBaseUrl(),
778
- getToken: () => options.token ?? currentAuthToken() ?? undefined,
779
- };
789
+ const transport = clientTransport(options.token);
780
790
  // Streaming response — use pylonFetchRaw so we can read .body
781
791
  // ourselves. URL + auth + credentials are centralized in the
782
792
  // transport.
@@ -877,10 +887,7 @@ export async function* resumeStream(
877
887
  streamId: string,
878
888
  options: { token?: string; since?: number } = {}
879
889
  ): AsyncGenerator<string, unknown, unknown> {
880
- const transport = {
881
- baseUrl: getBaseUrl(),
882
- getToken: () => options.token ?? currentAuthToken() ?? undefined,
883
- };
890
+ const transport = clientTransport(options.token);
884
891
  const cursor: SseCursor = {
885
892
  lastSeq: options.since ?? 0,
886
893
  terminal: false,
@@ -1018,10 +1025,7 @@ export async function uploadFile(
1018
1025
  filename ??= "upload";
1019
1026
  contentType ??= "application/octet-stream";
1020
1027
 
1021
- const transport = {
1022
- baseUrl: getBaseUrl(),
1023
- getToken: () => options.token ?? currentAuthToken() ?? undefined,
1024
- };
1028
+ const transport = clientTransport(options.token);
1025
1029
 
1026
1030
  const slot = await pylonFetch<{ uploadUrl: string; assetId: string }>(
1027
1031
  transport,