@astram/engine 0.1.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/LICENSE +21 -0
- package/dist/adapters/mariadb-audit-sink.d.ts +24 -0
- package/dist/adapters/mariadb-audit-sink.d.ts.map +1 -0
- package/dist/adapters/mariadb-audit-sink.js +40 -0
- package/dist/adapters/mariadb-audit-sink.js.map +1 -0
- package/dist/adapters/mariadb-unit-of-work.d.ts +27 -0
- package/dist/adapters/mariadb-unit-of-work.d.ts.map +1 -0
- package/dist/adapters/mariadb-unit-of-work.js +58 -0
- package/dist/adapters/mariadb-unit-of-work.js.map +1 -0
- package/dist/decorators/access-bundle.decorator.d.ts +66 -0
- package/dist/decorators/access-bundle.decorator.d.ts.map +1 -0
- package/dist/decorators/access-bundle.decorator.js +76 -0
- package/dist/decorators/access-bundle.decorator.js.map +1 -0
- package/dist/decorators/object-type.decorator.d.ts +116 -0
- package/dist/decorators/object-type.decorator.d.ts.map +1 -0
- package/dist/decorators/object-type.decorator.js +146 -0
- package/dist/decorators/object-type.decorator.js.map +1 -0
- package/dist/errors.d.ts +46 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +67 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +80 -0
- package/dist/index.js.map +1 -0
- package/dist/module/engine.module.d.ts +90 -0
- package/dist/module/engine.module.d.ts.map +1 -0
- package/dist/module/engine.module.js +218 -0
- package/dist/module/engine.module.js.map +1 -0
- package/dist/module/tokens.d.ts +12 -0
- package/dist/module/tokens.d.ts.map +1 -0
- package/dist/module/tokens.js +14 -0
- package/dist/pipe/context.d.ts +55 -0
- package/dist/pipe/context.d.ts.map +1 -0
- package/dist/pipe/context.js +18 -0
- package/dist/pipe/context.js.map +1 -0
- package/dist/pipe/record-engine.d.ts +72 -0
- package/dist/pipe/record-engine.d.ts.map +1 -0
- package/dist/pipe/record-engine.js +279 -0
- package/dist/pipe/record-engine.js.map +1 -0
- package/dist/pipe/records-service.d.ts +47 -0
- package/dist/pipe/records-service.d.ts.map +1 -0
- package/dist/pipe/records-service.js +108 -0
- package/dist/pipe/stages/audit.d.ts +15 -0
- package/dist/pipe/stages/audit.d.ts.map +1 -0
- package/dist/pipe/stages/audit.js +42 -0
- package/dist/pipe/stages/audit.js.map +1 -0
- package/dist/pipe/stages/derive-write-context.d.ts +22 -0
- package/dist/pipe/stages/derive-write-context.d.ts.map +1 -0
- package/dist/pipe/stages/derive-write-context.js +61 -0
- package/dist/pipe/stages/derive-write-context.js.map +1 -0
- package/dist/pipe/stages/fetch-owner.d.ts +28 -0
- package/dist/pipe/stages/fetch-owner.d.ts.map +1 -0
- package/dist/pipe/stages/fetch-owner.js +90 -0
- package/dist/pipe/stages/fetch-owner.js.map +1 -0
- package/dist/pipe/stages/junction-sync.d.ts +14 -0
- package/dist/pipe/stages/junction-sync.d.ts.map +1 -0
- package/dist/pipe/stages/junction-sync.js +63 -0
- package/dist/pipe/stages/junction-sync.js.map +1 -0
- package/dist/pipe/stages/permission-gate.d.ts +11 -0
- package/dist/pipe/stages/permission-gate.d.ts.map +1 -0
- package/dist/pipe/stages/permission-gate.js +53 -0
- package/dist/pipe/stages/permission-gate.js.map +1 -0
- package/dist/pipe/stages/validate.d.ts +11 -0
- package/dist/pipe/stages/validate.d.ts.map +1 -0
- package/dist/pipe/stages/validate.js +89 -0
- package/dist/pipe/stages/validate.js.map +1 -0
- package/dist/pipe/stages/write.d.ts +12 -0
- package/dist/pipe/stages/write.d.ts.map +1 -0
- package/dist/pipe/stages/write.js +109 -0
- package/dist/pipe/stages/write.js.map +1 -0
- package/dist/ports.d.ts +37 -0
- package/dist/ports.d.ts.map +1 -0
- package/dist/ports.js +11 -0
- package/dist/ports.js.map +1 -0
- package/dist/registry.d.ts +42 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +184 -0
- package/dist/registry.js.map +1 -0
- package/dist/types.d.ts +207 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +15 -0
- package/dist/types.js.map +1 -0
- package/package.json +49 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pipe context — the mutable envelope that flows through every stage. Each
|
|
3
|
+
* stage reads what it needs and enriches it for downstream stages. The engine
|
|
4
|
+
* builds the initial context, runs the ordered stages inside ONE transaction, and
|
|
5
|
+
* returns the result. A stage that throws aborts the pipe (rollback).
|
|
6
|
+
*/
|
|
7
|
+
import type { Principal, RecordId, RecordInput, ObjectType } from '../types.js';
|
|
8
|
+
import type { QueryFn } from '../ports.js';
|
|
9
|
+
/** The operation a write pipe is performing. */
|
|
10
|
+
export type WriteOp = 'create' | 'update' | 'delete';
|
|
11
|
+
/** Shared, immutable-per-run inputs every stage can read. */
|
|
12
|
+
export interface PipeBase {
|
|
13
|
+
readonly op: WriteOp;
|
|
14
|
+
readonly actor: Principal;
|
|
15
|
+
readonly registration: ObjectType;
|
|
16
|
+
/** The bound transactional query fn (sealed — stages use ONLY this for DB work). */
|
|
17
|
+
readonly query: QueryFn;
|
|
18
|
+
/** UTC epoch ms stamped once at pipe entry (deterministic within a run). */
|
|
19
|
+
readonly nowMs: number;
|
|
20
|
+
}
|
|
21
|
+
/** The evolving write context. */
|
|
22
|
+
export interface WriteContext extends PipeBase {
|
|
23
|
+
/** For update/delete: the target record id. */
|
|
24
|
+
id?: RecordId;
|
|
25
|
+
/** The caller's raw input (create/update). */
|
|
26
|
+
input?: RecordInput;
|
|
27
|
+
/**
|
|
28
|
+
* Field values AFTER derive-write-context + validate: trusted attributes have
|
|
29
|
+
* been overwritten with server values, types coerced. This is what `write`
|
|
30
|
+
* persists. Keyed by physical column name.
|
|
31
|
+
*/
|
|
32
|
+
columns: Record<string, unknown>;
|
|
33
|
+
/** Junction values to sync (foreign ids per junction name), post-validate. */
|
|
34
|
+
junctions: Record<string, readonly string[]>;
|
|
35
|
+
/** The id assigned/targeted by the write stage (set for create). */
|
|
36
|
+
resultId?: RecordId;
|
|
37
|
+
/**
|
|
38
|
+
* For an OWNED type's update/delete: the existing row's owner id, fetched
|
|
39
|
+
* tenant-scoped by the fetch-owner stage BEFORE the permission gate, and passed to
|
|
40
|
+
* `decide()` as `context.attributes.ownerId` so core's owner baseline can compare it
|
|
41
|
+
* to the actor. `undefined` when the type is not owned OR the target row does not
|
|
42
|
+
* exist in the actor's tenant (a missing row ⇒ absent owner ⇒ the baseline won't
|
|
43
|
+
* match ⇒ the gate denies; the row is never surfaced). NEVER set for create.
|
|
44
|
+
*/
|
|
45
|
+
ownerId?: string;
|
|
46
|
+
}
|
|
47
|
+
/** A stage: a small, ordered, independently-testable unit of the pipe. */
|
|
48
|
+
export interface Stage {
|
|
49
|
+
/** Stable stage name (for tests + tracing). */
|
|
50
|
+
readonly name: string;
|
|
51
|
+
run(ctx: WriteContext): Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
/** Build the initial write context for a create/update/delete. */
|
|
54
|
+
export declare function newWriteContext(base: PipeBase, partial: Partial<WriteContext>): WriteContext;
|
|
55
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/pipe/context.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAChF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAE3C,gDAAgD;AAChD,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAErD,6DAA6D;AAC7D,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,YAAY,EAAE,UAAU,CAAC;IAClC,oFAAoF;IACpF,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,4EAA4E;IAC5E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,kCAAkC;AAClC,MAAM,WAAW,YAAa,SAAQ,QAAQ;IAC5C,+CAA+C;IAC/C,EAAE,CAAC,EAAE,QAAQ,CAAC;IACd,8CAA8C;IAC9C,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB;;;;OAIG;IACH,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,8EAA8E;IAC9E,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC;IAC7C,oEAAoE;IACpE,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,0EAA0E;AAC1E,MAAM,WAAW,KAAK;IACpB,+CAA+C;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,GAAG,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAED,kEAAkE;AAClE,wBAAgB,eAAe,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,YAAY,CAO5F"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The pipe context — the mutable envelope that flows through every stage. Each
|
|
4
|
+
* stage reads what it needs and enriches it for downstream stages. The engine
|
|
5
|
+
* builds the initial context, runs the ordered stages inside ONE transaction, and
|
|
6
|
+
* returns the result. A stage that throws aborts the pipe (rollback).
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.newWriteContext = newWriteContext;
|
|
10
|
+
/** Build the initial write context for a create/update/delete. */
|
|
11
|
+
function newWriteContext(base, partial) {
|
|
12
|
+
return {
|
|
13
|
+
...base,
|
|
14
|
+
columns: {},
|
|
15
|
+
junctions: {},
|
|
16
|
+
...partial,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/pipe/context.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;AAsDH,0CAOC;AARD,kEAAkE;AAClE,SAAgB,eAAe,CAAC,IAAc,EAAE,OAA8B;IAC5E,OAAO;QACL,GAAG,IAAI;QACP,OAAO,EAAE,EAAE;QACX,SAAS,EAAE,EAAE;QACb,GAAG,OAAO;KACX,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The RecordEngine — the ONE facade a feature calls, and the composer of the pipe.
|
|
3
|
+
*
|
|
4
|
+
* Every write (create/update/delete) runs the FIXED, ordered stage sequence inside
|
|
5
|
+
* ONE transaction:
|
|
6
|
+
* permission-gate (MANDATORY) → derive-write-context → validate → compute (no-op
|
|
7
|
+
* Phase 1) → write → junction-sync → audit (MANDATORY) → emit-event (no-op).
|
|
8
|
+
* A caller cannot reorder, add, or drop stages — the pipe shape is fixed here, so
|
|
9
|
+
* the security gate + audit ALWAYS run (fixes ORS's optional gate + off-road writes).
|
|
10
|
+
* Any stage throwing aborts the transaction (rollback) — fail-closed.
|
|
11
|
+
*
|
|
12
|
+
* Reads (get/list) run a lean read path: permission-gate (read) → query → project.
|
|
13
|
+
* (Row/column access-filtering + ref/label projection are Phase-2 enrichments; the
|
|
14
|
+
* Phase-1 read still gates on decide() and never surfaces raw internal ids as labels.)
|
|
15
|
+
*/
|
|
16
|
+
import type { TableRegistry } from '../registry.js';
|
|
17
|
+
import type { AuditPort, DecisionPort, UnitOfWork, QueryFn } from '../ports.js';
|
|
18
|
+
import type { ListQuery, Principal, RecordEngine, RecordId, RecordInput, RecordPage, RecordView } from '../types.js';
|
|
19
|
+
export interface RecordEngineDeps {
|
|
20
|
+
readonly registry: TableRegistry;
|
|
21
|
+
readonly uow: UnitOfWork;
|
|
22
|
+
readonly decision: DecisionPort;
|
|
23
|
+
readonly audit: AuditPort;
|
|
24
|
+
/** Injectable clock (defaults to Date.now); keeps the pipe deterministic in tests. */
|
|
25
|
+
readonly nowMs?: () => number;
|
|
26
|
+
/**
|
|
27
|
+
* OPTIONAL post-delete hook (design §6.2 orphan-share cleanup). Called AFTER a successful
|
|
28
|
+
* delete of an `owned`-type record so the consumer can cascade-remove that record's
|
|
29
|
+
* `record_share` rows (guard's `makeShareDeleteCascade`).
|
|
30
|
+
*
|
|
31
|
+
* ⛔ SAME-TRANSACTION (mirrors {@link AuditPort.record}, ports.ts): the hook receives the
|
|
32
|
+
* pipe's TX-BOUND {@link QueryFn} as its FIRST arg and MUST run its cascade DELETE through
|
|
33
|
+
* THAT — so the share cleanup commits/rolls back ATOMICALLY with the record delete. If the
|
|
34
|
+
* hook ran on a separate pool connection (autocommit), a rolled-back delete would leave the
|
|
35
|
+
* shares destroyed for a still-existing record (and self-deadlock on the delete's row lock).
|
|
36
|
+
* Using the same `query` is what makes the atomicity real, not just documented.
|
|
37
|
+
*
|
|
38
|
+
* ⛔ LAYERING (ADR-0003): the engine must NOT depend on guard. This hook is the seam — the
|
|
39
|
+
* engine calls it if present; the CONSUMER wires it to guard's cascade function. Absent ⇒
|
|
40
|
+
* no cascade (correct for apps with no shares). Called only for `owned` types (the only
|
|
41
|
+
* shareable ones) and only on a delete whose stages produced no error.
|
|
42
|
+
*
|
|
43
|
+
* The hook runs INSIDE the write transaction's `uow.run` callback (after the delete
|
|
44
|
+
* stages), so a hook throw aborts + rolls back the delete — an un-cascadable share (a
|
|
45
|
+
* store error) prevents leaving the record deleted with orphaned grants (fail-closed).
|
|
46
|
+
*/
|
|
47
|
+
readonly onRecordDeleted?: (query: QueryFn, typeKey: string, recordId: string, tenantId: string) => Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
export declare class RecordEngineImpl implements RecordEngine {
|
|
50
|
+
private readonly registry;
|
|
51
|
+
private readonly uow;
|
|
52
|
+
private readonly decision;
|
|
53
|
+
private readonly audit;
|
|
54
|
+
private readonly now;
|
|
55
|
+
private readonly onRecordDeleted;
|
|
56
|
+
/** The FIXED write-stage order. Built once; identical for every write. */
|
|
57
|
+
private readonly writeStages;
|
|
58
|
+
constructor(deps: RecordEngineDeps);
|
|
59
|
+
private runWrite;
|
|
60
|
+
create(typeKey: string, input: RecordInput, actor: Principal): Promise<RecordId>;
|
|
61
|
+
update(typeKey: string, id: RecordId, input: RecordInput, actor: Principal): Promise<void>;
|
|
62
|
+
delete(typeKey: string, id: RecordId, actor: Principal): Promise<void>;
|
|
63
|
+
private gateRead;
|
|
64
|
+
private qualified;
|
|
65
|
+
/** Project a raw DB row → RecordView (field keys, not columns). */
|
|
66
|
+
private project;
|
|
67
|
+
get(typeKey: string, id: RecordId, actor: Principal): Promise<RecordView | null>;
|
|
68
|
+
list(typeKey: string, query: ListQuery, actor: Principal): Promise<RecordPage>;
|
|
69
|
+
/** The physical column of a `tenantId`-trusted field, if the table has one. */
|
|
70
|
+
private tenantColumn;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=record-engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record-engine.d.ts","sourceRoot":"","sources":["../../src/pipe/record-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAEhF,OAAO,KAAK,EACV,SAAS,EACT,SAAS,EACT,YAAY,EACZ,QAAQ,EACR,WAAW,EACX,UAAU,EACV,UAAU,EAEX,MAAM,aAAa,CAAC;AAWrB,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;IACjC,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;IAChC,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,sFAAsF;IACtF,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,MAAM,CAAC;IAC9B;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,CACzB,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,KACb,OAAO,CAAC,IAAI,CAAC,CAAC;CACpB;AAED,qBAAa,gBAAiB,YAAW,YAAY;IACnD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgB;IACzC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAa;IACjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAe;IACxC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAY;IAClC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAOlB;IACd,0EAA0E;IAC1E,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAmB;gBAEnC,IAAI,EAAE,gBAAgB;YAwBpB,QAAQ;IAsChB,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,QAAQ,CAAC;IAMhF,MAAM,CACV,OAAO,EAAE,MAAM,EACf,EAAE,EAAE,QAAQ,EACZ,KAAK,EAAE,WAAW,EAClB,KAAK,EAAE,SAAS,GACf,OAAO,CAAC,IAAI,CAAC;IAIV,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;YAM9D,QAAQ;IAiCtB,OAAO,CAAC,SAAS;IAIjB,mEAAmE;IACnE,OAAO,CAAC,OAAO;IAiBT,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IA8DhF,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC;IAyEpF,+EAA+E;IAC/E,OAAO,CAAC,YAAY;CAIrB"}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The RecordEngine — the ONE facade a feature calls, and the composer of the pipe.
|
|
4
|
+
*
|
|
5
|
+
* Every write (create/update/delete) runs the FIXED, ordered stage sequence inside
|
|
6
|
+
* ONE transaction:
|
|
7
|
+
* permission-gate (MANDATORY) → derive-write-context → validate → compute (no-op
|
|
8
|
+
* Phase 1) → write → junction-sync → audit (MANDATORY) → emit-event (no-op).
|
|
9
|
+
* A caller cannot reorder, add, or drop stages — the pipe shape is fixed here, so
|
|
10
|
+
* the security gate + audit ALWAYS run (fixes ORS's optional gate + off-road writes).
|
|
11
|
+
* Any stage throwing aborts the transaction (rollback) — fail-closed.
|
|
12
|
+
*
|
|
13
|
+
* Reads (get/list) run a lean read path: permission-gate (read) → query → project.
|
|
14
|
+
* (Row/column access-filtering + ref/label projection are Phase-2 enrichments; the
|
|
15
|
+
* Phase-1 read still gates on decide() and never surfaces raw internal ids as labels.)
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.RecordEngineImpl = void 0;
|
|
19
|
+
const errors_js_1 = require("../errors.js");
|
|
20
|
+
const registry_js_1 = require("../registry.js");
|
|
21
|
+
const context_js_1 = require("./context.js");
|
|
22
|
+
const fetch_owner_js_1 = require("./stages/fetch-owner.js");
|
|
23
|
+
const permission_gate_js_1 = require("./stages/permission-gate.js");
|
|
24
|
+
const derive_write_context_js_1 = require("./stages/derive-write-context.js");
|
|
25
|
+
const validate_js_1 = require("./stages/validate.js");
|
|
26
|
+
const write_js_1 = require("./stages/write.js");
|
|
27
|
+
const junction_sync_js_1 = require("./stages/junction-sync.js");
|
|
28
|
+
const audit_js_1 = require("./stages/audit.js");
|
|
29
|
+
const errors_js_2 = require("../errors.js");
|
|
30
|
+
class RecordEngineImpl {
|
|
31
|
+
registry;
|
|
32
|
+
uow;
|
|
33
|
+
decision;
|
|
34
|
+
audit;
|
|
35
|
+
now;
|
|
36
|
+
onRecordDeleted;
|
|
37
|
+
/** The FIXED write-stage order. Built once; identical for every write. */
|
|
38
|
+
writeStages;
|
|
39
|
+
constructor(deps) {
|
|
40
|
+
this.registry = deps.registry;
|
|
41
|
+
this.uow = deps.uow;
|
|
42
|
+
this.decision = deps.decision;
|
|
43
|
+
this.audit = deps.audit;
|
|
44
|
+
this.now = deps.nowMs ?? (() => Date.now());
|
|
45
|
+
this.onRecordDeleted = deps.onRecordDeleted;
|
|
46
|
+
// permission-gate + audit are ALWAYS present, in this order — no skip flag.
|
|
47
|
+
// fetch-owner runs FIRST (before the gate) so an OWNED update/delete's existing-row
|
|
48
|
+
// owner is available to decide() as context.attributes.ownerId (core owner baseline).
|
|
49
|
+
// It no-ops for create + non-owned types (no unnecessary query).
|
|
50
|
+
this.writeStages = [
|
|
51
|
+
(0, fetch_owner_js_1.fetchOwnerStage)(),
|
|
52
|
+
(0, permission_gate_js_1.permissionGateStage)(this.decision),
|
|
53
|
+
(0, derive_write_context_js_1.deriveWriteContextStage)(),
|
|
54
|
+
(0, validate_js_1.validateStage)(),
|
|
55
|
+
// compute stage: Phase-1 no-op (formula/lookup/rollup land with the no-code engine)
|
|
56
|
+
(0, write_js_1.writeStage)(),
|
|
57
|
+
(0, junction_sync_js_1.junctionSyncStage)(),
|
|
58
|
+
(0, audit_js_1.auditStage)(this.audit),
|
|
59
|
+
// emit-event stage: Phase-1 no-op (business rules land later)
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
async runWrite(op, typeKey, actor, opts) {
|
|
63
|
+
const registration = this.registry.require(typeKey); // resolve-schema
|
|
64
|
+
const nowMs = this.now();
|
|
65
|
+
return this.uow.run(async (query) => {
|
|
66
|
+
const ctx = (0, context_js_1.newWriteContext)({ op, actor, registration, query, nowMs }, {
|
|
67
|
+
...(opts.id !== undefined ? { id: opts.id } : {}),
|
|
68
|
+
...(opts.input !== undefined ? { input: opts.input } : {}),
|
|
69
|
+
junctions: { ...(opts.input?.junctions ?? {}) },
|
|
70
|
+
});
|
|
71
|
+
for (const stage of this.writeStages) {
|
|
72
|
+
await stage.run(ctx);
|
|
73
|
+
}
|
|
74
|
+
// Post-delete orphan-share cascade (design §6.2). Runs for an OWNED-type delete only
|
|
75
|
+
// (the only shareable types) when a cascade hook is wired. Passed the TX-BOUND `query`
|
|
76
|
+
// (the SAME connection the delete stages used) so the cascade commits/rolls back
|
|
77
|
+
// ATOMICALLY with the delete — a hook throw rolls the delete back (no record deleted
|
|
78
|
+
// with orphaned shares, and no separate-connection self-deadlock on the delete's row
|
|
79
|
+
// lock). No-op for create/update, non-owned types, a non-string id, or when no hook.
|
|
80
|
+
if (op === 'delete' &&
|
|
81
|
+
this.onRecordDeleted !== undefined &&
|
|
82
|
+
(0, registry_js_1.isOwned)(registration) &&
|
|
83
|
+
typeof opts.id === 'string') {
|
|
84
|
+
await this.onRecordDeleted(query, typeKey, opts.id, actor.tenantId);
|
|
85
|
+
}
|
|
86
|
+
return ctx.resultId;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
async create(typeKey, input, actor) {
|
|
90
|
+
const id = await this.runWrite('create', typeKey, actor, { input });
|
|
91
|
+
if (id === undefined)
|
|
92
|
+
throw new errors_js_1.RegistrationError(`create '${typeKey}' produced no id`);
|
|
93
|
+
return id;
|
|
94
|
+
}
|
|
95
|
+
async update(typeKey, id, input, actor) {
|
|
96
|
+
await this.runWrite('update', typeKey, actor, { id, input });
|
|
97
|
+
}
|
|
98
|
+
async delete(typeKey, id, actor) {
|
|
99
|
+
await this.runWrite('delete', typeKey, actor, { id });
|
|
100
|
+
}
|
|
101
|
+
// --- reads -----------------------------------------------------------------
|
|
102
|
+
async gateRead(registration, actor, id, ownerId) {
|
|
103
|
+
const action = { key: 'read' };
|
|
104
|
+
const resource = {
|
|
105
|
+
level: id !== undefined ? 'record' : 'object_type',
|
|
106
|
+
typeKey: registration.typeKey,
|
|
107
|
+
tenantId: actor.tenantId,
|
|
108
|
+
...(typeof id === 'string' ? { recordId: id } : {}),
|
|
109
|
+
};
|
|
110
|
+
// For an OWNED record read, pass the pre-fetched row owner so core's owner baseline
|
|
111
|
+
// can compare it to the actor. Absent ⇒ no owner match ⇒ deny (missing row / other
|
|
112
|
+
// tenant / non-owned type reads without it, as before).
|
|
113
|
+
const context = {
|
|
114
|
+
isAuthenticated: true,
|
|
115
|
+
...(ownerId !== undefined ? { attributes: { ownerId } } : {}),
|
|
116
|
+
};
|
|
117
|
+
let outcome;
|
|
118
|
+
try {
|
|
119
|
+
outcome = await this.decision.decide(actor, action, resource, context);
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
throw new errors_js_2.EngineUnavailableError(`decision authority unavailable for read ${registration.typeKey}: ${err.message}`);
|
|
123
|
+
}
|
|
124
|
+
if (outcome.effect !== 'allow') {
|
|
125
|
+
throw new errors_js_2.AuthorizationDeniedError(outcome.reason, outcome.detail);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
qualified(reg) {
|
|
129
|
+
return reg.schema ? `\`${reg.schema}\`.\`${reg.table}\`` : `\`${reg.table}\``;
|
|
130
|
+
}
|
|
131
|
+
/** Project a raw DB row → RecordView (field keys, not columns). */
|
|
132
|
+
project(reg, row) {
|
|
133
|
+
const fields = {};
|
|
134
|
+
for (const f of reg.fields) {
|
|
135
|
+
const col = (0, registry_js_1.columnOf)(f);
|
|
136
|
+
if (Object.prototype.hasOwnProperty.call(row, col))
|
|
137
|
+
fields[f.key] = row[col];
|
|
138
|
+
}
|
|
139
|
+
let id;
|
|
140
|
+
if (reg.primaryKey.kind === 'uuid') {
|
|
141
|
+
id = row[reg.primaryKey.column];
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
const key = {};
|
|
145
|
+
for (const c of reg.primaryKey.columns)
|
|
146
|
+
key[c] = String(row[c]);
|
|
147
|
+
id = key;
|
|
148
|
+
}
|
|
149
|
+
return { id, fields };
|
|
150
|
+
}
|
|
151
|
+
async get(typeKey, id, actor) {
|
|
152
|
+
const reg = this.registry.require(typeKey);
|
|
153
|
+
return this.uow.run(async (query) => {
|
|
154
|
+
const pk = reg.primaryKey;
|
|
155
|
+
let where;
|
|
156
|
+
let params;
|
|
157
|
+
if (pk.kind === 'uuid') {
|
|
158
|
+
if (typeof id !== 'string')
|
|
159
|
+
throw new errors_js_1.RegistrationError(`'${typeKey}' needs a string id`);
|
|
160
|
+
where = `\`${(0, registry_js_1.uuidPkColumn)(pk)}\` = ?`;
|
|
161
|
+
params = [id];
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
if (typeof id === 'string')
|
|
165
|
+
throw new errors_js_1.RegistrationError(`'${typeKey}' needs a key map`);
|
|
166
|
+
where = pk.columns.map((c) => `\`${c}\` = ?`).join(' AND ');
|
|
167
|
+
params = pk.columns.map((c) => id[c]);
|
|
168
|
+
}
|
|
169
|
+
// Tenant scoping: if the table has a tenant column, constrain to the actor's tenant.
|
|
170
|
+
const tenantCol = this.tenantColumn(reg);
|
|
171
|
+
if (tenantCol) {
|
|
172
|
+
where += ` AND \`${tenantCol}\` = ?`;
|
|
173
|
+
params = [...params, actor.tenantId];
|
|
174
|
+
}
|
|
175
|
+
if ((0, registry_js_1.isOwned)(reg)) {
|
|
176
|
+
// OWNED read — FETCH-THEN-GATE. Core's owner baseline needs the row's owner,
|
|
177
|
+
// which core (zero-dep) can't fetch. So we SELECT the row FIRST (tenant-scoped),
|
|
178
|
+
// extract its owner, and gate on it. ⛔ THE FETCHED ROW IS NEVER SURFACED UNLESS
|
|
179
|
+
// THE GATE ALLOWS: gateRead throws on deny BEFORE we project()/return, so a denied
|
|
180
|
+
// read leaks nothing. A missing row ⇒ no owner ⇒ deny (never "null" that reveals
|
|
181
|
+
// presence/absence differently from a real deny — both throw/deny before any row
|
|
182
|
+
// data is returned).
|
|
183
|
+
const rows = (await query(`SELECT * FROM ${this.qualified(reg)} WHERE ${where} LIMIT 1`, params));
|
|
184
|
+
const first = rows[0];
|
|
185
|
+
const ownerCol = (0, registry_js_1.ownerColumn)(reg);
|
|
186
|
+
const owner = first && ownerCol ? first[ownerCol] : undefined;
|
|
187
|
+
// gateRead THROWS AuthorizationDeniedError on deny → the row below is never reached.
|
|
188
|
+
await this.gateRead(reg, actor, id, typeof owner === 'string' ? owner : undefined);
|
|
189
|
+
// Allowed. Project the ALREADY-fetched row (no second query). A genuinely absent
|
|
190
|
+
// row (owner undefined) would have been denied above by the missing owner match,
|
|
191
|
+
// so reaching here with `!first` is not expected for owned types; guard anyway.
|
|
192
|
+
return first ? this.project(reg, first) : null;
|
|
193
|
+
}
|
|
194
|
+
// NON-owned / system type — gate BEFORE fetch (no owner to check; avoid an extra
|
|
195
|
+
// query on the deny path). This is the original lean read path.
|
|
196
|
+
await this.gateRead(reg, actor, id);
|
|
197
|
+
const rows = (await query(`SELECT * FROM ${this.qualified(reg)} WHERE ${where} LIMIT 1`, params));
|
|
198
|
+
const first = rows[0];
|
|
199
|
+
return first ? this.project(reg, first) : null;
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
async list(typeKey, query, actor) {
|
|
203
|
+
const reg = this.registry.require(typeKey);
|
|
204
|
+
await this.gateRead(reg, actor);
|
|
205
|
+
return this.uow.run(async (q) => {
|
|
206
|
+
const clauses = [];
|
|
207
|
+
const params = [];
|
|
208
|
+
// Mandatory tenant scoping when the table is tenant-owned (cross-tenant safety).
|
|
209
|
+
const tenantCol = this.tenantColumn(reg);
|
|
210
|
+
if (tenantCol) {
|
|
211
|
+
clauses.push(`\`${tenantCol}\` = ?`);
|
|
212
|
+
params.push(actor.tenantId);
|
|
213
|
+
}
|
|
214
|
+
// Equality filters on registered field columns only (unknown keys rejected).
|
|
215
|
+
// The COLUMN comes from a whitelisted field-key lookup; the VALUE is a bound
|
|
216
|
+
// `?` param — never interpolated.
|
|
217
|
+
for (const [k, v] of Object.entries(query.where ?? {})) {
|
|
218
|
+
const field = reg.fields.find((f) => f.key === k);
|
|
219
|
+
if (!field)
|
|
220
|
+
throw new errors_js_1.RegistrationError(`list '${typeKey}': unknown filter field '${k}'`);
|
|
221
|
+
clauses.push(`\`${(0, registry_js_1.columnOf)(field)}\` = ?`);
|
|
222
|
+
params.push(v);
|
|
223
|
+
}
|
|
224
|
+
// Free-text search: OR a `LIKE ?` across the named registered fields. Field
|
|
225
|
+
// keys are whitelisted (unknown → throw); the term is a bound `?` param with
|
|
226
|
+
// LIKE wildcards escaped, so a literal `%`/`_` in the term is not a wildcard.
|
|
227
|
+
const search = query.search;
|
|
228
|
+
if (search && search.fields.length > 0) {
|
|
229
|
+
const pattern = `%${escapeLikeTerm(search.term)}%`;
|
|
230
|
+
const ors = [];
|
|
231
|
+
for (const key of search.fields) {
|
|
232
|
+
const field = reg.fields.find((f) => f.key === key);
|
|
233
|
+
if (!field) {
|
|
234
|
+
throw new errors_js_1.RegistrationError(`list '${typeKey}': unknown search field '${key}'`);
|
|
235
|
+
}
|
|
236
|
+
ors.push(`\`${(0, registry_js_1.columnOf)(field)}\` LIKE ? ESCAPE '\\\\'`);
|
|
237
|
+
params.push(pattern);
|
|
238
|
+
}
|
|
239
|
+
clauses.push(`(${ors.join(' OR ')})`);
|
|
240
|
+
}
|
|
241
|
+
const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
|
|
242
|
+
// Sort: resolve `sortBy` to a column the SAME whitelisted way as where-keys
|
|
243
|
+
// (unknown key → throw). The direction is an enum-checked literal (only 'desc'
|
|
244
|
+
// flips to DESC; anything else is ascending) — neither is ever raw-interpolated
|
|
245
|
+
// user input.
|
|
246
|
+
let orderBy = '';
|
|
247
|
+
if (query.sortBy !== undefined) {
|
|
248
|
+
const field = reg.fields.find((f) => f.key === query.sortBy);
|
|
249
|
+
if (!field) {
|
|
250
|
+
throw new errors_js_1.RegistrationError(`list '${typeKey}': unknown sort field '${query.sortBy}'`);
|
|
251
|
+
}
|
|
252
|
+
const dir = query.sortOrder === 'desc' ? 'DESC' : 'ASC';
|
|
253
|
+
orderBy = `ORDER BY \`${(0, registry_js_1.columnOf)(field)}\` ${dir}`;
|
|
254
|
+
}
|
|
255
|
+
const limit = Math.min(Math.max(query.limit ?? 100, 1), 1000);
|
|
256
|
+
const offset = Math.max(query.offset ?? 0, 0);
|
|
257
|
+
const rows = (await q(`SELECT * FROM ${this.qualified(reg)} ${where} ${orderBy} LIMIT ? OFFSET ?`, [...params, limit, offset]));
|
|
258
|
+
const countRows = (await q(`SELECT COUNT(*) AS n FROM ${this.qualified(reg)} ${where}`, params));
|
|
259
|
+
const total = Number(countRows[0]?.n ?? 0);
|
|
260
|
+
return { rows: rows.map((r) => this.project(reg, r)), total };
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
/** The physical column of a `tenantId`-trusted field, if the table has one. */
|
|
264
|
+
tenantColumn(reg) {
|
|
265
|
+
const f = reg.fields.find((x) => x.trusted === 'tenantId');
|
|
266
|
+
return f ? (0, registry_js_1.columnOf)(f) : null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
exports.RecordEngineImpl = RecordEngineImpl;
|
|
270
|
+
/**
|
|
271
|
+
* Escape the SQL-LIKE meta-characters (`\`, `%`, `_`) in a user-supplied search
|
|
272
|
+
* term so they match literally. The engine's LIKE clause pairs this with
|
|
273
|
+
* `ESCAPE '\'`, so a `%` a user types finds a literal percent sign — not "match
|
|
274
|
+
* anything". The term itself is still bound as a `?` param (no interpolation);
|
|
275
|
+
* this only prevents accidental wildcard behavior, not injection.
|
|
276
|
+
*/
|
|
277
|
+
function escapeLikeTerm(term) {
|
|
278
|
+
return term.replace(/[\\%_]/g, (ch) => `\\${ch}`);
|
|
279
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record-engine.js","sourceRoot":"","sources":["../../src/pipe/record-engine.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Principal } from '@astram/core';
|
|
2
|
+
import type { RecordEngine, RecordInput, RecordId, RecordView, ListQuery, RecordPage } from '../types.js';
|
|
3
|
+
/** Round-2 port: resolves a service-account KEY to a seeded principal. */
|
|
4
|
+
export interface ServiceAccountResolver {
|
|
5
|
+
resolve(key: string): Principal;
|
|
6
|
+
}
|
|
7
|
+
export declare const SERVICE_ACCOUNT_RESOLVER: unique symbol;
|
|
8
|
+
/**
|
|
9
|
+
* The single record-CRUD entry point a consumer app uses. Request-scoped: it reads the
|
|
10
|
+
* guard-resolved `req.principal` and forwards it to the platform-agnostic (singleton)
|
|
11
|
+
* RecordEngine as the `actor` — so the app never threads identity, and record permission
|
|
12
|
+
* (decide()) runs in the framework with identity it already has. Fail-closed: no principal
|
|
13
|
+
* (and no `.as()`) ⇒ throw, never act as nobody.
|
|
14
|
+
*/
|
|
15
|
+
export declare class RecordsService {
|
|
16
|
+
private readonly engine;
|
|
17
|
+
private readonly req;
|
|
18
|
+
private readonly serviceAccounts?;
|
|
19
|
+
constructor(engine: RecordEngine, req: {
|
|
20
|
+
principal?: Principal;
|
|
21
|
+
}, serviceAccounts?: ServiceAccountResolver | undefined);
|
|
22
|
+
create(typeKey: string, input: RecordInput): Promise<RecordId>;
|
|
23
|
+
get(typeKey: string, id: RecordId): Promise<RecordView | null>;
|
|
24
|
+
list(typeKey: string, query: ListQuery): Promise<RecordPage>;
|
|
25
|
+
update(typeKey: string, id: RecordId, input: RecordInput): Promise<void>;
|
|
26
|
+
delete(typeKey: string, id: RecordId): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Run subsequent ops as a declared identity (an in-request act-as, or — via a
|
|
29
|
+
* registered resolver — a named service account). Explicit + auditable; never a
|
|
30
|
+
* silent default. A string key requires a ServiceAccountResolver (Round 2).
|
|
31
|
+
*/
|
|
32
|
+
as(who: string | Principal): BoundRecords;
|
|
33
|
+
private actor;
|
|
34
|
+
private resolveServiceAccount;
|
|
35
|
+
}
|
|
36
|
+
/** The `.as(...)` bound view — same 5 ops, fixed actor. */
|
|
37
|
+
export declare class BoundRecords {
|
|
38
|
+
private readonly engine;
|
|
39
|
+
private readonly actor;
|
|
40
|
+
constructor(engine: RecordEngine, actor: Principal);
|
|
41
|
+
create(typeKey: string, input: RecordInput): Promise<RecordId>;
|
|
42
|
+
get(typeKey: string, id: RecordId): Promise<RecordView | null>;
|
|
43
|
+
list(typeKey: string, query: ListQuery): Promise<RecordPage>;
|
|
44
|
+
update(typeKey: string, id: RecordId, input: RecordInput): Promise<void>;
|
|
45
|
+
delete(typeKey: string, id: RecordId): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=records-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"records-service.d.ts","sourceRoot":"","sources":["../../src/pipe/records-service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,QAAQ,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAG1G,0EAA0E;AAC1E,MAAM,WAAW,sBAAsB;IACrC,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC;AACD,eAAO,MAAM,wBAAwB,eAAqC,CAAC;AAE3E;;;;;;GAMG;AACH,qBACa,cAAc;IAEA,OAAO,CAAC,QAAQ,CAAC,MAAM;IAC7B,OAAO,CAAC,QAAQ,CAAC,GAAG;IACS,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC;gBAFvC,MAAM,EAAE,YAAY,EAC1B,GAAG,EAAE;QAAE,SAAS,CAAC,EAAE,SAAS,CAAA;KAAE,EACD,eAAe,CAAC,EAAE,sBAAsB,YAAA;IAWnG,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC;IAG9D,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAG9D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC;IAG5D,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAGxE,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAI1D;;;;OAIG;IACH,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,YAAY;IAKzC,OAAO,CAAC,KAAK;IAWb,OAAO,CAAC,qBAAqB;CAS9B;AAED,2DAA2D;AAC3D,qBAAa,YAAY;IAErB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,KAAK;gBADL,MAAM,EAAE,YAAY,EACpB,KAAK,EAAE,SAAS;IAEnC,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC;IAC9D,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAC9D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC;IAC5D,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IACxE,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;CACrD"}
|
|
@@ -0,0 +1,108 @@
|
|
|
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.BoundRecords = exports.RecordsService = exports.SERVICE_ACCOUNT_RESOLVER = void 0;
|
|
16
|
+
const common_1 = require("@nestjs/common");
|
|
17
|
+
const core_1 = require("@nestjs/core");
|
|
18
|
+
const tokens_js_1 = require("../module/tokens.js");
|
|
19
|
+
const errors_js_1 = require("../errors.js");
|
|
20
|
+
exports.SERVICE_ACCOUNT_RESOLVER = Symbol('SERVICE_ACCOUNT_RESOLVER');
|
|
21
|
+
/**
|
|
22
|
+
* The single record-CRUD entry point a consumer app uses. Request-scoped: it reads the
|
|
23
|
+
* guard-resolved `req.principal` and forwards it to the platform-agnostic (singleton)
|
|
24
|
+
* RecordEngine as the `actor` — so the app never threads identity, and record permission
|
|
25
|
+
* (decide()) runs in the framework with identity it already has. Fail-closed: no principal
|
|
26
|
+
* (and no `.as()`) ⇒ throw, never act as nobody.
|
|
27
|
+
*/
|
|
28
|
+
let RecordsService = class RecordsService {
|
|
29
|
+
engine;
|
|
30
|
+
req;
|
|
31
|
+
serviceAccounts;
|
|
32
|
+
constructor(engine, req, serviceAccounts) {
|
|
33
|
+
this.engine = engine;
|
|
34
|
+
this.req = req;
|
|
35
|
+
this.serviceAccounts = serviceAccounts;
|
|
36
|
+
}
|
|
37
|
+
// NOTE: `async` here is load-bearing, not stylistic. `this.actor()` throws
|
|
38
|
+
// SYNCHRONOUSLY when there is no request principal (fail-closed). Without
|
|
39
|
+
// `async`, that throw would escape as a synchronous exception from e.g.
|
|
40
|
+
// `svc.create(...)` — before a Promise even exists — which breaks every
|
|
41
|
+
// caller that (correctly) expects a Promise it can `await`/`.catch()`
|
|
42
|
+
// (incl. `expect(svc.create(...)).rejects...` in this file's own test
|
|
43
|
+
// suite). `async` captures the synchronous throw into a rejected Promise,
|
|
44
|
+
// matching the declared `Promise<...>` return type exactly.
|
|
45
|
+
async create(typeKey, input) {
|
|
46
|
+
return this.engine.create(typeKey, input, this.actor());
|
|
47
|
+
}
|
|
48
|
+
async get(typeKey, id) {
|
|
49
|
+
return this.engine.get(typeKey, id, this.actor());
|
|
50
|
+
}
|
|
51
|
+
async list(typeKey, query) {
|
|
52
|
+
return this.engine.list(typeKey, query, this.actor());
|
|
53
|
+
}
|
|
54
|
+
async update(typeKey, id, input) {
|
|
55
|
+
return this.engine.update(typeKey, id, input, this.actor());
|
|
56
|
+
}
|
|
57
|
+
async delete(typeKey, id) {
|
|
58
|
+
return this.engine.delete(typeKey, id, this.actor());
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Run subsequent ops as a declared identity (an in-request act-as, or — via a
|
|
62
|
+
* registered resolver — a named service account). Explicit + auditable; never a
|
|
63
|
+
* silent default. A string key requires a ServiceAccountResolver (Round 2).
|
|
64
|
+
*/
|
|
65
|
+
as(who) {
|
|
66
|
+
const actor = typeof who === 'string' ? this.resolveServiceAccount(who) : who;
|
|
67
|
+
return new BoundRecords(this.engine, actor);
|
|
68
|
+
}
|
|
69
|
+
actor() {
|
|
70
|
+
const p = this.req?.principal;
|
|
71
|
+
if (!p) {
|
|
72
|
+
throw new errors_js_1.EngineUnavailableError('RecordsService used with no request principal. In a request the guard sets it; ' +
|
|
73
|
+
'outside a request use records.as(<service-account>). Refusing to act as nobody.');
|
|
74
|
+
}
|
|
75
|
+
return p;
|
|
76
|
+
}
|
|
77
|
+
resolveServiceAccount(key) {
|
|
78
|
+
if (!this.serviceAccounts) {
|
|
79
|
+
throw new errors_js_1.EngineUnavailableError(`records.as('${key}') requires a ServiceAccountResolver, which is not configured. ` +
|
|
80
|
+
'Seed + register service accounts (Round 2) before using .as() with a key.');
|
|
81
|
+
}
|
|
82
|
+
return this.serviceAccounts.resolve(key);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
exports.RecordsService = RecordsService;
|
|
86
|
+
exports.RecordsService = RecordsService = __decorate([
|
|
87
|
+
(0, common_1.Injectable)({ scope: common_1.Scope.REQUEST }),
|
|
88
|
+
__param(0, (0, common_1.Inject)(tokens_js_1.RECORD_ENGINE)),
|
|
89
|
+
__param(1, (0, common_1.Inject)(core_1.REQUEST)),
|
|
90
|
+
__param(2, (0, common_1.Optional)()),
|
|
91
|
+
__param(2, (0, common_1.Inject)(exports.SERVICE_ACCOUNT_RESOLVER)),
|
|
92
|
+
__metadata("design:paramtypes", [Object, Object, Object])
|
|
93
|
+
], RecordsService);
|
|
94
|
+
/** The `.as(...)` bound view — same 5 ops, fixed actor. */
|
|
95
|
+
class BoundRecords {
|
|
96
|
+
engine;
|
|
97
|
+
actor;
|
|
98
|
+
constructor(engine, actor) {
|
|
99
|
+
this.engine = engine;
|
|
100
|
+
this.actor = actor;
|
|
101
|
+
}
|
|
102
|
+
create(typeKey, input) { return this.engine.create(typeKey, input, this.actor); }
|
|
103
|
+
get(typeKey, id) { return this.engine.get(typeKey, id, this.actor); }
|
|
104
|
+
list(typeKey, query) { return this.engine.list(typeKey, query, this.actor); }
|
|
105
|
+
update(typeKey, id, input) { return this.engine.update(typeKey, id, input, this.actor); }
|
|
106
|
+
delete(typeKey, id) { return this.engine.delete(typeKey, id, this.actor); }
|
|
107
|
+
}
|
|
108
|
+
exports.BoundRecords = BoundRecords;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage: audit (MANDATORY). Records an append-only audit event for the write via the
|
|
3
|
+
* injected {@link AuditPort} (core's AuditSink). Like the permission gate, this stage
|
|
4
|
+
* is a fixed segment of the pipe — there is no way for a caller to skip it, so EVERY
|
|
5
|
+
* write is attributable. Runs after the write succeeds, inside the same transaction,
|
|
6
|
+
* so the audit row commits atomically with the data (no data without its audit).
|
|
7
|
+
*
|
|
8
|
+
* The event carries the actor, tenant, action, and resource — NEVER the row's secret
|
|
9
|
+
* values (core's audit sink additionally strips sensitive keys). Actor is referenced
|
|
10
|
+
* by stable id; the presentation layer resolves it to a label (no raw ids to users).
|
|
11
|
+
*/
|
|
12
|
+
import type { AuditPort } from '../../ports.js';
|
|
13
|
+
import type { Stage } from '../context.js';
|
|
14
|
+
export declare function auditStage(audit: AuditPort): Stage;
|
|
15
|
+
//# sourceMappingURL=audit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../../src/pipe/stages/audit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAgC,MAAM,gBAAgB,CAAC;AAC9E,OAAO,KAAK,EAAE,KAAK,EAAyB,MAAM,eAAe,CAAC;AAMlE,wBAAgB,UAAU,CAAC,KAAK,EAAE,SAAS,GAAG,KAAK,CA0BlD"}
|