aws-classify-server 0.2.0 → 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.
@@ -2,7 +2,6 @@ import type { APIGatewayProxyEvent, APIGatewayProxyEventV2 } from 'aws-lambda';
2
2
  import type { AuthenticatedIdentity, RequestContext } from 'aws-classify-common';
3
3
  export type HttpEvent = APIGatewayProxyEvent | APIGatewayProxyEventV2;
4
4
  export interface ServerAuthenticationOptions {
5
- publicSuffix: string;
6
5
  connectionCredentialSeconds?: number;
7
6
  /** Adapt ONLY the trusted requestContext.authorizer emitted by a configured Gateway authorizer. */
8
7
  identityAdapter?: (authorizer: unknown) => AuthenticatedIdentity | undefined;
@@ -3,5 +3,6 @@ import { ClassifyResponse } from "./ClassifyResponse";
3
3
  export interface ClassDef<ServerClass extends ClassifyResponse, ClientClass> {
4
4
  serverClass: new () => ServerClass;
5
5
  clientClass: new () => ClientClass;
6
+ publicMethods: ReadonlySet<string>;
6
7
  authorizer: ((endPoint: ServerClass, method: string, args: IArguments, context?: RequestContext) => Promise<boolean>) | undefined;
7
8
  }
@@ -19,7 +19,9 @@ export declare class ClassifyServerless {
19
19
  setExpirationMinutes(minutes: number): void;
20
20
  setLogger(log: (_message: string) => void): void;
21
21
  setLogLevel(logLevel: Partial<typeof EndPointsLogging>): void;
22
- registerResponse<T>(responseClass: new () => T, authorizer?: (_endPoint: T, _method: string, _args: IArguments, _context?: RequestContext) => Promise<boolean>): void;
22
+ registerResponse<T>(responseClass: new () => T, authorizer?: (_endPoint: T, _method: string, _args: IArguments, _context?: RequestContext) => Promise<boolean>, options?: {
23
+ publicMethods?: readonly string[];
24
+ }): void;
23
25
  dispatch(ev: HttpEvent, context: Context, classes?: {}, entry?: 'protected' | 'public'): Promise<string>;
24
26
  private dispatchRequest;
25
27
  registerRequest<T>(requestClass: new () => T, classes?: any): new () => T;
@@ -29,11 +29,10 @@ class ClassifyServerless {
29
29
  this.expirationMinutes = 60 * 24;
30
30
  this.log = msg => console.log(msg);
31
31
  if (process.env.AWS_CLASSIFY_AUTHENTICATION === 'true')
32
- this.configureAuthentication({ publicSuffix: process.env.AWS_CLASSIFY_PUBLIC_SUFFIX || '' });
32
+ this.configureAuthentication({});
33
33
  }
34
34
  configureAuthentication(options) {
35
35
  var _a;
36
- (0, aws_classify_common_1.validatePublicSuffix)(options.publicSuffix);
37
36
  const seconds = (_a = options.connectionCredentialSeconds) !== null && _a !== void 0 ? _a : 60;
38
37
  if (!Number.isInteger(seconds) || seconds < 1 || seconds > 300)
39
38
  throw new Error('connectionCredentialSeconds must be 1–300');
@@ -55,7 +54,7 @@ class ClassifyServerless {
55
54
  setLogLevel(logLevel) {
56
55
  this.logLevel = logLevel;
57
56
  }
58
- registerResponse(responseClass, authorizer) {
57
+ registerResponse(responseClass, authorizer, options) {
59
58
  // Find ultimate base class
60
59
  let clientClass = responseClass;
61
60
  while (Object.getPrototypeOf(clientClass).prototype)
@@ -66,7 +65,14 @@ class ClassifyServerless {
66
65
  const interfaceName = clientClass['interfaceName'];
67
66
  if (!interfaceName)
68
67
  throw ('ClassifyServerless.registerResponse: Request must have interfaceName as a static property');
69
- 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) });
70
76
  responseClass.__interfaceName__ = interfaceName;
71
77
  }
72
78
  dispatch(ev, context, classes = {}, entry = 'protected') {
@@ -122,7 +128,7 @@ class ClassifyServerless {
122
128
  throw new Error(`No Response defined for ${request.interfaceName}`);
123
129
  if (request.methodName === 'constructor' || !Object.prototype.hasOwnProperty.call(classDef.clientClass.prototype, request.methodName) || typeof classDef.clientClass.prototype[request.methodName] !== 'function')
124
130
  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'))
131
+ if (this.authentication && classDef.publicMethods.has(request.methodName) !== (trusted.dispatch === 'public'))
126
132
  throw new Error('Member is not permitted on this dispatch endpoint');
127
133
  // If authorizer passed in make sure request is authorized
128
134
  if (!classDef.authorizer || (yield (this.authentication ? classDef.authorizer(request.interfaceName, request.methodName, request.args, trusted) : classDef.authorizer(request.interfaceName, request.methodName, request.args)))) {
@@ -26,13 +26,16 @@ const httpHandler = (event, context, entry) => __awaiter(void 0, void 0, void 0,
26
26
  }
27
27
  catch (err) {
28
28
  //console.log(`request.body = ${event.body}`);
29
- console.log('Dispatch failed');
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);
30
32
  return {
31
33
  statusCode: 200,
32
34
  headers: { 'Cache-Control': 'no-store' },
33
35
  body: (0, js_freeze_dry_1.serialize)({
34
36
  data: undefined,
35
- 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',
36
39
  cargo: undefined,
37
40
  sessionId: undefined
38
41
  })
@@ -2,7 +2,6 @@ import type { APIGatewayProxyEvent, APIGatewayProxyEventV2 } from 'aws-lambda';
2
2
  import type { AuthenticatedIdentity, RequestContext } from 'aws-classify-common';
3
3
  export type HttpEvent = APIGatewayProxyEvent | APIGatewayProxyEventV2;
4
4
  export interface ServerAuthenticationOptions {
5
- publicSuffix: string;
6
5
  connectionCredentialSeconds?: number;
7
6
  /** Adapt ONLY the trusted requestContext.authorizer emitted by a configured Gateway authorizer. */
8
7
  identityAdapter?: (authorizer: unknown) => AuthenticatedIdentity | undefined;
@@ -3,5 +3,6 @@ import { ClassifyResponse } from "./ClassifyResponse";
3
3
  export interface ClassDef<ServerClass extends ClassifyResponse, ClientClass> {
4
4
  serverClass: new () => ServerClass;
5
5
  clientClass: new () => ClientClass;
6
+ publicMethods: ReadonlySet<string>;
6
7
  authorizer: ((endPoint: ServerClass, method: string, args: IArguments, context?: RequestContext) => Promise<boolean>) | undefined;
7
8
  }
@@ -19,7 +19,9 @@ export declare class ClassifyServerless {
19
19
  setExpirationMinutes(minutes: number): void;
20
20
  setLogger(log: (_message: string) => void): void;
21
21
  setLogLevel(logLevel: Partial<typeof EndPointsLogging>): void;
22
- registerResponse<T>(responseClass: new () => T, authorizer?: (_endPoint: T, _method: string, _args: IArguments, _context?: RequestContext) => Promise<boolean>): void;
22
+ registerResponse<T>(responseClass: new () => T, authorizer?: (_endPoint: T, _method: string, _args: IArguments, _context?: RequestContext) => Promise<boolean>, options?: {
23
+ publicMethods?: readonly string[];
24
+ }): void;
23
25
  dispatch(ev: HttpEvent, context: Context, classes?: {}, entry?: 'protected' | 'public'): Promise<string>;
24
26
  private dispatchRequest;
25
27
  registerRequest<T>(requestClass: new () => T, classes?: any): new () => T;
@@ -10,7 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  import { deserialize, serialize } from "js-freeze-dry";
11
11
  import { sessionStore as ddbDocClient } from "./SessionStore";
12
12
  import { AsyncLocalStorage } from "node:async_hooks";
13
- import { validatePublicSuffix } from "aws-classify-common";
13
+ import { isPublicEndpoint } from "aws-classify-common";
14
14
  import { requestContext } from "./Authentication";
15
15
  import { ownedSession, sessionOwner, issueConnectionCredential } from "./AuthenticatedSessions";
16
16
  import { ApiGatewayManagementApiClient, PostToConnectionCommand } from "@aws-sdk/client-apigatewaymanagementapi";
@@ -26,11 +26,10 @@ export class ClassifyServerless {
26
26
  this.expirationMinutes = 60 * 24;
27
27
  this.log = msg => console.log(msg);
28
28
  if (process.env.AWS_CLASSIFY_AUTHENTICATION === 'true')
29
- this.configureAuthentication({ publicSuffix: process.env.AWS_CLASSIFY_PUBLIC_SUFFIX || '' });
29
+ this.configureAuthentication({});
30
30
  }
31
31
  configureAuthentication(options) {
32
32
  var _a;
33
- validatePublicSuffix(options.publicSuffix);
34
33
  const seconds = (_a = options.connectionCredentialSeconds) !== null && _a !== void 0 ? _a : 60;
35
34
  if (!Number.isInteger(seconds) || seconds < 1 || seconds > 300)
36
35
  throw new Error('connectionCredentialSeconds must be 1–300');
@@ -52,7 +51,7 @@ export class ClassifyServerless {
52
51
  setLogLevel(logLevel) {
53
52
  this.logLevel = logLevel;
54
53
  }
55
- registerResponse(responseClass, authorizer) {
54
+ registerResponse(responseClass, authorizer, options) {
56
55
  // Find ultimate base class
57
56
  let clientClass = responseClass;
58
57
  while (Object.getPrototypeOf(clientClass).prototype)
@@ -63,7 +62,14 @@ export class ClassifyServerless {
63
62
  const interfaceName = clientClass['interfaceName'];
64
63
  if (!interfaceName)
65
64
  throw ('ClassifyServerless.registerResponse: Request must have interfaceName as a static property');
66
- this.classDefs.set(interfaceName, { serverClass: responseClass, clientClass, authorizer });
65
+ const methods = Object.getOwnPropertyNames(clientClass.prototype).filter(method => method !== 'constructor' && typeof clientClass.prototype[method] === 'function');
66
+ const decorated = methods.filter(method => isPublicEndpoint(clientClass.prototype[method]));
67
+ if ((options === null || options === void 0 ? void 0 : options.publicMethods) !== undefined && decorated.length)
68
+ throw new Error('Use either @Public() or publicMethods, not both');
69
+ const publicMethods = (options === null || options === void 0 ? void 0 : options.publicMethods) === undefined ? decorated : [...options.publicMethods];
70
+ if (!publicMethods.every(method => typeof method === 'string' && methods.includes(method)) || new Set(publicMethods).size !== publicMethods.length)
71
+ throw new Error('publicMethods must be unique request-class method names');
72
+ this.classDefs.set(interfaceName, { serverClass: responseClass, clientClass, authorizer, publicMethods: new Set(publicMethods) });
67
73
  responseClass.__interfaceName__ = interfaceName;
68
74
  }
69
75
  dispatch(ev, context, classes = {}, entry = 'protected') {
@@ -119,7 +125,7 @@ export class ClassifyServerless {
119
125
  throw new Error(`No Response defined for ${request.interfaceName}`);
120
126
  if (request.methodName === 'constructor' || !Object.prototype.hasOwnProperty.call(classDef.clientClass.prototype, request.methodName) || typeof classDef.clientClass.prototype[request.methodName] !== 'function')
121
127
  throw new Error(`${request.methodName} not found in ${request.interfaceName} client request class`);
122
- if (this.authentication && request.methodName.endsWith(this.authentication.publicSuffix) !== (trusted.dispatch === 'public'))
128
+ if (this.authentication && classDef.publicMethods.has(request.methodName) !== (trusted.dispatch === 'public'))
123
129
  throw new Error('Member is not permitted on this dispatch endpoint');
124
130
  // If authorizer passed in make sure request is authorized
125
131
  if (!classDef.authorizer || (yield (this.authentication ? classDef.authorizer(request.interfaceName, request.methodName, request.args, trusted) : classDef.authorizer(request.interfaceName, request.methodName, request.args)))) {
@@ -23,13 +23,16 @@ const httpHandler = (event, context, entry) => __awaiter(void 0, void 0, void 0,
23
23
  }
24
24
  catch (err) {
25
25
  //console.log(`request.body = ${event.body}`);
26
- console.log('Dispatch failed');
26
+ // Do not reflect parse, database, or framework errors to an untrusted caller.
27
+ // Operators still receive the original error in Lambda's logs.
28
+ console.error('Dispatch failed', err);
27
29
  return {
28
30
  statusCode: 200,
29
31
  headers: { 'Cache-Control': 'no-store' },
30
32
  body: serialize({
31
33
  data: undefined,
32
- exception: `Internal Server Error (${err}) - see log at ${new Date()}`,
34
+ // Keep the existing serialized-error / HTTP-200 contract for clients.
35
+ exception: 'Internal Server Error',
33
36
  cargo: undefined,
34
37
  sessionId: undefined
35
38
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aws-classify-server",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "main": "./lib/cjs/index.js",
5
5
  "module": "./lib/esm/index.js",
6
6
  "types": "./lib/esm/index.d.ts",
@@ -34,7 +34,7 @@
34
34
  "@aws-sdk/client-apigatewaymanagementapi": "^3.1146.0",
35
35
  "@aws-sdk/client-dynamodb": "^3.1146.0",
36
36
  "@aws-sdk/lib-dynamodb": "^3.1146.0",
37
- "aws-classify-common": "0.2.0",
37
+ "aws-classify-common": "0.2.2",
38
38
  "js-freeze-dry": "^1.1.4"
39
39
  },
40
40
  "engines": {
@@ -1,5 +1,4 @@
1
1
  // Serverless v3/v4 file resolver. Loaded only by the authenticated functions include.
2
- const {validatePublicSuffix} = require('aws-classify-common');
3
2
  module.exports = async ({resolveVariable}) => {
4
3
  const config = await resolveVariable('self:custom.awsClassify, null');
5
4
  if (!config || !config.authorizer || typeof config.authorizer.name !== 'string' || !config.authorizer.name.trim())
@@ -10,9 +9,8 @@ module.exports = async ({resolveVariable}) => {
10
9
  const scopes = config.authorizer.scopes;
11
10
  if (scopes !== undefined && (!Array.isArray(scopes) || !scopes.every(scope => typeof scope === 'string' && scope.length && !/\s/.test(scope))))
12
11
  throw new Error('awsClassify authorizer scopes must be an array of nonempty scope names');
13
- const publicSuffix = validatePublicSuffix(config.publicSuffix);
14
12
  const logging = await resolveVariable('self:provider.logs.websocket, null');
15
13
  if (logging && (logging === true || logging.fullExecutionData !== false))
16
14
  throw new Error('Authenticated WebSocket logging requires fullExecutionData: false to protect connection credentials');
17
- return {authorizer: config.authorizer, publicSuffix};
15
+ return {authorizer: config.authorizer};
18
16
  };
@@ -1,7 +1,6 @@
1
1
  responseHandler:
2
2
  environment:
3
3
  AWS_CLASSIFY_AUTHENTICATION: 'true'
4
- AWS_CLASSIFY_PUBLIC_SUFFIX: ${file(${self:custom.yml}/authentication-config.js):publicSuffix}
5
4
  handler: ${self:custom.directories.responseHandlers}.responseHandler
6
5
  events:
7
6
  - httpApi:
@@ -11,14 +10,12 @@ responseHandler:
11
10
  connectHandler:
12
11
  environment:
13
12
  AWS_CLASSIFY_AUTHENTICATION: 'true'
14
- AWS_CLASSIFY_PUBLIC_SUFFIX: ${file(${self:custom.yml}/authentication-config.js):publicSuffix}
15
13
  handler: ${self:custom.directories.responseHandlers}.webSocketConnect
16
14
  events:
17
15
  - websocket: $connect
18
16
  disconnectHandler:
19
17
  environment:
20
18
  AWS_CLASSIFY_AUTHENTICATION: 'true'
21
- AWS_CLASSIFY_PUBLIC_SUFFIX: ${file(${self:custom.yml}/authentication-config.js):publicSuffix}
22
19
  handler: ${self:custom.directories.responseHandlers}.webSocketDisconnect
23
20
  events:
24
21
  - websocket: $disconnect
@@ -26,7 +23,6 @@ publicResponseHandler:
26
23
  name: ${self:service}-${sls:stage}-public
27
24
  environment:
28
25
  AWS_CLASSIFY_AUTHENTICATION: 'true'
29
- AWS_CLASSIFY_PUBLIC_SUFFIX: ${file(${self:custom.yml}/authentication-config.js):publicSuffix}
30
26
  handler: ${self:custom.directories.responseHandlers}.publicResponseHandler
31
27
  events:
32
28
  - httpApi:
@@ -6,4 +6,6 @@ Action:
6
6
  - dynamodb:PutItem
7
7
  - dynamodb:UpdateItem
8
8
  - dynamodb:DeleteItem
9
- Resource: "*"
9
+ Resource:
10
+ - !GetAtt AWSClassifySessionTable.Arn
11
+ - !Join ["", [!GetAtt AWSClassifySessionTable.Arn, "/index/*"]]
@@ -84,9 +84,12 @@ Resources:
84
84
  - CloudFrontDistribution
85
85
  - DomainName
86
86
  Outputs:
87
+ ContentBucket:
88
+ Description: Content bucket
89
+ Value: !Ref S3Bucket
87
90
  CloudFrontId:
88
91
  Description: CloudFront Id
89
92
  Value: !Ref CloudFrontDistribution
90
93
  WebsiteUrl:
91
94
  Description: Cloudfront URL
92
- Value: !GetAtt CloudFrontDistribution.DomainName
95
+ Value: !GetAtt CloudFrontDistribution.DomainName
@@ -61,9 +61,12 @@ Resources:
61
61
  Principal:
62
62
  CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId
63
63
  Outputs:
64
+ ContentBucket:
65
+ Description: Content bucket
66
+ Value: !Ref S3Bucket
64
67
  CloudFrontId:
65
68
  Description: CloudFront Id
66
69
  Value: !Ref CloudFrontDistribution
67
70
  WebsiteUrl:
68
71
  Description: Cloudfront URL
69
- Value: !GetAtt CloudFrontDistribution.DomainName
72
+ Value: !GetAtt CloudFrontDistribution.DomainName
@@ -67,9 +67,12 @@ Resources:
67
67
  Principal:
68
68
  CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId
69
69
  Outputs:
70
+ ContentBucket:
71
+ Description: Content bucket
72
+ Value: !Ref S3Bucket
70
73
  CloudFrontId:
71
74
  Description: CloudFront Id
72
75
  Value: !Ref CloudFrontDistribution
73
76
  WebsiteUrl:
74
77
  Description: Cloudfront URL
75
- Value: !GetAtt CloudFrontDistribution.DomainName
78
+ Value: !GetAtt CloudFrontDistribution.DomainName