@warlock.js/auth 4.0.39 → 4.0.42

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/esm/index.js CHANGED
@@ -1,8 +1,705 @@
1
- import {colors}from'@mongez/copper';import {config,command,rootPath,environment,t}from'@warlock.js/core';import {verify,hash}from'@mongez/password';import {Random}from'@mongez/reinforcements';import {migrate,Model}from'@warlock.js/cascade';import {v as v$1}from'@warlock.js/seal';import w from'@mongez/events';import {createVerifier,createSigner}from'fast-jwt';import {fileExistsAsync,getFileAsync,putFileAsync}from'@mongez/fs';import {log}from'@warlock.js/logger';var U=v$1.object({token:v$1.string().required(),lastAccess:v$1.date().default(()=>new Date),user:v$1.object({id:v$1.number().required(),userType:v$1.string()}).allowUnknown().required()}),l=class extends Model{static table="accessTokens";static schema=U};migrate(l,{name:"accessToken",up(){this.string("accessToken").index(),this.date("lastAccess");},down(){this.dropIndex("token");}});var $=v$1.object({token:v$1.string().required(),userId:v$1.number().required(),userType:v$1.string().required(),familyId:v$1.string().required(),expiresAt:v$1.date().required(),lastUsedAt:v$1.date().default(()=>new Date),revokedAt:v$1.date(),deviceInfo:v$1.record(v$1.any())}),u=class extends Model{static table="refreshTokens";static schema=$;get isExpired(){let e=this.get("expiresAt");return e?new Date>new Date(e):false}get isRevoked(){return !!this.get("revokedAt")}get isValid(){return !this.isExpired&&!this.isRevoked}async revoke(){return this.merge({revokedAt:new Date}).save()}async markAsUsed(){await this.merge({lastUsedAt:new Date}).save();}};var I=Symbol("NO_EXPIRATION");function b(t,e=36e5){if(t===void 0)return e;if(t!==I)return typeof t=="number"?t:typeof t=="string"?H(t):L(t)}function L(t){let e=0;return t.milliseconds&&(e+=t.milliseconds),t.seconds&&(e+=t.seconds*1e3),t.minutes&&(e+=t.minutes*60*1e3),t.hours&&(e+=t.hours*60*60*1e3),t.days&&(e+=t.days*24*60*60*1e3),t.weeks&&(e+=t.weeks*7*24*60*60*1e3),e}function H(t){let e=0,r=t.trim().split(/\s+/);for(let o of r){let n=o.match(/^(\d+(?:\.\d+)?)([smhdw])$/i);if(!n)continue;let s=parseFloat(n[1]);switch(n[2].toLowerCase()){case "s":e+=s*1e3;break;case "m":e+=s*60*1e3;break;case "h":e+=s*60*60*1e3;break;case "d":e+=s*24*60*60*1e3;break;case "w":e+=s*7*24*60*60*1e3;break}}return e||36e5}function S(t,e=36e5){let r=b(t,e);if(r!==void 0)return Math.floor(r/1e3)+"s"}var v="auth.",a={on(t,e){return w.subscribe(v+t,e)},subscribe(t,e){return this.on(t,e)},emit(t,...e){w.trigger(v+t,...e);},trigger(t,...e){this.emit(t,...e);},unsubscribeAll(){w.unsubscribeNamespace(v.slice(0,-1));},off(t){t?w.unsubscribe(v+t):this.unsubscribeAll();}};var j=()=>config.key("auth.jwt.secret"),g=()=>config.key("auth.jwt.algorithm"),M=()=>config.key("auth.jwt.refresh.secret"),X=()=>config.key("auth.jwt.refresh.expiresIn"),h={async generate(t,{key:e=j(),algorithm:r=g(),...o}={}){return createSigner({key:e,...o,algorithm:r})({...t})},async verify(t,{key:e=j(),algorithms:r=g()?[g()]:void 0,...o}={}){return await createVerifier({key:e,...o,algorithms:r})(t)},async generateRefreshToken(t,{key:e=M(),expiresIn:r=X(),algorithm:o=g(),...n}={}){return createSigner({key:e,expiresIn:r,algorithm:o,...n})({...t})},async verifyRefreshToken(t,{key:e=M(),algorithms:r=[g()],...o}={}){return await createVerifier({key:e,algorithms:r,...o})(t)}};var x=class{buildAccessTokenPayload(e){return {id:e.id,_id:e.get("_id"),userType:e.userType,createdAt:Date.now()}}async generateAccessToken(e,r){let o=r||this.buildAccessTokenPayload(e),n=config.key("auth.jwt.expiresIn"),s=S(n,36e5),i=s?await h.generate(o,{expiresIn:s}):await h.generate(o);return await l.create({token:i,user:o}),i}async createRefreshToken(e,r){let o=r?.familyId||Random.string(32),n=config.key("auth.jwt.refresh.expiresIn"),s=b(n,10080*60*1e3),i={userId:e.id,userType:e.userType,familyId:o},p=await h.generateRefreshToken(i);await this.enforceMaxRefreshTokens(e);let f=s?new Date(Date.now()+s):new Date(Date.now()+100*365*24*60*60*1e3);return u.create({token:p,userId:e.id,userType:e.userType,familyId:o,expiresAt:f,deviceInfo:r?{userAgent:r.userAgent,ip:r.ip,deviceId:r.deviceId}:void 0})}async createTokenPair(e,r){let o=await this.generateAccessToken(e,r?.payload),n=await this.createRefreshToken(e,r),s={accessToken:o,refreshToken:n.get("token"),expiresIn:config.key("auth.jwt.expiresIn","1h")};return a.emit("token.created",e,s),a.emit("session.created",e,n,r),s}async refreshTokens(e,r){try{let o=await h.verifyRefreshToken(e);if(!o)return null;let n=await u.first({token:e});if(!n?.isValid)return n&&await this.revokeTokenFamily(n.get("familyId")),null;let s=config.key(`auth.userType.${o.userType}`);if(!s)return null;let i=await s.find(o.userId);if(!i)return null;config.key("auth.jwt.refresh.rotation",!0)?await n.revoke():await n.markAsUsed();let f=await this.createTokenPair(i,{...r,familyId:n.get("familyId")});return a.emit("token.refreshed",i,f,n),f}catch{return null}}verifyPassword(e,r){return verify(String(e),String(r))}hashPassword(e){return hash(String(e),config.key("auth.password.salt",12))}async attemptLogin(e,r){let{password:o,...n}=r;a.emit("login.attempt",n);let s=await e.first(n);return s?this.verifyPassword(s.string("password"),o)?s:(a.emit("login.failed",n,"Invalid password"),null):(a.emit("login.failed",n,"User not found"),null)}async login(e,r,o){let n=await this.attemptLogin(e,r);if(!n)return null;if(!config.key("auth.jwt.refresh.enabled",true)){let i=await this.generateAccessToken(n,o?.payload);return {user:n,accessToken:i}}let s=await this.createTokenPair(n,o);return a.emit("login.success",n,s,o),{user:n,tokens:s}}async logout(e,r,o){if(r&&await this.removeAccessToken(e,r),o){let n=await u.first({token:o,userId:e.id});n&&(await n.revoke(),a.emit("session.destroyed",e,n));}else {if(config.key("auth.jwt.refresh.logoutWithoutToken","revoke-all")==="error")throw new Error("Refresh token required for logout");await this.revokeAllTokens(e),a.emit("logout.failsafe",e);}a.emit("logout",e);}async removeAccessToken(e,r){l.delete({token:r,"user.id":e.id});}async revokeAllTokens(e){let r=await u.query().where("userId",e.id).where("userType",e.userType).where("revokedAt",null).get();for(let o of r)await o.revoke(),a.emit("token.revoked",e,o);await l.delete({"user.id":e.id,"user.userType":e.userType}),a.emit("logout.all",e);}async revokeTokenFamily(e){let r=await u.query().where("familyId",e).where("revokedAt",null).get();for(let o of r)await o.revoke();a.emit("token.familyRevoked",e,r);}async cleanupExpiredTokens(){let e=await u.query().where("expiresAt","<",new Date).get();for(let r of e)a.emit("token.expired",r),await r.destroy();return a.emit("cleanup.completed",e.length),e.length}async enforceMaxRefreshTokens(e){let r=config.key("auth.jwt.refresh.maxPerUser",5),o=await u.query().where("userId",e.id).where("userType",e.userType).where("revokedAt",null).orderBy("createdAt","asc").get();if(o.length>=r){let n=o.slice(0,o.length-r+1);for(let s of n)await s.revoke();}}async getActiveSessions(e){return u.query().where("userId",e.id).where("userType",e.userType).where("revokedAt",null).where("expiresAt",">",new Date).orderBy("createdAt","desc").get()}},c=new x;function ze(){return command({name:"auth.cleanup",description:"Remove expired refresh tokens from the database",preload:{env:true,config:["auth","database"],connectors:["database"]},action:async()=>{console.log(colors.cyan("\u{1F9F9} Cleaning up expired tokens..."));let t=await c.cleanupExpiredTokens();console.log(t===0?colors.green("\u2705 No expired tokens found."):colors.green(`\u2705 Removed ${t} expired token(s).`));}})}async function N(){let t=rootPath(".env");log.info("jwt","generating","Generating JWT secrets");let e=environment();if(await fileExistsAsync(t)||(t=rootPath(e==="production"?".env.production":".env.development")),!await fileExistsAsync(t)){log.error("jwt","error",".env file not found");return}let r=await getFileAsync(t),o=r.includes("JWT_SECRET"),n=r.includes("JWT_REFRESH_SECRET");if(o&&n){log.warn("jwt","exists","JWT secrets already exist in the .env file.");return}let s="";if(o)log.info("jwt","exists","JWT_SECRET already exists in the .env file.");else {let i=Random.string(32);s+=`
1
+ import { colors } from '@mongez/copper';
2
+ import { config, command, rootPath, environment, t } from '@warlock.js/core';
3
+ import { verify, hash } from '@mongez/password';
4
+ import { Random } from '@mongez/reinforcements';
5
+ import { migrate, Model } from '@warlock.js/cascade';
6
+ import { v } from '@warlock.js/seal';
7
+ import events from '@mongez/events';
8
+ import { createVerifier, createSigner } from 'fast-jwt';
9
+ import { fileExistsAsync, getFileAsync, putFileAsync } from '@mongez/fs';
10
+ import { log } from '@warlock.js/logger';
11
+
12
+ // ../../warlock.js/auth/src/commands/auth-cleanup-command.ts
13
+ var accessTokenSchema = v.object({
14
+ token: v.string().required(),
15
+ lastAccess: v.date().default(() => /* @__PURE__ */ new Date()),
16
+ user: v.object({
17
+ id: v.number().required(),
18
+ userType: v.string()
19
+ }).allowUnknown().required()
20
+ });
21
+ var AccessToken = class extends Model {
22
+ /**
23
+ * {@inheritDoc}
24
+ */
25
+ static table = "accessTokens";
26
+ static schema = accessTokenSchema;
27
+ };
28
+ migrate(AccessToken, {
29
+ name: "accessToken",
30
+ up() {
31
+ this.string("accessToken").index();
32
+ this.date("lastAccess");
33
+ },
34
+ down() {
35
+ this.dropIndex("token");
36
+ }
37
+ });
38
+ var refreshTokenSchema = v.object({
39
+ token: v.string().required(),
40
+ userId: v.number().required(),
41
+ userType: v.string().required(),
42
+ familyId: v.string().required(),
43
+ expiresAt: v.date().required(),
44
+ lastUsedAt: v.date().default(() => /* @__PURE__ */ new Date()),
45
+ revokedAt: v.date(),
46
+ deviceInfo: v.record(v.any())
47
+ });
48
+ var RefreshToken = class extends Model {
49
+ /**
50
+ * {@inheritDoc}
51
+ */
52
+ static table = "refreshTokens";
53
+ /**
54
+ * {@inheritDoc}
55
+ */
56
+ static schema = refreshTokenSchema;
57
+ /**
58
+ * Check if token is expired
59
+ */
60
+ get isExpired() {
61
+ const expiresAt = this.get("expiresAt");
62
+ if (!expiresAt) return false;
63
+ return /* @__PURE__ */ new Date() > new Date(expiresAt);
64
+ }
65
+ /**
66
+ * Check if token is revoked
67
+ */
68
+ get isRevoked() {
69
+ return !!this.get("revokedAt");
70
+ }
71
+ /**
72
+ * Check if token is valid (not expired and not revoked)
73
+ */
74
+ get isValid() {
75
+ return !this.isExpired && !this.isRevoked;
76
+ }
77
+ /**
78
+ * Revoke this token
79
+ */
80
+ async revoke() {
81
+ return this.merge({ revokedAt: /* @__PURE__ */ new Date() }).save();
82
+ }
83
+ /**
84
+ * Mark token as used (update lastUsedAt)
85
+ */
86
+ async markAsUsed() {
87
+ await this.merge({ lastUsedAt: /* @__PURE__ */ new Date() }).save();
88
+ }
89
+ };
90
+
91
+ // ../../warlock.js/auth/src/contracts/types.ts
92
+ var NO_EXPIRATION = /* @__PURE__ */ Symbol("NO_EXPIRATION");
93
+
94
+ // ../../warlock.js/auth/src/utils/duration.ts
95
+ function parseExpirationToMs(expiration, defaultMs = 36e5) {
96
+ if (expiration === void 0) {
97
+ return defaultMs;
98
+ }
99
+ if (expiration === NO_EXPIRATION) {
100
+ return void 0;
101
+ }
102
+ if (typeof expiration === "number") {
103
+ return expiration;
104
+ }
105
+ if (typeof expiration === "string") {
106
+ return parseStringDuration(expiration);
107
+ }
108
+ return parseDurationObject(expiration);
109
+ }
110
+ function parseDurationObject(duration) {
111
+ let ms = 0;
112
+ if (duration.milliseconds) ms += duration.milliseconds;
113
+ if (duration.seconds) ms += duration.seconds * 1e3;
114
+ if (duration.minutes) ms += duration.minutes * 60 * 1e3;
115
+ if (duration.hours) ms += duration.hours * 60 * 60 * 1e3;
116
+ if (duration.days) ms += duration.days * 24 * 60 * 60 * 1e3;
117
+ if (duration.weeks) ms += duration.weeks * 7 * 24 * 60 * 60 * 1e3;
118
+ return ms;
119
+ }
120
+ function parseStringDuration(str) {
121
+ let totalMs = 0;
122
+ const parts = str.trim().split(/\s+/);
123
+ for (const part of parts) {
124
+ const match = part.match(/^(\d+(?:\.\d+)?)([smhdw])$/i);
125
+ if (!match) continue;
126
+ const value = parseFloat(match[1]);
127
+ const unit = match[2].toLowerCase();
128
+ switch (unit) {
129
+ case "s":
130
+ totalMs += value * 1e3;
131
+ break;
132
+ case "m":
133
+ totalMs += value * 60 * 1e3;
134
+ break;
135
+ case "h":
136
+ totalMs += value * 60 * 60 * 1e3;
137
+ break;
138
+ case "d":
139
+ totalMs += value * 24 * 60 * 60 * 1e3;
140
+ break;
141
+ case "w":
142
+ totalMs += value * 7 * 24 * 60 * 60 * 1e3;
143
+ break;
144
+ }
145
+ }
146
+ return totalMs || 36e5;
147
+ }
148
+ function toJwtExpiresIn(expiration, defaultMs = 36e5) {
149
+ const ms = parseExpirationToMs(expiration, defaultMs);
150
+ if (ms === void 0) return void 0;
151
+ return Math.floor(ms / 1e3) + "s";
152
+ }
153
+ var AUTH_EVENT_PREFIX = "auth.";
154
+ var authEvents = {
155
+ /**
156
+ * Subscribe to an auth event
157
+ */
158
+ on(event, callback) {
159
+ return events.subscribe(AUTH_EVENT_PREFIX + event, callback);
160
+ },
161
+ /**
162
+ * Subscribe to an auth event (alias for `on`)
163
+ */
164
+ subscribe(event, callback) {
165
+ return this.on(event, callback);
166
+ },
167
+ /**
168
+ * Emit an auth event
169
+ */
170
+ emit(event, ...args) {
171
+ events.trigger(AUTH_EVENT_PREFIX + event, ...args);
172
+ },
173
+ /**
174
+ * Emit an auth event (alias for `emit`)
175
+ */
176
+ trigger(event, ...args) {
177
+ this.emit(event, ...args);
178
+ },
179
+ /**
180
+ * Unsubscribe from all auth events
181
+ */
182
+ unsubscribeAll() {
183
+ events.unsubscribeNamespace(AUTH_EVENT_PREFIX.slice(0, -1));
184
+ },
185
+ /**
186
+ * Unsubscribe from a specific auth event
187
+ */
188
+ off(event) {
189
+ if (event) {
190
+ events.unsubscribe(AUTH_EVENT_PREFIX + event);
191
+ } else {
192
+ this.unsubscribeAll();
193
+ }
194
+ }
195
+ };
196
+ var getSecretKey = () => config.key("auth.jwt.secret");
197
+ var getAlgorithm = () => config.key("auth.jwt.algorithm");
198
+ var getRefreshSecretKey = () => config.key("auth.jwt.refresh.secret");
199
+ var getRefreshTokenValidity = () => config.key("auth.jwt.refresh.expiresIn");
200
+ var jwt = {
201
+ /**
202
+ * Generate a new JWT token for the user.
203
+ * @param payload The payload to encode in the JWT token.
204
+ */
205
+ async generate(payload, {
206
+ key = getSecretKey(),
207
+ algorithm = getAlgorithm(),
208
+ ...options
209
+ } = {}) {
210
+ const sign = createSigner({ key, ...options, algorithm });
211
+ return sign({ ...payload });
212
+ },
213
+ /**
214
+ * Verify the given token.
215
+ * @param token The JWT token to verify.
216
+ * @returns The decoded token payload if verification is successful.
217
+ */
218
+ async verify(token, {
219
+ key = getSecretKey(),
220
+ algorithms = getAlgorithm() ? [getAlgorithm()] : void 0,
221
+ ...options
222
+ } = {}) {
223
+ const verify2 = createVerifier({ key, ...options, algorithms });
224
+ return await verify2(token);
225
+ },
226
+ /**
227
+ * Generate a new refresh token for the user.
228
+ */
229
+ async generateRefreshToken(payload, {
230
+ key = getRefreshSecretKey(),
231
+ expiresIn = getRefreshTokenValidity(),
232
+ algorithm = getAlgorithm(),
233
+ ...options
234
+ } = {}) {
235
+ const sign = createSigner({ key, expiresIn, algorithm, ...options });
236
+ return sign({ ...payload });
237
+ },
238
+ /**
239
+ * Verify the given refresh token.
240
+ */
241
+ async verifyRefreshToken(token, {
242
+ key = getRefreshSecretKey(),
243
+ algorithms = [getAlgorithm()],
244
+ ...options
245
+ } = {}) {
246
+ const verify2 = createVerifier({ key, algorithms, ...options });
247
+ return await verify2(token);
248
+ }
249
+ };
250
+
251
+ // ../../warlock.js/auth/src/services/auth.service.ts
252
+ var AuthService = class {
253
+ /**
254
+ * Build access token payload from user
255
+ */
256
+ buildAccessTokenPayload(user) {
257
+ return {
258
+ id: user.id,
259
+ _id: user.get("_id"),
260
+ userType: user.userType,
261
+ createdAt: Date.now()
262
+ };
263
+ }
264
+ /**
265
+ * Generate access token for user
266
+ */
267
+ async generateAccessToken(user, payload) {
268
+ const data = payload || this.buildAccessTokenPayload(user);
269
+ const expiresInConfig = config.key("auth.jwt.expiresIn");
270
+ const expiresIn = toJwtExpiresIn(expiresInConfig, 36e5);
271
+ const token = expiresIn ? await jwt.generate(data, { expiresIn }) : await jwt.generate(data);
272
+ await AccessToken.create({
273
+ token,
274
+ user: data
275
+ });
276
+ return token;
277
+ }
278
+ /**
279
+ * Create refresh token for user
280
+ */
281
+ async createRefreshToken(user, deviceInfo) {
282
+ const familyId = deviceInfo?.familyId || Random.string(32);
283
+ const expiresInConfig = config.key("auth.jwt.refresh.expiresIn");
284
+ const expiresInMs = parseExpirationToMs(expiresInConfig, 7 * 24 * 60 * 60 * 1e3);
285
+ const payload = {
286
+ userId: user.id,
287
+ userType: user.userType,
288
+ familyId
289
+ };
290
+ const token = await jwt.generateRefreshToken(payload);
291
+ await this.enforceMaxRefreshTokens(user);
292
+ const expiresAt = expiresInMs ? new Date(Date.now() + expiresInMs) : new Date(Date.now() + 100 * 365 * 24 * 60 * 60 * 1e3);
293
+ return RefreshToken.create({
294
+ token,
295
+ userId: user.id,
296
+ userType: user.userType,
297
+ familyId,
298
+ expiresAt,
299
+ deviceInfo: deviceInfo ? {
300
+ userAgent: deviceInfo.userAgent,
301
+ ip: deviceInfo.ip,
302
+ deviceId: deviceInfo.deviceId
303
+ } : void 0
304
+ });
305
+ }
306
+ /**
307
+ * Create both access and refresh tokens
308
+ */
309
+ async createTokenPair(user, deviceInfo) {
310
+ const accessToken = await this.generateAccessToken(user, deviceInfo?.payload);
311
+ const refreshToken = await this.createRefreshToken(user, deviceInfo);
312
+ const tokenPair = {
313
+ accessToken,
314
+ refreshToken: refreshToken.get("token"),
315
+ expiresIn: config.key("auth.jwt.expiresIn", "1h")
316
+ };
317
+ authEvents.emit("token.created", user, tokenPair);
318
+ authEvents.emit("session.created", user, refreshToken, deviceInfo);
319
+ return tokenPair;
320
+ }
321
+ /**
322
+ * Refresh tokens using a refresh token
323
+ */
324
+ async refreshTokens(refreshTokenString, deviceInfo) {
325
+ try {
326
+ const decoded = await jwt.verifyRefreshToken(refreshTokenString);
327
+ if (!decoded) return null;
328
+ const refreshToken = await RefreshToken.first({ token: refreshTokenString });
329
+ if (!refreshToken?.isValid) {
330
+ if (refreshToken) {
331
+ await this.revokeTokenFamily(refreshToken.get("familyId"));
332
+ }
333
+ return null;
334
+ }
335
+ const UserModel = config.key(`auth.userType.${decoded.userType}`);
336
+ if (!UserModel) return null;
337
+ const user = await UserModel.find(decoded.userId);
338
+ if (!user) return null;
339
+ const rotationEnabled = config.key("auth.jwt.refresh.rotation", true);
340
+ if (rotationEnabled) {
341
+ await refreshToken.revoke();
342
+ } else {
343
+ await refreshToken.markAsUsed();
344
+ }
345
+ const newTokenPair = await this.createTokenPair(user, {
346
+ ...deviceInfo,
347
+ familyId: refreshToken.get("familyId")
348
+ });
349
+ authEvents.emit("token.refreshed", user, newTokenPair, refreshToken);
350
+ return newTokenPair;
351
+ } catch {
352
+ return null;
353
+ }
354
+ }
355
+ /**
356
+ * Verify password
357
+ */
358
+ verifyPassword(hashedPassword, plainPassword) {
359
+ return verify(String(hashedPassword), String(plainPassword));
360
+ }
361
+ /**
362
+ * Hash password
363
+ */
364
+ hashPassword(password) {
365
+ return hash(String(password), config.key("auth.password.salt", 12));
366
+ }
367
+ /**
368
+ * Attempt to login user with given credentials
369
+ */
370
+ async attemptLogin(Model4, data) {
371
+ const { password, ...otherData } = data;
372
+ authEvents.emit("login.attempt", otherData);
373
+ const user = await Model4.first(otherData);
374
+ if (!user) {
375
+ authEvents.emit("login.failed", otherData, "User not found");
376
+ return null;
377
+ }
378
+ if (!this.verifyPassword(user.string("password"), password)) {
379
+ authEvents.emit("login.failed", otherData, "Invalid password");
380
+ return null;
381
+ }
382
+ return user;
383
+ }
384
+ /**
385
+ * Full login flow: validate credentials, create tokens, emit events
386
+ * Returns token pair on success, null on failure
387
+ */
388
+ async login(Model4, credentials, deviceInfo) {
389
+ const user = await this.attemptLogin(Model4, credentials);
390
+ if (!user) {
391
+ return null;
392
+ }
393
+ if (!config.key("auth.jwt.refresh.enabled", true)) {
394
+ const accessToken = await this.generateAccessToken(user, deviceInfo?.payload);
395
+ return { user, accessToken };
396
+ }
397
+ const tokens = await this.createTokenPair(user, deviceInfo);
398
+ authEvents.emit("login.success", user, tokens, deviceInfo);
399
+ return { user, tokens };
400
+ }
401
+ /**
402
+ * Logout user
403
+ * @param user - The authenticated user
404
+ * @param accessToken - Optional access token string to revoke
405
+ * @param refreshToken - Optional refresh token string to revoke
406
+ * If refresh token is not provided, behavior is determined by config:
407
+ * - "revoke-all" (default): Revoke ALL refresh tokens for security
408
+ * - "error": Throw error requiring refresh token
409
+ */
410
+ async logout(user, accessToken, refreshToken) {
411
+ if (accessToken) {
412
+ await this.removeAccessToken(user, accessToken);
413
+ }
414
+ if (refreshToken) {
415
+ const token = await RefreshToken.first({
416
+ token: refreshToken,
417
+ userId: user.id
418
+ // Security: ensure token belongs to this user
419
+ });
420
+ if (token) {
421
+ await token.revoke();
422
+ authEvents.emit("session.destroyed", user, token);
423
+ }
424
+ } else {
425
+ const behavior = config.key("auth.jwt.refresh.logoutWithoutToken", "revoke-all");
426
+ if (behavior === "error") {
427
+ throw new Error("Refresh token required for logout");
428
+ }
429
+ await this.revokeAllTokens(user);
430
+ authEvents.emit("logout.failsafe", user);
431
+ }
432
+ authEvents.emit("logout", user);
433
+ }
434
+ /**
435
+ * Remove specific access token
436
+ */
437
+ async removeAccessToken(user, token) {
438
+ AccessToken.delete({
439
+ token,
440
+ "user.id": user.id
441
+ });
442
+ }
443
+ /**
444
+ * Revoke all tokens for a user
445
+ */
446
+ async revokeAllTokens(user) {
447
+ const refreshTokens = await RefreshToken.query().where("userId", user.id).where("userType", user.userType).where("revokedAt", null).get();
448
+ for (const token of refreshTokens) {
449
+ await token.revoke();
450
+ authEvents.emit("token.revoked", user, token);
451
+ }
452
+ await AccessToken.delete({
453
+ "user.id": user.id,
454
+ "user.userType": user.userType
455
+ });
456
+ authEvents.emit("logout.all", user);
457
+ }
458
+ /**
459
+ * Revoke entire token family (for rotation breach detection)
460
+ */
461
+ async revokeTokenFamily(familyId) {
462
+ const tokens = await RefreshToken.query().where("familyId", familyId).where("revokedAt", null).get();
463
+ for (const token of tokens) {
464
+ await token.revoke();
465
+ }
466
+ authEvents.emit("token.familyRevoked", familyId, tokens);
467
+ }
468
+ /**
469
+ * Cleanup expired tokens
470
+ */
471
+ async cleanupExpiredTokens() {
472
+ const expiredTokens = await RefreshToken.query().where("expiresAt", "<", /* @__PURE__ */ new Date()).get();
473
+ for (const token of expiredTokens) {
474
+ authEvents.emit("token.expired", token);
475
+ await token.destroy();
476
+ }
477
+ authEvents.emit("cleanup.completed", expiredTokens.length);
478
+ return expiredTokens.length;
479
+ }
480
+ /**
481
+ * Enforce max refresh tokens per user
482
+ */
483
+ async enforceMaxRefreshTokens(user) {
484
+ const maxPerUser = config.key("auth.jwt.refresh.maxPerUser", 5);
485
+ const activeTokens = await RefreshToken.query().where("userId", user.id).where("userType", user.userType).where("revokedAt", null).orderBy("createdAt", "asc").get();
486
+ if (activeTokens.length >= maxPerUser) {
487
+ const tokensToRevoke = activeTokens.slice(0, activeTokens.length - maxPerUser + 1);
488
+ for (const token of tokensToRevoke) {
489
+ await token.revoke();
490
+ }
491
+ }
492
+ }
493
+ /**
494
+ * Get active sessions for user
495
+ */
496
+ async getActiveSessions(user) {
497
+ return RefreshToken.query().where("userId", user.id).where("userType", user.userType).where("revokedAt", null).where("expiresAt", ">", /* @__PURE__ */ new Date()).orderBy("createdAt", "desc").get();
498
+ }
499
+ };
500
+ var authService = new AuthService();
501
+
502
+ // ../../warlock.js/auth/src/commands/auth-cleanup-command.ts
503
+ function registerAuthCleanupCommand() {
504
+ return command({
505
+ name: "auth.cleanup",
506
+ description: "Remove expired refresh tokens from the database",
507
+ preload: {
508
+ env: true,
509
+ config: ["auth", "database"],
510
+ connectors: ["database"]
511
+ },
512
+ action: async () => {
513
+ console.log(colors.cyan("\u{1F9F9} Cleaning up expired tokens..."));
514
+ const count = await authService.cleanupExpiredTokens();
515
+ if (count === 0) {
516
+ console.log(colors.green("\u2705 No expired tokens found."));
517
+ } else {
518
+ console.log(colors.green(`\u2705 Removed ${count} expired token(s).`));
519
+ }
520
+ }
521
+ });
522
+ }
523
+ async function generateJWTSecret() {
524
+ let envFile = rootPath(".env");
525
+ log.info("jwt", "generating", "Generating JWT secrets");
526
+ const environmentMode = environment();
527
+ if (!await fileExistsAsync(envFile)) {
528
+ const envFileType = environmentMode === "production" ? ".env.production" : ".env.development";
529
+ envFile = rootPath(envFileType);
530
+ }
531
+ if (!await fileExistsAsync(envFile)) {
532
+ log.error("jwt", "error", ".env file not found");
533
+ return;
534
+ }
535
+ let contents = await getFileAsync(envFile);
536
+ const hasJwtSecret = contents.includes("JWT_SECRET");
537
+ const hasJwtRefreshSecret = contents.includes("JWT_REFRESH_SECRET");
538
+ if (hasJwtSecret && hasJwtRefreshSecret) {
539
+ log.warn("jwt", "exists", "JWT secrets already exist in the .env file.");
540
+ return;
541
+ }
542
+ let secretsToAdd = "";
543
+ if (!hasJwtSecret) {
544
+ const jwtSecret = Random.string(32);
545
+ secretsToAdd += `
2
546
  # JWT Secret
3
- JWT_SECRET=${i}
4
- `,log.success("jwt","generated","JWT_SECRET generated and added to the .env file.");}if(n)log.info("jwt","exists","JWT_REFRESH_SECRET already exists in the .env file.");else {let i=Random.string(32);s+=`
547
+ JWT_SECRET=${jwtSecret}
548
+ `;
549
+ log.success("jwt", "generated", "JWT_SECRET generated and added to the .env file.");
550
+ } else {
551
+ log.info("jwt", "exists", "JWT_SECRET already exists in the .env file.");
552
+ }
553
+ if (!hasJwtRefreshSecret) {
554
+ const jwtRefreshSecret = Random.string(32);
555
+ secretsToAdd += `
5
556
  # JWT Refresh Secret
6
- JWT_REFRESH_SECRET=${i}
7
- `,log.success("jwt","generated","JWT_REFRESH_SECRET generated and added to the .env file.");}s&&(r+=s,await putFileAsync(t,r));}function Qe(){return command({name:"jwt.generate",description:"Generate JWT Secret key in .env file",action:N})}var O=(o=>(o.MissingAccessToken="EC001",o.InvalidAccessToken="EC002",o.Unauthorized="EC003",o))(O||{});function ct(t$1){let e=t$1?Array.isArray(t$1)?t$1:[t$1]:[];return async(o,n)=>{try{let s=o.authorizationValue;if(!e.length&&!s)return;if(!s)return n.unauthorized({error:t("auth.errors.missingAccessToken"),errorCode:"EC001"});let i=await h.verify(s);o.decodedAccessToken=i;let p=await l.first({token:s});if(!p)return n.unauthorized({error:t("auth.errors.invalidAccessToken"),errorCode:"EC002"});let f=i.userType||p.get("userType");if(e.length&&!e.includes(f))return n.unauthorized({error:t("auth.errors.unauthorized"),errorCode:"EC003"});let E=config.key(`auth.userType.${f}`);if(!E)throw new Error(`User type ${f} is unknown type.`);let R=await E.find(i.id);if(!R)return p.destroy(),n.unauthorized({error:t("auth.errors.invalidAccessToken"),errorCode:"EC002"});p.set("lastAccess",new Date),await p.save({skipEvents:!0}),o.user=R;}catch(s){return log.error("http","auth",s),o.clearCurrentUser(),n.unauthorized({error:t("auth.errors.invalidAccessToken"),errorCode:"EC002"})}}}var q=class extends Model{accessTokenPayload(){return c.buildAccessTokenPayload(this)}async createTokenPair(e){return c.createTokenPair(this,e)}async generateAccessToken(e){return c.generateAccessToken(this,e)}async generateRefreshToken(e){return c.createRefreshToken(this,e)}async removeAccessToken(e){return c.removeAccessToken(this,e)}async revokeAllTokens(){return c.revokeAllTokens(this)}async activeSessions(){return c.getActiveSessions(this)}static async attempt(e){return c.attemptLogin(this,e)}confirmPassword(e){return c.verifyPassword(this.string("password"),e)}};function At(t,e,r){return t?hash(String(t),config.key("auth.password.salt",12)):r.getInitial(e)}export{l as AccessToken,q as Auth,O as AuthErrorCodes,I as NO_EXPIRATION,u as RefreshToken,a as authEvents,ct as authMiddleware,c as authService,At as castPassword,N as generateJWTSecret,h as jwt,b as parseExpirationToMs,ze as registerAuthCleanupCommand,Qe as registerJWTSecretGeneratorCommand,S as toJwtExpiresIn};//# sourceMappingURL=index.js.map
557
+ JWT_REFRESH_SECRET=${jwtRefreshSecret}
558
+ `;
559
+ log.success("jwt", "generated", "JWT_REFRESH_SECRET generated and added to the .env file.");
560
+ } else {
561
+ log.info("jwt", "exists", "JWT_REFRESH_SECRET already exists in the .env file.");
562
+ }
563
+ if (secretsToAdd) {
564
+ contents += secretsToAdd;
565
+ await putFileAsync(envFile, contents);
566
+ }
567
+ }
568
+
569
+ // ../../warlock.js/auth/src/commands/jwt-secret-generator-command.ts
570
+ function registerJWTSecretGeneratorCommand() {
571
+ return command({
572
+ name: "jwt.generate",
573
+ description: "Generate JWT Secret key in .env file",
574
+ action: generateJWTSecret
575
+ });
576
+ }
577
+
578
+ // ../../warlock.js/auth/src/utils/auth-error-codes.ts
579
+ var AuthErrorCodes = /* @__PURE__ */ ((AuthErrorCodes2) => {
580
+ AuthErrorCodes2["MissingAccessToken"] = "EC001";
581
+ AuthErrorCodes2["InvalidAccessToken"] = "EC002";
582
+ AuthErrorCodes2["Unauthorized"] = "EC003";
583
+ return AuthErrorCodes2;
584
+ })(AuthErrorCodes || {});
585
+
586
+ // ../../warlock.js/auth/src/middleware/auth.middleware.ts
587
+ function authMiddleware(allowedUserType) {
588
+ const allowedTypes = !allowedUserType ? [] : Array.isArray(allowedUserType) ? allowedUserType : [allowedUserType];
589
+ const auth = async (request, response) => {
590
+ try {
591
+ const authorizationValue = request.authorizationValue;
592
+ if (!allowedTypes.length && !authorizationValue) return;
593
+ if (!authorizationValue) {
594
+ return response.unauthorized({
595
+ error: t("auth.errors.missingAccessToken"),
596
+ errorCode: "EC001" /* MissingAccessToken */
597
+ });
598
+ }
599
+ const user = await jwt.verify(authorizationValue);
600
+ request.decodedAccessToken = user;
601
+ const accessToken = await AccessToken.first({
602
+ token: authorizationValue
603
+ });
604
+ if (!accessToken) {
605
+ return response.unauthorized({
606
+ error: t("auth.errors.invalidAccessToken"),
607
+ errorCode: "EC002" /* InvalidAccessToken */
608
+ });
609
+ }
610
+ const userType = user.userType || accessToken.get("userType");
611
+ if (allowedTypes.length && !allowedTypes.includes(userType)) {
612
+ return response.unauthorized({
613
+ error: t("auth.errors.unauthorized"),
614
+ errorCode: "EC003" /* Unauthorized */
615
+ });
616
+ }
617
+ const UserModel = config.key(`auth.userType.${userType}`);
618
+ if (!UserModel) {
619
+ throw new Error(`User type ${userType} is unknown type.`);
620
+ }
621
+ const currentUser = await UserModel.find(user.id);
622
+ if (!currentUser) {
623
+ accessToken.destroy();
624
+ return response.unauthorized({
625
+ error: t("auth.errors.invalidAccessToken"),
626
+ errorCode: "EC002" /* InvalidAccessToken */
627
+ });
628
+ }
629
+ accessToken.set("lastAccess", /* @__PURE__ */ new Date());
630
+ await accessToken.save({ skipEvents: true });
631
+ request.user = currentUser;
632
+ } catch (err) {
633
+ log.error("http", "auth", err);
634
+ request.clearCurrentUser();
635
+ return response.unauthorized({
636
+ error: t("auth.errors.invalidAccessToken"),
637
+ errorCode: "EC002" /* InvalidAccessToken */
638
+ });
639
+ }
640
+ };
641
+ return auth;
642
+ }
643
+ var Auth = class extends Model {
644
+ /**
645
+ * Get access token payload
646
+ */
647
+ accessTokenPayload() {
648
+ return authService.buildAccessTokenPayload(this);
649
+ }
650
+ /**
651
+ * Create both access and refresh tokens
652
+ */
653
+ async createTokenPair(deviceInfo) {
654
+ return authService.createTokenPair(this, deviceInfo);
655
+ }
656
+ /**
657
+ * Generate access token
658
+ */
659
+ async generateAccessToken(data) {
660
+ return authService.generateAccessToken(this, data);
661
+ }
662
+ /**
663
+ * Generate refresh token
664
+ */
665
+ async generateRefreshToken(deviceInfo) {
666
+ return authService.createRefreshToken(this, deviceInfo);
667
+ }
668
+ /**
669
+ * Remove current access token
670
+ */
671
+ async removeAccessToken(token) {
672
+ return authService.removeAccessToken(this, token);
673
+ }
674
+ /**
675
+ * Revoke all tokens (logout from all devices)
676
+ */
677
+ async revokeAllTokens() {
678
+ return authService.revokeAllTokens(this);
679
+ }
680
+ /**
681
+ * Get active sessions
682
+ */
683
+ async activeSessions() {
684
+ return authService.getActiveSessions(this);
685
+ }
686
+ /**
687
+ * Attempt to login the user
688
+ */
689
+ static async attempt(data) {
690
+ return authService.attemptLogin(this, data);
691
+ }
692
+ /**
693
+ * Confirm password
694
+ */
695
+ confirmPassword(password) {
696
+ return authService.verifyPassword(this.string("password"), password);
697
+ }
698
+ };
699
+ function castPassword(value, column, model) {
700
+ return value ? hash(String(value), config.key("auth.password.salt", 12)) : model.getInitial(column);
701
+ }
702
+
703
+ export { AccessToken, Auth, AuthErrorCodes, NO_EXPIRATION, RefreshToken, authEvents, authMiddleware, authService, castPassword, generateJWTSecret, jwt, parseExpirationToMs, registerAuthCleanupCommand, registerJWTSecretGeneratorCommand, toJwtExpiresIn };
704
+ //# sourceMappingURL=index.js.map
8
705
  //# sourceMappingURL=index.js.map