wabe 0.6.9 → 0.6.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/README.md +156 -50
  2. package/bucket/b.txt +1 -0
  3. package/dev/index.ts +215 -0
  4. package/dist/authentication/Session.d.ts +4 -1
  5. package/dist/authentication/interface.d.ts +16 -0
  6. package/dist/cron/index.d.ts +0 -1
  7. package/dist/database/DatabaseController.d.ts +41 -13
  8. package/dist/database/interface.d.ts +1 -0
  9. package/dist/email/DevAdapter.d.ts +0 -1
  10. package/dist/email/interface.d.ts +1 -1
  11. package/dist/graphql/resolvers.d.ts +4 -2
  12. package/dist/hooks/index.d.ts +8 -2
  13. package/dist/index.d.ts +0 -1
  14. package/dist/index.js +32144 -32058
  15. package/dist/schema/Schema.d.ts +2 -1
  16. package/dist/server/index.d.ts +4 -2
  17. package/dist/utils/crypto.d.ts +7 -0
  18. package/dist/utils/helper.d.ts +5 -1
  19. package/generated/schema.graphql +22 -14
  20. package/generated/wabe.ts +4 -4
  21. package/package.json +23 -23
  22. package/src/authentication/OTP.test.ts +69 -0
  23. package/src/authentication/OTP.ts +64 -0
  24. package/src/authentication/Session.test.ts +629 -0
  25. package/src/authentication/Session.ts +493 -0
  26. package/src/authentication/defaultAuthentication.ts +209 -0
  27. package/src/authentication/index.ts +3 -0
  28. package/src/authentication/interface.ts +155 -0
  29. package/src/authentication/oauth/GitHub.test.ts +91 -0
  30. package/src/authentication/oauth/GitHub.ts +121 -0
  31. package/src/authentication/oauth/Google.test.ts +91 -0
  32. package/src/authentication/oauth/Google.ts +101 -0
  33. package/src/authentication/oauth/Oauth2Client.test.ts +219 -0
  34. package/src/authentication/oauth/Oauth2Client.ts +135 -0
  35. package/src/authentication/oauth/index.ts +2 -0
  36. package/src/authentication/oauth/utils.test.ts +33 -0
  37. package/src/authentication/oauth/utils.ts +27 -0
  38. package/src/authentication/providers/EmailOTP.test.ts +127 -0
  39. package/src/authentication/providers/EmailOTP.ts +84 -0
  40. package/src/authentication/providers/EmailPassword.test.ts +176 -0
  41. package/src/authentication/providers/EmailPassword.ts +116 -0
  42. package/src/authentication/providers/EmailPasswordSRP.test.ts +208 -0
  43. package/src/authentication/providers/EmailPasswordSRP.ts +179 -0
  44. package/src/authentication/providers/GitHub.ts +24 -0
  45. package/src/authentication/providers/Google.ts +24 -0
  46. package/src/authentication/providers/OAuth.test.ts +185 -0
  47. package/src/authentication/providers/OAuth.ts +106 -0
  48. package/src/authentication/providers/PhonePassword.test.ts +176 -0
  49. package/src/authentication/providers/PhonePassword.ts +115 -0
  50. package/src/authentication/providers/QRCodeOTP.test.ts +77 -0
  51. package/src/authentication/providers/QRCodeOTP.ts +58 -0
  52. package/src/authentication/providers/index.ts +6 -0
  53. package/src/authentication/resolvers/refreshResolver.test.ts +30 -0
  54. package/src/authentication/resolvers/refreshResolver.ts +19 -0
  55. package/src/authentication/resolvers/signInWithResolver.inte.test.ts +59 -0
  56. package/src/authentication/resolvers/signInWithResolver.test.ts +293 -0
  57. package/src/authentication/resolvers/signInWithResolver.ts +92 -0
  58. package/src/authentication/resolvers/signOutResolver.test.ts +38 -0
  59. package/src/authentication/resolvers/signOutResolver.ts +18 -0
  60. package/src/authentication/resolvers/signUpWithResolver.test.ts +180 -0
  61. package/src/authentication/resolvers/signUpWithResolver.ts +65 -0
  62. package/src/authentication/resolvers/verifyChallenge.test.ts +133 -0
  63. package/src/authentication/resolvers/verifyChallenge.ts +62 -0
  64. package/src/authentication/roles.test.ts +49 -0
  65. package/src/authentication/roles.ts +40 -0
  66. package/src/authentication/utils.test.ts +97 -0
  67. package/src/authentication/utils.ts +39 -0
  68. package/src/cache/InMemoryCache.test.ts +62 -0
  69. package/src/cache/InMemoryCache.ts +45 -0
  70. package/src/cron/index.test.ts +17 -0
  71. package/src/cron/index.ts +43 -0
  72. package/src/database/DatabaseController.test.ts +613 -0
  73. package/src/database/DatabaseController.ts +1007 -0
  74. package/src/database/index.test.ts +1372 -0
  75. package/src/database/index.ts +9 -0
  76. package/src/database/interface.ts +302 -0
  77. package/src/email/DevAdapter.ts +7 -0
  78. package/src/email/EmailController.test.ts +29 -0
  79. package/src/email/EmailController.ts +13 -0
  80. package/src/email/index.ts +2 -0
  81. package/src/email/interface.ts +36 -0
  82. package/src/email/templates/sendOtpCode.ts +120 -0
  83. package/src/file/FileController.ts +28 -0
  84. package/src/file/FileDevAdapter.ts +51 -0
  85. package/src/file/hookDeleteFile.ts +25 -0
  86. package/src/file/hookReadFile.ts +66 -0
  87. package/src/file/hookUploadFile.ts +50 -0
  88. package/src/file/index.test.ts +932 -0
  89. package/src/file/index.ts +2 -0
  90. package/src/file/interface.ts +39 -0
  91. package/src/graphql/GraphQLSchema.test.ts +4408 -0
  92. package/src/graphql/GraphQLSchema.ts +880 -0
  93. package/src/graphql/index.ts +2 -0
  94. package/src/graphql/parseGraphqlSchema.ts +85 -0
  95. package/src/graphql/parser.test.ts +203 -0
  96. package/src/graphql/parser.ts +542 -0
  97. package/src/graphql/pointerAndRelationFunction.ts +191 -0
  98. package/src/graphql/resolvers.ts +442 -0
  99. package/src/graphql/tests/aggregation.test.ts +1115 -0
  100. package/src/graphql/tests/e2e.test.ts +590 -0
  101. package/src/graphql/tests/scalars.test.ts +250 -0
  102. package/src/graphql/types.ts +227 -0
  103. package/src/hooks/HookObject.test.ts +122 -0
  104. package/src/hooks/HookObject.ts +165 -0
  105. package/src/hooks/authentication.ts +67 -0
  106. package/src/hooks/createUser.test.ts +77 -0
  107. package/src/hooks/createUser.ts +10 -0
  108. package/src/hooks/defaultFields.test.ts +176 -0
  109. package/src/hooks/defaultFields.ts +32 -0
  110. package/src/hooks/deleteSession.test.ts +181 -0
  111. package/src/hooks/deleteSession.ts +20 -0
  112. package/src/hooks/hashFieldHook.test.ts +152 -0
  113. package/src/hooks/hashFieldHook.ts +89 -0
  114. package/src/hooks/index.test.ts +258 -0
  115. package/src/hooks/index.ts +414 -0
  116. package/src/hooks/permissions.test.ts +412 -0
  117. package/src/hooks/permissions.ts +93 -0
  118. package/src/hooks/protected.test.ts +551 -0
  119. package/src/hooks/protected.ts +60 -0
  120. package/src/hooks/searchableFields.test.ts +147 -0
  121. package/src/hooks/searchableFields.ts +86 -0
  122. package/src/hooks/session.test.ts +134 -0
  123. package/src/hooks/session.ts +76 -0
  124. package/src/hooks/setEmail.test.ts +216 -0
  125. package/src/hooks/setEmail.ts +33 -0
  126. package/src/hooks/setupAcl.test.ts +618 -0
  127. package/src/hooks/setupAcl.ts +25 -0
  128. package/src/index.ts +9 -0
  129. package/src/schema/Schema.test.ts +482 -0
  130. package/src/schema/Schema.ts +757 -0
  131. package/src/schema/defaultResolvers.ts +93 -0
  132. package/src/schema/index.ts +1 -0
  133. package/src/schema/resolvers/meResolver.test.ts +62 -0
  134. package/src/schema/resolvers/meResolver.ts +10 -0
  135. package/src/schema/resolvers/resetPassword.test.ts +341 -0
  136. package/src/schema/resolvers/resetPassword.ts +63 -0
  137. package/src/schema/resolvers/sendEmail.test.ts +118 -0
  138. package/src/schema/resolvers/sendEmail.ts +21 -0
  139. package/src/schema/resolvers/sendOtpCode.test.ts +141 -0
  140. package/src/schema/resolvers/sendOtpCode.ts +52 -0
  141. package/src/security.test.ts +3434 -0
  142. package/src/server/defaultSessionHandler.test.ts +62 -0
  143. package/src/server/defaultSessionHandler.ts +105 -0
  144. package/src/server/generateCodegen.ts +433 -0
  145. package/src/server/index.test.ts +532 -0
  146. package/src/server/index.ts +334 -0
  147. package/src/server/interface.ts +11 -0
  148. package/src/server/routes/authHandler.ts +169 -0
  149. package/src/server/routes/index.ts +39 -0
  150. package/src/utils/crypto.test.ts +41 -0
  151. package/src/utils/crypto.ts +105 -0
  152. package/src/utils/export.ts +11 -0
  153. package/src/utils/helper.ts +204 -0
  154. package/src/utils/index.test.ts +11 -0
  155. package/src/utils/index.ts +189 -0
  156. package/src/utils/preload.ts +8 -0
  157. package/src/utils/testHelper.ts +116 -0
  158. package/tsconfig.json +32 -0
  159. package/bunfig.toml +0 -4
  160. package/dist/ai/index.d.ts +0 -1
  161. package/dist/ai/interface.d.ts +0 -9
  162. /package/dist/server/{defaultHandlers.d.ts → defaultSessionHandler.d.ts} +0 -0
@@ -1,5 +1,6 @@
1
1
  import type { WabeConfig, WabeTypes } from "../server";
2
2
  import type { HookObject } from "../hooks/HookObject";
3
+ export declare const defaultPrivateFields: unknown;
3
4
  export type WabePrimaryTypes = "String" | "Int" | "Float" | "Boolean" | "Email" | "Phone" | "Date" | "File" | "Hash";
4
5
  export type WabeCustomTypes = "Array" | "Object";
5
6
  export type WabeRelationTypes = "Pointer" | "Relation";
@@ -54,7 +55,7 @@ type TypeFieldFile = {
54
55
  type: "File";
55
56
  };
56
57
  type TypeFieldCustomScalars<T extends WabeTypes> = {
57
- type: T["scalars"];
58
+ type: T["scalars"] extends "" ? never : T["scalars"];
58
59
  required?: boolean;
59
60
  defaultValue?: any;
60
61
  };
@@ -11,7 +11,6 @@ import type { Context, CorsOptions, RateLimitOptions } from "wobe";
11
11
  import type { WabeContext } from "./interface";
12
12
  import type { EmailConfig } from "../email";
13
13
  import { EmailController } from "../email/EmailController";
14
- import type { AIConfig } from "../ai";
15
14
  import { FileController } from "../file/FileController";
16
15
  import type { CronConfig } from "../cron";
17
16
  import type { FileConfig } from "../file";
@@ -19,9 +18,13 @@ type SecurityConfig = {
19
18
  corsOptions?: CorsOptions;
20
19
  rateLimit?: RateLimitOptions;
21
20
  hideSensitiveErrorMessage?: boolean;
21
+ disableCSRFProtection?: boolean;
22
+ allowIntrospectionInProduction?: boolean;
23
+ maxGraphqlDepth?: number;
22
24
  };
23
25
  export * from "./interface";
24
26
  export * from "./routes";
27
+ export declare const defaultRoles: unknown;
25
28
  export interface WabeConfig<T extends WabeTypes> {
26
29
  port: number;
27
30
  isProduction: boolean;
@@ -41,7 +44,6 @@ export interface WabeConfig<T extends WabeTypes> {
41
44
  rootKey: string;
42
45
  hooks?: Hook<T, any>[];
43
46
  email?: EmailConfig;
44
- ai?: AIConfig;
45
47
  file?: FileConfig<T>;
46
48
  crons?: CronConfig<T>;
47
49
  }
@@ -9,3 +9,10 @@ export declare const hashArgon2: unknown;
9
9
  */
10
10
  export declare const verifyArgon2: unknown;
11
11
  export declare const isArgon2Hash: (value: string) => boolean;
12
+ /**
13
+ * Deterministic AES-256-GCM encryption for tokens.
14
+ * IV is derived via HMAC-SHA256(key, token) to allow equality checks without storing plaintext.
15
+ * Caller must provide a strong 32-byte key (already derived/hashed).
16
+ */
17
+ export declare const encryptDeterministicToken: (token: string, key: Buffer) => string;
18
+ export declare const decryptDeterministicToken: (encryptedToken: string | undefined, key: Buffer) => string | null;
@@ -7,10 +7,14 @@ export interface DevWabeTypes extends WabeTypes {
7
7
  enums: WabeSchemaEnums;
8
8
  where: WabeSchemaWhereTypes;
9
9
  }
10
+ export declare const selectFieldsWithoutPrivateFields: <T extends Record<string, any>>(select?: T) => T;
10
11
  export declare const firstLetterUpperCase: (str: string) => string;
11
12
  export declare const getGraphqlClient: (port: number) => GraphQLClient;
12
13
  export declare const getAnonymousClient: (port: number) => GraphQLClient;
13
- export declare const getUserClient: (port: number, accessToken: string) => GraphQLClient;
14
+ export declare const getUserClient: (port: number, options: {
15
+ accessToken?: string;
16
+ csrfToken?: string;
17
+ }) => GraphQLClient;
14
18
  export declare const getAdminUserClient: (port: number, wabe: Wabe<DevWabeTypes>, { email, password }: {
15
19
  email: string;
16
20
  password: string;
@@ -425,9 +425,9 @@ input PostRelationInput {
425
425
  type _Session {
426
426
  id: ID!
427
427
  user: User
428
- accessToken: String!
428
+ accessTokenEncrypted: String!
429
429
  accessTokenExpiresAt: Date!
430
- refreshToken: String
430
+ refreshTokenEncrypted: String!
431
431
  refreshTokenExpiresAt: Date!
432
432
  acl: _SessionACLObject
433
433
  createdAt: Date
@@ -454,9 +454,9 @@ type _SessionACLObjectRolesACL {
454
454
 
455
455
  input _SessionInput {
456
456
  user: UserPointerInput
457
- accessToken: String!
457
+ accessTokenEncrypted: String!
458
458
  accessTokenExpiresAt: Date!
459
- refreshToken: String
459
+ refreshTokenEncrypted: String!
460
460
  refreshTokenExpiresAt: Date!
461
461
  acl: _SessionACLObjectInput
462
462
  createdAt: Date
@@ -490,9 +490,9 @@ input _SessionPointerInput {
490
490
 
491
491
  input _SessionCreateFieldsInput {
492
492
  user: UserPointerInput
493
- accessToken: String
493
+ accessTokenEncrypted: String
494
494
  accessTokenExpiresAt: Date
495
- refreshToken: String
495
+ refreshTokenEncrypted: String
496
496
  refreshTokenExpiresAt: Date
497
497
  acl: _SessionACLObjectCreateFieldsInput
498
498
  createdAt: Date
@@ -766,6 +766,7 @@ input StringWhereInput {
766
766
  notEqualTo: String
767
767
  in: [String]
768
768
  notIn: [String]
769
+ exists: Boolean
769
770
  }
770
771
 
771
772
  input IntWhereInput {
@@ -777,6 +778,7 @@ input IntWhereInput {
777
778
  greaterThanOrEqualTo: Int
778
779
  in: [Int]
779
780
  notIn: [Int]
781
+ exists: Boolean
780
782
  }
781
783
 
782
784
  input EmailWhereInput {
@@ -784,6 +786,7 @@ input EmailWhereInput {
784
786
  notEqualTo: Email
785
787
  in: [Email]
786
788
  notIn: [Email]
789
+ exists: Boolean
787
790
  }
788
791
 
789
792
  input UserACLObjectWhereInput {
@@ -806,6 +809,7 @@ input BooleanWhereInput {
806
809
  notEqualTo: Boolean
807
810
  in: [Boolean]
808
811
  notIn: [Boolean]
812
+ exists: Boolean
809
813
  }
810
814
 
811
815
  input UserACLObjectRolesACLWhereInput {
@@ -825,6 +829,7 @@ input DateWhereInput {
825
829
  lessThanOrEqualTo: Date
826
830
  greaterThan: Date
827
831
  greaterThanOrEqualTo: Date
832
+ exists: Boolean
828
833
  }
829
834
 
830
835
  input SearchWhereInput {
@@ -865,6 +870,7 @@ input PhoneWhereInput {
865
870
  notEqualTo: Phone
866
871
  in: [Phone]
867
872
  notIn: [Phone]
873
+ exists: Boolean
868
874
  }
869
875
 
870
876
  input UserAuthenticationEmailPasswordWhereInput {
@@ -892,6 +898,7 @@ input UserAuthenticationGithubWhereInput {
892
898
  input AnyWhereInput {
893
899
  equalTo: Any
894
900
  notEqualTo: Any
901
+ exists: Boolean
895
902
  }
896
903
 
897
904
  """
@@ -937,9 +944,9 @@ input RoleACLObjectRolesACLWhereInput {
937
944
  input _SessionWhereInput {
938
945
  id: IdWhereInput
939
946
  user: UserWhereInput
940
- accessToken: StringWhereInput
947
+ accessTokenEncrypted: StringWhereInput
941
948
  accessTokenExpiresAt: DateWhereInput
942
- refreshToken: StringWhereInput
949
+ refreshTokenEncrypted: StringWhereInput
943
950
  refreshTokenExpiresAt: DateWhereInput
944
951
  acl: _SessionACLObjectWhereInput
945
952
  createdAt: DateWhereInput
@@ -1050,6 +1057,7 @@ input ArrayWhereInput {
1050
1057
  notEqualTo: Any
1051
1058
  contains: Any
1052
1059
  notContains: Any
1060
+ exists: Boolean
1053
1061
  }
1054
1062
 
1055
1063
  input PostACLObjectWhereInput {
@@ -1099,12 +1107,12 @@ enum PostOrder {
1099
1107
  enum _SessionOrder {
1100
1108
  user_ASC
1101
1109
  user_DESC
1102
- accessToken_ASC
1103
- accessToken_DESC
1110
+ accessTokenEncrypted_ASC
1111
+ accessTokenEncrypted_DESC
1104
1112
  accessTokenExpiresAt_ASC
1105
1113
  accessTokenExpiresAt_DESC
1106
- refreshToken_ASC
1107
- refreshToken_DESC
1114
+ refreshTokenEncrypted_ASC
1115
+ refreshTokenEncrypted_DESC
1108
1116
  refreshTokenExpiresAt_ASC
1109
1117
  refreshTokenExpiresAt_DESC
1110
1118
  acl_ASC
@@ -1507,9 +1515,9 @@ input Update_SessionInput {
1507
1515
 
1508
1516
  input _SessionUpdateFieldsInput {
1509
1517
  user: UserPointerInput
1510
- accessToken: String
1518
+ accessTokenEncrypted: String
1511
1519
  accessTokenExpiresAt: Date
1512
- refreshToken: String
1520
+ refreshTokenEncrypted: String
1513
1521
  refreshTokenExpiresAt: Date
1514
1522
  acl: _SessionACLObjectUpdateFieldsInput
1515
1523
  createdAt: Date
package/generated/wabe.ts CHANGED
@@ -116,9 +116,9 @@ export type Post = {
116
116
  export type _Session = {
117
117
  id: string,
118
118
  user: User,
119
- accessToken: string,
119
+ accessTokenEncrypted: string,
120
120
  accessTokenExpiresAt: string,
121
- refreshToken?: string,
121
+ refreshTokenEncrypted: string,
122
122
  refreshTokenExpiresAt: string,
123
123
  acl?: ACLObject,
124
124
  createdAt?: string,
@@ -181,9 +181,9 @@ export type WherePost = {
181
181
  export type Where_Session = {
182
182
  id: string,
183
183
  user: User,
184
- accessToken: string,
184
+ accessTokenEncrypted: string,
185
185
  accessTokenExpiresAt: Date,
186
- refreshToken?: string,
186
+ refreshTokenEncrypted: string,
187
187
  refreshTokenExpiresAt: Date,
188
188
  acl?: ACLObject,
189
189
  createdAt?: Date,
package/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "name": "wabe",
3
- "version": "0.6.9",
4
- "description": "Your backend in minutes not days",
5
- "homepage": "https://wabe.dev",
6
- "author": {
7
- "name": "coratgerl",
8
- "url": "https://github.com/coratgerl"
9
- },
10
- "license": "Apache-2.0",
3
+ "version": "0.6.11",
4
+ "description": "Your backend without vendor lock-in in Typescript",
11
5
  "keywords": [
6
+ "baas",
12
7
  "backend",
13
- "wabe",
14
8
  "graphql",
15
- "baas"
9
+ "wabe"
16
10
  ],
11
+ "homepage": "https://palixir.github.io/wabe/",
12
+ "license": "Apache-2.0",
13
+ "author": {
14
+ "name": "coratgerl",
15
+ "url": "https://github.com/coratgerl"
16
+ },
17
17
  "repository": {
18
18
  "type": "git",
19
19
  "url": "git+https://github.com/palixir/wabe.git"
@@ -22,33 +22,33 @@
22
22
  "scripts": {
23
23
  "build": "bun --filter wabe-build build:package $(pwd)",
24
24
  "check": "tsc --project $(pwd)/tsconfig.json",
25
- "lint": "biome lint . --no-errors-on-unmatched --config-path=../../",
25
+ "lint": "oxlint .",
26
26
  "ci": "bun generate:codegen && bun lint $(pwd) && bun check && bun test src",
27
- "format": "biome format --write . --config-path=../../",
27
+ "format": "oxfmt --write .",
28
28
  "dev": "bun run --watch dev/index.ts",
29
29
  "generate:codegen": "touch generated/wabe.ts && CODEGEN=true bun dev/index.ts"
30
30
  },
31
31
  "dependencies": {
32
- "@graphql-yoga/plugin-disable-introspection": "2.10.9",
33
32
  "croner": "9.0.0",
33
+ "graphql": "16.12.0",
34
34
  "js-srp6a": "1.0.2",
35
35
  "jsonwebtoken": "9.0.2",
36
36
  "libphonenumber-js": "1.11.18",
37
37
  "otplib": "12.0.1",
38
- "p-retry": "6.2.1",
39
- "wobe": "1.1.10",
40
- "wobe-graphql-yoga": "1.2.6"
38
+ "p-retry": "7.1.0",
39
+ "wobe": "1.1.15",
40
+ "wobe-graphql-yoga": "1.2.9"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/jsonwebtoken": "9.0.6",
44
44
  "@types/uuid": "9.0.6",
45
- "graphql-request": "6.1.0",
46
45
  "get-port": "7.1.0",
47
- "uuid": "10.0.0",
48
- "wabe-mongodb-launcher": "workspace:*",
49
- "wabe-pluralize": "workspace:*",
50
- "wabe-build": "workspace:*",
51
- "wabe-mongodb": "workspace:*",
52
- "wabe": "workspace:*"
46
+ "graphql-request": "6.1.0",
47
+ "uuid": "13.0.0",
48
+ "wabe": "0.6.11",
49
+ "wabe-build": "0.5.0",
50
+ "wabe-mongodb": "0.5.3",
51
+ "wabe-mongodb-launcher": "0.5.2",
52
+ "wabe-pluralize": "0.0.1"
53
53
  }
54
54
  }
@@ -0,0 +1,69 @@
1
+ import { describe, it, expect } from 'bun:test'
2
+ import { OTP } from './OTP'
3
+
4
+ describe('OTP', () => {
5
+ it('should generate a valid OTP code', () => {
6
+ const otp = new OTP('rootKey')
7
+
8
+ const otpValue = otp.generate('userId')
9
+
10
+ expect(otpValue.length).toBe(6)
11
+ })
12
+
13
+ it('should verify a valid OTP code', () => {
14
+ const otp = new OTP('rootKey')
15
+
16
+ const otpValue = otp.generate('userId')
17
+
18
+ expect(otpValue.length).toBe(6)
19
+
20
+ expect(otp.verify(otpValue, 'userId')).toBe(true)
21
+ })
22
+
23
+ it('should not verify an invalid OTP code', () => {
24
+ const otp = new OTP('rootKey')
25
+
26
+ const otpValue = otp.generate('userId')
27
+
28
+ expect(otpValue.length).toBe(6)
29
+
30
+ expect(otp.verify('invalidOtp', 'userId')).toBe(false)
31
+
32
+ const otpValue2 = otp.generate('invalidUserId')
33
+
34
+ expect(otpValue2.length).toBe(6)
35
+
36
+ expect(otp.verify(otpValue2, 'userId')).toBe(false)
37
+ })
38
+
39
+ it('should not verify an invalid OTP code (more than 5 minutes)', () => {
40
+ // Directly test the timeout is flaky we only test that the correct value is passed to totp
41
+ const otp = new OTP('rootKey')
42
+
43
+ expect(otp.internalTotp.options.window).toEqual([1, 0])
44
+ })
45
+
46
+ it('should generate a valid keyuri', () => {
47
+ const otp = new OTP('rootKey')
48
+
49
+ const keyuri = otp.generateKeyuri({
50
+ userId: 'userId',
51
+ emailOrUsername: 'email@test.fr',
52
+ applicationName: 'Wabe',
53
+ })
54
+
55
+ expect(keyuri).toBe(
56
+ 'otpauth://totp/Wabe:email%40test.fr?secret=O54OZDANWM2YFHJKJMMVMQSV7DUMUZFT3BWE4Z5NOQCAATGGHKYA&period=30&digits=6&algorithm=SHA1&issuer=Wabe',
57
+ )
58
+ })
59
+
60
+ it('should verify an OTP generated from authenticator', () => {
61
+ const otp = new OTP('rootKey')
62
+
63
+ const code = otp.authenticatorGenerate('userId')
64
+
65
+ const isValid = otp.authenticatorVerify(code, 'userId')
66
+
67
+ expect(isValid).toBe(true)
68
+ })
69
+ })
@@ -0,0 +1,64 @@
1
+ import { totp, authenticator } from 'otplib'
2
+ import type { TOTP } from 'otplib/core'
3
+ import { createHash } from 'node:crypto'
4
+ import { base32Encode } from 'src/utils'
5
+
6
+ const ONE_WINDOW = 1
7
+
8
+ export class OTP {
9
+ private secret: string
10
+ public internalTotp: TOTP
11
+
12
+ constructor(rootKey: string) {
13
+ this.secret = rootKey
14
+ this.internalTotp = totp.clone({
15
+ window: [ONE_WINDOW, 0],
16
+ })
17
+ }
18
+
19
+ deriveSecret(userId: string): string {
20
+ const hash = createHash('sha256').update(`${this.secret}:${userId}`).digest()
21
+
22
+ return base32Encode(hash, 'RFC4648', { padding: false })
23
+ }
24
+
25
+ generate(userId: string): string {
26
+ const secret = this.deriveSecret(userId)
27
+
28
+ return this.internalTotp.generate(secret)
29
+ }
30
+
31
+ verify(otp: string, userId: string): boolean {
32
+ const secret = this.deriveSecret(userId)
33
+
34
+ return this.internalTotp.verify({ secret, token: otp })
35
+ }
36
+
37
+ authenticatorGenerate(userId: string): string {
38
+ const secret = this.deriveSecret(userId)
39
+ return authenticator.generate(secret)
40
+ }
41
+
42
+ authenticatorVerify(otp: string, userId: string): boolean {
43
+ const secret = this.deriveSecret(userId)
44
+
45
+ return authenticator.verify({
46
+ secret,
47
+ token: otp,
48
+ })
49
+ }
50
+
51
+ generateKeyuri({
52
+ userId,
53
+ emailOrUsername,
54
+ applicationName,
55
+ }: {
56
+ userId: string
57
+ emailOrUsername: string
58
+ applicationName: string
59
+ }): string {
60
+ const secret = this.deriveSecret(userId)
61
+
62
+ return authenticator.keyuri(emailOrUsername, applicationName, secret)
63
+ }
64
+ }