aws-classify-server 0.1.8 → 0.2.0
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 -331
- package/lib/cjs/AuthenticatedSessions.d.ts +6 -0
- package/lib/cjs/AuthenticatedSessions.js +93 -0
- package/lib/cjs/Authentication.d.ts +11 -0
- package/lib/cjs/Authentication.js +37 -0
- package/lib/cjs/ClassDef.d.ts +2 -1
- package/lib/cjs/ClassifyServerless.d.ts +15 -4
- package/lib/cjs/ClassifyServerless.js +82 -28
- 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 +24 -7
- 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 +11 -0
- package/lib/esm/Authentication.js +32 -0
- package/lib/esm/ClassDef.d.ts +2 -1
- package/lib/esm/ClassifyServerless.d.ts +15 -4
- package/lib/esm/ClassifyServerless.js +76 -22
- 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 +22 -6
- 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 +18 -0
- package/yml/functions-authenticated.yml +34 -0
- package/yml/provider-iam.yml +1 -0
- package/yml/resources-custom-website.yml +5 -0
- package/yml/resources-website-authenticated.yml +69 -0
- package/yml/resources-website.yml +5 -0
|
@@ -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,9 @@ 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>): void;
|
|
13
|
-
dispatch(ev:
|
|
22
|
+
registerResponse<T>(responseClass: new () => T, authorizer?: (_endPoint: T, _method: string, _args: IArguments, _context?: RequestContext) => Promise<boolean>): void;
|
|
23
|
+
dispatch(ev: HttpEvent, context: Context, classes?: {}, entry?: 'protected' | 'public'): Promise<string>;
|
|
24
|
+
private dispatchRequest;
|
|
14
25
|
registerRequest<T>(requestClass: new () => T, classes?: any): new () => T;
|
|
15
26
|
getSessionId<T>(responseObj: T): any;
|
|
16
27
|
getUserId<T>(responseObj: T): any;
|
|
@@ -24,7 +35,7 @@ export declare class ClassifyServerless {
|
|
|
24
35
|
deleteSessions(): Promise<void>;
|
|
25
36
|
}
|
|
26
37
|
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>;
|
|
38
|
+
export declare function saveSessionData(sessionId: string, interfaceName?: string, sessionData?: string, connectionId?: string, userId?: string, expirationMinutes?: number, authOwner?: string): Promise<string>;
|
|
28
39
|
export declare const EndPointsLogging: {
|
|
29
40
|
create: boolean;
|
|
30
41
|
connect: boolean;
|
|
@@ -8,19 +8,40 @@ 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 { validatePublicSuffix } 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({ publicSuffix: process.env.AWS_CLASSIFY_PUBLIC_SUFFIX || '' });
|
|
30
|
+
}
|
|
31
|
+
configureAuthentication(options) {
|
|
32
|
+
var _a;
|
|
33
|
+
validatePublicSuffix(options.publicSuffix);
|
|
34
|
+
const seconds = (_a = options.connectionCredentialSeconds) !== null && _a !== void 0 ? _a : 60;
|
|
35
|
+
if (!Number.isInteger(seconds) || seconds < 1 || seconds > 300)
|
|
36
|
+
throw new Error('connectionCredentialSeconds must be 1–300');
|
|
37
|
+
this.authentication = Object.freeze(Object.assign(Object.assign({}, options), { connectionCredentialSeconds: seconds }));
|
|
38
|
+
}
|
|
39
|
+
get authenticationEnabled() { return !!this.authentication; }
|
|
40
|
+
getRequestContext(response) {
|
|
41
|
+
const current = this.contexts.getStore();
|
|
42
|
+
if (!current || !this.activeContexts.has(current))
|
|
43
|
+
return undefined;
|
|
44
|
+
return response ? (this.responseContexts.get(response) === current ? current : undefined) : current;
|
|
24
45
|
}
|
|
25
46
|
setExpirationMinutes(minutes) {
|
|
26
47
|
this.expirationMinutes = minutes;
|
|
@@ -45,14 +66,37 @@ export class ClassifyServerless {
|
|
|
45
66
|
this.classDefs.set(interfaceName, { serverClass: responseClass, clientClass, authorizer });
|
|
46
67
|
responseClass.__interfaceName__ = interfaceName;
|
|
47
68
|
}
|
|
48
|
-
dispatch(ev, context, classes = {}) {
|
|
69
|
+
dispatch(ev, context, classes = {}, entry = 'protected') {
|
|
70
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
71
|
+
if (entry === 'public' && !this.authentication)
|
|
72
|
+
throw new Error('Public dispatch requires authentication configuration');
|
|
73
|
+
const trusted = requestContext(ev, this.authentication ? entry : 'legacy', this.authentication);
|
|
74
|
+
this.activeContexts.add(trusted);
|
|
75
|
+
try {
|
|
76
|
+
return yield this.contexts.run(trusted, () => this.dispatchRequest(ev, context, classes, trusted));
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
this.activeContexts.delete(trusted);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
dispatchRequest(ev, context, classes, trusted) {
|
|
49
84
|
return __awaiter(this, void 0, void 0, function* () {
|
|
50
85
|
const request = deserialize(ev.body, classes);
|
|
86
|
+
if (!request || typeof request.interfaceName !== 'string' || typeof request.methodName !== 'string' || !Array.isArray(request.args) || typeof request.sessionId !== 'string')
|
|
87
|
+
throw new Error('Malformed dispatch request');
|
|
51
88
|
const logName = `${request.interfaceName}.${request.methodName}`;
|
|
52
89
|
let sessionId = request.sessionId; // May be empty
|
|
53
90
|
// Pseudo interface/method to request socket access and return a session id to use in connect
|
|
54
|
-
// session id
|
|
91
|
+
// Legacy mode passes the session id; authenticated mode issues a separate credential.
|
|
55
92
|
if (logName === '$WebSocket.$authorize') {
|
|
93
|
+
if (this.authentication) {
|
|
94
|
+
if (trusted.dispatch !== 'protected')
|
|
95
|
+
throw new Error('Socket authorization requires protected dispatch');
|
|
96
|
+
const session = yield ownedSession(sessionId, sessionOwner(trusted), this.expirationMinutes);
|
|
97
|
+
const data = yield issueConnectionCredential(session, this.authentication.connectionCredentialSeconds, process.env.IS_OFFLINE ? 'ws://localhost:3001' : `wss://${process.env.APIG_ENDPOINT}`);
|
|
98
|
+
return serialize({ data, sessionId: session.sessionId, exception: undefined, cargo: undefined }, classes);
|
|
99
|
+
}
|
|
56
100
|
const socketResult = yield getSessionData(sessionId, "");
|
|
57
101
|
if (!socketResult) {
|
|
58
102
|
sessionId = context.awsRequestId;
|
|
@@ -73,15 +117,19 @@ export class ClassifyServerless {
|
|
|
73
117
|
const classDef = this.classDefs.get(request.interfaceName);
|
|
74
118
|
if (!classDef)
|
|
75
119
|
throw new Error(`No Response defined for ${request.interfaceName}`);
|
|
76
|
-
if (!classDef.clientClass.prototype[request.methodName])
|
|
120
|
+
if (request.methodName === 'constructor' || !Object.prototype.hasOwnProperty.call(classDef.clientClass.prototype, request.methodName) || typeof classDef.clientClass.prototype[request.methodName] !== 'function')
|
|
77
121
|
throw new Error(`${request.methodName} not found in ${request.interfaceName} client request class`);
|
|
122
|
+
if (this.authentication && request.methodName.endsWith(this.authentication.publicSuffix) !== (trusted.dispatch === 'public'))
|
|
123
|
+
throw new Error('Member is not permitted on this dispatch endpoint');
|
|
78
124
|
// If authorizer passed in make sure request is authorized
|
|
79
|
-
if (!classDef.authorizer || (yield classDef.authorizer(request.interfaceName, request.methodName, request.args))) {
|
|
125
|
+
if (!classDef.authorizer || (yield (this.authentication ? classDef.authorizer(request.interfaceName, request.methodName, request.args, trusted) : classDef.authorizer(request.interfaceName, request.methodName, request.args)))) {
|
|
80
126
|
// Instantiate the class
|
|
81
127
|
const obj = new classDef.serverClass();
|
|
82
128
|
// Retrieve session data from DynamoDB base on sessionId in request
|
|
83
129
|
let orignalSessionData = "";
|
|
84
|
-
const result = yield getSessionData(sessionId, request.interfaceName);
|
|
130
|
+
const result = this.authentication ? yield ownedSession(sessionId, sessionOwner(trusted), this.expirationMinutes) : yield getSessionData(sessionId, request.interfaceName);
|
|
131
|
+
if (this.authentication)
|
|
132
|
+
sessionId = result.sessionId;
|
|
85
133
|
if (!result)
|
|
86
134
|
sessionId = context.awsRequestId;
|
|
87
135
|
else {
|
|
@@ -91,6 +139,7 @@ export class ClassifyServerless {
|
|
|
91
139
|
Object.assign(obj, orignalSessionData);
|
|
92
140
|
}
|
|
93
141
|
}
|
|
142
|
+
this.responseContexts.set(obj, trusted);
|
|
94
143
|
obj.__sessionId__ = sessionId;
|
|
95
144
|
obj.__connectionId__ = result === null || result === void 0 ? void 0 : result.connectionId;
|
|
96
145
|
// Call the class method and catch any exceptions it might throw to pass back
|
|
@@ -111,7 +160,7 @@ export class ClassifyServerless {
|
|
|
111
160
|
}
|
|
112
161
|
const updatedSessionData = serialize(obj, classes);
|
|
113
162
|
if (!result || updatedSessionData !== orignalSessionData || (Date.now() > (result.updated + updateDebounceInterval)))
|
|
114
|
-
sessionId = yield saveSessionData(sessionId, request.interfaceName, updatedSessionData, undefined, obj.__userId__, this.expirationMinutes);
|
|
163
|
+
sessionId = yield saveSessionData(sessionId, request.interfaceName, updatedSessionData, undefined, obj.__userId__, this.expirationMinutes, this.authentication ? sessionOwner(trusted) : undefined);
|
|
115
164
|
// Formulate response
|
|
116
165
|
const lambdaResponse = {
|
|
117
166
|
data: fnResponse,
|
|
@@ -146,7 +195,13 @@ export class ClassifyServerless {
|
|
|
146
195
|
try {
|
|
147
196
|
// Build request
|
|
148
197
|
const sessionId = this['__sessionId__'];
|
|
149
|
-
|
|
198
|
+
let connectionId = this['__connectionId__'];
|
|
199
|
+
if (expressServer.authentication && sessionId) {
|
|
200
|
+
const live = yield getSessionData(sessionId);
|
|
201
|
+
if (!(live === null || live === void 0 ? void 0 : live.authOwner) || live.authOwner === 'public' || !(live.expires > Math.floor(Date.now() / 1000)))
|
|
202
|
+
throw new Error('Invalid authenticated notification session');
|
|
203
|
+
connectionId = live.connectionId;
|
|
204
|
+
}
|
|
150
205
|
if (!sessionId)
|
|
151
206
|
throw new Error(`You must instantiate ${interfaceName} using the ClassifyServerless.createRequest`);
|
|
152
207
|
if (!connectionId)
|
|
@@ -213,6 +268,8 @@ export class ClassifyServerless {
|
|
|
213
268
|
const result = yield getSessionData(sessionId);
|
|
214
269
|
if (!result)
|
|
215
270
|
throw new Error(`ClassifyResponse.createRequestForSession: invalid session id`);
|
|
271
|
+
if (this.authentication && (!result.authOwner || result.authOwner === 'public' || !(result.expires > Math.floor(Date.now() / 1000))))
|
|
272
|
+
throw new Error('Invalid authenticated notification session');
|
|
216
273
|
if (!result.connectionId)
|
|
217
274
|
throw new Error(`WebSocket handshake not established for session`);
|
|
218
275
|
// Create request
|
|
@@ -230,6 +287,8 @@ export class ClassifyServerless {
|
|
|
230
287
|
const result = yield getSessionData(sessionId, interfaceName);
|
|
231
288
|
if (!result)
|
|
232
289
|
throw new Error(`ClassifyResponse.createResponse: invalid session id for interface ${interfaceName}`);
|
|
290
|
+
if (this.authentication && (!result.authOwner || result.authOwner === 'public' || !(result.expires > Math.floor(Date.now() / 1000))))
|
|
291
|
+
throw new Error('Invalid authenticated notification session');
|
|
233
292
|
const sessionData = result[`interface_${interfaceName}`];
|
|
234
293
|
// Instantiate the class
|
|
235
294
|
const obj = (new responseClass());
|
|
@@ -246,7 +305,7 @@ export class ClassifyServerless {
|
|
|
246
305
|
if (updatedSessionData !== sessionData || (Date.now() > (result.updated + updateDebounceInterval))) {
|
|
247
306
|
if (this.logLevel.calls)
|
|
248
307
|
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);
|
|
308
|
+
yield saveSessionData(sessionId, interfaceName, updatedSessionData, undefined, undefined, this.expirationMinutes, this.authentication ? result.authOwner : undefined);
|
|
250
309
|
}
|
|
251
310
|
return ret;
|
|
252
311
|
});
|
|
@@ -263,7 +322,7 @@ export class ClassifyServerless {
|
|
|
263
322
|
},
|
|
264
323
|
ProjectionExpression: `sessionId`
|
|
265
324
|
});
|
|
266
|
-
return data.Items ? data.Items.map(i => i.sessionId) : [];
|
|
325
|
+
return data.Items ? data.Items.filter(i => !this.authentication || !i.sessionId.startsWith('connection#')).map(i => i.sessionId) : [];
|
|
267
326
|
}
|
|
268
327
|
return [];
|
|
269
328
|
});
|
|
@@ -274,7 +333,7 @@ export class ClassifyServerless {
|
|
|
274
333
|
TableName: `classifySessionStore.${process.env.DOMAIN}`,
|
|
275
334
|
ProjectionExpression: `sessionId`
|
|
276
335
|
});
|
|
277
|
-
return data.Items ? data.Items.map(i => i.sessionId) : [];
|
|
336
|
+
return data.Items ? data.Items.filter(i => !this.authentication || !i.sessionId.startsWith('connection#')).map(i => i.sessionId) : [];
|
|
278
337
|
});
|
|
279
338
|
}
|
|
280
339
|
deleteSessionsForUserId(userId) {
|
|
@@ -322,14 +381,14 @@ export function getSessionData(sessionId, interfaceName = "") {
|
|
|
322
381
|
TableName: `classifySessionStore.${process.env.DOMAIN}`,
|
|
323
382
|
Key: { sessionId },
|
|
324
383
|
ConsistentRead: true,
|
|
325
|
-
ProjectionExpression: `interface_${interfaceName}, connectionId, userId, updated, expires`
|
|
384
|
+
ProjectionExpression: `interface_${interfaceName}, connectionId, userId, updated, expires, authOwner`
|
|
326
385
|
});
|
|
327
386
|
return data.Item;
|
|
328
387
|
}
|
|
329
388
|
return undefined;
|
|
330
389
|
});
|
|
331
390
|
}
|
|
332
|
-
export function saveSessionData(sessionId, interfaceName, sessionData, connectionId, userId, expirationMinutes) {
|
|
391
|
+
export function saveSessionData(sessionId, interfaceName, sessionData, connectionId, userId, expirationMinutes, authOwner) {
|
|
333
392
|
return __awaiter(this, void 0, void 0, function* () {
|
|
334
393
|
const updateExpressionComponents = ['updated = :time'];
|
|
335
394
|
const expressionAttributeValues = { ':time': new Date().getTime() };
|
|
@@ -352,12 +411,7 @@ export function saveSessionData(sessionId, interfaceName, sessionData, connectio
|
|
|
352
411
|
expressionAttributeValues[':userId'] = userId;
|
|
353
412
|
}
|
|
354
413
|
// 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
|
-
});
|
|
414
|
+
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
415
|
return sessionId;
|
|
362
416
|
});
|
|
363
417
|
}
|
|
@@ -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,23 +7,26 @@ 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
|
-
console.log(
|
|
26
|
+
console.log('Dispatch failed');
|
|
25
27
|
return {
|
|
26
28
|
statusCode: 200,
|
|
29
|
+
headers: { 'Cache-Control': 'no-store' },
|
|
27
30
|
body: serialize({
|
|
28
31
|
data: undefined,
|
|
29
32
|
exception: `Internal Server Error (${err}) - see log at ${new Date()}`,
|
|
@@ -33,11 +36,22 @@ export const responseHandler = (event, context) => __awaiter(void 0, void 0, voi
|
|
|
33
36
|
};
|
|
34
37
|
}
|
|
35
38
|
});
|
|
39
|
+
export const responseHandler = (event, context) => httpHandler(event, context, 'protected');
|
|
40
|
+
export const publicResponseHandler = (event, context) => httpHandler(event, context, 'public');
|
|
36
41
|
export const webSocketConnect = (event, _context) => __awaiter(void 0, void 0, void 0, function* () {
|
|
37
42
|
//console.log('connecting');
|
|
38
|
-
const sessionId = event.headers['Sec-WebSocket-Protocol'] || "";
|
|
43
|
+
const sessionId = event.headers['Sec-WebSocket-Protocol'] || event.headers['sec-websocket-protocol'] || "";
|
|
39
44
|
const connectId = event.requestContext.connectionId;
|
|
40
45
|
//console.log(JSON.stringify(event));
|
|
46
|
+
if (classifyServerless.authenticationEnabled) {
|
|
47
|
+
try {
|
|
48
|
+
yield attachAuthenticatedSocket(sessionId, connectId || '');
|
|
49
|
+
return { statusCode: 200, headers: { 'Sec-WebSocket-Protocol': sessionId }, body: '' };
|
|
50
|
+
}
|
|
51
|
+
catch (_a) {
|
|
52
|
+
return { statusCode: 403, body: 'Connection refused' };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
41
55
|
const result = yield getSessionData(sessionId); // Make sure session id passed in is valid
|
|
42
56
|
if (result) {
|
|
43
57
|
// Save connection id
|
|
@@ -54,14 +68,16 @@ export const webSocketConnect = (event, _context) => __awaiter(void 0, void 0, v
|
|
|
54
68
|
};
|
|
55
69
|
}
|
|
56
70
|
else {
|
|
57
|
-
console.log(
|
|
71
|
+
console.log('Invalid socket session');
|
|
58
72
|
return {
|
|
59
73
|
statusCode: 500,
|
|
60
74
|
body: 'Opps'
|
|
61
75
|
};
|
|
62
76
|
}
|
|
63
77
|
});
|
|
64
|
-
export const webSocketDisconnect = (
|
|
78
|
+
export const webSocketDisconnect = (event, _context) => __awaiter(void 0, void 0, void 0, function* () {
|
|
79
|
+
if (classifyServerless.authenticationEnabled && event.requestContext.connectionId)
|
|
80
|
+
yield detachAuthenticatedSocket(event.requestContext.connectionId);
|
|
65
81
|
//console.log('Disconnect ' + JSON.stringify(event));
|
|
66
82
|
//console.log(`webSocketDisconnect sessionId=${sessionId} connectId=${connectId}`);
|
|
67
83
|
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.0",
|
|
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.0",
|
|
34
38
|
"js-freeze-dry": "^1.1.4"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=20.0.0"
|
|
35
42
|
}
|
|
36
43
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Serverless v3/v4 file resolver. Loaded only by the authenticated functions include.
|
|
2
|
+
const {validatePublicSuffix} = require('aws-classify-common');
|
|
3
|
+
module.exports = async ({resolveVariable}) => {
|
|
4
|
+
const config = await resolveVariable('self:custom.awsClassify, null');
|
|
5
|
+
if (!config || !config.authorizer || typeof config.authorizer.name !== 'string' || !config.authorizer.name.trim())
|
|
6
|
+
throw new Error('awsClassify authentication requires an authorizer object with a nonempty name');
|
|
7
|
+
const definitions = await resolveVariable('self:provider.httpApi.authorizers, null');
|
|
8
|
+
if (!definitions || !Object.prototype.hasOwnProperty.call(definitions, config.authorizer.name))
|
|
9
|
+
throw new Error('awsClassify authorizer must be defined in provider.httpApi.authorizers');
|
|
10
|
+
const scopes = config.authorizer.scopes;
|
|
11
|
+
if (scopes !== undefined && (!Array.isArray(scopes) || !scopes.every(scope => typeof scope === 'string' && scope.length && !/\s/.test(scope))))
|
|
12
|
+
throw new Error('awsClassify authorizer scopes must be an array of nonempty scope names');
|
|
13
|
+
const publicSuffix = validatePublicSuffix(config.publicSuffix);
|
|
14
|
+
const logging = await resolveVariable('self:provider.logs.websocket, null');
|
|
15
|
+
if (logging && (logging === true || logging.fullExecutionData !== false))
|
|
16
|
+
throw new Error('Authenticated WebSocket logging requires fullExecutionData: false to protect connection credentials');
|
|
17
|
+
return {authorizer: config.authorizer, publicSuffix};
|
|
18
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
responseHandler:
|
|
2
|
+
environment:
|
|
3
|
+
AWS_CLASSIFY_AUTHENTICATION: 'true'
|
|
4
|
+
AWS_CLASSIFY_PUBLIC_SUFFIX: ${file(${self:custom.yml}/authentication-config.js):publicSuffix}
|
|
5
|
+
handler: ${self:custom.directories.responseHandlers}.responseHandler
|
|
6
|
+
events:
|
|
7
|
+
- httpApi:
|
|
8
|
+
path: /api/dispatch
|
|
9
|
+
method: ANY
|
|
10
|
+
authorizer: ${file(${self:custom.yml}/authentication-config.js):authorizer}
|
|
11
|
+
connectHandler:
|
|
12
|
+
environment:
|
|
13
|
+
AWS_CLASSIFY_AUTHENTICATION: 'true'
|
|
14
|
+
AWS_CLASSIFY_PUBLIC_SUFFIX: ${file(${self:custom.yml}/authentication-config.js):publicSuffix}
|
|
15
|
+
handler: ${self:custom.directories.responseHandlers}.webSocketConnect
|
|
16
|
+
events:
|
|
17
|
+
- websocket: $connect
|
|
18
|
+
disconnectHandler:
|
|
19
|
+
environment:
|
|
20
|
+
AWS_CLASSIFY_AUTHENTICATION: 'true'
|
|
21
|
+
AWS_CLASSIFY_PUBLIC_SUFFIX: ${file(${self:custom.yml}/authentication-config.js):publicSuffix}
|
|
22
|
+
handler: ${self:custom.directories.responseHandlers}.webSocketDisconnect
|
|
23
|
+
events:
|
|
24
|
+
- websocket: $disconnect
|
|
25
|
+
publicResponseHandler:
|
|
26
|
+
name: ${self:service}-${sls:stage}-public
|
|
27
|
+
environment:
|
|
28
|
+
AWS_CLASSIFY_AUTHENTICATION: 'true'
|
|
29
|
+
AWS_CLASSIFY_PUBLIC_SUFFIX: ${file(${self:custom.yml}/authentication-config.js):publicSuffix}
|
|
30
|
+
handler: ${self:custom.directories.responseHandlers}.publicResponseHandler
|
|
31
|
+
events:
|
|
32
|
+
- httpApi:
|
|
33
|
+
path: /api/dispatch/public
|
|
34
|
+
method: ANY
|
package/yml/provider-iam.yml
CHANGED
|
@@ -50,6 +50,11 @@ Resources:
|
|
|
50
50
|
# https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-origin-request-policies.html
|
|
51
51
|
OriginRequestPolicyId: 88a5eaf4-2fd4-4709-b370-b4c650ea3fcf
|
|
52
52
|
ViewerProtocolPolicy: redirect-to-https
|
|
53
|
+
CustomErrorResponses:
|
|
54
|
+
- ErrorCachingMinTTL: 1
|
|
55
|
+
ErrorCode: 403
|
|
56
|
+
ResponseCode: 200
|
|
57
|
+
ResponsePagePath: "/"
|
|
53
58
|
S3Bucket:
|
|
54
59
|
Type: AWS::S3::Bucket
|
|
55
60
|
Properties:
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
Resources:
|
|
2
|
+
CloudFrontOriginAccessIdentity:
|
|
3
|
+
Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
|
|
4
|
+
Properties:
|
|
5
|
+
CloudFrontOriginAccessIdentityConfig:
|
|
6
|
+
Comment: !Join ["-", ["access-identity", !GetAtt S3Bucket.DomainName]]
|
|
7
|
+
CloudFrontDistribution:
|
|
8
|
+
Type: AWS::CloudFront::Distribution
|
|
9
|
+
DependsOn:
|
|
10
|
+
- S3Bucket
|
|
11
|
+
Properties:
|
|
12
|
+
DistributionConfig:
|
|
13
|
+
PriceClass: PriceClass_All
|
|
14
|
+
ViewerCertificate:
|
|
15
|
+
CloudFrontDefaultCertificate: true
|
|
16
|
+
Enabled: true
|
|
17
|
+
HttpVersion: http2
|
|
18
|
+
DefaultRootObject: index.html
|
|
19
|
+
Origins:
|
|
20
|
+
- Id: api
|
|
21
|
+
DomainName: !Join [".", [!Ref HttpApi, "execute-api", !Ref AWS::Region, "amazonaws.com"]]
|
|
22
|
+
CustomOriginConfig:
|
|
23
|
+
OriginProtocolPolicy: https-only
|
|
24
|
+
- Id: static
|
|
25
|
+
DomainName: !GetAtt S3Bucket.RegionalDomainName
|
|
26
|
+
S3OriginConfig:
|
|
27
|
+
OriginAccessIdentity: !Join ["", ["origin-access-identity/cloudfront/", !Ref CloudFrontOriginAccessIdentity]]
|
|
28
|
+
CacheBehaviors:
|
|
29
|
+
- PathPattern: api/*
|
|
30
|
+
TargetOriginId: api
|
|
31
|
+
# Managed-CachingDisabled and AllViewerExceptHostHeader preserve Authorization.
|
|
32
|
+
CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad
|
|
33
|
+
OriginRequestPolicyId: b689b0a8-53d0-40ab-baf2-68738e2966ac
|
|
34
|
+
ViewerProtocolPolicy: redirect-to-https
|
|
35
|
+
AllowedMethods: [GET, HEAD, OPTIONS, PATCH, POST, PUT, DELETE]
|
|
36
|
+
DefaultCacheBehavior:
|
|
37
|
+
TargetOriginId: static
|
|
38
|
+
# 'Managed-CachingOptimized'
|
|
39
|
+
# https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-cache-policies.html
|
|
40
|
+
CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6
|
|
41
|
+
# 'Managed-CORS-S3Origin'
|
|
42
|
+
# https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-origin-request-policies.html
|
|
43
|
+
OriginRequestPolicyId: 88a5eaf4-2fd4-4709-b370-b4c650ea3fcf
|
|
44
|
+
ViewerProtocolPolicy: redirect-to-https
|
|
45
|
+
CustomErrorResponses:
|
|
46
|
+
- ErrorCachingMinTTL: 1
|
|
47
|
+
ErrorCode: 403
|
|
48
|
+
S3Bucket:
|
|
49
|
+
Type: AWS::S3::Bucket
|
|
50
|
+
Properties:
|
|
51
|
+
BucketName: ${self:custom.bucketName}
|
|
52
|
+
S3BucketPolicy:
|
|
53
|
+
Type: AWS::S3::BucketPolicy
|
|
54
|
+
Properties:
|
|
55
|
+
Bucket: !Ref S3Bucket
|
|
56
|
+
PolicyDocument:
|
|
57
|
+
Statement:
|
|
58
|
+
- Effect: Allow
|
|
59
|
+
Action: "s3:GetObject"
|
|
60
|
+
Resource: !Join ["", [!GetAtt S3Bucket.Arn, "/*"]]
|
|
61
|
+
Principal:
|
|
62
|
+
CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId
|
|
63
|
+
Outputs:
|
|
64
|
+
CloudFrontId:
|
|
65
|
+
Description: CloudFront Id
|
|
66
|
+
Value: !Ref CloudFrontDistribution
|
|
67
|
+
WebsiteUrl:
|
|
68
|
+
Description: Cloudfront URL
|
|
69
|
+
Value: !GetAtt CloudFrontDistribution.DomainName
|
|
@@ -46,6 +46,11 @@ Resources:
|
|
|
46
46
|
# https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-origin-request-policies.html
|
|
47
47
|
OriginRequestPolicyId: 88a5eaf4-2fd4-4709-b370-b4c650ea3fcf
|
|
48
48
|
ViewerProtocolPolicy: redirect-to-https
|
|
49
|
+
CustomErrorResponses:
|
|
50
|
+
- ErrorCachingMinTTL: 1
|
|
51
|
+
ErrorCode: 403
|
|
52
|
+
ResponseCode: 200
|
|
53
|
+
ResponsePagePath: "/"
|
|
49
54
|
S3Bucket:
|
|
50
55
|
Type: AWS::S3::Bucket
|
|
51
56
|
Properties:
|