@oxyhq/core 3.4.17 → 3.4.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/package.json
CHANGED
package/src/HttpService.ts
CHANGED
|
@@ -869,7 +869,10 @@ export class HttpService {
|
|
|
869
869
|
if (decoded.exp && decoded.exp - currentTime < 60) {
|
|
870
870
|
const refreshed = await this.refreshAccessToken('preflight');
|
|
871
871
|
if (refreshed) return `Bearer ${refreshed}`;
|
|
872
|
-
|
|
872
|
+
if (decoded.exp > currentTime) {
|
|
873
|
+
return `Bearer ${accessToken}`;
|
|
874
|
+
}
|
|
875
|
+
// Refresh failed — don't use an expired token (would cause 401 loop)
|
|
873
876
|
return null;
|
|
874
877
|
}
|
|
875
878
|
|
|
@@ -63,6 +63,74 @@ describe('HttpService CSRF behavior', () => {
|
|
|
63
63
|
expect(headers['X-CSRF-Token']).toBeUndefined();
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
+
it('keeps a valid near-expiry bearer token when preflight refresh cannot refresh', async () => {
|
|
67
|
+
const calls: FetchCall[] = [];
|
|
68
|
+
globalThis.fetch = async (input, init) => {
|
|
69
|
+
const url = String(input);
|
|
70
|
+
calls.push({ url, init });
|
|
71
|
+
return jsonResponse({ ok: true });
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const http = new HttpService({ baseURL: 'https://api.mention.earth', enableRetry: false });
|
|
75
|
+
const accessToken = createJwt({
|
|
76
|
+
userId: 'user_1',
|
|
77
|
+
exp: Math.floor(Date.now() / 1000) + 30,
|
|
78
|
+
});
|
|
79
|
+
let refreshAttempts = 0;
|
|
80
|
+
http.setTokens(accessToken);
|
|
81
|
+
http.setAuthRefreshHandler(async () => {
|
|
82
|
+
refreshAttempts += 1;
|
|
83
|
+
return null;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
await http.post('/posts', { text: 'hello' });
|
|
87
|
+
|
|
88
|
+
expect(refreshAttempts).toBe(1);
|
|
89
|
+
expect(calls).toHaveLength(1);
|
|
90
|
+
expect(calls[0].url).toBe('https://api.mention.earth/posts');
|
|
91
|
+
const headers = readHeaders(calls[0].init);
|
|
92
|
+
expect(headers.Authorization).toBe(`Bearer ${accessToken}`);
|
|
93
|
+
expect(headers['X-CSRF-Token']).toBeUndefined();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('does not use an expired bearer token when preflight refresh cannot refresh', async () => {
|
|
97
|
+
const calls: FetchCall[] = [];
|
|
98
|
+
globalThis.fetch = async (input, init) => {
|
|
99
|
+
const url = String(input);
|
|
100
|
+
calls.push({ url, init });
|
|
101
|
+
if (url.endsWith('/csrf-token')) {
|
|
102
|
+
return new Response(JSON.stringify({ csrfToken: 'csrf_1' }), {
|
|
103
|
+
status: 200,
|
|
104
|
+
headers: { 'content-type': 'application/json' },
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return jsonResponse({ ok: true });
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const http = new HttpService({ baseURL: 'https://api.mention.earth', enableRetry: false });
|
|
111
|
+
const accessToken = createJwt({
|
|
112
|
+
userId: 'user_1',
|
|
113
|
+
exp: Math.floor(Date.now() / 1000) - 10,
|
|
114
|
+
});
|
|
115
|
+
let refreshAttempts = 0;
|
|
116
|
+
http.setTokens(accessToken);
|
|
117
|
+
http.setAuthRefreshHandler(async () => {
|
|
118
|
+
refreshAttempts += 1;
|
|
119
|
+
return null;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
await http.post('/posts', { text: 'hello' });
|
|
123
|
+
|
|
124
|
+
expect(refreshAttempts).toBe(1);
|
|
125
|
+
expect(calls.map((call) => call.url)).toEqual([
|
|
126
|
+
'https://api.mention.earth/csrf-token',
|
|
127
|
+
'https://api.mention.earth/posts',
|
|
128
|
+
]);
|
|
129
|
+
const headers = readHeaders(calls[1].init);
|
|
130
|
+
expect(headers.Authorization).toBeUndefined();
|
|
131
|
+
expect(headers['X-CSRF-Token']).toBe('csrf_1');
|
|
132
|
+
});
|
|
133
|
+
|
|
66
134
|
it('still fetches csrf-token for cookie-authenticated writes without bearer', async () => {
|
|
67
135
|
const calls: FetchCall[] = [];
|
|
68
136
|
globalThis.fetch = async (input, init) => {
|