@oxyhq/core 1.11.20 → 1.11.22
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/README.md +6 -2
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/mixins/OxyServices.auth.js +14 -1
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/mixins/OxyServices.auth.js +14 -1
- package/dist/types/.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/mixins/OxyServices.auth.ts +16 -1
- package/src/mixins/__tests__/verifyChallenge.test.ts +135 -0
package/package.json
CHANGED
|
@@ -353,7 +353,7 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
353
353
|
deviceFingerprint?: string
|
|
354
354
|
): Promise<SessionLoginResponse> {
|
|
355
355
|
try {
|
|
356
|
-
|
|
356
|
+
const res = await this.makeRequest<SessionLoginResponse>('POST', '/auth/verify', {
|
|
357
357
|
publicKey,
|
|
358
358
|
challenge,
|
|
359
359
|
signature,
|
|
@@ -361,6 +361,21 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
361
361
|
deviceName,
|
|
362
362
|
deviceFingerprint,
|
|
363
363
|
}, { cache: false });
|
|
364
|
+
|
|
365
|
+
// Plant the freshly-minted tokens, mirroring `claimSessionByToken`.
|
|
366
|
+
// `/auth/verify` returns the first access token (and refresh token) in
|
|
367
|
+
// its body, so installing it here means callers get an authenticated
|
|
368
|
+
// client without a second round-trip — and, critically, without
|
|
369
|
+
// falling back to the bearer-protected `GET /session/token/:sessionId`
|
|
370
|
+
// (C1 hardening), which 401s for a brand-new identity that has no
|
|
371
|
+
// bearer yet. `accessToken`/`refreshToken` are optional on
|
|
372
|
+
// SessionLoginResponse; only plant when an access token is present and
|
|
373
|
+
// default the refresh token to an empty string.
|
|
374
|
+
if (res?.accessToken) {
|
|
375
|
+
this.setTokens(res.accessToken, res.refreshToken ?? '');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return res;
|
|
364
379
|
} catch (error) {
|
|
365
380
|
throw this.handleError(error);
|
|
366
381
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `verifyChallenge` token-planting regression tests.
|
|
3
|
+
*
|
|
4
|
+
* `OxyServices.verifyChallenge()` returns a `SessionLoginResponse` carrying the
|
|
5
|
+
* first `accessToken`/`refreshToken` minted by `POST /auth/verify`. It must
|
|
6
|
+
* PLANT those tokens internally — mirroring its sibling `claimSessionByToken` —
|
|
7
|
+
* so callers (e.g. @oxyhq/services' `useAuthOperations.performSignIn`) end up
|
|
8
|
+
* with an authenticated client WITHOUT falling back to the bearer-protected
|
|
9
|
+
* `GET /session/token/:sessionId`. That fallback 401s for a brand-new identity
|
|
10
|
+
* that has no bearer yet and previously broke the entire new-identity
|
|
11
|
+
* onboarding flow.
|
|
12
|
+
*
|
|
13
|
+
* These tests stub `makeRequest` so the planting logic is exercised end-to-end
|
|
14
|
+
* against a real OxyServices instance, with token state observed via the public
|
|
15
|
+
* `hasValidToken()` / `getAccessToken()` surface.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { OxyServices } from '../../OxyServices';
|
|
19
|
+
|
|
20
|
+
interface VerifyResponse {
|
|
21
|
+
sessionId: string;
|
|
22
|
+
deviceId: string;
|
|
23
|
+
expiresAt: string;
|
|
24
|
+
accessToken?: string;
|
|
25
|
+
refreshToken?: string;
|
|
26
|
+
user: { id: string; username: string };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function makeOxy(): OxyServices {
|
|
30
|
+
return new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('OxyServices.verifyChallenge token planting', () => {
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
jest.restoreAllMocks();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('plants the access + refresh token from the /auth/verify response body', async () => {
|
|
39
|
+
const oxy = makeOxy();
|
|
40
|
+
expect(oxy.hasValidToken()).toBe(false);
|
|
41
|
+
|
|
42
|
+
jest.spyOn(oxy, 'makeRequest').mockImplementation(async (_method, url) => {
|
|
43
|
+
if (url === '/auth/verify') {
|
|
44
|
+
return {
|
|
45
|
+
sessionId: 'sess_1',
|
|
46
|
+
deviceId: 'dev_1',
|
|
47
|
+
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
48
|
+
accessToken: 'access_verify',
|
|
49
|
+
refreshToken: 'refresh_verify',
|
|
50
|
+
user: { id: 'user_1', username: 'tester' },
|
|
51
|
+
} as never;
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`unexpected request to ${url}`);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const session = await oxy.verifyChallenge('pubkey', 'challenge', 'sig', 123, 'Device', 'fp');
|
|
57
|
+
|
|
58
|
+
// Response still carries the tokens for callers that want them.
|
|
59
|
+
expect(session.accessToken).toBe('access_verify');
|
|
60
|
+
// ...and they are now planted on the client so subsequent requests are
|
|
61
|
+
// authenticated without a second round-trip.
|
|
62
|
+
expect(oxy.hasValidToken()).toBe(true);
|
|
63
|
+
expect(oxy.getAccessToken()).toBe('access_verify');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('defaults the refresh token to an empty string when the response omits it', async () => {
|
|
67
|
+
const oxy = makeOxy();
|
|
68
|
+
|
|
69
|
+
jest.spyOn(oxy, 'makeRequest').mockImplementation(async (_method, url) => {
|
|
70
|
+
if (url === '/auth/verify') {
|
|
71
|
+
return {
|
|
72
|
+
sessionId: 'sess_2',
|
|
73
|
+
deviceId: 'dev_2',
|
|
74
|
+
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
75
|
+
accessToken: 'access_only',
|
|
76
|
+
user: { id: 'user_2', username: 'tester2' },
|
|
77
|
+
} as never;
|
|
78
|
+
}
|
|
79
|
+
throw new Error(`unexpected request to ${url}`);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const session = await oxy.verifyChallenge('pubkey', 'challenge', 'sig', 456);
|
|
83
|
+
|
|
84
|
+
expect(session.accessToken).toBe('access_only');
|
|
85
|
+
expect(oxy.hasValidToken()).toBe(true);
|
|
86
|
+
expect(oxy.getAccessToken()).toBe('access_only');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('does NOT plant (and stays unauthenticated) when the response carries no access token', async () => {
|
|
90
|
+
const oxy = makeOxy();
|
|
91
|
+
|
|
92
|
+
jest.spyOn(oxy, 'makeRequest').mockImplementation(async (_method, url) => {
|
|
93
|
+
if (url === '/auth/verify') {
|
|
94
|
+
// Token-less new identity (onboarding) — no access token in the body.
|
|
95
|
+
return {
|
|
96
|
+
sessionId: 'sess_3',
|
|
97
|
+
deviceId: 'dev_3',
|
|
98
|
+
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
99
|
+
user: { id: 'user_3', username: 'tester3' },
|
|
100
|
+
} as VerifyResponse as never;
|
|
101
|
+
}
|
|
102
|
+
throw new Error(`unexpected request to ${url}`);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const session = await oxy.verifyChallenge('pubkey', 'challenge', 'sig', 789);
|
|
106
|
+
|
|
107
|
+
expect(session.accessToken).toBeUndefined();
|
|
108
|
+
// No token to plant — the client stays unauthenticated. Crucially the
|
|
109
|
+
// method does NOT reach for the bearer-protected session-token endpoint.
|
|
110
|
+
expect(oxy.hasValidToken()).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('matches claimSessionByToken: both plant tokens via the same path', async () => {
|
|
114
|
+
const oxy = makeOxy();
|
|
115
|
+
|
|
116
|
+
jest.spyOn(oxy, 'makeRequest').mockImplementation(async (_method, url) => {
|
|
117
|
+
if (url === '/auth/session/claim') {
|
|
118
|
+
return {
|
|
119
|
+
accessToken: 'access_claim',
|
|
120
|
+
refreshToken: 'refresh_claim',
|
|
121
|
+
sessionId: 'sess_claim',
|
|
122
|
+
deviceId: 'dev_claim',
|
|
123
|
+
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
124
|
+
user: { id: 'user_claim', username: 'claimed' },
|
|
125
|
+
} as never;
|
|
126
|
+
}
|
|
127
|
+
throw new Error(`unexpected request to ${url}`);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
await oxy.claimSessionByToken('session-token-abc');
|
|
131
|
+
|
|
132
|
+
expect(oxy.hasValidToken()).toBe(true);
|
|
133
|
+
expect(oxy.getAccessToken()).toBe('access_claim');
|
|
134
|
+
});
|
|
135
|
+
});
|