cursedbelt-server 3.0.1 → 4.0.0

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.
Files changed (77) hide show
  1. package/dist/server/auth/jwt.d.ts +1 -1
  2. package/dist/server/auth/jwt.js +2 -2
  3. package/dist/server/master-lock/accountsPage.d.ts +39 -0
  4. package/dist/server/master-lock/accountsPage.js +455 -0
  5. package/dist/server/master-lock/guard.js +39 -1
  6. package/dist/server/master-lock/index.d.ts +10 -2
  7. package/dist/server/master-lock/index.js +10 -2
  8. package/dist/server/master-lock/lockPage.d.ts +9 -0
  9. package/dist/server/master-lock/lockPage.js +44 -36
  10. package/dist/server/media-bun/streaming.d.ts +0 -1
  11. package/dist/server/media-bun/streaming.js +1 -1
  12. package/dist/server/storage/files/catalogue.d.ts +2 -2
  13. package/dist/server/storage/files/catalogue.js +18 -208
  14. package/dist/server/storage/files/fileRoutes.d.ts +2 -20
  15. package/dist/server/storage/files/fileRoutes.js +1 -74
  16. package/dist/server/storage/files/index.d.ts +1 -1
  17. package/dist/server/storage/files/index.js +1 -1
  18. package/dist/server/storage/files/migrations.js +9 -6
  19. package/dist/server/storage/files/stack.d.ts +6 -31
  20. package/dist/server/storage/files/stack.js +0 -27
  21. package/dist/server/storage/files/types.d.ts +10 -60
  22. package/dist/server/storage/files/types.js +9 -0
  23. package/dist/server/storage/index.d.ts +2 -2
  24. package/dist/server/storage/index.js +1 -1
  25. package/dist/server/storage/mountMediaRoutes.d.ts +1 -4
  26. package/dist/server/storage/types.d.ts +0 -9
  27. package/dist/server/storage/types.js +2 -9
  28. package/package.json +2 -2
  29. package/src/leafSubpathsImportNothing.spec.ts +6 -4
  30. package/src/server/auth/jwt.ts +2 -2
  31. package/src/server/master-lock/accountsPage.spec.ts +231 -0
  32. package/src/server/master-lock/accountsPage.ts +465 -0
  33. package/src/server/master-lock/guard.ts +44 -1
  34. package/src/server/master-lock/index.ts +22 -2
  35. package/src/server/master-lock/lockPage.spec.ts +20 -16
  36. package/src/server/master-lock/lockPage.ts +45 -36
  37. package/src/server/media-bun/streaming.ts +0 -6
  38. package/src/server/storage/fileUploadClient.integration.spec.ts +2 -21
  39. package/src/server/storage/files/adapter.photo.display.spec.ts +2 -2
  40. package/src/server/storage/files/adapter.photo.spec.ts +2 -2
  41. package/src/server/storage/files/adapter.spec.ts +0 -1
  42. package/src/server/storage/files/catalogue.media-host.spec.ts +6 -71
  43. package/src/server/storage/files/catalogue.recover.spec.ts +14 -23
  44. package/src/server/storage/files/catalogue.ts +19 -240
  45. package/src/server/storage/files/fileRoutes.replace.spec.ts +2 -2
  46. package/src/server/storage/files/fileRoutes.spec.ts +3 -97
  47. package/src/server/storage/files/fileRoutes.ts +2 -96
  48. package/src/server/storage/files/index.ts +1 -4
  49. package/src/server/storage/files/migrations.ts +9 -7
  50. package/src/server/storage/files/stack.spec.ts +11 -111
  51. package/src/server/storage/files/stack.ts +9 -61
  52. package/src/server/storage/files/types.ts +20 -68
  53. package/src/server/storage/index.ts +0 -4
  54. package/src/server/storage/mountMediaRoutes.ts +1 -7
  55. package/src/server/storage/types.ts +2 -11
  56. package/src/shippedFilesAreTracked.spec.ts +3 -2
  57. package/dist/server/private-media/crypto.d.ts +0 -37
  58. package/dist/server/private-media/crypto.js +0 -111
  59. package/dist/server/private-media/index-store.d.ts +0 -31
  60. package/dist/server/private-media/index-store.js +0 -36
  61. package/dist/server/private-media/mountPrivateRoutes.d.ts +0 -26
  62. package/dist/server/private-media/mountPrivateRoutes.js +0 -219
  63. package/dist/server/private-media/pipeline.d.ts +0 -37
  64. package/dist/server/private-media/pipeline.js +0 -97
  65. package/dist/server/private-media/unlock.d.ts +0 -52
  66. package/dist/server/private-media/unlock.js +0 -55
  67. package/src/server/private-media/blobRoute.spec.ts +0 -165
  68. package/src/server/private-media/crypto.spec.ts +0 -81
  69. package/src/server/private-media/crypto.ts +0 -155
  70. package/src/server/private-media/gatePolicy.spec.ts +0 -98
  71. package/src/server/private-media/index-store.ts +0 -83
  72. package/src/server/private-media/mountPrivateRoutes.ts +0 -265
  73. package/src/server/private-media/pipeline.spec.ts +0 -111
  74. package/src/server/private-media/pipeline.ts +0 -170
  75. package/src/server/private-media/routes.spec.ts +0 -265
  76. package/src/server/private-media/unlock.ts +0 -112
  77. package/src/server/storage/files/catalogue.private.spec.ts +0 -106
@@ -6,4 +6,4 @@ export { createFileUrls, type FileUrls, type MountFileRoutesOptions, mountFileRo
6
6
  export { FILE_CATALOGUE_MIGRATIONS } from './migrations';
7
7
  export { createFilesStack, type FilesStack, type FilesStackMountAuth, type FilesStackOptions, } from './stack';
8
8
  export { createLedgeredDeletes, createStorageOpsLedger, createStorageReconciler, type StorageAction, type StorageOpRow, type StorageOpsLedger, type StorageReconciler, } from './storageOps';
9
- export { type CollectionKind, type CollectionVisibility, classifyFileType, FILE_SORTS, type FileCatalogue, type FileCollection, type FilePage, type FileRecord, type FileSort, type FileToPlayable, type FileType, type ListFilesOptions, type PrivateFileCatalogue, type PrivateFileItem, type PrivateKeyMode, type TagFacet, } from './types';
9
+ export { type CollectionKind, type CollectionVisibility, classifyFileType, FILE_SORTS, type FileCatalogue, type FileCollection, type FilePage, type FileRecord, type FileSort, type FileToPlayable, type FileType, type ListFilesOptions, type TagFacet, } from './types';
@@ -1,5 +1,5 @@
1
1
  // cursedbelt-server/storage FileCatalogue — a SQLite-backed tagged flat file system over the
2
- // MediaStore + media pipeline + private-media. One source of truth for a user's binaries; the
2
+ // MediaStore + media pipeline. One source of truth for a user's binaries; the
3
3
  // video gallery is a video-filtered view over it.
4
4
  export { createCatalogueMediaSource, createCataloguePhotoSource, createCatalogueVideoSource, createFileToPlayable, } from './adapter';
5
5
  export { catalogueUploadKey, createSqliteFileCatalogue, fileBinaryKey, } from './catalogue';
@@ -3,6 +3,15 @@
3
3
  // (not re-derived per list), tags live in an indexed junction (fast facets, not a JSON scan),
4
4
  // processing state is typed columns (not an opaque blob), and a storage-operations ledger backs
5
5
  // durable cross-boundary (binary-server/S3) ops. Flat — no parent_id tree.
6
+ //
7
+ // 🔴 `0001` is edited IN PLACE rather than followed by a drop-column migration (2026-09-17,
8
+ // removing private media). It is `CREATE TABLE IF NOT EXISTS` and is recorded in `_migrations`,
9
+ // so an existing database never re-runs it and simply keeps three vestigial columns
10
+ // (`is_private`, `enc_name`, `enc_dek`, `enc_key_type`, `file_tags.is_private`) that nothing
11
+ // reads; every one of them had `NOT NULL DEFAULT`, so the leaner INSERTs below still satisfy
12
+ // them. A fresh database gets the lean shape. That is safe here only because no app in the
13
+ // fleet mounts `createFilesStack` — if one ever does, removing a column becomes a real
14
+ // migration, not an edit.
6
15
  export const FILE_CATALOGUE_MIGRATIONS = [
7
16
  {
8
17
  name: '0001_file_catalogue',
@@ -15,7 +24,6 @@ export const FILE_CATALOGUE_MIGRATIONS = [
15
24
  mime TEXT NOT NULL DEFAULT '',
16
25
  size INTEGER NOT NULL DEFAULT 0,
17
26
  file_type TEXT NOT NULL DEFAULT 'other',
18
- is_private INTEGER NOT NULL DEFAULT 0,
19
27
  has_thumbnail INTEGER NOT NULL DEFAULT 0,
20
28
  has_hls INTEGER NOT NULL DEFAULT 0,
21
29
  has_sprites INTEGER NOT NULL DEFAULT 0,
@@ -23,9 +31,6 @@ export const FILE_CATALOGUE_MIGRATIONS = [
23
31
  processing_status TEXT NOT NULL DEFAULT 'ready',
24
32
  processing_progress INTEGER NOT NULL DEFAULT 100,
25
33
  storage_backend TEXT NOT NULL DEFAULT 'disk',
26
- enc_name TEXT NOT NULL DEFAULT '',
27
- enc_dek TEXT NOT NULL DEFAULT '',
28
- enc_key_type TEXT NOT NULL DEFAULT '',
29
34
  created_at INTEGER NOT NULL,
30
35
  updated_at INTEGER NOT NULL
31
36
  )
@@ -33,7 +38,6 @@ export const FILE_CATALOGUE_MIGRATIONS = [
33
38
  // Listing is cursor-keyed on (created_at, id); these cover the flat feed + smart-view filter.
34
39
  db.run(`CREATE INDEX IF NOT EXISTS idx_files_owner_created ON files(owner, created_at DESC, id DESC)`);
35
40
  db.run(`CREATE INDEX IF NOT EXISTS idx_files_owner_type_created ON files(owner, file_type, created_at DESC, id DESC)`);
36
- db.run(`CREATE INDEX IF NOT EXISTS idx_files_owner_private ON files(owner, is_private)`);
37
41
  // Tags as a junction (indexed) — single-tag filter + facet counts are index lookups, not a
38
42
  // full-table JSON parse (ca's weakness). `owner` denormalized so facet queries stay covering.
39
43
  db.run(`
@@ -41,7 +45,6 @@ export const FILE_CATALOGUE_MIGRATIONS = [
41
45
  file_id TEXT NOT NULL REFERENCES files(id) ON DELETE CASCADE,
42
46
  tag TEXT NOT NULL,
43
47
  owner TEXT NOT NULL,
44
- is_private INTEGER NOT NULL DEFAULT 0,
45
48
  PRIMARY KEY (file_id, tag)
46
49
  )
47
50
  `);
@@ -1,13 +1,12 @@
1
1
  import type { Database } from 'bun:sqlite';
2
2
  import type { Context, Hono } from 'hono';
3
- import { type UnlockPasswordPolicy, type UnlockService } from '../../private-media/unlock';
4
3
  import { type MediaUrls, SourceRegistry } from '../sources';
5
4
  import { type StreamTokenService } from '../streamToken';
6
5
  import type { MediaCtx, MediaGallerySource, MediaSource, MediaStore, PhotoSource } from '../types';
7
6
  import { type SqliteFileCatalogueOptions } from './catalogue';
8
7
  import { type ChunkedUploadService, type MintChunkUpload } from './chunkedUpload';
9
8
  import { type FileUrls } from './fileRoutes';
10
- import type { FileCatalogue, PrivateFileCatalogue } from './types';
9
+ import type { FileCatalogue } from './types';
11
10
  export interface FilesStackOptions {
12
11
  /** The catalogue database. The stack runs FILE_CATALOGUE_MIGRATIONS on it itself. */
13
12
  db: Database;
@@ -17,9 +16,9 @@ export interface FilesStackOptions {
17
16
  workDir: string;
18
17
  /** Secret for the stream-token service (path-embedded byte-route auth). */
19
18
  streamSecret: string;
20
- /** Owner keyspace segment. Defaults to the userId (files + private are personal) — note the
21
- * bare primitives default to tenantId; the stack picks the personal axis so every URL/route
22
- * agrees with the private layer's per-user owner. */
19
+ /** Owner keyspace segment. Defaults to the userId (files are personal) — note the bare
20
+ * primitives default to tenantId; the stack picks the personal axis so every URL and route
21
+ * agrees on one owner. */
23
22
  owner?: (ctx: MediaCtx) => string;
24
23
  /** Registered video-source id (also the media-token sourceId). Default 'files'. */
25
24
  sourceId?: string;
@@ -40,8 +39,6 @@ export interface FilesStackOptions {
40
39
  mediaPublicBase?: string;
41
40
  /** PUBLIC base for browser file URLs (split-origin counterpart of `filesBasePath`). */
42
41
  filesPublicBase?: string;
43
- /** Private (encrypted) route mount. Default '/_private'. */
44
- privateBasePath?: string;
45
42
  /** HLS ladder heights for video ingest (createSqliteFileCatalogue passthrough). */
46
43
  ladderHeights?: SqliteFileCatalogueOptions['ladderHeights'];
47
44
  /** Cap on transcode kicks per file before it's marked failed (createSqliteFileCatalogue
@@ -56,15 +53,6 @@ export interface FilesStackOptions {
56
53
  /** Image thumbnail/display derivative implementation (createSqliteFileCatalogue passthrough).
57
54
  * Default: local sharp with ffmpeg fallback. */
58
55
  imageDerivatives?: SqliteFileCatalogueOptions['imageDerivatives'];
59
- /** Enables the private (encrypted) stack: unlock service + /_private routes + private uploads.
60
- * Omit ⇒ `unlock` is null and mount() wires only the public routes. `passwordPolicy` sets what
61
- * a supplied password MEANS (derive = deniable sets, the default; gate = verify against the
62
- * app's accounts + recoverable server-key sets — see UnlockPasswordPolicy). */
63
- privateMedia?: {
64
- serverSecret: string;
65
- unlockSecret: string;
66
- passwordPolicy?: UnlockPasswordPolicy;
67
- };
68
56
  /** Re-kick transcodes a restart left stuck (catalogue.recoverProcessing) at build time.
69
57
  * Default true. */
70
58
  recoverProcessing?: boolean;
@@ -90,18 +78,8 @@ export interface FilesStackMountAuth {
90
78
  /** Session auth for uploads + listings (same guard mountMediaRoutes/mountFileRoutes take).
91
79
  * Return a MediaCtx to allow, null to reject. */
92
80
  authorizeUpload: (c: Context) => Promise<MediaCtx | null> | MediaCtx | null;
93
- /** Session auth for the private layer → {userId, userSalt}. REQUIRED when `privateMedia` is
94
- * configured (mount() throws otherwise); ignored when it isn't. */
95
- authorizePrivate?: (c: Context) => Promise<{
96
- userId: string;
97
- userSalt: string;
98
- } | null>;
99
81
  /** Wire large-file chunked uploads (mountFileRoutes' `chunked` passthrough). */
100
82
  mintChunkUpload?: ChunkedUploadService;
101
- /** Private-upload password header (default 'x-private-password'). */
102
- passwordHeader?: string;
103
- /** Private-upload unlock-token header (default 'x-unlock-token'). */
104
- tokenHeader?: string;
105
83
  }
106
84
  /**
107
85
  * The assembled stack. `catalogue` / `fileUrls` / `mediaUrls` / `videoSource` are intentionally
@@ -110,7 +88,7 @@ export interface FilesStackMountAuth {
110
88
  * not, layering) import cursedshell.
111
89
  */
112
90
  export interface FilesStack {
113
- catalogue: FileCatalogue & PrivateFileCatalogue;
91
+ catalogue: FileCatalogue;
114
92
  token: StreamTokenService;
115
93
  mediaUrls: MediaUrls;
116
94
  fileUrls: FileUrls;
@@ -120,15 +98,12 @@ export interface FilesStack {
120
98
  /** The MIXED view — photos + videos in one feed (media gallery data source). */
121
99
  mediaSource: MediaGallerySource;
122
100
  registry: SourceRegistry;
123
- /** Null unless `privateMedia` was configured. */
124
- unlock: UnlockService | null;
125
101
  /** The large-file chunked-upload service, or null unless `chunkedUpload` was configured. mount()
126
102
  * auto-wires it into the file routes; exposed for observability (pending()) + tests. */
127
103
  chunked: ChunkedUploadService | null;
128
104
  /**
129
105
  * Mount every byte/upload route the stack owns onto a Hono app: mountMediaRoutes
130
- * (mediaBasePath) + mountFileRoutes (filesBasePath, incl. privateUpload when unlock exists) +
131
- * mountPrivateRoutes (privateBasePath, when unlock exists). Auth stays host-injected.
106
+ * (mediaBasePath) + mountFileRoutes (filesBasePath). Auth stays host-injected.
132
107
  */
133
108
  mount(app: Hono, auth: FilesStackMountAuth): void;
134
109
  }
@@ -1,6 +1,4 @@
1
1
  import { runMigrations } from '../../migration/runner';
2
- import { mountPrivateRoutes } from '../../private-media/mountPrivateRoutes';
3
- import { createUnlockService, } from '../../private-media/unlock';
4
2
  import { mountMediaRoutes } from '../mountMediaRoutes';
5
3
  import { createMediaUrls, SourceRegistry } from '../sources';
6
4
  import { createStreamTokenService } from '../streamToken';
@@ -65,7 +63,6 @@ export function createFilesStack(opts) {
65
63
  videoSourceId: sourceId,
66
64
  });
67
65
  const registry = new SourceRegistry().register(videoSource);
68
- const unlock = opts.privateMedia ? createUnlockService(opts.privateMedia) : null;
69
66
  const chunked = opts.chunkedUpload
70
67
  ? createChunkedUploadService({
71
68
  catalogue,
@@ -85,13 +82,8 @@ export function createFilesStack(opts) {
85
82
  photoSource,
86
83
  mediaSource,
87
84
  registry,
88
- unlock,
89
85
  chunked,
90
86
  mount(app, auth) {
91
- if (unlock && !auth.authorizePrivate) {
92
- throw new Error('createFilesStack: privateMedia is configured but mount() got no authorizePrivate — ' +
93
- 'the private routes need a session→{userId, userSalt} resolver.');
94
- }
95
87
  mountMediaRoutes(app, {
96
88
  store,
97
89
  token,
@@ -107,26 +99,7 @@ export function createFilesStack(opts) {
107
99
  authorizeUpload: auth.authorizeUpload,
108
100
  // Prefer a mount-time override, else the service the stack built from `chunkedUpload` opts.
109
101
  chunked: auth.mintChunkUpload ?? chunked ?? undefined,
110
- privateUpload: unlock && auth.authorizePrivate
111
- ? {
112
- unlock,
113
- authorize: auth.authorizePrivate,
114
- passwordHeader: auth.passwordHeader,
115
- tokenHeader: auth.tokenHeader,
116
- passwordPolicy: opts.privateMedia?.passwordPolicy,
117
- }
118
- : undefined,
119
102
  });
120
- if (unlock && auth.authorizePrivate) {
121
- mountPrivateRoutes(app, {
122
- store,
123
- index: catalogue.asPrivateIndex(),
124
- unlock,
125
- basePath: opts.privateBasePath,
126
- authorize: auth.authorizePrivate,
127
- passwordPolicy: opts.privateMedia?.passwordPolicy,
128
- });
129
- }
130
103
  },
131
104
  };
132
105
  }
@@ -1,5 +1,4 @@
1
1
  import type { PlayableVideo, ProcessingStatus } from 'cursedbelt-core/media';
2
- import type { PrivateIndex } from '../../private-media/index-store';
3
2
  import type { MediaCtx } from '../types';
4
3
  export type FileType = 'image' | 'video' | 'audio' | 'doc' | 'code' | 'other';
5
4
  /** User-defined groupings (Apple-Photos-style). `favorites` is a system singleton per owner;
@@ -26,7 +25,6 @@ export type FileRecord = {
26
25
  size: number;
27
26
  fileType: FileType;
28
27
  tags: string[];
29
- isPrivate: boolean;
30
28
  hasThumbnail: boolean;
31
29
  hasHls: boolean;
32
30
  hasSprites: boolean;
@@ -67,11 +65,8 @@ export type ListFilesOptions = {
67
65
  type?: FileType;
68
66
  /** Single-tag filter (indexed via the join table). */
69
67
  tag?: string;
70
- /** Name substring (public files only — private names are sealed). */
68
+ /** Name substring. */
71
69
  query?: string;
72
- /** Include the caller's private files (requires an unlocked set — the caller resolves which
73
- * ones actually open). Omitted ⇒ public only, matching ca's default listing. */
74
- includePrivate?: boolean;
75
70
  /** Filter to members of a specific collection. An unlock-gated collection yields empty unless
76
71
  * `includeUnlockable` is also true. */
77
72
  collectionId?: string;
@@ -93,14 +88,11 @@ export declare function classifyFileType(mime: string, isText?: boolean): FileTy
93
88
  /** The catalogue contract. A SQLite implementation is the production default; the interface lets
94
89
  * an app swap it. All calls are owner-scoped via {@link MediaCtx} (one tenant never sees another). */
95
90
  /** Cheap owner-scoped aggregates (COUNT/SUM over the files table) — the admin
96
- * surface's "site stats" source. Private items report count + bytes only
97
- * (names/kinds stay sealed). */
91
+ * surface's "site stats" source. */
98
92
  export type CatalogueStats = {
99
93
  files: number;
100
94
  bytes: number;
101
95
  byType: Partial<Record<FileType, number>>;
102
- privateItems: number;
103
- privateBytes: number;
104
96
  processing: number;
105
97
  failed: number;
106
98
  };
@@ -109,9 +101,7 @@ export interface FileCatalogue {
109
101
  get(ctx: MediaCtx, id: string): Promise<FileRecord | null>;
110
102
  /** Owner-scoped aggregates for admin/stats surfaces. */
111
103
  stats(ctx: MediaCtx): Promise<CatalogueStats>;
112
- tagFacets(ctx: MediaCtx, opts?: {
113
- includePrivate?: boolean;
114
- }): Promise<TagFacet[]>;
104
+ tagFacets(ctx: MediaCtx): Promise<TagFacet[]>;
115
105
  /** Ingest a public upload: persist bytes, classify, kick off processing (thumbnail/HLS). */
116
106
  ingest(ctx: MediaCtx, upload: {
117
107
  name: string;
@@ -137,9 +127,8 @@ export interface FileCatalogue {
137
127
  }): Promise<FileRecord | null>;
138
128
  /** Overwrite a file's BYTES in place — the identity (id, tags, collections, favorites, view
139
129
  * history) survives while mime/size/fileType re-derive, stale derivatives are wiped, and
140
- * processing re-kicks (video transcode / image thumbnail). Public files only — replacing an
141
- * encrypted private item throws (delete + re-upload instead). Returns the refreshed record,
142
- * or null if the id is unknown/other-owner. */
130
+ * processing re-kicks (video transcode / image thumbnail). Returns the refreshed record, or
131
+ * null if the id is unknown/other-owner. */
143
132
  replaceContent(ctx: MediaCtx, id: string, upload: {
144
133
  mime: string;
145
134
  body: ReadableStream<Uint8Array>;
@@ -171,20 +160,20 @@ export interface FileCatalogue {
171
160
  reason: string;
172
161
  }[];
173
162
  }>;
174
- /** Recover from a restart that lost in-flight transcodes: re-kick public video rows still marked
175
- * pending/processing (their source persists), and mark stuck private rows failed. Call once at
176
- * app startup. Returns how many were re-queued vs. failed. */
163
+ /** Recover from a restart that lost in-flight transcodes: re-kick video rows still marked
164
+ * pending/processing (their source persists), and mark rows that have exhausted their kick
165
+ * budget failed. Call once at app startup. Returns how many were re-queued vs. failed. */
177
166
  recoverProcessing(): {
178
167
  requeued: number;
179
168
  failed: number;
180
169
  };
181
- /** Backfill grid thumbnails for finished public video rows that have no poster (landed while
170
+ /** Backfill grid thumbnails for finished video rows that have no poster (landed while
182
171
  * transcoding was off, or failed before the poster step). Re-kicks the poster pipeline for each.
183
172
  * Returns how many were re-queued. */
184
173
  backfillVideoPosters(): {
185
174
  requeued: number;
186
175
  };
187
- /** Backfill grid thumbnails for public image rows with no poster (e.g. HEIC photos whose sharp
176
+ /** Backfill grid thumbnails for image rows with no poster (e.g. HEIC photos whose sharp
188
177
  * thumbnail failed before the ffmpeg fallback). Re-kicks the image pipeline. */
189
178
  backfillImageThumbnails(): {
190
179
  requeued: number;
@@ -220,42 +209,3 @@ export interface FileCatalogue {
220
209
  }
221
210
  /** Resolve a video FileRecord to a PlayableVideo (URLs baked server-side). Non-video ⇒ null. */
222
211
  export type FileToPlayable = (ctx: MediaCtx, file: FileRecord) => Promise<PlayableVideo | null>;
223
- /** How a private item's content key is wrapped: `standard` (server-recoverable) vs `deniable`
224
- * (password-derived; zero-knowledge). Mirrors private-media `KeyMode`. */
225
- export type PrivateKeyMode = 'standard' | 'deniable';
226
- /** The OPAQUE locked-index row — only non-sensitive metadata (so a user can prune media whose
227
- * password is lost). NEVER name/tags/thumbnail: those are sealed. No set identifier is exposed. */
228
- export type PrivateFileItem = {
229
- id: string;
230
- size: number;
231
- createdAt: number;
232
- processingStatus: ProcessingStatus;
233
- processingProgress: number;
234
- hasHls: boolean;
235
- };
236
- /** Private-media capability of a catalogue. `createSqliteFileCatalogue` implements this alongside
237
- * {@link FileCatalogue}, storing private items as sealed rows in the SAME table (one source of
238
- * truth). Membership is by decrypt-success — no stored password/set hash.
239
- *
240
- * Deliberately small: the catalogue owns only INGEST (the new private-upload path). Everything
241
- * else — the locked index, unlock, try-a-password reveal, the gated content-key route, and the
242
- * encrypted-HLS byte serving — is the existing `mountPrivateRoutes`, which operates through a
243
- * {@link PrivateIndex}. `asPrivateIndex()` is the view that lets those routes read/write the
244
- * catalogue's private rows, so there is exactly one store and one set of routes. */
245
- export interface PrivateFileCatalogue {
246
- /** Ingest an encrypted video: stage plaintext (never persisted), transcode to AES-128 HLS, seal
247
- * the descriptor + wrap the content key under `setKey`, and insert a private row. */
248
- ingestPrivate(ctx: MediaCtx, upload: {
249
- name: string;
250
- mime: string;
251
- body: ReadableStream<Uint8Array>;
252
- size?: number;
253
- tags?: string[];
254
- }, opts: {
255
- setKey: Buffer;
256
- keyMode: PrivateKeyMode;
257
- }): Promise<PrivateFileItem>;
258
- /** A {@link PrivateIndex} view over this catalogue's private rows — pass to `mountPrivateRoutes`
259
- * so unlock/index/reveal/key/HLS all operate on the catalogue. `remove` also drops the bytes. */
260
- asPrivateIndex(): PrivateIndex;
261
- }
@@ -20,3 +20,12 @@ export function classifyFileType(mime, isText = false) {
20
20
  return 'doc';
21
21
  return 'other';
22
22
  }
23
+ // ── There is no private half, deliberately (removed 2026-09-17) ───────────────────
24
+ // A `PrivateFileCatalogue` used to sit here: sealed descriptors, wrapped content keys, a
25
+ // `KeyMode: 'standard' | 'deniable'` and membership "by decrypt-success — no stored password".
26
+ // It was 1,505 lines that no app ever mounted, over a byte store that encrypts nothing
27
+ // (binary-server's `exportFiles.ts`: *"Objects are stored unencrypted"*), so there was never a
28
+ // ciphertext behind it — and the owner chose to delete the whole module rather than carry a
29
+ // second, unexercised confidentiality story alongside the master lock, which is the one that
30
+ // actually guards these apps. This paragraph is the record; it is not an invitation to
31
+ // reintroduce it. The wall that IS real is `../../master-lock`.
@@ -4,12 +4,12 @@ export { createBinaryServerStore } from './binaryServerStore';
4
4
  export { type BinaryServerProcessingConfig, createBinaryServerProcessing, } from './binaryServerProcessing';
5
5
  export { createTranscodeJobClient, type TranscodeJobClient, type TranscodeJobClientConfig, type TranscodeJobResult, type TranscodeJobStatus, type TranscodeJobStatusBody, } from './transcodeJobs';
6
6
  export { createDiskStore } from './diskStore';
7
- export { type CatalogueStats, type ChunkedInit, type ChunkedUploadService, catalogueUploadKey, classifyFileType, createCatalogueMediaSource, createCataloguePhotoSource, createCatalogueVideoSource, createChunkedUploadService, createFilesStack, createFileToPlayable, createFileUrls, createLedgeredDeletes, createSqliteFileCatalogue, createStorageOpsLedger, createStorageReconciler, FILE_CATALOGUE_MIGRATIONS, type FileCatalogue, type FilePage, type FileRecord, type FilesStack, type FilesStackMountAuth, type FilesStackOptions, type FileToPlayable, type FileType, type FileUrls, fileBinaryKey, type ImageDerivatives, type ImageDerivativesOptions, type ListFilesOptions, type MintChunkUpload, type MountFileRoutesOptions, mountFileRoutes, type PrivateFileCatalogue, type PrivateFileItem, type PrivateKeyMode, type SqliteFileCatalogueOptions, type StorageAction, type StorageOpRow, type StorageOpsLedger, type StorageReconciler, type VideoProcessor, } from './files';
7
+ export { type CatalogueStats, type ChunkedInit, type ChunkedUploadService, catalogueUploadKey, classifyFileType, createCatalogueMediaSource, createCataloguePhotoSource, createCatalogueVideoSource, createChunkedUploadService, createFilesStack, createFileToPlayable, createFileUrls, createLedgeredDeletes, createSqliteFileCatalogue, createStorageOpsLedger, createStorageReconciler, FILE_CATALOGUE_MIGRATIONS, type FileCatalogue, type FilePage, type FileRecord, type FilesStack, type FilesStackMountAuth, type FilesStackOptions, type FileToPlayable, type FileType, type FileUrls, fileBinaryKey, type ImageDerivatives, type ImageDerivativesOptions, type ListFilesOptions, type MintChunkUpload, type MountFileRoutesOptions, mountFileRoutes, type SqliteFileCatalogueOptions, type StorageAction, type StorageOpRow, type StorageOpsLedger, type StorageReconciler, type VideoProcessor, } from './files';
8
8
  export { type CorsResolver, type MountMediaOptions, mountMediaRoutes } from './mountMediaRoutes';
9
9
  export { createDiskSource, createMediaUrls, type DiskSource, type MediaUrls, SourceRegistry, } from './sources';
10
10
  export { createS3ObjectStore, type S3ClientLike, type S3ObjectHandle, type S3ObjectStoreOptions, type S3ObjectWriter, } from './s3ObjectStore';
11
11
  export { decodeRange, type RangeDecision, type ServeRangeOpts, serveRange } from './streamRange';
12
12
  export { createStreamTokenService, type StreamGrant, type StreamTokenService } from './streamToken';
13
- export { fsHlsFileKey, fsHlsMasterKey, fsHlsPrefix, fsMp4Key, fsPosterKey, fsSourceKey, fsSpriteKey, fsVttKey, type MediaCtx, type MediaGallerySource, type MediaSource, type MediaStore, type PhotoSource, privateFsKey, } from './types';
13
+ export { fsHlsFileKey, fsHlsMasterKey, fsHlsPrefix, fsMp4Key, fsPosterKey, fsSourceKey, fsSpriteKey, fsVttKey, type MediaCtx, type MediaGallerySource, type MediaSource, type MediaStore, type PhotoSource, } from './types';
14
14
  export { type ProcessOptions, type ProcessResult, processVideo, } from './videoProcessor';
15
15
  export { objectStoreAsMediaStore } from './objectStoreMediaStore';
@@ -22,6 +22,6 @@ export { createDiskSource, createMediaUrls, SourceRegistry, } from './sources';
22
22
  export { createS3ObjectStore, } from './s3ObjectStore';
23
23
  export { decodeRange, serveRange } from './streamRange';
24
24
  export { createStreamTokenService } from './streamToken';
25
- export { fsHlsFileKey, fsHlsMasterKey, fsHlsPrefix, fsMp4Key, fsPosterKey, fsSourceKey, fsSpriteKey, fsVttKey, privateFsKey, } from './types';
25
+ export { fsHlsFileKey, fsHlsMasterKey, fsHlsPrefix, fsMp4Key, fsPosterKey, fsSourceKey, fsSpriteKey, fsVttKey, } from './types';
26
26
  export { processVideo, } from './videoProcessor';
27
27
  export { objectStoreAsMediaStore } from './objectStoreMediaStore';
@@ -1,6 +1,6 @@
1
1
  import type { Context, Hono } from 'hono';
2
2
  import type { SourceRegistry } from './sources';
3
- import type { StreamGrant, StreamTokenService } from './streamToken';
3
+ import type { StreamTokenService } from './streamToken';
4
4
  import { type MediaCtx, type MediaStore } from './types';
5
5
  /** Resolves the Access-Control-Allow-Origin for a request origin: a fixed origin, an allowlist
6
6
  * (echoes the request origin when allowed), or a custom resolver (return null to deny). */
@@ -21,8 +21,5 @@ export type MountMediaOptions = {
21
21
  /** Authorizes an upload request (it CAN send the Bearer, unlike a media element). Return a
22
22
  * MediaCtx to allow, or null to reject. Omit ⇒ uploads are disabled on this mount. */
23
23
  authorizeUpload?: (c: Context) => Promise<MediaCtx | null> | MediaCtx | null;
24
- /** Optional hook to decrypt bytes for the private (pfs/…) keyspace before serving. Wired by
25
- * the private-media layer; omitted ⇒ all bytes served as stored. */
26
- decryptStream?: (stream: ReadableStream<Uint8Array>, grant: StreamGrant) => ReadableStream<Uint8Array>;
27
24
  };
28
25
  export declare function mountMediaRoutes(app: Hono, opts: MountMediaOptions): void;
@@ -182,12 +182,3 @@ export declare const fsPosterKey: (owner: string, id: string) => string;
182
182
  export declare const fsDisplayKey: (owner: string, id: string) => string;
183
183
  export declare const fsSpriteKey: (owner: string, id: string) => string;
184
184
  export declare const fsVttKey: (owner: string, id: string) => string;
185
- /** Private (encrypted) namespace — the private-media layer stores ciphertext here. The HLS
186
- * segments under it are AES-128-encrypted at rest (the player fetches the content key via the
187
- * gated key route and decrypts); a distinct keyspace from public `hls/` so the two never collide. */
188
- export declare const privateFsKey: (owner: string, id: string) => string;
189
- export declare const privateHlsPrefix: (owner: string, id: string) => string;
190
- export declare const privateHlsMasterKey: (owner: string, id: string) => string;
191
- export declare const privateHlsFileKey: (owner: string, id: string, rel: string) => string;
192
- export declare const privatePosterKey: (owner: string, id: string) => string;
193
- export declare const privateSourceKey: (owner: string, id: string) => string;
@@ -30,12 +30,5 @@ export const fsPosterKey = (owner, id) => `fs/${owner}/${id}-poster.jpg`;
30
30
  export const fsDisplayKey = (owner, id) => `fs/${owner}/${id}-display.jpg`;
31
31
  export const fsSpriteKey = (owner, id) => `fs/${owner}/${id}-sprites.jpg`;
32
32
  export const fsVttKey = (owner, id) => `fs/${owner}/${id}-sprites.vtt`;
33
- /** Private (encrypted) namespace — the private-media layer stores ciphertext here. The HLS
34
- * segments under it are AES-128-encrypted at rest (the player fetches the content key via the
35
- * gated key route and decrypts); a distinct keyspace from public `hls/` so the two never collide. */
36
- export const privateFsKey = (owner, id) => `pfs/${owner}/${id}`;
37
- export const privateHlsPrefix = (owner, id) => `pfs/${owner}/${id}/hls`;
38
- export const privateHlsMasterKey = (owner, id) => `pfs/${owner}/${id}/hls/master.m3u8`;
39
- export const privateHlsFileKey = (owner, id, rel) => `pfs/${owner}/${id}/hls/${rel}`;
40
- export const privatePosterKey = (owner, id) => `pfs/${owner}/${id}/poster.enc`;
41
- export const privateSourceKey = (owner, id) => `pfs/${owner}/${id}/source.enc`;
33
+ // The `pfs/` (ciphertext) keyspace was deleted with private media on 2026-09-17 — nothing ever
34
+ // wrote a byte to it on this fleet, and binary-server stores objects unencrypted regardless.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursedbelt-server",
3
- "version": "3.0.1",
3
+ "version": "4.0.0",
4
4
  "license": "ISC",
5
5
  "type": "module",
6
6
  "description": "The app-facing Bun/Hono server tier of the cursedbelt split \u2014 storage, sharing, activity, guard, sync. React-free; cursedbelt-core below it.",
@@ -20,7 +20,7 @@
20
20
  "typecheck": "tsc -p tsconfig.build.json --noEmit",
21
21
  "test": "NODE_ENV=development bun test src",
22
22
  "build": "bun run clean && tsc -p tsconfig.build.json",
23
- "paths": "bun ../../tools/check-paths.ts .",
23
+ "paths": "bun run scripts/paths.ts",
24
24
  "verify": "bun run paths && bun run typecheck && bun run build && bun run test",
25
25
  "prepublishOnly": "bun run verify",
26
26
  "test:pg": "bun run scripts/testPg.ts"
@@ -85,8 +85,9 @@ import pkg from '../package.json';
85
85
  * fixture stopped demonstrating that peers are unreachable
86
86
  * → red on the control itself: "the fixture resolved 'otplib' … Every assertion below
87
87
  * is vacuous until that is fixed". The vacuity guard fires.
88
- * · `src/server/private-media/unlock.ts` put back to `import … from '../auth'` — the
89
- * second, two-hop edge, which grep for `../auth` inside `src/server/storage` cannot see
88
+ * · `src/server/private-media/unlock.ts` (deleted 2026-09-17) put back to
89
+ * `import … from '../auth'` — the second, two-hop edge, which grep for `../auth` inside
90
+ * `src/server/storage` cannot see
90
91
  * → red: "importing cursedbelt/server/storage statically pulls these OPTIONAL peers
91
92
  * … + otplib". This is the branch that found that edge in the first place.
92
93
  */
@@ -276,8 +277,9 @@ console.log(JSON.stringify(out));
276
277
  const NO_STATIC_OPTIONAL_PEERS = [
277
278
  {
278
279
  subpath: './storage',
279
- /** Its `streamToken.ts` and (two hops out) `private-media/unlock.ts` both imported the
280
- * `../auth` index for two jose functions, so `otplib` was static in here until 2.6.0. */
280
+ /** Its `streamToken.ts` and (two hops out) the since-deleted `private-media/unlock.ts`
281
+ * both imported the `../auth` index for two jose functions, so `otplib` was static in
282
+ * here until 2.6.0. The edge this guards against is the import, not the file. */
281
283
  entry: 'src/server/storage/index.ts',
282
284
  },
283
285
  ] as const;
@@ -4,7 +4,7 @@ import type { AppJwtPayload } from './types';
4
4
  /**
5
5
  * JWT sign/verify on `jose` (Web Crypto HMAC — native on Bun + Node, no native
6
6
  * deps; replaces `jsonwebtoken`). Every token system in the apps — the access
7
- * token plus the short-lived vault/private-media/files/yt-oauth unlock tokens —
7
+ * token plus the short-lived vault/files/yt-oauth unlock tokens —
8
8
  * goes through these, so there is exactly ONE jose integration to reason about.
9
9
  *
10
10
  * jose verifies any standard HS256 JWT regardless of which library signed it, so a
@@ -57,7 +57,7 @@ export async function verifyToken<T extends JWTPayload = AppJwtPayload>(
57
57
  }
58
58
 
59
59
  /**
60
- * Verify a single-purpose token (vault/private-media/files/yt-oauth) AND assert its
60
+ * Verify a single-purpose token (vault/files/yt-oauth) AND assert its
61
61
  * `purpose` claim matches — so an access token (or a token for a *different*
62
62
  * feature) can never be replayed as, say, a vault-unlock token. Throws on a bad
63
63
  * signature, expiry, or a purpose mismatch.