@nage-api/auth 1.0.0-beta.2
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/LICENSE +202 -0
- package/README.md +176 -0
- package/dist/api-key.service.d.ts +50 -0
- package/dist/api-key.service.js +110 -0
- package/dist/auth.controller.d.ts +48 -0
- package/dist/auth.controller.js +185 -0
- package/dist/auth.dto.d.ts +37 -0
- package/dist/auth.dto.js +117 -0
- package/dist/auth.guard.d.ts +29 -0
- package/dist/auth.guard.js +122 -0
- package/dist/auth.module.d.ts +61 -0
- package/dist/auth.module.js +226 -0
- package/dist/auth.service.d.ts +82 -0
- package/dist/auth.service.js +269 -0
- package/dist/authorization.guard.d.ts +24 -0
- package/dist/authorization.guard.js +107 -0
- package/dist/config.d.ts +71 -0
- package/dist/config.js +151 -0
- package/dist/decorators.d.ts +51 -0
- package/dist/decorators.js +70 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +96 -0
- package/dist/jwt.d.ts +50 -0
- package/dist/jwt.js +163 -0
- package/dist/lockout.service.d.ts +43 -0
- package/dist/lockout.service.js +94 -0
- package/dist/memory-stores.d.ts +84 -0
- package/dist/memory-stores.js +246 -0
- package/dist/otp.service.d.ts +47 -0
- package/dist/otp.service.js +137 -0
- package/dist/password.d.ts +51 -0
- package/dist/password.js +122 -0
- package/dist/policy.d.ts +44 -0
- package/dist/policy.js +61 -0
- package/dist/ports.d.ts +175 -0
- package/dist/ports.js +17 -0
- package/dist/principal.resolver.d.ts +52 -0
- package/dist/principal.resolver.js +125 -0
- package/dist/session.service.d.ts +71 -0
- package/dist/session.service.js +175 -0
- package/dist/tokens.d.ts +22 -0
- package/dist/tokens.js +23 -0
- package/package.json +66 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Turning a verified token into the principal the request runs as
|
|
4
|
+
* (PLAN.md §15.1).
|
|
5
|
+
*
|
|
6
|
+
* Two decisions worth stating, because both are places auth packages commonly
|
|
7
|
+
* go wrong:
|
|
8
|
+
*
|
|
9
|
+
* **Roles come from the store, not from the token.** A signed token proves who
|
|
10
|
+
* the caller is; it does not prove what they may still do. Trusting its `roles`
|
|
11
|
+
* claim means a demotion or a disabled account keeps working until the token
|
|
12
|
+
* expires. Claims are carried for observability and compared, not obeyed.
|
|
13
|
+
*
|
|
14
|
+
* **A revoked session stops working at once.** The token carries the session
|
|
15
|
+
* *family* id, and a revoked family is refused — so logging out actually logs
|
|
16
|
+
* you out, rather than leaving a 15-minute window in which the access token
|
|
17
|
+
* still works.
|
|
18
|
+
*
|
|
19
|
+
* Both cost a lookup per request, so both are memoised behind a short TTL. The
|
|
20
|
+
* TTL is the tunable: a few seconds of staleness in exchange for not hitting
|
|
21
|
+
* the database on every call.
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.PrincipalResolver = void 0;
|
|
25
|
+
const core_1 = require("@nage-api/core");
|
|
26
|
+
const config_js_1 = require("./config.js");
|
|
27
|
+
const DEFAULT_CACHE_TTL_MS = 5_000;
|
|
28
|
+
const DEFAULT_CACHE_MAX_ENTRIES = 10_000;
|
|
29
|
+
class PrincipalResolver {
|
|
30
|
+
#users;
|
|
31
|
+
#sessions;
|
|
32
|
+
#roles;
|
|
33
|
+
#cacheTtlMs;
|
|
34
|
+
#cacheMaxEntries;
|
|
35
|
+
#clock;
|
|
36
|
+
#cache = new Map();
|
|
37
|
+
#revoked = new Map();
|
|
38
|
+
constructor(options) {
|
|
39
|
+
this.#users = options.users;
|
|
40
|
+
this.#sessions = options.sessions;
|
|
41
|
+
this.#roles = options.roles;
|
|
42
|
+
this.#cacheTtlMs = options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
43
|
+
this.#cacheMaxEntries = options.cacheMaxEntries ?? DEFAULT_CACHE_MAX_ENTRIES;
|
|
44
|
+
this.#clock = options.clock ?? { now: () => Date.now() };
|
|
45
|
+
}
|
|
46
|
+
async resolve(input) {
|
|
47
|
+
if (await this.#isRevoked(input.sessionId)) {
|
|
48
|
+
throw new core_1.AuthenticationError('AUTH_SESSION_REVOKED', {
|
|
49
|
+
detail: `Session family ${input.sessionId} was revoked`,
|
|
50
|
+
meta: { sessionId: input.sessionId },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
const record = await this.#load(input.userId);
|
|
54
|
+
if (record === undefined) {
|
|
55
|
+
// The token is valid but the account is gone. `AUTH_TOKEN_INVALID` rather
|
|
56
|
+
// than a 404: whether a user id exists is not a caller's business.
|
|
57
|
+
throw new core_1.AuthenticationError('AUTH_TOKEN_INVALID', {
|
|
58
|
+
detail: `Token subject ${input.userId} does not resolve to a user`,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (record.disabled === true) {
|
|
62
|
+
throw new core_1.AuthenticationError('AUTH_SESSION_REVOKED', {
|
|
63
|
+
detail: `User ${input.userId} is disabled`,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
id: record.id,
|
|
68
|
+
...(record.email === '' ? {} : { email: record.email }),
|
|
69
|
+
roles: record.roles,
|
|
70
|
+
permissions: (0, config_js_1.resolvePermissions)(this.#roles, record.roles, record.permissions ?? []),
|
|
71
|
+
sessionId: input.sessionId,
|
|
72
|
+
...(record.tenantId === undefined ? {} : { tenantId: record.tenantId }),
|
|
73
|
+
...(input.impersonatedBy === undefined ? {} : { impersonatedBy: input.impersonatedBy }),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** Drop a user's cached record — call after a role or status change. */
|
|
77
|
+
invalidate(userId) {
|
|
78
|
+
this.#cache.delete(userId);
|
|
79
|
+
}
|
|
80
|
+
/** Drop everything; used by tests and by an administrative refresh. */
|
|
81
|
+
clear() {
|
|
82
|
+
this.#cache.clear();
|
|
83
|
+
this.#revoked.clear();
|
|
84
|
+
}
|
|
85
|
+
async #load(userId) {
|
|
86
|
+
const now = this.#clock.now();
|
|
87
|
+
if (this.#cacheTtlMs > 0) {
|
|
88
|
+
const cached = this.#cache.get(userId);
|
|
89
|
+
if (cached !== undefined && cached.expiresAt > now)
|
|
90
|
+
return cached.record;
|
|
91
|
+
}
|
|
92
|
+
const record = await this.#users.findById(userId);
|
|
93
|
+
if (record !== undefined && this.#cacheTtlMs > 0) {
|
|
94
|
+
this.#evictIfFull();
|
|
95
|
+
this.#cache.set(userId, { record, expiresAt: now + this.#cacheTtlMs });
|
|
96
|
+
}
|
|
97
|
+
return record;
|
|
98
|
+
}
|
|
99
|
+
async #isRevoked(sessionId) {
|
|
100
|
+
const now = this.#clock.now();
|
|
101
|
+
// Only negative answers are cached, and only briefly. A revocation caches
|
|
102
|
+
// permanently for this process because a family is never un-revoked.
|
|
103
|
+
const known = this.#revoked.get(sessionId);
|
|
104
|
+
if (known === Number.POSITIVE_INFINITY)
|
|
105
|
+
return true;
|
|
106
|
+
if (known !== undefined && known > now)
|
|
107
|
+
return false;
|
|
108
|
+
// An API-key principal has no session family to revoke.
|
|
109
|
+
if (sessionId.startsWith('api-key:'))
|
|
110
|
+
return false;
|
|
111
|
+
const revoked = await this.#sessions.isFamilyRevoked(sessionId);
|
|
112
|
+
this.#revoked.set(sessionId, revoked ? Number.POSITIVE_INFINITY : now + this.#cacheTtlMs);
|
|
113
|
+
return revoked;
|
|
114
|
+
}
|
|
115
|
+
#evictIfFull() {
|
|
116
|
+
if (this.#cache.size < this.#cacheMaxEntries)
|
|
117
|
+
return;
|
|
118
|
+
// Map preserves insertion order, so the first key is the oldest write.
|
|
119
|
+
const oldest = this.#cache.keys().next();
|
|
120
|
+
if (!oldest.done)
|
|
121
|
+
this.#cache.delete(oldest.value);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
exports.PrincipalResolver = PrincipalResolver;
|
|
125
|
+
//# sourceMappingURL=principal.resolver.js.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refresh sessions: issue, rotate, detect reuse, revoke (PLAN.md §12, §15.1).
|
|
3
|
+
*
|
|
4
|
+
* The refresh token is opaque and high-entropy (32 CSPRNG bytes), so it is
|
|
5
|
+
* stored as an HMAC rather than an argon2 hash — stretching adds nothing to a
|
|
6
|
+
* 256-bit random value, and a lookup by HMAC is a single indexed read instead
|
|
7
|
+
* of a table scan of candidate hashes.
|
|
8
|
+
*
|
|
9
|
+
* Rotation and reuse detection are the point of the design:
|
|
10
|
+
*
|
|
11
|
+
* 1. every refresh issues a new token and marks the old one rotated;
|
|
12
|
+
* 2. a rotated or revoked token coming back means someone replayed a token
|
|
13
|
+
* the legitimate client already spent — either the client or the attacker,
|
|
14
|
+
* and there is no way to tell which;
|
|
15
|
+
* 3. so the entire **family** is revoked, which logs both out and turns a
|
|
16
|
+
* silent theft into a visible re-login.
|
|
17
|
+
*/
|
|
18
|
+
import type { Id, SessionRecord } from '@nage-api/contracts';
|
|
19
|
+
import type { AuthAuditSink, Clock, SessionStore } from './ports.js';
|
|
20
|
+
import type { ResolvedAuthConfig } from './config.js';
|
|
21
|
+
export interface SessionServiceOptions {
|
|
22
|
+
readonly store: SessionStore;
|
|
23
|
+
readonly config: ResolvedAuthConfig;
|
|
24
|
+
/** Secret that makes a stolen session table unusable for replay. */
|
|
25
|
+
readonly pepper: string;
|
|
26
|
+
readonly clock?: Clock;
|
|
27
|
+
readonly audit?: AuthAuditSink;
|
|
28
|
+
}
|
|
29
|
+
export interface IssuedSession {
|
|
30
|
+
readonly session: SessionRecord;
|
|
31
|
+
/** The only moment the plaintext token exists; it is never stored. */
|
|
32
|
+
readonly refreshToken: string;
|
|
33
|
+
}
|
|
34
|
+
export interface SessionContext {
|
|
35
|
+
readonly userAgent?: string;
|
|
36
|
+
readonly ip?: string;
|
|
37
|
+
}
|
|
38
|
+
export declare class SessionService {
|
|
39
|
+
#private;
|
|
40
|
+
constructor(options: SessionServiceOptions);
|
|
41
|
+
/** Start a new session family — one login, one family. */
|
|
42
|
+
issue(userId: Id, context?: SessionContext): Promise<IssuedSession>;
|
|
43
|
+
/**
|
|
44
|
+
* Exchange a refresh token for a new one.
|
|
45
|
+
*
|
|
46
|
+
* @throws AuthenticationError `AUTH_TOKEN_REUSED` when the token was already
|
|
47
|
+
* rotated or revoked — after revoking the whole family.
|
|
48
|
+
*/
|
|
49
|
+
rotate(refreshToken: string, context?: SessionContext): Promise<IssuedSession>;
|
|
50
|
+
/** Look up a live session, or `undefined` if it is spent, revoked or expired. */
|
|
51
|
+
findLive(refreshToken: string): Promise<SessionRecord | undefined>;
|
|
52
|
+
/**
|
|
53
|
+
* Log out.
|
|
54
|
+
*
|
|
55
|
+
* Revokes the whole family, not just the presented token: a logout that left
|
|
56
|
+
* the previous, already-rotated sibling usable would not be a logout.
|
|
57
|
+
*/
|
|
58
|
+
revoke(refreshToken: string): Promise<boolean>;
|
|
59
|
+
/** Revoke every session a user holds — after a password reset, say. */
|
|
60
|
+
revokeAllForUser(userId: Id, reason: string): Promise<number>;
|
|
61
|
+
/** Drop expired rows; wire to a schedule in a long-running deployment. */
|
|
62
|
+
prune(): Promise<number>;
|
|
63
|
+
/**
|
|
64
|
+
* HMAC-SHA256 of the token under the session pepper.
|
|
65
|
+
*
|
|
66
|
+
* Public because a store implementation may need it to migrate existing rows,
|
|
67
|
+
* and because the tests assert that no plaintext token is ever persisted.
|
|
68
|
+
*/
|
|
69
|
+
hashToken(refreshToken: string): string;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=session.service.d.ts.map
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Refresh sessions: issue, rotate, detect reuse, revoke (PLAN.md §12, §15.1).
|
|
4
|
+
*
|
|
5
|
+
* The refresh token is opaque and high-entropy (32 CSPRNG bytes), so it is
|
|
6
|
+
* stored as an HMAC rather than an argon2 hash — stretching adds nothing to a
|
|
7
|
+
* 256-bit random value, and a lookup by HMAC is a single indexed read instead
|
|
8
|
+
* of a table scan of candidate hashes.
|
|
9
|
+
*
|
|
10
|
+
* Rotation and reuse detection are the point of the design:
|
|
11
|
+
*
|
|
12
|
+
* 1. every refresh issues a new token and marks the old one rotated;
|
|
13
|
+
* 2. a rotated or revoked token coming back means someone replayed a token
|
|
14
|
+
* the legitimate client already spent — either the client or the attacker,
|
|
15
|
+
* and there is no way to tell which;
|
|
16
|
+
* 3. so the entire **family** is revoked, which logs both out and turns a
|
|
17
|
+
* silent theft into a visible re-login.
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.SessionService = void 0;
|
|
21
|
+
const node_crypto_1 = require("node:crypto");
|
|
22
|
+
const core_1 = require("@nage-api/core");
|
|
23
|
+
const MIN_PEPPER_LENGTH = 16;
|
|
24
|
+
class SessionService {
|
|
25
|
+
#store;
|
|
26
|
+
#config;
|
|
27
|
+
#pepper;
|
|
28
|
+
#clock;
|
|
29
|
+
#audit;
|
|
30
|
+
constructor(options) {
|
|
31
|
+
if (options.pepper.length < MIN_PEPPER_LENGTH) {
|
|
32
|
+
throw new core_1.ConfigurationError({
|
|
33
|
+
detail: `The session pepper must be at least ${String(MIN_PEPPER_LENGTH)} characters`,
|
|
34
|
+
meta: { setting: 'SESSION_PEPPER', length: options.pepper.length },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
this.#store = options.store;
|
|
38
|
+
this.#config = options.config;
|
|
39
|
+
this.#pepper = options.pepper;
|
|
40
|
+
this.#clock = options.clock ?? { now: () => Date.now() };
|
|
41
|
+
this.#audit = options.audit;
|
|
42
|
+
}
|
|
43
|
+
/** Start a new session family — one login, one family. */
|
|
44
|
+
async issue(userId, context = {}) {
|
|
45
|
+
return this.#create(userId, (0, core_1.randomId)(), context);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Exchange a refresh token for a new one.
|
|
49
|
+
*
|
|
50
|
+
* @throws AuthenticationError `AUTH_TOKEN_REUSED` when the token was already
|
|
51
|
+
* rotated or revoked — after revoking the whole family.
|
|
52
|
+
*/
|
|
53
|
+
async rotate(refreshToken, context = {}) {
|
|
54
|
+
const now = this.#clock.now();
|
|
55
|
+
const session = await this.#store.findByHashedToken(this.hashToken(refreshToken));
|
|
56
|
+
if (session === undefined) {
|
|
57
|
+
// Indistinguishable from a random string: the client learns nothing about
|
|
58
|
+
// whether the token ever existed.
|
|
59
|
+
throw new core_1.AuthenticationError('AUTH_TOKEN_INVALID', {
|
|
60
|
+
detail: 'Refresh token does not match any session',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (session.revokedAt !== undefined || session.rotatedAt !== undefined) {
|
|
64
|
+
if (this.#config.refresh.reuseDetection) {
|
|
65
|
+
const revoked = await this.#store.revokeFamily(session.familyId, now);
|
|
66
|
+
await this.#audit?.record({
|
|
67
|
+
name: 'token.reuse-detected',
|
|
68
|
+
at: now,
|
|
69
|
+
userId: session.userId,
|
|
70
|
+
sessionId: session.id,
|
|
71
|
+
meta: { familyId: session.familyId, sessionsRevoked: revoked },
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
throw new core_1.AuthenticationError('AUTH_TOKEN_REUSED', {
|
|
75
|
+
detail: `Refresh token for session ${session.id} was replayed after rotation; family ${session.familyId} revoked`,
|
|
76
|
+
meta: { sessionId: session.id, familyId: session.familyId },
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (session.expiresAt <= now) {
|
|
80
|
+
throw new core_1.AuthenticationError('AUTH_TOKEN_EXPIRED', {
|
|
81
|
+
detail: `Refresh session ${session.id} expired`,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
if (!this.#config.refresh.rotate) {
|
|
85
|
+
// Rotation off: the same token stays valid, and no new row is written.
|
|
86
|
+
return { session, refreshToken };
|
|
87
|
+
}
|
|
88
|
+
await this.#store.markRotated(session.id, now);
|
|
89
|
+
const issued = await this.#create(session.userId, session.familyId, context);
|
|
90
|
+
await this.#audit?.record({
|
|
91
|
+
name: 'token.refreshed',
|
|
92
|
+
at: now,
|
|
93
|
+
userId: session.userId,
|
|
94
|
+
sessionId: issued.session.id,
|
|
95
|
+
meta: { familyId: session.familyId, previousSessionId: session.id },
|
|
96
|
+
});
|
|
97
|
+
return issued;
|
|
98
|
+
}
|
|
99
|
+
/** Look up a live session, or `undefined` if it is spent, revoked or expired. */
|
|
100
|
+
async findLive(refreshToken) {
|
|
101
|
+
const session = await this.#store.findByHashedToken(this.hashToken(refreshToken));
|
|
102
|
+
if (session === undefined)
|
|
103
|
+
return undefined;
|
|
104
|
+
const spent = session.revokedAt !== undefined ||
|
|
105
|
+
session.rotatedAt !== undefined ||
|
|
106
|
+
session.expiresAt <= this.#clock.now();
|
|
107
|
+
return spent ? undefined : session;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Log out.
|
|
111
|
+
*
|
|
112
|
+
* Revokes the whole family, not just the presented token: a logout that left
|
|
113
|
+
* the previous, already-rotated sibling usable would not be a logout.
|
|
114
|
+
*/
|
|
115
|
+
async revoke(refreshToken) {
|
|
116
|
+
const now = this.#clock.now();
|
|
117
|
+
const session = await this.#store.findByHashedToken(this.hashToken(refreshToken));
|
|
118
|
+
if (session === undefined)
|
|
119
|
+
return false;
|
|
120
|
+
await this.#store.revokeFamily(session.familyId, now);
|
|
121
|
+
await this.#audit?.record({
|
|
122
|
+
name: 'session.revoked',
|
|
123
|
+
at: now,
|
|
124
|
+
userId: session.userId,
|
|
125
|
+
sessionId: session.id,
|
|
126
|
+
meta: { familyId: session.familyId, reason: 'logout' },
|
|
127
|
+
});
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
/** Revoke every session a user holds — after a password reset, say. */
|
|
131
|
+
async revokeAllForUser(userId, reason) {
|
|
132
|
+
const now = this.#clock.now();
|
|
133
|
+
const revoked = await this.#store.revokeAllForUser(userId, now);
|
|
134
|
+
if (revoked > 0) {
|
|
135
|
+
await this.#audit?.record({
|
|
136
|
+
name: 'session.revoked',
|
|
137
|
+
at: now,
|
|
138
|
+
userId,
|
|
139
|
+
meta: { reason, sessionsRevoked: revoked },
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return revoked;
|
|
143
|
+
}
|
|
144
|
+
/** Drop expired rows; wire to a schedule in a long-running deployment. */
|
|
145
|
+
async prune() {
|
|
146
|
+
return this.#store.deleteExpired(this.#clock.now());
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* HMAC-SHA256 of the token under the session pepper.
|
|
150
|
+
*
|
|
151
|
+
* Public because a store implementation may need it to migrate existing rows,
|
|
152
|
+
* and because the tests assert that no plaintext token is ever persisted.
|
|
153
|
+
*/
|
|
154
|
+
hashToken(refreshToken) {
|
|
155
|
+
return (0, node_crypto_1.createHmac)('sha256', this.#pepper).update(refreshToken, 'utf8').digest('base64url');
|
|
156
|
+
}
|
|
157
|
+
async #create(userId, familyId, context) {
|
|
158
|
+
const now = this.#clock.now();
|
|
159
|
+
const refreshToken = (0, core_1.randomToken)(32);
|
|
160
|
+
const session = {
|
|
161
|
+
id: (0, core_1.randomId)(),
|
|
162
|
+
familyId,
|
|
163
|
+
userId,
|
|
164
|
+
hashedToken: this.hashToken(refreshToken),
|
|
165
|
+
issuedAt: now,
|
|
166
|
+
expiresAt: now + this.#config.refresh.ttlSeconds * 1000,
|
|
167
|
+
...(context.userAgent === undefined ? {} : { userAgent: context.userAgent }),
|
|
168
|
+
...(context.ip === undefined ? {} : { ip: context.ip }),
|
|
169
|
+
};
|
|
170
|
+
await this.#store.create(session);
|
|
171
|
+
return { session, refreshToken };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
exports.SessionService = SessionService;
|
|
175
|
+
//# sourceMappingURL=session.service.js.map
|
package/dist/tokens.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DI tokens for the auth ports (PLAN.md §7.3).
|
|
3
|
+
*
|
|
4
|
+
* An application binds these once in `NageAuthModule.forRoot`; nothing in this
|
|
5
|
+
* package imports a store implementation directly, which is what lets the same
|
|
6
|
+
* guards run against an in-memory store in tests and a SQL one in production.
|
|
7
|
+
*/
|
|
8
|
+
import { type Token } from '@nage-api/core';
|
|
9
|
+
import type { ApiKeyStore, AuthAuditSink, AuthUserStore, Clock, LockoutStore, OtpChannel, OtpStore, PasswordHasher, SessionStore, TokenSigner } from './ports.js';
|
|
10
|
+
import type { ResolvedAuthConfig } from './config.js';
|
|
11
|
+
export declare const NAGE_AUTH_CONFIG: Token<ResolvedAuthConfig>;
|
|
12
|
+
export declare const NAGE_AUTH_USER_STORE: Token<AuthUserStore>;
|
|
13
|
+
export declare const NAGE_SESSION_STORE: Token<SessionStore>;
|
|
14
|
+
export declare const NAGE_OTP_STORE: Token<OtpStore>;
|
|
15
|
+
export declare const NAGE_OTP_CHANNEL: Token<OtpChannel>;
|
|
16
|
+
export declare const NAGE_API_KEY_STORE: Token<ApiKeyStore>;
|
|
17
|
+
export declare const NAGE_LOCKOUT_STORE: Token<LockoutStore>;
|
|
18
|
+
export declare const NAGE_PASSWORD_HASHER: Token<PasswordHasher>;
|
|
19
|
+
export declare const NAGE_TOKEN_SIGNER: Token<TokenSigner>;
|
|
20
|
+
export declare const NAGE_AUTH_AUDIT: Token<AuthAuditSink>;
|
|
21
|
+
export declare const NAGE_AUTH_CLOCK: Token<Clock>;
|
|
22
|
+
//# sourceMappingURL=tokens.d.ts.map
|
package/dist/tokens.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* DI tokens for the auth ports (PLAN.md §7.3).
|
|
4
|
+
*
|
|
5
|
+
* An application binds these once in `NageAuthModule.forRoot`; nothing in this
|
|
6
|
+
* package imports a store implementation directly, which is what lets the same
|
|
7
|
+
* guards run against an in-memory store in tests and a SQL one in production.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.NAGE_AUTH_CLOCK = exports.NAGE_AUTH_AUDIT = exports.NAGE_TOKEN_SIGNER = exports.NAGE_PASSWORD_HASHER = exports.NAGE_LOCKOUT_STORE = exports.NAGE_API_KEY_STORE = exports.NAGE_OTP_CHANNEL = exports.NAGE_OTP_STORE = exports.NAGE_SESSION_STORE = exports.NAGE_AUTH_USER_STORE = exports.NAGE_AUTH_CONFIG = void 0;
|
|
11
|
+
const core_1 = require("@nage-api/core");
|
|
12
|
+
exports.NAGE_AUTH_CONFIG = (0, core_1.createToken)('NAGE_AUTH_CONFIG');
|
|
13
|
+
exports.NAGE_AUTH_USER_STORE = (0, core_1.createToken)('NAGE_AUTH_USER_STORE');
|
|
14
|
+
exports.NAGE_SESSION_STORE = (0, core_1.createToken)('NAGE_SESSION_STORE');
|
|
15
|
+
exports.NAGE_OTP_STORE = (0, core_1.createToken)('NAGE_OTP_STORE');
|
|
16
|
+
exports.NAGE_OTP_CHANNEL = (0, core_1.createToken)('NAGE_OTP_CHANNEL');
|
|
17
|
+
exports.NAGE_API_KEY_STORE = (0, core_1.createToken)('NAGE_API_KEY_STORE');
|
|
18
|
+
exports.NAGE_LOCKOUT_STORE = (0, core_1.createToken)('NAGE_LOCKOUT_STORE');
|
|
19
|
+
exports.NAGE_PASSWORD_HASHER = (0, core_1.createToken)('NAGE_PASSWORD_HASHER');
|
|
20
|
+
exports.NAGE_TOKEN_SIGNER = (0, core_1.createToken)('NAGE_TOKEN_SIGNER');
|
|
21
|
+
exports.NAGE_AUTH_AUDIT = (0, core_1.createToken)('NAGE_AUTH_AUDIT');
|
|
22
|
+
exports.NAGE_AUTH_CLOCK = (0, core_1.createToken)('NAGE_AUTH_CLOCK');
|
|
23
|
+
//# sourceMappingURL=tokens.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nage-api/auth",
|
|
3
|
+
"version": "1.0.0-beta.2",
|
|
4
|
+
"description": "Authentication and authorization for @nage-api — RS256 JWT, rotating refresh with reuse detection, argon2id passwords, OTP, API keys, RBAC",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"!dist/.tsbuildinfo",
|
|
20
|
+
"!dist/**/*.map",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@node-rs/argon2": "2.1.0",
|
|
28
|
+
"jose": "6.2.8",
|
|
29
|
+
"@nage-api/contracts": "1.0.0-beta.2",
|
|
30
|
+
"@nage-api/core": "1.0.0-beta.2"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@nestjs/common": "^11.0.0",
|
|
34
|
+
"@nestjs/core": "^11.0.0",
|
|
35
|
+
"reflect-metadata": "^0.2.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@nestjs/common": "11.1.29",
|
|
39
|
+
"@nestjs/core": "11.1.29",
|
|
40
|
+
"@nestjs/platform-express": "11.1.29",
|
|
41
|
+
"@nestjs/testing": "11.1.29",
|
|
42
|
+
"@swc/core": "1.15.47",
|
|
43
|
+
"@types/node": "22.20.1",
|
|
44
|
+
"@types/supertest": "6.0.3",
|
|
45
|
+
"@vitest/coverage-v8": "4.1.10",
|
|
46
|
+
"class-transformer": "0.5.1",
|
|
47
|
+
"class-validator": "0.15.1",
|
|
48
|
+
"reflect-metadata": "0.2.2",
|
|
49
|
+
"rimraf": "6.1.3",
|
|
50
|
+
"rxjs": "7.8.2",
|
|
51
|
+
"supertest": "7.1.4",
|
|
52
|
+
"typescript": "5.9.3",
|
|
53
|
+
"unplugin-swc": "1.5.11",
|
|
54
|
+
"vitest": "4.1.10",
|
|
55
|
+
"@nage-api/testing": "1.0.0-beta.2"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=22.0.0"
|
|
59
|
+
},
|
|
60
|
+
"scripts": {
|
|
61
|
+
"build": "tsc -b tsconfig.build.json",
|
|
62
|
+
"clean": "rimraf dist .turbo",
|
|
63
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
64
|
+
"test": "vitest run"
|
|
65
|
+
}
|
|
66
|
+
}
|