@astram/guard 0.1.1 → 0.3.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/account.controller.d.ts +245 -0
- package/dist/auth/account.controller.d.ts.map +1 -0
- package/dist/auth/account.controller.js +673 -0
- package/dist/auth/account.controller.js.map +1 -0
- package/dist/auth/auth.controller.d.ts +316 -0
- package/dist/auth/auth.controller.d.ts.map +1 -0
- package/dist/auth/auth.controller.js +1091 -0
- package/dist/auth/auth.controller.js.map +1 -0
- package/dist/auth/auth.module.d.ts +202 -0
- package/dist/auth/auth.module.d.ts.map +1 -0
- package/dist/auth/auth.module.js +610 -0
- package/dist/auth/auth.module.js.map +1 -0
- package/dist/auth/connections.d.ts +61 -0
- package/dist/auth/connections.d.ts.map +1 -0
- package/dist/auth/connections.js +116 -0
- package/dist/auth/connections.js.map +1 -0
- package/dist/auth/dto.d.ts +154 -0
- package/dist/auth/dto.d.ts.map +1 -0
- package/dist/auth/dto.js +29 -0
- package/dist/auth/dto.js.map +1 -0
- package/dist/auth/pending-mfa-store.d.ts +72 -0
- package/dist/auth/pending-mfa-store.d.ts.map +1 -0
- package/dist/auth/pending-mfa-store.js +106 -0
- package/dist/auth/pending-mfa-store.js.map +1 -0
- package/dist/auth/persistence.module.d.ts +42 -0
- package/dist/auth/persistence.module.d.ts.map +1 -0
- package/dist/auth/persistence.module.js +94 -0
- package/dist/auth/persistence.module.js.map +1 -0
- package/dist/auth/ports/audit-sink.d.ts +47 -0
- package/dist/auth/ports/audit-sink.d.ts.map +1 -0
- package/dist/auth/ports/audit-sink.js +20 -0
- package/dist/auth/ports/audit-sink.js.map +1 -0
- package/dist/auth/ports/credential-writer.d.ts +23 -0
- package/dist/auth/ports/credential-writer.d.ts.map +1 -0
- package/dist/auth/ports/credential-writer.js +2 -0
- package/dist/auth/ports/credential-writer.js.map +1 -0
- package/dist/auth/ports/email-sender.d.ts +14 -0
- package/dist/auth/ports/email-sender.d.ts.map +1 -0
- package/dist/auth/ports/email-sender.js +2 -0
- package/dist/auth/ports/email-sender.js.map +1 -0
- package/dist/auth/ports/mfa-device-store.d.ts +45 -0
- package/dist/auth/ports/mfa-device-store.d.ts.map +1 -0
- package/dist/auth/ports/mfa-device-store.js +13 -0
- package/dist/auth/ports/mfa-device-store.js.map +1 -0
- package/dist/auth/ports/profile-writer.d.ts +12 -0
- package/dist/auth/ports/profile-writer.d.ts.map +1 -0
- package/dist/auth/ports/profile-writer.js +2 -0
- package/dist/auth/ports/profile-writer.js.map +1 -0
- package/dist/auth/ports/recovery-code-store.d.ts +10 -0
- package/dist/auth/ports/recovery-code-store.d.ts.map +1 -0
- package/dist/auth/ports/recovery-code-store.js +2 -0
- package/dist/auth/ports/recovery-code-store.js.map +1 -0
- package/dist/auth/ports/reset-token-store.d.ts +12 -0
- package/dist/auth/ports/reset-token-store.d.ts.map +1 -0
- package/dist/auth/ports/reset-token-store.js +2 -0
- package/dist/auth/ports/reset-token-store.js.map +1 -0
- package/dist/auth/ports/session-facts-projector.d.ts +44 -0
- package/dist/auth/ports/session-facts-projector.d.ts.map +1 -0
- package/dist/auth/ports/session-facts-projector.js +30 -0
- package/dist/auth/ports/session-facts-projector.js.map +1 -0
- package/dist/auth/ports/session-sweeper.d.ts +12 -0
- package/dist/auth/ports/session-sweeper.d.ts.map +1 -0
- package/dist/auth/ports/session-sweeper.js +2 -0
- package/dist/auth/ports/session-sweeper.js.map +1 -0
- package/dist/auth/profile-resolver.d.ts +73 -0
- package/dist/auth/profile-resolver.d.ts.map +1 -0
- package/dist/auth/profile-resolver.js +28 -0
- package/dist/auth/profile-resolver.js.map +1 -0
- package/dist/auth/rate-limit/attempt-limiter.d.ts +91 -0
- package/dist/auth/rate-limit/attempt-limiter.d.ts.map +1 -0
- package/dist/auth/rate-limit/attempt-limiter.js +133 -0
- package/dist/auth/rate-limit/attempt-limiter.js.map +1 -0
- package/dist/auth/tokens.d.ts +236 -0
- package/dist/auth/tokens.d.ts.map +1 -0
- package/dist/auth/tokens.js +254 -0
- package/dist/auth/tokens.js.map +1 -0
- package/dist/auth/totp-enrollment.d.ts +42 -0
- package/dist/auth/totp-enrollment.d.ts.map +1 -0
- package/dist/auth/totp-enrollment.js +96 -0
- package/dist/auth/totp-enrollment.js.map +1 -0
- package/dist/authentication/argon2id-password-hasher.js.map +1 -0
- package/dist/authentication/mariadb-credential-resolver.d.ts +110 -0
- package/dist/authentication/mariadb-credential-resolver.d.ts.map +1 -0
- package/dist/authentication/mariadb-credential-resolver.js +170 -0
- package/dist/authentication/mariadb-credential-resolver.js.map +1 -0
- package/dist/authz/cross-tenant-grant-writer.d.ts +259 -0
- package/dist/authz/cross-tenant-grant-writer.d.ts.map +1 -0
- package/dist/authz/cross-tenant-grant-writer.js +397 -0
- package/dist/authz/cross-tenant-grant-writer.js.map +1 -0
- package/dist/authz/manifest-operation-authority-adapter.d.ts +65 -0
- package/dist/authz/manifest-operation-authority-adapter.d.ts.map +1 -0
- package/dist/authz/manifest-operation-authority-adapter.js +108 -0
- package/dist/authz/manifest-operation-authority-adapter.js.map +1 -0
- package/dist/authz/mariadb-cross-tenant-grant-adapter.d.ts +90 -0
- package/dist/authz/mariadb-cross-tenant-grant-adapter.d.ts.map +1 -0
- package/dist/authz/mariadb-cross-tenant-grant-adapter.js +211 -0
- package/dist/authz/mariadb-cross-tenant-grant-adapter.js.map +1 -0
- package/dist/authz/mariadb-record-rule-adapter.d.ts +60 -0
- package/dist/authz/mariadb-record-rule-adapter.d.ts.map +1 -0
- package/dist/authz/mariadb-record-rule-adapter.js +154 -0
- package/dist/authz/mariadb-record-rule-adapter.js.map +1 -0
- package/dist/authz/mariadb-record-share-adapter.d.ts +75 -0
- package/dist/authz/mariadb-record-share-adapter.d.ts.map +1 -0
- package/dist/authz/mariadb-record-share-adapter.js +106 -0
- package/dist/authz/mariadb-record-share-adapter.js.map +1 -0
- package/dist/authz/record-share-cascade.d.ts +52 -0
- package/dist/authz/record-share-cascade.d.ts.map +1 -0
- package/dist/authz/record-share-cascade.js +49 -0
- package/dist/authz/record-share-cascade.js.map +1 -0
- package/dist/authz/record-share-writer.d.ts +175 -0
- package/dist/authz/record-share-writer.d.ts.map +1 -0
- package/dist/authz/record-share-writer.js +225 -0
- package/dist/authz/record-share-writer.js.map +1 -0
- package/dist/authz/wire-record-authorization.d.ts +66 -0
- package/dist/authz/wire-record-authorization.d.ts.map +1 -0
- package/dist/authz/wire-record-authorization.js +34 -0
- package/dist/authz/wire-record-authorization.js.map +1 -0
- package/dist/boot/boot-sequence.js.map +1 -0
- package/dist/boot/boot.module.js.map +1 -0
- package/dist/completeness/catalog-reconciler.d.ts +21 -0
- package/dist/completeness/catalog-reconciler.d.ts.map +1 -0
- package/dist/completeness/catalog-reconciler.js +74 -0
- package/dist/completeness/catalog-reconciler.js.map +1 -0
- package/dist/completeness/operation-inventory-scanner.d.ts +17 -0
- package/dist/completeness/operation-inventory-scanner.d.ts.map +1 -0
- package/dist/completeness/operation-inventory-scanner.js +79 -0
- package/dist/completeness/operation-inventory-scanner.js.map +1 -0
- package/dist/completeness/ports/catalog-store.d.ts +33 -0
- package/dist/completeness/ports/catalog-store.d.ts.map +1 -0
- package/dist/completeness/ports/catalog-store.js +2 -0
- package/dist/completeness/ports/catalog-store.js.map +1 -0
- package/dist/config/app-config.d.ts +134 -1
- package/dist/config/app-config.d.ts.map +1 -1
- package/dist/config/app-config.js +144 -2
- package/dist/config/app-config.js.map +1 -0
- package/dist/config/config.module.js.map +1 -0
- package/dist/enforcement/classification.decorator.d.ts +33 -14
- package/dist/enforcement/classification.decorator.d.ts.map +1 -1
- package/dist/enforcement/classification.decorator.js +34 -14
- package/dist/enforcement/classification.decorator.js.map +1 -0
- package/dist/enforcement/core-seam.js.map +1 -0
- package/dist/enforcement/enforcement.module.js.map +1 -0
- package/dist/enforcement/global-enforcement.guard.d.ts +29 -19
- package/dist/enforcement/global-enforcement.guard.d.ts.map +1 -1
- package/dist/enforcement/global-enforcement.guard.js +108 -25
- package/dist/enforcement/global-enforcement.guard.js.map +1 -0
- package/dist/enforcement/reenroll-allowlist.d.ts +49 -0
- package/dist/enforcement/reenroll-allowlist.d.ts.map +1 -0
- package/dist/enforcement/reenroll-allowlist.js +77 -0
- package/dist/enforcement/reenroll-allowlist.js.map +1 -0
- package/dist/index.d.ts +43 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +198 -1
- package/dist/index.js.map +1 -0
- package/dist/module/shield-tokens.d.ts +15 -0
- package/dist/module/shield-tokens.d.ts.map +1 -0
- package/dist/module/shield-tokens.js +17 -0
- package/dist/module/shield-tokens.js.map +1 -0
- package/dist/module/shield.module.d.ts +203 -8
- package/dist/module/shield.module.d.ts.map +1 -1
- package/dist/module/shield.module.js +232 -14
- package/dist/module/shield.module.js.map +1 -0
- package/dist/request-auth/request-principal.d.ts +42 -0
- package/dist/request-auth/request-principal.d.ts.map +1 -0
- package/dist/request-auth/request-principal.js +87 -0
- package/dist/request-auth/request-principal.js.map +1 -0
- package/dist/request-auth/session-facts-als.d.ts +65 -0
- package/dist/request-auth/session-facts-als.d.ts.map +1 -0
- package/dist/request-auth/session-facts-als.js +57 -0
- package/dist/request-auth/session-facts-als.js.map +1 -0
- package/dist/request-auth/session-token-binder.d.ts +130 -0
- package/dist/request-auth/session-token-binder.d.ts.map +1 -0
- package/dist/request-auth/session-token-binder.js +277 -0
- package/dist/request-auth/session-token-binder.js.map +1 -0
- package/dist/request-auth/tokens.d.ts +14 -0
- package/dist/request-auth/tokens.d.ts.map +1 -0
- package/dist/request-auth/tokens.js +16 -0
- package/dist/request-auth/tokens.js.map +1 -0
- package/dist/session/crypto-adapters.js.map +1 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/mariadb-session-record-store.js.map +1 -0
- package/dist/session/redis-hot-session-store.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Phase 0b Auth-surface **DI provider tokens** — the shared injection seams the
|
|
4
|
+
* three parallel teams bind/consume so they never collide (ADR-0023: composition
|
|
5
|
+
* is the framework's job; these are the framework's internal wiring points).
|
|
6
|
+
*
|
|
7
|
+
* ⛔ FROZEN by this prelude. Team B (composition) PROVIDES these; Team A
|
|
8
|
+
* (credential store) provides the resolver behind {@link SHIELD_CREDENTIAL_RESOLVER};
|
|
9
|
+
* Team C (`/auth` controllers) CONSUMES {@link SHIELD_AUTHENTICATOR} +
|
|
10
|
+
* {@link SHIELD_SESSION_AUTHORITY}. Do not rename these Symbols — a rename is a
|
|
11
|
+
* breaking change to every team's wiring.
|
|
12
|
+
*
|
|
13
|
+
* NOTE — some session/hasher adapters already ship their OWN tokens in
|
|
14
|
+
* `@astram/guard` (`SHIELD_PASSWORD_HASHER`, `SHIELD_SESSION_RECORD_STORE`,
|
|
15
|
+
* `SHIELD_HOT_SESSION_STORE`, `SHIELD_TOKEN_GENERATOR`, `SHIELD_REFRESH_HASHER`,
|
|
16
|
+
* `SHIELD_CLOCK`). Those stay the canonical tokens for those adapters. This file
|
|
17
|
+
* adds ONLY the NEW seams Phase 0b introduces: the live connections
|
|
18
|
+
* ({@link SHIELD_DB_POOL} / {@link SHIELD_REDIS}), the credential resolver, and the
|
|
19
|
+
* two COMPOSED engines ({@link SHIELD_AUTHENTICATOR} / {@link SHIELD_SESSION_AUTHORITY}).
|
|
20
|
+
* {@link SHIELD_PASSWORD_HASHER} is re-exported from the argon2 adapter for
|
|
21
|
+
* convenience so a team can import every auth-relevant token from one module.
|
|
22
|
+
*
|
|
23
|
+
* ADR-0003: no framework/DB/crypto imports here — just Symbols (+ one re-export).
|
|
24
|
+
*/
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.SHIELD_RESET_LIMITER = exports.SHIELD_FORGOT_HINT_LIMITER = exports.SHIELD_FORGOT_LIMITER = exports.SHIELD_RECOVERY_LIMITER = exports.SHIELD_LOGIN_LIMITER = exports.SHIELD_SESSION_FACTS_PROJECTOR = exports.SHIELD_ROOT_IDS = exports.SHIELD_TOTP_VERIFIER_OVERRIDE = exports.SHIELD_TOTP_VERIFIER = exports.SHIELD_RESET_LINK_BASE_URL = exports.SHIELD_SESSION_SWEEPER = exports.SHIELD_MFA_DEVICE_STORE = exports.SHIELD_AUDIT_SINK = exports.SHIELD_PROFILE_WRITER = exports.SHIELD_RECOVERY_CODE_STORE = exports.SHIELD_EMAIL_SENDER = exports.SHIELD_RESET_TOKEN_STORE = exports.SHIELD_CREDENTIAL_WRITER = exports.SHIELD_SESSION_AUTHORITY = exports.SHIELD_AUTHENTICATOR = exports.SHIELD_PROFILE_RESOLVER = exports.SHIELD_CREDENTIAL_RESOLVER = exports.SHIELD_REDIS = exports.SHIELD_DB_POOL = exports.SHIELD_PASSWORD_HASHER = void 0;
|
|
27
|
+
var argon2id_password_hasher_js_1 = require("../authentication/argon2id-password-hasher.js");
|
|
28
|
+
Object.defineProperty(exports, "SHIELD_PASSWORD_HASHER", { enumerable: true, get: function () { return argon2id_password_hasher_js_1.SHIELD_PASSWORD_HASHER; } });
|
|
29
|
+
/**
|
|
30
|
+
* The ONE live `mariadb` `Pool` (the `shield_core` account — DML on
|
|
31
|
+
* `shield_identity`, ADR-0013/0014). **Provided by Team B** (the persistence
|
|
32
|
+
* factory — the single place a live DB connection is created). **Consumed by**
|
|
33
|
+
* the {@link SHIELD_CREDENTIAL_RESOLVER} factory and the
|
|
34
|
+
* {@link SHIELD_SESSION_RECORD_STORE} factory. Disposed on app shutdown.
|
|
35
|
+
*/
|
|
36
|
+
exports.SHIELD_DB_POOL = Symbol('SHIELD_DB_POOL');
|
|
37
|
+
/**
|
|
38
|
+
* The ONE live `ioredis` client (the hot session tier, ADR-0013). **Provided by
|
|
39
|
+
* Team B** (the persistence factory). **Consumed by** the
|
|
40
|
+
* {@link SHIELD_HOT_SESSION_STORE} factory. Disposed on app shutdown.
|
|
41
|
+
*/
|
|
42
|
+
exports.SHIELD_REDIS = Symbol('SHIELD_REDIS');
|
|
43
|
+
/**
|
|
44
|
+
* The `CredentialResolver` (core port) that resolves an identity hint →
|
|
45
|
+
* `ResolvedCredential | null`. **Provided by Team A** (the
|
|
46
|
+
* `MariaDbCredentialResolver`, injected with {@link SHIELD_DB_POOL}). **Consumed
|
|
47
|
+
* by** the {@link SHIELD_AUTHENTICATOR} factory (Team B) as
|
|
48
|
+
* `createAuthenticator({ resolver })`.
|
|
49
|
+
*/
|
|
50
|
+
exports.SHIELD_CREDENTIAL_RESOLVER = Symbol('SHIELD_CREDENTIAL_RESOLVER');
|
|
51
|
+
/**
|
|
52
|
+
* The `ProfileResolver` (guard port, see `../auth/profile-resolver.ts`) that turns
|
|
53
|
+
* an authenticated `Principal` → the current-user display profile for `GET /auth/me`.
|
|
54
|
+
* OPTIONAL: supplied by the consumer via `ShieldModule.forRoot({ profileResolver })`
|
|
55
|
+
* and threaded through `AuthModule.forRoot`. When ABSENT the `/auth/me` route returns
|
|
56
|
+
* an honest "profile unavailable" (the framework is org-schema-agnostic — see the
|
|
57
|
+
* port header). Consumed by {@link AuthController}'s `me()` handler.
|
|
58
|
+
*/
|
|
59
|
+
exports.SHIELD_PROFILE_RESOLVER = Symbol('SHIELD_PROFILE_RESOLVER');
|
|
60
|
+
/**
|
|
61
|
+
* The composed `Authenticator` (core engine) — `createAuthenticator({ hasher,
|
|
62
|
+
* resolver, identityBroker, factorVerifiers:[totp] })`. **Provided by Team B**
|
|
63
|
+
* (the AuthModule factory). **Consumed by** Team C's `/auth/login` +
|
|
64
|
+
* `/auth/mfa` controllers.
|
|
65
|
+
*/
|
|
66
|
+
exports.SHIELD_AUTHENTICATOR = Symbol('SHIELD_AUTHENTICATOR');
|
|
67
|
+
/**
|
|
68
|
+
* The composed `SessionAuthority` (core engine) — `createSessionAuthority({
|
|
69
|
+
* recordStore, hotStore, tokenGenerator, refreshHasher, clock, lifetimes })`.
|
|
70
|
+
* **Provided by Team B** (the AuthModule factory). **Consumed by** Team C's
|
|
71
|
+
* `/auth/login` (establish), `/auth/refresh` (rotate), `/auth/logout` (revoke),
|
|
72
|
+
* and `/auth/mfa` (establish after factor).
|
|
73
|
+
*/
|
|
74
|
+
exports.SHIELD_SESSION_AUTHORITY = Symbol('SHIELD_SESSION_AUTHORITY');
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Account & credential surface seams (forgot/reset, change-password, MFA
|
|
77
|
+
// enrollment, profile edit). All OPTIONAL, consumer-plugged through
|
|
78
|
+
// `ShieldModule.forRoot({...})` → `AuthModule.forRoot` (exactly like
|
|
79
|
+
// {@link SHIELD_PROFILE_RESOLVER}). Each binds to the supplied port impl, or to
|
|
80
|
+
// `null` when absent so the `@Optional()` injection in the routes resolves to "no
|
|
81
|
+
// seam" → the route returns 501. See `../auth/ports/*.ts` for each contract.
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
/**
|
|
84
|
+
* The `CredentialWriter` (guard port, `../auth/ports/credential-writer.ts`) — the
|
|
85
|
+
* WRITE side of the credential store (set password hash, read the stored hash by
|
|
86
|
+
* principal id). Consumed by change-password + reset-password. Absent (`null`) →
|
|
87
|
+
* those routes return 501. (TOTP secrets no longer live here — the multi-device
|
|
88
|
+
* MFA model stores them in {@link SHIELD_MFA_DEVICE_STORE}.)
|
|
89
|
+
*/
|
|
90
|
+
exports.SHIELD_CREDENTIAL_WRITER = Symbol('SHIELD_CREDENTIAL_WRITER');
|
|
91
|
+
/**
|
|
92
|
+
* The `ResetTokenStore` (guard port, `../auth/ports/reset-token-store.ts`) — the
|
|
93
|
+
* single-use, TTL'd password-reset token store (framework hands it a SHA-256 hash,
|
|
94
|
+
* never the raw token). Consumed by forgot-password + reset-password.
|
|
95
|
+
*/
|
|
96
|
+
exports.SHIELD_RESET_TOKEN_STORE = Symbol('SHIELD_RESET_TOKEN_STORE');
|
|
97
|
+
/**
|
|
98
|
+
* The `EmailSender` (guard port, `../auth/ports/email-sender.ts`) — the delivery
|
|
99
|
+
* seam (SendGrid/SMTP/test capturer). The framework builds the message; the
|
|
100
|
+
* consumer delivers it. Consumed by forgot-password.
|
|
101
|
+
*/
|
|
102
|
+
exports.SHIELD_EMAIL_SENDER = Symbol('SHIELD_EMAIL_SENDER');
|
|
103
|
+
/**
|
|
104
|
+
* The `RecoveryCodeStore` (guard port, `../auth/ports/recovery-code-store.ts`) —
|
|
105
|
+
* persists Argon2-hashed one-time MFA recovery codes. Consumed by MFA enrollment.
|
|
106
|
+
*/
|
|
107
|
+
exports.SHIELD_RECOVERY_CODE_STORE = Symbol('SHIELD_RECOVERY_CODE_STORE');
|
|
108
|
+
/**
|
|
109
|
+
* The `ProfileWriter` (guard port, `../auth/ports/profile-writer.ts`) — the
|
|
110
|
+
* self-service profile-edit seam (name/email). Consumed by `PATCH /account/profile`.
|
|
111
|
+
*/
|
|
112
|
+
exports.SHIELD_PROFILE_WRITER = Symbol('SHIELD_PROFILE_WRITER');
|
|
113
|
+
/**
|
|
114
|
+
* The `AuditSink` (guard port, `../auth/ports/audit-sink.ts`) — the auth-surface audit
|
|
115
|
+
* seam (spec §6.2). `record(event)` persists a framework-built {@link AuthAuditEvent}
|
|
116
|
+
* (the FIRST auth audit event: the recovery-code login). OPTIONAL, consumer-plugged
|
|
117
|
+
* exactly like {@link SHIELD_PROFILE_RESOLVER}: supplied → bound app-wide; omitted →
|
|
118
|
+
* bound `null`, so the `@Optional()` injection in {@link AuthController} resolves to
|
|
119
|
+
* "no seam" and the best-effort audit call is a no-op (the login still works,
|
|
120
|
+
* un-audited). Consumed by the `/auth/mfa` recovery branch (fire-and-forget).
|
|
121
|
+
*/
|
|
122
|
+
exports.SHIELD_AUDIT_SINK = Symbol('SHIELD_AUDIT_SINK');
|
|
123
|
+
/**
|
|
124
|
+
* The `MfaDeviceStore` (guard port, `../auth/ports/mfa-device-store.ts`) — persists
|
|
125
|
+
* a principal's N TOTP authenticator DEVICES (multi-device MFA). Consumed
|
|
126
|
+
* by the `/account/mfa/devices/*` routes (list/begin/confirm/rename/remove) + the
|
|
127
|
+
* consumer's login resolver (which bundles {@link MfaDeviceStore.listConfirmedSecrets}
|
|
128
|
+
* for the OR-match). Absent (`null`) → the device routes return 501.
|
|
129
|
+
*/
|
|
130
|
+
exports.SHIELD_MFA_DEVICE_STORE = Symbol('SHIELD_MFA_DEVICE_STORE');
|
|
131
|
+
/**
|
|
132
|
+
* The `SessionSweeper` (guard port, `../auth/ports/session-sweeper.ts`) — revokes a
|
|
133
|
+
* principal's sessions after a credential change (all, or all-but-current). The core
|
|
134
|
+
* has no revoke-all-for-principal, so the consumer implements it. Consumed by
|
|
135
|
+
* change-password (others) + reset-password (all).
|
|
136
|
+
*/
|
|
137
|
+
exports.SHIELD_SESSION_SWEEPER = Symbol('SHIELD_SESSION_SWEEPER');
|
|
138
|
+
/**
|
|
139
|
+
* The reset-link base URL (a plain `string` value, NOT a port). The framework
|
|
140
|
+
* composes the emailed reset link as `${base}?token=<rawToken>` so the
|
|
141
|
+
* {@link SHIELD_EMAIL_SENDER} seam stays a dumb delivery channel (no URL/token
|
|
142
|
+
* logic). Bound app-wide by `AuthModule` from the environment
|
|
143
|
+
* (`RESET_LINK_BASE_URL`, default `http://localhost:5173/reset-password`). Consumed
|
|
144
|
+
* by `POST /auth/forgot-password`.
|
|
145
|
+
*/
|
|
146
|
+
exports.SHIELD_RESET_LINK_BASE_URL = Symbol('SHIELD_RESET_LINK_BASE_URL');
|
|
147
|
+
/**
|
|
148
|
+
* The core single-secret `FactorVerifier` for TOTP (`createTotpFactorVerifier({
|
|
149
|
+
* hmacSha1 })`), PROVIDED by `AuthModule` (which loads the core factory lazily — the
|
|
150
|
+
* CJS↔ESM invariant, same as {@link SHIELD_AUTHENTICATOR}). It is the SAME base
|
|
151
|
+
* verifier the login default uses, provided pre-built app-wide so the multi-device
|
|
152
|
+
* MFA routes (Phase 3) can check a presented code against a SPECIFIC device's secret
|
|
153
|
+
* without importing `@astram/core` at the controller's top level (that becomes a
|
|
154
|
+
* runtime `require` on the ESM-only core in the built CJS dist). Distinct from the
|
|
155
|
+
* login OR-verifier the consumer plugs through {@link SHIELD_TOTP_VERIFIER_OVERRIDE}
|
|
156
|
+
* (Phase 2): this stays single-secret, for per-device checks.
|
|
157
|
+
*/
|
|
158
|
+
exports.SHIELD_TOTP_VERIFIER = Symbol('SHIELD_TOTP_VERIFIER');
|
|
159
|
+
/**
|
|
160
|
+
* OPTIONAL consumer override for the LOGIN `totp` {@link FactorVerifier} — the
|
|
161
|
+
* verifier the composed `Authenticator` uses to satisfy a `RequiredFactor` of
|
|
162
|
+
* factor `'totp'`. Threaded through `ShieldModule.forRoot({ totpVerifier })` →
|
|
163
|
+
* `AuthModule.forRoot`, `@Optional()` (bound `null` when absent). When supplied it
|
|
164
|
+
* REPLACES the framework's default `createTotpFactorVerifier({ hmacSha1 })` in the
|
|
165
|
+
* authenticator's `factorVerifiers` — the seam multi-device MFA needs so a
|
|
166
|
+
* consumer can plug an OR-matching verifier (one required-factor whose `secret` is a
|
|
167
|
+
* bundle of the user's N device secrets; the verifier matches ANY). Default absent →
|
|
168
|
+
* behavior EXACTLY unchanged (single-secret RFC-6238). Distinct from
|
|
169
|
+
* {@link SHIELD_TOTP_VERIFIER} (the pre-built single-secret verifier the account MFA
|
|
170
|
+
* routes use for per-device checks — that one is never overridden).
|
|
171
|
+
*/
|
|
172
|
+
exports.SHIELD_TOTP_VERIFIER_OVERRIDE = Symbol('SHIELD_TOTP_VERIFIER_OVERRIDE');
|
|
173
|
+
/**
|
|
174
|
+
* OPTIONAL root allowlist — the server-side, trusted set of canonical
|
|
175
|
+
* principal ids that ARE roots on the framework's Internal identity
|
|
176
|
+
* source. Threaded through `ShieldModule.forRoot({ rootIds })` →
|
|
177
|
+
* `AuthModule.forRoot` and passed to `createIdentityBroker({ internalRootIds })`,
|
|
178
|
+
* `@Optional()` (bound `[]` when absent → nobody is a root, default-deny).
|
|
179
|
+
*
|
|
180
|
+
* ROOT SAFETY (core `internal-adapter.ts`): the `isRoot` flag is set
|
|
181
|
+
* `true` iff the authenticated principal's `id` is in THIS allowlist — it is NEVER read
|
|
182
|
+
* from a caller-supplied claim. So this is the ONLY way a login mints a root, and
|
|
183
|
+
* an attacker who can influence claim VALUES cannot flip it (they'd have to be in this
|
|
184
|
+
* server-side list, which they can't write). Default absent → `[]` → default-deny,
|
|
185
|
+
* exactly the historical behavior (`createIdentityBroker()` with an empty allowlist).
|
|
186
|
+
*/
|
|
187
|
+
exports.SHIELD_ROOT_IDS = Symbol('SHIELD_ROOT_IDS');
|
|
188
|
+
/**
|
|
189
|
+
* OPTIONAL {@link import('./ports/session-facts-projector.js').SessionFactsProjector}
|
|
190
|
+
* — the ADR-0011 eager-projection seam. Threaded through
|
|
191
|
+
* `ShieldModule.forRoot({ sessionFactsProjector })` → `AuthModule.forRoot`, `@Optional()`
|
|
192
|
+
* (bound `null` when absent). When PRESENT, the auth controller calls `project(...)` at
|
|
193
|
+
* login + refresh and caches the returned `CoarseGrants` on the session binding, so gate
|
|
194
|
+
* 5 serves the principal's coarse grants from the request's `cachedFacts` instead of a DB
|
|
195
|
+
* reload every request. ABSENT → the controller omits `cachedFacts` → today's exact
|
|
196
|
+
* behavior (DB reload every request). Purely additive + opt-in. Only consulted under
|
|
197
|
+
* `auth: true`.
|
|
198
|
+
*/
|
|
199
|
+
exports.SHIELD_SESSION_FACTS_PROJECTOR = Symbol('SHIELD_SESSION_FACTS_PROJECTOR');
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// Rate-limit seams (anti-brute-force, spec §5) — ONE `AttemptLimiter` class
|
|
202
|
+
// (Redis-backed), provided as TWO instances with independent thresholds under
|
|
203
|
+
// these tokens. Both are always provided under `auth: true` (they need the live
|
|
204
|
+
// {@link SHIELD_REDIS}); the controller injects each `@Optional()` so an absent
|
|
205
|
+
// limiter degrades to "allow" (never a lock-out from missing wiring).
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
/**
|
|
208
|
+
* The password-login {@link AttemptLimiter} (spec §5 / IdP-hardening H1) — keyed
|
|
209
|
+
* `login:<identityHint>`, default 10 failures / 15-min window. Consumed at the TOP
|
|
210
|
+
* of `POST /auth/login` (`check` → generic 401 when locked) and on the outcome
|
|
211
|
+
* (`recordFailure` on a rejected wrong-password; `reset` on `authenticated` OR
|
|
212
|
+
* `mfa_required` — a correct password that merely needs MFA is NOT a failure).
|
|
213
|
+
*/
|
|
214
|
+
exports.SHIELD_LOGIN_LIMITER = Symbol('SHIELD_LOGIN_LIMITER');
|
|
215
|
+
/**
|
|
216
|
+
* The recovery-code {@link AttemptLimiter} (spec §5) — keyed `recovery:<principalId>`,
|
|
217
|
+
* default 5 failures / 15-min window (recovery codes are higher-value than a rotating
|
|
218
|
+
* 30-s TOTP). Consumed in the `/auth/mfa` recovery branch: `check` BEFORE `consume`
|
|
219
|
+
* (locked → generic 401 without consulting the store), `recordFailure` on a wrong
|
|
220
|
+
* code, `reset` on success.
|
|
221
|
+
*/
|
|
222
|
+
exports.SHIELD_RECOVERY_LIMITER = Symbol('SHIELD_RECOVERY_LIMITER');
|
|
223
|
+
/**
|
|
224
|
+
* The forgot-password IP {@link AttemptLimiter} (feature #141) — keyed
|
|
225
|
+
* `forgot:ip:<clientIp>`, default 5 per 15-min window. Consumed at the TOP of
|
|
226
|
+
* `POST /auth/forgot-password`. UNLIKE login, forgot-password is ALWAYS 204 (a 429
|
|
227
|
+
* would itself be an enumeration/probing signal), so this is a request-rate cap: the
|
|
228
|
+
* handler counts EVERY forgot request against the IP budget and, once the budget is
|
|
229
|
+
* exceeded, SILENTLY skips the email-send work while STILL returning 204 (the existing
|
|
230
|
+
* always-204 anti-enumeration posture is preserved). Fail-open on a Redis blip. The
|
|
231
|
+
* client IP is read from the TRUSTED `req.ip` (the consumer sets `trust proxy` so
|
|
232
|
+
* X-Forwarded-For can't be spoofed).
|
|
233
|
+
*/
|
|
234
|
+
exports.SHIELD_FORGOT_LIMITER = Symbol('SHIELD_FORGOT_LIMITER');
|
|
235
|
+
/**
|
|
236
|
+
* The forgot-password PER-HINT {@link AttemptLimiter} (feature #141) — keyed
|
|
237
|
+
* `forgot:<identityHint>`, default 3 per 60-min window (generous). Caps how often ANY
|
|
238
|
+
* caller (across IPs) can trigger a reset email for ONE account, so a single victim
|
|
239
|
+
* can't be email-bombed from a rotating IP pool. Same request-rate-cap + silent-skip-
|
|
240
|
+
* still-204 semantics as {@link SHIELD_FORGOT_LIMITER}; consumed alongside it in
|
|
241
|
+
* `POST /auth/forgot-password`.
|
|
242
|
+
*/
|
|
243
|
+
exports.SHIELD_FORGOT_HINT_LIMITER = Symbol('SHIELD_FORGOT_HINT_LIMITER');
|
|
244
|
+
/**
|
|
245
|
+
* The reset-password IP {@link AttemptLimiter} (feature #141) — keyed
|
|
246
|
+
* `reset:ip:<clientIp>`, default 10 per 15-min window. Consumed at the TOP of
|
|
247
|
+
* `POST /auth/reset-password` (`check` BEFORE token work). Reset is token-gated (a
|
|
248
|
+
* 256-bit single-use token), so this is DoS/abuse throttling, NOT anti-brute-force —
|
|
249
|
+
* and it MAY return a GENERIC 429 on lockout (a token-gated endpoint is not
|
|
250
|
+
* enumeration-sensitive). `recordFailure` on a bad/invalid token (the abuse signal);
|
|
251
|
+
* `reset` on a successful reset (204). Fail-open on a Redis blip. IP is the trusted
|
|
252
|
+
* `req.ip`.
|
|
253
|
+
*/
|
|
254
|
+
exports.SHIELD_RESET_LIMITER = Symbol('SHIELD_RESET_LIMITER');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokens.js","sourceRoot":"","sources":["../../src/auth/tokens.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;;;AAEH,6FAAuF;AAA9E,qIAAA,sBAAsB,OAAA;AAE/B;;;;;;GAMG;AACU,QAAA,cAAc,GAAG,MAAM,CAAC,gBAAgB,CAAC,CAAC;AAEvD;;;;GAIG;AACU,QAAA,YAAY,GAAG,MAAM,CAAC,cAAc,CAAC,CAAC;AAEnD;;;;;;GAMG;AACU,QAAA,0BAA0B,GAAG,MAAM,CAAC,4BAA4B,CAAC,CAAC;AAE/E;;;;;;;GAOG;AACU,QAAA,uBAAuB,GAAG,MAAM,CAAC,yBAAyB,CAAC,CAAC;AAEzE;;;;;GAKG;AACU,QAAA,oBAAoB,GAAG,MAAM,CAAC,sBAAsB,CAAC,CAAC;AAEnE;;;;;;GAMG;AACU,QAAA,wBAAwB,GAAG,MAAM,CAAC,0BAA0B,CAAC,CAAC;AAE3E,8EAA8E;AAC9E,yEAAyE;AACzE,oEAAoE;AACpE,qEAAqE;AACrE,gFAAgF;AAChF,kFAAkF;AAClF,6EAA6E;AAC7E,8EAA8E;AAE9E;;;;;;GAMG;AACU,QAAA,wBAAwB,GAAG,MAAM,CAAC,0BAA0B,CAAC,CAAC;AAE3E;;;;GAIG;AACU,QAAA,wBAAwB,GAAG,MAAM,CAAC,0BAA0B,CAAC,CAAC;AAE3E;;;;GAIG;AACU,QAAA,mBAAmB,GAAG,MAAM,CAAC,qBAAqB,CAAC,CAAC;AAEjE;;;GAGG;AACU,QAAA,0BAA0B,GAAG,MAAM,CAAC,4BAA4B,CAAC,CAAC;AAE/E;;;GAGG;AACU,QAAA,qBAAqB,GAAG,MAAM,CAAC,uBAAuB,CAAC,CAAC;AAErE;;;;;;;;GAQG;AACU,QAAA,iBAAiB,GAAG,MAAM,CAAC,mBAAmB,CAAC,CAAC;AAE7D;;;;;;GAMG;AACU,QAAA,uBAAuB,GAAG,MAAM,CAAC,yBAAyB,CAAC,CAAC;AAEzE;;;;;GAKG;AACU,QAAA,sBAAsB,GAAG,MAAM,CAAC,wBAAwB,CAAC,CAAC;AAEvE;;;;;;;GAOG;AACU,QAAA,0BAA0B,GAAG,MAAM,CAAC,4BAA4B,CAAC,CAAC;AAE/E;;;;;;;;;;GAUG;AACU,QAAA,oBAAoB,GAAG,MAAM,CAAC,sBAAsB,CAAC,CAAC;AAEnE;;;;;;;;;;;;GAYG;AACU,QAAA,6BAA6B,GAAG,MAAM,CAAC,+BAA+B,CAAC,CAAC;AAErF;;;;;;;;;;;;;GAaG;AACU,QAAA,eAAe,GAAG,MAAM,CAAC,iBAAiB,CAAC,CAAC;AAEzD;;;;;;;;;;GAUG;AACU,QAAA,8BAA8B,GAAG,MAAM,CAAC,gCAAgC,CAAC,CAAC;AAEvF,8EAA8E;AAC9E,4EAA4E;AAC5E,8EAA8E;AAC9E,gFAAgF;AAChF,gFAAgF;AAChF,sEAAsE;AACtE,8EAA8E;AAE9E;;;;;;GAMG;AACU,QAAA,oBAAoB,GAAG,MAAM,CAAC,sBAAsB,CAAC,CAAC;AAEnE;;;;;;GAMG;AACU,QAAA,uBAAuB,GAAG,MAAM,CAAC,yBAAyB,CAAC,CAAC;AAEzE;;;;;;;;;;GAUG;AACU,QAAA,qBAAqB,GAAG,MAAM,CAAC,uBAAuB,CAAC,CAAC;AAErE;;;;;;;GAOG;AACU,QAAA,0BAA0B,GAAG,MAAM,CAAC,4BAA4B,CAAC,CAAC;AAE/E;;;;;;;;;GASG;AACU,QAAA,oBAAoB,GAAG,MAAM,CAAC,sBAAsB,CAAC,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 4 MFA-enrollment — the **pure TOTP-enrollment helpers**: generate a fresh
|
|
3
|
+
* base32 TOTP secret, compose the `otpauth://` provisioning URI an authenticator app
|
|
4
|
+
* scans, and mint one-time recovery codes.
|
|
5
|
+
*
|
|
6
|
+
* ── WHY THIS LIVES IN GUARD (not core) ──────────────────────────────────────
|
|
7
|
+
* Core owns the TOTP *verifier* (`createTotpFactorVerifier`, RFC 6238) but is
|
|
8
|
+
* ZERO-dep + node-type-free (ADR-0003), so it cannot mint secrets (needs
|
|
9
|
+
* `node:crypto.randomBytes`) nor build a URI. Enrollment is the *provisioning* side
|
|
10
|
+
* — it produces the secret the verifier later checks against. Guard owns node
|
|
11
|
+
* builtins, so the generation lives here. NO core change: the secret this emits is a
|
|
12
|
+
* plain base32 string, exactly what `createTotpFactorVerifier` decodes.
|
|
13
|
+
*
|
|
14
|
+
* ── SECURITY ─────────────────────────────────────────────────────────────────
|
|
15
|
+
* The secret + recovery codes are credential material: never logged, returned to the
|
|
16
|
+
* user exactly ONCE at enrollment, and (recovery codes) stored only Argon2-hashed.
|
|
17
|
+
* These functions are PURE (no I/O, no storage) — the caller decides what to persist.
|
|
18
|
+
*
|
|
19
|
+
* ADR-0003: guard owns `node:crypto`; this file has NO framework/HTTP/DB coupling.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Generate a fresh, cryptographically-random base32 TOTP secret (160 bits → 32
|
|
23
|
+
* chars). This is what the user's authenticator app is provisioned with and what the
|
|
24
|
+
* core verifier later checks presented codes against.
|
|
25
|
+
*/
|
|
26
|
+
export declare function generateTotpSecret(): string;
|
|
27
|
+
/**
|
|
28
|
+
* Build the `otpauth://totp/...` provisioning URI an authenticator app scans (or the
|
|
29
|
+
* user enters manually). `label` identifies the account (shown in the app), `issuer`
|
|
30
|
+
* names the service. All three URI components are percent-encoded so a label/issuer
|
|
31
|
+
* with spaces or `:`/`?`/`&` cannot break the URI. Fixed to the defaults the core
|
|
32
|
+
* verifier uses (SHA-1, 6 digits, 30s period).
|
|
33
|
+
*/
|
|
34
|
+
export declare function buildOtpauthUri(secret: string, label: string, issuer: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Mint `count` unique one-time recovery codes (each `xxxxx-xxxxx`, 10 lowercase hex
|
|
37
|
+
* chars grouped for readability). Returned RAW to the caller ONCE at enrollment; the
|
|
38
|
+
* store persists them Argon2-hashed. Uniqueness is enforced (a hex collision at this
|
|
39
|
+
* entropy is astronomically unlikely, but the loop guarantees `count` distinct codes).
|
|
40
|
+
*/
|
|
41
|
+
export declare function generateRecoveryCodes(count?: number): string[];
|
|
42
|
+
//# sourceMappingURL=totp-enrollment.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"totp-enrollment.d.ts","sourceRoot":"","sources":["../../src/auth/totp-enrollment.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AA4CH;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAOrF;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,GAAE,MAAoC,GAAG,MAAM,EAAE,CAO3F"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Phase 4 MFA-enrollment — the **pure TOTP-enrollment helpers**: generate a fresh
|
|
4
|
+
* base32 TOTP secret, compose the `otpauth://` provisioning URI an authenticator app
|
|
5
|
+
* scans, and mint one-time recovery codes.
|
|
6
|
+
*
|
|
7
|
+
* ── WHY THIS LIVES IN GUARD (not core) ──────────────────────────────────────
|
|
8
|
+
* Core owns the TOTP *verifier* (`createTotpFactorVerifier`, RFC 6238) but is
|
|
9
|
+
* ZERO-dep + node-type-free (ADR-0003), so it cannot mint secrets (needs
|
|
10
|
+
* `node:crypto.randomBytes`) nor build a URI. Enrollment is the *provisioning* side
|
|
11
|
+
* — it produces the secret the verifier later checks against. Guard owns node
|
|
12
|
+
* builtins, so the generation lives here. NO core change: the secret this emits is a
|
|
13
|
+
* plain base32 string, exactly what `createTotpFactorVerifier` decodes.
|
|
14
|
+
*
|
|
15
|
+
* ── SECURITY ─────────────────────────────────────────────────────────────────
|
|
16
|
+
* The secret + recovery codes are credential material: never logged, returned to the
|
|
17
|
+
* user exactly ONCE at enrollment, and (recovery codes) stored only Argon2-hashed.
|
|
18
|
+
* These functions are PURE (no I/O, no storage) — the caller decides what to persist.
|
|
19
|
+
*
|
|
20
|
+
* ADR-0003: guard owns `node:crypto`; this file has NO framework/HTTP/DB coupling.
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.generateTotpSecret = generateTotpSecret;
|
|
24
|
+
exports.buildOtpauthUri = buildOtpauthUri;
|
|
25
|
+
exports.generateRecoveryCodes = generateRecoveryCodes;
|
|
26
|
+
const node_crypto_1 = require("node:crypto");
|
|
27
|
+
/** RFC 4648 base32 alphabet — the standard TOTP secret encoding (no padding here). */
|
|
28
|
+
const BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
29
|
+
/**
|
|
30
|
+
* Bytes of entropy behind a generated TOTP secret. 20 bytes = 160 bits → exactly
|
|
31
|
+
* 32 base32 characters (20 × 8 / 5 = 32, no remainder → no padding), the RFC 4226
|
|
32
|
+
* recommended HMAC-SHA1 key length.
|
|
33
|
+
*/
|
|
34
|
+
const SECRET_BYTES = 20;
|
|
35
|
+
/** How many recovery codes to mint by default (industry-standard batch of 10). */
|
|
36
|
+
const DEFAULT_RECOVERY_CODE_COUNT = 10;
|
|
37
|
+
/** Bytes per recovery code → 5 bytes = 10 lowercase hex chars, split into two groups. */
|
|
38
|
+
const RECOVERY_CODE_BYTES = 5;
|
|
39
|
+
/**
|
|
40
|
+
* Encode raw bytes into an RFC 4648 base32 string (no padding). Used to turn the
|
|
41
|
+
* random secret into the base32 string an authenticator app + the core verifier both
|
|
42
|
+
* expect. Pure bit-packing: 5 bits per output char, MSB-first.
|
|
43
|
+
*/
|
|
44
|
+
function base32Encode(bytes) {
|
|
45
|
+
let bits = 0;
|
|
46
|
+
let value = 0;
|
|
47
|
+
let out = '';
|
|
48
|
+
for (const byte of bytes) {
|
|
49
|
+
value = (value << 8) | byte;
|
|
50
|
+
bits += 8;
|
|
51
|
+
while (bits >= 5) {
|
|
52
|
+
bits -= 5;
|
|
53
|
+
out += BASE32_ALPHABET[(value >>> bits) & 0x1f];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (bits > 0) {
|
|
57
|
+
// Flush the remaining <5 bits, left-padded with zeros to a full 5-bit group.
|
|
58
|
+
out += BASE32_ALPHABET[(value << (5 - bits)) & 0x1f];
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Generate a fresh, cryptographically-random base32 TOTP secret (160 bits → 32
|
|
64
|
+
* chars). This is what the user's authenticator app is provisioned with and what the
|
|
65
|
+
* core verifier later checks presented codes against.
|
|
66
|
+
*/
|
|
67
|
+
function generateTotpSecret() {
|
|
68
|
+
return base32Encode((0, node_crypto_1.randomBytes)(SECRET_BYTES));
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Build the `otpauth://totp/...` provisioning URI an authenticator app scans (or the
|
|
72
|
+
* user enters manually). `label` identifies the account (shown in the app), `issuer`
|
|
73
|
+
* names the service. All three URI components are percent-encoded so a label/issuer
|
|
74
|
+
* with spaces or `:`/`?`/`&` cannot break the URI. Fixed to the defaults the core
|
|
75
|
+
* verifier uses (SHA-1, 6 digits, 30s period).
|
|
76
|
+
*/
|
|
77
|
+
function buildOtpauthUri(secret, label, issuer) {
|
|
78
|
+
const encIssuer = encodeURIComponent(issuer);
|
|
79
|
+
const encLabel = encodeURIComponent(label);
|
|
80
|
+
return (`otpauth://totp/${encIssuer}:${encLabel}` +
|
|
81
|
+
`?secret=${secret}&issuer=${encIssuer}&digits=6&period=30`);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Mint `count` unique one-time recovery codes (each `xxxxx-xxxxx`, 10 lowercase hex
|
|
85
|
+
* chars grouped for readability). Returned RAW to the caller ONCE at enrollment; the
|
|
86
|
+
* store persists them Argon2-hashed. Uniqueness is enforced (a hex collision at this
|
|
87
|
+
* entropy is astronomically unlikely, but the loop guarantees `count` distinct codes).
|
|
88
|
+
*/
|
|
89
|
+
function generateRecoveryCodes(count = DEFAULT_RECOVERY_CODE_COUNT) {
|
|
90
|
+
const codes = new Set();
|
|
91
|
+
while (codes.size < count) {
|
|
92
|
+
const hex = (0, node_crypto_1.randomBytes)(RECOVERY_CODE_BYTES).toString('hex'); // 10 hex chars
|
|
93
|
+
codes.add(`${hex.slice(0, 5)}-${hex.slice(5)}`);
|
|
94
|
+
}
|
|
95
|
+
return [...codes];
|
|
96
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"totp-enrollment.js","sourceRoot":"","sources":["../../src/auth/totp-enrollment.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;GAmBG;;AAiDH,gDAEC;AASD,0CAOC;AAQD,sDAOC;AAhFD,6CAA0C;AAE1C,sFAAsF;AACtF,MAAM,eAAe,GAAG,kCAAkC,CAAC;AAE3D;;;;GAIG;AACH,MAAM,YAAY,GAAG,EAAE,CAAC;AAExB,kFAAkF;AAClF,MAAM,2BAA2B,GAAG,EAAE,CAAC;AAEvC,yFAAyF;AACzF,MAAM,mBAAmB,GAAG,CAAC,CAAC;AAE9B;;;;GAIG;AACH,SAAS,YAAY,CAAC,KAAiB;IACrC,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,KAAK,GAAG,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC;QAC5B,IAAI,IAAI,CAAC,CAAC;QACV,OAAO,IAAI,IAAI,CAAC,EAAE,CAAC;YACjB,IAAI,IAAI,CAAC,CAAC;YACV,GAAG,IAAI,eAAe,CAAC,CAAC,KAAK,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;QAClD,CAAC;IACH,CAAC;IACD,IAAI,IAAI,GAAG,CAAC,EAAE,CAAC;QACb,6EAA6E;QAC7E,GAAG,IAAI,eAAe,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IACvD,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;GAIG;AACH,SAAgB,kBAAkB;IAChC,OAAO,YAAY,CAAC,IAAA,yBAAW,EAAC,YAAY,CAAC,CAAC,CAAC;AACjD,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,eAAe,CAAC,MAAc,EAAE,KAAa,EAAE,MAAc;IAC3E,MAAM,SAAS,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAC7C,MAAM,QAAQ,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC;IAC3C,OAAO,CACL,kBAAkB,SAAS,IAAI,QAAQ,EAAE;QACzC,WAAW,MAAM,WAAW,SAAS,qBAAqB,CAC3D,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,SAAgB,qBAAqB,CAAC,QAAgB,2BAA2B;IAC/E,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,OAAO,KAAK,CAAC,IAAI,GAAG,KAAK,EAAE,CAAC;QAC1B,MAAM,GAAG,GAAG,IAAA,yBAAW,EAAC,mBAAmB,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,eAAe;QAC7E,KAAK,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAClD,CAAC;IACD,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC;AACpB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"argon2id-password-hasher.js","sourceRoot":"","sources":["../../src/authentication/argon2id-password-hasher.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0EH,wDAQC;AAhFD,+CAAiC;AAmBjC,MAAM,cAAc,GAA6B;IAC/C,UAAU,EAAE,MAAM;IAClB,QAAQ,EAAE,CAAC;IACX,WAAW,EAAE,CAAC;CACf,CAAC;AAEF,0EAA0E;AAC7D,QAAA,sBAAsB,GAAG,MAAM,CAAC,wBAAwB,CAAC,CAAC;AAEvE;;;;GAIG;AACH,MAAa,sBAAsB;IAChB,OAAO,CAAuC;IAE/D,YAAY,SAAyB,EAAE;QACrC,IAAI,CAAC,OAAO,GAAG;YACb,IAAI,EAAE,MAAM,CAAC,QAAQ;YACrB,UAAU,EAAE,MAAM,CAAC,UAAU,IAAI,cAAc,CAAC,UAAU;YAC1D,QAAQ,EAAE,MAAM,CAAC,QAAQ,IAAI,cAAc,CAAC,QAAQ;YACpD,WAAW,EAAE,MAAM,CAAC,WAAW,IAAI,cAAc,CAAC,WAAW;SAC9D,CAAC;IACJ,CAAC;IAED,8EAA8E;IAC9E,KAAK,CAAC,IAAI,CAAC,SAAiB;QAC1B,6EAA6E;QAC7E,OAAO,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IAC9C,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,MAAM,CAAC,IAAY,EAAE,SAAiB;QAC1C,IAAI,CAAC;YACH,OAAO,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;QAC9C,CAAC;QAAC,MAAM,CAAC;YACP,yEAAyE;YACzE,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;CACF;AA/BD,wDA+BC;AAED;;;;;GAKG;AACH,SAAgB,sBAAsB,CAAC,SAAyB,EAAE;IAIhE,OAAO;QACL,OAAO,EAAE,8BAAsB;QAC/B,QAAQ,EAAE,IAAI,sBAAsB,CAAC,MAAM,CAAC;KAC7C,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `MariaDbCredentialResolver` — the framework-DEFAULT concrete
|
|
3
|
+
* {@link CredentialResolver} (core port) backed by the single
|
|
4
|
+
* `shield_identity.identity_credential` table. Resolves an `identityHint`
|
|
5
|
+
* (username / email) → a {@link ResolvedCredential} the Authentication engine
|
|
6
|
+
* verifies, or `null` for an unknown hint.
|
|
7
|
+
*
|
|
8
|
+
* ⛔ PASSWORD-ONLY BY DESIGN. This default resolver reads ONLY the columns the
|
|
9
|
+
* framework's shipped seed (`infra/mariadb/init/30-identity-credential.sql`)
|
|
10
|
+
* actually creates: `ic_password_hash, ic_principal_id, ic_tenant_id, ic_roles`.
|
|
11
|
+
* It therefore ALWAYS returns `requiredFactors: []` — it does NOT provide an
|
|
12
|
+
* MFA/second-factor challenge. There is no TOTP column here: the multi-device MFA
|
|
13
|
+
* model moved authenticator secrets out of `identity_credential` into a separate
|
|
14
|
+
* `mfa_device` table, which the framework ships only as an UNIMPLEMENTED port.
|
|
15
|
+
*
|
|
16
|
+
* MFA / required-factors are a CONSUMER concern, delivered by plugging a custom
|
|
17
|
+
* `credentialResolver` (+ a concrete `MfaDeviceStore`) via `ShieldModule.forRoot`
|
|
18
|
+
* — NOT by this default. See astram3's `IdentityCredentialResolver`, which reads
|
|
19
|
+
* this same table AND folds in the consumer's confirmed authenticators via
|
|
20
|
+
* `listConfirmedSecrets(mfa_device)` to populate `requiredFactors`. That seam is
|
|
21
|
+
* the supported way to add MFA-at-login on top of this password-only default.
|
|
22
|
+
*
|
|
23
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
24
|
+
* THE TABLE CONTRACT (infra seed `infra/mariadb/init/30-identity-credential.sql`,
|
|
25
|
+
* in `shield_identity`; the runtime `shield_core` account already has DML there —
|
|
26
|
+
* no new grant). Columns per spec §3 (prefix `ic_` to match the repo's
|
|
27
|
+
* session-table convention):
|
|
28
|
+
*
|
|
29
|
+
* identity_credential
|
|
30
|
+
* ┌────────────────────────┬──────────────┬────────────────────────────────────┐
|
|
31
|
+
* │ column │ type │ notes │
|
|
32
|
+
* ├────────────────────────┼──────────────┼────────────────────────────────────┤
|
|
33
|
+
* │ ic_identity_hint │ VARCHAR(255) │ UNIQUE — the login key (user/email)│
|
|
34
|
+
* │ ic_password_hash │ VARCHAR(255) │ Argon2id encoded hash (verify in) │
|
|
35
|
+
* │ ic_principal_id │ VARCHAR(255) │ → ResolvedCredential.claims.id │
|
|
36
|
+
* │ ic_tenant_id │ VARCHAR(255) │ → claims.tenantId │
|
|
37
|
+
* │ ic_roles │ JSON / TEXT │ role-id array → claims.roleIds │
|
|
38
|
+
* │ ic_created_at_ms │ BIGINT │ UTC epoch ms (platform sets UTC) │
|
|
39
|
+
* │ ic_updated_at_ms │ BIGINT │ UTC epoch ms │
|
|
40
|
+
* └────────────────────────┴──────────────┴────────────────────────────────────┘
|
|
41
|
+
* PRIMARY KEY / UNIQUE on ic_identity_hint (single-row resolve by hint).
|
|
42
|
+
* (No `ic_totp_secret` column — MFA secrets live in the consumer's `mfa_device`
|
|
43
|
+
* table, resolved by a consumer-plugged resolver, not by this default.)
|
|
44
|
+
*
|
|
45
|
+
* The resolver's SELECT must:
|
|
46
|
+
* 1. `SELECT ic_password_hash, ic_principal_id, ic_tenant_id, ic_roles
|
|
47
|
+
* FROM shield_identity.identity_credential
|
|
48
|
+
* WHERE ic_identity_hint = ? LIMIT 1` (parameterized — NEVER string-built).
|
|
49
|
+
* 2. Return `null` when NO row matches — the ONLY "not found" signal
|
|
50
|
+
* (anti-enumeration; MUST NOT throw for an unknown hint; the engine burns a
|
|
51
|
+
* decoy verify on `null` so timing is uniform).
|
|
52
|
+
* 3. Map a found row → `ResolvedCredential`:
|
|
53
|
+
* passwordHash = ic_password_hash
|
|
54
|
+
* claims = { id: ic_principal_id, tenantId: ic_tenant_id,
|
|
55
|
+
* roleIds: <parsed ic_roles> } // canonical Internal claims
|
|
56
|
+
* requiredFactors = [] // ALWAYS empty — password-only default (see above)
|
|
57
|
+
* 4. NEVER log the hash or the raw row.
|
|
58
|
+
* 5. A store outage (connection/query error) MAY throw — the engine treats a
|
|
59
|
+
* resolver throw as fail-closed `authentication_unavailable` (never a leak).
|
|
60
|
+
* It MUST NOT convert a throw into a `null` (that would make an outage look
|
|
61
|
+
* like "unknown user").
|
|
62
|
+
*
|
|
63
|
+
* (`claims.kind` is optional — the Internal adapter defaults an absent kind; add
|
|
64
|
+
* an `ic_kind` column later if a deployment needs non-`user` principals. Do NOT
|
|
65
|
+
* put any root flag in the row — root is the broker's server-side
|
|
66
|
+
* allowlist, never a claim, per core/identity.)
|
|
67
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
68
|
+
*
|
|
69
|
+
* ADR-0003: the concrete DB lives in guard (Core stays zero-dep). ADR-0013/0014:
|
|
70
|
+
* reads `shield_identity` via the hot-path `shield_core` account.
|
|
71
|
+
*/
|
|
72
|
+
import type { Pool } from 'mariadb';
|
|
73
|
+
import type { CredentialResolver, ResolvedCredential } from '@astram/core';
|
|
74
|
+
/** DI token for the resolver — re-exported from the shared auth tokens module. */
|
|
75
|
+
export { SHIELD_CREDENTIAL_RESOLVER } from '../auth/tokens.js';
|
|
76
|
+
/**
|
|
77
|
+
* The concrete MariaDB-backed {@link CredentialResolver}. Constructed with the
|
|
78
|
+
* live `shield_core` {@link Pool} (via {@link SHIELD_CREDENTIAL_RESOLVER}'s
|
|
79
|
+
* factory, which injects {@link SHIELD_DB_POOL}). Stateless per call apart from
|
|
80
|
+
* the injected pool.
|
|
81
|
+
*
|
|
82
|
+
* ⛔ SKELETON: `resolve()` throws until Team A implements the SELECT above.
|
|
83
|
+
*/
|
|
84
|
+
export declare class MariaDbCredentialResolver implements CredentialResolver {
|
|
85
|
+
private readonly pool;
|
|
86
|
+
constructor(pool: Pool);
|
|
87
|
+
/**
|
|
88
|
+
* Resolve `identityHint` → {@link ResolvedCredential}, or `null` when no such
|
|
89
|
+
* principal exists.
|
|
90
|
+
*
|
|
91
|
+
* - Parameterized SELECT (`WHERE ic_identity_hint = ? LIMIT 1`) — never
|
|
92
|
+
* string-built, so the hint can never inject SQL.
|
|
93
|
+
* - **No row ⇒ `null`** — the ONLY "not found" signal (anti-enumeration; the
|
|
94
|
+
* engine burns a decoy password verify on `null` so an unknown user is
|
|
95
|
+
* indistinguishable from a wrong password by content or timing). We do NOT
|
|
96
|
+
* throw for an unknown hint.
|
|
97
|
+
* - A **query/connection ERROR propagates** (rejects) — an outage is NOT an
|
|
98
|
+
* unknown user; the engine treats a resolver throw as fail-closed
|
|
99
|
+
* `authentication_unavailable`. We never convert an error into `null` (that
|
|
100
|
+
* would mask an outage AND make it look like "no such user").
|
|
101
|
+
* - The password hash is NEVER logged; the raw row is never logged. On success
|
|
102
|
+
* it is returned to the engine as a verify input only.
|
|
103
|
+
* - **Password-only:** `requiredFactors` is ALWAYS `[]`. This default provides
|
|
104
|
+
* no second-factor challenge; MFA-at-login is delivered by a consumer-plugged
|
|
105
|
+
* `credentialResolver` (+ `MfaDeviceStore`) — see the class doc header and
|
|
106
|
+
* astram3's `IdentityCredentialResolver`.
|
|
107
|
+
*/
|
|
108
|
+
resolve(identityHint: string): Promise<ResolvedCredential | null>;
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=mariadb-credential-resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mariadb-credential-resolver.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-credential-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsEG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EACV,kBAAkB,EAElB,kBAAkB,EACnB,MAAM,cAAc,CAAC;AAEtB,kFAAkF;AAClF,OAAO,EAAE,0BAA0B,EAAE,MAAM,mBAAmB,CAAC;AAqD/D;;;;;;;GAOG;AACH,qBAAa,yBAA0B,YAAW,kBAAkB;IACtD,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,IAAI;IAEvC;;;;;;;;;;;;;;;;;;;;OAoBG;IACG,OAAO,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;CAgCxE"}
|