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
|
@@ -11,19 +11,39 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.EndPointsLogging = exports.saveSessionData = exports.getSessionData = exports.ClassifyServerless = void 0;
|
|
13
13
|
const js_freeze_dry_1 = require("js-freeze-dry");
|
|
14
|
-
const
|
|
15
|
-
const
|
|
14
|
+
const SessionStore_1 = require("./SessionStore");
|
|
15
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
16
|
+
const aws_classify_common_1 = require("aws-classify-common");
|
|
17
|
+
const Authentication_1 = require("./Authentication");
|
|
18
|
+
const AuthenticatedSessions_1 = require("./AuthenticatedSessions");
|
|
16
19
|
const client_apigatewaymanagementapi_1 = require("@aws-sdk/client-apigatewaymanagementapi");
|
|
17
|
-
const ddbClient = new client_dynamodb_1.DynamoDBClient({ region: process.env.DD_REGION, endpoint: process.env.DD_ENDPOINT });
|
|
18
|
-
const ddbDocClient = lib_dynamodb_1.DynamoDBDocument.from(ddbClient);
|
|
19
20
|
const updateDebounceInterval = 1000 * 60 * 10; // Don't update session unless 10 minutes have passed or data changed
|
|
20
21
|
class ClassifyServerless {
|
|
21
22
|
constructor() {
|
|
23
|
+
this.contexts = new node_async_hooks_1.AsyncLocalStorage();
|
|
24
|
+
this.activeContexts = new Set();
|
|
25
|
+
this.responseContexts = new WeakMap();
|
|
22
26
|
this.logLevel = {};
|
|
23
27
|
this.classDefs = new Map();
|
|
24
28
|
this.requests = new Map();
|
|
25
29
|
this.expirationMinutes = 60 * 24;
|
|
26
30
|
this.log = msg => console.log(msg);
|
|
31
|
+
if (process.env.AWS_CLASSIFY_AUTHENTICATION === 'true')
|
|
32
|
+
this.configureAuthentication({});
|
|
33
|
+
}
|
|
34
|
+
configureAuthentication(options) {
|
|
35
|
+
var _a;
|
|
36
|
+
const seconds = (_a = options.connectionCredentialSeconds) !== null && _a !== void 0 ? _a : 60;
|
|
37
|
+
if (!Number.isInteger(seconds) || seconds < 1 || seconds > 300)
|
|
38
|
+
throw new Error('connectionCredentialSeconds must be 1–300');
|
|
39
|
+
this.authentication = Object.freeze(Object.assign(Object.assign({}, options), { connectionCredentialSeconds: seconds }));
|
|
40
|
+
}
|
|
41
|
+
get authenticationEnabled() { return !!this.authentication; }
|
|
42
|
+
getRequestContext(response) {
|
|
43
|
+
const current = this.contexts.getStore();
|
|
44
|
+
if (!current || !this.activeContexts.has(current))
|
|
45
|
+
return undefined;
|
|
46
|
+
return response ? (this.responseContexts.get(response) === current ? current : undefined) : current;
|
|
27
47
|
}
|
|
28
48
|
setExpirationMinutes(minutes) {
|
|
29
49
|
this.expirationMinutes = minutes;
|
|
@@ -34,7 +54,7 @@ class ClassifyServerless {
|
|
|
34
54
|
setLogLevel(logLevel) {
|
|
35
55
|
this.logLevel = logLevel;
|
|
36
56
|
}
|
|
37
|
-
registerResponse(responseClass, authorizer) {
|
|
57
|
+
registerResponse(responseClass, authorizer, options) {
|
|
38
58
|
// Find ultimate base class
|
|
39
59
|
let clientClass = responseClass;
|
|
40
60
|
while (Object.getPrototypeOf(clientClass).prototype)
|
|
@@ -45,17 +65,47 @@ class ClassifyServerless {
|
|
|
45
65
|
const interfaceName = clientClass['interfaceName'];
|
|
46
66
|
if (!interfaceName)
|
|
47
67
|
throw ('ClassifyServerless.registerResponse: Request must have interfaceName as a static property');
|
|
48
|
-
|
|
68
|
+
const methods = Object.getOwnPropertyNames(clientClass.prototype).filter(method => method !== 'constructor' && typeof clientClass.prototype[method] === 'function');
|
|
69
|
+
const decorated = methods.filter(method => (0, aws_classify_common_1.isPublicEndpoint)(clientClass.prototype[method]));
|
|
70
|
+
if ((options === null || options === void 0 ? void 0 : options.publicMethods) !== undefined && decorated.length)
|
|
71
|
+
throw new Error('Use either @Public() or publicMethods, not both');
|
|
72
|
+
const publicMethods = (options === null || options === void 0 ? void 0 : options.publicMethods) === undefined ? decorated : [...options.publicMethods];
|
|
73
|
+
if (!publicMethods.every(method => typeof method === 'string' && methods.includes(method)) || new Set(publicMethods).size !== publicMethods.length)
|
|
74
|
+
throw new Error('publicMethods must be unique request-class method names');
|
|
75
|
+
this.classDefs.set(interfaceName, { serverClass: responseClass, clientClass, authorizer, publicMethods: new Set(publicMethods) });
|
|
49
76
|
responseClass.__interfaceName__ = interfaceName;
|
|
50
77
|
}
|
|
51
|
-
dispatch(ev, context, classes = {}) {
|
|
78
|
+
dispatch(ev, context, classes = {}, entry = 'protected') {
|
|
79
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
80
|
+
if (entry === 'public' && !this.authentication)
|
|
81
|
+
throw new Error('Public dispatch requires authentication configuration');
|
|
82
|
+
const trusted = (0, Authentication_1.requestContext)(ev, this.authentication ? entry : 'legacy', this.authentication);
|
|
83
|
+
this.activeContexts.add(trusted);
|
|
84
|
+
try {
|
|
85
|
+
return yield this.contexts.run(trusted, () => this.dispatchRequest(ev, context, classes, trusted));
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
this.activeContexts.delete(trusted);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
dispatchRequest(ev, context, classes, trusted) {
|
|
52
93
|
return __awaiter(this, void 0, void 0, function* () {
|
|
53
94
|
const request = (0, js_freeze_dry_1.deserialize)(ev.body, classes);
|
|
95
|
+
if (!request || typeof request.interfaceName !== 'string' || typeof request.methodName !== 'string' || !Array.isArray(request.args) || typeof request.sessionId !== 'string')
|
|
96
|
+
throw new Error('Malformed dispatch request');
|
|
54
97
|
const logName = `${request.interfaceName}.${request.methodName}`;
|
|
55
98
|
let sessionId = request.sessionId; // May be empty
|
|
56
99
|
// Pseudo interface/method to request socket access and return a session id to use in connect
|
|
57
|
-
// session id
|
|
100
|
+
// Legacy mode passes the session id; authenticated mode issues a separate credential.
|
|
58
101
|
if (logName === '$WebSocket.$authorize') {
|
|
102
|
+
if (this.authentication) {
|
|
103
|
+
if (trusted.dispatch !== 'protected')
|
|
104
|
+
throw new Error('Socket authorization requires protected dispatch');
|
|
105
|
+
const session = yield (0, AuthenticatedSessions_1.ownedSession)(sessionId, (0, AuthenticatedSessions_1.sessionOwner)(trusted), this.expirationMinutes);
|
|
106
|
+
const data = yield (0, AuthenticatedSessions_1.issueConnectionCredential)(session, this.authentication.connectionCredentialSeconds, process.env.IS_OFFLINE ? 'ws://localhost:3001' : `wss://${process.env.APIG_ENDPOINT}`);
|
|
107
|
+
return (0, js_freeze_dry_1.serialize)({ data, sessionId: session.sessionId, exception: undefined, cargo: undefined }, classes);
|
|
108
|
+
}
|
|
59
109
|
const socketResult = yield getSessionData(sessionId, "");
|
|
60
110
|
if (!socketResult) {
|
|
61
111
|
sessionId = context.awsRequestId;
|
|
@@ -76,15 +126,19 @@ class ClassifyServerless {
|
|
|
76
126
|
const classDef = this.classDefs.get(request.interfaceName);
|
|
77
127
|
if (!classDef)
|
|
78
128
|
throw new Error(`No Response defined for ${request.interfaceName}`);
|
|
79
|
-
if (!classDef.clientClass.prototype[request.methodName])
|
|
129
|
+
if (request.methodName === 'constructor' || !Object.prototype.hasOwnProperty.call(classDef.clientClass.prototype, request.methodName) || typeof classDef.clientClass.prototype[request.methodName] !== 'function')
|
|
80
130
|
throw new Error(`${request.methodName} not found in ${request.interfaceName} client request class`);
|
|
131
|
+
if (this.authentication && classDef.publicMethods.has(request.methodName) !== (trusted.dispatch === 'public'))
|
|
132
|
+
throw new Error('Member is not permitted on this dispatch endpoint');
|
|
81
133
|
// If authorizer passed in make sure request is authorized
|
|
82
|
-
if (!classDef.authorizer || (yield classDef.authorizer(request.interfaceName, request.methodName, request.args))) {
|
|
134
|
+
if (!classDef.authorizer || (yield (this.authentication ? classDef.authorizer(request.interfaceName, request.methodName, request.args, trusted) : classDef.authorizer(request.interfaceName, request.methodName, request.args)))) {
|
|
83
135
|
// Instantiate the class
|
|
84
136
|
const obj = new classDef.serverClass();
|
|
85
137
|
// Retrieve session data from DynamoDB base on sessionId in request
|
|
86
138
|
let orignalSessionData = "";
|
|
87
|
-
const result = yield getSessionData(sessionId, request.interfaceName);
|
|
139
|
+
const result = this.authentication ? yield (0, AuthenticatedSessions_1.ownedSession)(sessionId, (0, AuthenticatedSessions_1.sessionOwner)(trusted), this.expirationMinutes) : yield getSessionData(sessionId, request.interfaceName);
|
|
140
|
+
if (this.authentication)
|
|
141
|
+
sessionId = result.sessionId;
|
|
88
142
|
if (!result)
|
|
89
143
|
sessionId = context.awsRequestId;
|
|
90
144
|
else {
|
|
@@ -94,6 +148,7 @@ class ClassifyServerless {
|
|
|
94
148
|
Object.assign(obj, orignalSessionData);
|
|
95
149
|
}
|
|
96
150
|
}
|
|
151
|
+
this.responseContexts.set(obj, trusted);
|
|
97
152
|
obj.__sessionId__ = sessionId;
|
|
98
153
|
obj.__connectionId__ = result === null || result === void 0 ? void 0 : result.connectionId;
|
|
99
154
|
// Call the class method and catch any exceptions it might throw to pass back
|
|
@@ -114,7 +169,7 @@ class ClassifyServerless {
|
|
|
114
169
|
}
|
|
115
170
|
const updatedSessionData = (0, js_freeze_dry_1.serialize)(obj, classes);
|
|
116
171
|
if (!result || updatedSessionData !== orignalSessionData || (Date.now() > (result.updated + updateDebounceInterval)))
|
|
117
|
-
sessionId = yield saveSessionData(sessionId, request.interfaceName, updatedSessionData, undefined, obj.__userId__, this.expirationMinutes);
|
|
172
|
+
sessionId = yield saveSessionData(sessionId, request.interfaceName, updatedSessionData, undefined, obj.__userId__, this.expirationMinutes, this.authentication ? (0, AuthenticatedSessions_1.sessionOwner)(trusted) : undefined);
|
|
118
173
|
// Formulate response
|
|
119
174
|
const lambdaResponse = {
|
|
120
175
|
data: fnResponse,
|
|
@@ -149,7 +204,13 @@ class ClassifyServerless {
|
|
|
149
204
|
try {
|
|
150
205
|
// Build request
|
|
151
206
|
const sessionId = this['__sessionId__'];
|
|
152
|
-
|
|
207
|
+
let connectionId = this['__connectionId__'];
|
|
208
|
+
if (expressServer.authentication && sessionId) {
|
|
209
|
+
const live = yield getSessionData(sessionId);
|
|
210
|
+
if (!(live === null || live === void 0 ? void 0 : live.authOwner) || live.authOwner === 'public' || !(live.expires > Math.floor(Date.now() / 1000)))
|
|
211
|
+
throw new Error('Invalid authenticated notification session');
|
|
212
|
+
connectionId = live.connectionId;
|
|
213
|
+
}
|
|
153
214
|
if (!sessionId)
|
|
154
215
|
throw new Error(`You must instantiate ${interfaceName} using the ClassifyServerless.createRequest`);
|
|
155
216
|
if (!connectionId)
|
|
@@ -216,6 +277,8 @@ class ClassifyServerless {
|
|
|
216
277
|
const result = yield getSessionData(sessionId);
|
|
217
278
|
if (!result)
|
|
218
279
|
throw new Error(`ClassifyResponse.createRequestForSession: invalid session id`);
|
|
280
|
+
if (this.authentication && (!result.authOwner || result.authOwner === 'public' || !(result.expires > Math.floor(Date.now() / 1000))))
|
|
281
|
+
throw new Error('Invalid authenticated notification session');
|
|
219
282
|
if (!result.connectionId)
|
|
220
283
|
throw new Error(`WebSocket handshake not established for session`);
|
|
221
284
|
// Create request
|
|
@@ -233,6 +296,8 @@ class ClassifyServerless {
|
|
|
233
296
|
const result = yield getSessionData(sessionId, interfaceName);
|
|
234
297
|
if (!result)
|
|
235
298
|
throw new Error(`ClassifyResponse.createResponse: invalid session id for interface ${interfaceName}`);
|
|
299
|
+
if (this.authentication && (!result.authOwner || result.authOwner === 'public' || !(result.expires > Math.floor(Date.now() / 1000))))
|
|
300
|
+
throw new Error('Invalid authenticated notification session');
|
|
236
301
|
const sessionData = result[`interface_${interfaceName}`];
|
|
237
302
|
// Instantiate the class
|
|
238
303
|
const obj = (new responseClass());
|
|
@@ -249,7 +314,7 @@ class ClassifyServerless {
|
|
|
249
314
|
if (updatedSessionData !== sessionData || (Date.now() > (result.updated + updateDebounceInterval))) {
|
|
250
315
|
if (this.logLevel.calls)
|
|
251
316
|
this.log(`saving session data for ${interfaceName} with sessionId=${sessionId} connectionId= ${obj.__connectionId__} sessionData=${updatedSessionData}`);
|
|
252
|
-
yield saveSessionData(sessionId, interfaceName, updatedSessionData, undefined, undefined, this.expirationMinutes);
|
|
317
|
+
yield saveSessionData(sessionId, interfaceName, updatedSessionData, undefined, undefined, this.expirationMinutes, this.authentication ? result.authOwner : undefined);
|
|
253
318
|
}
|
|
254
319
|
return ret;
|
|
255
320
|
});
|
|
@@ -257,7 +322,7 @@ class ClassifyServerless {
|
|
|
257
322
|
getSessionsForUserId(userId) {
|
|
258
323
|
return __awaiter(this, void 0, void 0, function* () {
|
|
259
324
|
if (userId) {
|
|
260
|
-
const data = yield
|
|
325
|
+
const data = yield SessionStore_1.sessionStore.query({
|
|
261
326
|
TableName: `classifySessionStore.${process.env.DOMAIN}`,
|
|
262
327
|
KeyConditionExpression: 'userId = :userId',
|
|
263
328
|
IndexName: 'userId',
|
|
@@ -266,24 +331,24 @@ class ClassifyServerless {
|
|
|
266
331
|
},
|
|
267
332
|
ProjectionExpression: `sessionId`
|
|
268
333
|
});
|
|
269
|
-
return data.Items ? data.Items.map(i => i.sessionId) : [];
|
|
334
|
+
return data.Items ? data.Items.filter(i => !this.authentication || !i.sessionId.startsWith('connection#')).map(i => i.sessionId) : [];
|
|
270
335
|
}
|
|
271
336
|
return [];
|
|
272
337
|
});
|
|
273
338
|
}
|
|
274
339
|
getSessions() {
|
|
275
340
|
return __awaiter(this, void 0, void 0, function* () {
|
|
276
|
-
const data = yield
|
|
341
|
+
const data = yield SessionStore_1.sessionStore.scan({
|
|
277
342
|
TableName: `classifySessionStore.${process.env.DOMAIN}`,
|
|
278
343
|
ProjectionExpression: `sessionId`
|
|
279
344
|
});
|
|
280
|
-
return data.Items ? data.Items.map(i => i.sessionId) : [];
|
|
345
|
+
return data.Items ? data.Items.filter(i => !this.authentication || !i.sessionId.startsWith('connection#')).map(i => i.sessionId) : [];
|
|
281
346
|
});
|
|
282
347
|
}
|
|
283
348
|
deleteSessionsForUserId(userId) {
|
|
284
349
|
return __awaiter(this, void 0, void 0, function* () {
|
|
285
350
|
if (userId) {
|
|
286
|
-
const data = yield
|
|
351
|
+
const data = yield SessionStore_1.sessionStore.query({
|
|
287
352
|
TableName: `classifySessionStore.${process.env.DOMAIN}`,
|
|
288
353
|
KeyConditionExpression: 'userId = :userId',
|
|
289
354
|
IndexName: 'userId',
|
|
@@ -299,7 +364,7 @@ class ClassifyServerless {
|
|
|
299
364
|
}
|
|
300
365
|
deleteSessions() {
|
|
301
366
|
return __awaiter(this, void 0, void 0, function* () {
|
|
302
|
-
const data = yield
|
|
367
|
+
const data = yield SessionStore_1.sessionStore.scan({
|
|
303
368
|
TableName: `classifySessionStore.${process.env.DOMAIN}`,
|
|
304
369
|
ProjectionExpression: `sessionId`
|
|
305
370
|
});
|
|
@@ -312,7 +377,7 @@ exports.ClassifyServerless = ClassifyServerless;
|
|
|
312
377
|
function deleteSessionByKeys(keys) {
|
|
313
378
|
return __awaiter(this, void 0, void 0, function* () {
|
|
314
379
|
for (let ix = 0; ix < keys.length; ++ix) {
|
|
315
|
-
yield
|
|
380
|
+
yield SessionStore_1.sessionStore.delete({
|
|
316
381
|
TableName: `classifySessionStore.${process.env.DOMAIN}`,
|
|
317
382
|
Key: { sessionId: keys[ix] }
|
|
318
383
|
});
|
|
@@ -322,11 +387,11 @@ function deleteSessionByKeys(keys) {
|
|
|
322
387
|
function getSessionData(sessionId, interfaceName = "") {
|
|
323
388
|
return __awaiter(this, void 0, void 0, function* () {
|
|
324
389
|
if (sessionId) {
|
|
325
|
-
const data = yield
|
|
390
|
+
const data = yield SessionStore_1.sessionStore.get({
|
|
326
391
|
TableName: `classifySessionStore.${process.env.DOMAIN}`,
|
|
327
392
|
Key: { sessionId },
|
|
328
393
|
ConsistentRead: true,
|
|
329
|
-
ProjectionExpression: `interface_${interfaceName}, connectionId, userId, updated, expires`
|
|
394
|
+
ProjectionExpression: `interface_${interfaceName}, connectionId, userId, updated, expires, authOwner`
|
|
330
395
|
});
|
|
331
396
|
return data.Item;
|
|
332
397
|
}
|
|
@@ -334,7 +399,7 @@ function getSessionData(sessionId, interfaceName = "") {
|
|
|
334
399
|
});
|
|
335
400
|
}
|
|
336
401
|
exports.getSessionData = getSessionData;
|
|
337
|
-
function saveSessionData(sessionId, interfaceName, sessionData, connectionId, userId, expirationMinutes) {
|
|
402
|
+
function saveSessionData(sessionId, interfaceName, sessionData, connectionId, userId, expirationMinutes, authOwner) {
|
|
338
403
|
return __awaiter(this, void 0, void 0, function* () {
|
|
339
404
|
const updateExpressionComponents = ['updated = :time'];
|
|
340
405
|
const expressionAttributeValues = { ':time': new Date().getTime() };
|
|
@@ -357,12 +422,7 @@ function saveSessionData(sessionId, interfaceName, sessionData, connectionId, us
|
|
|
357
422
|
expressionAttributeValues[':userId'] = userId;
|
|
358
423
|
}
|
|
359
424
|
// First try and out data only if the session id exists
|
|
360
|
-
yield
|
|
361
|
-
TableName: `classifySessionStore.${process.env.DOMAIN}`,
|
|
362
|
-
Key: { sessionId },
|
|
363
|
-
UpdateExpression: `set ${updateExpressionComponents.join(", ")}`,
|
|
364
|
-
ExpressionAttributeValues: expressionAttributeValues
|
|
365
|
-
});
|
|
425
|
+
yield SessionStore_1.sessionStore.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' } : {})));
|
|
366
426
|
return sessionId;
|
|
367
427
|
});
|
|
368
428
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sessionTable = exports.sessionStore = void 0;
|
|
4
|
+
const lib_dynamodb_1 = require("@aws-sdk/lib-dynamodb");
|
|
5
|
+
const client_dynamodb_1 = require("@aws-sdk/client-dynamodb");
|
|
6
|
+
exports.sessionStore = lib_dynamodb_1.DynamoDBDocument.from(new client_dynamodb_1.DynamoDBClient({ region: process.env.DD_REGION, endpoint: process.env.DD_ENDPOINT }));
|
|
7
|
+
const sessionTable = () => `classifySessionStore.${process.env.DOMAIN}`;
|
|
8
|
+
exports.sessionTable = sessionTable;
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.EndPointsLogging = void 0;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
connect: true,
|
|
7
|
-
exceptions: true,
|
|
8
|
-
calls: true,
|
|
9
|
-
requests: false,
|
|
10
|
-
data: false,
|
|
11
|
-
};
|
|
4
|
+
var aws_classify_common_1 = require("aws-classify-common");
|
|
5
|
+
Object.defineProperty(exports, "EndPointsLogging", { enumerable: true, get: function () { return aws_classify_common_1.EndPointsLogging; } });
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
2
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Object.defineProperty(exports, "EndPointsLogging", { enumerable: true, get: function () { return EndPointsLogging_1.EndPointsLogging; } });
|
|
6
|
-
const reqBody = () => { throw new Error('Request class not registered'); };
|
|
7
|
-
exports.reqBody = reqBody;
|
|
17
|
+
// Preserve the existing package subpath while sharing the common definitions.
|
|
18
|
+
__exportStar(require("aws-classify-common"), exports);
|
package/lib/cjs/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/cjs/dispatch.js
CHANGED
|
@@ -9,39 +9,57 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.webSocketDisconnect = exports.webSocketConnect = exports.responseHandler = exports.classifyServerless = void 0;
|
|
12
|
+
exports.webSocketDisconnect = exports.webSocketConnect = exports.publicResponseHandler = exports.responseHandler = exports.classifyServerless = void 0;
|
|
13
|
+
const AuthenticatedSessions_1 = require("./AuthenticatedSessions");
|
|
13
14
|
const index_1 = require("./index");
|
|
14
15
|
const js_freeze_dry_1 = require("js-freeze-dry");
|
|
15
16
|
const ClassifyServerless_1 = require("./ClassifyServerless");
|
|
16
17
|
exports.classifyServerless = new index_1.ClassifyServerless();
|
|
17
|
-
const
|
|
18
|
+
const httpHandler = (event, context, entry) => __awaiter(void 0, void 0, void 0, function* () {
|
|
18
19
|
try {
|
|
19
20
|
//console.log(`dispatching`);
|
|
20
21
|
return {
|
|
21
22
|
statusCode: 200,
|
|
22
|
-
|
|
23
|
+
headers: { 'Cache-Control': 'no-store' },
|
|
24
|
+
body: (yield exports.classifyServerless.dispatch(event, context, {}, entry)) || "",
|
|
23
25
|
};
|
|
24
26
|
}
|
|
25
27
|
catch (err) {
|
|
26
28
|
//console.log(`request.body = ${event.body}`);
|
|
27
|
-
|
|
29
|
+
// Do not reflect parse, database, or framework errors to an untrusted caller.
|
|
30
|
+
// Operators still receive the original error in Lambda's logs.
|
|
31
|
+
console.error('Dispatch failed', err);
|
|
28
32
|
return {
|
|
29
33
|
statusCode: 200,
|
|
34
|
+
headers: { 'Cache-Control': 'no-store' },
|
|
30
35
|
body: (0, js_freeze_dry_1.serialize)({
|
|
31
36
|
data: undefined,
|
|
32
|
-
|
|
37
|
+
// Keep the existing serialized-error / HTTP-200 contract for clients.
|
|
38
|
+
exception: 'Internal Server Error',
|
|
33
39
|
cargo: undefined,
|
|
34
40
|
sessionId: undefined
|
|
35
41
|
})
|
|
36
42
|
};
|
|
37
43
|
}
|
|
38
44
|
});
|
|
45
|
+
const responseHandler = (event, context) => httpHandler(event, context, 'protected');
|
|
39
46
|
exports.responseHandler = responseHandler;
|
|
47
|
+
const publicResponseHandler = (event, context) => httpHandler(event, context, 'public');
|
|
48
|
+
exports.publicResponseHandler = publicResponseHandler;
|
|
40
49
|
const webSocketConnect = (event, _context) => __awaiter(void 0, void 0, void 0, function* () {
|
|
41
50
|
//console.log('connecting');
|
|
42
|
-
const sessionId = event.headers['Sec-WebSocket-Protocol'] || "";
|
|
51
|
+
const sessionId = event.headers['Sec-WebSocket-Protocol'] || event.headers['sec-websocket-protocol'] || "";
|
|
43
52
|
const connectId = event.requestContext.connectionId;
|
|
44
53
|
//console.log(JSON.stringify(event));
|
|
54
|
+
if (exports.classifyServerless.authenticationEnabled) {
|
|
55
|
+
try {
|
|
56
|
+
yield (0, AuthenticatedSessions_1.attachAuthenticatedSocket)(sessionId, connectId || '');
|
|
57
|
+
return { statusCode: 200, headers: { 'Sec-WebSocket-Protocol': sessionId }, body: '' };
|
|
58
|
+
}
|
|
59
|
+
catch (_a) {
|
|
60
|
+
return { statusCode: 403, body: 'Connection refused' };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
45
63
|
const result = yield (0, ClassifyServerless_1.getSessionData)(sessionId); // Make sure session id passed in is valid
|
|
46
64
|
if (result) {
|
|
47
65
|
// Save connection id
|
|
@@ -58,7 +76,7 @@ const webSocketConnect = (event, _context) => __awaiter(void 0, void 0, void 0,
|
|
|
58
76
|
};
|
|
59
77
|
}
|
|
60
78
|
else {
|
|
61
|
-
console.log(
|
|
79
|
+
console.log('Invalid socket session');
|
|
62
80
|
return {
|
|
63
81
|
statusCode: 500,
|
|
64
82
|
body: 'Opps'
|
|
@@ -66,7 +84,9 @@ const webSocketConnect = (event, _context) => __awaiter(void 0, void 0, void 0,
|
|
|
66
84
|
}
|
|
67
85
|
});
|
|
68
86
|
exports.webSocketConnect = webSocketConnect;
|
|
69
|
-
const webSocketDisconnect = (
|
|
87
|
+
const webSocketDisconnect = (event, _context) => __awaiter(void 0, void 0, void 0, function* () {
|
|
88
|
+
if (exports.classifyServerless.authenticationEnabled && event.requestContext.connectionId)
|
|
89
|
+
yield (0, AuthenticatedSessions_1.detachAuthenticatedSocket)(event.requestContext.connectionId);
|
|
70
90
|
//console.log('Disconnect ' + JSON.stringify(event));
|
|
71
91
|
//console.log(`webSocketDisconnect sessionId=${sessionId} connectId=${connectId}`);
|
|
72
92
|
return {
|
package/lib/cjs/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/cjs/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.responseHandler = exports.webSocketDisconnect = exports.webSocketConnect = exports.classifyServerless = exports.ClassifyResponse = exports.ClassifyServerless = void 0;
|
|
3
|
+
exports.publicResponseHandler = exports.responseHandler = exports.webSocketDisconnect = exports.webSocketConnect = exports.classifyServerless = exports.ClassifyResponse = exports.ClassifyServerless = void 0;
|
|
4
4
|
var ClassifyServerless_1 = require("./ClassifyServerless");
|
|
5
5
|
Object.defineProperty(exports, "ClassifyServerless", { enumerable: true, get: function () { return ClassifyServerless_1.ClassifyServerless; } });
|
|
6
6
|
var ClassifyResponse_1 = require("./ClassifyResponse");
|
|
@@ -10,3 +10,4 @@ Object.defineProperty(exports, "classifyServerless", { enumerable: true, get: fu
|
|
|
10
10
|
Object.defineProperty(exports, "webSocketConnect", { enumerable: true, get: function () { return dispatch_1.webSocketConnect; } });
|
|
11
11
|
Object.defineProperty(exports, "webSocketDisconnect", { enumerable: true, get: function () { return dispatch_1.webSocketDisconnect; } });
|
|
12
12
|
Object.defineProperty(exports, "responseHandler", { enumerable: true, get: function () { return dispatch_1.responseHandler; } });
|
|
13
|
+
Object.defineProperty(exports, "publicResponseHandler", { enumerable: true, get: function () { return dispatch_1.publicResponseHandler; } });
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { RequestContext, SocketAuthorization } from 'aws-classify-common';
|
|
2
|
+
export declare function sessionOwner(context: RequestContext): string;
|
|
3
|
+
export declare function ownedSession(sessionId: string, owner: string, minutes: number): Promise<any>;
|
|
4
|
+
export declare function issueConnectionCredential(session: any, seconds: number, url: string): Promise<SocketAuthorization>;
|
|
5
|
+
export declare function attachAuthenticatedSocket(credential: string, connectionId: string): Promise<void>;
|
|
6
|
+
export declare function detachAuthenticatedSocket(connectionId: string): Promise<void>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
11
|
+
import { sessionStore as db, sessionTable } from './SessionStore';
|
|
12
|
+
const now = () => Math.floor(Date.now() / 1000);
|
|
13
|
+
export function sessionOwner(context) {
|
|
14
|
+
if (context.dispatch === 'public')
|
|
15
|
+
return 'public';
|
|
16
|
+
if (!context.identity) {
|
|
17
|
+
if (context.offline)
|
|
18
|
+
return 'offline-unvalidated';
|
|
19
|
+
throw new Error('Validated identity required');
|
|
20
|
+
}
|
|
21
|
+
return JSON.stringify([context.identity.issuer, context.identity.subject]);
|
|
22
|
+
}
|
|
23
|
+
export function ownedSession(sessionId, owner, minutes) {
|
|
24
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
25
|
+
if (sessionId) {
|
|
26
|
+
const result = yield db.get({ TableName: sessionTable(), Key: { sessionId }, ConsistentRead: true });
|
|
27
|
+
if (!result.Item || !(result.Item.expires > now()) || result.Item.authOwner !== owner)
|
|
28
|
+
throw new Error('Session is not owned by this caller or has expired');
|
|
29
|
+
return result.Item;
|
|
30
|
+
}
|
|
31
|
+
const item = { sessionId: randomUUID(), authOwner: owner, updated: Date.now(), expires: now() + Math.floor(minutes * 60) };
|
|
32
|
+
yield db.put({ TableName: sessionTable(), Item: item, ConditionExpression: 'attribute_not_exists(sessionId)' });
|
|
33
|
+
return item;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export function issueConnectionCredential(session, seconds, url) {
|
|
37
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
38
|
+
if (session.authOwner === 'public')
|
|
39
|
+
throw new Error('Public sessions cannot attach sockets');
|
|
40
|
+
const credential = `ac1.${Buffer.from(session.sessionId).toString('base64url')}.${randomBytes(32).toString('base64url')}`;
|
|
41
|
+
const expiresAt = Math.min(now() + seconds, session.expires);
|
|
42
|
+
yield db.update({ TableName: sessionTable(), Key: { sessionId: session.sessionId },
|
|
43
|
+
UpdateExpression: 'SET credentialHash = :hash, credentialExpires = :expiry',
|
|
44
|
+
ConditionExpression: 'authOwner = :owner AND expires > :now',
|
|
45
|
+
ExpressionAttributeValues: { ':hash': createHash('sha256').update(credential).digest('hex'), ':expiry': expiresAt, ':owner': session.authOwner, ':now': now() } });
|
|
46
|
+
return { url, credential, expiresAt };
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
export function attachAuthenticatedSocket(credential, connectionId) {
|
|
50
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
51
|
+
if (!/^ac1\.[A-Za-z0-9_-]{48}\.[A-Za-z0-9_-]{43}$/.test(credential) || !connectionId)
|
|
52
|
+
throw new Error('Invalid connection credential');
|
|
53
|
+
const sessionId = Buffer.from(credential.split('.')[1], 'base64url').toString();
|
|
54
|
+
if (!/^[a-f0-9-]{36}$/.test(sessionId))
|
|
55
|
+
throw new Error('Invalid connection credential');
|
|
56
|
+
const result = yield db.get({ TableName: sessionTable(), Key: { sessionId }, ConsistentRead: true });
|
|
57
|
+
const session = result.Item;
|
|
58
|
+
if (!(session === null || session === void 0 ? void 0 : session.authOwner) || session.authOwner === 'public')
|
|
59
|
+
throw new Error('Invalid connection credential');
|
|
60
|
+
// Consumption and connection recording succeed together. A replay or concurrent loser fails the condition.
|
|
61
|
+
yield db.transactWrite({ TransactItems: [
|
|
62
|
+
{ Update: { TableName: sessionTable(), Key: { sessionId },
|
|
63
|
+
UpdateExpression: 'SET connectionId = :connection REMOVE credentialHash, credentialExpires',
|
|
64
|
+
ConditionExpression: 'authOwner = :owner AND credentialHash = :hash AND credentialExpires > :now AND expires > :now',
|
|
65
|
+
ExpressionAttributeValues: { ':connection': connectionId, ':owner': session.authOwner, ':hash': createHash('sha256').update(credential).digest('hex'), ':now': now() } } },
|
|
66
|
+
{ Put: { TableName: sessionTable(), Item: { sessionId: `connection#${connectionId}`, targetSessionId: sessionId, expires: session.expires }, ConditionExpression: 'attribute_not_exists(sessionId)' } }
|
|
67
|
+
] });
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
export function detachAuthenticatedSocket(connectionId) {
|
|
71
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
72
|
+
const key = { sessionId: `connection#${connectionId}` };
|
|
73
|
+
const mapping = (yield db.get({ TableName: sessionTable(), Key: key, ConsistentRead: true })).Item;
|
|
74
|
+
if (!mapping)
|
|
75
|
+
return;
|
|
76
|
+
try {
|
|
77
|
+
yield db.update({ TableName: sessionTable(), Key: { sessionId: mapping.targetSessionId }, UpdateExpression: 'REMOVE connectionId', ConditionExpression: 'connectionId = :connection', ExpressionAttributeValues: { ':connection': connectionId } });
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (error.name !== 'ConditionalCheckFailedException')
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
yield db.delete({ TableName: sessionTable(), Key: key });
|
|
84
|
+
});
|
|
85
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { APIGatewayProxyEvent, APIGatewayProxyEventV2 } from 'aws-lambda';
|
|
2
|
+
import type { AuthenticatedIdentity, RequestContext } from 'aws-classify-common';
|
|
3
|
+
export type HttpEvent = APIGatewayProxyEvent | APIGatewayProxyEventV2;
|
|
4
|
+
export interface ServerAuthenticationOptions {
|
|
5
|
+
connectionCredentialSeconds?: number;
|
|
6
|
+
/** Adapt ONLY the trusted requestContext.authorizer emitted by a configured Gateway authorizer. */
|
|
7
|
+
identityAdapter?: (authorizer: unknown) => AuthenticatedIdentity | undefined;
|
|
8
|
+
}
|
|
9
|
+
export declare function jwtIdentity(authorizer: any): AuthenticatedIdentity | undefined;
|
|
10
|
+
export declare function requestContext(event: HttpEvent, mode: RequestContext['dispatch'], options?: ServerAuthenticationOptions): RequestContext;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function jwtIdentity(authorizer) {
|
|
2
|
+
var _a, _b;
|
|
3
|
+
const jwt = authorizer === null || authorizer === void 0 ? void 0 : authorizer.jwt;
|
|
4
|
+
if (!jwt || typeof ((_a = jwt.claims) === null || _a === void 0 ? void 0 : _a.sub) !== 'string' || !jwt.claims.sub || typeof ((_b = jwt.claims) === null || _b === void 0 ? void 0 : _b.iss) !== 'string' || !jwt.claims.iss)
|
|
5
|
+
return undefined;
|
|
6
|
+
return { subject: jwt.claims.sub, issuer: jwt.claims.iss, scopes: jwt.scopes || [], claims: jwt.claims };
|
|
7
|
+
}
|
|
8
|
+
function freeze(value) {
|
|
9
|
+
if (value && typeof value === 'object') {
|
|
10
|
+
for (const child of Object.values(value))
|
|
11
|
+
freeze(child);
|
|
12
|
+
Object.freeze(value);
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
export function requestContext(event, mode, options) {
|
|
17
|
+
var _a;
|
|
18
|
+
let identity;
|
|
19
|
+
const offline = process.env.IS_OFFLINE === 'true';
|
|
20
|
+
if (mode === 'protected') {
|
|
21
|
+
identity = offline ? undefined : ((options === null || options === void 0 ? void 0 : options.identityAdapter) || jwtIdentity)((_a = event.requestContext) === null || _a === void 0 ? void 0 : _a.authorizer);
|
|
22
|
+
if (!identity && !offline)
|
|
23
|
+
throw new Error('Validated identity required');
|
|
24
|
+
if (identity) {
|
|
25
|
+
if (typeof identity.subject !== 'string' || !identity.subject || typeof identity.issuer !== 'string' || !identity.issuer || !identity.claims || typeof identity.claims !== 'object' || Array.isArray(identity.claims) || !Array.isArray(identity.scopes) || !identity.scopes.every(scope => typeof scope === 'string'))
|
|
26
|
+
throw new Error('Invalid authorizer identity');
|
|
27
|
+
// Copy and deeply freeze to avoid changing the Gateway event or trusting mutable grants.
|
|
28
|
+
identity = freeze(JSON.parse(JSON.stringify(identity)));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return Object.freeze({ dispatch: mode, identity, offline, hasScope: (scope) => (identity === null || identity === void 0 ? void 0 : identity.scopes.includes(scope)) || false });
|
|
32
|
+
}
|