@getsoda/core 0.1.1 → 0.2.1

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 (120) hide show
  1. package/README.md +58 -0
  2. package/dist/app/app.d.ts +17 -1
  3. package/dist/app/app.d.ts.map +1 -1
  4. package/dist/app/app.js +46 -10
  5. package/dist/app/app.js.map +1 -1
  6. package/dist/di/container.d.ts +7 -0
  7. package/dist/di/container.d.ts.map +1 -1
  8. package/dist/di/container.js +24 -1
  9. package/dist/di/container.js.map +1 -1
  10. package/dist/di/graph.d.ts +2 -2
  11. package/dist/di/graph.d.ts.map +1 -1
  12. package/dist/di/graph.js +20 -10
  13. package/dist/di/graph.js.map +1 -1
  14. package/dist/http/arguments.d.ts +20 -2
  15. package/dist/http/arguments.d.ts.map +1 -1
  16. package/dist/http/arguments.js +29 -0
  17. package/dist/http/arguments.js.map +1 -1
  18. package/dist/http/body.d.ts +28 -0
  19. package/dist/http/body.d.ts.map +1 -1
  20. package/dist/http/body.js +96 -1
  21. package/dist/http/body.js.map +1 -1
  22. package/dist/http/context-key.d.ts +33 -0
  23. package/dist/http/context-key.d.ts.map +1 -0
  24. package/dist/http/context-key.js +37 -0
  25. package/dist/http/context-key.js.map +1 -0
  26. package/dist/http/context.d.ts +37 -3
  27. package/dist/http/context.d.ts.map +1 -1
  28. package/dist/http/context.js +63 -5
  29. package/dist/http/context.js.map +1 -1
  30. package/dist/http/cookies.d.ts +49 -0
  31. package/dist/http/cookies.d.ts.map +1 -0
  32. package/dist/http/cookies.js +77 -0
  33. package/dist/http/cookies.js.map +1 -0
  34. package/dist/http/cors.d.ts +41 -0
  35. package/dist/http/cors.d.ts.map +1 -0
  36. package/dist/http/cors.js +97 -0
  37. package/dist/http/cors.js.map +1 -0
  38. package/dist/http/index.d.ts +8 -2
  39. package/dist/http/index.d.ts.map +1 -1
  40. package/dist/http/index.js +4 -1
  41. package/dist/http/index.js.map +1 -1
  42. package/dist/http/middleware.d.ts +56 -0
  43. package/dist/http/middleware.d.ts.map +1 -0
  44. package/dist/http/middleware.js +83 -0
  45. package/dist/http/middleware.js.map +1 -0
  46. package/dist/http/parsers.d.ts +3 -0
  47. package/dist/http/parsers.d.ts.map +1 -1
  48. package/dist/http/parsers.js +2 -1
  49. package/dist/http/parsers.js.map +1 -1
  50. package/dist/http/routes.d.ts +10 -1
  51. package/dist/http/routes.d.ts.map +1 -1
  52. package/dist/http/routes.js +4 -1
  53. package/dist/http/routes.js.map +1 -1
  54. package/dist/index.d.ts +1 -0
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +1 -0
  57. package/dist/index.js.map +1 -1
  58. package/dist/jwt/algorithms.d.ts +19 -0
  59. package/dist/jwt/algorithms.d.ts.map +1 -0
  60. package/dist/jwt/algorithms.js +60 -0
  61. package/dist/jwt/algorithms.js.map +1 -0
  62. package/dist/jwt/errors.d.ts +24 -0
  63. package/dist/jwt/errors.d.ts.map +1 -0
  64. package/dist/jwt/errors.js +14 -0
  65. package/dist/jwt/errors.js.map +1 -0
  66. package/dist/jwt/guard.d.ts +30 -0
  67. package/dist/jwt/guard.d.ts.map +1 -0
  68. package/dist/jwt/guard.js +61 -0
  69. package/dist/jwt/guard.js.map +1 -0
  70. package/dist/jwt/index.d.ts +14 -0
  71. package/dist/jwt/index.d.ts.map +1 -0
  72. package/dist/jwt/index.js +10 -0
  73. package/dist/jwt/index.js.map +1 -0
  74. package/dist/jwt/module.d.ts +36 -0
  75. package/dist/jwt/module.d.ts.map +1 -0
  76. package/dist/jwt/module.js +38 -0
  77. package/dist/jwt/module.js.map +1 -0
  78. package/dist/jwt/service.d.ts +66 -0
  79. package/dist/jwt/service.d.ts.map +1 -0
  80. package/dist/jwt/service.js +138 -0
  81. package/dist/jwt/service.js.map +1 -0
  82. package/dist/logger/console-logger.d.ts +36 -0
  83. package/dist/logger/console-logger.d.ts.map +1 -0
  84. package/dist/logger/console-logger.js +117 -0
  85. package/dist/logger/console-logger.js.map +1 -0
  86. package/dist/logger/index.d.ts +9 -0
  87. package/dist/logger/index.d.ts.map +1 -0
  88. package/dist/logger/index.js +7 -0
  89. package/dist/logger/index.js.map +1 -0
  90. package/dist/logger/logger.d.ts +55 -0
  91. package/dist/logger/logger.d.ts.map +1 -0
  92. package/dist/logger/logger.js +66 -0
  93. package/dist/logger/logger.js.map +1 -0
  94. package/dist/routing/types.d.ts +1 -1
  95. package/dist/routing/types.d.ts.map +1 -1
  96. package/package.json +6 -12
  97. package/src/app/app.ts +77 -13
  98. package/src/di/container.ts +38 -2
  99. package/src/di/graph.ts +26 -11
  100. package/src/http/arguments.ts +42 -2
  101. package/src/http/body.ts +142 -1
  102. package/src/http/context-key.ts +35 -0
  103. package/src/http/context.ts +94 -8
  104. package/src/http/cookies.ts +96 -0
  105. package/src/http/cors.ts +143 -0
  106. package/src/http/index.ts +8 -2
  107. package/src/http/middleware.ts +124 -0
  108. package/src/http/parsers.ts +2 -1
  109. package/src/http/routes.ts +17 -2
  110. package/src/index.ts +1 -0
  111. package/src/jwt/algorithms.ts +113 -0
  112. package/src/jwt/errors.ts +30 -0
  113. package/src/jwt/guard.ts +74 -0
  114. package/src/jwt/index.ts +14 -0
  115. package/src/jwt/module.ts +58 -0
  116. package/src/jwt/service.ts +216 -0
  117. package/src/logger/console-logger.ts +163 -0
  118. package/src/logger/index.ts +9 -0
  119. package/src/logger/logger.ts +96 -0
  120. package/src/routing/types.ts +1 -1
@@ -0,0 +1,124 @@
1
+ /**
2
+ * @module
3
+ * Application middleware: runs around every request, before routing.
4
+ *
5
+ * Unlike guards and interceptors, which belong to a route, middleware sees
6
+ * every request: unknown paths (404), wrong methods (405), CORS preflights.
7
+ * That is where CORS, request ids, access logs or compression belong.
8
+ *
9
+ * ```ts
10
+ * const requestId: MiddlewareFunction = async (req, next) => {
11
+ * const response = await next()
12
+ *
13
+ * response.headers.set('x-request-id', crypto.randomUUID())
14
+ *
15
+ * return response
16
+ * }
17
+ *
18
+ * await createApp(AppModule, { middleware: [cors({ origin: 'https://app.example.com' }), requestId] })
19
+ * ```
20
+ *
21
+ * As guards: a class is created once by the container (root module) and gets
22
+ * lifecycle hooks, a function runs where `inject()` works, an instance is used as is.
23
+ */
24
+
25
+ import type { Constructor } from '../di/index.ts'
26
+ import type { NativeHandler } from '../routing/index.ts'
27
+
28
+ /** Continues with the next middleware, then the route. */
29
+ export type Next = () => Promise<Response>
30
+
31
+ /** Class middleware. Like Nest's `NestMiddleware`, for the Fetch API. */
32
+ export interface NestMiddleware {
33
+ use(req: Request, next: Next): Response | Promise<Response>
34
+ }
35
+
36
+ /** Function middleware; `inject()` works inside it, before the first `await`. */
37
+ export type MiddlewareFunction = (req: Request, next: Next) => Response | Promise<Response>
38
+
39
+ /** What `middleware` accepts. */
40
+ export type Middleware = Constructor<NestMiddleware> | NestMiddleware | MiddlewareFunction
41
+
42
+ type Step = (req: Request, next: Next) => Response | Promise<Response>
43
+
44
+ /** Runs a step, turning a synchronous throw into a rejection. */
45
+ const settle = (step: () => Response | Promise<Response>): Promise<Response> => {
46
+ try {
47
+ return Promise.resolve(step())
48
+ } catch (error) {
49
+ return Promise.reject(error)
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Turns middleware declarations into steps. Classes are created now, inside
55
+ * `runInContext`, and passed to `onCreate` for their lifecycle hooks.
56
+ */
57
+ export const resolveMiddleware = (
58
+ middleware: readonly Middleware[],
59
+ runInContext: <R>(fn: () => R) => R,
60
+ onCreate: (instance: unknown) => void
61
+ ): Step[] => {
62
+ const steps: Step[] = []
63
+
64
+ for (const entry of middleware) {
65
+ if (typeof entry === 'function' && typeof (entry.prototype as Record<string, unknown> | undefined)?.use === 'function') {
66
+ const instance = runInContext(() => new (entry as Constructor<NestMiddleware>)())
67
+
68
+ onCreate(instance)
69
+ steps.push((req, next) => instance.use(req, next))
70
+ } else if (typeof entry === 'function') {
71
+ const fn = entry as MiddlewareFunction
72
+
73
+ steps.push((req, next) => runInContext(() => fn(req, next)))
74
+ } else if (typeof entry === 'object' && entry !== null && typeof entry.use === 'function') {
75
+ steps.push((req, next) => entry.use(req, next))
76
+ } else {
77
+ throw new TypeError(`Invalid middleware: ${String(entry)} is neither a function nor an object with use()`)
78
+ }
79
+ }
80
+
81
+ return steps
82
+ }
83
+
84
+ /**
85
+ * Wraps `handler` with the middleware steps, first one outermost.
86
+ * Errors of middleware go to `onError`, which builds the response.
87
+ */
88
+ export const withMiddleware = <R extends Request>(
89
+ steps: readonly Step[],
90
+ handler: (req: R) => Response | Promise<Response>,
91
+ onError: (error: unknown, req: Request) => Response
92
+ ): ((req: R) => Promise<Response>) => {
93
+ const count = steps.length
94
+
95
+ return (req) => {
96
+ const call = (index: number): Promise<Response> =>
97
+ index === count ? settle(() => handler(req)) : settle(() => steps[index]!(req, () => call(index + 1)))
98
+
99
+ return call(0).catch((error: unknown) => onError(error, req))
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Applies middleware to every native route and to the fallback of a compiled table.
105
+ * Without middleware, the table is returned unchanged: no wrapper on the request path.
106
+ */
107
+ export const applyMiddleware = (
108
+ steps: readonly Step[],
109
+ routes: Record<string, NativeHandler>,
110
+ fetch: (req: Request) => Response | Promise<Response>,
111
+ onError: (error: unknown, req: Request) => Response
112
+ ): { routes: Record<string, NativeHandler>; fetch: (req: Request) => Response | Promise<Response> } => {
113
+ if (steps.length === 0) {
114
+ return { routes, fetch }
115
+ }
116
+
117
+ const wrapped: Record<string, NativeHandler> = {}
118
+
119
+ for (const key of Object.keys(routes)) {
120
+ wrapped[key] = withMiddleware(steps, routes[key]!, onError)
121
+ }
122
+
123
+ return { routes: wrapped, fetch: withMiddleware(steps, fetch, onError) }
124
+ }
@@ -39,7 +39,8 @@ const INTEGER = /^-?\d+$/
39
39
  const NUMBER = /^-?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?$/
40
40
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
41
41
 
42
- const failure = (name: string, expected: string): BadRequestException =>
42
+ /** The `400` of a value that is missing or not what a parser expects. */
43
+ export const failure = (name: string, expected: string): BadRequestException =>
43
44
  new BadRequestException(`Validation failed for "${name}" (${expected} is expected)`)
44
45
 
45
46
  const createParser = <T>(
@@ -11,7 +11,7 @@
11
11
  * ```
12
12
  */
13
13
 
14
- import type { Container, ControllerEntry, ModuleClass } from '../di/index.ts'
14
+ import type { Constructor, Container, ControllerEntry, ModuleClass } from '../di/index.ts'
15
15
  import type { RouteDefinition } from '../routing/index.ts'
16
16
  import { parsePath } from '../routing/path.ts'
17
17
  import { isThenable } from '../utils/promise.ts'
@@ -156,6 +156,7 @@ const resolveAll = <T, S>(
156
156
  */
157
157
  class RoutesBuilder {
158
158
  readonly routes: RouteDefinition[] = []
159
+ readonly controllers: MappedController[] = []
159
160
  /** Guards, interceptors and filters created from classes, in creation order. */
160
161
  readonly enhancers: unknown[] = []
161
162
  readonly #options: ControllerRoutesOptions
@@ -184,10 +185,14 @@ class RoutesBuilder {
184
185
 
185
186
  const basePath = joinPaths(this.#options.prefix ?? '', controllerPath)
186
187
 
188
+ const start = this.routes.length
189
+
187
190
  // The most derived function: an override without route decorators has no metadata.
188
191
  for (const name of methodNames(entry.instance as object)) {
189
192
  this.#addMethod(entry, basePath, name)
190
193
  }
194
+
195
+ this.controllers.push({ controller: entry.controller, path: basePath, routes: this.routes.slice(start) })
191
196
  }
192
197
 
193
198
  #resolverOf(entry: ControllerEntry): EnhancerResolver {
@@ -251,12 +256,22 @@ class RoutesBuilder {
251
256
  }
252
257
  }
253
258
 
259
+ /** A controller and the routes made from it, as `createApp` logs them at startup. */
260
+ export interface MappedController {
261
+ readonly controller: Constructor<unknown>
262
+ /** Path of the controller, with the global prefix. */
263
+ readonly path: string
264
+ readonly routes: readonly RouteDefinition[]
265
+ }
266
+
254
267
  /**
255
268
  * Routes of every controller, and the guards, interceptors and filters created
256
269
  * for them from classes (whose lifecycle hooks the application runs).
257
270
  */
258
271
  export interface BuiltRoutes {
259
272
  readonly routes: RouteDefinition[]
273
+ /** In module order. */
274
+ readonly controllers: readonly MappedController[]
260
275
  /** In creation order. */
261
276
  readonly enhancers: readonly unknown[]
262
277
  }
@@ -271,7 +286,7 @@ export const buildRoutes = (container: Container, options: ControllerRoutesOptio
271
286
  builder.addController(entry)
272
287
  }
273
288
 
274
- return { routes: builder.routes, enhancers: builder.enhancers }
289
+ return { routes: builder.routes, controllers: builder.controllers, enhancers: builder.enhancers }
275
290
  }
276
291
 
277
292
  /**
package/src/index.ts CHANGED
@@ -34,4 +34,5 @@
34
34
  export * from './app/index.ts'
35
35
  export * from './di/index.ts'
36
36
  export * from './http/index.ts'
37
+ export * from './logger/index.ts'
37
38
  export * from './routing/index.ts'
@@ -0,0 +1,113 @@
1
+ /**
2
+ * @module
3
+ * JWS algorithms (RFC 7518) on WebCrypto, and the keys they take.
4
+ */
5
+
6
+ import type { webcrypto } from 'node:crypto'
7
+
8
+ /** Algorithms a token can be signed with. `none` is not one of them. */
9
+ export type JwtAlgorithm =
10
+ | 'HS256'
11
+ | 'HS384'
12
+ | 'HS512'
13
+ | 'RS256'
14
+ | 'RS384'
15
+ | 'RS512'
16
+ | 'PS256'
17
+ | 'PS384'
18
+ | 'PS512'
19
+ | 'ES256'
20
+ | 'ES384'
21
+ | 'ES512'
22
+ | 'EdDSA'
23
+
24
+ /**
25
+ * A key: a `CryptoKey`, a JWK, a PEM string (`PRIVATE KEY` in PKCS #8 or
26
+ * `PUBLIC KEY` in SPKI), or the secret of an HMAC algorithm.
27
+ */
28
+ export type JwtKey = CryptoKey | webcrypto.JsonWebKey | string
29
+
30
+ type KeyParams = webcrypto.HmacImportParams | webcrypto.RsaHashedImportParams | webcrypto.EcKeyImportParams | webcrypto.Algorithm
31
+ type SignParams = webcrypto.Algorithm | webcrypto.EcdsaParams | webcrypto.RsaPssParams
32
+
33
+ /** Import parameters, and the signing parameters where they differ. */
34
+ const ALGORITHMS: Readonly<Record<JwtAlgorithm, { readonly key: KeyParams; readonly sign: SignParams }>> = {
35
+ HS256: hmac('SHA-256'),
36
+ HS384: hmac('SHA-384'),
37
+ HS512: hmac('SHA-512'),
38
+ RS256: rsa('RSASSA-PKCS1-v1_5', 'SHA-256'),
39
+ RS384: rsa('RSASSA-PKCS1-v1_5', 'SHA-384'),
40
+ RS512: rsa('RSASSA-PKCS1-v1_5', 'SHA-512'),
41
+ PS256: pss('SHA-256', 32),
42
+ PS384: pss('SHA-384', 48),
43
+ PS512: pss('SHA-512', 64),
44
+ ES256: ecdsa('P-256', 'SHA-256'),
45
+ ES384: ecdsa('P-384', 'SHA-384'),
46
+ ES512: ecdsa('P-521', 'SHA-512'),
47
+ EdDSA: { key: { name: 'Ed25519' }, sign: { name: 'Ed25519' } },
48
+ }
49
+
50
+ function hmac(hash: string) {
51
+ const params = { name: 'HMAC', hash }
52
+
53
+ return { key: params, sign: params }
54
+ }
55
+
56
+ function rsa(name: string, hash: string) {
57
+ return { key: { name, hash }, sign: { name } }
58
+ }
59
+
60
+ function pss(hash: string, saltLength: number) {
61
+ return { key: { name: 'RSA-PSS', hash }, sign: { name: 'RSA-PSS', saltLength } }
62
+ }
63
+
64
+ function ecdsa(namedCurve: string, hash: string) {
65
+ return { key: { name: 'ECDSA', namedCurve }, sign: { name: 'ECDSA', hash } }
66
+ }
67
+
68
+ export const isJwtAlgorithm = (value: unknown): value is JwtAlgorithm => typeof value === 'string' && Object.hasOwn(ALGORITHMS, value)
69
+
70
+ export const isHmac = (algorithm: JwtAlgorithm): boolean => algorithm.startsWith('HS')
71
+
72
+ const pemBody = (pem: string): Uint8Array<ArrayBuffer> =>
73
+ Uint8Array.fromBase64(pem.replace(/-----(?:BEGIN|END) [A-Z ]+-----/g, '').replace(/\s/g, ''))
74
+
75
+ /** Imports `key` once for `usage` with `algorithm`. */
76
+ export const importKey = async (key: JwtKey, algorithm: JwtAlgorithm, usage: 'sign' | 'verify'): Promise<CryptoKey> => {
77
+ const params = ALGORITHMS[algorithm].key
78
+
79
+ if (key instanceof CryptoKey) {
80
+ return key
81
+ }
82
+
83
+ if (typeof key === 'object') {
84
+ return crypto.subtle.importKey('jwk', key, params, false, [usage])
85
+ }
86
+
87
+ if (isHmac(algorithm)) {
88
+ return crypto.subtle.importKey('raw', new TextEncoder().encode(key), params, false, [usage])
89
+ }
90
+
91
+ if (key.includes('-----BEGIN PRIVATE KEY-----') && usage === 'sign') {
92
+ return crypto.subtle.importKey('pkcs8', pemBody(key), params, false, [usage])
93
+ }
94
+
95
+ if (key.includes('-----BEGIN PUBLIC KEY-----') && usage === 'verify') {
96
+ return crypto.subtle.importKey('spki', pemBody(key), params, false, [usage])
97
+ }
98
+
99
+ throw new TypeError(
100
+ `A ${algorithm} key must be a CryptoKey, a JWK or a PEM string: ` +
101
+ (usage === 'sign' ? 'a PKCS #8 "PRIVATE KEY" to sign' : 'an SPKI "PUBLIC KEY" to verify')
102
+ )
103
+ }
104
+
105
+ export const signBytes = async (algorithm: JwtAlgorithm, key: CryptoKey, data: Uint8Array<ArrayBuffer>): Promise<Uint8Array<ArrayBuffer>> =>
106
+ new Uint8Array(await crypto.subtle.sign(ALGORITHMS[algorithm].sign, key, data))
107
+
108
+ export const verifyBytes = (
109
+ algorithm: JwtAlgorithm,
110
+ key: CryptoKey,
111
+ signature: Uint8Array<ArrayBuffer>,
112
+ data: Uint8Array<ArrayBuffer>
113
+ ): Promise<boolean> => crypto.subtle.verify(ALGORITHMS[algorithm].sign, key, signature, data)
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @module
3
+ * Why a token was refused.
4
+ */
5
+
6
+ export type JwtErrorCode =
7
+ /** Not three base64url parts holding JSON objects. */
8
+ | 'malformed'
9
+ /** Signed with another algorithm than the one configured, or with critical extensions. */
10
+ | 'algorithm'
11
+ /** The signature does not match. */
12
+ | 'signature'
13
+ /** `exp` is past. */
14
+ | 'expired'
15
+ /** `nbf` is still to come. */
16
+ | 'not-active'
17
+ /** A registered claim has the wrong type, or `iss` or `aud` is not the expected one. */
18
+ | 'claims'
19
+
20
+ /** A token that `JwtService.verify()` refused. */
21
+ export class JwtError extends Error {
22
+ override readonly name = 'JwtError'
23
+
24
+ constructor(
25
+ readonly code: JwtErrorCode,
26
+ message: string
27
+ ) {
28
+ super(message)
29
+ }
30
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * @module
3
+ * `JwtGuard`: lets in requests carrying a valid token, the way
4
+ * `AuthGuard('jwt')` of `@nestjs/passport` does.
5
+ */
6
+
7
+ import { inject } from '../di/index.ts'
8
+ import type { Context } from '../http/context.ts'
9
+ import { ContextKey } from '../http/context-key.ts'
10
+ import type { CanActivate } from '../http/enhancers.ts'
11
+ import { UnauthorizedException } from '../http/exceptions.ts'
12
+ import { JwtError } from './errors.ts'
13
+ import { JWT_OPTIONS } from './module.ts'
14
+ import { JwtService } from './service.ts'
15
+ import type { JwtPayload } from './service.ts'
16
+
17
+ /** Payload of the token `JwtGuard` accepted: `ctx.get(JWT_PAYLOAD)` or `value(JWT_PAYLOAD)`. */
18
+ export const JWT_PAYLOAD = new ContextKey<JwtPayload>('jwtPayload')
19
+
20
+ const BEARER = /^Bearer +(\S+) *$/i
21
+
22
+ /** A 401 with the `WWW-Authenticate` challenge of RFC 6750. */
23
+ const unauthorized = (message: string, challenge: string, cause?: unknown): UnauthorizedException =>
24
+ new UnauthorizedException(message, { cause, headers: { 'www-authenticate': challenge } })
25
+
26
+ /**
27
+ * Accepts a request whose token `JwtService` verifies, read from
28
+ * `Authorization: Bearer <token>` or, without that header, from the cookie
29
+ * named by the `cookie` option of `JwtModule`. The payload goes to
30
+ * {@link JWT_PAYLOAD}; anything else is a `401`.
31
+ *
32
+ * ```ts
33
+ * @Get('me', [value(JWT_PAYLOAD)])
34
+ * @UseGuards(JwtGuard)
35
+ * me(payload: JwtPayload) {}
36
+ * ```
37
+ *
38
+ * Needs `JwtModule` imported in the controller's module, or registered as global.
39
+ */
40
+ export class JwtGuard implements CanActivate {
41
+ readonly #jwt = inject(JwtService)
42
+ readonly #cookie = inject(JWT_OPTIONS).cookie
43
+
44
+ async canActivate(ctx: Context): Promise<boolean> {
45
+ const header = ctx.header('authorization')
46
+ let token: string | undefined
47
+
48
+ if (header !== undefined) {
49
+ token = BEARER.exec(header)?.[1]
50
+
51
+ if (token === undefined) {
52
+ throw unauthorized('Authorization is not a bearer token', 'Bearer error="invalid_request"')
53
+ }
54
+ } else if (this.#cookie !== undefined) {
55
+ token = ctx.cookie(this.#cookie)
56
+ }
57
+
58
+ if (token === undefined) {
59
+ throw unauthorized('Missing token', 'Bearer')
60
+ }
61
+
62
+ try {
63
+ ctx.set(JWT_PAYLOAD, await this.#jwt.verify(token))
64
+ } catch (error) {
65
+ if (error instanceof JwtError) {
66
+ throw unauthorized(error.message, 'Bearer error="invalid_token"', error)
67
+ }
68
+
69
+ throw error
70
+ }
71
+
72
+ return true
73
+ }
74
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @module
3
+ * JSON Web Tokens: `JwtService` to sign and verify them, `JwtModule` to
4
+ * configure it, `JwtGuard` to protect routes with them.
5
+ */
6
+
7
+ export type { JwtAlgorithm, JwtKey } from './algorithms.ts'
8
+ export { JwtError } from './errors.ts'
9
+ export type { JwtErrorCode } from './errors.ts'
10
+ export { JWT_PAYLOAD, JwtGuard } from './guard.ts'
11
+ export { JWT_OPTIONS, JwtModule } from './module.ts'
12
+ export type { JwtModuleAsyncOptions, JwtModuleOptions } from './module.ts'
13
+ export { JwtService } from './service.ts'
14
+ export type { JwtOptions, JwtPayload, JwtSignOptions } from './service.ts'
@@ -0,0 +1,58 @@
1
+ /**
2
+ * @module
3
+ * `JwtModule`: provides a configured `JwtService`, like `@nestjs/jwt`.
4
+ */
5
+
6
+ import { InjectionToken, Module, provide } from '../di/index.ts'
7
+ import type { Dependency, DependencyValues, DynamicModule, ModuleImport } from '../di/index.ts'
8
+ import { JwtService } from './service.ts'
9
+ import type { JwtOptions } from './service.ts'
10
+
11
+ export interface JwtModuleOptions extends JwtOptions {
12
+ /** Cookie `JwtGuard` reads the token from when the request has no `Authorization` header. */
13
+ readonly cookie?: string
14
+ }
15
+
16
+ export interface JwtModuleAsyncOptions<D extends readonly Dependency[]> {
17
+ /** Modules providing what `inject` names. */
18
+ readonly imports?: readonly ModuleImport[]
19
+ readonly inject?: D
20
+ readonly useFactory: (...args: DependencyValues<D>) => JwtModuleOptions | Promise<JwtModuleOptions>
21
+ /** Makes `JwtService` visible in every module. */
22
+ readonly global?: boolean
23
+ }
24
+
25
+ /** Options the module was registered with. */
26
+ export const JWT_OPTIONS = new InjectionToken<JwtModuleOptions>('JWT_OPTIONS')
27
+
28
+ const service = () => provide(JwtService, { inject: [JWT_OPTIONS], useFactory: (options) => new JwtService(options) })
29
+
30
+ /**
31
+ * ```ts
32
+ * @Module({ imports: [JwtModule.register({ secret: process.env.JWT_SECRET!, expiresIn: 900, global: true })] })
33
+ * class AppModule {}
34
+ *
35
+ * JwtModule.registerAsync({ inject: [ConfigService], useFactory: (config) => ({ secret: config.get('JWT_SECRET') }) })
36
+ * ```
37
+ */
38
+ @Module({})
39
+ export class JwtModule {
40
+ static register(options: JwtModuleOptions & { readonly global?: boolean }): DynamicModule {
41
+ return {
42
+ module: JwtModule,
43
+ global: options.global ?? false,
44
+ providers: [provide(JWT_OPTIONS, { useValue: options }), service()],
45
+ exports: [JwtService, JWT_OPTIONS],
46
+ }
47
+ }
48
+
49
+ static registerAsync<const D extends readonly Dependency[] = []>(options: JwtModuleAsyncOptions<D>): DynamicModule {
50
+ return {
51
+ module: JwtModule,
52
+ global: options.global ?? false,
53
+ imports: options.imports ?? [],
54
+ providers: [provide<JwtModuleOptions, D>(JWT_OPTIONS, { inject: options.inject, useFactory: options.useFactory }), service()],
55
+ exports: [JwtService, JWT_OPTIONS],
56
+ }
57
+ }
58
+ }