1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// node --test javascript/src/oauth/renew-once.test.mjs (skips without Chrome, or when esm.sh is unreachable)
// The real BrowserOAuthClient (esm.sh, the pin examples/web/hello uses) in headless Chrome, signed in at a fake
// authorization server + PDS (javascript/src/oauth-server/fake.mjs), and two more pages of the same origin (frames:
// their own client and memory, the same IndexedDB session and Web Locks). Forced order: page 2's token is refused,
// page 1 renews it, and only then does page 2 go on with its refused token: sessionXrpc (renewOnce) must take the
// saved one, not refresh again (refresh tokens rotate). The library's own fetchHandler refreshes twice here.
import assert from "node:assert/strict";
import { test } from "node:test";
import { browse, CHROME } from "../../../docs/browser.mjs";
import { fakeAtproto } from "../oauth-server/fake.mjs";
const ROOT = new URL("../../../", import.meta.url).pathname;
const CLIENT = "https://esm.sh/@atproto/oauth-client-browser@0.5.14";
const online = await fetch(CLIENT, { signal: AbortSignal.timeout(5000) }).then((r) => r.ok, () => false);
// "/" (and "/?frame=<n>"): a page with its own BrowserOAuthClient; page.holdNextRefusal() holds its next refused
// response until released
const page = (fake) => `<!DOCTYPE html><script type="importmap">{"imports":{"atlibs/":"/javascript/src/","@atproto/oauth-client-browser":"${CLIENT}"}}</script>
<script type="module">
import { BrowserOAuthClient } from "@atproto/oauth-client-browser";
import { sessionXrpc } from "atlibs/run-module/host-call.js";
let hold;
const fetch = async (input, init) => {
const response = await globalThis.fetch(input, init);
if (hold && response.headers.get("www-authenticate")?.includes("invalid_token")) {
const held = hold;
hold = undefined;
held.reached();
await held.go;
}
return response;
};
const client = new BrowserOAuthClient({ handleResolver: "${fake}", plcDirectoryUrl: "${fake}", allowHttp: true, fetch });
window.page = {
ready: client.init(),
signIn: (handle) => void client.signIn(handle).catch(() => {}),
holdNextRefusal() {
const held = {};
const reached = new Promise((ok) => (held.reached = ok));
const go = new Promise((ok) => (held.go = ok));
hold = { reached: held.reached, go };
return { reached, release: held.go };
},
getSession: async (did) => sessionXrpc(await client.restore(did))("com.atproto.server.getSession"),
};
</script>`;
test("pages sharing a BrowserOAuthClient session renew a refused token once (sessionXrpc)", { skip: (!CHROME && "no Chrome") || (!online && `${CLIENT} unreachable`) }, async () => {
const fake = await fakeAtproto();
try {
await browse(
async ({ on, open, evaluate }) => {
await open("/");
await evaluate("page.ready");
const back = new Promise((ok) => on("Page.loadEventFired", ok));
await evaluate("page.signIn('alice.test')"); // to the fake's authorize, which approves and redirects back
await back;
const did = await evaluate("page.ready.then((signedIn) => signedIn?.session.did)");
assert.equal(did, fake.did("alice"));
await evaluate(`Promise.all([1, 2].map((n) => new Promise((ok) => { const f = document.createElement("iframe"); f.onload = ok; f.src = "/?frame=" + n; document.body.append(f) })))`);
await evaluate("Promise.all([frames[0], frames[1]].map((f) => f.page.ready))");
fake.expireAll();
fake.refreshes = 0;
await evaluate(`(window.held = frames[1].page.holdNextRefusal(), window.second = frames[1].page.getSession(${JSON.stringify(did)}), held.reached.then(() => true))`);
assert.equal(await evaluate(`frames[0].page.getSession(${JSON.stringify(did)}).then((s) => s.did)`), did);
assert.equal(fake.refreshes, 1, "page 1 renewed the refused token");
assert.equal(await evaluate("(held.release(), second.then((s) => s.did))"), did);
assert.equal(fake.refreshes, 1, "page 2 used page 1's renewal: one refresh across pages");
},
{ root: ROOT, index: page(fake.base) },
);
} finally {
await fake.close();
}
});