@astram/guard 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/dist/auth/account.controller.d.ts +245 -0
  2. package/dist/auth/account.controller.d.ts.map +1 -0
  3. package/dist/auth/account.controller.js +673 -0
  4. package/dist/auth/auth.controller.d.ts +219 -5
  5. package/dist/auth/auth.controller.d.ts.map +1 -1
  6. package/dist/auth/auth.controller.js +790 -12
  7. package/dist/auth/auth.module.d.ts +101 -3
  8. package/dist/auth/auth.module.d.ts.map +1 -1
  9. package/dist/auth/auth.module.js +300 -8
  10. package/dist/auth/dto.d.ts +16 -6
  11. package/dist/auth/dto.d.ts.map +1 -1
  12. package/dist/auth/ports/audit-sink.d.ts +47 -0
  13. package/dist/auth/ports/audit-sink.d.ts.map +1 -0
  14. package/dist/auth/ports/audit-sink.js +20 -0
  15. package/dist/auth/ports/credential-writer.d.ts +23 -0
  16. package/dist/auth/ports/credential-writer.d.ts.map +1 -0
  17. package/dist/auth/ports/credential-writer.js +2 -0
  18. package/dist/auth/ports/email-sender.d.ts +14 -0
  19. package/dist/auth/ports/email-sender.d.ts.map +1 -0
  20. package/dist/auth/ports/email-sender.js +2 -0
  21. package/dist/auth/ports/mfa-device-store.d.ts +45 -0
  22. package/dist/auth/ports/mfa-device-store.d.ts.map +1 -0
  23. package/dist/auth/ports/mfa-device-store.js +13 -0
  24. package/dist/auth/ports/profile-writer.d.ts +12 -0
  25. package/dist/auth/ports/profile-writer.d.ts.map +1 -0
  26. package/dist/auth/ports/profile-writer.js +2 -0
  27. package/dist/auth/ports/recovery-code-store.d.ts +10 -0
  28. package/dist/auth/ports/recovery-code-store.d.ts.map +1 -0
  29. package/dist/auth/ports/recovery-code-store.js +2 -0
  30. package/dist/auth/ports/reset-token-store.d.ts +12 -0
  31. package/dist/auth/ports/reset-token-store.d.ts.map +1 -0
  32. package/dist/auth/ports/reset-token-store.js +2 -0
  33. package/dist/auth/ports/session-facts-projector.d.ts +44 -0
  34. package/dist/auth/ports/session-facts-projector.d.ts.map +1 -0
  35. package/dist/auth/ports/session-facts-projector.js +30 -0
  36. package/dist/auth/ports/session-sweeper.d.ts +12 -0
  37. package/dist/auth/ports/session-sweeper.d.ts.map +1 -0
  38. package/dist/auth/ports/session-sweeper.js +2 -0
  39. package/dist/auth/profile-resolver.d.ts +73 -0
  40. package/dist/auth/profile-resolver.d.ts.map +1 -0
  41. package/dist/auth/profile-resolver.js +28 -0
  42. package/dist/auth/rate-limit/attempt-limiter.d.ts +91 -0
  43. package/dist/auth/rate-limit/attempt-limiter.d.ts.map +1 -0
  44. package/dist/auth/rate-limit/attempt-limiter.js +133 -0
  45. package/dist/auth/tokens.d.ts +174 -0
  46. package/dist/auth/tokens.d.ts.map +1 -1
  47. package/dist/auth/tokens.js +190 -1
  48. package/dist/auth/totp-enrollment.d.ts +42 -0
  49. package/dist/auth/totp-enrollment.d.ts.map +1 -0
  50. package/dist/auth/totp-enrollment.js +96 -0
  51. package/dist/authentication/mariadb-credential-resolver.d.ts +38 -24
  52. package/dist/authentication/mariadb-credential-resolver.d.ts.map +1 -1
  53. package/dist/authentication/mariadb-credential-resolver.js +42 -28
  54. package/dist/authz/cross-tenant-grant-writer.d.ts +259 -0
  55. package/dist/authz/cross-tenant-grant-writer.d.ts.map +1 -0
  56. package/dist/authz/cross-tenant-grant-writer.js +397 -0
  57. package/dist/authz/manifest-operation-authority-adapter.d.ts +65 -0
  58. package/dist/authz/manifest-operation-authority-adapter.d.ts.map +1 -0
  59. package/dist/authz/manifest-operation-authority-adapter.js +108 -0
  60. package/dist/authz/mariadb-cross-tenant-grant-adapter.d.ts +90 -0
  61. package/dist/authz/mariadb-cross-tenant-grant-adapter.d.ts.map +1 -0
  62. package/dist/authz/mariadb-cross-tenant-grant-adapter.js +211 -0
  63. package/dist/authz/mariadb-record-rule-adapter.d.ts +60 -0
  64. package/dist/authz/mariadb-record-rule-adapter.d.ts.map +1 -0
  65. package/dist/authz/mariadb-record-rule-adapter.js +154 -0
  66. package/dist/authz/mariadb-record-share-adapter.d.ts +75 -0
  67. package/dist/authz/mariadb-record-share-adapter.d.ts.map +1 -0
  68. package/dist/authz/mariadb-record-share-adapter.js +106 -0
  69. package/dist/authz/record-share-cascade.d.ts +52 -0
  70. package/dist/authz/record-share-cascade.d.ts.map +1 -0
  71. package/dist/authz/record-share-cascade.js +49 -0
  72. package/dist/authz/record-share-writer.d.ts +175 -0
  73. package/dist/authz/record-share-writer.d.ts.map +1 -0
  74. package/dist/authz/record-share-writer.js +225 -0
  75. package/dist/authz/wire-record-authorization.d.ts +66 -0
  76. package/dist/authz/wire-record-authorization.d.ts.map +1 -0
  77. package/dist/authz/wire-record-authorization.js +34 -0
  78. package/dist/completeness/catalog-reconciler.d.ts +21 -0
  79. package/dist/completeness/catalog-reconciler.d.ts.map +1 -0
  80. package/dist/completeness/catalog-reconciler.js +74 -0
  81. package/dist/completeness/operation-inventory-scanner.d.ts +17 -0
  82. package/dist/completeness/operation-inventory-scanner.d.ts.map +1 -0
  83. package/dist/completeness/operation-inventory-scanner.js +79 -0
  84. package/dist/completeness/ports/catalog-store.d.ts +33 -0
  85. package/dist/completeness/ports/catalog-store.d.ts.map +1 -0
  86. package/dist/completeness/ports/catalog-store.js +2 -0
  87. package/dist/config/app-config.d.ts +18 -16
  88. package/dist/config/app-config.d.ts.map +1 -1
  89. package/dist/config/app-config.js +68 -35
  90. package/dist/enforcement/classification.decorator.d.ts +33 -14
  91. package/dist/enforcement/classification.decorator.d.ts.map +1 -1
  92. package/dist/enforcement/classification.decorator.js +34 -14
  93. package/dist/enforcement/global-enforcement.guard.d.ts +29 -19
  94. package/dist/enforcement/global-enforcement.guard.d.ts.map +1 -1
  95. package/dist/enforcement/global-enforcement.guard.js +108 -25
  96. package/dist/enforcement/reenroll-allowlist.d.ts +49 -0
  97. package/dist/enforcement/reenroll-allowlist.d.ts.map +1 -0
  98. package/dist/enforcement/reenroll-allowlist.js +77 -0
  99. package/dist/index.d.ts +35 -3
  100. package/dist/index.d.ts.map +1 -1
  101. package/dist/index.js +159 -7
  102. package/dist/module/shield-tokens.d.ts +15 -0
  103. package/dist/module/shield-tokens.d.ts.map +1 -0
  104. package/dist/module/shield-tokens.js +17 -0
  105. package/dist/module/shield.module.d.ts +154 -10
  106. package/dist/module/shield.module.d.ts.map +1 -1
  107. package/dist/module/shield.module.js +181 -24
  108. package/dist/request-auth/request-principal.d.ts +42 -0
  109. package/dist/request-auth/request-principal.d.ts.map +1 -0
  110. package/dist/request-auth/request-principal.js +87 -0
  111. package/dist/request-auth/session-facts-als.d.ts +65 -0
  112. package/dist/request-auth/session-facts-als.d.ts.map +1 -0
  113. package/dist/request-auth/session-facts-als.js +57 -0
  114. package/dist/request-auth/session-token-binder.d.ts +130 -0
  115. package/dist/request-auth/session-token-binder.d.ts.map +1 -0
  116. package/dist/request-auth/session-token-binder.js +277 -0
  117. package/dist/request-auth/tokens.d.ts +14 -0
  118. package/dist/request-auth/tokens.d.ts.map +1 -0
  119. package/dist/request-auth/tokens.js +16 -0
  120. package/package.json +2 -2
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ /**
3
+ * AuditSink — the auth-surface **audit seam** (spec §6.2). The auth surface is
4
+ * otherwise un-audited (only the @astram/engine pipe audits record CRUD); this port
5
+ * introduces the FIRST auth audit event. The framework builds the event; the consumer
6
+ * persists it (astram3 writes `shield_audit.audit_log`).
7
+ *
8
+ * OPTIONAL, consumer-plugged through `ShieldModule.forRoot({ auditSink })` and threaded
9
+ * to `AuthModule.forRoot` exactly like the other account/credential seams. Absent
10
+ * (`null`) → the route still works, just un-audited (the `@Optional()` injection in
11
+ * `AuthController` resolves to no seam → the best-effort call is a no-op).
12
+ *
13
+ * BEST-EFFORT by contract: `record` is invoked fire-and-forget on the recovery-login
14
+ * success path — a throwing or slow sink NEVER fails the (legitimate) login. The
15
+ * consumer's impl should not surface errors into the response path.
16
+ *
17
+ * NO SECRET LEAK: the event carries NO password, recovery code, TOTP secret, or token —
18
+ * only the actor id, tenant, action key, and non-secret attributes.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,23 @@
1
+ /**
2
+ * CredentialWriter — the seam that WRITES a principal's password credential. The
3
+ * framework never owns the credential store; the consumer plugs this (like
4
+ * CredentialResolver, but the write side). Supplied via
5
+ * ShieldModule.forRoot({ credentialWriter }). Absent → the routes that need it
6
+ * return 501. Writes HASHES only — never plaintext.
7
+ *
8
+ * TOTP secrets are NOT written here — the multi-device MFA model stores
9
+ * every authenticator secret in the `MfaDeviceStore` (one storage location, one code
10
+ * path). So this seam is now password-only: `setPassword` + `getPasswordHash`.
11
+ */
12
+ export interface CredentialWriter {
13
+ /** Set the principal's password to an already-hashed value. */
14
+ setPassword(principalId: string, passwordHash: string): Promise<void>;
15
+ /**
16
+ * Read the stored password hash by principal id — needed by change-password, which
17
+ * holds the principal id (from the token), whereas CredentialResolver keys by
18
+ * identityHint. Null if the principal is unknown. (This is the ONE read on the
19
+ * writer seam; see Task 5 for why it lives here rather than a separate seam.)
20
+ */
21
+ getPasswordHash(principalId: string): Promise<string | null>;
22
+ }
23
+ //# sourceMappingURL=credential-writer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential-writer.d.ts","sourceRoot":"","sources":["../../../src/auth/ports/credential-writer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,WAAW,gBAAgB;IAC/B,+DAA+D;IAC/D,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtE;;;;;OAKG;IACH,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC9D"}
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * EmailSender — the delivery seam. The framework builds the message; the consumer
3
+ * delivers it (SendGrid, SMTP, or a test capturer). Consumer-plugged.
4
+ */
5
+ export interface EmailMessage {
6
+ readonly to: string;
7
+ readonly subject: string;
8
+ readonly html: string;
9
+ readonly text: string;
10
+ }
11
+ export interface EmailSender {
12
+ send(message: EmailMessage): Promise<void>;
13
+ }
14
+ //# sourceMappingURL=email-sender.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email-sender.d.ts","sourceRoot":"","sources":["../../../src/auth/ports/email-sender.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AACD,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5C"}
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,45 @@
1
+ /**
2
+ * MfaDeviceStore — the seam that persists a principal's TOTP authenticator DEVICES
3
+ * (multi-device MFA). The framework never owns the store; the consumer
4
+ * plugs this (like {@link CredentialWriter}, but for the N-device MFA model).
5
+ * Each device has its OWN base32 secret; login matches ANY confirmed device (the
6
+ * OR-match lives in the consumer resolver + a custom TOTP verifier, NOT here — this
7
+ * port is pure persistence). Supplied via `ShieldModule.forRoot({ mfaDeviceStore })`.
8
+ * Absent (`null`) → the device routes return 501. A device's `secret` is NEVER
9
+ * returned to a client (only {@link listConfirmedSecrets} for the login bundle,
10
+ * server-side) — the display shape {@link MfaDevice} deliberately omits it.
11
+ */
12
+ /** The DISPLAY shape of one MFA device (NO secret — never leaves the server). */
13
+ export interface MfaDevice {
14
+ readonly id: string;
15
+ readonly name: string;
16
+ readonly createdAtMs: number;
17
+ readonly lastUsedAtMs: number | null;
18
+ readonly confirmed: boolean;
19
+ }
20
+ export interface MfaDeviceStore {
21
+ /** All of the principal's devices incl. pending (internal/debug; routes filter to confirmed for display). */
22
+ listAll(principalId: string): Promise<MfaDevice[]>;
23
+ /** Confirmed devices' base32 secrets — for the login requiredFactor bundle + verify. */
24
+ listConfirmedSecrets(principalId: string): Promise<string[]>;
25
+ /**
26
+ * The base32 secret of a SINGLE device the principal owns (confirmed OR pending), or
27
+ * `null` if not found / not owned. Used ONLY server-side by `confirm` to check the
28
+ * presented code against the pending device's own secret — the secret is NEVER
29
+ * returned to a client (the display {@link MfaDevice} deliberately omits it).
30
+ */
31
+ getSecret(principalId: string, deviceId: string): Promise<string | null>;
32
+ /** Create a PENDING device (own id); prunes the caller's stale pending rows first. Returns the deviceId. */
33
+ begin(principalId: string, name: string, secret: string): Promise<string>;
34
+ /** Activate a pending device (confirmed). False if not found / not owned / not pending. */
35
+ confirm(principalId: string, deviceId: string): Promise<boolean>;
36
+ /** Rename a device. Returns the updated device, or null if not found / not owned. */
37
+ rename(principalId: string, deviceId: string, name: string): Promise<MfaDevice | null>;
38
+ /** Delete a device. False if not found / not owned. */
39
+ remove(principalId: string, deviceId: string): Promise<boolean>;
40
+ /** Count of CONFIRMED devices (drives "first device" / "mfa enabled"). */
41
+ countConfirmed(principalId: string): Promise<number>;
42
+ /** Best-effort: stamp last-used for the device whose secret verified (match by unique secret). */
43
+ stampUsed(secret: string): Promise<void>;
44
+ }
45
+ //# sourceMappingURL=mfa-device-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mfa-device-store.d.ts","sourceRoot":"","sources":["../../../src/auth/ports/mfa-device-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,iFAAiF;AACjF,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B,6GAA6G;IAC7G,OAAO,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IACnD,wFAAwF;IACxF,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC7D;;;;;OAKG;IACH,SAAS,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzE,4GAA4G;IAC5G,KAAK,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1E,2FAA2F;IAC3F,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjE,qFAAqF;IACrF,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IACvF,uDAAuD;IACvD,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAChE,0EAA0E;IAC1E,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,kGAAkG;IAClG,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C"}
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ /**
3
+ * MfaDeviceStore — the seam that persists a principal's TOTP authenticator DEVICES
4
+ * (multi-device MFA). The framework never owns the store; the consumer
5
+ * plugs this (like {@link CredentialWriter}, but for the N-device MFA model).
6
+ * Each device has its OWN base32 secret; login matches ANY confirmed device (the
7
+ * OR-match lives in the consumer resolver + a custom TOTP verifier, NOT here — this
8
+ * port is pure persistence). Supplied via `ShieldModule.forRoot({ mfaDeviceStore })`.
9
+ * Absent (`null`) → the device routes return 501. A device's `secret` is NEVER
10
+ * returned to a client (only {@link listConfirmedSecrets} for the login bundle,
11
+ * server-side) — the display shape {@link MfaDevice} deliberately omits it.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,12 @@
1
+ import type { CurrentUserProfile } from '../profile-resolver.js';
2
+ /**
3
+ * ProfileWriter — self-service profile edit (name/email). Returns the refreshed
4
+ * display profile (names, never UUIDs). Consumer-plugged.
5
+ */
6
+ export interface ProfileWriter {
7
+ update(principalId: string, changes: {
8
+ name?: string;
9
+ email?: string;
10
+ }): Promise<CurrentUserProfile>;
11
+ }
12
+ //# sourceMappingURL=profile-writer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile-writer.d.ts","sourceRoot":"","sources":["../../../src/auth/ports/profile-writer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEjE;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,MAAM,CACJ,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GACzC,OAAO,CAAC,kBAAkB,CAAC,CAAC;CAChC"}
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,10 @@
1
+ /**
2
+ * RecoveryCodeStore — persists Argon2-hashed one-time MFA recovery codes.
3
+ * `replaceAll` wipes prior codes on (re-)enroll. `consume` returns true iff a
4
+ * matching unconsumed code exists, then marks it consumed (single-use).
5
+ */
6
+ export interface RecoveryCodeStore {
7
+ replaceAll(principalId: string, codeHashes: readonly string[]): Promise<void>;
8
+ consume(principalId: string, rawCode: string): Promise<boolean>;
9
+ }
10
+ //# sourceMappingURL=recovery-code-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recovery-code-store.d.ts","sourceRoot":"","sources":["../../../src/auth/ports/recovery-code-store.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9E,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACjE"}
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * ResetTokenStore — persists single-use, TTL'd password-reset tokens. The framework
3
+ * hashes the raw token (SHA-256) before it reaches this store; the store never sees
4
+ * the raw token. `consume` returns the principalId iff the token is unexpired AND
5
+ * unconsumed, then marks it consumed (single-use). Consumer-plugged.
6
+ */
7
+ export interface ResetTokenStore {
8
+ issue(principalId: string, tokenHash: string, expiresAtMs: number): Promise<void>;
9
+ consume(tokenHash: string): Promise<string | null>;
10
+ invalidateAllForPrincipal(principalId: string): Promise<void>;
11
+ }
12
+ //# sourceMappingURL=reset-token-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reset-token-store.d.ts","sourceRoot":"","sources":["../../../src/auth/ports/reset-token-store.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClF,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACnD,yBAAyB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/D"}
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The `SessionFactsProjector` PORT (ADR-0011 eager projection) — the OPT-IN seam the
3
+ * auth surface uses to compute a principal's coarse grants at LOGIN / REFRESH time so
4
+ * they can be cached on the session binding, letting gate 5 serve them from the
5
+ * request's `cachedFacts` instead of a DB reload on every request.
6
+ *
7
+ * ── WHY A DEDICATED PORT (not the core `PermissionFactsPort` directly) ────────────
8
+ * The core `PermissionFactsPort` is an authorization-engine dep wired into the record
9
+ * decision authority (`recordAuthorization`), not something AuthModule injects. Rather
10
+ * than couple the auth controller to that engine wiring, the consumer supplies a thin
11
+ * `SessionFactsProjector` — which it typically implements by delegating to the SAME
12
+ * `DbPermissionFactsAdapter.loadCoarseGrants` it already hands the decision authority.
13
+ * One tiny interface, one clean seam, no engine coupling in AuthModule.
14
+ *
15
+ * ── OPT-IN / ADDITIVE ────────────────────────────────────────────────────────────
16
+ * ABSENT (the default): the auth controller omits `cachedFacts` when binding the
17
+ * session → gate 5 sees no cache → DB reload every request = today's exact behavior.
18
+ * PRESENT: the controller calls `project(...)` at login + refresh and stores the
19
+ * returned {@link CoarseGrants} on the binding, so subsequent requests hit the cache.
20
+ * Turning it on NEVER changes a decision — it only removes a DB round-trip when the
21
+ * cached version is fresh; a stale/absent cache still reloads (gate 5's version gate).
22
+ *
23
+ * ── FAIL-CLOSED ──────────────────────────────────────────────────────────────────
24
+ * `project(...)` returning `null` (or throwing — the caller swallows it best-effort)
25
+ * means "no facts cached" → the session binds WITHOUT `cachedFacts` → gate 5 DB-reloads.
26
+ * A projection failure therefore degrades safely to the correct (un-cached) behavior;
27
+ * it never fabricates or omits a grant that would change a decision.
28
+ */
29
+ import type { CoarseGrants } from '@astram/core';
30
+ export interface SessionFactsProjector {
31
+ /**
32
+ * Project the coarse feature/object-type grants for `principalId` in `tenantId` at
33
+ * login/refresh time (ADR-0011). Returns the {@link CoarseGrants} to cache on the
34
+ * session binding, or `null` when nothing should be cached (→ gate 5 DB-reloads).
35
+ *
36
+ * A principal with NO grants SHOULD return a {@link CoarseGrants} with an empty
37
+ * `grantedPermissionKeys` (tagged with the current version) — that is a legitimate
38
+ * "genuinely no grants" cache, distinct from `null` ("don't cache"). Implementations
39
+ * typically delegate to the same `PermissionFactsPort.loadCoarseGrants` the decision
40
+ * authority uses.
41
+ */
42
+ project(principalId: string, tenantId: string): Promise<CoarseGrants | null>;
43
+ }
44
+ //# sourceMappingURL=session-facts-projector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-facts-projector.d.ts","sourceRoot":"","sources":["../../../src/auth/ports/session-facts-projector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD,MAAM,WAAW,qBAAqB;IACpC;;;;;;;;;;OAUG;IACH,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;CAC9E"}
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ /**
3
+ * The `SessionFactsProjector` PORT (ADR-0011 eager projection) — the OPT-IN seam the
4
+ * auth surface uses to compute a principal's coarse grants at LOGIN / REFRESH time so
5
+ * they can be cached on the session binding, letting gate 5 serve them from the
6
+ * request's `cachedFacts` instead of a DB reload on every request.
7
+ *
8
+ * ── WHY A DEDICATED PORT (not the core `PermissionFactsPort` directly) ────────────
9
+ * The core `PermissionFactsPort` is an authorization-engine dep wired into the record
10
+ * decision authority (`recordAuthorization`), not something AuthModule injects. Rather
11
+ * than couple the auth controller to that engine wiring, the consumer supplies a thin
12
+ * `SessionFactsProjector` — which it typically implements by delegating to the SAME
13
+ * `DbPermissionFactsAdapter.loadCoarseGrants` it already hands the decision authority.
14
+ * One tiny interface, one clean seam, no engine coupling in AuthModule.
15
+ *
16
+ * ── OPT-IN / ADDITIVE ────────────────────────────────────────────────────────────
17
+ * ABSENT (the default): the auth controller omits `cachedFacts` when binding the
18
+ * session → gate 5 sees no cache → DB reload every request = today's exact behavior.
19
+ * PRESENT: the controller calls `project(...)` at login + refresh and stores the
20
+ * returned {@link CoarseGrants} on the binding, so subsequent requests hit the cache.
21
+ * Turning it on NEVER changes a decision — it only removes a DB round-trip when the
22
+ * cached version is fresh; a stale/absent cache still reloads (gate 5's version gate).
23
+ *
24
+ * ── FAIL-CLOSED ──────────────────────────────────────────────────────────────────
25
+ * `project(...)` returning `null` (or throwing — the caller swallows it best-effort)
26
+ * means "no facts cached" → the session binds WITHOUT `cachedFacts` → gate 5 DB-reloads.
27
+ * A projection failure therefore degrades safely to the correct (un-cached) behavior;
28
+ * it never fabricates or omits a grant that would change a decision.
29
+ */
30
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * SessionSweeper — revokes a principal's sessions after a credential change. The
3
+ * core has no revoke-all-for-principal, so the consumer implements this over its
4
+ * session store (there is an s_principal_id column + index). Consumer-plugged.
5
+ */
6
+ export interface SessionSweeper {
7
+ /** Revoke ALL of the principal's sessions (used after a password reset). */
8
+ revokeAllForPrincipal(principalId: string): Promise<void>;
9
+ /** Revoke all EXCEPT keepSessionId (used after an authenticated change-password). */
10
+ revokeOthersForPrincipal(principalId: string, keepSessionId: string): Promise<void>;
11
+ }
12
+ //# sourceMappingURL=session-sweeper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-sweeper.d.ts","sourceRoot":"","sources":["../../../src/auth/ports/session-sweeper.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,4EAA4E;IAC5E,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,qFAAqF;IACrF,wBAAwB,CAAC,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrF"}
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,73 @@
1
+ /**
2
+ * ProfileResolver — the seam that turns an authenticated `Principal` into the
3
+ * signed-in user's **display profile** for `GET /auth/me` (the current-user
4
+ * endpoint). This is the fill for the flagged gap: login/session return only IDs
5
+ * (`principalId` / `tenantId`), so a UI has no human-readable name/company/role to
6
+ * show without leaking a raw UUID.
7
+ *
8
+ * WHY THIS IS A SEAM (not baked into the framework): the framework owns the
9
+ * CREDENTIAL store (`shield_identity`) but is deliberately AGNOSTIC to the app's
10
+ * ORG model — how users, companies (tenants), and roles are named lives in the
11
+ * consumer's own schema (astram3's `shield_app` tables, another app's users/roles
12
+ * model, etc.). So the framework opens THIS port and the consumer implements it,
13
+ * exactly like {@link CredentialResolver}. Zero core changes; the framework never
14
+ * hard-codes a table or a display-column.
15
+ *
16
+ * SECURITY / CONTRACT:
17
+ * - Called ONLY after the request-auth layer has resolved a live bearer token to a
18
+ * `Principal` (so it always acts as an authenticated caller — never anonymous).
19
+ * - Returns display NAMES, never raw UUIDs (the never-show-UUIDs rule): the
20
+ * consumer resolves `principal.id` → the user's name/email, `principal.tenantId`
21
+ * → the company's display name, `principal.roleIds` → the roles' names.
22
+ * - `null` = the principal has no resolvable profile (e.g. the user row was
23
+ * deleted since login) — the endpoint returns 404-style "no profile", never the
24
+ * ids. A store OUTAGE should THROW (fail-closed) — the endpoint maps it to a
25
+ * generic error, not an empty profile.
26
+ */
27
+ import type { Principal } from '@astram/core';
28
+ /** A role as shown to the user — the display name, plus its id for the UI's keys. */
29
+ export interface ProfileRole {
30
+ /** The role's stable id (safe in `key=`/`data-*`, never rendered as text). */
31
+ readonly id: string;
32
+ /** The role's human-readable display name (what the UI renders). */
33
+ readonly name: string;
34
+ }
35
+ /** The signed-in user's tenant (company) as shown to the user. */
36
+ export interface ProfileTenant {
37
+ /** The tenant/company id (safe in machine contexts, never rendered as text). */
38
+ readonly id: string;
39
+ /** The company's human-readable display name (what the UI renders). */
40
+ readonly name: string;
41
+ }
42
+ /**
43
+ * The current-user display profile returned by `GET /auth/me`. Everything here is
44
+ * SAFE to render — names, not UUIDs. The bare `id` fields are for the UI's internal
45
+ * keys/routing only (the never-show-UUIDs rule allows ids in machine contexts).
46
+ */
47
+ export interface CurrentUserProfile {
48
+ /** The user's id (machine use only — routing/keys, never rendered as text). */
49
+ readonly id: string;
50
+ /** The user's display name (what the header/card renders). */
51
+ readonly name: string;
52
+ /** The user's email/username, if the app exposes one (else omit). */
53
+ readonly email?: string;
54
+ /** The active tenant/company (the boundary the session is scoped to). */
55
+ readonly company: ProfileTenant;
56
+ /** The roles the user holds in the active tenant (display names). */
57
+ readonly roles: readonly ProfileRole[];
58
+ }
59
+ /**
60
+ * The port a consumer implements to back `GET /auth/me`. Supplied to the framework
61
+ * via `ShieldModule.forRoot({ profileResolver })`. Framework-agnostic: the framework
62
+ * never reads the app's org tables — it calls this seam.
63
+ */
64
+ export interface ProfileResolver {
65
+ /**
66
+ * Resolve an authenticated `Principal` → the display profile, or `null` when no
67
+ * profile exists for this principal (deleted user, etc.). MUST return display
68
+ * NAMES (never raw UUIDs) for `name`/`company.name`/`roles[].name`. A store outage
69
+ * should THROW (fail-closed) rather than return a partial/empty profile.
70
+ */
71
+ resolve(principal: Principal): Promise<CurrentUserProfile | null>;
72
+ }
73
+ //# sourceMappingURL=profile-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile-resolver.d.ts","sourceRoot":"","sources":["../../src/auth/profile-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,qFAAqF;AACrF,MAAM,WAAW,WAAW;IAC1B,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,kEAAkE;AAClE,MAAM,WAAW,aAAa;IAC5B,gFAAgF;IAChF,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,qEAAqE;IACrE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,yEAAyE;IACzE,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,qEAAqE;IACrE,QAAQ,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,CAAC;CACxC;AAED;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;;OAKG;IACH,OAAO,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC;CACnE"}
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ /**
3
+ * ProfileResolver — the seam that turns an authenticated `Principal` into the
4
+ * signed-in user's **display profile** for `GET /auth/me` (the current-user
5
+ * endpoint). This is the fill for the flagged gap: login/session return only IDs
6
+ * (`principalId` / `tenantId`), so a UI has no human-readable name/company/role to
7
+ * show without leaking a raw UUID.
8
+ *
9
+ * WHY THIS IS A SEAM (not baked into the framework): the framework owns the
10
+ * CREDENTIAL store (`shield_identity`) but is deliberately AGNOSTIC to the app's
11
+ * ORG model — how users, companies (tenants), and roles are named lives in the
12
+ * consumer's own schema (astram3's `shield_app` tables, another app's users/roles
13
+ * model, etc.). So the framework opens THIS port and the consumer implements it,
14
+ * exactly like {@link CredentialResolver}. Zero core changes; the framework never
15
+ * hard-codes a table or a display-column.
16
+ *
17
+ * SECURITY / CONTRACT:
18
+ * - Called ONLY after the request-auth layer has resolved a live bearer token to a
19
+ * `Principal` (so it always acts as an authenticated caller — never anonymous).
20
+ * - Returns display NAMES, never raw UUIDs (the never-show-UUIDs rule): the
21
+ * consumer resolves `principal.id` → the user's name/email, `principal.tenantId`
22
+ * → the company's display name, `principal.roleIds` → the roles' names.
23
+ * - `null` = the principal has no resolvable profile (e.g. the user row was
24
+ * deleted since login) — the endpoint returns 404-style "no profile", never the
25
+ * ids. A store OUTAGE should THROW (fail-closed) — the endpoint maps it to a
26
+ * generic error, not an empty profile.
27
+ */
28
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,91 @@
1
+ /**
2
+ * `AttemptLimiter` — a guard-owned, Redis-backed sliding-window attempt counter for
3
+ * anti-brute-force on the auth surface (spec §5). ONE class, used at TWO call sites
4
+ * with independent thresholds: `/auth/login` (H1 password-login lockout, default
5
+ * 10/15min, keyed `login:<identityHint>`) and the `/auth/mfa` recovery branch
6
+ * (default 5/15min, keyed `recovery:<principalId>`).
7
+ *
8
+ * ── CONTRACT ─────────────────────────────────────────────────────────────────
9
+ * check(key) → `true` if the caller is UNDER the threshold (allowed);
10
+ * `false` if AT/over it (locked out for the window).
11
+ * recordFailure(key) → increment the counter; on the FIRST failure in a window,
12
+ * set the window TTL (`PEXPIRE windowMs`) so the counter
13
+ * auto-expires (a sliding window that resets on inactivity).
14
+ * RETURNS the new count so a caller can detect the LOCKOUT
15
+ * TRANSITION (count === maxAttempts is the exact edge where
16
+ * the NEXT check() flips to `false`) and act ONCE on it — e.g.
17
+ * emit a single lockout audit event, never one per attempt
18
+ * (feature #140's audit-on-the-edge, anti-flood). On a Redis
19
+ * best-effort miss it returns `0` (an unknown count → the
20
+ * caller simply doesn't treat the miss as a transition).
21
+ * reset(key) → clear the counter (a SUCCESS resets — a legitimate user is
22
+ * never penalized for their earlier fat-fingers).
23
+ *
24
+ * ── FAIL-OPEN-WITH-LOG (spec §5, the deliberate posture) ──────────────────────
25
+ * A limiter differs from the credential path: a Redis outage must NOT lock out
26
+ * every legitimate login/recovery. So `check()` FAILS OPEN on a Redis error —
27
+ * it returns `true` (allowed) and LOGS the blip. This never grants access on its
28
+ * own (the password / recovery code still gates); it only transiently LOOSENS the
29
+ * brute-force ceiling. `recordFailure`/`reset` are best-effort and swallow (a
30
+ * count we couldn't write just means the window is a touch more lenient — never a
31
+ * thrown error into the request path).
32
+ *
33
+ * ── ANTI-ENUMERATION ── the limiter itself is silent; the caller maps a `false`
34
+ * from `check()` to the SAME generic 401 as a wrong password / wrong code, so a
35
+ * locked-out account is indistinguishable from a bad credential.
36
+ *
37
+ * ADR-0023: guard owns the infra adapter (ioredis); the class is a thin, injectable
38
+ * collaborator the controller consumes via a DI token (`SHIELD_LOGIN_LIMITER` /
39
+ * `SHIELD_RECOVERY_LIMITER`), each provided with the live Redis + its config.
40
+ */
41
+ import type Redis from 'ioredis';
42
+ /** Per-limiter tuning: the failure ceiling and the sliding-window length (ms). */
43
+ export interface AttemptLimiterConfig {
44
+ /** Number of failures within the window that trips the lockout (check → false). */
45
+ readonly maxAttempts: number;
46
+ /** The window length in ms; the counter auto-expires after this idle span. */
47
+ readonly windowMs: number;
48
+ }
49
+ export declare class AttemptLimiter {
50
+ private readonly redis;
51
+ private readonly cfg;
52
+ /** One shared logger — the fail-open path warns so a Redis outage is observable. */
53
+ private static readonly logger;
54
+ constructor(redis: Redis, cfg: AttemptLimiterConfig);
55
+ /**
56
+ * The failure ceiling for this limiter (read-only). A caller uses it to detect the
57
+ * lockout TRANSITION: {@link recordFailure} returns the new count, and the caller
58
+ * acts ONCE when that count first equals `maxAttempts` (the edge where the next
59
+ * {@link check} flips to `false`) — the audit-on-the-edge / anti-flood contract.
60
+ */
61
+ get maxAttempts(): number;
62
+ /** Namespace every key so the two limiters + other Redis users never collide. */
63
+ private k;
64
+ /**
65
+ * `true` iff the caller is UNDER the threshold (allowed). FAILS OPEN on a Redis
66
+ * error — returns `true` and logs — so a Redis blip never locks out every login
67
+ * (spec §5). The credential/recovery check still gates access; this only bounds
68
+ * brute-force.
69
+ */
70
+ check(key: string): Promise<boolean>;
71
+ /**
72
+ * Increment the failure counter; set the window TTL on the FIRST failure so the
73
+ * counter auto-expires after `windowMs` of inactivity. Best-effort — a write we
74
+ * can't land just makes the window slightly more lenient (never a thrown error).
75
+ *
76
+ * RETURNS the new failure count so the caller can detect the lockout TRANSITION
77
+ * (count === {@link maxAttempts} is the exact edge where the next {@link check}
78
+ * flips to `false`) and act on it exactly ONCE — feature #140's audit-on-the-edge
79
+ * (never one audit event per attempt past the threshold). On a Redis best-effort
80
+ * miss it returns `0`: an unknown count the caller must NOT treat as a transition
81
+ * (so a Redis blip never spuriously fires — or suppresses — the lockout audit).
82
+ */
83
+ recordFailure(key: string): Promise<number>;
84
+ /**
85
+ * Clear the counter — a SUCCESS resets the window (the user proved themselves; do
86
+ * not carry their earlier failures forward). Best-effort; a swallowed error just
87
+ * leaves the (soon-to-expire) counter in place.
88
+ */
89
+ reset(key: string): Promise<void>;
90
+ }
91
+ //# sourceMappingURL=attempt-limiter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attempt-limiter.d.ts","sourceRoot":"","sources":["../../../src/auth/rate-limit/attempt-limiter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAGH,OAAO,KAAK,KAAK,MAAM,SAAS,CAAC;AAEjC,kFAAkF;AAClF,MAAM,WAAW,oBAAoB;IACnC,mFAAmF;IACnF,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,8EAA8E;IAC9E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,qBAAa,cAAc;IAKvB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,GAAG;IALtB,oFAAoF;IACpF,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAmC;gBAG9C,KAAK,EAAE,KAAK,EACZ,GAAG,EAAE,oBAAoB;IAG5C;;;;;OAKG;IACH,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,iFAAiF;IACjF,OAAO,CAAC,CAAC;IAIT;;;;;OAKG;IACG,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAc1C;;;;;;;;;;;OAWG;IACG,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAmBjD;;;;OAIG;IACG,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAOxC"}