@astram/guard 0.18.0 → 0.20.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 (144) hide show
  1. package/dist/audit/audit-chain-sealer.d.ts +90 -0
  2. package/dist/audit/audit-chain-sealer.d.ts.map +1 -0
  3. package/dist/audit/audit-chain-sealer.js +520 -0
  4. package/dist/audit/audit-seal-config.d.ts +22 -0
  5. package/dist/audit/audit-seal-config.d.ts.map +1 -0
  6. package/dist/audit/audit-seal-config.js +35 -0
  7. package/dist/audit/audit-sealer.service.d.ts +62 -0
  8. package/dist/audit/audit-sealer.service.d.ts.map +1 -0
  9. package/dist/audit/audit-sealer.service.js +197 -0
  10. package/dist/audit/node-sha256-hash.d.ts +6 -0
  11. package/dist/audit/node-sha256-hash.d.ts.map +1 -0
  12. package/dist/audit/node-sha256-hash.js +16 -0
  13. package/dist/auth/account.controller.d.ts +3 -1
  14. package/dist/auth/account.controller.d.ts.map +1 -1
  15. package/dist/auth/account.controller.js +79 -6
  16. package/dist/auth/admin-signin-controls.d.ts +80 -0
  17. package/dist/auth/admin-signin-controls.d.ts.map +1 -0
  18. package/dist/auth/admin-signin-controls.js +91 -0
  19. package/dist/auth/auth-audit.d.ts +59 -0
  20. package/dist/auth/auth-audit.d.ts.map +1 -0
  21. package/dist/auth/auth-audit.js +93 -0
  22. package/dist/auth/auth.controller.d.ts +53 -6
  23. package/dist/auth/auth.controller.d.ts.map +1 -1
  24. package/dist/auth/auth.controller.js +330 -44
  25. package/dist/auth/auth.module.d.ts +49 -0
  26. package/dist/auth/auth.module.d.ts.map +1 -1
  27. package/dist/auth/auth.module.js +149 -4
  28. package/dist/auth/captcha-gate.d.ts +103 -0
  29. package/dist/auth/captcha-gate.d.ts.map +1 -0
  30. package/dist/auth/captcha-gate.js +245 -0
  31. package/dist/auth/dto.d.ts +22 -1
  32. package/dist/auth/dto.d.ts.map +1 -1
  33. package/dist/auth/login-name-count-boot.d.ts +24 -0
  34. package/dist/auth/login-name-count-boot.d.ts.map +1 -0
  35. package/dist/auth/login-name-count-boot.js +47 -0
  36. package/dist/auth/logout-proof.d.ts +55 -0
  37. package/dist/auth/logout-proof.d.ts.map +1 -0
  38. package/dist/auth/logout-proof.js +127 -0
  39. package/dist/auth/pending-mfa-store.d.ts +5 -0
  40. package/dist/auth/pending-mfa-store.d.ts.map +1 -1
  41. package/dist/auth/pending-mfa-store.js +2 -0
  42. package/dist/auth/persistence.module.d.ts +2 -2
  43. package/dist/auth/persistence.module.d.ts.map +1 -1
  44. package/dist/auth/persistence.module.js +19 -3
  45. package/dist/auth/ports/audit-sink.d.ts +5 -2
  46. package/dist/auth/ports/audit-sink.d.ts.map +1 -1
  47. package/dist/auth/ports/mfa-device-store.d.ts +7 -0
  48. package/dist/auth/ports/mfa-device-store.d.ts.map +1 -1
  49. package/dist/auth/profile-resolver.d.ts +6 -0
  50. package/dist/auth/profile-resolver.d.ts.map +1 -1
  51. package/dist/auth/rate-limit/attempt-limiter.d.ts +6 -0
  52. package/dist/auth/rate-limit/attempt-limiter.d.ts.map +1 -1
  53. package/dist/auth/rate-limit/attempt-limiter.js +14 -0
  54. package/dist/auth/tokens.d.ts +59 -0
  55. package/dist/auth/tokens.d.ts.map +1 -1
  56. package/dist/auth/tokens.js +61 -1
  57. package/dist/authentication/credential-provisioner.d.ts +7 -0
  58. package/dist/authentication/credential-provisioner.d.ts.map +1 -1
  59. package/dist/authentication/mariadb-credential-provisioner.d.ts +1 -0
  60. package/dist/authentication/mariadb-credential-provisioner.d.ts.map +1 -1
  61. package/dist/authentication/mariadb-credential-provisioner.js +7 -0
  62. package/dist/authentication/mariadb-credential-resolver.d.ts +23 -2
  63. package/dist/authentication/mariadb-credential-resolver.d.ts.map +1 -1
  64. package/dist/authentication/mariadb-credential-resolver.js +45 -1
  65. package/dist/authentication/mariadb-mfa-device-store.d.ts +7 -0
  66. package/dist/authentication/mariadb-mfa-device-store.d.ts.map +1 -1
  67. package/dist/authentication/mariadb-mfa-device-store.js +32 -0
  68. package/dist/authentication/mariadb-mfa-requirement-source.d.ts +55 -0
  69. package/dist/authentication/mariadb-mfa-requirement-source.d.ts.map +1 -0
  70. package/dist/authentication/mariadb-mfa-requirement-source.js +164 -0
  71. package/dist/authentication/mariadb-mfa-resetter.d.ts +42 -0
  72. package/dist/authentication/mariadb-mfa-resetter.d.ts.map +1 -0
  73. package/dist/authentication/mariadb-mfa-resetter.js +59 -0
  74. package/dist/authentication/mariadb-profile-writer.d.ts.map +1 -1
  75. package/dist/authentication/mariadb-profile-writer.js +5 -0
  76. package/dist/authentication/sign-in-with.d.ts +14 -0
  77. package/dist/authentication/sign-in-with.d.ts.map +1 -0
  78. package/dist/authentication/sign-in-with.js +36 -0
  79. package/dist/authz/core-access.d.ts +11 -0
  80. package/dist/authz/core-access.d.ts.map +1 -0
  81. package/dist/authz/core-access.js +14 -0
  82. package/dist/authz/list-scope-sql.d.ts.map +1 -1
  83. package/dist/authz/list-scope-sql.js +15 -5
  84. package/dist/authz/wire-record-authorization.d.ts.map +1 -1
  85. package/dist/authz/wire-record-authorization.js +21 -3
  86. package/dist/captcha/captcha.providers.d.ts +18 -0
  87. package/dist/captcha/captcha.providers.d.ts.map +1 -0
  88. package/dist/captcha/captcha.providers.js +78 -0
  89. package/dist/captcha/recaptcha-verifier.d.ts +20 -0
  90. package/dist/captcha/recaptcha-verifier.d.ts.map +1 -0
  91. package/dist/captcha/recaptcha-verifier.js +48 -0
  92. package/dist/captcha/siteverify.d.ts +47 -0
  93. package/dist/captcha/siteverify.d.ts.map +1 -0
  94. package/dist/captcha/siteverify.js +98 -0
  95. package/dist/captcha/turnstile-verifier.d.ts +18 -0
  96. package/dist/captcha/turnstile-verifier.d.ts.map +1 -0
  97. package/dist/captcha/turnstile-verifier.js +41 -0
  98. package/dist/config/captcha-config.d.ts +47 -0
  99. package/dist/config/captcha-config.d.ts.map +1 -0
  100. package/dist/config/captcha-config.js +163 -0
  101. package/dist/config/reserved-principal-ids.d.ts +15 -0
  102. package/dist/config/reserved-principal-ids.d.ts.map +1 -1
  103. package/dist/config/reserved-principal-ids.js +20 -3
  104. package/dist/enforcement/global-enforcement.guard.d.ts +9 -1
  105. package/dist/enforcement/global-enforcement.guard.d.ts.map +1 -1
  106. package/dist/enforcement/global-enforcement.guard.js +52 -10
  107. package/dist/enforcement/reenroll-allowlist.d.ts +5 -0
  108. package/dist/enforcement/reenroll-allowlist.d.ts.map +1 -1
  109. package/dist/enforcement/reenroll-allowlist.js +5 -0
  110. package/dist/identity/auth-facts.controller.d.ts +31 -4
  111. package/dist/identity/auth-facts.controller.d.ts.map +1 -1
  112. package/dist/identity/auth-facts.controller.js +46 -11
  113. package/dist/identity/identity-actions.d.ts +2 -2
  114. package/dist/identity/identity-actions.d.ts.map +1 -1
  115. package/dist/identity/module-registry.bridge.d.ts.map +1 -1
  116. package/dist/identity/module-registry.bridge.js +3 -6
  117. package/dist/identity/permission-facts.adapter.d.ts +22 -0
  118. package/dist/identity/permission-facts.adapter.d.ts.map +1 -1
  119. package/dist/identity/permission-facts.adapter.js +26 -1
  120. package/dist/index.d.ts +11 -2
  121. package/dist/index.d.ts.map +1 -1
  122. package/dist/index.js +43 -11
  123. package/dist/internal.d.ts +27 -0
  124. package/dist/internal.d.ts.map +1 -1
  125. package/dist/internal.js +36 -1
  126. package/dist/module/shield.module.d.ts +27 -0
  127. package/dist/module/shield.module.d.ts.map +1 -1
  128. package/dist/module/shield.module.js +10 -0
  129. package/dist/request-auth/request-principal.d.ts +8 -1
  130. package/dist/request-auth/request-principal.d.ts.map +1 -1
  131. package/dist/request-auth/request-principal.js +11 -4
  132. package/dist/request-auth/session-token-binder.d.ts +82 -0
  133. package/dist/request-auth/session-token-binder.d.ts.map +1 -1
  134. package/dist/request-auth/session-token-binder.js +203 -1
  135. package/dist/session/mfa-requirement-service.d.ts +168 -0
  136. package/dist/session/mfa-requirement-service.d.ts.map +1 -0
  137. package/dist/session/mfa-requirement-service.js +461 -0
  138. package/dist/session/session-restriction-store.d.ts +36 -0
  139. package/dist/session/session-restriction-store.d.ts.map +1 -1
  140. package/dist/session/session-restriction-store.js +57 -6
  141. package/dist/session/session-restriction.d.ts +22 -0
  142. package/dist/session/session-restriction.d.ts.map +1 -0
  143. package/dist/session/session-restriction.js +23 -0
  144. package/package.json +10 -5
@@ -0,0 +1,55 @@
1
+ /**
2
+ * `MariaDbMfaRequirementSource` — the guard's MariaDB adapter for core's
3
+ * {@link MfaRequirementSource} port (auth capabilities B-1, R2.4, R2.8, R2.9). Guard never imports
4
+ * identity (ADR-0003): it reads identity's tables directly with its runtime account, as
5
+ * `permission-facts.adapter.ts` does.
6
+ *
7
+ * - scope chain: `<authz>.scope_closure` ancestors of the node (the node itself always included);
8
+ * - policies: `<app>.security_policy` rows on the chain, plus EVERY platform-floor row;
9
+ * - enrolled: a CONFIRMED `<identity>.mfa_device` row (R2.8: every device is TOTP today; pending
10
+ * devices and recovery codes never count);
11
+ * - the grace clock: `<identity>.mfa_requirement_state`, kept at the EARLIER value (`LEAST`).
12
+ *
13
+ * Lifecycle reads only (sign-in, a permission-version move, device removal): never on an unchanged
14
+ * version (the caller's stamp check guarantees it). Database names come from the resolved framework
15
+ * schemas, never a literal. The adapter is built only when the policy feature is wired, so a
16
+ * missing policy table (identity migration 104 not applied) is an error like any other: it
17
+ * propagates and the caller fails closed (review round 1, item D). A missing clock table still
18
+ * reads as "none stored" (the first clock write then fails closed). Every other error propagates.
19
+ */
20
+ import type { MfaPolicy, MfaRequirementSource } from '@astram/core';
21
+ import type { MfaStatusBatchSource } from '../session/mfa-requirement-service.js';
22
+ import { type FrameworkSchemaNames } from '../config/framework-schema-names.js';
23
+ /** The one pool method the adapter uses. */
24
+ export interface MfaSourcePool {
25
+ query(sql: string, params?: unknown[]): Promise<unknown>;
26
+ }
27
+ export declare class MariaDbMfaRequirementSource implements MfaRequirementSource, MfaStatusBatchSource {
28
+ private readonly pool;
29
+ private readonly closureTable;
30
+ private readonly policyTable;
31
+ private readonly deviceTable;
32
+ private readonly stateTable;
33
+ private readonly membershipTable;
34
+ constructor(pool: MfaSourcePool,
35
+ /** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
36
+ schemas?: FrameworkSchemaNames);
37
+ scopeChainOf(nodeId: string): Promise<readonly string[]>;
38
+ loadPolicies(nodeIds: readonly string[]): Promise<readonly MfaPolicy[]>;
39
+ hasConfirmedFactor(principalId: string): Promise<boolean>;
40
+ getRequiredSince(principalId: string): Promise<number | null>;
41
+ setRequiredSince(principalId: string, sinceMs: number): Promise<number>;
42
+ /** R11.3: the ids among `principalIds` with a CONFIRMED device. One `IN` query; none ⇒ no query. */
43
+ confirmedFactorsOf(principalIds: readonly string[]): Promise<ReadonlySet<string>>;
44
+ /** R11.3: stored grace clocks for `principalIds`. One `IN` query; no table ⇒ none. */
45
+ requiredSinceOf(principalIds: readonly string[]): Promise<ReadonlyMap<string, number>>;
46
+ /**
47
+ * Review item E2: the principal's ACTIVE membership nodes (`<app>.company_membership`,
48
+ * `cm_is_active = 1`, the predicate the users page uses), for the sign-in clock check. One query.
49
+ */
50
+ activeNodesOf(principalId: string): Promise<readonly string[]>;
51
+ /** R11.3: each node with its ancestors (the node itself always included). One `IN` query. */
52
+ scopeChainsOf(nodeIds: readonly string[]): Promise<ReadonlyMap<string, readonly string[]>>;
53
+ clearRequiredSince(principalId: string): Promise<void>;
54
+ }
55
+ //# sourceMappingURL=mariadb-mfa-requirement-source.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mariadb-mfa-requirement-source.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-mfa-requirement-source.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,oBAAoB,EAA4B,MAAM,cAAc,CAAC;AAC9F,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,uCAAuC,CAAC;AAClF,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,qCAAqC,CAAC;AAE7C,4CAA4C;AAC5C,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1D;AAsCD,qBAAa,2BAA4B,YAAW,oBAAoB,EAAE,oBAAoB;IAQ1F,OAAO,CAAC,QAAQ,CAAC,IAAI;IAPvB,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;gBAGtB,IAAI,EAAE,aAAa;IACpC,yFAAyF;IACzF,OAAO,CAAC,EAAE,oBAAoB;IAU1B,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC;IAUxD,YAAY,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,SAAS,SAAS,EAAE,CAAC;IAevE,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAQzD,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAiB7D,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAa7E,oGAAoG;IAC9F,kBAAkB,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;IAavF,sFAAsF;IAChF,eAAe,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IA4B5F;;;OAGG;IACG,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC;IASpE,6FAA6F;IACvF,aAAa,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,WAAW,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC;IAgB1F,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAS7D"}
@@ -0,0 +1,164 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MariaDbMfaRequirementSource = void 0;
4
+ const framework_schema_names_js_1 = require("../config/framework-schema-names.js");
5
+ /** MariaDB `ER_NO_SUCH_TABLE`. */
6
+ const ER_NO_SUCH_TABLE = 1146;
7
+ function isNoSuchTable(err) {
8
+ return err?.errno === ER_NO_SUCH_TABLE;
9
+ }
10
+ /** A stored row as the core rule reads it. Unknown kinds pass through (the rule applies them). */
11
+ function toPolicy(r) {
12
+ const platform = Number(r.sp_platform) === 1;
13
+ const grace = Number(r.sp_grace_days);
14
+ return {
15
+ ...(typeof r.sp_uuid === 'string' ? { id: r.sp_uuid } : {}),
16
+ scopeNodeId: typeof r.sp_scope_node_uuid === 'string' ? r.sp_scope_node_uuid : null,
17
+ platform,
18
+ kind: String(r.sp_target_kind),
19
+ target: typeof r.sp_target_value === 'string' ? r.sp_target_value : null,
20
+ // An unreadable grace becomes NaN, which the rule counts as 0 (the strictest).
21
+ graceDays: grace,
22
+ };
23
+ }
24
+ /** `?, ?, …` for an `IN` list. */
25
+ function placeholders(n) {
26
+ return Array.from({ length: n }, () => '?').join(', ');
27
+ }
28
+ class MariaDbMfaRequirementSource {
29
+ pool;
30
+ closureTable;
31
+ policyTable;
32
+ deviceTable;
33
+ stateTable;
34
+ membershipTable;
35
+ constructor(pool,
36
+ /** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
37
+ schemas) {
38
+ this.pool = pool;
39
+ const s = (0, framework_schema_names_js_1.schemaNamesAtConstruction)(schemas);
40
+ this.closureTable = (0, framework_schema_names_js_1.qualifiedTableName)(s.authz, 'scope_closure');
41
+ this.policyTable = (0, framework_schema_names_js_1.qualifiedTableName)(s.app, 'security_policy');
42
+ this.deviceTable = (0, framework_schema_names_js_1.qualifiedTableName)(s.identity, 'mfa_device');
43
+ this.stateTable = (0, framework_schema_names_js_1.qualifiedTableName)(s.identity, 'mfa_requirement_state');
44
+ this.membershipTable = (0, framework_schema_names_js_1.qualifiedTableName)(s.app, 'company_membership');
45
+ }
46
+ async scopeChainOf(nodeId) {
47
+ const rows = (await this.pool.query(`SELECT ancestor_uuid FROM ${this.closureTable} WHERE descendant_uuid = ?`, [nodeId]));
48
+ const chain = new Set([nodeId]);
49
+ for (const r of rows)
50
+ if (typeof r.ancestor_uuid === 'string')
51
+ chain.add(r.ancestor_uuid);
52
+ return [...chain];
53
+ }
54
+ async loadPolicies(nodeIds) {
55
+ const ids = nodeIds.filter((n) => typeof n === 'string' && n.length > 0);
56
+ const where = ids.length > 0
57
+ ? `sp_platform = 1 OR sp_scope_node_uuid IN (${ids.map(() => '?').join(', ')})`
58
+ : 'sp_platform = 1';
59
+ // Review round 1, item D: no fallback for a missing table (1146). This adapter exists only
60
+ // when the policy feature is wired, so the error propagates and the caller fails closed (503).
61
+ const rows = (await this.pool.query(`SELECT sp_uuid, sp_scope_node_uuid, sp_platform, sp_target_kind, sp_target_value, sp_grace_days FROM ${this.policyTable} WHERE ${where}`, [...ids]));
62
+ return rows.map(toPolicy);
63
+ }
64
+ async hasConfirmedFactor(principalId) {
65
+ const rows = (await this.pool.query(`SELECT 1 AS ok FROM ${this.deviceTable} WHERE md_principal_id = ? AND md_confirmed_at_ms IS NOT NULL LIMIT 1`, [principalId]));
66
+ return Array.isArray(rows) && rows.length > 0;
67
+ }
68
+ async getRequiredSince(principalId) {
69
+ let rows;
70
+ try {
71
+ rows = (await this.pool.query(`SELECT mrs_required_since_ms FROM ${this.stateTable} WHERE mrs_principal_id = ? LIMIT 1`, [principalId]));
72
+ }
73
+ catch (err) {
74
+ if (isNoSuchTable(err))
75
+ return null;
76
+ throw err;
77
+ }
78
+ const v = rows[0]?.mrs_required_since_ms;
79
+ if (v === undefined || v === null)
80
+ return null;
81
+ const n = Number(v);
82
+ return Number.isFinite(n) ? n : null;
83
+ }
84
+ async setRequiredSince(principalId, sinceMs) {
85
+ await this.pool.query(`INSERT INTO ${this.stateTable} (mrs_principal_id, mrs_required_since_ms) VALUES (?, ?)
86
+ ON DUPLICATE KEY UPDATE
87
+ mrs_required_since_ms = LEAST(mrs_required_since_ms, VALUES(mrs_required_since_ms))`, [principalId, sinceMs]);
88
+ const stored = await this.getRequiredSince(principalId);
89
+ // The row was just written; a missing read-back is not a value to trust (fail closed: the
90
+ // caller keeps its own, which is never later than the stored one could be).
91
+ return stored ?? sinceMs;
92
+ }
93
+ /** R11.3: the ids among `principalIds` with a CONFIRMED device. One `IN` query; none ⇒ no query. */
94
+ async confirmedFactorsOf(principalIds) {
95
+ const ids = [...new Set(principalIds.filter((i) => typeof i === 'string' && i.length > 0))];
96
+ if (ids.length === 0)
97
+ return new Set();
98
+ const rows = (await this.pool.query(`SELECT DISTINCT md_principal_id FROM ${this.deviceTable}
99
+ WHERE md_principal_id IN (${placeholders(ids.length)}) AND md_confirmed_at_ms IS NOT NULL`, ids));
100
+ return new Set(rows.map((r) => r.md_principal_id).filter((v) => typeof v === 'string'));
101
+ }
102
+ /** R11.3: stored grace clocks for `principalIds`. One `IN` query; no table ⇒ none. */
103
+ async requiredSinceOf(principalIds) {
104
+ const ids = [...new Set(principalIds.filter((i) => typeof i === 'string' && i.length > 0))];
105
+ const out = new Map();
106
+ if (ids.length === 0)
107
+ return out;
108
+ let rows;
109
+ try {
110
+ rows = (await this.pool.query(`SELECT mrs_principal_id, mrs_required_since_ms FROM ${this.stateTable}
111
+ WHERE mrs_principal_id IN (${placeholders(ids.length)})`, ids));
112
+ }
113
+ catch (err) {
114
+ if (isNoSuchTable(err))
115
+ return out;
116
+ throw err;
117
+ }
118
+ for (const r of rows) {
119
+ const n = Number(r.mrs_required_since_ms);
120
+ if (typeof r.mrs_principal_id === 'string' &&
121
+ r.mrs_required_since_ms !== null &&
122
+ Number.isFinite(n)) {
123
+ out.set(r.mrs_principal_id, n);
124
+ }
125
+ }
126
+ return out;
127
+ }
128
+ /**
129
+ * Review item E2: the principal's ACTIVE membership nodes (`<app>.company_membership`,
130
+ * `cm_is_active = 1`, the predicate the users page uses), for the sign-in clock check. One query.
131
+ */
132
+ async activeNodesOf(principalId) {
133
+ const rows = (await this.pool.query(`SELECT DISTINCT cm_company_uuid FROM ${this.membershipTable}
134
+ WHERE cm_user_uuid = ? AND cm_is_active = 1`, [principalId]));
135
+ return rows.map((r) => r.cm_company_uuid).filter((v) => typeof v === 'string');
136
+ }
137
+ /** R11.3: each node with its ancestors (the node itself always included). One `IN` query. */
138
+ async scopeChainsOf(nodeIds) {
139
+ const ids = [...new Set(nodeIds.filter((i) => typeof i === 'string' && i.length > 0))];
140
+ const chains = new Map(ids.map((n) => [n, new Set([n])]));
141
+ if (ids.length === 0)
142
+ return new Map();
143
+ const rows = (await this.pool.query(`SELECT descendant_uuid, ancestor_uuid FROM ${this.closureTable}
144
+ WHERE descendant_uuid IN (${placeholders(ids.length)})`, ids));
145
+ for (const r of rows) {
146
+ if (typeof r.descendant_uuid !== 'string' || typeof r.ancestor_uuid !== 'string')
147
+ continue;
148
+ chains.get(r.descendant_uuid)?.add(r.ancestor_uuid);
149
+ }
150
+ return new Map([...chains].map(([n, set]) => [n, [...set]]));
151
+ }
152
+ async clearRequiredSince(principalId) {
153
+ try {
154
+ await this.pool.query(`DELETE FROM ${this.stateTable} WHERE mrs_principal_id = ?`, [
155
+ principalId,
156
+ ]);
157
+ }
158
+ catch (err) {
159
+ if (!isNoSuchTable(err))
160
+ throw err;
161
+ }
162
+ }
163
+ }
164
+ exports.MariaDbMfaRequirementSource = MariaDbMfaRequirementSource;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * `MariaDbMfaResetter` — auth capabilities R11.1 (task 18): an admin "reset 2FA" for one principal.
3
+ * ONE transaction on the given pool deletes the principal's MFA devices (confirmed and pending),
4
+ * recovery codes and mandatory-2FA grace clock (`mfa_requirement_state`, so the clock restarts at
5
+ * their next sign-in, R2.4), then runs the caller's `withinTransaction` (identity writes its
6
+ * `auth.mfa.reset_by_admin` audit row there) and commits. Any error, the audit write included,
7
+ * rolls the whole reset back and propagates (the caller answers 503).
8
+ *
9
+ * Sessions are not touched here: the caller revokes them through the session sweeper after the
10
+ * commit. Guard never imports identity (ADR-0003): identity builds this over its runtime pool, as it
11
+ * does `MariaDbEmailAddressChanger`, and reaches it through `@astram/guard/internal`. Database names
12
+ * come from the resolved framework schemas, never a literal. An app without identity migration 104
13
+ * has no clock table: that delete is skipped.
14
+ */
15
+ import type { Pool } from 'mariadb';
16
+ import { type FrameworkSchemaNames } from '../config/framework-schema-names.js';
17
+ /** A query on the reset's own transaction connection. */
18
+ export type MfaResetQuery = (sql: string, params?: unknown[]) => Promise<unknown>;
19
+ export interface MfaResetResult {
20
+ /** Device rows deleted (audit or logging only; never returned to a client). */
21
+ readonly devicesRemoved: number;
22
+ }
23
+ export interface MfaResetRequest {
24
+ readonly principalId: string;
25
+ /** Runs on the reset's transaction before the commit; a throw rolls everything back. */
26
+ readonly withinTransaction: (query: MfaResetQuery, result: MfaResetResult) => Promise<void>;
27
+ }
28
+ /** The seam identity uses (built over its runtime pool). */
29
+ export interface MfaResetter {
30
+ reset(request: MfaResetRequest): Promise<MfaResetResult>;
31
+ }
32
+ export declare class MariaDbMfaResetter implements MfaResetter {
33
+ private readonly pool;
34
+ private readonly deviceTable;
35
+ private readonly recoveryTable;
36
+ private readonly stateTable;
37
+ constructor(pool: Pick<Pool, 'getConnection'>,
38
+ /** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
39
+ schemas?: FrameworkSchemaNames);
40
+ reset(request: MfaResetRequest): Promise<MfaResetResult>;
41
+ }
42
+ //# sourceMappingURL=mariadb-mfa-resetter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mariadb-mfa-resetter.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-mfa-resetter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,IAAI,EAAkB,MAAM,SAAS,CAAC;AACpD,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,qCAAqC,CAAC;AAK7C,yDAAyD;AACzD,MAAM,MAAM,aAAa,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAElF,MAAM,WAAW,cAAc;IAC7B,+EAA+E;IAC/E,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,wFAAwF;IACxF,QAAQ,CAAC,iBAAiB,EAAE,CAAC,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7F;AAED,4DAA4D;AAC5D,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CAC1D;AAED,qBAAa,kBAAmB,YAAW,WAAW;IAMlD,OAAO,CAAC,QAAQ,CAAC,IAAI;IALvB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;gBAGjB,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,eAAe,CAAC;IAClD,yFAAyF;IACzF,OAAO,CAAC,EAAE,oBAAoB;IAQ1B,KAAK,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC;CAoC/D"}
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MariaDbMfaResetter = void 0;
4
+ const framework_schema_names_js_1 = require("../config/framework-schema-names.js");
5
+ /** MariaDB `ER_NO_SUCH_TABLE`. */
6
+ const ER_NO_SUCH_TABLE = 1146;
7
+ class MariaDbMfaResetter {
8
+ pool;
9
+ deviceTable;
10
+ recoveryTable;
11
+ stateTable;
12
+ constructor(pool,
13
+ /** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
14
+ schemas) {
15
+ this.pool = pool;
16
+ const s = (0, framework_schema_names_js_1.schemaNamesAtConstruction)(schemas);
17
+ this.deviceTable = (0, framework_schema_names_js_1.qualifiedTableName)(s.identity, 'mfa_device');
18
+ this.recoveryTable = (0, framework_schema_names_js_1.qualifiedTableName)(s.identity, 'mfa_recovery_code');
19
+ this.stateTable = (0, framework_schema_names_js_1.qualifiedTableName)(s.identity, 'mfa_requirement_state');
20
+ }
21
+ async reset(request) {
22
+ const { principalId } = request;
23
+ if (typeof principalId !== 'string' || principalId === '') {
24
+ throw new Error('MariaDbMfaResetter: a principal id is required');
25
+ }
26
+ const conn = await this.pool.getConnection();
27
+ try {
28
+ await conn.beginTransaction();
29
+ try {
30
+ const devices = (await conn.query(`DELETE FROM ${this.deviceTable} WHERE md_principal_id = ?`, [principalId]));
31
+ await conn.query(`DELETE FROM ${this.recoveryTable} WHERE mrc_principal_id = ?`, [
32
+ principalId,
33
+ ]);
34
+ try {
35
+ await conn.query(`DELETE FROM ${this.stateTable} WHERE mrs_principal_id = ?`, [
36
+ principalId,
37
+ ]);
38
+ }
39
+ catch (err) {
40
+ // Statement-level failure only (the transaction stays open): no clock table, no clock.
41
+ if (err?.errno !== ER_NO_SUCH_TABLE)
42
+ throw err;
43
+ }
44
+ const result = { devicesRemoved: Number(devices?.affectedRows ?? 0) };
45
+ await request.withinTransaction((sql, params) => conn.query(sql, params), result);
46
+ await conn.commit();
47
+ return result;
48
+ }
49
+ catch (err) {
50
+ await conn.rollback().catch(() => undefined);
51
+ throw err;
52
+ }
53
+ }
54
+ finally {
55
+ conn.release();
56
+ }
57
+ }
58
+ }
59
+ exports.MariaDbMfaResetter = MariaDbMfaResetter;
@@ -1 +1 @@
1
- {"version":3,"file":"mariadb-profile-writer.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-profile-writer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAC5F,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAEtE,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,qCAAqC,CAAC;AAe7C,qBAAa,oBAAqB,YAAW,aAAa;IAKtD,OAAO,CAAC,QAAQ,CAAC,IAAI;IAJvB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyB;IAClD,gGAAgG;IAChG,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;gBAEV,IAAI,EAAE,IAAI;IAC3B,yFAAyF;IACzF,OAAO,CAAC,EAAE,oBAAoB;IAO1B,MAAM,CACV,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GACzC,OAAO,CAAC,kBAAkB,CAAC;CAgD/B"}
1
+ {"version":3,"file":"mariadb-profile-writer.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-profile-writer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAC5F,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAEtE,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,qCAAqC,CAAC;AAe7C,qBAAa,oBAAqB,YAAW,aAAa;IAKtD,OAAO,CAAC,QAAQ,CAAC,IAAI;IAJvB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyB;IAClD,gGAAgG;IAChG,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;gBAEV,IAAI,EAAE,IAAI;IAC3B,yFAAyF;IACzF,OAAO,CAAC,EAAE,oBAAoB;IAO1B,MAAM,CACV,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GACzC,OAAO,CAAC,kBAAkB,CAAC;CAqD/B"}
@@ -32,6 +32,11 @@ class MariaDbProfileWriter {
32
32
  if (changes.email !== undefined) {
33
33
  throw new Error('MariaDbProfileWriter: email changes go through the verified email-change flow only');
34
34
  }
35
+ // Auth capabilities R1.7 / D-8: the login name (`u_login_name`) is set by an admin or
36
+ // `bootstrap:org` only; a self-service writer never touches it. Refuse a caller that tries.
37
+ if (changes.loginName !== undefined) {
38
+ throw new Error('MariaDbProfileWriter: the login name is set by an administrator only');
39
+ }
35
40
  const sets = [];
36
41
  const params = [];
37
42
  if (changes.name !== undefined) {
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The `signInWith` option (auth capabilities R1.8): which typed forms `POST /auth/login` accepts.
3
+ * `['email']` (default) is today's behaviour; `['email', 'username']` also accepts the login name
4
+ * (`users.u_login_name`, admin- or bootstrap-set). Validated once at registration: an unknown
5
+ * value, an empty list or a list without `email` throws (email sign-in cannot be turned off here;
6
+ * password reset and invites depend on it).
7
+ */
8
+ /** A sign-in form `POST /auth/login` may accept. */
9
+ export type SignInMethod = 'email' | 'username';
10
+ /** The default: email only (today's behaviour). */
11
+ export declare const DEFAULT_SIGN_IN_WITH: readonly SignInMethod[];
12
+ /** Validate the option; `undefined` ⇒ {@link DEFAULT_SIGN_IN_WITH}. Throws a clear boot error. */
13
+ export declare function resolveSignInWith(value: unknown): readonly SignInMethod[];
14
+ //# sourceMappingURL=sign-in-with.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sign-in-with.d.ts","sourceRoot":"","sources":["../../src/authentication/sign-in-with.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,oDAAoD;AACpD,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,UAAU,CAAC;AAIhD,mDAAmD;AACnD,eAAO,MAAM,oBAAoB,EAAE,SAAS,YAAY,EAA6B,CAAC;AAEtF,kGAAkG;AAClG,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,YAAY,EAAE,CAsBzE"}
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ /**
3
+ * The `signInWith` option (auth capabilities R1.8): which typed forms `POST /auth/login` accepts.
4
+ * `['email']` (default) is today's behaviour; `['email', 'username']` also accepts the login name
5
+ * (`users.u_login_name`, admin- or bootstrap-set). Validated once at registration: an unknown
6
+ * value, an empty list or a list without `email` throws (email sign-in cannot be turned off here;
7
+ * password reset and invites depend on it).
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.DEFAULT_SIGN_IN_WITH = void 0;
11
+ exports.resolveSignInWith = resolveSignInWith;
12
+ const METHODS = ['email', 'username'];
13
+ /** The default: email only (today's behaviour). */
14
+ exports.DEFAULT_SIGN_IN_WITH = Object.freeze(['email']);
15
+ /** Validate the option; `undefined` ⇒ {@link DEFAULT_SIGN_IN_WITH}. Throws a clear boot error. */
16
+ function resolveSignInWith(value) {
17
+ if (value === undefined)
18
+ return exports.DEFAULT_SIGN_IN_WITH;
19
+ if (!Array.isArray(value) || value.length === 0) {
20
+ throw new Error("ShieldModule: signInWith must be a non-empty array of 'email' | 'username' " +
21
+ "(default ['email'])");
22
+ }
23
+ const out = [];
24
+ for (const v of value) {
25
+ if (typeof v !== 'string' || !METHODS.includes(v)) {
26
+ throw new Error(`ShieldModule: signInWith has an unknown value ${JSON.stringify(v)}; ` +
27
+ "allowed: 'email', 'username'");
28
+ }
29
+ if (!out.includes(v))
30
+ out.push(v);
31
+ }
32
+ if (!out.includes('email')) {
33
+ throw new Error("ShieldModule: signInWith must include 'email' (e.g. ['email', 'username'])");
34
+ }
35
+ return Object.freeze(out);
36
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The engine access declaration → core's record posture. Core knows `owned`, `tenantScoped` and
3
+ * `system`; a global reference type (G-022, ADR-0046) is presented as `system` — grant-governed,
4
+ * owner baseline allow, list owner baseline `all` — so core's frozen contract is unchanged (R9).
5
+ * The guard emits no tenant clause for it (`list-scope-sql.ts`) and loads its record rules for
6
+ * the caller's active tenant only (`wire-record-authorization.ts`).
7
+ */
8
+ import type { RecordAccessPosture } from '@astram/core';
9
+ import type { AccessDeclaration } from '@astram/engine';
10
+ export declare function coreAccessOf(access: AccessDeclaration | undefined): RecordAccessPosture;
11
+ //# sourceMappingURL=core-access.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core-access.d.ts","sourceRoot":"","sources":["../../src/authz/core-access.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAExD,wBAAgB,YAAY,CAAC,MAAM,EAAE,iBAAiB,GAAG,SAAS,GAAG,mBAAmB,CAQvF"}
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.coreAccessOf = coreAccessOf;
4
+ function coreAccessOf(access) {
5
+ if (access === undefined)
6
+ return {};
7
+ if (access.global === true)
8
+ return { system: true };
9
+ return {
10
+ ...(access.owned !== undefined ? { owned: access.owned } : {}),
11
+ ...(access.tenantScoped !== undefined ? { tenantScoped: access.tenantScoped } : {}),
12
+ ...(access.system !== undefined ? { system: access.system } : {}),
13
+ };
14
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"list-scope-sql.d.ts","sourceRoot":"","sources":["../../src/authz/list-scope-sql.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4EG;AAEH,OAAO,KAAK,EACV,aAAa,EACb,cAAc,EAIf,MAAM,cAAc,CAAC;AACtB,OAAO,EAKL,KAAK,UAAU,EAChB,MAAM,gBAAgB,CAAC;AAGxB,qFAAqF;AACrF,MAAM,WAAW,mBAAmB;IAClC,kFAAkF;IAClF,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,gFAAgF;IAChF,QAAQ,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;CAC5B;AAED;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;CACpC;AAED,yFAAyF;AACzF,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;CAO3C;AAmID;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,aAAa,EACnB,GAAG,EAAE,UAAU,EACf,SAAS,EAAE,kBAAkB,EAC7B,IAAI,EAAE;IACJ;;;;;;OAMG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,MAAM,CAAC;IACrB;;;;;;;;OAQG;IACH,gBAAgB,CAAC,EAAE,SAAS,CAAC,SAAS,cAAc,EAAE,CAAC,EAAE,CAAC;CAC3D,GACA,mBAAmB,CAoJrB"}
1
+ {"version":3,"file":"list-scope-sql.d.ts","sourceRoot":"","sources":["../../src/authz/list-scope-sql.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4EG;AAEH,OAAO,KAAK,EACV,aAAa,EACb,cAAc,EAIf,MAAM,cAAc,CAAC;AACtB,OAAO,EAML,KAAK,UAAU,EAChB,MAAM,gBAAgB,CAAC;AAGxB,qFAAqF;AACrF,MAAM,WAAW,mBAAmB;IAClC,kFAAkF;IAClF,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,gFAAgF;IAChF,QAAQ,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;CAC5B;AAED;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;CACpC;AAED,yFAAyF;AACzF,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;CAO3C;AAmID;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,aAAa,EACnB,GAAG,EAAE,UAAU,EACf,SAAS,EAAE,kBAAkB,EAC7B,IAAI,EAAE;IACJ;;;;;;OAMG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,MAAM,CAAC;IACrB;;;;;;;;OAQG;IACH,gBAAgB,CAAC,EAAE,SAAS,CAAC,SAAS,cAAc,EAAE,CAAC,EAAE,CAAC;CAC3D,GACA,mBAAmB,CAyJrB"}
@@ -252,8 +252,10 @@ function translateListScope(spec, reg, principal, opts) {
252
252
  // readScope, a subtree bound on a tenant-column-less type is a misconfiguration →
253
253
  // fail closed. tenantNodeIds stays null → the share term (which needs a tenant value)
254
254
  // is naturally omitted (a readScope type is not record-shared in v1).
255
- if (reg.readScope) {
256
- // defer to readScope — no tenant clause added; fall through to the union/deny.
255
+ // A global type (G-022, ADR-0046) has no wall at all: the same rows for every tenant,
256
+ // still governed by the positive union and deny below.
257
+ if (reg.readScope || (0, engine_1.isGlobalType)(reg)) {
258
+ // defer to readScope / global — no tenant clause added; fall through to the union/deny.
257
259
  }
258
260
  else {
259
261
  return { sql: 'FALSE', params: [] };
@@ -279,10 +281,15 @@ function translateListScope(spec, reg, principal, opts) {
279
281
  if (tenantCol === null) {
280
282
  // A tenant-scoped bound on a type with no tenant column is a misconfiguration →
281
283
  // fail closed (the whole fragment is FALSE — no rows). Documented (Decision).
282
- return { sql: 'FALSE', params: [] };
284
+ // G-022: a global type is the exception — no tenant clause, and tenantNodeIds stays
285
+ // null so the share term is omitted (a global type is never shared).
286
+ if (!(0, engine_1.isGlobalType)(reg))
287
+ return { sql: 'FALSE', params: [] };
288
+ }
289
+ else {
290
+ tenantClause = `\`${tenantCol}\` = ${p.bind(tenantValue)}`;
291
+ tenantNodeIds = [tenantValue]; // the flat case: the share is scoped to the one tenant
283
292
  }
284
- tenantClause = `\`${tenantCol}\` = ${p.bind(tenantValue)}`;
285
- tenantNodeIds = [tenantValue]; // the flat case: the share is scoped to the one tenant
286
293
  }
287
294
  if (tenantClause !== null)
288
295
  clauses.push(tenantClause);
@@ -423,6 +430,9 @@ function translateCrossTenant(spec, reg, schema, nowMs, p) {
423
430
  const xt = spec.crossTenant;
424
431
  if (xt.kind !== 'granted')
425
432
  return null;
433
+ // G-022 R6: a global type is omitted from cross-tenant reach (its rows belong to no tenant).
434
+ if ((0, engine_1.isGlobalType)(reg))
435
+ return null;
426
436
  const tenantCol = (0, engine_1.tenantColumnOf)(reg);
427
437
  const hasUuidPk = reg.primaryKey.kind === 'uuid';
428
438
  if (tenantCol === null && !hasUuidPk)
@@ -1 +1 @@
1
- {"version":3,"file":"wire-record-authorization.d.ts","sourceRoot":"","sources":["../../src/authz/wire-record-authorization.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,KAAK,EACV,iBAAiB,EACjB,cAAc,EAEd,mBAAmB,EACnB,SAAS,EAET,kBAAkB,EACnB,MAAM,cAAc,CAAC;AAMtB,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAC7E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAEL,KAAK,aAAa,EACnB,MAAM,kCAAkC,CAAC;AAO1C,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAEL,KAAK,sBAAsB,EAC5B,MAAM,2CAA2C,CAAC;AAEnD;;;;;;GAMG;AACH,MAAM,WAAW,yBAAyB;IACxC;;;;;;OAMG;IACH,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,uFAAuF;IACvF,QAAQ,CAAC,eAAe,EAAE,mBAAmB,CAAC;IAC9C,6FAA6F;IAC7F,QAAQ,CAAC,YAAY,EAAE,kBAAkB,CAAC;IAC1C,6GAA6G;IAC7G,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC;;;;;OAKG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,SAAS,sBAAsB,EAAE,CAAC;IAC/D;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,oBAAoB,CAAC;CACzC;AAOD;;;;;GAKG;AACH;;;;;;;;;;;;GAYG;AACH,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,mBAAmB,EACzB,KAAK,EAAE,uBAAuB,GAC7B,mBAAmB,CA2BrB;AAED,6EAA6E;AAC7E,MAAM,WAAW,+BAA+B;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,uBAAuB,CAAC;CACjD;AAED,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,yBAAyB,EACjC,OAAO,GAAE,+BAAoC,GAC5C,iBAAiB,CA8BnB;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,WAAW,GAAG,CACxB,GAAG,EAAE,UAAU,EACf,KAAK,EAAE,SAAS,EAChB,MAAM,EAAE,MAAM,KACX,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,EAAE,CAAA;CAAE,CAAC,CAAC;AAEjD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,yBAAyB,GAAG,WAAW,CAyE/E"}
1
+ {"version":3,"file":"wire-record-authorization.d.ts","sourceRoot":"","sources":["../../src/authz/wire-record-authorization.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,KAAK,EACV,iBAAiB,EAEjB,cAAc,EAEd,mBAAmB,EACnB,SAAS,EAET,kBAAkB,EACnB,MAAM,cAAc,CAAC;AAMtB,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAC7E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAGjD,OAAO,EAEL,KAAK,aAAa,EACnB,MAAM,kCAAkC,CAAC;AAO1C,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAEL,KAAK,sBAAsB,EAC5B,MAAM,2CAA2C,CAAC;AAEnD;;;;;;GAMG;AACH,MAAM,WAAW,yBAAyB;IACxC;;;;;;OAMG;IACH,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,uFAAuF;IACvF,QAAQ,CAAC,eAAe,EAAE,mBAAmB,CAAC;IAC9C,6FAA6F;IAC7F,QAAQ,CAAC,YAAY,EAAE,kBAAkB,CAAC;IAC1C,6GAA6G;IAC7G,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC;;;;;OAKG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,SAAS,sBAAsB,EAAE,CAAC;IAC/D;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,oBAAoB,CAAC;CACzC;AAOD;;;;;GAKG;AACH;;;;;;;;;;;;GAYG;AACH,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,mBAAmB,EACzB,KAAK,EAAE,uBAAuB,GAC7B,mBAAmB,CA2BrB;AAED,6EAA6E;AAC7E,MAAM,WAAW,+BAA+B;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,uBAAuB,CAAC;CACjD;AAED,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,yBAAyB,EACjC,OAAO,GAAE,+BAAoC,GAC5C,iBAAiB,CA8BnB;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,WAAW,GAAG,CACxB,GAAG,EAAE,UAAU,EACf,KAAK,EAAE,SAAS,EAChB,MAAM,EAAE,MAAM,KACX,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,EAAE,CAAA;CAAE,CAAC,CAAC;AAEjD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,yBAAyB,GAAG,WAAW,CA2F/E"}
@@ -4,6 +4,8 @@ exports.withRedisPermissionVersion = withRedisPermissionVersion;
4
4
  exports.buildRecordAuthorization = buildRecordAuthorization;
5
5
  exports.buildListScopeFn = buildListScopeFn;
6
6
  const core_1 = require("@astram/core");
7
+ const engine_1 = require("@astram/engine");
8
+ const core_access_js_1 = require("./core-access.js");
7
9
  const mariadb_record_rule_adapter_js_1 = require("./mariadb-record-rule-adapter.js");
8
10
  const mariadb_record_share_adapter_js_1 = require("./mariadb-record-share-adapter.js");
9
11
  const mariadb_cross_tenant_grant_adapter_js_1 = require("./mariadb-cross-tenant-grant-adapter.js");
@@ -128,15 +130,30 @@ function buildListScopeFn(inputs) {
128
130
  // `computeListScope` reads ONLY `descriptor.access` for the owner-baseline posture
129
131
  // (see list-scope.ts `ownerBaselineFor`), so `label`/`fields` are inert — the engine's
130
132
  // `reg.access` (`AccessDeclaration`) is structurally core's `RecordAccessPosture`.
133
+ // A global type (G-022) is presented as core `system` by `coreAccessOf`, as the bridge does.
131
134
  const registry = {
132
135
  resolveObjectType(typeKey) {
133
136
  return typeKey === reg.typeKey
134
- ? { typeKey: reg.typeKey, label: reg.typeKey, fields: [], access: reg.access }
137
+ ? {
138
+ typeKey: reg.typeKey,
139
+ label: reg.typeKey,
140
+ fields: [],
141
+ access: (0, core_access_js_1.coreAccessOf)(reg.access),
142
+ }
135
143
  : undefined;
136
144
  },
137
145
  };
146
+ // D-5 (G-022): a global row has no tenant, so the single-row check loads record rules for
147
+ // the caller's active tenant (`resource.tenantId ?? principal.tenantId`, record-rule
148
+ // adapter). The list must load the same set, not the whole subtree: otherwise a child
149
+ // tenant's deny would hide a global row from a parent's caller in lists only.
150
+ const scopePort = (0, engine_1.isGlobalType)(reg)
151
+ ? {
152
+ listPolicyConditions: (typeKey, _subtree, a, subject) => listScopePort.listPolicyConditions(typeKey, [actor.tenantId], a, subject),
153
+ }
154
+ : listScopePort;
138
155
  // A transient throw here PROPAGATES (fail-closed — NOT caught).
139
- const spec = await (0, core_1.computeListScope)({ registry, listScope: listScopePort }, { principal: actor, action, typeKey: reg.typeKey, tenantId: actor.tenantId });
156
+ const spec = await (0, core_1.computeListScope)({ registry, listScope: scopePort }, { principal: actor, action, typeKey: reg.typeKey, tenantId: actor.tenantId });
140
157
  // ── OWNING-TENANT DENY (Task 4.2 — the fail-open leak fix) ────────────────────────────
141
158
  // A cross-tenant grant reaches the OWNING tenant's records; `decide()` evaluates those at
142
159
  // the RESOURCE's (owning) tenant, so the owning tenant's DENY rules apply (deny-wins,
@@ -148,7 +165,8 @@ function buildListScopeFn(inputs) {
148
165
  // PROPAGATES (fail-closed — never drop the owning-tenant deny). Global (NULL-tenant) deny
149
166
  // rules come back in BOTH fetches, so they apply to both branches (as `decide()` does).
150
167
  let owningTenantDeny = [];
151
- if (spec.crossTenant.kind === 'granted') {
168
+ // A global type has no cross-tenant branch (`translateCrossTenant` omits it, G-022 R6).
169
+ if (spec.crossTenant.kind === 'granted' && !(0, engine_1.isGlobalType)(reg)) {
152
170
  const owningTenants = await crossTenantAdapter.listActiveOwningTenants({
153
171
  grantee: actor.id,
154
172
  roleIds: actor.roleIds,
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `AuthModule` providers for the `captcha` option (auth capabilities R7.1, D-6):
3
+ * - absent ⇒ `SHIELD_CAPTCHA_CONFIG` and `SHIELD_CAPTCHA_GATE` bound `null` (every route as today,
4
+ * no `CAPTCHA_*` variable read);
5
+ * - `'env'` ⇒ the config from `CAPTCHA_*` (`loadCaptchaConfig`, boot fails on a bad value), the
6
+ * provider's verifier, and the gate over the guard's Redis client and the auth audit sink. One
7
+ * boot line names the provider, mode, fail mode and endpoints (never the keys).
8
+ */
9
+ import { type Provider } from '@nestjs/common';
10
+ import type { CaptchaVerifier } from '@astram/core';
11
+ import { type CaptchaConfig } from '../config/captcha-config.js';
12
+ /** The `captcha` option on `ShieldModule.forRoot` / `forRootAsync` / `AuthModule.forRoot`. */
13
+ export type ShieldCaptchaOption = 'env';
14
+ /** Build the adapter named by `CAPTCHA_PROVIDER`. */
15
+ export declare function buildCaptchaVerifier(config: CaptchaConfig): CaptchaVerifier;
16
+ /** Providers for one `AuthModule` registration (see the file header). */
17
+ export declare function captchaProviders(option: ShieldCaptchaOption | undefined): Provider[];
18
+ //# sourceMappingURL=captcha.providers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"captcha.providers.d.ts","sourceRoot":"","sources":["../../src/captcha/captcha.providers.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAU,KAAK,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAEvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpD,OAAO,EAAqB,KAAK,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAcpF,8FAA8F;AAC9F,MAAM,MAAM,mBAAmB,GAAG,KAAK,CAAC;AAExC,qDAAqD;AACrD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,aAAa,GAAG,eAAe,CAe3E;AAED,yEAAyE;AACzE,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,mBAAmB,GAAG,SAAS,GAAG,QAAQ,EAAE,CAgDpF"}