@astram/identity 0.10.0 → 0.12.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-facts-keys.d.ts +16 -0
- package/dist/auth-facts-keys.d.ts.map +1 -1
- package/dist/auth-facts-keys.js +26 -10
- package/dist/cross-tenant-grant.object.d.ts +2 -2
- package/dist/cross-tenant-grant.object.d.ts.map +1 -1
- package/dist/cross-tenant-grant.object.js +8 -4
- package/dist/features/audit/directory-label-resolver.d.ts +4 -1
- package/dist/features/audit/directory-label-resolver.d.ts.map +1 -1
- package/dist/features/audit/directory-label-resolver.js +8 -5
- package/dist/features/audit/mariadb-audit-reader.d.ts +5 -1
- package/dist/features/audit/mariadb-audit-reader.d.ts.map +1 -1
- package/dist/features/audit/mariadb-audit-reader.js +10 -2
- package/dist/features/authz/access-bundle-reconcile.job.d.ts +4 -1
- package/dist/features/authz/access-bundle-reconcile.job.d.ts.map +1 -1
- package/dist/features/authz/access-bundle-reconcile.job.js +7 -4
- package/dist/features/authz/access-bundle-store.d.ts +4 -1
- package/dist/features/authz/access-bundle-store.d.ts.map +1 -1
- package/dist/features/authz/access-bundle-store.js +19 -16
- package/dist/features/authz/catalog-bootstrap.provider.d.ts +4 -1
- package/dist/features/authz/catalog-bootstrap.provider.d.ts.map +1 -1
- package/dist/features/authz/catalog-bootstrap.provider.js +37 -6
- package/dist/features/authz/catalog-reconcile.job.d.ts +3 -1
- package/dist/features/authz/catalog-reconcile.job.d.ts.map +1 -1
- package/dist/features/authz/catalog-reconcile.job.js +3 -2
- package/dist/features/authz/db-permission-facts.adapter.d.ts +9 -3
- package/dist/features/authz/db-permission-facts.adapter.d.ts.map +1 -1
- package/dist/features/authz/db-permission-facts.adapter.js +22 -16
- package/dist/features/authz/grant-admin.service.d.ts +4 -1
- package/dist/features/authz/grant-admin.service.d.ts.map +1 -1
- package/dist/features/authz/grant-admin.service.js +32 -37
- package/dist/features/authz/grant-ceiling.d.ts +6 -1
- package/dist/features/authz/grant-ceiling.d.ts.map +1 -1
- package/dist/features/authz/grant-ceiling.js +19 -16
- package/dist/features/authz/mariadb-catalog-store.d.ts +4 -1
- package/dist/features/authz/mariadb-catalog-store.d.ts.map +1 -1
- package/dist/features/authz/mariadb-catalog-store.js +10 -7
- package/dist/features/authz/permission-version.bumper.d.ts +6 -1
- package/dist/features/authz/permission-version.bumper.d.ts.map +1 -1
- package/dist/features/authz/permission-version.bumper.js +13 -11
- package/dist/features/authz/record-authorization.d.ts +8 -2
- package/dist/features/authz/record-authorization.d.ts.map +1 -1
- package/dist/features/authz/record-authorization.js +8 -4
- package/dist/features/authz/role-binding.d.ts +3 -2
- package/dist/features/authz/role-binding.d.ts.map +1 -1
- package/dist/features/authz/role-binding.js +7 -8
- package/dist/features/company/company-hierarchy.service.d.ts +6 -1
- package/dist/features/company/company-hierarchy.service.d.ts.map +1 -1
- package/dist/features/company/company-hierarchy.service.js +17 -16
- package/dist/features/cross-tenant/cross-tenant-grant-read.service.d.ts +11 -6
- package/dist/features/cross-tenant/cross-tenant-grant-read.service.d.ts.map +1 -1
- package/dist/features/cross-tenant/cross-tenant-grant-read.service.js +24 -19
- package/dist/features/files/files.controller.d.ts +27 -0
- package/dist/features/files/files.controller.d.ts.map +1 -0
- package/dist/features/files/files.controller.js +94 -0
- package/dist/features/files/files.service.d.ts +87 -0
- package/dist/features/files/files.service.d.ts.map +1 -0
- package/dist/features/files/files.service.js +311 -0
- package/dist/features/framework-schemas.token.d.ts +8 -0
- package/dist/features/framework-schemas.token.d.ts.map +1 -0
- package/dist/features/framework-schemas.token.js +10 -0
- package/dist/features/identity-feature.module.d.ts +29 -1
- package/dist/features/identity-feature.module.d.ts.map +1 -1
- package/dist/features/identity-feature.module.js +168 -41
- package/dist/features/membership/membership-checker.d.ts +7 -1
- package/dist/features/membership/membership-checker.d.ts.map +1 -1
- package/dist/features/membership/membership-checker.js +9 -4
- package/dist/features/migration-ledger-check.d.ts +15 -0
- package/dist/features/migration-ledger-check.d.ts.map +1 -0
- package/dist/features/migration-ledger-check.js +26 -0
- package/dist/features/policy/policy.service.d.ts +2 -1
- package/dist/features/policy/policy.service.d.ts.map +1 -1
- package/dist/features/policy/policy.service.js +15 -0
- package/dist/features/scope/scope-closure.service.d.ts +8 -1
- package/dist/features/scope/scope-closure.service.d.ts.map +1 -1
- package/dist/features/scope/scope-closure.service.js +22 -19
- package/dist/features/scope/scope-subtree.resolver.d.ts +7 -1
- package/dist/features/scope/scope-subtree.resolver.d.ts.map +1 -1
- package/dist/features/scope/scope-subtree.resolver.js +9 -4
- package/dist/features/service-account/lock-service-accounts.d.ts +3 -1
- package/dist/features/service-account/lock-service-accounts.d.ts.map +1 -1
- package/dist/features/service-account/lock-service-accounts.js +4 -4
- package/dist/features/service-account/no-expiry-key-report.d.ts +9 -3
- package/dist/features/service-account/no-expiry-key-report.d.ts.map +1 -1
- package/dist/features/service-account/no-expiry-key-report.js +17 -10
- package/dist/features/service-account/service-account.store.d.ts +8 -1
- package/dist/features/service-account/service-account.store.d.ts.map +1 -1
- package/dist/features/service-account/service-account.store.js +19 -14
- package/dist/features/share/share.service.d.ts +19 -2
- package/dist/features/share/share.service.d.ts.map +1 -1
- package/dist/features/share/share.service.js +36 -2
- package/dist/features/user/user.service.d.ts +7 -3
- package/dist/features/user/user.service.d.ts.map +1 -1
- package/dist/features/user/user.service.js +12 -4
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -1
- package/dist/objects.d.ts.map +1 -1
- package/dist/objects.js +2 -0
- package/dist/record-share.object.d.ts +1 -1
- package/dist/record-share.object.d.ts.map +1 -1
- package/dist/record-share.object.js +7 -3
- package/dist/schema.d.ts +15 -2
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +17 -2
- package/dist/service-account.object.d.ts +3 -2
- package/dist/service-account.object.d.ts.map +1 -1
- package/dist/service-account.object.js +4 -2
- package/dist/stored-file.object.d.ts +35 -0
- package/dist/stored-file.object.d.ts.map +1 -0
- package/dist/stored-file.object.js +94 -0
- package/migrations/1710000000001-CreateIdentityOrgModel.cjs +14 -4
- package/migrations/1710000000002-CreateMembershipAndPostings.cjs +14 -4
- package/migrations/1710000000003-CreateIdentityCredential.cjs +14 -4
- package/migrations/1710000000004-CreateSessionTables.cjs +14 -4
- package/migrations/1710000000005-CreateMfaAndTokenSurface.cjs +14 -4
- package/migrations/1710000000010-CreateAuditLog.cjs +14 -4
- package/migrations/1710000000011-CreateCrossTenantGrant.cjs +15 -4
- package/migrations/1710000000012-SeedCrossTenantGrantPerms.cjs +15 -4
- package/migrations/1710000000013-CreateSharedResource.cjs +14 -4
- package/migrations/1710000000018-CreateServiceAccount.cjs +13 -4
- package/migrations/1710000000019-AddSessionMustReenroll.cjs +14 -4
- package/migrations/1710000000020-CreateEmailChangeToken.cjs +15 -5
- package/migrations/1710000000021-AddUsersEmailUniqueIndex.cjs +24 -13
- package/migrations/1710000000022-CreateStoredFile.cjs +53 -0
- package/migrations/1710000000099-SeedDevIdentity.cjs +14 -6
- package/migrations/1710000000100-SeedDevTenantBAndRoot.cjs +14 -6
- package/package.json +8 -4
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { StreamableFile } from '@nestjs/common';
|
|
2
|
+
import { FilesService } from './files.service.js';
|
|
3
|
+
/**
|
|
4
|
+
* `filename*` value for a stored name: C0 controls and DEL dropped, the rest UTF-8 percent-encoded
|
|
5
|
+
* byte by byte (a lone surrogate becomes U+FFFD via `Buffer.from`, so this never throws). Quotes,
|
|
6
|
+
* CR, LF, `;` and spaces are always encoded, so the name cannot split or extend the header.
|
|
7
|
+
*/
|
|
8
|
+
export declare function rfc5987FileName(name: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* FilesController — the framework download route `GET /files/:uuid` (file-storage SPEC §3, §5 K2).
|
|
11
|
+
*
|
|
12
|
+
* - Gated on `stored_file:read` by the global guard (Shield #2/#3); `UuidPipe` answers 400 for a
|
|
13
|
+
* non-uuid before any service call. Record-level decide, the tenant wall, the key-prefix and
|
|
14
|
+
* sha256 checks and the `file_access` audit all happen in `FilesService.open`.
|
|
15
|
+
* - Errors map at source: 501/503/403 come from `FilesService` with fixed messages; an engine
|
|
16
|
+
* deny is rethrown here as the same 403 a missing row gets (SPEC §13: same body). Never echoes
|
|
17
|
+
* the storage key or the name in an error.
|
|
18
|
+
* - Success headers: `Cache-Control: no-store` and `nosniff` via `@Header`; `Content-Type` (from
|
|
19
|
+
* the record), `Content-Disposition` (attachment, RFC 5987 `filename*` only) and `Content-Length`
|
|
20
|
+
* via `StreamableFile`, all set before the body is written. No express types needed.
|
|
21
|
+
*/
|
|
22
|
+
export declare class FilesController {
|
|
23
|
+
private readonly files;
|
|
24
|
+
constructor(files: FilesService);
|
|
25
|
+
download(uuid: string): Promise<StreamableFile>;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=files.controller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"files.controller.d.ts","sourceRoot":"","sources":["../../../src/features/files/files.controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,cAAc,EAEf,MAAM,gBAAgB,CAAC;AAIxB,OAAO,EAAE,YAAY,EAAmB,MAAM,oBAAoB,CAAC;AAQnE;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAYpD;AAED;;;;;;;;;;;;GAYG;AACH,qBAEa,eAAe;IACd,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,YAAY;IAM1C,QAAQ,CAA0B,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;CAc/E"}
|
|
@@ -0,0 +1,94 @@
|
|
|
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.FilesController = void 0;
|
|
16
|
+
exports.rfc5987FileName = rfc5987FileName;
|
|
17
|
+
const common_1 = require("@nestjs/common");
|
|
18
|
+
const engine_1 = require("@astram/engine");
|
|
19
|
+
const guard_1 = require("@astram/guard");
|
|
20
|
+
const identity_error_interceptor_js_1 = require("../shared/identity-error.interceptor.js");
|
|
21
|
+
const files_service_js_1 = require("./files.service.js");
|
|
22
|
+
/** Same text as `FilesService`'s 403, so an engine deny and a missing row answer one body (§13). */
|
|
23
|
+
const MSG_FORBIDDEN = 'Forbidden';
|
|
24
|
+
/** RFC 5987 `attr-char` (RFC 8187 §3.2.1): every other UTF-8 byte is percent-encoded. */
|
|
25
|
+
const ATTR_CHAR = /^[A-Za-z0-9!#$&+\-.^_`|~]$/;
|
|
26
|
+
/**
|
|
27
|
+
* `filename*` value for a stored name: C0 controls and DEL dropped, the rest UTF-8 percent-encoded
|
|
28
|
+
* byte by byte (a lone surrogate becomes U+FFFD via `Buffer.from`, so this never throws). Quotes,
|
|
29
|
+
* CR, LF, `;` and spaces are always encoded, so the name cannot split or extend the header.
|
|
30
|
+
*/
|
|
31
|
+
function rfc5987FileName(name) {
|
|
32
|
+
// eslint-disable-next-line no-control-regex
|
|
33
|
+
const cleaned = name.replace(/[\u0000-\u001f\u007f]/g, '');
|
|
34
|
+
let out = '';
|
|
35
|
+
for (const byte of Buffer.from(cleaned.length > 0 ? cleaned : 'download', 'utf8')) {
|
|
36
|
+
const ch = String.fromCharCode(byte);
|
|
37
|
+
out +=
|
|
38
|
+
byte < 0x80 && ATTR_CHAR.test(ch)
|
|
39
|
+
? ch
|
|
40
|
+
: `%${byte.toString(16).toUpperCase().padStart(2, '0')}`;
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* FilesController — the framework download route `GET /files/:uuid` (file-storage SPEC §3, §5 K2).
|
|
46
|
+
*
|
|
47
|
+
* - Gated on `stored_file:read` by the global guard (Shield #2/#3); `UuidPipe` answers 400 for a
|
|
48
|
+
* non-uuid before any service call. Record-level decide, the tenant wall, the key-prefix and
|
|
49
|
+
* sha256 checks and the `file_access` audit all happen in `FilesService.open`.
|
|
50
|
+
* - Errors map at source: 501/503/403 come from `FilesService` with fixed messages; an engine
|
|
51
|
+
* deny is rethrown here as the same 403 a missing row gets (SPEC §13: same body). Never echoes
|
|
52
|
+
* the storage key or the name in an error.
|
|
53
|
+
* - Success headers: `Cache-Control: no-store` and `nosniff` via `@Header`; `Content-Type` (from
|
|
54
|
+
* the record), `Content-Disposition` (attachment, RFC 5987 `filename*` only) and `Content-Length`
|
|
55
|
+
* via `StreamableFile`, all set before the body is written. No express types needed.
|
|
56
|
+
*/
|
|
57
|
+
let FilesController = class FilesController {
|
|
58
|
+
files;
|
|
59
|
+
constructor(files) {
|
|
60
|
+
this.files = files;
|
|
61
|
+
}
|
|
62
|
+
async download(uuid) {
|
|
63
|
+
let file;
|
|
64
|
+
try {
|
|
65
|
+
file = await this.files.open(uuid);
|
|
66
|
+
}
|
|
67
|
+
catch (cause) {
|
|
68
|
+
if (cause instanceof engine_1.AuthorizationDeniedError)
|
|
69
|
+
throw new common_1.ForbiddenException(MSG_FORBIDDEN);
|
|
70
|
+
throw cause;
|
|
71
|
+
}
|
|
72
|
+
return new common_1.StreamableFile(Buffer.from(file.bytes), {
|
|
73
|
+
type: file.contentType,
|
|
74
|
+
disposition: `attachment; filename*=UTF-8''${rfc5987FileName(file.originalName)}`,
|
|
75
|
+
length: file.bytes.byteLength,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
exports.FilesController = FilesController;
|
|
80
|
+
__decorate([
|
|
81
|
+
(0, common_1.Get)(':uuid'),
|
|
82
|
+
(0, guard_1.Classified)({ typeKey: 'stored_file', action: 'read' }),
|
|
83
|
+
(0, common_1.Header)('Cache-Control', 'no-store'),
|
|
84
|
+
(0, common_1.Header)('X-Content-Type-Options', 'nosniff'),
|
|
85
|
+
__param(0, (0, common_1.Param)('uuid', guard_1.UuidPipe)),
|
|
86
|
+
__metadata("design:type", Function),
|
|
87
|
+
__metadata("design:paramtypes", [String]),
|
|
88
|
+
__metadata("design:returntype", Promise)
|
|
89
|
+
], FilesController.prototype, "download", null);
|
|
90
|
+
exports.FilesController = FilesController = __decorate([
|
|
91
|
+
(0, common_1.Controller)('files'),
|
|
92
|
+
(0, common_1.UseInterceptors)(identity_error_interceptor_js_1.IdentityErrorInterceptor),
|
|
93
|
+
__metadata("design:paramtypes", [files_service_js_1.FilesService])
|
|
94
|
+
], FilesController);
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { AuditSink, IngestedFile, ObjectStore } from '@astram/core';
|
|
2
|
+
import { type RecordsService } from '@astram/engine';
|
|
3
|
+
/**
|
|
4
|
+
* The canonical content types the intake allowlist can store: today the `xlsx` profile, the only
|
|
5
|
+
* profile `ShieldModule` registers on `SHIELD_FILE_INGEST`. Pass the app's own list when it
|
|
6
|
+
* registers more profiles.
|
|
7
|
+
*/
|
|
8
|
+
export declare const DEFAULT_SERVABLE_CONTENT_TYPES: readonly string[];
|
|
9
|
+
/** What `store()` takes: the ingest result (server-built key, sha256, canonical type) plus bytes. */
|
|
10
|
+
export interface StoreFileInput extends IngestedFile {
|
|
11
|
+
readonly bytes: Uint8Array;
|
|
12
|
+
}
|
|
13
|
+
/** What `store()` returns to the caller. Never carries the storage key (SPEC §5 I). */
|
|
14
|
+
export interface StoredFileSummary {
|
|
15
|
+
readonly uuid: string;
|
|
16
|
+
readonly sha256: string;
|
|
17
|
+
readonly sizeBytes: number;
|
|
18
|
+
readonly contentType: string;
|
|
19
|
+
readonly originalName: string;
|
|
20
|
+
}
|
|
21
|
+
/** What `open()` returns (SPEC §5 J). Never carries the storage key. */
|
|
22
|
+
export interface OpenedFile {
|
|
23
|
+
readonly bytes: Uint8Array;
|
|
24
|
+
readonly contentType: string;
|
|
25
|
+
readonly originalName: string;
|
|
26
|
+
readonly sizeBytes: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* FilesService — stored files on the engine pipe (file-storage SPEC §5 I-K).
|
|
30
|
+
*
|
|
31
|
+
* - The `stored_file` row is written ONLY here (KI #4): the key comes from `FileIntake` (built from
|
|
32
|
+
* the session tenant) and is re-checked against the actor's tenant before any put or create.
|
|
33
|
+
* Tenant and owner are stamped by the pipe from the session (Shield #7), never sent from here.
|
|
34
|
+
* - Every row access goes through `RecordsService` (record gate, tenant wall, read scope, audit);
|
|
35
|
+
* no DB pool, no bypass flag (Shield #3, #9). The bucket is read only in `open()`, after the gate.
|
|
36
|
+
* - Errors map at source: 501 when no store is bound, 503 (fixed message) for store failures and
|
|
37
|
+
* integrity faults, 403 (fixed message) for missing, foreign or denied rows alike.
|
|
38
|
+
* - No actor argument (SPEC §5 I-K): the key tenant and the `file_access` audit actor come from
|
|
39
|
+
* `RecordsService.requestPrincipal()`, the same principal the engine stamps and gates with, so
|
|
40
|
+
* key tenant, row tenant/owner and audit actor cannot diverge. No principal ⇒ fail closed.
|
|
41
|
+
* - The engine does not audit its own denials, so a 403 from open/remove writes a `file_access`
|
|
42
|
+
* failure event (`denied_or_missing`, no key, no name). Logs carry the storage key only.
|
|
43
|
+
*/
|
|
44
|
+
export declare class FilesService {
|
|
45
|
+
private readonly records;
|
|
46
|
+
private readonly objectStore;
|
|
47
|
+
private readonly audit;
|
|
48
|
+
private readonly logger;
|
|
49
|
+
private orphans;
|
|
50
|
+
constructor(records: RecordsService, objectStore: ObjectStore | null, audit: AuditSink, servableContentTypes?: readonly string[]);
|
|
51
|
+
/** Lower-cased allowlisted type -> its canonical spelling (what `open()` may serve). */
|
|
52
|
+
private readonly servable;
|
|
53
|
+
/**
|
|
54
|
+
* KI #13: the download Content-Type is re-checked against the intake allowlist, never taken
|
|
55
|
+
* from the row as-is. Anything else (a row written outside FileIntake, a profile since
|
|
56
|
+
* removed) is served as `application/octet-stream`.
|
|
57
|
+
*/
|
|
58
|
+
private servableType;
|
|
59
|
+
/** Objects left behind by a failed compensating or post-commit delete since boot. */
|
|
60
|
+
get orphanedObjectCount(): number;
|
|
61
|
+
/** Put the bytes, then create the `stored_file` record; on create failure delete the object. */
|
|
62
|
+
store(intake: StoreFileInput): Promise<StoredFileSummary>;
|
|
63
|
+
/** Read a file under record-level decide, with the key and sha256 re-checked (SPEC §5 J). */
|
|
64
|
+
open(uuid: string): Promise<OpenedFile>;
|
|
65
|
+
/**
|
|
66
|
+
* Hard delete (team default): the record in a transaction (gated and audited by the pipe), the
|
|
67
|
+
* object after commit, then one `file_access` delete event (SPEC §5 K). A foreign key is never
|
|
68
|
+
* passed to the store.
|
|
69
|
+
*/
|
|
70
|
+
remove(uuid: string): Promise<void>;
|
|
71
|
+
private requireStore;
|
|
72
|
+
/**
|
|
73
|
+
* Delete an object; true when the store confirmed it. On failure log the key only, count the
|
|
74
|
+
* orphan and answer false. Never throws.
|
|
75
|
+
*/
|
|
76
|
+
private deleteObject;
|
|
77
|
+
/**
|
|
78
|
+
* A `file_access` failure event for a 403 (missing, foreign, malformed or engine-denied: one
|
|
79
|
+
* reason, since the caller may not learn which). Best effort; the 403 is thrown regardless.
|
|
80
|
+
*/
|
|
81
|
+
private auditDenied;
|
|
82
|
+
/** A `file_access` failure event (best effort; the 503 is thrown regardless). */
|
|
83
|
+
private auditFailure;
|
|
84
|
+
/** `target` is the fetched row, or for a denial only the requested uuid (null if malformed). */
|
|
85
|
+
private event;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=files.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"files.service.d.ts","sourceRoot":"","sources":["../../../src/features/files/files.service.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAc,SAAS,EAAE,YAAY,EAAE,WAAW,EAAa,MAAM,cAAc,CAAC;AAEhG,OAAO,EAGL,KAAK,cAAc,EAEpB,MAAM,gBAAgB,CAAC;AAYxB;;;;GAIG;AACH,eAAO,MAAM,8BAA8B,EAAE,SAAS,MAAM,EAA8B,CAAC;AAE3F,qGAAqG;AACrG,MAAM,WAAW,cAAe,SAAQ,YAAY;IAClD,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;CAC5B;AAED,uFAAuF;AACvF,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED,wEAAwE;AACxE,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAqBD;;;;;;;;;;;;;;;GAeG;AACH,qBACa,YAAY;IAKrB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,KAAK;IANxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiC;IACxD,OAAO,CAAC,OAAO,CAAK;gBAGD,OAAO,EAAE,cAAc,EACvB,WAAW,EAAE,WAAW,GAAG,IAAI,EAC/B,KAAK,EAAE,SAAS,EACjC,oBAAoB,GAAE,SAAS,MAAM,EAAmC;IAK1E,wFAAwF;IACxF,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA8B;IAEvD;;;;OAIG;IACH,OAAO,CAAC,YAAY;IAKpB,qFAAqF;IACrF,IAAI,mBAAmB,IAAI,MAAM,CAEhC;IAED,gGAAgG;IAC1F,KAAK,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,iBAAiB,CAAC;IA4C/D,6FAA6F;IACvF,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAyD7C;;;;OAIG;IACG,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAyCzC,OAAO,CAAC,YAAY;IAOpB;;;OAGG;YACW,YAAY;IAa1B;;;OAGG;YACW,WAAW;IAczB,iFAAiF;YACnE,YAAY;IAQ1B,gGAAgG;IAChG,OAAO,CAAC,KAAK;CAwBd"}
|
|
@@ -0,0 +1,311 @@
|
|
|
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 FilesService_1;
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.FilesService = exports.DEFAULT_SERVABLE_CONTENT_TYPES = void 0;
|
|
14
|
+
const node_crypto_1 = require("node:crypto");
|
|
15
|
+
const common_1 = require("@nestjs/common");
|
|
16
|
+
const core_1 = require("@astram/core");
|
|
17
|
+
const engine_1 = require("@astram/engine");
|
|
18
|
+
const TYPE_KEY = 'stored_file';
|
|
19
|
+
/** Fixed messages: never a key, a name, bytes or a store detail (SPEC §11 secrets, §13). */
|
|
20
|
+
const MSG_NOT_CONFIGURED = 'File storage is not configured';
|
|
21
|
+
const MSG_UNAVAILABLE = 'File storage is unavailable';
|
|
22
|
+
const MSG_FORBIDDEN = 'Forbidden';
|
|
23
|
+
/** Served when a stored content type is not one the intake allowlist produces (KI #13). */
|
|
24
|
+
const OCTET_STREAM = 'application/octet-stream';
|
|
25
|
+
/**
|
|
26
|
+
* The canonical content types the intake allowlist can store: today the `xlsx` profile, the only
|
|
27
|
+
* profile `ShieldModule` registers on `SHIELD_FILE_INGEST`. Pass the app's own list when it
|
|
28
|
+
* registers more profiles.
|
|
29
|
+
*/
|
|
30
|
+
exports.DEFAULT_SERVABLE_CONTENT_TYPES = core_1.XLSX_PROFILE.contentTypes;
|
|
31
|
+
/**
|
|
32
|
+
* True when `key` is exactly `t/<tenantId>/<uuid>` for this tenant (KI #4). Both segments must be
|
|
33
|
+
* canonical uuids, so a traversal (`..`), an extra segment or a shared string prefix
|
|
34
|
+
* (`t/<tenant>extra/…`) never matches.
|
|
35
|
+
*/
|
|
36
|
+
function keyBelongsTo(key, tenantId) {
|
|
37
|
+
const tenant = (0, engine_1.canonicalUuid)(tenantId);
|
|
38
|
+
if (tenant === null || typeof key !== 'string')
|
|
39
|
+
return false;
|
|
40
|
+
const parts = key.split('/');
|
|
41
|
+
return (parts.length === 3 &&
|
|
42
|
+
parts[0] === 't' &&
|
|
43
|
+
parts[1] === tenant &&
|
|
44
|
+
(0, engine_1.canonicalUuid)(parts[2]) === parts[2]);
|
|
45
|
+
}
|
|
46
|
+
const sha256Hex = (bytes) => (0, node_crypto_1.createHash)('sha256').update(bytes).digest('hex');
|
|
47
|
+
/**
|
|
48
|
+
* FilesService — stored files on the engine pipe (file-storage SPEC §5 I-K).
|
|
49
|
+
*
|
|
50
|
+
* - The `stored_file` row is written ONLY here (KI #4): the key comes from `FileIntake` (built from
|
|
51
|
+
* the session tenant) and is re-checked against the actor's tenant before any put or create.
|
|
52
|
+
* Tenant and owner are stamped by the pipe from the session (Shield #7), never sent from here.
|
|
53
|
+
* - Every row access goes through `RecordsService` (record gate, tenant wall, read scope, audit);
|
|
54
|
+
* no DB pool, no bypass flag (Shield #3, #9). The bucket is read only in `open()`, after the gate.
|
|
55
|
+
* - Errors map at source: 501 when no store is bound, 503 (fixed message) for store failures and
|
|
56
|
+
* integrity faults, 403 (fixed message) for missing, foreign or denied rows alike.
|
|
57
|
+
* - No actor argument (SPEC §5 I-K): the key tenant and the `file_access` audit actor come from
|
|
58
|
+
* `RecordsService.requestPrincipal()`, the same principal the engine stamps and gates with, so
|
|
59
|
+
* key tenant, row tenant/owner and audit actor cannot diverge. No principal ⇒ fail closed.
|
|
60
|
+
* - The engine does not audit its own denials, so a 403 from open/remove writes a `file_access`
|
|
61
|
+
* failure event (`denied_or_missing`, no key, no name). Logs carry the storage key only.
|
|
62
|
+
*/
|
|
63
|
+
let FilesService = FilesService_1 = class FilesService {
|
|
64
|
+
records;
|
|
65
|
+
objectStore;
|
|
66
|
+
audit;
|
|
67
|
+
logger = new common_1.Logger(FilesService_1.name);
|
|
68
|
+
orphans = 0;
|
|
69
|
+
constructor(records, objectStore, audit, servableContentTypes = exports.DEFAULT_SERVABLE_CONTENT_TYPES) {
|
|
70
|
+
this.records = records;
|
|
71
|
+
this.objectStore = objectStore;
|
|
72
|
+
this.audit = audit;
|
|
73
|
+
this.servable = new Map(servableContentTypes.map((t) => [t.toLowerCase(), t]));
|
|
74
|
+
}
|
|
75
|
+
/** Lower-cased allowlisted type -> its canonical spelling (what `open()` may serve). */
|
|
76
|
+
servable;
|
|
77
|
+
/**
|
|
78
|
+
* KI #13: the download Content-Type is re-checked against the intake allowlist, never taken
|
|
79
|
+
* from the row as-is. Anything else (a row written outside FileIntake, a profile since
|
|
80
|
+
* removed) is served as `application/octet-stream`.
|
|
81
|
+
*/
|
|
82
|
+
servableType(stored) {
|
|
83
|
+
if (typeof stored !== 'string')
|
|
84
|
+
return OCTET_STREAM;
|
|
85
|
+
return this.servable.get(stored.toLowerCase()) ?? OCTET_STREAM;
|
|
86
|
+
}
|
|
87
|
+
/** Objects left behind by a failed compensating or post-commit delete since boot. */
|
|
88
|
+
get orphanedObjectCount() {
|
|
89
|
+
return this.orphans;
|
|
90
|
+
}
|
|
91
|
+
/** Put the bytes, then create the `stored_file` record; on create failure delete the object. */
|
|
92
|
+
async store(intake) {
|
|
93
|
+
const store = this.requireStore();
|
|
94
|
+
const actor = this.records.requestPrincipal();
|
|
95
|
+
if (!keyBelongsTo(intake.storageKey, actor.tenantId)) {
|
|
96
|
+
// A key outside the session tenant never reaches the bucket or the table (KI #4).
|
|
97
|
+
throw new common_1.ForbiddenException(MSG_FORBIDDEN);
|
|
98
|
+
}
|
|
99
|
+
const key = intake.storageKey;
|
|
100
|
+
try {
|
|
101
|
+
await store.put(key, intake.bytes, {
|
|
102
|
+
contentType: intake.contentType,
|
|
103
|
+
sha256: intake.sha256,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
catch (cause) {
|
|
107
|
+
this.logger.error(`file put failed (${errorName(cause)}): ${key}`);
|
|
108
|
+
throw new common_1.ServiceUnavailableException(MSG_UNAVAILABLE);
|
|
109
|
+
}
|
|
110
|
+
let uuid;
|
|
111
|
+
try {
|
|
112
|
+
const id = await this.records.create(TYPE_KEY, {
|
|
113
|
+
fields: {
|
|
114
|
+
storageKey: key,
|
|
115
|
+
sha256: intake.sha256,
|
|
116
|
+
contentType: intake.contentType,
|
|
117
|
+
sizeBytes: intake.sizeBytes,
|
|
118
|
+
originalName: intake.originalName,
|
|
119
|
+
createdAt: Date.now(),
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
uuid = String(id);
|
|
123
|
+
}
|
|
124
|
+
catch (cause) {
|
|
125
|
+
await this.deleteObject(store, key, 'compensating');
|
|
126
|
+
throw cause;
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
uuid,
|
|
130
|
+
sha256: intake.sha256,
|
|
131
|
+
sizeBytes: intake.sizeBytes,
|
|
132
|
+
contentType: intake.contentType,
|
|
133
|
+
originalName: intake.originalName,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/** Read a file under record-level decide, with the key and sha256 re-checked (SPEC §5 J). */
|
|
137
|
+
async open(uuid) {
|
|
138
|
+
const store = this.requireStore();
|
|
139
|
+
const actor = this.records.requestPrincipal();
|
|
140
|
+
const id = (0, engine_1.canonicalUuid)(uuid);
|
|
141
|
+
let row;
|
|
142
|
+
try {
|
|
143
|
+
if (id === null)
|
|
144
|
+
throw new common_1.ForbiddenException(MSG_FORBIDDEN);
|
|
145
|
+
row = await this.records.get(TYPE_KEY, id);
|
|
146
|
+
if (row === null)
|
|
147
|
+
throw new common_1.ForbiddenException(MSG_FORBIDDEN);
|
|
148
|
+
}
|
|
149
|
+
catch (cause) {
|
|
150
|
+
if (isDenial(cause))
|
|
151
|
+
await this.auditDenied(actor, 'read', id);
|
|
152
|
+
throw cause;
|
|
153
|
+
}
|
|
154
|
+
const f = row.fields;
|
|
155
|
+
const recordTenant = f.tenantId;
|
|
156
|
+
if (!keyBelongsTo(f.storageKey, recordTenant)) {
|
|
157
|
+
// The row points outside its own tenant: an integrity fault, never a cross-tenant read.
|
|
158
|
+
this.logger.error(`stored_file ${String(row.id)} key outside its tenant prefix; refused`);
|
|
159
|
+
await this.auditFailure(row, actor, 'key_prefix_mismatch');
|
|
160
|
+
throw new common_1.ServiceUnavailableException(MSG_UNAVAILABLE);
|
|
161
|
+
}
|
|
162
|
+
const key = f.storageKey;
|
|
163
|
+
let bytes;
|
|
164
|
+
try {
|
|
165
|
+
({ bytes } = await store.get(key));
|
|
166
|
+
}
|
|
167
|
+
catch (cause) {
|
|
168
|
+
if (cause instanceof core_1.ObjectNotFoundError) {
|
|
169
|
+
this.logger.error(`stored object missing: ${key}`);
|
|
170
|
+
await this.auditFailure(row, actor, 'object_missing');
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
this.logger.error(`file get failed (${errorName(cause)}): ${key}`);
|
|
174
|
+
}
|
|
175
|
+
throw new common_1.ServiceUnavailableException(MSG_UNAVAILABLE);
|
|
176
|
+
}
|
|
177
|
+
const sha = typeof f.sha256 === 'string' ? f.sha256.toLowerCase() : '';
|
|
178
|
+
if (sha.length !== 64 || sha256Hex(bytes) !== sha) {
|
|
179
|
+
this.logger.error(`stored object failed its sha256 check: ${key}`);
|
|
180
|
+
await this.auditFailure(row, actor, 'sha256_mismatch');
|
|
181
|
+
throw new common_1.ServiceUnavailableException(MSG_UNAVAILABLE);
|
|
182
|
+
}
|
|
183
|
+
const sizeBytes = Number(f.sizeBytes);
|
|
184
|
+
// Fail closed: no bytes leave without their read event (a sink error propagates).
|
|
185
|
+
await this.audit.record(this.event(actor, 'read', row, { sha256: sha, sizeBytes, outcome: 'success' }));
|
|
186
|
+
return {
|
|
187
|
+
bytes,
|
|
188
|
+
contentType: this.servableType(f.contentType),
|
|
189
|
+
originalName: String(f.originalName),
|
|
190
|
+
sizeBytes,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Hard delete (team default): the record in a transaction (gated and audited by the pipe), the
|
|
195
|
+
* object after commit, then one `file_access` delete event (SPEC §5 K). A foreign key is never
|
|
196
|
+
* passed to the store.
|
|
197
|
+
*/
|
|
198
|
+
async remove(uuid) {
|
|
199
|
+
const store = this.requireStore();
|
|
200
|
+
const actor = this.records.requestPrincipal();
|
|
201
|
+
const id = (0, engine_1.canonicalUuid)(uuid);
|
|
202
|
+
let row;
|
|
203
|
+
let key;
|
|
204
|
+
try {
|
|
205
|
+
if (id === null)
|
|
206
|
+
throw new common_1.ForbiddenException(MSG_FORBIDDEN);
|
|
207
|
+
({ row, key } = await this.records.transaction(async (tx) => {
|
|
208
|
+
const found = await tx.get(TYPE_KEY, id);
|
|
209
|
+
if (found === null)
|
|
210
|
+
throw new common_1.ForbiddenException(MSG_FORBIDDEN);
|
|
211
|
+
await tx.delete(TYPE_KEY, id);
|
|
212
|
+
const k = found.fields.storageKey;
|
|
213
|
+
return { row: found, key: keyBelongsTo(k, found.fields.tenantId) ? k : null };
|
|
214
|
+
}));
|
|
215
|
+
}
|
|
216
|
+
catch (cause) {
|
|
217
|
+
if (isDenial(cause))
|
|
218
|
+
await this.auditDenied(actor, 'delete', id);
|
|
219
|
+
throw cause;
|
|
220
|
+
}
|
|
221
|
+
let objectDeleted = false;
|
|
222
|
+
if (key === null) {
|
|
223
|
+
this.logger.error(`stored_file ${String(row.id)} key outside its tenant prefix; object not deleted`);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
objectDeleted = await this.deleteObject(store, key, 'post-commit');
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
await this.audit.record(this.event(actor, 'delete', row, { objectDeleted, outcome: 'success' }));
|
|
230
|
+
}
|
|
231
|
+
catch (cause) {
|
|
232
|
+
// The record delete is committed and audited by the pipe (`admin_action`); a failed
|
|
233
|
+
// file_access row must not invite a retry that now answers 403.
|
|
234
|
+
this.logger.error(`file_access delete audit failed (${errorName(cause)})`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
requireStore() {
|
|
238
|
+
if (this.objectStore === null || this.objectStore === undefined) {
|
|
239
|
+
throw new common_1.NotImplementedException(MSG_NOT_CONFIGURED);
|
|
240
|
+
}
|
|
241
|
+
return this.objectStore;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Delete an object; true when the store confirmed it. On failure log the key only, count the
|
|
245
|
+
* orphan and answer false. Never throws.
|
|
246
|
+
*/
|
|
247
|
+
async deleteObject(store, key, phase) {
|
|
248
|
+
try {
|
|
249
|
+
await store.delete(key);
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
catch (cause) {
|
|
253
|
+
this.orphans += 1;
|
|
254
|
+
this.logger.error(`orphaned object after ${phase} delete failed (${errorName(cause)}): ${key}`);
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* A `file_access` failure event for a 403 (missing, foreign, malformed or engine-denied: one
|
|
260
|
+
* reason, since the caller may not learn which). Best effort; the 403 is thrown regardless.
|
|
261
|
+
*/
|
|
262
|
+
async auditDenied(actor, action, id) {
|
|
263
|
+
try {
|
|
264
|
+
await this.audit.record(this.event(actor, action, id, { outcome: 'failure', reason: 'denied_or_missing' }));
|
|
265
|
+
}
|
|
266
|
+
catch (cause) {
|
|
267
|
+
this.logger.error(`file_access denial audit failed (${errorName(cause)})`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/** A `file_access` failure event (best effort; the 503 is thrown regardless). */
|
|
271
|
+
async auditFailure(row, actor, reason) {
|
|
272
|
+
try {
|
|
273
|
+
await this.audit.record(this.event(actor, 'read', row, { outcome: 'failure', reason }));
|
|
274
|
+
}
|
|
275
|
+
catch (cause) {
|
|
276
|
+
this.logger.error(`file_access failure audit failed (${errorName(cause)})`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/** `target` is the fetched row, or for a denial only the requested uuid (null if malformed). */
|
|
280
|
+
event(actor, action, target, attributes) {
|
|
281
|
+
const row = typeof target === 'string' || target === null ? null : target;
|
|
282
|
+
const recordId = row !== null ? String(row.id) : target;
|
|
283
|
+
const recordTenant = row !== null ? (0, engine_1.canonicalUuid)(row.fields.tenantId) : null;
|
|
284
|
+
return {
|
|
285
|
+
type: 'file_access',
|
|
286
|
+
occurredAtMs: Date.now(),
|
|
287
|
+
actorId: actor.id,
|
|
288
|
+
tenantId: actor.tenantId,
|
|
289
|
+
action: { key: action },
|
|
290
|
+
resource: {
|
|
291
|
+
level: 'record',
|
|
292
|
+
typeKey: TYPE_KEY,
|
|
293
|
+
...(recordId !== null ? { recordId } : {}),
|
|
294
|
+
...(recordTenant !== null ? { tenantId: recordTenant } : {}),
|
|
295
|
+
},
|
|
296
|
+
attributes,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
exports.FilesService = FilesService;
|
|
301
|
+
exports.FilesService = FilesService = FilesService_1 = __decorate([
|
|
302
|
+
(0, common_1.Injectable)(),
|
|
303
|
+
__metadata("design:paramtypes", [Function, Object, Object, Array])
|
|
304
|
+
], FilesService);
|
|
305
|
+
/** A 403-class outcome: our own missing/foreign/malformed refusal or an engine gate deny. */
|
|
306
|
+
function isDenial(cause) {
|
|
307
|
+
return cause instanceof common_1.ForbiddenException || cause instanceof engine_1.AuthorizationDeniedError;
|
|
308
|
+
}
|
|
309
|
+
function errorName(cause) {
|
|
310
|
+
return cause instanceof Error ? cause.name : 'error';
|
|
311
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The framework database names `IdentityFeatureModule.forRoot` resolved (configurable-framework-db,
|
|
3
|
+
* ADR-0037), for identity services Nest constructs (e.g. `UserService`'s email-change audit sink),
|
|
4
|
+
* so they use the SAME set as every store forRoot builds, not a second env read. Internal to
|
|
5
|
+
* identity; guard's `FRAMEWORK_SCHEMAS` is checked equal at boot.
|
|
6
|
+
*/
|
|
7
|
+
export declare const IDENTITY_FRAMEWORK_SCHEMAS: unique symbol;
|
|
8
|
+
//# sourceMappingURL=framework-schemas.token.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"framework-schemas.token.d.ts","sourceRoot":"","sources":["../../src/features/framework-schemas.token.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,eAAuC,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.IDENTITY_FRAMEWORK_SCHEMAS = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* The framework database names `IdentityFeatureModule.forRoot` resolved (configurable-framework-db,
|
|
6
|
+
* ADR-0037), for identity services Nest constructs (e.g. `UserService`'s email-change audit sink),
|
|
7
|
+
* so they use the SAME set as every store forRoot builds, not a second env read. Internal to
|
|
8
|
+
* identity; guard's `FRAMEWORK_SCHEMAS` is checked equal at boot.
|
|
9
|
+
*/
|
|
10
|
+
exports.IDENTITY_FRAMEWORK_SCHEMAS = Symbol('IDENTITY_FRAMEWORK_SCHEMAS');
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type DynamicModule } from '@nestjs/common';
|
|
2
|
+
import { type FrameworkSchemas } from '@astram/core';
|
|
2
3
|
import { type ObjectTypeInput } from '@astram/engine';
|
|
3
4
|
/**
|
|
4
5
|
* The boot check behind {@link IDENTITY_DECISION_AUTHORITY_CHECK}: with this module wired, the
|
|
@@ -8,6 +9,27 @@ import { type ObjectTypeInput } from '@astram/engine';
|
|
|
8
9
|
* different registry. (Another source next to this one is refused by `ShieldModule` itself.)
|
|
9
10
|
*/
|
|
10
11
|
export declare function assertRequestPathUsesIdentityAuthorization(authority: unknown): true;
|
|
12
|
+
/**
|
|
13
|
+
* The boot check behind {@link IDENTITY_FRAMEWORK_SCHEMAS_CHECK} (configurable-framework-db,
|
|
14
|
+
* ADR-0037; reviewer R2): the names `IdentityFeatureModule.forRoot` resolved — and handed to the
|
|
15
|
+
* engine, the audit sinks, the reader and every identity store — must equal guard's
|
|
16
|
+
* `FRAMEWORK_SCHEMAS`, or engine/guard/identity would split across databases. A guard that
|
|
17
|
+
* provides no `FRAMEWORK_SCHEMAS` (older than this seam) may only run on the default names.
|
|
18
|
+
* Throws (fail closed) with the namespaces and names only.
|
|
19
|
+
*/
|
|
20
|
+
export declare function assertFrameworkSchemasAgree(identity: FrameworkSchemas, guard: FrameworkSchemas | null | undefined): true;
|
|
21
|
+
/**
|
|
22
|
+
* The boot check behind {@link IDENTITY_FRAMEWORK_DATABASE_CHECK} (G-007 QA P2-5): the runtime
|
|
23
|
+
* database identity's engine and authz pool connect to (derived from `forRoot({ schemas })`)
|
|
24
|
+
* must equal guard's `APP_CONFIG.db.database`, or unqualified queries of guard and identity would
|
|
25
|
+
* land in different databases. No guard `APP_CONFIG` visible ⇒ nothing to compare. Throws (fail
|
|
26
|
+
* closed) with database names only.
|
|
27
|
+
*/
|
|
28
|
+
export declare function assertFrameworkDatabaseAgrees(identityDatabase: string, guardConfig: {
|
|
29
|
+
readonly db?: {
|
|
30
|
+
readonly database?: string;
|
|
31
|
+
};
|
|
32
|
+
} | null | undefined): true;
|
|
11
33
|
/**
|
|
12
34
|
* IdentityFeatureModule — the framework-owned identity FEATURE surface (company/user/role CRUD;
|
|
13
35
|
* more in later phases) as thin controllers over the engine's RecordsService. The CONSUMER imports
|
|
@@ -34,10 +56,16 @@ export declare class IdentityFeatureModule {
|
|
|
34
56
|
* cross-tenant / policy authority, grant ceiling, guided rule-builder schema and owner-share
|
|
35
57
|
* resolver. Without them a consumer type is unregistered everywhere (denied), not policy-
|
|
36
58
|
* authorable and not shareable ("type '…' is not registered — cannot share").
|
|
59
|
+
* @param schemas the resolved framework database names (configurable-framework-db, ADR-0037).
|
|
60
|
+
* Absent ⇒ resolved ONCE here from `SHIELD_DB_NAMESPACE` (unset ⇒ `shield_*`; invalid ⇒
|
|
61
|
+
* throws). The one set is handed to the engine (registrations + its audit sink), the seven
|
|
62
|
+
* audit sinks, the audit reader and every identity store; a boot check refuses names that
|
|
63
|
+
* differ from guard's `FRAMEWORK_SCHEMAS`. Never read per request.
|
|
37
64
|
*/
|
|
38
|
-
static forRoot({ rootIds, extraObjects, }?: {
|
|
65
|
+
static forRoot({ rootIds, extraObjects, schemas: givenSchemas, }?: {
|
|
39
66
|
rootIds?: readonly string[];
|
|
40
67
|
extraObjects?: readonly ObjectTypeInput[];
|
|
68
|
+
schemas?: FrameworkSchemas;
|
|
41
69
|
}): DynamicModule;
|
|
42
70
|
}
|
|
43
71
|
//# sourceMappingURL=identity-feature.module.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"identity-feature.module.d.ts","sourceRoot":"","sources":["../../src/features/identity-feature.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEpE,OAAO,
|
|
1
|
+
{"version":3,"file":"identity-feature.module.d.ts","sourceRoot":"","sources":["../../src/features/identity-feature.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEpE,OAAO,EAGL,KAAK,gBAAgB,EAEtB,MAAM,cAAc,CAAC;AACtB,OAAO,EAeL,KAAK,eAAe,EACrB,MAAM,gBAAgB,CAAC;AA0GxB;;;;;;GAMG;AACH,wBAAgB,0CAA0C,CAAC,SAAS,EAAE,OAAO,GAAG,IAAI,CAUnF;AAKD;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,gBAAgB,EAC1B,KAAK,EAAE,gBAAgB,GAAG,IAAI,GAAG,SAAS,GACzC,IAAI,CAuBN;AAKD;;;;;;GAMG;AACH,wBAAgB,6BAA6B,CAC3C,gBAAgB,EAAE,MAAM,EACxB,WAAW,EAAE;IAAE,QAAQ,CAAC,EAAE,CAAC,EAAE;QAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,GAC/E,IAAI,CAQN;AAwBD;;;;;;;;;;GAUG;AACH,qBACa,qBAAqB;IAChC;;;;;;;;;;;;;;;;;;;OAmBG;IACH,MAAM,CAAC,OAAO,CAAC,EACb,OAAO,EACP,YAAiB,EACjB,OAAO,EAAE,YAAY,GACtB,GAAE;QACD,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QAC5B,YAAY,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;QAC1C,OAAO,CAAC,EAAE,gBAAgB,CAAC;KACvB,GAAG,aAAa;CA2jBvB"}
|