@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.
Files changed (84) hide show
  1. package/LICENSE +21 -0
  2. package/dist/adapters/mariadb-audit-sink.d.ts +24 -0
  3. package/dist/adapters/mariadb-audit-sink.d.ts.map +1 -0
  4. package/dist/adapters/mariadb-audit-sink.js +40 -0
  5. package/dist/adapters/mariadb-audit-sink.js.map +1 -0
  6. package/dist/adapters/mariadb-unit-of-work.d.ts +27 -0
  7. package/dist/adapters/mariadb-unit-of-work.d.ts.map +1 -0
  8. package/dist/adapters/mariadb-unit-of-work.js +58 -0
  9. package/dist/adapters/mariadb-unit-of-work.js.map +1 -0
  10. package/dist/decorators/access-bundle.decorator.d.ts +66 -0
  11. package/dist/decorators/access-bundle.decorator.d.ts.map +1 -0
  12. package/dist/decorators/access-bundle.decorator.js +76 -0
  13. package/dist/decorators/access-bundle.decorator.js.map +1 -0
  14. package/dist/decorators/object-type.decorator.d.ts +116 -0
  15. package/dist/decorators/object-type.decorator.d.ts.map +1 -0
  16. package/dist/decorators/object-type.decorator.js +146 -0
  17. package/dist/decorators/object-type.decorator.js.map +1 -0
  18. package/dist/errors.d.ts +46 -0
  19. package/dist/errors.d.ts.map +1 -0
  20. package/dist/errors.js +67 -0
  21. package/dist/errors.js.map +1 -0
  22. package/dist/index.d.ts +30 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +80 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/module/engine.module.d.ts +90 -0
  27. package/dist/module/engine.module.d.ts.map +1 -0
  28. package/dist/module/engine.module.js +218 -0
  29. package/dist/module/engine.module.js.map +1 -0
  30. package/dist/module/tokens.d.ts +12 -0
  31. package/dist/module/tokens.d.ts.map +1 -0
  32. package/dist/module/tokens.js +14 -0
  33. package/dist/pipe/context.d.ts +55 -0
  34. package/dist/pipe/context.d.ts.map +1 -0
  35. package/dist/pipe/context.js +18 -0
  36. package/dist/pipe/context.js.map +1 -0
  37. package/dist/pipe/record-engine.d.ts +72 -0
  38. package/dist/pipe/record-engine.d.ts.map +1 -0
  39. package/dist/pipe/record-engine.js +279 -0
  40. package/dist/pipe/record-engine.js.map +1 -0
  41. package/dist/pipe/records-service.d.ts +47 -0
  42. package/dist/pipe/records-service.d.ts.map +1 -0
  43. package/dist/pipe/records-service.js +108 -0
  44. package/dist/pipe/stages/audit.d.ts +15 -0
  45. package/dist/pipe/stages/audit.d.ts.map +1 -0
  46. package/dist/pipe/stages/audit.js +42 -0
  47. package/dist/pipe/stages/audit.js.map +1 -0
  48. package/dist/pipe/stages/derive-write-context.d.ts +22 -0
  49. package/dist/pipe/stages/derive-write-context.d.ts.map +1 -0
  50. package/dist/pipe/stages/derive-write-context.js +61 -0
  51. package/dist/pipe/stages/derive-write-context.js.map +1 -0
  52. package/dist/pipe/stages/fetch-owner.d.ts +28 -0
  53. package/dist/pipe/stages/fetch-owner.d.ts.map +1 -0
  54. package/dist/pipe/stages/fetch-owner.js +90 -0
  55. package/dist/pipe/stages/fetch-owner.js.map +1 -0
  56. package/dist/pipe/stages/junction-sync.d.ts +14 -0
  57. package/dist/pipe/stages/junction-sync.d.ts.map +1 -0
  58. package/dist/pipe/stages/junction-sync.js +63 -0
  59. package/dist/pipe/stages/junction-sync.js.map +1 -0
  60. package/dist/pipe/stages/permission-gate.d.ts +11 -0
  61. package/dist/pipe/stages/permission-gate.d.ts.map +1 -0
  62. package/dist/pipe/stages/permission-gate.js +53 -0
  63. package/dist/pipe/stages/permission-gate.js.map +1 -0
  64. package/dist/pipe/stages/validate.d.ts +11 -0
  65. package/dist/pipe/stages/validate.d.ts.map +1 -0
  66. package/dist/pipe/stages/validate.js +89 -0
  67. package/dist/pipe/stages/validate.js.map +1 -0
  68. package/dist/pipe/stages/write.d.ts +12 -0
  69. package/dist/pipe/stages/write.d.ts.map +1 -0
  70. package/dist/pipe/stages/write.js +109 -0
  71. package/dist/pipe/stages/write.js.map +1 -0
  72. package/dist/ports.d.ts +37 -0
  73. package/dist/ports.d.ts.map +1 -0
  74. package/dist/ports.js +11 -0
  75. package/dist/ports.js.map +1 -0
  76. package/dist/registry.d.ts +42 -0
  77. package/dist/registry.d.ts.map +1 -0
  78. package/dist/registry.js +184 -0
  79. package/dist/registry.js.map +1 -0
  80. package/dist/types.d.ts +207 -0
  81. package/dist/types.d.ts.map +1 -0
  82. package/dist/types.js +15 -0
  83. package/dist/types.js.map +1 -0
  84. 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"}