wabe 0.6.8 → 0.6.10

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 (165) hide show
  1. package/README.md +140 -32
  2. package/dev/index.ts +215 -0
  3. package/dist/authentication/OTP.d.ts +3 -0
  4. package/dist/authentication/Session.d.ts +5 -2
  5. package/dist/authentication/interface.d.ts +25 -3
  6. package/dist/authentication/utils.d.ts +0 -1
  7. package/dist/database/DatabaseController.d.ts +10 -10
  8. package/dist/database/interface.d.ts +5 -13
  9. package/dist/email/interface.d.ts +1 -1
  10. package/dist/graphql/resolvers.d.ts +4 -2
  11. package/dist/hooks/authentication.d.ts +2 -0
  12. package/dist/hooks/index.d.ts +1 -0
  13. package/dist/index.d.ts +0 -1
  14. package/dist/index.js +9130 -9041
  15. package/dist/server/index.d.ts +4 -2
  16. package/dist/server/interface.d.ts +1 -0
  17. package/dist/utils/crypto.d.ts +18 -0
  18. package/dist/utils/export.d.ts +1 -0
  19. package/dist/utils/helper.d.ts +4 -1
  20. package/dist/utils/index.d.ts +16 -3
  21. package/generated/schema.graphql +18 -15
  22. package/generated/wabe.ts +6 -5
  23. package/package.json +52 -53
  24. package/src/authentication/OTP.test.ts +69 -0
  25. package/src/authentication/OTP.ts +66 -0
  26. package/src/authentication/Session.test.ts +665 -0
  27. package/src/authentication/Session.ts +529 -0
  28. package/src/authentication/defaultAuthentication.ts +214 -0
  29. package/src/authentication/index.ts +3 -0
  30. package/src/authentication/interface.ts +157 -0
  31. package/src/authentication/oauth/GitHub.test.ts +105 -0
  32. package/src/authentication/oauth/GitHub.ts +133 -0
  33. package/src/authentication/oauth/Google.test.ts +105 -0
  34. package/src/authentication/oauth/Google.ts +110 -0
  35. package/src/authentication/oauth/Oauth2Client.test.ts +225 -0
  36. package/src/authentication/oauth/Oauth2Client.ts +140 -0
  37. package/src/authentication/oauth/index.ts +2 -0
  38. package/src/authentication/oauth/utils.test.ts +35 -0
  39. package/src/authentication/oauth/utils.ts +28 -0
  40. package/src/authentication/providers/EmailOTP.test.ts +138 -0
  41. package/src/authentication/providers/EmailOTP.ts +93 -0
  42. package/src/authentication/providers/EmailPassword.test.ts +187 -0
  43. package/src/authentication/providers/EmailPassword.ts +130 -0
  44. package/src/authentication/providers/EmailPasswordSRP.test.ts +206 -0
  45. package/src/authentication/providers/EmailPasswordSRP.ts +184 -0
  46. package/src/authentication/providers/GitHub.ts +30 -0
  47. package/src/authentication/providers/Google.ts +30 -0
  48. package/src/authentication/providers/OAuth.test.ts +185 -0
  49. package/src/authentication/providers/OAuth.ts +112 -0
  50. package/src/authentication/providers/PhonePassword.test.ts +187 -0
  51. package/src/authentication/providers/PhonePassword.ts +129 -0
  52. package/src/authentication/providers/QRCodeOTP.test.ts +79 -0
  53. package/src/authentication/providers/QRCodeOTP.ts +65 -0
  54. package/src/authentication/providers/index.ts +6 -0
  55. package/src/authentication/resolvers/refreshResolver.test.ts +37 -0
  56. package/src/authentication/resolvers/refreshResolver.ts +20 -0
  57. package/src/authentication/resolvers/signInWithResolver.inte.test.ts +59 -0
  58. package/src/authentication/resolvers/signInWithResolver.test.ts +307 -0
  59. package/src/authentication/resolvers/signInWithResolver.ts +102 -0
  60. package/src/authentication/resolvers/signOutResolver.test.ts +41 -0
  61. package/src/authentication/resolvers/signOutResolver.ts +22 -0
  62. package/src/authentication/resolvers/signUpWithResolver.test.ts +186 -0
  63. package/src/authentication/resolvers/signUpWithResolver.ts +69 -0
  64. package/src/authentication/resolvers/verifyChallenge.test.ts +136 -0
  65. package/src/authentication/resolvers/verifyChallenge.ts +69 -0
  66. package/src/authentication/roles.test.ts +59 -0
  67. package/src/authentication/roles.ts +40 -0
  68. package/src/authentication/utils.test.ts +99 -0
  69. package/src/authentication/utils.ts +43 -0
  70. package/src/cache/InMemoryCache.test.ts +62 -0
  71. package/src/cache/InMemoryCache.ts +45 -0
  72. package/src/cron/index.test.ts +17 -0
  73. package/src/cron/index.ts +46 -0
  74. package/src/database/DatabaseController.test.ts +625 -0
  75. package/src/database/DatabaseController.ts +983 -0
  76. package/src/database/index.test.ts +1230 -0
  77. package/src/database/index.ts +9 -0
  78. package/src/database/interface.ts +312 -0
  79. package/src/email/DevAdapter.ts +8 -0
  80. package/src/email/EmailController.test.ts +29 -0
  81. package/src/email/EmailController.ts +13 -0
  82. package/src/email/index.ts +2 -0
  83. package/src/email/interface.ts +36 -0
  84. package/src/email/templates/sendOtpCode.ts +120 -0
  85. package/src/file/FileController.ts +28 -0
  86. package/src/file/FileDevAdapter.ts +54 -0
  87. package/src/file/hookDeleteFile.ts +27 -0
  88. package/src/file/hookReadFile.ts +70 -0
  89. package/src/file/hookUploadFile.ts +53 -0
  90. package/src/file/index.test.ts +979 -0
  91. package/src/file/index.ts +2 -0
  92. package/src/file/interface.ts +42 -0
  93. package/src/graphql/GraphQLSchema.test.ts +4399 -0
  94. package/src/graphql/GraphQLSchema.ts +928 -0
  95. package/src/graphql/index.ts +2 -0
  96. package/src/graphql/parseGraphqlSchema.ts +94 -0
  97. package/src/graphql/parser.test.ts +217 -0
  98. package/src/graphql/parser.ts +566 -0
  99. package/src/graphql/pointerAndRelationFunction.ts +200 -0
  100. package/src/graphql/resolvers.ts +467 -0
  101. package/src/graphql/tests/aggregation.test.ts +1123 -0
  102. package/src/graphql/tests/e2e.test.ts +596 -0
  103. package/src/graphql/tests/scalars.test.ts +250 -0
  104. package/src/graphql/types.ts +219 -0
  105. package/src/hooks/HookObject.test.ts +122 -0
  106. package/src/hooks/HookObject.ts +168 -0
  107. package/src/hooks/authentication.ts +76 -0
  108. package/src/hooks/createUser.test.ts +77 -0
  109. package/src/hooks/createUser.ts +10 -0
  110. package/src/hooks/defaultFields.test.ts +187 -0
  111. package/src/hooks/defaultFields.ts +40 -0
  112. package/src/hooks/deleteSession.test.ts +181 -0
  113. package/src/hooks/deleteSession.ts +20 -0
  114. package/src/hooks/hashFieldHook.test.ts +163 -0
  115. package/src/hooks/hashFieldHook.ts +97 -0
  116. package/src/hooks/index.test.ts +207 -0
  117. package/src/hooks/index.ts +430 -0
  118. package/src/hooks/permissions.test.ts +424 -0
  119. package/src/hooks/permissions.ts +113 -0
  120. package/src/hooks/protected.test.ts +551 -0
  121. package/src/hooks/protected.ts +72 -0
  122. package/src/hooks/searchableFields.test.ts +166 -0
  123. package/src/hooks/searchableFields.ts +98 -0
  124. package/src/hooks/session.test.ts +138 -0
  125. package/src/hooks/session.ts +78 -0
  126. package/src/hooks/setEmail.test.ts +216 -0
  127. package/src/hooks/setEmail.ts +35 -0
  128. package/src/hooks/setupAcl.test.ts +589 -0
  129. package/src/hooks/setupAcl.ts +29 -0
  130. package/src/index.ts +9 -0
  131. package/src/schema/Schema.test.ts +484 -0
  132. package/src/schema/Schema.ts +795 -0
  133. package/src/schema/defaultResolvers.ts +94 -0
  134. package/src/schema/index.ts +1 -0
  135. package/src/schema/resolvers/meResolver.test.ts +62 -0
  136. package/src/schema/resolvers/meResolver.ts +14 -0
  137. package/src/schema/resolvers/newFile.ts +0 -0
  138. package/src/schema/resolvers/resetPassword.test.ts +345 -0
  139. package/src/schema/resolvers/resetPassword.ts +64 -0
  140. package/src/schema/resolvers/sendEmail.test.ts +118 -0
  141. package/src/schema/resolvers/sendEmail.ts +21 -0
  142. package/src/schema/resolvers/sendOtpCode.test.ts +153 -0
  143. package/src/schema/resolvers/sendOtpCode.ts +52 -0
  144. package/src/security.test.ts +3461 -0
  145. package/src/server/defaultSessionHandler.test.ts +66 -0
  146. package/src/server/defaultSessionHandler.ts +115 -0
  147. package/src/server/generateCodegen.ts +476 -0
  148. package/src/server/index.test.ts +552 -0
  149. package/src/server/index.ts +354 -0
  150. package/src/server/interface.ts +11 -0
  151. package/src/server/routes/authHandler.ts +187 -0
  152. package/src/server/routes/index.ts +40 -0
  153. package/src/utils/crypto.test.ts +41 -0
  154. package/src/utils/crypto.ts +121 -0
  155. package/src/utils/export.ts +13 -0
  156. package/src/utils/helper.ts +195 -0
  157. package/src/utils/index.test.ts +11 -0
  158. package/src/utils/index.ts +201 -0
  159. package/src/utils/preload.ts +8 -0
  160. package/src/utils/testHelper.ts +117 -0
  161. package/tsconfig.json +32 -0
  162. package/bunfig.toml +0 -4
  163. package/dist/ai/index.d.ts +0 -1
  164. package/dist/ai/interface.d.ts +0 -9
  165. /package/dist/server/{defaultHandlers.d.ts → defaultSessionHandler.d.ts} +0 -0
@@ -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
  }
@@ -6,4 +6,5 @@ export interface WabeContext<T extends WabeTypes> {
6
6
  sessionId?: string | null;
7
7
  isRoot: boolean;
8
8
  wabe: Wabe<T>;
9
+ isGraphQLCall?: boolean;
9
10
  }
@@ -0,0 +1,18 @@
1
+ /*
2
+ * Hash a string with Argon2id and PHC format
3
+ * @return : Returns the PHC format of the hashed text
4
+ */
5
+ export declare const hashArgon2: unknown;
6
+ /*
7
+ * Verify if a hash matchs with a string
8
+ * @return : Returns true if the password matchs with the hash, false otherwise
9
+ */
10
+ export declare const verifyArgon2: unknown;
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;
@@ -1,3 +1,4 @@
1
1
  import type { WabeContext } from "../server/interface";
2
2
  export declare const contextWithRoot: (context: WabeContext<any>) => WabeContext<any>;
3
3
  export declare const notEmpty: <T>(value: T | null | undefined) => value is T;
4
+ export * from "./crypto";
@@ -10,7 +10,10 @@ export interface DevWabeTypes extends WabeTypes {
10
10
  export declare const firstLetterUpperCase: (str: string) => string;
11
11
  export declare const getGraphqlClient: (port: number) => GraphQLClient;
12
12
  export declare const getAnonymousClient: (port: number) => GraphQLClient;
13
- export declare const getUserClient: (port: number, accessToken: string) => GraphQLClient;
13
+ export declare const getUserClient: (port: number, options: {
14
+ accessToken?: string;
15
+ csrfToken?: string;
16
+ }) => GraphQLClient;
14
17
  export declare const getAdminUserClient: (port: number, wabe: Wabe<DevWabeTypes>, { email, password }: {
15
18
  email: string;
16
19
  password: string;
@@ -1,9 +1,21 @@
1
1
  import type { ClassInterface } from "../schema";
2
- import type { WabeTypes, WabeConfig } from "../server";
3
- export declare const toBase32: (stringToEncode: string) => string;
2
+ import type { WabeTypes, WabeConfig, WabeContext } from "../server";
3
+ export declare const contextWithoutGraphQLCall: (context: WabeContext<any>) => WabeContext<any>;
4
+ type Base32Variant = "RFC3548" | "RFC4648" | "RFC4648-HEX" | "Crockford";
5
+ interface Base32Options {
6
+ padding?: boolean;
7
+ }
8
+ /**
9
+ * Convert supported input types to Uint8Array.
10
+ */
11
+ export declare const toUint8Array: (data: string | ArrayBuffer | Uint8Array | Buffer) => Uint8Array;
12
+ /**
13
+ * Encode binary data to base32 using specified variant.
14
+ * Base on https://github.com/LinusU/base32-encode/blob/master/index.js
15
+ */
16
+ export declare const base32Encode: (data: string | ArrayBuffer | Uint8Array | Buffer, variant: Base32Variant, options?: Base32Options) => string;
4
17
  export declare const getNewObjectAfterUpdateNestedProperty: unknown;
5
18
  export declare const getNestedProperty: unknown;
6
- export declare const isArgon2Hash: (value: string) => boolean;
7
19
  export declare const firstLetterInUpperCase: unknown;
8
20
  export declare const firstLetterInLowerCase: unknown;
9
21
  export declare const getClassFromClassName: <T extends WabeTypes>(className: string, config: WabeConfig<any>) => ClassInterface<T>;
@@ -20,3 +32,4 @@ export declare const getCookieInRequestHeaders: unknown;
20
32
  * - trim
21
33
  */
22
34
  export declare const tokenize: unknown;
35
+ export {};
@@ -13,6 +13,7 @@ enum AuthenticationProvider {
13
13
 
14
14
  enum SecondaryFactor {
15
15
  emailOTP
16
+ qrcodeOTP
16
17
  }
17
18
 
18
19
  """Object containing information about the file"""
@@ -424,9 +425,9 @@ input PostRelationInput {
424
425
  type _Session {
425
426
  id: ID!
426
427
  user: User
427
- accessToken: String!
428
+ accessTokenEncrypted: String!
428
429
  accessTokenExpiresAt: Date!
429
- refreshToken: String
430
+ refreshTokenEncrypted: String!
430
431
  refreshTokenExpiresAt: Date!
431
432
  acl: _SessionACLObject
432
433
  createdAt: Date
@@ -453,9 +454,9 @@ type _SessionACLObjectRolesACL {
453
454
 
454
455
  input _SessionInput {
455
456
  user: UserPointerInput
456
- accessToken: String!
457
+ accessTokenEncrypted: String!
457
458
  accessTokenExpiresAt: Date!
458
- refreshToken: String
459
+ refreshTokenEncrypted: String!
459
460
  refreshTokenExpiresAt: Date!
460
461
  acl: _SessionACLObjectInput
461
462
  createdAt: Date
@@ -489,9 +490,9 @@ input _SessionPointerInput {
489
490
 
490
491
  input _SessionCreateFieldsInput {
491
492
  user: UserPointerInput
492
- accessToken: String
493
+ accessTokenEncrypted: String
493
494
  accessTokenExpiresAt: Date
494
- refreshToken: String
495
+ refreshTokenEncrypted: String
495
496
  refreshTokenExpiresAt: Date
496
497
  acl: _SessionACLObjectCreateFieldsInput
497
498
  createdAt: Date
@@ -936,9 +937,9 @@ input RoleACLObjectRolesACLWhereInput {
936
937
  input _SessionWhereInput {
937
938
  id: IdWhereInput
938
939
  user: UserWhereInput
939
- accessToken: StringWhereInput
940
+ accessTokenEncrypted: StringWhereInput
940
941
  accessTokenExpiresAt: DateWhereInput
941
- refreshToken: StringWhereInput
942
+ refreshTokenEncrypted: StringWhereInput
942
943
  refreshTokenExpiresAt: DateWhereInput
943
944
  acl: _SessionACLObjectWhereInput
944
945
  createdAt: DateWhereInput
@@ -1098,12 +1099,12 @@ enum PostOrder {
1098
1099
  enum _SessionOrder {
1099
1100
  user_ASC
1100
1101
  user_DESC
1101
- accessToken_ASC
1102
- accessToken_DESC
1102
+ accessTokenEncrypted_ASC
1103
+ accessTokenEncrypted_DESC
1103
1104
  accessTokenExpiresAt_ASC
1104
1105
  accessTokenExpiresAt_DESC
1105
- refreshToken_ASC
1106
- refreshToken_DESC
1106
+ refreshTokenEncrypted_ASC
1107
+ refreshTokenEncrypted_DESC
1107
1108
  refreshTokenExpiresAt_ASC
1108
1109
  refreshTokenExpiresAt_DESC
1109
1110
  acl_ASC
@@ -1506,9 +1507,9 @@ input Update_SessionInput {
1506
1507
 
1507
1508
  input _SessionUpdateFieldsInput {
1508
1509
  user: UserPointerInput
1509
- accessToken: String
1510
+ accessTokenEncrypted: String
1510
1511
  accessTokenExpiresAt: Date
1511
- refreshToken: String
1512
+ refreshTokenEncrypted: String
1512
1513
  refreshTokenExpiresAt: Date
1513
1514
  acl: _SessionACLObjectUpdateFieldsInput
1514
1515
  createdAt: Date
@@ -1742,9 +1743,10 @@ input SendEmailInput {
1742
1743
  }
1743
1744
 
1744
1745
  type SignInWithOutput {
1745
- id: String
1746
+ user: User
1746
1747
  accessToken: String
1747
1748
  refreshToken: String
1749
+ csrfToken: String
1748
1750
  srp: SignInWithOutputSRPOutputSignInWith
1749
1751
  }
1750
1752
 
@@ -1796,6 +1798,7 @@ type SignUpWithOutput {
1796
1798
  id: String
1797
1799
  accessToken: String!
1798
1800
  refreshToken: String!
1801
+ csrfToken: String
1799
1802
  }
1800
1803
 
1801
1804
  input SignUpWithInput {
package/generated/wabe.ts CHANGED
@@ -12,7 +12,8 @@ export enum AuthenticationProvider {
12
12
  }
13
13
 
14
14
  export enum SecondaryFactor {
15
- emailOTP = "emailOTP"
15
+ emailOTP = "emailOTP",
16
+ qrcodeOTP = "qrcodeOTP"
16
17
  }
17
18
 
18
19
  export type ACLObjectUsersACL = {
@@ -115,9 +116,9 @@ export type Post = {
115
116
  export type _Session = {
116
117
  id: string,
117
118
  user: User,
118
- accessToken: string,
119
+ accessTokenEncrypted: string,
119
120
  accessTokenExpiresAt: string,
120
- refreshToken?: string,
121
+ refreshTokenEncrypted: string,
121
122
  refreshTokenExpiresAt: string,
122
123
  acl?: ACLObject,
123
124
  createdAt?: string,
@@ -180,9 +181,9 @@ export type WherePost = {
180
181
  export type Where_Session = {
181
182
  id: string,
182
183
  user: User,
183
- accessToken: string,
184
+ accessTokenEncrypted: string,
184
185
  accessTokenExpiresAt: Date,
185
- refreshToken?: string,
186
+ refreshTokenEncrypted: string,
186
187
  refreshTokenExpiresAt: Date,
187
188
  acl?: ACLObject,
188
189
  createdAt?: Date,
package/package.json CHANGED
@@ -1,55 +1,54 @@
1
1
  {
2
- "name": "wabe",
3
- "version": "0.6.8",
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",
11
- "keywords": [
12
- "backend",
13
- "wabe",
14
- "graphql",
15
- "baas"
16
- ],
17
- "repository": {
18
- "type": "git",
19
- "url": "git+https://github.com/palixir/wabe.git"
20
- },
21
- "main": "dist/index.js",
22
- "scripts": {
23
- "build": "bun --filter wabe-build build:package $(pwd)",
24
- "check": "tsc --project $(pwd)/tsconfig.json",
25
- "lint": "biome lint . --no-errors-on-unmatched --config-path=../../",
26
- "ci": "bun generate:codegen && bun lint $(pwd) && bun check && bun test src",
27
- "format": "biome format --write . --config-path=../../",
28
- "dev": "bun run --watch dev/index.ts",
29
- "generate:codegen": "touch generated/wabe.ts && CODEGEN=true bun dev/index.ts"
30
- },
31
- "dependencies": {
32
- "@graphql-yoga/plugin-disable-introspection": "2.10.9",
33
- "@node-rs/argon2": "2.0.2",
34
- "croner": "9.0.0",
35
- "js-srp6a": "1.0.2",
36
- "jsonwebtoken": "9.0.2",
37
- "libphonenumber-js": "1.11.18",
38
- "otplib": "12.0.1",
39
- "p-retry": "6.2.1",
40
- "wobe": "1.1.10",
41
- "wobe-graphql-yoga": "1.2.6"
42
- },
43
- "devDependencies": {
44
- "@types/jsonwebtoken": "9.0.6",
45
- "@types/uuid": "9.0.6",
46
- "graphql-request": "6.1.0",
47
- "get-port": "7.1.0",
48
- "uuid": "10.0.0",
49
- "wabe-mongodb-launcher": "workspace:*",
50
- "wabe-pluralize": "workspace:*",
51
- "wabe-build": "workspace:*",
52
- "wabe-mongodb": "workspace:*",
53
- "wabe": "workspace:*"
54
- }
2
+ "name": "wabe",
3
+ "version": "0.6.10",
4
+ "description": "Your backend without vendor lock-in in Typescript",
5
+ "homepage": "https://palixir.github.io/wabe/",
6
+ "author": {
7
+ "name": "coratgerl",
8
+ "url": "https://github.com/coratgerl"
9
+ },
10
+ "license": "Apache-2.0",
11
+ "keywords": [
12
+ "backend",
13
+ "wabe",
14
+ "graphql",
15
+ "baas"
16
+ ],
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/palixir/wabe.git"
20
+ },
21
+ "main": "dist/index.js",
22
+ "scripts": {
23
+ "build": "bun --filter wabe-build build:package $(pwd)",
24
+ "check": "tsc --project $(pwd)/tsconfig.json",
25
+ "lint": "biome lint . --no-errors-on-unmatched",
26
+ "ci": "bun generate:codegen && bun lint $(pwd) && bun check && bun test src",
27
+ "format": "biome format --write .",
28
+ "dev": "bun run --watch dev/index.ts",
29
+ "generate:codegen": "touch generated/wabe.ts && CODEGEN=true bun dev/index.ts"
30
+ },
31
+ "dependencies": {
32
+ "croner": "9.0.0",
33
+ "graphql": "16.12.0",
34
+ "js-srp6a": "1.0.2",
35
+ "jsonwebtoken": "9.0.2",
36
+ "libphonenumber-js": "1.11.18",
37
+ "otplib": "12.0.1",
38
+ "p-retry": "7.1.0",
39
+ "wobe": "1.1.14",
40
+ "wobe-graphql-yoga": "1.2.9"
41
+ },
42
+ "devDependencies": {
43
+ "@types/jsonwebtoken": "9.0.6",
44
+ "@types/uuid": "9.0.6",
45
+ "graphql-request": "6.1.0",
46
+ "get-port": "7.1.0",
47
+ "uuid": "13.0.0",
48
+ "wabe-mongodb-launcher": "0.5.2",
49
+ "wabe-pluralize": "0.0.1",
50
+ "wabe-build": "0.5.0",
51
+ "wabe-mongodb": "0.5.2",
52
+ "wabe": "0.6.9"
53
+ }
55
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,66 @@
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')
21
+ .update(`${this.secret}:${userId}`)
22
+ .digest()
23
+
24
+ return base32Encode(hash, 'RFC4648', { padding: false })
25
+ }
26
+
27
+ generate(userId: string): string {
28
+ const secret = this.deriveSecret(userId)
29
+
30
+ return this.internalTotp.generate(secret)
31
+ }
32
+
33
+ verify(otp: string, userId: string): boolean {
34
+ const secret = this.deriveSecret(userId)
35
+
36
+ return this.internalTotp.verify({ secret, token: otp })
37
+ }
38
+
39
+ authenticatorGenerate(userId: string): string {
40
+ const secret = this.deriveSecret(userId)
41
+ return authenticator.generate(secret)
42
+ }
43
+
44
+ authenticatorVerify(otp: string, userId: string): boolean {
45
+ const secret = this.deriveSecret(userId)
46
+
47
+ return authenticator.verify({
48
+ secret,
49
+ token: otp,
50
+ })
51
+ }
52
+
53
+ generateKeyuri({
54
+ userId,
55
+ emailOrUsername,
56
+ applicationName,
57
+ }: {
58
+ userId: string
59
+ emailOrUsername: string
60
+ applicationName: string
61
+ }): string {
62
+ const secret = this.deriveSecret(userId)
63
+
64
+ return authenticator.keyuri(emailOrUsername, applicationName, secret)
65
+ }
66
+ }