@astram/guard 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/auth.module.d.ts +8 -1
- package/dist/auth/auth.module.d.ts.map +1 -1
- package/dist/auth/auth.module.js +76 -31
- package/dist/authentication/mariadb-api-key-store.d.ts +10 -1
- package/dist/authentication/mariadb-api-key-store.d.ts.map +1 -1
- package/dist/authentication/mariadb-api-key-store.js +20 -10
- package/dist/authentication/mariadb-credential-resolver.d.ts +8 -1
- package/dist/authentication/mariadb-credential-resolver.d.ts.map +1 -1
- package/dist/authentication/mariadb-credential-resolver.js +15 -5
- package/dist/authentication/mariadb-credential-writer.d.ts +6 -1
- package/dist/authentication/mariadb-credential-writer.d.ts.map +1 -1
- package/dist/authentication/mariadb-credential-writer.js +9 -3
- package/dist/authentication/mariadb-email-address-changer.d.ts +10 -1
- package/dist/authentication/mariadb-email-address-changer.d.ts.map +1 -1
- package/dist/authentication/mariadb-email-address-changer.js +22 -9
- package/dist/authentication/mariadb-email-change-token-store.d.ts +8 -1
- package/dist/authentication/mariadb-email-change-token-store.d.ts.map +1 -1
- package/dist/authentication/mariadb-email-change-token-store.js +18 -9
- package/dist/authentication/mariadb-mfa-device-store.d.ts +6 -1
- package/dist/authentication/mariadb-mfa-device-store.d.ts.map +1 -1
- package/dist/authentication/mariadb-mfa-device-store.js +22 -13
- package/dist/authentication/mariadb-profile-resolver.d.ts +6 -1
- package/dist/authentication/mariadb-profile-resolver.d.ts.map +1 -1
- package/dist/authentication/mariadb-profile-resolver.js +12 -6
- package/dist/authentication/mariadb-profile-writer.d.ts +6 -1
- package/dist/authentication/mariadb-profile-writer.d.ts.map +1 -1
- package/dist/authentication/mariadb-profile-writer.js +11 -4
- package/dist/authentication/mariadb-recovery-code-store.d.ts +6 -1
- package/dist/authentication/mariadb-recovery-code-store.d.ts.map +1 -1
- package/dist/authentication/mariadb-recovery-code-store.js +11 -5
- package/dist/authentication/mariadb-reset-token-store.d.ts +6 -1
- package/dist/authentication/mariadb-reset-token-store.d.ts.map +1 -1
- package/dist/authentication/mariadb-reset-token-store.js +11 -5
- package/dist/authentication/mariadb-session-sweeper.d.ts +6 -1
- package/dist/authentication/mariadb-session-sweeper.d.ts.map +1 -1
- package/dist/authentication/mariadb-session-sweeper.js +11 -5
- package/dist/authz/cross-tenant-grant-writer.d.ts.map +1 -1
- package/dist/authz/cross-tenant-grant-writer.js +4 -3
- package/dist/authz/list-scope-sql.d.ts +10 -2
- package/dist/authz/list-scope-sql.d.ts.map +1 -1
- package/dist/authz/list-scope-sql.js +4 -4
- package/dist/authz/mariadb-cross-tenant-grant-adapter.d.ts.map +1 -1
- package/dist/authz/mariadb-cross-tenant-grant-adapter.js +4 -3
- package/dist/authz/mariadb-list-scope-adapter.d.ts.map +1 -1
- package/dist/authz/mariadb-list-scope-adapter.js +4 -3
- package/dist/authz/mariadb-policy-store.d.ts.map +1 -1
- package/dist/authz/mariadb-policy-store.js +4 -3
- package/dist/authz/mariadb-record-rule-adapter.d.ts.map +1 -1
- package/dist/authz/mariadb-record-rule-adapter.js +4 -3
- package/dist/authz/mariadb-record-share-adapter.d.ts.map +1 -1
- package/dist/authz/mariadb-record-share-adapter.js +4 -3
- package/dist/authz/record-share-cascade.d.ts.map +1 -1
- package/dist/authz/record-share-cascade.js +4 -3
- package/dist/authz/record-share-writer.d.ts.map +1 -1
- package/dist/authz/record-share-writer.js +4 -3
- package/dist/authz/wire-record-authorization.d.ts +8 -0
- package/dist/authz/wire-record-authorization.d.ts.map +1 -1
- package/dist/authz/wire-record-authorization.js +14 -6
- package/dist/boot/boot-sequence.d.ts +10 -0
- package/dist/boot/boot-sequence.d.ts.map +1 -1
- package/dist/boot/boot-sequence.js +47 -3
- package/dist/boot/migration-ledger-check.d.ts +94 -0
- package/dist/boot/migration-ledger-check.d.ts.map +1 -0
- package/dist/boot/migration-ledger-check.js +280 -0
- package/dist/boot/single-layout-probe.d.ts +66 -0
- package/dist/boot/single-layout-probe.d.ts.map +1 -0
- package/dist/boot/single-layout-probe.js +334 -0
- package/dist/config/app-config.d.ts +46 -3
- package/dist/config/app-config.d.ts.map +1 -1
- package/dist/config/app-config.js +90 -11
- package/dist/config/config.module.d.ts +7 -0
- package/dist/config/config.module.d.ts.map +1 -1
- package/dist/config/config.module.js +8 -1
- package/dist/config/framework-schema-names.d.ts +29 -0
- package/dist/config/framework-schema-names.d.ts.map +1 -0
- package/dist/config/framework-schema-names.js +41 -0
- package/dist/identity/permission-facts.adapter.d.ts +7 -1
- package/dist/identity/permission-facts.adapter.d.ts.map +1 -1
- package/dist/identity/permission-facts.adapter.js +15 -6
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -2
- package/dist/module/shield.module.d.ts.map +1 -1
- package/dist/module/shield.module.js +43 -7
- package/dist/session/mariadb-session-record-store.d.ts +8 -1
- package/dist/session/mariadb-session-record-store.d.ts.map +1 -1
- package/dist/session/mariadb-session-record-store.js +23 -13
- package/dist/session/session-restriction-store.d.ts +8 -1
- package/dist/session/session-restriction-store.d.ts.map +1 -1
- package/dist/session/session-restriction-store.js +15 -5
- package/migrations/1710000000006-CreateAuthzSchema.cjs +15 -4
- package/migrations/1710000000007-ExtendPermissionAndRolePermission.cjs +11 -3
- package/migrations/1710000000008-CreateAccessBundleTables.cjs +14 -4
- package/migrations/1710000000009-CreateRecordPermissionRule.cjs +15 -4
- package/migrations/1710000000014-CreateRecordShare.cjs +15 -4
- package/migrations/1710000000015-CreateScopeClosure.cjs +14 -4
- package/migrations/1710000000016-AlterCompanyParentFkRestrict.cjs +20 -9
- package/migrations/1710000000017-CreateServiceAccountKey.cjs +14 -4
- package/package.json +3 -3
|
@@ -14,10 +14,17 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import type { Pool } from 'mariadb';
|
|
16
16
|
import type { EmailChangeTokenBinding, EmailChangeTokenIssue, EmailChangeTokenStore } from '../auth/ports/email-change-token-store.js';
|
|
17
|
+
import { type FrameworkSchemaNames } from '../config/framework-schema-names.js';
|
|
17
18
|
export declare class MariaDbEmailChangeTokenStore implements EmailChangeTokenStore {
|
|
18
19
|
private readonly pool;
|
|
19
20
|
private readonly now;
|
|
20
|
-
|
|
21
|
+
/** `<identity>`.`email_change_token`. */
|
|
22
|
+
private readonly table;
|
|
23
|
+
/** `<app>`.`users`. */
|
|
24
|
+
private readonly usersTable;
|
|
25
|
+
constructor(pool: Pool, now?: () => number,
|
|
26
|
+
/** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
|
|
27
|
+
schemas?: FrameworkSchemaNames);
|
|
21
28
|
issue(token: EmailChangeTokenIssue): Promise<void>;
|
|
22
29
|
consume(tokenHash: string): Promise<EmailChangeTokenBinding | null>;
|
|
23
30
|
invalidateAllForPrincipal(principalId: string): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mariadb-email-change-token-store.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-email-change-token-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,IAAI,EAAkB,MAAM,SAAS,CAAC;AACpD,OAAO,KAAK,EACV,uBAAuB,EACvB,qBAAqB,EACrB,qBAAqB,EACtB,MAAM,2CAA2C,CAAC;
|
|
1
|
+
{"version":3,"file":"mariadb-email-change-token-store.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-email-change-token-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,IAAI,EAAkB,MAAM,SAAS,CAAC;AACpD,OAAO,KAAK,EACV,uBAAuB,EACvB,qBAAqB,EACrB,qBAAqB,EACtB,MAAM,2CAA2C,CAAC;AACnD,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,qCAAqC,CAAC;AAU7C,qBAAa,4BAA6B,YAAW,qBAAqB;IAOtE,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAPtB,yCAAyC;IACzC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,uBAAuB;IACvB,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;gBAGjB,IAAI,EAAE,IAAI,EACV,GAAG,GAAE,MAAM,MAAiB;IAC7C,yFAAyF;IACzF,OAAO,CAAC,EAAE,oBAAoB;IAO1B,KAAK,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC;IA2ClD,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,GAAG,IAAI,CAAC;IAwBnE,yBAAyB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAOpE"}
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MariaDbEmailChangeTokenStore = void 0;
|
|
4
|
-
const
|
|
4
|
+
const framework_schema_names_js_1 = require("../config/framework-schema-names.js");
|
|
5
5
|
class MariaDbEmailChangeTokenStore {
|
|
6
6
|
pool;
|
|
7
7
|
now;
|
|
8
|
-
|
|
8
|
+
/** `<identity>`.`email_change_token`. */
|
|
9
|
+
table;
|
|
10
|
+
/** `<app>`.`users`. */
|
|
11
|
+
usersTable;
|
|
12
|
+
constructor(pool, now = Date.now,
|
|
13
|
+
/** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
|
|
14
|
+
schemas) {
|
|
9
15
|
this.pool = pool;
|
|
10
16
|
this.now = now;
|
|
17
|
+
const s = (0, framework_schema_names_js_1.schemaNamesAtConstruction)(schemas);
|
|
18
|
+
this.table = (0, framework_schema_names_js_1.qualifiedTableName)(s.identity, 'email_change_token');
|
|
19
|
+
this.usersTable = (0, framework_schema_names_js_1.qualifiedTableName)(s.app, 'users');
|
|
11
20
|
}
|
|
12
21
|
async issue(token) {
|
|
13
22
|
const now = this.now();
|
|
@@ -15,13 +24,13 @@ class MariaDbEmailChangeTokenStore {
|
|
|
15
24
|
try {
|
|
16
25
|
await conn.beginTransaction();
|
|
17
26
|
try {
|
|
18
|
-
const user = (await conn.query(
|
|
27
|
+
const user = (await conn.query(`SELECT u_uuid FROM ${this.usersTable} WHERE u_uuid = ? FOR UPDATE`, [token.principalId]));
|
|
19
28
|
if (user.length === 0)
|
|
20
29
|
throw new Error('email change: unknown user');
|
|
21
|
-
await conn.query(`DELETE FROM ${
|
|
22
|
-
await conn.query(`UPDATE ${
|
|
30
|
+
await conn.query(`DELETE FROM ${this.table} WHERE ect_expires_at_ms <= ?`, [now]);
|
|
31
|
+
await conn.query(`UPDATE ${this.table} SET ect_consumed_at_ms = ?
|
|
23
32
|
WHERE ect_principal_id = ? AND ect_consumed_at_ms IS NULL`, [now, token.principalId]);
|
|
24
|
-
await conn.query(`INSERT INTO ${
|
|
33
|
+
await conn.query(`INSERT INTO ${this.table}
|
|
25
34
|
(ect_token_hash, ect_principal_id, ect_new_email, ect_old_email, ect_session_id,
|
|
26
35
|
ect_tenant_id, ect_expires_at_ms, ect_consumed_at_ms, ect_created_at_ms)
|
|
27
36
|
VALUES (?, ?, ?, ?, ?, ?, ?, NULL, ?)`, [
|
|
@@ -47,12 +56,12 @@ class MariaDbEmailChangeTokenStore {
|
|
|
47
56
|
}
|
|
48
57
|
async consume(tokenHash) {
|
|
49
58
|
const now = this.now();
|
|
50
|
-
const res = (await this.pool.query(`UPDATE ${
|
|
59
|
+
const res = (await this.pool.query(`UPDATE ${this.table} SET ect_consumed_at_ms = ?
|
|
51
60
|
WHERE ect_token_hash = ? AND ect_consumed_at_ms IS NULL AND ect_expires_at_ms > ?`, [now, tokenHash, now]));
|
|
52
61
|
if (res.affectedRows !== 1)
|
|
53
62
|
return null;
|
|
54
63
|
const rows = (await this.pool.query(`SELECT ect_principal_id, ect_new_email, ect_old_email, ect_session_id, ect_tenant_id
|
|
55
|
-
FROM ${
|
|
64
|
+
FROM ${this.table} WHERE ect_token_hash = ? LIMIT 1`, [tokenHash]));
|
|
56
65
|
const row = rows[0];
|
|
57
66
|
if (row === undefined)
|
|
58
67
|
return null; // the user was deleted in between (FK cascade)
|
|
@@ -65,7 +74,7 @@ class MariaDbEmailChangeTokenStore {
|
|
|
65
74
|
};
|
|
66
75
|
}
|
|
67
76
|
async invalidateAllForPrincipal(principalId) {
|
|
68
|
-
await this.pool.query(`UPDATE ${
|
|
77
|
+
await this.pool.query(`UPDATE ${this.table} SET ect_consumed_at_ms = ?
|
|
69
78
|
WHERE ect_principal_id = ? AND ect_consumed_at_ms IS NULL`, [this.now(), principalId]);
|
|
70
79
|
}
|
|
71
80
|
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import type { Pool } from 'mariadb';
|
|
2
2
|
import type { MfaDevice, MfaDeviceStore } from '../auth/ports/mfa-device-store.js';
|
|
3
|
+
import { type FrameworkSchemaNames } from '../config/framework-schema-names.js';
|
|
3
4
|
export declare class MariaDbMfaDeviceStore implements MfaDeviceStore {
|
|
4
5
|
private readonly pool;
|
|
5
|
-
|
|
6
|
+
/** `<identity>`.`mfa_device`, resolved at construction. */
|
|
7
|
+
private readonly table;
|
|
8
|
+
constructor(pool: Pool,
|
|
9
|
+
/** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
|
|
10
|
+
schemas?: FrameworkSchemaNames);
|
|
6
11
|
listAll(principalId: string): Promise<MfaDevice[]>;
|
|
7
12
|
listConfirmedSecrets(principalId: string): Promise<string[]>;
|
|
8
13
|
getSecret(principalId: string, deviceId: string): Promise<string | null>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mariadb-mfa-device-store.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-mfa-device-store.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;
|
|
1
|
+
{"version":3,"file":"mariadb-mfa-device-store.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-mfa-device-store.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AACnF,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,qCAAqC,CAAC;AAuB7C,qBAAa,qBAAsB,YAAW,cAAc;IAKxD,OAAO,CAAC,QAAQ,CAAC,IAAI;IAJvB,2DAA2D;IAC3D,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;gBAGZ,IAAI,EAAE,IAAI;IAC3B,yFAAyF;IACzF,OAAO,CAAC,EAAE,oBAAoB;IAK1B,OAAO,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IASlD,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAS5D,SAAS,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IASxE,KAAK,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAiBzE,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAkBhE,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAetF,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAQ/D,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IASpD,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAU/C"}
|
|
@@ -24,6 +24,7 @@ exports.MariaDbMfaDeviceStore = void 0;
|
|
|
24
24
|
* other method's genuine store outage propagates (fails loud), never swallowed.
|
|
25
25
|
*/
|
|
26
26
|
const node_crypto_1 = require("node:crypto");
|
|
27
|
+
const framework_schema_names_js_1 = require("../config/framework-schema-names.js");
|
|
27
28
|
/** Pending-device TTL — a `begin` never confirmed within this window is self-pruned. */
|
|
28
29
|
const PENDING_TTL_MS = 15 * 60 * 1000;
|
|
29
30
|
/** Map a DB row → the display shape (NO secret). BIGINT columns arrive as bigint → Number. */
|
|
@@ -36,67 +37,75 @@ const toDevice = (r) => ({
|
|
|
36
37
|
});
|
|
37
38
|
class MariaDbMfaDeviceStore {
|
|
38
39
|
pool;
|
|
39
|
-
|
|
40
|
+
/** `<identity>`.`mfa_device`, resolved at construction. */
|
|
41
|
+
table;
|
|
42
|
+
constructor(pool,
|
|
43
|
+
/** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
|
|
44
|
+
schemas) {
|
|
40
45
|
this.pool = pool;
|
|
46
|
+
this.table = (0, framework_schema_names_js_1.qualifiedTableName)((0, framework_schema_names_js_1.schemaNamesAtConstruction)(schemas).identity, 'mfa_device');
|
|
41
47
|
}
|
|
42
48
|
async listAll(principalId) {
|
|
43
49
|
const rows = (await this.pool.query(`SELECT md_id, md_name, md_secret, md_created_at_ms, md_last_used_at_ms, md_confirmed_at_ms
|
|
44
|
-
FROM
|
|
50
|
+
FROM ${this.table} WHERE md_principal_id = ? ORDER BY md_created_at_ms ASC`, [principalId]));
|
|
45
51
|
return rows.map(toDevice);
|
|
46
52
|
}
|
|
47
53
|
async listConfirmedSecrets(principalId) {
|
|
48
|
-
const rows = (await this.pool.query(`SELECT md_secret FROM
|
|
54
|
+
const rows = (await this.pool.query(`SELECT md_secret FROM ${this.table}
|
|
49
55
|
WHERE md_principal_id = ? AND md_confirmed_at_ms IS NOT NULL`, [principalId]));
|
|
50
56
|
return rows.map((r) => r.md_secret);
|
|
51
57
|
}
|
|
52
58
|
async getSecret(principalId, deviceId) {
|
|
53
|
-
const rows = (await this.pool.query(`SELECT md_secret FROM
|
|
59
|
+
const rows = (await this.pool.query(`SELECT md_secret FROM ${this.table}
|
|
54
60
|
WHERE md_id = ? AND md_principal_id = ? LIMIT 1`, [deviceId, principalId]));
|
|
55
61
|
return rows[0]?.md_secret ?? null;
|
|
56
62
|
}
|
|
57
63
|
async begin(principalId, name, secret) {
|
|
58
64
|
// Prune the caller's stale pending rows first (self-pruning; no cron).
|
|
59
|
-
await this.pool.query(`DELETE FROM
|
|
65
|
+
await this.pool.query(`DELETE FROM ${this.table}
|
|
60
66
|
WHERE md_principal_id = ? AND md_confirmed_at_ms IS NULL AND md_created_at_ms < ?`, [principalId, Date.now() - PENDING_TTL_MS]);
|
|
61
67
|
const id = (0, node_crypto_1.randomUUID)();
|
|
62
|
-
await this.pool.query(`INSERT INTO
|
|
68
|
+
await this.pool.query(`INSERT INTO ${this.table}
|
|
63
69
|
(md_id, md_principal_id, md_name, md_secret, md_created_at_ms, md_last_used_at_ms, md_confirmed_at_ms)
|
|
64
70
|
VALUES (?, ?, ?, ?, ?, NULL, NULL)`, [id, principalId, name, secret, Date.now()]);
|
|
65
71
|
return id;
|
|
66
72
|
}
|
|
67
73
|
async confirm(principalId, deviceId) {
|
|
68
|
-
const res = (await this.pool.query(`UPDATE
|
|
74
|
+
const res = (await this.pool.query(`UPDATE ${this.table} SET md_confirmed_at_ms = ?
|
|
69
75
|
WHERE md_id = ? AND md_principal_id = ? AND md_confirmed_at_ms IS NULL`, [Date.now(), deviceId, principalId]));
|
|
70
76
|
if (res.affectedRows !== 1)
|
|
71
77
|
return false;
|
|
72
78
|
// KI #14 re-review: once a device is confirmed MFA is on, so every later begin needs a code.
|
|
73
79
|
// Drop the principal's OTHER pending devices: one begun by a stolen session while MFA was
|
|
74
80
|
// still off (between the user's own begin and confirm) must not stay confirmable.
|
|
75
|
-
await this.pool.query(`DELETE FROM
|
|
81
|
+
await this.pool.query(`DELETE FROM ${this.table}
|
|
76
82
|
WHERE md_principal_id = ? AND md_confirmed_at_ms IS NULL AND md_id <> ?`, [principalId, deviceId]);
|
|
77
83
|
return true;
|
|
78
84
|
}
|
|
79
85
|
async rename(principalId, deviceId, name) {
|
|
80
|
-
const res = (await this.pool.query(`UPDATE
|
|
86
|
+
const res = (await this.pool.query(`UPDATE ${this.table} SET md_name = ?
|
|
81
87
|
WHERE md_id = ? AND md_principal_id = ?`, [name, deviceId, principalId]));
|
|
82
88
|
if (res.affectedRows !== 1)
|
|
83
89
|
return null;
|
|
84
90
|
const rows = (await this.pool.query(`SELECT md_id, md_name, md_secret, md_created_at_ms, md_last_used_at_ms, md_confirmed_at_ms
|
|
85
|
-
FROM
|
|
91
|
+
FROM ${this.table} WHERE md_id = ? AND md_principal_id = ? LIMIT 1`, [deviceId, principalId]));
|
|
86
92
|
return rows[0] ? toDevice(rows[0]) : null;
|
|
87
93
|
}
|
|
88
94
|
async remove(principalId, deviceId) {
|
|
89
|
-
const res = (await this.pool.query(`DELETE FROM
|
|
95
|
+
const res = (await this.pool.query(`DELETE FROM ${this.table} WHERE md_id = ? AND md_principal_id = ?`, [deviceId, principalId]));
|
|
90
96
|
return res.affectedRows === 1;
|
|
91
97
|
}
|
|
92
98
|
async countConfirmed(principalId) {
|
|
93
|
-
const rows = (await this.pool.query(`SELECT COUNT(*) AS c FROM
|
|
99
|
+
const rows = (await this.pool.query(`SELECT COUNT(*) AS c FROM ${this.table}
|
|
94
100
|
WHERE md_principal_id = ? AND md_confirmed_at_ms IS NOT NULL`, [principalId]));
|
|
95
101
|
return Number(rows[0]?.c ?? 0);
|
|
96
102
|
}
|
|
97
103
|
async stampUsed(secret) {
|
|
98
104
|
try {
|
|
99
|
-
await this.pool.query(`UPDATE
|
|
105
|
+
await this.pool.query(`UPDATE ${this.table} SET md_last_used_at_ms = ? WHERE md_secret = ?`, [
|
|
106
|
+
Date.now(),
|
|
107
|
+
secret,
|
|
108
|
+
]);
|
|
100
109
|
}
|
|
101
110
|
catch {
|
|
102
111
|
// best-effort; a stamp failure must never fail a login.
|
|
@@ -14,10 +14,15 @@
|
|
|
14
14
|
import type { Pool } from 'mariadb';
|
|
15
15
|
import type { Principal } from '@astram/core';
|
|
16
16
|
import type { CurrentUserProfile, ProfileResolver } from '../auth/profile-resolver.js';
|
|
17
|
+
import { type FrameworkSchemaNames } from '../config/framework-schema-names.js';
|
|
17
18
|
export declare class MariaDbProfileResolver implements ProfileResolver {
|
|
18
19
|
private readonly pool;
|
|
19
20
|
private readonly now;
|
|
20
|
-
|
|
21
|
+
/** The org database (`<app>`), resolved at construction. */
|
|
22
|
+
private readonly app;
|
|
23
|
+
constructor(pool: Pool, now?: () => number,
|
|
24
|
+
/** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
|
|
25
|
+
schemas?: FrameworkSchemaNames);
|
|
21
26
|
resolve(principal: Principal): Promise<CurrentUserProfile | null>;
|
|
22
27
|
private resolveRoleNames;
|
|
23
28
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mariadb-profile-resolver.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-profile-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,eAAe,EAAe,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"mariadb-profile-resolver.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-profile-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,eAAe,EAAe,MAAM,6BAA6B,CAAC;AACpG,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,qCAAqC,CAAC;AAc7C,qBAAa,sBAAuB,YAAW,eAAe;IAK1D,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,GAAG;IALtB,4DAA4D;IAC5D,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;gBAGV,IAAI,EAAE,IAAI,EACV,GAAG,GAAE,MAAM,MAAiB;IAC7C,yFAAyF;IACzF,OAAO,CAAC,EAAE,oBAAoB;IAK1B,OAAO,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;YA4BzD,gBAAgB;CAc/B"}
|
|
@@ -14,21 +14,27 @@
|
|
|
14
14
|
*/
|
|
15
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
16
|
exports.MariaDbProfileResolver = void 0;
|
|
17
|
+
const framework_schema_names_js_1 = require("../config/framework-schema-names.js");
|
|
17
18
|
class MariaDbProfileResolver {
|
|
18
19
|
pool;
|
|
19
20
|
now;
|
|
20
|
-
|
|
21
|
+
/** The org database (`<app>`), resolved at construction. */
|
|
22
|
+
app;
|
|
23
|
+
constructor(pool, now = Date.now,
|
|
24
|
+
/** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
|
|
25
|
+
schemas) {
|
|
21
26
|
this.pool = pool;
|
|
22
27
|
this.now = now;
|
|
28
|
+
this.app = (0, framework_schema_names_js_1.schemaNamesAtConstruction)(schemas).app;
|
|
23
29
|
}
|
|
24
30
|
async resolve(principal) {
|
|
25
31
|
const userUuid = principal.id;
|
|
26
32
|
const companyUuid = principal.tenantId;
|
|
27
|
-
const users = (await this.pool.query(
|
|
33
|
+
const users = (await this.pool.query(`SELECT u_username, u_email FROM \`${this.app}\`.\`users\` WHERE u_uuid = ? LIMIT 1`, [userUuid]));
|
|
28
34
|
const user = users[0];
|
|
29
35
|
if (!user)
|
|
30
36
|
return null; // deleted user → no profile (never id-leak; endpoint 404s)
|
|
31
|
-
const companies = (await this.pool.query(
|
|
37
|
+
const companies = (await this.pool.query(`SELECT c_name FROM \`${this.app}\`.\`companies\` WHERE c_uuid = ? LIMIT 1`, [companyUuid]));
|
|
32
38
|
const companyName = companies[0]?.c_name ?? '(unknown company)';
|
|
33
39
|
const roles = await this.resolveRoleNames(userUuid, companyUuid);
|
|
34
40
|
return {
|
|
@@ -42,9 +48,9 @@ class MariaDbProfileResolver {
|
|
|
42
48
|
async resolveRoleNames(userUuid, companyUuid) {
|
|
43
49
|
const nowMs = this.now();
|
|
44
50
|
const rows = (await this.pool.query(`SELECT DISTINCT r.r_uuid, r.r_name
|
|
45
|
-
FROM
|
|
46
|
-
JOIN
|
|
47
|
-
JOIN
|
|
51
|
+
FROM \`${this.app}\`.\`membership_posting\` mp
|
|
52
|
+
JOIN \`${this.app}\`.\`company_membership\` cm ON cm.cm_uuid = mp.mp_membership_uuid
|
|
53
|
+
JOIN \`${this.app}\`.\`roles\` r ON r.r_uuid = mp.mp_role_uuid
|
|
48
54
|
WHERE cm.cm_user_uuid = ? AND cm.cm_company_uuid = ?
|
|
49
55
|
AND (mp.mp_effective_from_ms IS NULL OR mp.mp_effective_from_ms <= ?)
|
|
50
56
|
AND (mp.mp_effective_until_ms IS NULL OR mp.mp_effective_until_ms > ?)`, [userUuid, companyUuid, nowMs, nowMs]));
|
|
@@ -7,10 +7,15 @@
|
|
|
7
7
|
import type { Pool } from 'mariadb';
|
|
8
8
|
import { type ProfileWriter } from '../auth/ports/profile-writer.js';
|
|
9
9
|
import type { CurrentUserProfile } from '../auth/profile-resolver.js';
|
|
10
|
+
import { type FrameworkSchemaNames } from '../config/framework-schema-names.js';
|
|
10
11
|
export declare class MariaDbProfileWriter implements ProfileWriter {
|
|
11
12
|
private readonly pool;
|
|
12
13
|
private readonly resolver;
|
|
13
|
-
|
|
14
|
+
/** The org database (`<app>`), resolved at construction; the re-read resolver uses the same. */
|
|
15
|
+
private readonly app;
|
|
16
|
+
constructor(pool: Pool,
|
|
17
|
+
/** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
|
|
18
|
+
schemas?: FrameworkSchemaNames);
|
|
14
19
|
update(principalId: string, changes: {
|
|
15
20
|
name?: string;
|
|
16
21
|
email?: string;
|
|
@@ -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;
|
|
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"}
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.MariaDbProfileWriter = void 0;
|
|
4
4
|
const profile_writer_js_1 = require("../auth/ports/profile-writer.js");
|
|
5
5
|
const mariadb_profile_resolver_js_1 = require("./mariadb-profile-resolver.js");
|
|
6
|
+
const framework_schema_names_js_1 = require("../config/framework-schema-names.js");
|
|
6
7
|
/** MariaDB `ER_DUP_ENTRY`: here `IDX_users_username` refused the name (KI #97). */
|
|
7
8
|
const ER_DUP_ENTRY = 1062;
|
|
8
9
|
function isDuplicateKey(err) {
|
|
@@ -14,9 +15,15 @@ function isDuplicateKey(err) {
|
|
|
14
15
|
class MariaDbProfileWriter {
|
|
15
16
|
pool;
|
|
16
17
|
resolver;
|
|
17
|
-
|
|
18
|
+
/** The org database (`<app>`), resolved at construction; the re-read resolver uses the same. */
|
|
19
|
+
app;
|
|
20
|
+
constructor(pool,
|
|
21
|
+
/** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
|
|
22
|
+
schemas) {
|
|
18
23
|
this.pool = pool;
|
|
19
|
-
|
|
24
|
+
const s = (0, framework_schema_names_js_1.schemaNamesAtConstruction)(schemas);
|
|
25
|
+
this.app = s.app;
|
|
26
|
+
this.resolver = new mariadb_profile_resolver_js_1.MariaDbProfileResolver(pool, undefined, s);
|
|
20
27
|
}
|
|
21
28
|
async update(principalId, changes) {
|
|
22
29
|
// Phase 12 / KI #87: the email is written ONLY by the verified email-change flow
|
|
@@ -34,7 +41,7 @@ class MariaDbProfileWriter {
|
|
|
34
41
|
if (sets.length > 0) {
|
|
35
42
|
params.push(principalId);
|
|
36
43
|
try {
|
|
37
|
-
await this.pool.query(`UPDATE
|
|
44
|
+
await this.pool.query(`UPDATE \`${this.app}\`.\`users\` SET ${sets.join(', ')} WHERE u_uuid = ?`, params);
|
|
38
45
|
}
|
|
39
46
|
catch (err) {
|
|
40
47
|
// KI #97: the driver message quotes the value; drop it and answer a typed conflict.
|
|
@@ -44,7 +51,7 @@ class MariaDbProfileWriter {
|
|
|
44
51
|
}
|
|
45
52
|
}
|
|
46
53
|
// Resolve the caller's active company so the re-read profile is tenant-correct.
|
|
47
|
-
const active = (await this.pool.query(
|
|
54
|
+
const active = (await this.pool.query(`SELECT cm_company_uuid FROM \`${this.app}\`.\`company_membership\` WHERE cm_user_uuid = ? AND cm_is_active = 1 AND cm_status = 'active' LIMIT 1`, [principalId]));
|
|
48
55
|
const tenantId = active[0]?.cm_company_uuid ?? '';
|
|
49
56
|
const profile = await this.resolver.resolve({
|
|
50
57
|
kind: 'user',
|
|
@@ -21,14 +21,19 @@
|
|
|
21
21
|
import type { Pool } from 'mariadb';
|
|
22
22
|
import type { PasswordHasher } from '@astram/core';
|
|
23
23
|
import type { RecoveryCodeStore } from '../auth/ports/recovery-code-store.js';
|
|
24
|
+
import { type FrameworkSchemaNames } from '../config/framework-schema-names.js';
|
|
24
25
|
export declare class MariaDbRecoveryCodeStore implements RecoveryCodeStore {
|
|
25
26
|
private readonly pool;
|
|
26
27
|
/** The SAME Argon2id hasher the framework uses to hash the codes on enroll. */
|
|
27
28
|
private readonly hasher;
|
|
28
29
|
private readonly now;
|
|
30
|
+
/** `<identity>`.`mfa_recovery_code`, resolved at construction. */
|
|
31
|
+
private readonly table;
|
|
29
32
|
constructor(pool: Pool,
|
|
30
33
|
/** The SAME Argon2id hasher the framework uses to hash the codes on enroll. */
|
|
31
|
-
hasher: PasswordHasher, now?: () => number
|
|
34
|
+
hasher: PasswordHasher, now?: () => number,
|
|
35
|
+
/** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
|
|
36
|
+
schemas?: FrameworkSchemaNames);
|
|
32
37
|
/**
|
|
33
38
|
* Replace the principal's entire recovery-code set with `codeHashes` (already
|
|
34
39
|
* Argon2-hashed). Wipes any prior codes first, then inserts the new batch. Passing
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mariadb-recovery-code-store.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-recovery-code-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;
|
|
1
|
+
{"version":3,"file":"mariadb-recovery-code-store.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-recovery-code-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAC9E,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,qCAAqC,CAAC;AAM7C,qBAAa,wBAAyB,YAAW,iBAAiB;IAK9D,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,+EAA+E;IAC/E,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAPtB,kEAAkE;IAClE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;gBAGZ,IAAI,EAAE,IAAI;IAC3B,+EAA+E;IAC9D,MAAM,EAAE,cAAc,EACtB,GAAG,GAAE,MAAM,MAAiB;IAC7C,yFAAyF;IACzF,OAAO,CAAC,EAAE,oBAAoB;IAQhC;;;;OAIG;IACG,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAanF;;;;;;OAMG;IACG,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAqBtE"}
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MariaDbRecoveryCodeStore = void 0;
|
|
4
|
+
const framework_schema_names_js_1 = require("../config/framework-schema-names.js");
|
|
4
5
|
class MariaDbRecoveryCodeStore {
|
|
5
6
|
pool;
|
|
6
7
|
hasher;
|
|
7
8
|
now;
|
|
9
|
+
/** `<identity>`.`mfa_recovery_code`, resolved at construction. */
|
|
10
|
+
table;
|
|
8
11
|
constructor(pool,
|
|
9
12
|
/** The SAME Argon2id hasher the framework uses to hash the codes on enroll. */
|
|
10
|
-
hasher, now = Date.now
|
|
13
|
+
hasher, now = Date.now,
|
|
14
|
+
/** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
|
|
15
|
+
schemas) {
|
|
11
16
|
this.pool = pool;
|
|
12
17
|
this.hasher = hasher;
|
|
13
18
|
this.now = now;
|
|
19
|
+
this.table = (0, framework_schema_names_js_1.qualifiedTableName)((0, framework_schema_names_js_1.schemaNamesAtConstruction)(schemas).identity, 'mfa_recovery_code');
|
|
14
20
|
}
|
|
15
21
|
/**
|
|
16
22
|
* Replace the principal's entire recovery-code set with `codeHashes` (already
|
|
@@ -18,10 +24,10 @@ class MariaDbRecoveryCodeStore {
|
|
|
18
24
|
* an empty array simply wipes (used when MFA is disabled).
|
|
19
25
|
*/
|
|
20
26
|
async replaceAll(principalId, codeHashes) {
|
|
21
|
-
await this.pool.query(`DELETE FROM
|
|
27
|
+
await this.pool.query(`DELETE FROM ${this.table} WHERE mrc_principal_id = ?`, [principalId]);
|
|
22
28
|
const createdAt = this.now();
|
|
23
29
|
for (const hash of codeHashes) {
|
|
24
|
-
await this.pool.query(`INSERT INTO
|
|
30
|
+
await this.pool.query(`INSERT INTO ${this.table}
|
|
25
31
|
(mrc_principal_id, mrc_code_hash, mrc_consumed_at_ms, mrc_created_at_ms)
|
|
26
32
|
VALUES (?, ?, NULL, ?)`, [principalId, hash, createdAt]);
|
|
27
33
|
}
|
|
@@ -36,11 +42,11 @@ class MariaDbRecoveryCodeStore {
|
|
|
36
42
|
async consume(principalId, rawCode) {
|
|
37
43
|
if (typeof rawCode !== 'string' || rawCode.length === 0)
|
|
38
44
|
return false;
|
|
39
|
-
const rows = (await this.pool.query(`SELECT mrc_code_hash FROM
|
|
45
|
+
const rows = (await this.pool.query(`SELECT mrc_code_hash FROM ${this.table}
|
|
40
46
|
WHERE mrc_principal_id = ? AND mrc_consumed_at_ms IS NULL`, [principalId]));
|
|
41
47
|
for (const row of rows) {
|
|
42
48
|
if (await this.hasher.verify(row.mrc_code_hash, rawCode)) {
|
|
43
|
-
const res = (await this.pool.query(`UPDATE
|
|
49
|
+
const res = (await this.pool.query(`UPDATE ${this.table}
|
|
44
50
|
SET mrc_consumed_at_ms = ?
|
|
45
51
|
WHERE mrc_principal_id = ? AND mrc_code_hash = ? AND mrc_consumed_at_ms IS NULL`, [this.now(), principalId, row.mrc_code_hash]));
|
|
46
52
|
// Single-use guard: only the UPDATE that flips NULL→consumed wins.
|
|
@@ -15,10 +15,15 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import type { Pool } from 'mariadb';
|
|
17
17
|
import type { ResetTokenStore } from '../auth/ports/reset-token-store.js';
|
|
18
|
+
import { type FrameworkSchemaNames } from '../config/framework-schema-names.js';
|
|
18
19
|
export declare class MariaDbResetTokenStore implements ResetTokenStore {
|
|
19
20
|
private readonly pool;
|
|
20
21
|
private readonly now;
|
|
21
|
-
|
|
22
|
+
/** `<identity>`.`password_reset_token`, resolved at construction. */
|
|
23
|
+
private readonly table;
|
|
24
|
+
constructor(pool: Pool, now?: () => number,
|
|
25
|
+
/** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
|
|
26
|
+
schemas?: FrameworkSchemaNames);
|
|
22
27
|
issue(principalId: string, tokenHash: string, expiresAtMs: number): Promise<void>;
|
|
23
28
|
consume(tokenHash: string): Promise<string | null>;
|
|
24
29
|
invalidateAllForPrincipal(principalId: string): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mariadb-reset-token-store.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-reset-token-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;
|
|
1
|
+
{"version":3,"file":"mariadb-reset-token-store.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-reset-token-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAC1E,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,qCAAqC,CAAC;AAM7C,qBAAa,sBAAuB,YAAW,eAAe;IAK1D,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,GAAG;IALtB,qEAAqE;IACrE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;gBAGZ,IAAI,EAAE,IAAI,EACV,GAAG,GAAE,MAAM,MAAiB;IAC7C,yFAAyF;IACzF,OAAO,CAAC,EAAE,oBAAoB;IAQ1B,KAAK,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IASjF,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IA0BlD,yBAAyB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAUpE"}
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MariaDbResetTokenStore = void 0;
|
|
4
|
+
const framework_schema_names_js_1 = require("../config/framework-schema-names.js");
|
|
4
5
|
class MariaDbResetTokenStore {
|
|
5
6
|
pool;
|
|
6
7
|
now;
|
|
7
|
-
|
|
8
|
+
/** `<identity>`.`password_reset_token`, resolved at construction. */
|
|
9
|
+
table;
|
|
10
|
+
constructor(pool, now = Date.now,
|
|
11
|
+
/** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
|
|
12
|
+
schemas) {
|
|
8
13
|
this.pool = pool;
|
|
9
14
|
this.now = now;
|
|
15
|
+
this.table = (0, framework_schema_names_js_1.qualifiedTableName)((0, framework_schema_names_js_1.schemaNamesAtConstruction)(schemas).identity, 'password_reset_token');
|
|
10
16
|
}
|
|
11
17
|
async issue(principalId, tokenHash, expiresAtMs) {
|
|
12
|
-
await this.pool.query(`INSERT INTO
|
|
18
|
+
await this.pool.query(`INSERT INTO ${this.table}
|
|
13
19
|
(prt_token_hash, prt_principal_id, prt_expires_at_ms, prt_consumed_at_ms, prt_created_at_ms)
|
|
14
20
|
VALUES (?, ?, ?, NULL, ?)`, [tokenHash, principalId, expiresAtMs, this.now()]);
|
|
15
21
|
}
|
|
@@ -17,7 +23,7 @@ class MariaDbResetTokenStore {
|
|
|
17
23
|
const now = this.now();
|
|
18
24
|
// 1) Find an unexpired, unconsumed token for this hash.
|
|
19
25
|
const rows = (await this.pool.query(`SELECT prt_principal_id
|
|
20
|
-
FROM
|
|
26
|
+
FROM ${this.table}
|
|
21
27
|
WHERE prt_token_hash = ? AND prt_consumed_at_ms IS NULL AND prt_expires_at_ms > ?
|
|
22
28
|
LIMIT 1`, [tokenHash, now]));
|
|
23
29
|
const principalId = rows[0]?.prt_principal_id;
|
|
@@ -25,7 +31,7 @@ class MariaDbResetTokenStore {
|
|
|
25
31
|
return null;
|
|
26
32
|
// 2) Atomically claim it (single-use). The `IS NULL` guard + affectedRows check means a
|
|
27
33
|
// concurrent consume can never both succeed — the loser sees 0 rows and returns null.
|
|
28
|
-
const res = (await this.pool.query(`UPDATE
|
|
34
|
+
const res = (await this.pool.query(`UPDATE ${this.table}
|
|
29
35
|
SET prt_consumed_at_ms = ?
|
|
30
36
|
WHERE prt_token_hash = ? AND prt_consumed_at_ms IS NULL`, [now, tokenHash]));
|
|
31
37
|
if (res.affectedRows !== 1)
|
|
@@ -35,7 +41,7 @@ class MariaDbResetTokenStore {
|
|
|
35
41
|
async invalidateAllForPrincipal(principalId) {
|
|
36
42
|
// Burn every outstanding (unconsumed) token for the principal after a successful reset, so a
|
|
37
43
|
// second still-valid link cannot re-reset.
|
|
38
|
-
await this.pool.query(`UPDATE
|
|
44
|
+
await this.pool.query(`UPDATE ${this.table}
|
|
39
45
|
SET prt_consumed_at_ms = ?
|
|
40
46
|
WHERE prt_principal_id = ? AND prt_consumed_at_ms IS NULL`, [this.now(), principalId]);
|
|
41
47
|
}
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
import type { Pool } from 'mariadb';
|
|
24
24
|
import type { SessionSweeper } from '../auth/ports/session-sweeper.js';
|
|
25
25
|
import type { SessionTokenBinder } from '../request-auth/session-token-binder.js';
|
|
26
|
+
import { type FrameworkSchemaNames } from '../config/framework-schema-names.js';
|
|
26
27
|
/** The binder surface the sweeper needs; `unbindSessionStrict` (throws on a Redis error) is preferred. */
|
|
27
28
|
type SweepBinder = Pick<SessionTokenBinder, 'unbindSession'> & Partial<Pick<SessionTokenBinder, 'unbindSessionStrict'>>;
|
|
28
29
|
export declare class MariaDbSessionSweeper implements SessionSweeper {
|
|
@@ -30,9 +31,13 @@ export declare class MariaDbSessionSweeper implements SessionSweeper {
|
|
|
30
31
|
private readonly binder;
|
|
31
32
|
/** Clock for the "not yet expired" unbind window (epoch-ms). */
|
|
32
33
|
private readonly now;
|
|
34
|
+
/** `<identity>`.`session`, resolved at construction (configurable-framework-db). */
|
|
35
|
+
private readonly sessionTable;
|
|
33
36
|
constructor(pool: Pool, binder: SweepBinder,
|
|
34
37
|
/** Clock for the "not yet expired" unbind window (epoch-ms). */
|
|
35
|
-
now?: () => number
|
|
38
|
+
now?: () => number,
|
|
39
|
+
/** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
|
|
40
|
+
schemas?: FrameworkSchemaNames);
|
|
36
41
|
/**
|
|
37
42
|
* Unbind the given sessions in Redis. Every session is attempted; if any unbind failed, the
|
|
38
43
|
* FIRST error is thrown afterwards (Phase 3 review P1), so an administrative revoke reports it
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mariadb-session-sweeper.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-session-sweeper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AACvE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"mariadb-session-sweeper.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-session-sweeper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AACvE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAClF,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,qCAAqC,CAAC;AAM7C,0GAA0G;AAC1G,KAAK,WAAW,GAAG,IAAI,CAAC,kBAAkB,EAAE,eAAe,CAAC,GAC1D,OAAO,CAAC,IAAI,CAAC,kBAAkB,EAAE,qBAAqB,CAAC,CAAC,CAAC;AAE3D,qBAAa,qBAAsB,YAAW,cAAc;IAKxD,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,gEAAgE;IAChE,OAAO,CAAC,QAAQ,CAAC,GAAG;IAPtB,oFAAoF;IACpF,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;gBAGnB,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,WAAW;IACpC,gEAAgE;IAC/C,GAAG,GAAE,MAAM,MAAiB;IAC7C,yFAAyF;IACzF,OAAO,CAAC,EAAE,oBAAoB;IAKhC;;;;OAIG;YACW,SAAS;IAUjB,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAqBzD,wBAAwB,CAAC,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAkB1F"}
|
|
@@ -23,16 +23,22 @@
|
|
|
23
23
|
*/
|
|
24
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
25
|
exports.MariaDbSessionSweeper = void 0;
|
|
26
|
+
const framework_schema_names_js_1 = require("../config/framework-schema-names.js");
|
|
26
27
|
class MariaDbSessionSweeper {
|
|
27
28
|
pool;
|
|
28
29
|
binder;
|
|
29
30
|
now;
|
|
31
|
+
/** `<identity>`.`session`, resolved at construction (configurable-framework-db). */
|
|
32
|
+
sessionTable;
|
|
30
33
|
constructor(pool, binder,
|
|
31
34
|
/** Clock for the "not yet expired" unbind window (epoch-ms). */
|
|
32
|
-
now = Date.now
|
|
35
|
+
now = Date.now,
|
|
36
|
+
/** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
|
|
37
|
+
schemas) {
|
|
33
38
|
this.pool = pool;
|
|
34
39
|
this.binder = binder;
|
|
35
40
|
this.now = now;
|
|
41
|
+
this.sessionTable = (0, framework_schema_names_js_1.qualifiedTableName)((0, framework_schema_names_js_1.schemaNamesAtConstruction)(schemas).identity, 'session');
|
|
36
42
|
}
|
|
37
43
|
/**
|
|
38
44
|
* Unbind the given sessions in Redis. Every session is attempted; if any unbind failed, the
|
|
@@ -53,9 +59,9 @@ class MariaDbSessionSweeper {
|
|
|
53
59
|
// absolute expiry, WHATEVER its status — so a retry after a failed unbind (the durable row is
|
|
54
60
|
// already `revoked`) still drops the Redis keys. Read BEFORE the durable revoke.
|
|
55
61
|
const rows = (await this.pool.query(`SELECT s_session_id
|
|
56
|
-
FROM
|
|
62
|
+
FROM ${this.sessionTable}
|
|
57
63
|
WHERE s_principal_id = ? AND s_absolute_expires_at_ms > ?`, [principalId, this.now()]));
|
|
58
|
-
await this.pool.query(`UPDATE
|
|
64
|
+
await this.pool.query(`UPDATE ${this.sessionTable}
|
|
59
65
|
SET s_status = 'revoked'
|
|
60
66
|
WHERE s_principal_id = ? AND s_status = 'active'`, [principalId]);
|
|
61
67
|
// Drop each session's Redis liveness key so outstanding access tokens die immediately, not
|
|
@@ -65,9 +71,9 @@ class MariaDbSessionSweeper {
|
|
|
65
71
|
async revokeOthersForPrincipal(principalId, keepSessionId) {
|
|
66
72
|
// Same unbind set as revokeAll (not-yet-expired, any status) minus the kept session.
|
|
67
73
|
const rows = (await this.pool.query(`SELECT s_session_id
|
|
68
|
-
FROM
|
|
74
|
+
FROM ${this.sessionTable}
|
|
69
75
|
WHERE s_principal_id = ? AND s_absolute_expires_at_ms > ? AND s_session_id <> ?`, [principalId, this.now(), keepSessionId]));
|
|
70
|
-
await this.pool.query(`UPDATE
|
|
76
|
+
await this.pool.query(`UPDATE ${this.sessionTable}
|
|
71
77
|
SET s_status = 'revoked'
|
|
72
78
|
WHERE s_principal_id = ? AND s_status = 'active' AND s_session_id <> ?`, [principalId, keepSessionId]);
|
|
73
79
|
// Only the OTHER sessions are unbound — the kept (current) session's liveness key
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cross-tenant-grant-writer.d.ts","sourceRoot":"","sources":["../../src/authz/cross-tenant-grant-writer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4DG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"cross-tenant-grant-writer.d.ts","sourceRoot":"","sources":["../../src/authz/cross-tenant-grant-writer.ts"],"names":[],"mappings":"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"}
|