@pylonsync/sync 0.22.15 → 0.22.18
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 +11 -1
- package/dist/transport.d.ts +25 -0
- package/package.json +1 -1
- package/src/bearer-rejected.test.ts +146 -0
- package/src/index.ts +35 -1
- package/src/transport.ts +65 -5
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { SessionResolver } from "./session-resolver";
|
|
|
5
5
|
export { RoomSubscriptions, type RoomError, type RoomErrorCode, type RoomMember, type RoomMessage, type RoomMessageSubscriber, type RoomSubscriber, } from "./room-subscriptions";
|
|
6
6
|
export { IndexedDBPersistence, persistChange } from "./persistence";
|
|
7
7
|
export type { ReplicaPersistence } from "./persistence";
|
|
8
|
-
export { buildRequest, pylonFetch, pylonFetchRaw, PylonHttpError, resolveBaseUrl, } from "./transport";
|
|
8
|
+
export { BEARER_REJECTED_HEADER, bearerRejected, buildRequest, clearRejectedToken, pylonFetch, pylonFetchRaw, PylonHttpError, resolveBaseUrl, } from "./transport";
|
|
9
9
|
export type { PylonRequestInit, TransportConfig } from "./transport";
|
|
10
10
|
export { LocalStore } from "./local-store";
|
|
11
11
|
export { MutationQueue, MutationRejectedError, type MutationQueuePersistence, type PendingMutation, } from "./mutation-queue";
|
|
@@ -805,6 +805,16 @@ export declare class SyncEngine {
|
|
|
805
805
|
* A genuinely signed-out user has no stored token, so this stays quiet.
|
|
806
806
|
*/
|
|
807
807
|
private warnForeignToken;
|
|
808
|
+
/**
|
|
809
|
+
* The server answered with `X-Pylon-Bearer-Rejected`: the token the request
|
|
810
|
+
* sent resolved to no one (expired, revoked, or not a session of this app).
|
|
811
|
+
* Clear it from the app's namespace so later requests stop sending it and
|
|
812
|
+
* the session cookie, when there is one, applies alone. A token passed in
|
|
813
|
+
* `config.token` belongs to the caller and stays.
|
|
814
|
+
*/
|
|
815
|
+
private dropRejectedToken;
|
|
816
|
+
/** `dropRejectedToken` for a response to a request that sent `token`. */
|
|
817
|
+
private observeBearerVerdict;
|
|
808
818
|
/**
|
|
809
819
|
* Apply a freshly-observed session through the resolver and act on
|
|
810
820
|
* the verdict. Serialized via `sessionChain` so concurrent triggers
|
package/dist/transport.d.ts
CHANGED
|
@@ -16,7 +16,31 @@ export interface TransportConfig {
|
|
|
16
16
|
* header when the server sets one. Returning a Promise pauses no
|
|
17
17
|
* one — the transport doesn't await it. */
|
|
18
18
|
onChangeSeq?: (seq: number) => void;
|
|
19
|
+
/** Invoked with the bearer token a request sent when the server answers
|
|
20
|
+
* with `X-Pylon-Bearer-Rejected: 1`: the server resolved the token to no
|
|
21
|
+
* one (unknown, expired, or revoked). The code that stored the token
|
|
22
|
+
* clears it here, so later requests stop sending it. */
|
|
23
|
+
onBearerRejected?: (token: string) => void;
|
|
19
24
|
}
|
|
25
|
+
/** Response header the server sets when the request's bearer token resolved
|
|
26
|
+
* to no one. Lower case, as `Headers.get` matches without case. */
|
|
27
|
+
export declare const BEARER_REJECTED_HEADER = "x-pylon-bearer-rejected";
|
|
28
|
+
/** True when the server marked the bearer token the request sent as rejected. */
|
|
29
|
+
export declare function bearerRejected(res: Response): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Remove a token the server rejected from `storage`, together with its
|
|
32
|
+
* `:userId` sibling. Only removes it when `key` still holds that exact token,
|
|
33
|
+
* so a sign-in that wrote a new token while the request was in flight keeps
|
|
34
|
+
* its token.
|
|
35
|
+
*
|
|
36
|
+
* The default unprefixed key `pylon_token` is left alone. Every app on an
|
|
37
|
+
* origin that has no `appName` shares it, so the token can belong to another
|
|
38
|
+
* app. Returns true when the token was removed.
|
|
39
|
+
*/
|
|
40
|
+
export declare function clearRejectedToken(storage: {
|
|
41
|
+
get(key: string): string | null;
|
|
42
|
+
remove(key: string): void;
|
|
43
|
+
}, key: string, token: string): boolean;
|
|
20
44
|
/**
|
|
21
45
|
* Per-call init shape. Mirrors the standard `RequestInit` but
|
|
22
46
|
* normalizes the two body shapes (JSON-encode-this vs raw-pass-
|
|
@@ -56,6 +80,7 @@ export declare function resolveBaseUrl(config: TransportConfig): string;
|
|
|
56
80
|
export declare function buildRequest(config: TransportConfig, path: string, init?: PylonRequestInit): {
|
|
57
81
|
url: string;
|
|
58
82
|
init: RequestInit;
|
|
83
|
+
token?: string;
|
|
59
84
|
};
|
|
60
85
|
/** Error thrown by `pylonFetch` for non-2xx responses. Carries the
|
|
61
86
|
* status + structured error code + the parsed JSON body (if any). */
|
package/package.json
CHANGED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// A stored token the server rejects is cleared. The server answers a request
|
|
2
|
+
// whose bearer token resolved to no one (expired, revoked, or not a session
|
|
3
|
+
// of this app) with `X-Pylon-Bearer-Rejected: 1`, and falls back to the
|
|
4
|
+
// session cookie when it can. Without the client clearing the token, every
|
|
5
|
+
// later request keeps sending it.
|
|
6
|
+
|
|
7
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
BEARER_REJECTED_HEADER,
|
|
11
|
+
clearRejectedToken,
|
|
12
|
+
pylonFetch,
|
|
13
|
+
pylonFetchRaw,
|
|
14
|
+
SyncEngine,
|
|
15
|
+
} from "./index";
|
|
16
|
+
import type { Storage } from "./storage";
|
|
17
|
+
|
|
18
|
+
function memoryStorage(initial: Record<string, string> = {}): Storage & {
|
|
19
|
+
data: Map<string, string>;
|
|
20
|
+
} {
|
|
21
|
+
const data = new Map(Object.entries(initial));
|
|
22
|
+
return {
|
|
23
|
+
data,
|
|
24
|
+
get: (k) => data.get(k) ?? null,
|
|
25
|
+
set: (k, v) => void data.set(k, v),
|
|
26
|
+
remove: (k) => void data.delete(k),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const realFetch = globalThis.fetch;
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
globalThis.fetch = realFetch;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/** Stub fetch: records each request's Authorization header and answers
|
|
36
|
+
* with `X-Pylon-Bearer-Rejected: 1` while `rejected(token)` is true. */
|
|
37
|
+
function stubFetch(rejected: (token: string | undefined) => boolean, body: unknown = {}) {
|
|
38
|
+
const auth: Array<string | undefined> = [];
|
|
39
|
+
globalThis.fetch = (async (_input: RequestInfo | URL, init: RequestInit = {}) => {
|
|
40
|
+
const h = (init.headers ?? {}) as Record<string, string>;
|
|
41
|
+
const header = h.Authorization;
|
|
42
|
+
auth.push(header);
|
|
43
|
+
const token = header?.replace(/^Bearer /, "");
|
|
44
|
+
const headers: Record<string, string> = { "content-type": "application/json" };
|
|
45
|
+
if (rejected(token)) headers[BEARER_REJECTED_HEADER] = "1";
|
|
46
|
+
return new Response(JSON.stringify(body), { status: 200, headers });
|
|
47
|
+
}) as typeof fetch;
|
|
48
|
+
return auth;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe("transport: X-Pylon-Bearer-Rejected", () => {
|
|
52
|
+
test("pylonFetch reports the token it sent when the server rejects it", async () => {
|
|
53
|
+
stubFetch(() => true);
|
|
54
|
+
const seen: string[] = [];
|
|
55
|
+
await pylonFetch(
|
|
56
|
+
{ baseUrl: "http://t.invalid", token: "stale", onBearerRejected: (t) => seen.push(t) },
|
|
57
|
+
"/api/fn/x",
|
|
58
|
+
{ method: "POST", json: {} },
|
|
59
|
+
);
|
|
60
|
+
expect(seen).toEqual(["stale"]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("pylonFetchRaw reports it too", async () => {
|
|
64
|
+
stubFetch(() => true);
|
|
65
|
+
const seen: string[] = [];
|
|
66
|
+
await pylonFetchRaw(
|
|
67
|
+
{ baseUrl: "http://t.invalid", token: "stale", onBearerRejected: (t) => seen.push(t) },
|
|
68
|
+
"/api/fn/x",
|
|
69
|
+
{ method: "POST", json: {} },
|
|
70
|
+
);
|
|
71
|
+
expect(seen).toEqual(["stale"]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("nothing is reported without the header, or when no token was sent", async () => {
|
|
75
|
+
const seen: string[] = [];
|
|
76
|
+
stubFetch(() => false);
|
|
77
|
+
await pylonFetch(
|
|
78
|
+
{ baseUrl: "http://t.invalid", token: "good", onBearerRejected: (t) => seen.push(t) },
|
|
79
|
+
"/api/auth/me",
|
|
80
|
+
);
|
|
81
|
+
stubFetch(() => true);
|
|
82
|
+
await pylonFetch(
|
|
83
|
+
{ baseUrl: "http://t.invalid", onBearerRejected: (t) => seen.push(t) },
|
|
84
|
+
"/api/auth/me",
|
|
85
|
+
);
|
|
86
|
+
expect(seen).toEqual([]);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("clearRejectedToken", () => {
|
|
91
|
+
test("removes the app's token and user id when the key still holds that token", () => {
|
|
92
|
+
const s = memoryStorage({ "pylon:miles:token": "stale", "pylon:miles:userId": "u1" });
|
|
93
|
+
expect(clearRejectedToken(s, "pylon:miles:token", "stale")).toBe(true);
|
|
94
|
+
expect(s.data.size).toBe(0);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("keeps a token that a sign-in wrote while the request was in flight", () => {
|
|
98
|
+
const s = memoryStorage({ "pylon:miles:token": "fresh" });
|
|
99
|
+
expect(clearRejectedToken(s, "pylon:miles:token", "stale")).toBe(false);
|
|
100
|
+
expect(s.get("pylon:miles:token")).toBe("fresh");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("leaves the shared default key alone", () => {
|
|
104
|
+
const s = memoryStorage({ pylon_token: "stale" });
|
|
105
|
+
expect(clearRejectedToken(s, "pylon_token", "stale")).toBe(false);
|
|
106
|
+
expect(s.get("pylon_token")).toBe("stale");
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("SyncEngine clears a stored token the server rejects", () => {
|
|
111
|
+
function engine(storage: Storage, extra: Record<string, unknown> = {}) {
|
|
112
|
+
return new SyncEngine({
|
|
113
|
+
baseUrl: "http://t.invalid",
|
|
114
|
+
appName: "miles",
|
|
115
|
+
storage,
|
|
116
|
+
persist: false,
|
|
117
|
+
multiTab: false,
|
|
118
|
+
transport: "poll",
|
|
119
|
+
...extra,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
test("fn(): the next call sends no Authorization header", async () => {
|
|
124
|
+
const storage = memoryStorage({ "pylon:miles:token": "stale" });
|
|
125
|
+
const auth = stubFetch((t) => t === "stale", { ok: true });
|
|
126
|
+
const e = engine(storage);
|
|
127
|
+
await e.fn("resetDemoDealership", {});
|
|
128
|
+
expect(storage.get("pylon:miles:token")).toBeNull();
|
|
129
|
+
await e.fn("resetDemoDealership", {});
|
|
130
|
+
expect(auth).toEqual(["Bearer stale", undefined]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("a token the server accepts stays", async () => {
|
|
134
|
+
const storage = memoryStorage({ "pylon:miles:token": "good" });
|
|
135
|
+
stubFetch(() => false, { ok: true });
|
|
136
|
+
await engine(storage).fn("resetDemoDealership", {});
|
|
137
|
+
expect(storage.get("pylon:miles:token")).toBe("good");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("an explicitly configured token is the caller's and stays", async () => {
|
|
141
|
+
const storage = memoryStorage({ "pylon:miles:token": "stale" });
|
|
142
|
+
stubFetch(() => true, { ok: true });
|
|
143
|
+
await engine(storage, { token: "stale" }).fn("resetDemoDealership", {});
|
|
144
|
+
expect(storage.get("pylon:miles:token")).toBe("stale");
|
|
145
|
+
});
|
|
146
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
|
+
bearerRejected,
|
|
13
|
+
clearRejectedToken,
|
|
12
14
|
pylonFetch,
|
|
13
15
|
PylonHttpError,
|
|
14
16
|
type TransportConfig,
|
|
@@ -45,7 +47,10 @@ export {
|
|
|
45
47
|
export { IndexedDBPersistence, persistChange } from "./persistence";
|
|
46
48
|
export type { ReplicaPersistence } from "./persistence";
|
|
47
49
|
export {
|
|
50
|
+
BEARER_REJECTED_HEADER,
|
|
51
|
+
bearerRejected,
|
|
48
52
|
buildRequest,
|
|
53
|
+
clearRejectedToken,
|
|
49
54
|
pylonFetch,
|
|
50
55
|
pylonFetchRaw,
|
|
51
56
|
PylonHttpError,
|
|
@@ -1922,6 +1927,7 @@ export class SyncEngine {
|
|
|
1922
1927
|
void this.pull();
|
|
1923
1928
|
}
|
|
1924
1929
|
},
|
|
1930
|
+
onBearerRejected: (token) => this.dropRejectedToken(token),
|
|
1925
1931
|
},
|
|
1926
1932
|
path,
|
|
1927
1933
|
{
|
|
@@ -2700,6 +2706,29 @@ export class SyncEngine {
|
|
|
2700
2706
|
);
|
|
2701
2707
|
}
|
|
2702
2708
|
|
|
2709
|
+
/**
|
|
2710
|
+
* The server answered with `X-Pylon-Bearer-Rejected`: the token the request
|
|
2711
|
+
* sent resolved to no one (expired, revoked, or not a session of this app).
|
|
2712
|
+
* Clear it from the app's namespace so later requests stop sending it and
|
|
2713
|
+
* the session cookie, when there is one, applies alone. A token passed in
|
|
2714
|
+
* `config.token` belongs to the caller and stays.
|
|
2715
|
+
*/
|
|
2716
|
+
private dropRejectedToken(token: string): void {
|
|
2717
|
+
if (this.config.token) return;
|
|
2718
|
+
if (clearRejectedToken(this.storage, this.tokenStorageKey(), token)) {
|
|
2719
|
+
console.warn(
|
|
2720
|
+
"[pylon] the server rejected the stored auth token (expired, revoked, " +
|
|
2721
|
+
"or issued by another app) and it has been cleared. Requests now " +
|
|
2722
|
+
"authenticate with the session cookie when there is one.",
|
|
2723
|
+
);
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2727
|
+
/** `dropRejectedToken` for a response to a request that sent `token`. */
|
|
2728
|
+
private observeBearerVerdict(res: Response, token: string | null | undefined): void {
|
|
2729
|
+
if (token && bearerRejected(res)) this.dropRejectedToken(token);
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2703
2732
|
/**
|
|
2704
2733
|
* Apply a freshly-observed session through the resolver and act on
|
|
2705
2734
|
* the verdict. Serialized via `sessionChain` so concurrent triggers
|
|
@@ -2899,10 +2928,12 @@ export class SyncEngine {
|
|
|
2899
2928
|
// same → `resetReplica` never fires on /api/auth/select-org
|
|
2900
2929
|
// → the local store keeps every previous tenant's rows in
|
|
2901
2930
|
// cache and `db.useQuery` returns stale data after a switch.
|
|
2902
|
-
|
|
2931
|
+
const res = await fetch(`${this.config.baseUrl}${path}`, {
|
|
2903
2932
|
headers,
|
|
2904
2933
|
credentials: "include",
|
|
2905
2934
|
});
|
|
2935
|
+
this.observeBearerVerdict(res, token);
|
|
2936
|
+
return res;
|
|
2906
2937
|
}
|
|
2907
2938
|
|
|
2908
2939
|
/**
|
|
@@ -3040,6 +3071,7 @@ export class SyncEngine {
|
|
|
3040
3071
|
headers,
|
|
3041
3072
|
credentials: "include",
|
|
3042
3073
|
});
|
|
3074
|
+
this.observeBearerVerdict(res, token);
|
|
3043
3075
|
if (!res.ok) return null;
|
|
3044
3076
|
const body = (await res.json()) as { token?: string; url?: string };
|
|
3045
3077
|
if (!body.token || !body.url) return null;
|
|
@@ -3076,6 +3108,7 @@ export class SyncEngine {
|
|
|
3076
3108
|
credentials: "include",
|
|
3077
3109
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
3078
3110
|
});
|
|
3111
|
+
this.observeBearerVerdict(res, token);
|
|
3079
3112
|
const text = await res.text();
|
|
3080
3113
|
let parsed: unknown = null;
|
|
3081
3114
|
if (text) {
|
|
@@ -4220,6 +4253,7 @@ export class SyncEngine {
|
|
|
4220
4253
|
credentials: "include",
|
|
4221
4254
|
body: body ? JSON.stringify(body) : undefined,
|
|
4222
4255
|
});
|
|
4256
|
+
this.observeBearerVerdict(res, token);
|
|
4223
4257
|
|
|
4224
4258
|
if (!res.ok) {
|
|
4225
4259
|
// Surface the status so the caller can distinguish transient
|
package/src/transport.ts
CHANGED
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
// - X-Pylon-Change-Seq response header surfaced via the optional
|
|
17
17
|
// `onChangeSeq` callback (so callers can trigger catch-up pulls
|
|
18
18
|
// when a mutation's seq exceeds their local cursor)
|
|
19
|
+
// - X-Pylon-Bearer-Rejected response header surfaced via the optional
|
|
20
|
+
// `onBearerRejected` callback (so the owner of a stored token can
|
|
21
|
+
// clear a token the server no longer accepts)
|
|
19
22
|
//
|
|
20
23
|
// Streaming + raw-binary callers (file uploads, SSE) bypass the
|
|
21
24
|
// JSON-parse step but reuse the URL/auth/credentials logic via
|
|
@@ -40,6 +43,48 @@ export interface TransportConfig {
|
|
|
40
43
|
* header when the server sets one. Returning a Promise pauses no
|
|
41
44
|
* one — the transport doesn't await it. */
|
|
42
45
|
onChangeSeq?: (seq: number) => void;
|
|
46
|
+
/** Invoked with the bearer token a request sent when the server answers
|
|
47
|
+
* with `X-Pylon-Bearer-Rejected: 1`: the server resolved the token to no
|
|
48
|
+
* one (unknown, expired, or revoked). The code that stored the token
|
|
49
|
+
* clears it here, so later requests stop sending it. */
|
|
50
|
+
onBearerRejected?: (token: string) => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Response header the server sets when the request's bearer token resolved
|
|
54
|
+
* to no one. Lower case, as `Headers.get` matches without case. */
|
|
55
|
+
export const BEARER_REJECTED_HEADER = "x-pylon-bearer-rejected";
|
|
56
|
+
|
|
57
|
+
/** True when the server marked the bearer token the request sent as rejected. */
|
|
58
|
+
export function bearerRejected(res: Response): boolean {
|
|
59
|
+
return res.headers?.get?.(BEARER_REJECTED_HEADER) === "1";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Remove a token the server rejected from `storage`, together with its
|
|
64
|
+
* `:userId` sibling. Only removes it when `key` still holds that exact token,
|
|
65
|
+
* so a sign-in that wrote a new token while the request was in flight keeps
|
|
66
|
+
* its token.
|
|
67
|
+
*
|
|
68
|
+
* The default unprefixed key `pylon_token` is left alone. Every app on an
|
|
69
|
+
* origin that has no `appName` shares it, so the token can belong to another
|
|
70
|
+
* app. Returns true when the token was removed.
|
|
71
|
+
*/
|
|
72
|
+
export function clearRejectedToken(
|
|
73
|
+
storage: { get(key: string): string | null; remove(key: string): void },
|
|
74
|
+
key: string,
|
|
75
|
+
token: string,
|
|
76
|
+
): boolean {
|
|
77
|
+
if (key === "pylon_token") return false;
|
|
78
|
+
try {
|
|
79
|
+
if (storage.get(key) !== token) return false;
|
|
80
|
+
storage.remove(key);
|
|
81
|
+
if (key.endsWith(":token")) storage.remove(key.replace(/:token$/, ":userId"));
|
|
82
|
+
return true;
|
|
83
|
+
} catch {
|
|
84
|
+
// Storage can throw (Safari private mode). The token stays; the server
|
|
85
|
+
// keeps answering with the session cookie.
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
43
88
|
}
|
|
44
89
|
|
|
45
90
|
/**
|
|
@@ -90,13 +135,13 @@ export function buildRequest(
|
|
|
90
135
|
config: TransportConfig,
|
|
91
136
|
path: string,
|
|
92
137
|
init: PylonRequestInit = {},
|
|
93
|
-
): { url: string; init: RequestInit } {
|
|
138
|
+
): { url: string; init: RequestInit; token?: string } {
|
|
94
139
|
const base = resolveBaseUrl(config);
|
|
95
140
|
const url = `${base}${path}`;
|
|
96
141
|
const headers: Record<string, string> = { ...(init.headers ?? {}) };
|
|
97
142
|
// Auth: explicit getToken wins over static token. Either may be
|
|
98
143
|
// null/undefined — cookie-auth apps rely solely on credentials.
|
|
99
|
-
const token = config.getToken?.() ?? config.token;
|
|
144
|
+
const token = config.getToken?.() ?? config.token ?? undefined;
|
|
100
145
|
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
101
146
|
if (init.accept) headers["Accept"] = init.accept;
|
|
102
147
|
let body: BodyInit | undefined = init.body;
|
|
@@ -106,6 +151,7 @@ export function buildRequest(
|
|
|
106
151
|
}
|
|
107
152
|
return {
|
|
108
153
|
url,
|
|
154
|
+
token: token || undefined,
|
|
109
155
|
init: {
|
|
110
156
|
method: init.method ?? (body !== undefined ? "POST" : "GET"),
|
|
111
157
|
headers,
|
|
@@ -153,8 +199,9 @@ export async function pylonFetch<T = unknown>(
|
|
|
153
199
|
path: string,
|
|
154
200
|
init: PylonRequestInit = {},
|
|
155
201
|
): Promise<T> {
|
|
156
|
-
const { url, init: req } = buildRequest(config, path, init);
|
|
202
|
+
const { url, init: req, token } = buildRequest(config, path, init);
|
|
157
203
|
const res = await fetch(url, req);
|
|
204
|
+
reportBearerVerdict(config, res, token);
|
|
158
205
|
const text = await res.text();
|
|
159
206
|
let parsed: unknown = null;
|
|
160
207
|
if (text) {
|
|
@@ -200,6 +247,19 @@ export async function pylonFetchRaw(
|
|
|
200
247
|
path: string,
|
|
201
248
|
init: PylonRequestInit = {},
|
|
202
249
|
): Promise<Response> {
|
|
203
|
-
const { url, init: req } = buildRequest(config, path, init);
|
|
204
|
-
|
|
250
|
+
const { url, init: req, token } = buildRequest(config, path, init);
|
|
251
|
+
const res = await fetch(url, req);
|
|
252
|
+
reportBearerVerdict(config, res, token);
|
|
253
|
+
return res;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Call `config.onBearerRejected` when the server rejected the token sent. */
|
|
257
|
+
function reportBearerVerdict(
|
|
258
|
+
config: TransportConfig,
|
|
259
|
+
res: Response,
|
|
260
|
+
token: string | undefined,
|
|
261
|
+
): void {
|
|
262
|
+
if (token && config.onBearerRejected && bearerRejected(res)) {
|
|
263
|
+
config.onBearerRejected(token);
|
|
264
|
+
}
|
|
205
265
|
}
|