@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.
Files changed (43) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +176 -0
  3. package/dist/api-key.service.d.ts +50 -0
  4. package/dist/api-key.service.js +110 -0
  5. package/dist/auth.controller.d.ts +48 -0
  6. package/dist/auth.controller.js +185 -0
  7. package/dist/auth.dto.d.ts +37 -0
  8. package/dist/auth.dto.js +117 -0
  9. package/dist/auth.guard.d.ts +29 -0
  10. package/dist/auth.guard.js +122 -0
  11. package/dist/auth.module.d.ts +61 -0
  12. package/dist/auth.module.js +226 -0
  13. package/dist/auth.service.d.ts +82 -0
  14. package/dist/auth.service.js +269 -0
  15. package/dist/authorization.guard.d.ts +24 -0
  16. package/dist/authorization.guard.js +107 -0
  17. package/dist/config.d.ts +71 -0
  18. package/dist/config.js +151 -0
  19. package/dist/decorators.d.ts +51 -0
  20. package/dist/decorators.js +70 -0
  21. package/dist/index.d.ts +30 -0
  22. package/dist/index.js +96 -0
  23. package/dist/jwt.d.ts +50 -0
  24. package/dist/jwt.js +163 -0
  25. package/dist/lockout.service.d.ts +43 -0
  26. package/dist/lockout.service.js +94 -0
  27. package/dist/memory-stores.d.ts +84 -0
  28. package/dist/memory-stores.js +246 -0
  29. package/dist/otp.service.d.ts +47 -0
  30. package/dist/otp.service.js +137 -0
  31. package/dist/password.d.ts +51 -0
  32. package/dist/password.js +122 -0
  33. package/dist/policy.d.ts +44 -0
  34. package/dist/policy.js +61 -0
  35. package/dist/ports.d.ts +175 -0
  36. package/dist/ports.js +17 -0
  37. package/dist/principal.resolver.d.ts +52 -0
  38. package/dist/principal.resolver.js +125 -0
  39. package/dist/session.service.d.ts +71 -0
  40. package/dist/session.service.js +175 -0
  41. package/dist/tokens.d.ts +22 -0
  42. package/dist/tokens.js +23 -0
  43. package/package.json +66 -0
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ /**
3
+ * The global authorization guard (PLAN.md §15.2).
4
+ *
5
+ * Runs after `AuthGuard`, so a principal is already in the context. Three
6
+ * checks, all of which must pass: any of the required roles, **all** of the
7
+ * required permissions, and the named policy.
8
+ *
9
+ * Permissions are conjunctive on purpose. `@Permissions('order:read',
10
+ * 'order:write')` on a handler that reads and writes should mean both; an
11
+ * "any" reading would grant write access to a read-only caller.
12
+ */
13
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
14
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
15
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
16
+ 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;
17
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
18
+ };
19
+ var __metadata = (this && this.__metadata) || function (k, v) {
20
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
21
+ };
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.AuthorizationGuard = void 0;
24
+ const common_1 = require("@nestjs/common");
25
+ const core_1 = require("@nage-api/core");
26
+ const decorators_js_1 = require("./decorators.js");
27
+ let AuthorizationGuard = class AuthorizationGuard {
28
+ #reflector;
29
+ #policies;
30
+ constructor(options) {
31
+ this.#reflector = options.reflector;
32
+ this.#policies = options.policies;
33
+ }
34
+ async canActivate(context) {
35
+ if (context.getType() !== 'http')
36
+ return true;
37
+ const rules = (0, decorators_js_1.readAuthorizationRules)(this.#reflector, context);
38
+ const unrestricted = rules.roles.length === 0 && rules.permissions.length === 0 && rules.policy === undefined;
39
+ if (unrestricted)
40
+ return true;
41
+ // A route carrying authorization rules is not public, whatever `@Public()`
42
+ // says — the combination is a mistake, and resolving it towards "open"
43
+ // would silently unguard the route.
44
+ if ((0, core_1.isPublicRoute)(this.#reflector, context)) {
45
+ throw new core_1.AuthorizationError('FORBIDDEN', {
46
+ detail: 'Route is marked @Public() but also carries authorization rules; remove one',
47
+ });
48
+ }
49
+ const user = (0, core_1.getActiveContext)()?.user;
50
+ if (user === undefined) {
51
+ throw new core_1.AuthenticationError('AUTH_REQUIRED', {
52
+ detail: 'Authorization rules were evaluated without an authenticated principal',
53
+ });
54
+ }
55
+ this.#assertRoles(user, rules.roles);
56
+ this.#assertPermissions(user, rules.permissions);
57
+ if (rules.policy !== undefined) {
58
+ await this.#assertPolicy(user, rules.policy, context);
59
+ }
60
+ return true;
61
+ }
62
+ #assertRoles(user, required) {
63
+ if (required.length === 0)
64
+ return;
65
+ if (required.some((role) => user.roles.includes(role)))
66
+ return;
67
+ throw new core_1.AuthorizationError('INSUFFICIENT_ROLE', {
68
+ // The requirement is logged, never returned: telling a caller which role
69
+ // they lack maps out the permission model for them.
70
+ detail: `Principal ${String(user.id)} has [${user.roles.join(', ')}] but the route needs one of [${required.join(', ')}]`,
71
+ meta: { required, held: user.roles },
72
+ });
73
+ }
74
+ #assertPermissions(user, required) {
75
+ const missing = required.filter((permission) => !user.permissions.includes(permission));
76
+ if (missing.length === 0)
77
+ return;
78
+ throw new core_1.AuthorizationError('INSUFFICIENT_PERMISSION', {
79
+ detail: `Principal ${String(user.id)} is missing [${missing.join(', ')}]`,
80
+ meta: { required, missing },
81
+ });
82
+ }
83
+ async #assertPolicy(user, name, context) {
84
+ const request = context.switchToHttp().getRequest();
85
+ const policyContext = {
86
+ user,
87
+ method: request.method ?? 'GET',
88
+ path: request.originalUrl ?? request.url ?? '',
89
+ params: request.params ?? {},
90
+ query: request.query ?? {},
91
+ body: request.body,
92
+ };
93
+ const decision = await this.#policies.evaluate(name, policyContext);
94
+ if (decision.allowed)
95
+ return;
96
+ throw new core_1.AuthorizationError('POLICY_DENIED', {
97
+ detail: `Policy "${name}" denied ${String(user.id)}: ${decision.reason ?? 'no reason given'}`,
98
+ meta: { policy: name, reason: decision.reason },
99
+ });
100
+ }
101
+ };
102
+ exports.AuthorizationGuard = AuthorizationGuard;
103
+ exports.AuthorizationGuard = AuthorizationGuard = __decorate([
104
+ (0, common_1.Injectable)(),
105
+ __metadata("design:paramtypes", [Object])
106
+ ], AuthorizationGuard);
107
+ //# sourceMappingURL=authorization.guard.js.map
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Resolving `AuthConfig` into the settled values the services use
3
+ * (PLAN.md §11.1, §15).
4
+ *
5
+ * Two rules shape this file. Defaults are the **secure** ones, so an
6
+ * application that configures nothing still gets RS256, 15-minute access
7
+ * tokens, rotation and reuse detection. And every departure is validated here
8
+ * rather than discovered at the first login: a 24-hour access token or an
9
+ * HS256 signing key is a configuration error, not a runtime surprise.
10
+ */
11
+ import type { AuthConfig, NodeEnvironment, Permission, RoleMatrix } from '@nage-api/contracts';
12
+ /** Every value the auth services read, with nothing left optional. */
13
+ export interface ResolvedAuthConfig {
14
+ readonly environment: NodeEnvironment;
15
+ readonly jwt: {
16
+ readonly algorithm: 'RS256' | 'RS512' | 'ES256' | 'HS256';
17
+ readonly accessTtlSeconds: number;
18
+ readonly issuer: string;
19
+ readonly audience: string | readonly string[];
20
+ readonly keyId?: string;
21
+ /** Tolerated clock difference when verifying, in seconds. */
22
+ readonly clockToleranceSeconds: number;
23
+ };
24
+ readonly refresh: {
25
+ readonly ttlSeconds: number;
26
+ readonly rotate: boolean;
27
+ readonly reuseDetection: boolean;
28
+ };
29
+ readonly password: {
30
+ readonly minLength: number;
31
+ readonly requireMixedCase: boolean;
32
+ readonly requireNumber: boolean;
33
+ readonly requireSymbol: boolean;
34
+ };
35
+ readonly otp: {
36
+ readonly length: number;
37
+ readonly ttlSeconds: number;
38
+ readonly maxAttempts: number;
39
+ readonly resendCooldownSeconds: number;
40
+ };
41
+ readonly lockout: {
42
+ readonly maxAttempts: number;
43
+ readonly windowSeconds: number;
44
+ readonly durationSeconds: number;
45
+ };
46
+ readonly apiKeys: {
47
+ readonly enabled: boolean;
48
+ };
49
+ /** Role → permissions. Empty means permissions are granted per user only. */
50
+ readonly roles: RoleMatrix;
51
+ /** Roles allowed to impersonate another user via `POST /auth/user`. */
52
+ readonly impersonation: {
53
+ readonly enabled: boolean;
54
+ readonly allowedRoles: readonly string[];
55
+ readonly ttlSeconds: number;
56
+ };
57
+ }
58
+ export interface ResolveAuthConfigInput {
59
+ readonly auth?: AuthConfig;
60
+ readonly environment: NodeEnvironment;
61
+ readonly roles?: RoleMatrix;
62
+ readonly impersonation?: {
63
+ readonly enabled?: boolean;
64
+ readonly allowedRoles?: readonly string[];
65
+ readonly ttl?: string;
66
+ };
67
+ }
68
+ export declare function resolveAuthConfig(input: ResolveAuthConfigInput): ResolvedAuthConfig;
69
+ /** Permissions a set of roles grants, plus whatever was granted directly. */
70
+ export declare function resolvePermissions(matrix: RoleMatrix, roles: readonly string[], direct?: readonly Permission[]): readonly Permission[];
71
+ //# sourceMappingURL=config.d.ts.map
package/dist/config.js ADDED
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+ /**
3
+ * Resolving `AuthConfig` into the settled values the services use
4
+ * (PLAN.md §11.1, §15).
5
+ *
6
+ * Two rules shape this file. Defaults are the **secure** ones, so an
7
+ * application that configures nothing still gets RS256, 15-minute access
8
+ * tokens, rotation and reuse detection. And every departure is validated here
9
+ * rather than discovered at the first login: a 24-hour access token or an
10
+ * HS256 signing key is a configuration error, not a runtime surprise.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.resolveAuthConfig = resolveAuthConfig;
14
+ exports.resolvePermissions = resolvePermissions;
15
+ const core_1 = require("@nage-api/core");
16
+ const DEFAULTS = {
17
+ accessTtl: '15m',
18
+ refreshTtl: '30d',
19
+ otpTtl: '5m',
20
+ resendCooldown: '60s',
21
+ lockoutWindow: '15m',
22
+ lockoutDuration: '15m',
23
+ impersonationTtl: '15m',
24
+ };
25
+ /** Above this, a stolen access token stays useful long enough to matter (§12). */
26
+ const MAX_SAFE_ACCESS_TTL_SECONDS = 60 * 60;
27
+ function resolveAuthConfig(input) {
28
+ const auth = input.auth ?? {};
29
+ const jwt = auth.jwt;
30
+ if (jwt?.issuer === undefined || jwt.issuer.trim() === '') {
31
+ // Without an issuer, a token minted by any other service that shares the
32
+ // key verifies here too.
33
+ throw new core_1.ConfigurationError({
34
+ detail: 'auth.jwt.issuer must be set; it is what scopes a token to this deployment',
35
+ meta: { setting: 'auth.jwt.issuer' },
36
+ });
37
+ }
38
+ const algorithm = jwt.algorithm ?? 'RS256';
39
+ const production = input.environment === 'production' || input.environment === 'staging';
40
+ if (algorithm === 'HS256' && production) {
41
+ // Symmetric signing means every verifier can also mint tokens.
42
+ throw new core_1.ConfigurationError({
43
+ detail: 'auth.jwt.algorithm HS256 is not allowed in a deployed environment; use RS256',
44
+ meta: { setting: 'auth.jwt.algorithm', algorithm, environment: input.environment },
45
+ });
46
+ }
47
+ const accessTtlSeconds = seconds(jwt.accessTtl ?? DEFAULTS.accessTtl, 'auth.jwt.accessTtl');
48
+ if (production && accessTtlSeconds > MAX_SAFE_ACCESS_TTL_SECONDS) {
49
+ throw new core_1.ConfigurationError({
50
+ detail: `auth.jwt.accessTtl of ${jwt.accessTtl ?? ''} exceeds the one-hour ceiling`,
51
+ meta: { setting: 'auth.jwt.accessTtl', seconds: accessTtlSeconds },
52
+ });
53
+ }
54
+ const refresh = auth.refresh ?? {};
55
+ const rotate = refresh.rotate ?? true;
56
+ if (!rotate && (refresh.reuseDetection ?? true)) {
57
+ // Reuse detection is defined as "a rotated token came back"; without
58
+ // rotation there is nothing to detect, and claiming otherwise is worse than
59
+ // admitting it is off.
60
+ throw new core_1.ConfigurationError({
61
+ detail: 'auth.refresh.reuseDetection requires auth.refresh.rotate; a token that is never rotated cannot be replayed detectably',
62
+ meta: { setting: 'auth.refresh.rotate' },
63
+ });
64
+ }
65
+ const password = auth.password ?? {};
66
+ if (password.algorithm !== undefined && password.algorithm !== 'argon2id') {
67
+ throw new core_1.ConfigurationError({
68
+ detail: `auth.password.algorithm "${password.algorithm}" is not supported; this package hashes with argon2id`,
69
+ meta: { setting: 'auth.password.algorithm', algorithm: password.algorithm },
70
+ });
71
+ }
72
+ const otp = auth.otp ?? {};
73
+ const otpLength = otp.length ?? 6;
74
+ if (otpLength < 6) {
75
+ // A 4-digit code is 10 000 possibilities; with any resend at all that is
76
+ // brute-forceable regardless of the attempt limit.
77
+ throw new core_1.ConfigurationError({
78
+ detail: `auth.otp.length of ${String(otpLength)} is too short; use at least 6 digits`,
79
+ meta: { setting: 'auth.otp.length', length: otpLength },
80
+ });
81
+ }
82
+ const lockout = auth.lockout ?? {};
83
+ const impersonation = input.impersonation ?? {};
84
+ return {
85
+ environment: input.environment,
86
+ jwt: {
87
+ algorithm,
88
+ accessTtlSeconds,
89
+ issuer: jwt.issuer,
90
+ audience: jwt.audience ?? jwt.issuer,
91
+ ...(jwt.keyId === undefined ? {} : { keyId: jwt.keyId }),
92
+ clockToleranceSeconds: 5,
93
+ },
94
+ refresh: {
95
+ ttlSeconds: seconds(refresh.ttl ?? DEFAULTS.refreshTtl, 'auth.refresh.ttl'),
96
+ rotate,
97
+ reuseDetection: rotate && (refresh.reuseDetection ?? true),
98
+ },
99
+ password: {
100
+ minLength: password.minLength ?? 12,
101
+ requireMixedCase: password.requireMixedCase ?? true,
102
+ requireNumber: password.requireNumber ?? true,
103
+ requireSymbol: password.requireSymbol ?? false,
104
+ },
105
+ otp: {
106
+ length: otpLength,
107
+ ttlSeconds: seconds(otp.ttl ?? DEFAULTS.otpTtl, 'auth.otp.ttl'),
108
+ maxAttempts: otp.maxAttempts ?? 5,
109
+ resendCooldownSeconds: seconds(otp.resendCooldown ?? DEFAULTS.resendCooldown, 'auth.otp.resendCooldown'),
110
+ },
111
+ lockout: {
112
+ maxAttempts: lockout.maxAttempts ?? 5,
113
+ windowSeconds: seconds(lockout.window ?? DEFAULTS.lockoutWindow, 'auth.lockout.window'),
114
+ durationSeconds: seconds(lockout.duration ?? DEFAULTS.lockoutDuration, 'auth.lockout.duration'),
115
+ },
116
+ apiKeys: { enabled: auth.apiKeys?.enabled ?? false },
117
+ roles: input.roles ?? {},
118
+ impersonation: {
119
+ enabled: impersonation.enabled ?? false,
120
+ allowedRoles: impersonation.allowedRoles ?? ['admin'],
121
+ ttlSeconds: seconds(impersonation.ttl ?? DEFAULTS.impersonationTtl, 'auth.impersonation.ttl'),
122
+ },
123
+ };
124
+ }
125
+ /** Permissions a set of roles grants, plus whatever was granted directly. */
126
+ function resolvePermissions(matrix, roles, direct = []) {
127
+ const granted = new Set(direct);
128
+ for (const role of roles) {
129
+ // `Object.hasOwn` rather than a bare lookup: role names arrive from the users
130
+ // table, and `matrix['constructor']` reaches `Object.prototype` and yields a
131
+ // function. Iterating that threw a raw `TypeError` out of the auth guard, so
132
+ // one row with a role named `constructor` or `toString` turned every request
133
+ // that user made into a 500.
134
+ if (!Object.hasOwn(matrix, role))
135
+ continue;
136
+ for (const permission of matrix[role] ?? [])
137
+ granted.add(permission);
138
+ }
139
+ return [...granted].sort();
140
+ }
141
+ function seconds(duration, setting) {
142
+ const milliseconds = (0, core_1.parseDurationMs)(duration);
143
+ if (milliseconds <= 0) {
144
+ throw new core_1.ConfigurationError({
145
+ detail: `${setting} is not a duration ("${duration}"); use forms like 15m, 24h, 30d`,
146
+ meta: { setting, value: duration },
147
+ });
148
+ }
149
+ return Math.floor(milliseconds / 1000);
150
+ }
151
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Authorization decorators (PLAN.md §15.2).
3
+ *
4
+ * `@Roles` is the coarse gate, `@Permissions` the fine one, `@Policy` the
5
+ * resource-aware one. They compose: a handler may require a role *and* a
6
+ * permission *and* a policy, and all three must pass.
7
+ *
8
+ * Reading them is centralised in `readAuthorizationRules` so the guard cannot
9
+ * accidentally check the handler while ignoring the controller, which is the
10
+ * classic way an `@Roles('admin')` on a class stops applying.
11
+ */
12
+ import { type CustomDecorator, type ExecutionContext } from '@nestjs/common';
13
+ import type { Reflector } from '@nestjs/core';
14
+ import type { Permission } from '@nage-api/contracts';
15
+ export declare const AUTH_METADATA_KEYS: {
16
+ readonly roles: "nage:auth:roles";
17
+ readonly permissions: "nage:auth:permissions";
18
+ readonly policy: "nage:auth:policy";
19
+ readonly apiKeyAllowed: "nage:auth:api-key-allowed";
20
+ };
21
+ /** Require **any** of these roles. */
22
+ export declare const Roles: (...roles: readonly string[]) => CustomDecorator;
23
+ /** Require **every** one of these permissions. */
24
+ export declare const Permissions: (...permissions: readonly Permission[]) => CustomDecorator;
25
+ /** Require a registered policy to allow the request (opt-in ABAC). */
26
+ export declare const Policy: (name: string) => CustomDecorator;
27
+ /**
28
+ * Allow a machine credential on this route.
29
+ *
30
+ * Opt-in per route rather than global: an API key is a long-lived bearer
31
+ * secret, and letting one reach `POST /auth/password/reset` because it works
32
+ * everywhere else is how a scoped key becomes a full account takeover.
33
+ */
34
+ export declare const AllowApiKey: () => CustomDecorator;
35
+ export interface AuthorizationRules {
36
+ readonly roles: readonly string[];
37
+ readonly permissions: readonly Permission[];
38
+ readonly policy?: string;
39
+ }
40
+ /**
41
+ * Merge the handler's rules with its controller's.
42
+ *
43
+ * Union rather than override: a controller-level `@Roles('admin')` and a
44
+ * handler-level `@Permissions('user:write')` both apply. `getAllAndOverride`
45
+ * would silently drop the class-level rule, which loosens access — the wrong
46
+ * direction for a default.
47
+ */
48
+ export declare function readAuthorizationRules(reflector: Reflector, context: ExecutionContext): AuthorizationRules;
49
+ /** Whether an API key may be used on this route. */
50
+ export declare function isApiKeyAllowed(reflector: Reflector, context: ExecutionContext): boolean;
51
+ //# sourceMappingURL=decorators.d.ts.map
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ /**
3
+ * Authorization decorators (PLAN.md §15.2).
4
+ *
5
+ * `@Roles` is the coarse gate, `@Permissions` the fine one, `@Policy` the
6
+ * resource-aware one. They compose: a handler may require a role *and* a
7
+ * permission *and* a policy, and all three must pass.
8
+ *
9
+ * Reading them is centralised in `readAuthorizationRules` so the guard cannot
10
+ * accidentally check the handler while ignoring the controller, which is the
11
+ * classic way an `@Roles('admin')` on a class stops applying.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.AllowApiKey = exports.Policy = exports.Permissions = exports.Roles = exports.AUTH_METADATA_KEYS = void 0;
15
+ exports.readAuthorizationRules = readAuthorizationRules;
16
+ exports.isApiKeyAllowed = isApiKeyAllowed;
17
+ const common_1 = require("@nestjs/common");
18
+ exports.AUTH_METADATA_KEYS = {
19
+ roles: 'nage:auth:roles',
20
+ permissions: 'nage:auth:permissions',
21
+ policy: 'nage:auth:policy',
22
+ apiKeyAllowed: 'nage:auth:api-key-allowed',
23
+ };
24
+ /** Require **any** of these roles. */
25
+ const Roles = (...roles) => (0, common_1.SetMetadata)(exports.AUTH_METADATA_KEYS.roles, roles);
26
+ exports.Roles = Roles;
27
+ /** Require **every** one of these permissions. */
28
+ const Permissions = (...permissions) => (0, common_1.SetMetadata)(exports.AUTH_METADATA_KEYS.permissions, permissions);
29
+ exports.Permissions = Permissions;
30
+ /** Require a registered policy to allow the request (opt-in ABAC). */
31
+ const Policy = (name) => (0, common_1.SetMetadata)(exports.AUTH_METADATA_KEYS.policy, name);
32
+ exports.Policy = Policy;
33
+ /**
34
+ * Allow a machine credential on this route.
35
+ *
36
+ * Opt-in per route rather than global: an API key is a long-lived bearer
37
+ * secret, and letting one reach `POST /auth/password/reset` because it works
38
+ * everywhere else is how a scoped key becomes a full account takeover.
39
+ */
40
+ const AllowApiKey = () => (0, common_1.SetMetadata)(exports.AUTH_METADATA_KEYS.apiKeyAllowed, true);
41
+ exports.AllowApiKey = AllowApiKey;
42
+ /**
43
+ * Merge the handler's rules with its controller's.
44
+ *
45
+ * Union rather than override: a controller-level `@Roles('admin')` and a
46
+ * handler-level `@Permissions('user:write')` both apply. `getAllAndOverride`
47
+ * would silently drop the class-level rule, which loosens access — the wrong
48
+ * direction for a default.
49
+ */
50
+ function readAuthorizationRules(reflector, context) {
51
+ const targets = [context.getHandler(), context.getClass()];
52
+ // `getAllAndMerge` concatenates the array each decorator stored, so this is
53
+ // already a flat list of role names rather than a list of lists.
54
+ const roles = new Set(reflector.getAllAndMerge(exports.AUTH_METADATA_KEYS.roles, targets));
55
+ const permissions = new Set(reflector.getAllAndMerge(exports.AUTH_METADATA_KEYS.permissions, targets));
56
+ const policy = reflector.getAllAndOverride(exports.AUTH_METADATA_KEYS.policy, targets);
57
+ return {
58
+ roles: [...roles],
59
+ permissions: [...permissions],
60
+ ...(policy === undefined ? {} : { policy }),
61
+ };
62
+ }
63
+ /** Whether an API key may be used on this route. */
64
+ function isApiKeyAllowed(reflector, context) {
65
+ return (reflector.getAllAndOverride(exports.AUTH_METADATA_KEYS.apiKeyAllowed, [
66
+ context.getHandler(),
67
+ context.getClass(),
68
+ ]) === true);
69
+ }
70
+ //# sourceMappingURL=decorators.js.map
@@ -0,0 +1,30 @@
1
+ /**
2
+ * `@nage-api/auth` — authentication and authorization (PLAN.md §15).
3
+ *
4
+ * Opt-in and engine-agnostic: the package owns credentials, tokens, sessions
5
+ * and access decisions, and knows nothing about how they are stored. An
6
+ * application binds the ports in `NageAuthModule.forRoot` and gets a globally
7
+ * protected surface, with the legacy endpoint paths retained.
8
+ */
9
+ export type * from '@nage-api/contracts';
10
+ export { NageAuthModule, type AuthSecrets, type NageAuthModuleOptions } from './auth.module.js';
11
+ export { resolveAuthConfig, resolvePermissions, type ResolveAuthConfigInput, type ResolvedAuthConfig, } from './config.js';
12
+ export type { ApiKeyRecord, ApiKeyStore, AuthAuditSink, AuthEvent, AuthEventName, AuthUserRecord, AuthUserStore, Clock, LockoutRecord, LockoutStore, OtpChannel, OtpPurpose, OtpRecord, OtpStore, PasswordHasher, ResolvedPrincipal, SessionStore, TokenSigner, } from './ports.js';
13
+ export { systemClock } from './ports.js';
14
+ export { NAGE_API_KEY_STORE, NAGE_AUTH_AUDIT, NAGE_AUTH_CLOCK, NAGE_AUTH_CONFIG, NAGE_AUTH_USER_STORE, NAGE_LOCKOUT_STORE, NAGE_OTP_CHANNEL, NAGE_OTP_STORE, NAGE_PASSWORD_HASHER, NAGE_SESSION_STORE, NAGE_TOKEN_SIGNER, } from './tokens.js';
15
+ export { AuthService, normaliseEmail, type AuthServiceOptions, type LoginInput, type ResetPasswordInput, } from './auth.service.js';
16
+ export { SessionService, type IssuedSession, type SessionContext, type SessionServiceOptions, } from './session.service.js';
17
+ export { OtpService, type OtpIssueResult, type OtpServiceOptions } from './otp.service.js';
18
+ export { LockoutService, type LockoutStatus, type LockoutServiceOptions, } from './lockout.service.js';
19
+ export { ApiKeyService, API_KEY_PREFIX, type ApiKeyServiceOptions, type CreateApiKeyInput, type CreatedApiKey, } from './api-key.service.js';
20
+ export { PrincipalResolver, type PrincipalResolverOptions, type ResolveInput, } from './principal.resolver.js';
21
+ export { Argon2PasswordHasher, ARGON2_PARAMETERS, assertPasswordPolicy, parseArgon2Parameters, type Argon2Parameters, type Argon2PasswordHasherOptions, } from './password.js';
22
+ export { JoseTokenSigner, actorId, buildClaims, parseClaims, type AccessTokenInput, type JoseTokenSignerOptions, } from './jwt.js';
23
+ export { AllowApiKey, AUTH_METADATA_KEYS, Permissions, Policy, Roles, isApiKeyAllowed, readAuthorizationRules, type AuthorizationRules, } from './decorators.js';
24
+ export { PolicyRegistry, ownershipPolicy, type PolicyContext, type PolicyFunction, } from './policy.js';
25
+ export { AuthGuard, type AuthGuardOptions } from './auth.guard.js';
26
+ export { AuthorizationGuard, type AuthorizationGuardOptions } from './authorization.guard.js';
27
+ export { AuthController } from './auth.controller.js';
28
+ export { ForgotPasswordDto, ImpersonateDto, LoginDto, RefreshTokenDto, ResetPasswordDto, SendOtpDto, VerifyOtpDto, } from './auth.dto.js';
29
+ export { MemoryApiKeyStore, MemoryAuthAuditSink, MemoryAuthUserStore, MemoryLockoutStore, MemoryOtpChannel, MemoryOtpStore, MemorySessionStore, } from './memory-stores.js';
30
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ /**
3
+ * `@nage-api/auth` — authentication and authorization (PLAN.md §15).
4
+ *
5
+ * Opt-in and engine-agnostic: the package owns credentials, tokens, sessions
6
+ * and access decisions, and knows nothing about how they are stored. An
7
+ * application binds the ports in `NageAuthModule.forRoot` and gets a globally
8
+ * protected surface, with the legacy endpoint paths retained.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.VerifyOtpDto = exports.SendOtpDto = exports.ResetPasswordDto = exports.RefreshTokenDto = exports.LoginDto = exports.ImpersonateDto = exports.ForgotPasswordDto = exports.AuthController = exports.AuthorizationGuard = exports.AuthGuard = exports.ownershipPolicy = exports.PolicyRegistry = exports.readAuthorizationRules = exports.isApiKeyAllowed = exports.Roles = exports.Policy = exports.Permissions = exports.AUTH_METADATA_KEYS = exports.AllowApiKey = exports.parseClaims = exports.buildClaims = exports.actorId = exports.JoseTokenSigner = exports.parseArgon2Parameters = exports.assertPasswordPolicy = exports.ARGON2_PARAMETERS = exports.Argon2PasswordHasher = exports.PrincipalResolver = exports.API_KEY_PREFIX = exports.ApiKeyService = exports.LockoutService = exports.OtpService = exports.SessionService = exports.normaliseEmail = exports.AuthService = exports.NAGE_TOKEN_SIGNER = exports.NAGE_SESSION_STORE = exports.NAGE_PASSWORD_HASHER = exports.NAGE_OTP_STORE = exports.NAGE_OTP_CHANNEL = exports.NAGE_LOCKOUT_STORE = exports.NAGE_AUTH_USER_STORE = exports.NAGE_AUTH_CONFIG = exports.NAGE_AUTH_CLOCK = exports.NAGE_AUTH_AUDIT = exports.NAGE_API_KEY_STORE = exports.systemClock = exports.resolvePermissions = exports.resolveAuthConfig = exports.NageAuthModule = void 0;
12
+ exports.MemorySessionStore = exports.MemoryOtpStore = exports.MemoryOtpChannel = exports.MemoryLockoutStore = exports.MemoryAuthUserStore = exports.MemoryAuthAuditSink = exports.MemoryApiKeyStore = void 0;
13
+ // Composition.
14
+ var auth_module_js_1 = require("./auth.module.js");
15
+ Object.defineProperty(exports, "NageAuthModule", { enumerable: true, get: function () { return auth_module_js_1.NageAuthModule; } });
16
+ // Configuration.
17
+ var config_js_1 = require("./config.js");
18
+ Object.defineProperty(exports, "resolveAuthConfig", { enumerable: true, get: function () { return config_js_1.resolveAuthConfig; } });
19
+ Object.defineProperty(exports, "resolvePermissions", { enumerable: true, get: function () { return config_js_1.resolvePermissions; } });
20
+ var ports_js_1 = require("./ports.js");
21
+ Object.defineProperty(exports, "systemClock", { enumerable: true, get: function () { return ports_js_1.systemClock; } });
22
+ var tokens_js_1 = require("./tokens.js");
23
+ Object.defineProperty(exports, "NAGE_API_KEY_STORE", { enumerable: true, get: function () { return tokens_js_1.NAGE_API_KEY_STORE; } });
24
+ Object.defineProperty(exports, "NAGE_AUTH_AUDIT", { enumerable: true, get: function () { return tokens_js_1.NAGE_AUTH_AUDIT; } });
25
+ Object.defineProperty(exports, "NAGE_AUTH_CLOCK", { enumerable: true, get: function () { return tokens_js_1.NAGE_AUTH_CLOCK; } });
26
+ Object.defineProperty(exports, "NAGE_AUTH_CONFIG", { enumerable: true, get: function () { return tokens_js_1.NAGE_AUTH_CONFIG; } });
27
+ Object.defineProperty(exports, "NAGE_AUTH_USER_STORE", { enumerable: true, get: function () { return tokens_js_1.NAGE_AUTH_USER_STORE; } });
28
+ Object.defineProperty(exports, "NAGE_LOCKOUT_STORE", { enumerable: true, get: function () { return tokens_js_1.NAGE_LOCKOUT_STORE; } });
29
+ Object.defineProperty(exports, "NAGE_OTP_CHANNEL", { enumerable: true, get: function () { return tokens_js_1.NAGE_OTP_CHANNEL; } });
30
+ Object.defineProperty(exports, "NAGE_OTP_STORE", { enumerable: true, get: function () { return tokens_js_1.NAGE_OTP_STORE; } });
31
+ Object.defineProperty(exports, "NAGE_PASSWORD_HASHER", { enumerable: true, get: function () { return tokens_js_1.NAGE_PASSWORD_HASHER; } });
32
+ Object.defineProperty(exports, "NAGE_SESSION_STORE", { enumerable: true, get: function () { return tokens_js_1.NAGE_SESSION_STORE; } });
33
+ Object.defineProperty(exports, "NAGE_TOKEN_SIGNER", { enumerable: true, get: function () { return tokens_js_1.NAGE_TOKEN_SIGNER; } });
34
+ // Services.
35
+ var auth_service_js_1 = require("./auth.service.js");
36
+ Object.defineProperty(exports, "AuthService", { enumerable: true, get: function () { return auth_service_js_1.AuthService; } });
37
+ Object.defineProperty(exports, "normaliseEmail", { enumerable: true, get: function () { return auth_service_js_1.normaliseEmail; } });
38
+ var session_service_js_1 = require("./session.service.js");
39
+ Object.defineProperty(exports, "SessionService", { enumerable: true, get: function () { return session_service_js_1.SessionService; } });
40
+ var otp_service_js_1 = require("./otp.service.js");
41
+ Object.defineProperty(exports, "OtpService", { enumerable: true, get: function () { return otp_service_js_1.OtpService; } });
42
+ var lockout_service_js_1 = require("./lockout.service.js");
43
+ Object.defineProperty(exports, "LockoutService", { enumerable: true, get: function () { return lockout_service_js_1.LockoutService; } });
44
+ var api_key_service_js_1 = require("./api-key.service.js");
45
+ Object.defineProperty(exports, "ApiKeyService", { enumerable: true, get: function () { return api_key_service_js_1.ApiKeyService; } });
46
+ Object.defineProperty(exports, "API_KEY_PREFIX", { enumerable: true, get: function () { return api_key_service_js_1.API_KEY_PREFIX; } });
47
+ var principal_resolver_js_1 = require("./principal.resolver.js");
48
+ Object.defineProperty(exports, "PrincipalResolver", { enumerable: true, get: function () { return principal_resolver_js_1.PrincipalResolver; } });
49
+ // Credentials.
50
+ var password_js_1 = require("./password.js");
51
+ Object.defineProperty(exports, "Argon2PasswordHasher", { enumerable: true, get: function () { return password_js_1.Argon2PasswordHasher; } });
52
+ Object.defineProperty(exports, "ARGON2_PARAMETERS", { enumerable: true, get: function () { return password_js_1.ARGON2_PARAMETERS; } });
53
+ Object.defineProperty(exports, "assertPasswordPolicy", { enumerable: true, get: function () { return password_js_1.assertPasswordPolicy; } });
54
+ Object.defineProperty(exports, "parseArgon2Parameters", { enumerable: true, get: function () { return password_js_1.parseArgon2Parameters; } });
55
+ var jwt_js_1 = require("./jwt.js");
56
+ Object.defineProperty(exports, "JoseTokenSigner", { enumerable: true, get: function () { return jwt_js_1.JoseTokenSigner; } });
57
+ Object.defineProperty(exports, "actorId", { enumerable: true, get: function () { return jwt_js_1.actorId; } });
58
+ Object.defineProperty(exports, "buildClaims", { enumerable: true, get: function () { return jwt_js_1.buildClaims; } });
59
+ Object.defineProperty(exports, "parseClaims", { enumerable: true, get: function () { return jwt_js_1.parseClaims; } });
60
+ // Authorization.
61
+ var decorators_js_1 = require("./decorators.js");
62
+ Object.defineProperty(exports, "AllowApiKey", { enumerable: true, get: function () { return decorators_js_1.AllowApiKey; } });
63
+ Object.defineProperty(exports, "AUTH_METADATA_KEYS", { enumerable: true, get: function () { return decorators_js_1.AUTH_METADATA_KEYS; } });
64
+ Object.defineProperty(exports, "Permissions", { enumerable: true, get: function () { return decorators_js_1.Permissions; } });
65
+ Object.defineProperty(exports, "Policy", { enumerable: true, get: function () { return decorators_js_1.Policy; } });
66
+ Object.defineProperty(exports, "Roles", { enumerable: true, get: function () { return decorators_js_1.Roles; } });
67
+ Object.defineProperty(exports, "isApiKeyAllowed", { enumerable: true, get: function () { return decorators_js_1.isApiKeyAllowed; } });
68
+ Object.defineProperty(exports, "readAuthorizationRules", { enumerable: true, get: function () { return decorators_js_1.readAuthorizationRules; } });
69
+ var policy_js_1 = require("./policy.js");
70
+ Object.defineProperty(exports, "PolicyRegistry", { enumerable: true, get: function () { return policy_js_1.PolicyRegistry; } });
71
+ Object.defineProperty(exports, "ownershipPolicy", { enumerable: true, get: function () { return policy_js_1.ownershipPolicy; } });
72
+ var auth_guard_js_1 = require("./auth.guard.js");
73
+ Object.defineProperty(exports, "AuthGuard", { enumerable: true, get: function () { return auth_guard_js_1.AuthGuard; } });
74
+ var authorization_guard_js_1 = require("./authorization.guard.js");
75
+ Object.defineProperty(exports, "AuthorizationGuard", { enumerable: true, get: function () { return authorization_guard_js_1.AuthorizationGuard; } });
76
+ // HTTP surface.
77
+ var auth_controller_js_1 = require("./auth.controller.js");
78
+ Object.defineProperty(exports, "AuthController", { enumerable: true, get: function () { return auth_controller_js_1.AuthController; } });
79
+ var auth_dto_js_1 = require("./auth.dto.js");
80
+ Object.defineProperty(exports, "ForgotPasswordDto", { enumerable: true, get: function () { return auth_dto_js_1.ForgotPasswordDto; } });
81
+ Object.defineProperty(exports, "ImpersonateDto", { enumerable: true, get: function () { return auth_dto_js_1.ImpersonateDto; } });
82
+ Object.defineProperty(exports, "LoginDto", { enumerable: true, get: function () { return auth_dto_js_1.LoginDto; } });
83
+ Object.defineProperty(exports, "RefreshTokenDto", { enumerable: true, get: function () { return auth_dto_js_1.RefreshTokenDto; } });
84
+ Object.defineProperty(exports, "ResetPasswordDto", { enumerable: true, get: function () { return auth_dto_js_1.ResetPasswordDto; } });
85
+ Object.defineProperty(exports, "SendOtpDto", { enumerable: true, get: function () { return auth_dto_js_1.SendOtpDto; } });
86
+ Object.defineProperty(exports, "VerifyOtpDto", { enumerable: true, get: function () { return auth_dto_js_1.VerifyOtpDto; } });
87
+ // Reference implementations — real behaviour, usable as test doubles (§19).
88
+ var memory_stores_js_1 = require("./memory-stores.js");
89
+ Object.defineProperty(exports, "MemoryApiKeyStore", { enumerable: true, get: function () { return memory_stores_js_1.MemoryApiKeyStore; } });
90
+ Object.defineProperty(exports, "MemoryAuthAuditSink", { enumerable: true, get: function () { return memory_stores_js_1.MemoryAuthAuditSink; } });
91
+ Object.defineProperty(exports, "MemoryAuthUserStore", { enumerable: true, get: function () { return memory_stores_js_1.MemoryAuthUserStore; } });
92
+ Object.defineProperty(exports, "MemoryLockoutStore", { enumerable: true, get: function () { return memory_stores_js_1.MemoryLockoutStore; } });
93
+ Object.defineProperty(exports, "MemoryOtpChannel", { enumerable: true, get: function () { return memory_stores_js_1.MemoryOtpChannel; } });
94
+ Object.defineProperty(exports, "MemoryOtpStore", { enumerable: true, get: function () { return memory_stores_js_1.MemoryOtpStore; } });
95
+ Object.defineProperty(exports, "MemorySessionStore", { enumerable: true, get: function () { return memory_stores_js_1.MemorySessionStore; } });
96
+ //# sourceMappingURL=index.js.map
package/dist/jwt.d.ts ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Access-token signing and verification (PLAN.md §12, §15.1).
3
+ *
4
+ * RS256 by default: verifiers hold only the public key, so a compromised API
5
+ * gateway or a downstream service cannot mint tokens. `jose` does the crypto;
6
+ * this file is about the parts that get security reviews wrong — that the
7
+ * algorithm is pinned rather than read from the token header, that issuer and
8
+ * audience are actually checked, and that a verification failure is mapped to a
9
+ * typed error instead of leaking a library message to the caller.
10
+ */
11
+ import type { JwtClaims, Permission } from '@nage-api/contracts';
12
+ import type { TokenSigner } from './ports.js';
13
+ import type { ResolvedAuthConfig } from './config.js';
14
+ export interface JoseTokenSignerOptions {
15
+ readonly config: ResolvedAuthConfig;
16
+ /** PEM private key, or the shared secret when the algorithm is HS256. */
17
+ readonly privateKey: string;
18
+ /** PEM public key. Omitted for HS256, where the secret verifies as well. */
19
+ readonly publicKey?: string;
20
+ }
21
+ export declare class JoseTokenSigner implements TokenSigner {
22
+ #private;
23
+ constructor(options: JoseTokenSignerOptions);
24
+ sign(claims: Record<string, unknown>, options: {
25
+ expiresInSeconds: number;
26
+ }): Promise<string>;
27
+ verify(token: string): Promise<Record<string, unknown>>;
28
+ }
29
+ /** Claims minted for an authenticated principal. */
30
+ export interface AccessTokenInput {
31
+ readonly userId: string;
32
+ readonly sessionId: string;
33
+ readonly roles: readonly string[];
34
+ readonly permissions: readonly Permission[];
35
+ readonly tenantId?: string;
36
+ readonly impersonatedBy?: string;
37
+ }
38
+ /** The claim set, kept in one place so signing and parsing cannot disagree. */
39
+ export declare function buildClaims(input: AccessTokenInput): Record<string, unknown>;
40
+ /**
41
+ * Read a verified payload back into typed claims.
42
+ *
43
+ * The payload is verified but still **untrusted in shape**: a token minted by
44
+ * an older version of this service, or by a deliberately odd one, can carry a
45
+ * `roles` that is not an array. Anything malformed is dropped rather than cast.
46
+ */
47
+ export declare function parseClaims(payload: Record<string, unknown>): JwtClaims;
48
+ /** The impersonator's id, when the token carries an `act` claim (RFC 8693). */
49
+ export declare function actorId(payload: Record<string, unknown>): string | undefined;
50
+ //# sourceMappingURL=jwt.d.ts.map