@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  ## 0.6
2
2
 
3
+ ## 0.6.25
4
+
5
+ ### Patch Changes
6
+
7
+ - b774c7d: fix: coerce top level data from schema now includes date strings
8
+
9
+ ## 0.6.24
10
+
11
+ ### Patch Changes
12
+
13
+ - 531f4b5: refactor: using userSession.set to set cookies with middleware
14
+
3
15
  ## 0.6.23
4
16
 
5
17
  ### Patch Changes
@@ -7,8 +7,8 @@ export declare const getMatchingChannelConfig: (request: string) => {
7
7
  channelConfig: CoreAPIChannel<any, string, import("./channel.types.js").CoreChannelConnection<any, unknown, import("../index.js").CoreServices>, import("./channel.types.js").CoreChannelDisconnection<any, import("../index.js").CoreServices>, import("./channel.types.js").CoreChannelMessage<unknown, unknown, unknown, import("../index.js").CoreServices> | import("../index.js").CoreAPIFunction<unknown, unknown>, import("./channel.types.js").CoreChannelMessage<unknown, unknown, unknown, import("../index.js").CoreServices> | import("../index.js").CoreAPIFunction<unknown, unknown>, import("../index.js").CoreAPIPermission<any>>;
8
8
  schemaName: string | null | undefined;
9
9
  } | null;
10
- export declare const openChannel: ({ route, singletonServices, coerceToArray, request, }: Pick<CoreAPIChannel<unknown, string>, "route"> & RunChannelParams<unknown> & {
11
- userSessionService: UserSessionService<any>;
10
+ export declare const openChannel: ({ route, singletonServices, coerceDataFromSchema, request, }: Pick<CoreAPIChannel<unknown, string>, "route"> & RunChannelParams<unknown> & {
11
+ userSession: UserSessionService<any>;
12
12
  } & RunChannelOptions) => Promise<{
13
13
  openingData: unknown;
14
14
  channelConfig: CoreAPIChannel<unknown, any>;
@@ -1,6 +1,6 @@
1
1
  import { NotFoundError } from '../errors/errors.js';
2
2
  import { pikkuState } from '../pikku-state.js';
3
- import { coerceQueryStringToArray, validateSchema } from '../schema.js';
3
+ import { coerceTopLevelDataFromSchema, validateSchema } from '../schema.js';
4
4
  import { match } from 'path-to-regexp';
5
5
  export const addChannel = (channel) => {
6
6
  pikkuState('channel', 'channels').push(channel);
@@ -27,7 +27,7 @@ export const getMatchingChannelConfig = (request) => {
27
27
  }
28
28
  return null;
29
29
  };
30
- export const openChannel = async ({ route, singletonServices, coerceToArray = false, request, }) => {
30
+ export const openChannel = async ({ route, singletonServices, coerceDataFromSchema = true, request, }) => {
31
31
  const matchingChannel = getMatchingChannelConfig(route);
32
32
  if (!matchingChannel) {
33
33
  singletonServices.logger.info(`Channel not found: ${route}`);
@@ -40,10 +40,10 @@ export const openChannel = async ({ route, singletonServices, coerceToArray = fa
40
40
  let openingData;
41
41
  if (request) {
42
42
  openingData = await request.data();
43
- if (coerceToArray && schemaName) {
44
- coerceQueryStringToArray(schemaName, openingData);
43
+ if (coerceDataFromSchema && schemaName) {
44
+ coerceTopLevelDataFromSchema(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
  };
@@ -5,7 +5,7 @@ import { CoreAPIFunction, CoreAPIPermission } from '../types/functions.types.js'
5
5
  export type RunChannelOptions = Partial<{
6
6
  skipUserSession: boolean;
7
7
  respondWith404: boolean;
8
- coerceToArray: boolean;
8
+ coerceDataFromSchema: boolean;
9
9
  logWarningsForStatusCodes: number[];
10
10
  bubbleErrors: boolean;
11
11
  }>;
@@ -1,3 +1,3 @@
1
1
  import { CoreAPIChannel, RunChannelOptions, RunChannelParams } from '../channel.types.js';
2
2
  import { PikkuLocalChannelHandler } from './local-channel-handler.js';
3
- export declare const runLocalChannel: ({ singletonServices, channelId, request, response, route, createSessionServices, skipUserSession, respondWith404, coerceToArray, logWarningsForStatusCodes, bubbleErrors, }: Partial<Pick<CoreAPIChannel<unknown, any>, "route">> & RunChannelOptions & RunChannelParams<unknown>) => Promise<PikkuLocalChannelHandler | void>;
3
+ export declare const runLocalChannel: ({ singletonServices, channelId, request, response, route, createSessionServices, skipUserSession, respondWith404, coerceDataFromSchema, logWarningsForStatusCodes, bubbleErrors, }: Partial<Pick<CoreAPIChannel<unknown, any>, "route">> & RunChannelOptions & RunChannelParams<unknown>) => Promise<PikkuLocalChannelHandler | void>;
@@ -6,10 +6,10 @@ import { PikkuLocalChannelHandler } from './local-channel-handler.js';
6
6
  import { handleError } from '../../handle-error.js';
7
7
  import { runMiddleware } from '../../middleware-runner.js';
8
8
  import { PikkuUserSessionService } from '../../services/user-session-service.js';
9
- export const runLocalChannel = async ({ singletonServices, channelId, request, response, route, createSessionServices, skipUserSession = false, respondWith404 = true, coerceToArray = false, logWarningsForStatusCodes = [], bubbleErrors = false, }) => {
9
+ export const runLocalChannel = async ({ singletonServices, channelId, request, response, route, createSessionServices, skipUserSession = false, respondWith404 = true, coerceDataFromSchema = true, 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);
@@ -26,19 +26,19 @@ export const runLocalChannel = async ({ singletonServices, channelId, request, r
26
26
  route,
27
27
  singletonServices,
28
28
  skipUserSession,
29
- coerceToArray,
30
- userSessionService,
29
+ coerceDataFromSchema,
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
  };
@@ -5,6 +5,6 @@ export interface RunServerlessChannelParams<ChannelData> extends RunChannelParam
5
5
  channelHandlerFactory: PikkuChannelHandlerFactory;
6
6
  channelObject?: unknown;
7
7
  }
8
- export declare const runChannelConnect: ({ singletonServices, channelId, channelObject, request, response, route, createSessionServices, channelStore, channelHandlerFactory, coerceToArray, logWarningsForStatusCodes, respondWith404, bubbleErrors, }: Pick<CoreAPIChannel<unknown, any>, "route"> & RunChannelOptions & RunServerlessChannelParams<unknown>) => Promise<void>;
8
+ export declare const runChannelConnect: ({ singletonServices, channelId, channelObject, request, response, route, createSessionServices, channelStore, channelHandlerFactory, coerceDataFromSchema, logWarningsForStatusCodes, respondWith404, bubbleErrors, }: Pick<CoreAPIChannel<unknown, any>, "route"> & RunChannelOptions & RunServerlessChannelParams<unknown>) => Promise<void>;
9
9
  export declare const runChannelDisconnect: ({ singletonServices, ...params }: RunServerlessChannelParams<unknown>) => Promise<void>;
10
10
  export declare const runChannelMessage: ({ singletonServices, ...params }: RunServerlessChannelParams<unknown>, data: unknown) => Promise<unknown>;
@@ -20,21 +20,21 @@ const getVariablesForChannel = ({ channelId, channelName, channelHandlerFactory,
20
20
  channel: channelHandler.getChannel(),
21
21
  };
22
22
  };
23
- export const runChannelConnect = async ({ singletonServices, channelId, channelObject, request, response, route, createSessionServices, channelStore, channelHandlerFactory, coerceToArray = false, logWarningsForStatusCodes = [], respondWith404 = true, bubbleErrors = false, }) => {
23
+ export const runChannelConnect = async ({ singletonServices, channelId, channelObject, request, response, route, createSessionServices, channelStore, channelHandlerFactory, coerceDataFromSchema = true, logWarningsForStatusCodes = [], respondWith404 = true, bubbleErrors = false, }) => {
24
24
  let sessionServices;
25
25
  let http;
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,
33
33
  request,
34
34
  route,
35
35
  singletonServices,
36
- coerceToArray,
37
- userSessionService,
36
+ coerceDataFromSchema,
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 }) => {
@@ -87,4 +87,4 @@ export declare const pikkuFetch: <In, Out>(request: Request | PikkuHTTPRequest,
87
87
  * @param {RunRouteOptions & RunRouteParams} options - Options such as singleton services, session handling, and error configuration.
88
88
  * @returns {Promise<Out | void>} The output from the route handler or void if an error occurred.
89
89
  */
90
- export declare const fetchData: <In, Out>(request: Request | PikkuHTTPRequest, response: PikkuHTTPResponse, { singletonServices, createSessionServices, skipUserSession, respondWith404, logWarningsForStatusCodes, coerceToArray, bubbleErrors, }: RunRouteOptions & RunRouteParams) => Promise<Out | void>;
90
+ export declare const fetchData: <In, Out>(request: Request | PikkuHTTPRequest, response: PikkuHTTPResponse, { singletonServices, createSessionServices, skipUserSession, respondWith404, logWarningsForStatusCodes, coerceDataFromSchema, bubbleErrors, }: RunRouteOptions & RunRouteParams) => Promise<Out | void>;
@@ -2,8 +2,8 @@ import { verifyPermissions } from '../permissions.js';
2
2
  import { match } from 'path-to-regexp';
3
3
  import { ForbiddenError, MissingSessionError, NotFoundError, } from '../errors/errors.js';
4
4
  import { closeSessionServices } from '../utils.js';
5
- import { coerceQueryStringToArray, validateSchema } from '../schema.js';
6
- import { PikkuUserSessionService } from '../services/user-session-service.js';
5
+ import { coerceTopLevelDataFromSchema, validateSchema } from '../schema.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,18 +164,19 @@ 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
- // Coerce query string parameters to arrays if specified by the schema
177
- if (options.coerceToArray && schemaName) {
178
- coerceQueryStringToArray(schemaName, data);
176
+ await validateSchema(singletonServices.logger, singletonServices.schema, schemaName, data);
177
+ // Coerce (top level) query string parameters or date objects if specified by the schema
178
+ if (options.coerceDataFromSchema && schemaName) {
179
+ coerceTopLevelDataFromSchema(schemaName, data);
179
180
  }
180
181
  // Execute permission checks
181
182
  const permissioned = await verifyPermissions(route.permissions, allServices, data, session);
@@ -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
  /**
@@ -255,8 +256,8 @@ export const pikkuFetch = async (request, params) => {
255
256
  * @param {RunRouteOptions & RunRouteParams} options - Options such as singleton services, session handling, and error configuration.
256
257
  * @returns {Promise<Out | void>} The output from the route handler or void if an error occurred.
257
258
  */
258
- export const fetchData = async (request, response, { singletonServices, createSessionServices, skipUserSession = false, respondWith404 = true, logWarningsForStatusCodes = [], coerceToArray = false, bubbleErrors = false, }) => {
259
- const userSessionService = new PikkuUserSessionService();
259
+ export const fetchData = async (request, response, { singletonServices, createSessionServices, skipUserSession = false, respondWith404 = true, logWarningsForStatusCodes = [], coerceDataFromSchema = true, bubbleErrors = false, }) => {
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,10 +280,10 @@ 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
- }, matchedRoute, http, { coerceToArray }));
286
+ }, matchedRoute, http, { coerceDataFromSchema }));
286
287
  return result;
287
288
  }
288
289
  catch (e) {
@@ -8,7 +8,7 @@ export type RunRouteOptions = Partial<{
8
8
  skipUserSession: boolean;
9
9
  respondWith404: boolean;
10
10
  logWarningsForStatusCodes: number[];
11
- coerceToArray: boolean;
11
+ coerceDataFromSchema: boolean;
12
12
  bubbleErrors: boolean;
13
13
  }>;
14
14
  export type RunRouteParams = {
@@ -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(); }
package/dist/schema.d.ts CHANGED
@@ -12,5 +12,5 @@ export declare const addSchema: (name: string, value: any) => void;
12
12
  * @param logger - A logger for logging information.
13
13
  */
14
14
  export declare const compileAllSchemas: (logger: Logger, schemaService?: SchemaService) => void;
15
- export declare const coerceQueryStringToArray: (schemaName: string, data: any) => void;
15
+ export declare const coerceTopLevelDataFromSchema: (schemaName: string, data: any) => void;
16
16
  export declare const validateSchema: (logger: Logger, schemaService: SchemaService | undefined, schemaName: string | undefined | null, data: any) => Promise<void>;
package/dist/schema.js CHANGED
@@ -42,7 +42,7 @@ const validateAllSchemasLoaded = (logger, schemaService) => {
42
42
  logger.info('All schemas loaded');
43
43
  }
44
44
  };
45
- export const coerceQueryStringToArray = (schemaName, data) => {
45
+ export const coerceTopLevelDataFromSchema = (schemaName, data) => {
46
46
  const schema = pikkuState('misc', 'schemas').get(schemaName);
47
47
  for (const key in schema.properties) {
48
48
  const property = schema.properties[key];
@@ -56,6 +56,9 @@ export const coerceQueryStringToArray = (schemaName, data) => {
56
56
  if (type === 'array' && typeof data[key] === 'string') {
57
57
  data[key] = data[key].split(',');
58
58
  }
59
+ else if (type === 'string' && property.format === 'date-time') {
60
+ data[key] = new Date(data[key]);
61
+ }
59
62
  }
60
63
  };
61
64
  export const validateSchema = async (logger, schemaService, schemaName, data) => {