wabe 0.6.9 → 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 (158) hide show
  1. package/README.md +138 -32
  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/email/interface.d.ts +1 -1
  7. package/dist/graphql/resolvers.d.ts +4 -2
  8. package/dist/hooks/index.d.ts +1 -0
  9. package/dist/index.d.ts +0 -1
  10. package/dist/index.js +8713 -8867
  11. package/dist/server/index.d.ts +4 -2
  12. package/dist/utils/crypto.d.ts +7 -0
  13. package/dist/utils/helper.d.ts +4 -1
  14. package/generated/schema.graphql +16 -14
  15. package/generated/wabe.ts +4 -4
  16. package/package.json +15 -15
  17. package/src/authentication/OTP.test.ts +69 -0
  18. package/src/authentication/OTP.ts +66 -0
  19. package/src/authentication/Session.test.ts +665 -0
  20. package/src/authentication/Session.ts +529 -0
  21. package/src/authentication/defaultAuthentication.ts +214 -0
  22. package/src/authentication/index.ts +3 -0
  23. package/src/authentication/interface.ts +157 -0
  24. package/src/authentication/oauth/GitHub.test.ts +105 -0
  25. package/src/authentication/oauth/GitHub.ts +133 -0
  26. package/src/authentication/oauth/Google.test.ts +105 -0
  27. package/src/authentication/oauth/Google.ts +110 -0
  28. package/src/authentication/oauth/Oauth2Client.test.ts +225 -0
  29. package/src/authentication/oauth/Oauth2Client.ts +140 -0
  30. package/src/authentication/oauth/index.ts +2 -0
  31. package/src/authentication/oauth/utils.test.ts +35 -0
  32. package/src/authentication/oauth/utils.ts +28 -0
  33. package/src/authentication/providers/EmailOTP.test.ts +138 -0
  34. package/src/authentication/providers/EmailOTP.ts +93 -0
  35. package/src/authentication/providers/EmailPassword.test.ts +187 -0
  36. package/src/authentication/providers/EmailPassword.ts +130 -0
  37. package/src/authentication/providers/EmailPasswordSRP.test.ts +206 -0
  38. package/src/authentication/providers/EmailPasswordSRP.ts +184 -0
  39. package/src/authentication/providers/GitHub.ts +30 -0
  40. package/src/authentication/providers/Google.ts +30 -0
  41. package/src/authentication/providers/OAuth.test.ts +185 -0
  42. package/src/authentication/providers/OAuth.ts +112 -0
  43. package/src/authentication/providers/PhonePassword.test.ts +187 -0
  44. package/src/authentication/providers/PhonePassword.ts +129 -0
  45. package/src/authentication/providers/QRCodeOTP.test.ts +79 -0
  46. package/src/authentication/providers/QRCodeOTP.ts +65 -0
  47. package/src/authentication/providers/index.ts +6 -0
  48. package/src/authentication/resolvers/refreshResolver.test.ts +37 -0
  49. package/src/authentication/resolvers/refreshResolver.ts +20 -0
  50. package/src/authentication/resolvers/signInWithResolver.inte.test.ts +59 -0
  51. package/src/authentication/resolvers/signInWithResolver.test.ts +307 -0
  52. package/src/authentication/resolvers/signInWithResolver.ts +102 -0
  53. package/src/authentication/resolvers/signOutResolver.test.ts +41 -0
  54. package/src/authentication/resolvers/signOutResolver.ts +22 -0
  55. package/src/authentication/resolvers/signUpWithResolver.test.ts +186 -0
  56. package/src/authentication/resolvers/signUpWithResolver.ts +69 -0
  57. package/src/authentication/resolvers/verifyChallenge.test.ts +136 -0
  58. package/src/authentication/resolvers/verifyChallenge.ts +69 -0
  59. package/src/authentication/roles.test.ts +59 -0
  60. package/src/authentication/roles.ts +40 -0
  61. package/src/authentication/utils.test.ts +99 -0
  62. package/src/authentication/utils.ts +43 -0
  63. package/src/cache/InMemoryCache.test.ts +62 -0
  64. package/src/cache/InMemoryCache.ts +45 -0
  65. package/src/cron/index.test.ts +17 -0
  66. package/src/cron/index.ts +46 -0
  67. package/src/database/DatabaseController.test.ts +625 -0
  68. package/src/database/DatabaseController.ts +983 -0
  69. package/src/database/index.test.ts +1230 -0
  70. package/src/database/index.ts +9 -0
  71. package/src/database/interface.ts +312 -0
  72. package/src/email/DevAdapter.ts +8 -0
  73. package/src/email/EmailController.test.ts +29 -0
  74. package/src/email/EmailController.ts +13 -0
  75. package/src/email/index.ts +2 -0
  76. package/src/email/interface.ts +36 -0
  77. package/src/email/templates/sendOtpCode.ts +120 -0
  78. package/src/file/FileController.ts +28 -0
  79. package/src/file/FileDevAdapter.ts +54 -0
  80. package/src/file/hookDeleteFile.ts +27 -0
  81. package/src/file/hookReadFile.ts +70 -0
  82. package/src/file/hookUploadFile.ts +53 -0
  83. package/src/file/index.test.ts +979 -0
  84. package/src/file/index.ts +2 -0
  85. package/src/file/interface.ts +42 -0
  86. package/src/graphql/GraphQLSchema.test.ts +4399 -0
  87. package/src/graphql/GraphQLSchema.ts +928 -0
  88. package/src/graphql/index.ts +2 -0
  89. package/src/graphql/parseGraphqlSchema.ts +94 -0
  90. package/src/graphql/parser.test.ts +217 -0
  91. package/src/graphql/parser.ts +566 -0
  92. package/src/graphql/pointerAndRelationFunction.ts +200 -0
  93. package/src/graphql/resolvers.ts +467 -0
  94. package/src/graphql/tests/aggregation.test.ts +1123 -0
  95. package/src/graphql/tests/e2e.test.ts +596 -0
  96. package/src/graphql/tests/scalars.test.ts +250 -0
  97. package/src/graphql/types.ts +219 -0
  98. package/src/hooks/HookObject.test.ts +122 -0
  99. package/src/hooks/HookObject.ts +168 -0
  100. package/src/hooks/authentication.ts +76 -0
  101. package/src/hooks/createUser.test.ts +77 -0
  102. package/src/hooks/createUser.ts +10 -0
  103. package/src/hooks/defaultFields.test.ts +187 -0
  104. package/src/hooks/defaultFields.ts +40 -0
  105. package/src/hooks/deleteSession.test.ts +181 -0
  106. package/src/hooks/deleteSession.ts +20 -0
  107. package/src/hooks/hashFieldHook.test.ts +163 -0
  108. package/src/hooks/hashFieldHook.ts +97 -0
  109. package/src/hooks/index.test.ts +207 -0
  110. package/src/hooks/index.ts +430 -0
  111. package/src/hooks/permissions.test.ts +424 -0
  112. package/src/hooks/permissions.ts +113 -0
  113. package/src/hooks/protected.test.ts +551 -0
  114. package/src/hooks/protected.ts +72 -0
  115. package/src/hooks/searchableFields.test.ts +166 -0
  116. package/src/hooks/searchableFields.ts +98 -0
  117. package/src/hooks/session.test.ts +138 -0
  118. package/src/hooks/session.ts +78 -0
  119. package/src/hooks/setEmail.test.ts +216 -0
  120. package/src/hooks/setEmail.ts +35 -0
  121. package/src/hooks/setupAcl.test.ts +589 -0
  122. package/src/hooks/setupAcl.ts +29 -0
  123. package/src/index.ts +9 -0
  124. package/src/schema/Schema.test.ts +484 -0
  125. package/src/schema/Schema.ts +795 -0
  126. package/src/schema/defaultResolvers.ts +94 -0
  127. package/src/schema/index.ts +1 -0
  128. package/src/schema/resolvers/meResolver.test.ts +62 -0
  129. package/src/schema/resolvers/meResolver.ts +14 -0
  130. package/src/schema/resolvers/newFile.ts +0 -0
  131. package/src/schema/resolvers/resetPassword.test.ts +345 -0
  132. package/src/schema/resolvers/resetPassword.ts +64 -0
  133. package/src/schema/resolvers/sendEmail.test.ts +118 -0
  134. package/src/schema/resolvers/sendEmail.ts +21 -0
  135. package/src/schema/resolvers/sendOtpCode.test.ts +153 -0
  136. package/src/schema/resolvers/sendOtpCode.ts +52 -0
  137. package/src/security.test.ts +3461 -0
  138. package/src/server/defaultSessionHandler.test.ts +66 -0
  139. package/src/server/defaultSessionHandler.ts +115 -0
  140. package/src/server/generateCodegen.ts +476 -0
  141. package/src/server/index.test.ts +552 -0
  142. package/src/server/index.ts +354 -0
  143. package/src/server/interface.ts +11 -0
  144. package/src/server/routes/authHandler.ts +187 -0
  145. package/src/server/routes/index.ts +40 -0
  146. package/src/utils/crypto.test.ts +41 -0
  147. package/src/utils/crypto.ts +121 -0
  148. package/src/utils/export.ts +13 -0
  149. package/src/utils/helper.ts +195 -0
  150. package/src/utils/index.test.ts +11 -0
  151. package/src/utils/index.ts +201 -0
  152. package/src/utils/preload.ts +8 -0
  153. package/src/utils/testHelper.ts +117 -0
  154. package/tsconfig.json +32 -0
  155. package/bunfig.toml +0 -4
  156. package/dist/ai/index.d.ts +0 -1
  157. package/dist/ai/interface.d.ts +0 -9
  158. /package/dist/server/{defaultHandlers.d.ts → defaultSessionHandler.d.ts} +0 -0
@@ -0,0 +1,354 @@
1
+ import type { DatabaseConfig } from '../database'
2
+ import { DatabaseController } from '../database/DatabaseController'
3
+ import {
4
+ type EnumInterface,
5
+ Schema,
6
+ type SchemaInterface,
7
+ } from '../schema/Schema'
8
+ import { GraphQLError, GraphQLObjectType, GraphQLSchema } from 'graphql'
9
+ import { GraphQLSchema as WabeGraphQLSchema } from '../graphql'
10
+ import type { AuthenticationConfig } from '../authentication/interface'
11
+ import { type WabeRoute, defaultRoutes } from './routes'
12
+ import { type Hook, getDefaultHooks } from '../hooks'
13
+ import { generateCodegen } from './generateCodegen'
14
+ import { defaultAuthenticationMethods } from '../authentication/defaultAuthentication'
15
+ import { Wobe, cors, rateLimit } from 'wobe'
16
+ import type { Context, CorsOptions, RateLimitOptions } from 'wobe'
17
+ import type { WabeContext } from './interface'
18
+ import { initializeRoles } from '../authentication/roles'
19
+ import type { EmailConfig } from '../email'
20
+ import { EmailController } from '../email/EmailController'
21
+ import { FileController } from '../file/FileController'
22
+ import { defaultSessionHandler } from './defaultSessionHandler'
23
+ import type { CronConfig } from '../cron'
24
+ import type { FileConfig } from '../file'
25
+ import { WobeGraphqlYogaPlugin } from 'wobe-graphql-yoga'
26
+
27
+ type SecurityConfig = {
28
+ corsOptions?: CorsOptions
29
+ rateLimit?: RateLimitOptions
30
+ hideSensitiveErrorMessage?: boolean
31
+ disableCSRFProtection?: boolean
32
+ allowIntrospectionInProduction?: boolean
33
+ maxGraphqlDepth?: number
34
+ }
35
+
36
+ export * from './interface'
37
+ export * from './routes'
38
+
39
+ export const defaultRoles = ['DashboardAdmin']
40
+
41
+ export interface WabeConfig<T extends WabeTypes> {
42
+ port: number
43
+ isProduction: boolean
44
+ hostname?: string
45
+ security?: SecurityConfig
46
+ schema?: SchemaInterface<T>
47
+ graphqlSchema?: GraphQLSchema
48
+ database: DatabaseConfig<T>
49
+ codegen?:
50
+ | {
51
+ enabled: true
52
+ path: string
53
+ }
54
+ | { enabled?: false }
55
+ authentication?: AuthenticationConfig<T>
56
+ routes?: WabeRoute[]
57
+ rootKey: string
58
+ hooks?: Hook<T, any>[]
59
+ email?: EmailConfig
60
+ file?: FileConfig<T>
61
+ crons?: CronConfig<T>
62
+ }
63
+
64
+ export type WabeTypes = {
65
+ types: Record<any, any>
66
+ where: Record<any, any>
67
+ scalars: string
68
+ enums: Record<any, any>
69
+ }
70
+
71
+ export type WobeCustomContext<T extends WabeTypes> = Context & {
72
+ wabe: WabeContext<T>
73
+ }
74
+
75
+ type WabeControllers<T extends WabeTypes> = {
76
+ database: DatabaseController<T>
77
+ email?: EmailController
78
+ file?: FileController
79
+ }
80
+
81
+ export class Wabe<T extends WabeTypes> {
82
+ public server: Wobe<WobeCustomContext<T>>
83
+
84
+ public config: WabeConfig<T>
85
+ public controllers: WabeControllers<T>
86
+
87
+ constructor({
88
+ isProduction,
89
+ port,
90
+ hostname,
91
+ security,
92
+ schema,
93
+ database,
94
+ authentication,
95
+ rootKey,
96
+ codegen,
97
+ hooks,
98
+ file,
99
+ email,
100
+ routes,
101
+ crons,
102
+ }: WabeConfig<T>) {
103
+ this.config = {
104
+ isProduction,
105
+ port,
106
+ hostname,
107
+ security,
108
+ schema,
109
+ database,
110
+ codegen,
111
+ authentication,
112
+ rootKey,
113
+ hooks,
114
+ email,
115
+ routes,
116
+ file,
117
+ crons,
118
+ }
119
+
120
+ this.server = new Wobe<WobeCustomContext<T>>({ hostname }).get(
121
+ '/health',
122
+ (context) => {
123
+ context.res.status = 200
124
+ context.res.send('OK')
125
+ },
126
+ )
127
+
128
+ this.controllers = {
129
+ database: new DatabaseController<T>(database.adapter),
130
+ email: email?.adapter ? new EmailController(email.adapter) : undefined,
131
+ file: file?.adapter ? new FileController(file.adapter, this) : undefined,
132
+ }
133
+
134
+ this.loadCrons()
135
+ this.loadAuthenticationMethods()
136
+ this.loadRoleEnum()
137
+ this.loadRoutes()
138
+ this.loadHooks()
139
+ }
140
+
141
+ loadCrons() {
142
+ if (!this.config.crons) return
143
+
144
+ const crons = this.config.crons.map((cron) => ({
145
+ ...cron,
146
+ job: cron.cron(this),
147
+ }))
148
+
149
+ this.config.crons = crons
150
+ }
151
+
152
+ loadRoleEnum() {
153
+ const roles = [
154
+ ...defaultRoles,
155
+ ...(this.config.authentication?.roles || []),
156
+ ]
157
+
158
+ const roleEnum: EnumInterface = {
159
+ name: 'RoleEnum',
160
+ values: roles.reduce(
161
+ (acc, currentRole) => {
162
+ acc[currentRole] = currentRole
163
+ return acc
164
+ },
165
+ {} as Record<string, any>,
166
+ ),
167
+ }
168
+
169
+ this.config.schema = {
170
+ ...this.config.schema,
171
+ enums: [...(this.config.schema?.enums || []), roleEnum],
172
+ }
173
+ }
174
+
175
+ loadAuthenticationMethods() {
176
+ this.config.authentication = {
177
+ ...this.config.authentication,
178
+ customAuthenticationMethods: [
179
+ ...defaultAuthenticationMethods<T>(),
180
+ ...(this.config.authentication?.customAuthenticationMethods || []),
181
+ ],
182
+ }
183
+ }
184
+
185
+ loadHooks() {
186
+ if (this.config.hooks?.find((hook) => hook.priority <= 0))
187
+ throw new Error('Hook priority <= 0 is reserved for internal uses')
188
+
189
+ this.config.hooks = [...getDefaultHooks(), ...(this.config.hooks || [])]
190
+ }
191
+
192
+ loadRoutes() {
193
+ const wabeRoutes = [
194
+ ...defaultRoutes(
195
+ this.config.file?.devDirectory || `${__dirname}/../../bucket`,
196
+ ),
197
+ ...(this.config.routes || []),
198
+ ]
199
+
200
+ wabeRoutes.forEach((route) => {
201
+ const { method } = route
202
+
203
+ switch (method) {
204
+ case 'GET':
205
+ this.server.get(route.path, route.handler)
206
+ break
207
+ case 'POST':
208
+ this.server.post(route.path, route.handler)
209
+ break
210
+ case 'PUT':
211
+ this.server.put(route.path, route.handler)
212
+ break
213
+ case 'DELETE':
214
+ this.server.delete(route.path, route.handler)
215
+ break
216
+ default:
217
+ throw new Error('Invalid method for default route')
218
+ }
219
+ })
220
+ }
221
+
222
+ async start() {
223
+ if (
224
+ this.config.authentication?.session &&
225
+ !this.config.authentication.session.jwtSecret
226
+ )
227
+ throw new Error('Authentication session requires jwt secret')
228
+
229
+ const wabeSchema = new Schema(this.config)
230
+
231
+ this.config.schema = wabeSchema.schema
232
+
233
+ await this.controllers.database.initializeDatabase(wabeSchema.schema)
234
+
235
+ const graphqlSchema = new WabeGraphQLSchema(wabeSchema)
236
+
237
+ const types = graphqlSchema.createSchema()
238
+
239
+ this.config.graphqlSchema = new GraphQLSchema({
240
+ query: new GraphQLObjectType({
241
+ name: 'Query',
242
+ fields: types.queries,
243
+ }),
244
+ mutation: new GraphQLObjectType({
245
+ name: 'Mutation',
246
+ fields: types.mutations,
247
+ }),
248
+ types: [...types.scalars, ...types.enums, ...types.objects],
249
+ })
250
+
251
+ if (
252
+ !this.config.isProduction &&
253
+ process.env.NODE_ENV !== 'test' &&
254
+ this.config.codegen &&
255
+ this.config.codegen.enabled &&
256
+ this.config.codegen.path.length > 0
257
+ ) {
258
+ await generateCodegen({
259
+ path: this.config.codegen.path,
260
+ schema: wabeSchema.schema,
261
+ graphqlSchema: this.config.graphqlSchema,
262
+ })
263
+
264
+ // If we just want codegen we exit before server created.
265
+ // Not the best solution but useful to avoid multiple source of truth
266
+ if (process.env.CODEGEN) process.exit(0)
267
+ }
268
+
269
+ this.server.options(
270
+ '/*',
271
+ (ctx) => {
272
+ return ctx.res.send('OK')
273
+ },
274
+ cors(this.config.security?.corsOptions),
275
+ )
276
+
277
+ const rateLimitOptions = this.config.security?.rateLimit
278
+
279
+ if (rateLimitOptions) this.server.beforeHandler(rateLimit(rateLimitOptions))
280
+
281
+ this.server.beforeHandler(cors(this.config.security?.corsOptions))
282
+
283
+ // Set the wabe context
284
+ this.server.beforeHandler(
285
+ // @ts-expect-error
286
+ this.config.authentication?.sessionHandler ||
287
+ // @ts-expect-error
288
+ defaultSessionHandler(this),
289
+ )
290
+
291
+ const maxDepth = this.config.security?.maxGraphqlDepth ?? 50
292
+
293
+ await this.server.usePlugin(
294
+ WobeGraphqlYogaPlugin({
295
+ schema: this.config.graphqlSchema,
296
+ maskedErrors:
297
+ this.config.security?.hideSensitiveErrorMessage ||
298
+ this.config.isProduction,
299
+ allowIntrospection:
300
+ !!this.config.security?.allowIntrospectionInProduction,
301
+ maxDepth,
302
+ allowMultipleOperations: true,
303
+ graphqlEndpoint: '/graphql',
304
+ plugins: [
305
+ {
306
+ // @ts-expect-error
307
+ onValidate: ({ addValidationRule }) => {
308
+ // @ts-expect-error
309
+ addValidationRule((context) => {
310
+ return {
311
+ // @ts-expect-error
312
+ Field(node) {
313
+ const introspectionFields = [
314
+ '__schema',
315
+ '__type',
316
+ '__typeKind',
317
+ '__field',
318
+ '__inputValue',
319
+ '__enumValue',
320
+ '__directive',
321
+ ]
322
+
323
+ if (introspectionFields.includes(node.name.value)) {
324
+ context.reportError(
325
+ new GraphQLError(
326
+ 'GraphQL introspection is not allowed in production',
327
+ ),
328
+ )
329
+ }
330
+ },
331
+ }
332
+ })
333
+ },
334
+ },
335
+ ],
336
+ context: async (ctx): Promise<WabeContext<T>> => ctx.wabe,
337
+ }),
338
+ )
339
+
340
+ // @ts-expect-error
341
+ await Promise.all([initializeRoles(this)])
342
+
343
+ this.server.listen(this.config.port, ({ port }) => {
344
+ if (!process.env.TEST) console.log(`Server is running on port ${port}`)
345
+ })
346
+ }
347
+
348
+ async close() {
349
+ await this.controllers.database.close()
350
+ this.server.stop()
351
+ }
352
+ }
353
+
354
+ 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,187 @@
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 (
23
+ context: Context,
24
+ wabeContext: WabeContext<any>,
25
+ ) => {
26
+ try {
27
+ const state = decodeURIComponent(context.query.state || '')
28
+ const code = decodeURIComponent(context.query.code || '')
29
+
30
+ const stateInCookie = context.getCookie('state')
31
+
32
+ if (state !== stateInCookie) throw new Error('Authentication failed')
33
+
34
+ const codeVerifier = context.getCookie('code_verifier')
35
+ const provider = context.getCookie('provider')
36
+
37
+ const { signInWith } = await getGraphqlClient(
38
+ wabeContext.wabe.config.port,
39
+ ).request<any>(
40
+ gql`
41
+ mutation signInWith(
42
+ $authorizationCode: String!
43
+ $codeVerifier: String!
44
+ ) {
45
+ signInWith(
46
+ input: {
47
+ authentication: {
48
+ ${provider}: {
49
+ authorizationCode: $authorizationCode
50
+ codeVerifier: $codeVerifier
51
+ }
52
+ }
53
+ }
54
+ ){
55
+ accessToken
56
+ refreshToken
57
+ }
58
+ }
59
+ `,
60
+ {
61
+ authorizationCode: code,
62
+ codeVerifier,
63
+ },
64
+ )
65
+
66
+ const { accessToken, refreshToken } = signInWith
67
+
68
+ const isCookieSession =
69
+ !!wabeContext.wabe.config.authentication?.session?.cookieSession
70
+
71
+ context.res.setCookie('accessToken', accessToken, {
72
+ // If cookie session we put httpOnly to true, otherwise the front will need to get it
73
+ // So we keep it to false
74
+ httpOnly: isCookieSession,
75
+ path: '/',
76
+ maxAge:
77
+ (wabeContext.wabe.config.authentication?.session
78
+ ?.accessTokenExpiresInMs || 60 * 15 * 1000) / 1000, // 15 minutes in seconds
79
+ sameSite: 'None',
80
+ secure: true,
81
+ })
82
+
83
+ context.res.setCookie('refreshToken', refreshToken, {
84
+ // If cookie session we put httpOnly to true, otherwise the front will need to get it
85
+ // So we keep it to false
86
+ httpOnly: isCookieSession,
87
+ path: '/',
88
+ maxAge:
89
+ (wabeContext.wabe.config.authentication?.session
90
+ ?.accessTokenExpiresInMs || 60 * 15 * 1000) / 1000, // 15 minutes in seconds
91
+ sameSite: 'None',
92
+ secure: true,
93
+ })
94
+
95
+ context.redirect(
96
+ wabeContext.wabe.config.authentication?.successRedirectPath || '/',
97
+ )
98
+ } catch {
99
+ context.redirect(
100
+ wabeContext.wabe.config.authentication?.failureRedirectPath || '/',
101
+ )
102
+ }
103
+ }
104
+
105
+ export const authHandler = (
106
+ context: Context,
107
+ wabeContext: WabeContext<any>,
108
+ provider: ProviderEnum,
109
+ ) => {
110
+ if (!wabeContext.wabe.config) throw new Error('Wabe config not found')
111
+
112
+ context.res.setCookie('provider', provider, {
113
+ httpOnly: true,
114
+ path: '/',
115
+ maxAge: 60 * 5, // 5 minutes
116
+ secure: true,
117
+ })
118
+
119
+ switch (provider) {
120
+ case ProviderEnum.google: {
121
+ const googleOauth = new Google(wabeContext.wabe.config)
122
+
123
+ const state = generateRandomValues()
124
+ const codeVerifier = generateRandomValues()
125
+
126
+ context.res.setCookie('code_verifier', codeVerifier, {
127
+ httpOnly: true,
128
+ path: '/',
129
+ maxAge: 60 * 5, // 5 minutes
130
+ secure: true,
131
+ })
132
+
133
+ context.res.setCookie('state', state, {
134
+ httpOnly: true,
135
+ path: '/',
136
+ maxAge: 60 * 5, // 5 minutes
137
+ secure: true,
138
+ })
139
+
140
+ const authorizationURL = googleOauth.createAuthorizationURL(
141
+ state,
142
+ codeVerifier,
143
+ {
144
+ scopes: ['email'],
145
+ },
146
+ )
147
+
148
+ context.redirect(authorizationURL.toString())
149
+
150
+ break
151
+ }
152
+ case ProviderEnum.github: {
153
+ const githubOauth = new GitHub(wabeContext.wabe.config)
154
+
155
+ const state = generateRandomValues()
156
+ const codeVerifier = generateRandomValues()
157
+
158
+ context.res.setCookie('code_verifier', codeVerifier, {
159
+ httpOnly: true,
160
+ path: '/',
161
+ maxAge: 60 * 5, // 5 minutes
162
+ secure: true,
163
+ })
164
+
165
+ context.res.setCookie('state', state, {
166
+ httpOnly: true,
167
+ path: '/',
168
+ maxAge: 60 * 5, // 5 minutes
169
+ secure: true,
170
+ })
171
+
172
+ const authorizationURL = githubOauth.createAuthorizationURL(
173
+ state,
174
+ codeVerifier,
175
+ {
176
+ scopes: ['email'],
177
+ },
178
+ )
179
+
180
+ context.redirect(authorizationURL.toString())
181
+
182
+ break
183
+ }
184
+ default:
185
+ break
186
+ }
187
+ }
@@ -0,0 +1,40 @@
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)
21
+ throw new Error('Authentication failed, provider not found')
22
+
23
+ // TODO: Maybe check if the value is in the enum
24
+ return authHandler(context, context.wabe, provider as ProviderEnum)
25
+ },
26
+ },
27
+ {
28
+ method: 'GET',
29
+ path: '/auth/oauth/callback',
30
+ handler: (context) => oauthHandlerCallback(context, context.wabe),
31
+ },
32
+ {
33
+ method: 'GET',
34
+ path: '/bucket/:filename',
35
+ handler: uploadDirectory({ directory: devDirectory }),
36
+ },
37
+ ]
38
+
39
+ return routes
40
+ }
@@ -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
+ })