@proteinjs/db-file 1.7.4 → 1.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,25 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [1.7.6](https://github.com/proteinjs/db/compare/@proteinjs/db-file@1.7.5...@proteinjs/db-file@1.7.6) (2026-08-29)
7
+
8
+ **Note:** Version bump only for package @proteinjs/db-file
9
+
10
+
11
+
12
+
13
+
14
+ ## [1.7.5](https://github.com/proteinjs/db/compare/@proteinjs/db-file@1.7.4...@proteinjs/db-file@1.7.5) (2026-08-28)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * file byte access gates on the file row — the browser-facing byte ops refuse ids whose row the caller cannot reach ([3ba4a7c](https://github.com/proteinjs/db/commit/3ba4a7cf50d5055998ea3ccc479068fe8262aad0))
20
+
21
+
22
+
23
+
24
+
6
25
  ## [1.7.4](https://github.com/proteinjs/db/compare/@proteinjs/db-file@1.7.3...@proteinjs/db-file@1.7.4) (2026-08-28)
7
26
 
8
27
 
@@ -55,8 +55,18 @@ export declare class FileStorage implements FileStorageService {
55
55
  getFile(fileId: string): Promise<File>;
56
56
  /**
57
57
  * Retrieves the data chunks associated with a given file.
58
+ *
59
+ * Gated on the {@link getFile} row read — the same two-legged access decision every serving
60
+ * path derives from (the caller's SCOPED read, else shared-content reachability), so bytes are
61
+ * exactly as reachable as the row that names them. The gate lives here because this is the
62
+ * browser-facing service boundary (`FileStorageService` is `allUsers`) and the driver byte ops
63
+ * below are deliberately unscoped. Server-side doors that make their OWN documented access
64
+ * decision (the avatar route, issue/ticket attachment doors) read bytes through
65
+ * `FileStorage.getDriver()` instead.
58
66
  * @param fileId - The `id` of the file.
59
67
  * @returns The file data as a single string.
68
+ * @throws When the file row does not exist or is not readable by the caller — the same named
69
+ * miss either way, so existence is not leaked to unauthorized callers.
60
70
  */
61
71
  getFileData(fileId: string): Promise<string>;
62
72
  /**
@@ -75,8 +85,15 @@ export declare class FileStorage implements FileStorageService {
75
85
  }): Promise<string | undefined>;
76
86
  /**
77
87
  * Updates the data chunks associated with a given file.
88
+ *
89
+ * Gated on the caller's SCOPED row read — owner scope is the write grant on ScopedRecords
90
+ * (the same authorization a scoped row update enforces). Deliberately STRICTER than
91
+ * {@link getFileData}: the {@link FileReachabilityResolver} leg widens READS only, so a
92
+ * share recipient who can read the bytes still cannot write them.
78
93
  * @param fileId - The `id` of the file.
79
94
  * @param data - The new data string to replace the existing data.
95
+ * @throws When the file row does not exist or is not writable by the caller — the same named
96
+ * miss either way, so existence is not leaked to unauthorized callers.
80
97
  */
81
98
  updateFileData(fileId: string, data: string): Promise<void>;
82
99
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"FileStorage.d.ts","sourceRoot":"","sources":["../../src/FileStorage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAoC,MAAM,iBAAiB,CAAC;AACjF,OAAO,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAE1C,OAAO,EAAE,kBAAkB,EAAyB,MAAM,+BAA+B,CAAC;AAC1F,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAExD,OAAO,EAAE,QAAQ,EAAoB,MAAM,uBAAuB,CAAC;AAInE;;;KAGK;AACL,eAAO,MAAM,cAAc,mBACiE,CAAC;AAE7F;;GAEG;AACH,MAAM,WAAW,+BAAgC,SAAQ,QAAQ;IAC/D,SAAS,IAAI,iBAAiB,CAAC;CAChC;AAED;;;;GAIG;AACH,qBAAa,WAAY,YAAW,kBAAkB;IACpD,OAAO,CAAC,MAAM,CAAC,MAAM,CAAoB;IAElC,eAAe;;;;MAIpB;IAEF;;;;;OAKG;IACH,MAAM,CAAC,SAAS,IAAI,iBAAiB;IAkBrC;;;;;OAKG;IACG,UAAU,CAAC,YAAY,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,YAAY,CAAC,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAO/F;;;;;;;;;;;OAWG;IACG,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkB5C;;;;OAIG;IACG,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIlD;;;;;;;;;;OAUG;IACG,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAc7F;;;;OAIG;IACG,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIjE;;;OAGG;IACG,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,YAAY,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAUrE;;;;;;OAMG;IACG,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAIhD"}
1
+ {"version":3,"file":"FileStorage.d.ts","sourceRoot":"","sources":["../../src/FileStorage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAoC,MAAM,iBAAiB,CAAC;AACjF,OAAO,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAE1C,OAAO,EAAE,kBAAkB,EAAyB,MAAM,+BAA+B,CAAC;AAC1F,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAExD,OAAO,EAAE,QAAQ,EAAoB,MAAM,uBAAuB,CAAC;AAInE;;;KAGK;AACL,eAAO,MAAM,cAAc,mBACiE,CAAC;AAE7F;;GAEG;AACH,MAAM,WAAW,+BAAgC,SAAQ,QAAQ;IAC/D,SAAS,IAAI,iBAAiB,CAAC;CAChC;AAED;;;;GAIG;AACH,qBAAa,WAAY,YAAW,kBAAkB;IACpD,OAAO,CAAC,MAAM,CAAC,MAAM,CAAoB;IAElC,eAAe;;;;MAIpB;IAEF;;;;;OAKG;IACH,MAAM,CAAC,SAAS,IAAI,iBAAiB;IAkBrC;;;;;OAKG;IACG,UAAU,CAAC,YAAY,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,YAAY,CAAC,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAO/F;;;;;;;;;;;OAWG;IACG,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkB5C;;;;;;;;;;;;;;OAcG;IACG,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IASlD;;;;;;;;;;OAUG;IACG,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAc7F;;;;;;;;;;;OAWG;IACG,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUjE;;;OAGG;IACG,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,YAAY,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAUrE;;;;;;OAMG;IACG,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAIhD"}
@@ -158,15 +158,32 @@ var FileStorage = /** @class */ (function () {
158
158
  };
159
159
  /**
160
160
  * Retrieves the data chunks associated with a given file.
161
+ *
162
+ * Gated on the {@link getFile} row read — the same two-legged access decision every serving
163
+ * path derives from (the caller's SCOPED read, else shared-content reachability), so bytes are
164
+ * exactly as reachable as the row that names them. The gate lives here because this is the
165
+ * browser-facing service boundary (`FileStorageService` is `allUsers`) and the driver byte ops
166
+ * 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.
161
169
  * @param fileId - The `id` of the file.
162
170
  * @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.
163
173
  */
164
174
  FileStorage.prototype.getFileData = function (fileId) {
165
175
  return __awaiter(this, void 0, void 0, function () {
176
+ var file;
166
177
  return __generator(this, function (_a) {
167
178
  switch (_a.label) {
168
- case 0: return [4 /*yield*/, FileStorage.getDriver().getFileData(fileId)];
169
- case 1: return [2 /*return*/, _a.sent()];
179
+ case 0: return [4 /*yield*/, this.getFile(fileId)];
180
+ case 1:
181
+ file = _a.sent();
182
+ if (!file) {
183
+ throw new Error("File not found: ".concat(fileId));
184
+ }
185
+ return [4 /*yield*/, FileStorage.getDriver().getFileData(fileId)];
186
+ case 2: return [2 /*return*/, _a.sent()];
170
187
  }
171
188
  });
172
189
  });
@@ -205,15 +222,31 @@ var FileStorage = /** @class */ (function () {
205
222
  };
206
223
  /**
207
224
  * Updates the data chunks associated with a given file.
225
+ *
226
+ * Gated on the caller's SCOPED row read — owner scope is the write grant on ScopedRecords
227
+ * (the same authorization a scoped row update enforces). Deliberately STRICTER than
228
+ * {@link getFileData}: the {@link FileReachabilityResolver} leg widens READS only, so a
229
+ * share recipient who can read the bytes still cannot write them.
208
230
  * @param fileId - The `id` of the file.
209
231
  * @param data - The new data string to replace the existing data.
232
+ * @throws When the file row does not exist or is not writable by the caller — the same named
233
+ * miss either way, so existence is not leaked to unauthorized callers.
210
234
  */
211
235
  FileStorage.prototype.updateFileData = function (fileId, data) {
212
236
  return __awaiter(this, void 0, void 0, function () {
237
+ var db, file;
213
238
  return __generator(this, function (_a) {
214
239
  switch (_a.label) {
215
- case 0: return [4 /*yield*/, FileStorage.getDriver().updateFileData(fileId, data)];
240
+ case 0:
241
+ db = (0, user_1.getScopedDb)();
242
+ return [4 /*yield*/, db.get(tables_1.tables.File, { id: fileId })];
216
243
  case 1:
244
+ file = _a.sent();
245
+ if (!file) {
246
+ throw new Error("File not found: ".concat(fileId));
247
+ }
248
+ return [4 /*yield*/, FileStorage.getDriver().updateFileData(fileId, data)];
249
+ case 2:
217
250
  _a.sent();
218
251
  return [2 /*return*/];
219
252
  }
@@ -1 +1 @@
1
- {"version":3,"file":"FileStorage.js","sourceRoot":"","sources":["../../src/FileStorage.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"FileStorage.js","sourceRoot":"","sources":["../../src/FileStorage.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=FileByteAccess.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FileByteAccess.test.d.ts","sourceRoot":"","sources":["../../test/FileByteAccess.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,252 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __generator = (this && this.__generator) || function (thisArg, body) {
12
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
13
+ return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14
+ function verb(n) { return function (v) { return step([n, v]); }; }
15
+ function step(op) {
16
+ if (f) throw new TypeError("Generator is already executing.");
17
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
18
+ 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;
19
+ if (y = 0, t) op = [op[0] & 2, t.value];
20
+ switch (op[0]) {
21
+ case 0: case 1: t = op; break;
22
+ case 4: _.label++; return { value: op[1], done: false };
23
+ case 5: _.label++; y = op[1]; op = [0]; continue;
24
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
25
+ default:
26
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30
+ if (t[2]) _.ops.pop();
31
+ _.trys.pop(); continue;
32
+ }
33
+ op = body.call(thisArg, _);
34
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36
+ }
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ var reflection_1 = require("@proteinjs/reflection");
40
+ var FileStorage_1 = require("../src/FileStorage");
41
+ var DbFileStorageDriver_1 = require("../src/DbFileStorageDriver");
42
+ var FileTestEnvironment_1 = require("./FileTestEnvironment");
43
+ /** Every byte value once — a payload that corrupts under any text/base64 mix-up. */
44
+ var rawBytes = Buffer.from(Array.from({ length: 256 }, function (_, i) { return i; }));
45
+ var rawBase64 = rawBytes.toString('base64');
46
+ /** A same-length replacement payload, byte-distinct at every position — the poisoning attempt. */
47
+ var poisonBytes = Buffer.from(Array.from({ length: 256 }, function (_, i) { return 255 - i; }));
48
+ var poisonBase64 = poisonBytes.toString('base64');
49
+ /**
50
+ * The byte-access gates on the browser-facing service (FileStorageService is `allUsers`, so
51
+ * these methods are every logged-in user's to call with any file id they can guess or leak):
52
+ *
53
+ * - **Reads** (`getFileData`): gated on the {@link FileStorage.getFile} row read — the same
54
+ * two-legged access decision every serving path derives from (the caller's SCOPED read, else
55
+ * the FileReachabilityResolver shared-content leg). Bytes are exactly as reachable as the row
56
+ * that names them. Ungated, a foreign file id read another user's bytes outright (the driver
57
+ * byte ops are deliberately unscoped — the access wall lives at this service boundary).
58
+ * - **Writes** (`updateFileData`): gated on the caller's SCOPED row read — owner scope is the
59
+ * write grant on ScopedRecords. Stricter than reads: the resolver leg widens READS only, so a
60
+ * share recipient who can read bytes still cannot write them. Ungated, a foreign file id let
61
+ * any caller poison another user's bytes (with the DbFileStorageDriver: caller-scoped chunk
62
+ * inserts landing beside the owner's — this suite runs that real driver so the intact-bytes
63
+ * assertions prove no chunk ever landed).
64
+ *
65
+ * The resolver is stubbed at the seam (content packages sit above db-file); denial is the named
66
+ * `File not found` — existence is not leaked to unauthorized callers.
67
+ */
68
+ var testEnv = new FileTestEnvironment_1.FileTestEnvironment();
69
+ var objectCache = function () { return reflection_1.SourceRepository.get().objectCache; };
70
+ var owner;
71
+ var recipient;
72
+ var stranger;
73
+ /** File ids the stub resolver reports as reachable for the current user. */
74
+ var reachableFileIds = new Set();
75
+ /** How many times the seam was consulted — the owner's scoped leg must never pay for it. */
76
+ var resolverConsultations = 0;
77
+ beforeAll(function () { return __awaiter(void 0, void 0, void 0, function () {
78
+ return __generator(this, function (_a) {
79
+ switch (_a.label) {
80
+ case 0: return [4 /*yield*/, testEnv.beforeAll()];
81
+ case 1:
82
+ _a.sent();
83
+ // The seam getFile consults on a scoped-read miss, stubbed (the real thought-media resolver
84
+ // lives above this package).
85
+ objectCache()['@proteinjs/db-file/FileReachabilityResolver'] = [
86
+ {
87
+ canReadViaReference: function (fileId) { return __awaiter(void 0, void 0, void 0, function () {
88
+ return __generator(this, function (_a) {
89
+ resolverConsultations++;
90
+ return [2 /*return*/, reachableFileIds.has(fileId)];
91
+ });
92
+ }); },
93
+ },
94
+ ];
95
+ // The real chunked driver: write-gate bites are proven at the chunk-row level, not a mock.
96
+ testEnv.setDriver(new DbFileStorageDriver_1.DbFileStorageDriver());
97
+ return [4 /*yield*/, testEnv.createUser({ name: 'File owner', email: 'byte-owner@test.local' })];
98
+ case 2:
99
+ owner = _a.sent();
100
+ return [4 /*yield*/, testEnv.createUser({ name: 'Share recipient', email: 'byte-recipient@test.local' })];
101
+ case 3:
102
+ recipient = _a.sent();
103
+ return [4 /*yield*/, testEnv.createUser({ name: 'Stranger', email: 'byte-stranger@test.local' })];
104
+ case 4:
105
+ stranger = _a.sent();
106
+ testEnv.actAs(owner);
107
+ return [2 /*return*/];
108
+ }
109
+ });
110
+ }); });
111
+ afterAll(function () { return __awaiter(void 0, void 0, void 0, function () {
112
+ return __generator(this, function (_a) {
113
+ switch (_a.label) {
114
+ case 0:
115
+ delete objectCache()['@proteinjs/db-file/FileReachabilityResolver'];
116
+ return [4 /*yield*/, testEnv.afterAll()];
117
+ case 1:
118
+ _a.sent();
119
+ return [2 /*return*/];
120
+ }
121
+ });
122
+ }); });
123
+ beforeEach(function () {
124
+ reachableFileIds.clear();
125
+ resolverConsultations = 0;
126
+ testEnv.actAs(owner);
127
+ });
128
+ var createOwnerFile = function (name) { return __awaiter(void 0, void 0, void 0, function () {
129
+ return __generator(this, function (_a) {
130
+ switch (_a.label) {
131
+ case 0: return [4 /*yield*/, new FileStorage_1.FileStorage().createFile({ name: name, type: 'application/octet-stream', size: rawBytes.length }, rawBase64)];
132
+ case 1: return [2 /*return*/, _a.sent()];
133
+ }
134
+ });
135
+ }); };
136
+ describe('byte reads gate on the file-row read (the getFile decision)', function () {
137
+ it('the owner reads their bytes through the scoped leg alone — resolvers are never consulted', function () { return __awaiter(void 0, void 0, void 0, function () {
138
+ var file, fileDataBase64;
139
+ return __generator(this, function (_a) {
140
+ switch (_a.label) {
141
+ case 0: return [4 /*yield*/, createOwnerFile('own-bytes.bin')];
142
+ case 1:
143
+ file = _a.sent();
144
+ return [4 /*yield*/, new FileStorage_1.FileStorage().getFileData(file.id)];
145
+ case 2:
146
+ fileDataBase64 = _a.sent();
147
+ expect(fileDataBase64).toEqual(rawBase64);
148
+ expect(resolverConsultations).toEqual(0);
149
+ return [2 /*return*/];
150
+ }
151
+ });
152
+ }); });
153
+ it("THE VULN (read): a stranger holding only the file id is DENIED another user's bytes", function () { return __awaiter(void 0, void 0, void 0, function () {
154
+ var file;
155
+ return __generator(this, function (_a) {
156
+ switch (_a.label) {
157
+ case 0: return [4 /*yield*/, createOwnerFile('private-bytes.bin')];
158
+ case 1:
159
+ file = _a.sent();
160
+ testEnv.actAs(stranger);
161
+ return [4 /*yield*/, expect(new FileStorage_1.FileStorage().getFileData(file.id)).rejects.toThrow("File not found: ".concat(file.id))];
162
+ case 2:
163
+ _a.sent();
164
+ return [2 /*return*/];
165
+ }
166
+ });
167
+ }); });
168
+ it('a shared-content recipient (resolver-vouched) reads the bytes byte-identical', function () { return __awaiter(void 0, void 0, void 0, function () {
169
+ var file, fileDataBase64;
170
+ return __generator(this, function (_a) {
171
+ switch (_a.label) {
172
+ case 0: return [4 /*yield*/, createOwnerFile('shared-bytes.bin')];
173
+ case 1:
174
+ file = _a.sent();
175
+ reachableFileIds.add(file.id);
176
+ testEnv.actAs(recipient);
177
+ return [4 /*yield*/, new FileStorage_1.FileStorage().getFileData(file.id)];
178
+ case 2:
179
+ fileDataBase64 = _a.sent();
180
+ expect(fileDataBase64).toEqual(rawBase64);
181
+ return [2 /*return*/];
182
+ }
183
+ });
184
+ }); });
185
+ });
186
+ describe('byte writes gate on the caller WRITING the row (owner scope; read-reachability never grants it)', function () {
187
+ it("THE VULN (write): a stranger holding only the file id cannot poison another user's bytes", function () { return __awaiter(void 0, void 0, void 0, function () {
188
+ var file, _a;
189
+ return __generator(this, function (_b) {
190
+ switch (_b.label) {
191
+ case 0: return [4 /*yield*/, createOwnerFile('unpoisonable.bin')];
192
+ case 1:
193
+ file = _b.sent();
194
+ testEnv.actAs(stranger);
195
+ return [4 /*yield*/, expect(new FileStorage_1.FileStorage().updateFileData(file.id, poisonBase64)).rejects.toThrow("File not found: ".concat(file.id))];
196
+ case 2:
197
+ _b.sent();
198
+ // The outcome that matters: no chunk landed — the owner's bytes read back untouched.
199
+ testEnv.actAs(owner);
200
+ _a = expect;
201
+ return [4 /*yield*/, new FileStorage_1.FileStorage().getFileData(file.id)];
202
+ case 3:
203
+ _a.apply(void 0, [_b.sent()]).toEqual(rawBase64);
204
+ return [2 /*return*/];
205
+ }
206
+ });
207
+ }); });
208
+ it("read-reachability does NOT grant write: a vouched recipient's byte write is denied, bytes intact", function () { return __awaiter(void 0, void 0, void 0, function () {
209
+ var file, _a, _b;
210
+ return __generator(this, function (_c) {
211
+ switch (_c.label) {
212
+ case 0: return [4 /*yield*/, createOwnerFile('read-only-share.bin')];
213
+ case 1:
214
+ file = _c.sent();
215
+ reachableFileIds.add(file.id);
216
+ testEnv.actAs(recipient);
217
+ _a = expect;
218
+ return [4 /*yield*/, new FileStorage_1.FileStorage().getFileData(file.id)];
219
+ case 2:
220
+ _a.apply(void 0, [_c.sent()]).toEqual(rawBase64);
221
+ return [4 /*yield*/, expect(new FileStorage_1.FileStorage().updateFileData(file.id, poisonBase64)).rejects.toThrow("File not found: ".concat(file.id))];
222
+ case 3:
223
+ _c.sent();
224
+ testEnv.actAs(owner);
225
+ _b = expect;
226
+ return [4 /*yield*/, new FileStorage_1.FileStorage().getFileData(file.id)];
227
+ case 4:
228
+ _b.apply(void 0, [_c.sent()]).toEqual(rawBase64);
229
+ return [2 /*return*/];
230
+ }
231
+ });
232
+ }); });
233
+ it('the owner replaces their bytes wholesale', function () { return __awaiter(void 0, void 0, void 0, function () {
234
+ var file, _a;
235
+ return __generator(this, function (_b) {
236
+ switch (_b.label) {
237
+ case 0: return [4 /*yield*/, createOwnerFile('rewritable.bin')];
238
+ case 1:
239
+ file = _b.sent();
240
+ return [4 /*yield*/, new FileStorage_1.FileStorage().updateFileData(file.id, poisonBase64)];
241
+ case 2:
242
+ _b.sent();
243
+ _a = expect;
244
+ return [4 /*yield*/, new FileStorage_1.FileStorage().getFileData(file.id)];
245
+ case 3:
246
+ _a.apply(void 0, [_b.sent()]).toEqual(poisonBase64);
247
+ return [2 /*return*/];
248
+ }
249
+ });
250
+ }); });
251
+ });
252
+ //# sourceMappingURL=FileByteAccess.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FileByteAccess.test.js","sourceRoot":"","sources":["../../test/FileByteAccess.test.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proteinjs/db-file",
3
- "version": "1.7.4",
3
+ "version": "1.7.6",
4
4
  "description": "File storage in the db",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -22,7 +22,7 @@
22
22
  "watch": "reflection-watch"
23
23
  },
24
24
  "dependencies": {
25
- "@proteinjs/db": "^1.36.0",
25
+ "@proteinjs/db": "^1.37.0",
26
26
  "@proteinjs/logger": "^1.0.21",
27
27
  "@proteinjs/reflection": "^1.2.0",
28
28
  "@proteinjs/server-api": "^3.0.11",
@@ -31,7 +31,7 @@
31
31
  "moment": "2.29.4"
32
32
  },
33
33
  "devDependencies": {
34
- "@proteinjs/db-driver-spanner": "^1.24.0",
34
+ "@proteinjs/db-driver-spanner": "^1.25.0",
35
35
  "@proteinjs/reflection-build": "^2.0.1",
36
36
  "@types/jest": "29.5.5",
37
37
  "@types/node": "14.0.27",
@@ -46,5 +46,5 @@
46
46
  },
47
47
  "main": "./dist/generated/index.js",
48
48
  "types": "./dist/generated/index.d.ts",
49
- "gitHead": "1b1a4fa4c3a6bbf2b4e236735ab505bfe7b54ef6"
49
+ "gitHead": "cb0fda126d5d2bd5b17d1bc13e87c2373eb61f52"
50
50
  }
@@ -105,10 +105,25 @@ export class FileStorage implements FileStorageService {
105
105
 
106
106
  /**
107
107
  * Retrieves the data chunks associated with a given file.
108
+ *
109
+ * Gated on the {@link getFile} row read — the same two-legged access decision every serving
110
+ * path derives from (the caller's SCOPED read, else shared-content reachability), so bytes are
111
+ * exactly as reachable as the row that names them. The gate lives here because this is the
112
+ * browser-facing service boundary (`FileStorageService` is `allUsers`) and the driver byte ops
113
+ * below are deliberately unscoped. Server-side doors that make their OWN documented access
114
+ * decision (the avatar route, issue/ticket attachment doors) read bytes through
115
+ * `FileStorage.getDriver()` instead.
108
116
  * @param fileId - The `id` of the file.
109
117
  * @returns The file data as a single string.
118
+ * @throws When the file row does not exist or is not readable by the caller — the same named
119
+ * miss either way, so existence is not leaked to unauthorized callers.
110
120
  */
111
121
  async getFileData(fileId: string): Promise<string> {
122
+ const file = await this.getFile(fileId);
123
+ if (!file) {
124
+ throw new Error(`File not found: ${fileId}`);
125
+ }
126
+
112
127
  return await FileStorage.getDriver().getFileData(fileId);
113
128
  }
114
129
 
@@ -139,10 +154,23 @@ export class FileStorage implements FileStorageService {
139
154
 
140
155
  /**
141
156
  * Updates the data chunks associated with a given file.
157
+ *
158
+ * Gated on the caller's SCOPED row read — owner scope is the write grant on ScopedRecords
159
+ * (the same authorization a scoped row update enforces). Deliberately STRICTER than
160
+ * {@link getFileData}: the {@link FileReachabilityResolver} leg widens READS only, so a
161
+ * share recipient who can read the bytes still cannot write them.
142
162
  * @param fileId - The `id` of the file.
143
163
  * @param data - The new data string to replace the existing data.
164
+ * @throws When the file row does not exist or is not writable by the caller — the same named
165
+ * miss either way, so existence is not leaked to unauthorized callers.
144
166
  */
145
167
  async updateFileData(fileId: string, data: string): Promise<void> {
168
+ const db = getScopedDb();
169
+ const file = await db.get(tables.File, { id: fileId });
170
+ if (!file) {
171
+ throw new Error(`File not found: ${fileId}`);
172
+ }
173
+
146
174
  await FileStorage.getDriver().updateFileData(fileId, data);
147
175
  }
148
176
 
@@ -0,0 +1,143 @@
1
+ import { SourceRepository } from '@proteinjs/reflection';
2
+ import { User } from '@proteinjs/user';
3
+ import { File } from '../src/tables/FileTable';
4
+ import { FileStorage } from '../src/FileStorage';
5
+ import { DbFileStorageDriver } from '../src/DbFileStorageDriver';
6
+ import { FileTestEnvironment } from './FileTestEnvironment';
7
+
8
+ /** Every byte value once — a payload that corrupts under any text/base64 mix-up. */
9
+ const rawBytes = Buffer.from(Array.from({ length: 256 }, (_, i) => i));
10
+ const rawBase64 = rawBytes.toString('base64');
11
+ /** A same-length replacement payload, byte-distinct at every position — the poisoning attempt. */
12
+ const poisonBytes = Buffer.from(Array.from({ length: 256 }, (_, i) => 255 - i));
13
+ const poisonBase64 = poisonBytes.toString('base64');
14
+
15
+ /**
16
+ * The byte-access gates on the browser-facing service (FileStorageService is `allUsers`, so
17
+ * these methods are every logged-in user's to call with any file id they can guess or leak):
18
+ *
19
+ * - **Reads** (`getFileData`): gated on the {@link FileStorage.getFile} row read — the same
20
+ * two-legged access decision every serving path derives from (the caller's SCOPED read, else
21
+ * the FileReachabilityResolver shared-content leg). Bytes are exactly as reachable as the row
22
+ * that names them. Ungated, a foreign file id read another user's bytes outright (the driver
23
+ * byte ops are deliberately unscoped — the access wall lives at this service boundary).
24
+ * - **Writes** (`updateFileData`): gated on the caller's SCOPED row read — owner scope is the
25
+ * write grant on ScopedRecords. Stricter than reads: the resolver leg widens READS only, so a
26
+ * share recipient who can read bytes still cannot write them. Ungated, a foreign file id let
27
+ * any caller poison another user's bytes (with the DbFileStorageDriver: caller-scoped chunk
28
+ * inserts landing beside the owner's — this suite runs that real driver so the intact-bytes
29
+ * assertions prove no chunk ever landed).
30
+ *
31
+ * The resolver is stubbed at the seam (content packages sit above db-file); denial is the named
32
+ * `File not found` — existence is not leaked to unauthorized callers.
33
+ */
34
+
35
+ const testEnv = new FileTestEnvironment();
36
+ type SourceRepositoryInternals = { objectCache: Record<string, unknown[]> };
37
+ const objectCache = () => (SourceRepository.get() as unknown as SourceRepositoryInternals).objectCache;
38
+
39
+ let owner: User;
40
+ let recipient: User;
41
+ let stranger: User;
42
+ /** File ids the stub resolver reports as reachable for the current user. */
43
+ const reachableFileIds = new Set<string>();
44
+ /** How many times the seam was consulted — the owner's scoped leg must never pay for it. */
45
+ let resolverConsultations = 0;
46
+
47
+ beforeAll(async () => {
48
+ await testEnv.beforeAll();
49
+ // The seam getFile consults on a scoped-read miss, stubbed (the real thought-media resolver
50
+ // lives above this package).
51
+ objectCache()['@proteinjs/db-file/FileReachabilityResolver'] = [
52
+ {
53
+ canReadViaReference: async (fileId: string) => {
54
+ resolverConsultations++;
55
+ return reachableFileIds.has(fileId);
56
+ },
57
+ },
58
+ ];
59
+ // The real chunked driver: write-gate bites are proven at the chunk-row level, not a mock.
60
+ testEnv.setDriver(new DbFileStorageDriver());
61
+ owner = await testEnv.createUser({ name: 'File owner', email: 'byte-owner@test.local' });
62
+ recipient = await testEnv.createUser({ name: 'Share recipient', email: 'byte-recipient@test.local' });
63
+ stranger = await testEnv.createUser({ name: 'Stranger', email: 'byte-stranger@test.local' });
64
+ testEnv.actAs(owner);
65
+ });
66
+
67
+ afterAll(async () => {
68
+ delete objectCache()['@proteinjs/db-file/FileReachabilityResolver'];
69
+ await testEnv.afterAll();
70
+ });
71
+
72
+ beforeEach(() => {
73
+ reachableFileIds.clear();
74
+ resolverConsultations = 0;
75
+ testEnv.actAs(owner);
76
+ });
77
+
78
+ const createOwnerFile = async (name: string): Promise<File> =>
79
+ await new FileStorage().createFile(
80
+ { name, type: 'application/octet-stream', size: rawBytes.length } as File,
81
+ rawBase64
82
+ );
83
+
84
+ describe('byte reads gate on the file-row read (the getFile decision)', () => {
85
+ it('the owner reads their bytes through the scoped leg alone — resolvers are never consulted', async () => {
86
+ const file = await createOwnerFile('own-bytes.bin');
87
+
88
+ const fileDataBase64 = await new FileStorage().getFileData(file.id);
89
+
90
+ expect(fileDataBase64).toEqual(rawBase64);
91
+ expect(resolverConsultations).toEqual(0);
92
+ });
93
+
94
+ it("THE VULN (read): a stranger holding only the file id is DENIED another user's bytes", async () => {
95
+ const file = await createOwnerFile('private-bytes.bin');
96
+
97
+ testEnv.actAs(stranger);
98
+ await expect(new FileStorage().getFileData(file.id)).rejects.toThrow(`File not found: ${file.id}`);
99
+ });
100
+
101
+ it('a shared-content recipient (resolver-vouched) reads the bytes byte-identical', async () => {
102
+ const file = await createOwnerFile('shared-bytes.bin');
103
+ reachableFileIds.add(file.id);
104
+
105
+ testEnv.actAs(recipient);
106
+ const fileDataBase64 = await new FileStorage().getFileData(file.id);
107
+
108
+ expect(fileDataBase64).toEqual(rawBase64);
109
+ });
110
+ });
111
+
112
+ describe('byte writes gate on the caller WRITING the row (owner scope; read-reachability never grants it)', () => {
113
+ it("THE VULN (write): a stranger holding only the file id cannot poison another user's bytes", async () => {
114
+ const file = await createOwnerFile('unpoisonable.bin');
115
+
116
+ testEnv.actAs(stranger);
117
+ await expect(new FileStorage().updateFileData(file.id, poisonBase64)).rejects.toThrow(`File not found: ${file.id}`);
118
+
119
+ // The outcome that matters: no chunk landed — the owner's bytes read back untouched.
120
+ testEnv.actAs(owner);
121
+ expect(await new FileStorage().getFileData(file.id)).toEqual(rawBase64);
122
+ });
123
+
124
+ it("read-reachability does NOT grant write: a vouched recipient's byte write is denied, bytes intact", async () => {
125
+ const file = await createOwnerFile('read-only-share.bin');
126
+ reachableFileIds.add(file.id);
127
+
128
+ testEnv.actAs(recipient);
129
+ expect(await new FileStorage().getFileData(file.id)).toEqual(rawBase64);
130
+ await expect(new FileStorage().updateFileData(file.id, poisonBase64)).rejects.toThrow(`File not found: ${file.id}`);
131
+
132
+ testEnv.actAs(owner);
133
+ expect(await new FileStorage().getFileData(file.id)).toEqual(rawBase64);
134
+ });
135
+
136
+ it('the owner replaces their bytes wholesale', async () => {
137
+ const file = await createOwnerFile('rewritable.bin');
138
+
139
+ await new FileStorage().updateFileData(file.id, poisonBase64);
140
+
141
+ expect(await new FileStorage().getFileData(file.id)).toEqual(poisonBase64);
142
+ });
143
+ });