@zenmanage/nextjs 0.0.0-stage → 1.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/LICENSE +21 -0
- package/README.md +329 -2
- package/dist/chunk-HDORXS72.js +109 -0
- package/dist/chunk-HDORXS72.js.map +1 -0
- package/dist/chunk-PHODKVFM.js +238 -0
- package/dist/chunk-PHODKVFM.js.map +1 -0
- package/dist/client.cjs +287 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +38 -0
- package/dist/client.d.ts +38 -0
- package/dist/client.js +263 -0
- package/dist/client.js.map +1 -0
- package/dist/index.cjs +378 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +24 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware.cjs +199 -0
- package/dist/middleware.cjs.map +1 -0
- package/dist/middleware.d.cts +16 -0
- package/dist/middleware.d.ts +16 -0
- package/dist/middleware.js +10 -0
- package/dist/middleware.js.map +1 -0
- package/dist/server.cjs +183 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +35 -0
- package/dist/server.d.ts +35 -0
- package/dist/server.js +25 -0
- package/dist/server.js.map +1 -0
- package/dist/types-CFBzBNpk.d.cts +103 -0
- package/dist/types-CFBzBNpk.d.ts +103 -0
- package/package.json +137 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zenmanage
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,330 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Zenmanage Next.js SDK
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@zenmanage/nextjs)
|
|
4
|
+
[](https://github.com/zenmanage/zenmanage-nextjs/actions/workflows/ci.yml)
|
|
5
|
+
[](https://app.codacy.com/gh/zenmanage/zenmanage-nextjs/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
|
|
8
|
+
Feature flags for Next.js. Evaluate flags in Server Components, route handlers, `getServerSideProps` and middleware, and hand the results to Client Components so the first paint is already right.
|
|
9
|
+
|
|
10
|
+
It builds on two packages you install alongside it:
|
|
11
|
+
|
|
12
|
+
- [`@zenmanage/sdk`](https://www.npmjs.com/package/@zenmanage/sdk) evaluates flags on the server and at the edge.
|
|
13
|
+
- [`@zenmanage/react`](https://www.npmjs.com/package/@zenmanage/react) provides the provider and hooks for Client Components.
|
|
14
|
+
|
|
15
|
+
## What you get
|
|
16
|
+
|
|
17
|
+
- Server helpers for the App Router and the Pages Router.
|
|
18
|
+
- A bootstrap payload that carries server-evaluated flags to the browser. The server render and the first client render match, so there is no hydration mismatch and no flash of the default.
|
|
19
|
+
- Middleware helpers that build an evaluation context from the request. They run on the Edge runtime and on Node.js.
|
|
20
|
+
- Rules cached in memory and shared across calls, so a page with ten flags makes one fetch, not ten.
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install @zenmanage/nextjs @zenmanage/react @zenmanage/sdk
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Requires Next.js 13.4 or newer (up to 16), React 18.2 or 19, `@zenmanage/react` 1.0 or newer and `@zenmanage/sdk` 3.5 or newer. Tested against Next.js 13.4, 14, 15 and 16, on both routers.
|
|
29
|
+
|
|
30
|
+
## Keys
|
|
31
|
+
|
|
32
|
+
Two keys, and they never swap places:
|
|
33
|
+
|
|
34
|
+
- **Server key (`srv_...`)** for Server Components, route handlers, `getServerSideProps` and middleware. Keep it in a server-only environment variable such as `ZENMANAGE_ENVIRONMENT_TOKEN`.
|
|
35
|
+
- **Client key (`cli_...`)** for `NextFlagsProvider` in the browser. It is safe to expose, for example as `NEXT_PUBLIC_ZENMANAGE_ENVIRONMENT_TOKEN`.
|
|
36
|
+
|
|
37
|
+
Mobile keys (`mob_...`) are not valid here. `NextFlagsProvider` throws if you give it a server key, because anything you pass to a Client Component ends up in the page.
|
|
38
|
+
|
|
39
|
+
## Quick start
|
|
40
|
+
|
|
41
|
+
A Server Component that reads a flag:
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { Context } from '@zenmanage/sdk';
|
|
45
|
+
import { getServerFlag } from '@zenmanage/nextjs';
|
|
46
|
+
|
|
47
|
+
export default async function DashboardPage() {
|
|
48
|
+
const flag = await getServerFlag({
|
|
49
|
+
environmentToken: process.env.ZENMANAGE_ENVIRONMENT_TOKEN!,
|
|
50
|
+
key: 'dashboard-v2',
|
|
51
|
+
defaultValue: false,
|
|
52
|
+
context: Context.single('user', 'user-123', 'Jane'),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return <main>{flag.asBool() ? <NewDashboard /> : <LegacyDashboard />}</main>;
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
If Zenmanage can't be reached, you get `defaultValue`, not an error.
|
|
60
|
+
|
|
61
|
+
## Entry points
|
|
62
|
+
|
|
63
|
+
| Import | Runs in | Contains |
|
|
64
|
+
| ------------------------------ | ----------------- | --------------------------------------------------- |
|
|
65
|
+
| `@zenmanage/nextjs` | Server, Edge | Everything below except the client entry point |
|
|
66
|
+
| `@zenmanage/nextjs/server` | Server, Edge | Server helpers |
|
|
67
|
+
| `@zenmanage/nextjs/middleware` | Edge, Node.js | `buildContextFromRequest`, `evaluateMiddlewareFlag` |
|
|
68
|
+
| `@zenmanage/nextjs/client` | Client Components | `NextFlagsProvider`, `useFlag`, `useVariant` |
|
|
69
|
+
|
|
70
|
+
`@zenmanage/nextjs/client` starts with `'use client'`, so you can import it from a Server Component file without adding the directive yourself.
|
|
71
|
+
|
|
72
|
+
## Server helpers
|
|
73
|
+
|
|
74
|
+
All of them take your server key as `environmentToken`, plus the optional `apiEndpoint`, `cacheTtl`, `enableUsageReporting` and `logger`.
|
|
75
|
+
|
|
76
|
+
| Function | Returns | Use it to |
|
|
77
|
+
| ------------------------------- | ------------------ | ---------------------------------------------------------------------------------------- |
|
|
78
|
+
| `getServerFlag(input)` | `Flag` | Read one flag. Pass `key`, `defaultValue`, `context`, `defaults`. |
|
|
79
|
+
| `getFlagValue(input, fallback)` | the flag's value | Read one flag as a `boolean`, `number`, `string` or JSON value, typed after `fallback`. |
|
|
80
|
+
| `getServerFlags(input)` | `Flag[]` | Read every flag. |
|
|
81
|
+
| `getBootstrapPayload(input)` | `BootstrapPayload` | Evaluate flags for a client provider. See [Bootstrap](#bootstrapping-client-components). |
|
|
82
|
+
| `createServerManager(input)` | `FlagManager` | Evaluate several flags against one context. |
|
|
83
|
+
| `preloadServerFlags(input)` | `FlagManager` | Load the rules up front and keep working with the manager. |
|
|
84
|
+
| `createServerClient(options)` | `Zenmanage` | Get the shared SDK client. See the note below. |
|
|
85
|
+
| `clearServerClients()` | `void` | Drop every shared client and its cache. Useful in tests. |
|
|
86
|
+
|
|
87
|
+
`getFlagValue` returns `fallback` for a flag that doesn't exist, or when the API can't be reached. A `defaultValue` in the input, or an entry for the key in `defaults`, wins over it.
|
|
88
|
+
|
|
89
|
+
### Caching
|
|
90
|
+
|
|
91
|
+
Clients are shared per configuration (token, endpoint, TTL, usage setting and logger) for the life of the server process or Edge isolate. Rules are cached for 60 seconds unless you set `cacheTtl`. After that the next call fetches them again, so a flag change reaches your server within a minute.
|
|
92
|
+
|
|
93
|
+
Prefer the helpers above to calling `createServerClient(...).flags()` yourself. A `FlagManager` loads its rules once and keeps them, whatever the TTL. The helpers hand you a fresh manager on every call, so the TTL applies.
|
|
94
|
+
|
|
95
|
+
### Context
|
|
96
|
+
|
|
97
|
+
Pass a `Context` from `@zenmanage/sdk` to target by user or attribute:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { Attribute, Context } from '@zenmanage/sdk';
|
|
101
|
+
|
|
102
|
+
const context = new Context('user', 'Jane Doe', 'user-123', [
|
|
103
|
+
new Attribute('country', ['US']),
|
|
104
|
+
new Attribute('plan', ['pro']),
|
|
105
|
+
]);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
A percentage rollout buckets by the context's identifier, so use a stable one.
|
|
109
|
+
|
|
110
|
+
## Bootstrapping Client Components
|
|
111
|
+
|
|
112
|
+
Client Components render on the server too, and they can't read the server key. So the server evaluates the flags, and the provider shows those values until the browser has its own.
|
|
113
|
+
|
|
114
|
+
The pieces:
|
|
115
|
+
|
|
116
|
+
1. `getBootstrapPayload` evaluates flags on the server and returns plain JSON.
|
|
117
|
+
2. You pass that payload to `NextFlagsProvider` as a prop.
|
|
118
|
+
3. `useFlag` from `@zenmanage/nextjs/client` returns the server's value on the server render and on the first client render. Because both see the same value, React hydrates cleanly.
|
|
119
|
+
4. The browser loads its own rules. When they arrive, the live value replaces the server's. If they never arrive (a blocked CDN, no network), the server's value stays. The page doesn't switch to the code default.
|
|
120
|
+
|
|
121
|
+
### App Router
|
|
122
|
+
|
|
123
|
+
Evaluate in a Server Component and pass the payload down:
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
// app/layout.tsx
|
|
127
|
+
import { Context } from '@zenmanage/sdk';
|
|
128
|
+
import { getBootstrapPayload } from '@zenmanage/nextjs';
|
|
129
|
+
import { Providers } from './providers';
|
|
130
|
+
|
|
131
|
+
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
|
132
|
+
const bootstrapPayload = await getBootstrapPayload({
|
|
133
|
+
environmentToken: process.env.ZENMANAGE_ENVIRONMENT_TOKEN!,
|
|
134
|
+
context: Context.single('user', 'user-123'),
|
|
135
|
+
keys: ['new-checkout'],
|
|
136
|
+
includeContext: true,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<html lang="en">
|
|
141
|
+
<body>
|
|
142
|
+
<Providers bootstrapPayload={bootstrapPayload}>{children}</Providers>
|
|
143
|
+
</body>
|
|
144
|
+
</html>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
// app/providers.tsx
|
|
151
|
+
'use client';
|
|
152
|
+
|
|
153
|
+
import type { BootstrapPayload } from '@zenmanage/nextjs';
|
|
154
|
+
import { NextFlagsProvider } from '@zenmanage/nextjs/client';
|
|
155
|
+
|
|
156
|
+
export function Providers({
|
|
157
|
+
bootstrapPayload,
|
|
158
|
+
children,
|
|
159
|
+
}: {
|
|
160
|
+
bootstrapPayload: BootstrapPayload;
|
|
161
|
+
children: React.ReactNode;
|
|
162
|
+
}) {
|
|
163
|
+
return (
|
|
164
|
+
<NextFlagsProvider
|
|
165
|
+
environmentToken={process.env.NEXT_PUBLIC_ZENMANAGE_ENVIRONMENT_TOKEN!}
|
|
166
|
+
bootstrapPayload={bootstrapPayload}
|
|
167
|
+
>
|
|
168
|
+
{children}
|
|
169
|
+
</NextFlagsProvider>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
// app/checkout-button.tsx
|
|
176
|
+
'use client';
|
|
177
|
+
|
|
178
|
+
import { useFlag } from '@zenmanage/nextjs/client';
|
|
179
|
+
|
|
180
|
+
export function CheckoutButton() {
|
|
181
|
+
const { value: enabled } = useFlag('new-checkout', false);
|
|
182
|
+
|
|
183
|
+
return <button>{enabled ? 'Checkout (new)' : 'Checkout'}</button>;
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Pages Router
|
|
188
|
+
|
|
189
|
+
Evaluate in `getServerSideProps` and pass the payload through props. It is plain JSON, so Next.js serializes it as is:
|
|
190
|
+
|
|
191
|
+
```tsx
|
|
192
|
+
// pages/checkout.tsx
|
|
193
|
+
import type { GetServerSideProps } from 'next';
|
|
194
|
+
import { Context } from '@zenmanage/sdk';
|
|
195
|
+
import { getBootstrapPayload, type BootstrapPayload } from '@zenmanage/nextjs';
|
|
196
|
+
import { NextFlagsProvider, useFlag } from '@zenmanage/nextjs/client';
|
|
197
|
+
|
|
198
|
+
export const getServerSideProps: GetServerSideProps<{
|
|
199
|
+
bootstrapPayload: BootstrapPayload;
|
|
200
|
+
}> = async () => ({
|
|
201
|
+
props: {
|
|
202
|
+
bootstrapPayload: await getBootstrapPayload({
|
|
203
|
+
environmentToken: process.env.ZENMANAGE_ENVIRONMENT_TOKEN!,
|
|
204
|
+
context: Context.single('user', 'user-123'),
|
|
205
|
+
keys: ['new-checkout'],
|
|
206
|
+
includeContext: true,
|
|
207
|
+
}),
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
function Checkout() {
|
|
212
|
+
const { value: enabled } = useFlag('new-checkout', false);
|
|
213
|
+
return <button>{enabled ? 'Checkout (new)' : 'Checkout'}</button>;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export default function CheckoutPage({ bootstrapPayload }: { bootstrapPayload: BootstrapPayload }) {
|
|
217
|
+
return (
|
|
218
|
+
<NextFlagsProvider
|
|
219
|
+
environmentToken={process.env.NEXT_PUBLIC_ZENMANAGE_ENVIRONMENT_TOKEN!}
|
|
220
|
+
bootstrapPayload={bootstrapPayload}
|
|
221
|
+
>
|
|
222
|
+
<Checkout />
|
|
223
|
+
</NextFlagsProvider>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### What goes to the browser
|
|
229
|
+
|
|
230
|
+
The payload is written into the page, so anyone can read it.
|
|
231
|
+
|
|
232
|
+
- **`keys`** limits it to the flags your Client Components read. Leave it out and every flag in the environment is sent, names and values included.
|
|
233
|
+
- **`includeContext: true`** adds the context to the payload, so the browser evaluates for the same user the server did. Leave it out if the context holds anything you don't want in the page source. Without it, the browser evaluates with no context unless you pass `context` to the provider.
|
|
234
|
+
|
|
235
|
+
### `NextFlagsProvider`
|
|
236
|
+
|
|
237
|
+
Takes everything `FlagsProvider` from `@zenmanage/react` takes (`environmentToken`, `apiEndpoint`, `cacheTtl`, `enableUsageReporting`, `context`, `defaults`, `preload`, `onError`, `client`), plus:
|
|
238
|
+
|
|
239
|
+
| Prop | Use |
|
|
240
|
+
| -------------------- | ----------------------------------------------------------------------------------------------- |
|
|
241
|
+
| `bootstrapPayload` | The payload from `getBootstrapPayload`. Pass it as a prop. |
|
|
242
|
+
| `bootstrapElementId` | Read the payload from a `<script id="...">` tag instead. Defaults to `__ZENMANAGE_BOOTSTRAP__`. |
|
|
243
|
+
|
|
244
|
+
The browser reports itself to the API as `zenmanage-nextjs/<version>`.
|
|
245
|
+
|
|
246
|
+
`useFlag` and `useVariant` from `@zenmanage/nextjs/client` take the same arguments as the ones in `@zenmanage/react`. With a bootstrap value for the key they return it, with `isLoading: false`, until the live value arrives. Without one they behave the same as the React hooks.
|
|
247
|
+
|
|
248
|
+
`FlagGate` and `withFlag` come from `@zenmanage/react` and work inside `NextFlagsProvider`, but they don't use the bootstrap. They wait for the browser's own value.
|
|
249
|
+
|
|
250
|
+
### Script tag
|
|
251
|
+
|
|
252
|
+
If you can't pass props to the provider, render the payload as a JSON `<script>` tag and the provider picks it up:
|
|
253
|
+
|
|
254
|
+
```tsx
|
|
255
|
+
import { buildBootstrapPayload, buildBootstrapScript } from '@zenmanage/nextjs';
|
|
256
|
+
|
|
257
|
+
const payload = buildBootstrapPayload(await getServerFlags({ environmentToken, context }));
|
|
258
|
+
const html = buildBootstrapScript(payload); // <script id="__ZENMANAGE_BOOTSTRAP__" type="application/json">…</script>
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
The server renders without the payload, and the browser applies it right after hydration, so the first paint shows the default. Use the `bootstrapPayload` prop when you can.
|
|
262
|
+
|
|
263
|
+
The other bootstrap helpers: `buildBootstrapPayload`, `serializeBootstrapPayload`, `deserializeBootstrapPayload`, `readBootstrapPayloadFromDocument`, `defaultsFromBootstrapPayload`, `defaultsFromBootstrapDocument` and `contextFromBootstrapPayload`.
|
|
264
|
+
|
|
265
|
+
## Middleware
|
|
266
|
+
|
|
267
|
+
`evaluateMiddlewareFlag` builds a context from the request and evaluates one flag:
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
// middleware.ts
|
|
271
|
+
import { NextResponse } from 'next/server';
|
|
272
|
+
import type { NextRequest } from 'next/server';
|
|
273
|
+
import { evaluateMiddlewareFlag } from '@zenmanage/nextjs/middleware';
|
|
274
|
+
|
|
275
|
+
export async function middleware(request: NextRequest) {
|
|
276
|
+
const beta = await evaluateMiddlewareFlag({
|
|
277
|
+
environmentToken: process.env.ZENMANAGE_ENVIRONMENT_TOKEN!,
|
|
278
|
+
key: 'beta-rewrite',
|
|
279
|
+
defaultValue: false,
|
|
280
|
+
request,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
return beta.asBool() ? NextResponse.rewrite(new URL('/beta', request.url)) : NextResponse.next();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export const config = { matcher: ['/dashboard/:path*'] };
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
It works in `middleware.ts` on the Edge runtime (Next.js 13.4 and up) and in `proxy.ts` on Node.js (Next.js 16). Use a server key in both.
|
|
290
|
+
|
|
291
|
+
`buildContextFromRequest(request, options)` is what builds the context. It sets:
|
|
292
|
+
|
|
293
|
+
| Attribute | From |
|
|
294
|
+
| --------------------------- | ----------------------------------------------------------------------------------- |
|
|
295
|
+
| identifier | the `zenmanage_user_id` cookie, then the `x-zenmanage-user-id` header |
|
|
296
|
+
| `path` | the request path |
|
|
297
|
+
| `user-agent` | the `user-agent` header |
|
|
298
|
+
| `country`, `region`, `city` | `request.geo`, then the `x-vercel-ip-*` headers (country also from `cf-ipcountry`) |
|
|
299
|
+
| `ip` | only with `includeIp: true`: `request.ip`, then `x-forwarded-for`, then `x-real-ip` |
|
|
300
|
+
|
|
301
|
+
Set a different context type or name, change the cookie or header names, turn geo off, or add attributes with `contextType`, `contextName`, `identifierCookieName`, `identifierHeaderName`, `includeGeo` and `additionalAttributes`. Next.js 15 removed `request.ip` and `request.geo`, so the headers are what you get there.
|
|
302
|
+
|
|
303
|
+
An IP address is personal data, which is why it's off by default.
|
|
304
|
+
|
|
305
|
+
Next.js may print a build warning that `@zenmanage/sdk` uses `process.versions`, which the Edge runtime doesn't support. The SDK checks it exists before reading it, so it works. The warning is cosmetic.
|
|
306
|
+
|
|
307
|
+
## Examples
|
|
308
|
+
|
|
309
|
+
See [examples/README.md](examples/README.md). They cover Server Components, route handlers, `getServerSideProps`, the client provider and middleware, plus the usual targeting, defaults and rollout cases.
|
|
310
|
+
|
|
311
|
+
`examples/smoke-app` is a real Next.js app that `npm run test:smoke` builds from the packed package and runs against a mock Zenmanage API.
|
|
312
|
+
|
|
313
|
+
## Development
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
npm install
|
|
317
|
+
npm run validate
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Useful commands:
|
|
321
|
+
|
|
322
|
+
- `npm run lint`
|
|
323
|
+
- `npm run type-check`
|
|
324
|
+
- `npm run test:coverage`
|
|
325
|
+
- `npm run build`
|
|
326
|
+
- `npm run test:smoke` builds the package and checks it in a real Next.js app. Set `SMOKE_NEXT_VERSION` (for example `14`) to try another Next.js version, or `SMOKE_SERVE=1` to keep the app running so you can open it in a browser. Needs `openssl`.
|
|
327
|
+
|
|
328
|
+
## License
|
|
329
|
+
|
|
330
|
+
MIT
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getServerFlag
|
|
3
|
+
} from "./chunk-PHODKVFM.js";
|
|
4
|
+
|
|
5
|
+
// src/middleware.ts
|
|
6
|
+
import { Attribute, Context } from "@zenmanage/sdk";
|
|
7
|
+
function getCookieValue(cookies, name) {
|
|
8
|
+
if (!cookies) {
|
|
9
|
+
return void 0;
|
|
10
|
+
}
|
|
11
|
+
const value = cookies.get(name);
|
|
12
|
+
if (typeof value === "string") {
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
return value?.value;
|
|
16
|
+
}
|
|
17
|
+
function getRequestPath(request) {
|
|
18
|
+
if (request.nextUrl?.pathname) {
|
|
19
|
+
return request.nextUrl.pathname;
|
|
20
|
+
}
|
|
21
|
+
if (request.url) {
|
|
22
|
+
return new URL(request.url).pathname;
|
|
23
|
+
}
|
|
24
|
+
return "/";
|
|
25
|
+
}
|
|
26
|
+
function decodeHeader(value) {
|
|
27
|
+
if (!value) {
|
|
28
|
+
return void 0;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
return decodeURIComponent(value);
|
|
32
|
+
} catch {
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function getIp(request) {
|
|
37
|
+
if (request.ip) {
|
|
38
|
+
return request.ip;
|
|
39
|
+
}
|
|
40
|
+
const forwarded = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim();
|
|
41
|
+
return forwarded || request.headers.get("x-real-ip") || void 0;
|
|
42
|
+
}
|
|
43
|
+
function getGeo(request) {
|
|
44
|
+
const { headers, geo } = request;
|
|
45
|
+
return {
|
|
46
|
+
country: geo?.country || headers.get("x-vercel-ip-country") || headers.get("cf-ipcountry") || void 0,
|
|
47
|
+
region: geo?.region || headers.get("x-vercel-ip-country-region") || void 0,
|
|
48
|
+
city: geo?.city || decodeHeader(headers.get("x-vercel-ip-city"))
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function buildContextFromRequest(request, options = {}) {
|
|
52
|
+
const identifierCookieName = options.identifierCookieName ?? "zenmanage_user_id";
|
|
53
|
+
const identifierHeaderName = options.identifierHeaderName ?? "x-zenmanage-user-id";
|
|
54
|
+
const includeGeo = options.includeGeo ?? true;
|
|
55
|
+
const includeIp = options.includeIp ?? false;
|
|
56
|
+
const identifier = getCookieValue(request.cookies, identifierCookieName) ?? request.headers.get(identifierHeaderName) ?? void 0;
|
|
57
|
+
const context = new Context(options.contextType ?? "user", options.contextName, identifier);
|
|
58
|
+
const userAgent = request.headers.get("user-agent");
|
|
59
|
+
if (userAgent) {
|
|
60
|
+
context.addAttribute(new Attribute("user-agent", [userAgent]));
|
|
61
|
+
}
|
|
62
|
+
context.addAttribute(new Attribute("path", [getRequestPath(request)]));
|
|
63
|
+
const ip = includeIp ? getIp(request) : void 0;
|
|
64
|
+
if (ip) {
|
|
65
|
+
context.addAttribute(new Attribute("ip", [ip]));
|
|
66
|
+
}
|
|
67
|
+
if (includeGeo) {
|
|
68
|
+
const geo = getGeo(request);
|
|
69
|
+
if (geo.country) {
|
|
70
|
+
context.addAttribute(new Attribute("country", [geo.country]));
|
|
71
|
+
}
|
|
72
|
+
if (geo.region) {
|
|
73
|
+
context.addAttribute(new Attribute("region", [geo.region]));
|
|
74
|
+
}
|
|
75
|
+
if (geo.city) {
|
|
76
|
+
context.addAttribute(new Attribute("city", [geo.city]));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (options.additionalAttributes) {
|
|
80
|
+
for (const [key, value] of Object.entries(options.additionalAttributes)) {
|
|
81
|
+
if (!value) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const values = Array.isArray(value) ? value : [value];
|
|
85
|
+
context.addAttribute(new Attribute(key, values));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return context;
|
|
89
|
+
}
|
|
90
|
+
async function evaluateMiddlewareFlag(input) {
|
|
91
|
+
const context = buildContextFromRequest(input.request, input.requestContext);
|
|
92
|
+
return getServerFlag({
|
|
93
|
+
environmentToken: input.environmentToken,
|
|
94
|
+
apiEndpoint: input.apiEndpoint,
|
|
95
|
+
cacheTtl: input.cacheTtl,
|
|
96
|
+
enableUsageReporting: input.enableUsageReporting,
|
|
97
|
+
logger: input.logger,
|
|
98
|
+
key: input.key,
|
|
99
|
+
defaultValue: input.defaultValue,
|
|
100
|
+
defaults: input.defaults,
|
|
101
|
+
context
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export {
|
|
106
|
+
buildContextFromRequest,
|
|
107
|
+
evaluateMiddlewareFlag
|
|
108
|
+
};
|
|
109
|
+
//# sourceMappingURL=chunk-HDORXS72.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/middleware.ts"],"sourcesContent":["import { Attribute, Context, type Flag } from '@zenmanage/sdk';\nimport { getServerFlag } from './server';\nimport type {\n BuildRequestContextOptions,\n MiddlewareFlagInput,\n RequestCookiesLike,\n RequestLike,\n} from './types';\n\nfunction getCookieValue(cookies: RequestCookiesLike | undefined, name: string): string | undefined {\n if (!cookies) {\n return undefined;\n }\n\n const value = cookies.get(name);\n if (typeof value === 'string') {\n return value;\n }\n\n return value?.value;\n}\n\nfunction getRequestPath(request: RequestLike): string {\n if (request.nextUrl?.pathname) {\n return request.nextUrl.pathname;\n }\n\n if (request.url) {\n return new URL(request.url).pathname;\n }\n\n return '/';\n}\n\nfunction decodeHeader(value: string | null): string | undefined {\n if (!value) {\n return undefined;\n }\n\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n}\n\n// Next.js 15 removed `request.ip` and `request.geo`, so read what the platform forwards.\nfunction getIp(request: RequestLike): string | undefined {\n if (request.ip) {\n return request.ip;\n }\n\n const forwarded = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim();\n return forwarded || request.headers.get('x-real-ip') || undefined;\n}\n\nfunction getGeo(request: RequestLike): { country?: string; region?: string; city?: string } {\n const { headers, geo } = request;\n\n // Next.js 14 and earlier set `request.geo` even when nothing fills it in (it is `{}` when\n // self-hosted), so each field falls back to the headers on its own.\n return {\n country:\n geo?.country ||\n headers.get('x-vercel-ip-country') ||\n headers.get('cf-ipcountry') ||\n undefined,\n region: geo?.region || headers.get('x-vercel-ip-country-region') || undefined,\n city: geo?.city || decodeHeader(headers.get('x-vercel-ip-city')),\n };\n}\n\n/**\n * Builds an evaluation context from a request: an identifier (cookie, then header), the path,\n * the user agent and, when the platform reports it, the visitor's location. Works with\n * `NextRequest` and with a plain `Request`.\n */\nexport function buildContextFromRequest(\n request: RequestLike,\n options: BuildRequestContextOptions = {}\n): Context {\n const identifierCookieName = options.identifierCookieName ?? 'zenmanage_user_id';\n const identifierHeaderName = options.identifierHeaderName ?? 'x-zenmanage-user-id';\n const includeGeo = options.includeGeo ?? true;\n const includeIp = options.includeIp ?? false;\n\n const identifier =\n getCookieValue(request.cookies, identifierCookieName) ??\n request.headers.get(identifierHeaderName) ??\n undefined;\n\n const context = new Context(options.contextType ?? 'user', options.contextName, identifier);\n\n const userAgent = request.headers.get('user-agent');\n if (userAgent) {\n context.addAttribute(new Attribute('user-agent', [userAgent]));\n }\n\n context.addAttribute(new Attribute('path', [getRequestPath(request)]));\n\n const ip = includeIp ? getIp(request) : undefined;\n if (ip) {\n context.addAttribute(new Attribute('ip', [ip]));\n }\n\n if (includeGeo) {\n const geo = getGeo(request);\n\n if (geo.country) {\n context.addAttribute(new Attribute('country', [geo.country]));\n }\n\n if (geo.region) {\n context.addAttribute(new Attribute('region', [geo.region]));\n }\n\n if (geo.city) {\n context.addAttribute(new Attribute('city', [geo.city]));\n }\n }\n\n if (options.additionalAttributes) {\n for (const [key, value] of Object.entries(options.additionalAttributes)) {\n if (!value) {\n continue;\n }\n\n const values = Array.isArray(value) ? value : [value];\n context.addAttribute(new Attribute(key, values));\n }\n }\n\n return context;\n}\n\n/**\n * Evaluates one flag for the request. Runs on the Edge runtime (`middleware.ts`) and on the\n * Node.js runtime (`proxy.ts` in Next.js 16), and needs a server key in both.\n */\nexport async function evaluateMiddlewareFlag(input: MiddlewareFlagInput): Promise<Flag> {\n const context = buildContextFromRequest(input.request, input.requestContext);\n\n return getServerFlag({\n environmentToken: input.environmentToken,\n apiEndpoint: input.apiEndpoint,\n cacheTtl: input.cacheTtl,\n enableUsageReporting: input.enableUsageReporting,\n logger: input.logger,\n key: input.key,\n defaultValue: input.defaultValue,\n defaults: input.defaults,\n context,\n });\n}\n"],"mappings":";;;;;AAAA,SAAS,WAAW,eAA0B;AAS9C,SAAS,eAAe,SAAyC,MAAkC;AACjG,MAAI,CAAC,SAAS;AACZ,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ,QAAQ,IAAI,IAAI;AAC9B,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO;AAAA,EACT;AAEA,SAAO,OAAO;AAChB;AAEA,SAAS,eAAe,SAA8B;AACpD,MAAI,QAAQ,SAAS,UAAU;AAC7B,WAAO,QAAQ,QAAQ;AAAA,EACzB;AAEA,MAAI,QAAQ,KAAK;AACf,WAAO,IAAI,IAAI,QAAQ,GAAG,EAAE;AAAA,EAC9B;AAEA,SAAO;AACT;AAEA,SAAS,aAAa,OAA0C;AAC9D,MAAI,CAAC,OAAO;AACV,WAAO;AAAA,EACT;AAEA,MAAI;AACF,WAAO,mBAAmB,KAAK;AAAA,EACjC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,SAAS,MAAM,SAA0C;AACvD,MAAI,QAAQ,IAAI;AACd,WAAO,QAAQ;AAAA,EACjB;AAEA,QAAM,YAAY,QAAQ,QAAQ,IAAI,iBAAiB,GAAG,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK;AAC9E,SAAO,aAAa,QAAQ,QAAQ,IAAI,WAAW,KAAK;AAC1D;AAEA,SAAS,OAAO,SAA4E;AAC1F,QAAM,EAAE,SAAS,IAAI,IAAI;AAIzB,SAAO;AAAA,IACL,SACE,KAAK,WACL,QAAQ,IAAI,qBAAqB,KACjC,QAAQ,IAAI,cAAc,KAC1B;AAAA,IACF,QAAQ,KAAK,UAAU,QAAQ,IAAI,4BAA4B,KAAK;AAAA,IACpE,MAAM,KAAK,QAAQ,aAAa,QAAQ,IAAI,kBAAkB,CAAC;AAAA,EACjE;AACF;AAOO,SAAS,wBACd,SACA,UAAsC,CAAC,GAC9B;AACT,QAAM,uBAAuB,QAAQ,wBAAwB;AAC7D,QAAM,uBAAuB,QAAQ,wBAAwB;AAC7D,QAAM,aAAa,QAAQ,cAAc;AACzC,QAAM,YAAY,QAAQ,aAAa;AAEvC,QAAM,aACJ,eAAe,QAAQ,SAAS,oBAAoB,KACpD,QAAQ,QAAQ,IAAI,oBAAoB,KACxC;AAEF,QAAM,UAAU,IAAI,QAAQ,QAAQ,eAAe,QAAQ,QAAQ,aAAa,UAAU;AAE1F,QAAM,YAAY,QAAQ,QAAQ,IAAI,YAAY;AAClD,MAAI,WAAW;AACb,YAAQ,aAAa,IAAI,UAAU,cAAc,CAAC,SAAS,CAAC,CAAC;AAAA,EAC/D;AAEA,UAAQ,aAAa,IAAI,UAAU,QAAQ,CAAC,eAAe,OAAO,CAAC,CAAC,CAAC;AAErE,QAAM,KAAK,YAAY,MAAM,OAAO,IAAI;AACxC,MAAI,IAAI;AACN,YAAQ,aAAa,IAAI,UAAU,MAAM,CAAC,EAAE,CAAC,CAAC;AAAA,EAChD;AAEA,MAAI,YAAY;AACd,UAAM,MAAM,OAAO,OAAO;AAE1B,QAAI,IAAI,SAAS;AACf,cAAQ,aAAa,IAAI,UAAU,WAAW,CAAC,IAAI,OAAO,CAAC,CAAC;AAAA,IAC9D;AAEA,QAAI,IAAI,QAAQ;AACd,cAAQ,aAAa,IAAI,UAAU,UAAU,CAAC,IAAI,MAAM,CAAC,CAAC;AAAA,IAC5D;AAEA,QAAI,IAAI,MAAM;AACZ,cAAQ,aAAa,IAAI,UAAU,QAAQ,CAAC,IAAI,IAAI,CAAC,CAAC;AAAA,IACxD;AAAA,EACF;AAEA,MAAI,QAAQ,sBAAsB;AAChC,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,QAAQ,oBAAoB,GAAG;AACvE,UAAI,CAAC,OAAO;AACV;AAAA,MACF;AAEA,YAAM,SAAS,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK;AACpD,cAAQ,aAAa,IAAI,UAAU,KAAK,MAAM,CAAC;AAAA,IACjD;AAAA,EACF;AAEA,SAAO;AACT;AAMA,eAAsB,uBAAuB,OAA2C;AACtF,QAAM,UAAU,wBAAwB,MAAM,SAAS,MAAM,cAAc;AAE3E,SAAO,cAAc;AAAA,IACnB,kBAAkB,MAAM;AAAA,IACxB,aAAa,MAAM;AAAA,IACnB,UAAU,MAAM;AAAA,IAChB,sBAAsB,MAAM;AAAA,IAC5B,QAAQ,MAAM;AAAA,IACd,KAAK,MAAM;AAAA,IACX,cAAc,MAAM;AAAA,IACpB,UAAU,MAAM;AAAA,IAChB;AAAA,EACF,CAAC;AACH;","names":[]}
|