aws-classify-server 0.1.9 → 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 -336
- 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-website-authenticated.yml +69 -0
|
@@ -11,19 +11,40 @@ 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({ publicSuffix: process.env.AWS_CLASSIFY_PUBLIC_SUFFIX || '' });
|
|
33
|
+
}
|
|
34
|
+
configureAuthentication(options) {
|
|
35
|
+
var _a;
|
|
36
|
+
(0, aws_classify_common_1.validatePublicSuffix)(options.publicSuffix);
|
|
37
|
+
const seconds = (_a = options.connectionCredentialSeconds) !== null && _a !== void 0 ? _a : 60;
|
|
38
|
+
if (!Number.isInteger(seconds) || seconds < 1 || seconds > 300)
|
|
39
|
+
throw new Error('connectionCredentialSeconds must be 1–300');
|
|
40
|
+
this.authentication = Object.freeze(Object.assign(Object.assign({}, options), { connectionCredentialSeconds: seconds }));
|
|
41
|
+
}
|
|
42
|
+
get authenticationEnabled() { return !!this.authentication; }
|
|
43
|
+
getRequestContext(response) {
|
|
44
|
+
const current = this.contexts.getStore();
|
|
45
|
+
if (!current || !this.activeContexts.has(current))
|
|
46
|
+
return undefined;
|
|
47
|
+
return response ? (this.responseContexts.get(response) === current ? current : undefined) : current;
|
|
27
48
|
}
|
|
28
49
|
setExpirationMinutes(minutes) {
|
|
29
50
|
this.expirationMinutes = minutes;
|
|
@@ -48,14 +69,37 @@ class ClassifyServerless {
|
|
|
48
69
|
this.classDefs.set(interfaceName, { serverClass: responseClass, clientClass, authorizer });
|
|
49
70
|
responseClass.__interfaceName__ = interfaceName;
|
|
50
71
|
}
|
|
51
|
-
dispatch(ev, context, classes = {}) {
|
|
72
|
+
dispatch(ev, context, classes = {}, entry = 'protected') {
|
|
73
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
74
|
+
if (entry === 'public' && !this.authentication)
|
|
75
|
+
throw new Error('Public dispatch requires authentication configuration');
|
|
76
|
+
const trusted = (0, Authentication_1.requestContext)(ev, this.authentication ? entry : 'legacy', this.authentication);
|
|
77
|
+
this.activeContexts.add(trusted);
|
|
78
|
+
try {
|
|
79
|
+
return yield this.contexts.run(trusted, () => this.dispatchRequest(ev, context, classes, trusted));
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
this.activeContexts.delete(trusted);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
dispatchRequest(ev, context, classes, trusted) {
|
|
52
87
|
return __awaiter(this, void 0, void 0, function* () {
|
|
53
88
|
const request = (0, js_freeze_dry_1.deserialize)(ev.body, classes);
|
|
89
|
+
if (!request || typeof request.interfaceName !== 'string' || typeof request.methodName !== 'string' || !Array.isArray(request.args) || typeof request.sessionId !== 'string')
|
|
90
|
+
throw new Error('Malformed dispatch request');
|
|
54
91
|
const logName = `${request.interfaceName}.${request.methodName}`;
|
|
55
92
|
let sessionId = request.sessionId; // May be empty
|
|
56
93
|
// Pseudo interface/method to request socket access and return a session id to use in connect
|
|
57
|
-
// session id
|
|
94
|
+
// Legacy mode passes the session id; authenticated mode issues a separate credential.
|
|
58
95
|
if (logName === '$WebSocket.$authorize') {
|
|
96
|
+
if (this.authentication) {
|
|
97
|
+
if (trusted.dispatch !== 'protected')
|
|
98
|
+
throw new Error('Socket authorization requires protected dispatch');
|
|
99
|
+
const session = yield (0, AuthenticatedSessions_1.ownedSession)(sessionId, (0, AuthenticatedSessions_1.sessionOwner)(trusted), this.expirationMinutes);
|
|
100
|
+
const data = yield (0, AuthenticatedSessions_1.issueConnectionCredential)(session, this.authentication.connectionCredentialSeconds, process.env.IS_OFFLINE ? 'ws://localhost:3001' : `wss://${process.env.APIG_ENDPOINT}`);
|
|
101
|
+
return (0, js_freeze_dry_1.serialize)({ data, sessionId: session.sessionId, exception: undefined, cargo: undefined }, classes);
|
|
102
|
+
}
|
|
59
103
|
const socketResult = yield getSessionData(sessionId, "");
|
|
60
104
|
if (!socketResult) {
|
|
61
105
|
sessionId = context.awsRequestId;
|
|
@@ -76,15 +120,19 @@ class ClassifyServerless {
|
|
|
76
120
|
const classDef = this.classDefs.get(request.interfaceName);
|
|
77
121
|
if (!classDef)
|
|
78
122
|
throw new Error(`No Response defined for ${request.interfaceName}`);
|
|
79
|
-
if (!classDef.clientClass.prototype[request.methodName])
|
|
123
|
+
if (request.methodName === 'constructor' || !Object.prototype.hasOwnProperty.call(classDef.clientClass.prototype, request.methodName) || typeof classDef.clientClass.prototype[request.methodName] !== 'function')
|
|
80
124
|
throw new Error(`${request.methodName} not found in ${request.interfaceName} client request class`);
|
|
125
|
+
if (this.authentication && request.methodName.endsWith(this.authentication.publicSuffix) !== (trusted.dispatch === 'public'))
|
|
126
|
+
throw new Error('Member is not permitted on this dispatch endpoint');
|
|
81
127
|
// If authorizer passed in make sure request is authorized
|
|
82
|
-
if (!classDef.authorizer || (yield classDef.authorizer(request.interfaceName, request.methodName, request.args))) {
|
|
128
|
+
if (!classDef.authorizer || (yield (this.authentication ? classDef.authorizer(request.interfaceName, request.methodName, request.args, trusted) : classDef.authorizer(request.interfaceName, request.methodName, request.args)))) {
|
|
83
129
|
// Instantiate the class
|
|
84
130
|
const obj = new classDef.serverClass();
|
|
85
131
|
// Retrieve session data from DynamoDB base on sessionId in request
|
|
86
132
|
let orignalSessionData = "";
|
|
87
|
-
const result = yield getSessionData(sessionId, request.interfaceName);
|
|
133
|
+
const result = this.authentication ? yield (0, AuthenticatedSessions_1.ownedSession)(sessionId, (0, AuthenticatedSessions_1.sessionOwner)(trusted), this.expirationMinutes) : yield getSessionData(sessionId, request.interfaceName);
|
|
134
|
+
if (this.authentication)
|
|
135
|
+
sessionId = result.sessionId;
|
|
88
136
|
if (!result)
|
|
89
137
|
sessionId = context.awsRequestId;
|
|
90
138
|
else {
|
|
@@ -94,6 +142,7 @@ class ClassifyServerless {
|
|
|
94
142
|
Object.assign(obj, orignalSessionData);
|
|
95
143
|
}
|
|
96
144
|
}
|
|
145
|
+
this.responseContexts.set(obj, trusted);
|
|
97
146
|
obj.__sessionId__ = sessionId;
|
|
98
147
|
obj.__connectionId__ = result === null || result === void 0 ? void 0 : result.connectionId;
|
|
99
148
|
// Call the class method and catch any exceptions it might throw to pass back
|
|
@@ -114,7 +163,7 @@ class ClassifyServerless {
|
|
|
114
163
|
}
|
|
115
164
|
const updatedSessionData = (0, js_freeze_dry_1.serialize)(obj, classes);
|
|
116
165
|
if (!result || updatedSessionData !== orignalSessionData || (Date.now() > (result.updated + updateDebounceInterval)))
|
|
117
|
-
sessionId = yield saveSessionData(sessionId, request.interfaceName, updatedSessionData, undefined, obj.__userId__, this.expirationMinutes);
|
|
166
|
+
sessionId = yield saveSessionData(sessionId, request.interfaceName, updatedSessionData, undefined, obj.__userId__, this.expirationMinutes, this.authentication ? (0, AuthenticatedSessions_1.sessionOwner)(trusted) : undefined);
|
|
118
167
|
// Formulate response
|
|
119
168
|
const lambdaResponse = {
|
|
120
169
|
data: fnResponse,
|
|
@@ -149,7 +198,13 @@ class ClassifyServerless {
|
|
|
149
198
|
try {
|
|
150
199
|
// Build request
|
|
151
200
|
const sessionId = this['__sessionId__'];
|
|
152
|
-
|
|
201
|
+
let connectionId = this['__connectionId__'];
|
|
202
|
+
if (expressServer.authentication && sessionId) {
|
|
203
|
+
const live = yield getSessionData(sessionId);
|
|
204
|
+
if (!(live === null || live === void 0 ? void 0 : live.authOwner) || live.authOwner === 'public' || !(live.expires > Math.floor(Date.now() / 1000)))
|
|
205
|
+
throw new Error('Invalid authenticated notification session');
|
|
206
|
+
connectionId = live.connectionId;
|
|
207
|
+
}
|
|
153
208
|
if (!sessionId)
|
|
154
209
|
throw new Error(`You must instantiate ${interfaceName} using the ClassifyServerless.createRequest`);
|
|
155
210
|
if (!connectionId)
|
|
@@ -216,6 +271,8 @@ class ClassifyServerless {
|
|
|
216
271
|
const result = yield getSessionData(sessionId);
|
|
217
272
|
if (!result)
|
|
218
273
|
throw new Error(`ClassifyResponse.createRequestForSession: invalid session id`);
|
|
274
|
+
if (this.authentication && (!result.authOwner || result.authOwner === 'public' || !(result.expires > Math.floor(Date.now() / 1000))))
|
|
275
|
+
throw new Error('Invalid authenticated notification session');
|
|
219
276
|
if (!result.connectionId)
|
|
220
277
|
throw new Error(`WebSocket handshake not established for session`);
|
|
221
278
|
// Create request
|
|
@@ -233,6 +290,8 @@ class ClassifyServerless {
|
|
|
233
290
|
const result = yield getSessionData(sessionId, interfaceName);
|
|
234
291
|
if (!result)
|
|
235
292
|
throw new Error(`ClassifyResponse.createResponse: invalid session id for interface ${interfaceName}`);
|
|
293
|
+
if (this.authentication && (!result.authOwner || result.authOwner === 'public' || !(result.expires > Math.floor(Date.now() / 1000))))
|
|
294
|
+
throw new Error('Invalid authenticated notification session');
|
|
236
295
|
const sessionData = result[`interface_${interfaceName}`];
|
|
237
296
|
// Instantiate the class
|
|
238
297
|
const obj = (new responseClass());
|
|
@@ -249,7 +308,7 @@ class ClassifyServerless {
|
|
|
249
308
|
if (updatedSessionData !== sessionData || (Date.now() > (result.updated + updateDebounceInterval))) {
|
|
250
309
|
if (this.logLevel.calls)
|
|
251
310
|
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);
|
|
311
|
+
yield saveSessionData(sessionId, interfaceName, updatedSessionData, undefined, undefined, this.expirationMinutes, this.authentication ? result.authOwner : undefined);
|
|
253
312
|
}
|
|
254
313
|
return ret;
|
|
255
314
|
});
|
|
@@ -257,7 +316,7 @@ class ClassifyServerless {
|
|
|
257
316
|
getSessionsForUserId(userId) {
|
|
258
317
|
return __awaiter(this, void 0, void 0, function* () {
|
|
259
318
|
if (userId) {
|
|
260
|
-
const data = yield
|
|
319
|
+
const data = yield SessionStore_1.sessionStore.query({
|
|
261
320
|
TableName: `classifySessionStore.${process.env.DOMAIN}`,
|
|
262
321
|
KeyConditionExpression: 'userId = :userId',
|
|
263
322
|
IndexName: 'userId',
|
|
@@ -266,24 +325,24 @@ class ClassifyServerless {
|
|
|
266
325
|
},
|
|
267
326
|
ProjectionExpression: `sessionId`
|
|
268
327
|
});
|
|
269
|
-
return data.Items ? data.Items.map(i => i.sessionId) : [];
|
|
328
|
+
return data.Items ? data.Items.filter(i => !this.authentication || !i.sessionId.startsWith('connection#')).map(i => i.sessionId) : [];
|
|
270
329
|
}
|
|
271
330
|
return [];
|
|
272
331
|
});
|
|
273
332
|
}
|
|
274
333
|
getSessions() {
|
|
275
334
|
return __awaiter(this, void 0, void 0, function* () {
|
|
276
|
-
const data = yield
|
|
335
|
+
const data = yield SessionStore_1.sessionStore.scan({
|
|
277
336
|
TableName: `classifySessionStore.${process.env.DOMAIN}`,
|
|
278
337
|
ProjectionExpression: `sessionId`
|
|
279
338
|
});
|
|
280
|
-
return data.Items ? data.Items.map(i => i.sessionId) : [];
|
|
339
|
+
return data.Items ? data.Items.filter(i => !this.authentication || !i.sessionId.startsWith('connection#')).map(i => i.sessionId) : [];
|
|
281
340
|
});
|
|
282
341
|
}
|
|
283
342
|
deleteSessionsForUserId(userId) {
|
|
284
343
|
return __awaiter(this, void 0, void 0, function* () {
|
|
285
344
|
if (userId) {
|
|
286
|
-
const data = yield
|
|
345
|
+
const data = yield SessionStore_1.sessionStore.query({
|
|
287
346
|
TableName: `classifySessionStore.${process.env.DOMAIN}`,
|
|
288
347
|
KeyConditionExpression: 'userId = :userId',
|
|
289
348
|
IndexName: 'userId',
|
|
@@ -299,7 +358,7 @@ class ClassifyServerless {
|
|
|
299
358
|
}
|
|
300
359
|
deleteSessions() {
|
|
301
360
|
return __awaiter(this, void 0, void 0, function* () {
|
|
302
|
-
const data = yield
|
|
361
|
+
const data = yield SessionStore_1.sessionStore.scan({
|
|
303
362
|
TableName: `classifySessionStore.${process.env.DOMAIN}`,
|
|
304
363
|
ProjectionExpression: `sessionId`
|
|
305
364
|
});
|
|
@@ -312,7 +371,7 @@ exports.ClassifyServerless = ClassifyServerless;
|
|
|
312
371
|
function deleteSessionByKeys(keys) {
|
|
313
372
|
return __awaiter(this, void 0, void 0, function* () {
|
|
314
373
|
for (let ix = 0; ix < keys.length; ++ix) {
|
|
315
|
-
yield
|
|
374
|
+
yield SessionStore_1.sessionStore.delete({
|
|
316
375
|
TableName: `classifySessionStore.${process.env.DOMAIN}`,
|
|
317
376
|
Key: { sessionId: keys[ix] }
|
|
318
377
|
});
|
|
@@ -322,11 +381,11 @@ function deleteSessionByKeys(keys) {
|
|
|
322
381
|
function getSessionData(sessionId, interfaceName = "") {
|
|
323
382
|
return __awaiter(this, void 0, void 0, function* () {
|
|
324
383
|
if (sessionId) {
|
|
325
|
-
const data = yield
|
|
384
|
+
const data = yield SessionStore_1.sessionStore.get({
|
|
326
385
|
TableName: `classifySessionStore.${process.env.DOMAIN}`,
|
|
327
386
|
Key: { sessionId },
|
|
328
387
|
ConsistentRead: true,
|
|
329
|
-
ProjectionExpression: `interface_${interfaceName}, connectionId, userId, updated, expires`
|
|
388
|
+
ProjectionExpression: `interface_${interfaceName}, connectionId, userId, updated, expires, authOwner`
|
|
330
389
|
});
|
|
331
390
|
return data.Item;
|
|
332
391
|
}
|
|
@@ -334,7 +393,7 @@ function getSessionData(sessionId, interfaceName = "") {
|
|
|
334
393
|
});
|
|
335
394
|
}
|
|
336
395
|
exports.getSessionData = getSessionData;
|
|
337
|
-
function saveSessionData(sessionId, interfaceName, sessionData, connectionId, userId, expirationMinutes) {
|
|
396
|
+
function saveSessionData(sessionId, interfaceName, sessionData, connectionId, userId, expirationMinutes, authOwner) {
|
|
338
397
|
return __awaiter(this, void 0, void 0, function* () {
|
|
339
398
|
const updateExpressionComponents = ['updated = :time'];
|
|
340
399
|
const expressionAttributeValues = { ':time': new Date().getTime() };
|
|
@@ -357,12 +416,7 @@ function saveSessionData(sessionId, interfaceName, sessionData, connectionId, us
|
|
|
357
416
|
expressionAttributeValues[':userId'] = userId;
|
|
358
417
|
}
|
|
359
418
|
// 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
|
-
});
|
|
419
|
+
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
420
|
return sessionId;
|
|
367
421
|
});
|
|
368
422
|
}
|
|
@@ -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,24 +9,27 @@ 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
|
-
console.log(
|
|
29
|
+
console.log('Dispatch failed');
|
|
28
30
|
return {
|
|
29
31
|
statusCode: 200,
|
|
32
|
+
headers: { 'Cache-Control': 'no-store' },
|
|
30
33
|
body: (0, js_freeze_dry_1.serialize)({
|
|
31
34
|
data: undefined,
|
|
32
35
|
exception: `Internal Server Error (${err}) - see log at ${new Date()}`,
|
|
@@ -36,12 +39,24 @@ const responseHandler = (event, context) => __awaiter(void 0, void 0, void 0, fu
|
|
|
36
39
|
};
|
|
37
40
|
}
|
|
38
41
|
});
|
|
42
|
+
const responseHandler = (event, context) => httpHandler(event, context, 'protected');
|
|
39
43
|
exports.responseHandler = responseHandler;
|
|
44
|
+
const publicResponseHandler = (event, context) => httpHandler(event, context, 'public');
|
|
45
|
+
exports.publicResponseHandler = publicResponseHandler;
|
|
40
46
|
const webSocketConnect = (event, _context) => __awaiter(void 0, void 0, void 0, function* () {
|
|
41
47
|
//console.log('connecting');
|
|
42
|
-
const sessionId = event.headers['Sec-WebSocket-Protocol'] || "";
|
|
48
|
+
const sessionId = event.headers['Sec-WebSocket-Protocol'] || event.headers['sec-websocket-protocol'] || "";
|
|
43
49
|
const connectId = event.requestContext.connectionId;
|
|
44
50
|
//console.log(JSON.stringify(event));
|
|
51
|
+
if (exports.classifyServerless.authenticationEnabled) {
|
|
52
|
+
try {
|
|
53
|
+
yield (0, AuthenticatedSessions_1.attachAuthenticatedSocket)(sessionId, connectId || '');
|
|
54
|
+
return { statusCode: 200, headers: { 'Sec-WebSocket-Protocol': sessionId }, body: '' };
|
|
55
|
+
}
|
|
56
|
+
catch (_a) {
|
|
57
|
+
return { statusCode: 403, body: 'Connection refused' };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
45
60
|
const result = yield (0, ClassifyServerless_1.getSessionData)(sessionId); // Make sure session id passed in is valid
|
|
46
61
|
if (result) {
|
|
47
62
|
// Save connection id
|
|
@@ -58,7 +73,7 @@ const webSocketConnect = (event, _context) => __awaiter(void 0, void 0, void 0,
|
|
|
58
73
|
};
|
|
59
74
|
}
|
|
60
75
|
else {
|
|
61
|
-
console.log(
|
|
76
|
+
console.log('Invalid socket session');
|
|
62
77
|
return {
|
|
63
78
|
statusCode: 500,
|
|
64
79
|
body: 'Opps'
|
|
@@ -66,7 +81,9 @@ const webSocketConnect = (event, _context) => __awaiter(void 0, void 0, void 0,
|
|
|
66
81
|
}
|
|
67
82
|
});
|
|
68
83
|
exports.webSocketConnect = webSocketConnect;
|
|
69
|
-
const webSocketDisconnect = (
|
|
84
|
+
const webSocketDisconnect = (event, _context) => __awaiter(void 0, void 0, void 0, function* () {
|
|
85
|
+
if (exports.classifyServerless.authenticationEnabled && event.requestContext.connectionId)
|
|
86
|
+
yield (0, AuthenticatedSessions_1.detachAuthenticatedSocket)(event.requestContext.connectionId);
|
|
70
87
|
//console.log('Disconnect ' + JSON.stringify(event));
|
|
71
88
|
//console.log(`webSocketDisconnect sessionId=${sessionId} connectId=${connectId}`);
|
|
72
89
|
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,11 @@
|
|
|
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
|
+
publicSuffix: string;
|
|
6
|
+
connectionCredentialSeconds?: number;
|
|
7
|
+
/** Adapt ONLY the trusted requestContext.authorizer emitted by a configured Gateway authorizer. */
|
|
8
|
+
identityAdapter?: (authorizer: unknown) => AuthenticatedIdentity | undefined;
|
|
9
|
+
}
|
|
10
|
+
export declare function jwtIdentity(authorizer: any): AuthenticatedIdentity | undefined;
|
|
11
|
+
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
|
+
}
|
package/lib/esm/ClassDef.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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
|
-
authorizer: ((endPoint: ServerClass, method: string, args: IArguments) => Promise<boolean>) | undefined;
|
|
6
|
+
authorizer: ((endPoint: ServerClass, method: string, args: IArguments, context?: RequestContext) => Promise<boolean>) | undefined;
|
|
6
7
|
}
|