@astram/engine 0.4.0 → 0.5.1
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/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -6
- package/dist/internal.d.ts +10 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +13 -0
- package/dist/pipe/record-engine.d.ts +2 -1
- package/dist/pipe/record-engine.d.ts.map +1 -1
- package/dist/pipe/record-engine.js +5 -1
- package/dist/pipe/stages/fetch-record.d.ts +3 -1
- package/dist/pipe/stages/fetch-record.d.ts.map +1 -1
- package/dist/pipe/stages/fetch-record.js +3 -1
- package/dist/pipe/stages/permission-gate.d.ts +29 -0
- package/dist/pipe/stages/permission-gate.d.ts.map +1 -1
- package/dist/pipe/stages/permission-gate.js +63 -13
- package/package.json +13 -2
package/dist/index.d.ts
CHANGED
|
@@ -11,9 +11,11 @@
|
|
|
11
11
|
*
|
|
12
12
|
* FRAMEWORK-FACING (composition/tests; a FEATURE must NOT import these — the app's
|
|
13
13
|
* architectural lint rule blocks raw DB access under src/features/**):
|
|
14
|
-
* - the
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* - the `MariaDbAuditSink` adapter, the `RecordEngineImpl`, the `TableRegistry`, and the
|
|
15
|
+
* ports. Exported so the framework + integration tests can compose the engine
|
|
16
|
+
* directly; NOT for feature code.
|
|
17
|
+
* - The raw unit of work (`MariaDbUnitOfWork`, it owns a pool) is NOT here: it is on
|
|
18
|
+
* `@astram/engine/internal`, which only the composition root may import.
|
|
17
19
|
*/
|
|
18
20
|
export type { RecordEngine, AccessDeclaration, FieldSpec, FieldType, TrustedSource, IdentityRole, PkStrategy, JunctionSpec, RecordInput, RecordId, RecordView, ListQuery, RecordPage, Principal, ReadScope, ReadScopeCreateMode, } from './types.js';
|
|
19
21
|
export { READ_SCOPE_NODES_TOKEN } from './types.js';
|
|
@@ -24,7 +26,7 @@ export { EngineModule, RECORD_ENGINE, ENGINE_ALL_REGISTRATIONS, ENGINE_ALL_ACCES
|
|
|
24
26
|
export { TableRegistry, columnOf, uuidPkColumn, isOwned, ownerColumn, tenantColumnOf, columnForRole, requireRole, } from './registry.js';
|
|
25
27
|
export { RecordEngineImpl, type RecordEngineDeps } from './pipe/record-engine.js';
|
|
26
28
|
export { RecordsService, BoundRecords, type ServiceAccountResolver, SERVICE_ACCOUNT_RESOLVER, } from './pipe/records-service.js';
|
|
27
|
-
export {
|
|
29
|
+
export type { EngineDbConfig } from './adapters/mariadb-unit-of-work.js';
|
|
28
30
|
export { MariaDbAuditSink, type AuditSinkConfig } from './adapters/mariadb-audit-sink.js';
|
|
29
31
|
export type { QueryFn, UnitOfWork, DecisionPort, AuditPort } from './ports.js';
|
|
30
32
|
export type { Stage, WriteContext, WriteOp, PipeBase } from './pipe/context.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,YAAY,EACV,YAAY,EACZ,iBAAiB,EACjB,SAAS,EACT,SAAS,EACT,aAAa,EACb,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,WAAW,EACX,QAAQ,EACR,UAAU,EACV,SAAS,EACT,UAAU,EACV,SAAS,EACT,SAAS,EACT,mBAAmB,GACpB,MAAM,YAAY,CAAC;AAKpB,OAAO,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEpD,OAAO,EACL,WAAW,EACX,2BAA2B,EAC3B,iBAAiB,EACjB,wBAAwB,EACxB,eAAe,EACf,sBAAsB,GACvB,MAAM,aAAa,CAAC;AAiBrB,OAAO,EACL,UAAU,EACV,KAAK,EACL,UAAU,EACV,iBAAiB,EACjB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,iBAAiB,GACvB,MAAM,uCAAuC,CAAC;AAO/C,OAAO,EACL,YAAY,EACZ,mBAAmB,EACnB,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,gBAAgB,GACtB,MAAM,yCAAyC,CAAC;AAEjD,OAAO,EACL,YAAY,EACZ,aAAa,EACb,wBAAwB,EACxB,yBAAyB,EACzB,KAAK,mBAAmB,EACxB,KAAK,eAAe,GACrB,MAAM,2BAA2B,CAAC;AAGnC,OAAO,EACL,aAAa,EACb,QAAQ,EACR,YAAY,EACZ,OAAO,EACP,WAAW,EACX,cAAc,EACd,aAAa,EACb,WAAW,GACZ,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAClF,OAAO,EACL,cAAc,EACd,YAAY,EACZ,KAAK,sBAAsB,EAC3B,wBAAwB,GACzB,MAAM,2BAA2B,CAAC;AAGnC,YAAY,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AACzE,OAAO,EAAE,gBAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,kCAAkC,CAAC;AAC1F,YAAY,EAAE,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC/E,YAAY,EAAE,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -12,12 +12,14 @@
|
|
|
12
12
|
*
|
|
13
13
|
* FRAMEWORK-FACING (composition/tests; a FEATURE must NOT import these — the app's
|
|
14
14
|
* architectural lint rule blocks raw DB access under src/features/**):
|
|
15
|
-
* - the
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* - the `MariaDbAuditSink` adapter, the `RecordEngineImpl`, the `TableRegistry`, and the
|
|
16
|
+
* ports. Exported so the framework + integration tests can compose the engine
|
|
17
|
+
* directly; NOT for feature code.
|
|
18
|
+
* - The raw unit of work (`MariaDbUnitOfWork`, it owns a pool) is NOT here: it is on
|
|
19
|
+
* `@astram/engine/internal`, which only the composition root may import.
|
|
18
20
|
*/
|
|
19
21
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
-
exports.MariaDbAuditSink = exports.
|
|
22
|
+
exports.MariaDbAuditSink = exports.SERVICE_ACCOUNT_RESOLVER = exports.BoundRecords = exports.RecordsService = exports.RecordEngineImpl = exports.requireRole = exports.columnForRole = exports.tenantColumnOf = exports.ownerColumn = exports.isOwned = exports.uuidPkColumn = exports.columnOf = exports.TableRegistry = exports.ENGINE_ALL_ACCESS_BUNDLES = exports.ENGINE_ALL_REGISTRATIONS = exports.RECORD_ENGINE = exports.EngineModule = exports.reflectAccessBundle = exports.AccessBundle = exports.reflectObjectType = exports.PrimaryKey = exports.Field = exports.ObjectType = exports.EngineUnavailableError = exports.ValidationError = exports.AuthorizationDeniedError = exports.RegistrationError = exports.CapabilityNotSupportedError = exports.EngineError = exports.READ_SCOPE_NODES_TOKEN = void 0;
|
|
21
23
|
// The read-scope subtree-id placeholder token (ADR-0027) — a `subtreeSubquery` template
|
|
22
24
|
// uses it; the engine expands it to a `?`-list. Exported so per-type declarations can
|
|
23
25
|
// reference the canonical token (rather than a stringly-typed duplicate).
|
|
@@ -79,7 +81,5 @@ var records_service_js_1 = require("./pipe/records-service.js");
|
|
|
79
81
|
Object.defineProperty(exports, "RecordsService", { enumerable: true, get: function () { return records_service_js_1.RecordsService; } });
|
|
80
82
|
Object.defineProperty(exports, "BoundRecords", { enumerable: true, get: function () { return records_service_js_1.BoundRecords; } });
|
|
81
83
|
Object.defineProperty(exports, "SERVICE_ACCOUNT_RESOLVER", { enumerable: true, get: function () { return records_service_js_1.SERVICE_ACCOUNT_RESOLVER; } });
|
|
82
|
-
var mariadb_unit_of_work_js_1 = require("./adapters/mariadb-unit-of-work.js");
|
|
83
|
-
Object.defineProperty(exports, "MariaDbUnitOfWork", { enumerable: true, get: function () { return mariadb_unit_of_work_js_1.MariaDbUnitOfWork; } });
|
|
84
84
|
var mariadb_audit_sink_js_1 = require("./adapters/mariadb-audit-sink.js");
|
|
85
85
|
Object.defineProperty(exports, "MariaDbAuditSink", { enumerable: true, get: function () { return mariadb_audit_sink_js_1.MariaDbAuditSink; } });
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @astram/engine/internal — composition-root and framework-test surface (Phase 8 #5).
|
|
3
|
+
*
|
|
4
|
+
* The raw unit of work is NOT on the package entry point (`@astram/engine`): feature code gets
|
|
5
|
+
* only the RecordEngine facade (`RECORD_ENGINE` / `RecordsService`). Wiring code (an app's
|
|
6
|
+
* `src/app.module.ts` / `src/main.ts`, and framework tests) imports the unit of work from here;
|
|
7
|
+
* the scaffolded app's lint wall rejects this subpath everywhere else.
|
|
8
|
+
*/
|
|
9
|
+
export { MariaDbUnitOfWork, type EngineDbConfig } from './adapters/mariadb-unit-of-work.js';
|
|
10
|
+
//# sourceMappingURL=internal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["../src/internal.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,iBAAiB,EAAE,KAAK,cAAc,EAAE,MAAM,oCAAoC,CAAC"}
|
package/dist/internal.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MariaDbUnitOfWork = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* @astram/engine/internal — composition-root and framework-test surface (Phase 8 #5).
|
|
6
|
+
*
|
|
7
|
+
* The raw unit of work is NOT on the package entry point (`@astram/engine`): feature code gets
|
|
8
|
+
* only the RecordEngine facade (`RECORD_ENGINE` / `RecordsService`). Wiring code (an app's
|
|
9
|
+
* `src/app.module.ts` / `src/main.ts`, and framework tests) imports the unit of work from here;
|
|
10
|
+
* the scaffolded app's lint wall rejects this subpath everywhere else.
|
|
11
|
+
*/
|
|
12
|
+
var mariadb_unit_of_work_js_1 = require("./adapters/mariadb-unit-of-work.js");
|
|
13
|
+
Object.defineProperty(exports, "MariaDbUnitOfWork", { enumerable: true, get: function () { return mariadb_unit_of_work_js_1.MariaDbUnitOfWork; } });
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Every write (create/update/delete) runs the FIXED, ordered stage sequence inside
|
|
5
5
|
* ONE transaction:
|
|
6
|
-
* fetch-record (update/delete) → permission-gate (MANDATORY) →
|
|
6
|
+
* type-pre-gate (update/delete) → fetch-record (update/delete) → permission-gate (MANDATORY) →
|
|
7
|
+
* derive-write-context →
|
|
7
8
|
* validate → compute (no-op Phase 1) → write → junction-sync → audit (MANDATORY) →
|
|
8
9
|
* emit-event (no-op).
|
|
9
10
|
* A caller cannot reorder, add, or drop stages — the pipe shape is fixed here, so
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"record-engine.d.ts","sourceRoot":"","sources":["../../src/pipe/record-engine.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"record-engine.d.ts","sourceRoot":"","sources":["../../src/pipe/record-engine.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Every write (create/update/delete) runs the FIXED, ordered stage sequence inside
|
|
6
6
|
* ONE transaction:
|
|
7
|
-
* fetch-record (update/delete) → permission-gate (MANDATORY) →
|
|
7
|
+
* type-pre-gate (update/delete) → fetch-record (update/delete) → permission-gate (MANDATORY) →
|
|
8
|
+
* derive-write-context →
|
|
8
9
|
* validate → compute (no-op Phase 1) → write → junction-sync → audit (MANDATORY) →
|
|
9
10
|
* emit-event (no-op).
|
|
10
11
|
* A caller cannot reorder, add, or drop stages — the pipe shape is fixed here, so
|
|
@@ -65,7 +66,10 @@ class RecordEngineImpl {
|
|
|
65
66
|
// fetch-record runs FIRST (before the gate): for update/delete on EVERY type it row-locks
|
|
66
67
|
// and reads the existing row through the shared whereById, so decide() sees the record's
|
|
67
68
|
// fields, owner and tenant (block D, #10). A missing row throws there. No-op for create.
|
|
69
|
+
// type-pre-gate runs before it (Phase 8 #7): a coarse object-type decide() so an actor with
|
|
70
|
+
// no permission on the type is denied before it can take any row lock. No-op for create.
|
|
68
71
|
this.writeStages = [
|
|
72
|
+
(0, permission_gate_js_1.typePreGateStage)(this.decision),
|
|
69
73
|
(0, fetch_record_js_1.fetchRecordStage)(),
|
|
70
74
|
(0, permission_gate_js_1.permissionGateStage)(this.decision),
|
|
71
75
|
(0, derive_write_context_js_1.deriveWriteContextStage)(),
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Stage: fetch-record (update/delete on EVERY type — runs BEFORE the permission gate).
|
|
3
|
-
* Security hardening block D (#10); replaces the owner-only `fetch-owner` stage.
|
|
3
|
+
* Security hardening block D (#10); replaces the owner-only `fetch-owner` stage. Since Phase 8
|
|
4
|
+
* #7 it runs AFTER `type-pre-gate` (permission-gate.ts), so an actor with no permission on the
|
|
5
|
+
* type is denied before this stage can take a row lock.
|
|
4
6
|
*
|
|
5
7
|
* Core's decision authority is zero-dep (no DB), so it cannot read the record it judges. This
|
|
6
8
|
* stage reads the EXISTING row, inside the pipe's unit of work, with
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetch-record.d.ts","sourceRoot":"","sources":["../../../src/pipe/stages/fetch-record.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"fetch-record.d.ts","sourceRoot":"","sources":["../../../src/pipe/stages/fetch-record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EAAE,wBAAwB,EAAqB,MAAM,iBAAiB,CAAC;AAE9E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAAE,KAAK,EAAgB,MAAM,eAAe,CAAC;AAGzD,sDAAsD;AACtD,MAAM,WAAW,WAAW;IAC1B,mFAAmF;IACnF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzC,sFAAsF;IACtF,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,uFAAuF;IACvF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,2FAA2F;AAC3F,wBAAgB,WAAW,CAAC,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,WAAW,CAetF;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,UAAU,EACf,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,QAAQ,GAAG,SAAS,CAAC,GAC7C,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAOzB;AAED,uFAAuF;AACvF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,UAAU,GAAG,wBAAwB,CAK5E;AAED,wBAAgB,gBAAgB,IAAI,KAAK,CAwBxC"}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* Stage: fetch-record (update/delete on EVERY type — runs BEFORE the permission gate).
|
|
4
|
-
* Security hardening block D (#10); replaces the owner-only `fetch-owner` stage.
|
|
4
|
+
* Security hardening block D (#10); replaces the owner-only `fetch-owner` stage. Since Phase 8
|
|
5
|
+
* #7 it runs AFTER `type-pre-gate` (permission-gate.ts), so an actor with no permission on the
|
|
6
|
+
* type is denied before this stage can take a row lock.
|
|
5
7
|
*
|
|
6
8
|
* Core's decision authority is zero-dep (no DB), so it cannot read the record it judges. This
|
|
7
9
|
* stage reads the EXISTING row, inside the pipe's unit of work, with
|
|
@@ -8,4 +8,33 @@
|
|
|
8
8
|
import type { DecisionPort } from '../../ports.js';
|
|
9
9
|
import type { Stage } from '../context.js';
|
|
10
10
|
export declare function permissionGateStage(decision: DecisionPort): Stage;
|
|
11
|
+
/**
|
|
12
|
+
* Stage: type pre-gate (update/delete only; runs FIRST, before fetch-record's row lock).
|
|
13
|
+
* Security-hardening Phase 8 #7.
|
|
14
|
+
*
|
|
15
|
+
* fetch-record takes `SELECT * … FOR UPDATE` so the gate judges the row the write changes; but
|
|
16
|
+
* run before any check, that let an actor with NO permission on the type take row locks. This
|
|
17
|
+
* stage asks the same `decide()` the coarse question first — `{ level: 'object_type', typeKey,
|
|
18
|
+
* tenantId: actor.tenantId }` for the op's action, no attributes — and stops the pipe on deny
|
|
19
|
+
* BEFORE any SQL. On allow, fetch-record locks and the record-level permission gate runs
|
|
20
|
+
* unchanged.
|
|
21
|
+
*
|
|
22
|
+
* ── It never denies what the record-level gate would allow. In core's one decision path the
|
|
23
|
+
* record-level and object-type-level calls differ only after gate 5 (record ⇒ gate 6 runs;
|
|
24
|
+
* object type ⇒ allow terminates), and every input gates 0–5 and 7 read is identical here:
|
|
25
|
+
* - typeKey and action: identical;
|
|
26
|
+
* - resource.tenantId: the record gate uses the row's tenant column, and `whereById`
|
|
27
|
+
* pins that column to `actor.tenantId` (a type without one falls back to it) — so it is
|
|
28
|
+
* always `actor.tenantId`, exactly what this stage sends (gates 3 and 5 agree);
|
|
29
|
+
* - gate 5's coarse key and root-plane rule depend only on (typeKey, action, principal);
|
|
30
|
+
* - gate 7 (delegation) reads only the `shield.delegation` attribute, which the engine
|
|
31
|
+
* never supplies (the record gate's attributes are registered field values).
|
|
32
|
+
* So a deny here is a deny at gate ≤ 5 (or 7) that the record-level call reaches too.
|
|
33
|
+
*
|
|
34
|
+
* ── Existence: it runs before any read, so its answer cannot depend on whether the id exists
|
|
35
|
+
* (a missing and an existing id give the same error). It throws the decision's own reason,
|
|
36
|
+
* as the record gate would for the same actor. `create` no-ops: the permission gate already
|
|
37
|
+
* judges a create at object-type level, and a create takes no lock.
|
|
38
|
+
*/
|
|
39
|
+
export declare function typePreGateStage(decision: DecisionPort): Stage;
|
|
11
40
|
//# sourceMappingURL=permission-gate.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"permission-gate.d.ts","sourceRoot":"","sources":["../../../src/pipe/stages/permission-gate.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD,OAAO,KAAK,EAAE,KAAK,EAAyB,MAAM,eAAe,CAAC;AAQlE,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,YAAY,GAAG,KAAK,
|
|
1
|
+
{"version":3,"file":"permission-gate.d.ts","sourceRoot":"","sources":["../../../src/pipe/stages/permission-gate.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD,OAAO,KAAK,EAAE,KAAK,EAAyB,MAAM,eAAe,CAAC;AAQlE,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,YAAY,GAAG,KAAK,CAiCjE;AA2BD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,YAAY,GAAG,KAAK,CAe9D"}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
10
|
exports.permissionGateStage = permissionGateStage;
|
|
11
|
+
exports.typePreGateStage = typePreGateStage;
|
|
11
12
|
const errors_js_1 = require("../../errors.js");
|
|
12
13
|
const fetch_record_js_1 = require("./fetch-record.js");
|
|
13
14
|
/** Map the pipe op to the stable authorization action key. */
|
|
@@ -43,19 +44,68 @@ function permissionGateStage(decision) {
|
|
|
43
44
|
}
|
|
44
45
|
: {}),
|
|
45
46
|
};
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
47
|
+
await decideOrThrow(decision, ctx, action, resource, context);
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** Ask `decide()` once; throw on deny (AuthorizationDeniedError) or a throw (unavailable). */
|
|
52
|
+
async function decideOrThrow(decision, ctx, action, resource, context) {
|
|
53
|
+
let outcome;
|
|
54
|
+
try {
|
|
55
|
+
outcome = await decision.decide(ctx.actor, action, resource, context);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
// A thrown decision = transient/unavailable (ADR-0016). Fail-closed: do NOT
|
|
59
|
+
// proceed; signal retryable.
|
|
60
|
+
throw new errors_js_1.EngineUnavailableError(`decision authority unavailable for ${ctx.op} ${ctx.registration.typeKey}: ` +
|
|
61
|
+
`${err.message}`);
|
|
62
|
+
}
|
|
63
|
+
if (outcome.effect !== 'allow') {
|
|
64
|
+
throw new errors_js_1.AuthorizationDeniedError(outcome.reason, outcome.detail);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Stage: type pre-gate (update/delete only; runs FIRST, before fetch-record's row lock).
|
|
69
|
+
* Security-hardening Phase 8 #7.
|
|
70
|
+
*
|
|
71
|
+
* fetch-record takes `SELECT * … FOR UPDATE` so the gate judges the row the write changes; but
|
|
72
|
+
* run before any check, that let an actor with NO permission on the type take row locks. This
|
|
73
|
+
* stage asks the same `decide()` the coarse question first — `{ level: 'object_type', typeKey,
|
|
74
|
+
* tenantId: actor.tenantId }` for the op's action, no attributes — and stops the pipe on deny
|
|
75
|
+
* BEFORE any SQL. On allow, fetch-record locks and the record-level permission gate runs
|
|
76
|
+
* unchanged.
|
|
77
|
+
*
|
|
78
|
+
* ── It never denies what the record-level gate would allow. In core's one decision path the
|
|
79
|
+
* record-level and object-type-level calls differ only after gate 5 (record ⇒ gate 6 runs;
|
|
80
|
+
* object type ⇒ allow terminates), and every input gates 0–5 and 7 read is identical here:
|
|
81
|
+
* - typeKey and action: identical;
|
|
82
|
+
* - resource.tenantId: the record gate uses the row's tenant column, and `whereById`
|
|
83
|
+
* pins that column to `actor.tenantId` (a type without one falls back to it) — so it is
|
|
84
|
+
* always `actor.tenantId`, exactly what this stage sends (gates 3 and 5 agree);
|
|
85
|
+
* - gate 5's coarse key and root-plane rule depend only on (typeKey, action, principal);
|
|
86
|
+
* - gate 7 (delegation) reads only the `shield.delegation` attribute, which the engine
|
|
87
|
+
* never supplies (the record gate's attributes are registered field values).
|
|
88
|
+
* So a deny here is a deny at gate ≤ 5 (or 7) that the record-level call reaches too.
|
|
89
|
+
*
|
|
90
|
+
* ── Existence: it runs before any read, so its answer cannot depend on whether the id exists
|
|
91
|
+
* (a missing and an existing id give the same error). It throws the decision's own reason,
|
|
92
|
+
* as the record gate would for the same actor. `create` no-ops: the permission gate already
|
|
93
|
+
* judges a create at object-type level, and a create takes no lock.
|
|
94
|
+
*/
|
|
95
|
+
function typePreGateStage(decision) {
|
|
96
|
+
return {
|
|
97
|
+
name: 'type-pre-gate',
|
|
98
|
+
async run(ctx) {
|
|
99
|
+
if (ctx.op === 'create')
|
|
100
|
+
return;
|
|
101
|
+
const resource = {
|
|
102
|
+
level: 'object_type',
|
|
103
|
+
typeKey: ctx.registration.typeKey,
|
|
104
|
+
tenantId: ctx.actor.tenantId,
|
|
105
|
+
};
|
|
106
|
+
await decideOrThrow(decision, ctx, { key: actionKey(ctx.op) }, resource, {
|
|
107
|
+
isAuthenticated: true,
|
|
108
|
+
});
|
|
59
109
|
},
|
|
60
110
|
};
|
|
61
111
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astram/engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Astram framework — the paved road. ONE unbypassable write pipe (resolve-schema → permission gate → derive-write-context → validate → compute → write → junction-sync → audit → emit-event) over real physical tables, that every feature's CRUD flows through. Feature code receives ONLY the RecordEngine facade — never a raw pool/DataSource/QueryRunner (the physical wall). Security-gate + audit are mandatory stages. Missing capability → a typed CapabilityNotSupportedError (the gap-flag STOP). Builds on @astram/core (metadata contract, decide(), audit) + @astram/guard (decision authority).",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -10,6 +10,17 @@
|
|
|
10
10
|
".": {
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
12
12
|
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./internal": {
|
|
15
|
+
"types": "./dist/internal.d.ts",
|
|
16
|
+
"default": "./dist/internal.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"typesVersions": {
|
|
20
|
+
"*": {
|
|
21
|
+
"internal": [
|
|
22
|
+
"dist/internal.d.ts"
|
|
23
|
+
]
|
|
13
24
|
}
|
|
14
25
|
},
|
|
15
26
|
"files": [
|
|
@@ -20,7 +31,7 @@
|
|
|
20
31
|
"access": "public"
|
|
21
32
|
},
|
|
22
33
|
"dependencies": {
|
|
23
|
-
"mariadb": "^3.4
|
|
34
|
+
"mariadb": "^3.5.4",
|
|
24
35
|
"@astram/core": "1.4.0"
|
|
25
36
|
},
|
|
26
37
|
"peerDependencies": {
|