@astram/guard 0.13.0 → 0.15.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.
- package/dist/auth/auth.module.d.ts.map +1 -1
- package/dist/auth/auth.module.js +57 -30
- package/dist/auth/connections.d.ts +2 -2
- package/dist/auth/connections.d.ts.map +1 -1
- package/dist/auth/connections.js +4 -13
- package/dist/authentication/mariadb-api-key-store.d.ts +10 -1
- package/dist/authentication/mariadb-api-key-store.d.ts.map +1 -1
- package/dist/authentication/mariadb-api-key-store.js +20 -10
- package/dist/authentication/mariadb-credential-resolver.d.ts +8 -1
- package/dist/authentication/mariadb-credential-resolver.d.ts.map +1 -1
- package/dist/authentication/mariadb-credential-resolver.js +15 -5
- package/dist/authentication/mariadb-credential-writer.d.ts +6 -1
- package/dist/authentication/mariadb-credential-writer.d.ts.map +1 -1
- package/dist/authentication/mariadb-credential-writer.js +9 -3
- package/dist/authentication/mariadb-email-address-changer.d.ts +10 -1
- package/dist/authentication/mariadb-email-address-changer.d.ts.map +1 -1
- package/dist/authentication/mariadb-email-address-changer.js +22 -9
- package/dist/authentication/mariadb-email-change-token-store.d.ts +8 -1
- package/dist/authentication/mariadb-email-change-token-store.d.ts.map +1 -1
- package/dist/authentication/mariadb-email-change-token-store.js +18 -9
- package/dist/authentication/mariadb-mfa-device-store.d.ts +6 -1
- package/dist/authentication/mariadb-mfa-device-store.d.ts.map +1 -1
- package/dist/authentication/mariadb-mfa-device-store.js +22 -13
- package/dist/authentication/mariadb-profile-resolver.d.ts +6 -1
- package/dist/authentication/mariadb-profile-resolver.d.ts.map +1 -1
- package/dist/authentication/mariadb-profile-resolver.js +12 -6
- package/dist/authentication/mariadb-profile-writer.d.ts +6 -1
- package/dist/authentication/mariadb-profile-writer.d.ts.map +1 -1
- package/dist/authentication/mariadb-profile-writer.js +11 -4
- package/dist/authentication/mariadb-recovery-code-store.d.ts +6 -1
- package/dist/authentication/mariadb-recovery-code-store.d.ts.map +1 -1
- package/dist/authentication/mariadb-recovery-code-store.js +11 -5
- package/dist/authentication/mariadb-reset-token-store.d.ts +6 -1
- package/dist/authentication/mariadb-reset-token-store.d.ts.map +1 -1
- package/dist/authentication/mariadb-reset-token-store.js +11 -5
- package/dist/authentication/mariadb-session-sweeper.d.ts +6 -1
- package/dist/authentication/mariadb-session-sweeper.d.ts.map +1 -1
- package/dist/authentication/mariadb-session-sweeper.js +11 -5
- package/dist/authz/cross-tenant-grant-writer.d.ts.map +1 -1
- package/dist/authz/cross-tenant-grant-writer.js +4 -3
- package/dist/authz/list-scope-sql.d.ts +10 -2
- package/dist/authz/list-scope-sql.d.ts.map +1 -1
- package/dist/authz/list-scope-sql.js +4 -4
- package/dist/authz/mariadb-cross-tenant-grant-adapter.d.ts.map +1 -1
- package/dist/authz/mariadb-cross-tenant-grant-adapter.js +4 -3
- package/dist/authz/mariadb-list-scope-adapter.d.ts.map +1 -1
- package/dist/authz/mariadb-list-scope-adapter.js +4 -3
- package/dist/authz/mariadb-policy-store.d.ts.map +1 -1
- package/dist/authz/mariadb-policy-store.js +4 -3
- package/dist/authz/mariadb-record-rule-adapter.d.ts.map +1 -1
- package/dist/authz/mariadb-record-rule-adapter.js +4 -3
- package/dist/authz/mariadb-record-share-adapter.d.ts.map +1 -1
- package/dist/authz/mariadb-record-share-adapter.js +4 -3
- package/dist/authz/record-share-cascade.d.ts.map +1 -1
- package/dist/authz/record-share-cascade.js +4 -3
- package/dist/authz/record-share-writer.d.ts.map +1 -1
- package/dist/authz/record-share-writer.js +4 -3
- package/dist/authz/wire-record-authorization.d.ts +8 -0
- package/dist/authz/wire-record-authorization.d.ts.map +1 -1
- package/dist/authz/wire-record-authorization.js +14 -6
- package/dist/boot/boot-sequence.d.ts +4 -0
- package/dist/boot/boot-sequence.d.ts.map +1 -1
- package/dist/boot/boot-sequence.js +13 -1
- package/dist/boot/migration-ledger-check.d.ts +94 -0
- package/dist/boot/migration-ledger-check.d.ts.map +1 -0
- package/dist/boot/migration-ledger-check.js +280 -0
- package/dist/config/app-config.d.ts +72 -3
- package/dist/config/app-config.d.ts.map +1 -1
- package/dist/config/app-config.js +129 -7
- package/dist/config/config.module.d.ts +7 -0
- package/dist/config/config.module.d.ts.map +1 -1
- package/dist/config/config.module.js +13 -2
- package/dist/config/framework-schema-names.d.ts +29 -0
- package/dist/config/framework-schema-names.d.ts.map +1 -0
- package/dist/config/framework-schema-names.js +41 -0
- package/dist/config/loopback-host.d.ts +9 -0
- package/dist/config/loopback-host.d.ts.map +1 -0
- package/dist/config/loopback-host.js +19 -0
- package/dist/encryption/local-kek-crypto.d.ts +6 -0
- package/dist/encryption/local-kek-crypto.d.ts.map +1 -1
- package/dist/encryption/local-kek-crypto.js +8 -0
- package/dist/identity/identity-actions.d.ts +18 -7
- package/dist/identity/identity-actions.d.ts.map +1 -1
- package/dist/identity/identity-actions.js +16 -17
- package/dist/identity/module-registry.bridge.d.ts.map +1 -1
- package/dist/identity/module-registry.bridge.js +5 -3
- package/dist/identity/permission-facts.adapter.d.ts +7 -1
- package/dist/identity/permission-facts.adapter.d.ts.map +1 -1
- package/dist/identity/permission-facts.adapter.js +15 -6
- package/dist/index.d.ts +14 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +68 -22
- package/dist/module/shield-tokens.d.ts +11 -0
- package/dist/module/shield-tokens.d.ts.map +1 -1
- package/dist/module/shield-tokens.js +12 -1
- package/dist/module/shield.module.d.ts +10 -0
- package/dist/module/shield.module.d.ts.map +1 -1
- package/dist/module/shield.module.js +78 -12
- package/dist/secrets/boot-secrets.d.ts +33 -0
- package/dist/secrets/boot-secrets.d.ts.map +1 -0
- package/dist/secrets/boot-secrets.js +180 -0
- package/dist/session/mariadb-session-record-store.d.ts +8 -1
- package/dist/session/mariadb-session-record-store.d.ts.map +1 -1
- package/dist/session/mariadb-session-record-store.js +23 -13
- package/dist/session/session-restriction-store.d.ts +8 -1
- package/dist/session/session-restriction-store.d.ts.map +1 -1
- package/dist/session/session-restriction-store.js +15 -5
- package/dist/storage/file-intake.interceptor.d.ts +54 -0
- package/dist/storage/file-intake.interceptor.d.ts.map +1 -0
- package/dist/storage/file-intake.interceptor.js +223 -0
- package/dist/storage/node-sha256-digest.d.ts +6 -0
- package/dist/storage/node-sha256-digest.d.ts.map +1 -0
- package/dist/storage/node-sha256-digest.js +15 -0
- package/dist/storage/object-store-boot-check.d.ts +16 -0
- package/dist/storage/object-store-boot-check.d.ts.map +1 -0
- package/dist/storage/object-store-boot-check.js +52 -0
- package/dist/storage/optional-peer.d.ts +10 -0
- package/dist/storage/optional-peer.d.ts.map +1 -0
- package/dist/storage/optional-peer.js +26 -0
- package/dist/storage/s3-object-store.d.ts +35 -0
- package/dist/storage/s3-object-store.d.ts.map +1 -0
- package/dist/storage/s3-object-store.js +238 -0
- package/dist/storage/storage.providers.d.ts +10 -0
- package/dist/storage/storage.providers.d.ts.map +1 -0
- package/dist/storage/storage.providers.js +104 -0
- package/migrations/1710000000006-CreateAuthzSchema.cjs +15 -4
- package/migrations/1710000000007-ExtendPermissionAndRolePermission.cjs +11 -3
- package/migrations/1710000000008-CreateAccessBundleTables.cjs +14 -4
- package/migrations/1710000000009-CreateRecordPermissionRule.cjs +15 -4
- package/migrations/1710000000014-CreateRecordShare.cjs +15 -4
- package/migrations/1710000000015-CreateScopeClosure.cjs +14 -4
- package/migrations/1710000000016-AlterCompanyParentFkRestrict.cjs +20 -9
- package/migrations/1710000000017-CreateServiceAccountKey.cjs +14 -4
- package/package.json +11 -3
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.FILE_INTAKE_DEFAULT_MAX_BYTES = exports.FILE_INTAKE_REQUEST_KEY = void 0;
|
|
16
|
+
exports.FileIntake = FileIntake;
|
|
17
|
+
/**
|
|
18
|
+
* `FileIntake(options)` (FILE_STORAGE SPEC §5 G, §13): one multipart file, checked, then handed
|
|
19
|
+
* to the route handler.
|
|
20
|
+
*
|
|
21
|
+
* @Classified({ typeKey: 'stored_file', action: 'create' })
|
|
22
|
+
* @UseInterceptors(FileIntake({ field: 'file', profile: 'xlsx' }))
|
|
23
|
+
*
|
|
24
|
+
* - Interceptors run after guards, so the global guard has decided (and a denied caller is
|
|
25
|
+
* refused) before any multipart byte is read (Shield #1, #3). A request with no principal is
|
|
26
|
+
* refused here too, before parsing.
|
|
27
|
+
* - Parsing wraps `@nestjs/platform-express` `FileInterceptor` with multer's memory storage
|
|
28
|
+
* (no module-level `MulterModule` options apply: no disk storage, no consumer file filter) and
|
|
29
|
+
* limits `{ files: 1, fileSize: maxBytes + 1, fields: 0, parts: 2 }` and `defParamCharset:
|
|
30
|
+
* 'utf8'`. `maxBytes` defaults to the bound profile's configured cap (`OBJECT_STORAGE_MAX_BYTES`
|
|
31
|
+
* under `storage: 'env'`); an explicit smaller `maxBytes` wins; never above 50 MiB.
|
|
32
|
+
* - Then `SHIELD_FILE_INGEST` checks the bytes (size, extension, declared type, magic) with the
|
|
33
|
+
* tenant from the session (`req.principal.tenantId`, never the request body).
|
|
34
|
+
* - Errors are mapped here, at the source: 400 / 413 / 415 `HttpException`s carrying a fixed
|
|
35
|
+
* reason code. The file name, field name and bytes are never echoed (Shield #10).
|
|
36
|
+
* - On success the request carries `req[FILE_INTAKE_REQUEST_KEY] = { file, bytes }` for
|
|
37
|
+
* `FilesService.store`; multer's raw `req.file` (unsanitised name) is removed.
|
|
38
|
+
*/
|
|
39
|
+
const common_1 = require("@nestjs/common");
|
|
40
|
+
const rxjs_1 = require("rxjs");
|
|
41
|
+
const core_1 = require("@astram/core");
|
|
42
|
+
const shield_tokens_js_1 = require("../module/shield-tokens.js");
|
|
43
|
+
const optional_peer_js_1 = require("./optional-peer.js");
|
|
44
|
+
/** The request property {@link FileIntake} sets to a {@link FileIntakeResult}. */
|
|
45
|
+
exports.FILE_INTAKE_REQUEST_KEY = 'shieldFileIntake';
|
|
46
|
+
/** Fallback multipart cap (10 MiB, SPEC §10) when the bound ingest cannot report a profile cap. */
|
|
47
|
+
exports.FILE_INTAKE_DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
|
|
48
|
+
const STATUS_BY_REASON = {
|
|
49
|
+
empty: 400,
|
|
50
|
+
too_large: 413,
|
|
51
|
+
type_not_allowed: 415,
|
|
52
|
+
content_mismatch: 415,
|
|
53
|
+
too_many_files: 400,
|
|
54
|
+
missing_file: 400,
|
|
55
|
+
bad_request: 400,
|
|
56
|
+
};
|
|
57
|
+
/** The core reasons `createFileIngest` may throw (`bad_request` is intake-only). */
|
|
58
|
+
const CORE_REASONS = new Set([
|
|
59
|
+
'empty',
|
|
60
|
+
'too_large',
|
|
61
|
+
'type_not_allowed',
|
|
62
|
+
'content_mismatch',
|
|
63
|
+
'too_many_files',
|
|
64
|
+
'missing_file',
|
|
65
|
+
]);
|
|
66
|
+
/**
|
|
67
|
+
* Nest's messages for the multer limits that mean "more than one file, or extra fields" (SPEC
|
|
68
|
+
* §13 -> `too_many_files`): `LIMIT_FILE_COUNT`, `LIMIT_FIELD_COUNT`, `LIMIT_PART_COUNT` and
|
|
69
|
+
* `LIMIT_UNEXPECTED_FILE` (a file part under another field, or a second file in this one).
|
|
70
|
+
* Nest may append ` - <field>`, so the message is matched by prefix and never passed on.
|
|
71
|
+
*/
|
|
72
|
+
const TOO_MANY_PREFIXES = [
|
|
73
|
+
'Too many files',
|
|
74
|
+
'Too many fields',
|
|
75
|
+
'Too many parts',
|
|
76
|
+
'Unexpected file',
|
|
77
|
+
];
|
|
78
|
+
/** The fixed-body exception for a refused file (status per SPEC §13). */
|
|
79
|
+
function fileRejection(reason) {
|
|
80
|
+
const status = STATUS_BY_REASON[reason];
|
|
81
|
+
const body = { statusCode: status, message: `file rejected: ${reason}`, reason };
|
|
82
|
+
if (status === 413)
|
|
83
|
+
return new common_1.PayloadTooLargeException(body);
|
|
84
|
+
if (status === 415)
|
|
85
|
+
return new common_1.UnsupportedMediaTypeException(body);
|
|
86
|
+
return new common_1.BadRequestException(body);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Map a parser failure to a fixed reason, by status first. `FileInterceptor` has already turned
|
|
90
|
+
* multer/busboy errors into `PayloadTooLargeException` (`LIMIT_FILE_SIZE`) or
|
|
91
|
+
* `BadRequestException`:
|
|
92
|
+
* - 413 -> `too_large`;
|
|
93
|
+
* - 400 whose message is one of the count / unexpected-file limits -> `too_many_files`;
|
|
94
|
+
* - any other 4xx/5xx `HttpException` (malformed multipart, missing boundary, unexpected end of
|
|
95
|
+
* form, field name too long, ...) -> 400 `bad_request`: the request is broken, not missing a
|
|
96
|
+
* file.
|
|
97
|
+
* The parser's message may name the field, so it is only matched, never passed on.
|
|
98
|
+
*/
|
|
99
|
+
function parserRejection(err) {
|
|
100
|
+
if (!(err instanceof common_1.HttpException))
|
|
101
|
+
return err;
|
|
102
|
+
const status = err.getStatus();
|
|
103
|
+
if (status === 413)
|
|
104
|
+
return fileRejection('too_large');
|
|
105
|
+
if (status === 400) {
|
|
106
|
+
const res = err.getResponse();
|
|
107
|
+
const message = typeof res === 'string'
|
|
108
|
+
? res
|
|
109
|
+
: typeof res?.message === 'string'
|
|
110
|
+
? res.message
|
|
111
|
+
: '';
|
|
112
|
+
if (TOO_MANY_PREFIXES.some((p) => message.startsWith(p))) {
|
|
113
|
+
return fileRejection('too_many_files');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return fileRejection('bad_request');
|
|
117
|
+
}
|
|
118
|
+
function isFileRejection(e) {
|
|
119
|
+
if (e instanceof core_1.FileRejectedError)
|
|
120
|
+
return true;
|
|
121
|
+
// Duck-typed too: a dual-package (ESM/CJS) copy of core is a different class.
|
|
122
|
+
return (typeof e === 'object' &&
|
|
123
|
+
e !== null &&
|
|
124
|
+
e.name === 'FileRejectedError' &&
|
|
125
|
+
CORE_REASONS.has(String(e.reason)));
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The multipart cap for one route: the bound profile's configured cap (`OBJECT_STORAGE_MAX_BYTES`
|
|
129
|
+
* under `storage: 'env'`), lowered by an explicit `maxBytes`, never above the 50 MiB ceiling. An
|
|
130
|
+
* ingest that cannot report caps (a hand-written `FileIngest`) falls back to the explicit value or
|
|
131
|
+
* {@link FILE_INTAKE_DEFAULT_MAX_BYTES}. A profile the bound ingest does not know aborts boot.
|
|
132
|
+
*/
|
|
133
|
+
function routeMaxBytes(ingest, profile, explicit) {
|
|
134
|
+
if (typeof ingest?.maxBytesFor !== 'function') {
|
|
135
|
+
return Math.min(explicit ?? exports.FILE_INTAKE_DEFAULT_MAX_BYTES, core_1.FILE_MAX_BYTES_CEILING);
|
|
136
|
+
}
|
|
137
|
+
const profileCap = ingest.maxBytesFor(profile);
|
|
138
|
+
if (typeof profileCap !== 'number' ||
|
|
139
|
+
!Number.isInteger(profileCap) ||
|
|
140
|
+
profileCap <= 0 ||
|
|
141
|
+
profileCap > core_1.FILE_MAX_BYTES_CEILING) {
|
|
142
|
+
throw new Error(`FileIntake: profile '${profile}' is not registered on SHIELD_FILE_INGEST`);
|
|
143
|
+
}
|
|
144
|
+
return Math.min(explicit ?? profileCap, profileCap);
|
|
145
|
+
}
|
|
146
|
+
/** Build the intake interceptor for one route (see the file header). Throws at boot on bad options. */
|
|
147
|
+
function FileIntake(options) {
|
|
148
|
+
const { field, profile } = options;
|
|
149
|
+
if (typeof field !== 'string' || field.trim() === '') {
|
|
150
|
+
throw new Error('FileIntake: `field` must be a non-empty string');
|
|
151
|
+
}
|
|
152
|
+
if (typeof profile !== 'string' || profile.trim() === '') {
|
|
153
|
+
throw new Error('FileIntake: `profile` must be a non-empty string');
|
|
154
|
+
}
|
|
155
|
+
const explicitMax = options.maxBytes;
|
|
156
|
+
if (explicitMax !== undefined &&
|
|
157
|
+
(!Number.isInteger(explicitMax) || explicitMax <= 0 || explicitMax > core_1.FILE_MAX_BYTES_CEILING)) {
|
|
158
|
+
throw new Error(`FileIntake: maxBytes must be a positive integer of at most ${core_1.FILE_MAX_BYTES_CEILING}`);
|
|
159
|
+
}
|
|
160
|
+
// Loaded here, not at module top, so `require('@astram/guard')` does not need the optional
|
|
161
|
+
// peer `@nestjs/platform-express`; a missing package is a clear error at controller load.
|
|
162
|
+
const { FileInterceptor } = (0, optional_peer_js_1.requireOptionalPeer)('@nestjs/platform-express', 'FileIntake');
|
|
163
|
+
let FileIntakeInterceptor = class FileIntakeInterceptor {
|
|
164
|
+
ingest;
|
|
165
|
+
multipart;
|
|
166
|
+
constructor(ingest) {
|
|
167
|
+
this.ingest = ingest;
|
|
168
|
+
const maxBytes = routeMaxBytes(ingest, profile, explicitMax);
|
|
169
|
+
// No `storage`/`dest`: multer keeps the file in memory. Instantiated with empty module
|
|
170
|
+
// options, so an app-level MulterModule configuration cannot switch this route to disk.
|
|
171
|
+
// `defParamCharset: 'utf8'`: a raw UTF-8 `filename="..."` is decoded as UTF-8 (busboy's
|
|
172
|
+
// default is latin1), so it sanitises to the same name as the RFC 5987 `filename*` form.
|
|
173
|
+
const Multipart = FileInterceptor(field, {
|
|
174
|
+
limits: { files: 1, fileSize: maxBytes + 1, fields: 0, parts: 2 },
|
|
175
|
+
defParamCharset: 'utf8',
|
|
176
|
+
});
|
|
177
|
+
this.multipart = new Multipart({});
|
|
178
|
+
}
|
|
179
|
+
async intercept(context, next) {
|
|
180
|
+
const req = context.switchToHttp().getRequest();
|
|
181
|
+
const tenantId = req.principal?.tenantId;
|
|
182
|
+
if (typeof tenantId !== 'string' || tenantId === '') {
|
|
183
|
+
// No session principal (e.g. a misclassified public route): refuse before parsing.
|
|
184
|
+
throw new common_1.ForbiddenException();
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
// Resolves once multer has parsed the body; the inner `next` is a no-op.
|
|
188
|
+
await this.multipart.intercept(context, { handle: () => rxjs_1.EMPTY });
|
|
189
|
+
}
|
|
190
|
+
catch (e) {
|
|
191
|
+
throw parserRejection(e);
|
|
192
|
+
}
|
|
193
|
+
const part = req.file;
|
|
194
|
+
delete req.file;
|
|
195
|
+
if (!part || !Buffer.isBuffer(part.buffer))
|
|
196
|
+
throw fileRejection('missing_file');
|
|
197
|
+
const bytes = new Uint8Array(part.buffer.buffer, part.buffer.byteOffset, part.buffer.length);
|
|
198
|
+
let file;
|
|
199
|
+
try {
|
|
200
|
+
file = await this.ingest.ingest({
|
|
201
|
+
tenantId,
|
|
202
|
+
profile,
|
|
203
|
+
bytes,
|
|
204
|
+
declaredContentType: part.mimetype ?? '',
|
|
205
|
+
originalName: part.originalname ?? '',
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
catch (e) {
|
|
209
|
+
if (isFileRejection(e))
|
|
210
|
+
throw fileRejection(e.reason);
|
|
211
|
+
throw e;
|
|
212
|
+
}
|
|
213
|
+
req[exports.FILE_INTAKE_REQUEST_KEY] = { file, bytes };
|
|
214
|
+
return next.handle();
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
FileIntakeInterceptor = __decorate([
|
|
218
|
+
(0, common_1.Injectable)(),
|
|
219
|
+
__param(0, (0, common_1.Inject)(shield_tokens_js_1.SHIELD_FILE_INGEST)),
|
|
220
|
+
__metadata("design:paramtypes", [Object])
|
|
221
|
+
], FileIntakeInterceptor);
|
|
222
|
+
return (0, common_1.mixin)(FileIntakeInterceptor);
|
|
223
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-sha256-digest.d.ts","sourceRoot":"","sources":["../../src/storage/node-sha256-digest.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,qBAAa,gBAAiB,YAAW,UAAU;IACjD,8CAA8C;IACxC,MAAM,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC;CAGjD"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NodeSha256Digest = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* `ByteDigest` adapter over `node:crypto` (FILE_STORAGE SPEC §5 B, E). Core is zero-dependency,
|
|
6
|
+
* so the hash is injected into `createFileIngest`.
|
|
7
|
+
*/
|
|
8
|
+
const node_crypto_1 = require("node:crypto");
|
|
9
|
+
class NodeSha256Digest {
|
|
10
|
+
/** Lower-case hex sha256 of the raw bytes. */
|
|
11
|
+
async sha256(bytes) {
|
|
12
|
+
return (0, node_crypto_1.createHash)('sha256').update(bytes).digest('hex');
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
exports.NodeSha256Digest = NodeSha256Digest;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boot check for the bound `ObjectStore` (FILE_STORAGE SPEC §5 H, §12). Registered by
|
|
3
|
+
* `ShieldModule` only when a `storage` option is given; pings once at bootstrap and aborts the
|
|
4
|
+
* boot (Shield #1) when the bucket is unreachable, not writable, or refuses SSE.
|
|
5
|
+
*
|
|
6
|
+
* The message names the bucket (`OBJECT_STORAGE_BUCKET`) and the variables to check, never a
|
|
7
|
+
* credential, the endpoint or the underlying error text (Shield #10).
|
|
8
|
+
*/
|
|
9
|
+
import { type OnApplicationBootstrap } from '@nestjs/common';
|
|
10
|
+
import type { ObjectStore } from '@astram/core';
|
|
11
|
+
export declare class ObjectStoreBootCheck implements OnApplicationBootstrap {
|
|
12
|
+
private readonly store;
|
|
13
|
+
constructor(store: ObjectStore | null);
|
|
14
|
+
onApplicationBootstrap(): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=object-store-boot-check.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"object-store-boot-check.d.ts","sourceRoot":"","sources":["../../src/storage/object-store-boot-check.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAgC,KAAK,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAC3F,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAGhD,qBACa,oBAAqB,YAAW,sBAAsB;IAEtB,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,WAAW,GAAG,IAAI;IAG/E,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;CAc9C"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.ObjectStoreBootCheck = void 0;
|
|
16
|
+
/**
|
|
17
|
+
* Boot check for the bound `ObjectStore` (FILE_STORAGE SPEC §5 H, §12). Registered by
|
|
18
|
+
* `ShieldModule` only when a `storage` option is given; pings once at bootstrap and aborts the
|
|
19
|
+
* boot (Shield #1) when the bucket is unreachable, not writable, or refuses SSE.
|
|
20
|
+
*
|
|
21
|
+
* The message names the bucket (`OBJECT_STORAGE_BUCKET`) and the variables to check, never a
|
|
22
|
+
* credential, the endpoint or the underlying error text (Shield #10).
|
|
23
|
+
*/
|
|
24
|
+
const common_1 = require("@nestjs/common");
|
|
25
|
+
const shield_tokens_js_1 = require("../module/shield-tokens.js");
|
|
26
|
+
let ObjectStoreBootCheck = class ObjectStoreBootCheck {
|
|
27
|
+
store;
|
|
28
|
+
constructor(store) {
|
|
29
|
+
this.store = store;
|
|
30
|
+
}
|
|
31
|
+
async onApplicationBootstrap() {
|
|
32
|
+
if (!this.store)
|
|
33
|
+
return;
|
|
34
|
+
try {
|
|
35
|
+
await this.store.ping();
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
const bucket = process.env.OBJECT_STORAGE_BUCKET?.trim();
|
|
39
|
+
const named = bucket ? `bucket '${bucket}' (OBJECT_STORAGE_BUCKET)` : 'the configured bucket';
|
|
40
|
+
throw new Error(`Object storage boot check failed: ${named} is not reachable, not writable or refused ` +
|
|
41
|
+
'server-side encryption with the configured key. Check OBJECT_STORAGE_ENDPOINT, ' +
|
|
42
|
+
'OBJECT_STORAGE_ACCESS_KEY_ID, OBJECT_STORAGE_SECRET_ACCESS_KEY and the bucket policy.');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
exports.ObjectStoreBootCheck = ObjectStoreBootCheck;
|
|
47
|
+
exports.ObjectStoreBootCheck = ObjectStoreBootCheck = __decorate([
|
|
48
|
+
(0, common_1.Injectable)(),
|
|
49
|
+
__param(0, (0, common_1.Optional)()),
|
|
50
|
+
__param(0, (0, common_1.Inject)(shield_tokens_js_1.SHIELD_OBJECT_STORE)),
|
|
51
|
+
__metadata("design:paramtypes", [Object])
|
|
52
|
+
], ObjectStoreBootCheck);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synchronous load of an OPTIONAL peer dependency at the point a feature is used, so that
|
|
3
|
+
* `require('@astram/guard')` itself never loads it (review P1-2: `@nestjs/platform-express` is
|
|
4
|
+
* only needed by apps that use `FileIntake`). Guard is CommonJS, so `require` is the loader.
|
|
5
|
+
*
|
|
6
|
+
* A missing package becomes one clear error naming the package and the feature; any other load
|
|
7
|
+
* failure (the package is installed but throws) is rethrown unchanged.
|
|
8
|
+
*/
|
|
9
|
+
export declare function requireOptionalPeer<T>(name: string, feature: string, load?: (id: string) => unknown): T;
|
|
10
|
+
//# sourceMappingURL=optional-peer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"optional-peer.d.ts","sourceRoot":"","sources":["../../src/storage/optional-peer.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EACnC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,IAAI,GAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAiB,GACtC,CAAC,CAeH"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.requireOptionalPeer = requireOptionalPeer;
|
|
4
|
+
/**
|
|
5
|
+
* Synchronous load of an OPTIONAL peer dependency at the point a feature is used, so that
|
|
6
|
+
* `require('@astram/guard')` itself never loads it (review P1-2: `@nestjs/platform-express` is
|
|
7
|
+
* only needed by apps that use `FileIntake`). Guard is CommonJS, so `require` is the loader.
|
|
8
|
+
*
|
|
9
|
+
* A missing package becomes one clear error naming the package and the feature; any other load
|
|
10
|
+
* failure (the package is installed but throws) is rethrown unchanged.
|
|
11
|
+
*/
|
|
12
|
+
function requireOptionalPeer(name, feature, load = require) {
|
|
13
|
+
try {
|
|
14
|
+
return load(name);
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
const err = e;
|
|
18
|
+
const missing = err?.code === 'MODULE_NOT_FOUND' &&
|
|
19
|
+
typeof err.message === 'string' &&
|
|
20
|
+
err.message.includes(`'${name}'`);
|
|
21
|
+
if (!missing)
|
|
22
|
+
throw e;
|
|
23
|
+
throw new Error(`${feature} requires the optional peer dependency ${name}; install it in the app ` +
|
|
24
|
+
`(e.g. \`pnpm add ${name}\`)`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type ObjectPutMeta, type ObjectStore, type StoredObject, type StoredObjectHead } from '@astram/core';
|
|
2
|
+
import type { ObjectStorageConfig } from '../config/app-config.js';
|
|
3
|
+
/**
|
|
4
|
+
* The connection fields `S3ObjectStore` needs (the {@link ObjectStorageConfig} result).
|
|
5
|
+
* `maxBytes` caps what `get` will buffer (default and maximum: `FILE_MAX_BYTES_CEILING`).
|
|
6
|
+
*/
|
|
7
|
+
export type S3ObjectStoreConfig = Pick<ObjectStorageConfig, 'endpoint' | 'bucket' | 'region' | 'accessKeyId' | 'secretAccessKey' | 'sse'> & {
|
|
8
|
+
readonly maxBytes?: number;
|
|
9
|
+
};
|
|
10
|
+
export interface S3ObjectStoreOptions {
|
|
11
|
+
/**
|
|
12
|
+
* How `put` refuses to overwrite. `'if-none-match'` (default): a conditional PUT
|
|
13
|
+
* (`If-None-Match: *`), atomic on the server. `'head-first'`: for a backend that does not
|
|
14
|
+
* support conditional PUT (SPEC §5 E assumption) — `head` first, refuse when the key exists,
|
|
15
|
+
* then a plain PUT; not atomic, so it relies on the server-built uuid key for uniqueness.
|
|
16
|
+
*/
|
|
17
|
+
readonly noOverwrite?: 'if-none-match' | 'head-first';
|
|
18
|
+
}
|
|
19
|
+
export declare class S3ObjectStore implements ObjectStore {
|
|
20
|
+
private readonly config;
|
|
21
|
+
private readonly bucket;
|
|
22
|
+
private readonly sse;
|
|
23
|
+
private readonly noOverwrite;
|
|
24
|
+
private readonly maxGetBytes;
|
|
25
|
+
private connection;
|
|
26
|
+
constructor(config: S3ObjectStoreConfig, options?: S3ObjectStoreOptions);
|
|
27
|
+
/** The SDK and the client, built on first use. */
|
|
28
|
+
private connect;
|
|
29
|
+
put(key: string, bytes: Uint8Array, meta: ObjectPutMeta): Promise<void>;
|
|
30
|
+
get(key: string): Promise<StoredObject>;
|
|
31
|
+
head(key: string): Promise<StoredObjectHead | null>;
|
|
32
|
+
delete(key: string): Promise<void>;
|
|
33
|
+
ping(): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=s3-object-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"s3-object-store.d.ts","sourceRoot":"","sources":["../../src/storage/s3-object-store.ts"],"names":[],"mappings":"AAoBA,OAAO,EAIL,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACtB,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAiBnE;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GAAG,IAAI,CACpC,mBAAmB,EACnB,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,aAAa,GAAG,iBAAiB,GAAG,KAAK,CAC7E,GAAG;IAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnC,MAAM,WAAW,oBAAoB;IACnC;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,eAAe,GAAG,YAAY,CAAC;CACvD;AAWD,qBAAa,aAAc,YAAW,WAAW;IAC/C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAsB;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAW;IAC/B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAiC;IAC7D,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,UAAU,CAAkC;gBAExC,MAAM,EAAE,mBAAmB,EAAE,OAAO,GAAE,oBAAyB;IAc3E,kDAAkD;IAClD,OAAO,CAAC,OAAO;IAmBT,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAyBvE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IA2BvC,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC;IAcnD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUlC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAmB5B"}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.S3ObjectStore = void 0;
|
|
37
|
+
/**
|
|
38
|
+
* `ObjectStore` over the S3 API (FILE_STORAGE SPEC §5 E) for MinIO and S3-compatible hosts.
|
|
39
|
+
*
|
|
40
|
+
* - Path-style addressing (`forcePathStyle`), region and credentials from {@link ObjectStorageConfig}.
|
|
41
|
+
* - `put`: SSE-S3 (`ServerSideEncryption: 'AES256'`) on every write, no overwrite
|
|
42
|
+
* (`IfNoneMatch: '*'`), content type, and the sha256 both as `ChecksumSHA256` (the server
|
|
43
|
+
* verifies the bytes) and as `x-amz-meta-sha256`.
|
|
44
|
+
* - `get`/`head`/`delete`: a missing key maps to `ObjectNotFoundError` / `null` / no-op. `get`
|
|
45
|
+
* refuses an object whose `ContentLength` is missing or above the configured cap (at most the
|
|
46
|
+
* 50 MiB `FILE_MAX_BYTES_CEILING`) before reading any body byte.
|
|
47
|
+
* - Every other SDK failure becomes `ObjectStoreUnavailableError` with its fixed message and NO
|
|
48
|
+
* `cause`: SDK errors can carry the endpoint, request id or credentials (Shield #10).
|
|
49
|
+
* - `ping`: `HeadBucket`, then put/get/delete of one probe object under `_probe/`, so a missing
|
|
50
|
+
* write permission or a refused SSE header fails the boot check.
|
|
51
|
+
*
|
|
52
|
+
* `@aws-sdk/client-s3` is imported here as TYPES only. The runtime module is loaded on the first
|
|
53
|
+
* operation (one cached load per process), so `require('@astram/guard')` never loads the SDK.
|
|
54
|
+
*/
|
|
55
|
+
const node_crypto_1 = require("node:crypto");
|
|
56
|
+
const core_1 = require("@astram/core");
|
|
57
|
+
let sdkLoad;
|
|
58
|
+
/** Load `@aws-sdk/client-s3` once, on first use (a failed load is retried next time). */
|
|
59
|
+
function loadSdk() {
|
|
60
|
+
if (!sdkLoad) {
|
|
61
|
+
sdkLoad = Promise.resolve().then(() => __importStar(require('@aws-sdk/client-s3'))).catch((e) => {
|
|
62
|
+
sdkLoad = undefined;
|
|
63
|
+
throw e;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return sdkLoad;
|
|
67
|
+
}
|
|
68
|
+
const SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
69
|
+
const PROBE_BYTES = new TextEncoder().encode('shield object store probe');
|
|
70
|
+
const PROBE_CONTENT_TYPE = 'application/octet-stream';
|
|
71
|
+
class S3ObjectStore {
|
|
72
|
+
config;
|
|
73
|
+
bucket;
|
|
74
|
+
sse;
|
|
75
|
+
noOverwrite;
|
|
76
|
+
maxGetBytes;
|
|
77
|
+
connection;
|
|
78
|
+
constructor(config, options = {}) {
|
|
79
|
+
const maxBytes = config.maxBytes ?? core_1.FILE_MAX_BYTES_CEILING;
|
|
80
|
+
if (!Number.isInteger(maxBytes) || maxBytes <= 0 || maxBytes > core_1.FILE_MAX_BYTES_CEILING) {
|
|
81
|
+
throw new Error(`S3ObjectStore: maxBytes must be a positive integer of at most ${core_1.FILE_MAX_BYTES_CEILING}`);
|
|
82
|
+
}
|
|
83
|
+
this.config = config;
|
|
84
|
+
this.bucket = config.bucket;
|
|
85
|
+
this.sse = config.sse;
|
|
86
|
+
this.noOverwrite = options.noOverwrite ?? 'if-none-match';
|
|
87
|
+
this.maxGetBytes = maxBytes;
|
|
88
|
+
}
|
|
89
|
+
/** The SDK and the client, built on first use. */
|
|
90
|
+
connect() {
|
|
91
|
+
if (!this.connection) {
|
|
92
|
+
const { endpoint, region, accessKeyId, secretAccessKey } = this.config;
|
|
93
|
+
this.connection = loadSdk().then((sdk) => ({
|
|
94
|
+
sdk,
|
|
95
|
+
client: new sdk.S3Client({
|
|
96
|
+
endpoint,
|
|
97
|
+
region,
|
|
98
|
+
forcePathStyle: true,
|
|
99
|
+
credentials: { accessKeyId, secretAccessKey },
|
|
100
|
+
}),
|
|
101
|
+
}));
|
|
102
|
+
this.connection.catch(() => {
|
|
103
|
+
this.connection = undefined;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return this.connection;
|
|
107
|
+
}
|
|
108
|
+
async put(key, bytes, meta) {
|
|
109
|
+
if (!SHA256_HEX.test(meta.sha256))
|
|
110
|
+
throw new core_1.ObjectStoreUnavailableError();
|
|
111
|
+
if (this.noOverwrite === 'head-first' && (await this.head(key)) !== null) {
|
|
112
|
+
throw new core_1.ObjectStoreUnavailableError();
|
|
113
|
+
}
|
|
114
|
+
const { sdk, client } = await this.connect();
|
|
115
|
+
const input = {
|
|
116
|
+
Bucket: this.bucket,
|
|
117
|
+
Key: key,
|
|
118
|
+
Body: bytes,
|
|
119
|
+
ContentType: meta.contentType,
|
|
120
|
+
ContentLength: bytes.length,
|
|
121
|
+
ServerSideEncryption: this.sse,
|
|
122
|
+
ChecksumSHA256: Buffer.from(meta.sha256, 'hex').toString('base64'),
|
|
123
|
+
Metadata: { sha256: meta.sha256 },
|
|
124
|
+
...(this.noOverwrite === 'if-none-match' ? { IfNoneMatch: '*' } : {}),
|
|
125
|
+
};
|
|
126
|
+
try {
|
|
127
|
+
await client.send(new sdk.PutObjectCommand(input));
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// A 412 (key exists) is a refusal too: never overwrite. Fixed message, no cause.
|
|
131
|
+
throw new core_1.ObjectStoreUnavailableError();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async get(key) {
|
|
135
|
+
const { sdk, client } = await this.connect();
|
|
136
|
+
let bytes;
|
|
137
|
+
let contentType;
|
|
138
|
+
try {
|
|
139
|
+
const out = await client.send(new sdk.GetObjectCommand({ Bucket: this.bucket, Key: key }));
|
|
140
|
+
if (!out.Body)
|
|
141
|
+
throw new core_1.ObjectStoreUnavailableError();
|
|
142
|
+
const length = out.ContentLength;
|
|
143
|
+
if (typeof length !== 'number' || length < 0 || length > this.maxGetBytes) {
|
|
144
|
+
// Unknown or over the cap: refuse before buffering, and release the socket.
|
|
145
|
+
discardBody(out.Body);
|
|
146
|
+
throw new core_1.ObjectStoreUnavailableError();
|
|
147
|
+
}
|
|
148
|
+
bytes = await out.Body.transformToByteArray();
|
|
149
|
+
if (bytes.length > this.maxGetBytes)
|
|
150
|
+
throw new core_1.ObjectStoreUnavailableError();
|
|
151
|
+
contentType = out.ContentType;
|
|
152
|
+
}
|
|
153
|
+
catch (e) {
|
|
154
|
+
if (isNotFound(e))
|
|
155
|
+
throw new core_1.ObjectNotFoundError();
|
|
156
|
+
throw new core_1.ObjectStoreUnavailableError();
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
bytes,
|
|
160
|
+
contentType: contentType ?? PROBE_CONTENT_TYPE,
|
|
161
|
+
sizeBytes: bytes.length,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
async head(key) {
|
|
165
|
+
const { sdk, client } = await this.connect();
|
|
166
|
+
try {
|
|
167
|
+
const out = await client.send(new sdk.HeadObjectCommand({ Bucket: this.bucket, Key: key }));
|
|
168
|
+
return {
|
|
169
|
+
sizeBytes: out.ContentLength ?? 0,
|
|
170
|
+
contentType: out.ContentType ?? PROBE_CONTENT_TYPE,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
if (isNotFound(e))
|
|
175
|
+
return null;
|
|
176
|
+
throw new core_1.ObjectStoreUnavailableError();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async delete(key) {
|
|
180
|
+
const { sdk, client } = await this.connect();
|
|
181
|
+
try {
|
|
182
|
+
await client.send(new sdk.DeleteObjectCommand({ Bucket: this.bucket, Key: key }));
|
|
183
|
+
}
|
|
184
|
+
catch (e) {
|
|
185
|
+
if (isNotFound(e))
|
|
186
|
+
return;
|
|
187
|
+
throw new core_1.ObjectStoreUnavailableError();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async ping() {
|
|
191
|
+
const { sdk, client } = await this.connect();
|
|
192
|
+
try {
|
|
193
|
+
await client.send(new sdk.HeadBucketCommand({ Bucket: this.bucket }));
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
throw new core_1.ObjectStoreUnavailableError();
|
|
197
|
+
}
|
|
198
|
+
const key = `_probe/${(0, node_crypto_1.randomUUID)()}`;
|
|
199
|
+
const sha256 = (0, node_crypto_1.createHash)('sha256').update(PROBE_BYTES).digest('hex');
|
|
200
|
+
await this.put(key, PROBE_BYTES, { contentType: PROBE_CONTENT_TYPE, sha256 });
|
|
201
|
+
try {
|
|
202
|
+
const got = await this.get(key);
|
|
203
|
+
if (!sameBytes(got.bytes, PROBE_BYTES))
|
|
204
|
+
throw new core_1.ObjectStoreUnavailableError();
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
throw new core_1.ObjectStoreUnavailableError();
|
|
208
|
+
}
|
|
209
|
+
finally {
|
|
210
|
+
await this.delete(key).catch(() => undefined);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
exports.S3ObjectStore = S3ObjectStore;
|
|
215
|
+
/** Best-effort release of an unread response body (a Node stream in the SDK's Node runtime). */
|
|
216
|
+
function discardBody(body) {
|
|
217
|
+
const destroy = body.destroy;
|
|
218
|
+
if (typeof destroy === 'function') {
|
|
219
|
+
try {
|
|
220
|
+
destroy.call(body);
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
// Nothing to report: the request is refused either way.
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/** S3 "no such key" (GET: `NoSuchKey`; HEAD: `NotFound`; or a bare 404 that is not a missing bucket). */
|
|
228
|
+
function isNotFound(e) {
|
|
229
|
+
if (typeof e !== 'object' || e === null)
|
|
230
|
+
return false;
|
|
231
|
+
const err = e;
|
|
232
|
+
if (err.name === 'NoSuchKey' || err.name === 'NotFound')
|
|
233
|
+
return true;
|
|
234
|
+
return err.$metadata?.httpStatusCode === 404 && err.name !== 'NoSuchBucket';
|
|
235
|
+
}
|
|
236
|
+
function sameBytes(a, b) {
|
|
237
|
+
return a.length === b.length && Buffer.from(a).equals(Buffer.from(b));
|
|
238
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Provider } from '@nestjs/common';
|
|
2
|
+
import { type ObjectStore } from '@astram/core';
|
|
3
|
+
import { SHIELD_FILE_INGEST, SHIELD_OBJECT_STORE } from '../module/shield-tokens.js';
|
|
4
|
+
/** The `storage` option on `ShieldModule.forRoot` / `forRootAsync`. */
|
|
5
|
+
export type ShieldStorageOption = ObjectStore | 'env';
|
|
6
|
+
/** The tokens `ShieldModule` exports app-wide for the storage option. */
|
|
7
|
+
export declare const STORAGE_EXPORTS: readonly [typeof SHIELD_OBJECT_STORE, typeof SHIELD_FILE_INGEST];
|
|
8
|
+
/** Build the storage providers for one `ShieldModule` registration (see the file header). */
|
|
9
|
+
export declare function storageProviders(storage: ShieldStorageOption | undefined): Provider[];
|
|
10
|
+
//# sourceMappingURL=storage.providers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.providers.d.ts","sourceRoot":"","sources":["../../src/storage/storage.providers.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAmD,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAGjG,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAIrF,uEAAuE;AACvE,MAAM,MAAM,mBAAmB,GAAG,WAAW,GAAG,KAAK,CAAC;AAEtD,yEAAyE;AACzE,eAAO,MAAM,eAAe,kEAAqD,CAAC;AAIlF,6FAA6F;AAC7F,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,mBAAmB,GAAG,SAAS,GAAG,QAAQ,EAAE,CA6CrF"}
|