@astram/guard 0.12.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/auth/connections.d.ts +19 -1
  2. package/dist/auth/connections.d.ts.map +1 -1
  3. package/dist/auth/connections.js +44 -1
  4. package/dist/config/app-config.d.ts +66 -0
  5. package/dist/config/app-config.d.ts.map +1 -1
  6. package/dist/config/app-config.js +112 -1
  7. package/dist/config/config.module.d.ts.map +1 -1
  8. package/dist/config/config.module.js +5 -1
  9. package/dist/config/loopback-host.d.ts +9 -0
  10. package/dist/config/loopback-host.d.ts.map +1 -0
  11. package/dist/config/loopback-host.js +19 -0
  12. package/dist/encryption/local-kek-crypto.d.ts +6 -0
  13. package/dist/encryption/local-kek-crypto.d.ts.map +1 -1
  14. package/dist/encryption/local-kek-crypto.js +8 -0
  15. package/dist/identity/identity-actions.d.ts +18 -7
  16. package/dist/identity/identity-actions.d.ts.map +1 -1
  17. package/dist/identity/identity-actions.js +16 -17
  18. package/dist/identity/module-registry.bridge.d.ts.map +1 -1
  19. package/dist/identity/module-registry.bridge.js +5 -3
  20. package/dist/index.d.ts +11 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +56 -21
  23. package/dist/module/shield-tokens.d.ts +11 -0
  24. package/dist/module/shield-tokens.d.ts.map +1 -1
  25. package/dist/module/shield-tokens.js +12 -1
  26. package/dist/module/shield.module.d.ts +10 -0
  27. package/dist/module/shield.module.d.ts.map +1 -1
  28. package/dist/module/shield.module.js +35 -5
  29. package/dist/secrets/boot-secrets.d.ts +33 -0
  30. package/dist/secrets/boot-secrets.d.ts.map +1 -0
  31. package/dist/secrets/boot-secrets.js +180 -0
  32. package/dist/storage/file-intake.interceptor.d.ts +54 -0
  33. package/dist/storage/file-intake.interceptor.d.ts.map +1 -0
  34. package/dist/storage/file-intake.interceptor.js +223 -0
  35. package/dist/storage/node-sha256-digest.d.ts +6 -0
  36. package/dist/storage/node-sha256-digest.d.ts.map +1 -0
  37. package/dist/storage/node-sha256-digest.js +15 -0
  38. package/dist/storage/object-store-boot-check.d.ts +16 -0
  39. package/dist/storage/object-store-boot-check.d.ts.map +1 -0
  40. package/dist/storage/object-store-boot-check.js +52 -0
  41. package/dist/storage/optional-peer.d.ts +10 -0
  42. package/dist/storage/optional-peer.d.ts.map +1 -0
  43. package/dist/storage/optional-peer.js +26 -0
  44. package/dist/storage/s3-object-store.d.ts +35 -0
  45. package/dist/storage/s3-object-store.d.ts.map +1 -0
  46. package/dist/storage/s3-object-store.js +238 -0
  47. package/dist/storage/storage.providers.d.ts +10 -0
  48. package/dist/storage/storage.providers.d.ts.map +1 -0
  49. package/dist/storage/storage.providers.js +104 -0
  50. package/package.json +11 -3
@@ -0,0 +1,54 @@
1
+ /**
2
+ * `FileIntake(options)` (FILE_STORAGE SPEC §5 G, §13): one multipart file, checked, then handed
3
+ * to the route handler.
4
+ *
5
+ * @Classified({ typeKey: 'stored_file', action: 'create' })
6
+ * @UseInterceptors(FileIntake({ field: 'file', profile: 'xlsx' }))
7
+ *
8
+ * - Interceptors run after guards, so the global guard has decided (and a denied caller is
9
+ * refused) before any multipart byte is read (Shield #1, #3). A request with no principal is
10
+ * refused here too, before parsing.
11
+ * - Parsing wraps `@nestjs/platform-express` `FileInterceptor` with multer's memory storage
12
+ * (no module-level `MulterModule` options apply: no disk storage, no consumer file filter) and
13
+ * limits `{ files: 1, fileSize: maxBytes + 1, fields: 0, parts: 2 }` and `defParamCharset:
14
+ * 'utf8'`. `maxBytes` defaults to the bound profile's configured cap (`OBJECT_STORAGE_MAX_BYTES`
15
+ * under `storage: 'env'`); an explicit smaller `maxBytes` wins; never above 50 MiB.
16
+ * - Then `SHIELD_FILE_INGEST` checks the bytes (size, extension, declared type, magic) with the
17
+ * tenant from the session (`req.principal.tenantId`, never the request body).
18
+ * - Errors are mapped here, at the source: 400 / 413 / 415 `HttpException`s carrying a fixed
19
+ * reason code. The file name, field name and bytes are never echoed (Shield #10).
20
+ * - On success the request carries `req[FILE_INTAKE_REQUEST_KEY] = { file, bytes }` for
21
+ * `FilesService.store`; multer's raw `req.file` (unsanitised name) is removed.
22
+ */
23
+ import { type NestInterceptor, type Type } from '@nestjs/common';
24
+ import { type FileRejectionReason, type IngestedFile } from '@astram/core';
25
+ /** The HTTP reason codes `FileIntake` answers with: the core reasons plus `bad_request`. */
26
+ export type FileIntakeRejectionReason = FileRejectionReason | 'bad_request';
27
+ /** Options for {@link FileIntake}. */
28
+ export interface FileIntakeOptions {
29
+ /** The multipart field that carries the file. */
30
+ readonly field: string;
31
+ /** A profile name known to `SHIELD_FILE_INGEST` (e.g. `'xlsx'`). */
32
+ readonly profile: string;
33
+ /**
34
+ * Lower multipart byte cap for this route (at most 50 MiB). Default: the profile's configured
35
+ * cap on `SHIELD_FILE_INGEST` (`OBJECT_STORAGE_MAX_BYTES` under `storage: 'env'`, else 10 MiB);
36
+ * a larger value than the profile cap is lowered to it. The parser stops at `maxBytes + 1`;
37
+ * the profile's own cap still applies in the ingest check.
38
+ */
39
+ readonly maxBytes?: number;
40
+ }
41
+ /** What {@link FileIntake} attaches to the request for `FilesService.store`. */
42
+ export interface FileIntakeResult {
43
+ /** Server-built metadata (storage key, sha256, canonical type, size, sanitised name). */
44
+ readonly file: IngestedFile;
45
+ /** The exact bytes that were checked. */
46
+ readonly bytes: Uint8Array;
47
+ }
48
+ /** The request property {@link FileIntake} sets to a {@link FileIntakeResult}. */
49
+ export declare const FILE_INTAKE_REQUEST_KEY = "shieldFileIntake";
50
+ /** Fallback multipart cap (10 MiB, SPEC §10) when the bound ingest cannot report a profile cap. */
51
+ export declare const FILE_INTAKE_DEFAULT_MAX_BYTES: number;
52
+ /** Build the intake interceptor for one route (see the file header). Throws at boot on bad options. */
53
+ export declare function FileIntake(options: FileIntakeOptions): Type<NestInterceptor>;
54
+ //# sourceMappingURL=file-intake.interceptor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-intake.interceptor.d.ts","sourceRoot":"","sources":["../../src/storage/file-intake.interceptor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAWL,KAAK,eAAe,EACpB,KAAK,IAAI,EACV,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAIL,KAAK,mBAAmB,EACxB,KAAK,YAAY,EAClB,MAAM,cAAc,CAAC;AAMtB,4FAA4F;AAC5F,MAAM,MAAM,yBAAyB,GAAG,mBAAmB,GAAG,aAAa,CAAC;AAE5E,sCAAsC;AACtC,MAAM,WAAW,iBAAiB;IAChC,iDAAiD;IACjD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,oEAAoE;IACpE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,gFAAgF;AAChF,MAAM,WAAW,gBAAgB;IAC/B,yFAAyF;IACzF,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,yCAAyC;IACzC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;CAC5B;AAED,kFAAkF;AAClF,eAAO,MAAM,uBAAuB,qBAAqB,CAAC;AAE1D,mGAAmG;AACnG,eAAO,MAAM,6BAA6B,QAAmB,CAAC;AAuH9D,uGAAuG;AACvG,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAAC,eAAe,CAAC,CAkF5E"}
@@ -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,6 @@
1
+ import type { ByteDigest } from '@astram/core';
2
+ export declare class NodeSha256Digest implements ByteDigest {
3
+ /** Lower-case hex sha256 of the raw bytes. */
4
+ sha256(bytes: Uint8Array): Promise<string>;
5
+ }
6
+ //# sourceMappingURL=node-sha256-digest.d.ts.map
@@ -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"}