@astram/guard 0.2.0 → 0.4.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 (120) hide show
  1. package/dist/auth/account.controller.d.ts +245 -0
  2. package/dist/auth/account.controller.d.ts.map +1 -0
  3. package/dist/auth/account.controller.js +673 -0
  4. package/dist/auth/auth.controller.d.ts +219 -5
  5. package/dist/auth/auth.controller.d.ts.map +1 -1
  6. package/dist/auth/auth.controller.js +790 -12
  7. package/dist/auth/auth.module.d.ts +101 -3
  8. package/dist/auth/auth.module.d.ts.map +1 -1
  9. package/dist/auth/auth.module.js +300 -8
  10. package/dist/auth/dto.d.ts +16 -6
  11. package/dist/auth/dto.d.ts.map +1 -1
  12. package/dist/auth/ports/audit-sink.d.ts +47 -0
  13. package/dist/auth/ports/audit-sink.d.ts.map +1 -0
  14. package/dist/auth/ports/audit-sink.js +20 -0
  15. package/dist/auth/ports/credential-writer.d.ts +23 -0
  16. package/dist/auth/ports/credential-writer.d.ts.map +1 -0
  17. package/dist/auth/ports/credential-writer.js +2 -0
  18. package/dist/auth/ports/email-sender.d.ts +14 -0
  19. package/dist/auth/ports/email-sender.d.ts.map +1 -0
  20. package/dist/auth/ports/email-sender.js +2 -0
  21. package/dist/auth/ports/mfa-device-store.d.ts +45 -0
  22. package/dist/auth/ports/mfa-device-store.d.ts.map +1 -0
  23. package/dist/auth/ports/mfa-device-store.js +13 -0
  24. package/dist/auth/ports/profile-writer.d.ts +12 -0
  25. package/dist/auth/ports/profile-writer.d.ts.map +1 -0
  26. package/dist/auth/ports/profile-writer.js +2 -0
  27. package/dist/auth/ports/recovery-code-store.d.ts +10 -0
  28. package/dist/auth/ports/recovery-code-store.d.ts.map +1 -0
  29. package/dist/auth/ports/recovery-code-store.js +2 -0
  30. package/dist/auth/ports/reset-token-store.d.ts +12 -0
  31. package/dist/auth/ports/reset-token-store.d.ts.map +1 -0
  32. package/dist/auth/ports/reset-token-store.js +2 -0
  33. package/dist/auth/ports/session-facts-projector.d.ts +44 -0
  34. package/dist/auth/ports/session-facts-projector.d.ts.map +1 -0
  35. package/dist/auth/ports/session-facts-projector.js +30 -0
  36. package/dist/auth/ports/session-sweeper.d.ts +12 -0
  37. package/dist/auth/ports/session-sweeper.d.ts.map +1 -0
  38. package/dist/auth/ports/session-sweeper.js +2 -0
  39. package/dist/auth/profile-resolver.d.ts +73 -0
  40. package/dist/auth/profile-resolver.d.ts.map +1 -0
  41. package/dist/auth/profile-resolver.js +28 -0
  42. package/dist/auth/rate-limit/attempt-limiter.d.ts +91 -0
  43. package/dist/auth/rate-limit/attempt-limiter.d.ts.map +1 -0
  44. package/dist/auth/rate-limit/attempt-limiter.js +133 -0
  45. package/dist/auth/tokens.d.ts +174 -0
  46. package/dist/auth/tokens.d.ts.map +1 -1
  47. package/dist/auth/tokens.js +190 -1
  48. package/dist/auth/totp-enrollment.d.ts +42 -0
  49. package/dist/auth/totp-enrollment.d.ts.map +1 -0
  50. package/dist/auth/totp-enrollment.js +96 -0
  51. package/dist/authentication/mariadb-credential-resolver.d.ts +38 -24
  52. package/dist/authentication/mariadb-credential-resolver.d.ts.map +1 -1
  53. package/dist/authentication/mariadb-credential-resolver.js +42 -28
  54. package/dist/authz/cross-tenant-grant-writer.d.ts +259 -0
  55. package/dist/authz/cross-tenant-grant-writer.d.ts.map +1 -0
  56. package/dist/authz/cross-tenant-grant-writer.js +397 -0
  57. package/dist/authz/manifest-operation-authority-adapter.d.ts +65 -0
  58. package/dist/authz/manifest-operation-authority-adapter.d.ts.map +1 -0
  59. package/dist/authz/manifest-operation-authority-adapter.js +108 -0
  60. package/dist/authz/mariadb-cross-tenant-grant-adapter.d.ts +90 -0
  61. package/dist/authz/mariadb-cross-tenant-grant-adapter.d.ts.map +1 -0
  62. package/dist/authz/mariadb-cross-tenant-grant-adapter.js +211 -0
  63. package/dist/authz/mariadb-record-rule-adapter.d.ts +60 -0
  64. package/dist/authz/mariadb-record-rule-adapter.d.ts.map +1 -0
  65. package/dist/authz/mariadb-record-rule-adapter.js +154 -0
  66. package/dist/authz/mariadb-record-share-adapter.d.ts +75 -0
  67. package/dist/authz/mariadb-record-share-adapter.d.ts.map +1 -0
  68. package/dist/authz/mariadb-record-share-adapter.js +106 -0
  69. package/dist/authz/record-share-cascade.d.ts +52 -0
  70. package/dist/authz/record-share-cascade.d.ts.map +1 -0
  71. package/dist/authz/record-share-cascade.js +49 -0
  72. package/dist/authz/record-share-writer.d.ts +175 -0
  73. package/dist/authz/record-share-writer.d.ts.map +1 -0
  74. package/dist/authz/record-share-writer.js +225 -0
  75. package/dist/authz/wire-record-authorization.d.ts +66 -0
  76. package/dist/authz/wire-record-authorization.d.ts.map +1 -0
  77. package/dist/authz/wire-record-authorization.js +34 -0
  78. package/dist/completeness/catalog-reconciler.d.ts +21 -0
  79. package/dist/completeness/catalog-reconciler.d.ts.map +1 -0
  80. package/dist/completeness/catalog-reconciler.js +74 -0
  81. package/dist/completeness/operation-inventory-scanner.d.ts +17 -0
  82. package/dist/completeness/operation-inventory-scanner.d.ts.map +1 -0
  83. package/dist/completeness/operation-inventory-scanner.js +79 -0
  84. package/dist/completeness/ports/catalog-store.d.ts +33 -0
  85. package/dist/completeness/ports/catalog-store.d.ts.map +1 -0
  86. package/dist/completeness/ports/catalog-store.js +2 -0
  87. package/dist/config/app-config.d.ts +18 -16
  88. package/dist/config/app-config.d.ts.map +1 -1
  89. package/dist/config/app-config.js +68 -35
  90. package/dist/enforcement/classification.decorator.d.ts +33 -14
  91. package/dist/enforcement/classification.decorator.d.ts.map +1 -1
  92. package/dist/enforcement/classification.decorator.js +34 -14
  93. package/dist/enforcement/global-enforcement.guard.d.ts +29 -19
  94. package/dist/enforcement/global-enforcement.guard.d.ts.map +1 -1
  95. package/dist/enforcement/global-enforcement.guard.js +108 -25
  96. package/dist/enforcement/reenroll-allowlist.d.ts +49 -0
  97. package/dist/enforcement/reenroll-allowlist.d.ts.map +1 -0
  98. package/dist/enforcement/reenroll-allowlist.js +77 -0
  99. package/dist/index.d.ts +35 -3
  100. package/dist/index.d.ts.map +1 -1
  101. package/dist/index.js +159 -7
  102. package/dist/module/shield-tokens.d.ts +15 -0
  103. package/dist/module/shield-tokens.d.ts.map +1 -0
  104. package/dist/module/shield-tokens.js +17 -0
  105. package/dist/module/shield.module.d.ts +154 -10
  106. package/dist/module/shield.module.d.ts.map +1 -1
  107. package/dist/module/shield.module.js +181 -24
  108. package/dist/request-auth/request-principal.d.ts +42 -0
  109. package/dist/request-auth/request-principal.d.ts.map +1 -0
  110. package/dist/request-auth/request-principal.js +87 -0
  111. package/dist/request-auth/session-facts-als.d.ts +65 -0
  112. package/dist/request-auth/session-facts-als.d.ts.map +1 -0
  113. package/dist/request-auth/session-facts-als.js +57 -0
  114. package/dist/request-auth/session-token-binder.d.ts +130 -0
  115. package/dist/request-auth/session-token-binder.d.ts.map +1 -0
  116. package/dist/request-auth/session-token-binder.js +277 -0
  117. package/dist/request-auth/tokens.d.ts +14 -0
  118. package/dist/request-auth/tokens.d.ts.map +1 -0
  119. package/dist/request-auth/tokens.js +16 -0
  120. package/package.json +2 -2
@@ -0,0 +1,90 @@
1
+ /**
2
+ * `MariaDbCrossTenantGrantAdapter` — the concrete {@link CrossTenantGrantPort} for
3
+ * `@astram/guard`, backed by the `cross_tenant_grant` table (ADR-0009). It answers
4
+ * `findActiveGrant`: is there an ACTIVE (accepted, unexpired, unrevoked), consented grant
5
+ * that lets THIS grantee reach THIS owning tenant's object type (+ record) for an action?
6
+ *
7
+ * ── WHAT A CROSS-TENANT GRANT IS (ADR-0009): the ONLY sanctioned way (short of root)
8
+ * a principal reaches a resource owned by a DIFFERENT tenant. It is an explicit,
9
+ * CONSENTED handshake — the OWNING tenant (whose data is reached into) offers it, the
10
+ * GRANTEE tenant accepts it, either side may revoke it, and it always expires. It is NEVER
11
+ * a root bypass: it carries a HARD, non-wildcard action ceiling and a scope
12
+ * (whole object type, or a single record). This adapter only REPORTS an active grant; core's
13
+ * `isCrossTenantGrantApplicable` re-checks the ceiling + scope + expiry (defense-in-depth).
14
+ *
15
+ * ── WHY HERE, NOT CORE (ADR-0003): the `mariadb` driver is a real runtime dependency; core
16
+ * defines only the PORT. This adapter is INJECTED into core's wired decision authority. It
17
+ * uses `@astram/core` TYPES only (erased at build).
18
+ *
19
+ * ── THE MATCH (ADR-0009 — an ACTIVE grant): a row matches when ALL hold —
20
+ * • `ctg_owning_tenant_uuid` = the resource's owning tenant AND `ctg_type_key` = the object
21
+ * type;
22
+ * • the grant is ACTIVE: `ctg_status = 'accepted'` (consent completed) AND
23
+ * `ctg_revoked_at IS NULL` (not revoked by either side) AND `ctg_expires_at_ms > ?` (not
24
+ * expired — bound epoch-ms, timezone-immune) — a revoked/offered/expired grant is NEVER
25
+ * returned, so it can never open access even for a heartbeat;
26
+ * • the grantee is EITHER this user (`ctg_grantee_kind='user'` AND
27
+ * `ctg_grantee_uuid = grantee`) OR one of the grantee's roles (`ctg_grantee_kind='role'`
28
+ * AND `ctg_grantee_uuid IN (roleIds)`);
29
+ * • the action is in the ceiling — the action-ceiling membership check is left to core's
30
+ * grant-evaluator (which re-checks `allowedActions`), but we DO gate scope: an
31
+ * `object_type` grant matches any record of the type; a `record` grant matches ONLY when
32
+ * the query names the exact `ctg_record_uuid` (a record-scoped row is EXCLUDED for an
33
+ * object-type / record-less query so a NULL-compare never spuriously matches).
34
+ * ≥1 matching row ⇒ mapped to a {@link CrossTenantGrant} (first match). No matching row ⇒
35
+ * `null` (a NORMAL data-absence — no active consent — NOT an error, NOT a lockout).
36
+ *
37
+ * ── SCOPE + RECORD-ID HANDLING: `object_type` rows are always eligible; a `record` row is
38
+ * eligible ONLY when `query.recordId` is present AND equals `ctg_record_uuid`. When the
39
+ * query has NO recordId (an object-type-level question), the SQL restricts to
40
+ * `ctg_scope = 'object_type'` so a record row's `ctg_record_uuid` is never compared to a
41
+ * bound NULL (which would never match — but we make the intent explicit and avoid the
42
+ * accidental all-records reach a `record_uuid = NULL` OR-branch could imply).
43
+ *
44
+ * ── ACTION CEILING PARSING: `ctg_allowed_actions` is a JSON column (a JSON array of action-key
45
+ * strings). ⚠️ The mariadb driver AUTO-PARSES a JSON column into a JS ARRAY on read, so the
46
+ * cell arrives already-parsed — `parseAllowedActions` accepts BOTH an already-parsed array
47
+ * (production) and a JSON string (test mocks). A malformed/empty value ⇒ `[]` (an empty
48
+ * ceiling permits nothing — fail-closed; core's ceiling check then denies).
49
+ *
50
+ * ── EXPIRY (MUST-FIX #1 — timezone immunity): `ctg_expires_at_ms` is a BIGINT epoch-ms column
51
+ * (NOT a TIMESTAMP). The active-grant filter compares it `> ?` against a BOUND integer (the
52
+ * adapter's `now()` epoch-ms), so it is immune to host-timezone / `NOW()` wall-clock skew (a
53
+ * TIMESTAMP + `NOW()` comparison silently kept expired grants alive on a non-UTC host). Mirrors
54
+ * the reset-token store's `prt_expires_at_ms` BIGINT precedent.
55
+ *
56
+ * ── EMPTY roleIds: guarded — an empty role list must NOT emit `IN ()` (a MariaDB syntax
57
+ * error). We omit the role branch entirely when there are no roles, so a role-less grantee
58
+ * is matched by its user-grant rows alone.
59
+ *
60
+ * ── ADR-0016 (transient-vs-deny): a genuine store outage THROWS
61
+ * {@link AuthorizationUnavailableError} (→ 503 retryable) — NEVER a silent `null` that
62
+ * would mask an outage as "no grant".
63
+ *
64
+ * ── SAFETY: every value (owningTenantId, typeKey, grantee, each roleId, recordId) is BOUND
65
+ * as a `?` param — NEVER interpolated. A SQL payload in typeKey/recordId is data, not SQL.
66
+ * Mirrors {@link MariaDbRecordShareAdapter}.
67
+ */
68
+ import type { CrossTenantGrant, CrossTenantGrantPort, CrossTenantGrantQuery } from '@astram/core';
69
+ /** The DI token the adapter is bound under (parity with the share/rule-port tokens). */
70
+ export declare const SHIELD_CROSS_TENANT_GRANT_PORT: unique symbol;
71
+ /** The minimal query surface this adapter needs — a `mariadb` Pool satisfies it. */
72
+ export interface GrantQueryable {
73
+ query(sql: string, params?: readonly unknown[]): Promise<unknown>;
74
+ }
75
+ /**
76
+ * The concrete cross-tenant grant adapter. Answers `findActiveGrant` from `cross_tenant_grant`,
77
+ * matching user- and role-granted, ACCEPTED, unexpired, unrevoked grants for the owning tenant
78
+ * + object type (+ record for record-scoped).
79
+ */
80
+ export declare class MariaDbCrossTenantGrantAdapter implements CrossTenantGrantPort {
81
+ private readonly db;
82
+ private readonly schema;
83
+ private readonly now;
84
+ constructor(db: GrantQueryable, opts?: {
85
+ schema?: string;
86
+ nowMs?: () => number;
87
+ });
88
+ findActiveGrant(query: CrossTenantGrantQuery): Promise<CrossTenantGrant | null>;
89
+ }
90
+ //# sourceMappingURL=mariadb-cross-tenant-grant-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mariadb-cross-tenant-grant-adapter.d.ts","sourceRoot":"","sources":["../../src/authz/mariadb-cross-tenant-grant-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AAEH,OAAO,KAAK,EACV,gBAAgB,EAChB,oBAAoB,EACpB,qBAAqB,EAEtB,MAAM,cAAc,CAAC;AAGtB,wFAAwF;AACxF,eAAO,MAAM,8BAA8B,eAA2C,CAAC;AAKvF,oFAAoF;AACpF,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACnE;AAsDD;;;;GAIG;AACH,qBAAa,8BAA+B,YAAW,oBAAoB;IACzE,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAiB;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;gBAEvB,EAAE,EAAE,cAAc,EAAE,IAAI,GAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,MAAM,CAAA;KAAO;IAQ9E,eAAe,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC;CAuFtF"}
@@ -0,0 +1,211 @@
1
+ "use strict";
2
+ /**
3
+ * `MariaDbCrossTenantGrantAdapter` — the concrete {@link CrossTenantGrantPort} for
4
+ * `@astram/guard`, backed by the `cross_tenant_grant` table (ADR-0009). It answers
5
+ * `findActiveGrant`: is there an ACTIVE (accepted, unexpired, unrevoked), consented grant
6
+ * that lets THIS grantee reach THIS owning tenant's object type (+ record) for an action?
7
+ *
8
+ * ── WHAT A CROSS-TENANT GRANT IS (ADR-0009): the ONLY sanctioned way (short of root)
9
+ * a principal reaches a resource owned by a DIFFERENT tenant. It is an explicit,
10
+ * CONSENTED handshake — the OWNING tenant (whose data is reached into) offers it, the
11
+ * GRANTEE tenant accepts it, either side may revoke it, and it always expires. It is NEVER
12
+ * a root bypass: it carries a HARD, non-wildcard action ceiling and a scope
13
+ * (whole object type, or a single record). This adapter only REPORTS an active grant; core's
14
+ * `isCrossTenantGrantApplicable` re-checks the ceiling + scope + expiry (defense-in-depth).
15
+ *
16
+ * ── WHY HERE, NOT CORE (ADR-0003): the `mariadb` driver is a real runtime dependency; core
17
+ * defines only the PORT. This adapter is INJECTED into core's wired decision authority. It
18
+ * uses `@astram/core` TYPES only (erased at build).
19
+ *
20
+ * ── THE MATCH (ADR-0009 — an ACTIVE grant): a row matches when ALL hold —
21
+ * • `ctg_owning_tenant_uuid` = the resource's owning tenant AND `ctg_type_key` = the object
22
+ * type;
23
+ * • the grant is ACTIVE: `ctg_status = 'accepted'` (consent completed) AND
24
+ * `ctg_revoked_at IS NULL` (not revoked by either side) AND `ctg_expires_at_ms > ?` (not
25
+ * expired — bound epoch-ms, timezone-immune) — a revoked/offered/expired grant is NEVER
26
+ * returned, so it can never open access even for a heartbeat;
27
+ * • the grantee is EITHER this user (`ctg_grantee_kind='user'` AND
28
+ * `ctg_grantee_uuid = grantee`) OR one of the grantee's roles (`ctg_grantee_kind='role'`
29
+ * AND `ctg_grantee_uuid IN (roleIds)`);
30
+ * • the action is in the ceiling — the action-ceiling membership check is left to core's
31
+ * grant-evaluator (which re-checks `allowedActions`), but we DO gate scope: an
32
+ * `object_type` grant matches any record of the type; a `record` grant matches ONLY when
33
+ * the query names the exact `ctg_record_uuid` (a record-scoped row is EXCLUDED for an
34
+ * object-type / record-less query so a NULL-compare never spuriously matches).
35
+ * ≥1 matching row ⇒ mapped to a {@link CrossTenantGrant} (first match). No matching row ⇒
36
+ * `null` (a NORMAL data-absence — no active consent — NOT an error, NOT a lockout).
37
+ *
38
+ * ── SCOPE + RECORD-ID HANDLING: `object_type` rows are always eligible; a `record` row is
39
+ * eligible ONLY when `query.recordId` is present AND equals `ctg_record_uuid`. When the
40
+ * query has NO recordId (an object-type-level question), the SQL restricts to
41
+ * `ctg_scope = 'object_type'` so a record row's `ctg_record_uuid` is never compared to a
42
+ * bound NULL (which would never match — but we make the intent explicit and avoid the
43
+ * accidental all-records reach a `record_uuid = NULL` OR-branch could imply).
44
+ *
45
+ * ── ACTION CEILING PARSING: `ctg_allowed_actions` is a JSON column (a JSON array of action-key
46
+ * strings). ⚠️ The mariadb driver AUTO-PARSES a JSON column into a JS ARRAY on read, so the
47
+ * cell arrives already-parsed — `parseAllowedActions` accepts BOTH an already-parsed array
48
+ * (production) and a JSON string (test mocks). A malformed/empty value ⇒ `[]` (an empty
49
+ * ceiling permits nothing — fail-closed; core's ceiling check then denies).
50
+ *
51
+ * ── EXPIRY (MUST-FIX #1 — timezone immunity): `ctg_expires_at_ms` is a BIGINT epoch-ms column
52
+ * (NOT a TIMESTAMP). The active-grant filter compares it `> ?` against a BOUND integer (the
53
+ * adapter's `now()` epoch-ms), so it is immune to host-timezone / `NOW()` wall-clock skew (a
54
+ * TIMESTAMP + `NOW()` comparison silently kept expired grants alive on a non-UTC host). Mirrors
55
+ * the reset-token store's `prt_expires_at_ms` BIGINT precedent.
56
+ *
57
+ * ── EMPTY roleIds: guarded — an empty role list must NOT emit `IN ()` (a MariaDB syntax
58
+ * error). We omit the role branch entirely when there are no roles, so a role-less grantee
59
+ * is matched by its user-grant rows alone.
60
+ *
61
+ * ── ADR-0016 (transient-vs-deny): a genuine store outage THROWS
62
+ * {@link AuthorizationUnavailableError} (→ 503 retryable) — NEVER a silent `null` that
63
+ * would mask an outage as "no grant".
64
+ *
65
+ * ── SAFETY: every value (owningTenantId, typeKey, grantee, each roleId, recordId) is BOUND
66
+ * as a `?` param — NEVER interpolated. A SQL payload in typeKey/recordId is data, not SQL.
67
+ * Mirrors {@link MariaDbRecordShareAdapter}.
68
+ */
69
+ Object.defineProperty(exports, "__esModule", { value: true });
70
+ exports.MariaDbCrossTenantGrantAdapter = exports.SHIELD_CROSS_TENANT_GRANT_PORT = void 0;
71
+ const core_1 = require("@astram/core");
72
+ /** The DI token the adapter is bound under (parity with the share/rule-port tokens). */
73
+ exports.SHIELD_CROSS_TENANT_GRANT_PORT = Symbol('SHIELD_CROSS_TENANT_GRANT_PORT');
74
+ /** Schema the `cross_tenant_grant` table lives in (astram3 ships the migration). */
75
+ const DEFAULT_SCHEMA = 'shield_authz';
76
+ /**
77
+ * Parse `ctg_allowed_actions` to a `string[]`. ⚠️ The `ctg_allowed_actions` column is a JSON
78
+ * column, and the mariadb driver AUTO-PARSES it into a JS ARRAY on read — so `raw` is ALREADY an
79
+ * array in production. A test mock, however, may hand back a JSON STRING. This helper accepts
80
+ * BOTH:
81
+ * • already-parsed array ⇒ filter to strings, return directly (NO `JSON.parse` — parsing an
82
+ * array throws, which is exactly the bug that collapsed every ceiling to `[]`);
83
+ * • non-empty string ⇒ `JSON.parse`, then filter to strings;
84
+ * • anything else (null / empty / non-array-non-string / malformed) ⇒ `[]`.
85
+ * Non-string members are dropped (fail-closed — a hostile array can never widen the ceiling); an
86
+ * empty ceiling permits nothing.
87
+ */
88
+ function parseAllowedActions(raw) {
89
+ // Production path: the driver already parsed the JSON column into an array.
90
+ if (Array.isArray(raw)) {
91
+ return raw.filter((a) => typeof a === 'string');
92
+ }
93
+ // Mock / legacy path: a JSON string. Empty/null ⇒ empty ceiling.
94
+ if (typeof raw === 'string' && raw.length > 0) {
95
+ let parsed;
96
+ try {
97
+ parsed = JSON.parse(raw);
98
+ }
99
+ catch {
100
+ return [];
101
+ }
102
+ if (!Array.isArray(parsed))
103
+ return [];
104
+ return parsed.filter((a) => typeof a === 'string');
105
+ }
106
+ return [];
107
+ }
108
+ /** Coerce a BIGINT epoch-ms cell (number | bigint | numeric string | null) to a JS number. */
109
+ function toMs(value) {
110
+ if (value === null)
111
+ return 0;
112
+ const ms = typeof value === 'number' ? value : Number(value);
113
+ return Number.isFinite(ms) ? ms : 0;
114
+ }
115
+ /**
116
+ * The concrete cross-tenant grant adapter. Answers `findActiveGrant` from `cross_tenant_grant`,
117
+ * matching user- and role-granted, ACCEPTED, unexpired, unrevoked grants for the owning tenant
118
+ * + object type (+ record for record-scoped).
119
+ */
120
+ class MariaDbCrossTenantGrantAdapter {
121
+ db;
122
+ schema;
123
+ now;
124
+ constructor(db, opts = {}) {
125
+ this.db = db;
126
+ this.schema = opts.schema ?? DEFAULT_SCHEMA;
127
+ // The expiry filter binds THIS clock's epoch-ms against the BIGINT `ctg_expires_at_ms`
128
+ // column — timezone-immune (no Date/`NOW()` skew). Injectable for deterministic tests.
129
+ this.now = opts.nowMs ?? (() => Date.now());
130
+ }
131
+ async findActiveGrant(query) {
132
+ // Grantee predicate: the user grant OR (only when roles exist) a role grant. The role
133
+ // branch is OMITTED for an empty role list so we NEVER emit `IN ()` (a syntax error).
134
+ let granteeSql = `(ctg_grantee_kind = 'user' AND ctg_grantee_uuid = ?)`;
135
+ const granteeParams = [query.grantee];
136
+ if (query.roleIds.length > 0) {
137
+ const placeholders = query.roleIds.map(() => '?').join(', ');
138
+ granteeSql += ` OR (ctg_grantee_kind = 'role' AND ctg_grantee_uuid IN (${placeholders}))`;
139
+ granteeParams.push(...query.roleIds);
140
+ }
141
+ // Scope predicate. An `object_type` grant always matches this type; a `record` grant
142
+ // matches ONLY when the query names the exact record. When the query has NO recordId, we
143
+ // restrict to object_type grants (a record row's record_uuid must never be compared to a
144
+ // bound NULL — it would not match, but we make the intent explicit + never risk a NULL OR).
145
+ let scopeSql;
146
+ const scopeParams = [];
147
+ if (typeof query.recordId === 'string' && query.recordId.length > 0) {
148
+ scopeSql = `(ctg_scope = 'object_type' OR (ctg_scope = 'record' AND ctg_record_uuid = ?))`;
149
+ scopeParams.push(query.recordId);
150
+ }
151
+ else {
152
+ scopeSql = `ctg_scope = 'object_type'`;
153
+ }
154
+ // The expiry bound is a BIGINT epoch-ms comparison against a BOUND integer (this clock's
155
+ // now) — timezone-immune (no `NOW()`/Date wall-clock skew, MUST-FIX #1). Param order matches
156
+ // the SQL below exactly: owning_tenant, type_key, expiry_now, <grantee params>, <scope params>.
157
+ const nowMs = this.now();
158
+ const params = [
159
+ query.owningTenantId,
160
+ query.typeKey,
161
+ nowMs,
162
+ ...granteeParams,
163
+ ...scopeParams,
164
+ ];
165
+ let rows;
166
+ try {
167
+ rows = (await this.db.query(`SELECT ctg_scope, ctg_type_key, ctg_record_uuid, ctg_allowed_actions, ctg_expires_at_ms` +
168
+ ` FROM \`${this.schema}\`.\`cross_tenant_grant\`` +
169
+ ` WHERE ctg_owning_tenant_uuid = ?` +
170
+ ` AND ctg_type_key = ?` +
171
+ ` AND ctg_status = 'accepted'` +
172
+ ` AND ctg_revoked_at IS NULL` +
173
+ ` AND ctg_expires_at_ms > ?` +
174
+ ` AND (${granteeSql})` +
175
+ ` AND (${scopeSql})` +
176
+ // Prefer a record-scoped row over an object_type row when both match, and the
177
+ // latest-expiring first — deterministic single-match selection.
178
+ ` ORDER BY (ctg_scope = 'record') DESC, ctg_expires_at_ms DESC` +
179
+ ` LIMIT 1`, params));
180
+ }
181
+ catch (err) {
182
+ // Genuine store outage — fail closed as retryable (never mask as "no grant").
183
+ throw new core_1.AuthorizationUnavailableError(`cross_tenant_grant store unavailable for ` +
184
+ `${query.owningTenantId}:${query.typeKey}:${query.action}: ${err.message}`);
185
+ }
186
+ const row = rows[0];
187
+ if (!row) {
188
+ // No active consented grant — a NORMAL data-absence (NOT an error). Core reads this as
189
+ // "no cross-tenant permit" — deny-by-default for the reach.
190
+ return null;
191
+ }
192
+ // Map the row → CrossTenantGrant. Core's grant-evaluator re-checks allowedActions + scope
193
+ // + expiry; we still filtered status/expiry/revoke/scope in SQL so a revoked/expired/
194
+ // offered grant is never even returned.
195
+ const scope = row.ctg_scope === 'record' ? 'record' : 'object_type';
196
+ const grant = {
197
+ scope,
198
+ typeKey: row.ctg_type_key,
199
+ allowedActions: parseAllowedActions(row.ctg_allowed_actions),
200
+ // Already epoch-ms (BIGINT column) — just coerce the driver's number/string to a JS number.
201
+ expiresAtMs: toMs(row.ctg_expires_at_ms),
202
+ // `recordId` is only present for a record-scoped grant (exactOptionalPropertyTypes:
203
+ // never write an explicit `undefined` onto the optional field).
204
+ ...(scope === 'record' && row.ctg_record_uuid !== null
205
+ ? { recordId: row.ctg_record_uuid }
206
+ : {}),
207
+ };
208
+ return grant;
209
+ }
210
+ }
211
+ exports.MariaDbCrossTenantGrantAdapter = MariaDbCrossTenantGrantAdapter;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * `MariaDbRecordPermissionRuleAdapter` — the concrete {@link RecordPermissionRulePort}
3
+ * for `@astram/guard`, backed by the `record_permission_rule` table (design §5.1). It
4
+ * reads ADMIN rules ONLY and evaluates them for a single record+action, returning the
5
+ * `{ allow, explicitDeny }` verdict core's record-permit tier composes with the
6
+ * framework-owned owner BASELINE (design §5.2).
7
+ *
8
+ * ── ADMIN-ONLY BY CONSTRUCTION (not by a WHERE filter): the owner/system baselines are
9
+ * COMPUTED IN CORE (no rows), so this table is only ever WRITTEN admin-authored rules —
10
+ * there is no `provenance` column and every row here is an admin rule. ⚠️ If a future
11
+ * change EVER stores non-admin (e.g. `system`) rows in this table, this SELECT MUST add
12
+ * a `WHERE rpr_provenance = 'admin'` filter — otherwise a system row would be read as an
13
+ * admin rule and could conflict with core's immutable baseline.
14
+ *
15
+ * ── WHY HERE, NOT CORE (ADR-0003): the `mariadb` driver is a real runtime dependency;
16
+ * core defines only the PORT + the immutable owner baseline. This adapter is INJECTED
17
+ * into core's wired decision authority. It uses `@astram/core` TYPES only (erased).
18
+ *
19
+ * ── THE MODEL (design §5):
20
+ * • The OWNER baseline is CORE's job — it is NOT a row here. This table holds ONLY
21
+ * admin-authored rules that WIDEN the baseline (e.g. "Company Admins read all
22
+ * inspection_reports") or add explicit DENIES (deny-wins). There is no
23
+ * `owner`/`system` subject_kind row.
24
+ * • NO applicable admin rule ⇒ `{ allow: false, explicitDeny: false }` — a NORMAL
25
+ * data-absence (not a lockout): core's owner baseline still permits the owner. An
26
+ * empty table ⇒ owner-only access, exactly the safe default.
27
+ * • A matching `allow` rule ⇒ `allow: true`. A matching `deny` rule ⇒
28
+ * `explicitDeny: true` (deny-wins; core short-circuits the share on it).
29
+ *
30
+ * ── ADR-0016 (transient-vs-deny): a genuine store outage THROWS
31
+ * {@link AuthorizationUnavailableError} (→ 503 retryable) — NEVER a silent
32
+ * `{allow:false,explicitDeny:false}` that would mask an outage as a deny.
33
+ *
34
+ * ── SAFETY: every rule row is matched on `(tenant, type_key, action)` with all values
35
+ * BOUND as `?` params (no interpolation). The action match is exact OR the rule's
36
+ * wildcard `*`. Field conditions are evaluated in JS against the record attributes
37
+ * core supplies (design §11.1 builder shape) — the operator set is a fixed
38
+ * allow-list; an unknown operator makes the rule NOT match (fail-closed, never a
39
+ * spurious allow).
40
+ */
41
+ import type { RecordPermissionRulePort, RulePermit, RulePermitInput } from '@astram/core';
42
+ /** The DI token the adapter is bound under (parity with the session store token). */
43
+ export declare const SHIELD_RECORD_RULE_PORT: unique symbol;
44
+ /** The minimal query surface this adapter needs — a `mariadb` Pool satisfies it. */
45
+ export interface RuleQueryable {
46
+ query(sql: string, params?: readonly unknown[]): Promise<unknown>;
47
+ }
48
+ /**
49
+ * The concrete admin-rule adapter. Reads ADMIN rules for `(tenant, type_key, action|*)`
50
+ * and folds them into a `{ allow, explicitDeny }` verdict with deny-wins.
51
+ */
52
+ export declare class MariaDbRecordPermissionRuleAdapter implements RecordPermissionRulePort {
53
+ private readonly db;
54
+ private readonly schema;
55
+ constructor(db: RuleQueryable, opts?: {
56
+ schema?: string;
57
+ });
58
+ evaluate(input: RulePermitInput): Promise<RulePermit>;
59
+ }
60
+ //# sourceMappingURL=mariadb-record-rule-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mariadb-record-rule-adapter.d.ts","sourceRoot":"","sources":["../../src/authz/mariadb-record-rule-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,OAAO,KAAK,EACV,wBAAwB,EACxB,UAAU,EACV,eAAe,EAChB,MAAM,cAAc,CAAC;AAGtB,qFAAqF;AACrF,eAAO,MAAM,uBAAuB,eAAoC,CAAC;AAKzE,oFAAoF;AACpF,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACnE;AAsED;;;GAGG;AACH,qBAAa,kCAAmC,YAAW,wBAAwB;IACjF,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAgB;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBAEpB,EAAE,EAAE,aAAa,EAAE,IAAI,GAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAO;IAKvD,QAAQ,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,UAAU,CAAC;CAwC5D"}
@@ -0,0 +1,154 @@
1
+ "use strict";
2
+ /**
3
+ * `MariaDbRecordPermissionRuleAdapter` — the concrete {@link RecordPermissionRulePort}
4
+ * for `@astram/guard`, backed by the `record_permission_rule` table (design §5.1). It
5
+ * reads ADMIN rules ONLY and evaluates them for a single record+action, returning the
6
+ * `{ allow, explicitDeny }` verdict core's record-permit tier composes with the
7
+ * framework-owned owner BASELINE (design §5.2).
8
+ *
9
+ * ── ADMIN-ONLY BY CONSTRUCTION (not by a WHERE filter): the owner/system baselines are
10
+ * COMPUTED IN CORE (no rows), so this table is only ever WRITTEN admin-authored rules —
11
+ * there is no `provenance` column and every row here is an admin rule. ⚠️ If a future
12
+ * change EVER stores non-admin (e.g. `system`) rows in this table, this SELECT MUST add
13
+ * a `WHERE rpr_provenance = 'admin'` filter — otherwise a system row would be read as an
14
+ * admin rule and could conflict with core's immutable baseline.
15
+ *
16
+ * ── WHY HERE, NOT CORE (ADR-0003): the `mariadb` driver is a real runtime dependency;
17
+ * core defines only the PORT + the immutable owner baseline. This adapter is INJECTED
18
+ * into core's wired decision authority. It uses `@astram/core` TYPES only (erased).
19
+ *
20
+ * ── THE MODEL (design §5):
21
+ * • The OWNER baseline is CORE's job — it is NOT a row here. This table holds ONLY
22
+ * admin-authored rules that WIDEN the baseline (e.g. "Company Admins read all
23
+ * inspection_reports") or add explicit DENIES (deny-wins). There is no
24
+ * `owner`/`system` subject_kind row.
25
+ * • NO applicable admin rule ⇒ `{ allow: false, explicitDeny: false }` — a NORMAL
26
+ * data-absence (not a lockout): core's owner baseline still permits the owner. An
27
+ * empty table ⇒ owner-only access, exactly the safe default.
28
+ * • A matching `allow` rule ⇒ `allow: true`. A matching `deny` rule ⇒
29
+ * `explicitDeny: true` (deny-wins; core short-circuits the share on it).
30
+ *
31
+ * ── ADR-0016 (transient-vs-deny): a genuine store outage THROWS
32
+ * {@link AuthorizationUnavailableError} (→ 503 retryable) — NEVER a silent
33
+ * `{allow:false,explicitDeny:false}` that would mask an outage as a deny.
34
+ *
35
+ * ── SAFETY: every rule row is matched on `(tenant, type_key, action)` with all values
36
+ * BOUND as `?` params (no interpolation). The action match is exact OR the rule's
37
+ * wildcard `*`. Field conditions are evaluated in JS against the record attributes
38
+ * core supplies (design §11.1 builder shape) — the operator set is a fixed
39
+ * allow-list; an unknown operator makes the rule NOT match (fail-closed, never a
40
+ * spurious allow).
41
+ */
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.MariaDbRecordPermissionRuleAdapter = exports.SHIELD_RECORD_RULE_PORT = void 0;
44
+ const core_1 = require("@astram/core");
45
+ /** The DI token the adapter is bound under (parity with the session store token). */
46
+ exports.SHIELD_RECORD_RULE_PORT = Symbol('SHIELD_RECORD_RULE_PORT');
47
+ /** Schema the `record_permission_rule` table lives in (astram3 ships the migration). */
48
+ const DEFAULT_SCHEMA = 'shield_authz';
49
+ /** Whether the rule's subject matches this principal (role membership / everyone). */
50
+ function subjectMatches(row, input) {
51
+ const kind = row.rpr_subject_kind;
52
+ if (kind === 'everyone')
53
+ return true;
54
+ if (kind === 'role') {
55
+ return (row.rpr_subject_value !== null && input.principal.roleIds.includes(row.rpr_subject_value));
56
+ }
57
+ // Unknown subject kind → does NOT match (fail-closed).
58
+ return false;
59
+ }
60
+ /** Resolve a rule value token against the request (e.g. `$actor.id`) or use the literal. */
61
+ function resolveValue(raw, input) {
62
+ if (raw === '$actor.id')
63
+ return input.principal.id;
64
+ if (raw === '$actor.tenant')
65
+ return input.principal.tenantId;
66
+ return raw;
67
+ }
68
+ /** The value the rule's field condition tests, read from the record attributes core supplies. */
69
+ function fieldValue(field, input) {
70
+ return input.attributes?.[field];
71
+ }
72
+ /**
73
+ * Evaluate a rule's field condition. A rule with NO field (`rpr_field` null) is
74
+ * UNCONDITIONAL (applies whenever subject+action match). The operator set is a fixed
75
+ * allow-list; an unknown operator → the condition does NOT hold (fail-closed).
76
+ */
77
+ function conditionHolds(row, input) {
78
+ if (row.rpr_field === null)
79
+ return true; // unconditional
80
+ const actual = fieldValue(row.rpr_field, input);
81
+ const expectedRaw = row.rpr_value ?? '';
82
+ const expected = resolveValue(expectedRaw, input);
83
+ switch (row.rpr_operator) {
84
+ case 'equals':
85
+ return actual === expected;
86
+ case 'not_equals':
87
+ return actual !== expected;
88
+ case 'in': {
89
+ // comma-separated token list; membership by string comparison.
90
+ const set = String(expectedRaw)
91
+ .split(',')
92
+ .map((s) => s.trim());
93
+ return set.includes(String(actual));
94
+ }
95
+ case 'not_in': {
96
+ const set = String(expectedRaw)
97
+ .split(',')
98
+ .map((s) => s.trim());
99
+ return !set.includes(String(actual));
100
+ }
101
+ default:
102
+ return false; // unknown operator → fail-closed (rule does not apply)
103
+ }
104
+ }
105
+ /**
106
+ * The concrete admin-rule adapter. Reads ADMIN rules for `(tenant, type_key, action|*)`
107
+ * and folds them into a `{ allow, explicitDeny }` verdict with deny-wins.
108
+ */
109
+ class MariaDbRecordPermissionRuleAdapter {
110
+ db;
111
+ schema;
112
+ constructor(db, opts = {}) {
113
+ this.db = db;
114
+ this.schema = opts.schema ?? DEFAULT_SCHEMA;
115
+ }
116
+ async evaluate(input) {
117
+ // tenant scoping: match rows for the resource's tenant OR the global/system baseline
118
+ // (rpr_tenant_uuid IS NULL). Action match: the exact action OR the rule's `*` wildcard.
119
+ const tenantId = input.resource.tenantId ?? input.principal.tenantId;
120
+ let rows;
121
+ try {
122
+ rows = (await this.db.query(`SELECT rpr_effect, rpr_subject_kind, rpr_subject_value, rpr_field, rpr_operator, rpr_value
123
+ FROM \`${this.schema}\`.\`record_permission_rule\`
124
+ WHERE rpr_type_key = ?
125
+ AND (rpr_tenant_uuid = ? OR rpr_tenant_uuid IS NULL)
126
+ AND (rpr_action = ? OR rpr_action = '*')`, [input.resource.typeKey, tenantId, input.action.key]));
127
+ }
128
+ catch (err) {
129
+ // Genuine store outage — fail closed as retryable (never mask as a deny).
130
+ throw new core_1.AuthorizationUnavailableError(`record_permission_rule store unavailable for ` +
131
+ `${input.resource.typeKey}:${input.action.key}: ${err.message}`);
132
+ }
133
+ let allow = false;
134
+ let explicitDeny = false;
135
+ for (const row of rows) {
136
+ if (!subjectMatches(row, input))
137
+ continue;
138
+ if (!conditionHolds(row, input))
139
+ continue;
140
+ if (row.rpr_effect === 'deny') {
141
+ // Deny-wins — one matching deny is final for this tier.
142
+ explicitDeny = true;
143
+ }
144
+ else if (row.rpr_effect === 'allow') {
145
+ allow = true;
146
+ }
147
+ // any other effect string is ignored (fail-closed — no permit inferred).
148
+ }
149
+ // NO applicable admin rule ⇒ {false,false}: a normal data-absence. The CORE owner
150
+ // baseline still provides the owner permit — this is NOT a lockout.
151
+ return { allow, explicitDeny };
152
+ }
153
+ }
154
+ exports.MariaDbRecordPermissionRuleAdapter = MariaDbRecordPermissionRuleAdapter;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * `MariaDbRecordShareAdapter` — the concrete {@link RecordSharePort} for `@astram/guard`,
3
+ * backed by the `record_share` table (design §6.1). It answers `hasShare`: does the
4
+ * principal (by id or any of its roles) hold an active per-record share on this
5
+ * record+action?
6
+ *
7
+ * ── WHAT A SHARE IS: an INSTANCE-level grant (one row per record×grantee×action) that
8
+ * WIDENS gate 6 (design §6.3). It is a permit SOURCE unioned with the owner baseline +
9
+ * admin rules under deny-wins — a share can grant access an absent rule wouldn't, but it
10
+ * can NEVER override an explicit deny (core enforces that; this adapter only reports the
11
+ * share's presence via a strict boolean).
12
+ *
13
+ * ── WHY HERE, NOT CORE (ADR-0003): the `mariadb` driver is a real runtime dependency;
14
+ * core defines only the PORT. This adapter is INJECTED into core's wired decision
15
+ * authority. It uses `@astram/core` TYPES only (erased).
16
+ *
17
+ * ── THE MATCH (design §6.1): a row matches when ALL hold —
18
+ * • `rs_type_key` = the object type AND `rs_record_uuid` = the record;
19
+ * • the grantee is EITHER this user (`rs_grantee_kind='user'` AND
20
+ * `rs_grantee_uuid = principalId`) OR one of the principal's roles
21
+ * (`rs_grantee_kind='role'` AND `rs_grantee_uuid IN (roleIds)`);
22
+ * • the action matches exactly (`rs_action = action`) OR the row is all-actions
23
+ * (`rs_action = '*'`).
24
+ * ≥1 matching row ⇒ `true`. No matching row ⇒ `false` (a NORMAL data-absence — no
25
+ * share — NOT an error, NOT a lockout: the owner baseline / admin rules still govern).
26
+ *
27
+ * ── TENANT ISOLATION (design §6 "cross-tenant share impossible"): the {@link RecordSharePort}
28
+ * contract does NOT carry a tenant id (core evaluates same-tenant shares only — a
29
+ * cross-tenant reach is governed by the SEPARATE crossTenantGrant port, not a share).
30
+ * Isolation is therefore enforced at BOTH ends structurally:
31
+ * 1. WRITE-TIME (the authoritative bound, {@link RecordShareWriter}): a share row can
32
+ * ONLY be created for a record the sharer OWNS in the sharer's OWN tenant, and the
33
+ * row is stamped with that tenant (`rs_tenant_uuid`). A share for a foreign tenant's
34
+ * record is impossible to create.
35
+ * 2. READ-TIME (here): `rs_record_uuid` is a GLOBALLY-UNIQUE char36 record uuid — it
36
+ * identifies exactly one record in exactly one tenant. Matching on
37
+ * `(rs_type_key, rs_record_uuid)` therefore selects that one tenant's row by
38
+ * construction; a tenant-A share row can never match a tenant-B record because the
39
+ * two records have DIFFERENT uuids. The record uuid IS the tenant discriminator.
40
+ * (A future core-contract change that threads the resource tenant into `hasShare` would
41
+ * let this add an explicit `rs_tenant_uuid = ?` bound too — flagged for the architect;
42
+ * it is a defense-in-depth ADD, not a correctness fix, given uuid uniqueness.)
43
+ *
44
+ * ── EMPTY roleIds: guarded — an empty role list must NOT emit `IN ()` (a MariaDB syntax
45
+ * error). We omit the role branch entirely when there are no roles, so a role-less
46
+ * principal is matched by its user-share rows alone.
47
+ *
48
+ * ── ADR-0016 (transient-vs-deny): a genuine store outage THROWS
49
+ * {@link AuthorizationUnavailableError} (→ 503 retryable) — NEVER a silent `false` that
50
+ * would mask an outage as "no share".
51
+ *
52
+ * ── SAFETY: every value (typeKey, recordId, principalId, each roleId, action) is BOUND as
53
+ * a `?` param — NEVER interpolated. A SQL payload in typeKey/recordId is data, not SQL.
54
+ * Mirrors {@link MariaDbRecordPermissionRuleAdapter}.
55
+ */
56
+ import type { RecordSharePort } from '@astram/core';
57
+ /** The DI token the adapter is bound under (parity with the rule-port token). */
58
+ export declare const SHIELD_RECORD_SHARE_PORT: unique symbol;
59
+ /** The minimal query surface this adapter needs — a `mariadb` Pool satisfies it. */
60
+ export interface ShareQueryable {
61
+ query(sql: string, params?: readonly unknown[]): Promise<unknown>;
62
+ }
63
+ /**
64
+ * The concrete per-record share adapter. Answers `hasShare` from `record_share`, matching
65
+ * user- and role-granted shares with exact-or-wildcard action.
66
+ */
67
+ export declare class MariaDbRecordShareAdapter implements RecordSharePort {
68
+ private readonly db;
69
+ private readonly schema;
70
+ constructor(db: ShareQueryable, opts?: {
71
+ schema?: string;
72
+ });
73
+ hasShare(principalId: string, roleIds: readonly string[], typeKey: string, recordId: string, action: string): Promise<boolean>;
74
+ }
75
+ //# sourceMappingURL=mariadb-record-share-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mariadb-record-share-adapter.d.ts","sourceRoot":"","sources":["../../src/authz/mariadb-record-share-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAGpD,iFAAiF;AACjF,eAAO,MAAM,wBAAwB,eAAqC,CAAC;AAK3E,oFAAoF;AACpF,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACnE;AAOD;;;GAGG;AACH,qBAAa,yBAA0B,YAAW,eAAe;IAC/D,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAiB;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBAEpB,EAAE,EAAE,cAAc,EAAE,IAAI,GAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAO;IAKxD,QAAQ,CACZ,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,OAAO,CAAC;CAqCpB"}