@qelos/integrator-express 4.0.0 → 4.1.0
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/dist/cookies.d.ts +19 -0
- package/dist/cookies.js +43 -0
- package/dist/cookies.js.map +1 -0
- package/dist/index.d.ts +29 -2
- package/dist/index.js +34 -5
- package/dist/index.js.map +1 -1
- package/dist/middleware.d.ts +4 -8
- package/dist/middleware.js +64 -101
- package/dist/middleware.js.map +1 -1
- package/dist/proxy-target.d.ts +17 -0
- package/dist/proxy-target.js +29 -0
- package/dist/proxy-target.js.map +1 -0
- package/dist/proxy.d.ts +16 -0
- package/dist/proxy.js +132 -0
- package/dist/proxy.js.map +1 -0
- package/dist/sdk-factory.d.ts +3 -11
- package/dist/sdk-factory.js +13 -77
- package/dist/sdk-factory.js.map +1 -1
- package/dist/social-auth.d.ts +6 -5
- package/dist/social-auth.js +13 -6
- package/dist/social-auth.js.map +1 -1
- package/dist/types.d.ts +45 -11
- 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.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rewrite the `Domain=` attribute of a single `Set-Cookie` header value.
|
|
3
|
+
*
|
|
4
|
+
* - If the cookie contains a `Domain=<value>` attribute (case-insensitive), its
|
|
5
|
+
* value is replaced with `newDomain`, with any trailing `:port` suffix
|
|
6
|
+
* stripped first (cookie `Domain` attributes do not include a port).
|
|
7
|
+
* - If `newDomain` is `undefined` (or empty), the entire `; Domain=<value>`
|
|
8
|
+
* segment is removed so the browser scopes the cookie to the current host.
|
|
9
|
+
* - If no `Domain=` attribute is present, the input is returned unchanged.
|
|
10
|
+
*
|
|
11
|
+
* All other attributes, their casing/order and the original cookie name/value
|
|
12
|
+
* bytes are preserved.
|
|
13
|
+
*/
|
|
14
|
+
export declare function rewriteSetCookieDomain(setCookie: string, newDomain: string | undefined): string;
|
|
15
|
+
/**
|
|
16
|
+
* Apply {@link rewriteSetCookieDomain} to each entry of a `Set-Cookie` array
|
|
17
|
+
* (e.g. the result of `Headers.getSetCookie()`).
|
|
18
|
+
*/
|
|
19
|
+
export declare function rewriteSetCookieDomains(values: string[], newDomain: string | undefined): string[];
|
package/dist/cookies.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.rewriteSetCookieDomain = rewriteSetCookieDomain;
|
|
4
|
+
exports.rewriteSetCookieDomains = rewriteSetCookieDomains;
|
|
5
|
+
/**
|
|
6
|
+
* Rewrite the `Domain=` attribute of a single `Set-Cookie` header value.
|
|
7
|
+
*
|
|
8
|
+
* - If the cookie contains a `Domain=<value>` attribute (case-insensitive), its
|
|
9
|
+
* value is replaced with `newDomain`, with any trailing `:port` suffix
|
|
10
|
+
* stripped first (cookie `Domain` attributes do not include a port).
|
|
11
|
+
* - If `newDomain` is `undefined` (or empty), the entire `; Domain=<value>`
|
|
12
|
+
* segment is removed so the browser scopes the cookie to the current host.
|
|
13
|
+
* - If no `Domain=` attribute is present, the input is returned unchanged.
|
|
14
|
+
*
|
|
15
|
+
* All other attributes, their casing/order and the original cookie name/value
|
|
16
|
+
* bytes are preserved.
|
|
17
|
+
*/
|
|
18
|
+
function rewriteSetCookieDomain(setCookie, newDomain) {
|
|
19
|
+
const domainSegment = /;\s*Domain\s*=\s*[^;]+/i;
|
|
20
|
+
if (!domainSegment.test(setCookie))
|
|
21
|
+
return setCookie;
|
|
22
|
+
const stripped = newDomain ? stripPort(newDomain) : '';
|
|
23
|
+
if (!stripped) {
|
|
24
|
+
return setCookie.replace(domainSegment, '');
|
|
25
|
+
}
|
|
26
|
+
return setCookie.replace(/(;\s*Domain\s*=\s*)[^;]+/i, `$1${stripped}`);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Apply {@link rewriteSetCookieDomain} to each entry of a `Set-Cookie` array
|
|
30
|
+
* (e.g. the result of `Headers.getSetCookie()`).
|
|
31
|
+
*/
|
|
32
|
+
function rewriteSetCookieDomains(values, newDomain) {
|
|
33
|
+
return values.map((value) => rewriteSetCookieDomain(value, newDomain));
|
|
34
|
+
}
|
|
35
|
+
function stripPort(host) {
|
|
36
|
+
if (host.startsWith('[')) {
|
|
37
|
+
const end = host.indexOf(']');
|
|
38
|
+
return end === -1 ? host : host.slice(1, end);
|
|
39
|
+
}
|
|
40
|
+
const colon = host.indexOf(':');
|
|
41
|
+
return colon === -1 ? host : host.slice(0, colon);
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=cookies.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cookies.js","sourceRoot":"","sources":["../src/cookies.ts"],"names":[],"mappings":";;AAaA,wDAeC;AAMD,0DAKC;AAvCD;;;;;;;;;;;;GAYG;AACH,SAAgB,sBAAsB,CACpC,SAAiB,EACjB,SAA6B;IAE7B,MAAM,aAAa,GAAG,yBAAyB,CAAC;IAChD,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,SAAS,CAAC;QAAE,OAAO,SAAS,CAAC;IAErD,MAAM,QAAQ,GAAG,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACvD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,SAAS,CAAC,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC;IAC9C,CAAC;IACD,OAAO,SAAS,CAAC,OAAO,CACtB,2BAA2B,EAC3B,KAAK,QAAQ,EAAE,CAChB,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,SAAgB,uBAAuB,CACrC,MAAgB,EAChB,SAA6B;IAE7B,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,sBAAsB,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC;AACzE,CAAC;AAED,SAAS,SAAS,CAAC,IAAY;IAC7B,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACzB,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC9B,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAChD,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAChC,OAAO,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;AACpD,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
|
+
import type { RequestHandler } from 'express';
|
|
2
|
+
import { type CreateMiddlewareOptions } from './middleware';
|
|
1
3
|
export { createQelosMiddleware, requireUser, } from './middleware';
|
|
2
4
|
export type { CreateMiddlewareOptions, QelosMiddleware, } from './middleware';
|
|
5
|
+
export { createQelosProxy } from './proxy';
|
|
6
|
+
export type { CreateProxyOptions, QelosProxyHandler } from './proxy';
|
|
3
7
|
export { createRequestSdk } from './sdk-factory';
|
|
4
8
|
export type { CreateSdkParams } from './sdk-factory';
|
|
5
|
-
export
|
|
6
|
-
export {
|
|
9
|
+
export { rewriteSetCookieDomain, rewriteSetCookieDomains } from './cookies';
|
|
10
|
+
export { resolveQelosProxyTarget } from './proxy-target';
|
|
11
|
+
export type { QelosExpressConfig, QelosRequestContext, } from './types';
|
|
12
|
+
export { completeSocialAuthCallback, getSocialAuthSetCookieParts, parseSocialCallbackRefreshToken, type SocialAuthCallbackPayload, type SocialCallbackInput, } from './social-auth';
|
|
13
|
+
export interface QelosIntegrator {
|
|
14
|
+
/**
|
|
15
|
+
* User-resolution middleware. Forwards inbound cookies to `/api/me` and
|
|
16
|
+
* exposes `req.qelos.user` / `req.qelos.workspace` / `req.qelos.workspaces`
|
|
17
|
+
* / `req.qelos.sdk`.
|
|
18
|
+
*/
|
|
19
|
+
middleware: RequestHandler;
|
|
20
|
+
/**
|
|
21
|
+
* Catch-all reverse-proxy handler for `/api/**`. Mount it AFTER your own
|
|
22
|
+
* `/api/*` routes so user-defined endpoints still take precedence. Returns
|
|
23
|
+
* `null` when `config.disableProxy === true`.
|
|
24
|
+
*/
|
|
25
|
+
proxy: RequestHandler | null;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Build both the user-resolution middleware and the `/api/**` reverse proxy
|
|
29
|
+
* from a single config. When the proxy is enabled (the default), `/api/` is
|
|
30
|
+
* auto-prepended to `skipPaths` so the middleware doesn't shadow the proxy
|
|
31
|
+
* by running its own `/api/me` probe on every proxied request.
|
|
32
|
+
*/
|
|
33
|
+
export declare function createQelosIntegrator(options: CreateMiddlewareOptions): QelosIntegrator;
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,43 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.parseSocialCallbackRefreshToken = exports.getSocialAuthSetCookieParts = exports.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
exports.parseSocialCallbackRefreshToken = exports.getSocialAuthSetCookieParts = exports.completeSocialAuthCallback = exports.resolveQelosProxyTarget = exports.rewriteSetCookieDomains = exports.rewriteSetCookieDomain = exports.createRequestSdk = exports.createQelosProxy = exports.requireUser = exports.createQelosMiddleware = void 0;
|
|
4
|
+
exports.createQelosIntegrator = createQelosIntegrator;
|
|
5
|
+
const middleware_1 = require("./middleware");
|
|
6
|
+
const proxy_1 = require("./proxy");
|
|
7
|
+
var middleware_2 = require("./middleware");
|
|
8
|
+
Object.defineProperty(exports, "createQelosMiddleware", { enumerable: true, get: function () { return middleware_2.createQelosMiddleware; } });
|
|
9
|
+
Object.defineProperty(exports, "requireUser", { enumerable: true, get: function () { return middleware_2.requireUser; } });
|
|
10
|
+
var proxy_2 = require("./proxy");
|
|
11
|
+
Object.defineProperty(exports, "createQelosProxy", { enumerable: true, get: function () { return proxy_2.createQelosProxy; } });
|
|
7
12
|
var sdk_factory_1 = require("./sdk-factory");
|
|
8
13
|
Object.defineProperty(exports, "createRequestSdk", { enumerable: true, get: function () { return sdk_factory_1.createRequestSdk; } });
|
|
14
|
+
var cookies_1 = require("./cookies");
|
|
15
|
+
Object.defineProperty(exports, "rewriteSetCookieDomain", { enumerable: true, get: function () { return cookies_1.rewriteSetCookieDomain; } });
|
|
16
|
+
Object.defineProperty(exports, "rewriteSetCookieDomains", { enumerable: true, get: function () { return cookies_1.rewriteSetCookieDomains; } });
|
|
17
|
+
var proxy_target_1 = require("./proxy-target");
|
|
18
|
+
Object.defineProperty(exports, "resolveQelosProxyTarget", { enumerable: true, get: function () { return proxy_target_1.resolveQelosProxyTarget; } });
|
|
9
19
|
var social_auth_1 = require("./social-auth");
|
|
10
20
|
Object.defineProperty(exports, "completeSocialAuthCallback", { enumerable: true, get: function () { return social_auth_1.completeSocialAuthCallback; } });
|
|
11
|
-
Object.defineProperty(exports, "applySocialAuthCookiesToServerResponse", { enumerable: true, get: function () { return social_auth_1.applySocialAuthCookiesToServerResponse; } });
|
|
12
21
|
Object.defineProperty(exports, "getSocialAuthSetCookieParts", { enumerable: true, get: function () { return social_auth_1.getSocialAuthSetCookieParts; } });
|
|
13
22
|
Object.defineProperty(exports, "parseSocialCallbackRefreshToken", { enumerable: true, get: function () { return social_auth_1.parseSocialCallbackRefreshToken; } });
|
|
23
|
+
/**
|
|
24
|
+
* Build both the user-resolution middleware and the `/api/**` reverse proxy
|
|
25
|
+
* from a single config. When the proxy is enabled (the default), `/api/` is
|
|
26
|
+
* auto-prepended to `skipPaths` so the middleware doesn't shadow the proxy
|
|
27
|
+
* by running its own `/api/me` probe on every proxied request.
|
|
28
|
+
*/
|
|
29
|
+
function createQelosIntegrator(options) {
|
|
30
|
+
const proxyEnabled = options.config.disableProxy !== true;
|
|
31
|
+
let config = options.config;
|
|
32
|
+
if (proxyEnabled) {
|
|
33
|
+
const existingSkipPaths = options.config.skipPaths ?? [];
|
|
34
|
+
const alreadyCovered = existingSkipPaths.some((prefix) => '/api/'.startsWith(prefix));
|
|
35
|
+
if (!alreadyCovered) {
|
|
36
|
+
config = { ...options.config, skipPaths: ['/api/', ...existingSkipPaths] };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const middleware = (0, middleware_1.createQelosMiddleware)({ ...options, config });
|
|
40
|
+
const proxy = proxyEnabled ? (0, proxy_1.createQelosProxy)({ config }) : null;
|
|
41
|
+
return { middleware, proxy };
|
|
42
|
+
}
|
|
14
43
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAyDA,sDAkBC;AA1ED,6CAAmF;AACnF,mCAA2C;AAG3C,2CAGsB;AAFpB,mHAAA,qBAAqB,OAAA;AACrB,yGAAA,WAAW,OAAA;AAOb,iCAA2C;AAAlC,yGAAA,gBAAgB,OAAA;AAGzB,6CAAiD;AAAxC,+GAAA,gBAAgB,OAAA;AAGzB,qCAA4E;AAAnE,iHAAA,sBAAsB,OAAA;AAAE,kHAAA,uBAAuB,OAAA;AACxD,+CAAyD;AAAhD,uHAAA,uBAAuB,OAAA;AAOhC,6CAMuB;AALrB,yHAAA,0BAA0B,OAAA;AAC1B,0HAAA,2BAA2B,OAAA;AAC3B,8HAAA,+BAA+B,OAAA;AAoBjC;;;;;GAKG;AACH,SAAgB,qBAAqB,CACnC,OAAgC;IAEhC,MAAM,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,YAAY,KAAK,IAAI,CAAC;IAE1D,IAAI,MAAM,GAAuB,OAAO,CAAC,MAAM,CAAC;IAChD,IAAI,YAAY,EAAE,CAAC;QACjB,MAAM,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,SAAS,IAAI,EAAE,CAAC;QACzD,MAAM,cAAc,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;QACtF,IAAI,CAAC,cAAc,EAAE,CAAC;YACpB,MAAM,GAAG,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,OAAO,EAAE,GAAG,iBAAiB,CAAC,EAAE,CAAC;QAC7E,CAAC;IACH,CAAC;IAED,MAAM,UAAU,GAAG,IAAA,kCAAqB,EAAC,EAAE,GAAG,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;IACjE,MAAM,KAAK,GAAG,YAAY,CAAC,CAAC,CAAC,IAAA,wBAAgB,EAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAEjE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC;AAC/B,CAAC"}
|
package/dist/middleware.d.ts
CHANGED
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
import type { Request, RequestHandler } from 'express';
|
|
2
2
|
import type { IUser } from '@qelos/sdk/dist/authentication';
|
|
3
3
|
import type { IWorkspace } from '@qelos/sdk/workspaces';
|
|
4
|
-
import type { QelosExpressConfig
|
|
4
|
+
import type { QelosExpressConfig } from './types';
|
|
5
5
|
export interface CreateMiddlewareOptions {
|
|
6
6
|
config: QelosExpressConfig;
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
|
|
11
|
-
onTokenRefresh?: TokenRefreshHook;
|
|
12
|
-
/**
|
|
13
|
-
* Resolve the active workspace for a request. Defaults to picking the first
|
|
14
|
-
* workspace returned from `sdk.workspaces.getList()`.
|
|
8
|
+
* Resolve the active workspace for a request. Defaults to whatever
|
|
9
|
+
* `/api/me` reports on `user.workspace` (the user's currently activated
|
|
10
|
+
* workspace, or `null` when none is active).
|
|
15
11
|
*/
|
|
16
12
|
resolveWorkspace?: (params: {
|
|
17
13
|
req: Request;
|
package/dist/middleware.js
CHANGED
|
@@ -2,56 +2,17 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createQelosMiddleware = createQelosMiddleware;
|
|
4
4
|
exports.requireUser = requireUser;
|
|
5
|
+
const cookies_1 = require("./cookies");
|
|
6
|
+
const proxy_target_1 = require("./proxy-target");
|
|
5
7
|
const sdk_factory_1 = require("./sdk-factory");
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if (parsed && typeof parsed[name] === 'string') {
|
|
12
|
-
return parsed[name];
|
|
13
|
-
}
|
|
14
|
-
const header = req.headers.cookie;
|
|
15
|
-
if (!header)
|
|
16
|
-
return undefined;
|
|
17
|
-
const prefix = name + '=';
|
|
18
|
-
for (const part of header.split(';')) {
|
|
19
|
-
const trimmed = part.trim();
|
|
20
|
-
if (trimmed.startsWith(prefix)) {
|
|
21
|
-
try {
|
|
22
|
-
return decodeURIComponent(trimmed.slice(prefix.length));
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
return trimmed.slice(prefix.length);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return undefined;
|
|
30
|
-
}
|
|
31
|
-
function readTokens(req, config) {
|
|
32
|
-
const accessCookie = config.accessTokenCookie || DEFAULT_ACCESS_COOKIE;
|
|
33
|
-
const refreshCookie = config.refreshTokenCookie || DEFAULT_REFRESH_COOKIE;
|
|
34
|
-
const cookieAccess = readCookie(req, accessCookie);
|
|
35
|
-
const cookieRefresh = readCookie(req, refreshCookie);
|
|
36
|
-
const authHeader = req.headers.authorization;
|
|
37
|
-
const headerAccess = authHeader && authHeader.toLowerCase().startsWith('bearer ')
|
|
38
|
-
? authHeader.slice(7).trim()
|
|
39
|
-
: undefined;
|
|
40
|
-
return {
|
|
41
|
-
accessToken: headerAccess || cookieAccess || undefined,
|
|
42
|
-
refreshToken: cookieRefresh || undefined,
|
|
43
|
-
};
|
|
8
|
+
function shouldSkip(req, config) {
|
|
9
|
+
if (!config.skipPaths?.length)
|
|
10
|
+
return false;
|
|
11
|
+
const path = req.path || req.url || '';
|
|
12
|
+
return config.skipPaths.some((prefix) => path.startsWith(prefix));
|
|
44
13
|
}
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
`${name}=${encodeURIComponent(value)}`,
|
|
48
|
-
'Path=/',
|
|
49
|
-
'HttpOnly',
|
|
50
|
-
'SameSite=Lax',
|
|
51
|
-
];
|
|
52
|
-
if (secure)
|
|
53
|
-
parts.push('Secure');
|
|
54
|
-
return parts.join('; ');
|
|
14
|
+
function buildMeUrl(base) {
|
|
15
|
+
return `${base.replace(/\/+$/, '')}/api/me`;
|
|
55
16
|
}
|
|
56
17
|
function appendSetCookie(res, value) {
|
|
57
18
|
const existing = res.getHeader('set-cookie');
|
|
@@ -65,58 +26,24 @@ function appendSetCookie(res, value) {
|
|
|
65
26
|
res.setHeader('set-cookie', [value]);
|
|
66
27
|
}
|
|
67
28
|
}
|
|
68
|
-
function writeTokensToCookies(res, config, tokens) {
|
|
69
|
-
const accessCookie = config.accessTokenCookie || DEFAULT_ACCESS_COOKIE;
|
|
70
|
-
const refreshCookie = config.refreshTokenCookie || DEFAULT_REFRESH_COOKIE;
|
|
71
|
-
const secure = !/^http:\/\//i.test(config.appUrl);
|
|
72
|
-
const cookieRes = res;
|
|
73
|
-
if (typeof cookieRes.cookie === 'function') {
|
|
74
|
-
const cookieOptions = {
|
|
75
|
-
httpOnly: true,
|
|
76
|
-
secure,
|
|
77
|
-
sameSite: 'lax',
|
|
78
|
-
path: '/',
|
|
79
|
-
};
|
|
80
|
-
cookieRes.cookie(accessCookie, tokens.accessToken, cookieOptions);
|
|
81
|
-
if (tokens.refreshToken) {
|
|
82
|
-
cookieRes.cookie(refreshCookie, tokens.refreshToken, cookieOptions);
|
|
83
|
-
}
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
appendSetCookie(res, serializeCookie(accessCookie, tokens.accessToken, secure));
|
|
87
|
-
if (tokens.refreshToken) {
|
|
88
|
-
appendSetCookie(res, serializeCookie(refreshCookie, tokens.refreshToken, secure));
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
function shouldSkip(req, config) {
|
|
92
|
-
if (!config.skipPaths?.length)
|
|
93
|
-
return false;
|
|
94
|
-
const path = req.path || req.url || '';
|
|
95
|
-
return config.skipPaths.some((prefix) => path.startsWith(prefix));
|
|
96
|
-
}
|
|
97
29
|
function createQelosMiddleware(options) {
|
|
98
30
|
const { config, resolveWorkspace } = options;
|
|
99
|
-
const
|
|
100
|
-
(async ({ res, newTokens }) => {
|
|
101
|
-
writeTokensToCookies(res, config, newTokens);
|
|
102
|
-
});
|
|
31
|
+
const sdkFetch = (config.sdkOptions?.fetch ?? globalThis.fetch);
|
|
103
32
|
return async function qelosMiddleware(req, res, next) {
|
|
104
33
|
if (shouldSkip(req, config)) {
|
|
105
34
|
next();
|
|
106
35
|
return;
|
|
107
36
|
}
|
|
108
|
-
const
|
|
109
|
-
const sdk = (0, sdk_factory_1.createRequestSdk)({ config, tokens, req, res, onTokenRefresh });
|
|
37
|
+
const sdk = (0, sdk_factory_1.createRequestSdk)({ config, req });
|
|
110
38
|
const ctx = {
|
|
111
39
|
user: null,
|
|
112
40
|
workspace: null,
|
|
113
41
|
workspaces: [],
|
|
114
42
|
sdk,
|
|
115
|
-
tokens,
|
|
116
43
|
};
|
|
117
44
|
req.qelos = ctx;
|
|
118
|
-
const
|
|
119
|
-
if (!
|
|
45
|
+
const base = (0, proxy_target_1.resolveQelosProxyTarget)(config);
|
|
46
|
+
if (!base && !config.apiToken) {
|
|
120
47
|
if (config.requireAuth) {
|
|
121
48
|
res.status(401).json({ code: 'UNAUTHORIZED' });
|
|
122
49
|
return;
|
|
@@ -124,10 +51,44 @@ function createQelosMiddleware(options) {
|
|
|
124
51
|
next();
|
|
125
52
|
return;
|
|
126
53
|
}
|
|
127
|
-
|
|
128
|
-
|
|
54
|
+
if (base) {
|
|
55
|
+
const headers = {};
|
|
56
|
+
if (req.headers.cookie)
|
|
57
|
+
headers.cookie = req.headers.cookie;
|
|
58
|
+
if (req.headers.authorization) {
|
|
59
|
+
headers.authorization = req.headers.authorization;
|
|
60
|
+
}
|
|
61
|
+
const originalHost = req.headers.host;
|
|
62
|
+
let upstream;
|
|
63
|
+
try {
|
|
64
|
+
upstream = await sdkFetch(buildMeUrl(base), {
|
|
65
|
+
method: 'GET',
|
|
66
|
+
headers,
|
|
67
|
+
redirect: 'manual',
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
if (config.requireAuth) {
|
|
72
|
+
res.status(401).json({ code: 'UNAUTHORIZED' });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
next();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const setCookieValues = upstream.headers.getSetCookie?.() ?? [];
|
|
79
|
+
for (const value of setCookieValues) {
|
|
80
|
+
appendSetCookie(res, (0, cookies_1.rewriteSetCookieDomain)(value, originalHost));
|
|
81
|
+
}
|
|
82
|
+
if (upstream.ok) {
|
|
83
|
+
try {
|
|
84
|
+
ctx.user = (await upstream.json());
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
ctx.user = null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
129
90
|
}
|
|
130
|
-
|
|
91
|
+
if (!ctx.user) {
|
|
131
92
|
if (config.requireAuth) {
|
|
132
93
|
res.status(401).json({ code: 'UNAUTHORIZED' });
|
|
133
94
|
return;
|
|
@@ -141,18 +102,20 @@ function createQelosMiddleware(options) {
|
|
|
141
102
|
catch {
|
|
142
103
|
ctx.workspaces = [];
|
|
143
104
|
}
|
|
144
|
-
if (
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
105
|
+
if (resolveWorkspace) {
|
|
106
|
+
ctx.workspace =
|
|
107
|
+
(await resolveWorkspace({
|
|
108
|
+
req,
|
|
109
|
+
user: ctx.user,
|
|
110
|
+
workspaces: ctx.workspaces,
|
|
111
|
+
})) || null;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
// `/api/me` carries `user.workspace` (`{ _id, name, roles }`) only when
|
|
115
|
+
// the user has activated a workspace. Null means the frontend must
|
|
116
|
+
// prompt to activate/create one — don't silently pick workspaces[0].
|
|
117
|
+
ctx.workspace =
|
|
118
|
+
ctx.user.workspace || null;
|
|
156
119
|
}
|
|
157
120
|
next();
|
|
158
121
|
};
|
package/dist/middleware.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":";;AA2CA,sDA2GC;AASD,kCAUC;AAtKD,uCAAmD;AACnD,iDAAyD;AACzD,+CAAiD;AAiBjD,SAAS,UAAU,CAAC,GAAY,EAAE,MAA0B;IAC1D,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM;QAAE,OAAO,KAAK,CAAC;IAC5C,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,GAAG,IAAI,EAAE,CAAC;IACvC,OAAO,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;AACpE,CAAC;AAED,SAAS,UAAU,CAAC,IAAY;IAC9B,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,SAAS,CAAC;AAC9C,CAAC;AAED,SAAS,eAAe,CAAC,GAAa,EAAE,KAAa;IACnD,MAAM,QAAQ,GAAG,GAAG,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC;IAC7C,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC5B,GAAG,CAAC,SAAS,CAAC,YAAY,EAAE,CAAC,GAAG,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC;IACpD,CAAC;SAAM,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC;QACxC,GAAG,CAAC,SAAS,CAAC,YAAY,EAAE,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC;IACjD,CAAC;SAAM,CAAC;QACN,GAAG,CAAC,SAAS,CAAC,YAAY,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;IACvC,CAAC;AACH,CAAC;AAED,SAAgB,qBAAqB,CACnC,OAAgC;IAEhC,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,GAAG,OAAO,CAAC;IAC7C,MAAM,QAAQ,GAAG,CAAC,MAAM,CAAC,UAAU,EAAE,KAAK,IAAI,UAAU,CAAC,KAAK,CAA4B,CAAC;IAE3F,OAAO,KAAK,UAAU,eAAe,CACnC,GAAY,EACZ,GAAa,EACb,IAAkB;QAElB,IAAI,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC,EAAE,CAAC;YAC5B,IAAI,EAAE,CAAC;YACP,OAAO;QACT,CAAC;QAED,MAAM,GAAG,GAAG,IAAA,8BAAgB,EAAC,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;QAE9C,MAAM,GAAG,GAAwB;YAC/B,IAAI,EAAE,IAAI;YACV,SAAS,EAAE,IAAI;YACf,UAAU,EAAE,EAAE;YACd,GAAG;SACJ,CAAC;QACF,GAAG,CAAC,KAAK,GAAG,GAAG,CAAC;QAEhB,MAAM,IAAI,GAAG,IAAA,sCAAuB,EAAC,MAAM,CAAC,CAAC;QAC7C,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;YAC9B,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC;gBACvB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC,CAAC;gBAC/C,OAAO;YACT,CAAC;YACD,IAAI,EAAE,CAAC;YACP,OAAO;QACT,CAAC;QAED,IAAI,IAAI,EAAE,CAAC;YACT,MAAM,OAAO,GAA2B,EAAE,CAAC;YAC3C,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM;gBAAE,OAAO,CAAC,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC;YAC5D,IAAI,GAAG,CAAC,OAAO,CAAC,aAAa,EAAE,CAAC;gBAC9B,OAAO,CAAC,aAAa,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC;YACpD,CAAC;YAED,MAAM,YAAY,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC;YAEtC,IAAI,QAAyC,CAAC;YAC9C,IAAI,CAAC;gBACH,QAAQ,GAAG,MAAM,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE;oBAC1C,MAAM,EAAE,KAAK;oBACb,OAAO;oBACP,QAAQ,EAAE,QAAQ;iBACnB,CAAC,CAAC;YACL,CAAC;YAAC,MAAM,CAAC;gBACP,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC;oBACvB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC,CAAC;oBAC/C,OAAO;gBACT,CAAC;gBACD,IAAI,EAAE,CAAC;gBACP,OAAO;YACT,CAAC;YAED,MAAM,eAAe,GAAG,QAAQ,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,IAAI,EAAE,CAAC;YAChE,KAAK,MAAM,KAAK,IAAI,eAAe,EAAE,CAAC;gBACpC,eAAe,CAAC,GAAG,EAAE,IAAA,gCAAsB,EAAC,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC;YACpE,CAAC;YAED,IAAI,QAAQ,CAAC,EAAE,EAAE,CAAC;gBAChB,IAAI,CAAC;oBACH,GAAG,CAAC,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAU,CAAC;gBAC9C,CAAC;gBAAC,MAAM,CAAC;oBACP,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;gBAClB,CAAC;YACH,CAAC;QACH,CAAC;QAED,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;YACd,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC;gBACvB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC,CAAC;gBAC/C,OAAO;YACT,CAAC;YACD,IAAI,EAAE,CAAC;YACP,OAAO;QACT,CAAC;QAED,IAAI,CAAC;YACH,GAAG,CAAC,UAAU,GAAG,MAAM,GAAG,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC;QAClD,CAAC;QAAC,MAAM,CAAC;YACP,GAAG,CAAC,UAAU,GAAG,EAAE,CAAC;QACtB,CAAC;QAED,IAAI,gBAAgB,EAAE,CAAC;YACrB,GAAG,CAAC,SAAS;gBACX,CAAC,MAAM,gBAAgB,CAAC;oBACtB,GAAG;oBACH,IAAI,EAAE,GAAG,CAAC,IAAI;oBACd,UAAU,EAAE,GAAG,CAAC,UAAU;iBAC3B,CAAC,CAAC,IAAI,IAAI,CAAC;QAChB,CAAC;aAAM,CAAC;YACN,wEAAwE;YACxE,mEAAmE;YACnE,qEAAqE;YACrE,GAAG,CAAC,SAAS;gBACV,GAAG,CAAC,IAAqD,CAAC,SAAS,IAAI,IAAI,CAAC;QACjF,CAAC;QAED,IAAI,EAAE,CAAC;IACT,CAAC,CAAC;AACJ,CAAC;AAID;;;;GAIG;AACH,SAAgB,WAAW,CACzB,OAAuB;IAEvB,OAAO,SAAS,gBAAgB,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI;QAC7C,IAAI,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;YAClC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC,CAAC;YAC/C,OAAO;QACT,CAAC;QACD,OAAO,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;IACjC,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { QelosExpressConfig } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the origin to which `/api/**` requests are proxied. The managed
|
|
4
|
+
* Qelos app URL (`config.appUrl`) is the proxy target — env vars are only
|
|
5
|
+
* dev-time overrides when the configured `appUrl` is not reachable from
|
|
6
|
+
* the local host.
|
|
7
|
+
*
|
|
8
|
+
* Priority (highest first):
|
|
9
|
+
* 1. `process.env.QELOS_PROXY_TARGET`
|
|
10
|
+
* 2. `process.env.QELOS_IP`
|
|
11
|
+
* 3. `process.env.QELOS_API_IP`
|
|
12
|
+
* 4. `config.appUrl`
|
|
13
|
+
*
|
|
14
|
+
* Empty / whitespace-only values are skipped, and `undefined` is returned when
|
|
15
|
+
* nothing is configured so the caller can surface a clear error.
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveQelosProxyTarget(config: QelosExpressConfig): string | undefined;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveQelosProxyTarget = resolveQelosProxyTarget;
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the origin to which `/api/**` requests are proxied. The managed
|
|
6
|
+
* Qelos app URL (`config.appUrl`) is the proxy target — env vars are only
|
|
7
|
+
* dev-time overrides when the configured `appUrl` is not reachable from
|
|
8
|
+
* the local host.
|
|
9
|
+
*
|
|
10
|
+
* Priority (highest first):
|
|
11
|
+
* 1. `process.env.QELOS_PROXY_TARGET`
|
|
12
|
+
* 2. `process.env.QELOS_IP`
|
|
13
|
+
* 3. `process.env.QELOS_API_IP`
|
|
14
|
+
* 4. `config.appUrl`
|
|
15
|
+
*
|
|
16
|
+
* Empty / whitespace-only values are skipped, and `undefined` is returned when
|
|
17
|
+
* nothing is configured so the caller can surface a clear error.
|
|
18
|
+
*/
|
|
19
|
+
function resolveQelosProxyTarget(config) {
|
|
20
|
+
const env = process.env;
|
|
21
|
+
const fromEnv = env.QELOS_PROXY_TARGET?.trim() ||
|
|
22
|
+
env.QELOS_IP?.trim() ||
|
|
23
|
+
env.QELOS_API_IP?.trim();
|
|
24
|
+
if (fromEnv)
|
|
25
|
+
return fromEnv;
|
|
26
|
+
const fromAppUrl = config.appUrl?.trim();
|
|
27
|
+
return fromAppUrl || undefined;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=proxy-target.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy-target.js","sourceRoot":"","sources":["../src/proxy-target.ts"],"names":[],"mappings":";;AAiBA,0DAYC;AA3BD;;;;;;;;;;;;;;GAcG;AACH,SAAgB,uBAAuB,CACrC,MAA0B;IAE1B,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC;IACxB,MAAM,OAAO,GACX,GAAG,CAAC,kBAAkB,EAAE,IAAI,EAAE;QAC9B,GAAG,CAAC,QAAQ,EAAE,IAAI,EAAE;QACpB,GAAG,CAAC,YAAY,EAAE,IAAI,EAAE,CAAC;IAC3B,IAAI,OAAO;QAAE,OAAO,OAAO,CAAC;IAE5B,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC;IACzC,OAAO,UAAU,IAAI,SAAS,CAAC;AACjC,CAAC"}
|
package/dist/proxy.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { RequestHandler } from 'express';
|
|
2
|
+
import type { QelosExpressConfig } from './types';
|
|
3
|
+
export interface CreateProxyOptions {
|
|
4
|
+
config: QelosExpressConfig;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Catch-all `/api/**` reverse proxy. Forwards the inbound request to the
|
|
8
|
+
* configured Qelos origin, streaming bodies in both directions and rewriting
|
|
9
|
+
* the `Domain=` attribute of every upstream `Set-Cookie` header to the
|
|
10
|
+
* inbound request's host so the cookie becomes first-party.
|
|
11
|
+
*
|
|
12
|
+
* Responds with `503` when no proxy target is configured. Does not proxy
|
|
13
|
+
* WebSocket upgrades.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createQelosProxy(options: CreateProxyOptions): RequestHandler;
|
|
16
|
+
export type QelosProxyHandler = ReturnType<typeof createQelosProxy>;
|
package/dist/proxy.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.createQelosProxy = createQelosProxy;
|
|
27
|
+
const http = __importStar(require("node:http"));
|
|
28
|
+
const https = __importStar(require("node:https"));
|
|
29
|
+
const node_url_1 = require("node:url");
|
|
30
|
+
const cookies_1 = require("./cookies");
|
|
31
|
+
const proxy_target_1 = require("./proxy-target");
|
|
32
|
+
const HOP_BY_HOP = new Set([
|
|
33
|
+
'connection',
|
|
34
|
+
'keep-alive',
|
|
35
|
+
'proxy-authenticate',
|
|
36
|
+
'proxy-authorization',
|
|
37
|
+
'te',
|
|
38
|
+
'trailer',
|
|
39
|
+
'transfer-encoding',
|
|
40
|
+
'upgrade',
|
|
41
|
+
'host',
|
|
42
|
+
'content-length',
|
|
43
|
+
]);
|
|
44
|
+
function buildForwardedHeaders(source, targetHost) {
|
|
45
|
+
const out = {};
|
|
46
|
+
for (const [name, value] of Object.entries(source)) {
|
|
47
|
+
if (value === undefined)
|
|
48
|
+
continue;
|
|
49
|
+
if (HOP_BY_HOP.has(name.toLowerCase()))
|
|
50
|
+
continue;
|
|
51
|
+
out[name] = value;
|
|
52
|
+
}
|
|
53
|
+
out['host'] = targetHost;
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Catch-all `/api/**` reverse proxy. Forwards the inbound request to the
|
|
58
|
+
* configured Qelos origin, streaming bodies in both directions and rewriting
|
|
59
|
+
* the `Domain=` attribute of every upstream `Set-Cookie` header to the
|
|
60
|
+
* inbound request's host so the cookie becomes first-party.
|
|
61
|
+
*
|
|
62
|
+
* Responds with `503` when no proxy target is configured. Does not proxy
|
|
63
|
+
* WebSocket upgrades.
|
|
64
|
+
*/
|
|
65
|
+
function createQelosProxy(options) {
|
|
66
|
+
const { config } = options;
|
|
67
|
+
return function qelosApiProxy(req, res) {
|
|
68
|
+
const base = (0, proxy_target_1.resolveQelosProxyTarget)(config);
|
|
69
|
+
if (!base) {
|
|
70
|
+
res.status(503).json({
|
|
71
|
+
code: 'QELOS_PROXY_NOT_CONFIGURED',
|
|
72
|
+
message: '[@qelos/integrator-express] Qelos API proxy is not configured. ' +
|
|
73
|
+
'Set config.appUrl (or QELOS_PROXY_TARGET / QELOS_IP / QELOS_API_IP in development).',
|
|
74
|
+
});
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
let target;
|
|
78
|
+
try {
|
|
79
|
+
const baseUrl = new node_url_1.URL(base);
|
|
80
|
+
target = new node_url_1.URL(req.originalUrl || req.url, baseUrl);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
res.status(502).json({
|
|
84
|
+
code: 'QELOS_PROXY_BAD_TARGET',
|
|
85
|
+
message: '[@qelos/integrator-express] Invalid proxy target URL.',
|
|
86
|
+
});
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const client = target.protocol === 'https:' ? https : http;
|
|
90
|
+
const originalHost = req.headers.host;
|
|
91
|
+
const forwardedHeaders = buildForwardedHeaders(req.headers, target.host);
|
|
92
|
+
const upstreamReq = client.request({
|
|
93
|
+
protocol: target.protocol,
|
|
94
|
+
hostname: target.hostname,
|
|
95
|
+
port: target.port || (target.protocol === 'https:' ? 443 : 80),
|
|
96
|
+
method: req.method,
|
|
97
|
+
path: target.pathname + target.search,
|
|
98
|
+
headers: forwardedHeaders,
|
|
99
|
+
}, (upstreamRes) => {
|
|
100
|
+
const status = upstreamRes.statusCode ?? 502;
|
|
101
|
+
for (const [name, value] of Object.entries(upstreamRes.headers)) {
|
|
102
|
+
if (value === undefined)
|
|
103
|
+
continue;
|
|
104
|
+
const lower = name.toLowerCase();
|
|
105
|
+
if (HOP_BY_HOP.has(lower))
|
|
106
|
+
continue;
|
|
107
|
+
if (lower === 'set-cookie') {
|
|
108
|
+
const values = Array.isArray(value) ? value : [value];
|
|
109
|
+
res.setHeader('set-cookie', values.map((v) => (0, cookies_1.rewriteSetCookieDomain)(v, originalHost)));
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
res.setHeader(name, value);
|
|
113
|
+
}
|
|
114
|
+
res.status(status);
|
|
115
|
+
upstreamRes.pipe(res);
|
|
116
|
+
});
|
|
117
|
+
upstreamReq.on('error', (err) => {
|
|
118
|
+
if (!res.headersSent) {
|
|
119
|
+
res.status(502).json({
|
|
120
|
+
code: 'QELOS_PROXY_UPSTREAM_ERROR',
|
|
121
|
+
message: err.message,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
res.end();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
req.on('aborted', () => upstreamReq.destroy());
|
|
129
|
+
req.pipe(upstreamReq);
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=proxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy.js","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"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"}
|
package/dist/sdk-factory.d.ts
CHANGED
|
@@ -1,16 +1,8 @@
|
|
|
1
|
-
import type { Request
|
|
1
|
+
import type { Request } from 'express';
|
|
2
2
|
import QelosSDK from '@qelos/sdk';
|
|
3
|
-
import type { QelosExpressConfig
|
|
3
|
+
import type { QelosExpressConfig } from './types';
|
|
4
4
|
export interface CreateSdkParams {
|
|
5
5
|
config: QelosExpressConfig;
|
|
6
|
-
/**
|
|
7
|
-
* Tokens for the current request. The factory mutates this object in place
|
|
8
|
-
* when a token refresh occurs, so callers can read the latest pair after
|
|
9
|
-
* the SDK has been used.
|
|
10
|
-
*/
|
|
11
|
-
tokens: QelosTokenPair;
|
|
12
6
|
req: Request;
|
|
13
|
-
res: Response;
|
|
14
|
-
onTokenRefresh?: TokenRefreshHook;
|
|
15
7
|
}
|
|
16
|
-
export declare function createRequestSdk({ config,
|
|
8
|
+
export declare function createRequestSdk({ config, req }: CreateSdkParams): QelosSDK;
|
package/dist/sdk-factory.js
CHANGED
|
@@ -5,93 +5,29 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.createRequestSdk = createRequestSdk;
|
|
7
7
|
const sdk_1 = __importDefault(require("@qelos/sdk"));
|
|
8
|
-
|
|
9
|
-
'/api/token/refresh',
|
|
10
|
-
'/api/cookie/refresh',
|
|
11
|
-
'/api/signin',
|
|
12
|
-
'/api/signup',
|
|
13
|
-
]);
|
|
14
|
-
function createRequestSdk({ config, tokens, req, res, onTokenRefresh, }) {
|
|
15
|
-
let sdk;
|
|
16
|
-
let refreshInFlight = null;
|
|
8
|
+
function createRequestSdk({ config, req }) {
|
|
17
9
|
const baseOptions = config.sdkOptions || {};
|
|
18
|
-
async function performRefresh() {
|
|
19
|
-
if (!tokens.refreshToken && !tokens.accessToken) {
|
|
20
|
-
throw new Error('no refresh token available');
|
|
21
|
-
}
|
|
22
|
-
const previous = {
|
|
23
|
-
accessToken: tokens.accessToken,
|
|
24
|
-
refreshToken: tokens.refreshToken,
|
|
25
|
-
};
|
|
26
|
-
let refreshed;
|
|
27
|
-
if (tokens.refreshToken) {
|
|
28
|
-
const result = await sdk.authentication.refreshToken(tokens.refreshToken);
|
|
29
|
-
refreshed = {
|
|
30
|
-
accessToken: result.payload.token,
|
|
31
|
-
refreshToken: result.payload.refreshToken,
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
else {
|
|
35
|
-
const result = await sdk.authentication.refreshCookieToken(tokens.accessToken);
|
|
36
|
-
refreshed = {
|
|
37
|
-
accessToken: result.payload.cookieToken,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
tokens.accessToken = refreshed.accessToken;
|
|
41
|
-
tokens.refreshToken = refreshed.refreshToken;
|
|
42
|
-
if (onTokenRefresh) {
|
|
43
|
-
await onTokenRefresh({
|
|
44
|
-
req,
|
|
45
|
-
res,
|
|
46
|
-
oldTokens: previous,
|
|
47
|
-
newTokens: refreshed,
|
|
48
|
-
sdk,
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
function ensureRefresh() {
|
|
53
|
-
if (!refreshInFlight) {
|
|
54
|
-
refreshInFlight = performRefresh().finally(() => {
|
|
55
|
-
refreshInFlight = null;
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
return refreshInFlight;
|
|
59
|
-
}
|
|
60
10
|
const options = {
|
|
61
11
|
appUrl: config.appUrl,
|
|
62
12
|
fetch: globalThis.fetch,
|
|
63
|
-
forceRefresh: !config.apiToken,
|
|
64
13
|
...baseOptions,
|
|
65
14
|
};
|
|
66
15
|
if (config.apiToken) {
|
|
67
16
|
options.apiToken = config.apiToken;
|
|
17
|
+
return new sdk_1.default(options);
|
|
68
18
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
19
|
+
options.extraHeaders = async () => {
|
|
20
|
+
const headers = {};
|
|
21
|
+
const cookieHeader = req.headers.cookie;
|
|
22
|
+
if (cookieHeader) {
|
|
23
|
+
headers.cookie = cookieHeader;
|
|
72
24
|
}
|
|
73
|
-
|
|
74
|
-
|
|
25
|
+
const authHeader = req.headers.authorization;
|
|
26
|
+
if (authHeader) {
|
|
27
|
+
headers.authorization = authHeader;
|
|
75
28
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return headers;
|
|
80
|
-
}
|
|
81
|
-
if (forceRefresh && tokens.refreshToken) {
|
|
82
|
-
await ensureRefresh();
|
|
83
|
-
}
|
|
84
|
-
const token = sdk?.authentication?.accessToken || tokens.accessToken;
|
|
85
|
-
if (token) {
|
|
86
|
-
headers.authorization = 'Bearer ' + token;
|
|
87
|
-
}
|
|
88
|
-
return headers;
|
|
89
|
-
};
|
|
90
|
-
options.onFailedRefreshToken = async () => {
|
|
91
|
-
await ensureRefresh();
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
sdk = new sdk_1.default(options);
|
|
95
|
-
return sdk;
|
|
29
|
+
return headers;
|
|
30
|
+
};
|
|
31
|
+
return new sdk_1.default(options);
|
|
96
32
|
}
|
|
97
33
|
//# sourceMappingURL=sdk-factory.js.map
|
package/dist/sdk-factory.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sdk-factory.js","sourceRoot":"","sources":["../src/sdk-factory.ts"],"names":[],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"sdk-factory.js","sourceRoot":"","sources":["../src/sdk-factory.ts"],"names":[],"mappings":";;;;;AAUA,4CA4BC;AArCD,qDAAkC;AASlC,SAAgB,gBAAgB,CAAC,EAAE,MAAM,EAAE,GAAG,EAAmB;IAC/D,MAAM,WAAW,GAA6B,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC;IAEtE,MAAM,OAAO,GAAoB;QAC/B,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,KAAK,EAAE,UAAU,CAAC,KAA6B;QAC/C,GAAG,WAAW;KACf,CAAC;IAEF,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QACpB,OAAO,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;QACnC,OAAO,IAAI,aAAQ,CAAC,OAAO,CAAC,CAAC;IAC/B,CAAC;IAED,OAAO,CAAC,YAAY,GAAG,KAAK,IAAI,EAAE;QAChC,MAAM,OAAO,GAA2B,EAAE,CAAC;QAC3C,MAAM,YAAY,GAAG,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC;QACxC,IAAI,YAAY,EAAE,CAAC;YACjB,OAAO,CAAC,MAAM,GAAG,YAAY,CAAC;QAChC,CAAC;QACD,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC;QAC7C,IAAI,UAAU,EAAE,CAAC;YACf,OAAO,CAAC,aAAa,GAAG,UAAU,CAAC;QACrC,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC,CAAC;IAEF,OAAO,IAAI,aAAQ,CAAC,OAAO,CAAC,CAAC;AAC/B,CAAC"}
|
package/dist/social-auth.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import type { Request, Response } from 'express';
|
|
1
2
|
import type QelosSDK from '@qelos/sdk';
|
|
2
3
|
import type { SocialAuthCallbackPayload } from '@qelos/sdk';
|
|
3
4
|
/**
|
|
4
5
|
* Runs {@link QelosSDK.authentication.socialCallback} for `requestUrl` and
|
|
5
|
-
* forwards session `Set-Cookie` headers to the Express response
|
|
6
|
+
* forwards session `Set-Cookie` headers to the Express response, rewriting
|
|
7
|
+
* the `Domain=` attribute of each cookie to the inbound request's host so
|
|
8
|
+
* the upstream-issued cookies land as first-party on the integrator host.
|
|
6
9
|
*/
|
|
7
|
-
export declare function completeSocialAuthCallback(sdk: QelosSDK,
|
|
8
|
-
|
|
9
|
-
}): Promise<SocialAuthCallbackPayload>;
|
|
10
|
-
export { applySocialAuthCookiesToServerResponse, getSocialAuthSetCookieParts, parseSocialCallbackRefreshToken, type SocialAuthCallbackPayload, type SocialCallbackInput, } from '@qelos/sdk';
|
|
10
|
+
export declare function completeSocialAuthCallback(sdk: QelosSDK, req: Pick<Request, 'url' | 'headers'>, res: Pick<Response, 'setHeader'>): Promise<SocialAuthCallbackPayload>;
|
|
11
|
+
export { getSocialAuthSetCookieParts, parseSocialCallbackRefreshToken, type SocialAuthCallbackPayload, type SocialCallbackInput, } from '@qelos/sdk';
|
package/dist/social-auth.js
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.parseSocialCallbackRefreshToken = exports.getSocialAuthSetCookieParts =
|
|
3
|
+
exports.parseSocialCallbackRefreshToken = exports.getSocialAuthSetCookieParts = void 0;
|
|
4
4
|
exports.completeSocialAuthCallback = completeSocialAuthCallback;
|
|
5
5
|
const sdk_1 = require("@qelos/sdk");
|
|
6
|
+
const cookies_1 = require("./cookies");
|
|
6
7
|
/**
|
|
7
8
|
* Runs {@link QelosSDK.authentication.socialCallback} for `requestUrl` and
|
|
8
|
-
* forwards session `Set-Cookie` headers to the Express response
|
|
9
|
+
* forwards session `Set-Cookie` headers to the Express response, rewriting
|
|
10
|
+
* the `Domain=` attribute of each cookie to the inbound request's host so
|
|
11
|
+
* the upstream-issued cookies land as first-party on the integrator host.
|
|
9
12
|
*/
|
|
10
|
-
async function completeSocialAuthCallback(sdk,
|
|
11
|
-
const result = await sdk.authentication.socialCallback(
|
|
12
|
-
(0, sdk_1.
|
|
13
|
+
async function completeSocialAuthCallback(sdk, req, res) {
|
|
14
|
+
const result = await sdk.authentication.socialCallback(req.url);
|
|
15
|
+
const parts = (0, sdk_1.getSocialAuthSetCookieParts)(result);
|
|
16
|
+
if (parts.length) {
|
|
17
|
+
const host = req.headers.host;
|
|
18
|
+
const rewritten = parts.map((value) => (0, cookies_1.rewriteSetCookieDomain)(value, host));
|
|
19
|
+
res.setHeader('Set-Cookie', rewritten.length === 1 ? rewritten[0] : rewritten);
|
|
20
|
+
}
|
|
13
21
|
return result;
|
|
14
22
|
}
|
|
15
23
|
var sdk_2 = require("@qelos/sdk");
|
|
16
|
-
Object.defineProperty(exports, "applySocialAuthCookiesToServerResponse", { enumerable: true, get: function () { return sdk_2.applySocialAuthCookiesToServerResponse; } });
|
|
17
24
|
Object.defineProperty(exports, "getSocialAuthSetCookieParts", { enumerable: true, get: function () { return sdk_2.getSocialAuthSetCookieParts; } });
|
|
18
25
|
Object.defineProperty(exports, "parseSocialCallbackRefreshToken", { enumerable: true, get: function () { return sdk_2.parseSocialCallbackRefreshToken; } });
|
|
19
26
|
//# sourceMappingURL=social-auth.js.map
|
package/dist/social-auth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"social-auth.js","sourceRoot":"","sources":["../src/social-auth.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"social-auth.js","sourceRoot":"","sources":["../src/social-auth.ts"],"names":[],"mappings":";;;AAYA,gEAaC;AAtBD,oCAAyD;AACzD,uCAAmD;AAEnD;;;;;GAKG;AACI,KAAK,UAAU,0BAA0B,CAC9C,GAAa,EACb,GAAqC,EACrC,GAAgC;IAEhC,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,cAAc,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAChE,MAAM,KAAK,GAAG,IAAA,iCAA2B,EAAC,MAAM,CAAC,CAAC;IAClD,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;QACjB,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC;QAC9B,MAAM,SAAS,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAA,gCAAsB,EAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC;QAC5E,GAAG,CAAC,SAAS,CAAC,YAAY,EAAE,SAAS,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAClF,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,kCAKoB;AAJlB,kHAAA,2BAA2B,OAAA;AAC3B,sHAAA,+BAA+B,OAAA"}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,25 +1,59 @@
|
|
|
1
|
-
import type { Request, Response } from 'express';
|
|
2
1
|
import type QelosSDK from '@qelos/sdk';
|
|
3
2
|
import type { IUser } from '@qelos/sdk/dist/authentication';
|
|
4
3
|
import type { IWorkspace } from '@qelos/sdk/workspaces';
|
|
5
4
|
import type { QelosSDKOptions } from '@qelos/sdk/types';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
export interface QelosExpressConfig {
|
|
6
|
+
/**
|
|
7
|
+
* Base URL of the Qelos backend (e.g. https://yourdomain.com).
|
|
8
|
+
*/
|
|
9
|
+
appUrl: string;
|
|
10
|
+
/**
|
|
11
|
+
* Static API token used for service-to-service calls. When provided, no
|
|
12
|
+
* cookie-based authentication is performed.
|
|
13
|
+
*/
|
|
14
|
+
apiToken?: string;
|
|
15
|
+
/**
|
|
16
|
+
* If true, the request is rejected (401) when the user cannot be resolved.
|
|
17
|
+
* Defaults to `false` — anonymous requests pass through with
|
|
18
|
+
* `req.qelos.user = null`.
|
|
19
|
+
*/
|
|
20
|
+
requireAuth?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Skip the middleware entirely for requests matching any of these path
|
|
23
|
+
* prefixes. Useful for `/api/_auth`, `/health`, etc.
|
|
24
|
+
*/
|
|
25
|
+
skipPaths?: string[];
|
|
26
|
+
/**
|
|
27
|
+
* If true, the catch-all `/api/**` proxy handler is not registered when
|
|
28
|
+
* using `createQelosIntegrator`. Defaults to `false`.
|
|
29
|
+
*/
|
|
30
|
+
disableProxy?: boolean;
|
|
9
31
|
/**
|
|
10
32
|
* Optional extra options merged into the per-request SDK instance.
|
|
11
33
|
*/
|
|
12
34
|
sdkOptions?: Partial<QelosSDKOptions>;
|
|
13
35
|
}
|
|
14
|
-
export interface
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
36
|
+
export interface QelosRequestContext {
|
|
37
|
+
/**
|
|
38
|
+
* The authenticated user, or `null` when anonymous.
|
|
39
|
+
*/
|
|
40
|
+
user: IUser | null;
|
|
41
|
+
/**
|
|
42
|
+
* The active workspace for the request, or `null` when none is active /
|
|
43
|
+
* the user is anonymous.
|
|
44
|
+
*/
|
|
45
|
+
workspace: IWorkspace | null;
|
|
46
|
+
/**
|
|
47
|
+
* The full list of workspaces the user has access to.
|
|
48
|
+
*/
|
|
49
|
+
workspaces: IWorkspace[];
|
|
50
|
+
/**
|
|
51
|
+
* SDK instance bound to the current request's cookies. `extraHeaders`
|
|
52
|
+
* re-reads cookies live on each call, so cookie rotations applied by the
|
|
53
|
+
* middleware are picked up automatically.
|
|
54
|
+
*/
|
|
19
55
|
sdk: QelosSDK;
|
|
20
56
|
}
|
|
21
|
-
export type TokenRefreshHook = (ctx: TokenRefreshContext) => void | Promise<void>;
|
|
22
|
-
export type QelosRequestContext = QelosContext<QelosSDK, IUser, IWorkspace>;
|
|
23
57
|
declare module 'express' {
|
|
24
58
|
interface Request {
|
|
25
59
|
qelos: QelosRequestContext;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qelos/integrator-express",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.0",
|
|
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.1.0",
|
|
51
|
+
"@qelos/sdk": "4.1.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
|
}
|