@openparachute/hub 0.7.4-rc.13 → 0.7.4-rc.14

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": "@openparachute/hub",
3
- "version": "0.7.4-rc.13",
3
+ "version": "0.7.4-rc.14",
4
4
  "description": "parachute — the local hub for the Parachute ecosystem (discovery, ports, lifecycle, soon OAuth).",
5
5
  "license": "AGPL-3.0",
6
6
  "publishConfig": {
@@ -527,7 +527,13 @@ describe("admin-lock management API", () => {
527
527
  const r2 = await handleAdminLock(lockReq("/heartbeat", cookie, {}), "/heartbeat", {
528
528
  db: harness.db,
529
529
  });
530
- expect(((await r2.json()) as { locked: boolean }).locked).toBe(false);
530
+ const body2 = (await r2.json()) as { locked: boolean; idle_seconds?: number };
531
+ expect(body2.locked).toBe(false);
532
+ // The heartbeat MUST carry idle_seconds — the client re-anchors its local
533
+ // idle timer from it on every heartbeat. Omitting it poisoned the timer
534
+ // (undefined → NaN → instant re-lock). Regression guard for the PIN
535
+ // re-prompt loop.
536
+ expect(body2.idle_seconds).toBe(getIdleSeconds(harness.db));
531
537
  });
532
538
 
533
539
  test("unlock brute-force limiter: 6th attempt is 429", async () => {
@@ -159,8 +159,15 @@ export async function handleAdminLock(
159
159
  case "/heartbeat":
160
160
  // Slide the idle window forward if (and only if) currently unlocked.
161
161
  refreshActivity(gate.sessionId, getIdleSeconds(db), now().getTime());
162
+ // `idle_seconds` is part of the response so the heartbeat fulfills the
163
+ // same `AdminLockStatus` shape as GET status — the client re-anchors its
164
+ // local idle timer from it on every heartbeat, so it MUST be present.
165
+ // Omitting it poisoned the client timer with `undefined` (→ NaN → instant
166
+ // re-lock), the bug this fixes. It also lets a live session pick up an
167
+ // idle-window change the operator made in Settings mid-session.
162
168
  return json(200, {
163
169
  locked: isLockConfigured(db) && !isSessionUnlocked(gate.sessionId, now().getTime()),
170
+ idle_seconds: getIdleSeconds(db),
164
171
  unlock_seconds_remaining: unlockSecondsRemaining(gate.sessionId, now().getTime()),
165
172
  });
166
173
  default: