@qelos/integrator-nest 4.0.0 → 4.0.1
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 +65 -107
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,22 +1,18 @@
|
|
|
1
1
|
# @qelos/integrator-nest
|
|
2
2
|
|
|
3
|
-
NestJS module
|
|
4
|
-
active workspace before your route handler runs, exposing them on
|
|
5
|
-
`request.qelos.user` / `request.qelos.workspace`.
|
|
3
|
+
NestJS module for [Qelos](https://qelos.io). It wires global middleware so your Nest host acts as a same-origin BFF for a managed Qelos app: requests under `/api/**` are proxied to Qelos (unless you opt out), and every other request resolves the current user up front so controllers and request-scoped providers can use `request.qelos` and the `Qelos*` decorators.
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
shape exposed by `@qelos/integrator-express`, `@qelos/integrator-fastify`,
|
|
9
|
-
`@qelos/integrator-nuxt`, `@qelos/plugin-netlify-api`, etc. It works with
|
|
10
|
-
both Nest's Express adapter and its Fastify adapter.
|
|
5
|
+
Works with Nest’s **Express** and **Fastify** HTTP adapters — request/response objects are handled generically.
|
|
11
6
|
|
|
12
7
|
## Install
|
|
13
8
|
|
|
14
9
|
```sh
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
npm install @nestjs/common @nestjs/core
|
|
10
|
+
pnpm add @qelos/integrator-nest @qelos/sdk
|
|
11
|
+
pnpm add @nestjs/common @nestjs/core
|
|
18
12
|
```
|
|
19
13
|
|
|
14
|
+
Requires **Node 18+**. The user-resolution middleware uses [`Response.headers.getSetCookie()`](https://developer.mozilla.org/docs/Web/API/Headers/getSetCookie) when probing `/api/me` so each upstream `Set-Cookie` can be forwarded individually.
|
|
15
|
+
|
|
20
16
|
## Quick start
|
|
21
17
|
|
|
22
18
|
```ts
|
|
@@ -28,7 +24,7 @@ import { QelosModule } from '@qelos/integrator-nest';
|
|
|
28
24
|
imports: [
|
|
29
25
|
QelosModule.forRoot({
|
|
30
26
|
config: {
|
|
31
|
-
appUrl: process.env.QELOS_APP_URL!, //
|
|
27
|
+
appUrl: process.env.QELOS_APP_URL!, // managed Qelos app origin
|
|
32
28
|
},
|
|
33
29
|
}),
|
|
34
30
|
],
|
|
@@ -36,31 +32,50 @@ import { QelosModule } from '@qelos/integrator-nest';
|
|
|
36
32
|
export class AppModule {}
|
|
37
33
|
```
|
|
38
34
|
|
|
39
|
-
`forRoot` registers
|
|
40
|
-
its scope, import the module without `forRoot` and apply the middleware
|
|
41
|
-
yourself in `configure`:
|
|
35
|
+
`forRoot` registers:
|
|
42
36
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
QelosMiddleware,
|
|
46
|
-
QelosModule,
|
|
47
|
-
type QelosModuleOptions,
|
|
48
|
-
} from '@qelos/integrator-nest';
|
|
49
|
-
import { type MiddlewareConsumer, Module, type NestModule } from '@nestjs/common';
|
|
37
|
+
1. **`QelosMiddleware`** on `*` — resolves `request.qelos` via `/api/me` cookie pass-through (unless the path is skipped).
|
|
38
|
+
2. **`QelosProxyMiddleware`** on `api/*splat` — reverse-proxies `/api/**` to the same resolved origin, unless `disableProxy: true`.
|
|
50
39
|
|
|
51
|
-
|
|
52
|
-
config: { appUrl: process.env.QELOS_APP_URL! },
|
|
53
|
-
};
|
|
40
|
+
User-defined controllers and routes still take precedence for paths they own; the proxy only handles `/api/**` traffic that reaches the middleware stack.
|
|
54
41
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
42
|
+
To narrow where the **resolver** runs, import the module and apply `QelosMiddleware` yourself (see Nest docs on `MiddlewareConsumer`). You still need `QELOS_MODULE_OPTIONS` and `QelosProxyMiddleware` provided if you split registration.
|
|
43
|
+
|
|
44
|
+
## API proxy
|
|
45
|
+
|
|
46
|
+
The proxy forwards the inbound request (method, URL path + query, body stream, and headers except hop-by-hop) to `<proxyTarget>` + the same path. Upstream `Set-Cookie` headers are rewritten so `Domain=` matches the inbound `Host` (port stripped), making Qelos session cookies first-party on your Nest host.
|
|
47
|
+
|
|
48
|
+
### Resolving the proxy target
|
|
49
|
+
|
|
50
|
+
The managed app URL (`config.appUrl`) is the default target. Env vars are dev-time overrides when `appUrl` is not reachable from localhost:
|
|
51
|
+
|
|
52
|
+
1. `QELOS_PROXY_TARGET`
|
|
53
|
+
2. `QELOS_IP`
|
|
54
|
+
3. `QELOS_API_IP`
|
|
55
|
+
4. `config.appUrl`
|
|
56
|
+
|
|
57
|
+
Whitespace-only env values are ignored. If nothing resolves, the proxy responds with **503**. The user resolver uses the same chain; without a target and without `apiToken`, anonymous requests are allowed unless `requireAuth` is set.
|
|
58
|
+
|
|
59
|
+
### Opting out
|
|
60
|
+
|
|
61
|
+
Set `disableProxy: true` in config to skip registering `QelosProxyMiddleware` — for example when you implement every `/api/*` route in Nest or terminate the proxy elsewhere.
|
|
62
|
+
|
|
63
|
+
WebSocket upgrades are **not** proxied.
|
|
64
|
+
|
|
65
|
+
## User-resolution middleware
|
|
66
|
+
|
|
67
|
+
On non-skipped paths, the middleware:
|
|
68
|
+
|
|
69
|
+
1. Builds a per-request SDK (`createRequestSdk`) — with `apiToken`, static auth; otherwise `extraHeaders` forwards the live `Cookie` and `Authorization` headers on every SDK call.
|
|
70
|
+
2. Resolves the proxy target (same priority as above).
|
|
71
|
+
3. If there is no target and no `apiToken`, returns anonymous context (or **401** when `requireAuth` is true).
|
|
72
|
+
4. If there is a target, `GET ${target}/api/me` with cookies and authorization forwarded. Each upstream `Set-Cookie` is appended to the outgoing response with `Domain=` rewritten to the inbound host.
|
|
73
|
+
5. On success, loads workspaces via `sdk.workspaces.getList()` (errors → empty list).
|
|
74
|
+
6. Sets **active workspace** to `resolveWorkspace()` if provided, else `user.workspace` from `/api/me` — **not** `workspaces[0]`.
|
|
75
|
+
|
|
76
|
+
When `disableProxy !== true`, `/api/` is **prepended** to `skipPaths` automatically so proxied `/api/**` traffic is not double-handled by this `/api/me` probe.
|
|
77
|
+
|
|
78
|
+
> Rotated cookies may include `Secure`. Over plain HTTP, browsers may drop them — use HTTPS locally or configure Qelos for non-Secure cookies in dev.
|
|
64
79
|
|
|
65
80
|
## Use in controllers
|
|
66
81
|
|
|
@@ -79,7 +94,6 @@ import type { IWorkspace } from '@qelos/sdk/workspaces';
|
|
|
79
94
|
@Controller()
|
|
80
95
|
export class AppController {
|
|
81
96
|
@Get('me')
|
|
82
|
-
// user/workspace are null when the request is anonymous
|
|
83
97
|
me(
|
|
84
98
|
@QelosUser() user: IUser | null,
|
|
85
99
|
@QelosWorkspace() workspace: IWorkspace | null,
|
|
@@ -87,7 +101,6 @@ export class AppController {
|
|
|
87
101
|
return { user, workspace };
|
|
88
102
|
}
|
|
89
103
|
|
|
90
|
-
// Short-circuit with 401 when there is no authenticated user.
|
|
91
104
|
@Get('private')
|
|
92
105
|
@UseGuards(QelosAuthGuard)
|
|
93
106
|
private(@QelosCtx() ctx: QelosRequestContext) {
|
|
@@ -96,71 +109,22 @@ export class AppController {
|
|
|
96
109
|
}
|
|
97
110
|
```
|
|
98
111
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
1. Reads the access token from `Authorization: Bearer ...` or the
|
|
102
|
-
`q_access_token` cookie, and the refresh token from `q_refresh_token`.
|
|
103
|
-
2. Builds a per-request Qelos SDK instance bound to those tokens.
|
|
104
|
-
3. Calls `sdk.authentication.getLoggedInUser()` and
|
|
105
|
-
`sdk.workspaces.getList()`.
|
|
106
|
-
4. Picks the active workspace (first by default — override with
|
|
107
|
-
`resolveWorkspace`).
|
|
108
|
-
5. Attaches everything to `request.qelos`.
|
|
109
|
-
|
|
110
|
-
The middleware never throws for anonymous requests by default — it just
|
|
111
|
-
leaves `request.qelos.user` and `request.qelos.workspace` as `null`. Pass
|
|
112
|
-
`requireAuth: true` to short-circuit anonymous requests with `401`, or use
|
|
113
|
-
the per-route `QelosAuthGuard` for finer-grained control.
|
|
114
|
-
|
|
115
|
-
## Token refresh
|
|
116
|
-
|
|
117
|
-
When the access token is rejected, the SDK tries to recover, in order:
|
|
118
|
-
|
|
119
|
-
1. The **refresh token** (`q_refresh_token`) via
|
|
120
|
-
`sdk.authentication.refreshToken()` — issues a new access + refresh pair.
|
|
121
|
-
2. The **cookie token** (the access token cookie itself) via
|
|
122
|
-
`sdk.authentication.refreshCookieToken()` — used for cookie-only sessions
|
|
123
|
-
that do not carry a separate refresh token (e.g. social-auth flows).
|
|
112
|
+
`QelosRequestContext`:
|
|
124
113
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
114
|
+
| field | description |
|
|
115
|
+
|--------------|-------------|
|
|
116
|
+
| `user` | `IUser` from `/api/me`, or `null`. |
|
|
117
|
+
| `workspace` | Active workspace or `null`. |
|
|
118
|
+
| `workspaces` | Workspaces from `getList()`. |
|
|
119
|
+
| `sdk` | Request-scoped `QelosSDK` forwarding cookies live. |
|
|
128
120
|
|
|
129
|
-
|
|
130
|
-
push the new tokens into a session store:
|
|
121
|
+
## API token mode
|
|
131
122
|
|
|
132
|
-
|
|
133
|
-
QelosModule.forRoot({
|
|
134
|
-
config: { appUrl: process.env.QELOS_APP_URL! },
|
|
135
|
-
onTokenRefresh: async ({ request, response, newTokens }) => {
|
|
136
|
-
await sessionStore.rotate(request.session.id, newTokens);
|
|
137
|
-
},
|
|
138
|
-
});
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
The hook receives `{ request, response, oldTokens, newTokens, sdk }`. The
|
|
142
|
-
`request` and `response` types are intentionally generic since Nest can run
|
|
143
|
-
on either Express or Fastify.
|
|
144
|
-
|
|
145
|
-
### Manual cookie refresh
|
|
146
|
-
|
|
147
|
-
Long-lived integrator-hosted sessions can also call the SDK directly to
|
|
148
|
-
proactively refresh the cookie token:
|
|
149
|
-
|
|
150
|
-
```ts
|
|
151
|
-
@Get('refresh-session')
|
|
152
|
-
async refresh(@QelosCtx() ctx: QelosRequestContext) {
|
|
153
|
-
const result = await ctx.sdk.authentication.refreshCookieToken();
|
|
154
|
-
// result.headers['set-cookie'] — fresh cookie value to forward
|
|
155
|
-
return { user: result.payload.user };
|
|
156
|
-
}
|
|
157
|
-
```
|
|
123
|
+
For service-to-service traffic, set `apiToken` — the middleware skips the `/api/me` cookie flow for identity (you still get an SDK with the static token). Pair with `requireAuth` / `QelosGuard` as needed.
|
|
158
124
|
|
|
159
125
|
## Async configuration
|
|
160
126
|
|
|
161
127
|
```ts
|
|
162
|
-
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
163
|
-
|
|
164
128
|
QelosModule.forRootAsync({
|
|
165
129
|
imports: [ConfigModule],
|
|
166
130
|
inject: [ConfigService],
|
|
@@ -173,39 +137,33 @@ QelosModule.forRootAsync({
|
|
|
173
137
|
});
|
|
174
138
|
```
|
|
175
139
|
|
|
176
|
-
## Configuration
|
|
140
|
+
## Configuration reference
|
|
177
141
|
|
|
178
142
|
```ts
|
|
179
143
|
QelosModule.forRoot({
|
|
180
144
|
config: {
|
|
181
|
-
appUrl: 'https://
|
|
145
|
+
appUrl: 'https://your-managed-app.com',
|
|
182
146
|
|
|
183
|
-
// Service-to-service: use a static API token instead of cookies/refresh.
|
|
184
147
|
apiToken: process.env.QELOS_API_TOKEN,
|
|
185
148
|
|
|
186
|
-
// Cookie names. Defaults shown.
|
|
187
|
-
accessTokenCookie: 'q_access_token',
|
|
188
|
-
refreshTokenCookie: 'q_refresh_token',
|
|
189
|
-
|
|
190
|
-
// Reject anonymous requests with 401. Defaults to false.
|
|
191
149
|
requireAuth: false,
|
|
192
150
|
|
|
193
|
-
// Skip the middleware entirely for these path prefixes.
|
|
194
151
|
skipPaths: ['/health', '/metrics'],
|
|
195
152
|
|
|
196
|
-
|
|
153
|
+
disableProxy: false,
|
|
154
|
+
|
|
197
155
|
sdkOptions: {},
|
|
198
156
|
},
|
|
199
157
|
|
|
200
|
-
// Override workspace selection. Defaults to `workspaces[0]`.
|
|
201
158
|
resolveWorkspace: ({ request, user, workspaces }) => {
|
|
202
159
|
const headerId = request.headers['x-qelos-workspace'];
|
|
203
|
-
|
|
160
|
+
const raw = Array.isArray(headerId) ? headerId[0] : headerId;
|
|
161
|
+
return workspaces.find((w) => w._id === raw) || user.workspace || null;
|
|
204
162
|
},
|
|
205
163
|
});
|
|
206
164
|
```
|
|
207
165
|
|
|
208
166
|
## Requirements
|
|
209
167
|
|
|
210
|
-
- Node.js >= 18
|
|
211
|
-
- NestJS 9, 10, or 11
|
|
168
|
+
- Node.js >= 18
|
|
169
|
+
- NestJS 9, 10, or 11
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qelos/integrator-nest",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.1",
|
|
4
4
|
"description": "NestJS module that identifies the Qelos user and active workspace before your route handlers run",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
}
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@qelos/sdk": "
|
|
64
|
+
"@qelos/sdk": "4.0.0"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
67
|
"@nestjs/common": "^10.4.5",
|
|
@@ -75,6 +75,6 @@
|
|
|
75
75
|
"type-check": "tsc --noEmit",
|
|
76
76
|
"build": "tsc",
|
|
77
77
|
"pre-build": "tsc",
|
|
78
|
-
"test": "node --import tsx --test test/**/*.test.ts"
|
|
78
|
+
"test": "node --import tsx --test test/**/*.test.ts src/**/*.test.ts"
|
|
79
79
|
}
|
|
80
80
|
}
|