@pikku/core 0.9.12-next.0 → 0.10.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 (85) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/errors/errors.d.ts +2 -0
  3. package/dist/errors/errors.js +6 -0
  4. package/dist/function/function-runner.d.ts +5 -4
  5. package/dist/function/function-runner.js +10 -4
  6. package/dist/function/functions.types.d.ts +67 -1
  7. package/dist/function/functions.types.js +43 -1
  8. package/dist/index.d.ts +1 -7
  9. package/dist/index.js +1 -7
  10. package/dist/middleware/auth-apikey.d.ts +19 -11
  11. package/dist/middleware/auth-apikey.js +33 -33
  12. package/dist/middleware/auth-bearer.d.ts +34 -7
  13. package/dist/middleware/auth-bearer.js +53 -36
  14. package/dist/middleware/auth-cookie.d.ts +30 -12
  15. package/dist/middleware/auth-cookie.js +51 -43
  16. package/dist/middleware/timeout.d.ts +6 -2
  17. package/dist/middleware/timeout.js +7 -9
  18. package/dist/middleware-runner.d.ts +0 -18
  19. package/dist/middleware-runner.js +0 -21
  20. package/dist/permissions.d.ts +82 -20
  21. package/dist/permissions.js +175 -62
  22. package/dist/pikku-state.d.ts +22 -2
  23. package/dist/pikku-state.js +7 -1
  24. package/dist/types/core.types.d.ts +58 -1
  25. package/dist/types/core.types.js +23 -1
  26. package/dist/utils.d.ts +1 -1
  27. package/dist/wirings/channel/channel-handler.js +6 -5
  28. package/dist/wirings/channel/channel-runner.d.ts +3 -1
  29. package/dist/wirings/channel/channel-runner.js +4 -21
  30. package/dist/wirings/channel/channel.types.d.ts +12 -13
  31. package/dist/wirings/cli/channel/cli-channel-runner.js +5 -2
  32. package/dist/wirings/cli/cli-runner.d.ts +10 -0
  33. package/dist/wirings/cli/cli-runner.js +44 -20
  34. package/dist/wirings/cli/cli.types.d.ts +54 -27
  35. package/dist/wirings/cli/command-parser.js +34 -4
  36. package/dist/wirings/cli/index.d.ts +1 -1
  37. package/dist/wirings/cli/index.js +1 -1
  38. package/dist/wirings/http/http-runner.d.ts +31 -1
  39. package/dist/wirings/http/http-runner.js +36 -1
  40. package/dist/wirings/http/http.types.d.ts +2 -1
  41. package/dist/wirings/http/index.d.ts +1 -1
  42. package/dist/wirings/http/index.js +1 -1
  43. package/dist/wirings/mcp/mcp-endpoint-registry.d.ts +1 -1
  44. package/dist/wirings/mcp/mcp-runner.js +2 -0
  45. package/dist/wirings/mcp/mcp.types.d.ts +35 -14
  46. package/dist/wirings/queue/queue-runner.js +1 -0
  47. package/dist/wirings/scheduler/scheduler-runner.js +1 -0
  48. package/dist/wirings/scheduler/scheduler.types.d.ts +2 -2
  49. package/package.json +6 -5
  50. package/src/errors/errors.ts +6 -0
  51. package/src/factory-functions.test.ts +60 -1
  52. package/src/function/function-runner.test.ts +8 -3
  53. package/src/function/function-runner.ts +18 -5
  54. package/src/function/functions.types.ts +85 -2
  55. package/src/index.ts +1 -21
  56. package/src/middleware/auth-apikey.ts +42 -57
  57. package/src/middleware/auth-bearer.ts +66 -49
  58. package/src/middleware/auth-cookie.ts +63 -82
  59. package/src/middleware/timeout.ts +10 -14
  60. package/src/middleware-runner.ts +0 -25
  61. package/src/permissions.test.ts +26 -27
  62. package/src/permissions.ts +215 -104
  63. package/src/pikku-state.ts +35 -3
  64. package/src/types/core.types.ts +70 -3
  65. package/src/utils.ts +1 -1
  66. package/src/wirings/channel/channel-handler.ts +11 -10
  67. package/src/wirings/channel/channel-runner.ts +18 -27
  68. package/src/wirings/channel/channel.types.ts +24 -15
  69. package/src/wirings/cli/channel/cli-channel-runner.ts +8 -3
  70. package/src/wirings/cli/cli-runner.test.ts +80 -59
  71. package/src/wirings/cli/cli-runner.ts +57 -21
  72. package/src/wirings/cli/cli.types.ts +74 -44
  73. package/src/wirings/cli/command-parser.test.ts +91 -86
  74. package/src/wirings/cli/command-parser.ts +38 -4
  75. package/src/wirings/cli/index.ts +1 -0
  76. package/src/wirings/http/http-runner.ts +41 -1
  77. package/src/wirings/http/http.types.ts +3 -1
  78. package/src/wirings/http/index.ts +7 -1
  79. package/src/wirings/mcp/mcp-endpoint-registry.ts +1 -1
  80. package/src/wirings/mcp/mcp-runner.ts +2 -0
  81. package/src/wirings/mcp/mcp.types.ts +48 -12
  82. package/src/wirings/queue/queue-runner.ts +1 -0
  83. package/src/wirings/scheduler/scheduler-runner.ts +1 -0
  84. package/src/wirings/scheduler/scheduler.types.ts +2 -2
  85. package/tsconfig.tsbuildinfo +1 -1
@@ -21,11 +21,16 @@ const addTestFunction = (funcName: string, funcConfig: any) => {
21
21
  const middleware = funcConfig.tags
22
22
  ? funcConfig.tags.map((tag: string) => ({ type: 'tag' as const, tag }))
23
23
  : undefined
24
+ // Convert tags to permissions metadata
25
+ const permissions = funcConfig.tags
26
+ ? funcConfig.tags.map((tag: string) => ({ type: 'tag' as const, tag }))
27
+ : undefined
24
28
  pikkuState('function', 'meta')[funcName] = {
25
29
  pikkuFuncName: funcName,
26
30
  inputSchemaName: null,
27
31
  outputSchemaName: null,
28
32
  middleware,
33
+ permissions,
29
34
  }
30
35
  }
31
36
 
@@ -143,7 +148,7 @@ describe('runPikkuFunc - Integration Tests', () => {
143
148
  singletonServices: mockSingletonServices,
144
149
  getAllServices: () => mockServices,
145
150
  data: () => ({}),
146
- permissions: wiringPermissions,
151
+ wirePermissions: wiringPermissions,
147
152
  tags: ['wiringTag'],
148
153
  auth: false,
149
154
  interaction: {},
@@ -199,7 +204,7 @@ describe('runPikkuFunc - Integration Tests', () => {
199
204
  singletonServices: mockSingletonServices,
200
205
  getAllServices: () => mockServices,
201
206
  data: () => ({}),
202
- permissions: wiringPermissions,
207
+ wirePermissions: wiringPermissions,
203
208
  auth: false,
204
209
  interaction: {},
205
210
  }),
@@ -411,7 +416,7 @@ describe('runPikkuFunc - Integration Tests', () => {
411
416
  getAllServices: () => mockServices,
412
417
  data: () => ({}),
413
418
  wireMiddleware: [wiringMiddleware],
414
- permissions: wiringPermissions,
419
+ wirePermissions: wiringPermissions,
415
420
  auth: false,
416
421
  interaction: {},
417
422
  }
@@ -10,10 +10,12 @@ import {
10
10
  CoreSingletonServices,
11
11
  PikkuInteraction,
12
12
  MiddlewareMetadata,
13
+ PermissionMetadata,
13
14
  } from '../types/core.types.js'
14
15
  import {
15
16
  CorePermissionGroup,
16
17
  CorePikkuFunctionConfig,
18
+ CorePikkuPermission,
17
19
  } from './functions.types.js'
18
20
  import { UserSessionService } from '../services/user-session-service.js'
19
21
  import { ForbiddenError } from '../errors/errors.js'
@@ -48,9 +50,10 @@ export const runPikkuFunc = async <In = any, Out = any>(
48
50
  data,
49
51
  userSession,
50
52
  auth: wiringAuth,
51
- permissions: wiringPermissions,
52
53
  inheritedMiddleware,
53
54
  wireMiddleware,
55
+ inheritedPermissions,
56
+ wirePermissions,
54
57
  coerceDataFromSchema,
55
58
  tags = [],
56
59
  interaction,
@@ -62,9 +65,10 @@ export const runPikkuFunc = async <In = any, Out = any>(
62
65
  userSession?: UserSessionService<CoreUserSession>
63
66
  data: () => Promise<In> | In
64
67
  auth?: boolean
65
- permissions?: CorePermissionGroup
66
68
  inheritedMiddleware?: MiddlewareMetadata[]
67
69
  wireMiddleware?: CorePikkuMiddleware[]
70
+ inheritedPermissions?: PermissionMetadata[]
71
+ wirePermissions?: CorePermissionGroup | CorePikkuPermission[]
68
72
  coerceDataFromSchema?: boolean
69
73
  tags?: string[]
70
74
  interaction: PikkuInteraction
@@ -79,6 +83,12 @@ export const runPikkuFunc = async <In = any, Out = any>(
79
83
  throw new Error(`Function meta not found: ${funcName}`)
80
84
  }
81
85
 
86
+ // Convert tags to PermissionMetadata and merge with inheritedPermissions
87
+ const mergedInheritedPermissions: PermissionMetadata[] = [
88
+ ...(inheritedPermissions || []),
89
+ ...(tags?.map((tag) => ({ type: 'tag' as const, tag })) || []),
90
+ ]
91
+
82
92
  // Helper function to run permissions and execute the function
83
93
  const executeFunction = async () => {
84
94
  const session = userSession?.get()
@@ -115,15 +125,18 @@ export const runPikkuFunc = async <In = any, Out = any>(
115
125
  }
116
126
 
117
127
  const allServices = await getAllServices(session)
128
+
129
+ // Run permissions check with combined permissions: inheritedPermissions (including tags) → wirePermissions → funcPermissions
118
130
  await runPermissions(wireType, wireId, {
119
- wiringTags: tags,
120
- wiringPermissions,
121
- funcTags: funcConfig.tags,
131
+ wireInheritedPermissions: mergedInheritedPermissions,
132
+ wirePermissions: wirePermissions,
133
+ funcInheritedPermissions: funcMeta.permissions,
122
134
  funcPermissions: funcConfig.permissions,
123
135
  allServices,
124
136
  data: actualData,
125
137
  session,
126
138
  })
139
+
127
140
  return await funcConfig.func(allServices, actualData, session!)
128
141
  }
129
142
 
@@ -12,6 +12,7 @@ import type {
12
12
  *
13
13
  * @template In - The input type.
14
14
  * @template Out - The output type.
15
+ * @template ChannelData - The channel data type.
15
16
  * @template Services - The services type, defaults to `CoreServices`.
16
17
  * @template Session - The session type, defaults to `CoreUserSession`.
17
18
  */
@@ -39,6 +40,7 @@ export type CorePikkuFunction<
39
40
  *
40
41
  * @template In - The input type.
41
42
  * @template Out - The output type.
43
+ * @template ChannelData - The channel data type.
42
44
  * @template Services - The services type, defaults to `CoreServices`.
43
45
  * @template Session - The session type, defaults to `CoreUserSession`.
44
46
  */
@@ -74,17 +76,98 @@ export type CorePikkuPermission<
74
76
  Session extends CoreUserSession = CoreUserSession,
75
77
  > = (services: Services, data: In, session?: Session) => Promise<boolean>
76
78
 
79
+ /**
80
+ * Configuration object for creating a permission with metadata
81
+ *
82
+ * @template In - The input type.
83
+ * @template Services - The services type, defaults to `CoreServices`.
84
+ * @template Session - The session type, defaults to `CoreUserSession`.
85
+ */
86
+ export type CorePikkuPermissionConfig<
87
+ In = any,
88
+ Services extends CoreSingletonServices = CoreServices,
89
+ Session extends CoreUserSession = CoreUserSession,
90
+ > = {
91
+ /** The permission function */
92
+ func: CorePikkuPermission<In, Services, Session>
93
+ /** Optional human-readable name for the permission */
94
+ name?: string
95
+ /** Optional description of what the permission checks */
96
+ description?: string
97
+ }
98
+
77
99
  /**
78
100
  * Factory function for creating permissions with tree-shaking support
101
+ * Supports both direct function and configuration object syntax
102
+ *
103
+ * @example
104
+ * ```typescript
105
+ * // Direct function syntax
106
+ * export const adminPermission = pikkuPermission(
107
+ * async ({ logger }, data, session) => {
108
+ * return session?.role === 'admin'
109
+ * }
110
+ * )
111
+ *
112
+ * // Configuration object syntax with metadata
113
+ * export const adminPermission = pikkuPermission({
114
+ * name: 'Admin Permission',
115
+ * description: 'Checks if user has admin role',
116
+ * func: async ({ logger }, data, session) => {
117
+ * return session?.role === 'admin'
118
+ * }
119
+ * })
120
+ * ```
79
121
  */
80
122
  export const pikkuPermission = <
81
123
  In = any,
82
124
  Services extends CoreSingletonServices = CoreServices,
83
125
  Session extends CoreUserSession = CoreUserSession,
84
126
  >(
85
- permission: CorePikkuPermission<In, Services, Session>
127
+ permission:
128
+ | CorePikkuPermission<In, Services, Session>
129
+ | CorePikkuPermissionConfig<In, Services, Session>
86
130
  ): CorePikkuPermission<In, Services, Session> => {
87
- return permission
131
+ return typeof permission === 'function' ? permission : permission.func
132
+ }
133
+
134
+ /**
135
+ * A factory function that takes input and returns a permission
136
+ * Used when permissions need configuration/input parameters
137
+ *
138
+ * @template In - The input type for the factory.
139
+ * @template Services - The services type, defaults to `CoreServices`.
140
+ * @template Session - The session type, defaults to `CoreUserSession`.
141
+ */
142
+ export type CorePikkuPermissionFactory<
143
+ In = any,
144
+ Services extends CoreSingletonServices = CoreServices,
145
+ Session extends CoreUserSession = CoreUserSession,
146
+ > = (input: In) => CorePikkuPermission<any, Services, Session>
147
+
148
+ /**
149
+ * Factory function for creating permission factories
150
+ * Use this when your permission needs configuration/input parameters
151
+ *
152
+ * @example
153
+ * ```typescript
154
+ * export const requireRole = pikkuPermissionFactory<{ role: string }>(({
155
+ * role
156
+ * }) => {
157
+ * return pikkuPermission(async ({ logger }, data, session) => {
158
+ * if (!session || session.role !== role) {
159
+ * logger.warn(`Permission denied: required role '${role}'`)
160
+ * return false
161
+ * }
162
+ * return true
163
+ * })
164
+ * })
165
+ * ```
166
+ */
167
+ export const pikkuPermissionFactory = <In = any>(
168
+ factory: CorePikkuPermissionFactory<In>
169
+ ): CorePikkuPermissionFactory<In> => {
170
+ return factory
88
171
  }
89
172
 
90
173
  export type CorePermissionGroup<PikkuPermission = CorePikkuPermission<any>> =
package/src/index.ts CHANGED
@@ -24,27 +24,7 @@ export * from './time-utils.js'
24
24
  export { pikkuState } from './pikku-state.js'
25
25
  export {
26
26
  runMiddleware,
27
- registerMiddleware,
28
- getMiddlewareByName,
29
27
  addMiddleware,
28
+ getMiddlewareByName,
30
29
  } from './middleware-runner.js'
31
30
  export { addPermission } from './permissions.js'
32
- export { wireHTTP, addHTTPMiddleware } from './wirings/http/http-runner.js'
33
- export { wireChannel } from './wirings/channel/channel-runner.js'
34
- export { wireScheduler } from './wirings/scheduler/scheduler-runner.js'
35
- export { wireQueueWorker } from './wirings/queue/queue-runner.js'
36
- export {
37
- wireMCPResource,
38
- wireMCPTool,
39
- wireMCPPrompt,
40
- runMCPResource,
41
- runMCPTool,
42
- runMCPPrompt,
43
- getMCPTools,
44
- getMCPResources,
45
- getMCPPrompts,
46
- getMCPResourcesMeta,
47
- getMCPToolsMeta,
48
- getMCPPromptsMeta,
49
- } from './wirings/mcp/mcp-runner.js'
50
- export { wireCLI, runCLICommand } from './wirings/cli/cli-runner.js'
@@ -1,66 +1,51 @@
1
- import {
2
- CoreConfig,
3
- CoreSingletonServices,
4
- CoreUserSession,
5
- CorePikkuMiddleware,
6
- } from '../types/core.types.js'
1
+ import { pikkuMiddleware, pikkuMiddlewareFactory } from '../types/core.types.js'
7
2
 
8
3
  /**
9
- * API key middleware that retrieves a session from the 'x-api-key' header using a provided callback.
4
+ * API key middleware that retrieves a session from the 'x-api-key' header using JWT decoding.
10
5
  *
11
- * @param options.getSessionForAPIKey - A function that returns a session when provided an API key.
6
+ * Extracts API key from either the 'x-api-key' header or 'apiKey' query parameter
7
+ * and decodes it using the JWT service.
8
+ *
9
+ * @param options.source - Where to look for the API key: 'header', 'query', or 'all'
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * import { authAPIKey } from '@pikku/core/middleware'
14
+ *
15
+ * addHTTPMiddleware([
16
+ * authAPIKey({ source: 'header' })
17
+ * ])
18
+ * ```
12
19
  */
13
- export const authAPIKey = <
14
- SingletonServices extends CoreSingletonServices<CoreConfig>,
15
- UserSession extends CoreUserSession,
16
- >({
17
- source,
18
- getSessionForAPIKey,
19
- jwt,
20
- }: {
20
+ export const authAPIKey = pikkuMiddlewareFactory<{
21
21
  source: 'header' | 'query' | 'all'
22
- } & (
23
- | {
24
- getSessionForAPIKey?: undefined
25
- jwt?: true
26
- }
27
- | {
28
- getSessionForAPIKey: (
29
- services: SingletonServices,
30
- apiKey: string
31
- ) => Promise<any>
32
- jwt?: false
33
- }
34
- )) => {
35
- const middleware: CorePikkuMiddleware = async (services, { http }, next) => {
36
- const { userSession: userSessionService, jwt: jwtService } = services as any
37
- if (!http?.request || userSessionService.get()) {
38
- return next()
39
- }
22
+ }>(({ source }) =>
23
+ pikkuMiddleware(
24
+ async (
25
+ { userSession: userSessionService, jwt: jwtService },
26
+ { http },
27
+ next
28
+ ) => {
29
+ if (!http?.request || userSessionService.get()) {
30
+ return next()
31
+ }
40
32
 
41
- let apiKey: string | null = null
42
- if (source === 'header' || source === 'all') {
43
- apiKey = http.request.header('x-api-key') as string | null
44
- }
45
- if (!apiKey && (source === 'query' || source === 'all')) {
46
- apiKey = http.request.query().apiKey as string | null
47
- }
48
- if (apiKey) {
49
- let userSession: UserSession | null = null
50
- if (jwt) {
51
- if (!jwtService) {
52
- throw new Error('JWT service is required for JWT decoding.')
53
- }
54
- userSession = await jwtService.decode(apiKey)
55
- } else {
56
- userSession = await getSessionForAPIKey!(services as any, apiKey)
33
+ let apiKey: string | null = null
34
+ if (source === 'header' || source === 'all') {
35
+ apiKey = http.request.header('x-api-key') as string | null
57
36
  }
58
- if (userSession) {
59
- userSessionService.setInitial(userSession)
37
+ if (!apiKey && (source === 'query' || source === 'all')) {
38
+ apiKey = http.request.query().apiKey as string | null
39
+ }
40
+
41
+ if (apiKey && jwtService) {
42
+ const userSession = await jwtService.decode(apiKey)
43
+ if (userSession) {
44
+ userSessionService.setInitial(userSession)
45
+ }
60
46
  }
61
- }
62
- return next()
63
- }
64
47
 
65
- return middleware
66
- }
48
+ return next()
49
+ }
50
+ )
51
+ )
@@ -1,66 +1,83 @@
1
1
  import { InvalidSessionError } from '../errors/errors.js'
2
2
  import {
3
- CoreConfig,
4
- CoreSingletonServices,
5
3
  CoreUserSession,
6
- CorePikkuMiddleware,
4
+ pikkuMiddleware,
5
+ pikkuMiddlewareFactory,
7
6
  } from '../types/core.types.js'
8
7
 
9
8
  /**
10
- * Extracts the Bearer token from the Authorization header
9
+ * Bearer token middleware that extracts and validates tokens from the Authorization header.
10
+ *
11
+ * Supports two modes:
12
+ * - JWT decoding (default): Decodes bearer tokens using the JWT service
13
+ * - Static token: Provide a `token` object with a value and userSession for simple token validation
14
+ *
15
+ * @param options.token - Optional static token configuration { value: string, userSession: CoreUserSession }
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * import { authBearer } from '@pikku/core/middleware'
20
+ *
21
+ * // JWT mode (default)
22
+ * addHTTPMiddleware('*', [
23
+ * authBearer()
24
+ * ])
25
+ *
26
+ * // Static token mode
27
+ * addHTTPMiddleware('*', [
28
+ * authBearer({
29
+ * token: {
30
+ * value: process.env.API_TOKEN,
31
+ * userSession: { userId: 'system', role: 'admin' }
32
+ * }
33
+ * })
34
+ * ])
35
+ * ```
11
36
  */
12
- export const authBearer = <
13
- SingletonServices extends CoreSingletonServices<CoreConfig>,
14
- UserSession extends CoreUserSession,
15
- >({
16
- token,
17
- jwt,
18
- getSession,
19
- }: {
37
+ export const authBearer = pikkuMiddlewareFactory<{
20
38
  token?: {
21
39
  value: string
22
- userSession: UserSession
40
+ userSession: CoreUserSession
23
41
  }
24
- jwt?: boolean
25
- getSession?: (
26
- services: SingletonServices,
27
- token: string
28
- ) => Promise<UserSession> | UserSession
29
- } = {}): CorePikkuMiddleware => {
30
- const middleware: CorePikkuMiddleware = async (services, { http }, next) => {
31
- const { userSession: userSessionService, jwt: jwtService } = services as any
32
- // Skip if session already exists.
33
- if (!http?.request || userSessionService.get()) {
34
- return next()
35
- }
36
-
37
- const authHeader =
38
- http.request.header('authorization') ||
39
- http.request.header('Authorization')
40
- if (authHeader) {
41
- const [scheme, bearerToken] = authHeader.split(' ')
42
- if (scheme !== 'Bearer' || !token || !bearerToken) {
43
- throw new InvalidSessionError()
42
+ }>(({ token } = {}) =>
43
+ pikkuMiddleware(
44
+ async (
45
+ { userSession: userSessionService, jwt: jwtService },
46
+ { http },
47
+ next
48
+ ) => {
49
+ // Skip if session already exists.
50
+ if (!http?.request || userSessionService.get()) {
51
+ return next()
44
52
  }
45
- let userSession: UserSession | null = null
46
- if (jwt) {
47
- if (!jwtService) {
48
- throw new Error('JWT service is required for JWT decoding.')
53
+
54
+ const authHeader =
55
+ http.request.header('authorization') ||
56
+ http.request.header('Authorization')
57
+
58
+ if (authHeader) {
59
+ const [scheme, bearerToken] = authHeader.split(' ')
60
+ if (scheme !== 'Bearer' || !bearerToken) {
61
+ throw new InvalidSessionError()
49
62
  }
50
- userSession = await jwtService.decode(bearerToken)
51
- } else if (token) {
52
- if (bearerToken === token.value) {
63
+
64
+ let userSession: CoreUserSession | null = null
65
+
66
+ // If static token provided, validate against it
67
+ if (token && bearerToken === token.value) {
53
68
  userSession = token.userSession
54
69
  }
55
- } else if (getSession) {
56
- userSession = await getSession(services as any, token)
57
- }
70
+ // Otherwise, default to JWT decoding
71
+ else if (jwtService && !token) {
72
+ userSession = await jwtService.decode(bearerToken)
73
+ }
58
74
 
59
- if (userSession) {
60
- userSessionService.setInitial(userSession)
75
+ if (userSession) {
76
+ userSessionService.setInitial(userSession)
77
+ }
61
78
  }
79
+
80
+ return next()
62
81
  }
63
- return next()
64
- }
65
- return middleware
66
- }
82
+ )
83
+ )
@@ -1,103 +1,84 @@
1
1
  import { SerializeOptions } from 'cookie'
2
- import {
3
- CoreConfig,
4
- CoreSingletonServices,
5
- CoreUserSession,
6
- CorePikkuMiddleware,
7
- } from '../types/core.types.js'
2
+ import { pikkuMiddleware, pikkuMiddlewareFactory } from '../types/core.types.js'
8
3
  import {
9
4
  getRelativeTimeOffsetFromNow,
10
5
  RelativeTimeInput,
11
6
  } from '../time-utils.js'
12
7
 
13
8
  /**
14
- * Cookie middleware that extracts a session from cookies.
9
+ * Cookie-based session middleware that uses JWT for encoding/decoding session data.
10
+ *
11
+ * Reads session from a cookie on incoming requests and automatically updates the cookie
12
+ * when the session changes (e.g., after login).
13
+ *
14
+ * @param options.name - Cookie name
15
+ * @param options.expiresIn - Cookie expiration time (e.g., { value: 30, unit: 'day' })
16
+ * @param options.options - Cookie serialization options (httpOnly, secure, sameSite, etc.)
15
17
  *
16
- * @param options.name - List of cookie names to check.
17
- * @param options.getSessionForCookieValue - Function to retrieve a session using a cookie value.
18
+ * @example
19
+ * ```typescript
20
+ * import { authCookie } from '@pikku/core/middleware'
21
+ *
22
+ * addHTTPMiddleware([
23
+ * authCookie({
24
+ * name: 'session',
25
+ * expiresIn: { value: 30, unit: 'day' },
26
+ * options: {
27
+ * httpOnly: true,
28
+ * secure: true,
29
+ * sameSite: 'strict',
30
+ * path: '/'
31
+ * }
32
+ * })
33
+ * ])
34
+ * ```
18
35
  */
19
- export const authCookie = <
20
- SingletonServices extends CoreSingletonServices<CoreConfig>,
21
- UserSession extends CoreUserSession,
22
- >({
23
- name,
24
- getSessionForCookieValue,
25
- jwt,
26
- options,
27
- expiresIn,
28
- }: {
36
+ export const authCookie = pikkuMiddlewareFactory<{
29
37
  name: string
30
38
  options: SerializeOptions
31
39
  expiresIn: RelativeTimeInput
32
- } & (
33
- | {
34
- getSessionForCookieValue: (
35
- services: SingletonServices,
36
- cookieValue: string,
37
- cookieName: string
38
- ) => Promise<UserSession>
39
- jwt?: false
40
- }
41
- | {
42
- getSessionForCookieValue?: undefined
43
- jwt: true
44
- }
45
- )): CorePikkuMiddleware => {
46
- const middleware: CorePikkuMiddleware = async (services, { http }, next) => {
47
- const {
48
- userSession: userSessionService,
49
- jwt: jwtService,
50
- logger,
51
- } = services as any
52
- if (!http?.request || userSessionService.get()) {
53
- return next()
54
- }
40
+ }>(({ name, options, expiresIn }) =>
41
+ pikkuMiddleware(
42
+ async (
43
+ { userSession: userSessionService, jwt: jwtService, logger },
44
+ { http },
45
+ next
46
+ ) => {
47
+ if (!http?.request || userSessionService.get()) {
48
+ return next()
49
+ }
55
50
 
56
- let userSession: UserSession | null = null
57
- const cookieValue = http.request.cookie(name)
58
- if (cookieValue) {
59
- if (jwt) {
60
- if (!jwtService) {
61
- throw new Error('JWT service is required for JWT decoding.')
51
+ // Try to decode session from cookie
52
+ const cookieValue = http.request.cookie(name)
53
+ if (cookieValue && jwtService) {
54
+ const userSession = await jwtService.decode(cookieValue)
55
+ if (userSession) {
56
+ userSessionService.setInitial(userSession)
62
57
  }
63
- userSession = await jwtService.decode(cookieValue)
64
- } else if (getSessionForCookieValue) {
65
- userSession = await getSessionForCookieValue(
66
- services as any,
67
- cookieValue,
68
- name
69
- )
70
58
  }
71
- }
72
59
 
73
- if (userSession) {
74
- userSessionService.setInitial(userSession)
75
- }
76
- await next()
60
+ await next()
77
61
 
78
- // Set the cookie in the response if the session has changed
79
- if (!http?.response) {
80
- return
81
- }
62
+ // Set the cookie in the response if the session has changed
63
+ if (!http?.response) {
64
+ return
65
+ }
82
66
 
83
- if (userSessionService.sessionChanged) {
84
- const session = userSessionService.get()
85
- if (jwt) {
86
- if (!jwtService) {
87
- throw new Error('JWT service is required for JWT encoding.')
67
+ if (userSessionService.sessionChanged) {
68
+ const session = userSessionService.get()
69
+ if (jwtService) {
70
+ http.response.cookie(
71
+ name,
72
+ await jwtService.encode(expiresIn, session),
73
+ {
74
+ ...options,
75
+ expires: getRelativeTimeOffsetFromNow(expiresIn),
76
+ }
77
+ )
78
+ } else {
79
+ logger.warn('No JWT service available, unable to set cookie')
88
80
  }
89
- http.response.cookie(
90
- name,
91
- await jwtService.encode(expiresIn, session),
92
- {
93
- ...options,
94
- expires: getRelativeTimeOffsetFromNow(expiresIn),
95
- }
96
- )
97
- } else {
98
- logger.warn('No JWT service available, unable to set cookie')
99
81
  }
100
82
  }
101
- }
102
- return middleware
103
- }
83
+ )
84
+ )