@pikku/core 0.7.0 → 0.7.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.
- package/CHANGELOG.md +6 -0
- package/dist/channel/channel-runner.js +1 -1
- package/dist/channel/serverless/serverless-channel-runner.js +1 -1
- package/dist/http/http-runner.js +4 -2
- package/dist/http/log-http-routes.js +1 -1
- package/lcov.info +4585 -0
- package/package.json +1 -1
- package/src/channel/channel-handler.ts +165 -0
- package/src/channel/channel-runner.ts +153 -0
- package/src/channel/channel-store.ts +29 -0
- package/src/channel/channel.types.ts +133 -0
- package/src/channel/eventhub-service.ts +30 -0
- package/src/channel/eventhub-store.ts +8 -0
- package/src/channel/index.ts +8 -0
- package/src/channel/local/index.ts +3 -0
- package/src/channel/local/local-channel-handler.ts +50 -0
- package/src/channel/local/local-channel-runner.test.ts +157 -0
- package/src/channel/local/local-channel-runner.ts +139 -0
- package/src/channel/local/local-eventhub-service.test.ts +95 -0
- package/src/channel/local/local-eventhub-service.ts +95 -0
- package/src/channel/log-channels.ts +20 -0
- package/src/channel/pikku-abstract-channel-handler.test.ts +52 -0
- package/src/channel/pikku-abstract-channel-handler.ts +38 -0
- package/src/channel/serverless/index.ts +1 -0
- package/src/channel/serverless/serverless-channel-runner.ts +220 -0
- package/src/errors/error-handler.ts +61 -0
- package/src/errors/error.test.ts +54 -0
- package/src/errors/errors.ts +339 -0
- package/src/errors/index.ts +2 -0
- package/src/function/function-runner.ts +91 -0
- package/src/function/functions.types.ts +77 -0
- package/src/function/index.ts +2 -0
- package/src/handle-error.ts +73 -0
- package/src/http/http-runner.test.ts +140 -0
- package/src/http/http-runner.ts +493 -0
- package/src/http/http.types.ts +250 -0
- package/src/http/incomingmessage-to-request-convertor.ts +0 -0
- package/src/http/index.ts +8 -0
- package/src/http/log-http-routes.ts +22 -0
- package/src/http/pikku-fetch-http-request.test.ts +237 -0
- package/src/http/pikku-fetch-http-request.ts +163 -0
- package/src/http/pikku-fetch-http-response.test.ts +82 -0
- package/src/http/pikku-fetch-http-response.ts +138 -0
- package/src/index.ts +20 -0
- package/src/middleware/auth-apikey.ts +66 -0
- package/src/middleware/auth-bearer.ts +66 -0
- package/src/middleware/auth-cookie.ts +103 -0
- package/src/middleware/index.ts +3 -0
- package/src/middleware/timeout.ts +13 -0
- package/src/middleware-runner.ts +43 -0
- package/src/permissions.test.ts +58 -0
- package/src/permissions.ts +43 -0
- package/src/pikku-function.ts +1 -0
- package/src/pikku-request.ts +23 -0
- package/src/pikku-response.ts +5 -0
- package/src/pikku-state.ts +87 -0
- package/src/scheduler/index.ts +5 -0
- package/src/scheduler/log-schedulers.ts +20 -0
- package/src/scheduler/scheduler-runner.ts +105 -0
- package/src/scheduler/scheduler.types.ts +33 -0
- package/src/schema.test.ts +57 -0
- package/src/schema.ts +99 -0
- package/src/services/content-service.ts +68 -0
- package/src/services/index.ts +18 -0
- package/src/services/jwt-service.ts +30 -0
- package/src/services/local-content.ts +105 -0
- package/src/services/local-secrets.ts +43 -0
- package/src/services/local-variables.ts +17 -0
- package/src/services/logger-console.ts +97 -0
- package/src/services/logger.ts +57 -0
- package/src/services/schema-service.ts +26 -0
- package/src/services/secret-service.ts +17 -0
- package/src/services/user-session-service.ts +53 -0
- package/src/services/variables-service.ts +6 -0
- package/src/time-utils.test.ts +56 -0
- package/src/time-utils.ts +32 -0
- package/src/types/core.types.ts +178 -0
- package/src/utils.ts +40 -0
- package/tsconfig.json +13 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/dist/http/http-route-runner.d.ts +0 -90
- package/dist/http/http-route-runner.js +0 -325
- package/dist/http/http-routes.types.d.ts +0 -179
- package/dist/http/http-routes.types.js +0 -1
- package/dist/http/pikku-http-request.d.ts +0 -56
- package/dist/http/pikku-http-request.js +0 -95
- package/dist/http/pikku-http-response.d.ts +0 -13
- package/dist/http/pikku-http-response.js +0 -60
- package/dist/parse-relative-time-offset.d.ts +0 -12
- package/dist/parse-relative-time-offset.js +0 -20
- package/dist/pikku-func.d.ts +0 -12
- package/dist/pikku-func.js +0 -39
- package/dist/types/functions.types.d.ts +0 -37
- package/dist/types/functions.types.js +0 -1
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, test } from 'node:test'
|
|
2
|
+
import assert from 'assert'
|
|
3
|
+
import { PikkuFetchHTTPResponse } from './pikku-fetch-http-response.js' // Adjust path if needed
|
|
4
|
+
|
|
5
|
+
describe('PikkuFetchHTTPResponse', () => {
|
|
6
|
+
test('sets status code', () => {
|
|
7
|
+
const res = new PikkuFetchHTTPResponse().status(404).toResponse()
|
|
8
|
+
assert.strictEqual(res.status, 404)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
test('sets headers', async () => {
|
|
12
|
+
const res = new PikkuFetchHTTPResponse()
|
|
13
|
+
.header('X-Test', 'value')
|
|
14
|
+
.header('X-Multi', ['a', 'b'])
|
|
15
|
+
.toResponse()
|
|
16
|
+
|
|
17
|
+
assert.strictEqual(res.headers.get('X-Test'), 'value')
|
|
18
|
+
const multi = res.headers.get('X-Multi')?.split(', ')
|
|
19
|
+
assert.deepEqual(multi, ['a', 'b'])
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('sets cookies', () => {
|
|
23
|
+
const res = new PikkuFetchHTTPResponse()
|
|
24
|
+
.cookie('token', 'abc123', { httpOnly: true, path: '/' })
|
|
25
|
+
.cookie('other', 'val', { maxAge: 3600 })
|
|
26
|
+
.toResponse()
|
|
27
|
+
|
|
28
|
+
const setCookieHeader = res.headers.get('Set-Cookie')
|
|
29
|
+
assert.ok(setCookieHeader)
|
|
30
|
+
|
|
31
|
+
const cookies = setCookieHeader!.split(', ')
|
|
32
|
+
assert.ok(cookies.some((c) => c.startsWith('token=abc123')))
|
|
33
|
+
assert.ok(cookies.some((c) => c.startsWith('other=val')))
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('json sets correct content type and body', async () => {
|
|
37
|
+
const res = new PikkuFetchHTTPResponse()
|
|
38
|
+
.json({ hello: 'world' })
|
|
39
|
+
.toResponse()
|
|
40
|
+
|
|
41
|
+
assert.strictEqual(res.headers.get('Content-Type'), 'application/json')
|
|
42
|
+
const body = await res.json()
|
|
43
|
+
assert.deepEqual(body, { hello: 'world' })
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('text sets correct content type and body', async () => {
|
|
47
|
+
const res = new PikkuFetchHTTPResponse().text('hello').toResponse()
|
|
48
|
+
|
|
49
|
+
assert.strictEqual(res.headers.get('Content-Type'), 'text/plain')
|
|
50
|
+
const text = await res.text()
|
|
51
|
+
assert.strictEqual(text, 'hello')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('html sets correct content type and body', async () => {
|
|
55
|
+
const res = new PikkuFetchHTTPResponse().html('<p>hi</p>').toResponse()
|
|
56
|
+
|
|
57
|
+
assert.strictEqual(res.headers.get('Content-Type'), 'text/html')
|
|
58
|
+
const text = await res.text()
|
|
59
|
+
assert.strictEqual(text, '<p>hi</p>')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('arrayBuffer sets correct content type', async () => {
|
|
63
|
+
const buffer = new Uint8Array([1, 2, 3]).buffer
|
|
64
|
+
const res = new PikkuFetchHTTPResponse().arrayBuffer(buffer).toResponse()
|
|
65
|
+
|
|
66
|
+
assert.strictEqual(
|
|
67
|
+
res.headers.get('Content-Type'),
|
|
68
|
+
'application/octet-stream'
|
|
69
|
+
)
|
|
70
|
+
const body = await res.arrayBuffer()
|
|
71
|
+
assert.deepEqual(new Uint8Array(body), new Uint8Array([1, 2, 3]))
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('redirect sets Location header and status', () => {
|
|
75
|
+
const res = new PikkuFetchHTTPResponse()
|
|
76
|
+
.redirect('/login', 301)
|
|
77
|
+
.toResponse()
|
|
78
|
+
|
|
79
|
+
assert.strictEqual(res.status, 301)
|
|
80
|
+
assert.strictEqual(res.headers.get('Location'), '/login')
|
|
81
|
+
})
|
|
82
|
+
})
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { PikkuHTTPResponse } from './http.types.js'
|
|
2
|
+
import {
|
|
3
|
+
SerializeOptions as CookieSerializeOptions,
|
|
4
|
+
serialize as serializeCookie,
|
|
5
|
+
} from 'cookie'
|
|
6
|
+
|
|
7
|
+
export class PikkuFetchHTTPResponse implements PikkuHTTPResponse {
|
|
8
|
+
#statusCode: number = 200
|
|
9
|
+
#headers = new Headers()
|
|
10
|
+
#cookies = new Map<string, { value: string; flags: CookieSerializeOptions }>()
|
|
11
|
+
|
|
12
|
+
#body: BodyInit | null = null
|
|
13
|
+
#responseMode: 'stream' | null = null
|
|
14
|
+
#send: ((data: string) => void) | null = null
|
|
15
|
+
#close: (() => void) | null = null
|
|
16
|
+
|
|
17
|
+
public setMode(mode: 'stream') {
|
|
18
|
+
this.#responseMode = 'stream'
|
|
19
|
+
if (mode === 'stream') {
|
|
20
|
+
this.#body = this.createStream()
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public status(code: number): this {
|
|
25
|
+
this.#statusCode = code
|
|
26
|
+
return this
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public cookie(
|
|
30
|
+
name: string,
|
|
31
|
+
value: string,
|
|
32
|
+
flags: CookieSerializeOptions
|
|
33
|
+
): this {
|
|
34
|
+
this.#cookies.set(name, { value, flags })
|
|
35
|
+
return this
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public header(name: string, value: string | string[]): this {
|
|
39
|
+
this.#headers.delete(name)
|
|
40
|
+
if (Array.isArray(value)) {
|
|
41
|
+
value.forEach((v) => this.#headers.append(name, v))
|
|
42
|
+
} else {
|
|
43
|
+
this.#headers.set(name, value)
|
|
44
|
+
}
|
|
45
|
+
return this
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public arrayBuffer(data: XMLHttpRequestBodyInit): this {
|
|
49
|
+
if (this.#responseMode === 'stream') {
|
|
50
|
+
this.#send!(data as string)
|
|
51
|
+
} else {
|
|
52
|
+
this.#body = data
|
|
53
|
+
this.header('Content-Type', 'application/octet-stream')
|
|
54
|
+
}
|
|
55
|
+
return this
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public json(data: unknown): this {
|
|
59
|
+
if (this.#responseMode === 'stream') {
|
|
60
|
+
this.#send!(JSON.stringify(data))
|
|
61
|
+
} else {
|
|
62
|
+
this.#body = JSON.stringify(data)
|
|
63
|
+
this.header('Content-Type', 'application/json')
|
|
64
|
+
}
|
|
65
|
+
return this
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public text(content: string): this {
|
|
69
|
+
if (this.#responseMode === 'stream') {
|
|
70
|
+
this.#send!(content)
|
|
71
|
+
} else {
|
|
72
|
+
this.#body = content
|
|
73
|
+
this.header('Content-Type', 'text/plain')
|
|
74
|
+
}
|
|
75
|
+
return this
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public html(content: string): this {
|
|
79
|
+
if (this.#responseMode === 'stream') {
|
|
80
|
+
this.#send!(content)
|
|
81
|
+
} else {
|
|
82
|
+
this.#body = content
|
|
83
|
+
this.header('Content-Type', 'text/html')
|
|
84
|
+
}
|
|
85
|
+
return this
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// public body(body: BodyInit): this {
|
|
89
|
+
// this.#body = body
|
|
90
|
+
// return this
|
|
91
|
+
// }
|
|
92
|
+
|
|
93
|
+
public redirect(location: string, status: number = 302): this {
|
|
94
|
+
this.#statusCode = status
|
|
95
|
+
this.header('Location', location)
|
|
96
|
+
return this
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public close(): this {
|
|
100
|
+
if (this.#close) {
|
|
101
|
+
this.#close()
|
|
102
|
+
}
|
|
103
|
+
return this
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
public toResponse(args?: Record<string, any>): Response {
|
|
107
|
+
const cookieHeader = Array.from(this.#cookies.entries()).map(
|
|
108
|
+
([name, { value, flags }]) => serializeCookie(name, value, flags)
|
|
109
|
+
)
|
|
110
|
+
this.#headers.set('Set-Cookie', cookieHeader.join(', '))
|
|
111
|
+
return new Response(this.#body, {
|
|
112
|
+
...args,
|
|
113
|
+
status: this.#statusCode,
|
|
114
|
+
headers: this.#headers,
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private createStream(): ReadableStream<Uint8Array> {
|
|
119
|
+
const encoder = new TextEncoder()
|
|
120
|
+
return new ReadableStream({
|
|
121
|
+
start: (controller) => {
|
|
122
|
+
const send = (data: string) => {
|
|
123
|
+
controller.enqueue(encoder.encode(`data: ${data}\n\n`))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const close = () => {
|
|
127
|
+
controller.close()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this.#send = send
|
|
131
|
+
this.#close = close
|
|
132
|
+
|
|
133
|
+
// Force initial flush
|
|
134
|
+
controller.enqueue(encoder.encode(':\n\n'))
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @pikku/core
|
|
3
|
+
*/
|
|
4
|
+
export * from './types/core.types.js'
|
|
5
|
+
export * from './function/index.js'
|
|
6
|
+
export * from './pikku-request.js'
|
|
7
|
+
export * from './pikku-response.js'
|
|
8
|
+
export * from './services/index.js'
|
|
9
|
+
export * from './http/index.js'
|
|
10
|
+
export * from './channel/index.js'
|
|
11
|
+
export * from './scheduler/index.js'
|
|
12
|
+
export * from './errors/index.js'
|
|
13
|
+
export * from './middleware/index.js'
|
|
14
|
+
export * from './time-utils.js'
|
|
15
|
+
export * from './utils.js'
|
|
16
|
+
export { pikkuState } from './pikku-state.js'
|
|
17
|
+
export { runMiddleware } from './middleware-runner.js'
|
|
18
|
+
export { addHTTPRoute, addMiddleware } from './http/http-runner.js'
|
|
19
|
+
export { addChannel } from './channel/channel-runner.js'
|
|
20
|
+
export { addScheduledTask } from './scheduler/scheduler-runner.js'
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CoreConfig,
|
|
3
|
+
CoreSingletonServices,
|
|
4
|
+
CoreUserSession,
|
|
5
|
+
PikkuMiddleware,
|
|
6
|
+
} from '../types/core.types.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* API key middleware that retrieves a session from the 'x-api-key' header using a provided callback.
|
|
10
|
+
*
|
|
11
|
+
* @param options.getSessionForAPIKey - A function that returns a session when provided an API key.
|
|
12
|
+
*/
|
|
13
|
+
export const authAPIKey = <
|
|
14
|
+
SingletonServices extends CoreSingletonServices<CoreConfig>,
|
|
15
|
+
UserSession extends CoreUserSession,
|
|
16
|
+
>({
|
|
17
|
+
source,
|
|
18
|
+
getSessionForAPIKey,
|
|
19
|
+
jwt,
|
|
20
|
+
}: {
|
|
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: PikkuMiddleware = async (services, { http }, next) => {
|
|
36
|
+
const { userSession: userSessionService, jwt: jwtService } = services
|
|
37
|
+
if (!http?.request || userSessionService.get()) {
|
|
38
|
+
return next()
|
|
39
|
+
}
|
|
40
|
+
|
|
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)
|
|
57
|
+
}
|
|
58
|
+
if (userSession) {
|
|
59
|
+
userSessionService.setInitial(userSession)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return next()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return middleware
|
|
66
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { InvalidSessionError } from '../errors/errors.js'
|
|
2
|
+
import {
|
|
3
|
+
CoreConfig,
|
|
4
|
+
CoreSingletonServices,
|
|
5
|
+
CoreUserSession,
|
|
6
|
+
PikkuMiddleware,
|
|
7
|
+
} from '../types/core.types.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Extracts the Bearer token from the Authorization header
|
|
11
|
+
*/
|
|
12
|
+
export const authBearer = <
|
|
13
|
+
SingletonServices extends CoreSingletonServices<CoreConfig>,
|
|
14
|
+
UserSession extends CoreUserSession,
|
|
15
|
+
>({
|
|
16
|
+
token,
|
|
17
|
+
jwt,
|
|
18
|
+
getSession,
|
|
19
|
+
}: {
|
|
20
|
+
token?: {
|
|
21
|
+
value: string
|
|
22
|
+
userSession: UserSession
|
|
23
|
+
}
|
|
24
|
+
jwt?: boolean
|
|
25
|
+
getSession?: (
|
|
26
|
+
services: SingletonServices,
|
|
27
|
+
token: string
|
|
28
|
+
) => Promise<UserSession> | UserSession
|
|
29
|
+
} = {}): PikkuMiddleware => {
|
|
30
|
+
const middleware: PikkuMiddleware = async (services, { http }, next) => {
|
|
31
|
+
const { userSession: userSessionService, jwt: jwtService } = services
|
|
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()
|
|
44
|
+
}
|
|
45
|
+
let userSession: UserSession | null = null
|
|
46
|
+
if (jwt) {
|
|
47
|
+
if (!jwtService) {
|
|
48
|
+
throw new Error('JWT service is required for JWT decoding.')
|
|
49
|
+
}
|
|
50
|
+
userSession = await jwtService.decode(bearerToken)
|
|
51
|
+
} else if (token) {
|
|
52
|
+
if (bearerToken === token.value) {
|
|
53
|
+
userSession = token.userSession
|
|
54
|
+
}
|
|
55
|
+
} else if (getSession) {
|
|
56
|
+
userSession = await getSession(services as any, token)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (userSession) {
|
|
60
|
+
userSessionService.setInitial(userSession)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return next()
|
|
64
|
+
}
|
|
65
|
+
return middleware
|
|
66
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { SerializeOptions } from 'cookie'
|
|
2
|
+
import {
|
|
3
|
+
CoreConfig,
|
|
4
|
+
CoreSingletonServices,
|
|
5
|
+
CoreUserSession,
|
|
6
|
+
PikkuMiddleware,
|
|
7
|
+
} from '../types/core.types.js'
|
|
8
|
+
import {
|
|
9
|
+
getRelativeTimeOffsetFromNow,
|
|
10
|
+
RelativeTimeInput,
|
|
11
|
+
} from '../time-utils.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Cookie middleware that extracts a session from cookies.
|
|
15
|
+
*
|
|
16
|
+
* @param options.name - List of cookie names to check.
|
|
17
|
+
* @param options.getSessionForCookieValue - Function to retrieve a session using a cookie value.
|
|
18
|
+
*/
|
|
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
|
+
}: {
|
|
29
|
+
name: string
|
|
30
|
+
options: SerializeOptions
|
|
31
|
+
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
|
+
)): PikkuMiddleware => {
|
|
46
|
+
const middleware: PikkuMiddleware = async (services, { http }, next) => {
|
|
47
|
+
const {
|
|
48
|
+
userSession: userSessionService,
|
|
49
|
+
jwt: jwtService,
|
|
50
|
+
logger,
|
|
51
|
+
} = services
|
|
52
|
+
if (!http?.request || userSessionService.get()) {
|
|
53
|
+
return next()
|
|
54
|
+
}
|
|
55
|
+
|
|
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.')
|
|
62
|
+
}
|
|
63
|
+
userSession = await jwtService.decode(cookieValue)
|
|
64
|
+
} else if (getSessionForCookieValue) {
|
|
65
|
+
userSession = await getSessionForCookieValue(
|
|
66
|
+
services as any,
|
|
67
|
+
cookieValue,
|
|
68
|
+
name
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (userSession) {
|
|
74
|
+
userSessionService.setInitial(userSession)
|
|
75
|
+
}
|
|
76
|
+
await next()
|
|
77
|
+
|
|
78
|
+
// Set the cookie in the response if the session has changed
|
|
79
|
+
if (!http?.response) {
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
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.')
|
|
88
|
+
}
|
|
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
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return middleware
|
|
103
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { RequestTimeoutError } from '../errors/errors.js'
|
|
2
|
+
import { PikkuMiddleware } from '../types/core.types.js'
|
|
3
|
+
|
|
4
|
+
export const timeout = (timeout: number) => {
|
|
5
|
+
const middleware: PikkuMiddleware = async (_services, _interaction, next) => {
|
|
6
|
+
setTimeout(() => {
|
|
7
|
+
throw new RequestTimeoutError()
|
|
8
|
+
}, timeout)
|
|
9
|
+
|
|
10
|
+
await next()
|
|
11
|
+
}
|
|
12
|
+
return middleware
|
|
13
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { UserSessionService } from './services/user-session-service.js'
|
|
2
|
+
import {
|
|
3
|
+
CoreSingletonServices,
|
|
4
|
+
PikkuInteraction,
|
|
5
|
+
PikkuMiddleware,
|
|
6
|
+
} from './types/core.types.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Runs a chain of middleware functions in sequence before executing the main function.
|
|
10
|
+
*
|
|
11
|
+
* @param services - An object containing services (e.g., singletonServices, userSession, etc.)
|
|
12
|
+
* @param interaction - The interaction context, e.g., { http }.
|
|
13
|
+
* @param middlewares - An array of middleware functions to run.
|
|
14
|
+
* @param main - The main function to execute after all middleware have run.
|
|
15
|
+
* @returns A promise resolving to the result of the main function.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* runMiddleware(
|
|
19
|
+
* { ...services, userSession },
|
|
20
|
+
* { http },
|
|
21
|
+
* [middleware1, middleware2, middleware3],
|
|
22
|
+
* async () => { return await runMain(); }
|
|
23
|
+
* );
|
|
24
|
+
*/
|
|
25
|
+
export const runMiddleware = async (
|
|
26
|
+
services: CoreSingletonServices & {
|
|
27
|
+
userSession: UserSessionService<any>
|
|
28
|
+
},
|
|
29
|
+
interaction: PikkuInteraction,
|
|
30
|
+
middlewares: PikkuMiddleware[],
|
|
31
|
+
main?: () => Promise<void>
|
|
32
|
+
): Promise<void> => {
|
|
33
|
+
const dispatch = async (index: number): Promise<any> => {
|
|
34
|
+
if (middlewares && index < middlewares.length) {
|
|
35
|
+
await middlewares[index]!(services, interaction, () =>
|
|
36
|
+
dispatch(index + 1)
|
|
37
|
+
)
|
|
38
|
+
} else if (main) {
|
|
39
|
+
return await main()
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
await dispatch(0)
|
|
43
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { test, describe } from 'node:test'
|
|
2
|
+
import assert from 'assert'
|
|
3
|
+
import { getErrorResponse, addError } from './errors/error-handler.js'
|
|
4
|
+
import {
|
|
5
|
+
ForbiddenError,
|
|
6
|
+
BadRequestError,
|
|
7
|
+
NotFoundError,
|
|
8
|
+
} from './errors/errors.js'
|
|
9
|
+
|
|
10
|
+
describe('getErrorResponse', () => {
|
|
11
|
+
test('should return the correct error response for BadRequestError', () => {
|
|
12
|
+
const error = new BadRequestError()
|
|
13
|
+
const response = getErrorResponse(error)
|
|
14
|
+
assert.deepStrictEqual(response, {
|
|
15
|
+
status: 400,
|
|
16
|
+
message:
|
|
17
|
+
'The server cannot or will not process the request due to client error (e.g., malformed request syntax).',
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('should return the correct error response for NotFoundError', () => {
|
|
22
|
+
const error = new NotFoundError()
|
|
23
|
+
const response = getErrorResponse(error)
|
|
24
|
+
assert.deepStrictEqual(response, {
|
|
25
|
+
status: 404,
|
|
26
|
+
message: 'The server cannot find the requested resource.',
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('should return the correct error response for ForbiddenError', () => {
|
|
31
|
+
const error = new ForbiddenError()
|
|
32
|
+
const response = getErrorResponse(error)
|
|
33
|
+
assert.deepStrictEqual(response, {
|
|
34
|
+
status: 403,
|
|
35
|
+
message:
|
|
36
|
+
'The client does not have permission to access the requested resource.',
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('should return undefined for an unknown error', () => {
|
|
41
|
+
class UnknownError extends Error {}
|
|
42
|
+
const error = new UnknownError()
|
|
43
|
+
const response = getErrorResponse(error)
|
|
44
|
+
assert.strictEqual(response, undefined)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('should return the correct error response for a custom error added to apiErrors', () => {
|
|
48
|
+
class CustomError extends Error {}
|
|
49
|
+
const customError = new CustomError()
|
|
50
|
+
const customErrorDetails = { status: 400, message: 'Custom Error' }
|
|
51
|
+
|
|
52
|
+
// Add the custom error to the apiErrors map
|
|
53
|
+
addError(CustomError, customErrorDetails)
|
|
54
|
+
|
|
55
|
+
const response = getErrorResponse(customError)
|
|
56
|
+
assert.deepStrictEqual(response, customErrorDetails)
|
|
57
|
+
})
|
|
58
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { CoreServices, CoreUserSession } from './types/core.types.js'
|
|
2
|
+
import { CorePermissionGroup } from './function/functions.types.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* This function validates permissions by iterating over permission groups and executing the corresponding permission functions. If all functions in at least one group return true, the permission is considered valid.
|
|
6
|
+
* @param services - The core services required for permission validation.
|
|
7
|
+
* @param data - The data to be used in the permission validation functions.
|
|
8
|
+
* @param session - An optional user session for permission validation.
|
|
9
|
+
* @returns A promise that resolves to void.
|
|
10
|
+
*/
|
|
11
|
+
export const verifyPermissions = async (
|
|
12
|
+
permissions: CorePermissionGroup,
|
|
13
|
+
services: CoreServices,
|
|
14
|
+
data: any,
|
|
15
|
+
session?: CoreUserSession
|
|
16
|
+
): Promise<boolean> => {
|
|
17
|
+
if (!permissions) {
|
|
18
|
+
return true
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let valid = false
|
|
22
|
+
const permissionGroups = Object.values(permissions)
|
|
23
|
+
if (permissionGroups.length === 0) {
|
|
24
|
+
return true
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const funcs of permissionGroups) {
|
|
28
|
+
if (funcs instanceof Array) {
|
|
29
|
+
const permissioned = await Promise.all(
|
|
30
|
+
funcs.map((func) => func(services, data, session))
|
|
31
|
+
)
|
|
32
|
+
if (permissioned.every((result) => result)) {
|
|
33
|
+
valid = true
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
valid = await funcs(services, data, session)
|
|
37
|
+
}
|
|
38
|
+
if (valid) {
|
|
39
|
+
return true
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const callFunction = () => {}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simplest class to represent a pikku request.
|
|
3
|
+
* @template In - The type of the request body.
|
|
4
|
+
* @group RequestResponse
|
|
5
|
+
*/
|
|
6
|
+
export abstract class PikkuRequest<In = any> {
|
|
7
|
+
#data: In | undefined
|
|
8
|
+
|
|
9
|
+
constructor(data: In) {
|
|
10
|
+
this.#data = data
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Retrieves the data
|
|
15
|
+
* @returns A promise that resolves to an object containing the combined data.
|
|
16
|
+
*/
|
|
17
|
+
public async data(): Promise<In> {
|
|
18
|
+
if (!this.#data) {
|
|
19
|
+
throw new Error('Data not found')
|
|
20
|
+
}
|
|
21
|
+
return this.#data
|
|
22
|
+
}
|
|
23
|
+
}
|