aws-classify-server 0.1.9 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -336
- package/lib/cjs/AuthenticatedSessions.d.ts +6 -0
- package/lib/cjs/AuthenticatedSessions.js +93 -0
- package/lib/cjs/Authentication.d.ts +11 -0
- package/lib/cjs/Authentication.js +37 -0
- package/lib/cjs/ClassDef.d.ts +2 -1
- package/lib/cjs/ClassifyServerless.d.ts +15 -4
- package/lib/cjs/ClassifyServerless.js +82 -28
- package/lib/cjs/SessionStore.d.ts +3 -0
- package/lib/cjs/SessionStore.js +8 -0
- package/lib/cjs/aws-classify-common/EndPointsLogging.d.ts +1 -8
- package/lib/cjs/aws-classify-common/EndPointsLogging.js +2 -8
- package/lib/cjs/aws-classify-common/LambdaRequest.d.ts +1 -6
- package/lib/cjs/aws-classify-common/LambdaResponse.d.ts +1 -6
- package/lib/cjs/aws-classify-common/index.d.ts +1 -4
- package/lib/cjs/aws-classify-common/index.js +16 -5
- package/lib/cjs/dispatch.d.ts +4 -2
- package/lib/cjs/dispatch.js +24 -7
- package/lib/cjs/index.d.ts +3 -1
- package/lib/cjs/index.js +2 -1
- package/lib/esm/AuthenticatedSessions.d.ts +6 -0
- package/lib/esm/AuthenticatedSessions.js +85 -0
- package/lib/esm/Authentication.d.ts +11 -0
- package/lib/esm/Authentication.js +32 -0
- package/lib/esm/ClassDef.d.ts +2 -1
- package/lib/esm/ClassifyServerless.d.ts +15 -4
- package/lib/esm/ClassifyServerless.js +76 -22
- package/lib/esm/SessionStore.d.ts +3 -0
- package/lib/esm/SessionStore.js +4 -0
- package/lib/esm/aws-classify-common/EndPointsLogging.d.ts +1 -8
- package/lib/esm/aws-classify-common/EndPointsLogging.js +1 -8
- package/lib/esm/aws-classify-common/LambdaRequest.d.ts +1 -6
- package/lib/esm/aws-classify-common/LambdaResponse.d.ts +1 -6
- package/lib/esm/aws-classify-common/index.d.ts +1 -4
- package/lib/esm/aws-classify-common/index.js +2 -2
- package/lib/esm/dispatch.d.ts +4 -2
- package/lib/esm/dispatch.js +22 -6
- package/lib/esm/index.d.ts +3 -1
- package/lib/esm/index.js +1 -1
- package/package.json +20 -13
- package/yml/authentication-config.js +18 -0
- package/yml/functions-authenticated.yml +34 -0
- package/yml/provider-iam.yml +1 -0
- package/yml/resources-website-authenticated.yml +69 -0
package/README.md
CHANGED
|
@@ -1,336 +1,17 @@
|
|
|
1
|
-
# aws-classify
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
* Cloudfront with a custom domain name as a CDN
|
|
19
|
-
|
|
20
|
-
All of these resources are configured by the Serverless Framework and deployed by running a script. You only need to login to AWS in order to create credentials for the Serverless Framework and to register your domain name and create an SSL certificate for it.
|
|
21
|
-
|
|
22
|
-
### Use Cases
|
|
23
|
-
aws-classify harnesses AWS to make it easy to leverage a scalable cloud-based infrastructure with virtually no learning curve. From there you can leverage all of the other AWS resources your application may need. The primary use-case is startups and projects that want a complete "app in a box" solution.
|
|
24
|
-
|
|
25
|
-
## Installation
|
|
26
|
-
|
|
27
|
-
On the project that implements the lambda functions
|
|
28
|
-
|
|
29
|
-
```npm install aws-classify-common, aws-classify-server```
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
On the client project
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
```npm install aws-classify-common, aws-classify-client```
|
|
36
|
-
|
|
37
|
-
The framework has been tested with popular client software such as Expo for mobile and React Native for web.
|
|
38
|
-
## Kicking the Tires
|
|
39
|
-
|
|
40
|
-
Because aws-classify involves multiple sub-projects (web, mobile, aws back-end) the best way to get started is to fork or checkout this sample [chat application](https://github.com/selsamman/aws-classify-example-simple-chat) that contains some simple Lambda response classes and a mobile/web front-end.
|
|
41
|
-
|
|
42
|
-
## Calling Lambda Functions from the Client
|
|
43
|
-
|
|
44
|
-
### Create a Request/Response Pair of Classes
|
|
45
|
-
|
|
46
|
-
Create a request class that you will instantiate on the client:
|
|
47
|
-
|
|
48
|
-
```typescript
|
|
49
|
-
export class ServerRequest {
|
|
50
|
-
static interfaceName = 'ServerRequest';
|
|
51
|
-
async setCount (count : number) { reqBody() }
|
|
52
|
-
async getCount () : Promise<number> { return reqBody() }
|
|
53
|
-
}
|
|
54
|
-
```
|
|
55
|
-
Create a corresponding response class that extends the request class:
|
|
56
|
-
```typescript
|
|
57
|
-
export class ServerResponse extends ServerRequest {
|
|
58
|
-
count = 0;
|
|
59
|
-
async setCount(count: number) {this.count = count}
|
|
60
|
-
async getCount(): Promise<number> {return this.count}
|
|
61
|
-
}
|
|
62
|
-
```
|
|
63
|
-
Once these classes are registered and instantiated using aws-classify you simply call the request method and the response method is executed as a Lambda function. Any fields (e.g. count) in the response class are saved and restored between calls and serve as session data. Request classes must have a unique static interface name field so they are uniquely identified.
|
|
64
|
-
|
|
65
|
-
### Using the Request Class
|
|
66
|
-
|
|
67
|
-
You instantiate the request class and invoke its methods like this on the client:
|
|
68
|
-
```typescript
|
|
69
|
-
const classifyClient = new ClassifyClient(getSession, saveSession);
|
|
70
|
-
serverRequest = classifyClient.createRequest(ServerRequest);
|
|
71
|
-
await serverRequest.setCount(2023);
|
|
72
|
-
```
|
|
73
|
-
Since aws-classify is client platform-agnostic you need to implement getSession and saveSession for persisting client session data. In the browser it would look like this:
|
|
74
|
-
```typescript
|
|
75
|
-
async function getSession () {
|
|
76
|
-
return localStorage.getItem('ClassifySession') || "None"
|
|
77
|
-
}
|
|
78
|
-
async function setSession(sessionId : string) {
|
|
79
|
-
localStorage.setItem('ClassifySession', sessionId);
|
|
80
|
-
}
|
|
81
|
-
```
|
|
82
|
-
If the implementation on the response class throws an Error, the Error will be thrown when you call the request class so it is best to enclose the call in a try/catch:
|
|
83
|
-
```typescript
|
|
84
|
-
try {
|
|
85
|
-
await serverRequest.setCount(2023);
|
|
86
|
-
} catch (e) { console.log(e) }
|
|
87
|
-
```
|
|
88
|
-
### Using the Response class
|
|
89
|
-
You must register the class so it can be instantiated when the client calls. Usually this is done in the file where the class is defined:
|
|
90
|
-
```typescript
|
|
91
|
-
classifyServerless.registerRespon(ServerResponse)
|
|
92
|
-
```
|
|
93
|
-
Because class fields are persisted between calls from the same session, you need to declare the response class as serializable so that js-freezedry can serialize it. Usually this is done in the file where the class is defined:
|
|
94
|
-
```
|
|
95
|
-
serializable(ServerResponse)
|
|
96
|
-
```
|
|
97
|
-
## Calling Client Functions from Lambda Functions
|
|
98
|
-
Calling member functions implemented on the client use WebSockets. There are three distinct use cases:
|
|
99
|
-
* ***Same Session*** - Call a client-request for the same session as the caller of your Lambda function.
|
|
100
|
-
* ***Alternate Session*** - Instantiate a response class for a different session and then call a client request for that session. This allows means the response class for the alternate session can change it's session session data
|
|
101
|
-
* ***Direct to Alternate Session*** A shortcut whereby the Lambda fuction directly invokes a request method for an alternate session.
|
|
102
|
-
|
|
103
|
-
### Setup Requests and Response Classes
|
|
104
|
-
Your Lambda functions can also call requests that will be implemented in the browser. The class setup is analogous in that you define a request class
|
|
105
|
-
```typescript
|
|
106
|
-
export class ClientRequest {
|
|
107
|
-
static interfaceName = 'ClientRequest';
|
|
108
|
-
setCount(count : number) {reqBody()}
|
|
109
|
-
}
|
|
110
|
-
```
|
|
111
|
-
and a response class
|
|
112
|
-
```typescript
|
|
113
|
-
export class ClientResponse extends ClientRequest{
|
|
114
|
-
count = 0;
|
|
115
|
-
setCount(count: number) {
|
|
116
|
-
this.count = 0;
|
|
117
|
-
console.log(count);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
```
|
|
121
|
-
On the client you need to instantiate the response class like this:
|
|
122
|
-
```typescript
|
|
123
|
-
const classifyClient = new ClassifyClient(getSession, saveSession);
|
|
124
|
-
const clientResponse = classifyClient.createResponse(ClientResponse);
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
### Implement Lambda Response methods
|
|
128
|
-
In all cases you must register the request class.
|
|
129
|
-
```typescript
|
|
130
|
-
classifyServerless.registerRequest(ClientRequest);
|
|
131
|
-
```
|
|
132
|
-
For the ***Same Session*** case you instantiate the request object and call the method. The first parameter of **createRequest** is a response object from which the session is to be extracted:
|
|
133
|
-
```typescript
|
|
134
|
-
export class ServerResponse extends ServerRequest {
|
|
135
|
-
count = 0;
|
|
136
|
-
// ....
|
|
137
|
-
async sendCount() {
|
|
138
|
-
const clientRequest = classifyServerless.createRequest(this, ClientRequest);
|
|
139
|
-
clientRequest.setCount(this.count);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
```
|
|
143
|
-
For the ***Alternate Session*** case you first instantiate a response object for the target session and call a method that will invoke a request for the target session:
|
|
144
|
-
```typescript
|
|
145
|
-
export class ServerResponse extends ServerRequest {
|
|
146
|
-
count = 0;
|
|
147
|
-
// ...
|
|
148
|
-
async sendCountTo(sessionId: string) {
|
|
149
|
-
await classifyServerless.createResponse(ServerResponse, sessionId, async serverResponse => {
|
|
150
|
-
await serverResponse.sendCount()
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
async sendCount() {
|
|
154
|
-
const clientRequest = classifyServerless.createRequest(this, ClientRequest);
|
|
155
|
-
clientRequest.setCount(this.count);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
And for the ***Direct to Alternate Session*** case you may directly instantiate a request object for the target session and invoke its method
|
|
162
|
-
```typescript
|
|
163
|
-
export class ServerResponse extends ServerRequest {
|
|
164
|
-
count = 0;
|
|
165
|
-
// ...
|
|
166
|
-
async sendOurCountTo(sessionId: string) {
|
|
167
|
-
const clientRequest = await classifyServerless.createRequestForSession(sessionId, ClientRequest);
|
|
168
|
-
await clientRequest.setCount(this.count);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
```
|
|
172
|
-
The combination of these use cases allows for a rich set of interactions between code in the cloud and code on the server. For example:
|
|
173
|
-
* You have two sessions "chat" with other by sharing the session Id
|
|
174
|
-
* You can instantiate and invoke your class methods from any normal Lambda function provided that you have a session. This includes any of the AWS event mechanisms capable of invoking Lamda functions
|
|
175
|
-
## Project Structure
|
|
176
|
-
The mono repo structure is ideal for projects that use aws-classify as you typically will have a server sub-project and one or more client projects, perhaps mobile and web. These projects will share code as the request classes must be present in both the client and the server. The easiest way to accomplish this is using bisync which synchronizes your shared directories automatically.
|
|
177
|
-
|
|
178
|
-
```
|
|
179
|
-
Root
|
|
180
|
-
- bisync.json (configured to synchronized shared directories)
|
|
181
|
-
- package.json (just for bisync)
|
|
182
|
-
- node_modules (just bisync)
|
|
183
|
-
- cloud
|
|
184
|
-
- serverless.yml
|
|
185
|
-
- requests (request classes)
|
|
186
|
-
- responses (contains implementations of requests from client)
|
|
187
|
-
- web (could be a project created with create-react-app)
|
|
188
|
-
- requests (requests)
|
|
189
|
-
- responses (contains implementation of requests from the server)
|
|
190
|
-
- other files pertaining to the client and static website
|
|
191
|
-
```
|
|
192
|
-
You would then configure bisync.json to keep the shared code in sync
|
|
193
|
-
```
|
|
194
|
-
[
|
|
195
|
-
["./cloud/requests", "./web/requests"],
|
|
196
|
-
]
|
|
197
|
-
```
|
|
198
|
-
Implementing shared code this way avoid restrictions that packaging schemes have that effectively require code to be present in project tree
|
|
199
|
-
## Deployment
|
|
200
|
-
You will need a serverless.yml file for automatic deployment in your cloud folder
|
|
201
|
-
In the first two sections you need to setup these custom variables
|
|
202
|
-
* **service:** - A unique name for your project within your AWS account
|
|
203
|
-
* **domain:** - The domain name your will use(even it is not yet registered)
|
|
204
|
-
* **certificateARN:** - An AWS certificate which you can [generate on the AWS
|
|
205
|
-
console](https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-request-public.html)
|
|
206
|
-
* **prefixes** - The sub-domain to be used for the development stage. You may, of course, define other stages (e.g. staging, qa etc)
|
|
207
|
-
* **directories**
|
|
208
|
-
* **responseHandlers:** - A file that has the exports for the handlers aws-classify needs. This file should also register your responses. It looks like this:
|
|
209
|
-
```
|
|
210
|
-
export {responseHandler, webSocketConnect, webSocketDisconnect} from "../aws-classify-server"
|
|
211
|
-
import {classifyServerless} from "aws-classify-server";
|
|
212
|
-
|
|
213
|
-
import {ServerResponse} from "./ServerResponse"; // Your server responses
|
|
214
|
-
classifyServerless.registerResponse(ServerResponse);
|
|
215
|
-
```
|
|
216
|
-
* **staticWebsite:** - The directory where your static website will be built
|
|
217
|
-
* **includeFiles:** - Normally this just points to the yml directory in aws-classify
|
|
218
|
-
```
|
|
219
|
-
service: 'my-fantastic-project'
|
|
220
|
-
|
|
221
|
-
custom:
|
|
222
|
-
|
|
223
|
-
# Specify this even if you don't have a custom domain at this time
|
|
224
|
-
domain: awsclassify.com
|
|
225
|
-
|
|
226
|
-
# Get your AWS SSL Certificate and put it here if you have a custom domain
|
|
227
|
-
certificateARN: "arn:aws:acm:us-east-1:############:certificate/########-####-####-####-############"
|
|
228
|
-
|
|
229
|
-
# Map stages to domain prefixes
|
|
230
|
-
prefixes:
|
|
231
|
-
dev: test.
|
|
232
|
-
prod: ''
|
|
233
|
-
|
|
234
|
-
directories:
|
|
235
|
-
responseHandlers: "src/server-responses/index"
|
|
236
|
-
staticWebsite: "../web/build"
|
|
237
|
-
includeFiles: "node_modules/aws-classify-serverless/yml"
|
|
238
|
-
```
|
|
239
|
-
If you are using a web site that requires building you will want to force it to be built as part of the deployment by adding the script in the **scripts** custom variable:
|
|
240
|
-
```
|
|
241
|
-
scripts:
|
|
242
|
-
hooks:
|
|
243
|
-
'before:deploy:deploy': npm run build:static
|
|
244
|
-
```
|
|
245
|
-
The remaining custom variables just need to be copied as is. They will:
|
|
246
|
-
* set up the detailed names for your domain and buckets
|
|
247
|
-
* configure the s3Sync plugin. Note that noSync: true is specified here so you will have to run ```sls s3Sync``` to push our your site. Doing this way avoids s3Sync deploying your site when you use offline mode
|
|
248
|
-
* Configures other custom variables needed
|
|
249
|
-
```
|
|
250
|
-
yml: ${self:custom.directories.includeFiles}
|
|
251
|
-
stage: ${opt:stage,'dev'}
|
|
252
|
-
hostedZoneName: ${self:custom.domain}.
|
|
253
|
-
domainName: ${self:custom.prefixes.${self:custom.stage}}{$self:custome.domain}
|
|
254
|
-
handler: "src/server-responses/index"
|
|
255
|
-
bucketName: ${self:custom.domainName}
|
|
256
|
-
s3Sync:
|
|
257
|
-
noSync: true
|
|
258
|
-
buckets:
|
|
259
|
-
- bucketName: ${self:custom.bucketName}
|
|
260
|
-
localDir: ${self:custom.directories.includeFiles}
|
|
261
|
-
cloudfrontInvalidate: ${file(${self:custom.yml}/custom-cloudfront.yml)}
|
|
262
|
-
serverless-offline:
|
|
263
|
-
httpPort: 4000
|
|
264
|
-
dynamodb: ${file(${self:custom.yml}/custom-dynamodb.yml)}
|
|
265
|
-
```
|
|
266
|
-
The deployment is dependent on a number plugs which must be installed with npm install
|
|
267
|
-
```
|
|
268
|
-
plugins:
|
|
269
|
-
- serverless-plugin-scripts
|
|
270
|
-
- serverless-s3-sync
|
|
271
|
-
- "@serverless-aws/serverless-s3-remover"
|
|
272
|
-
- serverless-cloudfront-invalidate
|
|
273
|
-
- serverless-plugin-typescript
|
|
274
|
-
- serverless-dynamodb-local
|
|
275
|
-
- serverless-offline
|
|
276
|
-
```
|
|
277
|
-
The provider section details configuration the Serverless Framework handles. Many of the details are in included files to keep this simple and resilient to change
|
|
278
|
-
```
|
|
279
|
-
provider:
|
|
280
|
-
name: aws
|
|
281
|
-
runtime: nodejs16.x
|
|
282
|
-
region: us-east-1
|
|
283
|
-
websocketsApiRouteSelectionExpression: $request.body.action
|
|
284
|
-
logs:
|
|
285
|
-
websocket: true
|
|
286
|
-
environment:
|
|
287
|
-
APIG_ENDPOINT: ${file(${self:custom.yml}/apig.yml)}
|
|
288
|
-
DOMAIN: ${self:custom.domainName}
|
|
289
|
-
iam:
|
|
290
|
-
role:
|
|
291
|
-
statements:
|
|
292
|
-
- ${file(${self:custom.yml}/provider-iam.yml)}
|
|
293
|
-
```
|
|
294
|
-
The functions details each of the Lamda handlers. The details are in an include file
|
|
295
|
-
```
|
|
296
|
-
functions:
|
|
297
|
-
- ${file(${self:custom.yml}/functions.yml)}
|
|
298
|
-
```
|
|
299
|
-
Finally resources contains all of the resource definitions most of which are in include files. Important: If you don't yet have a custom domain use ```{file(${self:custom.yml}/resources-website.yml)}``` instead of ```{file(${self:custom.yml}/resources-custom-website.yml)}```
|
|
300
|
-
```
|
|
301
|
-
resources:
|
|
302
|
-
- ${file(${self:custom.yml}/resources-custom-website.yml)}
|
|
303
|
-
- ${file(${self:custom.yml}/resources-dynamodb.yml)}
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
To deploy you need to:
|
|
307
|
-
- Create AWS credentials using the AWS console as defined in the Serverless [documents](https://www.serverless.com/framework/docs/providers/aws/guide/credentials)
|
|
308
|
-
- Install and setup credentials You need to setup your credentials with serverless
|
|
309
|
-
```
|
|
310
|
-
serverless config credentials \
|
|
311
|
-
--provider aws \
|
|
312
|
-
--key AKIAIOSFODNN7EXAMPLE \
|
|
313
|
-
--secret wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
|
|
314
|
-
```
|
|
315
|
-
- Obtain a no-cost SSL certificate on the AWS console and put it in the serverless.yml file
|
|
316
|
-
- You can deploy a test version with
|
|
317
|
-
```
|
|
318
|
-
sls deploy --verbose --stage dev
|
|
319
|
-
sls s3sync
|
|
320
|
-
```
|
|
321
|
-
- Deploy production version with
|
|
322
|
-
```
|
|
323
|
-
serverless deploy stage prod
|
|
324
|
-
sls s3sync
|
|
325
|
-
```
|
|
326
|
-
## Limitations
|
|
327
|
-
* AWS Websockets are limited to 10 minutes
|
|
328
|
-
* No locking mechanism for simultaneous lambda function execution which could cause sessions to be over-ridden with the last one to complete
|
|
329
|
-
* The session key is not periodically updated as the actual database key is used
|
|
330
|
-
* No mechanism to have session alternatives to DynamoDB (for example, REDIS, JWT)
|
|
331
|
-
## Roadmap
|
|
332
|
-
* Create template/scripts for getting started (currently starting with chat) sample app is the easiest way to use this library.
|
|
333
|
-
* Continued shakeout with real-world applications
|
|
334
|
-
* Community feedback
|
|
335
|
-
|
|
336
|
-
At present the library should rightly be considered "beta" until it is more widely adopted. It is ideal for side projects and proof of concepts. It is also a useful reference on how to configure AWS services.
|
|
1
|
+
# aws-classify-server
|
|
2
|
+
|
|
3
|
+
aws-classify connects browser and React Native applications to AWS Lambda through
|
|
4
|
+
shared TypeScript request classes. This server package registers Lambda response
|
|
5
|
+
implementations, dispatches client calls, persists application sessions and sends
|
|
6
|
+
WebSocket notifications. Optional authentication uses API Gateway's trusted
|
|
7
|
+
authorizer context; application code decides member permissions.
|
|
8
|
+
|
|
9
|
+
It is typically installed alongside `aws-classify-client` in a project containing
|
|
10
|
+
a backend and web/mobile clients, with shared request classes in a private npm
|
|
11
|
+
workspace such as `common/requests`. Its Serverless templates can also be merged
|
|
12
|
+
into an existing service.
|
|
13
|
+
|
|
14
|
+
See the **[aws-classify user guide](https://selsamman.github.io/aws-classify/)** for
|
|
15
|
+
copyable deployment configuration, tutorials and API reference, and the
|
|
16
|
+
[simple chat application](https://github.com/selsamman/aws-classify-example-simple-chat)
|
|
17
|
+
for a complete example.
|
|
@@ -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,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.detachAuthenticatedSocket = exports.attachAuthenticatedSocket = exports.issueConnectionCredential = exports.ownedSession = exports.sessionOwner = void 0;
|
|
13
|
+
const node_crypto_1 = require("node:crypto");
|
|
14
|
+
const SessionStore_1 = require("./SessionStore");
|
|
15
|
+
const now = () => Math.floor(Date.now() / 1000);
|
|
16
|
+
function sessionOwner(context) {
|
|
17
|
+
if (context.dispatch === 'public')
|
|
18
|
+
return 'public';
|
|
19
|
+
if (!context.identity) {
|
|
20
|
+
if (context.offline)
|
|
21
|
+
return 'offline-unvalidated';
|
|
22
|
+
throw new Error('Validated identity required');
|
|
23
|
+
}
|
|
24
|
+
return JSON.stringify([context.identity.issuer, context.identity.subject]);
|
|
25
|
+
}
|
|
26
|
+
exports.sessionOwner = sessionOwner;
|
|
27
|
+
function ownedSession(sessionId, owner, minutes) {
|
|
28
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
29
|
+
if (sessionId) {
|
|
30
|
+
const result = yield SessionStore_1.sessionStore.get({ TableName: (0, SessionStore_1.sessionTable)(), Key: { sessionId }, ConsistentRead: true });
|
|
31
|
+
if (!result.Item || !(result.Item.expires > now()) || result.Item.authOwner !== owner)
|
|
32
|
+
throw new Error('Session is not owned by this caller or has expired');
|
|
33
|
+
return result.Item;
|
|
34
|
+
}
|
|
35
|
+
const item = { sessionId: (0, node_crypto_1.randomUUID)(), authOwner: owner, updated: Date.now(), expires: now() + Math.floor(minutes * 60) };
|
|
36
|
+
yield SessionStore_1.sessionStore.put({ TableName: (0, SessionStore_1.sessionTable)(), Item: item, ConditionExpression: 'attribute_not_exists(sessionId)' });
|
|
37
|
+
return item;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
exports.ownedSession = ownedSession;
|
|
41
|
+
function issueConnectionCredential(session, seconds, url) {
|
|
42
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
43
|
+
if (session.authOwner === 'public')
|
|
44
|
+
throw new Error('Public sessions cannot attach sockets');
|
|
45
|
+
const credential = `ac1.${Buffer.from(session.sessionId).toString('base64url')}.${(0, node_crypto_1.randomBytes)(32).toString('base64url')}`;
|
|
46
|
+
const expiresAt = Math.min(now() + seconds, session.expires);
|
|
47
|
+
yield SessionStore_1.sessionStore.update({ TableName: (0, SessionStore_1.sessionTable)(), Key: { sessionId: session.sessionId },
|
|
48
|
+
UpdateExpression: 'SET credentialHash = :hash, credentialExpires = :expiry',
|
|
49
|
+
ConditionExpression: 'authOwner = :owner AND expires > :now',
|
|
50
|
+
ExpressionAttributeValues: { ':hash': (0, node_crypto_1.createHash)('sha256').update(credential).digest('hex'), ':expiry': expiresAt, ':owner': session.authOwner, ':now': now() } });
|
|
51
|
+
return { url, credential, expiresAt };
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
exports.issueConnectionCredential = issueConnectionCredential;
|
|
55
|
+
function attachAuthenticatedSocket(credential, connectionId) {
|
|
56
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
57
|
+
if (!/^ac1\.[A-Za-z0-9_-]{48}\.[A-Za-z0-9_-]{43}$/.test(credential) || !connectionId)
|
|
58
|
+
throw new Error('Invalid connection credential');
|
|
59
|
+
const sessionId = Buffer.from(credential.split('.')[1], 'base64url').toString();
|
|
60
|
+
if (!/^[a-f0-9-]{36}$/.test(sessionId))
|
|
61
|
+
throw new Error('Invalid connection credential');
|
|
62
|
+
const result = yield SessionStore_1.sessionStore.get({ TableName: (0, SessionStore_1.sessionTable)(), Key: { sessionId }, ConsistentRead: true });
|
|
63
|
+
const session = result.Item;
|
|
64
|
+
if (!(session === null || session === void 0 ? void 0 : session.authOwner) || session.authOwner === 'public')
|
|
65
|
+
throw new Error('Invalid connection credential');
|
|
66
|
+
// Consumption and connection recording succeed together. A replay or concurrent loser fails the condition.
|
|
67
|
+
yield SessionStore_1.sessionStore.transactWrite({ TransactItems: [
|
|
68
|
+
{ Update: { TableName: (0, SessionStore_1.sessionTable)(), Key: { sessionId },
|
|
69
|
+
UpdateExpression: 'SET connectionId = :connection REMOVE credentialHash, credentialExpires',
|
|
70
|
+
ConditionExpression: 'authOwner = :owner AND credentialHash = :hash AND credentialExpires > :now AND expires > :now',
|
|
71
|
+
ExpressionAttributeValues: { ':connection': connectionId, ':owner': session.authOwner, ':hash': (0, node_crypto_1.createHash)('sha256').update(credential).digest('hex'), ':now': now() } } },
|
|
72
|
+
{ Put: { TableName: (0, SessionStore_1.sessionTable)(), Item: { sessionId: `connection#${connectionId}`, targetSessionId: sessionId, expires: session.expires }, ConditionExpression: 'attribute_not_exists(sessionId)' } }
|
|
73
|
+
] });
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
exports.attachAuthenticatedSocket = attachAuthenticatedSocket;
|
|
77
|
+
function detachAuthenticatedSocket(connectionId) {
|
|
78
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
79
|
+
const key = { sessionId: `connection#${connectionId}` };
|
|
80
|
+
const mapping = (yield SessionStore_1.sessionStore.get({ TableName: (0, SessionStore_1.sessionTable)(), Key: key, ConsistentRead: true })).Item;
|
|
81
|
+
if (!mapping)
|
|
82
|
+
return;
|
|
83
|
+
try {
|
|
84
|
+
yield SessionStore_1.sessionStore.update({ TableName: (0, SessionStore_1.sessionTable)(), Key: { sessionId: mapping.targetSessionId }, UpdateExpression: 'REMOVE connectionId', ConditionExpression: 'connectionId = :connection', ExpressionAttributeValues: { ':connection': connectionId } });
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (error.name !== 'ConditionalCheckFailedException')
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
yield SessionStore_1.sessionStore.delete({ TableName: (0, SessionStore_1.sessionTable)(), Key: key });
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
exports.detachAuthenticatedSocket = detachAuthenticatedSocket;
|
|
@@ -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,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.requestContext = exports.jwtIdentity = void 0;
|
|
4
|
+
function jwtIdentity(authorizer) {
|
|
5
|
+
var _a, _b;
|
|
6
|
+
const jwt = authorizer === null || authorizer === void 0 ? void 0 : authorizer.jwt;
|
|
7
|
+
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)
|
|
8
|
+
return undefined;
|
|
9
|
+
return { subject: jwt.claims.sub, issuer: jwt.claims.iss, scopes: jwt.scopes || [], claims: jwt.claims };
|
|
10
|
+
}
|
|
11
|
+
exports.jwtIdentity = jwtIdentity;
|
|
12
|
+
function freeze(value) {
|
|
13
|
+
if (value && typeof value === 'object') {
|
|
14
|
+
for (const child of Object.values(value))
|
|
15
|
+
freeze(child);
|
|
16
|
+
Object.freeze(value);
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function requestContext(event, mode, options) {
|
|
21
|
+
var _a;
|
|
22
|
+
let identity;
|
|
23
|
+
const offline = process.env.IS_OFFLINE === 'true';
|
|
24
|
+
if (mode === 'protected') {
|
|
25
|
+
identity = offline ? undefined : ((options === null || options === void 0 ? void 0 : options.identityAdapter) || jwtIdentity)((_a = event.requestContext) === null || _a === void 0 ? void 0 : _a.authorizer);
|
|
26
|
+
if (!identity && !offline)
|
|
27
|
+
throw new Error('Validated identity required');
|
|
28
|
+
if (identity) {
|
|
29
|
+
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'))
|
|
30
|
+
throw new Error('Invalid authorizer identity');
|
|
31
|
+
// Copy and deeply freeze to avoid changing the Gateway event or trusting mutable grants.
|
|
32
|
+
identity = freeze(JSON.parse(JSON.stringify(identity)));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return Object.freeze({ dispatch: mode, identity, offline, hasScope: (scope) => (identity === null || identity === void 0 ? void 0 : identity.scopes.includes(scope)) || false });
|
|
36
|
+
}
|
|
37
|
+
exports.requestContext = requestContext;
|
package/lib/cjs/ClassDef.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { RequestContext } from "aws-classify-common";
|
|
1
2
|
import { ClassifyResponse } from "./ClassifyResponse";
|
|
2
3
|
export interface ClassDef<ServerClass extends ClassifyResponse, ClientClass> {
|
|
3
4
|
serverClass: new () => ServerClass;
|
|
4
5
|
clientClass: new () => ClientClass;
|
|
5
|
-
authorizer: ((endPoint: ServerClass, method: string, args: IArguments) => Promise<boolean>) | undefined;
|
|
6
|
+
authorizer: ((endPoint: ServerClass, method: string, args: IArguments, context?: RequestContext) => Promise<boolean>) | undefined;
|
|
6
7
|
}
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { ClassDef } from "./ClassDef";
|
|
2
|
-
import {
|
|
2
|
+
import type { RequestContext } from "aws-classify-common";
|
|
3
|
+
import { HttpEvent, ServerAuthenticationOptions } from "./Authentication";
|
|
4
|
+
import { Context } from "aws-lambda";
|
|
3
5
|
export declare class ClassifyServerless {
|
|
6
|
+
private authentication;
|
|
7
|
+
private readonly contexts;
|
|
8
|
+
private readonly activeContexts;
|
|
9
|
+
private readonly responseContexts;
|
|
10
|
+
constructor();
|
|
11
|
+
configureAuthentication(options: ServerAuthenticationOptions): void;
|
|
12
|
+
get authenticationEnabled(): boolean;
|
|
13
|
+
getRequestContext(response?: object): RequestContext | undefined;
|
|
4
14
|
logLevel: Partial<typeof EndPointsLogging>;
|
|
5
15
|
classDefs: Map<any, ClassDef<any, any>>;
|
|
6
16
|
requests: Map<any, any>;
|
|
@@ -9,8 +19,9 @@ export declare class ClassifyServerless {
|
|
|
9
19
|
setExpirationMinutes(minutes: number): void;
|
|
10
20
|
setLogger(log: (_message: string) => void): void;
|
|
11
21
|
setLogLevel(logLevel: Partial<typeof EndPointsLogging>): void;
|
|
12
|
-
registerResponse<T>(responseClass: new () => T, authorizer?: (_endPoint: T, _method: string, _args: IArguments) => Promise<boolean>): void;
|
|
13
|
-
dispatch(ev:
|
|
22
|
+
registerResponse<T>(responseClass: new () => T, authorizer?: (_endPoint: T, _method: string, _args: IArguments, _context?: RequestContext) => Promise<boolean>): void;
|
|
23
|
+
dispatch(ev: HttpEvent, context: Context, classes?: {}, entry?: 'protected' | 'public'): Promise<string>;
|
|
24
|
+
private dispatchRequest;
|
|
14
25
|
registerRequest<T>(requestClass: new () => T, classes?: any): new () => T;
|
|
15
26
|
getSessionId<T>(responseObj: T): any;
|
|
16
27
|
getUserId<T>(responseObj: T): any;
|
|
@@ -24,7 +35,7 @@ export declare class ClassifyServerless {
|
|
|
24
35
|
deleteSessions(): Promise<void>;
|
|
25
36
|
}
|
|
26
37
|
export declare function getSessionData(sessionId: string, interfaceName?: string): Promise<Record<string, any> | undefined>;
|
|
27
|
-
export declare function saveSessionData(sessionId: string, interfaceName?: string, sessionData?: string, connectionId?: string, userId?: string, expirationMinutes?: number): Promise<string>;
|
|
38
|
+
export declare function saveSessionData(sessionId: string, interfaceName?: string, sessionData?: string, connectionId?: string, userId?: string, expirationMinutes?: number, authOwner?: string): Promise<string>;
|
|
28
39
|
export declare const EndPointsLogging: {
|
|
29
40
|
create: boolean;
|
|
30
41
|
connect: boolean;
|