@travetto/web-connect 6.0.0-rc.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.
- package/README.md +47 -0
- package/__index__.ts +2 -0
- package/package.json +47 -0
- package/src/connect.ts +155 -0
- package/src/util.ts +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<!-- This file was generated by @travetto/doc and should not be modified directly -->
|
|
2
|
+
<!-- Please modify https://github.com/travetto/travetto/tree/main/module/web-connect/DOC.tsx and execute "npx trv doc" to rebuild -->
|
|
3
|
+
# Web Connect Support
|
|
4
|
+
|
|
5
|
+
## Web integration for Connect-Like Resources
|
|
6
|
+
|
|
7
|
+
**Install: @travetto/web-connect**
|
|
8
|
+
```bash
|
|
9
|
+
npm install @travetto/web-connect
|
|
10
|
+
|
|
11
|
+
# or
|
|
12
|
+
|
|
13
|
+
yarn add @travetto/web-connect
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
This module provides basic integration for calling [connect](https://github.com/senchalabs/connect) related middleware with [Web API](https://github.com/travetto/travetto/tree/main/module/web#readme "Declarative api for Web Applications with support for the dependency injection."). This logic is not intended to be exhaustive, but intended to provide a quick bridge. This only consumer of this is [Web Auth Passport](https://github.com/travetto/travetto/tree/main/module/auth-web-passport#readme "Web authentication integration support for the Travetto framework") as it needs to bind the [WebRequest](https://github.com/travetto/travetto/tree/main/module/web/src/types/request.ts#L11) and [WebResponse](https://github.com/travetto/travetto/tree/main/module/web/src/types/response.ts#L3) to standard contracts for [passport](http://passportjs.org).
|
|
17
|
+
|
|
18
|
+
This module is already most likely compatible with quite a bit of middleware, but will fail under any of the following conditions:
|
|
19
|
+
* The calling code expects the request or response to be a proper [EventEmitter](https://nodejs.org/api/events.html#class-eventemitter)
|
|
20
|
+
* The calling code expects the request/response sockets to be live.
|
|
21
|
+
* The calling code modifies the shape of the objects (e.g. rewrites the close method on response).
|
|
22
|
+
Barring these exceptions, gaps will be filled in as more use cases arise. The above exceptions are non-negotiable as they are are enforced by the invocation method defined by [Web API](https://github.com/travetto/travetto/tree/main/module/web#readme "Declarative api for Web Applications with support for the dependency injection.").
|
|
23
|
+
|
|
24
|
+
**Code: Example of using the Connect Adaptor with Passport**
|
|
25
|
+
```typescript
|
|
26
|
+
async authenticate(input: object, ctx: WebFilterContext): Promise<Principal | undefined> {
|
|
27
|
+
const requestOptions = this.#passportOptions(ctx);
|
|
28
|
+
const options = {
|
|
29
|
+
session: this.session,
|
|
30
|
+
failWithError: true,
|
|
31
|
+
...requestOptions,
|
|
32
|
+
state: PassportUtil.enhanceState(ctx, requestOptions.state)
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const user = await WebConnectUtil.invoke<V>(ctx, (req, res, next) =>
|
|
36
|
+
passport.authenticate(this.#strategyName, options, next)(req, res)
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
if (user) {
|
|
40
|
+
delete user._raw;
|
|
41
|
+
delete user._json;
|
|
42
|
+
delete user.source;
|
|
43
|
+
return this.#toPrincipal(user, this.#strategyName);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
package/__index__.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@travetto/web-connect",
|
|
3
|
+
"version": "6.0.0-rc.2",
|
|
4
|
+
"description": "Web integration for Connect-Like Resources",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"web",
|
|
7
|
+
"connect",
|
|
8
|
+
"travetto",
|
|
9
|
+
"typescript"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://travetto.io",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": {
|
|
14
|
+
"email": "travetto.framework@gmail.com",
|
|
15
|
+
"name": "Travetto Framework"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"__index__.ts",
|
|
19
|
+
"src"
|
|
20
|
+
],
|
|
21
|
+
"main": "__index__.ts",
|
|
22
|
+
"repository": {
|
|
23
|
+
"url": "git+https://github.com/travetto/travetto.git",
|
|
24
|
+
"directory": "module/web-connect"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@travetto/web": "^6.0.0-rc.2"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"@travetto/cli": "^6.0.0-rc.2",
|
|
31
|
+
"@travetto/test": "^6.0.0-rc.2"
|
|
32
|
+
},
|
|
33
|
+
"peerDependenciesMeta": {
|
|
34
|
+
"@travetto/test": {
|
|
35
|
+
"optional": true
|
|
36
|
+
},
|
|
37
|
+
"@travetto/cli": {
|
|
38
|
+
"optional": true
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"travetto": {
|
|
42
|
+
"displayName": "Web Connect Support"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/connect.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { OutgoingHttpHeaders, IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
|
|
3
|
+
import { castTo } from '@travetto/runtime';
|
|
4
|
+
import { WebRequest, WebResponse } from '@travetto/web';
|
|
5
|
+
|
|
6
|
+
export class ConnectRequest implements Pick<IncomingMessage, 'url' | 'headers'> {
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get a connect incoming message given a framework request
|
|
10
|
+
*/
|
|
11
|
+
static get(request: WebRequest): ConnectRequest & IncomingMessage {
|
|
12
|
+
return castTo(new ConnectRequest(request));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
#request: WebRequest;
|
|
16
|
+
constructor(request: WebRequest) {
|
|
17
|
+
this.#request = request;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get path(): string {
|
|
21
|
+
return this.#request.context.path;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get url(): string {
|
|
25
|
+
return this.#request.context.path;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get headers(): Record<string, string> {
|
|
29
|
+
return Object.fromEntries(this.#request.headers.entries());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get query(): Record<string, unknown> {
|
|
33
|
+
return this.#request.context.httpQuery ?? {};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class ConnectResponse implements Pick<ServerResponse,
|
|
38
|
+
'getHeader' | 'getHeaderNames' | 'getHeaders' | 'hasHeader' |
|
|
39
|
+
'headersSent' | 'write' | 'flushHeaders'
|
|
40
|
+
> {
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get a connect server response given a framework response
|
|
44
|
+
*/
|
|
45
|
+
static get(response?: WebResponse): ConnectResponse & ServerResponse {
|
|
46
|
+
return castTo(new ConnectResponse(response));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
#response: WebResponse;
|
|
50
|
+
#headersSent = false;
|
|
51
|
+
#finished = false;
|
|
52
|
+
#written: Buffer[] = [];
|
|
53
|
+
|
|
54
|
+
constructor(response?: WebResponse) {
|
|
55
|
+
this.#response = response ?? new WebResponse();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get headersSent(): boolean {
|
|
59
|
+
return this.#headersSent;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get finished(): boolean {
|
|
63
|
+
return this.#finished;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
get statusCode(): number | undefined {
|
|
67
|
+
return this.#response.context.httpStatusCode;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
set statusCode(val: number) {
|
|
71
|
+
this.#response.context.httpStatusCode = val;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
writeHead(statusCode: unknown, statusMessage?: unknown, headers?: unknown): this {
|
|
75
|
+
this.#response.context.httpStatusCode = castTo(statusCode);
|
|
76
|
+
for (const [k, v] of Object.entries(headers ?? {})) {
|
|
77
|
+
this.#response.headers.set(k, v);
|
|
78
|
+
}
|
|
79
|
+
this.#headersSent = true;
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
setHeader(name: string, value: number | string | readonly string[]): this {
|
|
83
|
+
if (Array.isArray(value)) {
|
|
84
|
+
this.#response.headers.delete(name);
|
|
85
|
+
for (const item of value) {
|
|
86
|
+
this.#response.headers.append(name, item);
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
this.#response.headers.set(name, `${value}`);
|
|
90
|
+
}
|
|
91
|
+
return this;
|
|
92
|
+
}
|
|
93
|
+
appendHeader(name: string, value: string | readonly string[]): this {
|
|
94
|
+
if (Array.isArray(value)) {
|
|
95
|
+
for (const item of value) {
|
|
96
|
+
this.#response.headers.append(name, item);
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
this.#response.headers.append(name, `${value}`);
|
|
100
|
+
}
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
getHeader(name: string): number | string | string[] | undefined {
|
|
104
|
+
return this.#response.headers.get(name)!;
|
|
105
|
+
}
|
|
106
|
+
getHeaders(): OutgoingHttpHeaders {
|
|
107
|
+
return Object.fromEntries(this.#response.headers.entries());
|
|
108
|
+
}
|
|
109
|
+
getHeaderNames(): string[] {
|
|
110
|
+
return [...this.#response.headers.keys()];
|
|
111
|
+
}
|
|
112
|
+
hasHeader(name: string): boolean {
|
|
113
|
+
return this.#response.headers.has(name);
|
|
114
|
+
}
|
|
115
|
+
removeHeader(name: string): void {
|
|
116
|
+
this.#response.headers.delete(name);
|
|
117
|
+
}
|
|
118
|
+
flushHeaders(): void {
|
|
119
|
+
this.#headersSent = true;
|
|
120
|
+
}
|
|
121
|
+
write(chunk: unknown, encoding?: unknown, callback?: (err?: Error) => void): boolean {
|
|
122
|
+
if (this.#headersSent) {
|
|
123
|
+
this.flushHeaders();
|
|
124
|
+
}
|
|
125
|
+
if (!Buffer.isBuffer(chunk)) {
|
|
126
|
+
this.#written.push(Buffer.from(`${chunk}`, castTo(encoding)));
|
|
127
|
+
} else {
|
|
128
|
+
this.#written.push(chunk);
|
|
129
|
+
}
|
|
130
|
+
callback?.();
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
redirect(location: string, code?: number): this {
|
|
134
|
+
this.#response.context.httpStatusCode = code ?? 301;
|
|
135
|
+
this.#response.headers.set('Location', location);
|
|
136
|
+
return this;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
end(chunk?: unknown, encoding?: unknown, cb?: () => void): this {
|
|
140
|
+
this.flushHeaders();
|
|
141
|
+
if (chunk) {
|
|
142
|
+
this.write(chunk, encoding);
|
|
143
|
+
}
|
|
144
|
+
this.#finished = true;
|
|
145
|
+
cb?.();
|
|
146
|
+
return this;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
throwIfSent(): void {
|
|
150
|
+
if (!this.#headersSent) {
|
|
151
|
+
this.#response.body = Buffer.concat(this.#written);
|
|
152
|
+
throw this.#response;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
package/src/util.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
|
|
3
|
+
import { WebFilterContext } from '@travetto/web';
|
|
4
|
+
|
|
5
|
+
import { ConnectRequest, ConnectResponse } from './connect';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
type Middleware = (req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void) => void;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Utilities for invoking express middleware with a WebFilterContext
|
|
12
|
+
*/
|
|
13
|
+
export class WebConnectUtil {
|
|
14
|
+
/**
|
|
15
|
+
* Invoke express middleware, and return the result as a promise.
|
|
16
|
+
*
|
|
17
|
+
* NOTE: If a response is written, then it is thrown as an "error"
|
|
18
|
+
*/
|
|
19
|
+
static async invoke<T>(ctx: WebFilterContext,
|
|
20
|
+
handler: (
|
|
21
|
+
req: IncomingMessage,
|
|
22
|
+
res: ServerResponse,
|
|
23
|
+
next: (err: Error | null | undefined, value: T | undefined | null) => void
|
|
24
|
+
) => Middleware
|
|
25
|
+
): Promise<T | undefined> {
|
|
26
|
+
const connectReq = ConnectRequest.get(ctx.request);
|
|
27
|
+
const connectRes = ConnectResponse.get();
|
|
28
|
+
|
|
29
|
+
const p = Promise.withResolvers<T | undefined>();
|
|
30
|
+
handler(connectReq, connectRes, (err, value) => err ? p.reject(err) : p.resolve(value!));
|
|
31
|
+
const result = await p.promise;
|
|
32
|
+
connectRes.throwIfSent();
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
}
|