@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/core",
3
- "version": "3.4.17",
3
+ "version": "3.4.18",
4
4
  "description": "OxyHQ SDK Foundation — API client, authentication, cryptographic identity, and shared utilities",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -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
- // Refresh failed — don't use the expired token (would cause 401 loop)
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) => {