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
@@ -0,0 +1,334 @@
1
+ import type { DatabaseConfig } from '../database'
2
+ import { DatabaseController } from '../database/DatabaseController'
3
+ import { type EnumInterface, Schema, type SchemaInterface } from '../schema/Schema'
4
+ import { GraphQLError, GraphQLObjectType, GraphQLSchema } from 'graphql'
5
+ import { GraphQLSchema as WabeGraphQLSchema } from '../graphql'
6
+ import type { AuthenticationConfig } from '../authentication/interface'
7
+ import { type WabeRoute, defaultRoutes } from './routes'
8
+ import { type Hook, getDefaultHooks } from '../hooks'
9
+ import { generateCodegen } from './generateCodegen'
10
+ import { defaultAuthenticationMethods } from '../authentication/defaultAuthentication'
11
+ import { Wobe, cors, rateLimit } from 'wobe'
12
+ import type { Context, CorsOptions, RateLimitOptions } from 'wobe'
13
+ import type { WabeContext } from './interface'
14
+ import { initializeRoles } from '../authentication/roles'
15
+ import type { EmailConfig } from '../email'
16
+ import { EmailController } from '../email/EmailController'
17
+ import { FileController } from '../file/FileController'
18
+ import { defaultSessionHandler } from './defaultSessionHandler'
19
+ import type { CronConfig } from '../cron'
20
+ import type { FileConfig } from '../file'
21
+ import { WobeGraphqlYogaPlugin } from 'wobe-graphql-yoga'
22
+
23
+ type SecurityConfig = {
24
+ corsOptions?: CorsOptions
25
+ rateLimit?: RateLimitOptions
26
+ hideSensitiveErrorMessage?: boolean
27
+ disableCSRFProtection?: boolean
28
+ allowIntrospectionInProduction?: boolean
29
+ maxGraphqlDepth?: number
30
+ }
31
+
32
+ export * from './interface'
33
+ export * from './routes'
34
+
35
+ export const defaultRoles = ['DashboardAdmin']
36
+
37
+ export interface WabeConfig<T extends WabeTypes> {
38
+ port: number
39
+ isProduction: boolean
40
+ hostname?: string
41
+ security?: SecurityConfig
42
+ schema?: SchemaInterface<T>
43
+ graphqlSchema?: GraphQLSchema
44
+ database: DatabaseConfig<T>
45
+ codegen?:
46
+ | {
47
+ enabled: true
48
+ path: string
49
+ }
50
+ | { enabled?: false }
51
+ authentication?: AuthenticationConfig<T>
52
+ routes?: WabeRoute[]
53
+ rootKey: string
54
+ hooks?: Hook<T, any>[]
55
+ email?: EmailConfig
56
+ file?: FileConfig<T>
57
+ crons?: CronConfig<T>
58
+ }
59
+
60
+ export type WabeTypes = {
61
+ types: Record<any, any>
62
+ where: Record<any, any>
63
+ scalars: string
64
+ enums: Record<any, any>
65
+ }
66
+
67
+ export type WobeCustomContext<T extends WabeTypes> = Context & {
68
+ wabe: WabeContext<T>
69
+ }
70
+
71
+ type WabeControllers<T extends WabeTypes> = {
72
+ database: DatabaseController<T>
73
+ email?: EmailController
74
+ file?: FileController
75
+ }
76
+
77
+ export class Wabe<T extends WabeTypes> {
78
+ public server: Wobe<WobeCustomContext<T>>
79
+
80
+ public config: WabeConfig<T>
81
+ public controllers: WabeControllers<T>
82
+
83
+ constructor({
84
+ isProduction,
85
+ port,
86
+ hostname,
87
+ security,
88
+ schema,
89
+ database,
90
+ authentication,
91
+ rootKey,
92
+ codegen,
93
+ hooks,
94
+ file,
95
+ email,
96
+ routes,
97
+ crons,
98
+ }: WabeConfig<T>) {
99
+ this.config = {
100
+ isProduction,
101
+ port,
102
+ hostname,
103
+ security,
104
+ schema,
105
+ database,
106
+ codegen,
107
+ authentication,
108
+ rootKey,
109
+ hooks,
110
+ email,
111
+ routes,
112
+ file,
113
+ crons,
114
+ }
115
+
116
+ this.server = new Wobe<WobeCustomContext<T>>({ hostname }).get('/health', (context) => {
117
+ context.res.status = 200
118
+ context.res.send('OK')
119
+ })
120
+
121
+ this.controllers = {
122
+ database: new DatabaseController<T>(database.adapter),
123
+ email: email?.adapter ? new EmailController(email.adapter) : undefined,
124
+ file: file?.adapter ? new FileController(file.adapter, this) : undefined,
125
+ }
126
+
127
+ this.loadCrons()
128
+ this.loadAuthenticationMethods()
129
+ this.loadRoleEnum()
130
+ this.loadRoutes()
131
+ this.loadHooks()
132
+ }
133
+
134
+ loadCrons() {
135
+ if (!this.config.crons) return
136
+
137
+ const crons = this.config.crons.map((cron) => ({
138
+ ...cron,
139
+ job: cron.cron(this),
140
+ }))
141
+
142
+ this.config.crons = crons
143
+ }
144
+
145
+ loadRoleEnum() {
146
+ const roles = [...defaultRoles, ...(this.config.authentication?.roles || [])]
147
+
148
+ const roleEnum: EnumInterface = {
149
+ name: 'RoleEnum',
150
+ values: roles.reduce(
151
+ (acc, currentRole) => {
152
+ acc[currentRole] = currentRole
153
+ return acc
154
+ },
155
+ {} as Record<string, any>,
156
+ ),
157
+ }
158
+
159
+ this.config.schema = {
160
+ ...this.config.schema,
161
+ enums: [...(this.config.schema?.enums || []), roleEnum],
162
+ }
163
+ }
164
+
165
+ loadAuthenticationMethods() {
166
+ this.config.authentication = {
167
+ ...this.config.authentication,
168
+ customAuthenticationMethods: [
169
+ ...defaultAuthenticationMethods<T>(),
170
+ ...(this.config.authentication?.customAuthenticationMethods || []),
171
+ ],
172
+ }
173
+ }
174
+
175
+ loadHooks() {
176
+ if (this.config.hooks?.find((hook) => hook.priority <= 0))
177
+ throw new Error('Hook priority <= 0 is reserved for internal uses')
178
+
179
+ this.config.hooks = [...getDefaultHooks(), ...(this.config.hooks || [])]
180
+ }
181
+
182
+ loadRoutes() {
183
+ const wabeRoutes = [
184
+ ...defaultRoutes(this.config.file?.devDirectory || `${__dirname}/../../bucket`),
185
+ ...(this.config.routes || []),
186
+ ]
187
+
188
+ wabeRoutes.forEach((route) => {
189
+ const { method } = route
190
+
191
+ switch (method) {
192
+ case 'GET':
193
+ this.server.get(route.path, route.handler)
194
+ break
195
+ case 'POST':
196
+ this.server.post(route.path, route.handler)
197
+ break
198
+ case 'PUT':
199
+ this.server.put(route.path, route.handler)
200
+ break
201
+ case 'DELETE':
202
+ this.server.delete(route.path, route.handler)
203
+ break
204
+ default:
205
+ throw new Error('Invalid method for default route')
206
+ }
207
+ })
208
+ }
209
+
210
+ async start() {
211
+ if (this.config.authentication?.session && !this.config.authentication.session.jwtSecret)
212
+ throw new Error('Authentication session requires jwt secret')
213
+
214
+ const wabeSchema = new Schema(this.config)
215
+
216
+ this.config.schema = wabeSchema.schema
217
+
218
+ await this.controllers.database.initializeDatabase(wabeSchema.schema)
219
+
220
+ const graphqlSchema = new WabeGraphQLSchema(wabeSchema)
221
+
222
+ const types = graphqlSchema.createSchema()
223
+
224
+ this.config.graphqlSchema = new GraphQLSchema({
225
+ query: new GraphQLObjectType({
226
+ name: 'Query',
227
+ fields: types.queries,
228
+ }),
229
+ mutation: new GraphQLObjectType({
230
+ name: 'Mutation',
231
+ fields: types.mutations,
232
+ }),
233
+ types: [...types.scalars, ...types.enums, ...types.objects],
234
+ })
235
+
236
+ if (
237
+ !this.config.isProduction &&
238
+ process.env.NODE_ENV !== 'test' &&
239
+ this.config.codegen &&
240
+ this.config.codegen.enabled &&
241
+ this.config.codegen.path.length > 0
242
+ ) {
243
+ await generateCodegen({
244
+ path: this.config.codegen.path,
245
+ schema: wabeSchema.schema,
246
+ graphqlSchema: this.config.graphqlSchema,
247
+ })
248
+
249
+ // If we just want codegen we exit before server created.
250
+ // Not the best solution but useful to avoid multiple source of truth
251
+ if (process.env.CODEGEN) process.exit(0)
252
+ }
253
+
254
+ this.server.options(
255
+ '/*',
256
+ (ctx) => {
257
+ return ctx.res.send('OK')
258
+ },
259
+ cors(this.config.security?.corsOptions),
260
+ )
261
+
262
+ const rateLimitOptions = this.config.security?.rateLimit
263
+
264
+ if (rateLimitOptions) this.server.beforeHandler(rateLimit(rateLimitOptions))
265
+
266
+ this.server.beforeHandler(cors(this.config.security?.corsOptions))
267
+
268
+ // Set the wabe context
269
+ this.server.beforeHandler(
270
+ // @ts-expect-error
271
+ this.config.authentication?.sessionHandler ||
272
+ // @ts-expect-error
273
+ defaultSessionHandler(this),
274
+ )
275
+
276
+ const maxDepth = this.config.security?.maxGraphqlDepth ?? 50
277
+
278
+ await this.server.usePlugin(
279
+ WobeGraphqlYogaPlugin({
280
+ schema: this.config.graphqlSchema,
281
+ maskedErrors: this.config.security?.hideSensitiveErrorMessage || this.config.isProduction,
282
+ allowIntrospection: !!this.config.security?.allowIntrospectionInProduction,
283
+ maxDepth,
284
+ allowMultipleOperations: true,
285
+ graphqlEndpoint: '/graphql',
286
+ plugins: [
287
+ {
288
+ // @ts-expect-error
289
+ onValidate: ({ addValidationRule }) => {
290
+ // @ts-expect-error
291
+ addValidationRule((context) => {
292
+ return {
293
+ // @ts-expect-error
294
+ Field(node) {
295
+ const introspectionFields = [
296
+ '__schema',
297
+ '__type',
298
+ '__typeKind',
299
+ '__field',
300
+ '__inputValue',
301
+ '__enumValue',
302
+ '__directive',
303
+ ]
304
+
305
+ if (introspectionFields.includes(node.name.value)) {
306
+ context.reportError(
307
+ new GraphQLError('GraphQL introspection is not allowed in production'),
308
+ )
309
+ }
310
+ },
311
+ }
312
+ })
313
+ },
314
+ },
315
+ ],
316
+ context: async (ctx): Promise<WabeContext<T>> => ctx.wabe,
317
+ }),
318
+ )
319
+
320
+ this.server.listen(this.config.port, ({ port }) => {
321
+ if (!process.env.TEST) console.log(`Server is running on port ${port}`)
322
+ })
323
+
324
+ // @ts-expect-error
325
+ await initializeRoles(this)
326
+ }
327
+
328
+ async close() {
329
+ await this.controllers.database.close()
330
+ await this.server.stop()
331
+ }
332
+ }
333
+
334
+ export { generateCodegen }
@@ -0,0 +1,11 @@
1
+ import type { WobeResponse } from 'wobe'
2
+ import type { Wabe, WabeTypes } from '.'
3
+
4
+ export interface WabeContext<T extends WabeTypes> {
5
+ response?: WobeResponse
6
+ user?: T['types']['User'] | null
7
+ sessionId?: string | null
8
+ isRoot: boolean
9
+ wabe: Wabe<T>
10
+ isGraphQLCall?: boolean
11
+ }
@@ -0,0 +1,169 @@
1
+ import type { Context } from 'wobe'
2
+ import type { WabeContext } from '../interface'
3
+ import { ProviderEnum } from '../../authentication/interface'
4
+ import { getGraphqlClient } from '../../utils/helper'
5
+ import { gql } from 'graphql-request'
6
+ import { Google } from '../../authentication/oauth'
7
+ import { generateRandomValues } from '../../authentication/oauth/utils'
8
+ import { GitHub } from '../../authentication/oauth/GitHub'
9
+
10
+ /*
11
+ - Generate code verifier (back)
12
+ - Sent post request to a route on back with code verifier in url (back)
13
+ - Generate code challenge (back)
14
+ - Redirect the user to google auth page with code challenge (back -> front)
15
+ - User sign in with google (front)
16
+ - The user is redirected to the route with the code (front -> back)
17
+ - Get the code from the url (back)
18
+ - Validate and sign in with google provider (back)
19
+ */
20
+
21
+ // https://www.rfc-editor.org/rfc/rfc7636#section-4.4 not precise the storage of codeVerifier
22
+ export const oauthHandlerCallback = async (context: Context, wabeContext: WabeContext<any>) => {
23
+ try {
24
+ const state = decodeURIComponent(context.query.state || '')
25
+ const code = decodeURIComponent(context.query.code || '')
26
+
27
+ const stateInCookie = context.getCookie('state')
28
+
29
+ if (state !== stateInCookie) throw new Error('Authentication failed')
30
+
31
+ const codeVerifier = context.getCookie('code_verifier')
32
+ const provider = context.getCookie('provider')
33
+
34
+ const { signInWith } = await getGraphqlClient(wabeContext.wabe.config.port).request<any>(
35
+ gql`
36
+ mutation signInWith(
37
+ $authorizationCode: String!
38
+ $codeVerifier: String!
39
+ ) {
40
+ signInWith(
41
+ input: {
42
+ authentication: {
43
+ ${provider}: {
44
+ authorizationCode: $authorizationCode
45
+ codeVerifier: $codeVerifier
46
+ }
47
+ }
48
+ }
49
+ ){
50
+ accessToken
51
+ refreshToken
52
+ }
53
+ }
54
+ `,
55
+ {
56
+ authorizationCode: code,
57
+ codeVerifier,
58
+ },
59
+ )
60
+
61
+ const { accessToken, refreshToken } = signInWith
62
+
63
+ const isCookieSession = !!wabeContext.wabe.config.authentication?.session?.cookieSession
64
+
65
+ context.res.setCookie('accessToken', accessToken, {
66
+ // If cookie session we put httpOnly to true, otherwise the front will need to get it
67
+ // So we keep it to false
68
+ httpOnly: isCookieSession,
69
+ path: '/',
70
+ maxAge:
71
+ (wabeContext.wabe.config.authentication?.session?.accessTokenExpiresInMs ||
72
+ 60 * 15 * 1000) / 1000, // 15 minutes in seconds
73
+ sameSite: 'None',
74
+ secure: true,
75
+ })
76
+
77
+ context.res.setCookie('refreshToken', refreshToken, {
78
+ // If cookie session we put httpOnly to true, otherwise the front will need to get it
79
+ // So we keep it to false
80
+ httpOnly: isCookieSession,
81
+ path: '/',
82
+ maxAge:
83
+ (wabeContext.wabe.config.authentication?.session?.accessTokenExpiresInMs ||
84
+ 60 * 15 * 1000) / 1000, // 15 minutes in seconds
85
+ sameSite: 'None',
86
+ secure: true,
87
+ })
88
+
89
+ context.redirect(wabeContext.wabe.config.authentication?.successRedirectPath || '/')
90
+ } catch {
91
+ context.redirect(wabeContext.wabe.config.authentication?.failureRedirectPath || '/')
92
+ }
93
+ }
94
+
95
+ export const authHandler = (
96
+ context: Context,
97
+ wabeContext: WabeContext<any>,
98
+ provider: ProviderEnum,
99
+ ) => {
100
+ if (!wabeContext.wabe.config) throw new Error('Wabe config not found')
101
+
102
+ context.res.setCookie('provider', provider, {
103
+ httpOnly: true,
104
+ path: '/',
105
+ maxAge: 60 * 5, // 5 minutes
106
+ secure: true,
107
+ })
108
+
109
+ switch (provider) {
110
+ case ProviderEnum.google: {
111
+ const googleOauth = new Google(wabeContext.wabe.config)
112
+
113
+ const state = generateRandomValues()
114
+ const codeVerifier = generateRandomValues()
115
+
116
+ context.res.setCookie('code_verifier', codeVerifier, {
117
+ httpOnly: true,
118
+ path: '/',
119
+ maxAge: 60 * 5, // 5 minutes
120
+ secure: true,
121
+ })
122
+
123
+ context.res.setCookie('state', state, {
124
+ httpOnly: true,
125
+ path: '/',
126
+ maxAge: 60 * 5, // 5 minutes
127
+ secure: true,
128
+ })
129
+
130
+ const authorizationURL = googleOauth.createAuthorizationURL(state, codeVerifier, {
131
+ scopes: ['email'],
132
+ })
133
+
134
+ context.redirect(authorizationURL.toString())
135
+
136
+ break
137
+ }
138
+ case ProviderEnum.github: {
139
+ const githubOauth = new GitHub(wabeContext.wabe.config)
140
+
141
+ const state = generateRandomValues()
142
+ const codeVerifier = generateRandomValues()
143
+
144
+ context.res.setCookie('code_verifier', codeVerifier, {
145
+ httpOnly: true,
146
+ path: '/',
147
+ maxAge: 60 * 5, // 5 minutes
148
+ secure: true,
149
+ })
150
+
151
+ context.res.setCookie('state', state, {
152
+ httpOnly: true,
153
+ path: '/',
154
+ maxAge: 60 * 5, // 5 minutes
155
+ secure: true,
156
+ })
157
+
158
+ const authorizationURL = githubOauth.createAuthorizationURL(state, codeVerifier, {
159
+ scopes: ['email'],
160
+ })
161
+
162
+ context.redirect(authorizationURL.toString())
163
+
164
+ break
165
+ }
166
+ default:
167
+ break
168
+ }
169
+ }
@@ -0,0 +1,39 @@
1
+ import { type WobeHandler, uploadDirectory } from 'wobe'
2
+ import type { ProviderEnum } from '../../authentication/interface'
3
+ import { authHandler, oauthHandlerCallback } from './authHandler'
4
+ import type { WobeCustomContext } from '..'
5
+
6
+ export interface WabeRoute {
7
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE'
8
+ path: string
9
+ handler: WobeHandler<WobeCustomContext<any>>
10
+ }
11
+
12
+ export const defaultRoutes = (devDirectory: string): WabeRoute[] => {
13
+ const routes: WabeRoute[] = [
14
+ {
15
+ method: 'GET',
16
+ path: '/auth/oauth',
17
+ handler: (context) => {
18
+ const provider = context.query.provider
19
+
20
+ if (!provider) throw new Error('Authentication failed, provider not found')
21
+
22
+ // TODO: Maybe check if the value is in the enum
23
+ return authHandler(context, context.wabe, provider as ProviderEnum)
24
+ },
25
+ },
26
+ {
27
+ method: 'GET',
28
+ path: '/auth/oauth/callback',
29
+ handler: (context) => oauthHandlerCallback(context, context.wabe),
30
+ },
31
+ {
32
+ method: 'GET',
33
+ path: '/bucket/:filename',
34
+ handler: uploadDirectory({ directory: devDirectory }),
35
+ },
36
+ ]
37
+
38
+ return routes
39
+ }
@@ -0,0 +1,41 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+ import { encryptDeterministicToken, decryptDeterministicToken } from './crypto'
3
+
4
+ const key = Buffer.alloc(32, 1) // deterministic test key
5
+
6
+ describe('crypto deterministic token helpers', () => {
7
+ it('should encrypt and decrypt deterministically with same key', () => {
8
+ const token = 'my-token'
9
+ const encrypted = encryptDeterministicToken(token, key)
10
+ const decrypted = decryptDeterministicToken(encrypted, key)
11
+
12
+ expect(decrypted).toBe(token)
13
+ })
14
+
15
+ it('should produce the same ciphertext for the same token/key', () => {
16
+ const token = 'stable'
17
+ const enc1 = encryptDeterministicToken(token, key)
18
+ const enc2 = encryptDeterministicToken(token, key)
19
+
20
+ expect(enc1).toBe(enc2)
21
+ })
22
+
23
+ it('should produce different ciphertexts for different tokens', () => {
24
+ const enc1 = encryptDeterministicToken('a', key)
25
+ const enc2 = encryptDeterministicToken('b', key)
26
+
27
+ expect(enc1).not.toBe(enc2)
28
+ })
29
+
30
+ it('should return null when decrypting with the wrong key', () => {
31
+ const token = 'secret'
32
+ const encrypted = encryptDeterministicToken(token, key)
33
+ const wrongKey = Buffer.alloc(32, 2)
34
+
35
+ expect(decryptDeterministicToken(encrypted, wrongKey)).toBeNull()
36
+ })
37
+
38
+ it('should return null on malformed ciphertext', () => {
39
+ expect(decryptDeterministicToken('bad:format', key)).toBeNull()
40
+ })
41
+ })
@@ -0,0 +1,105 @@
1
+ import { randomBytes, createCipheriv, createDecipheriv, createHmac } from 'node:crypto'
2
+ import { promisify } from 'node:util'
3
+
4
+ const params = {
5
+ parallelism: 1,
6
+ tagLength: 64,
7
+ memory: 65536,
8
+ passes: 2,
9
+ }
10
+
11
+ /*
12
+ * Hash a string with Argon2id and PHC format
13
+ * @return : Returns the PHC format of the hashed text
14
+ */
15
+ export const hashArgon2 = async (text: string) => {
16
+ if (process.versions.bun) return Bun.password.hash(text, { algorithm: 'argon2id' })
17
+
18
+ // Node support
19
+ const argon2 = promisify(require('node:crypto').argon2)
20
+
21
+ const nonce = randomBytes(16)
22
+
23
+ const result = await argon2('argon2id', {
24
+ message: text,
25
+ nonce,
26
+ ...params,
27
+ })
28
+
29
+ return `$argon2id$v=19$m=${params.memory},t=${params.passes},p=${params.parallelism}$${nonce.toString('base64').replace(/=+$/, '')}$${result.toString('base64').replace(/=+$/, '')}`
30
+ }
31
+
32
+ /*
33
+ * Verify if a hash matchs with a string
34
+ * @return : Returns true if the password matchs with the hash, false otherwise
35
+ */
36
+ export const verifyArgon2 = async (password: string, hash: string) => {
37
+ if (process.versions.bun) return Bun.password.verify(password, hash, 'argon2id')
38
+
39
+ // Node support
40
+ const [, algorithm, , paramString, nonceHex, storedHashHex] = hash.split('$')
41
+
42
+ const kvPairs = paramString?.split(',')
43
+ const parsedParams = Object.fromEntries(
44
+ kvPairs?.map((pair) => {
45
+ const [key, value] = pair.split('=')
46
+ return [key, Number.parseInt(value || '', 10)]
47
+ }) || [],
48
+ )
49
+
50
+ const memory = parsedParams.m
51
+ const passes = parsedParams.t
52
+ const parallelism = parsedParams.p
53
+
54
+ const newDerived = await promisify(require('node:crypto'))(algorithm, {
55
+ nonce: Buffer.from(nonceHex || '', 'base64'),
56
+ parallelism,
57
+ tagLength: 64,
58
+ memory,
59
+ passes,
60
+ message: password,
61
+ })
62
+
63
+ const isMatch = crypto.timingSafeEqual(
64
+ Buffer.from(newDerived),
65
+ Buffer.from(storedHashHex || '', 'base64'),
66
+ )
67
+
68
+ return isMatch
69
+ }
70
+
71
+ export const isArgon2Hash = (value: string): boolean =>
72
+ typeof value === 'string' && value.startsWith('$argon2')
73
+
74
+ /**
75
+ * Deterministic AES-256-GCM encryption for tokens.
76
+ * IV is derived via HMAC-SHA256(key, token) to allow equality checks without storing plaintext.
77
+ * Caller must provide a strong 32-byte key (already derived/hashed).
78
+ */
79
+ export const encryptDeterministicToken = (token: string, key: Buffer): string => {
80
+ const iv = createHmac('sha256', key).update(token).digest().subarray(0, 12)
81
+ const cipher = createCipheriv('aes-256-gcm', key, iv)
82
+ const encrypted = Buffer.concat([cipher.update(token, 'utf8'), cipher.final()])
83
+ const tag = cipher.getAuthTag()
84
+ return `${iv.toString('hex')}:${tag.toString('hex')}:${encrypted.toString('hex')}`
85
+ }
86
+
87
+ export const decryptDeterministicToken = (
88
+ encryptedToken: string | undefined,
89
+ key: Buffer,
90
+ ): string | null => {
91
+ if (!encryptedToken) return null
92
+ const [ivHex, tagHex, valueHex] = encryptedToken.split(':')
93
+ if (!ivHex || !tagHex || !valueHex) return null
94
+ try {
95
+ const iv = Buffer.from(ivHex, 'hex')
96
+ const tag = Buffer.from(tagHex, 'hex')
97
+ const encryptedValue = Buffer.from(valueHex, 'hex')
98
+ const decipher = createDecipheriv('aes-256-gcm', key, iv)
99
+ decipher.setAuthTag(tag)
100
+ const decrypted = Buffer.concat([decipher.update(encryptedValue), decipher.final()])
101
+ return decrypted.toString('utf8')
102
+ } catch {
103
+ return null
104
+ }
105
+ }