@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.
Files changed (52) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/channel/channel-runner.d.ts +1 -1
  3. package/dist/channel/channel-runner.js +1 -1
  4. package/dist/channel/local/local-channel-runner.js +5 -5
  5. package/dist/channel/serverless/serverless-channel-runner.js +4 -4
  6. package/dist/http/http-route-runner.js +9 -8
  7. package/dist/http/pikku-fetch-http-request.d.ts +1 -0
  8. package/dist/http/pikku-fetch-http-request.js +64 -8
  9. package/dist/http/pikku-fetch-http-response.d.ts +2 -1
  10. package/dist/http/pikku-fetch-http-response.js +5 -1
  11. package/dist/index.d.ts +1 -0
  12. package/dist/index.js +1 -0
  13. package/dist/middleware/auth-apikey.js +2 -2
  14. package/dist/middleware/auth-bearer.js +2 -2
  15. package/dist/middleware/auth-cookie.d.ts +7 -3
  16. package/dist/middleware/auth-cookie.js +34 -17
  17. package/dist/middleware-runner.d.ts +3 -3
  18. package/dist/middleware-runner.js +2 -2
  19. package/dist/services/jwt-service.d.ts +2 -1
  20. package/dist/services/local-secrets.d.ts +2 -2
  21. package/dist/services/local-secrets.js +5 -5
  22. package/dist/services/user-session-service.d.ts +2 -0
  23. package/dist/services/user-session-service.js +3 -0
  24. package/dist/time-utils.d.ts +12 -0
  25. package/dist/time-utils.js +18 -0
  26. package/dist/types/core.types.d.ts +5 -5
  27. package/dist/types/functions.types.d.ts +1 -1
  28. package/lcov.info +1751 -1018
  29. package/package.json +1 -1
  30. package/src/channel/channel-runner.ts +2 -2
  31. package/src/channel/local/local-channel-runner.test.ts +0 -2
  32. package/src/channel/local/local-channel-runner.ts +5 -5
  33. package/src/channel/serverless/serverless-channel-runner.ts +4 -7
  34. package/src/http/http-route-runner.test.ts +1 -1
  35. package/src/http/http-route-runner.ts +18 -10
  36. package/src/http/pikku-fetch-http-request.test.ts +237 -0
  37. package/src/http/pikku-fetch-http-request.ts +64 -8
  38. package/src/http/pikku-fetch-http-response.test.ts +82 -0
  39. package/src/http/pikku-fetch-http-response.ts +16 -2
  40. package/src/index.ts +1 -1
  41. package/src/middleware/auth-apikey.ts +2 -2
  42. package/src/middleware/auth-bearer.ts +2 -2
  43. package/src/middleware/auth-cookie.ts +51 -21
  44. package/src/middleware-runner.ts +3 -3
  45. package/src/services/jwt-service.ts +6 -1
  46. package/src/services/local-secrets.ts +3 -3
  47. package/src/services/user-session-service.ts +4 -0
  48. package/src/time-utils.test.ts +56 -0
  49. package/src/time-utils.ts +32 -0
  50. package/src/types/core.types.ts +5 -5
  51. package/src/types/functions.types.ts +1 -1
  52. package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  ## 0.6
2
2
 
3
+ ## 0.6.24
4
+
5
+ ### Patch Changes
6
+
7
+ - 531f4b5: refactor: using userSession.set to set cookies with middleware
8
+
3
9
  ## 0.6.23
4
10
 
5
11
  ### Patch Changes
@@ -8,7 +8,7 @@ export declare const getMatchingChannelConfig: (request: string) => {
8
8
  schemaName: string | null | undefined;
9
9
  } | null;
10
10
  export declare const openChannel: ({ route, singletonServices, coerceToArray, request, }: Pick<CoreAPIChannel<unknown, string>, "route"> & RunChannelParams<unknown> & {
11
- userSessionService: UserSessionService<any>;
11
+ userSession: UserSessionService<any>;
12
12
  } & RunChannelOptions) => Promise<{
13
13
  openingData: unknown;
14
14
  channelConfig: CoreAPIChannel<unknown, any>;
@@ -43,7 +43,7 @@ export const openChannel = async ({ route, singletonServices, coerceToArray = fa
43
43
  if (coerceToArray && schemaName) {
44
44
  coerceQueryStringToArray(schemaName, openingData);
45
45
  }
46
- await validateSchema(singletonServices.logger, singletonServices.schemaService, schemaName, openingData);
46
+ await validateSchema(singletonServices.logger, singletonServices.schema, schemaName, openingData);
47
47
  }
48
48
  return { openingData, channelConfig };
49
49
  };
@@ -9,7 +9,7 @@ import { PikkuUserSessionService } from '../../services/user-session-service.js'
9
9
  export const runLocalChannel = async ({ singletonServices, channelId, request, response, route, createSessionServices, skipUserSession = false, respondWith404 = true, coerceToArray = false, logWarningsForStatusCodes = [], bubbleErrors = false, }) => {
10
10
  let sessionServices;
11
11
  let channelHandler;
12
- const userSessionService = new PikkuUserSessionService();
12
+ const userSession = new PikkuUserSessionService();
13
13
  let http;
14
14
  if (request) {
15
15
  http = createHTTPInteraction(request, response);
@@ -27,18 +27,18 @@ export const runLocalChannel = async ({ singletonServices, channelId, request, r
27
27
  singletonServices,
28
28
  skipUserSession,
29
29
  coerceToArray,
30
- userSessionService,
30
+ userSession,
31
31
  });
32
32
  channelHandler = new PikkuLocalChannelHandler(channelId, channelConfig.name, openingData);
33
33
  const channel = channelHandler.getChannel();
34
- const session = await userSessionService.get();
34
+ const session = await userSession.get();
35
35
  if (createSessionServices) {
36
36
  sessionServices = await createSessionServices(singletonServices, { http }, session);
37
37
  }
38
38
  const allServices = {
39
39
  ...singletonServices,
40
40
  ...sessionServices,
41
- userSession: userSessionService,
41
+ userSession: userSession,
42
42
  };
43
43
  channelHandler.registerOnOpen(() => {
44
44
  channelConfig.onConnect?.(allServices, channel);
@@ -62,7 +62,7 @@ export const runLocalChannel = async ({ singletonServices, channelId, request, r
62
62
  };
63
63
  await runMiddleware({
64
64
  ...singletonServices,
65
- userSessionService,
65
+ userSession,
66
66
  }, { http }, route.middleware || [], main);
67
67
  return channelHandler;
68
68
  };
@@ -26,7 +26,7 @@ export const runChannelConnect = async ({ singletonServices, channelId, channelO
26
26
  if (request instanceof Request) {
27
27
  http = createHTTPInteraction(new PikkuFetchHTTPRequest(request), response);
28
28
  }
29
- const userSessionService = new PikkuUserSessionService(channelStore, channelId);
29
+ const userSession = new PikkuUserSessionService(channelStore, channelId);
30
30
  const { channelConfig, openingData } = await openChannel({
31
31
  channelId,
32
32
  createSessionServices,
@@ -34,7 +34,7 @@ export const runChannelConnect = async ({ singletonServices, channelId, channelO
34
34
  route,
35
35
  singletonServices,
36
36
  coerceToArray,
37
- userSessionService,
37
+ userSession,
38
38
  });
39
39
  const main = async () => {
40
40
  try {
@@ -50,7 +50,7 @@ export const runChannelConnect = async ({ singletonServices, channelId, channelO
50
50
  channelName: channelConfig.name,
51
51
  });
52
52
  if (createSessionServices) {
53
- sessionServices = await createSessionServices(singletonServices, { http }, await userSessionService.get());
53
+ sessionServices = await createSessionServices(singletonServices, { http }, await userSession.get());
54
54
  }
55
55
  await channelConfig.onConnect?.({ ...singletonServices, ...sessionServices }, channel);
56
56
  http?.response?.status(101);
@@ -66,7 +66,7 @@ export const runChannelConnect = async ({ singletonServices, channelId, channelO
66
66
  };
67
67
  await runMiddleware({
68
68
  ...singletonServices,
69
- userSessionService,
69
+ userSession,
70
70
  }, { http }, channelConfig.middleware || [], main);
71
71
  };
72
72
  export const runChannelDisconnect = async ({ singletonServices, ...params }) => {
@@ -3,7 +3,7 @@ import { match } from 'path-to-regexp';
3
3
  import { ForbiddenError, MissingSessionError, NotFoundError, } from '../errors/errors.js';
4
4
  import { closeSessionServices } from '../utils.js';
5
5
  import { coerceQueryStringToArray, validateSchema } from '../schema.js';
6
- import { PikkuUserSessionService } from '../services/user-session-service.js';
6
+ import { PikkuUserSessionService, } from '../services/user-session-service.js';
7
7
  import { runMiddleware } from '../middleware-runner.js';
8
8
  import { handleError } from '../handle-error.js';
9
9
  import { pikkuState } from '../pikku-state.js';
@@ -142,7 +142,7 @@ export const createHTTPInteraction = (request, response) => {
142
142
  */
143
143
  const executeRouteWithMiddleware = async (services, matchedRoute, http, options) => {
144
144
  const { matchedPath, params, route, middleware, schemaName } = matchedRoute;
145
- const { singletonServices, userSessionService, createSessionServices, skipUserSession, } = services;
145
+ const { singletonServices, userSession, createSessionServices, skipUserSession, } = services;
146
146
  const requiresSession = route.auth !== false;
147
147
  let sessionServices;
148
148
  let result;
@@ -151,7 +151,7 @@ const executeRouteWithMiddleware = async (services, matchedRoute, http, options)
151
151
  singletonServices.logger.info(`Matched route: ${route.route} | method: ${route.method.toUpperCase()} | auth: ${requiresSession.toString()}`);
152
152
  // Main route execution logic wrapped for middleware handling
153
153
  const runMain = async () => {
154
- const session = userSessionService.get();
154
+ const session = userSession.get();
155
155
  // Ensure session is available when required
156
156
  if (skipUserSession && requiresSession) {
157
157
  throw new Error("Can't skip trying to get user session if auth is required");
@@ -164,15 +164,16 @@ const executeRouteWithMiddleware = async (services, matchedRoute, http, options)
164
164
  throw new MissingSessionError();
165
165
  }
166
166
  // Create session-specific services for handling the request
167
- sessionServices = await createSessionServices({ ...singletonServices, userSessionService }, { http }, session);
167
+ sessionServices = await createSessionServices({ ...singletonServices, userSession }, { http }, session);
168
168
  const allServices = {
169
169
  ...singletonServices,
170
170
  ...sessionServices,
171
+ userSession,
171
172
  http,
172
173
  };
173
174
  const data = await http?.request?.data();
174
175
  // Validate request data against the defined schema, if any
175
- await validateSchema(singletonServices.logger, singletonServices.schemaService, schemaName, data);
176
+ await validateSchema(singletonServices.logger, singletonServices.schema, schemaName, data);
176
177
  // Coerce query string parameters to arrays if specified by the schema
177
178
  if (options.coerceToArray && schemaName) {
178
179
  coerceQueryStringToArray(schemaName, data);
@@ -197,7 +198,7 @@ const executeRouteWithMiddleware = async (services, matchedRoute, http, options)
197
198
  return result;
198
199
  };
199
200
  // Execute middleware, then run the main logic
200
- await runMiddleware({ ...singletonServices, userSessionService }, { http }, middleware, runMain);
201
+ await runMiddleware({ ...singletonServices, userSession }, { http }, middleware, runMain);
201
202
  return sessionServices ? { result, sessionServices } : { result };
202
203
  };
203
204
  /**
@@ -256,7 +257,7 @@ export const pikkuFetch = async (request, params) => {
256
257
  * @returns {Promise<Out | void>} The output from the route handler or void if an error occurred.
257
258
  */
258
259
  export const fetchData = async (request, response, { singletonServices, createSessionServices, skipUserSession = false, respondWith404 = true, logWarningsForStatusCodes = [], coerceToArray = false, bubbleErrors = false, }) => {
259
- const userSessionService = new PikkuUserSessionService();
260
+ const userSession = new PikkuUserSessionService();
260
261
  let sessionServices;
261
262
  let result;
262
263
  // Combine the request and response into one interaction object
@@ -279,7 +280,7 @@ export const fetchData = async (request, response, { singletonServices, createSe
279
280
  ;
280
281
  ({ result, sessionServices } = await executeRouteWithMiddleware({
281
282
  singletonServices,
282
- userSessionService,
283
+ userSession,
283
284
  createSessionServices,
284
285
  skipUserSession,
285
286
  }, matchedRoute, http, { coerceToArray }));
@@ -51,4 +51,5 @@ export declare class PikkuFetchHTTPRequest<In = unknown> implements PikkuHTTPReq
51
51
  * @returns A promise that resolves to an object containing the combined data.
52
52
  */
53
53
  data(): Promise<In>;
54
+ private body;
54
55
  }
@@ -1,5 +1,6 @@
1
1
  import { parse as parseQuery } from 'picoquery';
2
2
  import { parse as parseCookie } from 'cookie';
3
+ import { UnprocessableContentError } from '../errors/errors.js';
3
4
  /**
4
5
  * Abstract class representing a pikku request.
5
6
  * @template In - The type of the request body.
@@ -77,16 +78,71 @@ export class PikkuFetchHTTPRequest {
77
78
  * @returns A promise that resolves to an object containing the combined data.
78
79
  */
79
80
  async data() {
81
+ const body = await this.body();
82
+ const parts = [this.params(), this.query(), body];
83
+ const merged = {};
84
+ for (const part of parts) {
85
+ for (const [key, value] of Object.entries(part)) {
86
+ if (key in merged && !valuesAreEquivalent(merged[key], value)) {
87
+ throw new UnprocessableContentError(`Conflicting values for key "${key}": "${merged[key]}" vs "${value}"`);
88
+ }
89
+ merged[key] ??= value;
90
+ }
91
+ }
92
+ return merged;
93
+ }
94
+ async body() {
95
+ const noBodyMethods = ['get', 'head', 'options'];
96
+ if (noBodyMethods.includes(this.method())) {
97
+ return {};
98
+ }
80
99
  let body = {};
100
+ const contentType = this.header('content-type') || '';
81
101
  try {
82
- body = await this.json();
102
+ if (contentType.includes('application/json')) {
103
+ const parsed = await this.json();
104
+ body =
105
+ typeof parsed === 'object' &&
106
+ parsed !== null &&
107
+ !Array.isArray(parsed)
108
+ ? parsed
109
+ : { data: parsed };
110
+ }
111
+ else if (contentType.includes('text/')) {
112
+ const text = await this.request.text();
113
+ body = { data: text };
114
+ }
115
+ else if (contentType.includes('application/octet-stream')) {
116
+ const buffer = await this.request.arrayBuffer();
117
+ body = { data: buffer };
118
+ }
119
+ else if (contentType === 'application/x-www-form-urlencoded') {
120
+ const text = await this.request.text();
121
+ body = Object.fromEntries(new URLSearchParams(text));
122
+ }
123
+ else {
124
+ throw new UnprocessableContentError(`Unsupported content type ${contentType}`);
125
+ }
126
+ }
127
+ catch (e) {
128
+ throw new UnprocessableContentError(`Error parsing body: ${e}`);
83
129
  }
84
- catch (e) { }
85
- return {
86
- ...this.params(),
87
- ...this.query(),
88
- // TODO: If body isn't an object, we should insert it as the word...
89
- ...body,
90
- };
130
+ return body;
131
+ }
132
+ }
133
+ function valuesAreEquivalent(a, b) {
134
+ return coerce(a) === coerce(b);
135
+ }
136
+ function coerce(value) {
137
+ if (typeof value === 'boolean' || typeof value === 'number')
138
+ return value;
139
+ if (typeof value === 'string') {
140
+ if (value === 'true')
141
+ return true;
142
+ if (value === 'false')
143
+ return false;
144
+ const num = Number(value);
145
+ return isNaN(num) ? value : num;
91
146
  }
147
+ return value;
92
148
  }
@@ -1,8 +1,9 @@
1
1
  import { PikkuHTTPResponse } from './http-routes.types.js';
2
+ import { SerializeOptions as CookieSerializeOptions } from 'cookie';
2
3
  export declare class PikkuFetchHTTPResponse implements PikkuHTTPResponse {
3
4
  #private;
4
5
  status(code: number): this;
5
- cookie(name: string, value: string, flags: any): this;
6
+ cookie(name: string, value: string, flags: CookieSerializeOptions): this;
6
7
  header(name: string, value: string | string[]): this;
7
8
  arrayBuffer(data: XMLHttpRequestBodyInit): this;
8
9
  json(data: unknown): this;
@@ -1,13 +1,15 @@
1
+ import { serialize as serializeCookie, } from 'cookie';
1
2
  export class PikkuFetchHTTPResponse {
2
3
  #statusCode = 200;
3
4
  #headers = new Headers();
5
+ #cookies = new Map();
4
6
  #body = null;
5
7
  status(code) {
6
8
  this.#statusCode = code;
7
9
  return this;
8
10
  }
9
11
  cookie(name, value, flags) {
10
- // TODO
12
+ this.#cookies.set(name, { value, flags });
11
13
  return this;
12
14
  }
13
15
  header(name, value) {
@@ -50,6 +52,8 @@ export class PikkuFetchHTTPResponse {
50
52
  return this;
51
53
  }
52
54
  toResponse(args) {
55
+ const cookieHeader = Array.from(this.#cookies.entries()).map(([name, { value, flags }]) => serializeCookie(name, value, flags));
56
+ this.#headers.set('Set-Cookie', cookieHeader.join(', '));
53
57
  return new Response(this.#body, {
54
58
  ...args,
55
59
  status: this.#statusCode,
package/dist/index.d.ts CHANGED
@@ -11,6 +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
+ export * from './time-utils.js';
14
15
  export { pikkuState } from './pikku-state.js';
15
16
  export { runMiddleware } from './middleware-runner.js';
16
17
  export { addRoute, addMiddleware } from './http/http-route-runner.js';
package/dist/index.js CHANGED
@@ -11,6 +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
+ export * from './time-utils.js';
14
15
  export { pikkuState } from './pikku-state.js';
15
16
  export { runMiddleware } from './middleware-runner.js';
16
17
  export { addRoute, addMiddleware } from './http/http-route-runner.js';
@@ -5,7 +5,7 @@
5
5
  */
6
6
  export const authAPIKey = ({ source, getSessionForAPIKey, jwt, }) => {
7
7
  const middleware = async (services, { http }, next) => {
8
- if (!http?.request || services.userSessionService.get()) {
8
+ if (!http?.request || services.userSession.get()) {
9
9
  return next();
10
10
  }
11
11
  let apiKey = null;
@@ -27,7 +27,7 @@ export const authAPIKey = ({ source, getSessionForAPIKey, jwt, }) => {
27
27
  userSession = await getSessionForAPIKey(services, apiKey);
28
28
  }
29
29
  if (userSession) {
30
- services.userSessionService.setInitial(userSession);
30
+ services.userSession.setInitial(userSession);
31
31
  }
32
32
  }
33
33
  return next();
@@ -5,7 +5,7 @@ import { InvalidSessionError } from '../errors/errors.js';
5
5
  export const authBearer = ({ token, jwt, getSession, } = {}) => {
6
6
  const middleware = async (services, { http }, next) => {
7
7
  // Skip if session already exists.
8
- if (!http?.request || services.userSessionService.get()) {
8
+ if (!http?.request || services.userSession.get()) {
9
9
  return next();
10
10
  }
11
11
  const authHeader = http.request.header('authorization') ||
@@ -31,7 +31,7 @@ export const authBearer = ({ token, jwt, getSession, } = {}) => {
31
31
  userSession = await getSession(services, token);
32
32
  }
33
33
  if (userSession) {
34
- services.userSessionService.setInitial(userSession);
34
+ services.userSession.setInitial(userSession);
35
35
  }
36
36
  }
37
37
  return next();
@@ -1,12 +1,16 @@
1
+ import { SerializeOptions } from 'cookie';
1
2
  import { CoreConfig, CoreSingletonServices, CoreUserSession, PikkuMiddleware } from '../types/core.types.js';
3
+ import { RelativeTimeInput } from '../time-utils.js';
2
4
  /**
3
5
  * Cookie middleware that extracts a session from cookies.
4
6
  *
5
- * @param options.cookieNames - List of cookie names to check.
7
+ * @param options.name - List of cookie names to check.
6
8
  * @param options.getSessionForCookieValue - Function to retrieve a session using a cookie value.
7
9
  */
8
- export declare const authCookie: <SingletonServices extends CoreSingletonServices<CoreConfig>, UserSession extends CoreUserSession>({ cookieNames, getSessionForCookieValue, jwt, }: {
9
- cookieNames: string[];
10
+ export declare const authCookie: <SingletonServices extends CoreSingletonServices<CoreConfig>, UserSession extends CoreUserSession>({ name, getSessionForCookieValue, jwt, options, expiresIn, }: {
11
+ name: string;
12
+ options: SerializeOptions;
13
+ expiresIn: RelativeTimeInput;
10
14
  } & ({
11
15
  getSessionForCookieValue: (services: SingletonServices, cookieValue: string, cookieName: string) => Promise<UserSession>;
12
16
  jwt?: false;
@@ -1,34 +1,51 @@
1
+ import { getRelativeTimeOffsetFromNow, } from '../time-utils.js';
1
2
  /**
2
3
  * Cookie middleware that extracts a session from cookies.
3
4
  *
4
- * @param options.cookieNames - List of cookie names to check.
5
+ * @param options.name - List of cookie names to check.
5
6
  * @param options.getSessionForCookieValue - Function to retrieve a session using a cookie value.
6
7
  */
7
- export const authCookie = ({ cookieNames, getSessionForCookieValue, jwt, }) => {
8
+ export const authCookie = ({ name, getSessionForCookieValue, jwt, options, expiresIn, }) => {
8
9
  const middleware = async (services, { http }, next) => {
9
- if (!http?.request || services.userSessionService.get()) {
10
+ if (!http?.request || services.userSession.get()) {
10
11
  return next();
11
12
  }
12
13
  let userSession = null;
13
- for (const cookieName of cookieNames) {
14
- const cookieValue = http.request.cookie(cookieName);
15
- if (cookieValue) {
16
- if (jwt) {
17
- if (!services.jwt) {
18
- throw new Error('JWT service is required for JWT decoding.');
19
- }
20
- userSession = await services.jwt.decode(cookieValue);
14
+ const cookieValue = http.request.cookie(name);
15
+ if (cookieValue) {
16
+ if (jwt) {
17
+ if (!services.jwt) {
18
+ throw new Error('JWT service is required for JWT decoding.');
21
19
  }
22
- else if (getSessionForCookieValue) {
23
- userSession = await getSessionForCookieValue(services, cookieValue, cookieName);
24
- }
25
- break;
20
+ userSession = await services.jwt.decode(cookieValue);
21
+ }
22
+ else if (getSessionForCookieValue) {
23
+ userSession = await getSessionForCookieValue(services, cookieValue, name);
26
24
  }
27
25
  }
28
26
  if (userSession) {
29
- services.userSessionService.setInitial(userSession);
27
+ services.userSession.setInitial(userSession);
28
+ }
29
+ await next();
30
+ // Set the cookie in the response if the session has changed
31
+ if (!http?.response) {
32
+ return;
33
+ }
34
+ if (services.userSession.sessionChanged) {
35
+ const session = services.userSession.get();
36
+ if (jwt) {
37
+ if (!services.jwt) {
38
+ throw new Error('JWT service is required for JWT encoding.');
39
+ }
40
+ http.response.cookie(name, await services.jwt.encode(expiresIn, session), {
41
+ ...options,
42
+ expires: getRelativeTimeOffsetFromNow(expiresIn),
43
+ });
44
+ }
45
+ else {
46
+ services.logger.warn('No JWT service available, unable to set cookie');
47
+ }
30
48
  }
31
- return next();
32
49
  };
33
50
  return middleware;
34
51
  };
@@ -3,7 +3,7 @@ import { CoreSingletonServices, PikkuInteraction, PikkuMiddleware } from './type
3
3
  /**
4
4
  * Runs a chain of middleware functions in sequence before executing the main function.
5
5
  *
6
- * @param services - An object containing services (e.g., singletonServices, userSessionService, etc.)
6
+ * @param services - An object containing services (e.g., singletonServices, userSession, etc.)
7
7
  * @param interaction - The interaction context, e.g., { http }.
8
8
  * @param middlewares - An array of middleware functions to run.
9
9
  * @param main - The main function to execute after all middleware have run.
@@ -11,12 +11,12 @@ import { CoreSingletonServices, PikkuInteraction, PikkuMiddleware } from './type
11
11
  *
12
12
  * @example
13
13
  * runMiddleware(
14
- * { ...services, userSessionService },
14
+ * { ...services, userSession },
15
15
  * { http },
16
16
  * [middleware1, middleware2, middleware3],
17
17
  * async () => { return await runMain(); }
18
18
  * );
19
19
  */
20
20
  export declare const runMiddleware: (services: CoreSingletonServices & {
21
- userSessionService: UserSessionService<any>;
21
+ userSession: UserSessionService<any>;
22
22
  }, interaction: PikkuInteraction, middlewares: PikkuMiddleware[], main?: () => Promise<void>) => Promise<void>;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Runs a chain of middleware functions in sequence before executing the main function.
3
3
  *
4
- * @param services - An object containing services (e.g., singletonServices, userSessionService, etc.)
4
+ * @param services - An object containing services (e.g., singletonServices, userSession, etc.)
5
5
  * @param interaction - The interaction context, e.g., { http }.
6
6
  * @param middlewares - An array of middleware functions to run.
7
7
  * @param main - The main function to execute after all middleware have run.
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * @example
11
11
  * runMiddleware(
12
- * { ...services, userSessionService },
12
+ * { ...services, userSession },
13
13
  * { http },
14
14
  * [middleware1, middleware2, middleware3],
15
15
  * async () => { return await runMain(); }
@@ -1,3 +1,4 @@
1
+ import { RelativeTimeInput } from '../time-utils.js';
1
2
  /**
2
3
  * Interface for handling JSON Web Tokens (JWT).
3
4
  */
@@ -8,7 +9,7 @@ export interface JWTService {
8
9
  * @param payload - The payload to encode.
9
10
  * @returns A promise that resolves to the encoded JWT.
10
11
  */
11
- encode: <T extends any>(expiresIn: string, payload: T) => Promise<string>;
12
+ encode: <T extends any>(expiresIn: RelativeTimeInput, payload: T) => Promise<string>;
12
13
  /**
13
14
  * Decodes a JWT into its payload.
14
15
  * @param hash - The JWT to decode.
@@ -4,11 +4,11 @@ import { VariablesService } from './variables-service.js';
4
4
  * Service for retrieving secrets from environment variables.
5
5
  */
6
6
  export declare class LocalSecretService implements SecretService {
7
- private variablesService;
7
+ private variables;
8
8
  /**
9
9
  * Creates an instance of LocalSecretService.
10
10
  */
11
- constructor(variablesService?: VariablesService);
11
+ constructor(variables?: VariablesService);
12
12
  /**
13
13
  * Retrieves a secret by key.
14
14
  * @param key - The key of the secret to retrieve.
@@ -3,12 +3,12 @@ import { LocalVariablesService } from './local-variables.js';
3
3
  * Service for retrieving secrets from environment variables.
4
4
  */
5
5
  export class LocalSecretService {
6
- variablesService;
6
+ variables;
7
7
  /**
8
8
  * Creates an instance of LocalSecretService.
9
9
  */
10
- constructor(variablesService = new LocalVariablesService()) {
11
- this.variablesService = variablesService;
10
+ constructor(variables = new LocalVariablesService()) {
11
+ this.variables = variables;
12
12
  }
13
13
  /**
14
14
  * Retrieves a secret by key.
@@ -17,7 +17,7 @@ export class LocalSecretService {
17
17
  * @throws {Error} If the secret is not found.
18
18
  */
19
19
  async getSecretJSON(key) {
20
- const value = await this.variablesService.get(key);
20
+ const value = await this.variables.get(key);
21
21
  if (value) {
22
22
  return JSON.parse(value);
23
23
  }
@@ -30,7 +30,7 @@ export class LocalSecretService {
30
30
  * @throws {Error} If the secret is not found.
31
31
  */
32
32
  async getSecret(key) {
33
- const value = await this.variablesService.get(key);
33
+ const value = await this.variables.get(key);
34
34
  if (value) {
35
35
  return value;
36
36
  }
@@ -1,6 +1,7 @@
1
1
  import { ChannelStore } from '../channel/channel-store.js';
2
2
  import { CoreUserSession } from '../types/core.types.js';
3
3
  export interface UserSessionService<UserSession extends CoreUserSession> {
4
+ sessionChanged: boolean;
4
5
  setInitial(session: UserSession): void;
5
6
  set(session: UserSession): Promise<void> | void;
6
7
  clear(): Promise<void> | void;
@@ -9,6 +10,7 @@ export interface UserSessionService<UserSession extends CoreUserSession> {
9
10
  export declare class PikkuUserSessionService<UserSession extends CoreUserSession> implements UserSessionService<UserSession> {
10
11
  private channelStore?;
11
12
  private channelId?;
13
+ sessionChanged: boolean;
12
14
  private session;
13
15
  constructor(channelStore?: ChannelStore<unknown, unknown, UserSession> | undefined, channelId?: string | undefined);
14
16
  setInitial(session: UserSession): void;
@@ -1,6 +1,7 @@
1
1
  export class PikkuUserSessionService {
2
2
  channelStore;
3
3
  channelId;
4
+ sessionChanged = false;
4
5
  session;
5
6
  constructor(channelStore, channelId) {
6
7
  this.channelStore = channelStore;
@@ -13,10 +14,12 @@ export class PikkuUserSessionService {
13
14
  this.session = session;
14
15
  }
15
16
  set(session) {
17
+ this.sessionChanged = true;
16
18
  this.session = session;
17
19
  return this.channelStore?.setUserSession(this.channelId, session);
18
20
  }
19
21
  clear() {
22
+ this.sessionChanged = true;
20
23
  this.session = undefined;
21
24
  return this.channelStore?.setUserSession(this.channelId, null);
22
25
  }
@@ -0,0 +1,12 @@
1
+ type TimeUnit = 'second' | 'minute' | 'hour' | 'day' | 'week' | 'year';
2
+ export interface RelativeTimeInput {
3
+ value: number;
4
+ unit: TimeUnit;
5
+ }
6
+ export declare const getRelativeTimeOffset: ({ value, unit, }: RelativeTimeInput) => number;
7
+ /**
8
+ * Returns a Unix timestamp (in seconds) offset from now by the given duration.
9
+ * e.g. value = -1 and unit = 'day' => 1 day ago => now - 86400 seconds
10
+ */
11
+ export declare const getRelativeTimeOffsetFromNow: (relativeTime: RelativeTimeInput) => Date;
12
+ export {};
@@ -0,0 +1,18 @@
1
+ const multipliers = {
2
+ second: 1,
3
+ minute: 60,
4
+ hour: 60 * 60,
5
+ day: 60 * 60 * 24,
6
+ week: 60 * 60 * 24 * 7,
7
+ year: Math.round(365.25 * 60 * 60 * 24),
8
+ };
9
+ export const getRelativeTimeOffset = ({ value, unit, }) => {
10
+ return Math.round(value * multipliers[unit]) * 1000; // convert to milliseconds
11
+ };
12
+ /**
13
+ * Returns a Unix timestamp (in seconds) offset from now by the given duration.
14
+ * e.g. value = -1 and unit = 'day' => 1 day ago => now - 86400 seconds
15
+ */
16
+ export const getRelativeTimeOffsetFromNow = (relativeTime) => {
17
+ return new Date(Date.now() + getRelativeTimeOffset(relativeTime));
18
+ };