]> git.ardis.lu - envpass/commitdiff
Calculate client-side window expiration from `exp` query param
authorArdis Lu <ardisglu@gmail.com>
Sat, 15 Aug 2026 05:59:59 +0000 (22:59 -0700)
committerArdis Lu <ardisglu@gmail.com>
Sat, 15 Aug 2026 05:59:59 +0000 (22:59 -0700)
This change makes the client-side expiration in sync with the server-side expiration. Previously, if the client-side window is opened later than the server's start time, the expirations would be de-synced. It is also a maintenance burden to keep the duration constants in sync between the two.

src/getPrf.html
src/getPrf.js
test/getPrf.test.js

index 91ab97aea36d66e478434366c3cfb319b1209960..255b4407648fcaaf6c176bdb38d1ac35bcb062d5 100644 (file)
   <p>Create or select a passkey, or <button class="link">click here</button> if no prompt appeared. This window will expire in 2 minutes.</p>
 
   <script type="module">
-    // Extract and delete challenge from URL
-    const { challenge } = Object.fromEntries(new URLSearchParams(location.search));
+    const { challenge, exp } = Object.fromEntries(new URLSearchParams(location.search));
     history.replaceState(null, '', location.pathname);
 
     const p = document.querySelector('p');
 
-    const WINDOW_EXPIRATION_DURATION = 2 * 60 * 1000; // 2 minutes in milliseconds
-    const timeoutId = setTimeout(() => p.textContent = 'This window has expired.', WINDOW_EXPIRATION_DURATION);
+    const expiration = Number(exp) - Date.now();
+    const timeoutId = setTimeout(() => p.textContent = 'This window has expired.', expiration);
 
     async function createPrf(signal) {
       signal.throwIfAborted();
index 3a66334ad54a6327bebfa773e91e023796ea684e..94922c2164239a7682c3cd6f47f1d844a6cb8a47 100644 (file)
@@ -94,6 +94,7 @@ export async function getPrf(options = {}) {
 
   // Flow expires after 2 minutes or canceled
   const timeoutId = setTimeout(() => server.close(() => abortResolve(null)), WINDOW_EXPIRATION_DURATION);
+  const timeoutExp = Date.now() + WINDOW_EXPIRATION_DURATION;
   if (signal !== undefined) {
     signal.addEventListener('abort', () => {
       clearTimeout(timeoutId);
@@ -193,7 +194,7 @@ export async function getPrf(options = {}) {
   server.listen(port, 'localhost');
   server.on('listening', () => {
     const { port } = /** @type {AddressInfo} */(server.address());
-    const url = `http://localhost:${port}?challenge=${challenge}`;
+    const url = `http://localhost:${port}?challenge=${challenge}&exp=${timeoutExp}`;
     urlResolve(url);
     if (autoOpen) { BROWSER.open(url); }
   });
index e5bdedfacedad86e0d5e7e36ac0a805f5800d0eb..917a4f938986e28aa22c9aa9d38d09b4c00ebf65 100644 (file)
@@ -224,6 +224,20 @@ suite('getPrf.html (client)', () => {
     await page.clock.runFor(WINDOW_EXPIRATION_DURATION);
     match(await page.locator('p').textContent() ?? '', /^This window has expired/);
   });
+  test('window expiration is calculated from a set date, not from hardcoded duration', async (t) => {
+    const { setAutomaticSignIn, page } = await setupPlaywright(t);
+    const { url } = await setupServer(t);
+
+    await setAutomaticSignIn(false);
+
+    await page.clock.install();
+    await page.clock.runFor(WINDOW_EXPIRATION_DURATION / 2);
+    await page.goto(url);
+
+    match(await page.locator('p').textContent() ?? '', /^Create or select a passkey/);
+    await page.clock.runFor(WINDOW_EXPIRATION_DURATION / 2);
+    match(await page.locator('p').textContent() ?? '', /^This window has expired/);
+  });
   test('create passkey on page load works', async (t) => {
     const { page } = await setupPlaywright(t);
     const { url, prf } = await setupServer(t);
This page took 0.043014 seconds and 4 git commands to generate.