@astram/guard 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/auth.controller.d.ts +102 -0
- package/dist/auth/auth.controller.d.ts.map +1 -0
- package/dist/auth/auth.controller.js +313 -0
- package/dist/auth/auth.module.d.ts +104 -0
- package/dist/auth/auth.module.d.ts.map +1 -0
- package/dist/auth/auth.module.js +318 -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/dto.d.ts +144 -0
- package/dist/auth/dto.d.ts.map +1 -0
- package/dist/auth/dto.js +29 -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/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/tokens.d.ts +62 -0
- package/dist/auth/tokens.d.ts.map +1 -0
- package/dist/auth/tokens.js +65 -0
- package/dist/authentication/mariadb-credential-resolver.d.ts +96 -0
- package/dist/authentication/mariadb-credential-resolver.d.ts.map +1 -0
- package/dist/authentication/mariadb-credential-resolver.js +156 -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/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +46 -1
- package/dist/module/shield.module.d.ts +51 -0
- package/dist/module/shield.module.d.ts.map +1 -1
- package/dist/module/shield.module.js +65 -4
- package/package.json +1 -1
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `AuthModule` — the Phase 0b **composition seam** that assembles the core auth
|
|
4
|
+
* engines from guard's adapters + the live connections, and exposes them under
|
|
5
|
+
* {@link SHIELD_AUTHENTICATOR} / {@link SHIELD_SESSION_AUTHORITY} for Team C's
|
|
6
|
+
* `/auth/*` controllers (spec §4).
|
|
7
|
+
*
|
|
8
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
* Team B (connection + composition) OWNS this file. It wires the frozen tokens to
|
|
10
|
+
* the real engines:
|
|
11
|
+
*
|
|
12
|
+
* SHIELD_PASSWORD_HASHER → new Argon2idPasswordHasher() (the flagged fix —
|
|
13
|
+
* the authenticator factory injects it, so THIS module must provide it).
|
|
14
|
+
* SHIELD_CREDENTIAL_RESOLVER → new MariaDbCredentialResolver(pool) (Team A's
|
|
15
|
+
* class; its `resolve()` body is Team A's — until then it throws not-implemented,
|
|
16
|
+
* which the engine treats as fail-closed `authentication_unavailable`).
|
|
17
|
+
* SHIELD_SESSION_RECORD_STORE → new MariaDbSessionRecordStore(pool).
|
|
18
|
+
* SHIELD_HOT_SESSION_STORE → new RedisHotSessionStore(redis).
|
|
19
|
+
* SHIELD_TOKEN_GENERATOR → new CryptoTokenGenerator().
|
|
20
|
+
* SHIELD_CLOCK → new SystemClock().
|
|
21
|
+
* SHIELD_REFRESH_HASHER → new Sha256RefreshTokenHasher(
|
|
22
|
+
* assertRefreshHmacKey(config.refreshHmacKey)) ⛔ FAIL-CLOSED gate:
|
|
23
|
+
* `assertRefreshHmacKey` THROWS on a blank key, so auth can NEVER compose with
|
|
24
|
+
* an unkeyed refresh-token hasher. This is the boot-time fail-closed point for
|
|
25
|
+
* the required secret (loadAppConfig deliberately does NOT throw, so a
|
|
26
|
+
* non-auth scaffold still boots with no `.env` — see app-config.ts).
|
|
27
|
+
*
|
|
28
|
+
* SHIELD_SESSION_AUTHORITY → createSessionAuthority({ recordStore, hotStore,
|
|
29
|
+
* tokenGenerator, refreshHasher, clock, lifetimes: config.lifetimes }).
|
|
30
|
+
* `createSessionAuthority` validates lifetime bounds eagerly (throws LOUD on a
|
|
31
|
+
* nonsensical config — construction-time, correct).
|
|
32
|
+
*
|
|
33
|
+
* SHIELD_AUTHENTICATOR → createAuthenticator({ hasher, resolver,
|
|
34
|
+
* identityBroker: createIdentityBroker(), // default Internal, empty
|
|
35
|
+
* factorVerifiers: [ createTotpFactorVerifier({ hmacSha1 }) ] }). The TOTP
|
|
36
|
+
* verifier is wired so MFA works; `hmacSha1` is the {@link hmacSha1} node:crypto
|
|
37
|
+
* one-liner (guard owns node:crypto, core does not). `createIdentityBroker()`
|
|
38
|
+
* with no args registers the default Internal adapter with an EMPTY superuser
|
|
39
|
+
* allowlist (default-deny).
|
|
40
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
41
|
+
*
|
|
42
|
+
* ⛔ OPT-IN (how the integration owner gates this — for the no-`.env` boot):
|
|
43
|
+
* AuthModule opens live connections (via PersistenceModule) and fires the
|
|
44
|
+
* fail-closed `assertRefreshHmacKey` gate the moment it composes. So it MUST be
|
|
45
|
+
* imported ONLY when a deployment wants auth. `ShieldModule.forRoot(options)`
|
|
46
|
+
* should conditionally add `AuthModule.forRoot()` to its `imports` gated behind an
|
|
47
|
+
* option (e.g. `ShieldModule.forRoot({ auth: true })`) or an env flag:
|
|
48
|
+
*
|
|
49
|
+
* imports: [
|
|
50
|
+
* EnforcementModule, BootModule,
|
|
51
|
+
* ...(options?.auth ? [AuthModule.forRoot()] : []), // ← opt-in
|
|
52
|
+
* ],
|
|
53
|
+
* exports: [APP_CONFIG, SHIELD_DECISION_AUTHORITY,
|
|
54
|
+
* ...(options?.auth ? [SHIELD_AUTHENTICATOR, SHIELD_SESSION_AUTHORITY] : [])],
|
|
55
|
+
*
|
|
56
|
+
* An enforcement-only app that does NOT opt in never imports AuthModule → never
|
|
57
|
+
* opens a pool/redis → never calls `assertRefreshHmacKey` → keeps the no-`.env`
|
|
58
|
+
* boot. An auth-enabled app fails LOUD at boot if `SHIELD_REFRESH_HMAC_KEY` is
|
|
59
|
+
* blank — auth can never compose unkeyed. `APP_CONFIG` is already `@Global` from
|
|
60
|
+
* `ShieldModule`, so AuthModule's config-injecting factories resolve. See DONE.md.
|
|
61
|
+
*
|
|
62
|
+
* ADR-0003: the core factories (`createAuthenticator`/`createSessionAuthority`/
|
|
63
|
+
* `createIdentityBroker`/`createTotpFactorVerifier`) are pure; the adapters + the
|
|
64
|
+
* node:crypto HMAC live in guard. Core stays zero-dep + unchanged.
|
|
65
|
+
*/
|
|
66
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
67
|
+
if (k2 === undefined) k2 = k;
|
|
68
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
69
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
70
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
71
|
+
}
|
|
72
|
+
Object.defineProperty(o, k2, desc);
|
|
73
|
+
}) : (function(o, m, k, k2) {
|
|
74
|
+
if (k2 === undefined) k2 = k;
|
|
75
|
+
o[k2] = m[k];
|
|
76
|
+
}));
|
|
77
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
78
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
79
|
+
}) : function(o, v) {
|
|
80
|
+
o["default"] = v;
|
|
81
|
+
});
|
|
82
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
83
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
84
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
85
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
86
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
87
|
+
};
|
|
88
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
89
|
+
var ownKeys = function(o) {
|
|
90
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
91
|
+
var ar = [];
|
|
92
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
93
|
+
return ar;
|
|
94
|
+
};
|
|
95
|
+
return ownKeys(o);
|
|
96
|
+
};
|
|
97
|
+
return function (mod) {
|
|
98
|
+
if (mod && mod.__esModule) return mod;
|
|
99
|
+
var result = {};
|
|
100
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
101
|
+
__setModuleDefault(result, mod);
|
|
102
|
+
return result;
|
|
103
|
+
};
|
|
104
|
+
})();
|
|
105
|
+
var AuthModule_1;
|
|
106
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
107
|
+
exports.AuthModule = exports.hmacSha1 = void 0;
|
|
108
|
+
const common_1 = require("@nestjs/common");
|
|
109
|
+
const node_crypto_1 = require("node:crypto");
|
|
110
|
+
const app_config_js_1 = require("../config/app-config.js");
|
|
111
|
+
const config_module_js_1 = require("../config/config.module.js");
|
|
112
|
+
const argon2id_password_hasher_js_1 = require("../authentication/argon2id-password-hasher.js");
|
|
113
|
+
const mariadb_credential_resolver_js_1 = require("../authentication/mariadb-credential-resolver.js");
|
|
114
|
+
const index_js_1 = require("../session/index.js");
|
|
115
|
+
const persistence_module_js_1 = require("./persistence.module.js");
|
|
116
|
+
const auth_controller_js_1 = require("./auth.controller.js");
|
|
117
|
+
const pending_mfa_store_js_1 = require("./pending-mfa-store.js");
|
|
118
|
+
const tokens_js_1 = require("./tokens.js");
|
|
119
|
+
/**
|
|
120
|
+
* The injected HMAC-SHA1 primitive the core TOTP verifier needs (Core is zero-dep
|
|
121
|
+
* + node-type-free, so it cannot import `node:crypto`). Guard owns node:crypto and
|
|
122
|
+
* supplies this one-liner: raw key bytes + message bytes → the 20-byte HMAC-SHA1
|
|
123
|
+
* digest. Never logs the key or message.
|
|
124
|
+
*/
|
|
125
|
+
const hmacSha1 = (key, message) => new Uint8Array((0, node_crypto_1.createHmac)('sha1', Buffer.from(key)).update(Buffer.from(message)).digest());
|
|
126
|
+
exports.hmacSha1 = hmacSha1;
|
|
127
|
+
/**
|
|
128
|
+
* A `Function`-wrapped dynamic `import()` that survives the CommonJS **build**.
|
|
129
|
+
* Under `module: commonjs`, `tsc` rewrites a literal `import()` into
|
|
130
|
+
* `Promise.resolve().then(() => require(...))` — which fails on the ESM-only core.
|
|
131
|
+
* Routing the import through the `Function` constructor hides it from that
|
|
132
|
+
* transform, so the emitted dist keeps a genuine native `import()` and Node's
|
|
133
|
+
* CJS→ESM interop loads core at runtime. (This path is used by the BUILT dist —
|
|
134
|
+
* e.g. example-backend. It does NOT work under vitest, whose sandbox can't resolve
|
|
135
|
+
* a specifier from an eval'd context — hence the two-strategy loadCore() below.)
|
|
136
|
+
*/
|
|
137
|
+
const evalImport = new Function('specifier', 'return import(specifier);');
|
|
138
|
+
let cachedCore;
|
|
139
|
+
/**
|
|
140
|
+
* Lazily load `@astram/core`'s four auth factory functions — invoked ONLY inside
|
|
141
|
+
* the two async provider factories (i.e. only when `forRoot()` actually composes
|
|
142
|
+
* auth), NEVER at module top-level. This keeps a runtime `require('@astram/core')`
|
|
143
|
+
* out of the guard barrel's load path (see the top-of-file note), so importing
|
|
144
|
+
* `@astram/guard`'s built dist for the enforcement-only path does not touch core.
|
|
145
|
+
*
|
|
146
|
+
* TWO strategies, so it works in BOTH runtimes:
|
|
147
|
+
* 1. A plain dynamic `import('@astram/core')` — the vitest/SWC source path keeps
|
|
148
|
+
* this native, and vitest's resolver maps the workspace package. (In the
|
|
149
|
+
* tsc-built CJS dist this literal is downleveled to a `require`, which throws on
|
|
150
|
+
* the ESM core — caught below.)
|
|
151
|
+
* 2. On failure, the {@link evalImport} `Function`-wrapped native `import()` — the
|
|
152
|
+
* BUILT dist path (Node CJS→ESM interop), which vitest can't use.
|
|
153
|
+
* Cached after the first success.
|
|
154
|
+
*/
|
|
155
|
+
async function loadCore() {
|
|
156
|
+
if (cachedCore !== undefined)
|
|
157
|
+
return cachedCore;
|
|
158
|
+
try {
|
|
159
|
+
// Strategy 1 — native in vitest/SWC; a `require` (that may throw) in the CJS dist.
|
|
160
|
+
cachedCore = (await Promise.resolve().then(() => __importStar(require('@astram/core'))));
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// Strategy 2 — the built-dist path: a genuine `import()` via the Function shim.
|
|
164
|
+
cachedCore = (await evalImport('@astram/core'));
|
|
165
|
+
}
|
|
166
|
+
return cachedCore;
|
|
167
|
+
}
|
|
168
|
+
// --- Adapter-backed providers (Team A resolver + session adapters) ----------
|
|
169
|
+
/** Argon2id password hasher (⛔ the flagged fix — provide it HERE so the authenticator factory's `inject` resolves). */
|
|
170
|
+
const passwordHasherProvider = {
|
|
171
|
+
provide: argon2id_password_hasher_js_1.SHIELD_PASSWORD_HASHER,
|
|
172
|
+
useFactory: () => new argon2id_password_hasher_js_1.Argon2idPasswordHasher(),
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* Credential resolver (the store login checks against).
|
|
176
|
+
*
|
|
177
|
+
* DEFAULT (no consumer override): a `useFactory` that constructs the framework's
|
|
178
|
+
* {@link MariaDbCredentialResolver} injected with the live {@link SHIELD_DB_POOL}
|
|
179
|
+
* — the historical binding, unchanged.
|
|
180
|
+
*
|
|
181
|
+
* OVERRIDE (Phase 1a seam): when the consumer supplies a `credentialResolver`
|
|
182
|
+
* through {@link ShieldModuleOptions.credentialResolver} → {@link AuthModule.forRoot},
|
|
183
|
+
* the token instead binds a plain **value provider** to that instance, so login
|
|
184
|
+
* authenticates against the consumer's OWN store (e.g. a richer users/roles model)
|
|
185
|
+
* WITHOUT editing the framework. The seam swaps the STORE only — the supplied
|
|
186
|
+
* resolver still flows through the SAME composed authenticator (anti-enumeration
|
|
187
|
+
* decoy verify, MFA, no-secret-leak); its `null`/throw contract is unchanged.
|
|
188
|
+
*/
|
|
189
|
+
function makeCredentialResolverProvider(override) {
|
|
190
|
+
return override
|
|
191
|
+
? { provide: tokens_js_1.SHIELD_CREDENTIAL_RESOLVER, useValue: override }
|
|
192
|
+
: {
|
|
193
|
+
provide: tokens_js_1.SHIELD_CREDENTIAL_RESOLVER,
|
|
194
|
+
useFactory: (pool) => new mariadb_credential_resolver_js_1.MariaDbCredentialResolver(pool),
|
|
195
|
+
inject: [tokens_js_1.SHIELD_DB_POOL],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/** Durable session record store (MariaDB adapter, injected with the live pool). */
|
|
199
|
+
const sessionRecordStoreProvider = {
|
|
200
|
+
provide: index_js_1.SHIELD_SESSION_RECORD_STORE,
|
|
201
|
+
useFactory: (pool) => new index_js_1.MariaDbSessionRecordStore(pool),
|
|
202
|
+
inject: [tokens_js_1.SHIELD_DB_POOL],
|
|
203
|
+
};
|
|
204
|
+
/** Hot session store (Redis adapter, injected with the live client). */
|
|
205
|
+
const hotSessionStoreProvider = {
|
|
206
|
+
provide: index_js_1.SHIELD_HOT_SESSION_STORE,
|
|
207
|
+
useFactory: (redis) => new index_js_1.RedisHotSessionStore(redis),
|
|
208
|
+
inject: [tokens_js_1.SHIELD_REDIS],
|
|
209
|
+
};
|
|
210
|
+
/** Opaque token generator (node:crypto adapter). */
|
|
211
|
+
const tokenGeneratorProvider = {
|
|
212
|
+
provide: index_js_1.SHIELD_TOKEN_GENERATOR,
|
|
213
|
+
useFactory: () => new index_js_1.CryptoTokenGenerator(),
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* Keyed refresh hasher (HMAC adapter). ⛔ FAIL-CLOSED HMAC-key gate:
|
|
217
|
+
* `assertRefreshHmacKey` THROWS on a blank key, so auth can never compose with an
|
|
218
|
+
* unkeyed refresh-token hasher.
|
|
219
|
+
*/
|
|
220
|
+
const refreshHasherProvider = {
|
|
221
|
+
provide: index_js_1.SHIELD_REFRESH_HASHER,
|
|
222
|
+
useFactory: (config) => new index_js_1.Sha256RefreshTokenHasher((0, app_config_js_1.assertRefreshHmacKey)(config.refreshHmacKey)),
|
|
223
|
+
inject: [config_module_js_1.APP_CONFIG],
|
|
224
|
+
};
|
|
225
|
+
/** System clock. */
|
|
226
|
+
const clockProvider = {
|
|
227
|
+
provide: index_js_1.SHIELD_CLOCK,
|
|
228
|
+
useFactory: () => new index_js_1.SystemClock(),
|
|
229
|
+
};
|
|
230
|
+
// --- Composed core engines --------------------------------------------------
|
|
231
|
+
/** The composed SessionAuthority (core) — establish / rotate / revoke. */
|
|
232
|
+
const sessionAuthorityProvider = {
|
|
233
|
+
provide: tokens_js_1.SHIELD_SESSION_AUTHORITY,
|
|
234
|
+
// async: loads the core factory lazily (CJS→ESM, see loadCore()); Nest awaits it.
|
|
235
|
+
useFactory: async (recordStore, hotStore, tokenGenerator, refreshHasher, clock, config) => {
|
|
236
|
+
const { createSessionAuthority } = await loadCore();
|
|
237
|
+
return createSessionAuthority({
|
|
238
|
+
recordStore,
|
|
239
|
+
hotStore,
|
|
240
|
+
tokenGenerator,
|
|
241
|
+
refreshHasher,
|
|
242
|
+
clock,
|
|
243
|
+
lifetimes: config.lifetimes,
|
|
244
|
+
});
|
|
245
|
+
},
|
|
246
|
+
inject: [
|
|
247
|
+
index_js_1.SHIELD_SESSION_RECORD_STORE,
|
|
248
|
+
index_js_1.SHIELD_HOT_SESSION_STORE,
|
|
249
|
+
index_js_1.SHIELD_TOKEN_GENERATOR,
|
|
250
|
+
index_js_1.SHIELD_REFRESH_HASHER,
|
|
251
|
+
index_js_1.SHIELD_CLOCK,
|
|
252
|
+
config_module_js_1.APP_CONFIG,
|
|
253
|
+
],
|
|
254
|
+
};
|
|
255
|
+
/** The composed Authenticator (core) — password + TOTP factor, anti-enumeration. */
|
|
256
|
+
const authenticatorProvider = {
|
|
257
|
+
provide: tokens_js_1.SHIELD_AUTHENTICATOR,
|
|
258
|
+
// async: loads the core factories lazily (CJS→ESM, see loadCore()); Nest awaits it.
|
|
259
|
+
useFactory: async (hasher, resolver) => {
|
|
260
|
+
const { createAuthenticator, createIdentityBroker, createTotpFactorVerifier } = await loadCore();
|
|
261
|
+
return createAuthenticator({
|
|
262
|
+
hasher,
|
|
263
|
+
resolver,
|
|
264
|
+
identityBroker: createIdentityBroker(),
|
|
265
|
+
factorVerifiers: [createTotpFactorVerifier({ hmacSha1: exports.hmacSha1 })],
|
|
266
|
+
});
|
|
267
|
+
},
|
|
268
|
+
inject: [argon2id_password_hasher_js_1.SHIELD_PASSWORD_HASHER, tokens_js_1.SHIELD_CREDENTIAL_RESOLVER],
|
|
269
|
+
};
|
|
270
|
+
/**
|
|
271
|
+
* The auth composition module. Imports {@link PersistenceModule} (the live
|
|
272
|
+
* connections) and provides the adapters + composed engines. Exports the two
|
|
273
|
+
* engines (Team C consumes them) and the resolver token. Opt-in: imported ONLY by
|
|
274
|
+
* an auth-enabled `ShieldModule.forRoot({ auth: true })` — see the file header +
|
|
275
|
+
* DONE.md for the exact gating the integration owner applies.
|
|
276
|
+
*/
|
|
277
|
+
let AuthModule = AuthModule_1 = class AuthModule {
|
|
278
|
+
/**
|
|
279
|
+
* Compose the auth surface. `options.credentialResolver`, when supplied, REPLACES
|
|
280
|
+
* the default MariaDb store with the consumer's own (a value provider under
|
|
281
|
+
* {@link SHIELD_CREDENTIAL_RESOLVER}); omitted → the historical default binding,
|
|
282
|
+
* EXACTLY unchanged (zero behavior change for existing consumers).
|
|
283
|
+
*/
|
|
284
|
+
static forRoot(options = {}) {
|
|
285
|
+
return {
|
|
286
|
+
module: AuthModule_1,
|
|
287
|
+
imports: [persistence_module_js_1.PersistenceModule.forRoot()],
|
|
288
|
+
// Team C's four `@Public()` /auth/* endpoints. Registered here (integration
|
|
289
|
+
// wiring) so the routes exist the moment auth is opted in — the controller
|
|
290
|
+
// injects the two composed engines (below) + PendingMfaStore.
|
|
291
|
+
controllers: [auth_controller_js_1.AuthController],
|
|
292
|
+
providers: [
|
|
293
|
+
passwordHasherProvider,
|
|
294
|
+
makeCredentialResolverProvider(options.credentialResolver),
|
|
295
|
+
sessionRecordStoreProvider,
|
|
296
|
+
hotSessionStoreProvider,
|
|
297
|
+
tokenGeneratorProvider,
|
|
298
|
+
refreshHasherProvider,
|
|
299
|
+
clockProvider,
|
|
300
|
+
sessionAuthorityProvider,
|
|
301
|
+
authenticatorProvider,
|
|
302
|
+
// Team C's server-side, single-use, TTL'd challenge store backing the
|
|
303
|
+
// login→mfa handoff (a plain class provider the controller injects).
|
|
304
|
+
pending_mfa_store_js_1.PendingMfaStore,
|
|
305
|
+
],
|
|
306
|
+
exports: [
|
|
307
|
+
tokens_js_1.SHIELD_AUTHENTICATOR,
|
|
308
|
+
tokens_js_1.SHIELD_SESSION_AUTHORITY,
|
|
309
|
+
tokens_js_1.SHIELD_CREDENTIAL_RESOLVER,
|
|
310
|
+
argon2id_password_hasher_js_1.SHIELD_PASSWORD_HASHER,
|
|
311
|
+
],
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
exports.AuthModule = AuthModule;
|
|
316
|
+
exports.AuthModule = AuthModule = AuthModule_1 = __decorate([
|
|
317
|
+
(0, common_1.Module)({})
|
|
318
|
+
], AuthModule);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live-connection factories + a shutdown registrar for the Phase 0b auth surface
|
|
3
|
+
* (Team B). This is the ONE module in the framework that opens a real MariaDB pool
|
|
4
|
+
* and a real Redis client (spec §4); everything auth persists (sessions,
|
|
5
|
+
* credentials, hot state) flows through the two connections created here.
|
|
6
|
+
*
|
|
7
|
+
* WHY a separate helper (not inline in the provider factory): a Nest `useFactory`
|
|
8
|
+
* value provider is NOT itself lifecycle-aware — `onModuleDestroy` only fires on
|
|
9
|
+
* PROVIDER CLASSES with that method. So the pool/redis are created by the pure
|
|
10
|
+
* factories below, and a tiny {@link ConnectionLifecycle} provider (a class Nest
|
|
11
|
+
* WILL call `onModuleDestroy` on) holds their handles and closes them on shutdown.
|
|
12
|
+
* This keeps `SHIELD_DB_POOL` / `SHIELD_REDIS` as the raw driver objects the
|
|
13
|
+
* adapters expect (`new MariaDbSessionRecordStore(pool)` etc.) while still getting
|
|
14
|
+
* graceful teardown.
|
|
15
|
+
*
|
|
16
|
+
* ⛔ SECURITY — never logs a credential. The DB password + Redis url (which may
|
|
17
|
+
* carry a password) are handed straight to the drivers; nothing here stringifies
|
|
18
|
+
* or logs the config.
|
|
19
|
+
*
|
|
20
|
+
* ADR-0003: the real `mariadb` / `ioredis` live in guard (Core stays zero-dep).
|
|
21
|
+
* ADR-0018: the pool cap is taken from the connection-pool budget
|
|
22
|
+
* (`config.pool.perAccountMax.shield_core`), NOT a duplicate knob.
|
|
23
|
+
*/
|
|
24
|
+
import { type OnModuleDestroy } from '@nestjs/common';
|
|
25
|
+
import { type Pool } from 'mariadb';
|
|
26
|
+
import Redis from 'ioredis';
|
|
27
|
+
import type { AppConfig } from '../config/app-config.js';
|
|
28
|
+
/**
|
|
29
|
+
* Create the ONE live `mariadb` {@link Pool} for the `shield_core` account from
|
|
30
|
+
* {@link AppConfig}. The `connectionLimit` comes from the ADR-0018 budget
|
|
31
|
+
* (`config.pool.perAccountMax.shield_core`) so per-worker pools stay within the DB
|
|
32
|
+
* `max_connections` ceiling — it is NOT a separate configurable.
|
|
33
|
+
*
|
|
34
|
+
* A blank `db.password` is ALLOWED here (it is not hard-required at config load);
|
|
35
|
+
* a wrong/blank password fails LOUD at connect time, which is the correct place
|
|
36
|
+
* for a runtime credential error. The host/port/user/database always have
|
|
37
|
+
* documented defaults, so the descriptor is always well-formed.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createDbPool(config: AppConfig): Pool;
|
|
40
|
+
/**
|
|
41
|
+
* Create the ONE live `ioredis` client (the hot session tier, ADR-0013) from
|
|
42
|
+
* {@link AppConfig}. `config.redis.url` is the canonical `ioredis`-native form
|
|
43
|
+
* (`redis://[:pw@]host:port[/db]`) with a documented default. Lazy-connect is left
|
|
44
|
+
* at ioredis's default (connects on first command); a dead Redis surfaces as the
|
|
45
|
+
* `unavailable` hot-store outcome (fail-closed), never a silent `absent`.
|
|
46
|
+
*/
|
|
47
|
+
export declare function createRedisClient(config: AppConfig): Redis;
|
|
48
|
+
/**
|
|
49
|
+
* Lifecycle-aware holder that owns the pool + redis handles and closes them on
|
|
50
|
+
* app shutdown. Provided ALONGSIDE the two raw-connection providers so Nest calls
|
|
51
|
+
* `onModuleDestroy` here (value providers get no lifecycle callback). The teardown
|
|
52
|
+
* is best-effort + idempotent: a close error is swallowed so shutdown never hangs
|
|
53
|
+
* or throws, and both connections are always attempted.
|
|
54
|
+
*/
|
|
55
|
+
export declare class ConnectionLifecycle implements OnModuleDestroy {
|
|
56
|
+
private readonly pool;
|
|
57
|
+
private readonly redis;
|
|
58
|
+
constructor(pool: Pool, redis: Redis);
|
|
59
|
+
onModuleDestroy(): Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=connections.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connections.d.ts","sourceRoot":"","sources":["../../src/auth/connections.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAc,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAClE,OAAgB,EAAE,KAAK,IAAI,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,KAAK,MAAM,SAAS,CAAC;AAC5B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEzD;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAcpD;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,GAAG,KAAK,CAI1D;AAED;;;;;;GAMG;AACH,qBACa,mBAAoB,YAAW,eAAe;IAEvD,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,KAAK;gBADL,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,KAAK;IAGzB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;CAavC"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Live-connection factories + a shutdown registrar for the Phase 0b auth surface
|
|
4
|
+
* (Team B). This is the ONE module in the framework that opens a real MariaDB pool
|
|
5
|
+
* and a real Redis client (spec §4); everything auth persists (sessions,
|
|
6
|
+
* credentials, hot state) flows through the two connections created here.
|
|
7
|
+
*
|
|
8
|
+
* WHY a separate helper (not inline in the provider factory): a Nest `useFactory`
|
|
9
|
+
* value provider is NOT itself lifecycle-aware — `onModuleDestroy` only fires on
|
|
10
|
+
* PROVIDER CLASSES with that method. So the pool/redis are created by the pure
|
|
11
|
+
* factories below, and a tiny {@link ConnectionLifecycle} provider (a class Nest
|
|
12
|
+
* WILL call `onModuleDestroy` on) holds their handles and closes them on shutdown.
|
|
13
|
+
* This keeps `SHIELD_DB_POOL` / `SHIELD_REDIS` as the raw driver objects the
|
|
14
|
+
* adapters expect (`new MariaDbSessionRecordStore(pool)` etc.) while still getting
|
|
15
|
+
* graceful teardown.
|
|
16
|
+
*
|
|
17
|
+
* ⛔ SECURITY — never logs a credential. The DB password + Redis url (which may
|
|
18
|
+
* carry a password) are handed straight to the drivers; nothing here stringifies
|
|
19
|
+
* or logs the config.
|
|
20
|
+
*
|
|
21
|
+
* ADR-0003: the real `mariadb` / `ioredis` live in guard (Core stays zero-dep).
|
|
22
|
+
* ADR-0018: the pool cap is taken from the connection-pool budget
|
|
23
|
+
* (`config.pool.perAccountMax.shield_core`), NOT a duplicate knob.
|
|
24
|
+
*/
|
|
25
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
26
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
27
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
28
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
29
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
30
|
+
};
|
|
31
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
32
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
33
|
+
};
|
|
34
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
35
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
36
|
+
};
|
|
37
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
+
exports.ConnectionLifecycle = void 0;
|
|
39
|
+
exports.createDbPool = createDbPool;
|
|
40
|
+
exports.createRedisClient = createRedisClient;
|
|
41
|
+
const common_1 = require("@nestjs/common");
|
|
42
|
+
const mariadb_1 = __importDefault(require("mariadb"));
|
|
43
|
+
const ioredis_1 = __importDefault(require("ioredis"));
|
|
44
|
+
/**
|
|
45
|
+
* Create the ONE live `mariadb` {@link Pool} for the `shield_core` account from
|
|
46
|
+
* {@link AppConfig}. The `connectionLimit` comes from the ADR-0018 budget
|
|
47
|
+
* (`config.pool.perAccountMax.shield_core`) so per-worker pools stay within the DB
|
|
48
|
+
* `max_connections` ceiling — it is NOT a separate configurable.
|
|
49
|
+
*
|
|
50
|
+
* A blank `db.password` is ALLOWED here (it is not hard-required at config load);
|
|
51
|
+
* a wrong/blank password fails LOUD at connect time, which is the correct place
|
|
52
|
+
* for a runtime credential error. The host/port/user/database always have
|
|
53
|
+
* documented defaults, so the descriptor is always well-formed.
|
|
54
|
+
*/
|
|
55
|
+
function createDbPool(config) {
|
|
56
|
+
const { db, pool } = config;
|
|
57
|
+
return mariadb_1.default.createPool({
|
|
58
|
+
host: db.host,
|
|
59
|
+
port: db.port,
|
|
60
|
+
user: db.user,
|
|
61
|
+
password: db.password,
|
|
62
|
+
database: db.database,
|
|
63
|
+
connectionLimit: pool.perAccountMax.shield_core,
|
|
64
|
+
// Fail fast on an unreachable DB rather than hanging the boot indefinitely.
|
|
65
|
+
connectTimeout: 10_000,
|
|
66
|
+
// All platform timestamps are UTC (parity with the session adapters).
|
|
67
|
+
timezone: 'Z',
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Create the ONE live `ioredis` client (the hot session tier, ADR-0013) from
|
|
72
|
+
* {@link AppConfig}. `config.redis.url` is the canonical `ioredis`-native form
|
|
73
|
+
* (`redis://[:pw@]host:port[/db]`) with a documented default. Lazy-connect is left
|
|
74
|
+
* at ioredis's default (connects on first command); a dead Redis surfaces as the
|
|
75
|
+
* `unavailable` hot-store outcome (fail-closed), never a silent `absent`.
|
|
76
|
+
*/
|
|
77
|
+
function createRedisClient(config) {
|
|
78
|
+
// `maxRetriesPerRequest: null` keeps a transient blip from turning into a thrown
|
|
79
|
+
// command (the hot-store adapter classifies unreachable as `unavailable`).
|
|
80
|
+
return new ioredis_1.default(config.redis.url, { maxRetriesPerRequest: null, lazyConnect: false });
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Lifecycle-aware holder that owns the pool + redis handles and closes them on
|
|
84
|
+
* app shutdown. Provided ALONGSIDE the two raw-connection providers so Nest calls
|
|
85
|
+
* `onModuleDestroy` here (value providers get no lifecycle callback). The teardown
|
|
86
|
+
* is best-effort + idempotent: a close error is swallowed so shutdown never hangs
|
|
87
|
+
* or throws, and both connections are always attempted.
|
|
88
|
+
*/
|
|
89
|
+
let ConnectionLifecycle = class ConnectionLifecycle {
|
|
90
|
+
pool;
|
|
91
|
+
redis;
|
|
92
|
+
constructor(pool, redis) {
|
|
93
|
+
this.pool = pool;
|
|
94
|
+
this.redis = redis;
|
|
95
|
+
}
|
|
96
|
+
async onModuleDestroy() {
|
|
97
|
+
// Close both; never let one failure prevent the other, never throw.
|
|
98
|
+
try {
|
|
99
|
+
await this.pool.end();
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// best-effort — a pool already ending / a mid-query close is non-fatal here.
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
await this.redis.quit();
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// best-effort — quit on an already-closed client is non-fatal.
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
exports.ConnectionLifecycle = ConnectionLifecycle;
|
|
113
|
+
exports.ConnectionLifecycle = ConnectionLifecycle = __decorate([
|
|
114
|
+
(0, common_1.Injectable)(),
|
|
115
|
+
__metadata("design:paramtypes", [Object, ioredis_1.default])
|
|
116
|
+
], ConnectionLifecycle);
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 0b Auth-surface **endpoint DTOs / view types** — the request/response
|
|
3
|
+
* SHAPES the four `@Public()` `/auth/*` endpoints speak (spec §2). ⛔ FROZEN by
|
|
4
|
+
* this prelude; **Team C builds controllers to these**. Types only — no NestJS /
|
|
5
|
+
* class-validator decorators here (the prelude froze the wire shape; Team C adds
|
|
6
|
+
* the validation decorators on its own DTO classes if it wants runtime
|
|
7
|
+
* validation, keeping this file framework-agnostic and importable anywhere).
|
|
8
|
+
*
|
|
9
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
10
|
+
* SECURITY RULES these shapes ENCODE (spec §2, ACs 2/3/6; enforce in controllers):
|
|
11
|
+
*
|
|
12
|
+
* • ANTI-ENUMERATION — `/auth/login` and `/auth/mfa` failure responses are a
|
|
13
|
+
* SINGLE generic 401 for {unknown user, wrong password, wrong/expired factor}.
|
|
14
|
+
* The response body MUST NOT distinguish which failed (the engine's decoy-verify
|
|
15
|
+
* already equalizes timing). NEVER add a `reason`/`field`/`code` that leaks
|
|
16
|
+
* "no such user" vs "bad password".
|
|
17
|
+
* • NO SECRET LEAK — NO response/log ever carries a password, a password hash, a
|
|
18
|
+
* TOTP secret, or a RAW refresh/access token OTHER than the two token fields a
|
|
19
|
+
* successful lifecycle event legitimately returns ({@link SessionTokensView}).
|
|
20
|
+
* The {@link SessionView} below deliberately OMITS every secret — it is the
|
|
21
|
+
* ONLY session shape a response body may embed.
|
|
22
|
+
* • FAIL-CLOSED — a store/transient failure on refresh is the ADR-0016 retryable
|
|
23
|
+
* signal (503-style), NOT a terminal deny/logout (see {@link RefreshResponse}).
|
|
24
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
*
|
|
26
|
+
* ADR-0003: framework-agnostic — no framework / HTTP / DB imports.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* The client-safe view of an established session. Carries ONLY non-secret
|
|
30
|
+
* lifecycle facts a caller may see. ⛔ It MUST NOT contain any token, hash, or
|
|
31
|
+
* secret — tokens travel ONLY in {@link SessionTokensView}. `principalId` /
|
|
32
|
+
* `tenantId` are opaque internal ids (not rendered to end users as UUIDs — the UI
|
|
33
|
+
* resolves display labels; these are for the client's own session bookkeeping).
|
|
34
|
+
*/
|
|
35
|
+
export interface SessionView {
|
|
36
|
+
readonly sessionId: string;
|
|
37
|
+
readonly principalId: string;
|
|
38
|
+
readonly tenantId: string;
|
|
39
|
+
readonly status: 'active' | 'rotated' | 'revoked' | 'expired';
|
|
40
|
+
/** UTC epoch ms. */
|
|
41
|
+
readonly issuedAtMs: number;
|
|
42
|
+
/** UTC epoch ms. */
|
|
43
|
+
readonly expiresAtMs: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The token pair minted at a session lifecycle event. ⛔ These are SECRETS in
|
|
47
|
+
* transit — returned to the caller EXACTLY at login / mfa-success / refresh, never
|
|
48
|
+
* persisted raw, never logged. The refresh token is stored server-side only as
|
|
49
|
+
* its keyed hash.
|
|
50
|
+
*/
|
|
51
|
+
export interface SessionTokensView {
|
|
52
|
+
readonly accessToken: string;
|
|
53
|
+
readonly refreshToken: string;
|
|
54
|
+
}
|
|
55
|
+
/** A successful session establishment payload (login / mfa / refresh success). */
|
|
56
|
+
export interface SessionSuccess {
|
|
57
|
+
readonly session: SessionView;
|
|
58
|
+
readonly tokens: SessionTokensView;
|
|
59
|
+
}
|
|
60
|
+
/** `POST /auth/login` request body. */
|
|
61
|
+
export interface LoginRequest {
|
|
62
|
+
/** Username or email — resolved to a principal by the credential resolver. */
|
|
63
|
+
readonly identityHint: string;
|
|
64
|
+
/** ⛔ The plaintext password — verified then discarded; NEVER logged/echoed. */
|
|
65
|
+
readonly password: string;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The MFA-challenge branch of a login/step-up response: the password was correct
|
|
69
|
+
* but a second factor is required, so NO session is established yet. The caller
|
|
70
|
+
* re-submits to `/auth/mfa` with the `challengeToken`. ⛔ `challengeToken` is a
|
|
71
|
+
* short-TTL opaque handle to the pending authentication — it is NOT a session
|
|
72
|
+
* token and grants no access on its own; it carries no secret about the user.
|
|
73
|
+
*/
|
|
74
|
+
export interface MfaChallengeResponse {
|
|
75
|
+
readonly mfaRequired: true;
|
|
76
|
+
readonly challengeToken: string;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* `POST /auth/login` response — a discriminated union:
|
|
80
|
+
* • {@link SessionSuccess} — password-only user, authenticated + session;
|
|
81
|
+
* • {@link MfaChallengeResponse} — MFA-enabled user, challenge issued (no session).
|
|
82
|
+
* A FAILURE (unknown user / wrong password) is NOT modeled here — it is a generic
|
|
83
|
+
* HTTP 401 with a non-distinguishing body (anti-enumeration; see file header).
|
|
84
|
+
*/
|
|
85
|
+
export type LoginResponse = SessionSuccess | MfaChallengeResponse;
|
|
86
|
+
/**
|
|
87
|
+
* `POST /auth/mfa` request body — completes a pending MFA challenge. `factor` is
|
|
88
|
+
* `'totp'` in Phase 0b (the only real factor; WebAuthn/recovery are contract slots
|
|
89
|
+
* only). `code` is the presented one-time code (digits). ⛔ `code` is single-use
|
|
90
|
+
* material — never logged.
|
|
91
|
+
*/
|
|
92
|
+
export interface MfaRequest {
|
|
93
|
+
/** The opaque handle from {@link MfaChallengeResponse}. Short-TTL, single-use. */
|
|
94
|
+
readonly challengeToken: string;
|
|
95
|
+
/** The second-factor class being satisfied (Phase 0b: `'totp'`). */
|
|
96
|
+
readonly factor: 'totp';
|
|
97
|
+
/** The presented one-time code. NEVER logged. */
|
|
98
|
+
readonly code: string;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* `POST /auth/mfa` response — on success, the session + tokens
|
|
102
|
+
* ({@link SessionSuccess}). A wrong/expired code is a generic HTTP 401 (no session,
|
|
103
|
+
* no leak of why), NOT modeled as a body variant.
|
|
104
|
+
*/
|
|
105
|
+
export type MfaResponse = SessionSuccess;
|
|
106
|
+
/**
|
|
107
|
+
* `POST /auth/refresh` request body. The refresh token may alternatively arrive
|
|
108
|
+
* via an HttpOnly cookie (Team C's transport choice) — the shape is the same.
|
|
109
|
+
* ⛔ Secret — never logged; server matches it by keyed hash only.
|
|
110
|
+
*/
|
|
111
|
+
export interface RefreshRequest {
|
|
112
|
+
readonly refreshToken: string;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* `POST /auth/refresh` response — new tokens + the (rotated) session on success
|
|
116
|
+
* ({@link SessionSuccess}).
|
|
117
|
+
*
|
|
118
|
+
* ⛔ TWO distinct failure modes the controller MUST keep separate (spec §2, AC 7):
|
|
119
|
+
* • reuse/invalid/expired token → generic `session_rejected` (HTTP 401), lineage
|
|
120
|
+
* killed (ADR-0015) — a TERMINAL deny.
|
|
121
|
+
* • transient store outage (hot store unavailable) → the ADR-0016 RETRYABLE
|
|
122
|
+
* signal (HTTP 503-style), NOT a deny/logout. Conflating the two either logs
|
|
123
|
+
* everyone out on a blip or fails open — both are bugs.
|
|
124
|
+
* Neither failure is a body variant here; both are HTTP-status outcomes with
|
|
125
|
+
* non-secret bodies.
|
|
126
|
+
*/
|
|
127
|
+
export type RefreshResponse = SessionSuccess;
|
|
128
|
+
/**
|
|
129
|
+
* `POST /auth/logout` request body. `sessionId` may alternatively be derived from
|
|
130
|
+
* the access token (Team C's choice). Idempotent — revoking an already-revoked /
|
|
131
|
+
* unknown session still succeeds (no existence leak).
|
|
132
|
+
*/
|
|
133
|
+
export interface LogoutRequest {
|
|
134
|
+
readonly sessionId: string;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* `POST /auth/logout` acknowledgement. The endpoint returns HTTP 204 (no body) on
|
|
138
|
+
* success; this type documents an OPTIONAL ack body if Team C prefers 200 — it
|
|
139
|
+
* carries NO secret and NO session detail (just a flag). Prefer 204 per spec §2.
|
|
140
|
+
*/
|
|
141
|
+
export interface LogoutAck {
|
|
142
|
+
readonly revoked: true;
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=dto.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dto.d.ts","sourceRoot":"","sources":["../../src/auth/dto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AASH;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;IAC9D,oBAAoB;IACpB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,oBAAoB;IACpB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED,kFAAkF;AAClF,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;CACpC;AAMD,uCAAuC;AACvC,MAAM,WAAW,YAAY;IAC3B,8EAA8E;IAC9E,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,+EAA+E;IAC/E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,WAAW,EAAE,IAAI,CAAC;IAC3B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAED;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,cAAc,GAAG,oBAAoB,CAAC;AAMlE;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,kFAAkF;IAClF,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,oEAAoE;IACpE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,iDAAiD;IACjD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;;;GAIG;AACH,MAAM,MAAM,WAAW,GAAG,cAAc,CAAC;AAMzC;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,eAAe,GAAG,cAAc,CAAC;AAM7C;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC;CACxB"}
|