aws-classify-server 0.1.9 → 0.2.2
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/README.md +17 -336
- package/lib/cjs/AuthenticatedSessions.d.ts +6 -0
- package/lib/cjs/AuthenticatedSessions.js +93 -0
- package/lib/cjs/Authentication.d.ts +10 -0
- package/lib/cjs/Authentication.js +37 -0
- package/lib/cjs/ClassDef.d.ts +3 -1
- package/lib/cjs/ClassifyServerless.d.ts +17 -4
- package/lib/cjs/ClassifyServerless.js +90 -30
- package/lib/cjs/SessionStore.d.ts +3 -0
- package/lib/cjs/SessionStore.js +8 -0
- package/lib/cjs/aws-classify-common/EndPointsLogging.d.ts +1 -8
- package/lib/cjs/aws-classify-common/EndPointsLogging.js +2 -8
- package/lib/cjs/aws-classify-common/LambdaRequest.d.ts +1 -6
- package/lib/cjs/aws-classify-common/LambdaResponse.d.ts +1 -6
- package/lib/cjs/aws-classify-common/index.d.ts +1 -4
- package/lib/cjs/aws-classify-common/index.js +16 -5
- package/lib/cjs/dispatch.d.ts +4 -2
- package/lib/cjs/dispatch.js +28 -8
- package/lib/cjs/index.d.ts +3 -1
- package/lib/cjs/index.js +2 -1
- package/lib/esm/AuthenticatedSessions.d.ts +6 -0
- package/lib/esm/AuthenticatedSessions.js +85 -0
- package/lib/esm/Authentication.d.ts +10 -0
- package/lib/esm/Authentication.js +32 -0
- package/lib/esm/ClassDef.d.ts +3 -1
- package/lib/esm/ClassifyServerless.d.ts +17 -4
- package/lib/esm/ClassifyServerless.js +84 -24
- package/lib/esm/SessionStore.d.ts +3 -0
- package/lib/esm/SessionStore.js +4 -0
- package/lib/esm/aws-classify-common/EndPointsLogging.d.ts +1 -8
- package/lib/esm/aws-classify-common/EndPointsLogging.js +1 -8
- package/lib/esm/aws-classify-common/LambdaRequest.d.ts +1 -6
- package/lib/esm/aws-classify-common/LambdaResponse.d.ts +1 -6
- package/lib/esm/aws-classify-common/index.d.ts +1 -4
- package/lib/esm/aws-classify-common/index.js +2 -2
- package/lib/esm/dispatch.d.ts +4 -2
- package/lib/esm/dispatch.js +26 -7
- package/lib/esm/index.d.ts +3 -1
- package/lib/esm/index.js +1 -1
- package/package.json +20 -13
- package/yml/authentication-config.js +16 -0
- package/yml/functions-authenticated.yml +30 -0
- package/yml/provider-iam.yml +4 -1
- package/yml/resources-custom-website.yml +4 -1
- package/yml/resources-website-authenticated.yml +72 -0
- package/yml/resources-website.yml +4 -1
package/lib/esm/ClassDef.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import type { RequestContext } from "aws-classify-common";
|
|
1
2
|
import { ClassifyResponse } from "./ClassifyResponse";
|
|
2
3
|
export interface ClassDef<ServerClass extends ClassifyResponse, ClientClass> {
|
|
3
4
|
serverClass: new () => ServerClass;
|
|
4
5
|
clientClass: new () => ClientClass;
|
|
5
|
-
|
|
6
|
+
publicMethods: ReadonlySet<string>;
|
|
7
|
+
authorizer: ((endPoint: ServerClass, method: string, args: IArguments, context?: RequestContext) => Promise<boolean>) | undefined;
|
|
6
8
|
}
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { ClassDef } from "./ClassDef";
|
|
2
|
-
import {
|
|
2
|
+
import type { RequestContext } from "aws-classify-common";
|
|
3
|
+
import { HttpEvent, ServerAuthenticationOptions } from "./Authentication";
|
|
4
|
+
import { Context } from "aws-lambda";
|
|
3
5
|
export declare class ClassifyServerless {
|
|
6
|
+
private authentication;
|
|
7
|
+
private readonly contexts;
|
|
8
|
+
private readonly activeContexts;
|
|
9
|
+
private readonly responseContexts;
|
|
10
|
+
constructor();
|
|
11
|
+
configureAuthentication(options: ServerAuthenticationOptions): void;
|
|
12
|
+
get authenticationEnabled(): boolean;
|
|
13
|
+
getRequestContext(response?: object): RequestContext | undefined;
|
|
4
14
|
logLevel: Partial<typeof EndPointsLogging>;
|
|
5
15
|
classDefs: Map<any, ClassDef<any, any>>;
|
|
6
16
|
requests: Map<any, any>;
|
|
@@ -9,8 +19,11 @@ export declare class ClassifyServerless {
|
|
|
9
19
|
setExpirationMinutes(minutes: number): void;
|
|
10
20
|
setLogger(log: (_message: string) => void): void;
|
|
11
21
|
setLogLevel(logLevel: Partial<typeof EndPointsLogging>): void;
|
|
12
|
-
registerResponse<T>(responseClass: new () => T, authorizer?: (_endPoint: T, _method: string, _args: IArguments) => Promise<boolean
|
|
13
|
-
|
|
22
|
+
registerResponse<T>(responseClass: new () => T, authorizer?: (_endPoint: T, _method: string, _args: IArguments, _context?: RequestContext) => Promise<boolean>, options?: {
|
|
23
|
+
publicMethods?: readonly string[];
|
|
24
|
+
}): void;
|
|
25
|
+
dispatch(ev: HttpEvent, context: Context, classes?: {}, entry?: 'protected' | 'public'): Promise<string>;
|
|
26
|
+
private dispatchRequest;
|
|
14
27
|
registerRequest<T>(requestClass: new () => T, classes?: any): new () => T;
|
|
15
28
|
getSessionId<T>(responseObj: T): any;
|
|
16
29
|
getUserId<T>(responseObj: T): any;
|
|
@@ -24,7 +37,7 @@ export declare class ClassifyServerless {
|
|
|
24
37
|
deleteSessions(): Promise<void>;
|
|
25
38
|
}
|
|
26
39
|
export declare function getSessionData(sessionId: string, interfaceName?: string): Promise<Record<string, any> | undefined>;
|
|
27
|
-
export declare function saveSessionData(sessionId: string, interfaceName?: string, sessionData?: string, connectionId?: string, userId?: string, expirationMinutes?: number): Promise<string>;
|
|
40
|
+
export declare function saveSessionData(sessionId: string, interfaceName?: string, sessionData?: string, connectionId?: string, userId?: string, expirationMinutes?: number, authOwner?: string): Promise<string>;
|
|
28
41
|
export declare const EndPointsLogging: {
|
|
29
42
|
create: boolean;
|
|
30
43
|
connect: boolean;
|
|
@@ -8,19 +8,39 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
10
|
import { deserialize, serialize } from "js-freeze-dry";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
11
|
+
import { sessionStore as ddbDocClient } from "./SessionStore";
|
|
12
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
13
|
+
import { isPublicEndpoint } from "aws-classify-common";
|
|
14
|
+
import { requestContext } from "./Authentication";
|
|
15
|
+
import { ownedSession, sessionOwner, issueConnectionCredential } from "./AuthenticatedSessions";
|
|
13
16
|
import { ApiGatewayManagementApiClient, PostToConnectionCommand } from "@aws-sdk/client-apigatewaymanagementapi";
|
|
14
|
-
const ddbClient = new DynamoDBClient({ region: process.env.DD_REGION, endpoint: process.env.DD_ENDPOINT });
|
|
15
|
-
const ddbDocClient = DynamoDBDocument.from(ddbClient);
|
|
16
17
|
const updateDebounceInterval = 1000 * 60 * 10; // Don't update session unless 10 minutes have passed or data changed
|
|
17
18
|
export class ClassifyServerless {
|
|
18
19
|
constructor() {
|
|
20
|
+
this.contexts = new AsyncLocalStorage();
|
|
21
|
+
this.activeContexts = new Set();
|
|
22
|
+
this.responseContexts = new WeakMap();
|
|
19
23
|
this.logLevel = {};
|
|
20
24
|
this.classDefs = new Map();
|
|
21
25
|
this.requests = new Map();
|
|
22
26
|
this.expirationMinutes = 60 * 24;
|
|
23
27
|
this.log = msg => console.log(msg);
|
|
28
|
+
if (process.env.AWS_CLASSIFY_AUTHENTICATION === 'true')
|
|
29
|
+
this.configureAuthentication({});
|
|
30
|
+
}
|
|
31
|
+
configureAuthentication(options) {
|
|
32
|
+
var _a;
|
|
33
|
+
const seconds = (_a = options.connectionCredentialSeconds) !== null && _a !== void 0 ? _a : 60;
|
|
34
|
+
if (!Number.isInteger(seconds) || seconds < 1 || seconds > 300)
|
|
35
|
+
throw new Error('connectionCredentialSeconds must be 1–300');
|
|
36
|
+
this.authentication = Object.freeze(Object.assign(Object.assign({}, options), { connectionCredentialSeconds: seconds }));
|
|
37
|
+
}
|
|
38
|
+
get authenticationEnabled() { return !!this.authentication; }
|
|
39
|
+
getRequestContext(response) {
|
|
40
|
+
const current = this.contexts.getStore();
|
|
41
|
+
if (!current || !this.activeContexts.has(current))
|
|
42
|
+
return undefined;
|
|
43
|
+
return response ? (this.responseContexts.get(response) === current ? current : undefined) : current;
|
|
24
44
|
}
|
|
25
45
|
setExpirationMinutes(minutes) {
|
|
26
46
|
this.expirationMinutes = minutes;
|
|
@@ -31,7 +51,7 @@ export class ClassifyServerless {
|
|
|
31
51
|
setLogLevel(logLevel) {
|
|
32
52
|
this.logLevel = logLevel;
|
|
33
53
|
}
|
|
34
|
-
registerResponse(responseClass, authorizer) {
|
|
54
|
+
registerResponse(responseClass, authorizer, options) {
|
|
35
55
|
// Find ultimate base class
|
|
36
56
|
let clientClass = responseClass;
|
|
37
57
|
while (Object.getPrototypeOf(clientClass).prototype)
|
|
@@ -42,17 +62,47 @@ export class ClassifyServerless {
|
|
|
42
62
|
const interfaceName = clientClass['interfaceName'];
|
|
43
63
|
if (!interfaceName)
|
|
44
64
|
throw ('ClassifyServerless.registerResponse: Request must have interfaceName as a static property');
|
|
45
|
-
|
|
65
|
+
const methods = Object.getOwnPropertyNames(clientClass.prototype).filter(method => method !== 'constructor' && typeof clientClass.prototype[method] === 'function');
|
|
66
|
+
const decorated = methods.filter(method => isPublicEndpoint(clientClass.prototype[method]));
|
|
67
|
+
if ((options === null || options === void 0 ? void 0 : options.publicMethods) !== undefined && decorated.length)
|
|
68
|
+
throw new Error('Use either @Public() or publicMethods, not both');
|
|
69
|
+
const publicMethods = (options === null || options === void 0 ? void 0 : options.publicMethods) === undefined ? decorated : [...options.publicMethods];
|
|
70
|
+
if (!publicMethods.every(method => typeof method === 'string' && methods.includes(method)) || new Set(publicMethods).size !== publicMethods.length)
|
|
71
|
+
throw new Error('publicMethods must be unique request-class method names');
|
|
72
|
+
this.classDefs.set(interfaceName, { serverClass: responseClass, clientClass, authorizer, publicMethods: new Set(publicMethods) });
|
|
46
73
|
responseClass.__interfaceName__ = interfaceName;
|
|
47
74
|
}
|
|
48
|
-
dispatch(ev, context, classes = {}) {
|
|
75
|
+
dispatch(ev, context, classes = {}, entry = 'protected') {
|
|
76
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
77
|
+
if (entry === 'public' && !this.authentication)
|
|
78
|
+
throw new Error('Public dispatch requires authentication configuration');
|
|
79
|
+
const trusted = requestContext(ev, this.authentication ? entry : 'legacy', this.authentication);
|
|
80
|
+
this.activeContexts.add(trusted);
|
|
81
|
+
try {
|
|
82
|
+
return yield this.contexts.run(trusted, () => this.dispatchRequest(ev, context, classes, trusted));
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
this.activeContexts.delete(trusted);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
dispatchRequest(ev, context, classes, trusted) {
|
|
49
90
|
return __awaiter(this, void 0, void 0, function* () {
|
|
50
91
|
const request = deserialize(ev.body, classes);
|
|
92
|
+
if (!request || typeof request.interfaceName !== 'string' || typeof request.methodName !== 'string' || !Array.isArray(request.args) || typeof request.sessionId !== 'string')
|
|
93
|
+
throw new Error('Malformed dispatch request');
|
|
51
94
|
const logName = `${request.interfaceName}.${request.methodName}`;
|
|
52
95
|
let sessionId = request.sessionId; // May be empty
|
|
53
96
|
// Pseudo interface/method to request socket access and return a session id to use in connect
|
|
54
|
-
// session id
|
|
97
|
+
// Legacy mode passes the session id; authenticated mode issues a separate credential.
|
|
55
98
|
if (logName === '$WebSocket.$authorize') {
|
|
99
|
+
if (this.authentication) {
|
|
100
|
+
if (trusted.dispatch !== 'protected')
|
|
101
|
+
throw new Error('Socket authorization requires protected dispatch');
|
|
102
|
+
const session = yield ownedSession(sessionId, sessionOwner(trusted), this.expirationMinutes);
|
|
103
|
+
const data = yield issueConnectionCredential(session, this.authentication.connectionCredentialSeconds, process.env.IS_OFFLINE ? 'ws://localhost:3001' : `wss://${process.env.APIG_ENDPOINT}`);
|
|
104
|
+
return serialize({ data, sessionId: session.sessionId, exception: undefined, cargo: undefined }, classes);
|
|
105
|
+
}
|
|
56
106
|
const socketResult = yield getSessionData(sessionId, "");
|
|
57
107
|
if (!socketResult) {
|
|
58
108
|
sessionId = context.awsRequestId;
|
|
@@ -73,15 +123,19 @@ export class ClassifyServerless {
|
|
|
73
123
|
const classDef = this.classDefs.get(request.interfaceName);
|
|
74
124
|
if (!classDef)
|
|
75
125
|
throw new Error(`No Response defined for ${request.interfaceName}`);
|
|
76
|
-
if (!classDef.clientClass.prototype[request.methodName])
|
|
126
|
+
if (request.methodName === 'constructor' || !Object.prototype.hasOwnProperty.call(classDef.clientClass.prototype, request.methodName) || typeof classDef.clientClass.prototype[request.methodName] !== 'function')
|
|
77
127
|
throw new Error(`${request.methodName} not found in ${request.interfaceName} client request class`);
|
|
128
|
+
if (this.authentication && classDef.publicMethods.has(request.methodName) !== (trusted.dispatch === 'public'))
|
|
129
|
+
throw new Error('Member is not permitted on this dispatch endpoint');
|
|
78
130
|
// If authorizer passed in make sure request is authorized
|
|
79
|
-
if (!classDef.authorizer || (yield classDef.authorizer(request.interfaceName, request.methodName, request.args))) {
|
|
131
|
+
if (!classDef.authorizer || (yield (this.authentication ? classDef.authorizer(request.interfaceName, request.methodName, request.args, trusted) : classDef.authorizer(request.interfaceName, request.methodName, request.args)))) {
|
|
80
132
|
// Instantiate the class
|
|
81
133
|
const obj = new classDef.serverClass();
|
|
82
134
|
// Retrieve session data from DynamoDB base on sessionId in request
|
|
83
135
|
let orignalSessionData = "";
|
|
84
|
-
const result = yield getSessionData(sessionId, request.interfaceName);
|
|
136
|
+
const result = this.authentication ? yield ownedSession(sessionId, sessionOwner(trusted), this.expirationMinutes) : yield getSessionData(sessionId, request.interfaceName);
|
|
137
|
+
if (this.authentication)
|
|
138
|
+
sessionId = result.sessionId;
|
|
85
139
|
if (!result)
|
|
86
140
|
sessionId = context.awsRequestId;
|
|
87
141
|
else {
|
|
@@ -91,6 +145,7 @@ export class ClassifyServerless {
|
|
|
91
145
|
Object.assign(obj, orignalSessionData);
|
|
92
146
|
}
|
|
93
147
|
}
|
|
148
|
+
this.responseContexts.set(obj, trusted);
|
|
94
149
|
obj.__sessionId__ = sessionId;
|
|
95
150
|
obj.__connectionId__ = result === null || result === void 0 ? void 0 : result.connectionId;
|
|
96
151
|
// Call the class method and catch any exceptions it might throw to pass back
|
|
@@ -111,7 +166,7 @@ export class ClassifyServerless {
|
|
|
111
166
|
}
|
|
112
167
|
const updatedSessionData = serialize(obj, classes);
|
|
113
168
|
if (!result || updatedSessionData !== orignalSessionData || (Date.now() > (result.updated + updateDebounceInterval)))
|
|
114
|
-
sessionId = yield saveSessionData(sessionId, request.interfaceName, updatedSessionData, undefined, obj.__userId__, this.expirationMinutes);
|
|
169
|
+
sessionId = yield saveSessionData(sessionId, request.interfaceName, updatedSessionData, undefined, obj.__userId__, this.expirationMinutes, this.authentication ? sessionOwner(trusted) : undefined);
|
|
115
170
|
// Formulate response
|
|
116
171
|
const lambdaResponse = {
|
|
117
172
|
data: fnResponse,
|
|
@@ -146,7 +201,13 @@ export class ClassifyServerless {
|
|
|
146
201
|
try {
|
|
147
202
|
// Build request
|
|
148
203
|
const sessionId = this['__sessionId__'];
|
|
149
|
-
|
|
204
|
+
let connectionId = this['__connectionId__'];
|
|
205
|
+
if (expressServer.authentication && sessionId) {
|
|
206
|
+
const live = yield getSessionData(sessionId);
|
|
207
|
+
if (!(live === null || live === void 0 ? void 0 : live.authOwner) || live.authOwner === 'public' || !(live.expires > Math.floor(Date.now() / 1000)))
|
|
208
|
+
throw new Error('Invalid authenticated notification session');
|
|
209
|
+
connectionId = live.connectionId;
|
|
210
|
+
}
|
|
150
211
|
if (!sessionId)
|
|
151
212
|
throw new Error(`You must instantiate ${interfaceName} using the ClassifyServerless.createRequest`);
|
|
152
213
|
if (!connectionId)
|
|
@@ -213,6 +274,8 @@ export class ClassifyServerless {
|
|
|
213
274
|
const result = yield getSessionData(sessionId);
|
|
214
275
|
if (!result)
|
|
215
276
|
throw new Error(`ClassifyResponse.createRequestForSession: invalid session id`);
|
|
277
|
+
if (this.authentication && (!result.authOwner || result.authOwner === 'public' || !(result.expires > Math.floor(Date.now() / 1000))))
|
|
278
|
+
throw new Error('Invalid authenticated notification session');
|
|
216
279
|
if (!result.connectionId)
|
|
217
280
|
throw new Error(`WebSocket handshake not established for session`);
|
|
218
281
|
// Create request
|
|
@@ -230,6 +293,8 @@ export class ClassifyServerless {
|
|
|
230
293
|
const result = yield getSessionData(sessionId, interfaceName);
|
|
231
294
|
if (!result)
|
|
232
295
|
throw new Error(`ClassifyResponse.createResponse: invalid session id for interface ${interfaceName}`);
|
|
296
|
+
if (this.authentication && (!result.authOwner || result.authOwner === 'public' || !(result.expires > Math.floor(Date.now() / 1000))))
|
|
297
|
+
throw new Error('Invalid authenticated notification session');
|
|
233
298
|
const sessionData = result[`interface_${interfaceName}`];
|
|
234
299
|
// Instantiate the class
|
|
235
300
|
const obj = (new responseClass());
|
|
@@ -246,7 +311,7 @@ export class ClassifyServerless {
|
|
|
246
311
|
if (updatedSessionData !== sessionData || (Date.now() > (result.updated + updateDebounceInterval))) {
|
|
247
312
|
if (this.logLevel.calls)
|
|
248
313
|
this.log(`saving session data for ${interfaceName} with sessionId=${sessionId} connectionId= ${obj.__connectionId__} sessionData=${updatedSessionData}`);
|
|
249
|
-
yield saveSessionData(sessionId, interfaceName, updatedSessionData, undefined, undefined, this.expirationMinutes);
|
|
314
|
+
yield saveSessionData(sessionId, interfaceName, updatedSessionData, undefined, undefined, this.expirationMinutes, this.authentication ? result.authOwner : undefined);
|
|
250
315
|
}
|
|
251
316
|
return ret;
|
|
252
317
|
});
|
|
@@ -263,7 +328,7 @@ export class ClassifyServerless {
|
|
|
263
328
|
},
|
|
264
329
|
ProjectionExpression: `sessionId`
|
|
265
330
|
});
|
|
266
|
-
return data.Items ? data.Items.map(i => i.sessionId) : [];
|
|
331
|
+
return data.Items ? data.Items.filter(i => !this.authentication || !i.sessionId.startsWith('connection#')).map(i => i.sessionId) : [];
|
|
267
332
|
}
|
|
268
333
|
return [];
|
|
269
334
|
});
|
|
@@ -274,7 +339,7 @@ export class ClassifyServerless {
|
|
|
274
339
|
TableName: `classifySessionStore.${process.env.DOMAIN}`,
|
|
275
340
|
ProjectionExpression: `sessionId`
|
|
276
341
|
});
|
|
277
|
-
return data.Items ? data.Items.map(i => i.sessionId) : [];
|
|
342
|
+
return data.Items ? data.Items.filter(i => !this.authentication || !i.sessionId.startsWith('connection#')).map(i => i.sessionId) : [];
|
|
278
343
|
});
|
|
279
344
|
}
|
|
280
345
|
deleteSessionsForUserId(userId) {
|
|
@@ -322,14 +387,14 @@ export function getSessionData(sessionId, interfaceName = "") {
|
|
|
322
387
|
TableName: `classifySessionStore.${process.env.DOMAIN}`,
|
|
323
388
|
Key: { sessionId },
|
|
324
389
|
ConsistentRead: true,
|
|
325
|
-
ProjectionExpression: `interface_${interfaceName}, connectionId, userId, updated, expires`
|
|
390
|
+
ProjectionExpression: `interface_${interfaceName}, connectionId, userId, updated, expires, authOwner`
|
|
326
391
|
});
|
|
327
392
|
return data.Item;
|
|
328
393
|
}
|
|
329
394
|
return undefined;
|
|
330
395
|
});
|
|
331
396
|
}
|
|
332
|
-
export function saveSessionData(sessionId, interfaceName, sessionData, connectionId, userId, expirationMinutes) {
|
|
397
|
+
export function saveSessionData(sessionId, interfaceName, sessionData, connectionId, userId, expirationMinutes, authOwner) {
|
|
333
398
|
return __awaiter(this, void 0, void 0, function* () {
|
|
334
399
|
const updateExpressionComponents = ['updated = :time'];
|
|
335
400
|
const expressionAttributeValues = { ':time': new Date().getTime() };
|
|
@@ -352,12 +417,7 @@ export function saveSessionData(sessionId, interfaceName, sessionData, connectio
|
|
|
352
417
|
expressionAttributeValues[':userId'] = userId;
|
|
353
418
|
}
|
|
354
419
|
// First try and out data only if the session id exists
|
|
355
|
-
yield ddbDocClient.update({
|
|
356
|
-
TableName: `classifySessionStore.${process.env.DOMAIN}`,
|
|
357
|
-
Key: { sessionId },
|
|
358
|
-
UpdateExpression: `set ${updateExpressionComponents.join(", ")}`,
|
|
359
|
-
ExpressionAttributeValues: expressionAttributeValues
|
|
360
|
-
});
|
|
420
|
+
yield ddbDocClient.update(Object.assign({ TableName: `classifySessionStore.${process.env.DOMAIN}`, Key: { sessionId }, UpdateExpression: `set ${updateExpressionComponents.join(", ")}`, ExpressionAttributeValues: Object.assign(Object.assign({}, expressionAttributeValues), (authOwner ? { ':owner': authOwner, ':now': Math.floor(Date.now() / 1000) } : {})) }, (authOwner ? { ConditionExpression: 'authOwner = :owner AND expires > :now' } : {})));
|
|
361
421
|
return sessionId;
|
|
362
422
|
});
|
|
363
423
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { DynamoDBDocument } from '@aws-sdk/lib-dynamodb';
|
|
2
|
+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
3
|
+
export const sessionStore = DynamoDBDocument.from(new DynamoDBClient({ region: process.env.DD_REGION, endpoint: process.env.DD_ENDPOINT }));
|
|
4
|
+
export const sessionTable = () => `classifySessionStore.${process.env.DOMAIN}`;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
1
|
+
// Preserve the existing package subpath while sharing the common definitions.
|
|
2
|
+
export * from "aws-classify-common";
|
package/lib/esm/dispatch.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { HttpEvent } from './Authentication';
|
|
1
2
|
import { APIGatewayProxyEvent, Context } from 'aws-lambda';
|
|
2
3
|
import { ClassifyServerless } from "./index";
|
|
3
4
|
import { APIGatewayProxyStructuredResultV2 } from "aws-lambda/trigger/api-gateway-proxy";
|
|
4
5
|
export declare const classifyServerless: ClassifyServerless;
|
|
5
|
-
export declare const responseHandler: (event:
|
|
6
|
+
export declare const responseHandler: (event: HttpEvent, context: Context) => Promise<APIGatewayProxyStructuredResultV2>;
|
|
7
|
+
export declare const publicResponseHandler: (event: HttpEvent, context: Context) => Promise<APIGatewayProxyStructuredResultV2>;
|
|
6
8
|
export declare const webSocketConnect: (event: APIGatewayProxyEvent, _context: Context) => Promise<APIGatewayProxyStructuredResultV2>;
|
|
7
|
-
export declare const webSocketDisconnect: (
|
|
9
|
+
export declare const webSocketDisconnect: (event: APIGatewayProxyEvent, _context: Context) => Promise<APIGatewayProxyStructuredResultV2>;
|
package/lib/esm/dispatch.js
CHANGED
|
@@ -7,37 +7,54 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
+
import { attachAuthenticatedSocket, detachAuthenticatedSocket } from './AuthenticatedSessions';
|
|
10
11
|
import { ClassifyServerless } from "./index";
|
|
11
12
|
import { serialize } from "js-freeze-dry";
|
|
12
13
|
import { getSessionData, saveSessionData } from "./ClassifyServerless";
|
|
13
14
|
export const classifyServerless = new ClassifyServerless();
|
|
14
|
-
|
|
15
|
+
const httpHandler = (event, context, entry) => __awaiter(void 0, void 0, void 0, function* () {
|
|
15
16
|
try {
|
|
16
17
|
//console.log(`dispatching`);
|
|
17
18
|
return {
|
|
18
19
|
statusCode: 200,
|
|
19
|
-
|
|
20
|
+
headers: { 'Cache-Control': 'no-store' },
|
|
21
|
+
body: (yield classifyServerless.dispatch(event, context, {}, entry)) || "",
|
|
20
22
|
};
|
|
21
23
|
}
|
|
22
24
|
catch (err) {
|
|
23
25
|
//console.log(`request.body = ${event.body}`);
|
|
24
|
-
|
|
26
|
+
// Do not reflect parse, database, or framework errors to an untrusted caller.
|
|
27
|
+
// Operators still receive the original error in Lambda's logs.
|
|
28
|
+
console.error('Dispatch failed', err);
|
|
25
29
|
return {
|
|
26
30
|
statusCode: 200,
|
|
31
|
+
headers: { 'Cache-Control': 'no-store' },
|
|
27
32
|
body: serialize({
|
|
28
33
|
data: undefined,
|
|
29
|
-
|
|
34
|
+
// Keep the existing serialized-error / HTTP-200 contract for clients.
|
|
35
|
+
exception: 'Internal Server Error',
|
|
30
36
|
cargo: undefined,
|
|
31
37
|
sessionId: undefined
|
|
32
38
|
})
|
|
33
39
|
};
|
|
34
40
|
}
|
|
35
41
|
});
|
|
42
|
+
export const responseHandler = (event, context) => httpHandler(event, context, 'protected');
|
|
43
|
+
export const publicResponseHandler = (event, context) => httpHandler(event, context, 'public');
|
|
36
44
|
export const webSocketConnect = (event, _context) => __awaiter(void 0, void 0, void 0, function* () {
|
|
37
45
|
//console.log('connecting');
|
|
38
|
-
const sessionId = event.headers['Sec-WebSocket-Protocol'] || "";
|
|
46
|
+
const sessionId = event.headers['Sec-WebSocket-Protocol'] || event.headers['sec-websocket-protocol'] || "";
|
|
39
47
|
const connectId = event.requestContext.connectionId;
|
|
40
48
|
//console.log(JSON.stringify(event));
|
|
49
|
+
if (classifyServerless.authenticationEnabled) {
|
|
50
|
+
try {
|
|
51
|
+
yield attachAuthenticatedSocket(sessionId, connectId || '');
|
|
52
|
+
return { statusCode: 200, headers: { 'Sec-WebSocket-Protocol': sessionId }, body: '' };
|
|
53
|
+
}
|
|
54
|
+
catch (_a) {
|
|
55
|
+
return { statusCode: 403, body: 'Connection refused' };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
41
58
|
const result = yield getSessionData(sessionId); // Make sure session id passed in is valid
|
|
42
59
|
if (result) {
|
|
43
60
|
// Save connection id
|
|
@@ -54,14 +71,16 @@ export const webSocketConnect = (event, _context) => __awaiter(void 0, void 0, v
|
|
|
54
71
|
};
|
|
55
72
|
}
|
|
56
73
|
else {
|
|
57
|
-
console.log(
|
|
74
|
+
console.log('Invalid socket session');
|
|
58
75
|
return {
|
|
59
76
|
statusCode: 500,
|
|
60
77
|
body: 'Opps'
|
|
61
78
|
};
|
|
62
79
|
}
|
|
63
80
|
});
|
|
64
|
-
export const webSocketDisconnect = (
|
|
81
|
+
export const webSocketDisconnect = (event, _context) => __awaiter(void 0, void 0, void 0, function* () {
|
|
82
|
+
if (classifyServerless.authenticationEnabled && event.requestContext.connectionId)
|
|
83
|
+
yield detachAuthenticatedSocket(event.requestContext.connectionId);
|
|
65
84
|
//console.log('Disconnect ' + JSON.stringify(event));
|
|
66
85
|
//console.log(`webSocketDisconnect sessionId=${sessionId} connectId=${connectId}`);
|
|
67
86
|
return {
|
package/lib/esm/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { ClassifyServerless } from './ClassifyServerless';
|
|
2
2
|
export { ClassifyResponse } from './ClassifyResponse';
|
|
3
|
-
export { classifyServerless, webSocketConnect, webSocketDisconnect, responseHandler } from './dispatch';
|
|
3
|
+
export { classifyServerless, webSocketConnect, webSocketDisconnect, responseHandler, publicResponseHandler } from './dispatch';
|
|
4
|
+
export type { ServerAuthenticationOptions, HttpEvent } from './Authentication';
|
|
5
|
+
export type { RequestContext, AuthenticatedIdentity } from 'aws-classify-common';
|
package/lib/esm/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { ClassifyServerless } from './ClassifyServerless';
|
|
2
2
|
export { ClassifyResponse } from './ClassifyResponse';
|
|
3
|
-
export { classifyServerless, webSocketConnect, webSocketDisconnect, responseHandler } from './dispatch';
|
|
3
|
+
export { classifyServerless, webSocketConnect, webSocketDisconnect, responseHandler, publicResponseHandler } from './dispatch';
|
package/package.json
CHANGED
|
@@ -1,36 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aws-classify-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"main": "./lib/cjs/index.js",
|
|
5
5
|
"module": "./lib/esm/index.js",
|
|
6
6
|
"types": "./lib/esm/index.d.ts",
|
|
7
7
|
"files": [
|
|
8
|
-
"/lib",
|
|
8
|
+
"/lib",
|
|
9
|
+
"yml"
|
|
9
10
|
],
|
|
10
11
|
"description": "Typescript Interface to Lambda and Websockets",
|
|
11
12
|
"scripts": {
|
|
12
|
-
"prepare": "
|
|
13
|
+
"prepare": "npm run build",
|
|
13
14
|
"build": "npm run build:esm && npm run build:cjs",
|
|
14
15
|
"build:esm": "tsc",
|
|
15
16
|
"build:cjs": "tsc --module commonjs --outDir lib/cjs"
|
|
16
17
|
},
|
|
17
|
-
"repository":
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/selsamman/aws-classify.git",
|
|
21
|
+
"directory": "aws-classify-server"
|
|
22
|
+
},
|
|
18
23
|
"author": "Sam Elsamman",
|
|
19
24
|
"license": "MIT",
|
|
20
25
|
"devDependencies": {
|
|
21
26
|
"@types/aws-lambda": "^8.10.103",
|
|
22
27
|
"@types/node": "^18.8.3",
|
|
23
|
-
"@typescript-eslint/eslint-plugin": "^
|
|
24
|
-
"@typescript-eslint/parser": "^
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"typescript": "^4.8.2"
|
|
28
|
+
"@typescript-eslint/eslint-plugin": "^8.71.0",
|
|
29
|
+
"@typescript-eslint/parser": "^8.71.0",
|
|
30
|
+
"eslint": "^8.57.1",
|
|
31
|
+
"typescript": "^4.9.5"
|
|
28
32
|
},
|
|
29
33
|
"dependencies": {
|
|
30
|
-
"@aws-sdk/client-apigatewaymanagementapi": "^3.
|
|
31
|
-
"@aws-sdk/client-dynamodb": "^3.
|
|
32
|
-
"@aws-sdk/lib-dynamodb": "^3.
|
|
33
|
-
"aws-classify-common": "0.
|
|
34
|
+
"@aws-sdk/client-apigatewaymanagementapi": "^3.1146.0",
|
|
35
|
+
"@aws-sdk/client-dynamodb": "^3.1146.0",
|
|
36
|
+
"@aws-sdk/lib-dynamodb": "^3.1146.0",
|
|
37
|
+
"aws-classify-common": "0.2.2",
|
|
34
38
|
"js-freeze-dry": "^1.1.4"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=20.0.0"
|
|
35
42
|
}
|
|
36
43
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Serverless v3/v4 file resolver. Loaded only by the authenticated functions include.
|
|
2
|
+
module.exports = async ({resolveVariable}) => {
|
|
3
|
+
const config = await resolveVariable('self:custom.awsClassify, null');
|
|
4
|
+
if (!config || !config.authorizer || typeof config.authorizer.name !== 'string' || !config.authorizer.name.trim())
|
|
5
|
+
throw new Error('awsClassify authentication requires an authorizer object with a nonempty name');
|
|
6
|
+
const definitions = await resolveVariable('self:provider.httpApi.authorizers, null');
|
|
7
|
+
if (!definitions || !Object.prototype.hasOwnProperty.call(definitions, config.authorizer.name))
|
|
8
|
+
throw new Error('awsClassify authorizer must be defined in provider.httpApi.authorizers');
|
|
9
|
+
const scopes = config.authorizer.scopes;
|
|
10
|
+
if (scopes !== undefined && (!Array.isArray(scopes) || !scopes.every(scope => typeof scope === 'string' && scope.length && !/\s/.test(scope))))
|
|
11
|
+
throw new Error('awsClassify authorizer scopes must be an array of nonempty scope names');
|
|
12
|
+
const logging = await resolveVariable('self:provider.logs.websocket, null');
|
|
13
|
+
if (logging && (logging === true || logging.fullExecutionData !== false))
|
|
14
|
+
throw new Error('Authenticated WebSocket logging requires fullExecutionData: false to protect connection credentials');
|
|
15
|
+
return {authorizer: config.authorizer};
|
|
16
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
responseHandler:
|
|
2
|
+
environment:
|
|
3
|
+
AWS_CLASSIFY_AUTHENTICATION: 'true'
|
|
4
|
+
handler: ${self:custom.directories.responseHandlers}.responseHandler
|
|
5
|
+
events:
|
|
6
|
+
- httpApi:
|
|
7
|
+
path: /api/dispatch
|
|
8
|
+
method: ANY
|
|
9
|
+
authorizer: ${file(${self:custom.yml}/authentication-config.js):authorizer}
|
|
10
|
+
connectHandler:
|
|
11
|
+
environment:
|
|
12
|
+
AWS_CLASSIFY_AUTHENTICATION: 'true'
|
|
13
|
+
handler: ${self:custom.directories.responseHandlers}.webSocketConnect
|
|
14
|
+
events:
|
|
15
|
+
- websocket: $connect
|
|
16
|
+
disconnectHandler:
|
|
17
|
+
environment:
|
|
18
|
+
AWS_CLASSIFY_AUTHENTICATION: 'true'
|
|
19
|
+
handler: ${self:custom.directories.responseHandlers}.webSocketDisconnect
|
|
20
|
+
events:
|
|
21
|
+
- websocket: $disconnect
|
|
22
|
+
publicResponseHandler:
|
|
23
|
+
name: ${self:service}-${sls:stage}-public
|
|
24
|
+
environment:
|
|
25
|
+
AWS_CLASSIFY_AUTHENTICATION: 'true'
|
|
26
|
+
handler: ${self:custom.directories.responseHandlers}.publicResponseHandler
|
|
27
|
+
events:
|
|
28
|
+
- httpApi:
|
|
29
|
+
path: /api/dispatch/public
|
|
30
|
+
method: ANY
|
package/yml/provider-iam.yml
CHANGED
|
@@ -84,9 +84,12 @@ Resources:
|
|
|
84
84
|
- CloudFrontDistribution
|
|
85
85
|
- DomainName
|
|
86
86
|
Outputs:
|
|
87
|
+
ContentBucket:
|
|
88
|
+
Description: Content bucket
|
|
89
|
+
Value: !Ref S3Bucket
|
|
87
90
|
CloudFrontId:
|
|
88
91
|
Description: CloudFront Id
|
|
89
92
|
Value: !Ref CloudFrontDistribution
|
|
90
93
|
WebsiteUrl:
|
|
91
94
|
Description: Cloudfront URL
|
|
92
|
-
Value: !GetAtt CloudFrontDistribution.DomainName
|
|
95
|
+
Value: !GetAtt CloudFrontDistribution.DomainName
|