@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.
- package/CHANGELOG.md +12 -0
- package/dist/channel/channel-handler.js +1 -1
- package/dist/channel/channel-runner.d.ts +1 -1
- package/dist/channel/channel-runner.js +1 -1
- package/dist/channel/local/local-channel-runner.js +5 -5
- package/dist/channel/serverless/serverless-channel-runner.js +4 -4
- package/dist/http/http-route-runner.js +9 -14
- 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/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 +1694 -967
- package/package.json +1 -1
- package/src/channel/channel-handler.ts +1 -1
- package/src/channel/channel-runner.ts +3 -3
- package/src/channel/local/local-channel-runner.test.ts +0 -2
- package/src/channel/local/local-channel-runner.ts +5 -5
- package/src/channel/serverless/serverless-channel-runner.ts +4 -7
- package/src/http/http-route-runner.test.ts +1 -1
- package/src/http/http-route-runner.ts +23 -20
- 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/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
|
@@ -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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
};
|
|
@@ -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
|
|
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
|
-
|
|
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 }) => {
|
|
@@ -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,
|
|
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,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,
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
283
|
+
userSession,
|
|
289
284
|
createSessionServices,
|
|
290
285
|
skipUserSession,
|
|
291
286
|
}, matchedRoute, http, { coerceToArray }));
|
|
@@ -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(); }
|
|
@@ -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:
|
|
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
|
|
7
|
+
private variables;
|
|
8
8
|
/**
|
|
9
9
|
* Creates an instance of LocalSecretService.
|
|
10
10
|
*/
|
|
11
|
-
constructor(
|
|
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
|
-
|
|
6
|
+
variables;
|
|
7
7
|
/**
|
|
8
8
|
* Creates an instance of LocalSecretService.
|
|
9
9
|
*/
|
|
10
|
-
constructor(
|
|
11
|
-
this.
|
|
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.
|
|
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.
|
|
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 {};
|