@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.
Files changed (2) hide show
  1. package/README.md +159 -112
  2. package/package.json +4 -4
package/README.md CHANGED
@@ -1,11 +1,10 @@
1
1
  # @qelos/integrator-express
2
2
 
3
- Express middleware that calls the Qelos SDK to identify the current user and
4
- their active workspace before your route handler runs, exposing them on
5
- `req.qelos.user` / `req.qelos.workspace`.
6
-
7
- This is the Express implementation of the Qelos integrator contract — the same
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 { createQelosMiddleware, requireUser } from '@qelos/integrator-express';
25
+ import { createQelosIntegrator } from '@qelos/integrator-express';
23
26
 
24
27
  const app = express();
25
28
 
26
- app.use(
27
- createQelosMiddleware({
28
- config: {
29
- appUrl: process.env.QELOS_APP_URL!, // e.g. https://yourdomain.com
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
- // Short-circuit with 401 when there is no authenticated user:
43
- app.get(
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
- 1. Reads the access token from `Authorization: Bearer ...` or the
54
- `q_access_token` cookie, and the refresh token from `q_refresh_token`.
55
- 2. Builds a per-request Qelos SDK instance bound to those tokens.
56
- 3. Calls `sdk.authentication.getLoggedInUser()` and
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
- You can supply your own for example, to mint your own session cookie or push
81
- the new tokens into a session store:
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
- ```ts
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
- The hook receives `{ req, res, oldTokens, newTokens, sdk }`. Throwing aborts
95
- the in-flight request.
96
-
97
- ### Manual cookie refresh
98
-
99
- Long-lived integrator-hosted sessions can also call the SDK directly to
100
- proactively refresh the cookie token (e.g. before a navigation that hands the
101
- session over to a downstream service):
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
- const result = await req.qelos!.sdk.authentication.refreshCookieToken();
105
- // result.headers['set-cookie'] — fresh cookie value to forward
106
- // result.payload.user — refreshed user
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
- ## Configuration
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
- createQelosMiddleware({
113
- config: {
114
- appUrl: 'https://yourdomain.com', // required
158
+ import { requireUser } from '@qelos/integrator-express';
115
159
 
116
- // Service-to-service: use a static API token instead of cookies/refresh.
117
- apiToken: process.env.QELOS_API_TOKEN,
160
+ app.get('/private', requireUser((req, res) => res.json(req.qelos.user)));
161
+ ```
118
162
 
119
- // Cookie names. Defaults shown.
120
- accessTokenCookie: 'q_access_token',
121
- refreshTokenCookie: 'q_refresh_token',
163
+ ## Workspace resolution
122
164
 
123
- // Reject anonymous requests with 401. Defaults to false.
124
- requireAuth: false,
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
- // Skip the middleware entirely for these path prefixes.
127
- skipPaths: ['/health', '/metrics'],
173
+ To override the default (e.g. force a particular workspace per request),
174
+ pass a `resolveWorkspace` callback:
128
175
 
129
- // Anything you want passed through to the per-request SDK.
130
- sdkOptions: {},
131
- },
176
+ ```ts
177
+ import { createQelosIntegrator } from '@qelos/integrator-express';
132
178
 
133
- // Override workspace selection. Defaults to `workspaces[0]`.
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 workspaces.find((w) => w._id === headerId) || workspaces[0] || null;
183
+ return (
184
+ workspaces.find((w) => w._id === headerId) ||
185
+ user.workspace ||
186
+ null
187
+ );
137
188
  },
138
189
  });
139
190
  ```
140
191
 
141
- ## TypeScript
192
+ ## API token mode
142
193
 
143
- Importing from `@qelos/integrator-express` augments the Express `Request` type
144
- (via `declare module 'express'` and `declare module 'express-serve-static-core'`)
145
- so `req.qelos` is typed everywhere in your app. The shape is:
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
- interface QelosRequestContext {
149
- user: IUser | null;
150
- workspace: IWorkspace | null;
151
- workspaces: IWorkspace[];
152
- sdk: QelosSDK; // bound to the current request's tokens
153
- tokens: QelosTokenPair; // mutated in place when a refresh occurs
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 the global `fetch`).
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.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": "^4.0.0",
51
- "@qelos/sdk": "^4.0.0"
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
  }