@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.
- package/CHANGELOG.md +12 -0
- package/dist/channel/channel-runner.d.ts +2 -2
- package/dist/channel/channel-runner.js +5 -5
- package/dist/channel/channel.types.d.ts +1 -1
- package/dist/channel/local/local-channel-runner.d.ts +1 -1
- package/dist/channel/local/local-channel-runner.js +7 -7
- package/dist/channel/serverless/serverless-channel-runner.d.ts +1 -1
- package/dist/channel/serverless/serverless-channel-runner.js +6 -6
- package/dist/http/http-route-runner.d.ts +1 -1
- package/dist/http/http-route-runner.js +15 -14
- package/dist/http/http-routes.types.d.ts +1 -1
- package/dist/http/pikku-fetch-http-request.d.ts +1 -0
- package/dist/http/pikku-fetch-http-request.js +64 -8
- package/dist/http/pikku-fetch-http-response.d.ts +2 -1
- package/dist/http/pikku-fetch-http-response.js +5 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/middleware/auth-apikey.js +2 -2
- package/dist/middleware/auth-bearer.js +2 -2
- package/dist/middleware/auth-cookie.d.ts +7 -3
- package/dist/middleware/auth-cookie.js +34 -17
- package/dist/middleware-runner.d.ts +3 -3
- package/dist/middleware-runner.js +2 -2
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +4 -1
- package/dist/services/jwt-service.d.ts +2 -1
- package/dist/services/local-secrets.d.ts +2 -2
- package/dist/services/local-secrets.js +5 -5
- package/dist/services/user-session-service.d.ts +2 -0
- package/dist/services/user-session-service.js +3 -0
- package/dist/time-utils.d.ts +12 -0
- package/dist/time-utils.js +18 -0
- package/dist/types/core.types.d.ts +5 -5
- package/dist/types/functions.types.d.ts +1 -1
- package/lcov.info +1767 -1031
- package/package.json +1 -1
- package/src/channel/channel-runner.ts +6 -6
- package/src/channel/channel.types.ts +1 -1
- package/src/channel/local/local-channel-runner.test.ts +0 -2
- package/src/channel/local/local-channel-runner.ts +7 -7
- package/src/channel/serverless/serverless-channel-runner.ts +6 -9
- package/src/http/http-route-runner.test.ts +1 -1
- package/src/http/http-route-runner.ts +25 -17
- package/src/http/http-routes.types.ts +1 -1
- package/src/http/pikku-fetch-http-request.test.ts +237 -0
- package/src/http/pikku-fetch-http-request.ts +64 -8
- package/src/http/pikku-fetch-http-response.test.ts +82 -0
- package/src/http/pikku-fetch-http-response.ts +16 -2
- package/src/index.ts +1 -1
- package/src/middleware/auth-apikey.ts +2 -2
- package/src/middleware/auth-bearer.ts +2 -2
- package/src/middleware/auth-cookie.ts +51 -21
- package/src/middleware-runner.ts +3 -3
- package/src/schema.test.ts +6 -6
- package/src/schema.ts +3 -1
- package/src/services/jwt-service.ts +6 -1
- package/src/services/local-secrets.ts +3 -3
- package/src/services/user-session-service.ts +4 -0
- package/src/time-utils.test.ts +56 -0
- package/src/time-utils.ts +32 -0
- package/src/types/core.types.ts +5 -5
- package/src/types/functions.types.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
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,
|
|
11
|
-
|
|
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 {
|
|
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,
|
|
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 (
|
|
44
|
-
|
|
43
|
+
if (coerceDataFromSchema && schemaName) {
|
|
44
|
+
coerceTopLevelDataFromSchema(schemaName, openingData);
|
|
45
45
|
}
|
|
46
|
-
await validateSchema(singletonServices.logger, singletonServices.
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
30
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
37
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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 {
|
|
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,
|
|
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 =
|
|
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,
|
|
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.
|
|
176
|
-
// Coerce query string parameters
|
|
177
|
-
if (options.
|
|
178
|
-
|
|
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,
|
|
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 = [],
|
|
259
|
-
const
|
|
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
|
-
|
|
283
|
+
userSession,
|
|
283
284
|
createSessionServices,
|
|
284
285
|
skipUserSession,
|
|
285
|
-
}, matchedRoute, http, {
|
|
286
|
+
}, matchedRoute, http, { coerceDataFromSchema }));
|
|
286
287
|
return result;
|
|
287
288
|
}
|
|
288
289
|
catch (e) {
|
|
@@ -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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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>({
|
|
9
|
-
|
|
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.
|
|
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 = ({
|
|
8
|
+
export const authCookie = ({ name, getSessionForCookieValue, jwt, options, expiresIn, }) => {
|
|
8
9
|
const middleware = async (services, { http }, next) => {
|
|
9
|
-
if (!http?.request || services.
|
|
10
|
+
if (!http?.request || services.userSession.get()) {
|
|
10
11
|
return next();
|
|
11
12
|
}
|
|
12
13
|
let userSession = null;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if (
|
|
16
|
-
if (jwt) {
|
|
17
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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.
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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) => {
|