@qelos/integrator-next 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 +109 -164
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
# @qelos/integrator-next
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
every
|
|
3
|
+
Next.js integrator for [Qelos](https://qelos.io). It turns your Next.js host into
|
|
4
|
+
a same-origin BFF for a managed Qelos app: Edge middleware resolves the current
|
|
5
|
+
user via `GET /api/me` with inbound cookies forwarded verbatim, a Node catch-all
|
|
6
|
+
route proxies `/api/**` to Qelos with `Set-Cookie` domain rewriting, and the
|
|
7
|
+
SDK reads live `Cookie` / `Authorization` headers on every call — no
|
|
8
|
+
integrator-managed access or refresh tokens.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
Supports the **App Router** and the **Pages Router** on Next.js **14** and
|
|
11
|
+
**15**.
|
|
12
|
+
|
|
13
|
+
> Requires **Node 18+** — the middleware and proxy use
|
|
14
|
+
> [`Response.headers.getSetCookie()`](https://developer.mozilla.org/docs/Web/API/Headers/getSetCookie)
|
|
15
|
+
> to forward individual upstream `Set-Cookie` headers.
|
|
12
16
|
|
|
13
17
|
## Install
|
|
14
18
|
|
|
@@ -16,13 +20,11 @@ shape (`{ user, workspace, workspaces, sdk, tokens }`).
|
|
|
16
20
|
pnpm add @qelos/integrator-next @qelos/sdk
|
|
17
21
|
```
|
|
18
22
|
|
|
19
|
-
`next` (>=13.4) and `react` are peer dependencies.
|
|
20
|
-
and 15.x on both the App Router and the Pages Router.
|
|
23
|
+
`next` (>=13.4) and `react` are peer dependencies.
|
|
21
24
|
|
|
22
25
|
## Quick start
|
|
23
26
|
|
|
24
|
-
Set `QELOS_APP_URL
|
|
25
|
-
re-export the pre-built middleware and use the no-arg context loader:
|
|
27
|
+
Set `QELOS_APP_URL`, then wire Edge middleware and a catch-all API proxy route.
|
|
26
28
|
|
|
27
29
|
```ts
|
|
28
30
|
// middleware.ts
|
|
@@ -33,6 +35,16 @@ export const config = {
|
|
|
33
35
|
};
|
|
34
36
|
```
|
|
35
37
|
|
|
38
|
+
```ts
|
|
39
|
+
// app/api/[...qelos]/route.ts
|
|
40
|
+
import { createQelosApiProxyHandlers } from '@qelos/integrator-next/runtime/api-proxy';
|
|
41
|
+
|
|
42
|
+
const { runtime, GET, POST, PUT, PATCH, DELETE, OPTIONS } =
|
|
43
|
+
createQelosApiProxyHandlers({ appUrl: process.env.QELOS_APP_URL! });
|
|
44
|
+
|
|
45
|
+
export { runtime, GET, POST, PUT, PATCH, DELETE, OPTIONS };
|
|
46
|
+
```
|
|
47
|
+
|
|
36
48
|
```tsx
|
|
37
49
|
// app/dashboard/page.tsx
|
|
38
50
|
import { getQelosContext } from '@qelos/integrator-next/context';
|
|
@@ -41,86 +53,86 @@ export default async function Dashboard() {
|
|
|
41
53
|
const { user, sdk } = await getQelosContext();
|
|
42
54
|
if (!user) return <p>Please sign in.</p>;
|
|
43
55
|
const products = await sdk.entities('products').getList();
|
|
44
|
-
return
|
|
56
|
+
return (
|
|
57
|
+
<div>
|
|
58
|
+
Welcome {user.fullName}, you have {products.length} products
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
45
61
|
}
|
|
46
62
|
```
|
|
47
63
|
|
|
48
64
|
### Environment variables
|
|
49
65
|
|
|
50
66
|
| Variable | Default | Description |
|
|
51
|
-
|
|
52
|
-
| `QELOS_APP_URL` | — (required) | Qelos
|
|
53
|
-
| `QELOS_API_TOKEN` | — | Service-to-service token (skips
|
|
54
|
-
| `
|
|
55
|
-
| `
|
|
56
|
-
| `
|
|
57
|
-
| `QELOS_SKIP_PATHS` | — | Comma-separated path prefixes to bypass entirely |
|
|
67
|
+
|----------|---------|-------------|
|
|
68
|
+
| `QELOS_APP_URL` | — (required for default exports) | Qelos managed-app base URL |
|
|
69
|
+
| `QELOS_API_TOKEN` | — | Service-to-service token (skips cookie identity for anonymous resolution) |
|
|
70
|
+
| `QELOS_REQUIRE_AUTH` | `false` | `true` / `1` → anonymous requests get `401` from middleware |
|
|
71
|
+
| `QELOS_SKIP_PATHS` | — | Comma-separated path prefixes to bypass middleware |
|
|
72
|
+
| `QELOS_DISABLE_PROXY` | `false` | `true` when you are **not** using the catch-all `/api/**` proxy — skips auto `/api/` in middleware `skipPaths` |
|
|
58
73
|
|
|
59
|
-
|
|
74
|
+
Dev-time proxy target overrides (same resolution order as the proxy):
|
|
60
75
|
|
|
61
|
-
|
|
62
|
-
|
|
76
|
+
1. `NEXT_QELOS_PROXY_TARGET`
|
|
77
|
+
2. `QELOS_IP`
|
|
78
|
+
3. `QELOS_API_IP`
|
|
79
|
+
4. `appUrl` from config / `QELOS_APP_URL`
|
|
63
80
|
|
|
64
|
-
|
|
81
|
+
## BFF proxy (`/api/**`)
|
|
65
82
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
83
|
+
Register a catch-all App Router handler (see above) that exports the handlers
|
|
84
|
+
from `createQelosApiProxyHandlers`. Every request your app does not handle
|
|
85
|
+
itself is forwarded to the configured Qelos origin; the response body streams
|
|
86
|
+
back and upstream `Set-Cookie` headers are rewritten so `Domain=` matches the
|
|
87
|
+
inbound `Host` (port stripped).
|
|
69
88
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
appUrl: process.env.QELOS_APP_URL!,
|
|
73
|
-
skipPaths: ['/_next', '/favicon.ico'],
|
|
74
|
-
requireAuth: false,
|
|
75
|
-
},
|
|
76
|
-
resolveWorkspace: ({ req, workspaces }) => {
|
|
77
|
-
const slug = req.nextUrl.searchParams.get('workspace');
|
|
78
|
-
return workspaces.find((w) => (w as any).slug === slug) ?? workspaces[0] ?? null;
|
|
79
|
-
},
|
|
80
|
-
});
|
|
89
|
+
User-defined routes under `app/api/...` still take precedence over the catch-all
|
|
90
|
+
segment — the proxy only runs for paths nothing else matched.
|
|
81
91
|
|
|
82
|
-
|
|
83
|
-
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
|
|
84
|
-
};
|
|
85
|
-
```
|
|
92
|
+
### Opting out (`disableProxy`)
|
|
86
93
|
|
|
87
|
-
|
|
94
|
+
Set `disableProxy: true` on `QelosNextConfig` when you do not use the catch-all
|
|
95
|
+
proxy. Middleware will then **not** auto-prepend `/api/` to `skipPaths`, so you
|
|
96
|
+
can run your own `/api/*` handlers without the integrator skipping them for the
|
|
97
|
+
proxy.
|
|
88
98
|
|
|
89
|
-
|
|
90
|
-
`Authorization` header.
|
|
91
|
-
- Calls the Qelos SDK to resolve the user and the active workspace.
|
|
92
|
-
- Forwards the resolved ids to downstream handlers via the
|
|
93
|
-
`x-qelos-user-id` and `x-qelos-workspace-id` request headers.
|
|
94
|
-
- Refreshes the access/refresh token pair when needed and writes the new
|
|
95
|
-
cookies onto the outbound response.
|
|
96
|
-
- Returns `401` early when `config.requireAuth: true` and the user cannot be
|
|
97
|
-
resolved.
|
|
99
|
+
WebSocket upgrades are not proxied.
|
|
98
100
|
|
|
99
|
-
|
|
101
|
+
## Edge middleware
|
|
100
102
|
|
|
101
|
-
|
|
102
|
-
Next.js 14 (sync `cookies()` / `headers()`) and Next.js 15 (async). Wrap it
|
|
103
|
-
with React's `cache()` so re-renders within the same request reuse the result:
|
|
103
|
+
On each matched request, middleware:
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
1. Resolves the upstream origin (`NEXT_QELOS_PROXY_TARGET` → `QELOS_IP` →
|
|
106
|
+
`QELOS_API_IP` → `appUrl`). If neither that nor `apiToken` is configured and
|
|
107
|
+
`requireAuth` is false, identity headers are cleared and the request
|
|
108
|
+
continues anonymously.
|
|
109
|
+
2. When an upstream origin exists, issues `fetch('${origin}/api/me', { redirect:
|
|
110
|
+
'manual' })` with the inbound `Cookie` and `Authorization` headers forwarded.
|
|
111
|
+
3. Appends every upstream `Set-Cookie` to the outgoing response after rewriting
|
|
112
|
+
`Domain=` to the inbound `Host`.
|
|
113
|
+
4. On `200` JSON, uses that body as `IUser`, loads workspaces via the SDK, and
|
|
114
|
+
sets the active workspace from `user.workspace` (or your `resolveWorkspace`
|
|
115
|
+
callback) — **not** `workspaces[0]`.
|
|
116
|
+
5. Forwards `x-qelos-user-id` and `x-qelos-workspace-id` on the rewritten request
|
|
117
|
+
headers for downstream App Router code.
|
|
109
118
|
|
|
110
|
-
|
|
111
|
-
|
|
119
|
+
When the proxy is enabled (`disableProxy !== true`), `/api/` is prepended to
|
|
120
|
+
`skipPaths` so middleware does not run the `/api/me` probe on proxied API
|
|
121
|
+
traffic (avoids double upstream calls).
|
|
112
122
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
123
|
+
> Edge middleware runs in the Edge runtime. The `/api/me` round-trip uses the
|
|
124
|
+
> standard `fetch` API with `redirect: 'manual'`. In local dev over plain HTTP,
|
|
125
|
+
> browsers may drop upstream `Secure` cookies — use HTTPS locally or configure
|
|
126
|
+
> Qelos for non-`Secure` cookies in development.
|
|
116
127
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
128
|
+
## App Router server code
|
|
129
|
+
|
|
130
|
+
Wrap `getQelosContext()` with React `cache()` if you want one resolution per
|
|
131
|
+
request. It performs the same `/api/me` identification as middleware (with live
|
|
132
|
+
headers) and attempts to apply upstream `Set-Cookie` values through
|
|
133
|
+
`next/headers` `cookies().set` when the runtime allows it.
|
|
122
134
|
|
|
123
|
-
###
|
|
135
|
+
### Route handlers
|
|
124
136
|
|
|
125
137
|
```ts
|
|
126
138
|
// app/api/me/route.ts
|
|
@@ -133,26 +145,8 @@ export const GET = withQelosRoute((_req, _ctx, qelos) => {
|
|
|
133
145
|
});
|
|
134
146
|
```
|
|
135
147
|
|
|
136
|
-
Anywhere downstream — including modules outside the React tree — can read the
|
|
137
|
-
active context from AsyncLocalStorage via `getStoredQelosContext()`, provided
|
|
138
|
-
execution went through `withQelosRoute`, `withQelosContext`, or one of the
|
|
139
|
-
Pages Router wrappers.
|
|
140
|
-
|
|
141
|
-
```ts
|
|
142
|
-
// services/products.ts
|
|
143
|
-
import { getStoredQelosContext } from '@qelos/integrator-next/context';
|
|
144
|
-
|
|
145
|
-
export async function listProducts() {
|
|
146
|
-
const qelos = getStoredQelosContext();
|
|
147
|
-
if (!qelos?.sdk) throw new Error('no qelos context');
|
|
148
|
-
return qelos.sdk.entities('products').getList();
|
|
149
|
-
}
|
|
150
|
-
```
|
|
151
|
-
|
|
152
148
|
## Pages Router
|
|
153
149
|
|
|
154
|
-
### API routes
|
|
155
|
-
|
|
156
150
|
```ts
|
|
157
151
|
// pages/api/me.ts
|
|
158
152
|
import { withQelosApi } from '@qelos/integrator-next/pages';
|
|
@@ -165,92 +159,43 @@ export default withQelosApi(
|
|
|
165
159
|
);
|
|
166
160
|
```
|
|
167
161
|
|
|
168
|
-
### `getServerSideProps`
|
|
169
|
-
|
|
170
162
|
```ts
|
|
171
163
|
// pages/dashboard.tsx
|
|
172
164
|
import { withQelosSSR } from '@qelos/integrator-next/pages';
|
|
173
165
|
|
|
174
166
|
export const getServerSideProps = withQelosSSR(
|
|
175
|
-
async (_ctx, qelos) => ({
|
|
167
|
+
async (_ctx, qelos) => ({
|
|
168
|
+
props: { user: qelos.user, workspace: qelos.workspace },
|
|
169
|
+
}),
|
|
176
170
|
{ config: { appUrl: process.env.QELOS_APP_URL!, requireAuth: true } },
|
|
177
171
|
);
|
|
178
172
|
```
|
|
179
173
|
|
|
180
|
-
##
|
|
181
|
-
|
|
182
|
-
When a request arrives with an expired access token, the SDK's failed-auth
|
|
183
|
-
path triggers a refresh, in order:
|
|
184
|
-
|
|
185
|
-
1. If a refresh token is present, calls
|
|
186
|
-
`sdk.authentication.refreshToken()` (`POST /api/token/refresh`) — issues a
|
|
187
|
-
new access + refresh pair.
|
|
188
|
-
2. Otherwise, calls `sdk.authentication.refreshCookieToken()`
|
|
189
|
-
(`POST /api/cookie/refresh`) — used for cookie-only sessions that do not
|
|
190
|
-
carry a separate refresh token (e.g. social-auth flows).
|
|
191
|
-
|
|
192
|
-
The new pair is then written back to cookies using the configured
|
|
193
|
-
`accessTokenCookie` / `refreshTokenCookie` names — automatically in App
|
|
194
|
-
Router middleware (`response.cookies.set`), Pages Router API routes
|
|
195
|
-
(`Set-Cookie` header), and `getServerSideProps` responses.
|
|
196
|
-
|
|
197
|
-
App Router server components and route handlers cannot mutate cookies after
|
|
198
|
-
rendering starts, so the default refresh hook there is a no-op — the new
|
|
199
|
-
tokens are still applied to the in-memory pair so subsequent SDK calls in the
|
|
200
|
-
same request use the fresh access token. To persist the refreshed pair, hand
|
|
201
|
-
the request off to a route handler that calls `cookies().set(...)`.
|
|
202
|
-
|
|
203
|
-
Override the behaviour by passing `onTokenRefresh` to any of the wrappers.
|
|
204
|
-
The hook receives:
|
|
205
|
-
|
|
206
|
-
```ts
|
|
207
|
-
interface TokenRefreshContext<T> {
|
|
208
|
-
target: T; // NextResponse | NextApiResponse | ServerResponse | null
|
|
209
|
-
oldTokens: { accessToken?: string; refreshToken?: string };
|
|
210
|
-
newTokens: { accessToken: string; refreshToken?: string };
|
|
211
|
-
sdk: QelosSDK;
|
|
212
|
-
}
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
### Manual cookie refresh
|
|
216
|
-
|
|
217
|
-
Long-lived integrator-hosted sessions can also call the SDK directly to
|
|
218
|
-
proactively refresh the cookie token. From a route handler that owns the
|
|
219
|
-
response (so it can mutate cookies):
|
|
174
|
+
## Configuration (`QelosNextConfig`)
|
|
220
175
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
return NextResponse.json({ user: result.payload.user });
|
|
230
|
-
});
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
## Configuration
|
|
234
|
-
|
|
235
|
-
```ts
|
|
236
|
-
interface QelosNextConfig {
|
|
237
|
-
appUrl: string; // Qelos backend base URL
|
|
238
|
-
apiToken?: string; // service-to-service token (skips refresh logic)
|
|
239
|
-
accessTokenCookie?: string; // default 'q_access_token'
|
|
240
|
-
refreshTokenCookie?: string; // default 'q_refresh_token'
|
|
241
|
-
requireAuth?: boolean; // 401/redirect on anonymous (default false)
|
|
242
|
-
skipPaths?: string[]; // path prefixes to bypass entirely
|
|
243
|
-
sdkOptions?: Partial<QelosSDKOptions>;
|
|
244
|
-
}
|
|
245
|
-
```
|
|
176
|
+
| Field | Description |
|
|
177
|
+
|-------|-------------|
|
|
178
|
+
| `appUrl` | Qelos managed-app base URL |
|
|
179
|
+
| `apiToken` | Static token — no `/api/me` cookie identity |
|
|
180
|
+
| `requireAuth` | Reject anonymous requests (`401` / redirect) |
|
|
181
|
+
| `skipPaths` | Path prefixes to bypass middleware (and `/api/` is auto-prepended when the proxy is enabled) |
|
|
182
|
+
| `disableProxy` | Opt out of auto `/api/` skip for middleware |
|
|
183
|
+
| `sdkOptions` | Extra `QelosSDK` constructor options |
|
|
246
184
|
|
|
247
185
|
## Subpath exports
|
|
248
186
|
|
|
249
187
|
| Import | Purpose |
|
|
250
|
-
|
|
251
|
-
| `@qelos/integrator-next` |
|
|
252
|
-
| `@qelos/integrator-next/middleware` | Edge middleware
|
|
253
|
-
| `@qelos/integrator-next/context` |
|
|
254
|
-
| `@qelos/integrator-next/route` |
|
|
255
|
-
| `@qelos/integrator-next/pages` | Pages Router wrappers
|
|
256
|
-
| `@qelos/integrator-next/
|
|
188
|
+
|--------|---------|
|
|
189
|
+
| `@qelos/integrator-next` | Barrel |
|
|
190
|
+
| `@qelos/integrator-next/middleware` | Edge middleware |
|
|
191
|
+
| `@qelos/integrator-next/context` | `getQelosContext`, AsyncLocalStorage helpers |
|
|
192
|
+
| `@qelos/integrator-next/route` | `withQelosRoute` |
|
|
193
|
+
| `@qelos/integrator-next/pages` | Pages Router wrappers |
|
|
194
|
+
| `@qelos/integrator-next/runtime/api-proxy` | Catch-all proxy handlers (`runtime = 'nodejs'`) |
|
|
195
|
+
| `@qelos/integrator-next/types` | Types and header name constants |
|
|
196
|
+
|
|
197
|
+
## Social auth
|
|
198
|
+
|
|
199
|
+
`completeSocialAuthCallback` and the re-exported SDK helpers forward Qelos
|
|
200
|
+
session cookies from `socialCallback` onto your Node response — use them from
|
|
201
|
+
a route handler that owns the response object.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qelos/integrator-next",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.1",
|
|
4
4
|
"description": "Qelos integrator middleware for Next.js (App Router and Pages Router): identifies the user and workspace via the Qelos SDK before the application's own handler runs.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -42,6 +42,12 @@
|
|
|
42
42
|
"require": "./dist/route.js",
|
|
43
43
|
"default": "./dist/route.js"
|
|
44
44
|
},
|
|
45
|
+
"./runtime/api-proxy": {
|
|
46
|
+
"types": "./dist/runtime/api-proxy.d.ts",
|
|
47
|
+
"import": "./dist/runtime/api-proxy.js",
|
|
48
|
+
"require": "./dist/runtime/api-proxy.js",
|
|
49
|
+
"default": "./dist/runtime/api-proxy.js"
|
|
50
|
+
},
|
|
45
51
|
"./types": {
|
|
46
52
|
"types": "./dist/types.d.ts",
|
|
47
53
|
"import": "./dist/types.js",
|
|
@@ -64,8 +70,7 @@
|
|
|
64
70
|
}
|
|
65
71
|
},
|
|
66
72
|
"dependencies": {
|
|
67
|
-
"@qelos/
|
|
68
|
-
"@qelos/sdk": "^4.0.0"
|
|
73
|
+
"@qelos/sdk": "4.0.0"
|
|
69
74
|
},
|
|
70
75
|
"devDependencies": {
|
|
71
76
|
"@types/node": "^22.5.4",
|