cursedbelt-server 4.46.0 → 4.46.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.
@@ -417,13 +417,16 @@ export function createBinaryStore(cfg) {
417
417
  * caller asked for `paced` and the budget allows, otherwise thrown as a typed error that
418
418
  * keeps the `[binary-store] <op> <key>: 429` message batch callers already match.
419
419
  */
420
- const sendRead = async (op, key, url, init, opts) => {
420
+ const sendRead = async (op, key,
421
+ /** Signs the URL. Called for EVERY attempt: a read token lives 60 s and a paced read may wait
422
+ * 120 s, so a URL signed once went out dead after the first long retry-after (forge task 3302). */
423
+ mint, init, opts) => {
421
424
  const budget = opts?.paced
422
425
  ? (typeof opts.paced === 'object' ? opts.paced.maxWaitMs : undefined) ?? BINARY_PACED_MAX_WAIT_MS
423
426
  : 0;
424
427
  let waited = 0;
425
428
  for (let attempt = 0;; attempt++) {
426
- const res = await send(url, init);
429
+ const res = await send(await mint(), init);
427
430
  if (res.status !== 429)
428
431
  return res;
429
432
  const retryAfterSec = retryAfterSeconds(res.headers.get('retry-after'), attempt);
@@ -548,7 +551,7 @@ export function createBinaryStore(cfg) {
548
551
  if (!opts?.fresh && knownAbsent(key))
549
552
  return null;
550
553
  countWireRead(key);
551
- const res = await sendRead('get', key, await internalMediaUrl(key), undefined, opts);
554
+ const res = await sendRead('get', key, () => internalMediaUrl(key), undefined, opts);
552
555
  if (res.status === 404) {
553
556
  rememberAbsent(key);
554
557
  return null;
@@ -624,7 +627,7 @@ export function createBinaryStore(cfg) {
624
627
  return null;
625
628
  }
626
629
  countWireRead(key);
627
- const res = await sendRead('meta', key, await metaUrl(key), undefined, opts);
630
+ const res = await sendRead('meta', key, () => metaUrl(key), undefined, opts);
628
631
  if (res.status === 404) {
629
632
  rememberAbsent(key);
630
633
  return null;
@@ -671,7 +674,7 @@ export function createBinaryStore(cfg) {
671
674
  throw why;
672
675
  }
673
676
  countWireRead(key);
674
- const res = await sendRead('stat', key, await internalMediaUrl(key), { headers: { Range: 'bytes=0-0' } }, opts);
677
+ const res = await sendRead('stat', key, () => internalMediaUrl(key), { headers: { Range: 'bytes=0-0' } }, opts);
675
678
  if (res.status === 404) {
676
679
  rememberAbsent(key);
677
680
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursedbelt-server",
3
- "version": "4.46.0",
3
+ "version": "4.46.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.",
@@ -26,7 +26,7 @@
26
26
  * server; the RSA keypair is generated in-process and the signature is verified with
27
27
  * `node:crypto`, which is what makes the token assertions real rather than shape-deep.
28
28
  */
29
- import { afterAll, afterEach, beforeEach, describe, expect, it } from 'bun:test';
29
+ import { afterAll, afterEach, beforeEach, describe, expect, it, setSystemTime } from 'bun:test';
30
30
  import { generateKeyPairSync, verify as cryptoVerify } from 'node:crypto';
31
31
  import {
32
32
  AUTO_CHUNK_THRESHOLD_BYTES,
@@ -1056,6 +1056,29 @@ describe('🔴 a read the store PACES is waited out when asked, typed when not (
1056
1056
  expect(calls).toHaveLength(4);
1057
1057
  });
1058
1058
 
1059
+ it('🔴 every attempt goes with a token signed for IT — a 60 s token outlives no 120 s wait (forge task 3302)', async () => {
1060
+ // The URL was signed once, before the first attempt, so a paced read that waited out 30 s
1061
+ // refusals sent a dead token on its third try, and binary-server answered 403 — not 429.
1062
+ try {
1063
+ let n = 0;
1064
+ reply = () => (n++ < 3 ? limited('30') : ok(new Uint8Array([7, 8])));
1065
+ const store = makeStore({
1066
+ sleep: (ms) => {
1067
+ setSystemTime(new Date(Date.now() + ms));
1068
+ return Promise.resolve();
1069
+ },
1070
+ });
1071
+ await store.get('parts/0', { paced: true });
1072
+ expect(calls).toHaveLength(4);
1073
+ // The last attempt went 90 s after the first: its token must still have time left.
1074
+ const token = new URL(String(calls.at(-1)?.url)).searchParams.get('token') ?? '';
1075
+ const { exp } = JSON.parse(Buffer.from(token.split('.')[1] ?? '', 'base64url').toString('utf8')) as { exp: number };
1076
+ expect(exp * 1000).toBeGreaterThan(Date.now());
1077
+ } finally {
1078
+ setSystemTime();
1079
+ }
1080
+ });
1081
+
1059
1082
  it('without paced, a 429 is the typed error carrying retryAfterSec — and the old message', async () => {
1060
1083
  const { store, slept } = pacedStore(1, '7');
1061
1084
  const error = await store.get('parts/0').catch((e: unknown) => e);
@@ -728,7 +728,9 @@ export function createBinaryStore(cfg: BinaryStoreConfig): BinaryStore {
728
728
  const sendRead = async (
729
729
  op: string,
730
730
  key: string,
731
- url: string,
731
+ /** Signs the URL. Called for EVERY attempt: a read token lives 60 s and a paced read may wait
732
+ * 120 s, so a URL signed once went out dead after the first long retry-after (forge task 3302). */
733
+ mint: () => Promise<string>,
732
734
  init: RequestInit | undefined,
733
735
  opts: ReadOptions | undefined,
734
736
  ): Promise<Response> => {
@@ -737,7 +739,7 @@ export function createBinaryStore(cfg: BinaryStoreConfig): BinaryStore {
737
739
  : 0;
738
740
  let waited = 0;
739
741
  for (let attempt = 0; ; attempt++) {
740
- const res = await send(url, init);
742
+ const res = await send(await mint(), init);
741
743
  if (res.status !== 429) return res;
742
744
  const retryAfterSec = retryAfterSeconds(res.headers.get('retry-after'), attempt);
743
745
  const ms = retryAfterSec * 1000;
@@ -875,7 +877,7 @@ export function createBinaryStore(cfg: BinaryStoreConfig): BinaryStore {
875
877
  // short-circuit here — we want the bytes, and only the store has them.
876
878
  if (!opts?.fresh && knownAbsent(key)) return null;
877
879
  countWireRead(key);
878
- const res = await sendRead('get', key, await internalMediaUrl(key), undefined, opts);
880
+ const res = await sendRead('get', key, () => internalMediaUrl(key), undefined, opts);
879
881
  if (res.status === 404) {
880
882
  rememberAbsent(key);
881
883
  return null;
@@ -952,7 +954,7 @@ export function createBinaryStore(cfg: BinaryStoreConfig): BinaryStore {
952
954
  if (knownAbsent(key)) return null;
953
955
  }
954
956
  countWireRead(key);
955
- const res = await sendRead('meta', key, await metaUrl(key), undefined, opts);
957
+ const res = await sendRead('meta', key, () => metaUrl(key), undefined, opts);
956
958
  if (res.status === 404) {
957
959
  rememberAbsent(key);
958
960
  return null;
@@ -995,7 +997,7 @@ export function createBinaryStore(cfg: BinaryStoreConfig): BinaryStore {
995
997
  if (why instanceof BinaryStoreRateLimitedError) throw why;
996
998
  }
997
999
  countWireRead(key);
998
- const res = await sendRead('stat', key, await internalMediaUrl(key), { headers: { Range: 'bytes=0-0' } }, opts);
1000
+ const res = await sendRead('stat', key, () => internalMediaUrl(key), { headers: { Range: 'bytes=0-0' } }, opts);
999
1001
  if (res.status === 404) {
1000
1002
  rememberAbsent(key);
1001
1003
  return null;