@proteinjs/db-file 1.8.4 → 1.9.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 (97) hide show
  1. package/CHANGELOG.md +30 -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 +145 -11
  18. package/dist/src/FileStorage.d.ts.map +1 -1
  19. package/dist/src/FileStorage.js +537 -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 +61 -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 +55 -2
  48. package/dist/src/tables/FileTable.d.ts.map +1 -1
  49. package/dist/src/tables/FileTable.js +8 -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/FileMediaMetadata.test.js +60 -5
  62. package/dist/test/FileMediaMetadata.test.js.map +1 -1
  63. package/dist/test/FileStorageError.test.d.ts +2 -0
  64. package/dist/test/FileStorageError.test.d.ts.map +1 -0
  65. package/dist/test/FileStorageError.test.js +68 -0
  66. package/dist/test/FileStorageError.test.js.map +1 -0
  67. package/dist/test/FileTestEnvironment.d.ts.map +1 -1
  68. package/dist/test/FileTestEnvironment.js +3 -0
  69. package/dist/test/FileTestEnvironment.js.map +1 -1
  70. package/dist/test/FileVariants.test.d.ts +2 -0
  71. package/dist/test/FileVariants.test.d.ts.map +1 -0
  72. package/dist/test/FileVariants.test.js +1008 -0
  73. package/dist/test/FileVariants.test.js.map +1 -0
  74. package/dist/test/GetFileRoute.test.js +29 -0
  75. package/dist/test/GetFileRoute.test.js.map +1 -1
  76. package/generated/index.ts +3 -1
  77. package/index.ts +4 -0
  78. package/package.json +5 -5
  79. package/src/DbFileStorageDriver.ts +32 -21
  80. package/src/FileCopyForOthers.ts +71 -0
  81. package/src/FileStorage.ts +351 -18
  82. package/src/FileStorageDriver.ts +12 -0
  83. package/src/FileStorageError.ts +123 -0
  84. package/src/FileVariantMaker.ts +84 -0
  85. package/src/routes/FileResponder.ts +85 -0
  86. package/src/routes/getFile.ts +9 -44
  87. package/src/routes/getFileVariant.ts +53 -0
  88. package/src/routes/getFileVariantRoute.ts +7 -0
  89. package/src/tables/FileTable.ts +57 -1
  90. package/test/DbFileStorageDriverMissingBytes.test.ts +85 -0
  91. package/test/FileByteAccess.test.ts +7 -1
  92. package/test/FileCopyForOthers.test.ts +702 -0
  93. package/test/FileMediaMetadata.test.ts +44 -0
  94. package/test/FileStorageError.test.ts +79 -0
  95. package/test/FileTestEnvironment.ts +3 -0
  96. package/test/FileVariants.test.ts +604 -0
  97. package/test/GetFileRoute.test.ts +19 -0
@@ -0,0 +1,1143 @@
1
+ "use strict";
2
+ var __extends = (this && this.__extends) || (function () {
3
+ var extendStatics = function (d, b) {
4
+ extendStatics = Object.setPrototypeOf ||
5
+ ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
6
+ function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
7
+ return extendStatics(d, b);
8
+ };
9
+ return function (d, b) {
10
+ if (typeof b !== "function" && b !== null)
11
+ throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
12
+ extendStatics(d, b);
13
+ function __() { this.constructor = d; }
14
+ d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
15
+ };
16
+ })();
17
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
18
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
19
+ return new (P || (P = Promise))(function (resolve, reject) {
20
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
21
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
22
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
23
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
24
+ });
25
+ };
26
+ var __generator = (this && this.__generator) || function (thisArg, body) {
27
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
28
+ return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
29
+ function verb(n) { return function (v) { return step([n, v]); }; }
30
+ function step(op) {
31
+ if (f) throw new TypeError("Generator is already executing.");
32
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
33
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
34
+ if (y = 0, t) op = [op[0] & 2, t.value];
35
+ switch (op[0]) {
36
+ case 0: case 1: t = op; break;
37
+ case 4: _.label++; return { value: op[1], done: false };
38
+ case 5: _.label++; y = op[1]; op = [0]; continue;
39
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
40
+ default:
41
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
42
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
43
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
44
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
45
+ if (t[2]) _.ops.pop();
46
+ _.trys.pop(); continue;
47
+ }
48
+ op = body.call(thisArg, _);
49
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
50
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
51
+ }
52
+ };
53
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
54
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
55
+ if (ar || !(i in from)) {
56
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
57
+ ar[i] = from[i];
58
+ }
59
+ }
60
+ return to.concat(ar || Array.prototype.slice.call(from));
61
+ };
62
+ Object.defineProperty(exports, "__esModule", { value: true });
63
+ var db_1 = require("@proteinjs/db");
64
+ var logger_1 = require("@proteinjs/logger");
65
+ var reflection_1 = require("@proteinjs/reflection");
66
+ var serializer_1 = require("@proteinjs/serializer");
67
+ var user_1 = require("@proteinjs/user");
68
+ var tables_1 = require("../src/tables/tables");
69
+ var FileStorage_1 = require("../src/FileStorage");
70
+ var service_1 = require("@proteinjs/service");
71
+ // The router and executor every browser-facing service call runs through (ServiceRouter →
72
+ // ServiceExecutor), not part of the package's entry: what they log and answer for this door — the
73
+ // status and the body the browser reads — is the outcome under test.
74
+ var ServiceExecutor_1 = require("@proteinjs/service/dist/src/ServiceExecutor");
75
+ var ServiceRouter_1 = require("@proteinjs/service/dist/src/ServiceRouter");
76
+ var getFile_1 = require("../src/routes/getFile");
77
+ var FileTestEnvironment_1 = require("./FileTestEnvironment");
78
+ /**
79
+ * A file's bytes are the owner's, kept as they arrived; what anyone ELSE is served is a copy the
80
+ * registered maker derives — made once, stored as its own File in the owner's scope, named on the
81
+ * original's row, dropped when the bytes change, deleted with the original. This suite proves the
82
+ * seam contract with a stub maker (the real one — a picture without its location — lives in the
83
+ * package that knows media): who gets which bytes, through the service door and both shapes of
84
+ * the route; that the copy is cached and its cache never crosses to the owner; how it dies.
85
+ *
86
+ * The stub maker marks the bytes it makes: the original carries `LOCATION-MARKER` (what a phone
87
+ * photo's GPS block is to the real maker), the copy does not.
88
+ */
89
+ var LOCATION_MARKER = Buffer.from('LOCATION-MARKER');
90
+ /** Every byte value once, then the marker — a payload the copy must be seen to differ from. */
91
+ var originalBytes = Buffer.concat([Buffer.from(Array.from({ length: 256 }, function (_, i) { return i; })), LOCATION_MARKER]);
92
+ var copyOf = function (bytes) { return Buffer.from(bytes.subarray(0, bytes.length - LOCATION_MARKER.length)); };
93
+ /** In-memory proxy-shape driver (no signed URLs) — the DbFileStorageDriver serving shape. */
94
+ var ProxyDriver = /** @class */ (function () {
95
+ function ProxyDriver() {
96
+ this.store = new Map();
97
+ }
98
+ ProxyDriver.prototype.createFile = function (file, fileData) {
99
+ return __awaiter(this, void 0, void 0, function () {
100
+ return __generator(this, function (_a) {
101
+ switch (_a.label) {
102
+ case 0:
103
+ this.store.set(file.id, Buffer.from(fileData, 'base64'));
104
+ return [4 /*yield*/, atBarrier()];
105
+ case 1:
106
+ _a.sent(); // the racing case: every copy's bytes are stored before any row is asked its word
107
+ return [2 /*return*/];
108
+ }
109
+ });
110
+ });
111
+ };
112
+ ProxyDriver.prototype.getFileData = function (fileId) {
113
+ return __awaiter(this, void 0, void 0, function () {
114
+ var data;
115
+ return __generator(this, function (_a) {
116
+ data = this.store.get(fileId);
117
+ if (data === undefined) {
118
+ throw new Error("No such object: ".concat(fileId));
119
+ }
120
+ return [2 /*return*/, data.toString('base64')];
121
+ });
122
+ });
123
+ };
124
+ ProxyDriver.prototype.updateFileData = function (fileId, data) {
125
+ return __awaiter(this, void 0, void 0, function () {
126
+ return __generator(this, function (_a) {
127
+ this.store.set(fileId, Buffer.from(data, 'base64'));
128
+ return [2 /*return*/];
129
+ });
130
+ });
131
+ };
132
+ ProxyDriver.prototype.deleteFile = function (fileId) {
133
+ return __awaiter(this, void 0, void 0, function () {
134
+ return __generator(this, function (_a) {
135
+ this.store.delete(fileId);
136
+ return [2 /*return*/];
137
+ });
138
+ });
139
+ };
140
+ return ProxyDriver;
141
+ }());
142
+ /** Proxy driver plus a signed-URL mint that names the object it signs — the GCS serving shape. */
143
+ var SignedUrlDriver = /** @class */ (function (_super) {
144
+ __extends(SignedUrlDriver, _super);
145
+ function SignedUrlDriver() {
146
+ return _super !== null && _super.apply(this, arguments) || this;
147
+ }
148
+ SignedUrlDriver.prototype.getSignedUrl = function (fileId) {
149
+ return __awaiter(this, void 0, void 0, function () {
150
+ return __generator(this, function (_a) {
151
+ return [2 /*return*/, "https://signed.test/blob/".concat(fileId, "?sig=test")];
152
+ });
153
+ });
154
+ };
155
+ return SignedUrlDriver;
156
+ }(ProxyDriver));
157
+ var ResponseRecorder = /** @class */ (function () {
158
+ function ResponseRecorder() {
159
+ this.headers = {};
160
+ }
161
+ ResponseRecorder.prototype.status = function (code) {
162
+ this.statusCode = code;
163
+ return this;
164
+ };
165
+ ResponseRecorder.prototype.send = function (body) {
166
+ this.body = body;
167
+ return this;
168
+ };
169
+ ResponseRecorder.prototype.setHeader = function (name, value) {
170
+ this.headers[name.toLowerCase()] = value;
171
+ };
172
+ ResponseRecorder.prototype.redirect = function (status, url) {
173
+ this.statusCode = status;
174
+ this.redirectUrl = url;
175
+ };
176
+ return ResponseRecorder;
177
+ }());
178
+ var invokeRoute = function (fileId) { return __awaiter(void 0, void 0, void 0, function () {
179
+ var response;
180
+ return __generator(this, function (_a) {
181
+ switch (_a.label) {
182
+ case 0:
183
+ response = new ResponseRecorder();
184
+ return [4 /*yield*/, getFile_1.getFile.onRequest({ params: { id: fileId }, headers: {} }, response)];
185
+ case 1:
186
+ _a.sent();
187
+ return [2 /*return*/, response];
188
+ }
189
+ });
190
+ }); };
191
+ /** A logger that writes into `entries` — what a door logged, read back as the outcome. */
192
+ var loggerInto = function (name, entries) {
193
+ return new logger_1.Logger({ name: name, logWriter: { write: function (log) { return entries.push(log); } } });
194
+ };
195
+ var SERVICE_PATH = '/service/@proteinjs/db-file/FileStorageService/getFileData';
196
+ /**
197
+ * One call of the browser's door — `FileStorageService.getFileData` — through the REAL service
198
+ * router (its executor seeded by path, the way the router builds it from the source graph): the
199
+ * status and the body the browser reads, every entry the executor logged (`entries`) and every
200
+ * entry the file storage itself logged (`storageEntries`).
201
+ */
202
+ var invokeServiceDoor = function (fileId) { return __awaiter(void 0, void 0, void 0, function () {
203
+ var returnType, method, storage, storageEntries, executor, entries, router, response;
204
+ var _a;
205
+ var _b;
206
+ return __generator(this, function (_c) {
207
+ switch (_c.label) {
208
+ case 0:
209
+ returnType = { name: 'Promise<string>' };
210
+ method = new reflection_1.Method('getFileData', returnType, true, false, false, false, 'public', []);
211
+ storage = new FileStorage_1.FileStorage();
212
+ storageEntries = [];
213
+ storage.logger = loggerInto('FileStorage', storageEntries);
214
+ executor = new ServiceExecutor_1.ServiceExecutor(storage, new reflection_1.Interface('@proteinjs/db-file', 'FileStorageService', [], [method]), method);
215
+ entries = [];
216
+ executor.logger = loggerInto('FileStorageService.getFileData', entries);
217
+ router = new ServiceRouter_1.ServiceRouter();
218
+ router.serviceExecutorMap = (_a = {},
219
+ _a[SERVICE_PATH] = executor,
220
+ _a);
221
+ response = new ResponseRecorder();
222
+ return [4 /*yield*/, router.onRequest({ path: SERVICE_PATH, body: serializer_1.Serializer.serialize([fileId]) }, response)];
223
+ case 1:
224
+ _c.sent();
225
+ return [2 /*return*/, { status: (_b = response.statusCode) !== null && _b !== void 0 ? _b : 200, body: response.body, entries: entries, storageEntries: storageEntries }];
226
+ }
227
+ });
228
+ }); };
229
+ /** The status the service router answers a thrown error with: a refusal's own, else 400. */
230
+ var routerStatus = function (thrown) { return (service_1.ServiceRefusal.is(thrown) ? thrown.status : 400); };
231
+ var testEnv = new FileTestEnvironment_1.FileTestEnvironment();
232
+ var objectCache = function () { return reflection_1.SourceRepository.get().objectCache; };
233
+ var owner;
234
+ var recipient;
235
+ var reachableFileIds = new Set();
236
+ /** Every call the maker took: which file, and the bytes it was handed. */
237
+ var makerCalls = [];
238
+ /** What the stub maker applies to — the suite's own rule (the real maker's is "pictures and clips"). */
239
+ var makerAppliesTo = function (file) { return file.type.startsWith('image/'); };
240
+ var makerFails = false;
241
+ /** What the stub maker throws when it fails — its own internal words, the server's to keep. */
242
+ var MAKER_REASON = 'no copy can be made of this file';
243
+ /**
244
+ * A barrier for the racing case: with `makerBarrier = n`, the stub maker holds every call until n
245
+ * calls are inside it, then answers them all on the same tick — so the reads that follow (the
246
+ * copy's row, its bytes, the row's word) run in lockstep, the tightest race the seam can meet.
247
+ */
248
+ var makerBarrier = 0;
249
+ var makerWaiting = [];
250
+ /** Hold until `makerBarrier` callers are here, then release them all on one tick (no-op when 0). */
251
+ var atBarrier = function () { return __awaiter(void 0, void 0, void 0, function () {
252
+ return __generator(this, function (_a) {
253
+ switch (_a.label) {
254
+ case 0:
255
+ if (makerBarrier <= 0) {
256
+ return [2 /*return*/];
257
+ }
258
+ return [4 /*yield*/, new Promise(function (resolve) {
259
+ makerWaiting.push(resolve);
260
+ if (makerWaiting.length >= makerBarrier) {
261
+ var release = makerWaiting;
262
+ makerWaiting = [];
263
+ release.forEach(function (go) { return go(); });
264
+ }
265
+ })];
266
+ case 1:
267
+ _a.sent();
268
+ return [2 /*return*/];
269
+ }
270
+ });
271
+ }); };
272
+ var registerMaker = function () {
273
+ objectCache()['@proteinjs/db-file/FileCopyForOthers'] = [
274
+ {
275
+ appliesTo: function (file) { return makerAppliesTo(file); },
276
+ make: function (file, bytes) { return __awaiter(void 0, void 0, void 0, function () {
277
+ return __generator(this, function (_a) {
278
+ switch (_a.label) {
279
+ case 0:
280
+ makerCalls.push({ fileId: file.id, bytes: Buffer.from(bytes) });
281
+ return [4 /*yield*/, atBarrier()];
282
+ case 1:
283
+ _a.sent();
284
+ if (makerFails) {
285
+ throw new Error(MAKER_REASON);
286
+ }
287
+ return [2 /*return*/, copyOf(bytes)];
288
+ }
289
+ });
290
+ }); },
291
+ },
292
+ ];
293
+ };
294
+ beforeAll(function () { return __awaiter(void 0, void 0, void 0, function () {
295
+ return __generator(this, function (_a) {
296
+ switch (_a.label) {
297
+ case 0: return [4 /*yield*/, testEnv.beforeAll()];
298
+ case 1:
299
+ _a.sent();
300
+ objectCache()['@proteinjs/user-auth/AuthenticatedUserRepo'] = [new user_1.UserRepo()];
301
+ user_1.UserAuth.userRepo = undefined;
302
+ objectCache()['@proteinjs/db-file/FileReachabilityResolver'] = [
303
+ { canReadViaReference: function (fileId) { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
304
+ return [2 /*return*/, reachableFileIds.has(fileId)];
305
+ }); }); } },
306
+ ];
307
+ registerMaker();
308
+ return [4 /*yield*/, testEnv.createUser({ name: 'File owner', email: 'copy-owner@test.local' })];
309
+ case 2:
310
+ owner = _a.sent();
311
+ return [4 /*yield*/, testEnv.createUser({ name: 'Share recipient', email: 'copy-recipient@test.local' })];
312
+ case 3:
313
+ recipient = _a.sent();
314
+ testEnv.actAs(owner);
315
+ return [2 /*return*/];
316
+ }
317
+ });
318
+ }); });
319
+ afterAll(function () { return __awaiter(void 0, void 0, void 0, function () {
320
+ return __generator(this, function (_a) {
321
+ switch (_a.label) {
322
+ case 0:
323
+ delete objectCache()['@proteinjs/db-file/FileReachabilityResolver'];
324
+ delete objectCache()['@proteinjs/db-file/FileCopyForOthers'];
325
+ user_1.UserAuth.userRepo = undefined;
326
+ return [4 /*yield*/, testEnv.afterAll()];
327
+ case 1:
328
+ _a.sent();
329
+ return [2 /*return*/];
330
+ }
331
+ });
332
+ }); });
333
+ beforeEach(function () {
334
+ reachableFileIds.clear();
335
+ makerCalls = [];
336
+ makerAppliesTo = function (file) { return file.type.startsWith('image/'); };
337
+ makerFails = false;
338
+ makerBarrier = 0;
339
+ makerWaiting = [];
340
+ registerMaker();
341
+ testEnv.actAs(owner);
342
+ });
343
+ var createOwnerFile = function (name, type, bytes) {
344
+ if (bytes === void 0) { bytes = originalBytes; }
345
+ return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
346
+ switch (_a.label) {
347
+ case 0: return [4 /*yield*/, new FileStorage_1.FileStorage().createFile({ name: name, type: type, size: bytes.length }, bytes.toString('base64'))];
348
+ case 1: return [2 /*return*/, _a.sent()];
349
+ }
350
+ }); });
351
+ };
352
+ var rowAsSystem = function (fileId) { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
353
+ switch (_a.label) {
354
+ case 0: return [4 /*yield*/, (0, db_1.getDbAsSystem)().get(tables_1.tables.File, { id: fileId })];
355
+ case 1: return [2 /*return*/, (_a.sent())];
356
+ }
357
+ }); }); };
358
+ var bytesAs = function (user, fileId) { return __awaiter(void 0, void 0, void 0, function () {
359
+ var _a, _b;
360
+ return __generator(this, function (_c) {
361
+ switch (_c.label) {
362
+ case 0:
363
+ testEnv.actAs(user);
364
+ _b = (_a = Buffer).from;
365
+ return [4 /*yield*/, new FileStorage_1.FileStorage().getFileData(fileId)];
366
+ case 1: return [2 /*return*/, _b.apply(_a, [_c.sent(), 'base64'])];
367
+ }
368
+ });
369
+ }); };
370
+ var fileRowsInScope = function (scope) { return __awaiter(void 0, void 0, void 0, function () {
371
+ return __generator(this, function (_a) {
372
+ switch (_a.label) {
373
+ case 0: return [4 /*yield*/, (0, db_1.getDbAsSystem)().query(tables_1.tables.File, new db_1.QueryBuilderFactory().getQueryBuilder(tables_1.tables.File).condition({ field: 'scope', operator: '=', value: scope }))];
374
+ case 1: return [2 /*return*/, _a.sent()];
375
+ }
376
+ });
377
+ }); };
378
+ describe('the copy for others — the service door (proxy serving)', function () {
379
+ var driver = new ProxyDriver();
380
+ beforeAll(function () {
381
+ testEnv.setDriver(driver);
382
+ });
383
+ it('the owner is served the original, and no copy is made for them', function () { return __awaiter(void 0, void 0, void 0, function () {
384
+ var file, served, _a;
385
+ var _b, _c;
386
+ return __generator(this, function (_d) {
387
+ switch (_d.label) {
388
+ case 0: return [4 /*yield*/, createOwnerFile('mine.jpg', 'image/jpeg')];
389
+ case 1:
390
+ file = _d.sent();
391
+ return [4 /*yield*/, bytesAs(owner, file.id)];
392
+ case 2:
393
+ served = _d.sent();
394
+ expect(served.equals(originalBytes)).toBe(true);
395
+ expect(makerCalls).toEqual([]);
396
+ _a = expect;
397
+ return [4 /*yield*/, rowAsSystem(file.id)];
398
+ case 3:
399
+ _a.apply(void 0, [(_c = (_b = (_d.sent()).copyForOthers) === null || _b === void 0 ? void 0 : _b._id) !== null && _c !== void 0 ? _c : null]).toBeNull();
400
+ return [2 /*return*/];
401
+ }
402
+ });
403
+ }); });
404
+ it("a recipient the content reaches is served the maker's copy — the original's bytes never leave to them", function () { return __awaiter(void 0, void 0, void 0, function () {
405
+ var file, served;
406
+ return __generator(this, function (_a) {
407
+ switch (_a.label) {
408
+ case 0: return [4 /*yield*/, createOwnerFile('shared.jpg', 'image/jpeg')];
409
+ case 1:
410
+ file = _a.sent();
411
+ reachableFileIds.add(file.id);
412
+ return [4 /*yield*/, bytesAs(recipient, file.id)];
413
+ case 2:
414
+ served = _a.sent();
415
+ expect(served.includes(LOCATION_MARKER)).toBe(false);
416
+ expect(served.equals(copyOf(originalBytes))).toBe(true);
417
+ expect(makerCalls.map(function (call) { return call.fileId; })).toEqual([file.id]);
418
+ expect(makerCalls[0].bytes.equals(originalBytes)).toBe(true);
419
+ return [2 /*return*/];
420
+ }
421
+ });
422
+ }); });
423
+ it("the copy is a File of the OWNER's, named on the original's row, with the original's name, type and dimensions", function () { return __awaiter(void 0, void 0, void 0, function () {
424
+ var file, row, copyId, copy;
425
+ var _a;
426
+ return __generator(this, function (_b) {
427
+ switch (_b.label) {
428
+ case 0: return [4 /*yield*/, createOwnerFile('framed.jpg', 'image/jpeg')];
429
+ case 1:
430
+ file = _b.sent();
431
+ return [4 /*yield*/, (0, db_1.getDbAsSystem)().update(tables_1.tables.File, { id: file.id, width: 640, height: 480 })];
432
+ case 2:
433
+ _b.sent();
434
+ reachableFileIds.add(file.id);
435
+ return [4 /*yield*/, bytesAs(recipient, file.id)];
436
+ case 3:
437
+ _b.sent();
438
+ return [4 /*yield*/, rowAsSystem(file.id)];
439
+ case 4:
440
+ row = (_b.sent());
441
+ copyId = (_a = row.copyForOthers) === null || _a === void 0 ? void 0 : _a._id;
442
+ expect(copyId).toBeTruthy();
443
+ return [4 /*yield*/, rowAsSystem(copyId)];
444
+ case 5:
445
+ copy = (_b.sent());
446
+ expect(copy.scope).toEqual(owner.id);
447
+ expect([copy.name, copy.type, copy.size, copy.width, copy.height]).toEqual([
448
+ 'framed.jpg',
449
+ 'image/jpeg',
450
+ copyOf(originalBytes).length,
451
+ 640,
452
+ 480,
453
+ ]);
454
+ expect(driver.store.get(copyId).equals(copyOf(originalBytes))).toBe(true);
455
+ // The original is untouched: its bytes, its size.
456
+ expect(driver.store.get(file.id).equals(originalBytes)).toBe(true);
457
+ expect(row.size).toEqual(originalBytes.length);
458
+ return [2 /*return*/];
459
+ }
460
+ });
461
+ }); });
462
+ it('the copy is made ONCE: every later non-owner read serves the same copy without the maker', function () { return __awaiter(void 0, void 0, void 0, function () {
463
+ var file, copyId, again, onceMore, _a;
464
+ return __generator(this, function (_b) {
465
+ switch (_b.label) {
466
+ case 0: return [4 /*yield*/, createOwnerFile('popular.jpg', 'image/jpeg')];
467
+ case 1:
468
+ file = _b.sent();
469
+ reachableFileIds.add(file.id);
470
+ return [4 /*yield*/, bytesAs(recipient, file.id)];
471
+ case 2:
472
+ _b.sent();
473
+ return [4 /*yield*/, rowAsSystem(file.id)];
474
+ case 3:
475
+ copyId = (_b.sent()).copyForOthers._id;
476
+ return [4 /*yield*/, bytesAs(recipient, file.id)];
477
+ case 4:
478
+ again = _b.sent();
479
+ return [4 /*yield*/, bytesAs(recipient, file.id)];
480
+ case 5:
481
+ onceMore = _b.sent();
482
+ expect(again.equals(copyOf(originalBytes))).toBe(true);
483
+ expect(onceMore.equals(copyOf(originalBytes))).toBe(true);
484
+ expect(makerCalls).toHaveLength(1);
485
+ _a = expect;
486
+ return [4 /*yield*/, rowAsSystem(file.id)];
487
+ case 6:
488
+ _a.apply(void 0, [(_b.sent()).copyForOthers._id]).toEqual(copyId);
489
+ return [2 /*return*/];
490
+ }
491
+ });
492
+ }); });
493
+ it("TWO NON-OWNERS RACING: both are served the copy, ONE copy row survives in the owner's scope, the row names it, the loser's object is gone", function () { return __awaiter(void 0, void 0, void 0, function () {
494
+ var file, readers, served, _i, served_1, bytes, named, copies, liveIds, _a, _b, again;
495
+ return __generator(this, function (_c) {
496
+ switch (_c.label) {
497
+ case 0: return [4 /*yield*/, createOwnerFile('raced.jpg', 'image/jpeg')];
498
+ case 1:
499
+ file = _c.sent();
500
+ reachableFileIds.add(file.id);
501
+ readers = 4;
502
+ makerBarrier = readers;
503
+ // Warm the driver's session pool so the readers' round trips really overlap (a cold pool serializes them).
504
+ return [4 /*yield*/, Promise.all(Array.from({ length: readers * 2 }, function () { return rowAsSystem(file.id); }))];
505
+ case 2:
506
+ // Warm the driver's session pool so the readers' round trips really overlap (a cold pool serializes them).
507
+ _c.sent();
508
+ testEnv.actAs(recipient);
509
+ return [4 /*yield*/, Promise.all(Array.from({ length: readers }, function () { return new FileStorage_1.FileStorage().getFileData(file.id); }))];
510
+ case 3:
511
+ served = _c.sent();
512
+ for (_i = 0, served_1 = served; _i < served_1.length; _i++) {
513
+ bytes = served_1[_i];
514
+ expect(Buffer.from(bytes, 'base64').equals(copyOf(originalBytes))).toBe(true);
515
+ }
516
+ expect(makerCalls).toHaveLength(readers); // all raced into the maker; the contract is about what SURVIVES
517
+ return [4 /*yield*/, rowAsSystem(file.id)];
518
+ case 4:
519
+ named = (_c.sent()).copyForOthers._id;
520
+ return [4 /*yield*/, fileRowsInScope(owner.id)];
521
+ case 5:
522
+ copies = (_c.sent()).filter(function (row) { return row.id !== file.id && row.name === 'raced.jpg'; });
523
+ expect(copies.map(function (row) { return row.id; })).toEqual([named]);
524
+ _a = Set.bind;
525
+ return [4 /*yield*/, (0, db_1.getDbAsSystem)().query(tables_1.tables.File, new db_1.QueryBuilderFactory().getQueryBuilder(tables_1.tables.File))];
526
+ case 6:
527
+ liveIds = new (_a.apply(Set, [void 0, (_c.sent()).map(function (row) { return row.id; })]))();
528
+ expect(Array.from(driver.store.keys()).filter(function (id) { return !liveIds.has(id); })).toEqual([]);
529
+ _b = expect;
530
+ return [4 /*yield*/, fileRowsInScope(recipient.id)];
531
+ case 7:
532
+ _b.apply(void 0, [_c.sent()]).toEqual([]);
533
+ makerBarrier = 0;
534
+ return [4 /*yield*/, bytesAs(recipient, file.id)];
535
+ case 8:
536
+ again = _c.sent();
537
+ expect(again.equals(copyOf(originalBytes))).toBe(true);
538
+ expect(makerCalls).toHaveLength(readers);
539
+ return [2 /*return*/];
540
+ }
541
+ });
542
+ }); });
543
+ it("THE CACHE NEVER CROSSES TO THE OWNER: once a copy exists, the owner's read still serves the original", function () { return __awaiter(void 0, void 0, void 0, function () {
544
+ var file, _a, served;
545
+ var _b;
546
+ return __generator(this, function (_c) {
547
+ switch (_c.label) {
548
+ case 0: return [4 /*yield*/, createOwnerFile('still-mine.jpg', 'image/jpeg')];
549
+ case 1:
550
+ file = _c.sent();
551
+ reachableFileIds.add(file.id);
552
+ return [4 /*yield*/, bytesAs(recipient, file.id)];
553
+ case 2:
554
+ _c.sent();
555
+ _a = expect;
556
+ return [4 /*yield*/, rowAsSystem(file.id)];
557
+ case 3:
558
+ _a.apply(void 0, [(_b = (_c.sent()).copyForOthers) === null || _b === void 0 ? void 0 : _b._id]).toBeTruthy();
559
+ return [4 /*yield*/, bytesAs(owner, file.id)];
560
+ case 4:
561
+ served = _c.sent();
562
+ expect(served.equals(originalBytes)).toBe(true);
563
+ expect(served.includes(LOCATION_MARKER)).toBe(true);
564
+ return [2 /*return*/];
565
+ }
566
+ });
567
+ }); });
568
+ it('a file the maker does not apply to serves as itself to a recipient — no bytes read, no copy made', function () { return __awaiter(void 0, void 0, void 0, function () {
569
+ var file, rowsBefore, served, _a, _b;
570
+ var _c, _d;
571
+ return __generator(this, function (_e) {
572
+ switch (_e.label) {
573
+ case 0: return [4 /*yield*/, createOwnerFile('notes.pdf', 'application/pdf')];
574
+ case 1:
575
+ file = _e.sent();
576
+ reachableFileIds.add(file.id);
577
+ return [4 /*yield*/, fileRowsInScope(owner.id)];
578
+ case 2:
579
+ rowsBefore = (_e.sent()).length;
580
+ return [4 /*yield*/, bytesAs(recipient, file.id)];
581
+ case 3:
582
+ served = _e.sent();
583
+ expect(served.equals(originalBytes)).toBe(true);
584
+ expect(makerCalls).toEqual([]);
585
+ _a = expect;
586
+ return [4 /*yield*/, rowAsSystem(file.id)];
587
+ case 4:
588
+ _a.apply(void 0, [(_d = (_c = (_e.sent()).copyForOthers) === null || _c === void 0 ? void 0 : _c._id) !== null && _d !== void 0 ? _d : null]).toBeNull();
589
+ _b = expect;
590
+ return [4 /*yield*/, fileRowsInScope(owner.id)];
591
+ case 5:
592
+ _b.apply(void 0, [(_e.sent()).length]).toEqual(rowsBefore);
593
+ return [2 /*return*/];
594
+ }
595
+ });
596
+ }); });
597
+ it("the copy's own id is the owner's alone: a recipient cannot reach it, the owner can", function () { return __awaiter(void 0, void 0, void 0, function () {
598
+ var file, copyId, _a, _b, _c;
599
+ return __generator(this, function (_d) {
600
+ switch (_d.label) {
601
+ case 0: return [4 /*yield*/, createOwnerFile('guarded.jpg', 'image/jpeg')];
602
+ case 1:
603
+ file = _d.sent();
604
+ reachableFileIds.add(file.id);
605
+ return [4 /*yield*/, bytesAs(recipient, file.id)];
606
+ case 2:
607
+ _d.sent();
608
+ return [4 /*yield*/, rowAsSystem(file.id)];
609
+ case 3:
610
+ copyId = (_d.sent()).copyForOthers._id;
611
+ testEnv.actAs(recipient);
612
+ return [4 /*yield*/, expect(new FileStorage_1.FileStorage().getFileData(copyId)).rejects.toThrow('File not found')];
613
+ case 4:
614
+ _d.sent();
615
+ testEnv.actAs(owner);
616
+ _a = expect;
617
+ _c = (_b = Buffer).from;
618
+ return [4 /*yield*/, new FileStorage_1.FileStorage().getFileData(copyId)];
619
+ case 5:
620
+ _a.apply(void 0, [_c.apply(_b, [_d.sent(), 'base64']).equals(copyOf(originalBytes))]).toBe(true);
621
+ return [2 /*return*/];
622
+ }
623
+ });
624
+ }); });
625
+ it('when no copy can be made, the recipient is served NOTHING — never the original', function () { return __awaiter(void 0, void 0, void 0, function () {
626
+ var file, refusal, _a, _b;
627
+ var _c, _d;
628
+ return __generator(this, function (_e) {
629
+ switch (_e.label) {
630
+ case 0: return [4 /*yield*/, createOwnerFile('unrewritable.jpg', 'image/jpeg')];
631
+ case 1:
632
+ file = _e.sent();
633
+ reachableFileIds.add(file.id);
634
+ makerFails = true;
635
+ testEnv.actAs(recipient);
636
+ return [4 /*yield*/, new FileStorage_1.FileStorage().getFileData(file.id).then(function () { return undefined; }, function (error) { return error; })];
637
+ case 2:
638
+ refusal = _e.sent();
639
+ // Not there for this caller — a 404 refusal in the words a missing file gets, the same at every door.
640
+ expect(service_1.ServiceRefusal.is(refusal)).toBe(true);
641
+ expect(refusal.status).toBe(404);
642
+ expect(refusal.message).toEqual('File not found');
643
+ _a = expect;
644
+ return [4 /*yield*/, rowAsSystem(file.id)];
645
+ case 3:
646
+ _a.apply(void 0, [(_d = (_c = (_e.sent()).copyForOthers) === null || _c === void 0 ? void 0 : _c._id) !== null && _d !== void 0 ? _d : null]).toBeNull();
647
+ expect(Array.from(driver.store.keys())).toContain(file.id);
648
+ _b = expect;
649
+ return [4 /*yield*/, fileRowsInScope(owner.id)];
650
+ case 4:
651
+ _b.apply(void 0, [(_e.sent()).find(function (row) { return row.id !== file.id && row.name === 'unrewritable.jpg'; })]).toBeUndefined();
652
+ return [2 /*return*/];
653
+ }
654
+ });
655
+ }); });
656
+ it('with no maker registered at all, a recipient is served the original (the seam is opt-in)', function () { return __awaiter(void 0, void 0, void 0, function () {
657
+ var file, served, _a;
658
+ var _b, _c;
659
+ return __generator(this, function (_d) {
660
+ switch (_d.label) {
661
+ case 0:
662
+ objectCache()['@proteinjs/db-file/FileCopyForOthers'] = [];
663
+ return [4 /*yield*/, createOwnerFile('unguarded.jpg', 'image/jpeg')];
664
+ case 1:
665
+ file = _d.sent();
666
+ reachableFileIds.add(file.id);
667
+ return [4 /*yield*/, bytesAs(recipient, file.id)];
668
+ case 2:
669
+ served = _d.sent();
670
+ expect(served.equals(originalBytes)).toBe(true);
671
+ _a = expect;
672
+ return [4 /*yield*/, rowAsSystem(file.id)];
673
+ case 3:
674
+ _a.apply(void 0, [(_c = (_b = (_d.sent()).copyForOthers) === null || _b === void 0 ? void 0 : _b._id) !== null && _c !== void 0 ? _c : null]).toBeNull();
675
+ return [2 /*return*/];
676
+ }
677
+ });
678
+ }); });
679
+ it("NEW BYTES DROP THE COPY: after the owner rewrites the file, the recipient's next read is a fresh copy of the new bytes, the old copy's row and bytes gone", function () { return __awaiter(void 0, void 0, void 0, function () {
680
+ var file, staleCopyId, newBytes, _a, _b, served;
681
+ var _c, _d;
682
+ return __generator(this, function (_e) {
683
+ switch (_e.label) {
684
+ case 0: return [4 /*yield*/, createOwnerFile('edited.jpg', 'image/jpeg')];
685
+ case 1:
686
+ file = _e.sent();
687
+ reachableFileIds.add(file.id);
688
+ return [4 /*yield*/, bytesAs(recipient, file.id)];
689
+ case 2:
690
+ _e.sent();
691
+ return [4 /*yield*/, rowAsSystem(file.id)];
692
+ case 3:
693
+ staleCopyId = (_e.sent()).copyForOthers._id;
694
+ newBytes = Buffer.concat([Buffer.from('v2-'), originalBytes]);
695
+ testEnv.actAs(owner);
696
+ return [4 /*yield*/, new FileStorage_1.FileStorage().updateFileData(file.id, newBytes.toString('base64'))];
697
+ case 4:
698
+ _e.sent();
699
+ _a = expect;
700
+ return [4 /*yield*/, rowAsSystem(file.id)];
701
+ case 5:
702
+ _a.apply(void 0, [(_d = (_c = (_e.sent()).copyForOthers) === null || _c === void 0 ? void 0 : _c._id) !== null && _d !== void 0 ? _d : null]).toBeNull();
703
+ _b = expect;
704
+ return [4 /*yield*/, rowAsSystem(staleCopyId)];
705
+ case 6:
706
+ _b.apply(void 0, [_e.sent()]).toBeUndefined();
707
+ expect(driver.store.has(staleCopyId)).toBe(false);
708
+ return [4 /*yield*/, bytesAs(recipient, file.id)];
709
+ case 7:
710
+ served = _e.sent();
711
+ expect(served.equals(copyOf(newBytes))).toBe(true);
712
+ expect(makerCalls).toHaveLength(2);
713
+ expect(makerCalls[1].bytes.equals(newBytes)).toBe(true);
714
+ return [2 /*return*/];
715
+ }
716
+ });
717
+ }); });
718
+ it("DELETING THE ORIGINAL DELETES THE COPY: the copy's row and bytes go with the file", function () { return __awaiter(void 0, void 0, void 0, function () {
719
+ var file, copyId, _a, _b;
720
+ return __generator(this, function (_c) {
721
+ switch (_c.label) {
722
+ case 0: return [4 /*yield*/, createOwnerFile('gone.jpg', 'image/jpeg')];
723
+ case 1:
724
+ file = _c.sent();
725
+ reachableFileIds.add(file.id);
726
+ return [4 /*yield*/, bytesAs(recipient, file.id)];
727
+ case 2:
728
+ _c.sent();
729
+ return [4 /*yield*/, rowAsSystem(file.id)];
730
+ case 3:
731
+ copyId = (_c.sent()).copyForOthers._id;
732
+ testEnv.actAs(owner);
733
+ return [4 /*yield*/, new FileStorage_1.FileStorage().deleteFile(file.id)];
734
+ case 4:
735
+ _c.sent();
736
+ _a = expect;
737
+ return [4 /*yield*/, rowAsSystem(file.id)];
738
+ case 5:
739
+ _a.apply(void 0, [_c.sent()]).toBeUndefined();
740
+ _b = expect;
741
+ return [4 /*yield*/, rowAsSystem(copyId)];
742
+ case 6:
743
+ _b.apply(void 0, [_c.sent()]).toBeUndefined();
744
+ expect(driver.store.has(file.id)).toBe(false);
745
+ expect(driver.store.has(copyId)).toBe(false);
746
+ return [2 /*return*/];
747
+ }
748
+ });
749
+ }); });
750
+ });
751
+ describe('the copy for others — the route, both serving shapes', function () {
752
+ it("proxy shape: a recipient's GET /file/:id serves the copy's bytes under the ORIGINAL's name and type; the owner's serves the original", function () { return __awaiter(void 0, void 0, void 0, function () {
753
+ var driver, file, theirs, mine;
754
+ var _a;
755
+ return __generator(this, function (_b) {
756
+ switch (_b.label) {
757
+ case 0:
758
+ driver = new ProxyDriver();
759
+ testEnv.setDriver(driver);
760
+ return [4 /*yield*/, createOwnerFile('route-shot.jpg', 'image/jpeg')];
761
+ case 1:
762
+ file = _b.sent();
763
+ reachableFileIds.add(file.id);
764
+ testEnv.actAs(recipient);
765
+ return [4 /*yield*/, invokeRoute(file.id)];
766
+ case 2:
767
+ theirs = _b.sent();
768
+ testEnv.actAs(owner);
769
+ return [4 /*yield*/, invokeRoute(file.id)];
770
+ case 3:
771
+ mine = _b.sent();
772
+ expect((_a = theirs.statusCode) !== null && _a !== void 0 ? _a : 200).toEqual(200);
773
+ expect(theirs.headers['content-type']).toEqual('image/jpeg');
774
+ expect(theirs.headers['content-disposition']).toEqual('inline; filename="route-shot.jpg"');
775
+ expect(theirs.body.equals(copyOf(originalBytes))).toBe(true);
776
+ expect(mine.body.equals(originalBytes)).toBe(true);
777
+ return [2 /*return*/];
778
+ }
779
+ });
780
+ }); });
781
+ it("signed-URL shape (the production store): a recipient is redirected to the COPY's object, the owner to the original's", function () { return __awaiter(void 0, void 0, void 0, function () {
782
+ var driver, file, theirs, copyId, mine;
783
+ return __generator(this, function (_a) {
784
+ switch (_a.label) {
785
+ case 0:
786
+ driver = new SignedUrlDriver();
787
+ testEnv.setDriver(driver);
788
+ return [4 /*yield*/, createOwnerFile('route-clip-poster.jpg', 'image/jpeg')];
789
+ case 1:
790
+ file = _a.sent();
791
+ reachableFileIds.add(file.id);
792
+ testEnv.actAs(recipient);
793
+ return [4 /*yield*/, invokeRoute(file.id)];
794
+ case 2:
795
+ theirs = _a.sent();
796
+ return [4 /*yield*/, rowAsSystem(file.id)];
797
+ case 3:
798
+ copyId = (_a.sent()).copyForOthers._id;
799
+ testEnv.actAs(owner);
800
+ return [4 /*yield*/, invokeRoute(file.id)];
801
+ case 4:
802
+ mine = _a.sent();
803
+ expect(theirs.statusCode).toEqual(302);
804
+ expect(theirs.redirectUrl).toEqual("https://signed.test/blob/".concat(copyId, "?sig=test"));
805
+ expect(copyId).not.toEqual(file.id);
806
+ expect(mine.statusCode).toEqual(302);
807
+ expect(mine.redirectUrl).toEqual("https://signed.test/blob/".concat(file.id, "?sig=test"));
808
+ return [2 /*return*/];
809
+ }
810
+ });
811
+ }); });
812
+ it.each([
813
+ ['signed-URL', function () { return new SignedUrlDriver(); }],
814
+ ['proxy', function () { return new ProxyDriver(); }],
815
+ ])("a file no copy can be made of (%s shape): a recipient's GET /file/:id is a quiet 404 File not found — not a 500, no error printed; the owner's still serves", function (_shape, makeDriver) { return __awaiter(void 0, void 0, void 0, function () {
816
+ var driver, file, consoleError, theirs, mine;
817
+ return __generator(this, function (_a) {
818
+ switch (_a.label) {
819
+ case 0:
820
+ driver = makeDriver();
821
+ testEnv.setDriver(driver);
822
+ return [4 /*yield*/, createOwnerFile('unrewritable-route.jpg', 'image/jpeg')];
823
+ case 1:
824
+ file = _a.sent();
825
+ reachableFileIds.add(file.id);
826
+ makerFails = true;
827
+ consoleError = jest.spyOn(console, 'error').mockImplementation(function () { return undefined; });
828
+ _a.label = 2;
829
+ case 2:
830
+ _a.trys.push([2, , 5, 6]);
831
+ testEnv.actAs(recipient);
832
+ return [4 /*yield*/, invokeRoute(file.id)];
833
+ case 3:
834
+ theirs = _a.sent();
835
+ testEnv.actAs(owner);
836
+ return [4 /*yield*/, invokeRoute(file.id)];
837
+ case 4:
838
+ mine = _a.sent();
839
+ expect([theirs.statusCode, theirs.body]).toEqual([404, 'File not found']);
840
+ expect(theirs.redirectUrl).toBeUndefined();
841
+ expect(consoleError).not.toHaveBeenCalled();
842
+ if (driver instanceof SignedUrlDriver) {
843
+ expect(mine.statusCode).toEqual(302);
844
+ expect(mine.redirectUrl).toEqual("https://signed.test/blob/".concat(file.id, "?sig=test"));
845
+ }
846
+ else {
847
+ expect(mine.body.equals(originalBytes)).toBe(true);
848
+ }
849
+ return [3 /*break*/, 6];
850
+ case 5:
851
+ consoleError.mockRestore();
852
+ return [7 /*endfinally*/];
853
+ case 6: return [2 /*return*/];
854
+ }
855
+ });
856
+ }); });
857
+ it("a signed URL for a file the maker does not apply to still names the original's object", function () { return __awaiter(void 0, void 0, void 0, function () {
858
+ var driver, file, theirs;
859
+ return __generator(this, function (_a) {
860
+ switch (_a.label) {
861
+ case 0:
862
+ driver = new SignedUrlDriver();
863
+ testEnv.setDriver(driver);
864
+ return [4 /*yield*/, createOwnerFile('route-notes.pdf', 'application/pdf')];
865
+ case 1:
866
+ file = _a.sent();
867
+ reachableFileIds.add(file.id);
868
+ testEnv.actAs(recipient);
869
+ return [4 /*yield*/, invokeRoute(file.id)];
870
+ case 2:
871
+ theirs = _a.sent();
872
+ expect(theirs.redirectUrl).toEqual("https://signed.test/blob/".concat(file.id, "?sig=test"));
873
+ return [2 /*return*/];
874
+ }
875
+ });
876
+ }); });
877
+ });
878
+ describe('the copy for others — a server-side door that made its own access decision', function () {
879
+ var driver = new ProxyDriver();
880
+ /** What such a door does: reads the row as system (its own rule opened it), then asks which bytes this caller is served. */
881
+ var throughTheDoor = function (user, fileId) { return __awaiter(void 0, void 0, void 0, function () {
882
+ var row, _a, _b;
883
+ return __generator(this, function (_c) {
884
+ switch (_c.label) {
885
+ case 0: return [4 /*yield*/, rowAsSystem(fileId)];
886
+ case 1:
887
+ row = (_c.sent());
888
+ testEnv.actAs(user);
889
+ _b = (_a = Buffer).from;
890
+ return [4 /*yield*/, new FileStorage_1.FileStorage().getAuthorizedFileData(row)];
891
+ case 2: return [2 /*return*/, _b.apply(_a, [_c.sent(), 'base64'])];
892
+ }
893
+ });
894
+ }); };
895
+ beforeAll(function () {
896
+ testEnv.setDriver(driver);
897
+ });
898
+ it("a caller neither scope nor a resolver opens, whom the door vouches for, is served the maker's copy — the same copy the row names for every other non-owner", function () { return __awaiter(void 0, void 0, void 0, function () {
899
+ var file, served, copyId, _a;
900
+ var _b;
901
+ return __generator(this, function (_c) {
902
+ switch (_c.label) {
903
+ case 0: return [4 /*yield*/, createOwnerFile('attached.jpg', 'image/jpeg')];
904
+ case 1:
905
+ file = _c.sent();
906
+ return [4 /*yield*/, throughTheDoor(recipient, file.id)];
907
+ case 2:
908
+ served = _c.sent();
909
+ expect(served.includes(LOCATION_MARKER)).toBe(false);
910
+ expect(served.equals(copyOf(originalBytes))).toBe(true);
911
+ return [4 /*yield*/, rowAsSystem(file.id)];
912
+ case 3:
913
+ copyId = (_b = (_c.sent()).copyForOthers) === null || _b === void 0 ? void 0 : _b._id;
914
+ expect(copyId).toBeTruthy();
915
+ expect(driver.store.get(copyId).equals(served)).toBe(true);
916
+ // A share recipient later reaching the same file is served that same copy, never a second one.
917
+ reachableFileIds.add(file.id);
918
+ _a = expect;
919
+ return [4 /*yield*/, bytesAs(recipient, file.id)];
920
+ case 4:
921
+ _a.apply(void 0, [(_c.sent()).equals(served)]).toBe(true);
922
+ expect(makerCalls.map(function (call) { return call.fileId; })).toEqual([file.id]);
923
+ return [2 /*return*/];
924
+ }
925
+ });
926
+ }); });
927
+ it('the owner through the same door is served the original, byte for byte, and no copy is made', function () { return __awaiter(void 0, void 0, void 0, function () {
928
+ var file, served, _a;
929
+ var _b, _c;
930
+ return __generator(this, function (_d) {
931
+ switch (_d.label) {
932
+ case 0: return [4 /*yield*/, createOwnerFile('own-attachment.jpg', 'image/jpeg')];
933
+ case 1:
934
+ file = _d.sent();
935
+ return [4 /*yield*/, throughTheDoor(owner, file.id)];
936
+ case 2:
937
+ served = _d.sent();
938
+ expect(served.equals(originalBytes)).toBe(true);
939
+ expect(makerCalls).toEqual([]);
940
+ _a = expect;
941
+ return [4 /*yield*/, rowAsSystem(file.id)];
942
+ case 3:
943
+ _a.apply(void 0, [(_c = (_b = (_d.sent()).copyForOthers) === null || _b === void 0 ? void 0 : _b._id) !== null && _c !== void 0 ? _c : null]).toBeNull();
944
+ return [2 /*return*/];
945
+ }
946
+ });
947
+ }); });
948
+ it("when no copy can be made, the door's caller is refused as NOT FOUND — a 404 ServiceRefusal in a missing file's words, a refusal and never a failure — and never served the original", function () { return __awaiter(void 0, void 0, void 0, function () {
949
+ var file, refusal, _a;
950
+ var _b, _c;
951
+ return __generator(this, function (_d) {
952
+ switch (_d.label) {
953
+ case 0: return [4 /*yield*/, createOwnerFile('raw-attachment.jpg', 'image/jpeg')];
954
+ case 1:
955
+ file = _d.sent();
956
+ makerFails = true;
957
+ return [4 /*yield*/, throughTheDoor(recipient, file.id).then(function () { return undefined; }, function (error) { return error; })];
958
+ case 2:
959
+ refusal = _d.sent();
960
+ // The status the service router answers with and the executor logs at WARN: 404 leaks nothing
961
+ // about the file's existence to a caller who may not read it.
962
+ expect(service_1.ServiceRefusal.is(refusal)).toBe(true);
963
+ expect(refusal.status).toBe(404);
964
+ expect(refusal.message).toEqual('File not found');
965
+ _a = expect;
966
+ return [4 /*yield*/, rowAsSystem(file.id)];
967
+ case 3:
968
+ _a.apply(void 0, [(_c = (_b = (_d.sent()).copyForOthers) === null || _b === void 0 ? void 0 : _b._id) !== null && _c !== void 0 ? _c : null]).toBeNull();
969
+ return [2 /*return*/];
970
+ }
971
+ });
972
+ }); });
973
+ });
974
+ describe('ONE REFUSAL AT EVERY DOOR — a file no copy can be made of is not found for anyone but its owner (404), whichever door asks', function () {
975
+ /** A file the recipient reaches whose copy the maker refuses — both serving shapes hold its bytes. */
976
+ var refusedFile = function (name) { return __awaiter(void 0, void 0, void 0, function () {
977
+ var proxy, signed, file;
978
+ return __generator(this, function (_a) {
979
+ switch (_a.label) {
980
+ case 0:
981
+ proxy = new ProxyDriver();
982
+ signed = new SignedUrlDriver();
983
+ testEnv.setDriver(proxy);
984
+ return [4 /*yield*/, createOwnerFile(name, 'image/jpeg')];
985
+ case 1:
986
+ file = _a.sent();
987
+ signed.store.set(file.id, proxy.store.get(file.id));
988
+ reachableFileIds.add(file.id);
989
+ makerFails = true;
990
+ return [2 /*return*/, { file: file, proxy: proxy, signed: signed }];
991
+ }
992
+ });
993
+ }); };
994
+ /** Every door a recipient can ask, in one order: the browser service, the route (proxy, then signed-URL), the server-side door. */
995
+ var askEveryDoor = function (file, signed) { return __awaiter(void 0, void 0, void 0, function () {
996
+ var service, proxyRoute, serverSide, _a, _b, signedRoute;
997
+ return __generator(this, function (_c) {
998
+ switch (_c.label) {
999
+ case 0:
1000
+ testEnv.actAs(recipient);
1001
+ return [4 /*yield*/, invokeServiceDoor(file.id)];
1002
+ case 1:
1003
+ service = _c.sent();
1004
+ return [4 /*yield*/, invokeRoute(file.id)];
1005
+ case 2:
1006
+ proxyRoute = _c.sent();
1007
+ _b = (_a = new FileStorage_1.FileStorage()).getAuthorizedFileData;
1008
+ return [4 /*yield*/, rowAsSystem(file.id)];
1009
+ case 3: return [4 /*yield*/, _b.apply(_a, [(_c.sent())]).then(function () { return undefined; }, function (error) { return error; })];
1010
+ case 4:
1011
+ serverSide = _c.sent();
1012
+ testEnv.setDriver(signed);
1013
+ return [4 /*yield*/, invokeRoute(file.id)];
1014
+ case 5:
1015
+ signedRoute = _c.sent();
1016
+ return [2 /*return*/, { service: service, proxyRoute: proxyRoute, serverSide: serverSide, signedRoute: signedRoute }];
1017
+ }
1018
+ });
1019
+ }); };
1020
+ it('the browser service (through the router), GET /file/:id in both shapes and the server-side door answer one status, 404 — the browser door logs one WARN with its status and no ERROR', function () { return __awaiter(void 0, void 0, void 0, function () {
1021
+ var _a, file, signed, consoleError, _b, service, proxyRoute, serverSide, signedRoute, warnings;
1022
+ return __generator(this, function (_c) {
1023
+ switch (_c.label) {
1024
+ case 0: return [4 /*yield*/, refusedFile('one-answer.jpg')];
1025
+ case 1:
1026
+ _a = _c.sent(), file = _a.file, signed = _a.signed;
1027
+ consoleError = jest.spyOn(console, 'error').mockImplementation(function () { return undefined; });
1028
+ _c.label = 2;
1029
+ case 2:
1030
+ _c.trys.push([2, , 4, 5]);
1031
+ return [4 /*yield*/, askEveryDoor(file, signed)];
1032
+ case 3:
1033
+ _b = _c.sent(), service = _b.service, proxyRoute = _b.proxyRoute, serverSide = _b.serverSide, signedRoute = _b.signedRoute;
1034
+ expect([service.status, signedRoute.statusCode, proxyRoute.statusCode, routerStatus(serverSide)]).toEqual([
1035
+ 404, 404, 404, 404,
1036
+ ]);
1037
+ // The browser door's log: one WARN naming the status and the operation — a refusal, never a failure.
1038
+ expect(service.entries.filter(function (entry) { return entry.logLevel === 'error'; })).toEqual([]);
1039
+ warnings = service.entries.filter(function (entry) { return entry.logLevel === 'warn'; });
1040
+ expect(warnings).toHaveLength(1);
1041
+ expect(warnings[0].obj).toMatchObject({ status: 404, functionName: 'FileStorageService.getFileData' });
1042
+ expect(consoleError).not.toHaveBeenCalled();
1043
+ // And never the original, at any door.
1044
+ expect([signedRoute.redirectUrl, proxyRoute.body]).toEqual([undefined, 'File not found']);
1045
+ return [3 /*break*/, 5];
1046
+ case 4:
1047
+ consoleError.mockRestore();
1048
+ return [7 /*endfinally*/];
1049
+ case 5: return [2 /*return*/];
1050
+ }
1051
+ });
1052
+ }); });
1053
+ it("ONE BODY TOO: every door answers the words a missing file gets — \"File not found\" — never the maker's reason, the file's id or its owner; the reason stays on the server, in the storage's one WARN", function () { return __awaiter(void 0, void 0, void 0, function () {
1054
+ var _a, file, signed, _b, service, proxyRoute, serverSide, signedRoute, read, _i, _c, words, storageWarnings;
1055
+ return __generator(this, function (_d) {
1056
+ switch (_d.label) {
1057
+ case 0: return [4 /*yield*/, refusedFile('one-body.jpg')];
1058
+ case 1:
1059
+ _a = _d.sent(), file = _a.file, signed = _a.signed;
1060
+ return [4 /*yield*/, askEveryDoor(file, signed)];
1061
+ case 2:
1062
+ _b = _d.sent(), service = _b.service, proxyRoute = _b.proxyRoute, serverSide = _b.serverSide, signedRoute = _b.signedRoute;
1063
+ read = [service.body, signedRoute.body, proxyRoute.body, serverSide === null || serverSide === void 0 ? void 0 : serverSide.message];
1064
+ expect(read).toEqual([{ error: 'File not found' }, 'File not found', 'File not found', 'File not found']);
1065
+ for (_i = 0, _c = read.map(function (answer) { return JSON.stringify(answer); }); _i < _c.length; _i++) {
1066
+ words = _c[_i];
1067
+ expect(words).not.toContain(MAKER_REASON);
1068
+ expect(words).not.toContain(file.id);
1069
+ expect(words).not.toContain('owner');
1070
+ }
1071
+ storageWarnings = service.storageEntries.filter(function (entry) { return entry.logLevel === 'warn'; });
1072
+ expect(storageWarnings).toHaveLength(1);
1073
+ expect(storageWarnings[0].obj).toEqual({ fileId: file.id, reason: MAKER_REASON });
1074
+ expect(__spreadArray(__spreadArray([], service.entries, true), service.storageEntries, true).filter(function (entry) { return entry.logLevel === 'error'; })).toEqual([]);
1075
+ return [2 /*return*/];
1076
+ }
1077
+ });
1078
+ }); });
1079
+ it("A MISS ANSWERS WHAT A REFUSAL ANSWERS: a file that is not there, and one the caller cannot reach, get the refused copy's status and body at every door — the browser service, the route in both shapes, the signed-URL mint — never the id", function () { return __awaiter(void 0, void 0, void 0, function () {
1080
+ var _a, refused, proxy, signed, unreachable, notRoutine, answersFor, refusal, _b, _c;
1081
+ return __generator(this, function (_d) {
1082
+ switch (_d.label) {
1083
+ case 0: return [4 /*yield*/, refusedFile('refused-beside-a-miss.jpg')];
1084
+ case 1:
1085
+ _a = _d.sent(), refused = _a.file, proxy = _a.proxy, signed = _a.signed;
1086
+ testEnv.actAs(owner);
1087
+ return [4 /*yield*/, createOwnerFile('not-theirs.jpg', 'image/jpeg')];
1088
+ case 2:
1089
+ unreachable = _d.sent();
1090
+ signed.store.set(unreachable.id, proxy.store.get(unreachable.id));
1091
+ notRoutine = function (level) { return level === 'warn' || level === 'error'; };
1092
+ answersFor = function (fileId) { return __awaiter(void 0, void 0, void 0, function () {
1093
+ var service, proxyRoute, signedRoute, mint;
1094
+ return __generator(this, function (_a) {
1095
+ switch (_a.label) {
1096
+ case 0:
1097
+ testEnv.actAs(recipient);
1098
+ testEnv.setDriver(proxy);
1099
+ return [4 /*yield*/, invokeServiceDoor(fileId)];
1100
+ case 1:
1101
+ service = _a.sent();
1102
+ return [4 /*yield*/, invokeRoute(fileId)];
1103
+ case 2:
1104
+ proxyRoute = _a.sent();
1105
+ testEnv.setDriver(signed);
1106
+ return [4 /*yield*/, invokeRoute(fileId)];
1107
+ case 3:
1108
+ signedRoute = _a.sent();
1109
+ return [4 /*yield*/, new FileStorage_1.FileStorage().getSignedUrl(fileId).then(function () { return undefined; }, function (error) { return error; })];
1110
+ case 4:
1111
+ mint = _a.sent();
1112
+ return [2 /*return*/, [
1113
+ [service.status, service.body, service.entries.map(function (entry) { return entry.logLevel; }).filter(notRoutine)],
1114
+ [proxyRoute.statusCode, proxyRoute.body, proxyRoute.redirectUrl],
1115
+ [signedRoute.statusCode, signedRoute.body, signedRoute.redirectUrl],
1116
+ [service_1.ServiceRefusal.is(mint) ? mint.status : 'not a refusal', mint === null || mint === void 0 ? void 0 : mint.message],
1117
+ ]];
1118
+ }
1119
+ });
1120
+ }); };
1121
+ return [4 /*yield*/, answersFor(refused.id)];
1122
+ case 3:
1123
+ refusal = _d.sent();
1124
+ expect(refusal).toEqual([
1125
+ [404, { error: 'File not found' }, ['warn']],
1126
+ [404, 'File not found', undefined],
1127
+ [404, 'File not found', undefined],
1128
+ [404, 'File not found'],
1129
+ ]);
1130
+ _b = expect;
1131
+ return [4 /*yield*/, answersFor(unreachable.id)];
1132
+ case 4:
1133
+ _b.apply(void 0, [_d.sent()]).toEqual(refusal);
1134
+ _c = expect;
1135
+ return [4 /*yield*/, answersFor('no-such-file')];
1136
+ case 5:
1137
+ _c.apply(void 0, [_d.sent()]).toEqual(refusal);
1138
+ return [2 /*return*/];
1139
+ }
1140
+ });
1141
+ }); });
1142
+ });
1143
+ //# sourceMappingURL=FileCopyForOthers.test.js.map