cursedbelt-server 4.43.0 → 4.43.2

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/README.md CHANGED
@@ -81,7 +81,8 @@ after typing it (task 2138). `src/server/master-lock/sessions.ts` has the why.
81
81
 
82
82
  ## Standards
83
83
 
84
- `docs/retention.md`, `docs/activity.md`, `docs/notifications.md`, `docs/engagement.md` — the
84
+ `docs/retention.md`, `docs/activity.md`, `docs/notifications.md`, `docs/engagement.md`,
85
+ `docs/ctgr.md` (the GET-tunnel, getOnly and CSRF — register routes through `buildRoutes`) — the
85
86
  contracts every app that uses those modules is agreeing to. Cite them from an app with the
86
87
  package prefix (`cursedbelt-server/docs/retention.md`).
87
88
 
@@ -21,8 +21,8 @@
21
21
  *
22
22
  * and on the client:
23
23
  *
24
- * const { store } = createNotificationsBinding(); // cursedbelt/react/notifications
25
- * <NotificationBell store={store} />
24
+ * const binding = useNotificationsBinding(); // cursedbelt/react/notifications
25
+ * if (binding) return <NotificationBell store={binding.store} />;
26
26
  *
27
27
  * ── The three properties worth knowing ──────────────────────────────────────
28
28
  * 1. **A notification is a row before it is an event.** Delivery is a cache of
@@ -21,8 +21,8 @@
21
21
  *
22
22
  * and on the client:
23
23
  *
24
- * const { store } = createNotificationsBinding(); // cursedbelt/react/notifications
25
- * <NotificationBell store={store} />
24
+ * const binding = useNotificationsBinding(); // cursedbelt/react/notifications
25
+ * if (binding) return <NotificationBell store={binding.store} />;
26
26
  *
27
27
  * ── The three properties worth knowing ──────────────────────────────────────
28
28
  * 1. **A notification is a row before it is an event.** Delivery is a cache of
package/docs/ctgr.md ADDED
@@ -0,0 +1,24 @@
1
+ # ctgr + getOnly — the server invariants (don't reinvent the transport)
2
+
3
+ This package owns the GET-tunnel ("ctgr") and the auth guards (`src/server/ctgr.ts`,
4
+ `src/server/ctgrTunnel.ts`, `src/server/middleware/`). Use the primitives; do
5
+ not re-implement the wiring per app or per route.
6
+
7
+ - **Register routes with `buildRoutes`.** One `register(method, path, ...handlers)` call wires
8
+ both the real verb route **and** its `/ctgr<XX>` GET twin from a single call. Don't register
9
+ the twin by hand.
10
+ - **Mount the decode middleware on `CTGR_ROUTE_PATTERN`**, never on a literal `/ctgr*`. Hono's
11
+ router does **not** treat `/ctgr*` as a prefix wildcard (a bare-suffix `*` matches only one
12
+ following segment) — `CTGR_ROUTE_PATTERN` (`/:ctgrTwin{ctgr.+}`) is the correct portable
13
+ matcher. Reuse it everywhere a `/ctgr*` mount is meant.
14
+ - **`unGETify` only decodes** — it turns a `/ctgr<XX>` twin's query params back into the
15
+ original request and stashes the decoded request/method on the context. Protection lives
16
+ elsewhere by design:
17
+ - **getOnly** keys off the **RAW** request method (always `GET` on a twin) in the auth
18
+ middleware — that's what makes a tunneled write safe.
19
+ - **CSRF** double-submit fires on **every** `/ctgr*` path.
20
+ - **Binary/download routes MUST NOT go through the tunnel.** Register those as a plain
21
+ `app.get(path, handler)` so their bytes never ride the base64url codec and get mangled.
22
+
23
+ Moved here from `cursedbelt/docs/STANDARDS.md` §5 on 2026-10-05: it described this package's
24
+ server tier, which `cursedbelt` stopped shipping at the 2026-09-15 split.
@@ -7,13 +7,13 @@ carrying one goes red until it is removed. Inherited from `cursedbelt` at the
7
7
 
8
8
  | file | waiting for |
9
9
  | --- | --- |
10
- | `src/server/search/pgSearchProvider.pg.test.ts` | 24 | 22 | 2026-09-13 |
11
- | `src/server/storage/transcode.integration.spec.ts` | 7 | 5 | 2026-09-13 |
12
- | `src/server/storage/binaryServerProcessing.integration.spec.ts` | 4 | 2 | 2026-09-13 |
13
- | `src/server/storage/chunked.integration.spec.ts` | 3 | 1 | 2026-09-13 |
14
- | `src/server/media-bun/image.spec.ts` | the optional `sharp` peer | no |
15
- | `src/server/media-bun/video.spec.ts` | `ffmpeg` + `ffprobe` on `PATH` | no |
16
- | `src/server/storage/imageThumbnail.spec.ts` | `ffmpeg` on `PATH` | no |
17
- | `src/server/storage/videoProcessor.posterOnly.spec.ts` | `ffmpeg` on `PATH` | no |
18
- | `src/server/storage/files/adapter.photo.display.spec.ts` | `ffmpeg` on `PATH` | no |
19
- | `src/server/storage/fileUploadClient-chunked.integration.spec.ts` | a real `binary-server` checkout — set `BINARY_SERVER_DIR`. No default by design: a published package must not go looking in one machine's directory layout, and `binary-server` has not been re-homed into this generation, so it skips on every host today. Moved here from `cursedbelt` at the 2026-09-15 split, with the storage internals it drives. |
10
+ | `src/server/search/pgSearchProvider.pg.test.ts` | `CC_PG_URL` naming a throwaway Postgres | `bun run test:pg` supplies one |
11
+ | `src/server/storage/transcode.integration.spec.ts` | a real `binary-server` checkout — set `BINARY_SERVER_DIR`. It starts binary-server's processing worker beside the store, because only the worker runs a job |
12
+ | `src/server/storage/binaryServerProcessing.integration.spec.ts` | a real `binary-server` checkout (`BINARY_SERVER_DIR`) and `ffmpeg` on `PATH`. It starts the processing worker too |
13
+ | `src/server/storage/chunked.integration.spec.ts` | a real `binary-server` checkout — set `BINARY_SERVER_DIR` |
14
+ | `src/server/media-bun/image.spec.ts` | the optional `sharp` peer |
15
+ | `src/server/media-bun/video.spec.ts` | `ffmpeg` + `ffprobe` on `PATH` |
16
+ | `src/server/storage/imageThumbnail.spec.ts` | `ffmpeg` on `PATH` |
17
+ | `src/server/storage/videoProcessor.posterOnly.spec.ts` | `ffmpeg` on `PATH` |
18
+ | `src/server/storage/files/adapter.photo.display.spec.ts` | `ffmpeg` on `PATH` |
19
+ | `src/server/storage/fileUploadClient-chunked.integration.spec.ts` | a real `binary-server` checkout — set `BINARY_SERVER_DIR` to its path (`apps/binary-server` in this generation). No default by design: a published package must not go looking in one machine's directory layout. Moved here from `cursedbelt` at the 2026-09-15 split, with the storage internals it drives. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursedbelt-server",
3
- "version": "4.43.0",
3
+ "version": "4.43.2",
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.",
@@ -365,7 +365,7 @@
365
365
  "dependencies": {
366
366
  "argon2id": "1.0.1",
367
367
  "cursedbelt-core": "^2.1.1",
368
- "cursedops": ">=0.8.1 <0.14.0",
368
+ "cursedops": ">=0.8.1 <1.0.0",
369
369
  "cwip": "^4.6.0",
370
370
  "jose": "^6.2.3"
371
371
  },
@@ -438,5 +438,9 @@
438
438
  "why": "build starts with `rm -rf dist`, and barrelsReachNoOptionalPeer / the publish-shape specs read dist/; racing it they read a directory that is not there yet"
439
439
  }
440
440
  ]
441
+ },
442
+ "repository": {
443
+ "type": "git",
444
+ "url": "git+https://github.com/wizardmordamir/cursedforge-cursedbelt-server.git"
441
445
  }
442
446
  }
@@ -0,0 +1,26 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import pkg from '../package.json';
3
+
4
+ /**
5
+ * `cursedops` is a dependency here, so an app that takes a cursedops this range does not admit
6
+ * installs TWO copies — the app's at the top and a nested one under `cursedbelt-server`. The
7
+ * range used to stop at the next 0.x minor (`<0.10.0`, `<0.12.0`, `<0.13.0`, `<0.14.0`), and every
8
+ * cursedops minor needed a hand-widening here before the ship could bump apps onto it with one
9
+ * copy (4.30.1, 4.40.3, 4.40.4, and again for cursedops 0.14.0 — forge task 2832). All this
10
+ * package takes from it is `forgeState` from `cursedops/roots`, and `tools/publish-libs.sh`'s
11
+ * consumer preflight runs every app on a new cursedops before it ships, so the cap bought nothing
12
+ * but the chore. It is gone; this keeps it gone for the whole 0.x line.
13
+ */
14
+ describe('cursedops range', () => {
15
+ const range = pkg.dependencies.cursedops;
16
+
17
+ it('admits every later cursedops 0.x minor, so an app bumping cursedops keeps one copy', () => {
18
+ for (const version of ['0.14.0', '0.15.0', '0.20.3', '0.99.0']) {
19
+ expect({ version, admitted: Bun.semver.satisfies(version, range) }).toEqual({ version, admitted: true });
20
+ }
21
+ });
22
+
23
+ it('still refuses what predates forgeState', () => {
24
+ expect(Bun.semver.satisfies('0.8.0', range)).toBe(false);
25
+ });
26
+ });
@@ -21,8 +21,8 @@
21
21
  *
22
22
  * and on the client:
23
23
  *
24
- * const { store } = createNotificationsBinding(); // cursedbelt/react/notifications
25
- * <NotificationBell store={store} />
24
+ * const binding = useNotificationsBinding(); // cursedbelt/react/notifications
25
+ * if (binding) return <NotificationBell store={binding.store} />;
26
26
  *
27
27
  * ── The three properties worth knowing ──────────────────────────────────────
28
28
  * 1. **A notification is a row before it is an event.** Delivery is a cache of
@@ -1,4 +1,5 @@
1
1
  import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
2
+ import type { Subprocess } from 'bun';
2
3
  import { existsSync, mkdirSync } from 'node:fs';
3
4
  import { mkdtemp, rm } from 'node:fs/promises';
4
5
  import { tmpdir } from 'node:os';
@@ -19,7 +20,11 @@ const FFMPEG_PRESENT = Bun.which('ffmpeg') !== null;
19
20
 
20
21
  // SKIPPED until a real binary-server checkout is present: point BINARY_SERVER_DIR at one.
21
22
  // There is deliberately no default — a published package must not go looking in one machine's
22
- // directory layout — and `binary-server` has not been re-homed here, so this skips on every host today.
23
+ // directory layout. In this generation that checkout is `$FORGE/apps/binary-server`.
24
+ // BS_ALLOW_NEW_STORE=1: the spawned server refuses to invent a store it was not given, and this
25
+ // throwaway one is exactly a store it should create.
26
+ // BS_SECRETS_FILE=/dev/null: otherwise it folds in this machine's binary-server.env — the real R2
27
+ // keys and passcode — and a throwaway store has no business holding them.
23
28
  // ffmpeg must be on PATH too: the proof is that the REMOTE side runs real ffmpeg, not that the client posted a job.
24
29
  describe.skipIf(!BS_PRESENT || !FFMPEG_PRESENT)(
25
30
  'binaryServerProcessing → real binary-server (integration)',
@@ -27,6 +32,7 @@ describe.skipIf(!BS_PRESENT || !FFMPEG_PRESENT)(
27
32
  let port: number;
28
33
  let storeDir: string;
29
34
  let server: TestServer | null = null;
35
+ let worker: Subprocess | null = null;
30
36
  let tmpData: string;
31
37
  const INTERNAL_SECRET = 'test-processing-secret';
32
38
  const APP = 'ptest';
@@ -36,21 +42,33 @@ describe.skipIf(!BS_PRESENT || !FFMPEG_PRESENT)(
36
42
  storeDir = path.join(tmpData, 'binaries');
37
43
  mkdirSync(storeDir, { recursive: true });
38
44
  port = await pickFreePort();
45
+ const env = {
46
+ PORT: String(port),
47
+ HOST: '127.0.0.1',
48
+ STORAGE_ROOT: storeDir,
49
+ DB_PATH: path.join(tmpData, 'test.sqlite'),
50
+ BS_ALLOW_NEW_STORE: '1',
51
+ BS_SECRETS_FILE: '/dev/null',
52
+ INTERNAL_SECRET,
53
+ NODE_ENV: 'test',
54
+ };
39
55
  server = await startTestServer({
40
56
  cwd: BS_DIR,
41
57
  cmd: ['bun', path.join(BS_DIR, 'src', 'index.ts')],
42
58
  port,
43
- env: {
44
- PORT: String(port),
45
- HOST: '127.0.0.1',
46
- STORAGE_ROOT: storeDir,
47
- DB_PATH: path.join(tmpData, 'test.sqlite'),
48
- INTERNAL_SECRET,
49
- NODE_ENV: 'test',
50
- },
59
+ env,
51
60
  healthPath: '/healthz',
52
61
  timeoutMs: 90_000,
53
62
  });
63
+ // The store only ENQUEUES a job; binary-server's processing worker (its own process since
64
+ // bs task 2694) leases and runs it, and writes each output back through the store's API.
65
+ // Without one, every job stays `pending` and the client times out.
66
+ worker = Bun.spawn(['bun', path.join(BS_DIR, 'src', 'processingWorker.ts')], {
67
+ cwd: BS_DIR,
68
+ env: { ...process.env, ...env },
69
+ stdout: 'inherit',
70
+ stderr: 'inherit',
71
+ });
54
72
 
55
73
  // Register the tenant, the way `register-app` does for a real one. This is
56
74
  // not harness ceremony: since binary-server started INDEXING every transcode
@@ -71,7 +89,7 @@ describe.skipIf(!BS_PRESENT || !FFMPEG_PRESENT)(
71
89
  ],
72
90
  {
73
91
  cwd: BS_DIR,
74
- env: { ...process.env, STORAGE_ROOT: storeDir, DB_PATH: path.join(tmpData, 'test.sqlite') },
92
+ env: { ...process.env, STORAGE_ROOT: storeDir, DB_PATH: path.join(tmpData, 'test.sqlite'), BS_ALLOW_NEW_STORE: '1', BS_SECRETS_FILE: '/dev/null' },
75
93
  stdout: 'inherit',
76
94
  stderr: 'inherit',
77
95
  },
@@ -80,6 +98,8 @@ describe.skipIf(!BS_PRESENT || !FFMPEG_PRESENT)(
80
98
  }, 90_000);
81
99
 
82
100
  afterAll(async () => {
101
+ worker?.kill();
102
+ await worker?.exited;
83
103
  await server?.stop();
84
104
  await rm(tmpData, { recursive: true, force: true }).catch(() => void 0);
85
105
  });
@@ -97,7 +117,7 @@ describe.skipIf(!BS_PRESENT || !FFMPEG_PRESENT)(
97
117
  `;
98
118
  const proc = Bun.spawn(['bun', '-e', helper, `${APP}/${key}`, localFile], {
99
119
  cwd: BS_DIR,
100
- env: { ...process.env, STORAGE_ROOT: storeDir, DB_PATH: path.join(tmpData, 'test.sqlite') },
120
+ env: { ...process.env, STORAGE_ROOT: storeDir, DB_PATH: path.join(tmpData, 'test.sqlite'), BS_ALLOW_NEW_STORE: '1', BS_SECRETS_FILE: '/dev/null' },
101
121
  stdout: 'inherit',
102
122
  stderr: 'inherit',
103
123
  });
@@ -40,7 +40,11 @@ const pem = (der: ArrayBuffer, label: string) =>
40
40
 
41
41
  // SKIPPED until a real binary-server checkout is present: point BINARY_SERVER_DIR at one.
42
42
  // There is deliberately no default — a published package must not go looking in one machine's
43
- // directory layout — and `binary-server` has not been re-homed here, so this skips on every host today.
43
+ // directory layout. In this generation that checkout is `$FORGE/apps/binary-server`.
44
+ // BS_ALLOW_NEW_STORE=1: the spawned server refuses to invent a store it was not given, and this
45
+ // throwaway one is exactly a store it should create.
46
+ // BS_SECRETS_FILE=/dev/null: otherwise it folds in this machine's binary-server.env — the real R2
47
+ // keys and passcode — and a throwaway store has no business holding them.
44
48
  describe.skipIf(!BS_PRESENT)('chunked upload → real binary-server (integration)', () => {
45
49
  let server: TestServer | null = null;
46
50
  let tmp = '';
@@ -83,7 +87,7 @@ describe.skipIf(!BS_PRESENT)('chunked upload → real binary-server (integration
83
87
  PORT: String(port),
84
88
  HOST: '127.0.0.1',
85
89
  STORAGE_ROOT: path.join(tmp, 'binaries'),
86
- DB_PATH: path.join(tmp, 'bs.sqlite'),
90
+ DB_PATH: path.join(tmp, 'bs.sqlite'), BS_ALLOW_NEW_STORE: '1', BS_SECRETS_FILE: '/dev/null',
87
91
  FILE_TOKEN_PUBLIC_KEY: spki,
88
92
  FILE_TOKEN_ISSUER: ISSUER,
89
93
  CA_APP_ID: APP_ID,
@@ -50,7 +50,11 @@ const pem = (der: ArrayBuffer, label: string) =>
50
50
 
51
51
  // SKIPPED until a real binary-server checkout is present: point BINARY_SERVER_DIR at one.
52
52
  // There is deliberately no default — a published package must not go looking in one machine's
53
- // directory layout — and `binary-server` has not been re-homed here, so this skips on every host today.
53
+ // directory layout. In this generation that checkout is `$FORGE/apps/binary-server`.
54
+ // BS_ALLOW_NEW_STORE=1: the spawned server refuses to invent a store it was not given, and this
55
+ // throwaway one is exactly a store it should create.
56
+ // BS_SECRETS_FILE=/dev/null: otherwise it folds in this machine's binary-server.env — the real R2
57
+ // keys and passcode — and a throwaway store has no business holding them.
54
58
  describe.skipIf(!BS_PRESENT)('fileUploadClient chunked path → real binary-server (integration)', () => {
55
59
  let server: TestServer | null = null;
56
60
  let tmp = '';
@@ -90,7 +94,7 @@ describe.skipIf(!BS_PRESENT)('fileUploadClient chunked path → real binary-serv
90
94
  PORT: String(port),
91
95
  HOST: '127.0.0.1',
92
96
  STORAGE_ROOT: path.join(tmp, 'binaries'),
93
- DB_PATH: path.join(tmp, 'bs.sqlite'),
97
+ DB_PATH: path.join(tmp, 'bs.sqlite'), BS_ALLOW_NEW_STORE: '1', BS_SECRETS_FILE: '/dev/null',
94
98
  FILE_TOKEN_PUBLIC_KEY: spki,
95
99
  FILE_TOKEN_ISSUER: ISSUER,
96
100
  CA_APP_ID: APP_ID,
@@ -1,4 +1,5 @@
1
1
  import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
2
+ import type { Subprocess } from 'bun';
2
3
  import { existsSync, mkdirSync } from 'node:fs';
3
4
  import { mkdtemp, rm } from 'node:fs/promises';
4
5
  import { tmpdir } from 'node:os';
@@ -18,11 +19,16 @@ const BS_PRESENT = BS_DIR !== '' && existsSync(path.join(BS_DIR, 'src', 'index.t
18
19
 
19
20
  // SKIPPED until a real binary-server checkout is present: point BINARY_SERVER_DIR at one.
20
21
  // There is deliberately no default — a published package must not go looking in one machine's
21
- // directory layout — and `binary-server` has not been re-homed here, so this skips on every host today.
22
+ // directory layout. In this generation that checkout is `$FORGE/apps/binary-server`.
23
+ // BS_ALLOW_NEW_STORE=1: the spawned server refuses to invent a store it was not given, and this
24
+ // throwaway one is exactly a store it should create.
25
+ // BS_SECRETS_FILE=/dev/null: otherwise it folds in this machine's binary-server.env — the real R2
26
+ // keys and passcode — and a throwaway store has no business holding them.
22
27
  describe.skipIf(!BS_PRESENT)('transcode API → real binary-server (integration)', () => {
23
28
  let port: number;
24
29
  let storeDir: string;
25
30
  let server: TestServer | null = null;
31
+ let worker: Subprocess | null = null;
26
32
  let tmpData: string;
27
33
  const INTERNAL_SECRET = 'test-transcode-secret';
28
34
 
@@ -32,24 +38,37 @@ describe.skipIf(!BS_PRESENT)('transcode API → real binary-server (integration)
32
38
  mkdirSync(storeDir, { recursive: true });
33
39
 
34
40
  port = await pickFreePort();
41
+ const env = {
42
+ PORT: String(port),
43
+ HOST: '127.0.0.1',
44
+ STORAGE_ROOT: storeDir,
45
+ DB_PATH: path.join(tmpData, 'test.sqlite'),
46
+ BS_ALLOW_NEW_STORE: '1',
47
+ BS_SECRETS_FILE: '/dev/null',
48
+ INTERNAL_SECRET,
49
+ NODE_ENV: 'test',
50
+ };
35
51
  server = await startTestServer({
36
52
  cwd: BS_DIR,
37
53
  cmd: ['bun', path.join(BS_DIR, 'src', 'index.ts')],
38
54
  port,
39
- env: {
40
- PORT: String(port),
41
- HOST: '127.0.0.1',
42
- STORAGE_ROOT: storeDir,
43
- DB_PATH: path.join(tmpData, 'test.sqlite'),
44
- INTERNAL_SECRET,
45
- NODE_ENV: 'test',
46
- },
55
+ env,
47
56
  healthPath: '/healthz',
48
57
  timeoutMs: 90_000,
49
58
  });
59
+ // The store only ENQUEUES a job; binary-server's processing worker (its own process since
60
+ // bs task 2694) leases and runs it. Without one, every job stays `pending` forever.
61
+ worker = Bun.spawn(['bun', path.join(BS_DIR, 'src', 'processingWorker.ts')], {
62
+ cwd: BS_DIR,
63
+ env: { ...process.env, ...env },
64
+ stdout: 'inherit',
65
+ stderr: 'inherit',
66
+ });
50
67
  }, 90_000);
51
68
 
52
69
  afterAll(async () => {
70
+ worker?.kill();
71
+ await worker?.exited;
53
72
  await server?.stop();
54
73
  await rm(tmpData, { recursive: true, force: true }).catch(() => void 0);
55
74
  });
@@ -118,7 +137,9 @@ describe.skipIf(!BS_PRESENT)('transcode API → real binary-server (integration)
118
137
  await Bun.sleep(100);
119
138
  }
120
139
  expect(status).toBe('failed');
121
- });
140
+ // Past bun's 5 s default: a timeout kills the spawned server, and the tests after this one
141
+ // then fail at `fetch` instead of here, naming the wrong thing.
142
+ }, 15_000);
122
143
 
123
144
  test('GET /api/transcode/:jobId returns 404 for unknown job', async () => {
124
145
  const res = await fetch(`http://localhost:${port}/api/transcode/nonexistent-job-id`, {