Skip to content

Commit faba6fa

Browse files
committed
fix(client): send authorizedFetch calls under the adapter mount as adapter calls
An application reaching the adapter's own routes through authorizedFetch (its session list, a passthrough it adds) got no transport header, so in bearer transport the adapter answered in cookie mode and refused the call. A URL under the mount now takes the same road as the client's own calls: the header, the route's identity, and its effect on the held tokens.
1 parent 94833ac commit faba6fa

4 files changed

Lines changed: 54 additions & 22 deletions

File tree

‎packages/client/README.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,10 @@ mirroring the server adapter's own map.
5656
`client.authorizedFetch(input, init)` is a fetch for the application's own API
5757
that carries the session the same way: cookies in cookie transport, the access
5858
token with one refresh-and-retry on a 401 in bearer transport. It never reads
59-
tokens out of the response, since that body is the application's.
59+
tokens out of the response, since that body is the application's. A URL under
60+
the adapter's own mount (`/auth/sessions`, a passthrough the adapter adds) is
61+
sent the way the client's own calls are, transport header included, so an
62+
application can reach every adapter route through the one fetch.
6063

6164
## Ports
6265

‎packages/client/src/transport.ts‎

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -347,29 +347,40 @@ export function createTransport(options: TransportOptions): Transport {
347347
return response;
348348
}
349349

350+
const mount = `${buildUrl(options.apiHost, basePath, '')}/`;
351+
352+
const fetchUnderMount: FetchWithAuth = async (input, init) => {
353+
const path = normalizePath(input);
354+
const rule = resolveRouteRule(path);
355+
356+
const response = await sendWithRefresh(
357+
buildUrl(options.apiHost, basePath, path),
358+
init,
359+
rule.identity,
360+
true
361+
);
362+
363+
if (response.ok && rule.effect) {
364+
await applyEffect(rule.effect, response);
365+
}
366+
367+
return response;
368+
};
369+
350370
return {
351371
mode,
352372
clearTokens,
353-
fetch: async (input, init) => {
354-
const path = normalizePath(input);
355-
const rule = resolveRouteRule(path);
356-
357-
const response = await sendWithRefresh(
358-
buildUrl(options.apiHost, basePath, path),
359-
init,
360-
rule.identity,
361-
true
362-
);
363-
364-
if (response.ok && rule.effect) {
365-
await applyEffect(rule.effect, response);
366-
}
367-
368-
return response;
369-
},
373+
fetch: fetchUnderMount,
370374
// The transport header is the adapter's; an application's own API only
371-
// needs the bearer token, which requireAuth reads.
372-
authorizedFetch: (input, init) =>
373-
sendWithRefresh(String(input), init, 'access', false),
375+
// needs the bearer token, which requireAuth reads. A URL under the
376+
// adapter's own mount (its session list, a passthrough it adds) is the
377+
// adapter's, though, and without the header it would answer in cookie
378+
// mode, so it takes the same road as the client's own calls.
379+
authorizedFetch: (input, init) => {
380+
const url = String(input);
381+
return url.startsWith(mount)
382+
? fetchUnderMount(url.slice(mount.length - 1), init)
383+
: sendWithRefresh(url, init, 'access', false);
384+
},
374385
};
375386
}

‎packages/client/tests/transport.node.test.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,23 @@ describe('bearer transport', () => {
457457
});
458458
});
459459

460+
it('authorizedFetch treats a URL under the adapter mount as an adapter call', async () => {
461+
const storage = createMemoryTokenStorage();
462+
await storage.set({ accessToken: 'access-1', refreshToken: 'refresh-1' });
463+
const { calls, fetchImpl } = scriptedFetch(() => jsonResponse(200, { sessions: [] }));
464+
465+
const response = await bearer(fetchImpl, storage).authorizedFetch(`${API}/auth/sessions`, {
466+
method: 'GET',
467+
});
468+
469+
expect(response.status).toBe(200);
470+
expect(calls[0].url).toBe(`${API}/auth/sessions`);
471+
expect(headersOf(calls[0])).toEqual({
472+
Authorization: 'Bearer access-1',
473+
[AUTH_TRANSPORT_HEADER]: 'bearer',
474+
});
475+
});
476+
460477
it('authorizedFetch refreshes once on a 401 and retries', async () => {
461478
const storage = createMemoryTokenStorage();
462479
await storage.set({ accessToken: 'access-old', refreshToken: 'refresh-old' });

‎packages/react-native/README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ function SignIn() {
9191
```
9292

9393
`useAuthorizedFetch()` is a fetch for your own API that carries the access
94-
token and refreshes it once on a 401; a path resolves on `apiHost`:
94+
token and refreshes it once on a 401; a path resolves on `apiHost`, and a path
95+
under the adapter's mount (`/auth/sessions`) is sent as an adapter call:
9596

9697
```ts
9798
const authorizedFetch = useAuthorizedFetch();

0 commit comments

Comments
 (0)