cursedbelt-server 4.26.0 → 4.26.1

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.
@@ -257,6 +257,16 @@ export declare class MasterLock {
257
257
  * `req` is optional only so a health check can ask without one. Pass it wherever there
258
258
  * IS a request: without it the reply carries no account, and a client would read that
259
259
  * as "locked" while holding a perfectly good unlock.
260
+ *
261
+ * 🔴 With a request, `locked` is THIS CALLER's answer — the one {@link accountFor} gives —
262
+ * and never the site's. Until 4.26.1 it read {@link unlocked} ("somebody's device is
263
+ * open"), which the lock page's own script takes as "you are in" and answers with
264
+ * `location.reload()`; the guard, asking the per-caller question, serves the lock page
265
+ * again, and the page spins for as long as any other browser stays unlocked — no password
266
+ * box ever settles. Measured 2026-09-23 on the binary-server inspector: a second Chromium
267
+ * context reloaded `/__lock/` in a tight loop while `/__lock/status` told it
268
+ * `"locked": false` with no cookie at all. Without a request it stays the site-wide
269
+ * health reading, which is all a caller with no request can mean.
260
270
  */
261
271
  status(req?: Request): MasterLockStatus;
262
272
  /**
@@ -191,6 +191,16 @@ export class MasterLock {
191
191
  * `req` is optional only so a health check can ask without one. Pass it wherever there
192
192
  * IS a request: without it the reply carries no account, and a client would read that
193
193
  * as "locked" while holding a perfectly good unlock.
194
+ *
195
+ * 🔴 With a request, `locked` is THIS CALLER's answer — the one {@link accountFor} gives —
196
+ * and never the site's. Until 4.26.1 it read {@link unlocked} ("somebody's device is
197
+ * open"), which the lock page's own script takes as "you are in" and answers with
198
+ * `location.reload()`; the guard, asking the per-caller question, serves the lock page
199
+ * again, and the page spins for as long as any other browser stays unlocked — no password
200
+ * box ever settles. Measured 2026-09-23 on the binary-server inspector: a second Chromium
201
+ * context reloaded `/__lock/` in a tight loop while `/__lock/status` told it
202
+ * `"locked": false` with no cookie at all. Without a request it stays the site-wide
203
+ * health reading, which is all a caller with no request can mean.
194
204
  */
195
205
  status(req) {
196
206
  const accountId = req ? this.accountFor(req) : null;
@@ -198,7 +208,7 @@ export class MasterLock {
198
208
  return {
199
209
  configured: this.configured,
200
210
  enrollable: this.awaitingEnrollment,
201
- locked: !this.unlocked,
211
+ locked: req ? accountId === null : !this.unlocked,
202
212
  kdf: this.kdf,
203
213
  idleMs: this.idleMs,
204
214
  remainingMs: this.remainingMs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursedbelt-server",
3
- "version": "4.26.0",
3
+ "version": "4.26.1",
4
4
  "license": "ISC",
5
5
  "type": "module",
6
6
  "description": "The app-facing Bun/Hono server tier of the cursedbelt split — storage, sharing, activity, guard, sync. React-free; cursedbelt-core below it.",
@@ -432,3 +432,26 @@ describe("two locked apps sharing a hostname", () => {
432
432
  expect((await a.guard.handle(xhr("/api/items", onlyB)))?.status).toBe(401);
433
433
  });
434
434
  });
435
+
436
+ describe("🔴 the status route answers for THIS CALLER, never the site", () => {
437
+ // The lock page's script reloads whenever status says `locked: false`. Read site-wide, one
438
+ // unlocked browser made every OTHER browser's lock page reload for ever (4.26.1).
439
+ test("another browser's unlock does not tell a cookieless caller it is in", async () => {
440
+ const { guard, lock } = build();
441
+ const cookie = await open(guard);
442
+ expect(lock.unlocked).toBe(true); // the site IS open, for somebody
443
+
444
+ const stranger = await (await guard.handle(xhr(MASTER_LOCK_PATHS.status)))?.json();
445
+ expect(stranger.locked).toBe(true);
446
+
447
+ const owner = await (await guard.handle(xhr(MASTER_LOCK_PATHS.status, cookie)))?.json();
448
+ expect(owner.locked).toBe(false);
449
+ });
450
+
451
+ test("a request-less health reading is still the site's", async () => {
452
+ const { guard, lock } = build();
453
+ expect(lock.status().locked).toBe(true);
454
+ await open(guard);
455
+ expect(lock.status().locked).toBe(false);
456
+ });
457
+ });
@@ -375,6 +375,16 @@ export class MasterLock {
375
375
  * `req` is optional only so a health check can ask without one. Pass it wherever there
376
376
  * IS a request: without it the reply carries no account, and a client would read that
377
377
  * as "locked" while holding a perfectly good unlock.
378
+ *
379
+ * 🔴 With a request, `locked` is THIS CALLER's answer — the one {@link accountFor} gives —
380
+ * and never the site's. Until 4.26.1 it read {@link unlocked} ("somebody's device is
381
+ * open"), which the lock page's own script takes as "you are in" and answers with
382
+ * `location.reload()`; the guard, asking the per-caller question, serves the lock page
383
+ * again, and the page spins for as long as any other browser stays unlocked — no password
384
+ * box ever settles. Measured 2026-09-23 on the binary-server inspector: a second Chromium
385
+ * context reloaded `/__lock/` in a tight loop while `/__lock/status` told it
386
+ * `"locked": false` with no cookie at all. Without a request it stays the site-wide
387
+ * health reading, which is all a caller with no request can mean.
378
388
  */
379
389
  status(req?: Request): MasterLockStatus {
380
390
  const accountId = req ? this.accountFor(req) : null;
@@ -382,7 +392,7 @@ export class MasterLock {
382
392
  return {
383
393
  configured: this.configured,
384
394
  enrollable: this.awaitingEnrollment,
385
- locked: !this.unlocked,
395
+ locked: req ? accountId === null : !this.unlocked,
386
396
  kdf: this.kdf,
387
397
  idleMs: this.idleMs,
388
398
  remainingMs: this.remainingMs,