@qelos/integrator-express 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 +159 -112
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
# @qelos/integrator-express
|
|
2
2
|
|
|
3
|
-
Express
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
shape exposed by `@qelos/integrator-nuxt`, `@qelos/plugin-netlify-api`, etc.
|
|
3
|
+
Express integrator for [Qelos](https://qelos.io). It plugs into an Express app
|
|
4
|
+
to make the Node host act as a same-origin BFF for a managed Qelos app:
|
|
5
|
+
requests under `/api/**` are proxied to Qelos, and every other request resolves
|
|
6
|
+
the current user up front so your route handlers can use `req.qelos.user` /
|
|
7
|
+
`req.qelos.workspace` / `req.qelos.workspaces` / `req.qelos.sdk` directly.
|
|
9
8
|
|
|
10
9
|
## Install
|
|
11
10
|
|
|
@@ -15,149 +14,197 @@ npm install @qelos/integrator-express @qelos/sdk
|
|
|
15
14
|
npm install express
|
|
16
15
|
```
|
|
17
16
|
|
|
17
|
+
> Requires Node 18+ — the middleware uses
|
|
18
|
+
> [`Response.headers.getSetCookie()`](https://developer.mozilla.org/docs/Web/API/Headers/getSetCookie)
|
|
19
|
+
> to pipe individual upstream `Set-Cookie` headers back to the client.
|
|
20
|
+
|
|
18
21
|
## Quick start
|
|
19
22
|
|
|
20
23
|
```ts
|
|
21
24
|
import express from 'express';
|
|
22
|
-
import {
|
|
25
|
+
import { createQelosIntegrator } from '@qelos/integrator-express';
|
|
23
26
|
|
|
24
27
|
const app = express();
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
},
|
|
31
|
-
}),
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
app.get('/me', (req, res) => {
|
|
35
|
-
// user/workspace are null when the request is anonymous
|
|
36
|
-
res.json({
|
|
37
|
-
user: req.qelos!.user,
|
|
38
|
-
workspace: req.qelos!.workspace,
|
|
39
|
-
});
|
|
29
|
+
const qelos = createQelosIntegrator({
|
|
30
|
+
config: {
|
|
31
|
+
appUrl: process.env.QELOS_APP_URL!, // e.g. https://yourdomain.com
|
|
32
|
+
},
|
|
40
33
|
});
|
|
41
34
|
|
|
42
|
-
//
|
|
43
|
-
app.
|
|
44
|
-
'/private',
|
|
45
|
-
requireUser((req, res) => {
|
|
46
|
-
res.json(req.qelos!.user);
|
|
47
|
-
}),
|
|
48
|
-
);
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
## What the middleware does
|
|
35
|
+
// User-resolution middleware. Runs on every non-`/api/` request.
|
|
36
|
+
app.use(qelos.middleware);
|
|
52
37
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
`sdk.workspaces.getList()`.
|
|
58
|
-
4. Picks the active workspace (first by default — override with
|
|
59
|
-
`resolveWorkspace`).
|
|
60
|
-
5. Attaches everything to `req.qelos` and calls `next()`.
|
|
61
|
-
|
|
62
|
-
The middleware never throws for anonymous requests by default — it just leaves
|
|
63
|
-
`req.qelos.user` and `req.qelos.workspace` as `null`. Pass `requireAuth: true`
|
|
64
|
-
to short-circuit anonymous requests with `401`.
|
|
65
|
-
|
|
66
|
-
## Token refresh
|
|
67
|
-
|
|
68
|
-
When the access token is rejected, the SDK tries to recover, in order:
|
|
69
|
-
|
|
70
|
-
1. The **refresh token** (`q_refresh_token`) via
|
|
71
|
-
`sdk.authentication.refreshToken()` — issues a new access + refresh pair.
|
|
72
|
-
2. The **cookie token** (the access token cookie itself) via
|
|
73
|
-
`sdk.authentication.refreshCookieToken()` — used for cookie-only sessions
|
|
74
|
-
that do not carry a separate refresh token (e.g. social-auth flows).
|
|
75
|
-
|
|
76
|
-
After a successful refresh the middleware fires the `onTokenRefresh` hook.
|
|
77
|
-
The default implementation writes the new tokens back to the response cookies
|
|
78
|
-
(`HttpOnly`, `SameSite=Lax`, `Secure` whenever `appUrl` is `https://...`).
|
|
38
|
+
// User-defined routes still take precedence — mount them BEFORE the proxy.
|
|
39
|
+
app.get('/me', (req, res) => {
|
|
40
|
+
res.json({ user: req.qelos.user, workspace: req.qelos.workspace });
|
|
41
|
+
});
|
|
79
42
|
|
|
80
|
-
|
|
81
|
-
|
|
43
|
+
// Catch-all reverse proxy for `/api/**`. Mount it AFTER your own routes.
|
|
44
|
+
if (qelos.proxy) {
|
|
45
|
+
app.use('/api', qelos.proxy);
|
|
46
|
+
}
|
|
82
47
|
|
|
83
|
-
|
|
84
|
-
app.use(
|
|
85
|
-
createQelosMiddleware({
|
|
86
|
-
config: { appUrl: process.env.QELOS_APP_URL! },
|
|
87
|
-
onTokenRefresh: async ({ req, res, newTokens, oldTokens }) => {
|
|
88
|
-
await sessionStore.rotate(req.sessionID, newTokens);
|
|
89
|
-
},
|
|
90
|
-
}),
|
|
91
|
-
);
|
|
48
|
+
app.listen(3000);
|
|
92
49
|
```
|
|
93
50
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
51
|
+
If you'd rather wire the middleware and proxy manually, the lower-level
|
|
52
|
+
`createQelosMiddleware` and `createQelosProxy` factories are exported too.
|
|
53
|
+
|
|
54
|
+
## API proxy
|
|
55
|
+
|
|
56
|
+
When the integrator is built with `disableProxy !== true`, `createQelosProxy`
|
|
57
|
+
returns an Express `RequestHandler` that transparently proxies any `/api/**`
|
|
58
|
+
request the consuming app does not handle itself to the configured Qelos
|
|
59
|
+
managed-app origin. This lets the Express app act as a same-origin BFF:
|
|
60
|
+
`@qelos/sdk` calls from the browser, server, or the Qelos web SDK can all hit
|
|
61
|
+
`/api/...` on the Express host and reach Qelos without CORS or cross-site
|
|
62
|
+
cookie pain.
|
|
63
|
+
|
|
64
|
+
User-defined routes still take precedence — mount the proxy after the rest of
|
|
65
|
+
your `/api/*` routes so it only catches requests no other handler matched.
|
|
66
|
+
|
|
67
|
+
### Cookie domain rewrite
|
|
68
|
+
|
|
69
|
+
The proxy forwards the incoming `Cookie` header as-is (the Qelos session
|
|
70
|
+
cookie name is treated as opaque) and forwards upstream `Set-Cookie` headers
|
|
71
|
+
back to the client, rewriting the `Domain=` attribute on every upstream
|
|
72
|
+
cookie to the inbound request's own host. That way the session cookie set by
|
|
73
|
+
Qelos is valid on the Express app's domain regardless of which host Qelos
|
|
74
|
+
issues cookies from.
|
|
75
|
+
|
|
76
|
+
### Resolving the proxy target
|
|
77
|
+
|
|
78
|
+
The managed Qelos app URL (`config.appUrl`) is the proxy target. Env vars are
|
|
79
|
+
only dev-time overrides for when the configured `appUrl` isn't reachable from
|
|
80
|
+
the local host:
|
|
81
|
+
|
|
82
|
+
1. `QELOS_PROXY_TARGET` env var.
|
|
83
|
+
2. `QELOS_IP` env var (dev fallback).
|
|
84
|
+
3. `QELOS_API_IP` env var (dev fallback).
|
|
85
|
+
4. `config.appUrl`.
|
|
86
|
+
|
|
87
|
+
If none of these are set, the proxy handler responds with `503` so
|
|
88
|
+
misconfiguration fails loudly.
|
|
89
|
+
|
|
90
|
+
### Opting out
|
|
91
|
+
|
|
92
|
+
Set `disableProxy: true` on the config to skip registration of the proxy
|
|
93
|
+
handler — useful when the Express app implements every `/api/*` route itself
|
|
94
|
+
or terminates the proxy elsewhere (CDN, reverse proxy, etc.). When the proxy
|
|
95
|
+
is disabled, `qelos.proxy` is `null` and `/api/` is **not** auto-added to
|
|
96
|
+
`skipPaths`, so you remain in control of every request.
|
|
97
|
+
|
|
98
|
+
WebSocket upgrades are not proxied; route them explicitly if needed.
|
|
99
|
+
|
|
100
|
+
## Middleware
|
|
101
|
+
|
|
102
|
+
On every non-`/api/` request, the server middleware identifies the current
|
|
103
|
+
user by calling the managed Qelos app directly:
|
|
104
|
+
|
|
105
|
+
1. Resolve the upstream origin the same way the `/api/**` proxy does
|
|
106
|
+
(`QELOS_PROXY_TARGET` → `QELOS_IP` → `QELOS_API_IP` → `config.appUrl`).
|
|
107
|
+
2. Issue `fetch('${upstream}/api/me')` with the incoming request's `Cookie`
|
|
108
|
+
header forwarded verbatim — the Qelos session cookie name is opaque to
|
|
109
|
+
the integrator, so the whole header is piped through unchanged. Any
|
|
110
|
+
incoming `Authorization` header is forwarded too.
|
|
111
|
+
3. For every `Set-Cookie` header on the upstream response, rewrite the
|
|
112
|
+
`Domain=` attribute to the inbound request's `Host` (port stripped) and
|
|
113
|
+
append it to the outgoing response. This is how session rotations from
|
|
114
|
+
Qelos reach the browser when the managed app and the Express host live on
|
|
115
|
+
different origins.
|
|
116
|
+
4. On `2xx`, parse the JSON body and expose it as `req.qelos.user`. On any
|
|
117
|
+
other status (or a network error), leave `user = null` — and respond with
|
|
118
|
+
`401` if `requireAuth` is set.
|
|
119
|
+
|
|
120
|
+
The middleware is independent from the `/api/**` proxy: the proxy handles
|
|
121
|
+
forwarding of API calls themselves, while the middleware identifies the user
|
|
122
|
+
on every page/non-API request before your route handlers run. To avoid
|
|
123
|
+
double-hitting Qelos for `/api/me` (once for proxying, once for user
|
|
124
|
+
resolution), `createQelosIntegrator` adds `/api/` to `skipPaths` automatically
|
|
125
|
+
when the proxy is enabled.
|
|
126
|
+
|
|
127
|
+
> The middleware does not attempt to strip the `Secure` attribute from
|
|
128
|
+
> rotated cookies. In local dev over plain HTTP, browsers will drop `Secure`
|
|
129
|
+
> cookies — configure the managed Qelos app to issue non-`Secure` cookies in
|
|
130
|
+
> that environment, or run the Express host over HTTPS.
|
|
131
|
+
|
|
132
|
+
## Use in route handlers
|
|
133
|
+
|
|
134
|
+
`req.qelos` is augmented onto the Express `Request` type and gives you a typed
|
|
135
|
+
`QelosRequestContext`:
|
|
136
|
+
|
|
137
|
+
| field | description |
|
|
138
|
+
|--------------|-------------------------------------------------------------------|
|
|
139
|
+
| `user` | The `IUser` body returned by `/api/me`, or `null` when anonymous. |
|
|
140
|
+
| `workspace` | The active `IWorkspace` for the request, or `null`. |
|
|
141
|
+
| `workspaces` | All workspaces the user has access to. |
|
|
142
|
+
| `sdk` | A request-scoped `QelosSDK` instance bound to the live cookies. |
|
|
102
143
|
|
|
103
144
|
```ts
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
145
|
+
app.get('/products', requireUser(async (req, res) => {
|
|
146
|
+
const products = await req.qelos.sdk.entities('products').getList();
|
|
147
|
+
res.json(products);
|
|
148
|
+
}));
|
|
107
149
|
```
|
|
108
150
|
|
|
109
|
-
|
|
151
|
+
The `sdk` reads cookies from the current request on every call, so any
|
|
152
|
+
session rotation piped through by the middleware is picked up automatically
|
|
153
|
+
by subsequent SDK requests in the same handler.
|
|
154
|
+
|
|
155
|
+
Use `requireUser` to short-circuit unauthenticated requests with `401`:
|
|
110
156
|
|
|
111
157
|
```ts
|
|
112
|
-
|
|
113
|
-
config: {
|
|
114
|
-
appUrl: 'https://yourdomain.com', // required
|
|
158
|
+
import { requireUser } from '@qelos/integrator-express';
|
|
115
159
|
|
|
116
|
-
|
|
117
|
-
|
|
160
|
+
app.get('/private', requireUser((req, res) => res.json(req.qelos.user)));
|
|
161
|
+
```
|
|
118
162
|
|
|
119
|
-
|
|
120
|
-
accessTokenCookie: 'q_access_token',
|
|
121
|
-
refreshTokenCookie: 'q_refresh_token',
|
|
163
|
+
## Workspace resolution
|
|
122
164
|
|
|
123
|
-
|
|
124
|
-
|
|
165
|
+
`req.qelos.workspace` defaults to whatever the managed Qelos app reports on
|
|
166
|
+
`user.workspace` from `/api/me`. That field is non-null only when the user
|
|
167
|
+
has already activated a workspace on the Qelos side; when it is `null`, the
|
|
168
|
+
frontend is expected to prompt the user to either activate an existing
|
|
169
|
+
workspace or create a new one. The middleware deliberately does **not**
|
|
170
|
+
auto-pick `workspaces[0]` — that would silently put the user into the wrong
|
|
171
|
+
workspace.
|
|
125
172
|
|
|
126
|
-
|
|
127
|
-
|
|
173
|
+
To override the default (e.g. force a particular workspace per request),
|
|
174
|
+
pass a `resolveWorkspace` callback:
|
|
128
175
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
},
|
|
176
|
+
```ts
|
|
177
|
+
import { createQelosIntegrator } from '@qelos/integrator-express';
|
|
132
178
|
|
|
133
|
-
|
|
179
|
+
const qelos = createQelosIntegrator({
|
|
180
|
+
config: { appUrl: process.env.QELOS_APP_URL! },
|
|
134
181
|
resolveWorkspace: ({ req, user, workspaces }) => {
|
|
135
182
|
const headerId = req.headers['x-qelos-workspace'];
|
|
136
|
-
return
|
|
183
|
+
return (
|
|
184
|
+
workspaces.find((w) => w._id === headerId) ||
|
|
185
|
+
user.workspace ||
|
|
186
|
+
null
|
|
187
|
+
);
|
|
137
188
|
},
|
|
138
189
|
});
|
|
139
190
|
```
|
|
140
191
|
|
|
141
|
-
##
|
|
192
|
+
## API token mode
|
|
142
193
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
194
|
+
For service-to-service deployments where every request shares one Qelos API
|
|
195
|
+
token, set `apiToken` and the middleware will skip the cookie flow entirely
|
|
196
|
+
and build an SDK that authenticates with the static token:
|
|
146
197
|
|
|
147
198
|
```ts
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
199
|
+
createQelosIntegrator({
|
|
200
|
+
config: {
|
|
201
|
+
appUrl: process.env.QELOS_APP_URL!,
|
|
202
|
+
apiToken: process.env.QELOS_API_TOKEN,
|
|
203
|
+
},
|
|
204
|
+
});
|
|
155
205
|
```
|
|
156
206
|
|
|
157
|
-
`req.qelos` is typed as non-optional. If you use `skipPaths`, the property is
|
|
158
|
-
unset for skipped requests — guard with `if (req.qelos) { ... }` in those routes.
|
|
159
|
-
|
|
160
207
|
## Requirements
|
|
161
208
|
|
|
162
|
-
- Node.js >= 18 (uses
|
|
209
|
+
- Node.js >= 18 (uses global `fetch` and `Headers.getSetCookie`).
|
|
163
210
|
- Express 4 or 5.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qelos/integrator-express",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.1",
|
|
4
4
|
"description": "Express middleware 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",
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
}
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@qelos/global-types": "
|
|
51
|
-
"@qelos/sdk": "
|
|
50
|
+
"@qelos/global-types": "4.0.0",
|
|
51
|
+
"@qelos/sdk": "4.0.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@types/express": "^4.17.21",
|
|
@@ -61,6 +61,6 @@
|
|
|
61
61
|
"type-check": "tsc --noEmit",
|
|
62
62
|
"build": "tsc",
|
|
63
63
|
"pre-build": "tsc",
|
|
64
|
-
"test": "node --import tsx --test test/**/*.test.ts"
|
|
64
|
+
"test": "node --import tsx --test src/**/*.test.ts test/**/*.test.ts"
|
|
65
65
|
}
|
|
66
66
|
}
|