@pikku/core 0.6.22 → 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 (54) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/channel/channel-handler.js +1 -1
  3. package/dist/channel/channel-runner.d.ts +1 -1
  4. package/dist/channel/channel-runner.js +1 -1
  5. package/dist/channel/local/local-channel-runner.js +5 -5
  6. package/dist/channel/serverless/serverless-channel-runner.js +4 -4
  7. package/dist/http/http-route-runner.js +9 -14
  8. package/dist/http/pikku-fetch-http-request.d.ts +1 -0
  9. package/dist/http/pikku-fetch-http-request.js +64 -8
  10. package/dist/http/pikku-fetch-http-response.d.ts +2 -1
  11. package/dist/http/pikku-fetch-http-response.js +5 -1
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.js +1 -0
  14. package/dist/middleware/auth-apikey.js +2 -2
  15. package/dist/middleware/auth-bearer.js +2 -2
  16. package/dist/middleware/auth-cookie.d.ts +7 -3
  17. package/dist/middleware/auth-cookie.js +34 -17
  18. package/dist/middleware-runner.d.ts +3 -3
  19. package/dist/middleware-runner.js +2 -2
  20. package/dist/services/jwt-service.d.ts +2 -1
  21. package/dist/services/local-secrets.d.ts +2 -2
  22. package/dist/services/local-secrets.js +5 -5
  23. package/dist/services/user-session-service.d.ts +2 -0
  24. package/dist/services/user-session-service.js +3 -0
  25. package/dist/time-utils.d.ts +12 -0
  26. package/dist/time-utils.js +18 -0
  27. package/dist/types/core.types.d.ts +5 -5
  28. package/dist/types/functions.types.d.ts +1 -1
  29. package/lcov.info +1694 -967
  30. package/package.json +1 -1
  31. package/src/channel/channel-handler.ts +1 -1
  32. package/src/channel/channel-runner.ts +3 -3
  33. package/src/channel/local/local-channel-runner.test.ts +0 -2
  34. package/src/channel/local/local-channel-runner.ts +5 -5
  35. package/src/channel/serverless/serverless-channel-runner.ts +4 -7
  36. package/src/http/http-route-runner.test.ts +1 -1
  37. package/src/http/http-route-runner.ts +23 -20
  38. package/src/http/pikku-fetch-http-request.test.ts +237 -0
  39. package/src/http/pikku-fetch-http-request.ts +64 -8
  40. package/src/http/pikku-fetch-http-response.test.ts +82 -0
  41. package/src/http/pikku-fetch-http-response.ts +16 -2
  42. package/src/index.ts +1 -1
  43. package/src/middleware/auth-apikey.ts +2 -2
  44. package/src/middleware/auth-bearer.ts +2 -2
  45. package/src/middleware/auth-cookie.ts +51 -21
  46. package/src/middleware-runner.ts +3 -3
  47. package/src/services/jwt-service.ts +6 -1
  48. package/src/services/local-secrets.ts +3 -3
  49. package/src/services/user-session-service.ts +4 -0
  50. package/src/time-utils.test.ts +56 -0
  51. package/src/time-utils.ts +32 -0
  52. package/src/types/core.types.ts +5 -5
  53. package/src/types/functions.types.ts +1 -1
  54. package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
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
+
9
+ ## 0.6.23
10
+
11
+ ### Patch Changes
12
+
13
+ - 1c8c470: fix: await schema validation
14
+
3
15
  ## 0.6.22
4
16
 
5
17
  ### Patch Changes
@@ -71,7 +71,7 @@ export const processMessageHandlers = (services, session, channelConfig, channel
71
71
  }
72
72
  const routeMeta = getRouteMeta(channelConfig.name, routingProperty, routerValue);
73
73
  if (routeMeta) {
74
- validateSchema(services.logger, data, routeMeta);
74
+ await validateSchema(services.logger, data, routeMeta);
75
75
  }
76
76
  const hasPermission = await validatePermissions(services, channelHandler, onMessage, data);
77
77
  if (!hasPermission) {
@@ -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
- 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,21 +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
- try {
176
- console.log(schemaName, data);
177
- validateSchema(singletonServices.logger, singletonServices.schemaService, schemaName, data);
178
- }
179
- catch (e) {
180
- // TODO: Implement proper handling for schema validation failures
181
- }
176
+ await validateSchema(singletonServices.logger, singletonServices.schema, schemaName, data);
182
177
  // Coerce query string parameters to arrays if specified by the schema
183
178
  if (options.coerceToArray && schemaName) {
184
179
  coerceQueryStringToArray(schemaName, data);
@@ -203,7 +198,7 @@ const executeRouteWithMiddleware = async (services, matchedRoute, http, options)
203
198
  return result;
204
199
  };
205
200
  // Execute middleware, then run the main logic
206
- await runMiddleware({ ...singletonServices, userSessionService }, { http }, middleware, runMain);
201
+ await runMiddleware({ ...singletonServices, userSession }, { http }, middleware, runMain);
207
202
  return sessionServices ? { result, sessionServices } : { result };
208
203
  };
209
204
  /**
@@ -262,7 +257,7 @@ export const pikkuFetch = async (request, params) => {
262
257
  * @returns {Promise<Out | void>} The output from the route handler or void if an error occurred.
263
258
  */
264
259
  export const fetchData = async (request, response, { singletonServices, createSessionServices, skipUserSession = false, respondWith404 = true, logWarningsForStatusCodes = [], coerceToArray = false, bubbleErrors = false, }) => {
265
- const userSessionService = new PikkuUserSessionService();
260
+ const userSession = new PikkuUserSessionService();
266
261
  let sessionServices;
267
262
  let result;
268
263
  // Combine the request and response into one interaction object
@@ -285,7 +280,7 @@ export const fetchData = async (request, response, { singletonServices, createSe
285
280
  ;
286
281
  ({ result, sessionServices } = await executeRouteWithMiddleware({
287
282
  singletonServices,
288
- userSessionService,
283
+ userSession,
289
284
  createSessionServices,
290
285
  skipUserSession,
291
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 {};