@pikku/core 0.6.23 → 0.6.25

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 (63) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/channel/channel-runner.d.ts +2 -2
  3. package/dist/channel/channel-runner.js +5 -5
  4. package/dist/channel/channel.types.d.ts +1 -1
  5. package/dist/channel/local/local-channel-runner.d.ts +1 -1
  6. package/dist/channel/local/local-channel-runner.js +7 -7
  7. package/dist/channel/serverless/serverless-channel-runner.d.ts +1 -1
  8. package/dist/channel/serverless/serverless-channel-runner.js +6 -6
  9. package/dist/http/http-route-runner.d.ts +1 -1
  10. package/dist/http/http-route-runner.js +15 -14
  11. package/dist/http/http-routes.types.d.ts +1 -1
  12. package/dist/http/pikku-fetch-http-request.d.ts +1 -0
  13. package/dist/http/pikku-fetch-http-request.js +64 -8
  14. package/dist/http/pikku-fetch-http-response.d.ts +2 -1
  15. package/dist/http/pikku-fetch-http-response.js +5 -1
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +1 -0
  18. package/dist/middleware/auth-apikey.js +2 -2
  19. package/dist/middleware/auth-bearer.js +2 -2
  20. package/dist/middleware/auth-cookie.d.ts +7 -3
  21. package/dist/middleware/auth-cookie.js +34 -17
  22. package/dist/middleware-runner.d.ts +3 -3
  23. package/dist/middleware-runner.js +2 -2
  24. package/dist/schema.d.ts +1 -1
  25. package/dist/schema.js +4 -1
  26. package/dist/services/jwt-service.d.ts +2 -1
  27. package/dist/services/local-secrets.d.ts +2 -2
  28. package/dist/services/local-secrets.js +5 -5
  29. package/dist/services/user-session-service.d.ts +2 -0
  30. package/dist/services/user-session-service.js +3 -0
  31. package/dist/time-utils.d.ts +12 -0
  32. package/dist/time-utils.js +18 -0
  33. package/dist/types/core.types.d.ts +5 -5
  34. package/dist/types/functions.types.d.ts +1 -1
  35. package/lcov.info +1767 -1031
  36. package/package.json +1 -1
  37. package/src/channel/channel-runner.ts +6 -6
  38. package/src/channel/channel.types.ts +1 -1
  39. package/src/channel/local/local-channel-runner.test.ts +0 -2
  40. package/src/channel/local/local-channel-runner.ts +7 -7
  41. package/src/channel/serverless/serverless-channel-runner.ts +6 -9
  42. package/src/http/http-route-runner.test.ts +1 -1
  43. package/src/http/http-route-runner.ts +25 -17
  44. package/src/http/http-routes.types.ts +1 -1
  45. package/src/http/pikku-fetch-http-request.test.ts +237 -0
  46. package/src/http/pikku-fetch-http-request.ts +64 -8
  47. package/src/http/pikku-fetch-http-response.test.ts +82 -0
  48. package/src/http/pikku-fetch-http-response.ts +16 -2
  49. package/src/index.ts +1 -1
  50. package/src/middleware/auth-apikey.ts +2 -2
  51. package/src/middleware/auth-bearer.ts +2 -2
  52. package/src/middleware/auth-cookie.ts +51 -21
  53. package/src/middleware-runner.ts +3 -3
  54. package/src/schema.test.ts +6 -6
  55. package/src/schema.ts +3 -1
  56. package/src/services/jwt-service.ts +6 -1
  57. package/src/services/local-secrets.ts +3 -3
  58. package/src/services/user-session-service.ts +4 -0
  59. package/src/time-utils.test.ts +56 -0
  60. package/src/time-utils.ts +32 -0
  61. package/src/types/core.types.ts +5 -5
  62. package/src/types/functions.types.ts +1 -1
  63. package/tsconfig.tsbuildinfo +1 -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
+ })
@@ -1,8 +1,14 @@
1
1
  import { PikkuHTTPResponse } from './http-routes.types.js'
2
+ import {
3
+ SerializeOptions as CookieSerializeOptions,
4
+ serialize as serializeCookie,
5
+ } from 'cookie'
2
6
 
3
7
  export class PikkuFetchHTTPResponse implements PikkuHTTPResponse {
4
8
  #statusCode: number = 200
5
9
  #headers = new Headers()
10
+ #cookies = new Map<string, { value: string; flags: CookieSerializeOptions }>()
11
+
6
12
  #body: BodyInit | null = null
7
13
 
8
14
  public status(code: number): this {
@@ -10,8 +16,12 @@ export class PikkuFetchHTTPResponse implements PikkuHTTPResponse {
10
16
  return this
11
17
  }
12
18
 
13
- public cookie(name: string, value: string, flags: any): this {
14
- // TODO
19
+ public cookie(
20
+ name: string,
21
+ value: string,
22
+ flags: CookieSerializeOptions
23
+ ): this {
24
+ this.#cookies.set(name, { value, flags })
15
25
  return this
16
26
  }
17
27
 
@@ -61,6 +71,10 @@ export class PikkuFetchHTTPResponse implements PikkuHTTPResponse {
61
71
  }
62
72
 
63
73
  public toResponse(args?: Record<string, any>): Response {
74
+ const cookieHeader = Array.from(this.#cookies.entries()).map(
75
+ ([name, { value, flags }]) => serializeCookie(name, value, flags)
76
+ )
77
+ this.#headers.set('Set-Cookie', cookieHeader.join(', '))
64
78
  return new Response(this.#body, {
65
79
  ...args,
66
80
  status: this.#statusCode,
package/src/index.ts CHANGED
@@ -11,7 +11,7 @@ export * from './channel/index.js'
11
11
  export * from './scheduler/index.js'
12
12
  export * from './errors/index.js'
13
13
  export * from './middleware/index.js'
14
-
14
+ export * from './time-utils.js'
15
15
  export { pikkuState } from './pikku-state.js'
16
16
  export { runMiddleware } from './middleware-runner.js'
17
17
  export { addRoute, addMiddleware } from './http/http-route-runner.js'
@@ -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.userSessionService.get()) {
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.userSessionService.setInitial(userSession)
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.userSessionService.get()) {
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.userSessionService.setInitial(userSession)
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.cookieNames - List of cookie names to check.
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
- cookieNames,
23
+ name,
19
24
  getSessionForCookieValue,
20
25
  jwt,
26
+ options,
27
+ expiresIn,
21
28
  }: {
22
- cookieNames: string[]
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.userSessionService.get()) {
47
+ if (!http?.request || services.userSession.get()) {
39
48
  return next()
40
49
  }
41
50
 
42
51
  let userSession: UserSession | null = null
43
- for (const cookieName of cookieNames) {
44
- const cookieValue = http.request.cookie(cookieName)
45
- if (cookieValue) {
46
- if (jwt) {
47
- if (!services.jwt) {
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
- break
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.userSessionService.setInitial(userSession)
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
  }
@@ -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, userSessionService, etc.)
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, userSessionService },
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
- userSessionService: UserSessionService<any>
27
+ userSession: UserSessionService<any>
28
28
  },
29
29
  interaction: PikkuInteraction,
30
30
  middlewares: PikkuMiddleware[],
@@ -1,6 +1,6 @@
1
1
  import { test, describe, before } from 'node:test'
2
2
  import * as assert from 'assert'
3
- import { addSchema, coerceQueryStringToArray } from './schema.js'
3
+ import { addSchema, coerceTopLevelDataFromSchema } from './schema.js'
4
4
 
5
5
  describe('Schema', () => {
6
6
  describe('coerceQueryStringToArray', () => {
@@ -24,32 +24,32 @@ describe('Schema', () => {
24
24
 
25
25
  test('should split a string into an array for properties of type array', () => {
26
26
  const data = { tags: 'a,b,c' }
27
- coerceQueryStringToArray('testSchema', data)
27
+ coerceTopLevelDataFromSchema('testSchema', data)
28
28
  assert.deepStrictEqual(data.tags, ['a', 'b', 'c'])
29
29
  })
30
30
 
31
31
  test('should not modify properties of type array if they are already arrays', () => {
32
32
  const data = { tags: ['a', 'b', 'c'] }
33
- coerceQueryStringToArray('testSchema', data)
33
+ coerceTopLevelDataFromSchema('testSchema', data)
34
34
  assert.deepStrictEqual(data.tags, ['a', 'b', 'c'])
35
35
  })
36
36
 
37
37
  test('should not modify properties that are not type array', () => {
38
38
  const data = { count: 5, name: 'example' }
39
- coerceQueryStringToArray('testSchema', data)
39
+ coerceTopLevelDataFromSchema('testSchema', data)
40
40
  assert.strictEqual(data.count, 5)
41
41
  assert.strictEqual(data.name, 'example')
42
42
  })
43
43
 
44
44
  test('should handle cases where the data object does not have a key present in the schema', () => {
45
45
  const data = { unknownKey: 'shouldRemain' }
46
- coerceQueryStringToArray('testSchema', data)
46
+ coerceTopLevelDataFromSchema('testSchema', data)
47
47
  assert.strictEqual(data.unknownKey, 'shouldRemain')
48
48
  })
49
49
 
50
50
  test('should handle cases where schema properties contain boolean values', () => {
51
51
  const data = { tags: 'a,b,c', isActive: 'true' }
52
- coerceQueryStringToArray('booleanSchema', data)
52
+ coerceTopLevelDataFromSchema('booleanSchema', data)
53
53
  assert.deepStrictEqual(data.tags, ['a', 'b', 'c'])
54
54
  assert.strictEqual(data.isActive, 'true') // No coercion should happen
55
55
  })
package/src/schema.ts CHANGED
@@ -58,7 +58,7 @@ const validateAllSchemasLoaded = (
58
58
  }
59
59
  }
60
60
 
61
- export const coerceQueryStringToArray = (schemaName: string, data: any) => {
61
+ export const coerceTopLevelDataFromSchema = (schemaName: string, data: any) => {
62
62
  const schema = pikkuState('misc', 'schemas').get(schemaName)
63
63
  for (const key in schema.properties) {
64
64
  const property = schema.properties[key]
@@ -71,6 +71,8 @@ export const coerceQueryStringToArray = (schemaName: string, data: any) => {
71
71
  }
72
72
  if (type === 'array' && typeof data[key] === 'string') {
73
73
  data[key] = data[key].split(',')
74
+ } else if (type === 'string' && property.format === 'date-time') {
75
+ data[key] = new Date(data[key])
74
76
  }
75
77
  }
76
78
  }
@@ -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>(expiresIn: string, payload: T) => Promise<string>
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 variablesService: VariablesService = new LocalVariablesService()
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.variablesService.get(key)
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.variablesService.get(key)
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
+ }
@@ -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
- schemaService?: SchemaService
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
- variablesService: VariablesService
74
+ variables: VariablesService
75
75
  /** The subscription service that is passed to streams */
76
76
  eventHub?: EventHubService<unknown>
77
77
  /** SecretServce */
78
- secretService?: SecretService
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
- userSessionService: UserSessionService<UserSession>
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
- variablesService?: VariablesService
148
+ variables?: VariablesService
149
149
  ) => Promise<Config>
150
150
 
151
151
  /**
@@ -46,7 +46,7 @@ export type CoreAPIFunctionSessionless<
46
46
  > = (
47
47
  services: Services,
48
48
  data: In,
49
- session: Session
49
+ session?: Session
50
50
  ) => Channel extends true ? Promise<Out> | Promise<void> : Promise<Out>
51
51
 
52
52
  /**