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.
Files changed (46) hide show
  1. package/README.md +17 -336
  2. package/lib/cjs/AuthenticatedSessions.d.ts +6 -0
  3. package/lib/cjs/AuthenticatedSessions.js +93 -0
  4. package/lib/cjs/Authentication.d.ts +10 -0
  5. package/lib/cjs/Authentication.js +37 -0
  6. package/lib/cjs/ClassDef.d.ts +3 -1
  7. package/lib/cjs/ClassifyServerless.d.ts +17 -4
  8. package/lib/cjs/ClassifyServerless.js +90 -30
  9. package/lib/cjs/SessionStore.d.ts +3 -0
  10. package/lib/cjs/SessionStore.js +8 -0
  11. package/lib/cjs/aws-classify-common/EndPointsLogging.d.ts +1 -8
  12. package/lib/cjs/aws-classify-common/EndPointsLogging.js +2 -8
  13. package/lib/cjs/aws-classify-common/LambdaRequest.d.ts +1 -6
  14. package/lib/cjs/aws-classify-common/LambdaResponse.d.ts +1 -6
  15. package/lib/cjs/aws-classify-common/index.d.ts +1 -4
  16. package/lib/cjs/aws-classify-common/index.js +16 -5
  17. package/lib/cjs/dispatch.d.ts +4 -2
  18. package/lib/cjs/dispatch.js +28 -8
  19. package/lib/cjs/index.d.ts +3 -1
  20. package/lib/cjs/index.js +2 -1
  21. package/lib/esm/AuthenticatedSessions.d.ts +6 -0
  22. package/lib/esm/AuthenticatedSessions.js +85 -0
  23. package/lib/esm/Authentication.d.ts +10 -0
  24. package/lib/esm/Authentication.js +32 -0
  25. package/lib/esm/ClassDef.d.ts +3 -1
  26. package/lib/esm/ClassifyServerless.d.ts +17 -4
  27. package/lib/esm/ClassifyServerless.js +84 -24
  28. package/lib/esm/SessionStore.d.ts +3 -0
  29. package/lib/esm/SessionStore.js +4 -0
  30. package/lib/esm/aws-classify-common/EndPointsLogging.d.ts +1 -8
  31. package/lib/esm/aws-classify-common/EndPointsLogging.js +1 -8
  32. package/lib/esm/aws-classify-common/LambdaRequest.d.ts +1 -6
  33. package/lib/esm/aws-classify-common/LambdaResponse.d.ts +1 -6
  34. package/lib/esm/aws-classify-common/index.d.ts +1 -4
  35. package/lib/esm/aws-classify-common/index.js +2 -2
  36. package/lib/esm/dispatch.d.ts +4 -2
  37. package/lib/esm/dispatch.js +26 -7
  38. package/lib/esm/index.d.ts +3 -1
  39. package/lib/esm/index.js +1 -1
  40. package/package.json +20 -13
  41. package/yml/authentication-config.js +16 -0
  42. package/yml/functions-authenticated.yml +30 -0
  43. package/yml/provider-iam.yml +4 -1
  44. package/yml/resources-custom-website.yml +4 -1
  45. package/yml/resources-website-authenticated.yml +72 -0
  46. 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 lib_dynamodb_1 = require("@aws-sdk/lib-dynamodb");
15
- const client_dynamodb_1 = require("@aws-sdk/client-dynamodb");
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
- this.classDefs.set(interfaceName, { serverClass: responseClass, clientClass, authorizer });
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 will be passed in sec-websocket-protocol on connect request
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
- const connectionId = this['__connectionId__'];
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 ddbDocClient.query({
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 ddbDocClient.scan({
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 ddbDocClient.query({
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 ddbDocClient.scan({
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 ddbDocClient.delete({
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 ddbDocClient.get({
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 ddbDocClient.update({
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,3 @@
1
+ import { DynamoDBDocument } from '@aws-sdk/lib-dynamodb';
2
+ export declare const sessionStore: DynamoDBDocument;
3
+ export declare const sessionTable: () => string;
@@ -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,8 +1 @@
1
- export declare const EndPointsLogging: {
2
- create: boolean;
3
- connect: boolean;
4
- exceptions: boolean;
5
- calls: boolean;
6
- requests: boolean;
7
- data: boolean;
8
- };
1
+ export { EndPointsLogging } from "aws-classify-common";
@@ -1,11 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.EndPointsLogging = void 0;
4
- exports.EndPointsLogging = {
5
- create: true,
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,6 +1 @@
1
- export interface LambdaRequest {
2
- interfaceName: string;
3
- methodName: string;
4
- args: any;
5
- sessionId: string;
6
- }
1
+ export type { LambdaRequest } from "aws-classify-common";
@@ -1,6 +1 @@
1
- export interface LambdaResponse {
2
- data: any;
3
- cargo: any;
4
- exception: string | undefined;
5
- sessionId: string;
6
- }
1
+ export type { LambdaResponse } from "aws-classify-common";
@@ -1,4 +1 @@
1
- export type { LambdaRequest } from './LambdaRequest';
2
- export type { LambdaResponse } from './LambdaResponse';
3
- export { EndPointsLogging } from './EndPointsLogging';
4
- export declare const reqBody: () => never;
1
+ export * from "aws-classify-common";
@@ -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
- exports.reqBody = exports.EndPointsLogging = void 0;
4
- var EndPointsLogging_1 = require("./EndPointsLogging");
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);
@@ -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: APIGatewayProxyEvent, context: Context) => Promise<APIGatewayProxyStructuredResultV2>;
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: (_event: APIGatewayProxyEvent, _context: Context) => Promise<APIGatewayProxyStructuredResultV2>;
9
+ export declare const webSocketDisconnect: (event: APIGatewayProxyEvent, _context: Context) => Promise<APIGatewayProxyStructuredResultV2>;
@@ -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 responseHandler = (event, context) => __awaiter(void 0, void 0, void 0, function* () {
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
- body: (yield exports.classifyServerless.dispatch(event, context)) || "",
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(`error = ${err.message} ${err.stack}`);
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
- exception: `Internal Server Error (${err}) - see log at ${new Date()}`,
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(`invalid sessionId - webSocketConnect sessionId=${sessionId} connectId=${connectId}`);
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 = (_event, _context) => __awaiter(void 0, void 0, void 0, function* () {
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 {
@@ -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
+ }