@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 +19 -0
- package/dist/src/FileStorage.d.ts +17 -0
- package/dist/src/FileStorage.d.ts.map +1 -1
- package/dist/src/FileStorage.js +36 -3
- package/dist/src/FileStorage.js.map +1 -1
- package/dist/test/FileByteAccess.test.d.ts +2 -0
- package/dist/test/FileByteAccess.test.d.ts.map +1 -0
- package/dist/test/FileByteAccess.test.js +252 -0
- package/dist/test/FileByteAccess.test.js.map +1 -0
- package/package.json +4 -4
- package/src/FileStorage.ts +28 -0
- package/test/FileByteAccess.test.ts +143 -0
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
|
|
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"}
|
package/dist/src/FileStorage.js
CHANGED
|
@@ -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*/,
|
|
169
|
-
case 1:
|
|
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:
|
|
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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,wCAAiF;AAEjF,0CAAyC;AACzC,oEAA0F;AAE1F,uEAA0E;AAC1E,oDAAmE;AACnE,4CAA2C;AAC3C,6DAA4D;AAE5D;;;KAGK;AACE,IAAM,cAAc,GAAG;IAC5B,OAAA,OAAO,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,IAAI,WAAW,EAAE,CAAC,CAAC,CAAE,IAAA,0CAAqB,GAAkB;AAA1F,CAA0F,CAAC;AADhF,QAAA,cAAc,kBACkE;AAS7F;;;;GAIG;AACH;IAAA;QAGS,oBAAe,GAAG;YACvB,IAAI,EAAE;gBACJ,QAAQ,EAAE,IAAI;aACf;SACF,CAAC;
|
|
1
|
+
{"version":3,"file":"FileStorage.js","sourceRoot":"","sources":["../../src/FileStorage.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,wCAAiF;AAEjF,0CAAyC;AACzC,oEAA0F;AAE1F,uEAA0E;AAC1E,oDAAmE;AACnE,4CAA2C;AAC3C,6DAA4D;AAE5D;;;KAGK;AACE,IAAM,cAAc,GAAG;IAC5B,OAAA,OAAO,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,IAAI,WAAW,EAAE,CAAC,CAAC,CAAE,IAAA,0CAAqB,GAAkB;AAA1F,CAA0F,CAAC;AADhF,QAAA,cAAc,kBACkE;AAS7F;;;;GAIG;AACH;IAAA;QAGS,oBAAe,GAAG;YACvB,IAAI,EAAE;gBACJ,QAAQ,EAAE,IAAI;aACf;SACF,CAAC;IAqKJ,CAAC;IAnKC;;;;;OAKG;IACI,qBAAS,GAAhB;QACE,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE;YACvB,IAAM,oBAAoB,GAAG,6BAAgB,CAAC,GAAG,EAAE,CAAC,MAAM,CACxD,oDAAoD,CACrD,CAAC;YACF,IAAI,oBAAoB,EAAE;gBACxB,WAAW,CAAC,MAAM,GAAG,oBAAoB,CAAC,SAAS,EAAE,CAAC;aACvD;iBAAM;gBACL,IAAI,eAAM,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC,CAAC,IAAI,CAAC;oBACvC,OAAO,EAAE,2EAA2E;iBACrF,CAAC,CAAC;gBACH,WAAW,CAAC,MAAM,GAAG,IAAI,yCAAmB,EAAE,CAAC;aAChD;SACF;QAED,OAAO,WAAW,CAAC,MAAM,CAAC;IAC5B,CAAC;IAED;;;;;OAKG;IACG,gCAAU,GAAhB,UAAiB,YAA4C,EAAE,QAAgB;;;;;;wBACvE,EAAE,GAAG,IAAA,kBAAW,GAAE,CAAC;wBACZ,qBAAM,EAAE,CAAC,MAAM,CAAC,eAAM,CAAC,IAAI,EAAE,YAAY,CAAC,EAAA;;wBAAjD,IAAI,GAAG,SAA0C;wBACvD,qBAAM,WAAW,CAAC,SAAS,EAAE,CAAC,UAAU,CAAC,IAAI,EAAE,QAAQ,CAAC,EAAA;;wBAAxD,SAAwD,CAAC;wBACzD,sBAAO,IAAI,EAAC;;;;KACb;IAED;;;;;;;;;;;OAWG;IACG,6BAAO,GAAb,UAAc,MAAc;;;;;;wBACpB,EAAE,GAAG,IAAA,kBAAW,GAAE,CAAC;wBACZ,qBAAM,EAAE,CAAC,GAAG,CAAC,eAAM,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,EAAA;;wBAAhD,IAAI,GAAG,SAAyC;wBACtD,IAAI,IAAI,EAAE;4BACR,sBAAO,IAAI,EAAC;yBACb;8BAEoD,EAA9B,SAAA,uDAA4B,GAAE;;;6BAA9B,CAAA,cAA8B,CAAA;wBAA1C,QAAQ;wBACb,qBAAM,QAAQ,CAAC,mBAAmB,CAAC,MAAM,CAAC,EAAA;;6BAA1C,SAA0C,EAA1C,wBAA0C;wBAGrC,qBAAM,IAAA,0BAAmB,GAAE,CAAC,GAAG,CAAC,eAAM,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,EAAA;;oBAFnE,sFAAsF;oBACtF,sEAAsE;oBACtE,sBAAO,SAA4D,EAAC;;wBAJjD,IAA8B,CAAA;;4BAQrD,sBAAO,IAAI,EAAC;;;;KACb;IAED;;;;;;;;;;;;;;OAcG;IACG,iCAAW,GAAjB,UAAkB,MAAc;;;;;4BACjB,qBAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAA;;wBAAjC,IAAI,GAAG,SAA0B;wBACvC,IAAI,CAAC,IAAI,EAAE;4BACT,MAAM,IAAI,KAAK,CAAC,0BAAmB,MAAM,CAAE,CAAC,CAAC;yBAC9C;wBAEM,qBAAM,WAAW,CAAC,SAAS,EAAE,CAAC,WAAW,CAAC,MAAM,CAAC,EAAA;4BAAxD,sBAAO,SAAiD,EAAC;;;;KAC1D;IAED;;;;;;;;;;OAUG;IACG,kCAAY,GAAlB,UAAmB,MAAc,EAAE,OAA4B;;;;;4BAChD,qBAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAA;;wBAAjC,IAAI,GAAG,SAA0B;wBACvC,IAAI,CAAC,IAAI,EAAE;4BACT,MAAM,IAAI,KAAK,CAAC,0BAAmB,MAAM,CAAE,CAAC,CAAC;yBAC9C;wBAEK,MAAM,GAAG,WAAW,CAAC,SAAS,EAAE,CAAC;wBACvC,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE;4BACxB,sBAAO,SAAS,EAAC;yBAClB;wBAEM,qBAAM,MAAM,CAAC,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,EAAA;4BAAjD,sBAAO,SAA0C,EAAC;;;;KACnD;IAED;;;;;;;;;;;OAWG;IACG,oCAAc,GAApB,UAAqB,MAAc,EAAE,IAAY;;;;;;wBACzC,EAAE,GAAG,IAAA,kBAAW,GAAE,CAAC;wBACZ,qBAAM,EAAE,CAAC,GAAG,CAAC,eAAM,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,EAAA;;wBAAhD,IAAI,GAAG,SAAyC;wBACtD,IAAI,CAAC,IAAI,EAAE;4BACT,MAAM,IAAI,KAAK,CAAC,0BAAmB,MAAM,CAAE,CAAC,CAAC;yBAC9C;wBAED,qBAAM,WAAW,CAAC,SAAS,EAAE,CAAC,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,EAAA;;wBAA1D,SAA0D,CAAC;;;;;KAC5D;IAED;;;OAGG;IACG,gCAAU,GAAhB,UAAiB,IAAoC;;;;;;wBAC7C,EAAE,GAAG,IAAA,kBAAW,GAAE,CAAC;wBACzB,qBAAM,EAAE,CAAC,MAAM,CAAC,eAAM,CAAC,IAAI,EAAE,IAAI,CAAC,EAAA;;wBAAlC,SAAkC,CAAC;wBAE7B,MAAM,GAAG,WAAW,CAAC,SAAS,EAAE,CAAC;6BACnC,MAAM,CAAC,UAAU,EAAjB,wBAAiB;wBACnB,qBAAM,MAAM,CAAC,UAAU,CAAC,IAAY,CAAC,EAAA;;wBAArC,SAAqC,CAAC;;;;;;KAEzC;IAED;;;;;;OAMG;IACG,gCAAU,GAAhB,UAAiB,MAAc;;;;;;wBACvB,EAAE,GAAG,IAAA,kBAAW,GAAE,CAAC;wBACzB,qBAAM,EAAE,CAAC,MAAM,CAAC,eAAM,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,EAAA;;wBAA5C,SAA4C,CAAC;;;;;KAC9C;IACH,kBAAC;AAAD,CAAC,AA5KD,IA4KC;AA5KY,kCAAW"}
|
|
@@ -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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,oDAAyD;AAGzD,kDAAiD;AACjD,kEAAiE;AACjE,6DAA4D;AAE5D,oFAAoF;AACpF,IAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,UAAC,CAAC,EAAE,CAAC,IAAK,OAAA,CAAC,EAAD,CAAC,CAAC,CAAC,CAAC;AACvE,IAAM,SAAS,GAAG,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AAC9C,kGAAkG;AAClG,IAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,UAAC,CAAC,EAAE,CAAC,IAAK,OAAA,GAAG,GAAG,CAAC,EAAP,CAAO,CAAC,CAAC,CAAC;AAChF,IAAM,YAAY,GAAG,WAAW,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AAEpD;;;;;;;;;;;;;;;;;;GAkBG;AAEH,IAAM,OAAO,GAAG,IAAI,yCAAmB,EAAE,CAAC;AAE1C,IAAM,WAAW,GAAG,cAAM,OAAC,6BAAgB,CAAC,GAAG,EAA2C,CAAC,WAAW,EAA5E,CAA4E,CAAC;AAEvG,IAAI,KAAW,CAAC;AAChB,IAAI,SAAe,CAAC;AACpB,IAAI,QAAc,CAAC;AACnB,4EAA4E;AAC5E,IAAM,gBAAgB,GAAG,IAAI,GAAG,EAAU,CAAC;AAC3C,4FAA4F;AAC5F,IAAI,qBAAqB,GAAG,CAAC,CAAC;AAE9B,SAAS,CAAC;;;oBACR,qBAAM,OAAO,CAAC,SAAS,EAAE,EAAA;;gBAAzB,SAAyB,CAAC;gBAC1B,4FAA4F;gBAC5F,6BAA6B;gBAC7B,WAAW,EAAE,CAAC,6CAA6C,CAAC,GAAG;oBAC7D;wBACE,mBAAmB,EAAE,UAAO,MAAc;;gCACxC,qBAAqB,EAAE,CAAC;gCACxB,sBAAO,gBAAgB,CAAC,GAAG,CAAC,MAAM,CAAC,EAAC;;6BACrC;qBACF;iBACF,CAAC;gBACF,2FAA2F;gBAC3F,OAAO,CAAC,SAAS,CAAC,IAAI,yCAAmB,EAAE,CAAC,CAAC;gBACrC,qBAAM,OAAO,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,uBAAuB,EAAE,CAAC,EAAA;;gBAAxF,KAAK,GAAG,SAAgF,CAAC;gBAC7E,qBAAM,OAAO,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,iBAAiB,EAAE,KAAK,EAAE,2BAA2B,EAAE,CAAC,EAAA;;gBAArG,SAAS,GAAG,SAAyF,CAAC;gBAC3F,qBAAM,OAAO,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,0BAA0B,EAAE,CAAC,EAAA;;gBAA5F,QAAQ,GAAG,SAAiF,CAAC;gBAC7F,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;;;;KACtB,CAAC,CAAC;AAEH,QAAQ,CAAC;;;;gBACP,OAAO,WAAW,EAAE,CAAC,6CAA6C,CAAC,CAAC;gBACpE,qBAAM,OAAO,CAAC,QAAQ,EAAE,EAAA;;gBAAxB,SAAwB,CAAC;;;;KAC1B,CAAC,CAAC;AAEH,UAAU,CAAC;IACT,gBAAgB,CAAC,KAAK,EAAE,CAAC;IACzB,qBAAqB,GAAG,CAAC,CAAC;IAC1B,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC,CAAC,CAAC;AAEH,IAAM,eAAe,GAAG,UAAO,IAAY;;;oBACzC,qBAAM,IAAI,yBAAW,EAAE,CAAC,UAAU,CAChC,EAAE,IAAI,MAAA,EAAE,IAAI,EAAE,0BAA0B,EAAE,IAAI,EAAE,QAAQ,CAAC,MAAM,EAAU,EACzE,SAAS,CACV,EAAA;oBAHD,sBAAA,SAGC,EAAA;;;KAAA,CAAC;AAEJ,QAAQ,CAAC,6DAA6D,EAAE;IACtE,EAAE,CAAC,0FAA0F,EAAE;;;;wBAChF,qBAAM,eAAe,CAAC,eAAe,CAAC,EAAA;;oBAA7C,IAAI,GAAG,SAAsC;oBAE5B,qBAAM,IAAI,yBAAW,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,EAAA;;oBAA7D,cAAc,GAAG,SAA4C;oBAEnE,MAAM,CAAC,cAAc,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;oBAC1C,MAAM,CAAC,qBAAqB,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;;;;SAC1C,CAAC,CAAC;IAEH,EAAE,CAAC,qFAAqF,EAAE;;;;wBAC3E,qBAAM,eAAe,CAAC,mBAAmB,CAAC,EAAA;;oBAAjD,IAAI,GAAG,SAA0C;oBAEvD,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;oBACxB,qBAAM,MAAM,CAAC,IAAI,yBAAW,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,0BAAmB,IAAI,CAAC,EAAE,CAAE,CAAC,EAAA;;oBAAlG,SAAkG,CAAC;;;;SACpG,CAAC,CAAC;IAEH,EAAE,CAAC,8EAA8E,EAAE;;;;wBACpE,qBAAM,eAAe,CAAC,kBAAkB,CAAC,EAAA;;oBAAhD,IAAI,GAAG,SAAyC;oBACtD,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;oBAE9B,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;oBACF,qBAAM,IAAI,yBAAW,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,EAAA;;oBAA7D,cAAc,GAAG,SAA4C;oBAEnE,MAAM,CAAC,cAAc,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;;;;SAC3C,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,iGAAiG,EAAE;IAC1G,EAAE,CAAC,0FAA0F,EAAE;;;;wBAChF,qBAAM,eAAe,CAAC,kBAAkB,CAAC,EAAA;;oBAAhD,IAAI,GAAG,SAAyC;oBAEtD,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;oBACxB,qBAAM,MAAM,CAAC,IAAI,yBAAW,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,EAAE,EAAE,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,0BAAmB,IAAI,CAAC,EAAE,CAAE,CAAC,EAAA;;oBAAnH,SAAmH,CAAC;oBAEpH,qFAAqF;oBACrF,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;oBACrB,KAAA,MAAM,CAAA;oBAAC,qBAAM,IAAI,yBAAW,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,EAAA;;oBAAnD,kBAAO,SAA4C,EAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;;;;SACzE,CAAC,CAAC;IAEH,EAAE,CAAC,kGAAkG,EAAE;;;;wBACxF,qBAAM,eAAe,CAAC,qBAAqB,CAAC,EAAA;;oBAAnD,IAAI,GAAG,SAA4C;oBACzD,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;oBAE9B,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;oBACzB,KAAA,MAAM,CAAA;oBAAC,qBAAM,IAAI,yBAAW,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,EAAA;;oBAAnD,kBAAO,SAA4C,EAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;oBACxE,qBAAM,MAAM,CAAC,IAAI,yBAAW,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,EAAE,EAAE,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,0BAAmB,IAAI,CAAC,EAAE,CAAE,CAAC,EAAA;;oBAAnH,SAAmH,CAAC;oBAEpH,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;oBACrB,KAAA,MAAM,CAAA;oBAAC,qBAAM,IAAI,yBAAW,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,EAAA;;oBAAnD,kBAAO,SAA4C,EAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;;;;SACzE,CAAC,CAAC;IAEH,EAAE,CAAC,0CAA0C,EAAE;;;;wBAChC,qBAAM,eAAe,CAAC,gBAAgB,CAAC,EAAA;;oBAA9C,IAAI,GAAG,SAAuC;oBAEpD,qBAAM,IAAI,yBAAW,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,EAAE,EAAE,YAAY,CAAC,EAAA;;oBAA7D,SAA6D,CAAC;oBAE9D,KAAA,MAAM,CAAA;oBAAC,qBAAM,IAAI,yBAAW,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,EAAA;;oBAAnD,kBAAO,SAA4C,EAAC,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;;;;SAC5E,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proteinjs/db-file",
|
|
3
|
-
"version": "1.7.
|
|
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.
|
|
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.
|
|
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": "
|
|
49
|
+
"gitHead": "cb0fda126d5d2bd5b17d1bc13e87c2373eb61f52"
|
|
50
50
|
}
|
package/src/FileStorage.ts
CHANGED
|
@@ -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
|
+
});
|