@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,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"}
@@ -0,0 +1,104 @@
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.STORAGE_EXPORTS = void 0;
37
+ exports.storageProviders = storageProviders;
38
+ /**
39
+ * `ShieldModule` providers for the `storage` option (FILE_STORAGE SPEC §6):
40
+ * - `SHIELD_OBJECT_STORE`: the given `ObjectStore`, an `S3ObjectStore` built from
41
+ * `OBJECT_STORAGE_*` (`'env'`; `null` when `OBJECT_STORAGE_BUCKET` is unset), or `null` when
42
+ * the option is absent (the `makeAuditSinkProvider` null-binding pattern; callers answer 501).
43
+ * - `SHIELD_FILE_INGEST`: core `createFileIngest` over `NodeSha256Digest`, with the `xlsx`
44
+ * profile (its cap set from `OBJECT_STORAGE_MAX_BYTES` under `'env'`). Always bound.
45
+ * - `ObjectStoreBootCheck`: registered only when the option is given (fail-closed ping at boot).
46
+ *
47
+ * `S3ObjectStore` (and so `@aws-sdk/client-s3`) is loaded lazily, only for `'env'` with a bucket.
48
+ */
49
+ const node_crypto_1 = require("node:crypto");
50
+ const core_1 = require("@astram/core");
51
+ const app_config_js_1 = require("../config/app-config.js");
52
+ const boot_secrets_js_1 = require("../secrets/boot-secrets.js");
53
+ const shield_tokens_js_1 = require("../module/shield-tokens.js");
54
+ const node_sha256_digest_js_1 = require("./node-sha256-digest.js");
55
+ const object_store_boot_check_js_1 = require("./object-store-boot-check.js");
56
+ /** The tokens `ShieldModule` exports app-wide for the storage option. */
57
+ exports.STORAGE_EXPORTS = [shield_tokens_js_1.SHIELD_OBJECT_STORE, shield_tokens_js_1.SHIELD_FILE_INGEST];
58
+ const STORE_METHODS = ['put', 'get', 'head', 'delete', 'ping'];
59
+ /** Build the storage providers for one `ShieldModule` registration (see the file header). */
60
+ function storageProviders(storage) {
61
+ if (storage !== undefined && storage !== 'env') {
62
+ const s = storage;
63
+ if (STORE_METHODS.some((m) => typeof s?.[m] !== 'function')) {
64
+ throw new Error("ShieldModule: `storage` must be an ObjectStore (put/get/head/delete/ping) or 'env'");
65
+ }
66
+ }
67
+ const configToken = Symbol('SHIELD_OBJECT_STORAGE_CONFIG');
68
+ return [
69
+ {
70
+ provide: configToken,
71
+ useFactory: () => storage === 'env'
72
+ ? ((0, app_config_js_1.loadObjectStorageConfig)(process.env, {
73
+ resolvedFromSecretRef: boot_secrets_js_1.wasResolvedFromSecretRef,
74
+ }) ?? null)
75
+ : null,
76
+ },
77
+ {
78
+ provide: shield_tokens_js_1.SHIELD_OBJECT_STORE,
79
+ useFactory: async (config) => {
80
+ if (storage === undefined)
81
+ return null;
82
+ if (storage !== 'env')
83
+ return storage;
84
+ if (config === null)
85
+ return null;
86
+ const { S3ObjectStore } = await Promise.resolve().then(() => __importStar(require('./s3-object-store.js')));
87
+ return new S3ObjectStore(config);
88
+ },
89
+ inject: [configToken],
90
+ },
91
+ {
92
+ provide: shield_tokens_js_1.SHIELD_FILE_INGEST,
93
+ useFactory: (config) => (0, core_1.createFileIngest)({
94
+ digest: new node_sha256_digest_js_1.NodeSha256Digest(),
95
+ newId: node_crypto_1.randomUUID,
96
+ profiles: {
97
+ xlsx: config ? { ...core_1.XLSX_PROFILE, maxBytes: config.maxBytes } : core_1.XLSX_PROFILE,
98
+ },
99
+ }),
100
+ inject: [configToken],
101
+ },
102
+ ...(storage !== undefined ? [object_store_boot_check_js_1.ObjectStoreBootCheck] : []),
103
+ ];
104
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astram/guard",
3
- "version": "0.12.1",
3
+ "version": "0.14.0",
4
4
  "license": "MIT",
5
5
  "description": "Astram framework — the enforcement guard + real-world adapters (the 'hands'). One global deny-by-default gate every request passes through (ADR-0002), classification decorators, boot-pool assertion (ADR-0018), the forRoot() registration seam, and the DB/Redis/crypto adapters that connect the sealed core to a real backend. Depends on @astram/core (ADR-0003, ADR-0023).",
6
6
  "type": "commonjs",
@@ -32,21 +32,29 @@
32
32
  "access": "public"
33
33
  },
34
34
  "dependencies": {
35
+ "@aws-sdk/client-s3": "3.1142.0",
35
36
  "argon2": "^0.41.1",
36
37
  "ioredis": "^5.4.1",
37
38
  "mariadb": "^3.5.4",
38
- "@astram/core": "1.5.0",
39
- "@astram/engine": "0.5.3"
39
+ "@astram/core": "1.6.0",
40
+ "@astram/engine": "0.7.0"
40
41
  },
41
42
  "peerDependencies": {
42
43
  "@nestjs/common": "^11",
43
44
  "@nestjs/core": "^11",
45
+ "@nestjs/platform-express": "^11",
44
46
  "reflect-metadata": "^0.2",
45
47
  "rxjs": "^7"
46
48
  },
49
+ "peerDependenciesMeta": {
50
+ "@nestjs/platform-express": {
51
+ "optional": true
52
+ }
53
+ },
47
54
  "devDependencies": {
48
55
  "@nestjs/common": "^11.0.0",
49
56
  "@nestjs/core": "^11.0.0",
57
+ "@nestjs/platform-express": "^11.0.0",
50
58
  "@nestjs/testing": "^11.0.0",
51
59
  "@types/node": "^22.0.0",
52
60
  "@types/supertest": "^6.0.2",