@astram/identity 0.9.3 → 0.11.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 (44) hide show
  1. package/dist/auth-facts-keys.d.ts +16 -0
  2. package/dist/auth-facts-keys.d.ts.map +1 -1
  3. package/dist/auth-facts-keys.js +26 -10
  4. package/dist/cross-tenant-grant.object.d.ts +2 -2
  5. package/dist/cross-tenant-grant.object.d.ts.map +1 -1
  6. package/dist/cross-tenant-grant.object.js +5 -2
  7. package/dist/features/authz/catalog-bootstrap.provider.d.ts +4 -1
  8. package/dist/features/authz/catalog-bootstrap.provider.d.ts.map +1 -1
  9. package/dist/features/authz/catalog-bootstrap.provider.js +37 -6
  10. package/dist/features/authz/record-authorization.d.ts.map +1 -1
  11. package/dist/features/authz/record-authorization.js +3 -0
  12. package/dist/features/cross-tenant/cross-tenant-grant-read.service.d.ts +6 -5
  13. package/dist/features/cross-tenant/cross-tenant-grant-read.service.d.ts.map +1 -1
  14. package/dist/features/cross-tenant/cross-tenant-grant-read.service.js +6 -5
  15. package/dist/features/files/files.controller.d.ts +27 -0
  16. package/dist/features/files/files.controller.d.ts.map +1 -0
  17. package/dist/features/files/files.controller.js +94 -0
  18. package/dist/features/files/files.service.d.ts +87 -0
  19. package/dist/features/files/files.service.d.ts.map +1 -0
  20. package/dist/features/files/files.service.js +311 -0
  21. package/dist/features/identity-feature.module.d.ts.map +1 -1
  22. package/dist/features/identity-feature.module.js +40 -11
  23. package/dist/features/policy/policy.service.d.ts +2 -1
  24. package/dist/features/policy/policy.service.d.ts.map +1 -1
  25. package/dist/features/policy/policy.service.js +15 -0
  26. package/dist/features/share/share.service.d.ts +19 -2
  27. package/dist/features/share/share.service.d.ts.map +1 -1
  28. package/dist/features/share/share.service.js +36 -2
  29. package/dist/index.d.ts +9 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +13 -1
  32. package/dist/objects.d.ts.map +1 -1
  33. package/dist/objects.js +2 -0
  34. package/dist/record-share.object.d.ts +1 -1
  35. package/dist/record-share.object.d.ts.map +1 -1
  36. package/dist/record-share.object.js +4 -1
  37. package/dist/service-account.object.d.ts +3 -2
  38. package/dist/service-account.object.d.ts.map +1 -1
  39. package/dist/service-account.object.js +4 -2
  40. package/dist/stored-file.object.d.ts +35 -0
  41. package/dist/stored-file.object.d.ts.map +1 -0
  42. package/dist/stored-file.object.js +94 -0
  43. package/migrations/1710000000022-CreateStoredFile.cjs +43 -0
  44. package/package.json +8 -4
@@ -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
+ }
@@ -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,EAYL,KAAK,eAAe,EACrB,MAAM,gBAAgB,CAAC;AA+FxB;;;;;;GAMG;AACH,wBAAgB,0CAA0C,CAAC,SAAS,EAAE,OAAO,GAAG,IAAI,CAUnF;AAiBD;;;;;;;;;;GAUG;AACH,qBACa,qBAAqB;IAChC;;;;;;;;;;;;;;OAcG;IACH,MAAM,CAAC,OAAO,CAAC,EACb,OAAO,EACP,YAAiB,GAClB,GAAE;QACD,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QAC5B,YAAY,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;KACtC,GAAG,aAAa;CAoevB"}
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;AAGpE,OAAO,EAcL,KAAK,eAAe,EACrB,MAAM,gBAAgB,CAAC;AAiGxB;;;;;;GAMG;AACH,wBAAgB,0CAA0C,CAAC,SAAS,EAAE,OAAO,GAAG,IAAI,CAUnF;AAoBD;;;;;;;;;;GAUG;AACH,qBACa,qBAAqB;IAChC;;;;;;;;;;;;;;OAcG;IACH,MAAM,CAAC,OAAO,CAAC,EACb,OAAO,EACP,YAAiB,GAClB,GAAE;QACD,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QAC5B,YAAY,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;KACtC,GAAG,aAAa;CAigBvB"}
@@ -56,6 +56,8 @@ const audit_controller_js_1 = require("./audit/audit.controller.js");
56
56
  const cross_tenant_grant_read_service_js_1 = require("./cross-tenant/cross-tenant-grant-read.service.js");
57
57
  const cross_tenant_grant_controller_js_1 = require("./cross-tenant/cross-tenant-grant.controller.js");
58
58
  const grant_authorize_js_1 = require("./cross-tenant/grant-authorize.js");
59
+ const files_service_js_1 = require("./files/files.service.js");
60
+ const files_controller_js_1 = require("./files/files.controller.js");
59
61
  /** DI token of the provider that links the guard's Redis version cache to the bumper (ADR-0035). */
60
62
  const IDENTITY_VERSION_PUBLISHER_LINK = Symbol('IDENTITY_VERSION_PUBLISHER_LINK');
61
63
  /** DI token of the boot check that warns when a user disable cannot revoke sessions. */
@@ -92,6 +94,7 @@ function assertRequestPathUsesIdentityAuthorization(authority) {
92
94
  /** Map guard's env DbConfig → the engine's EngineDbConfig (adds the pool cap the engine needs). */
93
95
  function engineDbFromEnv() {
94
96
  const db = (0, guard_1.loadDbConfig)();
97
+ const ssl = (0, guard_1.mariadbSslOption)(db.tls);
95
98
  return {
96
99
  host: db.host,
97
100
  port: db.port,
@@ -101,6 +104,8 @@ function engineDbFromEnv() {
101
104
  // The engine's runtime CRUD pool cap. A conservative default; the app can raise it later via
102
105
  // an explicit EngineModule.forRoot if it needs more. (ADR-0018 budget lives app-side.)
103
106
  connectionLimit: Number(process.env.APP_DB_MAX_CONNECTIONS ?? 10),
107
+ // Phase 16: APP_DB_TLS / APP_DB_TLS_CA (production accounts are REQUIRE SSL).
108
+ ...(ssl ? { ssl } : {}),
104
109
  };
105
110
  }
106
111
  /**
@@ -152,10 +157,6 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
152
157
  // A role delete (Phase 8 #2): its holder tenants, read before the delete cascades postings.
153
158
  tenantsForRole: (roleUuid) => bumper.tenantsForRole(roleUuid, authzPool),
154
159
  };
155
- // The core ModuleRegistry the catalog reconcile job needs, built from the identity
156
- // object registrations only (guard's bridge reflects decorated classes → registry): it
157
- // catalogs the identity permissions. Authorities use the shared `recordAuthorization.registry`.
158
- const identityRegistry = (0, guard_1.buildIdentityModuleRegistry)([...objects_js_1.identityObjects]);
159
160
  // The grant-admin collaborators built at forRoot() over the shared pool (mirrors the
160
161
  // postingVersionBump closure pattern above). GrantCeiling (ADR-0029) enforces the
161
162
  // self-escalation ceiling: a non-root grantor may only grant a key they themselves hold —
@@ -269,7 +270,15 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
269
270
  // The read side (inbox/mine/incoming/offerable-types) over the same pool + the merged object
270
271
  // set (offerable = tenantScoped types only, the consumer's included, KI #57) + the per-type
271
272
  // action vocabulary.
272
- const crossTenantGrantReadService = new cross_tenant_grant_read_service_js_1.CrossTenantGrantReadService(authzPool, [...objects_js_1.identityObjects, ...extraObjects], guard_1.actionsFor);
273
+ // The ONE registration-based action lookup (ADR-0038): typeKey → the type's declared
274
+ // vocabulary (`effectiveActions`, validated) over identity + the consumer's extraObjects.
275
+ // `undefined` for an unregistered type. Shared by every action-vocabulary consumer here.
276
+ const actionsByTypeKey = new Map([...objects_js_1.identityObjects, ...extraObjects].map((input) => {
277
+ const reg = typeof input === 'function' ? (0, engine_1.reflectObjectType)(input) : input;
278
+ return [reg.typeKey, (0, engine_1.effectiveActions)(reg)];
279
+ }));
280
+ const actionsByType = (typeKey) => actionsByTypeKey.get(typeKey);
281
+ const crossTenantGrantReadService = new cross_tenant_grant_read_service_js_1.CrossTenantGrantReadService(authzPool, [...objects_js_1.identityObjects, ...extraObjects], (typeKey) => actionsByType(typeKey) ?? []);
273
282
  // ── GUIDED RULE-BUILDER (list-scoping engine Phase 5, §9.1) ──────────────────────────────
274
283
  // The policy-authoring surface over the guard `MariaDbPolicyStore` + core `makeAuthorPolicy`.
275
284
  // Reuses the SAME shared registry + `crossTenantAuthority` (a DecisionAuthority over the
@@ -301,7 +310,7 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
301
310
  return {
302
311
  typeKey: reg.typeKey,
303
312
  label: reg.typeKey,
304
- actions: (0, guard_1.actionsFor)(reg.typeKey),
313
+ actions: actionsByType(reg.typeKey) ?? [],
305
314
  fields: reg.fields.map((f) => ({ key: f.key, type: f.type })),
306
315
  };
307
316
  });
@@ -370,7 +379,7 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
370
379
  },
371
380
  },
372
381
  });
373
- const shareService = new share_service_js_1.ShareService(recordShareWriter, auditLabels);
382
+ const shareService = new share_service_js_1.ShareService(recordShareWriter, auditLabels, actionsByType);
374
383
  // ── SERVICE ACCOUNTS + API KEYS (ADR-0033, Phase 2) ──────────────────────────────────────
375
384
  // The machine-identity admin surface: create/scope service accounts + issue/rotate/revoke
376
385
  // their `ask_…` API keys. Self-wired here over the SAME shared authzPool (which reaches BOTH
@@ -415,6 +424,13 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
415
424
  // boundary made an identity-provided authenticator resolve to `undefined` → `ask_…` bearers
416
425
  // 401'd). This module owns only the ACCOUNT ADMIN surface (store + service + controller); the
417
426
  // guard owns authentication. (ADR-0033.)
427
+ // Stored files (file-storage SPEC §5 I-K): `file_access` events go to the SAME shield_audit
428
+ // sink over the shared authzPool. Not best-effort here: FilesService decides (a read event
429
+ // that cannot be written withholds the bytes).
430
+ const filesAuditSink = new engine_1.MariaDbAuditSink({}); // shield_audit.audit_log defaults
431
+ const filesAudit = {
432
+ record: (event) => filesAuditSink.record(event, (sql, params) => authzPool.query(sql, params)),
433
+ };
418
434
  return {
419
435
  module: IdentityFeatureModule_1,
420
436
  imports: [
@@ -466,6 +482,9 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
466
482
  // Service accounts (ADR-0033) — `service-accounts/*` collides with nothing; its static
467
483
  // sub-paths (/keys, /keys/rotate, /keys/revoke) sit under `:id` and are method-distinct.
468
484
  service_account_controller_js_1.ServiceAccountController,
485
+ // Stored-file download (file-storage SPEC §3) — `files/:uuid` collides with nothing.
486
+ // Gated on `stored_file:read`; answers 501 when no object store is bound. Appended last.
487
+ files_controller_js_1.FilesController,
469
488
  ],
470
489
  providers: [
471
490
  company_service_js_1.CompanyService,
@@ -538,10 +557,12 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
538
557
  // enforcement guard can authenticate `ask_…` bearers out-of-the-box. Both useValue.
539
558
  { provide: service_account_service_js_1.ServiceAccountService, useValue: serviceAccountService },
540
559
  // The mutating half of auto-catalog: a single CatalogReconcileJob (post-boot pass +
541
- // cron), over the shared pool + the identity registry.
560
+ // cron), over the shared pool + the ONE record-authorization registry (identity objects +
561
+ // `extraObjects`, KI #70): the catalog holds exactly the keys decide() and the grant
562
+ // ceiling know, so a consumer type's keys are grantable and its rows are not orphaned.
542
563
  {
543
564
  provide: catalog_reconcile_job_js_1.CatalogReconcileJob,
544
- useValue: new catalog_reconcile_job_js_1.CatalogReconcileJob(authzPool, identityRegistry),
565
+ useValue: new catalog_reconcile_job_js_1.CatalogReconcileJob(authzPool, recordAuthorization.registry),
545
566
  },
546
567
  // The bundle half: reconciles the merged AccessBundleSpec[] (ENGINE_ALL_ACCESS_BUNDLES,
547
568
  // reflected by the engine from identityAccessBundles) into shield_authz.access_bundle.
@@ -557,10 +578,18 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
557
578
  // Boot: read-only verifyGatable (fail-closed). Post-boot: kicks the jobs once (catalog
558
579
  // THEN bundles, THEN the scope-closure reconcile) + schedules them (DISABLE_ALL_CRONS-
559
580
  // gated). Injects DiscoveryService/MetadataScanner/Reflector (from DiscoveryModule
560
- // above), the jobs, the ScopeClosureService, and ENGINE_ALL_REGISTRATIONS.
581
+ // above), the jobs, the ScopeClosureService, ENGINE_ALL_REGISTRATIONS and the record
582
+ // authorization (whose registry is the verify key universe, KI #70).
561
583
  catalog_bootstrap_provider_js_1.CatalogBootstrapProvider,
584
+ // Stored files (file-storage Phase 5): the request-principal RecordsService + the guard's
585
+ // object store (`null`/absent ⇒ every method answers 501) + the file_access audit bridge.
586
+ {
587
+ provide: files_service_js_1.FilesService,
588
+ useFactory: (records, store) => new files_service_js_1.FilesService(records, store ?? null, filesAudit),
589
+ inject: [engine_1.RecordsService, { token: guard_1.SHIELD_OBJECT_STORE, optional: true }],
590
+ },
562
591
  ],
563
- exports: [company_service_js_1.CompanyService, role_service_js_1.RoleService, user_service_js_1.UserService],
592
+ exports: [company_service_js_1.CompanyService, role_service_js_1.RoleService, user_service_js_1.UserService, files_service_js_1.FilesService],
564
593
  };
565
594
  }
566
595
  };
@@ -91,7 +91,8 @@ export interface PolicyServiceDeps {
91
91
  /**
92
92
  * The authorable-type field/action metadata (built in the module from the reflected object
93
93
  * types). Drives `schema()` — the guided builder's field + operator dropdowns. A type absent
94
- * here is NOT authorable via the builder (schema → NotFound).
94
+ * here is NOT authorable via the builder (schema → NotFound), and `author` rejects any non-`'*'`
95
+ * action for it (declared-action check, fail closed — SPEC v2 §5G).
95
96
  */
96
97
  readonly schemaSource?: readonly TypeSchemaSource[];
97
98
  readonly now?: () => number;
@@ -1 +1 @@
1
- {"version":3,"file":"policy.service.d.ts","sourceRoot":"","sources":["../../../src/features/policy/policy.service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"policy.service.d.ts","sourceRoot":"","sources":["../../../src/features/policy/policy.service.ts"],"names":[],"mappings":"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"}
@@ -106,6 +106,21 @@ class PolicyService {
106
106
  * reasons to typed errors; a transient throw PROPAGATES (→ 503).
107
107
  */
108
108
  async author(actor, policy) {
109
+ // Declared-action check (SPEC v2 §5G): before core `makeAuthorPolicy`, so an undeclared
110
+ // action saves nothing and audits nothing. `'*'` stays allowed. Fail closed: with no schema
111
+ // source (or no entry for the type) every non-'*' action is undeclared.
112
+ // SPEC v3 §11 rule 6: a record rule on `create` is rejected even when `create` is declared.
113
+ // Record rules are evaluated per record (core gate 6) and the create gate is type level, so
114
+ // such a rule would never take effect. `'*'` stays allowed (it applies to the other actions).
115
+ if (policy.action === 'create') {
116
+ throw new InvalidPolicyError("Record rules are evaluated per record and never apply at create; a rule on action 'create' would never take effect. Use the object-type create grant instead.");
117
+ }
118
+ if (policy.action !== '*') {
119
+ const declared = this.schemaByType.get(policy.typeKey)?.actions;
120
+ if (declared === undefined || !declared.includes(policy.action)) {
121
+ throw new InvalidPolicyError(`Action '${policy.action}' is not declared on type '${policy.typeKey}'.`);
122
+ }
123
+ }
109
124
  const mutation = {
110
125
  kind: 'author_policy',
111
126
  admin: actor,
@@ -69,17 +69,34 @@ export interface ShareRequest {
69
69
  readonly granteeUuid: string;
70
70
  readonly action: string;
71
71
  }
72
+ /** typeKey → the type's declared action vocabulary (`effectiveActions`); `undefined` = unknown type. */
73
+ export type ActionsByType = (typeKey: string) => readonly string[] | undefined;
72
74
  export declare class ShareService {
73
75
  private readonly writer;
74
76
  private readonly labels;
75
- constructor(writer: RecordShareWriter | ShareWriterLike, labels: ShareLabelResolver);
77
+ private readonly actionsByType;
78
+ /**
79
+ * @param actionsByType the registration-based action lookup built in `forRoot` (SPEC v2 §5G).
80
+ * Required: share rejects an unknown type or an action the type does not declare (`'*'`
81
+ * stays allowed) with 400 BEFORE the writer is called; unshare rejects only an unknown type
82
+ * (SPEC v3 §5G: revoking cannot grant).
83
+ */
84
+ constructor(writer: RecordShareWriter | ShareWriterLike, labels: ShareLabelResolver, actionsByType: ActionsByType);
85
+ /** 400 on an unknown type; returns the type's declared actions (§5G, §13). */
86
+ private assertKnownType;
87
+ /** 400 on an unknown type or an undeclared action — runs before any writer call (§5G). */
88
+ private assertDeclaredAction;
76
89
  /**
77
90
  * Share a record the caller owns with a user/role for an action. The tenant + sharer are the
78
91
  * caller's own (from `actor`), NEVER the body — a forged owner/tenant cannot reach the writer.
79
92
  * The writer proves `actor.id` owns the record before writing (fail-closed).
80
93
  */
81
94
  share(actor: Principal, req: ShareRequest): Promise<void>;
82
- /** Revoke a share the caller owns. Same owner bound (only the owner may unshare). */
95
+ /**
96
+ * Revoke a share the caller owns. Same owner bound (only the owner may unshare).
97
+ * SPEC v3 §5G: NO declared-action check — revoking cannot grant, so a share for an action the
98
+ * type has since stopped declaring stays revocable. An unknown type still gets 400 (§13).
99
+ */
83
100
  unshare(actor: Principal, req: ShareRequest): Promise<void>;
84
101
  /**
85
102
  * List the shares on a record the caller owns — grantee NAMES resolved (no raw uuid). Only the
@@ -1 +1 @@
1
- {"version":3,"file":"share.service.d.ts","sourceRoot":"","sources":["../../../src/features/share/share.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEzE,6FAA6F;AAC7F,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACrF;AAED,4FAA4F;AAC5F,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,KAAK,EAAE;QACX,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,WAAW,EAAE,gBAAgB,CAAC;QAC9B,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;KAClB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClB,OAAO,CAAC,KAAK,EAAE;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,WAAW,EAAE,gBAAgB,CAAC;QAC9B,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;KAClB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClB,mBAAmB,CAAC,KAAK,EAAE;QACzB,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;KAClB,GAAG,OAAO,CACT,aAAa,CAAC;QACZ,WAAW,EAAE,gBAAgB,CAAC;QAC9B,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC,CACH,CAAC;CACH;AAED,oFAAoF;AACpF,MAAM,WAAW,SAAS;IACxB,yDAAyD;IACzD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC,gFAAgF;IAChF,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,kGAAkG;AAClG,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,YAAY;IAErB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBADN,MAAM,EAAE,iBAAiB,GAAG,eAAe,EAC3C,MAAM,EAAE,kBAAkB;IAG7C;;;;OAIG;IACG,KAAK,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAY/D,qFAAqF;IAC/E,OAAO,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAYjE;;;OAGG;IACG,UAAU,CACd,KAAK,EAAE,SAAS,EAChB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,SAAS,SAAS,EAAE,CAAC;CAoBjC"}
1
+ {"version":3,"file":"share.service.d.ts","sourceRoot":"","sources":["../../../src/features/share/share.service.ts"],"names":[],"mappings":"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"}
@@ -1,12 +1,40 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ShareService = void 0;
4
+ const identity_errors_js_1 = require("../shared/identity-errors.js");
4
5
  class ShareService {
5
6
  writer;
6
7
  labels;
7
- constructor(writer, labels) {
8
+ actionsByType;
9
+ /**
10
+ * @param actionsByType the registration-based action lookup built in `forRoot` (SPEC v2 §5G).
11
+ * Required: share rejects an unknown type or an action the type does not declare (`'*'`
12
+ * stays allowed) with 400 BEFORE the writer is called; unshare rejects only an unknown type
13
+ * (SPEC v3 §5G: revoking cannot grant).
14
+ */
15
+ constructor(writer, labels, actionsByType) {
8
16
  this.writer = writer;
9
17
  this.labels = labels;
18
+ this.actionsByType = actionsByType;
19
+ }
20
+ /** 400 on an unknown type; returns the type's declared actions (§5G, §13). */
21
+ assertKnownType(req) {
22
+ // Fail closed: a missing lookup (an untyped JS caller) rejects rather than skipping the check.
23
+ if (typeof this.actionsByType !== 'function') {
24
+ throw new identity_errors_js_1.IdentityBadRequestError(`Type '${req.typeKey}' is not registered.`);
25
+ }
26
+ const declared = this.actionsByType(req.typeKey);
27
+ if (declared === undefined) {
28
+ throw new identity_errors_js_1.IdentityBadRequestError(`Type '${req.typeKey}' is not registered.`);
29
+ }
30
+ return declared;
31
+ }
32
+ /** 400 on an unknown type or an undeclared action — runs before any writer call (§5G). */
33
+ assertDeclaredAction(req) {
34
+ const declared = this.assertKnownType(req);
35
+ if (req.action !== '*' && !declared.includes(req.action)) {
36
+ throw new identity_errors_js_1.IdentityBadRequestError(`Action '${req.action}' is not declared on type '${req.typeKey}'.`);
37
+ }
10
38
  }
11
39
  /**
12
40
  * Share a record the caller owns with a user/role for an action. The tenant + sharer are the
@@ -14,6 +42,7 @@ class ShareService {
14
42
  * The writer proves `actor.id` owns the record before writing (fail-closed).
15
43
  */
16
44
  async share(actor, req) {
45
+ this.assertDeclaredAction(req);
17
46
  await this.writer.share({
18
47
  tenantId: actor.tenantId, // SERVER-resolved
19
48
  typeKey: req.typeKey,
@@ -24,8 +53,13 @@ class ShareService {
24
53
  sharerId: actor.id, // SERVER-resolved — the writer verifies ownership
25
54
  });
26
55
  }
27
- /** Revoke a share the caller owns. Same owner bound (only the owner may unshare). */
56
+ /**
57
+ * Revoke a share the caller owns. Same owner bound (only the owner may unshare).
58
+ * SPEC v3 §5G: NO declared-action check — revoking cannot grant, so a share for an action the
59
+ * type has since stopped declaring stays revocable. An unknown type still gets 400 (§13).
60
+ */
28
61
  async unshare(actor, req) {
62
+ this.assertKnownType(req);
29
63
  await this.writer.unshare({
30
64
  tenantId: actor.tenantId,
31
65
  typeKey: req.typeKey,
package/dist/index.d.ts CHANGED
@@ -14,6 +14,7 @@ export { MembershipPosting } from './membership-posting.object.js';
14
14
  export { CompanyDepartment } from './company-department.object.js';
15
15
  export { CompanyBranch } from './company-branch.object.js';
16
16
  export { ServiceAccount } from './service-account.object.js';
17
+ export { StoredFile } from './stored-file.object.js';
17
18
  export { identityMigrationsDir } from './migrations-dir.js';
18
19
  /**
19
20
  * All identity objects, registered with the engine at boot (same set + order as before).
@@ -29,6 +30,9 @@ export { identityObjects } from './objects.js';
29
30
  * control-plane keys WITHOUT guard importing this package.
30
31
  */
31
32
  export { identityAuthActionKeys, identityControlPlaneKeys } from './auth-facts-keys.js';
33
+ /** KI #70: the same key universe with the consumer's `extraObjects` (pass the SAME list to
34
+ * `IdentityFeatureModule.forRoot({ extraObjects })`). */
35
+ export { buildIdentityAuthKeys, type IdentityAuthKeys } from './auth-facts-keys.js';
32
36
  /**
33
37
  * The framework-owned identity FEATURE module (company/user/role CRUD). The consumer imports
34
38
  * `IdentityFeatureModule.forRoot()` in one line alongside `ShieldModule.forRoot(...)` (ADR-0026);
@@ -36,6 +40,11 @@ export { identityAuthActionKeys, identityControlPlaneKeys } from './auth-facts-k
36
40
  * `identityObjects`, and mounts the CRUD controllers — no separate engine setup needed.
37
41
  */
38
42
  export { IdentityFeatureModule } from './features/identity-feature.module.js';
43
+ /**
44
+ * Stored files (file-storage SPEC §5 I-K): `store` / `open` / `remove` on the engine pipe, provided
45
+ * and exported by `IdentityFeatureModule`. Apps call it from their own `@Classified` routes.
46
+ */
47
+ export { FilesService, type StoreFileInput, type StoredFileSummary, type OpenedFile, } from './features/files/files.service.js';
39
48
  /**
40
49
  * The ADR-0011 per-request session-facts reader (ALS hot path), re-exported so a consumer
41
50
  * that wants to construct it directly can do so.