@astram/guard 0.6.0 → 0.8.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 (124) hide show
  1. package/dist/auth/account.controller.d.ts +3 -1
  2. package/dist/auth/account.controller.d.ts.map +1 -1
  3. package/dist/auth/account.controller.js +12 -5
  4. package/dist/auth/auth.controller.d.ts +130 -21
  5. package/dist/auth/auth.controller.d.ts.map +1 -1
  6. package/dist/auth/auth.controller.js +537 -143
  7. package/dist/auth/auth.module.d.ts +15 -1
  8. package/dist/auth/auth.module.d.ts.map +1 -1
  9. package/dist/auth/auth.module.js +241 -30
  10. package/dist/auth/connections.d.ts +1 -1
  11. package/dist/auth/connections.d.ts.map +1 -1
  12. package/dist/auth/connections.js +14 -3
  13. package/dist/auth/default-session-facts-projector.d.ts +26 -0
  14. package/dist/auth/default-session-facts-projector.d.ts.map +1 -0
  15. package/dist/auth/default-session-facts-projector.js +21 -0
  16. package/dist/auth/mfa-step-up.d.ts +32 -0
  17. package/dist/auth/mfa-step-up.d.ts.map +1 -0
  18. package/dist/auth/mfa-step-up.js +109 -0
  19. package/dist/auth/pending-mfa-store.d.ts +50 -52
  20. package/dist/auth/pending-mfa-store.d.ts.map +1 -1
  21. package/dist/auth/pending-mfa-store.js +103 -61
  22. package/dist/auth/ports/session-facts-projector.d.ts +5 -0
  23. package/dist/auth/ports/session-facts-projector.d.ts.map +1 -1
  24. package/dist/auth/rate-limit/canonical-hint.d.ts +24 -0
  25. package/dist/auth/rate-limit/canonical-hint.d.ts.map +1 -0
  26. package/dist/auth/rate-limit/canonical-hint.js +63 -0
  27. package/dist/auth/rate-limit/trust-proxy-check.d.ts +46 -0
  28. package/dist/auth/rate-limit/trust-proxy-check.d.ts.map +1 -0
  29. package/dist/auth/rate-limit/trust-proxy-check.js +94 -0
  30. package/dist/auth/tokens.d.ts +70 -11
  31. package/dist/auth/tokens.d.ts.map +1 -1
  32. package/dist/auth/tokens.js +71 -12
  33. package/dist/authentication/log-email-sender.d.ts +9 -4
  34. package/dist/authentication/log-email-sender.d.ts.map +1 -1
  35. package/dist/authentication/mariadb-api-key-store.d.ts +46 -0
  36. package/dist/authentication/mariadb-api-key-store.d.ts.map +1 -0
  37. package/dist/authentication/mariadb-api-key-store.js +111 -0
  38. package/dist/authentication/mariadb-credential-resolver.d.ts +79 -65
  39. package/dist/authentication/mariadb-credential-resolver.d.ts.map +1 -1
  40. package/dist/authentication/mariadb-credential-resolver.js +116 -73
  41. package/dist/authentication/mariadb-credential-writer.d.ts.map +1 -1
  42. package/dist/authentication/mariadb-or-totp-verifier.d.ts +21 -3
  43. package/dist/authentication/mariadb-or-totp-verifier.d.ts.map +1 -1
  44. package/dist/authentication/mariadb-or-totp-verifier.js +37 -6
  45. package/dist/authentication/mariadb-profile-resolver.d.ts.map +1 -1
  46. package/dist/authentication/mariadb-profile-writer.d.ts.map +1 -1
  47. package/dist/authentication/mariadb-session-sweeper.d.ts +14 -1
  48. package/dist/authentication/mariadb-session-sweeper.d.ts.map +1 -1
  49. package/dist/authentication/mariadb-session-sweeper.js +29 -11
  50. package/dist/authentication/noop-email-sender.d.ts +12 -0
  51. package/dist/authentication/noop-email-sender.d.ts.map +1 -0
  52. package/dist/authentication/noop-email-sender.js +9 -0
  53. package/dist/authz/cross-tenant-grant-writer.d.ts.map +1 -1
  54. package/dist/authz/manifest-operation-authority-adapter.d.ts.map +1 -1
  55. package/dist/authz/mariadb-cross-tenant-grant-adapter.d.ts.map +1 -1
  56. package/dist/authz/mariadb-cross-tenant-grant-adapter.js +3 -1
  57. package/dist/authz/mariadb-policy-store.js +1 -1
  58. package/dist/authz/mariadb-record-rule-adapter.d.ts.map +1 -1
  59. package/dist/authz/mariadb-record-rule-adapter.js +1 -4
  60. package/dist/authz/permission-version-cache.d.ts +38 -0
  61. package/dist/authz/permission-version-cache.d.ts.map +1 -0
  62. package/dist/authz/permission-version-cache.js +81 -0
  63. package/dist/authz/wire-record-authorization.d.ts +25 -1
  64. package/dist/authz/wire-record-authorization.d.ts.map +1 -1
  65. package/dist/authz/wire-record-authorization.js +55 -2
  66. package/dist/completeness/catalog-reconciler.d.ts.map +1 -1
  67. package/dist/config/app-config.d.ts +52 -0
  68. package/dist/config/app-config.d.ts.map +1 -1
  69. package/dist/config/app-config.js +66 -4
  70. package/dist/encryption/local-kek-crypto.d.ts +15 -0
  71. package/dist/encryption/local-kek-crypto.d.ts.map +1 -0
  72. package/dist/encryption/local-kek-crypto.js +79 -0
  73. package/dist/enforcement/enforcement.module.d.ts.map +1 -1
  74. package/dist/enforcement/enforcement.module.js +9 -0
  75. package/dist/enforcement/engine-error.filter.d.ts +57 -0
  76. package/dist/enforcement/engine-error.filter.d.ts.map +1 -0
  77. package/dist/enforcement/engine-error.filter.js +118 -0
  78. package/dist/enforcement/global-enforcement.guard.d.ts +31 -2
  79. package/dist/enforcement/global-enforcement.guard.d.ts.map +1 -1
  80. package/dist/enforcement/global-enforcement.guard.js +148 -6
  81. package/dist/identity/auth-facts.controller.d.ts.map +1 -1
  82. package/dist/identity/identity-actions.d.ts +9 -0
  83. package/dist/identity/identity-actions.d.ts.map +1 -1
  84. package/dist/identity/identity-actions.js +16 -1
  85. package/dist/identity/permission-facts.adapter.d.ts.map +1 -1
  86. package/dist/identity/permission-facts.adapter.js +2 -3
  87. package/dist/index.d.ts +17 -7
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/index.js +82 -25
  90. package/dist/module/shield.module.d.ts +9 -2
  91. package/dist/module/shield.module.d.ts.map +1 -1
  92. package/dist/module/shield.module.js +47 -38
  93. package/dist/request-auth/request-principal.d.ts +11 -3
  94. package/dist/request-auth/request-principal.d.ts.map +1 -1
  95. package/dist/request-auth/request-principal.js +58 -2
  96. package/dist/request-auth/session-facts-als.d.ts +16 -1
  97. package/dist/request-auth/session-facts-als.d.ts.map +1 -1
  98. package/dist/request-auth/session-facts-als.js +23 -0
  99. package/dist/request-auth/session-token-binder.d.ts +166 -10
  100. package/dist/request-auth/session-token-binder.d.ts.map +1 -1
  101. package/dist/request-auth/session-token-binder.js +408 -28
  102. package/dist/request-auth/tokens.d.ts +10 -0
  103. package/dist/request-auth/tokens.d.ts.map +1 -1
  104. package/dist/request-auth/tokens.js +11 -1
  105. package/dist/secrets/encrypted-file-secrets.d.ts +16 -0
  106. package/dist/secrets/encrypted-file-secrets.d.ts.map +1 -0
  107. package/dist/secrets/encrypted-file-secrets.js +98 -0
  108. package/dist/session/hot-revoking-session-record-store.d.ts +46 -0
  109. package/dist/session/hot-revoking-session-record-store.d.ts.map +1 -0
  110. package/dist/session/hot-revoking-session-record-store.js +58 -0
  111. package/dist/session/index.d.ts +3 -1
  112. package/dist/session/index.d.ts.map +1 -1
  113. package/dist/session/index.js +8 -1
  114. package/dist/session/mariadb-session-record-store.d.ts +6 -0
  115. package/dist/session/mariadb-session-record-store.d.ts.map +1 -1
  116. package/dist/session/mariadb-session-record-store.js +16 -1
  117. package/dist/session/session-lifetimes.d.ts +30 -0
  118. package/dist/session/session-lifetimes.d.ts.map +1 -0
  119. package/dist/session/session-lifetimes.js +21 -0
  120. package/dist/session/session-restriction-store.d.ts +43 -0
  121. package/dist/session/session-restriction-store.d.ts.map +1 -0
  122. package/dist/session/session-restriction-store.js +35 -0
  123. package/migrations/1710000000017-CreateServiceAccountKey.cjs +41 -0
  124. package/package.json +3 -3
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EncryptedFileSecretsProvider = void 0;
4
+ /**
5
+ * `EncryptedFileSecretsProvider` — the reference {@link SecretsProvider} adapter (ADR-0032). An
6
+ * on-box secrets store where every secret is ENCRYPTED AT REST (AES-256-GCM under the box KEK, via
7
+ * the ADR-0031 `CryptoPort`), so the file on disk is ciphertext — not a plaintext `.env` password.
8
+ *
9
+ * This is the dev + air-gapped / single-box backend (what astram-infra's box uses). Vault / AWS
10
+ * Secrets Manager / GCP Secret Manager implement the SAME `SecretsProvider` and drop in later
11
+ * (Rule of Three) — no consumer change.
12
+ *
13
+ * SECURITY: never logs a secret value. `resolve` fails closed (throws SecretNotFoundError on an
14
+ * absent path — never returns ''). The store file is written 0600. The value is bound to its path
15
+ * as AEAD AAD, so a ciphertext for one path can't be replayed under another.
16
+ *
17
+ * Store format (JSON): { "<path>": { "version": n, "ciphertext": "<base64>" }, ... }
18
+ * The ciphertext is the CryptoPort AES-GCM sealing of the secret value, AAD = the secret path.
19
+ */
20
+ const node_fs_1 = require("node:fs");
21
+ const node_path_1 = require("node:path");
22
+ const core_1 = require("@astram/core");
23
+ const enc = new TextEncoder();
24
+ const dec = new TextDecoder();
25
+ class EncryptedFileSecretsProvider {
26
+ #file;
27
+ #crypto;
28
+ #dek;
29
+ /**
30
+ * @param file path to the encrypted store file (created 0600 on first write).
31
+ * @param crypto the ADR-0031 CryptoPort (AES-GCM); the SAME box KEK protects secrets + DEKs.
32
+ * @param dek the data key used to encrypt secret values. Provisioned/managed by the caller (a
33
+ * dedicated secrets DEK, wrapped by the KEK) — it is NEVER written to the store in plaintext.
34
+ */
35
+ constructor(file, crypto, dek) {
36
+ this.#file = file;
37
+ this.#crypto = crypto;
38
+ this.#dek = dek;
39
+ }
40
+ async resolve(path) {
41
+ const store = this.#read();
42
+ const entry = store[path];
43
+ if (!entry)
44
+ throw new core_1.SecretNotFoundError(path);
45
+ // AAD = path (bound), so a ciphertext moved to another path fails to decrypt.
46
+ const plain = await this.#crypto.decrypt(fromB64(entry.ciphertext), this.#dek, path);
47
+ return dec.decode(plain);
48
+ }
49
+ async store(path, value) {
50
+ return this.#write(path, value, /*rotate*/ false);
51
+ }
52
+ async rotate(path, newValue) {
53
+ return this.#write(path, newValue, /*rotate*/ true);
54
+ }
55
+ async currentVersion(path) {
56
+ return this.#read()[path]?.version ?? 0;
57
+ }
58
+ // ── internals ─────────────────────────────────────────────────────────────
59
+ async #write(path, value, _rotate) {
60
+ const store = this.#read();
61
+ const ct = await this.#crypto.encrypt(enc.encode(value), this.#dek, path);
62
+ const version = (store[path]?.version ?? 0) + 1;
63
+ store[path] = { version, ciphertext: toB64(ct) };
64
+ this.#persist(store);
65
+ return version;
66
+ }
67
+ #read() {
68
+ if (!(0, node_fs_1.existsSync)(this.#file))
69
+ return {};
70
+ try {
71
+ const raw = (0, node_fs_1.readFileSync)(this.#file, 'utf8');
72
+ return raw.trim() === '' ? {} : JSON.parse(raw);
73
+ }
74
+ catch {
75
+ // A corrupt store must NOT silently become empty (that would erase secrets); fail loud.
76
+ throw new Error(`secrets store is unreadable/corrupt: ${this.#file}`);
77
+ }
78
+ }
79
+ #persist(store) {
80
+ const dir = (0, node_path_1.dirname)(this.#file);
81
+ if (!(0, node_fs_1.existsSync)(dir))
82
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true, mode: 0o700 });
83
+ (0, node_fs_1.writeFileSync)(this.#file, JSON.stringify(store, null, 2), { mode: 0o600 });
84
+ try {
85
+ (0, node_fs_1.chmodSync)(this.#file, 0o600);
86
+ }
87
+ catch {
88
+ /* best-effort on platforms without chmod */
89
+ }
90
+ }
91
+ }
92
+ exports.EncryptedFileSecretsProvider = EncryptedFileSecretsProvider;
93
+ function toB64(b) {
94
+ return Buffer.from(b).toString('base64');
95
+ }
96
+ function fromB64(s) {
97
+ return new Uint8Array(Buffer.from(s, 'base64'));
98
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * `HotRevokingSessionRecordStore` — a decorator on `SHIELD_SESSION_RECORD_STORE`
3
+ * (security hardening M-A).
4
+ *
5
+ * WHY: core's `SessionAuthority` revokes through the durable store only — the refresh
6
+ * reuse-kill (`revokeLineage`) and the absolute-cap / logout path (`revokeSession`) mark
7
+ * MariaDB rows revoked, but the guard's per-request path reads Redis alone (invariant #12)
8
+ * and never consults MariaDB. Without this decorator a stolen-and-replayed refresh token
9
+ * kills the lineage durably while the live access token keeps working until its TTL.
10
+ *
11
+ * WHAT: after a durable revoke SUCCEEDS, unbind the affected sessions in Redis
12
+ * (`SessionTokenBinder.unbindSession` drops the session-claims liveness entry, so every
13
+ * access token of the session is denied on its next request). Durable first; a durable
14
+ * failure propagates and nothing is unbound. The Redis step is best-effort: a failure there
15
+ * never fails the durable revoke, and the exposure is bounded by the access TTL (#8).
16
+ *
17
+ * No core change: the port is decorated, not extended. The lineage → session lookup is a
18
+ * guard-side read on the durable adapter ({@link LineageSessionLookup}).
19
+ */
20
+ import type { CreateSessionInput, InsertRefreshInput, RefreshTokenRecord, SessionRecord, SessionRecordStore } from '@astram/core';
21
+ /** The Redis side of a revoke (the token binder implements it). */
22
+ export interface SessionHotRevoker {
23
+ unbindSession(sessionId: string): Promise<void>;
24
+ }
25
+ /** Resolve the sessions a refresh lineage belongs to (a lifecycle-time durable read). */
26
+ export interface LineageSessionLookup {
27
+ sessionIdsForLineage(lineageId: string): Promise<string[]>;
28
+ }
29
+ export declare class HotRevokingSessionRecordStore implements SessionRecordStore {
30
+ private readonly inner;
31
+ private readonly hot;
32
+ private readonly lineages;
33
+ constructor(inner: SessionRecordStore, hot: SessionHotRevoker, lineages: LineageSessionLookup);
34
+ createSession(input: CreateSessionInput): Promise<SessionRecord>;
35
+ insertRefreshRecord(input: InsertRefreshInput): Promise<RefreshTokenRecord>;
36
+ findRefreshByHash(tokenHash: string): Promise<RefreshTokenRecord | null>;
37
+ findSessionById(sessionId: string): Promise<SessionRecord | null>;
38
+ markRotatedIfActive(refreshId: string): Promise<boolean>;
39
+ markRevoked(refreshId: string): Promise<void>;
40
+ /** Durable lineage kill, then unbind every session of the lineage in Redis. */
41
+ revokeLineage(lineageId: string): Promise<void>;
42
+ /** Durable session revoke, then unbind it in Redis. */
43
+ revokeSession(sessionId: string): Promise<void>;
44
+ private unbind;
45
+ }
46
+ //# sourceMappingURL=hot-revoking-session-record-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hot-revoking-session-record-store.d.ts","sourceRoot":"","sources":["../../src/session/hot-revoking-session-record-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EACV,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,aAAa,EACb,kBAAkB,EACnB,MAAM,cAAc,CAAC;AAEtB,mEAAmE;AACnE,MAAM,WAAW,iBAAiB;IAChC,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACjD;AAED,yFAAyF;AACzF,MAAM,WAAW,oBAAoB;IACnC,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CAC5D;AAED,qBAAa,6BAA8B,YAAW,kBAAkB;IAEpE,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAFR,KAAK,EAAE,kBAAkB,EACzB,GAAG,EAAE,iBAAiB,EACtB,QAAQ,EAAE,oBAAoB;IAGjD,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,aAAa,CAAC;IAIhE,mBAAmB,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAI3E,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;IAIxE,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAIjE,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAIxD,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI7C,+EAA+E;IACzE,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAWrD,uDAAuD;IACjD,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAKvC,MAAM;CAOrB"}
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HotRevokingSessionRecordStore = void 0;
4
+ class HotRevokingSessionRecordStore {
5
+ inner;
6
+ hot;
7
+ lineages;
8
+ constructor(inner, hot, lineages) {
9
+ this.inner = inner;
10
+ this.hot = hot;
11
+ this.lineages = lineages;
12
+ }
13
+ createSession(input) {
14
+ return this.inner.createSession(input);
15
+ }
16
+ insertRefreshRecord(input) {
17
+ return this.inner.insertRefreshRecord(input);
18
+ }
19
+ findRefreshByHash(tokenHash) {
20
+ return this.inner.findRefreshByHash(tokenHash);
21
+ }
22
+ findSessionById(sessionId) {
23
+ return this.inner.findSessionById(sessionId);
24
+ }
25
+ markRotatedIfActive(refreshId) {
26
+ return this.inner.markRotatedIfActive(refreshId);
27
+ }
28
+ markRevoked(refreshId) {
29
+ return this.inner.markRevoked(refreshId);
30
+ }
31
+ /** Durable lineage kill, then unbind every session of the lineage in Redis. */
32
+ async revokeLineage(lineageId) {
33
+ await this.inner.revokeLineage(lineageId);
34
+ let sessionIds = [];
35
+ try {
36
+ sessionIds = await this.lineages.sessionIdsForLineage(lineageId);
37
+ }
38
+ catch {
39
+ return; // best-effort: bounded by the access TTL (#8).
40
+ }
41
+ for (const sessionId of sessionIds)
42
+ await this.unbind(sessionId);
43
+ }
44
+ /** Durable session revoke, then unbind it in Redis. */
45
+ async revokeSession(sessionId) {
46
+ await this.inner.revokeSession(sessionId);
47
+ await this.unbind(sessionId);
48
+ }
49
+ async unbind(sessionId) {
50
+ try {
51
+ await this.hot.unbindSession(sessionId);
52
+ }
53
+ catch {
54
+ // best-effort: never fail a durable revoke on a Redis blip.
55
+ }
56
+ }
57
+ }
58
+ exports.HotRevokingSessionRecordStore = HotRevokingSessionRecordStore;
@@ -12,6 +12,8 @@
12
12
  * A consumer wires these + `createSessionAuthority(...)` from `@astram/core`.
13
13
  */
14
14
  export { MariaDbSessionRecordStore, SHIELD_SESSION_RECORD_STORE, } from './mariadb-session-record-store.js';
15
- export { RedisHotSessionStore, SHIELD_HOT_SESSION_STORE, } from './redis-hot-session-store.js';
15
+ export { RedisHotSessionStore, SHIELD_HOT_SESSION_STORE } from './redis-hot-session-store.js';
16
+ export { HotRevokingSessionRecordStore, type SessionHotRevoker, type LineageSessionLookup, } from './hot-revoking-session-record-store.js';
17
+ export { SHIELD_SESSION_LIFETIMES, ROTATE_GRACE_MS, DEFAULT_ACCESS_TTL_MS, accessBindingTtlMs, } from './session-lifetimes.js';
16
18
  export { CryptoTokenGenerator, Sha256RefreshTokenHasher, SystemClock, cryptoTokenGeneratorProvider, refreshHasherProvider, systemClockProvider, SHIELD_TOKEN_GENERATOR, SHIELD_REFRESH_HASHER, SHIELD_CLOCK, } from './crypto-adapters.js';
17
19
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/session/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EACL,yBAAyB,EACzB,2BAA2B,GAC5B,MAAM,mCAAmC,CAAC;AAE3C,OAAO,EACL,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EACL,oBAAoB,EACpB,wBAAwB,EACxB,WAAW,EACX,4BAA4B,EAC5B,qBAAqB,EACrB,mBAAmB,EACnB,sBAAsB,EACtB,qBAAqB,EACrB,YAAY,GACb,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/session/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EACL,yBAAyB,EACzB,2BAA2B,GAC5B,MAAM,mCAAmC,CAAC;AAE3C,OAAO,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AAE9F,OAAO,EACL,6BAA6B,EAC7B,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,GAC1B,MAAM,wCAAwC,CAAC;AAEhD,OAAO,EACL,wBAAwB,EACxB,eAAe,EACf,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACL,oBAAoB,EACpB,wBAAwB,EACxB,WAAW,EACX,4BAA4B,EAC5B,qBAAqB,EACrB,mBAAmB,EACnB,sBAAsB,EACtB,qBAAqB,EACrB,YAAY,GACb,MAAM,sBAAsB,CAAC"}
@@ -13,13 +13,20 @@
13
13
  * A consumer wires these + `createSessionAuthority(...)` from `@astram/core`.
14
14
  */
15
15
  Object.defineProperty(exports, "__esModule", { value: true });
16
- exports.SHIELD_CLOCK = exports.SHIELD_REFRESH_HASHER = exports.SHIELD_TOKEN_GENERATOR = exports.systemClockProvider = exports.refreshHasherProvider = exports.cryptoTokenGeneratorProvider = exports.SystemClock = exports.Sha256RefreshTokenHasher = exports.CryptoTokenGenerator = exports.SHIELD_HOT_SESSION_STORE = exports.RedisHotSessionStore = exports.SHIELD_SESSION_RECORD_STORE = exports.MariaDbSessionRecordStore = void 0;
16
+ exports.SHIELD_CLOCK = exports.SHIELD_REFRESH_HASHER = exports.SHIELD_TOKEN_GENERATOR = exports.systemClockProvider = exports.refreshHasherProvider = exports.cryptoTokenGeneratorProvider = exports.SystemClock = exports.Sha256RefreshTokenHasher = exports.CryptoTokenGenerator = exports.accessBindingTtlMs = exports.DEFAULT_ACCESS_TTL_MS = exports.ROTATE_GRACE_MS = exports.SHIELD_SESSION_LIFETIMES = exports.HotRevokingSessionRecordStore = exports.SHIELD_HOT_SESSION_STORE = exports.RedisHotSessionStore = exports.SHIELD_SESSION_RECORD_STORE = exports.MariaDbSessionRecordStore = void 0;
17
17
  var mariadb_session_record_store_js_1 = require("./mariadb-session-record-store.js");
18
18
  Object.defineProperty(exports, "MariaDbSessionRecordStore", { enumerable: true, get: function () { return mariadb_session_record_store_js_1.MariaDbSessionRecordStore; } });
19
19
  Object.defineProperty(exports, "SHIELD_SESSION_RECORD_STORE", { enumerable: true, get: function () { return mariadb_session_record_store_js_1.SHIELD_SESSION_RECORD_STORE; } });
20
20
  var redis_hot_session_store_js_1 = require("./redis-hot-session-store.js");
21
21
  Object.defineProperty(exports, "RedisHotSessionStore", { enumerable: true, get: function () { return redis_hot_session_store_js_1.RedisHotSessionStore; } });
22
22
  Object.defineProperty(exports, "SHIELD_HOT_SESSION_STORE", { enumerable: true, get: function () { return redis_hot_session_store_js_1.SHIELD_HOT_SESSION_STORE; } });
23
+ var hot_revoking_session_record_store_js_1 = require("./hot-revoking-session-record-store.js");
24
+ Object.defineProperty(exports, "HotRevokingSessionRecordStore", { enumerable: true, get: function () { return hot_revoking_session_record_store_js_1.HotRevokingSessionRecordStore; } });
25
+ var session_lifetimes_js_1 = require("./session-lifetimes.js");
26
+ Object.defineProperty(exports, "SHIELD_SESSION_LIFETIMES", { enumerable: true, get: function () { return session_lifetimes_js_1.SHIELD_SESSION_LIFETIMES; } });
27
+ Object.defineProperty(exports, "ROTATE_GRACE_MS", { enumerable: true, get: function () { return session_lifetimes_js_1.ROTATE_GRACE_MS; } });
28
+ Object.defineProperty(exports, "DEFAULT_ACCESS_TTL_MS", { enumerable: true, get: function () { return session_lifetimes_js_1.DEFAULT_ACCESS_TTL_MS; } });
29
+ Object.defineProperty(exports, "accessBindingTtlMs", { enumerable: true, get: function () { return session_lifetimes_js_1.accessBindingTtlMs; } });
23
30
  var crypto_adapters_js_1 = require("./crypto-adapters.js");
24
31
  Object.defineProperty(exports, "CryptoTokenGenerator", { enumerable: true, get: function () { return crypto_adapters_js_1.CryptoTokenGenerator; } });
25
32
  Object.defineProperty(exports, "Sha256RefreshTokenHasher", { enumerable: true, get: function () { return crypto_adapters_js_1.Sha256RefreshTokenHasher; } });
@@ -43,6 +43,12 @@ export declare class MariaDbSessionRecordStore implements SessionRecordStore {
43
43
  * session in a lineage. One transaction so the kill is all-or-nothing.
44
44
  */
45
45
  revokeLineage(lineageId: string): Promise<void>;
46
+ /**
47
+ * The session ids a refresh lineage belongs to (normally exactly one). Read at a
48
+ * lifecycle event (after a reuse-kill) by `HotRevokingSessionRecordStore` to unbind
49
+ * those sessions in Redis — never on the per-request path.
50
+ */
51
+ sessionIdsForLineage(lineageId: string): Promise<string[]>;
46
52
  revokeSession(sessionId: string): Promise<void>;
47
53
  /** Run `fn` in a transaction, releasing the connection either way. */
48
54
  private withTx;
@@ -1 +1 @@
1
- {"version":3,"file":"mariadb-session-record-store.d.ts","sourceRoot":"","sources":["../../src/session/mariadb-session-record-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAkB,MAAM,SAAS,CAAC;AACpD,OAAO,KAAK,EACV,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,aAAa,EACb,kBAAkB,EACnB,MAAM,cAAc,CAAC;AAGtB,6CAA6C;AAC7C,eAAO,MAAM,2BAA2B,eAAwC,CAAC;AAmDjF,qBAAa,yBAA0B,YAAW,kBAAkB;IACtD,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,IAAI;IAEjC,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,aAAa,CAAC;IAiBhE,mBAAmB,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IA4B3E,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;IASxE,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IASvE;;;;;;;OAOG;IACG,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IASxD,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQnD;;;OAGG;IACG,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiB/C,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAarD,sEAAsE;YACxD,MAAM;CAiBrB"}
1
+ {"version":3,"file":"mariadb-session-record-store.d.ts","sourceRoot":"","sources":["../../src/session/mariadb-session-record-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAkB,MAAM,SAAS,CAAC;AACpD,OAAO,KAAK,EACV,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,aAAa,EACb,kBAAkB,EACnB,MAAM,cAAc,CAAC;AAGtB,6CAA6C;AAC7C,eAAO,MAAM,2BAA2B,eAAwC,CAAC;AAmDjF,qBAAa,yBAA0B,YAAW,kBAAkB;IACtD,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,IAAI;IAEjC,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,aAAa,CAAC;IAuBhE,mBAAmB,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IA4B3E,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;IASxE,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IASvE;;;;;;;OAOG;IACG,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IASxD,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQnD;;;OAGG;IACG,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBrD;;;;OAIG;IACG,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAQ1D,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAarD,sEAAsE;YACxD,MAAM;CAiBrB"}
@@ -56,7 +56,13 @@ class MariaDbSessionRecordStore {
56
56
  async createSession(input) {
57
57
  await this.pool.query(`INSERT INTO shield_identity.session
58
58
  (s_session_id, s_principal_id, s_tenant_id, s_status, s_issued_at_ms, s_absolute_expires_at_ms)
59
- VALUES (?, ?, ?, 'active', ?, ?)`, [input.sessionId, input.principalId, input.tenantId, input.issuedAtMs, input.absoluteExpiresAtMs]);
59
+ VALUES (?, ?, ?, 'active', ?, ?)`, [
60
+ input.sessionId,
61
+ input.principalId,
62
+ input.tenantId,
63
+ input.issuedAtMs,
64
+ input.absoluteExpiresAtMs,
65
+ ]);
60
66
  return {
61
67
  sessionId: input.sessionId,
62
68
  principalId: input.principalId,
@@ -131,6 +137,15 @@ class MariaDbSessionRecordStore {
131
137
  )`, [lineageId]);
132
138
  });
133
139
  }
140
+ /**
141
+ * The session ids a refresh lineage belongs to (normally exactly one). Read at a
142
+ * lifecycle event (after a reuse-kill) by `HotRevokingSessionRecordStore` to unbind
143
+ * those sessions in Redis — never on the per-request path.
144
+ */
145
+ async sessionIdsForLineage(lineageId) {
146
+ const rows = (await this.pool.query(`SELECT DISTINCT rt_session_id FROM shield_identity.refresh_token WHERE rt_lineage_id = ?`, [lineageId]));
147
+ return rows.map((r) => r.rt_session_id);
148
+ }
134
149
  async revokeSession(sessionId) {
135
150
  await this.withTx(async (conn) => {
136
151
  await conn.query(`UPDATE shield_identity.session SET s_status = 'revoked' WHERE s_session_id = ?`, [sessionId]);
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The ONE place the guard reads the resolved session lifetimes (ADR-0015) and derives the
3
+ * Redis binding TTLs from them (security hardening #8, plan decision D7).
4
+ *
5
+ * - {@link SHIELD_SESSION_LIFETIMES} — the resolved, validated {@link SessionLifetimeConfig}
6
+ * (core `resolveLifetimes(config.lifetimes)`). The composed `SessionAuthority`, the token
7
+ * binder and the auth controller all read this one value, so the lifetime maths is never
8
+ * duplicated or allowed to drift.
9
+ * - {@link accessBindingTtlMs} — the access-token binding TTL:
10
+ * `clamp(session.expiresAtMs − now, 1, accessTtlMs)`. Used on login AND refresh. The old
11
+ * refresh formula (`expiresAtMs − issuedAtMs`) grew with the session's age, so a token
12
+ * minted on day 10 lived ~10 days.
13
+ * - {@link ROTATE_GRACE_MS} — how long the PREVIOUS access token stays bound after a
14
+ * refresh rotates it (in-flight requests are not cut off), then it expires.
15
+ */
16
+ import type { SessionLifetimeConfig } from '@astram/core';
17
+ /** DI token for the resolved {@link SessionLifetimeConfig}. */
18
+ export declare const SHIELD_SESSION_LIFETIMES: unique symbol;
19
+ /** ADR-0015 default access-token TTL — the fallback when no lifetimes are injected. */
20
+ export declare const DEFAULT_ACCESS_TTL_MS: number;
21
+ /** Grace for the previous access token after a refresh rotates it (plan decision D7). */
22
+ export declare const ROTATE_GRACE_MS = 30000;
23
+ /**
24
+ * The access-binding TTL: the time left until the session's access expiry, never below
25
+ * 1 ms and never above the configured access TTL. A non-finite input collapses to 1 ms
26
+ * (fail closed: the binding expires immediately rather than living indefinitely).
27
+ */
28
+ export declare function accessBindingTtlMs(sessionExpiresAtMs: number, nowMs: number, accessTtlMs: number): number;
29
+ export type { SessionLifetimeConfig };
30
+ //# sourceMappingURL=session-lifetimes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-lifetimes.d.ts","sourceRoot":"","sources":["../../src/session/session-lifetimes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAE1D,+DAA+D;AAC/D,eAAO,MAAM,wBAAwB,eAAqC,CAAC;AAE3E,uFAAuF;AACvF,eAAO,MAAM,qBAAqB,QAAiB,CAAC;AAEpD,yFAAyF;AACzF,eAAO,MAAM,eAAe,QAAS,CAAC;AAEtC;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,kBAAkB,EAAE,MAAM,EAC1B,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,GAClB,MAAM,CAIR;AAED,YAAY,EAAE,qBAAqB,EAAE,CAAC"}
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ROTATE_GRACE_MS = exports.DEFAULT_ACCESS_TTL_MS = exports.SHIELD_SESSION_LIFETIMES = void 0;
4
+ exports.accessBindingTtlMs = accessBindingTtlMs;
5
+ /** DI token for the resolved {@link SessionLifetimeConfig}. */
6
+ exports.SHIELD_SESSION_LIFETIMES = Symbol('SHIELD_SESSION_LIFETIMES');
7
+ /** ADR-0015 default access-token TTL — the fallback when no lifetimes are injected. */
8
+ exports.DEFAULT_ACCESS_TTL_MS = 15 * 60 * 1000;
9
+ /** Grace for the previous access token after a refresh rotates it (plan decision D7). */
10
+ exports.ROTATE_GRACE_MS = 30_000;
11
+ /**
12
+ * The access-binding TTL: the time left until the session's access expiry, never below
13
+ * 1 ms and never above the configured access TTL. A non-finite input collapses to 1 ms
14
+ * (fail closed: the binding expires immediately rather than living indefinitely).
15
+ */
16
+ function accessBindingTtlMs(sessionExpiresAtMs, nowMs, accessTtlMs) {
17
+ const remaining = Math.floor(sessionExpiresAtMs - nowMs);
18
+ if (!Number.isFinite(remaining) || !Number.isFinite(accessTtlMs))
19
+ return 1;
20
+ return Math.min(Math.max(remaining, 1), Math.max(Math.floor(accessTtlMs), 1));
21
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Durable session restrictions (security hardening Phase 3 part B review P1, ADR-0035 §3).
3
+ *
4
+ * The refresh-time silent recovery rebuilds a lost Redis binding from DURABLE state. It must never
5
+ * mint more authority than the session had, so two facts that used to live only in Redis are read
6
+ * from MariaDB here:
7
+ * - the session's RE-ENROLL restriction (`shield_identity.session.s_must_reenroll`, identity
8
+ * migration 1710000000019): set at a recovery-code login, cleared when a fresh MFA device is
9
+ * confirmed. A restricted session rebuilds `mustReenroll: true` and never root;
10
+ * - the ACCOUNT STATUS (`shield_app.users.u_status`, the identity org table the default profile
11
+ * resolver already reads): disabling a user does not revoke its sessions, so a disabled user
12
+ * gets no rebuild.
13
+ *
14
+ * Lifecycle-time reads only (login, refresh rebuild, device confirm) — never on the request path.
15
+ * Every read error propagates: the caller fails closed.
16
+ */
17
+ import type { Pool } from 'mariadb';
18
+ /** What the refresh rebuild needs to know about a session and its principal. */
19
+ export interface SessionRebuildFacts {
20
+ /** The session was opened by a recovery-code login and is not yet re-secured. */
21
+ readonly mustReenroll: boolean;
22
+ /**
23
+ * `false` when the principal's account row exists and is not `active`. `true` when it is active
24
+ * or when no `shield_app.users` table exists (a guard-only app that keeps users elsewhere).
25
+ */
26
+ readonly accountActive: boolean;
27
+ }
28
+ export interface SessionRestrictionStore {
29
+ /** Persist the re-enroll restriction on the durable session (recovery-code login). Throws on error. */
30
+ markMustReenroll(sessionId: string): Promise<void>;
31
+ /** Lift it (a fresh device was confirmed). Throws on error. */
32
+ clearMustReenroll(sessionId: string): Promise<void>;
33
+ /** Read the restriction + account status for a rebuild. Throws on error (→ no rebuild). */
34
+ rebuildFacts(sessionId: string, principalId: string): Promise<SessionRebuildFacts>;
35
+ }
36
+ export declare class MariaDbSessionRestrictionStore implements SessionRestrictionStore {
37
+ private readonly pool;
38
+ constructor(pool: Pool);
39
+ markMustReenroll(sessionId: string): Promise<void>;
40
+ clearMustReenroll(sessionId: string): Promise<void>;
41
+ rebuildFacts(sessionId: string, principalId: string): Promise<SessionRebuildFacts>;
42
+ }
43
+ //# sourceMappingURL=session-restriction-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-restriction-store.d.ts","sourceRoot":"","sources":["../../src/session/session-restriction-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAEpC,gFAAgF;AAChF,MAAM,WAAW,mBAAmB;IAClC,iFAAiF;IACjF,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;CACjC;AAED,MAAM,WAAW,uBAAuB;IACtC,uGAAuG;IACvG,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,+DAA+D;IAC/D,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,2FAA2F;IAC3F,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;CACpF;AAKD,qBAAa,8BAA+B,YAAW,uBAAuB;IAChE,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,IAAI;IAEjC,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOlD,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOnD,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;CAqBzF"}
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MariaDbSessionRestrictionStore = void 0;
4
+ /** MariaDB `ER_NO_SUCH_TABLE`. */
5
+ const ER_NO_SUCH_TABLE = 1146;
6
+ class MariaDbSessionRestrictionStore {
7
+ pool;
8
+ constructor(pool) {
9
+ this.pool = pool;
10
+ }
11
+ async markMustReenroll(sessionId) {
12
+ await this.pool.query('UPDATE `shield_identity`.`session` SET s_must_reenroll = 1 WHERE s_session_id = ?', [sessionId]);
13
+ }
14
+ async clearMustReenroll(sessionId) {
15
+ await this.pool.query('UPDATE `shield_identity`.`session` SET s_must_reenroll = 0 WHERE s_session_id = ?', [sessionId]);
16
+ }
17
+ async rebuildFacts(sessionId, principalId) {
18
+ const rows = (await this.pool.query('SELECT s_must_reenroll FROM `shield_identity`.`session` WHERE s_session_id = ? LIMIT 1', [sessionId]));
19
+ // No row → treat as restricted (the caller also re-reads the session and refuses a missing one).
20
+ const mustReenroll = rows[0] === undefined || Number(rows[0].s_must_reenroll) !== 0;
21
+ let accountActive = true;
22
+ try {
23
+ const users = (await this.pool.query('SELECT u_status FROM `shield_app`.`users` WHERE u_uuid = ? LIMIT 1', [principalId]));
24
+ if (users[0] !== undefined)
25
+ accountActive = users[0].u_status === 'active';
26
+ }
27
+ catch (err) {
28
+ // Only an absent org table means "this app keeps no users here"; any other error fails closed.
29
+ if (err.errno !== ER_NO_SUCH_TABLE)
30
+ throw err;
31
+ }
32
+ return { mustReenroll, accountActive };
33
+ }
34
+ }
35
+ exports.MariaDbSessionRestrictionStore = MariaDbSessionRestrictionStore;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Framework authz migration — `shield_authz.service_account_key`: the HASH-ONLY store of
3
+ * service-account API keys (ADR-0033). One row = one KEY VERSION of one service account; the
4
+ * account presents `ask_<sak_account_id>_<secret>` and the engine verifies `<secret>` against
5
+ * `sak_secret_hash` (Argon2id) — the raw secret is NEVER stored, logged, or re-fetchable.
6
+ *
7
+ * This table is the physical backing of `packages/guard/src/authentication/mariadb-api-key-store.ts`
8
+ * (`MariaDbApiKeyStore`): its columns MUST match that adapter's SELECT/INSERT/UPDATE EXACTLY
9
+ * (rule 30 — the store is the source of truth). Rotation INSERTs a new `(account, version)` row;
10
+ * `getByAccountId` returns the highest version; `revoke` stamps `sak_revoked_at_ms` on all of an
11
+ * account's rows. A revoked/expired key fails closed at authenticate time.
12
+ *
13
+ * Schema `shield_authz` is created by `1710000000006-CreateAuthzSchema.cjs` — NOT recreated here.
14
+ * Idempotent (CREATE TABLE IF NOT EXISTS). No hard-coded UUIDs, no seed (keys are admin-issued).
15
+ * `down` drops the table. Run by the DDL account.
16
+ */
17
+
18
+ const SCHEMA = 'shield_authz';
19
+
20
+ async function up({ query }) {
21
+ await query(`CREATE TABLE IF NOT EXISTS \`${SCHEMA}\`.\`service_account_key\` (
22
+ sak_account_id VARCHAR(36) NOT NULL, -- the <accountId> inside the ask_ key (= service_account.sa_id)
23
+ sak_version INT NOT NULL, -- monotonic per account; rotation bumps it
24
+ sak_tenant_id VARCHAR(36) NOT NULL, -- server-stamped; tenant the key is bound to
25
+ sak_scope_subtree JSON NOT NULL, -- reachable node ids for the key's Principal (least privilege)
26
+ sak_role_ids JSON NOT NULL, -- roles the key's Principal holds
27
+ sak_secret_hash VARCHAR(255) NOT NULL, -- Argon2id hash of the secret — NEVER the plaintext
28
+ sak_expires_at_ms BIGINT NULL, -- absolute expiry (epoch-ms); NULL = non-expiring (discouraged)
29
+ sak_revoked_at_ms BIGINT NULL, -- set on revoke (epoch-ms); a revoked key fails closed
30
+ sak_created_at_ms BIGINT NOT NULL, -- issue time (epoch-ms)
31
+ PRIMARY KEY (sak_account_id, sak_version),
32
+ INDEX idx_sak_account (sak_account_id),
33
+ INDEX idx_sak_tenant (sak_tenant_id)
34
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`);
35
+ }
36
+
37
+ async function down({ query }) {
38
+ await query(`DROP TABLE IF EXISTS \`${SCHEMA}\`.\`service_account_key\``);
39
+ }
40
+
41
+ module.exports = { up, down };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astram/guard",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "license": "MIT",
5
5
  "description": "Astram framework — the enforcement guard + real-world adapters (the 'hands'). One global deny-by-default gate every request passes through (ADR-0002), classification decorators, boot-pool assertion (ADR-0018), the forRoot() registration seam, and the DB/Redis/crypto adapters that connect the sealed core to a real backend. Depends on @astram/core (ADR-0003, ADR-0023).",
6
6
  "type": "commonjs",
@@ -24,8 +24,8 @@
24
24
  "argon2": "^0.41.1",
25
25
  "ioredis": "^5.4.1",
26
26
  "mariadb": "^3.4.0",
27
- "@astram/engine": "0.2.0",
28
- "@astram/core": "1.2.0"
27
+ "@astram/core": "1.4.0",
28
+ "@astram/engine": "0.4.0"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "@nestjs/common": "^11",