@proteinjs/db-file 1.8.4 → 1.9.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.
Files changed (102) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/generated/index.d.ts.map +1 -1
  3. package/dist/generated/index.js +3 -1
  4. package/dist/generated/index.js.map +1 -1
  5. package/dist/index.d.ts +4 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +4 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/src/DbFileStorageDriver.d.ts +7 -0
  10. package/dist/src/DbFileStorageDriver.d.ts.map +1 -1
  11. package/dist/src/DbFileStorageDriver.js +69 -41
  12. package/dist/src/DbFileStorageDriver.js.map +1 -1
  13. package/dist/src/FileCopyForOthers.d.ts +52 -0
  14. package/dist/src/FileCopyForOthers.d.ts.map +1 -0
  15. package/dist/src/FileCopyForOthers.js +62 -0
  16. package/dist/src/FileCopyForOthers.js.map +1 -0
  17. package/dist/src/FileStorage.d.ts +158 -11
  18. package/dist/src/FileStorage.d.ts.map +1 -1
  19. package/dist/src/FileStorage.js +562 -41
  20. package/dist/src/FileStorage.js.map +1 -1
  21. package/dist/src/FileStorageDriver.d.ts +11 -0
  22. package/dist/src/FileStorageDriver.d.ts.map +1 -1
  23. package/dist/src/FileStorageError.d.ts +62 -0
  24. package/dist/src/FileStorageError.d.ts.map +1 -0
  25. package/dist/src/FileStorageError.js +118 -0
  26. package/dist/src/FileStorageError.js.map +1 -0
  27. package/dist/src/FileVariantMaker.d.ts +66 -0
  28. package/dist/src/FileVariantMaker.d.ts.map +1 -0
  29. package/dist/src/FileVariantMaker.js +62 -0
  30. package/dist/src/FileVariantMaker.js.map +1 -0
  31. package/dist/src/routes/FileResponder.d.ts +41 -0
  32. package/dist/src/routes/FileResponder.d.ts.map +1 -0
  33. package/dist/src/routes/FileResponder.js +135 -0
  34. package/dist/src/routes/FileResponder.js.map +1 -0
  35. package/dist/src/routes/getFile.d.ts +5 -13
  36. package/dist/src/routes/getFile.d.ts.map +1 -1
  37. package/dist/src/routes/getFile.js +20 -56
  38. package/dist/src/routes/getFile.js.map +1 -1
  39. package/dist/src/routes/getFileVariant.d.ts +20 -0
  40. package/dist/src/routes/getFileVariant.d.ts.map +1 -0
  41. package/dist/src/routes/getFileVariant.js +112 -0
  42. package/dist/src/routes/getFileVariant.js.map +1 -0
  43. package/dist/src/routes/getFileVariantRoute.d.ts +7 -0
  44. package/dist/src/routes/getFileVariantRoute.d.ts.map +1 -0
  45. package/dist/src/routes/getFileVariantRoute.js +9 -0
  46. package/dist/src/routes/getFileVariantRoute.js.map +1 -0
  47. package/dist/src/tables/FileTable.d.ts +70 -2
  48. package/dist/src/tables/FileTable.d.ts.map +1 -1
  49. package/dist/src/tables/FileTable.js +30 -0
  50. package/dist/src/tables/FileTable.js.map +1 -1
  51. package/dist/test/DbFileStorageDriverMissingBytes.test.d.ts +2 -0
  52. package/dist/test/DbFileStorageDriverMissingBytes.test.d.ts.map +1 -0
  53. package/dist/test/DbFileStorageDriverMissingBytes.test.js +184 -0
  54. package/dist/test/DbFileStorageDriverMissingBytes.test.js.map +1 -0
  55. package/dist/test/FileByteAccess.test.js +6 -3
  56. package/dist/test/FileByteAccess.test.js.map +1 -1
  57. package/dist/test/FileCopyForOthers.test.d.ts +2 -0
  58. package/dist/test/FileCopyForOthers.test.d.ts.map +1 -0
  59. package/dist/test/FileCopyForOthers.test.js +1143 -0
  60. package/dist/test/FileCopyForOthers.test.js.map +1 -0
  61. package/dist/test/FileDerivedProvenance.test.d.ts +2 -0
  62. package/dist/test/FileDerivedProvenance.test.d.ts.map +1 -0
  63. package/dist/test/FileDerivedProvenance.test.js +420 -0
  64. package/dist/test/FileDerivedProvenance.test.js.map +1 -0
  65. package/dist/test/FileMediaMetadata.test.js +60 -5
  66. package/dist/test/FileMediaMetadata.test.js.map +1 -1
  67. package/dist/test/FileStorageError.test.d.ts +2 -0
  68. package/dist/test/FileStorageError.test.d.ts.map +1 -0
  69. package/dist/test/FileStorageError.test.js +68 -0
  70. package/dist/test/FileStorageError.test.js.map +1 -0
  71. package/dist/test/FileTestEnvironment.d.ts.map +1 -1
  72. package/dist/test/FileTestEnvironment.js +3 -0
  73. package/dist/test/FileTestEnvironment.js.map +1 -1
  74. package/dist/test/FileVariants.test.d.ts +2 -0
  75. package/dist/test/FileVariants.test.d.ts.map +1 -0
  76. package/dist/test/FileVariants.test.js +1008 -0
  77. package/dist/test/FileVariants.test.js.map +1 -0
  78. package/dist/test/GetFileRoute.test.js +29 -0
  79. package/dist/test/GetFileRoute.test.js.map +1 -1
  80. package/generated/index.ts +3 -1
  81. package/index.ts +4 -0
  82. package/package.json +5 -5
  83. package/src/DbFileStorageDriver.ts +32 -21
  84. package/src/FileCopyForOthers.ts +71 -0
  85. package/src/FileStorage.ts +377 -19
  86. package/src/FileStorageDriver.ts +12 -0
  87. package/src/FileStorageError.ts +123 -0
  88. package/src/FileVariantMaker.ts +89 -0
  89. package/src/routes/FileResponder.ts +85 -0
  90. package/src/routes/getFile.ts +9 -44
  91. package/src/routes/getFileVariant.ts +53 -0
  92. package/src/routes/getFileVariantRoute.ts +7 -0
  93. package/src/tables/FileTable.ts +82 -1
  94. package/test/DbFileStorageDriverMissingBytes.test.ts +85 -0
  95. package/test/FileByteAccess.test.ts +7 -1
  96. package/test/FileCopyForOthers.test.ts +702 -0
  97. package/test/FileDerivedProvenance.test.ts +248 -0
  98. package/test/FileMediaMetadata.test.ts +44 -0
  99. package/test/FileStorageError.test.ts +79 -0
  100. package/test/FileTestEnvironment.ts +3 -0
  101. package/test/FileVariants.test.ts +604 -0
  102. package/test/GetFileRoute.test.ts +19 -0
@@ -1,4 +1,15 @@
1
1
  "use strict";
2
+ var __assign = (this && this.__assign) || function () {
3
+ __assign = Object.assign || function(t) {
4
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
5
+ s = arguments[i];
6
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7
+ t[p] = s[p];
8
+ }
9
+ return t;
10
+ };
11
+ return __assign.apply(this, arguments);
12
+ };
2
13
  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
14
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
15
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -35,14 +46,28 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
35
46
  if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36
47
  }
37
48
  };
49
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
50
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
51
+ if (ar || !(i in from)) {
52
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
53
+ ar[i] = from[i];
54
+ }
55
+ }
56
+ return to.concat(ar || Array.prototype.slice.call(from));
57
+ };
38
58
  Object.defineProperty(exports, "__esModule", { value: true });
39
59
  exports.FileStorage = exports.getFileStorage = void 0;
60
+ var db_1 = require("@proteinjs/db");
40
61
  var user_1 = require("@proteinjs/user");
62
+ var FileTable_1 = require("./tables/FileTable");
41
63
  var tables_1 = require("./tables/tables");
42
64
  var FileStorageService_1 = require("./services/FileStorageService");
43
65
  var FileReachabilityResolver_1 = require("./FileReachabilityResolver");
66
+ var FileCopyForOthers_1 = require("./FileCopyForOthers");
67
+ var FileVariantMaker_1 = require("./FileVariantMaker");
44
68
  var reflection_1 = require("@proteinjs/reflection");
45
69
  var logger_1 = require("@proteinjs/logger");
70
+ var service_1 = require("@proteinjs/service");
46
71
  var DbFileStorageDriver_1 = require("./DbFileStorageDriver");
47
72
  /**
48
73
  * A convenience factory function so code using this is portable (can be used in server or browser).
@@ -64,6 +89,7 @@ var FileStorage = /** @class */ (function () {
64
89
  allUsers: true,
65
90
  },
66
91
  };
92
+ this.logger = new logger_1.Logger({ name: this.constructor.name });
67
93
  }
68
94
  /**
69
95
  * The `FileStorageDriver` for this process — provided by the `DefaultFileStorageDriverFactory`
@@ -114,44 +140,158 @@ var FileStorage = /** @class */ (function () {
114
140
  * Retrieves the metadata of a given file — THE file-read access decision every serving path
115
141
  * derives from (`/file/:id`, signed-URL minting, the browser service).
116
142
  *
117
- * Two legs: the caller's own file resolves by the SCOPED read; when that misses, the
118
- * shared-content leg asks the registered {@link FileReachabilityResolver}s whether the caller
119
- * can read a row that REFERENCES the file (a shared thought's media node) — content access
120
- * confers file access, through the content's own grant-filtered read, never by widening file
121
- * scope. No resolver vouching means the miss stands. Reads only: writes/deletes stay scoped.
143
+ * Three legs, cheapest first: the caller's own file resolves by the SCOPED read. When that
144
+ * misses, the derived leg: a VARIANT the seam made (`File.variantOf`) is readable by whoever
145
+ * can read its original — this same decision, asked of the original — so a variant derived
146
+ * after the content was placed (no content row names it) is reachable by exactly the readers
147
+ * of the file it was made from; one system point read decides whether the row is a variant at
148
+ * all. Last, the shared-content leg asks the registered {@link FileReachabilityResolver}s
149
+ * whether the caller can read a row that REFERENCES the file (a shared thought's media node) —
150
+ * content access confers file access, through the content's own grant-filtered read, never by
151
+ * widening file scope. No leg vouching means the miss stands. Reads only: writes/deletes stay
152
+ * scoped.
122
153
  * @param fileId - The `id` of the file.
123
154
  * @returns The file metadata, or `undefined` when the caller can neither own nor reach it.
124
155
  */
125
156
  FileStorage.prototype.getFile = function (fileId) {
157
+ var _a;
126
158
  return __awaiter(this, void 0, void 0, function () {
127
- var db, file, _i, _a, resolver;
128
- return __generator(this, function (_b) {
129
- switch (_b.label) {
159
+ var db, file, row, _i, _b, resolver;
160
+ return __generator(this, function (_c) {
161
+ switch (_c.label) {
130
162
  case 0:
131
163
  db = (0, user_1.getScopedDb)();
132
164
  return [4 /*yield*/, db.get(tables_1.tables.File, { id: fileId })];
133
165
  case 1:
134
- file = _b.sent();
166
+ file = _c.sent();
135
167
  if (file) {
136
168
  return [2 /*return*/, file];
137
169
  }
138
- _i = 0, _a = (0, FileReachabilityResolver_1.getFileReachabilityResolvers)();
139
- _b.label = 2;
170
+ return [4 /*yield*/, (0, user_1.getScopedDbAsSystem)().get(tables_1.tables.File, { id: fileId })];
140
171
  case 2:
141
- if (!(_i < _a.length)) return [3 /*break*/, 6];
142
- resolver = _a[_i];
172
+ row = _c.sent();
173
+ if (!row) {
174
+ return [2 /*return*/, undefined];
175
+ }
176
+ if (!((_a = row.variantOf) === null || _a === void 0 ? void 0 : _a._id)) return [3 /*break*/, 4];
177
+ return [4 /*yield*/, this.getFile(row.variantOf._id)];
178
+ case 3:
179
+ // A variant's reachability is its original's — the copy for others carries no `variantOf`,
180
+ // so its own id stays nobody's but the owner's.
181
+ return [2 /*return*/, (_c.sent()) ? row : undefined];
182
+ case 4:
183
+ _i = 0, _b = (0, FileReachabilityResolver_1.getFileReachabilityResolvers)();
184
+ _c.label = 5;
185
+ case 5:
186
+ if (!(_i < _b.length)) return [3 /*break*/, 8];
187
+ resolver = _b[_i];
143
188
  return [4 /*yield*/, resolver.canReadViaReference(fileId)];
189
+ case 6:
190
+ if (_c.sent()) {
191
+ // Reachability established through the caller's grant-filtered content read — the row
192
+ // itself lives in the owner's scope, so it is served via system read.
193
+ return [2 /*return*/, row];
194
+ }
195
+ _c.label = 7;
196
+ case 7:
197
+ _i++;
198
+ return [3 /*break*/, 5];
199
+ case 8: return [2 /*return*/, undefined];
200
+ }
201
+ });
202
+ });
203
+ };
204
+ /**
205
+ * The variant of this kind of a file — the derived File the row names, or one made now, ONCE,
206
+ * by the registered {@link FileVariantMaker} (the read path's door, behind
207
+ * `GET /file/:id/variant/:kind`: a file made before the seam existed, or by a producer that did
208
+ * not derive it, gets its variant on the first request from a surface that draws it; every
209
+ * later request is served the same row without the maker). Server-only, like `getSignedUrl` —
210
+ * the browser asks by URL, never by service call. Access is the original's ({@link getFile});
211
+ * the variant is stored as the owner's own File whoever's read made it, carrying the original's
212
+ * provenance ({@link FileTable.provenanceColumns}), and is served like any File afterwards —
213
+ * with the same non-owner copy rule.
214
+ * @returns The variant's File row, or `undefined` when the caller cannot read the original, no
215
+ * maker is registered, the maker does not apply to this file for this kind, or the
216
+ * maker could not make it of these bytes (the consumer then draws the original).
217
+ */
218
+ FileStorage.prototype.getVariant = function (fileId, kind) {
219
+ var _a;
220
+ return __awaiter(this, void 0, void 0, function () {
221
+ var file, named, maker, variantId, _b;
222
+ return __generator(this, function (_c) {
223
+ switch (_c.label) {
224
+ case 0: return [4 /*yield*/, this.getFile(fileId)];
225
+ case 1:
226
+ file = _c.sent();
227
+ if (!file) {
228
+ return [2 /*return*/, undefined];
229
+ }
230
+ named = (_a = file[kind]) === null || _a === void 0 ? void 0 : _a._id;
231
+ if (!named) return [3 /*break*/, 3];
232
+ return [4 /*yield*/, (0, user_1.getScopedDbAsSystem)().get(tables_1.tables.File, { id: named })];
233
+ case 2: return [2 /*return*/, _c.sent()];
144
234
  case 3:
145
- if (!_b.sent()) return [3 /*break*/, 5];
146
- return [4 /*yield*/, (0, user_1.getScopedDbAsSystem)().get(tables_1.tables.File, { id: fileId })];
147
- case 4:
148
- // Reachability established through the caller's grant-filtered content read — the row
149
- // itself lives in the owner's scope, so it is served via system read.
150
- return [2 /*return*/, _b.sent()];
235
+ maker = (0, FileVariantMaker_1.getFileVariantMaker)();
236
+ if (!maker || !maker.appliesTo(file, kind)) {
237
+ return [2 /*return*/, undefined];
238
+ }
239
+ return [4 /*yield*/, this.deriveOnce(file, kind)];
240
+ case 4:
241
+ variantId = _c.sent();
242
+ if (!variantId) return [3 /*break*/, 6];
243
+ return [4 /*yield*/, (0, user_1.getScopedDbAsSystem)().get(tables_1.tables.File, { id: variantId })];
151
244
  case 5:
245
+ _b = _c.sent();
246
+ return [3 /*break*/, 7];
247
+ case 6:
248
+ _b = undefined;
249
+ _c.label = 7;
250
+ case 7: return [2 /*return*/, _b];
251
+ }
252
+ });
253
+ });
254
+ };
255
+ /**
256
+ * Make and store every variant the registered maker applies to, with the original's bytes in
257
+ * hand (the ingest's door — one decode of bytes just stored, never a re-read). Each is named on
258
+ * the row and carries the original's provenance ({@link FileTable.provenanceColumns}); a kind
259
+ * the maker does not apply to is left unset (the read path may derive it later if the maker's
260
+ * rule changes). With no maker registered nothing is made.
261
+ * @returns The caller's file row with each variant made here named on it (the row as the caller
262
+ * holds it, not a re-read — an unset column stays unset), and the variant Files made.
263
+ */
264
+ FileStorage.prototype.deriveVariants = function (file, bytes) {
265
+ var _a;
266
+ return __awaiter(this, void 0, void 0, function () {
267
+ var maker, variants, named, _i, FILE_VARIANT_KINDS_1, kind, variantId, _b, _c;
268
+ return __generator(this, function (_d) {
269
+ switch (_d.label) {
270
+ case 0:
271
+ maker = (0, FileVariantMaker_1.getFileVariantMaker)();
272
+ variants = {};
273
+ named = {};
274
+ if (!maker) return [3 /*break*/, 5];
275
+ _i = 0, FILE_VARIANT_KINDS_1 = FileVariantMaker_1.FILE_VARIANT_KINDS;
276
+ _d.label = 1;
277
+ case 1:
278
+ if (!(_i < FILE_VARIANT_KINDS_1.length)) return [3 /*break*/, 5];
279
+ kind = FILE_VARIANT_KINDS_1[_i];
280
+ if (!(!((_a = file[kind]) === null || _a === void 0 ? void 0 : _a._id) && maker.appliesTo(file, kind))) return [3 /*break*/, 4];
281
+ return [4 /*yield*/, this.makeVariant(file, kind, bytes)];
282
+ case 2:
283
+ variantId = _d.sent();
284
+ _b = variants;
285
+ _c = kind;
286
+ return [4 /*yield*/, (0, user_1.getScopedDbAsSystem)().get(tables_1.tables.File, { id: variantId })];
287
+ case 3:
288
+ _b[_c] = _d.sent();
289
+ named[kind] = new db_1.Reference(tables_1.tables.File.name, variantId);
290
+ _d.label = 4;
291
+ case 4:
152
292
  _i++;
153
- return [3 /*break*/, 2];
154
- case 6: return [2 /*return*/, file];
293
+ return [3 /*break*/, 1];
294
+ case 5: return [2 /*return*/, { file: __assign(__assign({}, file), named), variants: variants }];
155
295
  }
156
296
  });
157
297
  });
@@ -164,26 +304,63 @@ var FileStorage = /** @class */ (function () {
164
304
  * exactly as reachable as the row that names them. The gate lives here because this is the
165
305
  * browser-facing service boundary (`FileStorageService` is `allUsers`) and the driver byte ops
166
306
  * below are deliberately unscoped. Server-side doors that make their OWN documented access
167
- * decision (the avatar route, issue/ticket attachment doors) read bytes through
168
- * `FileStorage.getDriver()` instead.
307
+ * decision (an attachment door keyed by a record's linkage, say) serve through
308
+ * {@link getAuthorizedFileData} — the same owner-or-copy bytes, without this gate.
309
+ *
310
+ * WHICH bytes: the owner's read serves the original; anyone else's — a read that reached the
311
+ * file through the shared-content leg — serves the {@link FileCopyForOthers} copy
312
+ * ({@link servedFileId}): the same substitution `getSignedUrl` makes, so the proxy route, the
313
+ * signed-URL route and every server-side reader of this door agree.
169
314
  * @param fileId - The `id` of the file.
170
315
  * @returns The file data as a single string.
171
- * @throws When the file row does not exist or is not readable by the caller — the same named
172
- * miss either way, so existence is not leaked to unauthorized callers.
316
+ * @throws ServiceRefusal (404, `File not found`) when the file row does not exist or is not
317
+ * readable by the caller, and when the caller is not the owner and no copy can be made
318
+ * ({@link servedFileId}) — ONE answer for all three ({@link notFound}), so no door tells
319
+ * a missing file from one this caller may not have.
173
320
  */
174
321
  FileStorage.prototype.getFileData = function (fileId) {
175
322
  return __awaiter(this, void 0, void 0, function () {
176
- var file;
177
- return __generator(this, function (_a) {
178
- switch (_a.label) {
323
+ var file, _a, _b;
324
+ return __generator(this, function (_c) {
325
+ switch (_c.label) {
179
326
  case 0: return [4 /*yield*/, this.getFile(fileId)];
180
327
  case 1:
181
- file = _a.sent();
328
+ file = _c.sent();
182
329
  if (!file) {
183
- throw new Error("File not found: ".concat(fileId));
330
+ throw FileStorage.notFound();
184
331
  }
185
- return [4 /*yield*/, FileStorage.getDriver().getFileData(fileId)];
186
- case 2: return [2 /*return*/, _a.sent()];
332
+ _b = (_a = FileStorage.getDriver()).getFileData;
333
+ return [4 /*yield*/, this.servedFileId(file)];
334
+ case 2: return [4 /*yield*/, _b.apply(_a, [_c.sent()])];
335
+ case 3: return [2 /*return*/, _c.sent()];
336
+ }
337
+ });
338
+ });
339
+ };
340
+ /**
341
+ * The bytes THIS caller is served for a file a server-side door has ALREADY decided the caller
342
+ * may read, by its own documented rule (an attachment door keyed by a record's linkage, say —
343
+ * a read neither the caller's scope nor a {@link FileReachabilityResolver} would open). The
344
+ * door vouches for access; this decides WHICH bytes, exactly as {@link getFileData} does: the
345
+ * original for the file's owner, the {@link FileCopyForOthers} copy for anyone else — the same
346
+ * copy, made once and named on the row, that every other door serves them. Server-only (not
347
+ * part of the browser-facing `FileStorageService`), so the rule that opens the file stays the
348
+ * door's.
349
+ * @param file - The file's row, as the door read it (its `scope` names the owner).
350
+ * @returns The file data as a single string.
351
+ * @throws ServiceRefusal (404) when the caller is not the owner and no copy can be made — the same
352
+ * refusal every door gives ({@link servedFileId}).
353
+ */
354
+ FileStorage.prototype.getAuthorizedFileData = function (file) {
355
+ return __awaiter(this, void 0, void 0, function () {
356
+ var _a, _b;
357
+ return __generator(this, function (_c) {
358
+ switch (_c.label) {
359
+ case 0:
360
+ _b = (_a = FileStorage.getDriver()).getFileData;
361
+ return [4 /*yield*/, this.servedFileId(file)];
362
+ case 1: return [4 /*yield*/, _b.apply(_a, [_c.sent()])];
363
+ case 2: return [2 /*return*/, _c.sent()];
187
364
  }
188
365
  });
189
366
  });
@@ -192,30 +369,35 @@ var FileStorage = /** @class */ (function () {
192
369
  * Mint a short-lived, read-only URL for the file's bytes (server-only; not part of the
193
370
  * browser-facing `FileStorageService`). A signed URL is a bearer capability, so minting
194
371
  * verifies the caller can read the file row (the two-legged {@link getFile} read — own scope
195
- * or shared-content reachability) before signing.
372
+ * or shared-content reachability) before signing — and signs the bytes THAT caller is served:
373
+ * the original for the owner, the {@link FileCopyForOthers} copy for anyone else.
196
374
  * @param fileId - The `id` of the file.
197
375
  * @param options.ttlMs - How long the URL stays valid; the driver applies its default when omitted.
198
376
  * @returns The signed URL, or `undefined` when the driver has no external URL space
199
377
  * (`DbFileStorageDriver`) — the caller then serves bytes through the proxy route.
200
- * @throws When the file row does not exist or is not readable by the caller.
378
+ * @throws ServiceRefusal (404, `File not found`) when the file row does not exist or is not
379
+ * readable by the caller, and when the caller is not the owner and no copy can be made —
380
+ * the one answer {@link getFileData} gives ({@link notFound}).
201
381
  */
202
382
  FileStorage.prototype.getSignedUrl = function (fileId, options) {
203
383
  return __awaiter(this, void 0, void 0, function () {
204
- var file, driver;
205
- return __generator(this, function (_a) {
206
- switch (_a.label) {
384
+ var file, driver, _a, _b;
385
+ return __generator(this, function (_c) {
386
+ switch (_c.label) {
207
387
  case 0: return [4 /*yield*/, this.getFile(fileId)];
208
388
  case 1:
209
- file = _a.sent();
389
+ file = _c.sent();
210
390
  if (!file) {
211
- throw new Error("File not found: ".concat(fileId));
391
+ throw FileStorage.notFound();
212
392
  }
213
393
  driver = FileStorage.getDriver();
214
394
  if (!driver.getSignedUrl) {
215
395
  return [2 /*return*/, undefined];
216
396
  }
217
- return [4 /*yield*/, driver.getSignedUrl(fileId, options)];
218
- case 2: return [2 /*return*/, _a.sent()];
397
+ _b = (_a = driver).getSignedUrl;
398
+ return [4 /*yield*/, this.servedFileId(file)];
399
+ case 2: return [4 /*yield*/, _b.apply(_a, [_c.sent(), options])];
400
+ case 3: return [2 /*return*/, _c.sent()];
219
401
  }
220
402
  });
221
403
  });
@@ -227,6 +409,10 @@ var FileStorage = /** @class */ (function () {
227
409
  * (the same authorization a scoped row update enforces). Deliberately STRICTER than
228
410
  * {@link getFileData}: the {@link FileReachabilityResolver} leg widens READS only, so a
229
411
  * share recipient who can read the bytes still cannot write them.
412
+ *
413
+ * New bytes make every derived File stale — the copy others were served, the preview, the
414
+ * stage variant: each is dropped here — the row forgets it, then its row (and, through the
415
+ * delete watcher, its bytes) goes — and the next read that wants one makes a fresh one.
230
416
  * @param fileId - The `id` of the file.
231
417
  * @param data - The new data string to replace the existing data.
232
418
  * @throws When the file row does not exist or is not writable by the caller — the same named
@@ -247,6 +433,9 @@ var FileStorage = /** @class */ (function () {
247
433
  }
248
434
  return [4 /*yield*/, FileStorage.getDriver().updateFileData(fileId, data)];
249
435
  case 2:
436
+ _a.sent();
437
+ return [4 /*yield*/, this.dropDerivedFiles(file)];
438
+ case 3:
250
439
  _a.sent();
251
440
  return [2 /*return*/];
252
441
  }
@@ -300,6 +489,338 @@ var FileStorage = /** @class */ (function () {
300
489
  });
301
490
  });
302
491
  };
492
+ /**
493
+ * The id whose bytes this caller is served: the file's own for its owner, or for a file with
494
+ * no maker (or one the maker does not apply to); otherwise the {@link FileCopyForOthers} copy —
495
+ * the one already named on the row, or made now, once, and named for every later read.
496
+ *
497
+ * ONE REFUSAL, EVERY DOOR: when no copy can be made (the seam's {@link FileCopyRefused}), the file
498
+ * is served to no one but its owner, so to this caller it is not there — a
499
+ * `ServiceRefusal(404, 'File not found')`, thrown here so the browser service, `GET /file/:id` in
500
+ * both its shapes and the server-side door answer the same by construction: the service router
501
+ * answers 404 `{ error: 'File not found' }`, the executor logs one WARN (a refusal, never a
502
+ * failure), the route answers `404 File not found` — the same answer, at every door, as a row the
503
+ * caller cannot read ({@link notFound}). The refusal carries no reason, file id or owner: the
504
+ * maker's reason is the server's, logged here in one WARN, so nothing a caller reads tells a
505
+ * refused copy from a file that is not there.
506
+ */
507
+ FileStorage.prototype.servedFileId = function (file) {
508
+ var _a;
509
+ return __awaiter(this, void 0, void 0, function () {
510
+ var maker, error_1;
511
+ return __generator(this, function (_b) {
512
+ switch (_b.label) {
513
+ case 0:
514
+ if (this.ownedByCaller(file)) {
515
+ return [2 /*return*/, file.id];
516
+ }
517
+ if ((_a = file.copyForOthers) === null || _a === void 0 ? void 0 : _a._id) {
518
+ return [2 /*return*/, file.copyForOthers._id];
519
+ }
520
+ maker = (0, FileCopyForOthers_1.getFileCopyForOthers)();
521
+ if (!maker || !maker.appliesTo(file)) {
522
+ return [2 /*return*/, file.id];
523
+ }
524
+ _b.label = 1;
525
+ case 1:
526
+ _b.trys.push([1, 3, , 4]);
527
+ return [4 /*yield*/, this.makeCopyForOthers(file)];
528
+ case 2: return [2 /*return*/, _b.sent()];
529
+ case 3:
530
+ error_1 = _b.sent();
531
+ if (FileCopyForOthers_1.FileCopyRefused.is(error_1)) {
532
+ this.logger.warn({
533
+ message: 'No copy of a file can be made for anyone but its owner; refused as not found',
534
+ obj: { fileId: error_1.fileId, reason: error_1.reason },
535
+ });
536
+ throw FileStorage.notFound();
537
+ }
538
+ throw error_1;
539
+ case 4: return [2 /*return*/];
540
+ }
541
+ });
542
+ });
543
+ };
544
+ /**
545
+ * Whether the caller is the file's owner — the row's `scope` is the owner (the scoped read
546
+ * {@link getFile} makes first is exactly `scope = the caller`), so a row {@link getFile} handed
547
+ * back whose scope is someone else's was reached through the shared-content leg. A row with no
548
+ * owner recorded serves as itself: there is no one to withhold it from.
549
+ */
550
+ FileStorage.prototype.ownedByCaller = function (file) {
551
+ return !file.scope || file.scope === new user_1.UserRepo().getUser().id;
552
+ };
553
+ /**
554
+ * Makes the copy (the maker's bytes, from the original's), stores it as a File in the owner's
555
+ * scope — the original's name, type, dimensions and provenance ({@link provenanceOf}) on it —
556
+ * and names it on the original's row. Two non-owners reading at once may both get here:
557
+ * the row's word wins — decided in ONE transaction (the read of the row and the write of its
558
+ * word together, so racing writers are serialized and the loser's transaction, retried, finds
559
+ * the winner named) — and a copy that lost is deleted after the commit, the first is served.
560
+ */
561
+ FileStorage.prototype.makeCopyForOthers = function (file) {
562
+ return __awaiter(this, void 0, void 0, function () {
563
+ var maker, driver, original, _a, _b, copyBytes, cause_1, system, copy, named;
564
+ var _this = this;
565
+ return __generator(this, function (_c) {
566
+ switch (_c.label) {
567
+ case 0:
568
+ maker = (0, FileCopyForOthers_1.getFileCopyForOthers)();
569
+ driver = FileStorage.getDriver();
570
+ _b = (_a = Buffer).from;
571
+ return [4 /*yield*/, driver.getFileData(file.id)];
572
+ case 1:
573
+ original = _b.apply(_a, [_c.sent(), 'base64']);
574
+ _c.label = 2;
575
+ case 2:
576
+ _c.trys.push([2, 4, , 5]);
577
+ return [4 /*yield*/, maker.make(file, original)];
578
+ case 3:
579
+ copyBytes = _c.sent();
580
+ return [3 /*break*/, 5];
581
+ case 4:
582
+ cause_1 = _c.sent();
583
+ // The maker's contract: a throw means no copy can be made — the file is the owner's alone.
584
+ throw new FileCopyForOthers_1.FileCopyRefused(file.id, cause_1);
585
+ case 5:
586
+ system = (0, db_1.getDbAsSystem)();
587
+ return [4 /*yield*/, system.insert(tables_1.tables.File, __assign(__assign(__assign(__assign(__assign({ name: file.name, type: file.type, size: copyBytes.length }, (file.width !== undefined && file.width !== null ? { width: file.width } : {})), (file.height !== undefined && file.height !== null ? { height: file.height } : {})), (file.durationMs !== undefined && file.durationMs !== null ? { durationMs: file.durationMs } : {})), this.provenanceOf(file)), { scope: file.scope }))];
588
+ case 6:
589
+ copy = _c.sent();
590
+ return [4 /*yield*/, driver.createFile(copy, copyBytes.toString('base64'))];
591
+ case 7:
592
+ _c.sent();
593
+ return [4 /*yield*/, system.runTransaction(function () { return __awaiter(_this, void 0, void 0, function () {
594
+ var current;
595
+ var _a;
596
+ return __generator(this, function (_b) {
597
+ switch (_b.label) {
598
+ case 0: return [4 /*yield*/, system.get(tables_1.tables.File, { id: file.id })];
599
+ case 1:
600
+ current = _b.sent();
601
+ if ((_a = current === null || current === void 0 ? void 0 : current.copyForOthers) === null || _a === void 0 ? void 0 : _a._id) {
602
+ return [2 /*return*/, current.copyForOthers._id];
603
+ }
604
+ return [4 /*yield*/, system.update(tables_1.tables.File, { id: file.id, copyForOthers: new db_1.Reference(tables_1.tables.File.name, copy.id) })];
605
+ case 2:
606
+ _b.sent();
607
+ return [2 /*return*/, copy.id];
608
+ }
609
+ });
610
+ }); })];
611
+ case 8:
612
+ named = _c.sent();
613
+ if (!(named !== copy.id)) return [3 /*break*/, 10];
614
+ // Outside the transaction: the row's delete takes the bytes with it (the delete watcher), a
615
+ // side effect that must not ride a transaction the runner may retry.
616
+ return [4 /*yield*/, system.delete(tables_1.tables.File, { id: copy.id })];
617
+ case 9:
618
+ // Outside the transaction: the row's delete takes the bytes with it (the delete watcher), a
619
+ // side effect that must not ride a transaction the runner may retry.
620
+ _c.sent();
621
+ _c.label = 10;
622
+ case 10: return [2 /*return*/, named];
623
+ }
624
+ });
625
+ });
626
+ };
627
+ /**
628
+ * The read path's derivation, ONCE per file and kind at a time in this process: the readers
629
+ * that ask for the same absent variant while one derives (a shared document opened by many on
630
+ * one tick — every face asks the same URL) wait for that derivation's answer instead of each
631
+ * reading the original, encoding it and storing an object the row's word would then discard.
632
+ * Across processes the row's word in one transaction ({@link makeVariant}) still decides: this
633
+ * bounds the cost, that bounds the outcome. A maker that cannot make the variant of these
634
+ * bytes (a picture its decoder cannot read) answers `undefined` — nothing is stored, the reader
635
+ * draws the file itself, the next reader asks again — said in the log, never as a failed read;
636
+ * any other failure (the store, the database) is the reader's as it would be for any File.
637
+ */
638
+ FileStorage.prototype.deriveOnce = function (file, kind) {
639
+ var _this = this;
640
+ var key = "".concat(file.id, ":").concat(kind);
641
+ var running = FileStorage.derivations.get(key);
642
+ if (running) {
643
+ return running;
644
+ }
645
+ var derivation = (function () { return __awaiter(_this, void 0, void 0, function () {
646
+ var original, _a, _b, error_2;
647
+ return __generator(this, function (_c) {
648
+ switch (_c.label) {
649
+ case 0:
650
+ _b = (_a = Buffer).from;
651
+ return [4 /*yield*/, FileStorage.getDriver().getFileData(file.id)];
652
+ case 1:
653
+ original = _b.apply(_a, [_c.sent(), 'base64']);
654
+ _c.label = 2;
655
+ case 2:
656
+ _c.trys.push([2, 4, , 5]);
657
+ return [4 /*yield*/, this.makeVariant(file, kind, original)];
658
+ case 3: return [2 /*return*/, _c.sent()];
659
+ case 4:
660
+ error_2 = _c.sent();
661
+ if (!FileVariantMaker_1.FileVariantNotMade.is(error_2)) {
662
+ throw error_2;
663
+ }
664
+ new logger_1.Logger({ name: 'FileStorage' }).warn({
665
+ message: "No ".concat(kind, " variant could be made of file ").concat(file.id, "; the file itself is served"),
666
+ obj: { reason: error_2.reason },
667
+ });
668
+ return [2 /*return*/, undefined];
669
+ case 5: return [2 /*return*/];
670
+ }
671
+ });
672
+ }); })().finally(function () { return FileStorage.derivations.delete(key); });
673
+ FileStorage.derivations.set(key, derivation);
674
+ return derivation;
675
+ };
676
+ /**
677
+ * Makes the variant (the maker's answer from the original's bytes), stores it as a File in the
678
+ * OWNER's scope naming its original and carrying the original's provenance ({@link provenanceOf}
679
+ * — the maker answers the bytes, their type and their dimensions, never where they came from),
680
+ * and names it on the original's row. Two readers may derive
681
+ * the same kind at once: the row's word wins — decided in ONE transaction, exactly as the copy
682
+ * for others is named — and the variant that lost is deleted after the commit. The maker's own
683
+ * failure (it could not make the variant of these bytes) is thrown as {@link FileVariantNotMade}
684
+ * with the maker's reason, so the doors can tell it from a failure of the store or the database;
685
+ * nothing is stored on it.
686
+ * @returns The id of the variant the row names.
687
+ */
688
+ FileStorage.prototype.makeVariant = function (file, kind, bytes) {
689
+ return __awaiter(this, void 0, void 0, function () {
690
+ var maker, driver, made, cause_2, system, variant, named;
691
+ var _this = this;
692
+ return __generator(this, function (_a) {
693
+ switch (_a.label) {
694
+ case 0:
695
+ maker = (0, FileVariantMaker_1.getFileVariantMaker)();
696
+ driver = FileStorage.getDriver();
697
+ _a.label = 1;
698
+ case 1:
699
+ _a.trys.push([1, 3, , 4]);
700
+ return [4 /*yield*/, maker.make(file, bytes, kind)];
701
+ case 2:
702
+ made = _a.sent();
703
+ return [3 /*break*/, 4];
704
+ case 3:
705
+ cause_2 = _a.sent();
706
+ // The maker's contract: a throw means no variant can be made of these bytes.
707
+ throw new FileVariantMaker_1.FileVariantNotMade(file.id, kind, cause_2);
708
+ case 4:
709
+ system = (0, db_1.getDbAsSystem)();
710
+ return [4 /*yield*/, system.insert(tables_1.tables.File, __assign(__assign(__assign(__assign({ name: "(".concat(kind, ") ").concat(file.name), type: made.type, size: made.bytes.length }, (made.width !== undefined ? { width: made.width } : {})), (made.height !== undefined ? { height: made.height } : {})), this.provenanceOf(file)), { variantOf: new db_1.Reference(tables_1.tables.File.name, file.id), scope: file.scope }))];
711
+ case 5:
712
+ variant = _a.sent();
713
+ return [4 /*yield*/, driver.createFile(variant, made.bytes.toString('base64'))];
714
+ case 6:
715
+ _a.sent();
716
+ return [4 /*yield*/, system.runTransaction(function () { return __awaiter(_this, void 0, void 0, function () {
717
+ var current;
718
+ var _a;
719
+ var _b;
720
+ return __generator(this, function (_c) {
721
+ switch (_c.label) {
722
+ case 0: return [4 /*yield*/, system.get(tables_1.tables.File, { id: file.id })];
723
+ case 1:
724
+ current = _c.sent();
725
+ if ((_b = current === null || current === void 0 ? void 0 : current[kind]) === null || _b === void 0 ? void 0 : _b._id) {
726
+ return [2 /*return*/, current[kind]._id];
727
+ }
728
+ return [4 /*yield*/, system.update(tables_1.tables.File, (_a = { id: file.id }, _a[kind] = new db_1.Reference(tables_1.tables.File.name, variant.id), _a))];
729
+ case 2:
730
+ _c.sent();
731
+ return [2 /*return*/, variant.id];
732
+ }
733
+ });
734
+ }); })];
735
+ case 7:
736
+ named = _a.sent();
737
+ if (!(named !== variant.id)) return [3 /*break*/, 9];
738
+ // Outside the transaction: the row's delete takes the bytes with it (the delete watcher), a
739
+ // side effect that must not ride a transaction the runner may retry.
740
+ return [4 /*yield*/, system.delete(tables_1.tables.File, { id: variant.id })];
741
+ case 8:
742
+ // Outside the transaction: the row's delete takes the bytes with it (the delete watcher), a
743
+ // side effect that must not ride a transaction the runner may retry.
744
+ _a.sent();
745
+ _a.label = 9;
746
+ case 9: return [2 /*return*/, named];
747
+ }
748
+ });
749
+ });
750
+ };
751
+ /**
752
+ * The row forgets each derived File first (nothing serves a copy or a variant the row no longer
753
+ * names), then their rows go — the bytes with them, through the delete watcher: the copy for
754
+ * others and every variant kind, one rule.
755
+ */
756
+ FileStorage.prototype.dropDerivedFiles = function (file) {
757
+ return __awaiter(this, void 0, void 0, function () {
758
+ var seats, stale, system, forgotten, _i, stale_1, seat, _a, stale_2, seat;
759
+ return __generator(this, function (_b) {
760
+ switch (_b.label) {
761
+ case 0:
762
+ seats = __spreadArray(['copyForOthers'], FileVariantMaker_1.FILE_VARIANT_KINDS, true);
763
+ stale = seats.filter(function (seat) { var _a; return !!((_a = file[seat]) === null || _a === void 0 ? void 0 : _a._id); });
764
+ if (stale.length === 0) {
765
+ return [2 /*return*/];
766
+ }
767
+ system = (0, db_1.getDbAsSystem)();
768
+ forgotten = { id: file.id };
769
+ for (_i = 0, stale_1 = stale; _i < stale_1.length; _i++) {
770
+ seat = stale_1[_i];
771
+ forgotten[seat] = null;
772
+ }
773
+ return [4 /*yield*/, system.update(tables_1.tables.File, forgotten)];
774
+ case 1:
775
+ _b.sent();
776
+ _a = 0, stale_2 = stale;
777
+ _b.label = 2;
778
+ case 2:
779
+ if (!(_a < stale_2.length)) return [3 /*break*/, 5];
780
+ seat = stale_2[_a];
781
+ return [4 /*yield*/, system.delete(tables_1.tables.File, { id: file[seat]._id })];
782
+ case 3:
783
+ _b.sent();
784
+ _b.label = 4;
785
+ case 4:
786
+ _a++;
787
+ return [3 /*break*/, 2];
788
+ case 5: return [2 /*return*/];
789
+ }
790
+ });
791
+ });
792
+ };
793
+ /**
794
+ * The provenance a derived File carries — its original's, column for column, as the table names
795
+ * them ({@link FileTable.provenanceColumns}): a rendition of the bytes came to exist the way the
796
+ * bytes did, from where they did, under the rights they carry. Only what the row holds is
797
+ * copied, so a row with no provenance derives rows with none — nothing is invented. A row that
798
+ * is itself derived already carries its original's, so a derivation of it carries the same.
799
+ */
800
+ FileStorage.prototype.provenanceOf = function (file) {
801
+ var _a;
802
+ var provenance = {};
803
+ for (var _i = 0, _b = FileTable_1.FileTable.provenanceColumns; _i < _b.length; _i++) {
804
+ var column = _b[_i];
805
+ var value = file[column];
806
+ if (value !== undefined && value !== null) {
807
+ Object.assign(provenance, (_a = {}, _a[column] = value, _a));
808
+ }
809
+ }
810
+ return provenance;
811
+ };
812
+ /**
813
+ * THE answer a byte door gives a caller who does not get the file's bytes — a row that is not
814
+ * there, one this caller cannot read, or a copy that cannot be made for them: a 404 refusal in
815
+ * the same words every time, never the id, so the answer reveals nothing about existence (the
816
+ * service router sends `{ error: 'File not found' }`, the executor logs it at WARN, the routes
817
+ * send `404 File not found`).
818
+ */
819
+ FileStorage.notFound = function () {
820
+ return new service_1.ServiceRefusal(404, 'File not found');
821
+ };
822
+ /** The read path's derivations running in this process, by file and kind — see {@link deriveOnce}. */
823
+ FileStorage.derivations = new Map();
303
824
  return FileStorage;
304
825
  }());
305
826
  exports.FileStorage = FileStorage;