@pikku/core 0.9.12-next.0 → 0.10.0
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.
- package/CHANGELOG.md +8 -0
- package/dist/errors/errors.d.ts +2 -0
- package/dist/errors/errors.js +6 -0
- package/dist/function/function-runner.d.ts +5 -4
- package/dist/function/function-runner.js +10 -4
- package/dist/function/functions.types.d.ts +67 -1
- package/dist/function/functions.types.js +43 -1
- package/dist/index.d.ts +1 -7
- package/dist/index.js +1 -7
- package/dist/middleware/auth-apikey.d.ts +19 -11
- package/dist/middleware/auth-apikey.js +33 -33
- package/dist/middleware/auth-bearer.d.ts +34 -7
- package/dist/middleware/auth-bearer.js +53 -36
- package/dist/middleware/auth-cookie.d.ts +30 -12
- package/dist/middleware/auth-cookie.js +51 -43
- package/dist/middleware/timeout.d.ts +6 -2
- package/dist/middleware/timeout.js +7 -9
- package/dist/middleware-runner.d.ts +0 -18
- package/dist/middleware-runner.js +0 -21
- package/dist/permissions.d.ts +82 -20
- package/dist/permissions.js +175 -62
- package/dist/pikku-state.d.ts +22 -2
- package/dist/pikku-state.js +7 -1
- package/dist/types/core.types.d.ts +58 -1
- package/dist/types/core.types.js +23 -1
- package/dist/utils.d.ts +1 -1
- package/dist/wirings/channel/channel-handler.js +6 -5
- package/dist/wirings/channel/channel-runner.d.ts +3 -1
- package/dist/wirings/channel/channel-runner.js +4 -21
- package/dist/wirings/channel/channel.types.d.ts +12 -13
- package/dist/wirings/cli/channel/cli-channel-runner.js +5 -2
- package/dist/wirings/cli/cli-runner.d.ts +10 -0
- package/dist/wirings/cli/cli-runner.js +44 -20
- package/dist/wirings/cli/cli.types.d.ts +54 -27
- package/dist/wirings/cli/command-parser.js +34 -4
- package/dist/wirings/cli/index.d.ts +1 -1
- package/dist/wirings/cli/index.js +1 -1
- package/dist/wirings/http/http-runner.d.ts +31 -1
- package/dist/wirings/http/http-runner.js +36 -1
- package/dist/wirings/http/http.types.d.ts +2 -1
- package/dist/wirings/http/index.d.ts +1 -1
- package/dist/wirings/http/index.js +1 -1
- package/dist/wirings/mcp/mcp-endpoint-registry.d.ts +1 -1
- package/dist/wirings/mcp/mcp-runner.js +2 -0
- package/dist/wirings/mcp/mcp.types.d.ts +35 -14
- package/dist/wirings/scheduler/scheduler-runner.js +1 -0
- package/dist/wirings/scheduler/scheduler.types.d.ts +2 -2
- package/package.json +6 -5
- package/src/errors/errors.ts +6 -0
- package/src/factory-functions.test.ts +60 -1
- package/src/function/function-runner.test.ts +8 -3
- package/src/function/function-runner.ts +18 -5
- package/src/function/functions.types.ts +85 -2
- package/src/index.ts +1 -21
- package/src/middleware/auth-apikey.ts +42 -57
- package/src/middleware/auth-bearer.ts +66 -49
- package/src/middleware/auth-cookie.ts +63 -82
- package/src/middleware/timeout.ts +10 -14
- package/src/middleware-runner.ts +0 -25
- package/src/permissions.test.ts +26 -27
- package/src/permissions.ts +215 -104
- package/src/pikku-state.ts +35 -3
- package/src/types/core.types.ts +70 -3
- package/src/utils.ts +1 -1
- package/src/wirings/channel/channel-handler.ts +11 -10
- package/src/wirings/channel/channel-runner.ts +18 -27
- package/src/wirings/channel/channel.types.ts +24 -15
- package/src/wirings/cli/channel/cli-channel-runner.ts +8 -3
- package/src/wirings/cli/cli-runner.test.ts +80 -59
- package/src/wirings/cli/cli-runner.ts +57 -21
- package/src/wirings/cli/cli.types.ts +74 -44
- package/src/wirings/cli/command-parser.test.ts +91 -86
- package/src/wirings/cli/command-parser.ts +38 -4
- package/src/wirings/cli/index.ts +1 -0
- package/src/wirings/http/http-runner.ts +41 -1
- package/src/wirings/http/http.types.ts +3 -1
- package/src/wirings/http/index.ts +7 -1
- package/src/wirings/mcp/mcp-endpoint-registry.ts +1 -1
- package/src/wirings/mcp/mcp-runner.ts +2 -0
- package/src/wirings/mcp/mcp.types.ts +48 -12
- package/src/wirings/scheduler/scheduler-runner.ts +1 -0
- package/src/wirings/scheduler/scheduler.types.ts +2 -2
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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:
|
|
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
|
|
4
|
+
* API key middleware that retrieves a session from the 'x-api-key' header using JWT decoding.
|
|
10
5
|
*
|
|
11
|
-
*
|
|
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
|
-
|
|
25
|
-
jwt
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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 (
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4
|
+
pikkuMiddleware,
|
|
5
|
+
pikkuMiddlewareFactory,
|
|
7
6
|
} from '../types/core.types.js'
|
|
8
7
|
|
|
9
8
|
/**
|
|
10
|
-
*
|
|
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:
|
|
40
|
+
userSession: CoreUserSession
|
|
23
41
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
70
|
+
// Otherwise, default to JWT decoding
|
|
71
|
+
else if (jwtService && !token) {
|
|
72
|
+
userSession = await jwtService.decode(bearerToken)
|
|
73
|
+
}
|
|
58
74
|
|
|
59
|
-
|
|
60
|
-
|
|
75
|
+
if (userSession) {
|
|
76
|
+
userSessionService.setInitial(userSession)
|
|
77
|
+
}
|
|
61
78
|
}
|
|
79
|
+
|
|
80
|
+
return next()
|
|
62
81
|
}
|
|
63
|
-
|
|
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
|
|
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
|
-
* @
|
|
17
|
-
*
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (
|
|
61
|
-
|
|
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
|
-
|
|
74
|
-
userSessionService.setInitial(userSession)
|
|
75
|
-
}
|
|
76
|
-
await next()
|
|
60
|
+
await next()
|
|
77
61
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
62
|
+
// Set the cookie in the response if the session has changed
|
|
63
|
+
if (!http?.response) {
|
|
64
|
+
return
|
|
65
|
+
}
|
|
82
66
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
103
|
-
}
|
|
83
|
+
)
|
|
84
|
+
)
|