@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.
Files changed (2) hide show
  1. package/README.md +65 -107
  2. package/package.json +3 -3
package/README.md CHANGED
@@ -1,22 +1,18 @@
1
1
  # @qelos/integrator-nest
2
2
 
3
- NestJS module that calls the Qelos SDK to identify the current user and their
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
- This is the NestJS implementation of the Qelos integrator contract the same
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
- npm install @qelos/integrator-nest @qelos/sdk
16
- # Nest is a peer dependency
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!, // e.g. https://yourdomain.com
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 `QelosMiddleware` on every route by default. To restrict
40
- its scope, import the module without `forRoot` and apply the middleware
41
- yourself in `configure`:
35
+ `forRoot` registers:
42
36
 
43
- ```ts
44
- import {
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
- const options: QelosModuleOptions = {
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
- @Module({
56
- imports: [QelosModule.forRoot(options)],
57
- })
58
- export class AppModule implements NestModule {
59
- configure(consumer: MiddlewareConsumer) {
60
- consumer.apply(QelosMiddleware).forRoutes('api/*');
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
- ## What the middleware does
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
- After a successful refresh the middleware fires the `onTokenRefresh` hook.
126
- The default implementation writes the new tokens back to the response cookies
127
- (`HttpOnly`, `SameSite=Lax`, `Secure` whenever `appUrl` is `https://...`).
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
- You can supply your own — for example, to mint your own session cookie or
130
- push the new tokens into a session store:
121
+ ## API token mode
131
122
 
132
- ```ts
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://yourdomain.com', // required
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
- // Anything you want passed through to the per-request SDK.
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
- return workspaces.find((w) => w._id === headerId) || workspaces[0] || null;
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 (uses the global `fetch`).
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.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": "^4.0.0"
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
  }