@pikku/core 0.6.23 → 0.6.24
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.d.ts +1 -1
- package/dist/channel/channel-runner.js +1 -1
- package/dist/channel/local/local-channel-runner.js +5 -5
- package/dist/channel/serverless/serverless-channel-runner.js +4 -4
- package/dist/http/http-route-runner.js +9 -8
- package/dist/http/pikku-fetch-http-request.d.ts +1 -0
- package/dist/http/pikku-fetch-http-request.js +64 -8
- package/dist/http/pikku-fetch-http-response.d.ts +2 -1
- package/dist/http/pikku-fetch-http-response.js +5 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/middleware/auth-apikey.js +2 -2
- package/dist/middleware/auth-bearer.js +2 -2
- package/dist/middleware/auth-cookie.d.ts +7 -3
- package/dist/middleware/auth-cookie.js +34 -17
- package/dist/middleware-runner.d.ts +3 -3
- package/dist/middleware-runner.js +2 -2
- package/dist/services/jwt-service.d.ts +2 -1
- package/dist/services/local-secrets.d.ts +2 -2
- package/dist/services/local-secrets.js +5 -5
- package/dist/services/user-session-service.d.ts +2 -0
- package/dist/services/user-session-service.js +3 -0
- package/dist/time-utils.d.ts +12 -0
- package/dist/time-utils.js +18 -0
- package/dist/types/core.types.d.ts +5 -5
- package/dist/types/functions.types.d.ts +1 -1
- package/lcov.info +1751 -1018
- package/package.json +1 -1
- package/src/channel/channel-runner.ts +2 -2
- package/src/channel/local/local-channel-runner.test.ts +0 -2
- package/src/channel/local/local-channel-runner.ts +5 -5
- package/src/channel/serverless/serverless-channel-runner.ts +4 -7
- package/src/http/http-route-runner.test.ts +1 -1
- package/src/http/http-route-runner.ts +18 -10
- package/src/http/pikku-fetch-http-request.test.ts +237 -0
- package/src/http/pikku-fetch-http-request.ts +64 -8
- package/src/http/pikku-fetch-http-response.test.ts +82 -0
- package/src/http/pikku-fetch-http-response.ts +16 -2
- package/src/index.ts +1 -1
- package/src/middleware/auth-apikey.ts +2 -2
- package/src/middleware/auth-bearer.ts +2 -2
- package/src/middleware/auth-cookie.ts +51 -21
- package/src/middleware-runner.ts +3 -3
- package/src/services/jwt-service.ts +6 -1
- package/src/services/local-secrets.ts +3 -3
- package/src/services/user-session-service.ts +4 -0
- package/src/time-utils.test.ts +56 -0
- package/src/time-utils.ts +32 -0
- package/src/types/core.types.ts +5 -5
- package/src/types/functions.types.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -33,7 +33,7 @@ export const authAPIKey = <
|
|
|
33
33
|
}
|
|
34
34
|
)) => {
|
|
35
35
|
const middleware: PikkuMiddleware = async (services, { http }, next) => {
|
|
36
|
-
if (!http?.request || services.
|
|
36
|
+
if (!http?.request || services.userSession.get()) {
|
|
37
37
|
return next()
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -55,7 +55,7 @@ export const authAPIKey = <
|
|
|
55
55
|
userSession = await getSessionForAPIKey!(services as any, apiKey)
|
|
56
56
|
}
|
|
57
57
|
if (userSession) {
|
|
58
|
-
services.
|
|
58
|
+
services.userSession.setInitial(userSession)
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
return next()
|
|
@@ -29,7 +29,7 @@ export const authBearer = <
|
|
|
29
29
|
} = {}): PikkuMiddleware => {
|
|
30
30
|
const middleware: PikkuMiddleware = async (services, { http }, next) => {
|
|
31
31
|
// Skip if session already exists.
|
|
32
|
-
if (!http?.request || services.
|
|
32
|
+
if (!http?.request || services.userSession.get()) {
|
|
33
33
|
return next()
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -56,7 +56,7 @@ export const authBearer = <
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
if (userSession) {
|
|
59
|
-
services.
|
|
59
|
+
services.userSession.setInitial(userSession)
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
return next()
|
|
@@ -1,25 +1,34 @@
|
|
|
1
|
+
import { SerializeOptions } from 'cookie'
|
|
1
2
|
import {
|
|
2
3
|
CoreConfig,
|
|
3
4
|
CoreSingletonServices,
|
|
4
5
|
CoreUserSession,
|
|
5
6
|
PikkuMiddleware,
|
|
6
7
|
} from '../types/core.types.js'
|
|
8
|
+
import {
|
|
9
|
+
getRelativeTimeOffsetFromNow,
|
|
10
|
+
RelativeTimeInput,
|
|
11
|
+
} from '../time-utils.js'
|
|
7
12
|
|
|
8
13
|
/**
|
|
9
14
|
* Cookie middleware that extracts a session from cookies.
|
|
10
15
|
*
|
|
11
|
-
* @param options.
|
|
16
|
+
* @param options.name - List of cookie names to check.
|
|
12
17
|
* @param options.getSessionForCookieValue - Function to retrieve a session using a cookie value.
|
|
13
18
|
*/
|
|
14
19
|
export const authCookie = <
|
|
15
20
|
SingletonServices extends CoreSingletonServices<CoreConfig>,
|
|
16
21
|
UserSession extends CoreUserSession,
|
|
17
22
|
>({
|
|
18
|
-
|
|
23
|
+
name,
|
|
19
24
|
getSessionForCookieValue,
|
|
20
25
|
jwt,
|
|
26
|
+
options,
|
|
27
|
+
expiresIn,
|
|
21
28
|
}: {
|
|
22
|
-
|
|
29
|
+
name: string
|
|
30
|
+
options: SerializeOptions
|
|
31
|
+
expiresIn: RelativeTimeInput
|
|
23
32
|
} & (
|
|
24
33
|
| {
|
|
25
34
|
getSessionForCookieValue: (
|
|
@@ -35,34 +44,55 @@ export const authCookie = <
|
|
|
35
44
|
}
|
|
36
45
|
)): PikkuMiddleware => {
|
|
37
46
|
const middleware: PikkuMiddleware = async (services, { http }, next) => {
|
|
38
|
-
if (!http?.request || services.
|
|
47
|
+
if (!http?.request || services.userSession.get()) {
|
|
39
48
|
return next()
|
|
40
49
|
}
|
|
41
50
|
|
|
42
51
|
let userSession: UserSession | null = null
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (
|
|
46
|
-
if (jwt) {
|
|
47
|
-
|
|
48
|
-
throw new Error('JWT service is required for JWT decoding.')
|
|
49
|
-
}
|
|
50
|
-
userSession = await services.jwt.decode(cookieValue)
|
|
51
|
-
} else if (getSessionForCookieValue) {
|
|
52
|
-
userSession = await getSessionForCookieValue(
|
|
53
|
-
services as any,
|
|
54
|
-
cookieValue,
|
|
55
|
-
cookieName
|
|
56
|
-
)
|
|
52
|
+
const cookieValue = http.request.cookie(name)
|
|
53
|
+
if (cookieValue) {
|
|
54
|
+
if (jwt) {
|
|
55
|
+
if (!services.jwt) {
|
|
56
|
+
throw new Error('JWT service is required for JWT decoding.')
|
|
57
57
|
}
|
|
58
|
-
|
|
58
|
+
userSession = await services.jwt.decode(cookieValue)
|
|
59
|
+
} else if (getSessionForCookieValue) {
|
|
60
|
+
userSession = await getSessionForCookieValue(
|
|
61
|
+
services as any,
|
|
62
|
+
cookieValue,
|
|
63
|
+
name
|
|
64
|
+
)
|
|
59
65
|
}
|
|
60
66
|
}
|
|
61
67
|
|
|
62
68
|
if (userSession) {
|
|
63
|
-
services.
|
|
69
|
+
services.userSession.setInitial(userSession)
|
|
70
|
+
}
|
|
71
|
+
await next()
|
|
72
|
+
|
|
73
|
+
// Set the cookie in the response if the session has changed
|
|
74
|
+
if (!http?.response) {
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (services.userSession.sessionChanged) {
|
|
79
|
+
const session = services.userSession.get()
|
|
80
|
+
if (jwt) {
|
|
81
|
+
if (!services.jwt) {
|
|
82
|
+
throw new Error('JWT service is required for JWT encoding.')
|
|
83
|
+
}
|
|
84
|
+
http.response.cookie(
|
|
85
|
+
name,
|
|
86
|
+
await services.jwt.encode(expiresIn, session),
|
|
87
|
+
{
|
|
88
|
+
...options,
|
|
89
|
+
expires: getRelativeTimeOffsetFromNow(expiresIn),
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
} else {
|
|
93
|
+
services.logger.warn('No JWT service available, unable to set cookie')
|
|
94
|
+
}
|
|
64
95
|
}
|
|
65
|
-
return next()
|
|
66
96
|
}
|
|
67
97
|
return middleware
|
|
68
98
|
}
|
package/src/middleware-runner.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
/**
|
|
9
9
|
* Runs a chain of middleware functions in sequence before executing the main function.
|
|
10
10
|
*
|
|
11
|
-
* @param services - An object containing services (e.g., singletonServices,
|
|
11
|
+
* @param services - An object containing services (e.g., singletonServices, userSession, etc.)
|
|
12
12
|
* @param interaction - The interaction context, e.g., { http }.
|
|
13
13
|
* @param middlewares - An array of middleware functions to run.
|
|
14
14
|
* @param main - The main function to execute after all middleware have run.
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
*
|
|
17
17
|
* @example
|
|
18
18
|
* runMiddleware(
|
|
19
|
-
* { ...services,
|
|
19
|
+
* { ...services, userSession },
|
|
20
20
|
* { http },
|
|
21
21
|
* [middleware1, middleware2, middleware3],
|
|
22
22
|
* async () => { return await runMain(); }
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
*/
|
|
25
25
|
export const runMiddleware = async (
|
|
26
26
|
services: CoreSingletonServices & {
|
|
27
|
-
|
|
27
|
+
userSession: UserSessionService<any>
|
|
28
28
|
},
|
|
29
29
|
interaction: PikkuInteraction,
|
|
30
30
|
middlewares: PikkuMiddleware[],
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { RelativeTimeInput } from '../time-utils.js'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Interface for handling JSON Web Tokens (JWT).
|
|
3
5
|
*/
|
|
@@ -8,7 +10,10 @@ export interface JWTService {
|
|
|
8
10
|
* @param payload - The payload to encode.
|
|
9
11
|
* @returns A promise that resolves to the encoded JWT.
|
|
10
12
|
*/
|
|
11
|
-
encode: <T extends any>(
|
|
13
|
+
encode: <T extends any>(
|
|
14
|
+
expiresIn: RelativeTimeInput,
|
|
15
|
+
payload: T
|
|
16
|
+
) => Promise<string>
|
|
12
17
|
|
|
13
18
|
/**
|
|
14
19
|
* Decodes a JWT into its payload.
|
|
@@ -10,7 +10,7 @@ export class LocalSecretService implements SecretService {
|
|
|
10
10
|
* Creates an instance of LocalSecretService.
|
|
11
11
|
*/
|
|
12
12
|
constructor(
|
|
13
|
-
private
|
|
13
|
+
private variables: VariablesService = new LocalVariablesService()
|
|
14
14
|
) {}
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -20,7 +20,7 @@ export class LocalSecretService implements SecretService {
|
|
|
20
20
|
* @throws {Error} If the secret is not found.
|
|
21
21
|
*/
|
|
22
22
|
public async getSecretJSON<R>(key: string): Promise<R> {
|
|
23
|
-
const value = await this.
|
|
23
|
+
const value = await this.variables.get(key)
|
|
24
24
|
if (value) {
|
|
25
25
|
return JSON.parse(value)
|
|
26
26
|
}
|
|
@@ -34,7 +34,7 @@ export class LocalSecretService implements SecretService {
|
|
|
34
34
|
* @throws {Error} If the secret is not found.
|
|
35
35
|
*/
|
|
36
36
|
public async getSecret(key: string): Promise<string> {
|
|
37
|
-
const value = await this.
|
|
37
|
+
const value = await this.variables.get(key)
|
|
38
38
|
if (value) {
|
|
39
39
|
return value
|
|
40
40
|
}
|
|
@@ -2,6 +2,7 @@ import { ChannelStore } from '../channel/channel-store.js'
|
|
|
2
2
|
import { CoreUserSession } from '../types/core.types.js'
|
|
3
3
|
|
|
4
4
|
export interface UserSessionService<UserSession extends CoreUserSession> {
|
|
5
|
+
sessionChanged: boolean
|
|
5
6
|
setInitial(session: UserSession): void
|
|
6
7
|
set(session: UserSession): Promise<void> | void
|
|
7
8
|
clear(): Promise<void> | void
|
|
@@ -11,6 +12,7 @@ export interface UserSessionService<UserSession extends CoreUserSession> {
|
|
|
11
12
|
export class PikkuUserSessionService<UserSession extends CoreUserSession>
|
|
12
13
|
implements UserSessionService<UserSession>
|
|
13
14
|
{
|
|
15
|
+
public sessionChanged = false
|
|
14
16
|
private session: UserSession | undefined
|
|
15
17
|
constructor(
|
|
16
18
|
private channelStore?: ChannelStore<unknown, unknown, UserSession>,
|
|
@@ -26,11 +28,13 @@ export class PikkuUserSessionService<UserSession extends CoreUserSession>
|
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
public set(session: UserSession) {
|
|
31
|
+
this.sessionChanged = true
|
|
29
32
|
this.session = session
|
|
30
33
|
return this.channelStore?.setUserSession(this.channelId!, session)
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
public clear() {
|
|
37
|
+
this.sessionChanged = true
|
|
34
38
|
this.session = undefined
|
|
35
39
|
return this.channelStore?.setUserSession(this.channelId!, null)
|
|
36
40
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
getRelativeTimeOffset,
|
|
6
|
+
getRelativeTimeOffsetFromNow,
|
|
7
|
+
RelativeTimeInput,
|
|
8
|
+
} from './time-utils'
|
|
9
|
+
|
|
10
|
+
const approxEqual = (a: number, b: number, delta: number = 10): boolean =>
|
|
11
|
+
Math.abs(a - b) <= delta
|
|
12
|
+
|
|
13
|
+
describe('Time Utils', () => {
|
|
14
|
+
const offsetCases: Array<{ input: RelativeTimeInput; expected: number }> = [
|
|
15
|
+
{ input: { value: 1, unit: 'second' }, expected: 1000 },
|
|
16
|
+
{ input: { value: 2, unit: 'minute' }, expected: 120000 },
|
|
17
|
+
{ input: { value: 1.5, unit: 'hour' }, expected: 5400000 },
|
|
18
|
+
{ input: { value: -1, unit: 'day' }, expected: -86400000 },
|
|
19
|
+
{ input: { value: 1, unit: 'week' }, expected: 604800000 },
|
|
20
|
+
{
|
|
21
|
+
input: { value: 2, unit: 'year' },
|
|
22
|
+
expected: Math.round(2 * 365.25 * 86400 * 1000),
|
|
23
|
+
},
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
describe('getRelativeTimeOffset', () => {
|
|
27
|
+
for (const { input, expected } of offsetCases) {
|
|
28
|
+
test(`returns ${expected} ms for ${input.value} ${input.unit}`, () => {
|
|
29
|
+
const actual = getRelativeTimeOffset(input)
|
|
30
|
+
assert.strictEqual(actual, expected)
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
describe('getRelativeTimeOffsetFromNow', () => {
|
|
36
|
+
test('returns a Date ~1 minute in the future', () => {
|
|
37
|
+
const now = Date.now()
|
|
38
|
+
const result = getRelativeTimeOffsetFromNow({ value: 1, unit: 'minute' })
|
|
39
|
+
const delta = result.getTime() - now
|
|
40
|
+
assert.ok(
|
|
41
|
+
approxEqual(delta, 60000),
|
|
42
|
+
`Expected ~60000ms ahead, got ${delta}ms`
|
|
43
|
+
)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('returns a Date ~2 hours in the past', () => {
|
|
47
|
+
const now = Date.now()
|
|
48
|
+
const result = getRelativeTimeOffsetFromNow({ value: -2, unit: 'hour' })
|
|
49
|
+
const delta = now - result.getTime()
|
|
50
|
+
assert.ok(
|
|
51
|
+
approxEqual(delta, 7200000),
|
|
52
|
+
`Expected ~7200000ms ago, got ${delta}ms`
|
|
53
|
+
)
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
type TimeUnit = 'second' | 'minute' | 'hour' | 'day' | 'week' | 'year'
|
|
2
|
+
|
|
3
|
+
export interface RelativeTimeInput {
|
|
4
|
+
value: number // negative = in the past
|
|
5
|
+
unit: TimeUnit
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const multipliers: Record<TimeUnit, number> = {
|
|
9
|
+
second: 1,
|
|
10
|
+
minute: 60,
|
|
11
|
+
hour: 60 * 60,
|
|
12
|
+
day: 60 * 60 * 24,
|
|
13
|
+
week: 60 * 60 * 24 * 7,
|
|
14
|
+
year: Math.round(365.25 * 60 * 60 * 24),
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const getRelativeTimeOffset = ({
|
|
18
|
+
value,
|
|
19
|
+
unit,
|
|
20
|
+
}: RelativeTimeInput): number => {
|
|
21
|
+
return Math.round(value * multipliers[unit]) * 1000 // convert to milliseconds
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Returns a Unix timestamp (in seconds) offset from now by the given duration.
|
|
26
|
+
* e.g. value = -1 and unit = 'day' => 1 day ago => now - 86400 seconds
|
|
27
|
+
*/
|
|
28
|
+
export const getRelativeTimeOffsetFromNow = (
|
|
29
|
+
relativeTime: RelativeTimeInput
|
|
30
|
+
): Date => {
|
|
31
|
+
return new Date(Date.now() + getRelativeTimeOffset(relativeTime))
|
|
32
|
+
}
|
package/src/types/core.types.ts
CHANGED
|
@@ -65,17 +65,17 @@ export interface CoreSingletonServices<Config extends CoreConfig = CoreConfig> {
|
|
|
65
65
|
/** JWT Service */
|
|
66
66
|
jwt?: JWTService
|
|
67
67
|
/** The schema library used to validate data */
|
|
68
|
-
|
|
68
|
+
schema?: SchemaService
|
|
69
69
|
/** The core configuration for the application. */
|
|
70
70
|
config: Config
|
|
71
71
|
/** The logger used by the application. */
|
|
72
72
|
logger: Logger
|
|
73
73
|
/** The variable service to be used */
|
|
74
|
-
|
|
74
|
+
variables: VariablesService
|
|
75
75
|
/** The subscription service that is passed to streams */
|
|
76
76
|
eventHub?: EventHubService<unknown>
|
|
77
77
|
/** SecretServce */
|
|
78
|
-
|
|
78
|
+
secrets?: SecretService
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
/**
|
|
@@ -93,7 +93,7 @@ export type PikkuMiddleware<
|
|
|
93
93
|
UserSession extends CoreUserSession = CoreUserSession,
|
|
94
94
|
> = (
|
|
95
95
|
services: SingletonServices & {
|
|
96
|
-
|
|
96
|
+
userSession: UserSessionService<UserSession>
|
|
97
97
|
},
|
|
98
98
|
interactions: PikkuInteraction,
|
|
99
99
|
next: () => Promise<void>
|
|
@@ -145,7 +145,7 @@ export type CreateSessionServices<
|
|
|
145
145
|
* Defines a function type for creating config.
|
|
146
146
|
*/
|
|
147
147
|
export type CreateConfig<Config extends CoreConfig> = (
|
|
148
|
-
|
|
148
|
+
variables?: VariablesService
|
|
149
149
|
) => Promise<Config>
|
|
150
150
|
|
|
151
151
|
/**
|