@agentforge-io/core 2.0.24 → 2.1.1
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/factory.js +56 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7 -1
- package/dist/services/agent-runner.service.js +57 -4
- package/dist/services/agent.service.d.ts +21 -1
- package/dist/services/agent.service.js +42 -8
- package/dist/services/orchestrator.service.d.ts +40 -1
- package/dist/services/orchestrator.service.js +220 -0
- package/dist/types/agent.types.d.ts +31 -6
- package/package.json +1 -1
- package/dist/adapters/billing/billing-adapter.interface.d.ts +0 -41
- package/dist/adapters/billing/billing-adapter.interface.js +0 -5
- package/dist/adapters/billing/stripe/stripe.adapter.d.ts +0 -30
- package/dist/adapters/billing/stripe/stripe.adapter.js +0 -122
- package/dist/adapters/email/email-adapter.interface.d.ts +0 -25
- package/dist/adapters/email/email-adapter.interface.js +0 -6
- package/dist/adapters/email/noop.adapter.d.ts +0 -10
- package/dist/adapters/email/noop.adapter.js +0 -15
- package/dist/adapters/email/resend.adapter.d.ts +0 -8
- package/dist/adapters/email/resend.adapter.js +0 -39
- package/dist/adapters/upload/noop.adapter.d.ts +0 -9
- package/dist/adapters/upload/noop.adapter.js +0 -14
- package/dist/adapters/upload/s3.adapter.d.ts +0 -38
- package/dist/adapters/upload/s3.adapter.js +0 -69
- package/dist/adapters/upload/upload-adapter.interface.d.ts +0 -37
- package/dist/adapters/upload/upload-adapter.interface.js +0 -15
- package/dist/billing/index.d.ts +0 -12
- package/dist/billing/index.js +0 -28
- package/dist/domain/agent.d.ts +0 -59
- package/dist/domain/agent.js +0 -2
- package/dist/domain/api-key.d.ts +0 -28
- package/dist/domain/api-key.js +0 -2
- package/dist/domain/auth-identity.d.ts +0 -10
- package/dist/domain/auth-identity.js +0 -2
- package/dist/domain/email-token.d.ts +0 -11
- package/dist/domain/email-token.js +0 -2
- package/dist/domain/external-user.d.ts +0 -23
- package/dist/domain/external-user.js +0 -2
- package/dist/domain/plan.d.ts +0 -20
- package/dist/domain/plan.js +0 -2
- package/dist/domain/platform-secret.d.ts +0 -24
- package/dist/domain/platform-secret.js +0 -8
- package/dist/domain/refresh-token.d.ts +0 -15
- package/dist/domain/refresh-token.js +0 -2
- package/dist/domain/subscription.d.ts +0 -21
- package/dist/domain/subscription.js +0 -2
- package/dist/domain/tenant.d.ts +0 -21
- package/dist/domain/tenant.js +0 -2
- package/dist/domain/usage-record.d.ts +0 -15
- package/dist/domain/usage-record.js +0 -2
- package/dist/domain/user.d.ts +0 -43
- package/dist/domain/user.js +0 -2
- package/dist/services/agent-config.service.d.ts +0 -45
- package/dist/services/agent-config.service.js +0 -114
- package/dist/services/api-key.service.d.ts +0 -41
- package/dist/services/api-key.service.js +0 -80
- package/dist/services/auth.service.d.ts +0 -133
- package/dist/services/auth.service.js +0 -411
- package/dist/services/billing.service.d.ts +0 -67
- package/dist/services/billing.service.js +0 -254
- package/dist/services/email-templates.d.ts +0 -18
- package/dist/services/email-templates.js +0 -39
- package/dist/services/email.service.d.ts +0 -26
- package/dist/services/email.service.js +0 -42
- package/dist/services/errors.d.ts +0 -7
- package/dist/services/errors.js +0 -27
- package/dist/services/oauth.service.d.ts +0 -73
- package/dist/services/oauth.service.js +0 -174
- package/dist/services/plan.service.d.ts +0 -54
- package/dist/services/plan.service.js +0 -120
- package/dist/services/refresh-token.service.d.ts +0 -38
- package/dist/services/refresh-token.service.js +0 -73
- package/dist/services/secrets/crypto.d.ts +0 -37
- package/dist/services/secrets/crypto.js +0 -110
- package/dist/services/secrets/known-keys.d.ts +0 -38
- package/dist/services/secrets/known-keys.js +0 -50
- package/dist/services/secrets.service.d.ts +0 -91
- package/dist/services/secrets.service.js +0 -193
- package/dist/services/tenant-billing.service.d.ts +0 -121
- package/dist/services/tenant-billing.service.js +0 -290
- package/dist/services/tenant.service.d.ts +0 -54
- package/dist/services/tenant.service.js +0 -96
- package/dist/services/upload.service.d.ts +0 -37
- package/dist/services/upload.service.js +0 -84
- package/dist/services/usage.service.d.ts +0 -34
- package/dist/services/usage.service.js +0 -108
- package/dist/types/billing.types.d.ts +0 -82
- package/dist/types/billing.types.js +0 -3
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import type { UserRepository, UserListOptions, UserListResult, AuthIdentityRepository, EmailTokenRepository } from '../repositories';
|
|
2
|
-
import type { User } from '../domain/user';
|
|
3
|
-
import type { RefreshTokenService } from './refresh-token.service';
|
|
4
|
-
import type { EmailService } from './email.service';
|
|
5
|
-
import type { Logger } from './tool-registry.service';
|
|
6
|
-
export interface AuthUser {
|
|
7
|
-
userId: string;
|
|
8
|
-
email?: string;
|
|
9
|
-
name?: string;
|
|
10
|
-
/** RBAC role from the `af_users.role` column. */
|
|
11
|
-
role?: 'user' | 'platform_admin';
|
|
12
|
-
/** Convenience boolean derived from `role === 'platform_admin'`. */
|
|
13
|
-
isPlatformAdmin?: boolean;
|
|
14
|
-
}
|
|
15
|
-
export interface AuthResult {
|
|
16
|
-
accessToken: string;
|
|
17
|
-
refreshToken: string;
|
|
18
|
-
user: {
|
|
19
|
-
id: string;
|
|
20
|
-
email?: string;
|
|
21
|
-
name?: string;
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
export interface SessionMeta {
|
|
25
|
-
userAgent?: string;
|
|
26
|
-
ip?: string;
|
|
27
|
-
}
|
|
28
|
-
export interface AuthServiceOptions {
|
|
29
|
-
jwtSecret: string;
|
|
30
|
-
/** Access-token TTL — string accepted by jsonwebtoken (e.g. '15m'). Default: '15m'. */
|
|
31
|
-
accessTokenTtl?: string;
|
|
32
|
-
/** Lock account after N consecutive failed logins. Default: 5. */
|
|
33
|
-
lockoutThreshold?: number;
|
|
34
|
-
/** Lockout duration in minutes. Default: 15. */
|
|
35
|
-
lockoutMinutes?: number;
|
|
36
|
-
/** When true, login is blocked until the user has clicked the verify link. Default: false. */
|
|
37
|
-
requireEmailVerification?: boolean;
|
|
38
|
-
/** Default plan id assigned at registration. */
|
|
39
|
-
defaultPlanId?: string;
|
|
40
|
-
/** Initial credit balance for newly-registered users. */
|
|
41
|
-
initialCredits?: number;
|
|
42
|
-
/** Verify-email token TTL in hours. Default: 24. */
|
|
43
|
-
verifyTokenTtlHours?: number;
|
|
44
|
-
/** Password-reset token TTL in minutes. Default: 60. */
|
|
45
|
-
resetTokenTtlMinutes?: number;
|
|
46
|
-
/** Hook fired after a user is created via any strategy. */
|
|
47
|
-
onUserCreated?: (user: {
|
|
48
|
-
id: string;
|
|
49
|
-
email?: string;
|
|
50
|
-
name?: string;
|
|
51
|
-
}) => void | Promise<void>;
|
|
52
|
-
logger?: Logger;
|
|
53
|
-
}
|
|
54
|
-
export declare class AuthService {
|
|
55
|
-
private readonly users;
|
|
56
|
-
private readonly identities;
|
|
57
|
-
private readonly emailTokens;
|
|
58
|
-
private readonly refreshTokens;
|
|
59
|
-
private readonly email;
|
|
60
|
-
private readonly opts;
|
|
61
|
-
private readonly logger;
|
|
62
|
-
constructor(users: UserRepository, identities: AuthIdentityRepository, emailTokens: EmailTokenRepository, refreshTokens: RefreshTokenService, email: EmailService, opts: AuthServiceOptions);
|
|
63
|
-
register(params: {
|
|
64
|
-
email: string;
|
|
65
|
-
password: string;
|
|
66
|
-
name?: string;
|
|
67
|
-
}, meta?: SessionMeta): Promise<AuthResult>;
|
|
68
|
-
validateCredentials(email: string, password: string): Promise<User>;
|
|
69
|
-
login(email: string, password: string, meta?: SessionMeta): Promise<AuthResult>;
|
|
70
|
-
refresh(rawToken: string, meta?: SessionMeta): Promise<AuthResult>;
|
|
71
|
-
logout(rawRefreshToken?: string): Promise<void>;
|
|
72
|
-
logoutEverywhere(userId: string): Promise<void>;
|
|
73
|
-
signInWithProvider(profile: {
|
|
74
|
-
provider: string;
|
|
75
|
-
providerId: string;
|
|
76
|
-
email?: string;
|
|
77
|
-
emailVerified?: boolean;
|
|
78
|
-
name?: string;
|
|
79
|
-
metadata?: Record<string, unknown>;
|
|
80
|
-
}, meta?: SessionMeta): Promise<AuthResult>;
|
|
81
|
-
sendVerificationEmail(user: User): Promise<void>;
|
|
82
|
-
resendVerificationEmail(userId: string): Promise<void>;
|
|
83
|
-
verifyEmail(rawToken: string): Promise<{
|
|
84
|
-
userId: string;
|
|
85
|
-
}>;
|
|
86
|
-
/** Always returns success — never reveals whether the email exists. */
|
|
87
|
-
requestPasswordReset(email: string): Promise<void>;
|
|
88
|
-
resetPassword(rawToken: string, newPassword: string): Promise<void>;
|
|
89
|
-
/**
|
|
90
|
-
* Verify a JWT and return the resolved user. Returns null if the token is
|
|
91
|
-
* invalid/expired or the user no longer exists / is disabled.
|
|
92
|
-
*/
|
|
93
|
-
/**
|
|
94
|
-
* Resolve a user from their id. Used by adapter-level JWT strategies that
|
|
95
|
-
* receive the decoded token payload from passport-jwt and need to populate
|
|
96
|
-
* `req.user`.
|
|
97
|
-
*/
|
|
98
|
-
getById(userId: string): Promise<AuthUser | null>;
|
|
99
|
-
verifyAccessToken(rawToken: string): Promise<AuthUser | null>;
|
|
100
|
-
private toAuthUser;
|
|
101
|
-
/**
|
|
102
|
-
* Update a user's role. Caller is responsible for authorization; this
|
|
103
|
-
* method only enforces invariants:
|
|
104
|
-
* - Demoting a platform_admin is blocked if it would leave zero platform
|
|
105
|
-
* admins, to prevent locking yourself out of admin operations.
|
|
106
|
-
*/
|
|
107
|
-
setUserRole(userId: string, role: User['role']): Promise<User>;
|
|
108
|
-
listUsers(opts?: UserListOptions): Promise<UserListResult>;
|
|
109
|
-
/**
|
|
110
|
-
* Look up a user by id and return the full `User` row (role, plan, last
|
|
111
|
-
* login, etc.) — distinct from `getById` which returns the lightweight
|
|
112
|
-
* `AuthUser` projection used in request contexts. Used by admin detail
|
|
113
|
-
* pages. Returns null when not found.
|
|
114
|
-
*/
|
|
115
|
-
getFullUserById(userId: string): Promise<User | null>;
|
|
116
|
-
/**
|
|
117
|
-
* Boot-time backfill for instances upgrading to RBAC. If no user holds the
|
|
118
|
-
* `platform_admin` role, the oldest user is promoted so the operator isn't
|
|
119
|
-
* locked out. Idempotent — no-op when at least one admin already exists.
|
|
120
|
-
*
|
|
121
|
-
* Call once from your server bootstrap after the DB is connected.
|
|
122
|
-
*/
|
|
123
|
-
ensurePlatformAdminBootstrap(): Promise<void>;
|
|
124
|
-
issueSession(user: User, meta?: SessionMeta): Promise<AuthResult>;
|
|
125
|
-
private signAccess;
|
|
126
|
-
private recordFailedLogin;
|
|
127
|
-
private recordSuccessfulLogin;
|
|
128
|
-
private mintEmailToken;
|
|
129
|
-
private consumeEmailToken;
|
|
130
|
-
private hashToken;
|
|
131
|
-
cleanupExpiredEmailTokens(olderThan?: Date): Promise<number>;
|
|
132
|
-
private runCreatedHook;
|
|
133
|
-
}
|
|
@@ -1,411 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.AuthService = void 0;
|
|
37
|
-
const bcrypt = __importStar(require("bcryptjs"));
|
|
38
|
-
const jwt = __importStar(require("jsonwebtoken"));
|
|
39
|
-
const crypto_1 = require("crypto");
|
|
40
|
-
const errors_1 = require("./errors");
|
|
41
|
-
const BCRYPT_COST = 12;
|
|
42
|
-
const noopLogger = { log: () => { }, warn: () => { }, debug: () => { }, error: () => { } };
|
|
43
|
-
class AuthService {
|
|
44
|
-
constructor(users, identities, emailTokens, refreshTokens, email, opts) {
|
|
45
|
-
this.users = users;
|
|
46
|
-
this.identities = identities;
|
|
47
|
-
this.emailTokens = emailTokens;
|
|
48
|
-
this.refreshTokens = refreshTokens;
|
|
49
|
-
this.email = email;
|
|
50
|
-
this.opts = opts;
|
|
51
|
-
if (!opts.jwtSecret)
|
|
52
|
-
throw new Error('AuthService: jwtSecret is required');
|
|
53
|
-
this.logger = opts.logger ?? noopLogger;
|
|
54
|
-
}
|
|
55
|
-
// ─── Local strategy ───────────────────────────────────────────────────────
|
|
56
|
-
async register(params, meta = {}) {
|
|
57
|
-
const email = params.email.trim().toLowerCase();
|
|
58
|
-
const existing = await this.users.findByEmail(email);
|
|
59
|
-
if (existing)
|
|
60
|
-
throw new errors_1.AuthError('email_exists', 'An account with that email already exists');
|
|
61
|
-
const passwordHash = await bcrypt.hash(params.password, BCRYPT_COST);
|
|
62
|
-
// Bootstrap: the very first user on a fresh install is auto-promoted to
|
|
63
|
-
// platform_admin. Subsequent users default to `user` and can be promoted
|
|
64
|
-
// through the admin UI by an existing platform admin.
|
|
65
|
-
const isFirstUser = (await this.users.count()) === 0;
|
|
66
|
-
if (isFirstUser) {
|
|
67
|
-
this.logger.log('Bootstrap: first user — granting platform_admin role.');
|
|
68
|
-
}
|
|
69
|
-
const user = await this.users.create({
|
|
70
|
-
id: (0, crypto_1.randomUUID)(),
|
|
71
|
-
email,
|
|
72
|
-
name: params.name,
|
|
73
|
-
passwordHash,
|
|
74
|
-
authProvider: 'local',
|
|
75
|
-
currentPlanId: this.opts.defaultPlanId ?? 'free',
|
|
76
|
-
creditsBalance: this.opts.initialCredits ?? 0,
|
|
77
|
-
role: isFirstUser ? 'platform_admin' : 'user',
|
|
78
|
-
isActive: true,
|
|
79
|
-
failedLoginCount: 0,
|
|
80
|
-
});
|
|
81
|
-
await this.runCreatedHook(user);
|
|
82
|
-
if (this.email.isEnabled()) {
|
|
83
|
-
this.sendVerificationEmail(user).catch((err) => this.logger.warn(`Verification email on register failed: ${err.message}`));
|
|
84
|
-
}
|
|
85
|
-
return this.issueSession(user, meta);
|
|
86
|
-
}
|
|
87
|
-
async validateCredentials(email, password) {
|
|
88
|
-
const normalized = email.trim().toLowerCase();
|
|
89
|
-
const user = await this.users.findByEmailWithSecrets(normalized);
|
|
90
|
-
const invalid = () => new errors_1.AuthError('invalid_credentials', 'Invalid email or password');
|
|
91
|
-
if (!user || !user.passwordHash)
|
|
92
|
-
throw invalid();
|
|
93
|
-
if (!user.isActive)
|
|
94
|
-
throw new errors_1.AuthError('account_disabled', 'Account is disabled');
|
|
95
|
-
if (user.lockedUntil && user.lockedUntil > new Date()) {
|
|
96
|
-
const minutes = Math.ceil((user.lockedUntil.getTime() - Date.now()) / 60000);
|
|
97
|
-
throw new errors_1.AuthError('account_locked', `Account temporarily locked. Try again in ${minutes} minute${minutes === 1 ? '' : 's'}.`);
|
|
98
|
-
}
|
|
99
|
-
if (this.opts.requireEmailVerification && !user.emailVerifiedAt) {
|
|
100
|
-
throw new errors_1.AuthError('email_unverified', 'Please verify your email before signing in.');
|
|
101
|
-
}
|
|
102
|
-
const ok = await bcrypt.compare(password, user.passwordHash);
|
|
103
|
-
if (!ok) {
|
|
104
|
-
await this.recordFailedLogin(user);
|
|
105
|
-
throw invalid();
|
|
106
|
-
}
|
|
107
|
-
await this.recordSuccessfulLogin(user);
|
|
108
|
-
return user;
|
|
109
|
-
}
|
|
110
|
-
async login(email, password, meta = {}) {
|
|
111
|
-
const user = await this.validateCredentials(email, password);
|
|
112
|
-
return this.issueSession(user, meta);
|
|
113
|
-
}
|
|
114
|
-
async refresh(rawToken, meta = {}) {
|
|
115
|
-
const rotated = await this.refreshTokens.rotate(rawToken, meta);
|
|
116
|
-
const user = await this.users.findById(rotated.userId);
|
|
117
|
-
if (!user || !user.isActive) {
|
|
118
|
-
throw new errors_1.AuthError('account_disabled', 'Account is disabled');
|
|
119
|
-
}
|
|
120
|
-
return {
|
|
121
|
-
accessToken: this.signAccess(user),
|
|
122
|
-
refreshToken: rotated.token,
|
|
123
|
-
user: { id: user.id, email: user.email, name: user.name },
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
async logout(rawRefreshToken) {
|
|
127
|
-
if (rawRefreshToken)
|
|
128
|
-
await this.refreshTokens.revoke(rawRefreshToken);
|
|
129
|
-
}
|
|
130
|
-
async logoutEverywhere(userId) {
|
|
131
|
-
await this.refreshTokens.revokeAllForUser(userId);
|
|
132
|
-
}
|
|
133
|
-
// ─── OAuth / external providers ───────────────────────────────────────────
|
|
134
|
-
async signInWithProvider(profile, meta = {}) {
|
|
135
|
-
const identity = await this.identities.findByProvider(profile.provider, profile.providerId);
|
|
136
|
-
if (identity) {
|
|
137
|
-
const user = await this.users.findById(identity.userId);
|
|
138
|
-
if (!user || !user.isActive)
|
|
139
|
-
throw new errors_1.AuthError('account_disabled', 'Account is disabled');
|
|
140
|
-
return this.issueSession(user, meta);
|
|
141
|
-
}
|
|
142
|
-
const email = profile.email?.trim().toLowerCase();
|
|
143
|
-
let user = null;
|
|
144
|
-
if (email && profile.emailVerified) {
|
|
145
|
-
user = await this.users.findByEmail(email);
|
|
146
|
-
}
|
|
147
|
-
if (!user) {
|
|
148
|
-
user = await this.users.create({
|
|
149
|
-
id: (0, crypto_1.randomUUID)(),
|
|
150
|
-
email,
|
|
151
|
-
name: profile.name,
|
|
152
|
-
authProvider: profile.provider,
|
|
153
|
-
currentPlanId: this.opts.defaultPlanId ?? 'free',
|
|
154
|
-
creditsBalance: this.opts.initialCredits ?? 0,
|
|
155
|
-
isActive: true,
|
|
156
|
-
failedLoginCount: 0,
|
|
157
|
-
});
|
|
158
|
-
await this.runCreatedHook(user);
|
|
159
|
-
}
|
|
160
|
-
else if (!user.isActive) {
|
|
161
|
-
throw new errors_1.AuthError('account_disabled', 'Account is disabled');
|
|
162
|
-
}
|
|
163
|
-
await this.identities.create({
|
|
164
|
-
userId: user.id,
|
|
165
|
-
provider: profile.provider,
|
|
166
|
-
providerId: profile.providerId,
|
|
167
|
-
email,
|
|
168
|
-
metadata: profile.metadata,
|
|
169
|
-
});
|
|
170
|
-
return this.issueSession(user, meta);
|
|
171
|
-
}
|
|
172
|
-
// ─── Email verification + password reset ──────────────────────────────────
|
|
173
|
-
async sendVerificationEmail(user) {
|
|
174
|
-
if (!user.email)
|
|
175
|
-
return;
|
|
176
|
-
if (user.emailVerifiedAt)
|
|
177
|
-
return;
|
|
178
|
-
const ttlHours = this.opts.verifyTokenTtlHours ?? 24;
|
|
179
|
-
const token = await this.mintEmailToken(user.id, 'verify_email', ttlHours * 60 * 60 * 1000);
|
|
180
|
-
await this.email.sendVerifyEmail({ to: user.email, token });
|
|
181
|
-
}
|
|
182
|
-
async resendVerificationEmail(userId) {
|
|
183
|
-
const user = await this.users.findById(userId);
|
|
184
|
-
if (!user || !user.email) {
|
|
185
|
-
throw new errors_1.AuthError('no_email_on_file', 'No email on file for this user');
|
|
186
|
-
}
|
|
187
|
-
if (user.emailVerifiedAt)
|
|
188
|
-
return;
|
|
189
|
-
await this.sendVerificationEmail(user);
|
|
190
|
-
}
|
|
191
|
-
async verifyEmail(rawToken) {
|
|
192
|
-
const entity = await this.consumeEmailToken(rawToken, 'verify_email');
|
|
193
|
-
await this.users.update(entity.userId, { emailVerifiedAt: new Date() });
|
|
194
|
-
return { userId: entity.userId };
|
|
195
|
-
}
|
|
196
|
-
/** Always returns success — never reveals whether the email exists. */
|
|
197
|
-
async requestPasswordReset(email) {
|
|
198
|
-
const normalized = email.trim().toLowerCase();
|
|
199
|
-
const user = await this.users.findByEmail(normalized);
|
|
200
|
-
if (!user || !user.email || !user.isActive) {
|
|
201
|
-
// Constant-time-ish jitter so request timing doesn't leak existence either.
|
|
202
|
-
await new Promise((r) => setTimeout(r, 100 + Math.random() * 200));
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
const ttlMinutes = this.opts.resetTokenTtlMinutes ?? 60;
|
|
206
|
-
const token = await this.mintEmailToken(user.id, 'reset_password', ttlMinutes * 60 * 1000);
|
|
207
|
-
try {
|
|
208
|
-
await this.email.sendPasswordResetEmail({ to: user.email, token });
|
|
209
|
-
}
|
|
210
|
-
catch (err) {
|
|
211
|
-
this.logger.warn(`Password-reset email failed for user ${user.id}: ${err.message}`);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
async resetPassword(rawToken, newPassword) {
|
|
215
|
-
if (!newPassword || newPassword.length < 8) {
|
|
216
|
-
throw new errors_1.AuthError('invalid_password', 'Password must be at least 8 characters');
|
|
217
|
-
}
|
|
218
|
-
const entity = await this.consumeEmailToken(rawToken, 'reset_password');
|
|
219
|
-
const passwordHash = await bcrypt.hash(newPassword, BCRYPT_COST);
|
|
220
|
-
await this.users.update(entity.userId, {
|
|
221
|
-
passwordHash,
|
|
222
|
-
failedLoginCount: 0,
|
|
223
|
-
lockedUntil: undefined,
|
|
224
|
-
});
|
|
225
|
-
await this.refreshTokens.revokeAllForUser(entity.userId);
|
|
226
|
-
}
|
|
227
|
-
// ─── Token verification (used by HTTP middleware) ─────────────────────────
|
|
228
|
-
/**
|
|
229
|
-
* Verify a JWT and return the resolved user. Returns null if the token is
|
|
230
|
-
* invalid/expired or the user no longer exists / is disabled.
|
|
231
|
-
*/
|
|
232
|
-
/**
|
|
233
|
-
* Resolve a user from their id. Used by adapter-level JWT strategies that
|
|
234
|
-
* receive the decoded token payload from passport-jwt and need to populate
|
|
235
|
-
* `req.user`.
|
|
236
|
-
*/
|
|
237
|
-
async getById(userId) {
|
|
238
|
-
const user = await this.users.findById(userId);
|
|
239
|
-
if (!user || !user.isActive)
|
|
240
|
-
return null;
|
|
241
|
-
return this.toAuthUser(user);
|
|
242
|
-
}
|
|
243
|
-
async verifyAccessToken(rawToken) {
|
|
244
|
-
let payload;
|
|
245
|
-
try {
|
|
246
|
-
payload = jwt.verify(rawToken, this.opts.jwtSecret);
|
|
247
|
-
}
|
|
248
|
-
catch {
|
|
249
|
-
return null;
|
|
250
|
-
}
|
|
251
|
-
if (!payload.sub)
|
|
252
|
-
return null;
|
|
253
|
-
const user = await this.users.findById(payload.sub);
|
|
254
|
-
if (!user || !user.isActive)
|
|
255
|
-
return null;
|
|
256
|
-
return this.toAuthUser(user);
|
|
257
|
-
}
|
|
258
|
-
toAuthUser(user) {
|
|
259
|
-
return {
|
|
260
|
-
userId: user.id,
|
|
261
|
-
email: user.email,
|
|
262
|
-
name: user.name,
|
|
263
|
-
role: user.role,
|
|
264
|
-
isPlatformAdmin: user.role === 'platform_admin',
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
// ─── User management (platform-admin operations) ──────────────────────────
|
|
268
|
-
/**
|
|
269
|
-
* Update a user's role. Caller is responsible for authorization; this
|
|
270
|
-
* method only enforces invariants:
|
|
271
|
-
* - Demoting a platform_admin is blocked if it would leave zero platform
|
|
272
|
-
* admins, to prevent locking yourself out of admin operations.
|
|
273
|
-
*/
|
|
274
|
-
async setUserRole(userId, role) {
|
|
275
|
-
const user = await this.users.findById(userId);
|
|
276
|
-
if (!user)
|
|
277
|
-
throw new errors_1.AuthError('user_not_found', 'User not found');
|
|
278
|
-
if (user.role === role)
|
|
279
|
-
return user;
|
|
280
|
-
if (user.role === 'platform_admin' && role !== 'platform_admin') {
|
|
281
|
-
const remaining = await this.users.countByRole('platform_admin');
|
|
282
|
-
if (remaining <= 1) {
|
|
283
|
-
throw new errors_1.AuthError('last_admin', 'Cannot remove the last platform admin — promote someone else first.');
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
await this.users.update(userId, { role });
|
|
287
|
-
return { ...user, role };
|
|
288
|
-
}
|
|
289
|
-
async listUsers(opts) {
|
|
290
|
-
return this.users.list(opts);
|
|
291
|
-
}
|
|
292
|
-
/**
|
|
293
|
-
* Look up a user by id and return the full `User` row (role, plan, last
|
|
294
|
-
* login, etc.) — distinct from `getById` which returns the lightweight
|
|
295
|
-
* `AuthUser` projection used in request contexts. Used by admin detail
|
|
296
|
-
* pages. Returns null when not found.
|
|
297
|
-
*/
|
|
298
|
-
async getFullUserById(userId) {
|
|
299
|
-
return this.users.findById(userId);
|
|
300
|
-
}
|
|
301
|
-
/**
|
|
302
|
-
* Boot-time backfill for instances upgrading to RBAC. If no user holds the
|
|
303
|
-
* `platform_admin` role, the oldest user is promoted so the operator isn't
|
|
304
|
-
* locked out. Idempotent — no-op when at least one admin already exists.
|
|
305
|
-
*
|
|
306
|
-
* Call once from your server bootstrap after the DB is connected.
|
|
307
|
-
*/
|
|
308
|
-
async ensurePlatformAdminBootstrap() {
|
|
309
|
-
const adminCount = await this.users.countByRole('platform_admin');
|
|
310
|
-
if (adminCount > 0)
|
|
311
|
-
return;
|
|
312
|
-
const totalUsers = await this.users.count();
|
|
313
|
-
if (totalUsers === 0) {
|
|
314
|
-
// Fresh install — the first `register()` call will promote naturally.
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
// Existing install with users but no admin. Prefer the oldest user that
|
|
318
|
-
// has an email — that's a real signup, not a test fixture seeded by
|
|
319
|
-
// earlier scripts. Fall back to the absolute oldest if no emailed user
|
|
320
|
-
// exists.
|
|
321
|
-
const all = await this.users.list();
|
|
322
|
-
// `list()` returns newest-first, so iterate from the tail (oldest first).
|
|
323
|
-
const ordered = [...all.items].reverse();
|
|
324
|
-
const target = ordered.find((u) => !!u.email) ?? ordered[0];
|
|
325
|
-
if (!target)
|
|
326
|
-
return;
|
|
327
|
-
await this.users.update(target.id, { role: 'platform_admin' });
|
|
328
|
-
this.logger.warn(`RBAC bootstrap: no platform admins found, promoted ` +
|
|
329
|
-
`${target.email ?? target.id} to platform_admin.`);
|
|
330
|
-
}
|
|
331
|
-
// ─── Internals ────────────────────────────────────────────────────────────
|
|
332
|
-
async issueSession(user, meta = {}) {
|
|
333
|
-
const accessToken = this.signAccess(user);
|
|
334
|
-
const minted = await this.refreshTokens.issue(user.id, meta);
|
|
335
|
-
return {
|
|
336
|
-
accessToken,
|
|
337
|
-
refreshToken: minted.token,
|
|
338
|
-
user: { id: user.id, email: user.email, name: user.name },
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
signAccess(user) {
|
|
342
|
-
const ttl = this.opts.accessTokenTtl ?? '15m';
|
|
343
|
-
return jwt.sign({ sub: user.id, email: user.email, name: user.name }, this.opts.jwtSecret,
|
|
344
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
345
|
-
{ expiresIn: ttl });
|
|
346
|
-
}
|
|
347
|
-
async recordFailedLogin(user) {
|
|
348
|
-
const threshold = this.opts.lockoutThreshold ?? 5;
|
|
349
|
-
const lockMinutes = this.opts.lockoutMinutes ?? 15;
|
|
350
|
-
const next = (user.failedLoginCount ?? 0) + 1;
|
|
351
|
-
if (next >= threshold) {
|
|
352
|
-
this.logger.warn(`User ${user.id} locked for ${lockMinutes}m after ${threshold} failed logins`);
|
|
353
|
-
await this.users.update(user.id, {
|
|
354
|
-
failedLoginCount: 0,
|
|
355
|
-
lockedUntil: new Date(Date.now() + lockMinutes * 60_000),
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
else {
|
|
359
|
-
await this.users.update(user.id, { failedLoginCount: next });
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
async recordSuccessfulLogin(user) {
|
|
363
|
-
await this.users.update(user.id, {
|
|
364
|
-
failedLoginCount: 0,
|
|
365
|
-
lockedUntil: undefined,
|
|
366
|
-
lastLoginAt: new Date(),
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
async mintEmailToken(userId, purpose, ttlMs) {
|
|
370
|
-
await this.emailTokens.invalidateActive(userId, purpose);
|
|
371
|
-
const raw = (0, crypto_1.randomBytes)(32).toString('hex');
|
|
372
|
-
await this.emailTokens.create({
|
|
373
|
-
userId,
|
|
374
|
-
tokenHash: this.hashToken(raw),
|
|
375
|
-
purpose,
|
|
376
|
-
expiresAt: new Date(Date.now() + ttlMs),
|
|
377
|
-
});
|
|
378
|
-
return raw;
|
|
379
|
-
}
|
|
380
|
-
async consumeEmailToken(rawToken, purpose) {
|
|
381
|
-
const tokenHash = this.hashToken(rawToken);
|
|
382
|
-
const entity = await this.emailTokens.findByHash(tokenHash);
|
|
383
|
-
if (!entity || entity.purpose !== purpose) {
|
|
384
|
-
throw new errors_1.AuthError('invalid_token', 'Invalid or expired token');
|
|
385
|
-
}
|
|
386
|
-
if (entity.consumedAt)
|
|
387
|
-
throw new errors_1.AuthError('token_consumed', 'Token already used');
|
|
388
|
-
if (entity.expiresAt <= new Date())
|
|
389
|
-
throw new errors_1.AuthError('token_expired', 'Token expired');
|
|
390
|
-
await this.emailTokens.markConsumed(entity.id);
|
|
391
|
-
return entity;
|
|
392
|
-
}
|
|
393
|
-
hashToken(token) {
|
|
394
|
-
return (0, crypto_1.createHash)('sha256').update(token).digest('hex');
|
|
395
|
-
}
|
|
396
|
-
async cleanupExpiredEmailTokens(olderThan = new Date()) {
|
|
397
|
-
return this.emailTokens.deleteExpired(olderThan);
|
|
398
|
-
}
|
|
399
|
-
async runCreatedHook(user) {
|
|
400
|
-
const hook = this.opts.onUserCreated;
|
|
401
|
-
if (!hook)
|
|
402
|
-
return;
|
|
403
|
-
try {
|
|
404
|
-
await hook({ id: user.id, email: user.email, name: user.name });
|
|
405
|
-
}
|
|
406
|
-
catch (err) {
|
|
407
|
-
this.logger.warn(`onUserCreated hook failed: ${err.message}`);
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
exports.AuthService = AuthService;
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import type { SubscriptionRepository, UserRepository, UsageRecordRepository } from '../repositories';
|
|
2
|
-
import type { IBillingAdapter } from '../adapters/billing/billing-adapter.interface';
|
|
3
|
-
import type { BillingConfig } from '../types/config.types';
|
|
4
|
-
import type { Plan } from '../domain/plan';
|
|
5
|
-
import type { UsageService } from './usage.service';
|
|
6
|
-
import type { PlanService } from './plan.service';
|
|
7
|
-
import type { BillingOverviewResult, CheckoutResult, PortalResult } from '../types/billing.types';
|
|
8
|
-
export interface BillingServiceOptions {
|
|
9
|
-
billing?: BillingConfig;
|
|
10
|
-
defaultLimits?: {
|
|
11
|
-
requestsPerMonth?: number;
|
|
12
|
-
tokensPerMonth?: number;
|
|
13
|
-
};
|
|
14
|
-
logger?: {
|
|
15
|
-
warn: (m: string) => void;
|
|
16
|
-
log: (m: string) => void;
|
|
17
|
-
debug?: (m: string) => void;
|
|
18
|
-
};
|
|
19
|
-
/** Source of truth for plans (DB-backed in production). */
|
|
20
|
-
plans: PlanService;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Billing for the B2C path: the end user is the payer. Mirrors
|
|
24
|
-
* TenantBillingService, scoped to individual users.
|
|
25
|
-
*/
|
|
26
|
-
export declare class BillingService {
|
|
27
|
-
private readonly users;
|
|
28
|
-
private readonly subscriptions;
|
|
29
|
-
private readonly usageRecords;
|
|
30
|
-
private readonly adapter;
|
|
31
|
-
private readonly usageService;
|
|
32
|
-
private readonly opts;
|
|
33
|
-
constructor(users: UserRepository, subscriptions: SubscriptionRepository, usageRecords: UsageRecordRepository, adapter: IBillingAdapter, usageService: UsageService, opts: BillingServiceOptions);
|
|
34
|
-
getPlans(): Promise<Plan[]>;
|
|
35
|
-
getPlan(planId: string): Promise<Plan>;
|
|
36
|
-
getDefaultPlanId(): Promise<string>;
|
|
37
|
-
createCheckout(input: {
|
|
38
|
-
userId: string;
|
|
39
|
-
planId: string;
|
|
40
|
-
/** Override URLs per request (e.g. coming from a frontend on a custom domain). */
|
|
41
|
-
successUrl?: string;
|
|
42
|
-
cancelUrl?: string;
|
|
43
|
-
appUrl?: string;
|
|
44
|
-
}): Promise<CheckoutResult>;
|
|
45
|
-
getPortalUrl(userId: string, returnUrl?: string): Promise<PortalResult>;
|
|
46
|
-
handleWebhook(payload: Buffer, signature: string): Promise<void>;
|
|
47
|
-
/**
|
|
48
|
-
* Same as handleWebhook but accepts an already-verified event. Used by
|
|
49
|
-
* routers that want to dispatch a single event to both the tenant and the
|
|
50
|
-
* user-centric handlers without re-verifying the signature.
|
|
51
|
-
*/
|
|
52
|
-
handleWebhookEvent(event: {
|
|
53
|
-
type: string;
|
|
54
|
-
data: Record<string, unknown>;
|
|
55
|
-
}): Promise<void>;
|
|
56
|
-
private onCheckoutCompleted;
|
|
57
|
-
private onSubscriptionUpdated;
|
|
58
|
-
private onSubscriptionDeleted;
|
|
59
|
-
private onPaymentFailed;
|
|
60
|
-
getOverview(userId: string): Promise<BillingOverviewResult>;
|
|
61
|
-
cancelSubscription(userId: string, immediately?: boolean): Promise<void>;
|
|
62
|
-
}
|
|
63
|
-
export declare class BillingError extends Error {
|
|
64
|
-
status: number;
|
|
65
|
-
code: 'not_found' | 'plan_not_found' | 'plan_not_purchasable' | 'no_customer' | 'no_active_subscription';
|
|
66
|
-
constructor(code: 'not_found' | 'plan_not_found' | 'plan_not_purchasable' | 'no_customer' | 'no_active_subscription', message: string);
|
|
67
|
-
}
|