aws-classify-server 0.1.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +17 -331
  2. package/lib/cjs/AuthenticatedSessions.d.ts +6 -0
  3. package/lib/cjs/AuthenticatedSessions.js +93 -0
  4. package/lib/cjs/Authentication.d.ts +11 -0
  5. package/lib/cjs/Authentication.js +37 -0
  6. package/lib/cjs/ClassDef.d.ts +2 -1
  7. package/lib/cjs/ClassifyServerless.d.ts +15 -4
  8. package/lib/cjs/ClassifyServerless.js +82 -28
  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 +24 -7
  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 +11 -0
  24. package/lib/esm/Authentication.js +32 -0
  25. package/lib/esm/ClassDef.d.ts +2 -1
  26. package/lib/esm/ClassifyServerless.d.ts +15 -4
  27. package/lib/esm/ClassifyServerless.js +76 -22
  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 +22 -6
  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 +18 -0
  42. package/yml/functions-authenticated.yml +34 -0
  43. package/yml/provider-iam.yml +1 -0
  44. package/yml/resources-custom-website.yml +5 -0
  45. package/yml/resources-website-authenticated.yml +69 -0
  46. package/yml/resources-website.yml +5 -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 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({ 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 will be passed in sec-websocket-protocol on connect request
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
- const connectionId = this['__connectionId__'];
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 ddbDocClient.query({
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 ddbDocClient.scan({
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 ddbDocClient.query({
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 ddbDocClient.scan({
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 ddbDocClient.delete({
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 ddbDocClient.get({
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 ddbDocClient.update({
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,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,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 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
+ 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(`invalid sessionId - webSocketConnect sessionId=${sessionId} connectId=${connectId}`);
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 = (_event, _context) => __awaiter(void 0, void 0, void 0, function* () {
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 {
@@ -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
+ }
@@ -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
  }