@arkstack/auth 0.5.2 → 0.5.5

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/app.d.ts CHANGED
@@ -1,9 +1,19 @@
1
- import type { User } from '.'
2
- import type { Auth } from '.'
1
+ import type { User } from '@arkstack/auth'
2
+ import type { Auth } from '@arkstack/auth'
3
+
4
+ declare module 'node:http' {
5
+ interface IncomingMessage {
6
+ user?: User | undefined;
7
+ auth?: Auth | undefined;
8
+ authUser?: User | undefined;
9
+ authToken?: string | undefined;
10
+ }
11
+ }
3
12
 
4
13
  declare module 'clear-router/types/h3' {
5
14
  interface HttpRequest {
6
15
  user?: User | undefined;
16
+ auth?: CurrentSession | undefined;
7
17
  authUser?: User | undefined;
8
18
  authToken?: string | undefined;
9
19
  }
@@ -12,7 +22,7 @@ declare module 'clear-router/types/h3' {
12
22
  declare module 'clear-router' {
13
23
  interface HttpRequests {
14
24
  user?: User | undefined;
15
- auth?: CurrentSession | undefined;
25
+ auth?: Auth | undefined;
16
26
  authUser?: User | undefined;
17
27
  authToken?: string | undefined;
18
28
  }
package/dist/index.d.ts CHANGED
@@ -1,15 +1,16 @@
1
1
  /// <reference path="./app.d.ts" />
2
2
  import { Exception } from "@arkstack/common";
3
3
  import { Request, RequestSource, Response, ResponseSource } from "@arkstack/http";
4
+ import * as _$otpauth from "otpauth";
4
5
  import { Model } from "@arkstack/database";
5
6
 
6
7
  //#region src/Contracts/PersonalAccessToken.d.ts
7
8
  declare abstract class PersonalAccessToken extends Model {
8
- id: number;
9
+ [key: string]: any;
9
10
  name: string;
10
11
  token: string;
11
12
  abilities: string[];
12
- userId: number;
13
+ userId: never;
13
14
  createdAt: Date;
14
15
  expiresAt: Date | null;
15
16
  lastUsedAt: Date | null;
@@ -26,7 +27,7 @@ declare class CurrentSession {
26
27
  //#endregion
27
28
  //#region src/Contracts/User.d.ts
28
29
  declare abstract class User extends Model {
29
- id: number;
30
+ [key: string]: any;
30
31
  email: string;
31
32
  name: string;
32
33
  password: string;
@@ -120,329 +121,6 @@ declare class SessionDevice {
120
121
  private static detectDeviceType;
121
122
  }
122
123
  //#endregion
123
- //#region ../../node_modules/.pnpm/otpauth@9.5.1/node_modules/otpauth/dist/otpauth.d.ts
124
- /**
125
- * OTP secret key.
126
- */
127
- declare class Secret {
128
- /**
129
- * Converts a Latin-1 string to a Secret object.
130
- * @param {string} str Latin-1 string.
131
- * @returns {Secret} Secret object.
132
- */
133
- static fromLatin1(str: string): Secret;
134
- /**
135
- * Converts an UTF-8 string to a Secret object.
136
- * @param {string} str UTF-8 string.
137
- * @returns {Secret} Secret object.
138
- */
139
- static fromUTF8(str: string): Secret;
140
- /**
141
- * Converts a base32 string to a Secret object.
142
- * @param {string} str Base32 string.
143
- * @returns {Secret} Secret object.
144
- */
145
- static fromBase32(str: string): Secret;
146
- /**
147
- * Converts a hexadecimal string to a Secret object.
148
- * @param {string} str Hexadecimal string.
149
- * @returns {Secret} Secret object.
150
- */
151
- static fromHex(str: string): Secret;
152
- /**
153
- * Creates a secret key object.
154
- * @param {Object} [config] Configuration options.
155
- * @param {ArrayBufferLike} [config.buffer] Secret key buffer.
156
- * @param {number} [config.size=20] Number of random bytes to generate, ignored if 'buffer' is provided.
157
- */
158
- constructor({
159
- buffer,
160
- size
161
- }?: {
162
- buffer?: ArrayBufferLike | undefined;
163
- size?: number | undefined;
164
- });
165
- /**
166
- * Secret key.
167
- * @type {Uint8Array}
168
- * @readonly
169
- */
170
- readonly bytes: Uint8Array;
171
- /**
172
- * Secret key buffer.
173
- * @deprecated For backward compatibility, the "bytes" property should be used instead.
174
- * @type {ArrayBufferLike}
175
- */
176
- get buffer(): ArrayBufferLike;
177
- /**
178
- * Latin-1 string representation of secret key.
179
- * @type {string}
180
- */
181
- get latin1(): string;
182
- /**
183
- * UTF-8 string representation of secret key.
184
- * @type {string}
185
- */
186
- get utf8(): string;
187
- /**
188
- * Base32 string representation of secret key.
189
- * @type {string}
190
- */
191
- get base32(): string;
192
- /**
193
- * Hexadecimal string representation of secret key.
194
- * @type {string}
195
- */
196
- get hex(): string;
197
- }
198
- /**
199
- * HOTP: An HMAC-based One-time Password Algorithm.
200
- * @see [RFC 4226](https://datatracker.ietf.org/doc/html/rfc4226)
201
- */
202
- /**
203
- * TOTP: Time-Based One-Time Password Algorithm.
204
- * @see [RFC 6238](https://datatracker.ietf.org/doc/html/rfc6238)
205
- */
206
- declare class TOTP {
207
- /**
208
- * Default configuration.
209
- * @type {{
210
- * issuer: string,
211
- * label: string,
212
- * issuerInLabel: boolean,
213
- * algorithm: string,
214
- * digits: number,
215
- * period: number
216
- * window: number
217
- * }}
218
- */
219
- static get defaults(): {
220
- issuer: string;
221
- label: string;
222
- issuerInLabel: boolean;
223
- algorithm: string;
224
- digits: number;
225
- period: number;
226
- window: number;
227
- };
228
- /**
229
- * Calculates the counter. i.e. the number of periods since timestamp 0.
230
- * @param {Object} [config] Configuration options.
231
- * @param {number} [config.period=30] Token time-step duration.
232
- * @param {number} [config.timestamp=Date.now] Timestamp value in milliseconds.
233
- * @returns {number} Counter.
234
- */
235
- static counter({
236
- period,
237
- timestamp
238
- }?: {
239
- period?: number | undefined;
240
- timestamp?: number | undefined;
241
- }): number;
242
- /**
243
- * Calculates the remaining time in milliseconds until the next token is generated.
244
- * @param {Object} [config] Configuration options.
245
- * @param {number} [config.period=30] Token time-step duration.
246
- * @param {number} [config.timestamp=Date.now] Timestamp value in milliseconds.
247
- * @returns {number} counter.
248
- */
249
- static remaining({
250
- period,
251
- timestamp
252
- }?: {
253
- period?: number | undefined;
254
- timestamp?: number | undefined;
255
- }): number;
256
- /**
257
- * Generates a TOTP token.
258
- * @param {Object} config Configuration options.
259
- * @param {Secret} config.secret Secret key.
260
- * @param {string} [config.algorithm='SHA1'] HMAC hashing algorithm.
261
- * @param {number} [config.digits=6] Token length.
262
- * @param {number} [config.period=30] Token time-step duration.
263
- * @param {number} [config.timestamp=Date.now] Timestamp value in milliseconds.
264
- * @param {(algorithm: string, key: Uint8Array, message: Uint8Array) => Uint8Array} [config.hmac] Custom HMAC function.
265
- * @returns {string} Token.
266
- */
267
- static generate({
268
- secret,
269
- algorithm,
270
- digits,
271
- period,
272
- timestamp,
273
- hmac
274
- }: {
275
- secret: Secret;
276
- algorithm?: string | undefined;
277
- digits?: number | undefined;
278
- period?: number | undefined;
279
- timestamp?: number | undefined;
280
- hmac?: ((algorithm: string, key: Uint8Array, message: Uint8Array) => Uint8Array) | undefined;
281
- }): string;
282
- /**
283
- * Validates a TOTP token.
284
- * @param {Object} config Configuration options.
285
- * @param {string} config.token Token value.
286
- * @param {Secret} config.secret Secret key.
287
- * @param {string} [config.algorithm='SHA1'] HMAC hashing algorithm.
288
- * @param {number} [config.digits=6] Token length.
289
- * @param {number} [config.period=30] Token time-step duration.
290
- * @param {number} [config.timestamp=Date.now] Timestamp value in milliseconds.
291
- * @param {number} [config.window=1] Window of counter values to test.
292
- * @param {(algorithm: string, key: Uint8Array, message: Uint8Array) => Uint8Array} [config.hmac] Custom HMAC function.
293
- * @returns {number|null} Token delta or null if it is not found in the search window, in which case it should be considered invalid.
294
- */
295
- static validate({
296
- token,
297
- secret,
298
- algorithm,
299
- digits,
300
- period,
301
- timestamp,
302
- window,
303
- hmac
304
- }: {
305
- token: string;
306
- secret: Secret;
307
- algorithm?: string | undefined;
308
- digits?: number | undefined;
309
- period?: number | undefined;
310
- timestamp?: number | undefined;
311
- window?: number | undefined;
312
- hmac?: ((algorithm: string, key: Uint8Array, message: Uint8Array) => Uint8Array) | undefined;
313
- }): number | null;
314
- /**
315
- * Creates a TOTP object.
316
- * @param {Object} [config] Configuration options.
317
- * @param {string} [config.issuer=''] Account provider.
318
- * @param {string} [config.label='OTPAuth'] Account label.
319
- * @param {boolean} [config.issuerInLabel=true] Include issuer prefix in label.
320
- * @param {Secret|string} [config.secret=Secret] Secret key.
321
- * @param {string} [config.algorithm='SHA1'] HMAC hashing algorithm.
322
- * @param {number} [config.digits=6] Token length.
323
- * @param {number} [config.period=30] Token time-step duration.
324
- * @param {(algorithm: string, key: Uint8Array, message: Uint8Array) => Uint8Array} [config.hmac] Custom HMAC function.
325
- */
326
- constructor({
327
- issuer,
328
- label,
329
- issuerInLabel,
330
- secret,
331
- algorithm,
332
- digits,
333
- period,
334
- hmac
335
- }?: {
336
- issuer?: string | undefined;
337
- label?: string | undefined;
338
- issuerInLabel?: boolean | undefined;
339
- secret?: string | Secret | undefined;
340
- algorithm?: string | undefined;
341
- digits?: number | undefined;
342
- period?: number | undefined;
343
- hmac?: ((algorithm: string, key: Uint8Array, message: Uint8Array) => Uint8Array) | undefined;
344
- });
345
- /**
346
- * Account provider.
347
- * @type {string}
348
- */
349
- issuer: string;
350
- /**
351
- * Account label.
352
- * @type {string}
353
- */
354
- label: string;
355
- /**
356
- * Include issuer prefix in label.
357
- * @type {boolean}
358
- */
359
- issuerInLabel: boolean;
360
- /**
361
- * Secret key.
362
- * @type {Secret}
363
- */
364
- secret: Secret;
365
- /**
366
- * HMAC hashing algorithm.
367
- * @type {string}
368
- */
369
- algorithm: string;
370
- /**
371
- * Token length.
372
- * @type {number}
373
- */
374
- digits: number;
375
- /**
376
- * Token time-step duration.
377
- * @type {number}
378
- */
379
- period: number;
380
- /**
381
- * Custom HMAC function.
382
- * @type {((algorithm: string, key: Uint8Array, message: Uint8Array) => Uint8Array)|undefined}
383
- */
384
- hmac: ((algorithm: string, key: Uint8Array, message: Uint8Array) => Uint8Array) | undefined;
385
- /**
386
- * Calculates the counter. i.e. the number of periods since timestamp 0.
387
- * @param {Object} [config] Configuration options.
388
- * @param {number} [config.timestamp=Date.now] Timestamp value in milliseconds.
389
- * @returns {number} Counter.
390
- */
391
- counter({
392
- timestamp
393
- }?: {
394
- timestamp?: number | undefined;
395
- }): number;
396
- /**
397
- * Calculates the remaining time in milliseconds until the next token is generated.
398
- * @param {Object} [config] Configuration options.
399
- * @param {number} [config.timestamp=Date.now] Timestamp value in milliseconds.
400
- * @returns {number} counter.
401
- */
402
- remaining({
403
- timestamp
404
- }?: {
405
- timestamp?: number | undefined;
406
- }): number;
407
- /**
408
- * Generates a TOTP token.
409
- * @param {Object} [config] Configuration options.
410
- * @param {number} [config.timestamp=Date.now] Timestamp value in milliseconds.
411
- * @returns {string} Token.
412
- */
413
- generate({
414
- timestamp
415
- }?: {
416
- timestamp?: number | undefined;
417
- }): string;
418
- /**
419
- * Validates a TOTP token.
420
- * @param {Object} config Configuration options.
421
- * @param {string} config.token Token value.
422
- * @param {number} [config.timestamp=Date.now] Timestamp value in milliseconds.
423
- * @param {number} [config.window=1] Window of counter values to test.
424
- * @returns {number|null} Token delta or null if it is not found in the search window, in which case it should be considered invalid.
425
- */
426
- validate({
427
- token,
428
- timestamp,
429
- window
430
- }: {
431
- token: string;
432
- timestamp?: number | undefined;
433
- window?: number | undefined;
434
- }): number | null;
435
- /**
436
- * Returns a Google Authenticator key URI.
437
- * @returns {string} URI.
438
- */
439
- toString(): string;
440
- }
441
- /**
442
- * HOTP/TOTP object/string conversion.
443
- * @see [Key URI Format](https://github.com/google/google-authenticator/wiki/Key-Uri-Format)
444
- */
445
- //#endregion
446
124
  //#region src/types/TwoFactor.d.ts
447
125
  type TwoFactorMethod = 'authenticator' | 'sms';
448
126
  type SmsCodePurpose = 'setup' | 'login';
@@ -464,13 +142,14 @@ type IssuedSmsCode = {
464
142
  //#endregion
465
143
  //#region src/TwoFactor.d.ts
466
144
  declare class TwoFactor {
145
+ static smsCodeTtlMinutes: number;
467
146
  private static getModel;
468
147
  private static getRecord;
469
148
  private static upsert;
470
149
  static normalizeMethod(method?: string | null): TwoFactorMethod | null;
471
150
  static maskPhone(phone?: string | null): string | null;
472
151
  static getLabel(user: User): string;
473
- static getTotp(user: User, secret: string): TOTP;
152
+ static getTotp(user: User, secret: string): _$otpauth.TOTP;
474
153
  static generateSecret(size?: number): string;
475
154
  static createSetup(user: User, secret?: string): TwoFactorSetup;
476
155
  static verifyCode(user: User, secret: string, code: string): boolean;
@@ -496,7 +175,7 @@ declare class TwoFactor {
496
175
  //#endregion
497
176
  //#region src/Contracts/UserTwoFactor.d.ts
498
177
  declare abstract class UserTwoFactor extends Model {
499
- id: number | string;
178
+ [key: string]: any;
500
179
  userId: User['id'];
501
180
  method: TwoFactorMethod | null;
502
181
  secretCiphertext: string | null;
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ import { Encryption, Exception, Hash, env, getModel } from "@arkstack/common";
2
2
  import { SignJWT, jwtVerify } from "jose";
3
3
  import { Request, Response } from "@arkstack/http";
4
4
  import { UAParser } from "ua-parser-js";
5
+ import { Secret } from "otpauth";
5
6
  import { randomBytes } from "node:crypto";
6
7
  import { Model } from "@arkstack/database";
7
8
  //#region src/Contracts/AuthContract.ts
@@ -577,10 +578,8 @@ var Auth = class Auth extends AuthContract {
577
578
  };
578
579
  //#endregion
579
580
  //#region src/TwoFactor.ts
580
- const secretAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
581
- const appName = () => env("APP_NAME", "Arkstack");
582
- const smsCodeTtlMinutes = () => Number(env("TWO_FACTOR_SMS_TTL_MINUTES", 10)) || 10;
583
- var TwoFactor = class {
581
+ var TwoFactor = class TwoFactor {
582
+ static smsCodeTtlMinutes = Number(env("TWO_FACTOR_SMS_TTL_MINUTES", 10)) || 10;
584
583
  static async getModel() {
585
584
  return await getModel("UserTwoFactor");
586
585
  }
@@ -600,18 +599,40 @@ var TwoFactor = class {
600
599
  if (normalized.length <= 4) return normalized;
601
600
  return `${"*".repeat(Math.max(normalized.length - 4, 2))}${normalized.slice(-4)}`;
602
601
  }
602
+ /**
603
+ * Build the account label used inside the OTP URI.
604
+ *
605
+ * @param user
606
+ * @returns
607
+ */
603
608
  static getLabel(user) {
604
- return user.email || `${appName()}:${user.id}`;
609
+ return user.email || `${env("APP_NAME", "Arkstack")}:${user.id}`;
605
610
  }
611
+ /**
612
+ * Create the per-user TOTP instance for setup and verification.
613
+ *
614
+ * @param user
615
+ * @param secret
616
+ * @returns
617
+ */
606
618
  static getTotp(user, secret) {
607
- return Hash.totp(secret, this.getLabel(user), appName());
619
+ return Hash.totp(secret, this.getLabel(user), env("APP_NAME", "Arkstack"));
608
620
  }
621
+ /**
622
+ * Generate a new shared secret for authenticator-based 2FA.
623
+ *
624
+ * @returns The generated secret in base32 format.
625
+ */
609
626
  static generateSecret(size = 20) {
610
- const bytes = randomBytes(size);
611
- let secret = "";
612
- for (const byte of bytes) secret += secretAlphabet[byte % 32];
613
- return secret;
627
+ return new Secret({ size }).base32;
614
628
  }
629
+ /**
630
+ * Build the setup payload returned to the client.
631
+ *
632
+ * @param user The user for whom the setup is being created.
633
+ * @param secret Optional existing secret to use for the setup.
634
+ * @returns An object containing the secret and the OTPAuth URL.
635
+ */
615
636
  static createSetup(user, secret) {
616
637
  const resolvedSecret = secret ?? this.generateSecret();
617
638
  return {
@@ -619,6 +640,14 @@ var TwoFactor = class {
619
640
  otpauthUrl: this.getTotp(user, resolvedSecret).toString()
620
641
  };
621
642
  }
643
+ /**
644
+ * Verify a 6-digit authenticator code for a user.
645
+ *
646
+ * @param user The user for whom the code is being verified.
647
+ * @param secret The secret used to generate the code.
648
+ * @param code The 6-digit code to verify.
649
+ * @returns True if the code is valid, false otherwise.
650
+ */
622
651
  static verifyCode(user, secret, code) {
623
652
  return this.getTotp(user, secret).validate({
624
653
  token: code,
@@ -632,39 +661,98 @@ var TwoFactor = class {
632
661
  static async setMethod(userId, method) {
633
662
  await this.upsert(userId, { method });
634
663
  }
664
+ /**
665
+ * Read the setup secret stored for a user.
666
+ *
667
+ * @param userId The ID of the user.
668
+ * @returns The stored secret, or null if not found.
669
+ */
635
670
  static async getSecret(userId) {
636
671
  const record = await this.getRecord(userId);
637
672
  return record?.secretCiphertext ? Encryption.decrypt(record.secretCiphertext) : null;
638
673
  }
674
+ /**
675
+ * Store the setup secret for a user.
676
+ *
677
+ * @param userId The ID of the user.
678
+ * @param secret The secret to store.
679
+ */
639
680
  static async setSecret(userId, secret) {
640
681
  await this.upsert(userId, { secretCiphertext: Encryption.encrypt(secret) });
641
682
  }
642
683
  static async clearSecret(userId) {
643
684
  await this.upsert(userId, { secretCiphertext: null });
644
685
  }
686
+ /**
687
+ * Read the timestamp indicating whether 2FA is enabled.
688
+ *
689
+ * @param userId The ID of the user.
690
+ * @returns The timestamp when 2FA was enabled, or null if not enabled.
691
+ */
645
692
  static async getEnabledAt(userId) {
646
693
  return (await this.getRecord(userId))?.enabledAt?.toISOString() ?? null;
647
694
  }
695
+ /**
696
+ * Persist the timestamp marking 2FA as enabled.
697
+ *
698
+ * @param userId The ID of the user.
699
+ * @param enabledAt The timestamp to store.
700
+ */
648
701
  static async setEnabledAt(userId, enabledAt = /* @__PURE__ */ new Date()) {
649
702
  await this.upsert(userId, { enabledAt: typeof enabledAt === "string" ? new Date(enabledAt) : enabledAt });
650
703
  }
704
+ /**
705
+ * Remove all persisted 2FA state for a user.
706
+ *
707
+ * @param userId The ID of the user.
708
+ */
651
709
  static async clear(userId) {
652
710
  await (await this.getModel()).query().where({ userId }).delete();
653
711
  }
712
+ /**
713
+ * Generate one-time recovery codes shown when 2FA is enabled.
714
+ *
715
+ * @returns An array of recovery codes.
716
+ */
654
717
  static generateBackupCodes(count = 8) {
655
718
  return Array.from({ length: count }, () => {
656
719
  return `${randomBytes(3).toString("hex").slice(0, 4).toUpperCase()}-${randomBytes(3).toString("hex").slice(0, 4).toUpperCase()}`;
657
720
  });
658
721
  }
722
+ /**
723
+ * Hash recovery codes before persisting them.
724
+ *
725
+ * @param codes An array of recovery codes to hash.
726
+ * @returns An array of hashed recovery codes.
727
+ */
659
728
  static async hashBackupCodes(codes) {
660
729
  return await Promise.all(codes.map(async (code) => await Hash.make(code)));
661
730
  }
731
+ /**
732
+ * Read stored recovery-code hashes for a user.
733
+ *
734
+ * @param userId The ID of the user.
735
+ * @returns An array of recovery-code hashes.
736
+ */
662
737
  static async readRecoveryCodeHashes(userId) {
663
738
  return (await this.getRecord(userId))?.recoveryCodeHashes ?? [];
664
739
  }
740
+ /**
741
+ * Persist recovery-code hashes on the user's dedicated 2FA record.
742
+ *
743
+ * @param userId
744
+ * @param hashes
745
+ */
665
746
  static async writeRecoveryCodeHashes(userId, hashes) {
666
747
  await this.upsert(userId, { recoveryCodeHashes: hashes });
667
748
  }
749
+ /**
750
+ * Consume a valid recovery code and invalidate it immediately.
751
+ *
752
+ * @param userId The ID of the user.
753
+ * @param recoveryCode The recovery code to consume.
754
+ * @returns True if the recovery code was valid and consumed, false otherwise.
755
+ */
668
756
  static async consumeRecoveryCode(userId, recoveryCode) {
669
757
  const hashes = await this.readRecoveryCodeHashes(userId);
670
758
  for (const [index, hash] of hashes.entries()) if (await Hash.verify(recoveryCode, hash)) {
@@ -673,6 +761,12 @@ var TwoFactor = class {
673
761
  }
674
762
  return false;
675
763
  }
764
+ /**
765
+ * Return the public 2FA status payload for a user.
766
+ *
767
+ * @param userId The ID of the user.
768
+ * @returns An object containing the 2FA status and recovery codes remaining.
769
+ */
676
770
  static async readStatus(userId) {
677
771
  const record = await this.getRecord(userId);
678
772
  const enabledAt = record?.enabledAt?.toISOString() ?? null;
@@ -687,11 +781,17 @@ var TwoFactor = class {
687
781
  static createSmsCode() {
688
782
  return Math.floor(1e5 + Math.random() * 9e5).toString();
689
783
  }
784
+ /**
785
+ * Issue a new SMS code for the given user and send it via SMS for the specified purpose.
786
+ *
787
+ * @param user
788
+ * @param purpose
789
+ */
690
790
  static async issueSmsCode(user, purpose) {
691
791
  if (!user.phone) throw new Error("A phone number is required to issue a two-factor SMS code.");
692
792
  const code = this.createSmsCode();
693
793
  const smsCodeHash = await Hash.make(code);
694
- const expiresAt = new Date(Date.now() + smsCodeTtlMinutes() * 60 * 1e3);
794
+ const expiresAt = new Date(Date.now() + TwoFactor.smsCodeTtlMinutes * 60 * 1e3);
695
795
  await this.upsert(user.id, {
696
796
  smsCodeHash,
697
797
  smsCodeExpiresAt: expiresAt,
@@ -710,6 +810,14 @@ var TwoFactor = class {
710
810
  smsCodePurpose: null
711
811
  });
712
812
  }
813
+ /**
814
+ * Verify a submitted SMS code for a user and purpose, consuming the code if valid.
815
+ *
816
+ * @param userId
817
+ * @param code
818
+ * @param purpose
819
+ * @returns
820
+ */
713
821
  static async verifySmsCode(userId, code, purpose) {
714
822
  const record = await this.getRecord(userId);
715
823
  if (!record?.smsCodeHash || !record.smsCodeExpiresAt || record.smsCodePurpose !== purpose) return false;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["#request","#response","#errors","#user"],"sources":["../src/Contracts/AuthContract.ts","../src/Exceptions/AuthenticationException.ts","../src/CurrentSession.ts","../src/SessionDevice.ts","../src/Auth.ts","../src/TwoFactor.ts","../src/Contracts/PersonalAccessToken.ts","../src/Contracts/User.ts","../src/Contracts/UserTwoFactor.ts"],"sourcesContent":["import { CurrentSession } from '../CurrentSession'\nimport { PersonalAccessToken } from './PersonalAccessToken'\nimport { Request, type RequestSource } from '@arkstack/http'\nimport { User } from './User'\n\n/**\n * The Auth class provides methods for user authentication, including verifying \n * credentials, logging in, logging out, and managing personal access tokens. \n * \n * @author Legacy (3m1n3nc3)\n */\nexport abstract class AuthContract {\n /**\n * Set the current HTTP request instance being processed.\n * \n * @param req The HTTP request instance to be set.\n * @returns The Auth instance itself for method chaining.\n */\n abstract setRequest (req: Request<User> | RequestSource<User>): this\n\n /**\n * Get the current HTTP request instance being processed, which may contain\n * user information and other request-specific data relevant to authentication operations.\n * \n * @returns The current HTTP request instance or undefined if not set.\n */\n abstract getRequest (): Request<User> | undefined\n\n /**\n * Get the currently authenticated user\n * \n * @returns The currently authenticated user or null if not authenticated.\n */\n abstract user (): User | null\n\n /**\n * Verify user credentials\n * \n * @param email The email address of the user.\n * @param password The password of the user.\n * @returns A boolean indicating whether the credentials are valid.\n */\n abstract verify (email: string, password: string): Promise<boolean>\n\n /**\n * Attempt to authenticate a user with the given email and password.\n * \n * @param email \n * @param password \n * @returns \n */\n abstract attempt (email: string, password: string): Promise<User>\n\n /**\n * Login a user and create a personal access token\n * \n * @param email \n * @param password \n * @returns \n */\n abstract login (email: string, password: string): Promise<PersonalAccessToken>\n\n /**\n * Create a temporary token for a user with a specific purpose, such as\n * two-factor authentication.\n * \n * @param user \n * @param purpose \n * @param expiresIn \n * @returns \n */\n abstract createTemporaryToken (user: User, purpose: string, expiresIn?: string): Promise<string>\n\n /**\n * Authorize a temporary token and return the associated user if the token is \n * valid and matches the expected purpose.\n * \n * @param token \n * @param purpose \n * @returns \n */\n abstract authorizeTemporaryToken (token: string, purpose: string): Promise<User>\n\n /**\n * Logout the currently authenticated user and delete all their personal access tokens\n * \n * @param token \n * @returns \n */\n abstract logout (token?: string | PersonalAccessToken): Promise<void>\n\n /**\n * Check if the user is authenticated\n * \n * @returns \n */\n abstract check (): Promise<boolean>\n\n /**\n * Get the current session's personal access token\n * \n * @returns \n */\n abstract currentSession (): CurrentSession\n\n /**\n * Create a personal access token for a user\n * \n * @param user \n * @returns \n */\n abstract create (user: User): Promise<PersonalAccessToken>\n\n /**\n * Authorize a token and return the associated user\n * \n * @param token \n * @returns \n */\n abstract authorizeToken (token: string): Promise<User>\n}\n","import { Request, Response, type RequestSource, type ResponseSource } from '@arkstack/http'\n\nimport { Exception } from '@arkstack/common'\n\nexport class AuthenticationException extends Exception {\n #errors?: Record<string, any>\n #request?: Request\n #response?: Response\n statusCode: number = 401\n name: string\n\n constructor(\n message: string = 'Authentication failed',\n ctx?: {\n req?: Request | RequestSource,\n res?: Response | ResponseSource,\n status?: number,\n errors?: Record<string, any>\n }\n ) {\n super(message)\n this.name = 'AuthenticationException'\n this.statusCode = ctx?.status ?? 401\n if (ctx) {\n this.#request = Request.from(ctx.req)\n this.#response = Response.from(ctx.res)\n this.#errors = ctx.errors\n }\n\n void this.#response\n void this.#request\n }\n\n errors (): Record<string, any> | undefined {\n return this.#errors\n }\n}\n","import { AuthContract } from './Contracts/AuthContract'\nimport { PersonalAccessToken } from './Contracts/PersonalAccessToken'\nimport { getModel } from '@arkstack/common'\n\n/**\n * The CurrentSession class represents the current authentication session and provides \n * methods to manage it, such as destroying the session (logging out) and retrieving \n * the current personal access token. It is used internally by the Auth class to \n * handle session-specific operations.\n * \n * @author Legacy (3m1n3nc3)\n * @since 1.0.0\n * @version 1.0.0\n * @see Auth\n */\nexport class CurrentSession {\n constructor(private auth: AuthContract) { }\n\n /**\n * Destroy the current session's personal access token, effectively \n * logging out the user from this session.\n */\n async destroy () {\n const pat = await this.token()\n\n if (pat) {\n await this.auth.logout(pat)\n }\n }\n\n /**\n * Get the current session's personal access token\n * \n * @returns \n */\n async token (): Promise<PersonalAccessToken | null> {\n if (!this.auth.getRequest()) {\n return null\n }\n\n const token = this.auth.getRequest()!.bearerToken()\n\n if (!token) {\n return null\n }\n\n const Model = await getModel<typeof PersonalAccessToken>('PersonalAccessToken')\n const pat = await Model.query().where({ token }).first()\n\n return pat\n }\n}\n","import { type Request } from '@arkstack/http'\nimport { AuthAgentPayload, SessionDeviceInfo } from './types/Session'\nimport { UAParser } from 'ua-parser-js'\n\nexport class SessionDevice {\n private static readonly uniqueIdentityFields = [\n 'deviceName',\n 'manufacturer',\n 'model',\n 'platform',\n 'os',\n 'osVersion',\n 'browser',\n 'deviceType',\n ] as const\n\n /**\n * Extracts device information from the incoming request to build a SessionDeviceInfo object.\n * \n * @param req The incoming HTTP request object.\n * @returns A SessionDeviceInfo object containing information about the client's device.\n */\n static fromRequest (req?: Request): SessionDeviceInfo {\n const userAgent = this.readUserAgent(req)\n const ua = new UAParser(userAgent ?? undefined).getResult()\n const ca = this.readAuthAgent(req)\n\n return {\n browser: this.readString(ua.browser.name) ?? this.detectBrowser(userAgent),\n os: ca?.os ?? this.readString(ua.os.name) ?? this.detectOs(userAgent),\n osVersion: ca?.osVersion ?? this.readString(ua.os.version),\n deviceType: ca?.deviceType ?? this.normalizeDeviceType(ua.device.type) ?? this.detectDeviceType(userAgent),\n deviceName: ca?.deviceName ?? null,\n manufacturer: ca?.manufacturer ?? this.readString(ua.device.vendor),\n model: ca?.model ?? this.readString(ua.device.model),\n platform: ca?.platform ?? null,\n ipAddress: this.detectIpAddress(req),\n userAgent,\n }\n }\n\n /**\n * Generates a human-readable display name for the device based on available information.\n * \n * @param deviceInfo A record containing device information.\n * @returns A string representing the display name of the device.\n */\n static getDisplayName (deviceInfo?: Record<string, unknown> | null) {\n const deviceName = this.readString(deviceInfo?.deviceName)\n const manufacturer = this.readString(deviceInfo?.manufacturer)\n const model = this.readString(deviceInfo?.model)\n const browser = this.readString(deviceInfo?.browser)\n const os = this.readString(deviceInfo?.os)\n const deviceType = this.readString(deviceInfo?.deviceType)\n\n if (manufacturer && model) {\n return model.startsWith(manufacturer) ? model : `${manufacturer} ${model}`\n }\n\n if (model) {\n return model\n }\n\n if (deviceName) {\n return deviceName\n }\n\n if (browser && os) {\n return `${browser} on ${os}`\n }\n\n if (os && deviceType && deviceType !== 'unknown') {\n return `${os} ${deviceType}`\n }\n\n if (browser) {\n return browser\n }\n\n return 'Unknown device'\n }\n\n /**\n * Builds a stable device key for matching previously issued sessions to the\n * current request device.\n *\n * @param deviceInfo A record containing device information.\n * @returns A normalized device key or null when there is not enough signal.\n */\n static getUniqueKey (deviceInfo?: Record<string, unknown> | null) {\n const parts = this.uniqueIdentityFields\n .map((field) => [field, this.readString(deviceInfo?.[field])?.toLowerCase()] as const)\n .filter(([, value]) => !!value)\n\n if (parts.length < 2) {\n const fallbackUserAgent = this.readString(deviceInfo?.userAgent)?.toLowerCase()\n\n return fallbackUserAgent ?? null\n }\n\n return parts.map(([field, value]) => `${field}:${value}`).join('|')\n }\n\n /**\n * Determines whether two device payloads represent the same device.\n *\n * @param left The first device payload.\n * @param right The second device payload.\n * @returns True when both payloads resolve to the same device key.\n */\n static matches (left?: Record<string, unknown> | null, right?: Record<string, unknown> | null) {\n const leftKey = this.getUniqueKey(left)\n const rightKey = this.getUniqueKey(right)\n\n if (!leftKey || !rightKey) {\n return false\n }\n\n return leftKey === rightKey\n }\n\n /**\n * Safely reads the user agent string from the request headers.\n * \n * @param req \n * @returns \n */\n private static readUserAgent (req?: Request) {\n const userAgent = req?.header('user-agent')\n\n return typeof userAgent === 'string' ? userAgent : null\n }\n\n /**\n * Safely reads a string value, ensuring it's a non-empty string or returns null.\n * \n * @param value \n * @returns \n */\n private static readString (value: unknown) {\n return typeof value === 'string' && value.length > 0 ? value : null\n }\n\n /**\n * Reads a specific device-related header from the request\n * \n * @param req \n * @param headerName \n * @returns \n */\n private static readAuthAgent (req?: Request): AuthAgentPayload | null {\n const value = req?.header('x-auth-agent')\n\n if (typeof value !== 'string' || value.length < 1) {\n return null\n }\n\n try {\n const parsed: AuthAgentPayload = JSON.parse(value)\n\n return {\n deviceName: this.readString(parsed.deviceName) ?? undefined,\n manufacturer: this.readString(parsed.manufacturer) ?? undefined,\n model: this.readString(parsed.model) ?? undefined,\n platform: this.readString(parsed.platform) ?? undefined,\n os: this.readString(parsed.os) ?? undefined,\n osVersion: this.readString(parsed.osVersion) ?? undefined,\n deviceType: this.normalizeDeviceType(parsed.deviceType) ?? undefined,\n }\n } catch {\n return null\n }\n }\n\n private static normalizeDeviceType (value: unknown): SessionDeviceInfo['deviceType'] | null {\n if (value === 'mobile' || value === 'tablet' || value === 'desktop' || value === 'bot' || value === 'unknown') {\n return value\n }\n\n return null\n }\n\n /**\n * Detects the client's IP address from the request, considering common headers set by proxies.\n * \n * @param req \n * @returns \n */\n private static detectIpAddress (req?: Request) {\n const forwarded = req?.header('x-forwarded-for')\n\n if (typeof forwarded === 'string' && forwarded.length > 0) {\n return forwarded.split(',')[0].trim()\n }\n\n return req?.ip ?? null\n }\n\n /**\n * Detects the browser from the user agent string.\n * \n * @param userAgent \n * @returns \n */\n private static detectBrowser (userAgent: string | null) {\n if (!userAgent) return null\n if (/Edg\\//i.test(userAgent)) return 'Edge'\n if (/OPR\\//i.test(userAgent)) return 'Opera'\n if (/SamsungBrowser\\//i.test(userAgent)) return 'Samsung Internet'\n if (/Chrome\\//i.test(userAgent) && !/Edg\\//i.test(userAgent)) return 'Chrome'\n if (/Firefox\\//i.test(userAgent)) return 'Firefox'\n if (/Safari\\//i.test(userAgent) && !/Chrome\\//i.test(userAgent)) return 'Safari'\n if (/PostmanRuntime\\//i.test(userAgent)) return 'Postman'\n if (/okhttp\\//i.test(userAgent)) return 'OkHttp'\n if (/curl\\//i.test(userAgent)) return 'cURL'\n\n return null\n }\n\n /**\n * Detects the operating system from the user agent string.\n * \n * @param userAgent \n * @returns \n */\n private static detectOs (userAgent: string | null) {\n if (!userAgent) return null\n if (/iPhone|iPad|iPod/i.test(userAgent)) return 'iOS'\n if (/Android/i.test(userAgent)) return 'Android'\n if (/Mac OS X|Macintosh/i.test(userAgent)) return 'macOS'\n if (/Windows NT/i.test(userAgent)) return 'Windows'\n if (/Linux/i.test(userAgent)) return 'Linux'\n\n return null\n }\n\n /**\n * Detects the device type from the user agent string.\n * \n * @param userAgent \n * @returns \n */\n private static detectDeviceType (userAgent: string | null): SessionDeviceInfo['deviceType'] {\n if (!userAgent) return 'unknown'\n if (/bot|spider|crawl/i.test(userAgent)) return 'bot'\n if (/iPad|Tablet/i.test(userAgent)) return 'tablet'\n if (/Mobile|iPhone|Android/i.test(userAgent)) return 'mobile'\n\n return 'desktop'\n }\n}\n","import { Hash, env, getModel } from '@arkstack/common'\nimport { JWTPayload, SignJWT, jwtVerify } from 'jose'\n\nimport { AuthContract } from './Contracts/AuthContract'\nimport { AuthenticationException } from './Exceptions/AuthenticationException'\nimport { CurrentSession } from './CurrentSession'\nimport { PersonalAccessToken } from './Contracts/PersonalAccessToken'\nimport { Request, type RequestSource } from '@arkstack/http'\nimport { SessionDevice } from './SessionDevice'\nimport { User } from './Contracts/User'\n\n/**\n * The Auth class provides methods for user authentication, including verifying \n * credentials, logging in, logging out, and managing personal access tokens. \n * \n * @author Legacy (3m1n3nc3)\n */\nexport class Auth extends AuthContract {\n protected static req?: Request<User>\n private configuredSecret?: string\n #user: User | null = null\n\n constructor(secret?: string, req?: Request<User> | RequestSource<User>) {\n super()\n Auth.req = Request.from<User>(req)\n this.configuredSecret = secret\n }\n\n /**\n * Create a new instance of the Auth class with an optional secret for JWT \n * signing and verification.\n * \n * @param secret The secret key used for signing and verifying JWTs.\n * @returns A new instance of the Auth class.\n */\n static make (secret?: string) {\n return new Auth(secret)\n }\n\n /**\n * Set the current HTTP request instance being processed.\n * \n * @param req The HTTP request instance to be set.\n * @returns The Auth class itself for method chaining.\n */\n static setRequest (req: Request<User> | RequestSource<User>) {\n this.req = Request.from<User>(req)\n\n return this\n }\n\n /**\n * Set the current HTTP request instance being processed.\n * \n * @param req The HTTP request instance to be set.\n * @returns The Auth instance itself for method chaining.\n */\n setRequest (req: Request<User> | RequestSource<User>) {\n Auth.req ??= Request.from<User>(req)\n\n return this\n }\n\n /**\n * Get the current HTTP request instance being processed, which may contain\n * user information and other request-specific data relevant to authentication operations.\n * \n * @returns The current HTTP request instance or undefined if not set.\n */\n getRequest (): Request<User> | undefined {\n return Auth.req\n }\n\n /**\n * Get the currently authenticated user\n * \n * @returns The currently authenticated user or null if not authenticated.\n */\n user (): User | null {\n return this.#user\n }\n\n /**\n * Verify user credentials\n * \n * @param email The email address of the user.\n * @param password The password of the user.\n * @returns A boolean indicating whether the credentials are valid.\n */\n async verify (email: string, password: string): Promise<boolean> {\n const user = await (await getModel<typeof User>('User')).query().where({ email }).first()\n\n return !!user && await Hash.verify(password, user.password)\n }\n\n /**\n * Attempt to authenticate a user with the given email and password.\n * \n * @param email \n * @param password \n * @returns \n */\n async attempt (email: string, password: string): Promise<User> {\n const user = await (await getModel<typeof User>('User')).query().where({ email }).first()\n\n if (!user) {\n throw new AuthenticationException('User account not found', { req: Auth.req, status: 422, errors: { email: ['No account found for this email address'] } })\n }\n\n const isValid = await Hash.verify(password, user.password)\n\n if (!isValid) {\n throw new AuthenticationException('Invalid credentials', { req: Auth.req, status: 422, errors: { password: ['Invalid password'] } })\n }\n\n Auth.req?.setUser(user)\n\n this.#user = user\n\n return user\n }\n\n /**\n * Login a user and create a personal access token\n * \n * @param email \n * @param password \n * @returns \n */\n async login (email: string, password: string): Promise<PersonalAccessToken> {\n const user = await this.attempt(email, password)\n\n return await this.create(user)\n }\n\n /**\n * Create a temporary token for a user with a specific purpose, such as\n * two-factor authentication.\n * \n * @param user \n * @param purpose \n * @param expiresIn \n * @returns \n */\n async createTemporaryToken (user: User, purpose: string, expiresIn: string = '10m'): Promise<string> {\n return await this.createJWT({\n sub: user.id.toString(),\n email: user.email,\n purpose,\n }, expiresIn)\n }\n\n /**\n * Authorize a temporary token and return the associated user if the token is \n * valid and matches the expected purpose.\n * \n * @param token \n * @param purpose \n * @returns \n */\n async authorizeTemporaryToken (token: string, purpose: string): Promise<User> {\n const payload = await this.verifyJWT(token)\n\n if (!payload || payload.purpose !== purpose || !payload.sub) {\n throw new AuthenticationException(\n 'Invalid or expired two-factor session',\n { req: Auth.req, status: 401 }\n )\n }\n\n const user = await (await getModel<typeof User>('User')).query().find(payload.sub)\n\n if (!user) {\n throw new AuthenticationException(\n 'User account not found',\n { req: Auth.req, status: 401 }\n )\n }\n\n Auth.req?.setUser(user)\n\n this.#user = user\n\n return user\n }\n\n /**\n * Logout the currently authenticated user and delete all their personal access tokens\n * \n * @param token \n * @returns \n */\n async logout (token?: string | PersonalAccessToken): Promise<void> {\n if (!this.#user && !token) {\n return\n }\n\n if (token) {\n if (typeof token === 'string') {\n const TokenModel = await getModel<typeof PersonalAccessToken>('PersonalAccessToken')\n\n await TokenModel.query().where({ token }).delete()\n } else {\n await token.delete()\n }\n } else {\n const TokenModel = await getModel<typeof PersonalAccessToken>('PersonalAccessToken')\n\n await TokenModel.query().where({ userId: this.#user!.id }).delete()\n }\n\n this.#user = null\n }\n\n /**\n * Check if the user is authenticated\n * \n * @returns \n */\n async check (): Promise<boolean> {\n return !!this.#user\n }\n\n /**\n * Get the current session's personal access token\n * \n * @returns \n */\n currentSession () {\n return new CurrentSession(this)\n }\n\n /**\n * Create a personal access token for a user\n * \n * @param user \n * @returns \n */\n async create (user: User): Promise<PersonalAccessToken> {\n const payload: JWTPayload = {\n sub: user.id.toString(),\n email: user.email,\n }\n\n Auth.req?.setUser(user)\n\n const token = await this.createJWT(payload)\n const deviceInfo = SessionDevice.fromRequest(Auth.req)\n\n const pat = await this.upsertDeviceToken(user, token, deviceInfo)\n\n pat.setLoadedRelation('user', user)\n\n return pat\n }\n\n /**\n * Create or replace the personal access token for the same user and device\n * while keeping a single active session record for that device.\n *\n * @param user The authenticated user.\n * @param token The new bearer token to persist.\n * @param deviceInfo The current request's device information.\n */\n private async upsertDeviceToken (user: User, token: string, deviceInfo: Record<string, unknown> | null) {\n const TokenModel = await getModel<typeof PersonalAccessToken>('PersonalAccessToken')\n const deviceKey = SessionDevice.getUniqueKey(deviceInfo)\n const payload = {\n abilities: [],\n token,\n name: SessionDevice.getDisplayName(deviceInfo),\n userId: user.id,\n lastUsedAt: new Date(),\n } as {\n abilities: string[]\n token: string\n name: string\n userId: User['id']\n deviceInfo?: Record<string, unknown> | null\n lastUsedAt: Date\n }\n\n if (!deviceKey) {\n return await TokenModel.query().create(payload)\n }\n\n payload.deviceInfo = deviceInfo\n\n const existingSessions = (await TokenModel.query().where({ userId: user.id }).get()).all()\n const matchingSessions = existingSessions\n .filter((session) => SessionDevice.matches(session.deviceInfo, deviceInfo))\n .sort((left, right) => {\n const leftTime = (left.lastUsedAt ?? left.createdAt).getTime()\n const rightTime = (right.lastUsedAt ?? right.createdAt).getTime()\n\n return rightTime - leftTime\n })\n\n if (matchingSessions.length < 1) {\n return await TokenModel.query().create(payload)\n }\n\n const [currentSession, ...duplicateSessions] = matchingSessions\n\n if (duplicateSessions.length > 0) {\n await Promise.all(duplicateSessions.map(async (session) => await session.delete()))\n }\n\n await TokenModel.query().where({ id: currentSession.id }).update(payload)\n\n currentSession.token = payload.token\n currentSession.name = payload.name\n currentSession.userId = payload.userId\n currentSession.deviceInfo = payload.deviceInfo\n currentSession.lastUsedAt = payload.lastUsedAt\n\n return currentSession\n }\n\n /**\n * Authorize a token and return the associated user\n * \n * @param token \n * @returns \n */\n async authorizeToken (token: string): Promise<User> {\n const payload = await this.verifyJWT(token)\n\n if (!payload) {\n throw new AuthenticationException(\n 'Invalid or expired session',\n { req: Auth.req, status: 401 }\n )\n }\n\n const TokenModel = await getModel<typeof PersonalAccessToken>('PersonalAccessToken')\n const pat = await TokenModel.query().where({ token }).first()\n\n if (!pat) {\n throw new AuthenticationException(\n 'Invalid or expired access token',\n { req: Auth.req, status: 401 }\n )\n }\n\n const user = await (await getModel<typeof User>('User')).query().find(payload.sub!)\n\n if (!user) {\n throw new AuthenticationException(\n 'User account not found',\n { req: Auth.req, status: 401 }\n )\n }\n\n Auth.req?.setUser(user)\n\n void this.touchSession(pat).catch((error) => {\n if (env('NODE_ENV') === 'development') {\n console.error('Failed to update session activity', error)\n }\n })\n\n this.#user = user\n\n return user\n }\n\n /**\n * Create a JWT token\n * \n * @param payload \n * @returns \n */\n private async createJWT (payload: JWTPayload, expiresIn: string = env('JWT_EXPIRES_IN', '1h')): Promise<string> {\n const jwt = await new SignJWT(payload)\n .setProtectedHeader({ alg: 'HS256' })\n .setIssuedAt()\n .setExpirationTime(expiresIn)\n .sign(new TextEncoder().encode(this.getSecret()))\n\n return jwt\n }\n\n /**\n * Verify a JWT token\n * \n * @param token \n * @returns \n */\n private async verifyJWT (token: string): Promise<JWTPayload | null> {\n try {\n const { payload } = await jwtVerify(token, new TextEncoder().encode(this.getSecret()))\n\n return payload\n } catch {\n return null\n }\n }\n\n private getSecret (): string {\n return this.configuredSecret ?? env('JWT_SECRET', 'default_secret')\n }\n\n /**\n * Update the last used timestamp and device information of a personal \n * access token to keep the session active and reflect the latest device details.\n * \n * @param pat The personal access token to update.\n * @returns A promise that resolves when the update is complete.\n */\n private async touchSession (pat: PersonalAccessToken) {\n const now = new Date()\n const currentDeviceInfo = SessionDevice.fromRequest(Auth.req)\n const shouldUpdateLastUsedAt = !pat.lastUsedAt || (now.getTime() - pat.lastUsedAt.getTime()) > 5 * 60 * 1000\n const hasDeviceInfo = !!pat.deviceInfo\n const currentDisplayName = SessionDevice.getDisplayName(currentDeviceInfo)\n const storedDisplayName = SessionDevice.getDisplayName(pat.deviceInfo)\n const shouldRefreshDeviceInfo = !hasDeviceInfo || storedDisplayName !== currentDisplayName\n\n if (!shouldUpdateLastUsedAt && !shouldRefreshDeviceInfo) {\n return\n }\n\n const payload: {\n lastUsedAt: Date\n deviceInfo?: Record<string, unknown> | null\n name?: string\n } = {\n lastUsedAt: now,\n }\n\n if (shouldRefreshDeviceInfo) {\n payload.deviceInfo = currentDeviceInfo\n payload.name = currentDisplayName\n }\n\n const TokenModel = await getModel<typeof PersonalAccessToken>('PersonalAccessToken')\n\n await TokenModel.query().where({ id: pat.id }).update(payload)\n\n pat.lastUsedAt = now\n\n if (payload.deviceInfo !== undefined) {\n pat.deviceInfo = payload.deviceInfo\n }\n\n if (payload.name !== undefined) {\n pat.name = payload.name\n }\n }\n}\n","import { Encryption, Hash, env, getModel } from '@arkstack/common'\nimport { randomBytes } from 'node:crypto'\n\nimport { User } from './Contracts/User'\nimport { UserTwoFactor } from './Contracts/UserTwoFactor'\nimport type { IssuedSmsCode, SmsCodePurpose, TwoFactorMethod, TwoFactorSetup, TwoFactorStatus } from './types/TwoFactor'\n\ntype TwoFactorUser = User & {\n phone?: string | null\n}\n\nconst secretAlphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'\nconst appName = () => env('APP_NAME', 'Arkstack')\nconst smsCodeTtlMinutes = () => Number(env('TWO_FACTOR_SMS_TTL_MINUTES', 10)) || 10\n\nexport class TwoFactor {\n private static async getModel () {\n return await getModel<typeof UserTwoFactor>('UserTwoFactor')\n }\n\n private static async getRecord (userId: User['id']) {\n const Model = await this.getModel()\n\n return await Model.query().where({ userId }).first()\n }\n\n private static async upsert (\n userId: User['id'],\n attributes: Partial<Pick<UserTwoFactor,\n | 'method' | 'secretCiphertext' | 'smsCodeHash' | 'smsCodeExpiresAt'\n | 'smsCodePurpose' | 'enabledAt' | 'recoveryCodeHashes'\n >>\n ) {\n const Model = await this.getModel()\n\n await Model.query().updateOrInsert({ userId }, attributes)\n }\n\n static normalizeMethod (method?: string | null): TwoFactorMethod | null {\n if (method === 'authenticator' || method === 'sms') {\n return method\n }\n\n return null\n }\n\n static maskPhone (phone?: string | null) {\n if (!phone) {\n return null\n }\n\n const normalized = phone.replace(/\\s+/g, '')\n\n if (normalized.length <= 4) {\n return normalized\n }\n\n return `${'*'.repeat(Math.max(normalized.length - 4, 2))}${normalized.slice(-4)}`\n }\n\n static getLabel (user: User) {\n return user.email || `${appName()}:${user.id}`\n }\n\n static getTotp (user: User, secret: string) {\n return Hash.totp(secret, this.getLabel(user), appName())\n }\n\n static generateSecret (size = 20) {\n const bytes = randomBytes(size)\n let secret = ''\n\n for (const byte of bytes) {\n secret += secretAlphabet[byte % secretAlphabet.length]\n }\n\n return secret\n }\n\n static createSetup (user: User, secret?: string): TwoFactorSetup {\n const resolvedSecret = secret ?? this.generateSecret()\n const totp = this.getTotp(user, resolvedSecret)\n\n return {\n secret: resolvedSecret,\n otpauthUrl: totp.toString(),\n }\n }\n\n static verifyCode (user: User, secret: string, code: string) {\n return this.getTotp(user, secret).validate({ token: code, window: 1 }) !== null\n }\n\n static async getMethod (userId: User['id']) {\n const record = await this.getRecord(userId)\n\n return this.normalizeMethod(record?.method)\n }\n\n static async setMethod (userId: User['id'], method: TwoFactorMethod) {\n await this.upsert(userId, { method })\n }\n\n static async getSecret (userId: User['id']) {\n const record = await this.getRecord(userId)\n\n return record?.secretCiphertext ? Encryption.decrypt(record.secretCiphertext) : null\n }\n\n static async setSecret (userId: User['id'], secret: string) {\n await this.upsert(userId, { secretCiphertext: Encryption.encrypt(secret) })\n }\n\n static async clearSecret (userId: User['id']) {\n await this.upsert(userId, { secretCiphertext: null })\n }\n\n static async getEnabledAt (userId: User['id']) {\n const record = await this.getRecord(userId)\n\n return record?.enabledAt?.toISOString() ?? null\n }\n\n static async setEnabledAt (userId: User['id'], enabledAt: string | Date = new Date()) {\n await this.upsert(userId, {\n enabledAt: typeof enabledAt === 'string' ? new Date(enabledAt) : enabledAt,\n })\n }\n\n static async clear (userId: User['id']) {\n const Model = await this.getModel()\n\n await Model.query().where({ userId }).delete()\n }\n\n static generateBackupCodes (count = 8) {\n return Array.from({ length: count }, () => {\n const left = randomBytes(3).toString('hex').slice(0, 4).toUpperCase()\n const right = randomBytes(3).toString('hex').slice(0, 4).toUpperCase()\n\n return `${left}-${right}`\n })\n }\n\n static async hashBackupCodes (codes: string[]) {\n return await Promise.all(codes.map(async code => await Hash.make(code)))\n }\n\n static async readRecoveryCodeHashes (userId: User['id']) {\n const record = await this.getRecord(userId)\n\n return record?.recoveryCodeHashes ?? []\n }\n\n static async writeRecoveryCodeHashes (userId: User['id'], hashes: string[]) {\n await this.upsert(userId, { recoveryCodeHashes: hashes })\n }\n\n static async consumeRecoveryCode (userId: User['id'], recoveryCode: string) {\n const hashes = await this.readRecoveryCodeHashes(userId)\n\n for (const [index, hash] of hashes.entries()) {\n if (await Hash.verify(recoveryCode, hash)) {\n await this.writeRecoveryCodeHashes(\n userId,\n hashes.filter((_, currentIndex) => currentIndex !== index),\n )\n\n return true\n }\n }\n\n return false\n }\n\n static async readStatus (userId: User['id']): Promise<TwoFactorStatus> {\n const record = await this.getRecord(userId)\n const enabledAt = record?.enabledAt?.toISOString() ?? null\n const recoveryCodes = record?.recoveryCodeHashes ?? []\n\n return {\n enabled: !!enabledAt,\n enabledAt,\n method: this.normalizeMethod(record?.method),\n recoveryCodesRemaining: recoveryCodes.length,\n }\n }\n\n static createSmsCode () {\n return Math.floor(100000 + Math.random() * 900000).toString()\n }\n\n static async issueSmsCode (user: User, purpose: SmsCodePurpose): Promise<IssuedSmsCode> {\n if (!(user as TwoFactorUser).phone) {\n throw new Error('A phone number is required to issue a two-factor SMS code.')\n }\n\n const code = this.createSmsCode()\n const smsCodeHash = await Hash.make(code)\n const expiresAt = new Date(Date.now() + smsCodeTtlMinutes() * 60 * 1000)\n\n await this.upsert(user.id, {\n smsCodeHash,\n smsCodeExpiresAt: expiresAt,\n smsCodePurpose: purpose,\n })\n\n return {\n code,\n expiresAt,\n purpose,\n }\n }\n\n static async clearSmsCode (userId: User['id']) {\n await this.upsert(userId, {\n smsCodeHash: null,\n smsCodeExpiresAt: null,\n smsCodePurpose: null,\n })\n }\n\n static async verifySmsCode (userId: User['id'], code: string, purpose: SmsCodePurpose) {\n const record = await this.getRecord(userId)\n\n if (!record?.smsCodeHash || !record.smsCodeExpiresAt || record.smsCodePurpose !== purpose) {\n return false\n }\n\n if (record.smsCodeExpiresAt.getTime() < Date.now()) {\n await this.clearSmsCode(userId)\n\n return false\n }\n\n const isValid = await Hash.verify(code, record.smsCodeHash)\n\n if (isValid) {\n await this.clearSmsCode(userId)\n }\n\n return isValid\n }\n}\n","import { Model } from '@arkstack/database'\n\nexport abstract class PersonalAccessToken extends Model {\n declare id: number\n declare name: string\n declare token: string\n declare abilities: string[]\n declare userId: number\n declare createdAt: Date\n declare expiresAt: Date | null\n declare lastUsedAt: Date | null\n declare deviceInfo: Record<string, unknown> | null\n}\n","import { Model } from '@arkstack/database'\n\nexport abstract class User extends Model {\n declare id: number\n declare email: string\n declare name: string\n declare password: string\n declare createdAt: Date\n declare updatedAt: Date\n\n protected static table?: string | undefined = 'users'\n}","import { Model } from '@arkstack/database'\n\nimport type { SmsCodePurpose, TwoFactorMethod } from '../types/TwoFactor'\nimport type { User } from './User'\n\nexport abstract class UserTwoFactor extends Model {\n declare id: number | string\n declare userId: User['id']\n declare method: TwoFactorMethod | null\n declare secretCiphertext: string | null\n declare smsCodeHash: string | null\n declare smsCodeExpiresAt: Date | null\n declare smsCodePurpose: SmsCodePurpose | null\n declare enabledAt: Date | null\n declare recoveryCodeHashes: string[] | null\n declare createdAt: Date\n declare updatedAt: Date\n\n protected static override table?: string | undefined = 'user_two_factors'\n\n protected casts = {\n recoveryCodeHashes: 'json',\n } as const\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["#request","#response","#errors","#user"],"sources":["../src/Contracts/AuthContract.ts","../src/Exceptions/AuthenticationException.ts","../src/CurrentSession.ts","../src/SessionDevice.ts","../src/Auth.ts","../src/TwoFactor.ts","../src/Contracts/PersonalAccessToken.ts","../src/Contracts/User.ts","../src/Contracts/UserTwoFactor.ts"],"sourcesContent":["import { CurrentSession } from '../CurrentSession'\nimport { PersonalAccessToken } from './PersonalAccessToken'\nimport { Request, type RequestSource } from '@arkstack/http'\nimport { User } from './User'\n\n/**\n * The Auth class provides methods for user authentication, including verifying \n * credentials, logging in, logging out, and managing personal access tokens. \n * \n * @author Legacy (3m1n3nc3)\n */\nexport abstract class AuthContract {\n /**\n * Set the current HTTP request instance being processed.\n * \n * @param req The HTTP request instance to be set.\n * @returns The Auth instance itself for method chaining.\n */\n abstract setRequest (req: Request<User> | RequestSource<User>): this\n\n /**\n * Get the current HTTP request instance being processed, which may contain\n * user information and other request-specific data relevant to authentication operations.\n * \n * @returns The current HTTP request instance or undefined if not set.\n */\n abstract getRequest (): Request<User> | undefined\n\n /**\n * Get the currently authenticated user\n * \n * @returns The currently authenticated user or null if not authenticated.\n */\n abstract user (): User | null\n\n /**\n * Verify user credentials\n * \n * @param email The email address of the user.\n * @param password The password of the user.\n * @returns A boolean indicating whether the credentials are valid.\n */\n abstract verify (email: string, password: string): Promise<boolean>\n\n /**\n * Attempt to authenticate a user with the given email and password.\n * \n * @param email \n * @param password \n * @returns \n */\n abstract attempt (email: string, password: string): Promise<User>\n\n /**\n * Login a user and create a personal access token\n * \n * @param email \n * @param password \n * @returns \n */\n abstract login (email: string, password: string): Promise<PersonalAccessToken>\n\n /**\n * Create a temporary token for a user with a specific purpose, such as\n * two-factor authentication.\n * \n * @param user \n * @param purpose \n * @param expiresIn \n * @returns \n */\n abstract createTemporaryToken (user: User, purpose: string, expiresIn?: string): Promise<string>\n\n /**\n * Authorize a temporary token and return the associated user if the token is \n * valid and matches the expected purpose.\n * \n * @param token \n * @param purpose \n * @returns \n */\n abstract authorizeTemporaryToken (token: string, purpose: string): Promise<User>\n\n /**\n * Logout the currently authenticated user and delete all their personal access tokens\n * \n * @param token \n * @returns \n */\n abstract logout (token?: string | PersonalAccessToken): Promise<void>\n\n /**\n * Check if the user is authenticated\n * \n * @returns \n */\n abstract check (): Promise<boolean>\n\n /**\n * Get the current session's personal access token\n * \n * @returns \n */\n abstract currentSession (): CurrentSession\n\n /**\n * Create a personal access token for a user\n * \n * @param user \n * @returns \n */\n abstract create (user: User): Promise<PersonalAccessToken>\n\n /**\n * Authorize a token and return the associated user\n * \n * @param token \n * @returns \n */\n abstract authorizeToken (token: string): Promise<User>\n}\n","import { Request, Response, type RequestSource, type ResponseSource } from '@arkstack/http'\n\nimport { Exception } from '@arkstack/common'\n\nexport class AuthenticationException extends Exception {\n #errors?: Record<string, any>\n #request?: Request\n #response?: Response\n statusCode: number = 401\n name: string\n\n constructor(\n message: string = 'Authentication failed',\n ctx?: {\n req?: Request | RequestSource,\n res?: Response | ResponseSource,\n status?: number,\n errors?: Record<string, any>\n }\n ) {\n super(message)\n this.name = 'AuthenticationException'\n this.statusCode = ctx?.status ?? 401\n if (ctx) {\n this.#request = Request.from(ctx.req)\n this.#response = Response.from(ctx.res)\n this.#errors = ctx.errors\n }\n\n void this.#response\n void this.#request\n }\n\n errors (): Record<string, any> | undefined {\n return this.#errors\n }\n}\n","import { AuthContract } from './Contracts/AuthContract'\nimport { PersonalAccessToken } from './Contracts/PersonalAccessToken'\nimport { getModel } from '@arkstack/common'\n\n/**\n * The CurrentSession class represents the current authentication session and provides \n * methods to manage it, such as destroying the session (logging out) and retrieving \n * the current personal access token. It is used internally by the Auth class to \n * handle session-specific operations.\n * \n * @author Legacy (3m1n3nc3)\n * @since 1.0.0\n * @version 1.0.0\n * @see Auth\n */\nexport class CurrentSession {\n constructor(private auth: AuthContract) { }\n\n /**\n * Destroy the current session's personal access token, effectively \n * logging out the user from this session.\n */\n async destroy () {\n const pat = await this.token()\n\n if (pat) {\n await this.auth.logout(pat)\n }\n }\n\n /**\n * Get the current session's personal access token\n * \n * @returns \n */\n async token (): Promise<PersonalAccessToken | null> {\n if (!this.auth.getRequest()) {\n return null\n }\n\n const token = this.auth.getRequest()!.bearerToken()\n\n if (!token) {\n return null\n }\n\n const Model = await getModel<typeof PersonalAccessToken>('PersonalAccessToken')\n const pat = await Model.query().where({ token }).first()\n\n return pat\n }\n}\n","import { type Request } from '@arkstack/http'\nimport { AuthAgentPayload, SessionDeviceInfo } from './types/Session'\nimport { UAParser } from 'ua-parser-js'\n\nexport class SessionDevice {\n private static readonly uniqueIdentityFields = [\n 'deviceName',\n 'manufacturer',\n 'model',\n 'platform',\n 'os',\n 'osVersion',\n 'browser',\n 'deviceType',\n ] as const\n\n /**\n * Extracts device information from the incoming request to build a SessionDeviceInfo object.\n * \n * @param req The incoming HTTP request object.\n * @returns A SessionDeviceInfo object containing information about the client's device.\n */\n static fromRequest (req?: Request): SessionDeviceInfo {\n const userAgent = this.readUserAgent(req)\n const ua = new UAParser(userAgent ?? undefined).getResult()\n const ca = this.readAuthAgent(req)\n\n return {\n browser: this.readString(ua.browser.name) ?? this.detectBrowser(userAgent),\n os: ca?.os ?? this.readString(ua.os.name) ?? this.detectOs(userAgent),\n osVersion: ca?.osVersion ?? this.readString(ua.os.version),\n deviceType: ca?.deviceType ?? this.normalizeDeviceType(ua.device.type) ?? this.detectDeviceType(userAgent),\n deviceName: ca?.deviceName ?? null,\n manufacturer: ca?.manufacturer ?? this.readString(ua.device.vendor),\n model: ca?.model ?? this.readString(ua.device.model),\n platform: ca?.platform ?? null,\n ipAddress: this.detectIpAddress(req),\n userAgent,\n }\n }\n\n /**\n * Generates a human-readable display name for the device based on available information.\n * \n * @param deviceInfo A record containing device information.\n * @returns A string representing the display name of the device.\n */\n static getDisplayName (deviceInfo?: Record<string, unknown> | null) {\n const deviceName = this.readString(deviceInfo?.deviceName)\n const manufacturer = this.readString(deviceInfo?.manufacturer)\n const model = this.readString(deviceInfo?.model)\n const browser = this.readString(deviceInfo?.browser)\n const os = this.readString(deviceInfo?.os)\n const deviceType = this.readString(deviceInfo?.deviceType)\n\n if (manufacturer && model) {\n return model.startsWith(manufacturer) ? model : `${manufacturer} ${model}`\n }\n\n if (model) {\n return model\n }\n\n if (deviceName) {\n return deviceName\n }\n\n if (browser && os) {\n return `${browser} on ${os}`\n }\n\n if (os && deviceType && deviceType !== 'unknown') {\n return `${os} ${deviceType}`\n }\n\n if (browser) {\n return browser\n }\n\n return 'Unknown device'\n }\n\n /**\n * Builds a stable device key for matching previously issued sessions to the\n * current request device.\n *\n * @param deviceInfo A record containing device information.\n * @returns A normalized device key or null when there is not enough signal.\n */\n static getUniqueKey (deviceInfo?: Record<string, unknown> | null) {\n const parts = this.uniqueIdentityFields\n .map((field) => [field, this.readString(deviceInfo?.[field])?.toLowerCase()] as const)\n .filter(([, value]) => !!value)\n\n if (parts.length < 2) {\n const fallbackUserAgent = this.readString(deviceInfo?.userAgent)?.toLowerCase()\n\n return fallbackUserAgent ?? null\n }\n\n return parts.map(([field, value]) => `${field}:${value}`).join('|')\n }\n\n /**\n * Determines whether two device payloads represent the same device.\n *\n * @param left The first device payload.\n * @param right The second device payload.\n * @returns True when both payloads resolve to the same device key.\n */\n static matches (left?: Record<string, unknown> | null, right?: Record<string, unknown> | null) {\n const leftKey = this.getUniqueKey(left)\n const rightKey = this.getUniqueKey(right)\n\n if (!leftKey || !rightKey) {\n return false\n }\n\n return leftKey === rightKey\n }\n\n /**\n * Safely reads the user agent string from the request headers.\n * \n * @param req \n * @returns \n */\n private static readUserAgent (req?: Request) {\n const userAgent = req?.header('user-agent')\n\n return typeof userAgent === 'string' ? userAgent : null\n }\n\n /**\n * Safely reads a string value, ensuring it's a non-empty string or returns null.\n * \n * @param value \n * @returns \n */\n private static readString (value: unknown) {\n return typeof value === 'string' && value.length > 0 ? value : null\n }\n\n /**\n * Reads a specific device-related header from the request\n * \n * @param req \n * @param headerName \n * @returns \n */\n private static readAuthAgent (req?: Request): AuthAgentPayload | null {\n const value = req?.header('x-auth-agent')\n\n if (typeof value !== 'string' || value.length < 1) {\n return null\n }\n\n try {\n const parsed: AuthAgentPayload = JSON.parse(value)\n\n return {\n deviceName: this.readString(parsed.deviceName) ?? undefined,\n manufacturer: this.readString(parsed.manufacturer) ?? undefined,\n model: this.readString(parsed.model) ?? undefined,\n platform: this.readString(parsed.platform) ?? undefined,\n os: this.readString(parsed.os) ?? undefined,\n osVersion: this.readString(parsed.osVersion) ?? undefined,\n deviceType: this.normalizeDeviceType(parsed.deviceType) ?? undefined,\n }\n } catch {\n return null\n }\n }\n\n private static normalizeDeviceType (value: unknown): SessionDeviceInfo['deviceType'] | null {\n if (value === 'mobile' || value === 'tablet' || value === 'desktop' || value === 'bot' || value === 'unknown') {\n return value\n }\n\n return null\n }\n\n /**\n * Detects the client's IP address from the request, considering common headers set by proxies.\n * \n * @param req \n * @returns \n */\n private static detectIpAddress (req?: Request) {\n const forwarded = req?.header('x-forwarded-for')\n\n if (typeof forwarded === 'string' && forwarded.length > 0) {\n return forwarded.split(',')[0].trim()\n }\n\n return req?.ip ?? null\n }\n\n /**\n * Detects the browser from the user agent string.\n * \n * @param userAgent \n * @returns \n */\n private static detectBrowser (userAgent: string | null) {\n if (!userAgent) return null\n if (/Edg\\//i.test(userAgent)) return 'Edge'\n if (/OPR\\//i.test(userAgent)) return 'Opera'\n if (/SamsungBrowser\\//i.test(userAgent)) return 'Samsung Internet'\n if (/Chrome\\//i.test(userAgent) && !/Edg\\//i.test(userAgent)) return 'Chrome'\n if (/Firefox\\//i.test(userAgent)) return 'Firefox'\n if (/Safari\\//i.test(userAgent) && !/Chrome\\//i.test(userAgent)) return 'Safari'\n if (/PostmanRuntime\\//i.test(userAgent)) return 'Postman'\n if (/okhttp\\//i.test(userAgent)) return 'OkHttp'\n if (/curl\\//i.test(userAgent)) return 'cURL'\n\n return null\n }\n\n /**\n * Detects the operating system from the user agent string.\n * \n * @param userAgent \n * @returns \n */\n private static detectOs (userAgent: string | null) {\n if (!userAgent) return null\n if (/iPhone|iPad|iPod/i.test(userAgent)) return 'iOS'\n if (/Android/i.test(userAgent)) return 'Android'\n if (/Mac OS X|Macintosh/i.test(userAgent)) return 'macOS'\n if (/Windows NT/i.test(userAgent)) return 'Windows'\n if (/Linux/i.test(userAgent)) return 'Linux'\n\n return null\n }\n\n /**\n * Detects the device type from the user agent string.\n * \n * @param userAgent \n * @returns \n */\n private static detectDeviceType (userAgent: string | null): SessionDeviceInfo['deviceType'] {\n if (!userAgent) return 'unknown'\n if (/bot|spider|crawl/i.test(userAgent)) return 'bot'\n if (/iPad|Tablet/i.test(userAgent)) return 'tablet'\n if (/Mobile|iPhone|Android/i.test(userAgent)) return 'mobile'\n\n return 'desktop'\n }\n}\n","import { Hash, env, getModel } from '@arkstack/common'\nimport { JWTPayload, SignJWT, jwtVerify } from 'jose'\n\nimport { AuthContract } from './Contracts/AuthContract'\nimport { AuthenticationException } from './Exceptions/AuthenticationException'\nimport { CurrentSession } from './CurrentSession'\nimport { PersonalAccessToken } from './Contracts/PersonalAccessToken'\nimport { Request, type RequestSource } from '@arkstack/http'\nimport { SessionDevice } from './SessionDevice'\nimport { User } from './Contracts/User'\n\n/**\n * The Auth class provides methods for user authentication, including verifying \n * credentials, logging in, logging out, and managing personal access tokens. \n * \n * @author Legacy (3m1n3nc3)\n */\nexport class Auth extends AuthContract {\n protected static req?: Request<User>\n private configuredSecret?: string\n #user: User | null = null\n\n constructor(secret?: string, req?: Request<User> | RequestSource<User>) {\n super()\n Auth.req = Request.from<User>(req)\n this.configuredSecret = secret\n }\n\n /**\n * Create a new instance of the Auth class with an optional secret for JWT \n * signing and verification.\n * \n * @param secret The secret key used for signing and verifying JWTs.\n * @returns A new instance of the Auth class.\n */\n static make (secret?: string) {\n return new Auth(secret)\n }\n\n /**\n * Set the current HTTP request instance being processed.\n * \n * @param req The HTTP request instance to be set.\n * @returns The Auth class itself for method chaining.\n */\n static setRequest (req: Request<User> | RequestSource<User>) {\n this.req = Request.from<User>(req)\n\n return this\n }\n\n /**\n * Set the current HTTP request instance being processed.\n * \n * @param req The HTTP request instance to be set.\n * @returns The Auth instance itself for method chaining.\n */\n setRequest (req: Request<User> | RequestSource<User>) {\n Auth.req ??= Request.from<User>(req)\n\n return this\n }\n\n /**\n * Get the current HTTP request instance being processed, which may contain\n * user information and other request-specific data relevant to authentication operations.\n * \n * @returns The current HTTP request instance or undefined if not set.\n */\n getRequest (): Request<User> | undefined {\n return Auth.req\n }\n\n /**\n * Get the currently authenticated user\n * \n * @returns The currently authenticated user or null if not authenticated.\n */\n user (): User | null {\n return this.#user\n }\n\n /**\n * Verify user credentials\n * \n * @param email The email address of the user.\n * @param password The password of the user.\n * @returns A boolean indicating whether the credentials are valid.\n */\n async verify (email: string, password: string): Promise<boolean> {\n const user = await (await getModel<typeof User>('User')).query().where({ email }).first()\n\n return !!user && await Hash.verify(password, user.password)\n }\n\n /**\n * Attempt to authenticate a user with the given email and password.\n * \n * @param email \n * @param password \n * @returns \n */\n async attempt (email: string, password: string): Promise<User> {\n const user = await (await getModel<typeof User>('User')).query().where({ email }).first()\n\n if (!user) {\n throw new AuthenticationException('User account not found', { req: Auth.req, status: 422, errors: { email: ['No account found for this email address'] } })\n }\n\n const isValid = await Hash.verify(password, user.password)\n\n if (!isValid) {\n throw new AuthenticationException('Invalid credentials', { req: Auth.req, status: 422, errors: { password: ['Invalid password'] } })\n }\n\n Auth.req?.setUser(user)\n\n this.#user = user\n\n return user\n }\n\n /**\n * Login a user and create a personal access token\n * \n * @param email \n * @param password \n * @returns \n */\n async login (email: string, password: string): Promise<PersonalAccessToken> {\n const user = await this.attempt(email, password)\n\n return await this.create(user)\n }\n\n /**\n * Create a temporary token for a user with a specific purpose, such as\n * two-factor authentication.\n * \n * @param user \n * @param purpose \n * @param expiresIn \n * @returns \n */\n async createTemporaryToken (user: User, purpose: string, expiresIn: string = '10m'): Promise<string> {\n return await this.createJWT({\n sub: user.id.toString(),\n email: user.email,\n purpose,\n }, expiresIn)\n }\n\n /**\n * Authorize a temporary token and return the associated user if the token is \n * valid and matches the expected purpose.\n * \n * @param token \n * @param purpose \n * @returns \n */\n async authorizeTemporaryToken (token: string, purpose: string): Promise<User> {\n const payload = await this.verifyJWT(token)\n\n if (!payload || payload.purpose !== purpose || !payload.sub) {\n throw new AuthenticationException(\n 'Invalid or expired two-factor session',\n { req: Auth.req, status: 401 }\n )\n }\n\n const user = await (await getModel<typeof User>('User')).query().find(payload.sub)\n\n if (!user) {\n throw new AuthenticationException(\n 'User account not found',\n { req: Auth.req, status: 401 }\n )\n }\n\n Auth.req?.setUser(user)\n\n this.#user = user\n\n return user\n }\n\n /**\n * Logout the currently authenticated user and delete all their personal access tokens\n * \n * @param token \n * @returns \n */\n async logout (token?: string | PersonalAccessToken): Promise<void> {\n if (!this.#user && !token) {\n return\n }\n\n if (token) {\n if (typeof token === 'string') {\n const TokenModel = await getModel<typeof PersonalAccessToken>('PersonalAccessToken')\n\n await TokenModel.query().where({ token }).delete()\n } else {\n await token.delete()\n }\n } else {\n const TokenModel = await getModel<typeof PersonalAccessToken>('PersonalAccessToken')\n\n await TokenModel.query().where({ userId: this.#user!.id }).delete()\n }\n\n this.#user = null\n }\n\n /**\n * Check if the user is authenticated\n * \n * @returns \n */\n async check (): Promise<boolean> {\n return !!this.#user\n }\n\n /**\n * Get the current session's personal access token\n * \n * @returns \n */\n currentSession () {\n return new CurrentSession(this)\n }\n\n /**\n * Create a personal access token for a user\n * \n * @param user \n * @returns \n */\n async create (user: User): Promise<PersonalAccessToken> {\n const payload: JWTPayload = {\n sub: user.id.toString(),\n email: user.email,\n }\n\n Auth.req?.setUser(user)\n\n const token = await this.createJWT(payload)\n const deviceInfo = SessionDevice.fromRequest(Auth.req)\n\n const pat = await this.upsertDeviceToken(user, token, deviceInfo)\n\n pat.setLoadedRelation('user', user)\n\n return pat\n }\n\n /**\n * Create or replace the personal access token for the same user and device\n * while keeping a single active session record for that device.\n *\n * @param user The authenticated user.\n * @param token The new bearer token to persist.\n * @param deviceInfo The current request's device information.\n */\n private async upsertDeviceToken (user: User, token: string, deviceInfo: Record<string, unknown> | null) {\n const TokenModel = await getModel<typeof PersonalAccessToken>('PersonalAccessToken')\n const deviceKey = SessionDevice.getUniqueKey(deviceInfo)\n const payload = {\n abilities: [],\n token,\n name: SessionDevice.getDisplayName(deviceInfo),\n userId: user.id,\n lastUsedAt: new Date(),\n } as {\n abilities: string[]\n token: string\n name: string\n userId: User['id']\n deviceInfo?: Record<string, unknown> | null\n lastUsedAt: Date\n }\n\n if (!deviceKey) {\n return await TokenModel.query().create(payload)\n }\n\n payload.deviceInfo = deviceInfo\n\n const existingSessions = (await TokenModel.query().where({ userId: user.id }).get()).all()\n const matchingSessions = existingSessions\n .filter((session) => SessionDevice.matches(session.deviceInfo, deviceInfo))\n .sort((left, right) => {\n const leftTime = (left.lastUsedAt ?? left.createdAt).getTime()\n const rightTime = (right.lastUsedAt ?? right.createdAt).getTime()\n\n return rightTime - leftTime\n })\n\n if (matchingSessions.length < 1) {\n return await TokenModel.query().create(payload)\n }\n\n const [currentSession, ...duplicateSessions] = matchingSessions\n\n if (duplicateSessions.length > 0) {\n await Promise.all(duplicateSessions.map(async (session) => await session.delete()))\n }\n\n await TokenModel.query().where({ id: currentSession.id }).update(payload)\n\n currentSession.token = payload.token\n currentSession.name = payload.name\n currentSession.userId = payload.userId\n currentSession.deviceInfo = payload.deviceInfo\n currentSession.lastUsedAt = payload.lastUsedAt\n\n return currentSession\n }\n\n /**\n * Authorize a token and return the associated user\n * \n * @param token \n * @returns \n */\n async authorizeToken (token: string): Promise<User> {\n const payload = await this.verifyJWT(token)\n\n if (!payload) {\n throw new AuthenticationException(\n 'Invalid or expired session',\n { req: Auth.req, status: 401 }\n )\n }\n\n const TokenModel = await getModel<typeof PersonalAccessToken>('PersonalAccessToken')\n const pat = await TokenModel.query().where({ token }).first()\n\n if (!pat) {\n throw new AuthenticationException(\n 'Invalid or expired access token',\n { req: Auth.req, status: 401 }\n )\n }\n\n const user = await (await getModel<typeof User>('User')).query().find(payload.sub!)\n\n if (!user) {\n throw new AuthenticationException(\n 'User account not found',\n { req: Auth.req, status: 401 }\n )\n }\n\n Auth.req?.setUser(user)\n\n void this.touchSession(pat).catch((error) => {\n if (env('NODE_ENV') === 'development') {\n console.error('Failed to update session activity', error)\n }\n })\n\n this.#user = user\n\n return user\n }\n\n /**\n * Create a JWT token\n * \n * @param payload \n * @returns \n */\n private async createJWT (payload: JWTPayload, expiresIn: string = env('JWT_EXPIRES_IN', '1h')): Promise<string> {\n const jwt = await new SignJWT(payload)\n .setProtectedHeader({ alg: 'HS256' })\n .setIssuedAt()\n .setExpirationTime(expiresIn)\n .sign(new TextEncoder().encode(this.getSecret()))\n\n return jwt\n }\n\n /**\n * Verify a JWT token\n * \n * @param token \n * @returns \n */\n private async verifyJWT (token: string): Promise<JWTPayload | null> {\n try {\n const { payload } = await jwtVerify(token, new TextEncoder().encode(this.getSecret()))\n\n return payload\n } catch {\n return null\n }\n }\n\n private getSecret (): string {\n return this.configuredSecret ?? env('JWT_SECRET', 'default_secret')\n }\n\n /**\n * Update the last used timestamp and device information of a personal \n * access token to keep the session active and reflect the latest device details.\n * \n * @param pat The personal access token to update.\n * @returns A promise that resolves when the update is complete.\n */\n private async touchSession (pat: PersonalAccessToken) {\n const now = new Date()\n const currentDeviceInfo = SessionDevice.fromRequest(Auth.req)\n const shouldUpdateLastUsedAt = !pat.lastUsedAt || (now.getTime() - pat.lastUsedAt.getTime()) > 5 * 60 * 1000\n const hasDeviceInfo = !!pat.deviceInfo\n const currentDisplayName = SessionDevice.getDisplayName(currentDeviceInfo)\n const storedDisplayName = SessionDevice.getDisplayName(pat.deviceInfo)\n const shouldRefreshDeviceInfo = !hasDeviceInfo || storedDisplayName !== currentDisplayName\n\n if (!shouldUpdateLastUsedAt && !shouldRefreshDeviceInfo) {\n return\n }\n\n const payload: {\n lastUsedAt: Date\n deviceInfo?: Record<string, unknown> | null\n name?: string\n } = {\n lastUsedAt: now,\n }\n\n if (shouldRefreshDeviceInfo) {\n payload.deviceInfo = currentDeviceInfo\n payload.name = currentDisplayName\n }\n\n const TokenModel = await getModel<typeof PersonalAccessToken>('PersonalAccessToken')\n\n await TokenModel.query().where({ id: pat.id }).update(payload)\n\n pat.lastUsedAt = now\n\n if (payload.deviceInfo !== undefined) {\n pat.deviceInfo = payload.deviceInfo\n }\n\n if (payload.name !== undefined) {\n pat.name = payload.name\n }\n }\n}\n","import { Encryption, Hash, env, getModel } from '@arkstack/common'\nimport type { IssuedSmsCode, SmsCodePurpose, TwoFactorMethod, TwoFactorSetup, TwoFactorStatus } from './types/TwoFactor'\n\nimport { Secret } from 'otpauth'\nimport { User } from './Contracts/User'\nimport { UserTwoFactor } from './Contracts/UserTwoFactor'\nimport { randomBytes } from 'node:crypto'\n\ntype TwoFactorUser = User & {\n phone?: string | null\n}\n\n\nexport class TwoFactor {\n static smsCodeTtlMinutes: number = Number(env('TWO_FACTOR_SMS_TTL_MINUTES', 10)) || 10\n\n private static async getModel () {\n return await getModel<typeof UserTwoFactor>('UserTwoFactor')\n }\n\n private static async getRecord (userId: User['id']) {\n const Model = await this.getModel()\n\n return await Model.query().where({ userId }).first()\n }\n\n private static async upsert (\n userId: User['id'],\n attributes: Partial<Pick<UserTwoFactor,\n | 'method' | 'secretCiphertext' | 'smsCodeHash' | 'smsCodeExpiresAt'\n | 'smsCodePurpose' | 'enabledAt' | 'recoveryCodeHashes'\n >>\n ) {\n const Model = await this.getModel()\n\n await Model.query().updateOrInsert({ userId }, attributes)\n }\n\n static normalizeMethod (method?: string | null): TwoFactorMethod | null {\n if (method === 'authenticator' || method === 'sms') {\n return method\n }\n\n return null\n }\n\n static maskPhone (phone?: string | null) {\n if (!phone) {\n return null\n }\n\n const normalized = phone.replace(/\\s+/g, '')\n\n if (normalized.length <= 4) {\n return normalized\n }\n\n return `${'*'.repeat(Math.max(normalized.length - 4, 2))}${normalized.slice(-4)}`\n }\n\n /** \n * Build the account label used inside the OTP URI. \n * \n * @param user \n * @returns \n */\n static getLabel (user: User) {\n return user.email || `${env('APP_NAME', 'Arkstack')}:${user.id}`\n }\n\n /** \n * Create the per-user TOTP instance for setup and verification. \n * \n * @param user \n * @param secret \n * @returns \n */\n static getTotp (user: User, secret: string) {\n return Hash.totp(secret, this.getLabel(user), env('APP_NAME', 'Arkstack'))\n }\n\n /** \n * Generate a new shared secret for authenticator-based 2FA. \n * \n * @returns The generated secret in base32 format.\n */\n static generateSecret (size = 20) {\n return new Secret({ size }).base32\n }\n\n /** \n * Build the setup payload returned to the client.\n * \n * @param user The user for whom the setup is being created.\n * @param secret Optional existing secret to use for the setup.\n * @returns An object containing the secret and the OTPAuth URL.\n */\n static createSetup (user: User, secret?: string): TwoFactorSetup {\n const resolvedSecret = secret ?? this.generateSecret()\n const totp = this.getTotp(user, resolvedSecret)\n\n return {\n secret: resolvedSecret,\n otpauthUrl: totp.toString(),\n }\n }\n\n /** \n * Verify a 6-digit authenticator code for a user.\n * \n * @param user The user for whom the code is being verified.\n * @param secret The secret used to generate the code.\n * @param code The 6-digit code to verify.\n * @returns True if the code is valid, false otherwise.\n */\n static verifyCode (user: User, secret: string, code: string) {\n return this.getTotp(user, secret).validate({ token: code, window: 1 }) !== null\n }\n\n static async getMethod (userId: User['id']) {\n const record = await this.getRecord(userId)\n\n return this.normalizeMethod(record?.method)\n }\n\n static async setMethod (userId: User['id'], method: TwoFactorMethod) {\n await this.upsert(userId, { method })\n }\n\n /** \n * Read the setup secret stored for a user.\n * \n * @param userId The ID of the user.\n * @returns The stored secret, or null if not found.\n */\n static async getSecret (userId: User['id']) {\n const record = await this.getRecord(userId)\n\n return record?.secretCiphertext ? Encryption.decrypt(record.secretCiphertext) : null\n }\n\n /** \n * Store the setup secret for a user.\n * \n * @param userId The ID of the user.\n * @param secret The secret to store.\n */\n static async setSecret (userId: User['id'], secret: string) {\n await this.upsert(userId, { secretCiphertext: Encryption.encrypt(secret) })\n }\n\n static async clearSecret (userId: User['id']) {\n await this.upsert(userId, { secretCiphertext: null })\n }\n\n /** \n * Read the timestamp indicating whether 2FA is enabled.\n * \n * @param userId The ID of the user.\n * @returns The timestamp when 2FA was enabled, or null if not enabled.\n */\n static async getEnabledAt (userId: User['id']) {\n const record = await this.getRecord(userId)\n\n return record?.enabledAt?.toISOString() ?? null\n }\n\n /** \n * Persist the timestamp marking 2FA as enabled.\n * \n * @param userId The ID of the user.\n * @param enabledAt The timestamp to store.\n */\n static async setEnabledAt (userId: User['id'], enabledAt: string | Date = new Date()) {\n await this.upsert(userId, {\n enabledAt: typeof enabledAt === 'string' ? new Date(enabledAt) : enabledAt,\n })\n }\n\n /** \n * Remove all persisted 2FA state for a user.\n * \n * @param userId The ID of the user.\n */\n static async clear (userId: User['id']) {\n const Model = await this.getModel()\n\n await Model.query().where({ userId }).delete()\n }\n\n /** \n * Generate one-time recovery codes shown when 2FA is enabled.\n * \n * @returns An array of recovery codes.\n */\n static generateBackupCodes (count = 8) {\n return Array.from({ length: count }, () => {\n const left = randomBytes(3).toString('hex').slice(0, 4).toUpperCase()\n const right = randomBytes(3).toString('hex').slice(0, 4).toUpperCase()\n\n return `${left}-${right}`\n })\n\n // return Array.from({ length: 8 }, () => {\n // const left = Math.random().toString(36).slice(2, 6).toUpperCase()\n // const right = Math.random().toString(36).slice(2, 6).toUpperCase()\n\n // return `${left}-${right}`\n // })\n }\n\n /** \n * Hash recovery codes before persisting them.\n * \n * @param codes An array of recovery codes to hash.\n * @returns An array of hashed recovery codes.\n */\n static async hashBackupCodes (codes: string[]) {\n return await Promise.all(codes.map(async code => await Hash.make(code)))\n }\n\n /** \n * Read stored recovery-code hashes for a user.\n * \n * @param userId The ID of the user.\n * @returns An array of recovery-code hashes.\n */\n static async readRecoveryCodeHashes (userId: User['id']) {\n const record = await this.getRecord(userId)\n\n return record?.recoveryCodeHashes ?? []\n }\n\n /** \n * Persist recovery-code hashes on the user's dedicated 2FA record.\n * \n * @param userId \n * @param hashes \n */\n static async writeRecoveryCodeHashes (userId: User['id'], hashes: string[]) {\n await this.upsert(userId, { recoveryCodeHashes: hashes })\n }\n\n /** \n * Consume a valid recovery code and invalidate it immediately.\n * \n * @param userId The ID of the user.\n * @param recoveryCode The recovery code to consume.\n * @returns True if the recovery code was valid and consumed, false otherwise.\n */\n static async consumeRecoveryCode (userId: User['id'], recoveryCode: string) {\n const hashes = await this.readRecoveryCodeHashes(userId)\n\n for (const [index, hash] of hashes.entries()) {\n if (await Hash.verify(recoveryCode, hash)) {\n await this.writeRecoveryCodeHashes(\n userId,\n hashes.filter((_, currentIndex) => currentIndex !== index),\n )\n\n return true\n }\n }\n\n return false\n }\n\n /** \n * Return the public 2FA status payload for a user.\n * \n * @param userId The ID of the user.\n * @returns An object containing the 2FA status and recovery codes remaining.\n */\n static async readStatus (userId: User['id']): Promise<TwoFactorStatus> {\n const record = await this.getRecord(userId)\n const enabledAt = record?.enabledAt?.toISOString() ?? null\n const recoveryCodes = record?.recoveryCodeHashes ?? []\n\n return {\n enabled: !!enabledAt,\n enabledAt,\n method: this.normalizeMethod(record?.method),\n recoveryCodesRemaining: recoveryCodes.length,\n }\n }\n\n static createSmsCode () {\n return Math.floor(100000 + Math.random() * 900000).toString()\n }\n\n /**\n * Issue a new SMS code for the given user and send it via SMS for the specified purpose.\n * \n * @param user \n * @param purpose \n */\n static async issueSmsCode (user: User, purpose: SmsCodePurpose): Promise<IssuedSmsCode> {\n if (!(user as TwoFactorUser).phone) {\n throw new Error('A phone number is required to issue a two-factor SMS code.')\n }\n\n const code = this.createSmsCode()\n const smsCodeHash = await Hash.make(code)\n const expiresAt = new Date(Date.now() + TwoFactor.smsCodeTtlMinutes * 60 * 1000)\n\n await this.upsert(user.id, {\n smsCodeHash,\n smsCodeExpiresAt: expiresAt,\n smsCodePurpose: purpose,\n })\n\n return {\n code,\n expiresAt,\n purpose,\n }\n }\n\n static async clearSmsCode (userId: User['id']) {\n await this.upsert(userId, {\n smsCodeHash: null,\n smsCodeExpiresAt: null,\n smsCodePurpose: null,\n })\n }\n\n /**\n * Verify a submitted SMS code for a user and purpose, consuming the code if valid.\n * \n * @param userId \n * @param code \n * @param purpose \n * @returns \n */\n static async verifySmsCode (userId: User['id'], code: string, purpose: SmsCodePurpose) {\n const record = await this.getRecord(userId)\n\n if (!record?.smsCodeHash || !record.smsCodeExpiresAt || record.smsCodePurpose !== purpose) {\n return false\n }\n\n if (record.smsCodeExpiresAt.getTime() < Date.now()) {\n await this.clearSmsCode(userId)\n\n return false\n }\n\n const isValid = await Hash.verify(code, record.smsCodeHash)\n\n if (isValid) {\n await this.clearSmsCode(userId)\n }\n\n return isValid\n }\n}\n","import { Model } from '@arkstack/database'\n\nexport abstract class PersonalAccessToken extends Model {\n [key: string]: any\n declare name: string\n declare token: string\n declare abilities: string[]\n declare userId: never\n declare createdAt: Date\n declare expiresAt: Date | null\n declare lastUsedAt: Date | null\n declare deviceInfo: Record<string, unknown> | null\n}\n","import { Model } from '@arkstack/database'\n\nexport abstract class User extends Model {\n [key: string]: any\n declare email: string\n declare name: string\n declare password: string\n declare createdAt: Date\n declare updatedAt: Date\n\n protected static table?: string | undefined = 'users'\n}","import { Model } from '@arkstack/database'\n\nimport type { SmsCodePurpose, TwoFactorMethod } from '../types/TwoFactor'\nimport type { User } from './User'\n\nexport abstract class UserTwoFactor extends Model {\n [key: string]: any\n declare userId: User['id']\n declare method: TwoFactorMethod | null\n declare secretCiphertext: string | null\n declare smsCodeHash: string | null\n declare smsCodeExpiresAt: Date | null\n declare smsCodePurpose: SmsCodePurpose | null\n declare enabledAt: Date | null\n declare recoveryCodeHashes: string[] | null\n declare createdAt: Date\n declare updatedAt: Date\n\n protected static override table?: string | undefined = 'user_two_factors'\n\n protected casts = {\n recoveryCodeHashes: 'json',\n } as const\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arkstack/auth",
3
- "version": "0.5.2",
3
+ "version": "0.5.5",
4
4
  "type": "module",
5
5
  "description": "Authentication module for Arkstack, providing core authentication and identity features.",
6
6
  "homepage": "https://arkstack.toneflix.net/guide/auth",
@@ -36,17 +36,15 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "jose": "^6.2.3",
39
+ "otpauth": "^9.5.1",
39
40
  "ua-parser-js": "^2.0.9",
40
- "@arkstack/common": "^0.5.2",
41
- "@arkstack/http": "^0.5.2"
41
+ "@arkstack/common": "^0.5.5",
42
+ "@arkstack/http": "^0.5.5"
42
43
  },
43
44
  "peerDependencies": {
44
45
  "@h3ravel/support": "^0.15.11",
45
- "@arkstack/database": "^0.5.2",
46
- "@arkstack/notifications": "^0.5.2"
47
- },
48
- "inlinedDependencies": {
49
- "otpauth": "9.5.1"
46
+ "@arkstack/database": "^0.5.5",
47
+ "@arkstack/notifications": "^0.5.5"
50
48
  },
51
49
  "scripts": {
52
50
  "build": "tsdown --config-loader unrun",