@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,42 @@
1
+ "use strict";
2
+ /**
3
+ * Stage: audit (MANDATORY). Records an append-only audit event for the write via the
4
+ * injected {@link AuditPort} (core's AuditSink). Like the permission gate, this stage
5
+ * is a fixed segment of the pipe — there is no way for a caller to skip it, so EVERY
6
+ * write is attributable. Runs after the write succeeds, inside the same transaction,
7
+ * so the audit row commits atomically with the data (no data without its audit).
8
+ *
9
+ * The event carries the actor, tenant, action, and resource — NEVER the row's secret
10
+ * values (core's audit sink additionally strips sensitive keys). Actor is referenced
11
+ * by stable id; the presentation layer resolves it to a label (no raw ids to users).
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.auditStage = auditStage;
15
+ function opToActionKey(op) {
16
+ return op;
17
+ }
18
+ function auditStage(audit) {
19
+ return {
20
+ name: 'audit',
21
+ async run(ctx) {
22
+ const action = { key: opToActionKey(ctx.op) };
23
+ const resource = {
24
+ level: ctx.resultId !== undefined && typeof ctx.resultId === 'string' ? 'record' : 'object_type',
25
+ typeKey: ctx.registration.typeKey,
26
+ tenantId: ctx.actor.tenantId,
27
+ ...(typeof ctx.resultId === 'string' ? { recordId: ctx.resultId } : {}),
28
+ };
29
+ const event = {
30
+ type: 'admin_action',
31
+ occurredAtMs: ctx.nowMs,
32
+ actorId: ctx.actor.id,
33
+ tenantId: ctx.actor.tenantId,
34
+ action,
35
+ resource,
36
+ attributes: { op: ctx.op, typeKey: ctx.registration.typeKey },
37
+ };
38
+ // Write the audit row on the pipe's OWN transaction (atomic with the data).
39
+ await audit.record(event, ctx.query);
40
+ },
41
+ };
42
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.js","sourceRoot":"","sources":["../../../src/pipe/stages/audit.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;AASH,gCA0BC;AA9BD,SAAS,aAAa,CAAC,EAAW;IAChC,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,SAAgB,UAAU,CAAC,KAAgB;IACzC,OAAO;QACL,IAAI,EAAE,OAAO;QACb,KAAK,CAAC,GAAG,CAAC,GAAiB;YACzB,MAAM,MAAM,GAAW,EAAE,GAAG,EAAE,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;YACtD,MAAM,QAAQ,GAAa;gBACzB,KAAK,EAAE,GAAG,CAAC,QAAQ,KAAK,SAAS,IAAI,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,aAAa;gBAChG,OAAO,EAAE,GAAG,CAAC,YAAY,CAAC,OAAO;gBACjC,QAAQ,EAAE,GAAG,CAAC,KAAK,CAAC,QAAQ;gBAC5B,GAAG,CAAC,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACxE,CAAC;YAEF,MAAM,KAAK,GAAe;gBACxB,IAAI,EAAE,cAAc;gBACpB,YAAY,EAAE,GAAG,CAAC,KAAK;gBACvB,OAAO,EAAE,GAAG,CAAC,KAAK,CAAC,EAAE;gBACrB,QAAQ,EAAE,GAAG,CAAC,KAAK,CAAC,QAAQ;gBAC5B,MAAM;gBACN,QAAQ;gBACR,UAAU,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,OAAO,EAAE,GAAG,CAAC,YAAY,CAAC,OAAO,EAAE;aAC9D,CAAC;YAEF,4EAA4E;YAC5E,MAAM,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC;QACvC,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Stage: derive-write-context (anti-forgery). Server OVERWRITES every field marked
3
+ * `trusted` with a value derived from the actor/context — NEVER the client body.
4
+ * This closes the "client claims a different tenantId/owner" forgery that a raw
5
+ * write would allow. Runs BEFORE validate so the trusted values are validated too.
6
+ *
7
+ * The trusted sources (Phase 1): `tenantId` (the actor's tenant) and `actorId`
8
+ * (the acting principal's id). A field declared `trusted` ignores client input.
9
+ *
10
+ * CHOKEPOINT 2 (§4.2 of the framework-owns-data-layer design). This stamp is now
11
+ * STRUCTURALLY MANDATORY for a governed type: chokepoint 1 (the registry `validate`)
12
+ * GUARANTEES that a type declaring `access.owned` has a `trusted:'actorId'` column and
13
+ * a type declaring `access.tenantScoped` has a `trusted:'tenantId'` column. Because
14
+ * this stage runs in the FIXED `writeStages` sequence ("ALWAYS present, no skip flag")
15
+ * and always overwrites those columns from the actor/context, no code path can create
16
+ * a governed record with a forged or missing owner/tenant. The enforcement is the pair
17
+ * (chokepoint 1 guarantees the column exists + this stage always fills it); no extra
18
+ * per-write assertion is needed — the trusted mechanism below IS the stamp.
19
+ */
20
+ import type { Stage } from '../context.js';
21
+ export declare function deriveWriteContextStage(): Stage;
22
+ //# sourceMappingURL=derive-write-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"derive-write-context.d.ts","sourceRoot":"","sources":["../../../src/pipe/stages/derive-write-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,KAAK,EAAE,KAAK,EAAgB,MAAM,eAAe,CAAC;AAYzD,wBAAgB,uBAAuB,IAAI,KAAK,CA4B/C"}
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ /**
3
+ * Stage: derive-write-context (anti-forgery). Server OVERWRITES every field marked
4
+ * `trusted` with a value derived from the actor/context — NEVER the client body.
5
+ * This closes the "client claims a different tenantId/owner" forgery that a raw
6
+ * write would allow. Runs BEFORE validate so the trusted values are validated too.
7
+ *
8
+ * The trusted sources (Phase 1): `tenantId` (the actor's tenant) and `actorId`
9
+ * (the acting principal's id). A field declared `trusted` ignores client input.
10
+ *
11
+ * CHOKEPOINT 2 (§4.2 of the framework-owns-data-layer design). This stamp is now
12
+ * STRUCTURALLY MANDATORY for a governed type: chokepoint 1 (the registry `validate`)
13
+ * GUARANTEES that a type declaring `access.owned` has a `trusted:'actorId'` column and
14
+ * a type declaring `access.tenantScoped` has a `trusted:'tenantId'` column. Because
15
+ * this stage runs in the FIXED `writeStages` sequence ("ALWAYS present, no skip flag")
16
+ * and always overwrites those columns from the actor/context, no code path can create
17
+ * a governed record with a forged or missing owner/tenant. The enforcement is the pair
18
+ * (chokepoint 1 guarantees the column exists + this stage always fills it); no extra
19
+ * per-write assertion is needed — the trusted mechanism below IS the stamp.
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.deriveWriteContextStage = deriveWriteContextStage;
23
+ const registry_js_1 = require("../../registry.js");
24
+ function trustedValue(source, ctx) {
25
+ switch (source) {
26
+ case 'tenantId':
27
+ return ctx.actor.tenantId;
28
+ case 'actorId':
29
+ return ctx.actor.id;
30
+ }
31
+ }
32
+ function deriveWriteContextStage() {
33
+ return {
34
+ name: 'derive-write-context',
35
+ async run(ctx) {
36
+ // delete carries no field body — nothing to derive.
37
+ if (ctx.op === 'delete')
38
+ return;
39
+ const input = ctx.input?.fields ?? {};
40
+ for (const field of ctx.registration.fields) {
41
+ const col = (0, registry_js_1.columnOf)(field);
42
+ if (field.trusted) {
43
+ // Trusted (owner/tenant) fields are SET-ONCE-AT-CREATE and immutable thereafter.
44
+ // On create: stamp from the actor/context (client input discarded — anti-forgery).
45
+ // On update: do NOT re-stamp — re-stamping `actorId` would silently TRANSFER
46
+ // ownership to the editor (Bob edits Alice's record → created_by becomes Bob,
47
+ // breaking the `created_by == actor` ownership baseline); re-stamping `tenantId`
48
+ // is benign but a record never changes tenant, so leave all trusted fields
49
+ // untouched on update. (Any client-supplied value for a trusted field on update
50
+ // is IGNORED here — the column simply isn't written, so the stored value stands.)
51
+ if (ctx.op === 'create') {
52
+ ctx.columns[col] = trustedValue(field.trusted, ctx);
53
+ }
54
+ }
55
+ else if (Object.prototype.hasOwnProperty.call(input, field.key)) {
56
+ ctx.columns[col] = input[field.key];
57
+ }
58
+ }
59
+ },
60
+ };
61
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"derive-write-context.js","sourceRoot":"","sources":["../../../src/pipe/stages/derive-write-context.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;GAkBG;;AAeH,0DA4BC;AAzCD,mDAA6C;AAI7C,SAAS,YAAY,CAAC,MAAqB,EAAE,GAAiB;IAC5D,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,UAAU;YACb,OAAO,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC;QAC5B,KAAK,SAAS;YACZ,OAAO,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;IACxB,CAAC;AACH,CAAC;AAED,SAAgB,uBAAuB;IACrC,OAAO;QACL,IAAI,EAAE,sBAAsB;QAC5B,KAAK,CAAC,GAAG,CAAC,GAAiB;YACzB,oDAAoD;YACpD,IAAI,GAAG,CAAC,EAAE,KAAK,QAAQ;gBAAE,OAAO;YAChC,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,EAAE,MAAM,IAAI,EAAE,CAAC;YAEtC,KAAK,MAAM,KAAK,IAAI,GAAG,CAAC,YAAY,CAAC,MAA8B,EAAE,CAAC;gBACpE,MAAM,GAAG,GAAG,IAAA,sBAAQ,EAAC,KAAK,CAAC,CAAC;gBAC5B,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;oBAClB,iFAAiF;oBACjF,mFAAmF;oBACnF,6EAA6E;oBAC7E,8EAA8E;oBAC9E,iFAAiF;oBACjF,2EAA2E;oBAC3E,gFAAgF;oBAChF,kFAAkF;oBAClF,IAAI,GAAG,CAAC,EAAE,KAAK,QAAQ,EAAE,CAAC;wBACxB,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,YAAY,CAAC,KAAK,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;oBACtD,CAAC;gBACH,CAAC;qBAAM,IAAI,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;oBAClE,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;gBACtC,CAAC;YACH,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Stage: fetch-owner (record-level OWNED write ONLY — runs BEFORE the permission gate).
3
+ *
4
+ * Core's decision authority is zero-dep (no DB) and decides BEFORE the row is fetched,
5
+ * so it cannot see a record's owner to check DAC ownership. This stage closes that gap
6
+ * for `owned` types on update/delete: it SELECTs the EXISTING row's owner column —
7
+ * TENANT-SCOPED in the WHERE so it can never read another tenant's row — and stashes it
8
+ * on `ctx.ownerId`. The permission-gate stage then passes it to `decide()` as
9
+ * `context.attributes.ownerId`, where core's owner baseline compares it to the actor.
10
+ *
11
+ * ── EXEMPTIONS (no pre-fetch):
12
+ * - `create` — there is no existing record; the owner is being SET by the
13
+ * derive-write-context stage, not checked. (This stage no-ops for create.)
14
+ * - NON-owned / system types — no owner column to check; the gate runs on the
15
+ * grant/system baseline alone. (No unnecessary query.)
16
+ *
17
+ * ── SECURITY (fail-closed):
18
+ * - The row does NOT exist (or is in another tenant) ⇒ `ctx.ownerId` stays undefined
19
+ * ⇒ core's owner baseline finds no match ⇒ the gate denies (no_record_permit). A
20
+ * missing record NEVER leaks and NEVER passes.
21
+ * - This stage only READS the owner column (tenant-scoped) — it performs no write and
22
+ * surfaces no row content; the fetched owner is an opaque id fed to the gate.
23
+ * - The stage runs inside the SAME transaction as the gate + write (one UoW), so the
24
+ * owner it read is the owner the write will act on (no TOCTOU within the txn).
25
+ */
26
+ import type { Stage } from '../context.js';
27
+ export declare function fetchOwnerStage(): Stage;
28
+ //# sourceMappingURL=fetch-owner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch-owner.d.ts","sourceRoot":"","sources":["../../../src/pipe/stages/fetch-owner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAIH,OAAO,KAAK,EAAE,KAAK,EAAgB,MAAM,eAAe,CAAC;AAqCzD,wBAAgB,eAAe,IAAI,KAAK,CAgCvC"}
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ /**
3
+ * Stage: fetch-owner (record-level OWNED write ONLY — runs BEFORE the permission gate).
4
+ *
5
+ * Core's decision authority is zero-dep (no DB) and decides BEFORE the row is fetched,
6
+ * so it cannot see a record's owner to check DAC ownership. This stage closes that gap
7
+ * for `owned` types on update/delete: it SELECTs the EXISTING row's owner column —
8
+ * TENANT-SCOPED in the WHERE so it can never read another tenant's row — and stashes it
9
+ * on `ctx.ownerId`. The permission-gate stage then passes it to `decide()` as
10
+ * `context.attributes.ownerId`, where core's owner baseline compares it to the actor.
11
+ *
12
+ * ── EXEMPTIONS (no pre-fetch):
13
+ * - `create` — there is no existing record; the owner is being SET by the
14
+ * derive-write-context stage, not checked. (This stage no-ops for create.)
15
+ * - NON-owned / system types — no owner column to check; the gate runs on the
16
+ * grant/system baseline alone. (No unnecessary query.)
17
+ *
18
+ * ── SECURITY (fail-closed):
19
+ * - The row does NOT exist (or is in another tenant) ⇒ `ctx.ownerId` stays undefined
20
+ * ⇒ core's owner baseline finds no match ⇒ the gate denies (no_record_permit). A
21
+ * missing record NEVER leaks and NEVER passes.
22
+ * - This stage only READS the owner column (tenant-scoped) — it performs no write and
23
+ * surfaces no row content; the fetched owner is an opaque id fed to the gate.
24
+ * - The stage runs inside the SAME transaction as the gate + write (one UoW), so the
25
+ * owner it read is the owner the write will act on (no TOCTOU within the txn).
26
+ */
27
+ Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.fetchOwnerStage = fetchOwnerStage;
29
+ const errors_js_1 = require("../../errors.js");
30
+ const registry_js_1 = require("../../registry.js");
31
+ function qualified(ctx) {
32
+ const schema = ctx.registration.schema;
33
+ const t = ctx.registration.table;
34
+ return schema ? `\`${schema}\`.\`${t}\`` : `\`${t}\``;
35
+ }
36
+ /** WHERE (pk [+ tenant]) for the target id — the SAME tenant-scoping the write stage uses. */
37
+ function whereById(ctx, id) {
38
+ const pk = ctx.registration.primaryKey;
39
+ let sql;
40
+ let params;
41
+ if (pk.kind === 'uuid') {
42
+ if (typeof id !== 'string') {
43
+ throw new errors_js_1.RegistrationError(`uuid PK table '${ctx.registration.typeKey}' needs a string id`);
44
+ }
45
+ sql = `\`${(0, registry_js_1.uuidPkColumn)(pk)}\` = ?`;
46
+ params = [id];
47
+ }
48
+ else {
49
+ if (typeof id === 'string') {
50
+ throw new errors_js_1.RegistrationError(`composite PK table '${ctx.registration.typeKey}' needs a key map, not a string id`);
51
+ }
52
+ sql = pk.columns.map((c) => `\`${c}\` = ?`).join(' AND ');
53
+ params = pk.columns.map((c) => id[c]);
54
+ }
55
+ const tenantCol = (0, registry_js_1.tenantColumnOf)(ctx.registration);
56
+ if (tenantCol) {
57
+ sql += ` AND \`${tenantCol}\` = ?`;
58
+ params = [...params, ctx.actor.tenantId];
59
+ }
60
+ return { sql, params };
61
+ }
62
+ function fetchOwnerStage() {
63
+ return {
64
+ name: 'fetch-owner',
65
+ async run(ctx) {
66
+ // Only OWNED update/delete pre-fetch. create + non-owned types no-op.
67
+ if (ctx.op === 'create')
68
+ return;
69
+ if (!(0, registry_js_1.isOwned)(ctx.registration))
70
+ return;
71
+ if (ctx.id === undefined)
72
+ return;
73
+ const ownerCol = (0, registry_js_1.ownerColumn)(ctx.registration);
74
+ if (!ownerCol) {
75
+ // An owned type without an actorId column is a boot-time impossibility
76
+ // (chokepoint 1). Fail-closed rather than silently skipping the owner check.
77
+ throw new errors_js_1.RegistrationError(`owned type '${ctx.registration.typeKey}' has no trusted:'actorId' owner column`);
78
+ }
79
+ const where = whereById(ctx, ctx.id);
80
+ const rows = (await ctx.query(`SELECT \`${ownerCol}\` AS owner_id FROM ${qualified(ctx)} WHERE ${where.sql} LIMIT 1`, where.params));
81
+ const first = rows[0];
82
+ // Missing row (or wrong tenant) ⇒ ownerId stays undefined ⇒ the gate denies.
83
+ // Only stash a genuine string owner; a null/non-string owner is left undefined so
84
+ // the baseline's strict === can never spuriously match.
85
+ if (first && typeof first.owner_id === 'string') {
86
+ ctx.ownerId = first.owner_id;
87
+ }
88
+ },
89
+ };
90
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch-owner.js","sourceRoot":"","sources":["../../../src/pipe/stages/fetch-owner.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Stage: junction-sync. Syncs composite-key link tables (e.g. user_roles) after the
3
+ * base row is written. Phase-1 semantics: for each junction named in the input, the
4
+ * engine DELETEs the local side's existing links and re-INSERTs the supplied foreign
5
+ * ids (set-replace), applying any server-set payload defaults (e.g. is_active=1).
6
+ * All within the same transaction as the base write (atomic).
7
+ *
8
+ * Junction writes are parameterized. On delete of the base record, all its junction
9
+ * links are removed (referential cleanup) since Phase-1 tables may not have DB-level
10
+ * cascade.
11
+ */
12
+ import type { Stage } from '../context.js';
13
+ export declare function junctionSyncStage(): Stage;
14
+ //# sourceMappingURL=junction-sync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"junction-sync.d.ts","sourceRoot":"","sources":["../../../src/pipe/stages/junction-sync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAgB,MAAM,eAAe,CAAC;AAkBzD,wBAAgB,iBAAiB,IAAI,KAAK,CAsCzC"}
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ /**
3
+ * Stage: junction-sync. Syncs composite-key link tables (e.g. user_roles) after the
4
+ * base row is written. Phase-1 semantics: for each junction named in the input, the
5
+ * engine DELETEs the local side's existing links and re-INSERTs the supplied foreign
6
+ * ids (set-replace), applying any server-set payload defaults (e.g. is_active=1).
7
+ * All within the same transaction as the base write (atomic).
8
+ *
9
+ * Junction writes are parameterized. On delete of the base record, all its junction
10
+ * links are removed (referential cleanup) since Phase-1 tables may not have DB-level
11
+ * cascade.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.junctionSyncStage = junctionSyncStage;
15
+ function localId(ctx, junction) {
16
+ // The local side is this record's id. For uuid PKs it's the resultId string; for
17
+ // composite tables a junction rarely applies, but if it does the caller supplies
18
+ // the local key via the base columns — resolved by the junction's localColumn.
19
+ const id = ctx.resultId ?? ctx.id;
20
+ if (typeof id === 'string')
21
+ return id;
22
+ if (id && typeof id === 'object')
23
+ return id[junction.localColumn];
24
+ return undefined;
25
+ }
26
+ function jtable(ctx, j) {
27
+ const schema = ctx.registration.schema;
28
+ return schema ? `\`${schema}\`.\`${j.table}\`` : `\`${j.table}\``;
29
+ }
30
+ function junctionSyncStage() {
31
+ return {
32
+ name: 'junction-sync',
33
+ async run(ctx) {
34
+ const junctions = ctx.registration.junctions ?? [];
35
+ if (junctions.length === 0)
36
+ return;
37
+ for (const j of junctions) {
38
+ const local = localId(ctx, j);
39
+ if (local === undefined)
40
+ continue;
41
+ const table = jtable(ctx, j);
42
+ if (ctx.op === 'delete') {
43
+ // Remove all links for this local id.
44
+ await ctx.query(`DELETE FROM ${table} WHERE \`${j.localColumn}\` = ?`, [local]);
45
+ continue;
46
+ }
47
+ // create/update: only touch a junction the caller actually provided.
48
+ const foreignIds = ctx.junctions[j.name];
49
+ if (foreignIds === undefined)
50
+ continue;
51
+ // Set-replace: clear then insert the supplied foreign ids.
52
+ await ctx.query(`DELETE FROM ${table} WHERE \`${j.localColumn}\` = ?`, [local]);
53
+ const payloadCols = Object.keys(j.payloadDefaults ?? {});
54
+ const cols = [j.localColumn, j.foreignColumn, ...payloadCols];
55
+ const placeholders = cols.map(() => '?').join(', ');
56
+ for (const fid of foreignIds) {
57
+ const params = [local, fid, ...payloadCols.map((c) => j.payloadDefaults[c])];
58
+ await ctx.query(`INSERT INTO ${table} (${cols.map((c) => `\`${c}\``).join(', ')}) VALUES (${placeholders})`, params);
59
+ }
60
+ }
61
+ },
62
+ };
63
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"junction-sync.js","sourceRoot":"","sources":["../../../src/pipe/stages/junction-sync.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Stage: permission gate (MANDATORY). Calls the framework's single decision
3
+ * authority (`decide()`), and STOPS the pipe (fail-closed) on deny or transient.
4
+ * This stage has NO skip flag — it is a fixed segment of the pipe (§design: fixes
5
+ * ORS's `@Optional()`/`skipPermissionCheck` bypass). The engine never invents its
6
+ * own allow/deny logic; it asks `decide()` and obeys.
7
+ */
8
+ import type { DecisionPort } from '../../ports.js';
9
+ import type { Stage } from '../context.js';
10
+ export declare function permissionGateStage(decision: DecisionPort): Stage;
11
+ //# sourceMappingURL=permission-gate.d.ts.map
@@ -0,0 +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;AAOlE,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,YAAY,GAAG,KAAK,CAwCjE"}
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ /**
3
+ * Stage: permission gate (MANDATORY). Calls the framework's single decision
4
+ * authority (`decide()`), and STOPS the pipe (fail-closed) on deny or transient.
5
+ * This stage has NO skip flag — it is a fixed segment of the pipe (§design: fixes
6
+ * ORS's `@Optional()`/`skipPermissionCheck` bypass). The engine never invents its
7
+ * own allow/deny logic; it asks `decide()` and obeys.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.permissionGateStage = permissionGateStage;
11
+ const errors_js_1 = require("../../errors.js");
12
+ /** Map the pipe op to the stable authorization action key. */
13
+ function actionKey(op) {
14
+ return op; // 'create' | 'update' | 'delete' — stable action keys (core §5.2)
15
+ }
16
+ function permissionGateStage(decision) {
17
+ return {
18
+ name: 'permission-gate',
19
+ async run(ctx) {
20
+ const action = { key: actionKey(ctx.op) };
21
+ const resource = {
22
+ level: ctx.id !== undefined ? 'record' : 'object_type',
23
+ typeKey: ctx.registration.typeKey,
24
+ // The resource's owning tenant is the ACTOR's tenant for a write — a write
25
+ // into another tenant is denied by the tenant gate downstream in decide().
26
+ tenantId: ctx.actor.tenantId,
27
+ ...(typeof ctx.id === 'string' ? { recordId: ctx.id } : {}),
28
+ };
29
+ // For an OWNED update/delete, the fetch-owner stage (which ran BEFORE this one)
30
+ // read the existing row's owner tenant-scoped and stashed it on ctx.ownerId. Pass
31
+ // it to decide() so core's owner baseline can compare it to the actor. Absent (not
32
+ // owned, or the row is missing/other-tenant) ⇒ no attribute ⇒ the baseline finds
33
+ // no owner match ⇒ deny (the missing row never leaks).
34
+ const context = {
35
+ isAuthenticated: true,
36
+ ...(ctx.ownerId !== undefined ? { attributes: { ownerId: ctx.ownerId } } : {}),
37
+ };
38
+ let outcome;
39
+ try {
40
+ outcome = await decision.decide(ctx.actor, action, resource, context);
41
+ }
42
+ catch (err) {
43
+ // A thrown decision = transient/unavailable (ADR-0016). Fail-closed: do NOT
44
+ // proceed; signal retryable.
45
+ throw new errors_js_1.EngineUnavailableError(`decision authority unavailable for ${ctx.op} ${ctx.registration.typeKey}: ` +
46
+ `${err.message}`);
47
+ }
48
+ if (outcome.effect !== 'allow') {
49
+ throw new errors_js_1.AuthorizationDeniedError(outcome.reason, outcome.detail);
50
+ }
51
+ },
52
+ };
53
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission-gate.js","sourceRoot":"","sources":["../../../src/pipe/stages/permission-gate.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;AAYH,kDAwCC;AAlDD,+CAAmF;AAKnF,8DAA8D;AAC9D,SAAS,SAAS,CAAC,EAAW;IAC5B,OAAO,EAAE,CAAC,CAAC,kEAAkE;AAC/E,CAAC;AAED,SAAgB,mBAAmB,CAAC,QAAsB;IACxD,OAAO;QACL,IAAI,EAAE,iBAAiB;QACvB,KAAK,CAAC,GAAG,CAAC,GAAiB;YACzB,MAAM,MAAM,GAAW,EAAE,GAAG,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;YAClD,MAAM,QAAQ,GAAa;gBACzB,KAAK,EAAE,GAAG,CAAC,EAAE,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,aAAa;gBACtD,OAAO,EAAE,GAAG,CAAC,YAAY,CAAC,OAAO;gBACjC,2EAA2E;gBAC3E,2EAA2E;gBAC3E,QAAQ,EAAE,GAAG,CAAC,KAAK,CAAC,QAAQ;gBAC5B,GAAG,CAAC,OAAO,GAAG,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC5D,CAAC;YACF,gFAAgF;YAChF,kFAAkF;YAClF,mFAAmF;YACnF,iFAAiF;YACjF,uDAAuD;YACvD,MAAM,OAAO,GAAoB;gBAC/B,eAAe,EAAE,IAAI;gBACrB,GAAG,CAAC,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC/E,CAAC;YAEF,IAAI,OAAO,CAAC;YACZ,IAAI,CAAC;gBACH,OAAO,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;YACxE,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,4EAA4E;gBAC5E,6BAA6B;gBAC7B,MAAM,IAAI,kCAAsB,CAC9B,sCAAsC,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,YAAY,CAAC,OAAO,IAAI;oBAC1E,GAAI,GAAa,CAAC,OAAO,EAAE,CAC9B,CAAC;YACJ,CAAC;YAED,IAAI,OAAO,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC;gBAC/B,MAAM,IAAI,oCAAwB,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;YACrE,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Stage: validate. Checks the derived columns against the registration's field
3
+ * specs — required-on-create, type correctness, unknown-field rejection — and
4
+ * coerces where safe. An unsupported field TYPE surfaces the gap-flag STOP
5
+ * (`CapabilityNotSupportedError`), never a silent pass.
6
+ *
7
+ * Runs AFTER derive-write-context so trusted (server) values are validated too.
8
+ */
9
+ import type { Stage } from '../context.js';
10
+ export declare function validateStage(): Stage;
11
+ //# sourceMappingURL=validate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../src/pipe/stages/validate.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,KAAK,EAAgB,MAAM,eAAe,CAAC;AAiDzD,wBAAgB,aAAa,IAAI,KAAK,CAmCrC"}
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ /**
3
+ * Stage: validate. Checks the derived columns against the registration's field
4
+ * specs — required-on-create, type correctness, unknown-field rejection — and
5
+ * coerces where safe. An unsupported field TYPE surfaces the gap-flag STOP
6
+ * (`CapabilityNotSupportedError`), never a silent pass.
7
+ *
8
+ * Runs AFTER derive-write-context so trusted (server) values are validated too.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.validateStage = validateStage;
12
+ const errors_js_1 = require("../../errors.js");
13
+ const registry_js_1 = require("../../registry.js");
14
+ const SUPPORTED = new Set([
15
+ 'string',
16
+ 'number',
17
+ 'boolean',
18
+ 'uuid',
19
+ 'timestamp',
20
+ 'json',
21
+ ]);
22
+ const UUID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
23
+ /** Validate + coerce one value against a field type. Returns the coerced value. */
24
+ function coerce(field, value) {
25
+ switch (field.type) {
26
+ case 'string':
27
+ if (typeof value !== 'string')
28
+ throw new errors_js_1.ValidationError(field.key, 'expected a string');
29
+ return value;
30
+ case 'number':
31
+ if (typeof value !== 'number' || Number.isNaN(value)) {
32
+ throw new errors_js_1.ValidationError(field.key, 'expected a number');
33
+ }
34
+ return value;
35
+ case 'boolean':
36
+ if (typeof value === 'boolean')
37
+ return value ? 1 : 0; // MariaDB tinyint
38
+ throw new errors_js_1.ValidationError(field.key, 'expected a boolean');
39
+ case 'uuid':
40
+ if (typeof value !== 'string' || !UUID_RE.test(value)) {
41
+ throw new errors_js_1.ValidationError(field.key, 'expected a uuid');
42
+ }
43
+ return value;
44
+ case 'timestamp':
45
+ if (value instanceof Date)
46
+ return value;
47
+ if (typeof value === 'number')
48
+ return new Date(value);
49
+ if (typeof value === 'string' && !Number.isNaN(Date.parse(value)))
50
+ return new Date(value);
51
+ throw new errors_js_1.ValidationError(field.key, 'expected a timestamp (Date | epoch ms | ISO string)');
52
+ case 'json':
53
+ return JSON.stringify(value);
54
+ default:
55
+ // A field type the pipe does not implement → the gap-flag STOP.
56
+ throw new errors_js_1.CapabilityNotSupportedError(`fieldType:${field.type}`, `field '${field.key}' declares unsupported type '${field.type}'`);
57
+ }
58
+ }
59
+ function validateStage() {
60
+ return {
61
+ name: 'validate',
62
+ async run(ctx) {
63
+ if (ctx.op === 'delete')
64
+ return;
65
+ // Reject any input key that is not a registered field (no silent drop).
66
+ const known = new Set(ctx.registration.fields.map((f) => f.key));
67
+ for (const key of Object.keys(ctx.input?.fields ?? {})) {
68
+ if (!known.has(key)) {
69
+ throw new errors_js_1.ValidationError(key, `unknown field for '${ctx.registration.typeKey}'`);
70
+ }
71
+ }
72
+ for (const field of ctx.registration.fields) {
73
+ // Guard-rail: reject a field type the pipe can't handle even if unset.
74
+ if (!SUPPORTED.has(field.type)) {
75
+ throw new errors_js_1.CapabilityNotSupportedError(`fieldType:${field.type}`, `field '${field.key}' declares unsupported type '${field.type}'`);
76
+ }
77
+ const col = (0, registry_js_1.columnOf)(field);
78
+ const present = Object.prototype.hasOwnProperty.call(ctx.columns, col);
79
+ if (!present) {
80
+ if (ctx.op === 'create' && field.required) {
81
+ throw new errors_js_1.ValidationError(field.key, 'is required');
82
+ }
83
+ continue; // absent + optional (or update patch) → skip
84
+ }
85
+ ctx.columns[col] = coerce(field, ctx.columns[col]);
86
+ }
87
+ },
88
+ };
89
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.js","sourceRoot":"","sources":["../../../src/pipe/stages/validate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Stage: write. Persists the validated columns to the real physical table via the
3
+ * bound transactional query fn (parameterized — never string-built). Handles the
4
+ * create (INSERT with a pre-allocated uuid PK), update (UPDATE by id), and delete
5
+ * (DELETE by id). This is the ONLY stage that writes the base row; extension-field
6
+ * sidecar writes attach here too when the table is extensible.
7
+ *
8
+ * uuid generation uses node:crypto (guard/engine own node built-ins; core does not).
9
+ */
10
+ import type { Stage } from '../context.js';
11
+ export declare function writeStage(): Stage;
12
+ //# sourceMappingURL=write.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"write.d.ts","sourceRoot":"","sources":["../../../src/pipe/stages/write.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,KAAK,EAAE,KAAK,EAAgB,MAAM,eAAe,CAAC;AAoDzD,wBAAgB,UAAU,IAAI,KAAK,CAkDlC"}