#
tokens: 48171/50000 25/327 files (page 4/9)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 4 of 9. Use http://codebase.md/bucketco/bucket-javascript-sdk?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .editorconfig
├── .gitattributes
├── .github
│   └── workflows
│       ├── package-ci.yml
│       └── publish.yml
├── .gitignore
├── .nvmrc
├── .prettierignore
├── .vscode
│   ├── extensions.json
│   └── settings.json
├── .yarnrc.yml
├── docs.sh
├── lerna.json
├── LICENSE
├── package.json
├── packages
│   ├── browser-sdk
│   │   ├── .prettierignore
│   │   ├── eslint.config.js
│   │   ├── example
│   │   │   ├── feedback
│   │   │   │   ├── feedback.html
│   │   │   │   └── Feedback.jsx
│   │   │   └── typescript
│   │   │       ├── app.ts
│   │   │       └── index.html
│   │   ├── FEEDBACK.md
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── playwright.config.ts
│   │   ├── postcss.config.js
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── client.ts
│   │   │   ├── config.ts
│   │   │   ├── context.ts
│   │   │   ├── feedback
│   │   │   │   ├── feedback.ts
│   │   │   │   ├── prompts.ts
│   │   │   │   ├── promptStorage.ts
│   │   │   │   └── ui
│   │   │   │       ├── Button.css
│   │   │   │       ├── Button.tsx
│   │   │   │       ├── config
│   │   │   │       │   └── defaultTranslations.tsx
│   │   │   │       ├── css.d.ts
│   │   │   │       ├── FeedbackDialog.css
│   │   │   │       ├── FeedbackDialog.tsx
│   │   │   │       ├── FeedbackForm.css
│   │   │   │       ├── FeedbackForm.tsx
│   │   │   │       ├── hooks
│   │   │   │       │   └── useTimer.ts
│   │   │   │       ├── index.css
│   │   │   │       ├── index.ts
│   │   │   │       ├── Plug.tsx
│   │   │   │       ├── RadialProgress.css
│   │   │   │       ├── RadialProgress.tsx
│   │   │   │       ├── StarRating.css
│   │   │   │       ├── StarRating.tsx
│   │   │   │       └── types.ts
│   │   │   ├── flag
│   │   │   │   ├── flagCache.ts
│   │   │   │   └── flags.ts
│   │   │   ├── hooksManager.ts
│   │   │   ├── httpClient.ts
│   │   │   ├── index.ts
│   │   │   ├── logger.ts
│   │   │   ├── rateLimiter.ts
│   │   │   ├── sse.ts
│   │   │   ├── toolbar
│   │   │   │   ├── Flags.css
│   │   │   │   ├── Flags.tsx
│   │   │   │   ├── index.css
│   │   │   │   ├── index.ts
│   │   │   │   ├── Switch.css
│   │   │   │   ├── Switch.tsx
│   │   │   │   ├── Toolbar.css
│   │   │   │   └── Toolbar.tsx
│   │   │   └── ui
│   │   │       ├── constants.ts
│   │   │       ├── Dialog.css
│   │   │       ├── Dialog.tsx
│   │   │       ├── icons
│   │   │       │   ├── Check.tsx
│   │   │       │   ├── CheckCircle.tsx
│   │   │       │   ├── Close.tsx
│   │   │       │   ├── Dissatisfied.tsx
│   │   │       │   ├── Logo.tsx
│   │   │       │   ├── Neutral.tsx
│   │   │       │   ├── Satisfied.tsx
│   │   │       │   ├── VeryDissatisfied.tsx
│   │   │       │   └── VerySatisfied.tsx
│   │   │       ├── packages
│   │   │       │   └── floating-ui-preact-dom
│   │   │       │       ├── arrow.ts
│   │   │       │       ├── index.ts
│   │   │       │       ├── README.md
│   │   │       │       ├── types.ts
│   │   │       │       ├── useFloating.ts
│   │   │       │       └── utils
│   │   │       │           ├── deepEqual.ts
│   │   │       │           ├── getDPR.ts
│   │   │       │           ├── roundByDPR.ts
│   │   │       │           └── useLatestRef.ts
│   │   │       ├── types.ts
│   │   │       └── utils.ts
│   │   ├── test
│   │   │   ├── client.test.ts
│   │   │   ├── e2e
│   │   │   │   ├── acceptance.browser.spec.ts
│   │   │   │   ├── empty.html
│   │   │   │   ├── feedback-widget.browser.spec.ts
│   │   │   │   └── give-feedback-button.html
│   │   │   ├── flagCache.test.ts
│   │   │   ├── flags.test.ts
│   │   │   ├── hooksManager.test.ts
│   │   │   ├── httpClient.test.ts
│   │   │   ├── init.test.ts
│   │   │   ├── mocks
│   │   │   │   ├── handlers.ts
│   │   │   │   └── server.ts
│   │   │   ├── prompts.test.ts
│   │   │   ├── promptStorage.test.ts
│   │   │   ├── rateLimiter.test.ts
│   │   │   ├── sse.test.ts
│   │   │   ├── testLogger.ts
│   │   │   └── usage.test.ts
│   │   ├── tsconfig.build.json
│   │   ├── tsconfig.eslint.json
│   │   ├── tsconfig.json
│   │   ├── typedoc.json
│   │   ├── vite.config.mjs
│   │   ├── vite.e2e.config.js
│   │   └── vitest.setup.ts
│   ├── cli
│   │   ├── .prettierignore
│   │   ├── commands
│   │   │   ├── apps.ts
│   │   │   ├── auth.ts
│   │   │   ├── flags.ts
│   │   │   ├── init.ts
│   │   │   ├── mcp.ts
│   │   │   ├── new.ts
│   │   │   └── rules.ts
│   │   ├── eslint.config.js
│   │   ├── index.ts
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── schema.json
│   │   ├── services
│   │   │   ├── bootstrap.ts
│   │   │   ├── flags.ts
│   │   │   ├── mcp.ts
│   │   │   └── rules.ts
│   │   ├── stores
│   │   │   ├── auth.ts
│   │   │   └── config.ts
│   │   ├── test
│   │   │   └── json.test.ts
│   │   ├── tsconfig.eslint.json
│   │   ├── tsconfig.json
│   │   ├── utils
│   │   │   ├── auth.ts
│   │   │   ├── commander.ts
│   │   │   ├── constants.ts
│   │   │   ├── errors.ts
│   │   │   ├── file.ts
│   │   │   ├── gen.ts
│   │   │   ├── json.ts
│   │   │   ├── options.ts
│   │   │   ├── schemas.ts
│   │   │   ├── types.ts
│   │   │   ├── urls.ts
│   │   │   └── version.ts
│   │   └── vite.config.js
│   ├── eslint-config
│   │   ├── base.js
│   │   └── package.json
│   ├── flag-evaluation
│   │   ├── .prettierignore
│   │   ├── eslint.config.js
│   │   ├── jest.config.js
│   │   ├── package.json
│   │   ├── src
│   │   │   └── index.ts
│   │   ├── test
│   │   │   └── index.test.ts
│   │   ├── tsconfig.build.json
│   │   ├── tsconfig.eslint.json
│   │   └── tsconfig.json
│   ├── node-sdk
│   │   ├── .prettierignore
│   │   ├── docs
│   │   │   ├── type-check-failed.png
│   │   │   └── type-check-payload-failed.png
│   │   ├── eslint.config.js
│   │   ├── examples
│   │   │   ├── cloudflare-worker
│   │   │   │   ├── .gitignore
│   │   │   │   ├── .prettierignore
│   │   │   │   ├── .vscode
│   │   │   │   │   └── settings.json
│   │   │   │   ├── package.json
│   │   │   │   ├── README.md
│   │   │   │   ├── src
│   │   │   │   │   └── index.ts
│   │   │   │   ├── tsconfig.json
│   │   │   │   ├── vitest.config.mts
│   │   │   │   ├── worker-configuration.d.ts
│   │   │   │   ├── wrangler.jsonc
│   │   │   │   └── yarn.lock
│   │   │   └── express
│   │   │       ├── app.test.ts
│   │   │       ├── app.ts
│   │   │       ├── bucket.ts
│   │   │       ├── bucketConfig.json
│   │   │       ├── package.json
│   │   │       ├── README.md
│   │   │       ├── serve.ts
│   │   │       ├── tsconfig.json
│   │   │       └── yarn.lock
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── batch-buffer.ts
│   │   │   ├── client.ts
│   │   │   ├── config.ts
│   │   │   ├── edgeClient.ts
│   │   │   ├── fetch-http-client.ts
│   │   │   ├── flusher.ts
│   │   │   ├── index.ts
│   │   │   ├── inRequestCache.ts
│   │   │   ├── periodicallyUpdatingCache.ts
│   │   │   ├── rate-limiter.ts
│   │   │   ├── types.ts
│   │   │   └── utils.ts
│   │   ├── test
│   │   │   ├── batch-buffer.test.ts
│   │   │   ├── client.test.ts
│   │   │   ├── config.test.ts
│   │   │   ├── fetch-http-client.test.ts
│   │   │   ├── flusher.test.ts
│   │   │   ├── inRequestCache.test.ts
│   │   │   ├── periodicallyUpdatingCache.test.ts
│   │   │   ├── rate-limiter.test.ts
│   │   │   ├── testConfig.json
│   │   │   └── utils.test.ts
│   │   ├── tsconfig.build.json
│   │   ├── tsconfig.eslint.json
│   │   ├── tsconfig.json
│   │   ├── typedoc.json
│   │   └── vite.config.js
│   ├── openfeature-browser-provider
│   │   ├── .prettierignore
│   │   ├── eslint.config.js
│   │   ├── example
│   │   │   ├── .eslintrc.json
│   │   │   ├── .gitignore
│   │   │   ├── app
│   │   │   │   ├── featureManagement.ts
│   │   │   │   ├── globals.css
│   │   │   │   ├── layout.tsx
│   │   │   │   └── page.tsx
│   │   │   ├── components
│   │   │   │   ├── Context.tsx
│   │   │   │   ├── HuddleFeature.tsx
│   │   │   │   └── OpenFeatureProvider.tsx
│   │   │   ├── next.config.mjs
│   │   │   ├── package.json
│   │   │   ├── postcss.config.mjs
│   │   │   ├── README.md
│   │   │   ├── tailwind.config.ts
│   │   │   └── tsconfig.json
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── index.test.ts
│   │   │   └── index.ts
│   │   ├── tsconfig.build.json
│   │   ├── tsconfig.eslint.json
│   │   ├── tsconfig.json
│   │   └── vite.config.js
│   ├── openfeature-node-provider
│   │   ├── .prettierignore
│   │   ├── eslint.config.js
│   │   ├── example
│   │   │   ├── app.ts
│   │   │   ├── package.json
│   │   │   ├── README.md
│   │   │   ├── reflag.ts
│   │   │   ├── serve.ts
│   │   │   ├── tsconfig.json
│   │   │   └── yarn.lock
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── index.test.ts
│   │   │   └── index.ts
│   │   ├── tsconfig.build.json
│   │   ├── tsconfig.eslint.json
│   │   ├── tsconfig.json
│   │   └── vite.config.js
│   ├── react-sdk
│   │   ├── .prettierignore
│   │   ├── dev
│   │   │   ├── .env
│   │   │   ├── nextjs-bootstrap-demo
│   │   │   │   ├── .eslintrc.json
│   │   │   │   ├── .gitignore
│   │   │   │   ├── app
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── favicon.ico
│   │   │   │   │   ├── globals.css
│   │   │   │   │   ├── layout.tsx
│   │   │   │   │   └── page.tsx
│   │   │   │   ├── components
│   │   │   │   │   └── Flags.tsx
│   │   │   │   ├── next.config.mjs
│   │   │   │   ├── package.json
│   │   │   │   ├── postcss.config.mjs
│   │   │   │   ├── public
│   │   │   │   │   ├── next.svg
│   │   │   │   │   └── vercel.svg
│   │   │   │   ├── README.md
│   │   │   │   ├── tailwind.config.ts
│   │   │   │   └── tsconfig.json
│   │   │   ├── nextjs-flag-demo
│   │   │   │   ├── .eslintrc.json
│   │   │   │   ├── .gitignore
│   │   │   │   ├── app
│   │   │   │   │   ├── favicon.ico
│   │   │   │   │   ├── globals.css
│   │   │   │   │   ├── layout.tsx
│   │   │   │   │   └── page.tsx
│   │   │   │   ├── components
│   │   │   │   │   ├── Flags.tsx
│   │   │   │   │   └── Providers.tsx
│   │   │   │   ├── next.config.mjs
│   │   │   │   ├── package.json
│   │   │   │   ├── postcss.config.mjs
│   │   │   │   ├── public
│   │   │   │   │   ├── next.svg
│   │   │   │   │   └── vercel.svg
│   │   │   │   ├── README.md
│   │   │   │   ├── tailwind.config.ts
│   │   │   │   └── tsconfig.json
│   │   │   └── plain
│   │   │       ├── app.tsx
│   │   │       ├── index.html
│   │   │       ├── index.tsx
│   │   │       ├── tsconfig.json
│   │   │       └── vite-env.d.ts
│   │   ├── eslint.config.js
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   └── index.tsx
│   │   ├── test
│   │   │   └── usage.test.tsx
│   │   ├── tsconfig.build.json
│   │   ├── tsconfig.eslint.json
│   │   ├── tsconfig.json
│   │   ├── typedoc.json
│   │   └── vite.config.mjs
│   ├── tsconfig
│   │   ├── library.json
│   │   └── package.json
│   └── vue-sdk
│       ├── .prettierignore
│       ├── dev
│       │   └── plain
│       │       ├── App.vue
│       │       ├── components
│       │       │   ├── Events.vue
│       │       │   ├── FlagsList.vue
│       │       │   ├── MissingKeyMessage.vue
│       │       │   ├── RequestFeedback.vue
│       │       │   ├── Section.vue
│       │       │   ├── StartHuddlesButton.vue
│       │       │   └── Track.vue
│       │       ├── env.d.ts
│       │       ├── index.html
│       │       └── index.ts
│       ├── eslint.config.js
│       ├── package.json
│       ├── README.md
│       ├── src
│       │   ├── hooks.ts
│       │   ├── index.ts
│       │   ├── ReflagBootstrappedProvider.vue
│       │   ├── ReflagClientProvider.vue
│       │   ├── ReflagProvider.vue
│       │   ├── types.ts
│       │   ├── version.ts
│       │   └── vue.d.ts
│       ├── test
│       │   └── usage.test.ts
│       ├── tsconfig.build.json
│       ├── tsconfig.eslint.json
│       ├── tsconfig.json
│       ├── typedoc.json
│       └── vite.config.mjs
├── README.md
├── typedoc.json
├── vitest.workspace.js
└── yarn.lock
```

# Files

--------------------------------------------------------------------------------
/packages/node-sdk/examples/express/app.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import reflag from "./reflag";
  2 | import express from "express";
  3 | import { BoundReflagClient } from "../src";
  4 | 
  5 | // Augment the Express types to include the `reflagUser` property on the `res.locals` object
  6 | // This will allow us to access the ReflagClient instance in our route handlers
  7 | // without having to pass it around manually
  8 | declare global {
  9 |   namespace Express {
 10 |     interface Locals {
 11 |       reflagUser: BoundReflagClient;
 12 |     }
 13 |   }
 14 | }
 15 | 
 16 | const app = express();
 17 | 
 18 | app.use(express.json());
 19 | app.use((req, res, next) => {
 20 |   // Extract the user and company IDs from the request headers
 21 |   // You'll want to use a proper authentication and identification
 22 |   // mechanism in a real-world application
 23 |   const { user, company } = extractReflagContextFromHeader(req);
 24 | 
 25 |   // Create a new BoundReflagClient instance by calling the `bindClient` method on a `ReflagClient` instance
 26 |   // This will create a new instance that is bound to the user/company given.
 27 |   const reflagUser = reflag.bindClient({ user, company });
 28 | 
 29 |   // Store the BoundReflagClient instance in the `res.locals` object so we can access it in our route handlers
 30 |   res.locals.reflagUser = reflagUser;
 31 |   next();
 32 | });
 33 | 
 34 | export const todos = ["Buy milk", "Walk the dog"];
 35 | 
 36 | app.get("/", (_req, res) => {
 37 |   res.locals.reflagUser.track("Front Page Viewed");
 38 |   res.json({ message: "Ready to manage some TODOs!" });
 39 | });
 40 | 
 41 | // Return todos if the feature is enabled for the user
 42 | app.get("/todos", async (_req, res) => {
 43 |   // We use the `getFlag` method to check if the user has the "show-todos" feature enabled.
 44 |   // Note that "show-todos" is a feature that we defined in the `Flags` interface in the `reflag.ts` file.
 45 |   // and that the indexing for feature name below is type-checked at compile time.
 46 |   const { isEnabled, track } = res.locals.reflagUser.getFlag("show-todos");
 47 | 
 48 |   if (isEnabled) {
 49 |     track();
 50 | 
 51 |     // You can instead also send any custom event if you prefer, including attributes.
 52 |     // res.locals.reflagUser.track("Todo's viewed", { attributes: { access: "api" } });
 53 | 
 54 |     return res.json({ todos });
 55 |   }
 56 | 
 57 |   // Return no todos if the feature is disabled for the user
 58 |   return res.json({ todos: [] });
 59 | });
 60 | 
 61 | app.post("/todos", (req, res) => {
 62 |   const { todo } = req.body;
 63 | 
 64 |   if (typeof todo !== "string") {
 65 |     return res.status(400).json({ error: "Invalid todo" });
 66 |   }
 67 | 
 68 |   const { track, isEnabled, config } =
 69 |     res.locals.reflagUser.getFlag("create-todos");
 70 | 
 71 |   // Check if the user has the "create-todos" feature enabled
 72 |   if (isEnabled) {
 73 |     // Check if the todo is at least N characters long
 74 |     if (todo.length < config.payload.minimumLength) {
 75 |       return res
 76 |         .status(400)
 77 |         .json({ error: "Todo must be at least 5 characters long" });
 78 |     }
 79 | 
 80 |     // Track the feature usage
 81 |     track();
 82 |     todos.push(todo);
 83 | 
 84 |     return res.status(201).json({ todo });
 85 |   }
 86 | 
 87 |   res
 88 |     .status(403)
 89 |     .json({ error: "You do not have access to this feature yet!" });
 90 | });
 91 | 
 92 | app.delete("/todos/:idx", (req, res) => {
 93 |   const idx = parseInt(req.params.idx);
 94 | 
 95 |   if (isNaN(idx) || idx < 0 || idx >= todos.length) {
 96 |     return res.status(400).json({ error: "Invalid index" });
 97 |   }
 98 | 
 99 |   const { track, isEnabled } = res.locals.reflagUser.getFlag("delete-todos");
100 | 
101 |   if (isEnabled) {
102 |     todos.splice(idx, 1);
103 | 
104 |     track();
105 |     res.json({});
106 |   }
107 | 
108 |   res
109 |     .status(403)
110 |     .json({ error: "You do not have access to this feature yet!" });
111 | });
112 | 
113 | app.get("/features", async (_req, res) => {
114 |   const features = await res.locals.reflagUser.getFlagsRemote();
115 |   res.json(features);
116 | });
117 | 
118 | export default app;
119 | 
120 | function extractReflagContextFromHeader(req: express.Request) {
121 |   const user = req.headers["x-reflag-user-id"]
122 |     ? {
123 |         id: req.headers["x-reflag-user-id"] as string,
124 |         role: req.headers["x-reflag-is-admin"] ? "admin" : "user",
125 |       }
126 |     : undefined;
127 |   const company = req.headers["x-reflag-company-id"]
128 |     ? {
129 |         id: req.headers["x-reflag-company-id"] as string,
130 |         betaUser: !!req.headers["x-reflag-company-beta-user"],
131 |       }
132 |     : undefined;
133 |   return { user, company };
134 | }
135 | 
```

--------------------------------------------------------------------------------
/packages/cli/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | import chalk from "chalk";
  3 | import { program } from "commander";
  4 | import ora from "ora";
  5 | 
  6 | import { registerAppCommands } from "./commands/apps.js";
  7 | import { registerAuthCommands } from "./commands/auth.js";
  8 | import { registerFlagCommands } from "./commands/flags.js";
  9 | import { registerInitCommand } from "./commands/init.js";
 10 | import { registerMcpCommand } from "./commands/mcp.js";
 11 | import { registerNewCommand } from "./commands/new.js";
 12 | import { registerRulesCommand } from "./commands/rules.js";
 13 | import { bootstrap, getReflagUser } from "./services/bootstrap.js";
 14 | import { authStore } from "./stores/auth.js";
 15 | import { configStore } from "./stores/config.js";
 16 | import { commandName } from "./utils/commander.js";
 17 | import { handleError } from "./utils/errors.js";
 18 | import {
 19 |   apiKeyOption,
 20 |   apiUrlOption,
 21 |   baseUrlOption,
 22 |   debugOption,
 23 | } from "./utils/options.js";
 24 | import { stripTrailingSlash } from "./utils/urls.js";
 25 | import { checkLatest as checkLatestVersion } from "./utils/version.js";
 26 | 
 27 | const skipBootstrapCommands = [/^login/, /^logout/, /^rules/];
 28 | 
 29 | type Options = {
 30 |   debug?: boolean;
 31 |   baseUrl?: string;
 32 |   apiUrl?: string;
 33 |   apiKey?: string;
 34 | };
 35 | 
 36 | async function main() {
 37 |   // Start a version check in the background
 38 |   // unhandled promise rejection can happen even without the `await`
 39 |   // so we need a `catch` here.
 40 |   const cliVersionCheckPromise = checkLatestVersion().catch(() => ({
 41 |     latestVersion: "unknown",
 42 |     currentVersion: "unknown",
 43 |     isNewerAvailable: false,
 44 |   }));
 45 | 
 46 |   // Must load tokens and config before anything else
 47 |   await authStore.initialize();
 48 |   await configStore.initialize();
 49 | 
 50 |   // Global options
 51 |   program.addOption(debugOption);
 52 |   program.addOption(baseUrlOption);
 53 |   program.addOption(apiUrlOption);
 54 |   program.addOption(apiKeyOption);
 55 | 
 56 |   // Pre-action hook
 57 |   program.hook("preAction", async (_, actionCommand) => {
 58 |     const {
 59 |       debug,
 60 |       baseUrl,
 61 |       apiUrl,
 62 |       apiKey: explicitApiKey,
 63 |     } = program.opts<Options>();
 64 |     const cleanedBaseUrl = stripTrailingSlash(baseUrl?.trim());
 65 |     const cleanedApiUrl = stripTrailingSlash(apiUrl?.trim());
 66 | 
 67 |     const apiKey = explicitApiKey ?? process.env.REFLAG_API_KEY;
 68 | 
 69 |     if (typeof apiKey === "string" && apiKey.length > 0) {
 70 |       console.info(
 71 |         chalk.yellow(
 72 |           "API key supplied. Using it instead of normal personal authentication.",
 73 |         ),
 74 |       );
 75 |       authStore.useApiKey(apiKey);
 76 |     }
 77 | 
 78 |     // Set baseUrl and apiUrl in config store, will skip if undefined
 79 |     configStore.setConfig({
 80 |       baseUrl: cleanedBaseUrl,
 81 |       apiUrl: cleanedApiUrl || (cleanedBaseUrl && `${cleanedBaseUrl}/api`),
 82 |     });
 83 | 
 84 |     // Skip bootstrapping for commands that don't require it
 85 |     if (
 86 |       !skipBootstrapCommands.some((cmd) => cmd.test(commandName(actionCommand)))
 87 |     ) {
 88 |       const spinner = ora("Bootstrapping...").start();
 89 | 
 90 |       try {
 91 |         // Load bootstrap data if not already loaded
 92 |         await bootstrap();
 93 |         spinner.stop();
 94 |       } catch (error) {
 95 |         spinner.fail("Bootstrap failed.");
 96 |         handleError(error, "Connect");
 97 |       }
 98 |     }
 99 | 
100 |     const { latestVersion, currentVersion, isNewerAvailable } =
101 |       await cliVersionCheckPromise;
102 | 
103 |     if (isNewerAvailable) {
104 |       console.info(
105 |         `A new version of the CLI is available: ${chalk.yellow(
106 |           currentVersion,
107 |         )} -> ${chalk.green(latestVersion)}. Update to ensure you have the latest features and bug fixes.`,
108 |       );
109 |     }
110 | 
111 |     if (debug) {
112 |       console.debug(chalk.cyan("\nDebug mode enabled."));
113 |       const user = getReflagUser();
114 |       console.debug(`Logged in as ${chalk.cyan(user.name ?? user.email)}.`);
115 |       console.debug(
116 |         "Reading config from:",
117 |         chalk.cyan(configStore.getConfigPath()),
118 |       );
119 |       console.table(configStore.getConfig());
120 |     }
121 |   });
122 | 
123 |   // Main program
124 |   registerNewCommand(program);
125 |   registerInitCommand(program);
126 |   registerAuthCommands(program);
127 |   registerAppCommands(program);
128 |   registerFlagCommands(program);
129 |   registerMcpCommand(program);
130 |   registerRulesCommand(program);
131 | 
132 |   program.parse(process.argv);
133 | }
134 | 
135 | void main();
136 | 
```

--------------------------------------------------------------------------------
/packages/browser-sdk/test/hooksManager.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { beforeEach, describe, expect, it, vi } from "vitest";
  2 | 
  3 | import { CompanyContext, UserContext } from "../src";
  4 | import { CheckEvent, RawFlags } from "../src/flag/flags";
  5 | import { HooksManager } from "../src/hooksManager";
  6 | 
  7 | describe("HookManager", () => {
  8 |   let hookManager: HooksManager;
  9 | 
 10 |   beforeEach(() => {
 11 |     hookManager = new HooksManager();
 12 |   });
 13 | 
 14 |   it("should add and trigger `check` hooks (is-enabled)", () => {
 15 |     const callback = vi.fn();
 16 |     hookManager.addHook("check", callback);
 17 | 
 18 |     const checkEvent: CheckEvent = {
 19 |       action: "check-is-enabled",
 20 |       key: "test-key",
 21 |       value: true,
 22 |     };
 23 |     hookManager.trigger("check", checkEvent);
 24 | 
 25 |     expect(callback).toHaveBeenCalledWith(checkEvent);
 26 |   });
 27 | 
 28 |   it("should add and trigger `check` hooks (config)", () => {
 29 |     const callback = vi.fn();
 30 |     hookManager.addHook("check", callback);
 31 | 
 32 |     const checkEvent: CheckEvent = {
 33 |       action: "check-config",
 34 |       key: "test-key",
 35 |       value: { key: "key", payload: "payload" },
 36 |     };
 37 |     hookManager.trigger("check", checkEvent);
 38 | 
 39 |     expect(callback).toHaveBeenCalledWith(checkEvent);
 40 |   });
 41 | 
 42 |   it("should add and trigger `flagsUpdated` hooks", () => {
 43 |     const callback = vi.fn();
 44 |     hookManager.addHook("flagsUpdated", callback);
 45 | 
 46 |     const flags: RawFlags = {
 47 |       /* mock RawFlags data */
 48 |     };
 49 |     hookManager.trigger("flagsUpdated", flags);
 50 | 
 51 |     expect(callback).toHaveBeenCalledWith(flags);
 52 |   });
 53 | 
 54 |   it("should add and trigger `track` hooks", () => {
 55 |     const callback = vi.fn();
 56 |     const user: UserContext = { id: "user-id", name: "user-name" };
 57 |     const company: CompanyContext = { id: "company-id", name: "company-name" };
 58 |     hookManager.addHook("track", callback);
 59 | 
 60 |     const eventName = "test-event";
 61 |     const attributes = { key: "value" };
 62 |     hookManager.trigger("track", { eventName, attributes, user, company });
 63 | 
 64 |     expect(callback).toHaveBeenCalledWith({
 65 |       eventName,
 66 |       attributes,
 67 |       user,
 68 |       company,
 69 |     });
 70 |   });
 71 | 
 72 |   it("should add and trigger `user` hooks", () => {
 73 |     const callback = vi.fn();
 74 | 
 75 |     hookManager.addHook("user", callback);
 76 | 
 77 |     const user = { id: "user-id", name: "user-name" };
 78 |     hookManager.trigger("user", user);
 79 | 
 80 |     expect(callback).toHaveBeenCalledWith(user);
 81 |   });
 82 | 
 83 |   it("should add and trigger `company` hooks", () => {
 84 |     const callback = vi.fn();
 85 |     hookManager.addHook("company", callback);
 86 | 
 87 |     const company = { id: "company-id", name: "company-name" };
 88 |     hookManager.trigger("company", company);
 89 | 
 90 |     expect(callback).toHaveBeenCalledWith(company);
 91 |   });
 92 | 
 93 |   it("should handle multiple hooks of the same type", () => {
 94 |     const callback1 = vi.fn();
 95 |     const callback2 = vi.fn();
 96 | 
 97 |     hookManager.addHook("check", callback1);
 98 |     hookManager.addHook("check", callback2);
 99 | 
100 |     const checkEvent: CheckEvent = {
101 |       action: "check-is-enabled",
102 |       key: "test-key",
103 |       value: true,
104 |     };
105 |     hookManager.trigger("check", checkEvent);
106 | 
107 |     expect(callback1).toHaveBeenCalledWith(checkEvent);
108 |     expect(callback2).toHaveBeenCalledWith(checkEvent);
109 |   });
110 | 
111 |   it("should remove the given hook and no other hooks", () => {
112 |     const callback1 = vi.fn();
113 |     const callback2 = vi.fn();
114 | 
115 |     hookManager.addHook("check", callback1);
116 |     hookManager.addHook("check", callback2);
117 |     hookManager.removeHook("check", callback1);
118 | 
119 |     const checkEvent: CheckEvent = {
120 |       action: "check-is-enabled",
121 |       key: "test-key",
122 |       value: true,
123 |     };
124 |     hookManager.trigger("check", checkEvent);
125 | 
126 |     expect(callback1).not.toHaveBeenCalled();
127 |     expect(callback2).toHaveBeenCalledWith(checkEvent);
128 |   });
129 | 
130 |   it("should remove the hook using the function returned from addHook", () => {
131 |     const callback1 = vi.fn();
132 |     const callback2 = vi.fn();
133 | 
134 |     const removeHook1 = hookManager.addHook("check", callback1);
135 |     hookManager.addHook("check", callback2);
136 |     removeHook1();
137 | 
138 |     const checkEvent: CheckEvent = {
139 |       action: "check-is-enabled",
140 |       key: "test-key",
141 |       value: true,
142 |     };
143 |     hookManager.trigger("check", checkEvent);
144 | 
145 |     expect(callback1).not.toHaveBeenCalled();
146 |     expect(callback2).toHaveBeenCalledWith(checkEvent);
147 |   });
148 | });
149 | 
```

--------------------------------------------------------------------------------
/packages/node-sdk/test/fetch-http-client.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { afterEach, describe, expect, it, vi } from "vitest";
  2 | 
  3 | import { API_TIMEOUT_MS } from "../src/config";
  4 | import fetchClient from "../src/fetch-http-client";
  5 | 
  6 | // mock environment variables
  7 | vi.mock("../src/config", () => ({ API_TIMEOUT_MS: 100 }));
  8 | 
  9 | describe("fetchClient", () => {
 10 |   const url = "https://example.com/api";
 11 |   const headers = { "Content-Type": "application/json" };
 12 | 
 13 |   afterEach(() => {
 14 |     vi.resetAllMocks();
 15 |   });
 16 | 
 17 |   it("should make a POST request and return the response", async () => {
 18 |     const body = { key: "value" };
 19 |     const response = { ok: true, status: 200, body: { success: true } };
 20 | 
 21 |     global.fetch = vi.fn().mockResolvedValue({
 22 |       ok: true,
 23 |       status: 200,
 24 |       json: async () => ({
 25 |         success: true,
 26 |       }),
 27 |     } as Response);
 28 | 
 29 |     const result = await fetchClient.post<typeof body, typeof response>(
 30 |       url,
 31 |       headers,
 32 |       body,
 33 |     );
 34 | 
 35 |     expect(result).toEqual(response);
 36 |     expect(global.fetch).toHaveBeenCalledTimes(1);
 37 |     expect(global.fetch).toHaveBeenCalledWith(
 38 |       url,
 39 |       expect.objectContaining({
 40 |         method: "post",
 41 |         headers,
 42 |         body: JSON.stringify(body),
 43 |         signal: expect.any(AbortSignal),
 44 |       }),
 45 |     );
 46 |   });
 47 | 
 48 |   it("should make a GET request and return the response", async () => {
 49 |     const response = { ok: true, status: 200, body: { success: true } };
 50 | 
 51 |     global.fetch = vi.fn().mockResolvedValue({
 52 |       ok: true,
 53 |       status: 200,
 54 |       json: async () => ({
 55 |         success: true,
 56 |       }),
 57 |     } as Response);
 58 | 
 59 |     const result = await fetchClient.get<typeof response>(url, headers);
 60 | 
 61 |     expect(result).toEqual(response);
 62 |     expect(global.fetch).toHaveBeenCalledTimes(1);
 63 |     expect(global.fetch).toHaveBeenCalledWith(
 64 |       url,
 65 |       expect.objectContaining({
 66 |         method: "get",
 67 |         headers,
 68 |         signal: expect.any(AbortSignal),
 69 |       }),
 70 |     );
 71 |   });
 72 | 
 73 |   it("should timeout a POST request that takes too long", async () => {
 74 |     global.fetch = vi
 75 |       .fn()
 76 |       .mockImplementation(
 77 |         () =>
 78 |           new Promise((resolve) =>
 79 |             setTimeout(
 80 |               () => resolve({ ok: true, json: async () => ({}) }),
 81 |               API_TIMEOUT_MS + 100,
 82 |             ),
 83 |           ),
 84 |       );
 85 | 
 86 |     await fetchClient.post(url, headers, {});
 87 |     expect(vi.mocked(global.fetch).mock.calls[0][1]?.signal?.aborted).toBe(
 88 |       true,
 89 |     );
 90 |   });
 91 | 
 92 |   it("should timeout a GET request that takes too long", async () => {
 93 |     global.fetch = vi
 94 |       .fn()
 95 |       .mockImplementation(
 96 |         () =>
 97 |           new Promise((resolve) =>
 98 |             setTimeout(
 99 |               () => resolve({ ok: true, json: async () => ({}) }),
100 |               API_TIMEOUT_MS + 100,
101 |             ),
102 |           ),
103 |       );
104 | 
105 |     await fetchClient.get(url, headers);
106 |     expect(vi.mocked(global.fetch).mock.calls[0][1]?.signal?.aborted).toBe(
107 |       true,
108 |     );
109 |   });
110 | 
111 |   it("should handle POST non-20x responses", async () => {
112 |     const response = {
113 |       ok: false,
114 |       status: 400,
115 |       body: { error: "Something went wrong" },
116 |     };
117 | 
118 |     global.fetch = vi.fn().mockResolvedValue({
119 |       ok: false,
120 |       status: 400,
121 |       json: async () => ({ error: "Something went wrong" }),
122 |     } as Response);
123 | 
124 |     const result = await fetchClient.post(url, headers, {});
125 | 
126 |     expect(result).toEqual(response);
127 |   });
128 | 
129 |   it("should handle GET non-20x responses", async () => {
130 |     const response = {
131 |       ok: false,
132 |       status: 400,
133 |       body: { error: "Something went wrong" },
134 |     };
135 | 
136 |     global.fetch = vi.fn().mockResolvedValue({
137 |       ok: false,
138 |       status: 400,
139 |       json: async () => ({ error: "Something went wrong" }),
140 |     } as Response);
141 | 
142 |     const result = await fetchClient.get(url, headers);
143 | 
144 |     expect(result).toEqual(response);
145 |   });
146 | 
147 |   it("should not handle POST exceptions", async () => {
148 |     global.fetch = vi.fn().mockRejectedValue(new Error("Network error"));
149 | 
150 |     await expect(fetchClient.post(url, headers, {})).rejects.toThrow(
151 |       "Network error",
152 |     );
153 |   });
154 | 
155 |   it("should not handle GET exceptions", async () => {
156 |     global.fetch = vi.fn().mockRejectedValue(new Error("Network error"));
157 | 
158 |     await expect(fetchClient.get(url, headers)).rejects.toThrow(
159 |       "Network error",
160 |     );
161 |   });
162 | });
163 | 
```

--------------------------------------------------------------------------------
/packages/cli/utils/gen.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { camelCase, kebabCase, pascalCase, snakeCase } from "change-case";
  2 | import { mkdir, writeFile } from "node:fs/promises";
  3 | import { dirname, isAbsolute, join } from "node:path";
  4 | 
  5 | import { Flag, RemoteConfig } from "../services/flags.js";
  6 | 
  7 | import { JSONToType, quoteKey } from "./json.js";
  8 | 
  9 | export type GenFormat = "react" | "node";
 10 | 
 11 | // Keep in sync with Reflag main repo
 12 | export const KeyFormats = [
 13 |   "custom",
 14 |   "pascalCase",
 15 |   "camelCase",
 16 |   "snakeCaseUpper",
 17 |   "snakeCaseLower",
 18 |   "kebabCaseUpper",
 19 |   "kebabCaseLower",
 20 | ] as const;
 21 | 
 22 | export type KeyFormat = (typeof KeyFormats)[number];
 23 | 
 24 | type KeyFormatPattern = {
 25 |   transform: (key: string) => string;
 26 |   regex: RegExp;
 27 |   message: string;
 28 | };
 29 | 
 30 | export const KeyFormatPatterns: Record<KeyFormat, KeyFormatPattern> = {
 31 |   custom: {
 32 |     transform: (key) => key?.trim(),
 33 |     regex: /^[\p{L}\p{N}\p{P}\p{S}\p{Z}]+$/u,
 34 |     message:
 35 |       "Key must contain only letters, numbers, punctuation, symbols, or spaces.",
 36 |   },
 37 |   pascalCase: {
 38 |     transform: (key) => pascalCase(key),
 39 |     regex: /^[\p{Lu}][\p{L}\p{N}]*$/u,
 40 |     message:
 41 |       "Key must start with uppercase letter and contain only letters and numbers.",
 42 |   },
 43 |   camelCase: {
 44 |     transform: (key) => camelCase(key),
 45 |     regex: /^[\p{Ll}][\p{L}\p{N}]*$/u,
 46 |     message:
 47 |       "Key must start with lowercase letter and contain only letters and numbers.",
 48 |   },
 49 |   snakeCaseUpper: {
 50 |     transform: (key) => snakeCase(key).toUpperCase(),
 51 |     regex: /^[\p{Lu}][\p{Lu}\p{N}]*(?:_[\p{Lu}\p{N}]+)*$/u,
 52 |     message: "Key must be uppercase with words separated by underscores.",
 53 |   },
 54 |   snakeCaseLower: {
 55 |     transform: (key) => snakeCase(key).toLowerCase(),
 56 |     regex: /^[\p{Ll}][\p{Ll}\p{N}]*(?:_[\p{Ll}\p{N}]+)*$/u,
 57 |     message: "Key must be lowercase with words separated by underscores.",
 58 |   },
 59 |   kebabCaseUpper: {
 60 |     transform: (key) => kebabCase(key).toUpperCase(),
 61 |     regex: /^[\p{Lu}][\p{Lu}\p{N}]*(?:-[\p{Lu}\p{N}]+)*$/u,
 62 |     message: "Key must be uppercase with words separated by hyphens.",
 63 |   },
 64 |   kebabCaseLower: {
 65 |     transform: (key) => kebabCase(key).toLowerCase(),
 66 |     regex: /^[\p{Ll}][\p{Ll}\p{N}]*(?:-[\p{Ll}\p{N}]+)*$/u,
 67 |     message: "Key must be lowercase with words separated by hyphens.",
 68 |   },
 69 | };
 70 | 
 71 | export function indentLines(
 72 |   str: string,
 73 |   indent = 2,
 74 |   lineBreak = "\n",
 75 |   trim = false,
 76 | ): string {
 77 |   const indentStr = " ".repeat(indent);
 78 |   return str
 79 |     .split(lineBreak)
 80 |     .map((line) => `${indentStr}${trim ? line.trim() : line}`)
 81 |     .join(lineBreak);
 82 | }
 83 | 
 84 | export function genFlagKey(input: string, format: KeyFormat): string {
 85 |   return KeyFormatPatterns[format].transform(input);
 86 | }
 87 | 
 88 | export function genRemoteConfig(remoteConfigs?: RemoteConfig[]) {
 89 |   const variants = remoteConfigs?.[0]?.variants;
 90 |   if (!variants?.length) return;
 91 |   return JSONToType(
 92 |     remoteConfigs![0].variants?.map(({ variant: { payload } }) => payload),
 93 |   );
 94 | }
 95 | 
 96 | export function genTypes(flags: Flag[], format: GenFormat = "react") {
 97 |   const configDefs = new Map<string, { name: string; definition: string }>();
 98 |   flags.forEach(({ key, name, remoteConfigs }) => {
 99 |     const definition = genRemoteConfig(remoteConfigs);
100 | 
101 |     if (!definition) {
102 |       return;
103 |     }
104 | 
105 |     const configName = `${pascalCase(name)}ConfigPayload`;
106 |     configDefs.set(key, { name: configName, definition });
107 |   });
108 | 
109 |   return /* ts */ `
110 | // DO NOT EDIT THIS FILE. IT IS GENERATED BY THE REFLAG CLI AND WILL BE OVERWRITTEN.
111 | // eslint-disable
112 | // prettier-ignore
113 | import "@reflag/${format}-sdk";
114 | 
115 | declare module "@reflag/${format}-sdk" {
116 |   export interface Flags {
117 | ${flags
118 |   .map(({ key }) => {
119 |     const config = configDefs.get(key);
120 |     return indentLines(
121 |       `${quoteKey(key)}: ${config?.definition ? `{ config: { payload: ${config.name} } }` : "boolean"};`,
122 |       4,
123 |     );
124 |   })
125 |   .join("\n")}
126 |   }
127 | 
128 | ${Array.from(configDefs.values())
129 |   .map(({ name, definition }) => {
130 |     return indentLines(`export type ${name} = ${definition}`);
131 |   })
132 |   .join("\n\n")}
133 | }
134 | `.trim();
135 | }
136 | 
137 | export async function writeTypesToFile(
138 |   types: string,
139 |   outPath: string,
140 |   projectPath: string,
141 | ) {
142 |   const fullPath = isAbsolute(outPath) ? outPath : join(projectPath, outPath);
143 | 
144 |   await mkdir(dirname(fullPath), { recursive: true });
145 |   await writeFile(fullPath, types);
146 | 
147 |   return fullPath;
148 | }
149 | 
```

--------------------------------------------------------------------------------
/packages/node-sdk/src/config.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { readFileSync } from "fs";
  2 | 
  3 | import { version } from "../package.json";
  4 | 
  5 | import { LOG_LEVELS } from "./types";
  6 | import { isObject, ok } from "./utils";
  7 | 
  8 | export const API_BASE_URL = "https://front.reflag.com";
  9 | export const SDK_VERSION_HEADER_NAME = "reflag-sdk-version";
 10 | export const SDK_VERSION = `node-sdk/${version}`;
 11 | export const API_TIMEOUT_MS = 10000;
 12 | export const END_FLUSH_TIMEOUT_MS = 5000;
 13 | 
 14 | export const REFLAG_LOG_PREFIX = "[Reflag]";
 15 | 
 16 | export const FLAG_EVENT_RATE_LIMITER_WINDOW_SIZE_MS = 60 * 1000;
 17 | 
 18 | export const FLAGS_REFETCH_MS = 60 * 1000; // re-fetch every 60 seconds
 19 | 
 20 | export const BATCH_MAX_SIZE = 100;
 21 | export const BATCH_INTERVAL_MS = 10 * 1000;
 22 | 
 23 | function parseOverrides(config: object | undefined) {
 24 |   if (!config) return {};
 25 |   if ("flagOverrides" in config && isObject(config.flagOverrides)) {
 26 |     Object.entries(config.flagOverrides).forEach(([key, value]) => {
 27 |       ok(
 28 |         typeof value === "boolean" || isObject(value),
 29 |         `invalid type "${typeof value}" for key ${key}, expected boolean or object`,
 30 |       );
 31 |       if (isObject(value)) {
 32 |         ok(
 33 |           "isEnabled" in value && typeof value.isEnabled === "boolean",
 34 |           `invalid type "${typeof value.isEnabled}" for key ${key}.isEnabled, expected boolean`,
 35 |         );
 36 |         ok(
 37 |           value.config === undefined || isObject(value.config),
 38 |           `invalid type "${typeof value.config}" for key ${key}.config, expected object or undefined`,
 39 |         );
 40 |         if (isObject(value.config)) {
 41 |           ok(
 42 |             "key" in value.config && typeof value.config.key === "string",
 43 |             `invalid type "${typeof value.config.key}" for key ${key}.config.key, expected string`,
 44 |           );
 45 |         }
 46 |       }
 47 |     });
 48 | 
 49 |     return config.flagOverrides;
 50 |   }
 51 | 
 52 |   return {};
 53 | }
 54 | 
 55 | function loadConfigFile(file: string) {
 56 |   const configJson = readFileSync(file, "utf-8");
 57 |   const config = JSON.parse(configJson);
 58 | 
 59 |   ok(typeof config === "object", "config must be an object");
 60 |   const { secretKey, logLevel, offline, host, apiBaseUrl } = config;
 61 | 
 62 |   ok(
 63 |     typeof secretKey === "undefined" || typeof secretKey === "string",
 64 |     "secret must be a string",
 65 |   );
 66 |   ok(
 67 |     typeof apiBaseUrl === "undefined" || typeof apiBaseUrl === "string",
 68 |     "apiBaseUrl must be a string",
 69 |   );
 70 |   ok(
 71 |     typeof logLevel === "undefined" ||
 72 |       (typeof logLevel === "string" && LOG_LEVELS.includes(logLevel as any)),
 73 |     `logLevel must one of ${LOG_LEVELS.join(", ")}`,
 74 |   );
 75 |   ok(
 76 |     typeof offline === "undefined" || typeof offline === "boolean",
 77 |     "offline must be a boolean",
 78 |   );
 79 | 
 80 |   return {
 81 |     flagOverrides: parseOverrides(config),
 82 |     secretKey,
 83 |     logLevel,
 84 |     offline,
 85 |     apiBaseUrl: host ?? apiBaseUrl,
 86 |   };
 87 | }
 88 | 
 89 | function loadEnvVars() {
 90 |   const secretKey = process.env.REFLAG_SECRET_KEY;
 91 |   const enabledFlags = process.env.REFLAG_FLAGS_ENABLED;
 92 |   const disabledFlags = process.env.REFLAG_FLAGS_DISABLED;
 93 |   const logLevel = process.env.REFLAG_LOG_LEVEL;
 94 |   const apiBaseUrl = process.env.REFLAG_API_BASE_URL;
 95 |   const offline =
 96 |     process.env.REFLAG_OFFLINE !== undefined
 97 |       ? ["true", "on"].includes(process.env.REFLAG_OFFLINE)
 98 |       : undefined;
 99 | 
100 |   let flagOverrides: Record<string, boolean> = {};
101 |   if (enabledFlags) {
102 |     flagOverrides = enabledFlags.split(",").reduce(
103 |       (acc, f) => {
104 |         const key = f.trim();
105 |         if (key) acc[key] = true;
106 |         return acc;
107 |       },
108 |       {} as Record<string, boolean>,
109 |     );
110 |   }
111 | 
112 |   if (disabledFlags) {
113 |     flagOverrides = {
114 |       ...flagOverrides,
115 |       ...disabledFlags.split(",").reduce(
116 |         (acc, f) => {
117 |           const key = f.trim();
118 |           if (key) acc[key] = false;
119 |           return acc;
120 |         },
121 |         {} as Record<string, boolean>,
122 |       ),
123 |     };
124 |   }
125 | 
126 |   return { secretKey, flagOverrides, logLevel, offline, apiBaseUrl };
127 | }
128 | 
129 | export function loadConfig(file?: string) {
130 |   let fileConfig;
131 |   if (file) {
132 |     fileConfig = loadConfigFile(file);
133 |   }
134 | 
135 |   const envConfig = loadEnvVars();
136 | 
137 |   return {
138 |     secretKey: envConfig.secretKey || fileConfig?.secretKey,
139 |     logLevel: envConfig.logLevel || fileConfig?.logLevel,
140 |     offline: envConfig.offline ?? fileConfig?.offline,
141 |     apiBaseUrl: envConfig.apiBaseUrl ?? fileConfig?.apiBaseUrl,
142 |     flagOverrides: {
143 |       ...fileConfig?.flagOverrides,
144 |       ...envConfig.flagOverrides,
145 |     },
146 |   };
147 | }
148 | 
```

--------------------------------------------------------------------------------
/packages/browser-sdk/src/toolbar/Flags.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import { Fragment, h } from "preact";
  2 | 
  3 | import { Switch } from "./Switch";
  4 | import { FlagItem } from "./Toolbar";
  5 | 
  6 | const isFound = (flagKey: string, searchQuery: string | null) => {
  7 |   return flagKey.toLocaleLowerCase().includes(searchQuery ?? "");
  8 | };
  9 | 
 10 | export function FlagsTable({
 11 |   flags,
 12 |   searchQuery,
 13 |   appBaseUrl,
 14 |   setIsEnabledOverride,
 15 | }: {
 16 |   flags: FlagItem[];
 17 |   searchQuery: string | null;
 18 |   appBaseUrl: string;
 19 |   setIsEnabledOverride: (key: string, isEnabled: boolean | null) => void;
 20 | }) {
 21 |   const hasFlags = flags.length > 0;
 22 |   const hasShownFlags = flags.some((flag) =>
 23 |     isFound(flag.flagKey, searchQuery),
 24 |   );
 25 | 
 26 |   // List flags that match the search query first then alphabetically
 27 |   const searchedFlags =
 28 |     searchQuery === null
 29 |       ? flags
 30 |       : [...flags].sort((a, b) => {
 31 |           const aMatches = isFound(a.flagKey, searchQuery);
 32 |           const bMatches = isFound(b.flagKey, searchQuery);
 33 | 
 34 |           // If both match or both don't match, sort alphabetically
 35 |           if (aMatches === bMatches) {
 36 |             const aStartsWith = a.flagKey
 37 |               .toLocaleLowerCase()
 38 |               .startsWith(searchQuery);
 39 |             const bStartsWith = b.flagKey
 40 |               .toLocaleLowerCase()
 41 |               .startsWith(searchQuery);
 42 | 
 43 |             // If one starts with search query and the other doesn't, prioritize the one that starts with it
 44 |             if (aStartsWith && !bStartsWith) return -1;
 45 |             if (bStartsWith && !aStartsWith) return 1;
 46 | 
 47 |             // Otherwise sort alphabetically
 48 |             return a.flagKey.localeCompare(b.flagKey);
 49 |           }
 50 | 
 51 |           // Otherwise, matching flags come first
 52 |           return aMatches ? -1 : 1;
 53 |         });
 54 | 
 55 |   return (
 56 |     <Fragment>
 57 |       {(!hasFlags || !hasShownFlags) && (
 58 |         <div class="flags-table-empty">
 59 |           No flags {hasFlags ? `matching "${searchQuery}"` : "found"}
 60 |         </div>
 61 |       )}
 62 |       <table class="flags-table">
 63 |         <tbody>
 64 |           {searchedFlags.map((flag, index) => (
 65 |             <FlagRow
 66 |               key={flag.flagKey}
 67 |               appBaseUrl={appBaseUrl}
 68 |               flag={flag}
 69 |               index={index}
 70 |               isNotVisible={
 71 |                 searchQuery !== null && !isFound(flag.flagKey, searchQuery)
 72 |               }
 73 |               setEnabledOverride={(override) =>
 74 |                 setIsEnabledOverride(flag.flagKey, override)
 75 |               }
 76 |             />
 77 |           ))}
 78 |         </tbody>
 79 |       </table>
 80 |     </Fragment>
 81 |   );
 82 | }
 83 | 
 84 | function FlagRow({
 85 |   setEnabledOverride,
 86 |   appBaseUrl,
 87 |   flag,
 88 |   index,
 89 |   isNotVisible,
 90 | }: {
 91 |   flag: FlagItem;
 92 |   appBaseUrl: string;
 93 |   setEnabledOverride: (isEnabled: boolean | null) => void;
 94 |   index: number;
 95 |   isNotVisible: boolean;
 96 | }) {
 97 |   const isEnabledOverride = flag.isEnabledOverride !== null;
 98 |   return (
 99 |     <tr
100 |       key={flag.flagKey}
101 |       class={["flag-row", isNotVisible ? "not-visible" : undefined].join(" ")}
102 |     >
103 |       <td class="flag-name-cell">
104 |         <a
105 |           class="flag-link"
106 |           href={`${appBaseUrl}/env-current/flags/by-key/${flag.flagKey}`}
107 |           rel="noreferrer"
108 |           tabIndex={index + 1}
109 |           target="_blank"
110 |         >
111 |           {flag.flagKey}
112 |         </a>
113 |       </td>
114 |       <td class="flag-reset-cell">
115 |         {isEnabledOverride ? (
116 |           <Reset setEnabledOverride={setEnabledOverride} tabIndex={index + 1} />
117 |         ) : null}
118 |       </td>
119 |       <td class="flag-switch-cell">
120 |         <Switch
121 |           checked={flag.isEnabledOverride ?? flag.isEnabled}
122 |           tabIndex={index + 1}
123 |           onChange={(e) => {
124 |             const isChecked = e.currentTarget.checked;
125 |             setEnabledOverride(!isEnabledOverride ? isChecked : null);
126 |           }}
127 |         />
128 |       </td>
129 |     </tr>
130 |   );
131 | }
132 | 
133 | export function FlagSearch({ onSearch }: { onSearch: (val: string) => void }) {
134 |   return (
135 |     <input
136 |       class="search-input"
137 |       placeholder="Search flags"
138 |       tabIndex={0}
139 |       type="search"
140 |       autoFocus
141 |       onInput={(s) => onSearch(s.currentTarget.value)}
142 |     />
143 |   );
144 | }
145 | 
146 | function Reset({
147 |   setEnabledOverride,
148 |   ...props
149 | }: {
150 |   setEnabledOverride: (isEnabled: boolean | null) => void;
151 | } & h.JSX.HTMLAttributes<HTMLAnchorElement>) {
152 |   return (
153 |     <a
154 |       class="reset"
155 |       href=""
156 |       onClick={(e) => {
157 |         e.preventDefault();
158 |         setEnabledOverride(null);
159 |       }}
160 |       {...props}
161 |     >
162 |       reset
163 |     </a>
164 |   );
165 | }
166 | 
```

--------------------------------------------------------------------------------
/packages/node-sdk/test/periodicallyUpdatingCache.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import {
  2 |   afterAll,
  3 |   afterEach,
  4 |   beforeAll,
  5 |   beforeEach,
  6 |   describe,
  7 |   expect,
  8 |   it,
  9 |   vi,
 10 | } from "vitest";
 11 | 
 12 | import cache from "../src/periodicallyUpdatingCache";
 13 | import { Logger } from "../src/types";
 14 | 
 15 | describe("cache", () => {
 16 |   let fn: () => Promise<number>;
 17 |   let logger: Logger;
 18 | 
 19 |   beforeAll(() => {
 20 |     vi.useFakeTimers({ shouldAdvanceTime: true });
 21 |   });
 22 | 
 23 |   afterAll(() => {
 24 |     vi.useRealTimers();
 25 |   });
 26 | 
 27 |   beforeEach(() => {
 28 |     fn = vi.fn().mockResolvedValue(42);
 29 |     logger = {
 30 |       debug: vi.fn(),
 31 |       info: vi.fn(),
 32 |       warn: vi.fn(),
 33 |       error: vi.fn(),
 34 |     };
 35 |   });
 36 | 
 37 |   it("should update the cached value when refreshing", async () => {
 38 |     const cached = cache(1000, 2000, logger, fn);
 39 | 
 40 |     const result = await cached.refresh();
 41 | 
 42 |     expect(result).toBe(42);
 43 |     expect(logger.debug).toHaveBeenCalledWith(
 44 |       expect.stringMatching("updated cached value"),
 45 |       42,
 46 |     );
 47 |   });
 48 | 
 49 |   it("should not allow multiple refreses at the same time", async () => {
 50 |     const cached = cache(1000, 2000, logger, fn);
 51 | 
 52 |     void cached.refresh();
 53 |     void cached.refresh();
 54 |     void cached.refresh();
 55 |     await cached.refresh();
 56 | 
 57 |     expect(fn).toHaveBeenCalledTimes(1);
 58 |     expect(logger.debug).toHaveBeenNthCalledWith(
 59 |       1,
 60 |       expect.stringMatching("updated cached value"),
 61 |       42,
 62 |     );
 63 | 
 64 |     void cached.refresh();
 65 |     await cached.refresh();
 66 | 
 67 |     expect(fn).toHaveBeenCalledTimes(2);
 68 |     expect(logger.debug).toHaveBeenNthCalledWith(
 69 |       2,
 70 |       expect.stringMatching("updated cached value"),
 71 |       42,
 72 |     );
 73 |   });
 74 | 
 75 |   it("should warn if the cached value is stale", async () => {
 76 |     const cached = cache(1000, 2000, logger, fn);
 77 | 
 78 |     await cached.refresh();
 79 | 
 80 |     vi.advanceTimersByTime(2500);
 81 | 
 82 |     const result = cached.get();
 83 | 
 84 |     expect(result).toBe(42);
 85 |     expect(logger.warn).toHaveBeenCalledWith(
 86 |       expect.stringMatching("cached value is stale"),
 87 |       {
 88 |         age: expect.any(Number),
 89 |         cachedValue: 42,
 90 |       },
 91 |     );
 92 |   });
 93 | 
 94 |   it("should update the cached value after ttl", async () => {
 95 |     const newValue = 84;
 96 |     fn = vi.fn().mockResolvedValueOnce(42).mockResolvedValueOnce(newValue);
 97 | 
 98 |     const cached = cache(1000, 2000, logger, fn);
 99 | 
100 |     const first = await cached.refresh();
101 | 
102 |     expect(first).toBe(42);
103 |     expect(fn).toHaveBeenCalledTimes(1);
104 | 
105 |     await vi.advanceTimersToNextTimerAsync();
106 | 
107 |     const second = cached.get();
108 | 
109 |     expect(second).toBe(newValue);
110 |     expect(fn).toHaveBeenCalledTimes(2);
111 | 
112 |     expect(logger.debug).toHaveBeenCalledWith(
113 |       expect.stringMatching("updated cached value"),
114 |       newValue,
115 |     );
116 |   });
117 | 
118 |   it("should handle update failures gracefully", async () => {
119 |     const error = new Error("update failed");
120 |     fn = vi.fn().mockRejectedValueOnce(error).mockResolvedValueOnce(42);
121 | 
122 |     const cached = cache(1000, 2000, logger, fn);
123 | 
124 |     const first = await cached.refresh();
125 | 
126 |     expect(first).toBeUndefined();
127 |     expect(logger.error).toHaveBeenCalledWith(
128 |       expect.stringMatching("failed to update cached value"),
129 |       error,
130 |     );
131 |     expect(fn).toHaveBeenCalledTimes(1);
132 | 
133 |     await vi.advanceTimersToNextTimerAsync();
134 | 
135 |     expect(fn).toHaveBeenCalledTimes(2);
136 |     expect(logger.debug).toHaveBeenCalledWith(
137 |       expect.stringMatching("updated cached value"),
138 |       42,
139 |     );
140 | 
141 |     const second = cached.get();
142 |     expect(second).toBe(42);
143 |   });
144 | 
145 |   it("should retain the cached value if the new value is undefined", async () => {
146 |     fn = vi.fn().mockResolvedValueOnce(42).mockResolvedValueOnce(undefined);
147 |     const cached = cache(1000, 2000, logger, fn);
148 | 
149 |     await cached.refresh();
150 | 
151 |     await vi.advanceTimersToNextTimerAsync();
152 | 
153 |     const second = cached.get();
154 |     expect(second).toBe(42);
155 | 
156 |     vi.advanceTimersByTime(2500);
157 | 
158 |     const result = cached.get();
159 | 
160 |     expect(result).toBe(42);
161 |     expect(logger.warn).toHaveBeenCalledWith(
162 |       expect.stringMatching("cached value is stale"),
163 |       {
164 |         age: expect.any(Number),
165 |         cachedValue: 42,
166 |       },
167 |     );
168 |   });
169 | 
170 |   it("should not update if cached value is still valid", async () => {
171 |     const cached = cache(1000, 2000, logger, fn);
172 | 
173 |     const first = await cached.refresh();
174 | 
175 |     vi.advanceTimersByTime(500);
176 | 
177 |     const second = cached.get();
178 | 
179 |     expect(first).toBe(second);
180 |     expect(logger.debug).toHaveBeenCalledTimes(1); // Only one update call
181 |   });
182 | 
183 |   afterEach(() => {
184 |     vi.clearAllTimers();
185 |     vi.restoreAllMocks();
186 |   });
187 | });
188 | 
```

--------------------------------------------------------------------------------
/packages/browser-sdk/src/toolbar/Toolbar.css:
--------------------------------------------------------------------------------

```css
  1 | /* Animations */
  2 | 
  3 | @keyframes bounceInUp {
  4 |   from,
  5 |   60%,
  6 |   75%,
  7 |   90%,
  8 |   to {
  9 |     animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
 10 |   }
 11 | 
 12 |   from {
 13 |     opacity: 0;
 14 |     transform: translate3d(0, 50px, 0) scaleY(2);
 15 |   }
 16 | 
 17 |   60% {
 18 |     opacity: 1;
 19 |     transform: translate3d(0, -6px, 0) scaleY(0.9);
 20 |   }
 21 | 
 22 |   75% {
 23 |     transform: translate3d(0, 3px, 0) scaleY(0.95);
 24 |   }
 25 | 
 26 |   90% {
 27 |     transform: translate3d(0, -2px, 0) scaleY(0.985);
 28 |   }
 29 | 
 30 |   to {
 31 |     transform: translate3d(0, 0, 0);
 32 |   }
 33 | }
 34 | 
 35 | @keyframes gelatine {
 36 |   from,
 37 |   to {
 38 |     transform: scale(1, 1);
 39 |   }
 40 |   25% {
 41 |     transform: scale(0.9, 1.1);
 42 |   }
 43 |   50% {
 44 |     transform: scale(1.1, 0.9);
 45 |   }
 46 |   75% {
 47 |     transform: scale(0.95, 1.05);
 48 |   }
 49 | }
 50 | 
 51 | /* Toolbar */
 52 | 
 53 | .toolbar {
 54 |   --brand300: #9cc4d3;
 55 |   --brand400: #77adc1;
 56 |   --gray500: #787c91;
 57 |   --gray600: #3c3d49;
 58 |   --gray700: #22232a;
 59 |   --gray800: #17181c;
 60 |   --gray900: #0e0e11;
 61 |   --gray950: #09090b;
 62 |   --black: #1e1f24;
 63 |   --white: white;
 64 | 
 65 |   --bg-color: var(--gray900);
 66 |   --bg-light-color: var(--gray700);
 67 |   --border-color: var(--gray700);
 68 |   --dimmed-color: var(--gray500);
 69 | 
 70 |   --logo-color: white;
 71 |   --text-color: white;
 72 |   --text-size: 13px;
 73 |   --text-small-size: 12px;
 74 |   font-family:
 75 |     system-ui,
 76 |     -apple-system,
 77 |     BlinkMacSystemFont,
 78 |     "Segoe UI",
 79 |     Roboto,
 80 |     Oxygen,
 81 |     Ubuntu,
 82 |     Cantarell,
 83 |     "Open Sans",
 84 |     "Helvetica Neue",
 85 |     sans-serif;
 86 |   font-size: var(--text-size);
 87 | }
 88 | 
 89 | :focus {
 90 |   outline: none;
 91 | }
 92 | 
 93 | .dialog {
 94 |   color: #ffffff;
 95 |   box-sizing: border-box;
 96 |   background: var(--bg-color);
 97 | 
 98 |   border: 0;
 99 |   box-shadow:
100 |     0 10px 15px -3px rgba(0, 0, 0, 0.15),
101 |     0 4px 6px -2px rgba(0, 0, 0, 0.1),
102 |     0 -1px rgba(255, 255, 255, 0.1),
103 |     0 0 0 1px var(--border-color);
104 |   border-radius: 7px;
105 |   z-index: 999999;
106 |   min-width: 240px;
107 |   padding: 0;
108 | 
109 |   --visible-flags: 15;
110 |   max-height: min(
111 |     calc(100vh - 36px - 35px),
112 |     calc(45px + (var(--visible-flags) * 27px))
113 |   );
114 |   height: auto;
115 | 
116 |   &[open] {
117 |     display: flex;
118 |     flex-direction: column;
119 |   }
120 | }
121 | 
122 | .dialog-content {
123 |   overflow-y: auto;
124 |   max-height: 100%;
125 |   flex-grow: 1;
126 |   margin: 3px 3px 3px 0;
127 | 
128 |   &::-webkit-scrollbar {
129 |     width: 8px;
130 |     height: 8px;
131 |   }
132 |   &::-webkit-scrollbar-track {
133 |     background: transparent;
134 |   }
135 |   &::-webkit-scrollbar-thumb {
136 |     background-color: rgba(255, 255, 255, 0.2);
137 |     border-radius: 999px;
138 |     transition: background-color 0.1s ease;
139 | 
140 |     &:hover {
141 |       background-color: rgba(255, 255, 255, 0.3);
142 |     }
143 |   }
144 |   &::-webkit-scrollbar-button {
145 |     display: none;
146 |   }
147 | }
148 | 
149 | .toolbar-toggle {
150 |   width: 36px;
151 |   height: 36px;
152 |   position: fixed;
153 |   z-index: 999999;
154 |   padding: 0;
155 |   margin: 0;
156 |   box-sizing: border-box;
157 | 
158 |   color: var(--logo-color);
159 |   background: var(--bg-color);
160 | 
161 |   box-shadow:
162 |     0 10px 15px -3px rgba(0, 0, 0, 0.15),
163 |     0 4px 6px -2px rgba(0, 0, 0, 0.1),
164 |     0 -1px rgba(255, 255, 255, 0.1),
165 |     0 0 0 1px var(--border-color);
166 |   border-radius: 999px;
167 | 
168 |   cursor: pointer;
169 | 
170 |   display: flex;
171 |   justify-content: center;
172 |   align-items: center;
173 | 
174 |   animation: bounceInUp 0.2s ease-out;
175 | 
176 |   transition: background 0.1s ease;
177 | 
178 |   &.open {
179 |     background: var(--bg-light-color);
180 |   }
181 | 
182 |   & .override-indicator {
183 |     position: absolute;
184 |     top: 1px;
185 |     right: 1px;
186 |     width: 8px;
187 |     height: 8px;
188 |     background-color: var(--brand400);
189 |     border-radius: 50%;
190 |     opacity: 0;
191 |     transition: opacity 0.1s ease-in-out;
192 |     box-shadow: inset 0px 1px rgba(255, 255, 255, 0.1);
193 | 
194 |     &.show {
195 |       opacity: 1;
196 |       animation: gelatine 0.5s;
197 |     }
198 |   }
199 | }
200 | 
201 | .toolbar-header-button {
202 |   background: none;
203 |   border: none;
204 |   color: var(--dimmed-color);
205 |   cursor: pointer;
206 |   border-radius: 4px;
207 |   transition: background-color 0.1s ease;
208 |   flex-shrink: 0;
209 |   display: flex;
210 |   align-items: center;
211 |   justify-content: center;
212 |   height: 28px;
213 |   width: 28px;
214 | 
215 |   &:hover {
216 |     background-color: var(--bg-light-color);
217 |     color: var(--text-color);
218 |   }
219 | 
220 |   &:focus-visible {
221 |     outline: 1px solid #fff;
222 |     outline-offset: 0px;
223 |   }
224 | 
225 |   & + .button-tooltip {
226 |     pointer-events: none;
227 |     opacity: 0;
228 | 
229 |     background: var(--bg-color);
230 |     color: var(--text-color);
231 |     padding: 6px 8px;
232 |     border-radius: 4px;
233 |     font-size: 13px;
234 |   }
235 | 
236 |   &:hover + .button-tooltip {
237 |     opacity: 1;
238 |   }
239 | }
240 | 
241 | [data-tooltip] {
242 |   position: relative;
243 | }
244 | 
245 | [data-tooltip]:after {
246 |   content: attr(data-tooltip);
247 |   position: absolute;
248 |   right: 100%;
249 |   top: 0%;
250 |   margin-right: 3px;
251 |   user-select: none;
252 |   pointer-events: none;
253 |   background-color: var(--bg-color);
254 |   border: 1px solid var(--border-color);
255 |   border-radius: 4px;
256 |   padding: 0px 6px;
257 |   height: 28px;
258 |   line-height: 26px;
259 |   color: var(--text-color);
260 |   font-size: var(--text-small-size);
261 |   font-weight: normal;
262 |   width: max-content;
263 |   display: none;
264 |   box-sizing: border-box;
265 | }
266 | 
267 | [data-tooltip]:hover:after {
268 |   display: block;
269 | }
270 | 
```

--------------------------------------------------------------------------------
/packages/browser-sdk/test/mocks/handlers.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { DefaultBodyType, http, HttpResponse, StrictRequest } from "msw";
  2 | 
  3 | import { RawFlags } from "../../src/flag/flags";
  4 | 
  5 | export const testChannel = "testChannel";
  6 | 
  7 | export const flagResponse = {
  8 |   success: true,
  9 |   features: {
 10 |     flagA: {
 11 |       isEnabled: true,
 12 |       key: "flagA",
 13 |       targetingVersion: 1,
 14 |       config: undefined,
 15 |       ruleEvaluationResults: [false, true],
 16 |       missingContextFields: ["field1", "field2"],
 17 |     },
 18 |     flagB: {
 19 |       isEnabled: true,
 20 |       targetingVersion: 11,
 21 |       key: "flagB",
 22 |       config: {
 23 |         version: 12,
 24 |         key: "gpt3",
 25 |         payload: { model: "gpt-something", temperature: 0.5 },
 26 |         ruleEvaluationResults: [true, false, false],
 27 |         missingContextFields: ["field3"],
 28 |       },
 29 |     },
 30 |   },
 31 | };
 32 | 
 33 | export const flagsResult = Object.entries(flagResponse.features).reduce(
 34 |   (acc, [key, flag]) => {
 35 |     acc[key] = {
 36 |       ...flag!,
 37 |       config: flag.config,
 38 |       isEnabledOverride: null,
 39 |     };
 40 |     return acc;
 41 |   },
 42 |   {} as RawFlags,
 43 | );
 44 | 
 45 | function checkRequest(request: StrictRequest<DefaultBodyType>) {
 46 |   const url = new URL(request.url);
 47 |   const hasKey =
 48 |     url.searchParams.get("publishableKey") ||
 49 |     request.headers.get("Authorization");
 50 | 
 51 |   const hasSdkVersion =
 52 |     url.searchParams.get("reflag-sdk-version") ||
 53 |     request.headers.get("reflag-sdk-version");
 54 | 
 55 |   const valid = hasKey && hasSdkVersion;
 56 |   if (!valid) {
 57 |     console.log(
 58 |       "missing token or sdk: " +
 59 |         request.url.toString() +
 60 |         " " +
 61 |         JSON.stringify(request.headers),
 62 |     );
 63 |   }
 64 |   return valid;
 65 | }
 66 | 
 67 | const invalidReqResponse = new HttpResponse("missing token or sdk", {
 68 |   status: 400,
 69 | });
 70 | 
 71 | export function getFlags({
 72 |   request,
 73 | }: {
 74 |   request: StrictRequest<DefaultBodyType>;
 75 | }) {
 76 |   if (!checkRequest(request)) return invalidReqResponse;
 77 | 
 78 |   return HttpResponse.json(flagResponse);
 79 | }
 80 | 
 81 | export const handlers = [
 82 |   http.post("https://front.reflag.com/user", async ({ request }) => {
 83 |     if (!checkRequest(request)) return invalidReqResponse;
 84 | 
 85 |     const data = await request.json();
 86 |     if (
 87 |       typeof data !== "object" ||
 88 |       !data ||
 89 |       !data["userId"] ||
 90 |       !data["attributes"]
 91 |     ) {
 92 |       return HttpResponse.error();
 93 |     }
 94 | 
 95 |     return HttpResponse.json({
 96 |       success: true,
 97 |     });
 98 |   }),
 99 |   http.post("https://front.reflag.com/company", async ({ request }) => {
100 |     if (!checkRequest(request)) return invalidReqResponse;
101 |     const data = await request.json();
102 | 
103 |     if (
104 |       typeof data !== "object" ||
105 |       !data ||
106 |       !data["companyId"] ||
107 |       !data["attributes"]
108 |     ) {
109 |       return HttpResponse.error();
110 |     }
111 | 
112 |     return HttpResponse.json({
113 |       success: true,
114 |     });
115 |   }),
116 |   http.post("https://front.reflag.com/event", async ({ request }) => {
117 |     if (!checkRequest(request)) return invalidReqResponse;
118 |     const data = await request.json();
119 | 
120 |     if (typeof data !== "object" || !data || !data["userId"]) {
121 |       return HttpResponse.error();
122 |     }
123 | 
124 |     return HttpResponse.json({
125 |       success: true,
126 |     });
127 |   }),
128 |   http.post("https://front.reflag.com/features/events", async ({ request }) => {
129 |     if (!checkRequest(request)) return invalidReqResponse;
130 |     const data = await request.json();
131 | 
132 |     if (typeof data !== "object" || !data || !data["userId"]) {
133 |       return HttpResponse.error();
134 |     }
135 | 
136 |     return HttpResponse.json({
137 |       success: true,
138 |     });
139 |   }),
140 |   http.post("https://front.reflag.com/feedback", async ({ request }) => {
141 |     if (!checkRequest(request)) return invalidReqResponse;
142 |     const data = await request.json();
143 |     if (
144 |       typeof data !== "object" ||
145 |       !data ||
146 |       !data["userId"] ||
147 |       typeof data["score"] !== "number" ||
148 |       (!data["featureId"] && !data["key"])
149 |     ) {
150 |       return HttpResponse.error();
151 |     }
152 | 
153 |     return HttpResponse.json({
154 |       success: true,
155 |     });
156 |   }),
157 |   http.get("https://front.reflag.com/features/enabled", getFlags),
158 |   http.get("https://front.reflag.com/features/evaluated", getFlags),
159 |   http.post(
160 |     "https://front.reflag.com/feedback/prompting-init",
161 |     ({ request }) => {
162 |       if (!checkRequest(request)) return invalidReqResponse;
163 | 
164 |       return HttpResponse.json({ success: true, channel: testChannel });
165 |     },
166 |   ),
167 |   http.get(
168 |     "https://front.reflag.com/feedback/prompting-auth",
169 |     ({ request }) => {
170 |       if (!checkRequest(request)) return invalidReqResponse;
171 |       return HttpResponse.json({ success: true, keyName: "keyName" });
172 |     },
173 |   ),
174 |   http.post(
175 |     "https://livemessaging.bucket.co/keys/keyName/requestToken",
176 |     async ({ request }) => {
177 |       const data = await request.json();
178 |       if (typeof data !== "object") {
179 |         return HttpResponse.error();
180 |       }
181 | 
182 |       return HttpResponse.json({
183 |         success: true,
184 |         token: "token",
185 |         expires: 1234567890,
186 |       });
187 |     },
188 |   ),
189 | ];
190 | 
```

--------------------------------------------------------------------------------
/packages/eslint-config/base.js:
--------------------------------------------------------------------------------

```javascript
  1 | const jsPlugin = require("@eslint/js");
  2 | const importsPlugin = require("eslint-plugin-import");
  3 | const unusedImportsPlugin = require("eslint-plugin-unused-imports");
  4 | const sortImportsPlugin = require("eslint-plugin-simple-import-sort");
  5 | const { builtinModules } = require("module");
  6 | const globals = require("globals");
  7 | const tsPlugin = require("@typescript-eslint/eslint-plugin");
  8 | const tsParser = require("@typescript-eslint/parser");
  9 | const prettierConfig = require("eslint-config-prettier");
 10 | const vuePlugin = require("eslint-plugin-vue");
 11 | 
 12 | module.exports = [
 13 |   {
 14 |     // Blacklisted Folders, including **/node_modules/ and .git/
 15 |     ignores: ["build/", "**/gen"],
 16 |   },
 17 |   {
 18 |     // All files
 19 |     files: [
 20 |       "**/*.js",
 21 |       "**/*.cjs",
 22 |       "**/*.mjs",
 23 |       "**/*.jsx",
 24 |       "**/*.ts",
 25 |       "**/*.tsx",
 26 |       "**/*.vue",
 27 |     ],
 28 |     plugins: {
 29 |       import: importsPlugin,
 30 |       "unused-imports": unusedImportsPlugin,
 31 |       "simple-import-sort": sortImportsPlugin,
 32 |     },
 33 |     languageOptions: {
 34 |       globals: {
 35 |         ...globals.node,
 36 |         ...globals.browser,
 37 |       },
 38 |       parserOptions: {
 39 |         // Eslint doesn't supply ecmaVersion in `parser.js` `context.parserOptions`
 40 |         // This is required to avoid ecmaVersion < 2015 error or 'import' / 'export' error
 41 |         sourceType: "module",
 42 |         ecmaVersion: "latest",
 43 |         ecmaFeatures: {
 44 |           modules: true,
 45 |           impliedStrict: true,
 46 |           jsx: true,
 47 |         },
 48 |       },
 49 |     },
 50 |     settings: {
 51 |       react: {
 52 |         version: "detect",
 53 |       },
 54 |       "import/parsers": {
 55 |         // Workaround until import supports flat config
 56 |         // https://github.com/import-js/eslint-plugin-import/issues/2556
 57 |         espree: [".js", ".cjs", ".mjs", ".jsx"],
 58 |       },
 59 |       "import/resolver": {
 60 |         typescript: {
 61 |           alwaysTryTypes: true,
 62 |         },
 63 |       },
 64 |     },
 65 |     rules: {
 66 |       ...jsPlugin.configs.recommended.rules,
 67 |       ...importsPlugin.configs.recommended.rules,
 68 | 
 69 |       // Imports
 70 |       "unused-imports/no-unused-vars": [
 71 |         "warn",
 72 |         {
 73 |           vars: "all",
 74 |           varsIgnorePattern: "^_",
 75 |           args: "after-used",
 76 |           argsIgnorePattern: "^_",
 77 |         },
 78 |       ],
 79 |       "unused-imports/no-unused-imports": ["warn"],
 80 |       "import/first": ["warn"],
 81 |       "import/newline-after-import": ["warn"],
 82 |       "import/no-named-as-default": ["off"],
 83 |       "simple-import-sort/exports": ["warn"],
 84 |       "simple-import-sort/imports": [
 85 |         "warn",
 86 |         {
 87 |           groups: [
 88 |             // Side effect imports.
 89 |             ["^\\u0000"],
 90 |             // Node.js builtins, react, and third-party packages.
 91 |             [
 92 |               `^(${builtinModules.join("|")})(/|$)`,
 93 |               "^react",
 94 |               "^(?!@reflag)@?\\w",
 95 |             ],
 96 |             // Shared reflag packages.
 97 |             ["^@reflag/(.*)$"],
 98 |             // Path aliased root, parent imports, and just `..`.
 99 |             ["^@/", "^\\.\\.(?!/?$)", "^\\.\\./?$"],
100 |             // Relative imports, same-folder imports, and just `.`.
101 |             ["^\\./(?=.*/)(?!/?$)", "^\\.(?!/?$)", "^\\./?$"],
102 |             // Style imports.
103 |             ["^.+\\.s?css$"],
104 |           ],
105 |         },
106 |       ],
107 |     },
108 |   },
109 |   {
110 |     // TypeScript files
111 |     files: ["**/*.ts", "**/*.tsx"],
112 |     plugins: {
113 |       "@typescript-eslint": tsPlugin,
114 |       vue: vuePlugin,
115 |     },
116 |     languageOptions: {
117 |       parser: tsParser,
118 |       parserOptions: {
119 |         project: "./tsconfig.eslint.json",
120 |       },
121 |     },
122 |     settings: {
123 |       ...importsPlugin.configs.typescript.settings,
124 |       "import/resolver": {
125 |         ...importsPlugin.configs.typescript.settings["import/resolver"],
126 |         typescript: {
127 |           project: "./tsconfig.json",
128 |         },
129 |       },
130 |     },
131 |     rules: {
132 |       ...importsPlugin.configs.typescript.rules,
133 |       ...tsPlugin.configs["eslint-recommended"].overrides[0].rules,
134 |       ...tsPlugin.configs.recommended.rules,
135 | 
136 |       // Typescript Specific
137 |       "@typescript-eslint/no-unused-vars": ["off"], // handled by unused-imports
138 |       "@typescript-eslint/explicit-module-boundary-types": ["off"],
139 |       "@typescript-eslint/no-floating-promises": ["error"],
140 |       "@typescript-eslint/switch-exhaustiveness-check": ["warn"],
141 |       "@typescript-eslint/no-non-null-assertion": ["off"],
142 |       "@typescript-eslint/no-empty-function": ["warn"],
143 |       "@typescript-eslint/no-explicit-any": ["off"],
144 |       "@typescript-eslint/no-use-before-define": ["off"],
145 |       "@typescript-eslint/no-shadow": ["warn"],
146 |     },
147 |   },
148 | 
149 |   {
150 |     files: ["**/*.tsx"],
151 |     rules: {
152 |       "react/prop-types": "off",
153 |     },
154 |   },
155 |   {
156 |     // Prettier Overrides
157 |     files: [
158 |       "**/*.js",
159 |       "**/*.cjs",
160 |       "**/*.mjs",
161 |       "**/*.jsx",
162 |       "**/*.ts",
163 |       "**/*.tsx",
164 |       "**/*.vue",
165 |     ],
166 |     rules: {
167 |       ...prettierConfig.rules,
168 |     },
169 |   },
170 | ];
171 | 
```

--------------------------------------------------------------------------------
/packages/browser-sdk/src/ui/packages/floating-ui-preact-dom/useFloating.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { computePosition } from "@floating-ui/dom";
  2 | import {
  3 |   useCallback,
  4 |   useLayoutEffect,
  5 |   useMemo,
  6 |   useRef,
  7 |   useState,
  8 | } from "preact/hooks";
  9 | 
 10 | import { deepEqual } from "./utils/deepEqual";
 11 | import { getDPR } from "./utils/getDPR";
 12 | import { roundByDPR } from "./utils/roundByDPR";
 13 | import { useLatestRef } from "./utils/useLatestRef";
 14 | import type {
 15 |   ComputePositionConfig,
 16 |   ReferenceType,
 17 |   UseFloatingData,
 18 |   UseFloatingOptions,
 19 |   UseFloatingReturn,
 20 | } from "./types";
 21 | 
 22 | /**
 23 |  * Provides data to position a floating element.
 24 |  * @see https://floating-ui.com/docs/react
 25 |  */
 26 | export function useFloating<RT extends ReferenceType = ReferenceType>(
 27 |   options: UseFloatingOptions = {},
 28 | ): UseFloatingReturn<RT> {
 29 |   const {
 30 |     placement = "bottom",
 31 |     strategy = "absolute",
 32 |     middleware = [],
 33 |     platform,
 34 |     elements: { reference: externalReference, floating: externalFloating } = {},
 35 |     transform = true,
 36 |     whileElementsMounted,
 37 |     open,
 38 |   } = options;
 39 | 
 40 |   const [data, setData] = useState<UseFloatingData>({
 41 |     x: 0,
 42 |     y: 0,
 43 |     strategy,
 44 |     placement,
 45 |     middlewareData: {},
 46 |     isPositioned: false,
 47 |   });
 48 | 
 49 |   const [latestMiddleware, setLatestMiddleware] = useState(middleware);
 50 | 
 51 |   if (!deepEqual(latestMiddleware, middleware)) {
 52 |     setLatestMiddleware(middleware);
 53 |   }
 54 | 
 55 |   const [_reference, _setReference] = useState<RT | null>(null);
 56 |   const [_floating, _setFloating] = useState<HTMLElement | null>(null);
 57 | 
 58 |   const setReference = useCallback(
 59 |     (node: RT | null) => {
 60 |       if (node != referenceRef.current) {
 61 |         referenceRef.current = node;
 62 |         _setReference(node);
 63 |       }
 64 |     },
 65 |     [_setReference],
 66 |   );
 67 | 
 68 |   const setFloating = useCallback(
 69 |     (node: HTMLElement | null) => {
 70 |       if (node !== floatingRef.current) {
 71 |         floatingRef.current = node;
 72 |         _setFloating(node);
 73 |       }
 74 |     },
 75 |     [_setFloating],
 76 |   );
 77 | 
 78 |   const referenceEl = (externalReference || _reference) as RT | null;
 79 |   const floatingEl = externalFloating || _floating;
 80 | 
 81 |   const referenceRef = useRef<RT | null>(null);
 82 |   const floatingRef = useRef<HTMLElement | null>(null);
 83 |   const dataRef = useRef(data);
 84 | 
 85 |   const whileElementsMountedRef = useLatestRef(whileElementsMounted);
 86 |   const platformRef = useLatestRef(platform);
 87 | 
 88 |   const update = useCallback(() => {
 89 |     if (!referenceRef.current || !floatingRef.current) {
 90 |       return;
 91 |     }
 92 | 
 93 |     const config: ComputePositionConfig = {
 94 |       placement,
 95 |       strategy,
 96 |       middleware: latestMiddleware,
 97 |     };
 98 | 
 99 |     if (platformRef.current) {
100 |       config.platform = platformRef.current;
101 |     }
102 | 
103 |     /*eslint-disable-next-line @typescript-eslint/no-floating-promises*/
104 |     computePosition(referenceRef.current, floatingRef.current, config).then(
105 |       (positionData) => {
106 |         const fullData = { ...positionData, isPositioned: true };
107 |         if (isMountedRef.current && !deepEqual(dataRef.current, fullData)) {
108 |           dataRef.current = fullData;
109 |           setData(fullData);
110 |         }
111 |       },
112 |     );
113 |   }, [latestMiddleware, placement, strategy, platformRef]);
114 | 
115 |   useLayoutEffect(() => {
116 |     if (open === false && dataRef.current.isPositioned) {
117 |       dataRef.current.isPositioned = false;
118 |       setData((positionData) => ({ ...positionData, isPositioned: false }));
119 |     }
120 |   }, [open]);
121 | 
122 |   const isMountedRef = useRef(false);
123 |   useLayoutEffect(() => {
124 |     isMountedRef.current = true;
125 |     return () => {
126 |       isMountedRef.current = false;
127 |     };
128 |   }, []);
129 | 
130 |   useLayoutEffect(() => {
131 |     if (referenceEl) referenceRef.current = referenceEl;
132 |     if (floatingEl) floatingRef.current = floatingEl;
133 | 
134 |     if (referenceEl && floatingEl) {
135 |       if (whileElementsMountedRef.current) {
136 |         return whileElementsMountedRef.current(referenceEl, floatingEl, update);
137 |       } else {
138 |         return update();
139 |       }
140 |     }
141 |   }, [referenceEl, floatingEl, update, whileElementsMountedRef]);
142 | 
143 |   const refs = useMemo(
144 |     () => ({
145 |       reference: referenceRef,
146 |       floating: floatingRef,
147 |       setReference,
148 |       setFloating,
149 |     }),
150 |     [setReference, setFloating],
151 |   );
152 | 
153 |   const elements = useMemo(
154 |     () => ({ reference: referenceEl, floating: floatingEl }),
155 |     [referenceEl, floatingEl],
156 |   );
157 | 
158 |   const floatingStyles = useMemo(() => {
159 |     const initialStyles = {
160 |       position: strategy,
161 |       left: 0,
162 |       top: 0,
163 |     };
164 | 
165 |     if (!elements.floating) {
166 |       return initialStyles;
167 |     }
168 | 
169 |     const x = roundByDPR(elements.floating, data.x);
170 |     const y = roundByDPR(elements.floating, data.y);
171 | 
172 |     if (transform) {
173 |       return {
174 |         ...initialStyles,
175 |         transform: `translate(${x}px, ${y}px)`,
176 |         ...(getDPR(elements.floating) >= 1.5 && { willChange: "transform" }),
177 |       };
178 |     }
179 | 
180 |     return {
181 |       position: strategy,
182 |       left: x,
183 |       top: y,
184 |     };
185 |   }, [strategy, transform, elements.floating, data.x, data.y]);
186 | 
187 |   return useMemo(
188 |     () => ({
189 |       ...data,
190 |       update,
191 |       refs,
192 |       elements,
193 |       floatingStyles,
194 |     }),
195 |     [data, update, refs, elements, floatingStyles],
196 |   );
197 | }
198 | 
```

--------------------------------------------------------------------------------
/packages/browser-sdk/src/feedback/ui/StarRating.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import { Fragment, FunctionComponent, h } from "preact";
  2 | import { useRef } from "preact/hooks";
  3 | 
  4 | import { Dissatisfied } from "../../ui/icons/Dissatisfied";
  5 | import { Neutral } from "../../ui/icons/Neutral";
  6 | import { Satisfied } from "../../ui/icons/Satisfied";
  7 | import { VeryDissatisfied } from "../../ui/icons/VeryDissatisfied";
  8 | import { VerySatisfied } from "../../ui/icons/VerySatisfied";
  9 | import {
 10 |   arrow,
 11 |   offset,
 12 |   useFloating,
 13 | } from "../../ui/packages/floating-ui-preact-dom";
 14 | 
 15 | import { FeedbackTranslations } from "./types";
 16 | 
 17 | const scores = [
 18 |   {
 19 |     color: "var(--reflag-feedback-dialog-rating-1-color, #dd6b20)",
 20 |     bg: "var(--reflag-feedback-dialog-rating-1-background-color, #fbd38d)",
 21 |     icon: <VeryDissatisfied />,
 22 |     getLabel: (t: FeedbackTranslations) => t.ScoreVeryDissatisfiedLabel,
 23 |     value: 1,
 24 |   },
 25 |   {
 26 |     color: "var(--reflag-feedback-dialog-rating-2-color, #ed8936)",
 27 |     bg: "var(--reflag-feedback-dialog-rating-2-background-color, #feebc8)",
 28 |     icon: <Dissatisfied />,
 29 |     getLabel: (t: FeedbackTranslations) => t.ScoreDissatisfiedLabel,
 30 |     value: 2,
 31 |   },
 32 |   {
 33 |     color: "var(--reflag-feedback-dialog-rating-3-color, #787c91)",
 34 |     bg: "var(--reflag-feedback-dialog-rating-3-background-color, #e9e9ed)",
 35 |     icon: <Neutral />,
 36 |     getLabel: (t: FeedbackTranslations) => t.ScoreNeutralLabel,
 37 |     value: 3,
 38 |   },
 39 |   {
 40 |     color: "var(--reflag-feedback-dialog-rating-4-color, #48bb78)",
 41 |     bg: "var(--reflag-feedback-dialog-rating-4-background-color, #c6f6d5)",
 42 |     icon: <Satisfied />,
 43 |     getLabel: (t: FeedbackTranslations) => t.ScoreSatisfiedLabel,
 44 |     value: 4,
 45 |   },
 46 |   {
 47 |     color: "var(--reflag-feedback-dialog-rating-5-color, #38a169)",
 48 |     bg: "var(--reflag-feedback-dialog-rating-5-background-color, #9ae6b4)",
 49 |     icon: <VerySatisfied />,
 50 |     getLabel: (t: FeedbackTranslations) => t.ScoreVerySatisfiedLabel,
 51 |     value: 5,
 52 |   },
 53 | ] as const;
 54 | 
 55 | type ScoreNumber = (typeof scores)[number];
 56 | 
 57 | export type StarRatingProps = {
 58 |   name: string;
 59 |   selectedValue?: number;
 60 |   onChange?: h.JSX.GenericEventHandler<HTMLInputElement>;
 61 |   t: FeedbackTranslations;
 62 | };
 63 | 
 64 | export const StarRating: FunctionComponent<StarRatingProps> = ({
 65 |   t,
 66 |   name,
 67 |   selectedValue,
 68 |   onChange,
 69 | }) => {
 70 |   return (
 71 |     <div class="star-rating">
 72 |       <style>
 73 |         {scores.map(
 74 |           ({ bg, color }, index) => `
 75 |             .star-rating-icons > input:nth-of-type(${
 76 |               index + 1
 77 |             }):checked + .button {
 78 |               border-color: ${color};
 79 |             }
 80 | 
 81 |             .star-rating-icons > input:nth-of-type(${
 82 |               index + 1
 83 |             }):checked + .button > div {
 84 |               background-color: ${bg};
 85 |             }
 86 | 
 87 |             .star-rating-icons > input:nth-of-type(${
 88 |               index + 1
 89 |             }):checked ~ input:nth-of-type(${index + 2}) + .button {
 90 |               border-left-color: ${color};
 91 |             }
 92 |           `,
 93 |         )}
 94 |       </style>
 95 |       <div class="star-rating-icons">
 96 |         {scores.map((score) => (
 97 |           <Score
 98 |             key={score.value}
 99 |             isSelected={score.value === selectedValue}
100 |             name={name}
101 |             score={score}
102 |             t={t}
103 |             onChange={onChange}
104 |           />
105 |         ))}
106 |       </div>
107 |     </div>
108 |   );
109 | };
110 | 
111 | const Score = ({
112 |   isSelected,
113 |   name,
114 |   onChange,
115 |   score,
116 |   t,
117 | }: {
118 |   isSelected: boolean;
119 |   name: string;
120 |   onChange?: h.JSX.GenericEventHandler<HTMLInputElement>;
121 |   score: ScoreNumber;
122 |   t: FeedbackTranslations;
123 | }) => {
124 |   const arrowRef = useRef<HTMLDivElement>(null);
125 |   const { refs, floatingStyles, middlewareData } = useFloating({
126 |     placement: "top",
127 |     middleware: [
128 |       offset(4),
129 |       arrow({
130 |         element: arrowRef,
131 |       }),
132 |     ],
133 |   });
134 | 
135 |   return (
136 |     <>
137 |       <input
138 |         defaultChecked={isSelected}
139 |         id={`reflag-feedback-score-${score.value}`}
140 |         name={name}
141 |         type="radio"
142 |         value={score.value}
143 |         onChange={onChange}
144 |       />
145 |       <label
146 |         ref={refs.setReference}
147 |         aria-label={score.getLabel(t)}
148 |         class="button"
149 |         for={`reflag-feedback-score-${score.value}`}
150 |         style={{ color: score.color }}
151 |       >
152 |         <div
153 |           style={{
154 |             position: "absolute",
155 |             top: 0,
156 |             left: 0,
157 |             width: "100%",
158 |             height: "100%",
159 |             opacity: 0.2, // TODO: fix overflow
160 |             zIndex: 1,
161 |           }}
162 |         />
163 |         <span style={{ zIndex: 2, display: "flex", alignItems: "center" }}>
164 |           {score.icon}
165 |         </span>
166 |       </label>
167 |       <div ref={refs.setFloating} class="button-tooltip" style={floatingStyles}>
168 |         {score.getLabel(t)}
169 |         <div
170 |           ref={arrowRef}
171 |           class="button-tooltip-arrow"
172 |           style={{
173 |             left:
174 |               middlewareData.arrow?.x != null
175 |                 ? `${middlewareData.arrow.x}px`
176 |                 : "",
177 |             top:
178 |               middlewareData.arrow?.y != null
179 |                 ? `${middlewareData.arrow.y}px`
180 |                 : "",
181 |           }}
182 |         />
183 |       </div>
184 |     </>
185 |   );
186 | };
187 | 
```

--------------------------------------------------------------------------------
/packages/browser-sdk/test/prompts.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
  2 | 
  3 | import {
  4 |   parsePromptMessage,
  5 |   processPromptMessage,
  6 | } from "../src/feedback/prompts";
  7 | import {
  8 |   checkPromptMessageCompleted,
  9 |   markPromptMessageCompleted,
 10 | } from "../src/feedback/promptStorage";
 11 | 
 12 | vi.mock("../src/feedback/promptStorage", () => {
 13 |   return {
 14 |     markPromptMessageCompleted: vi.fn(),
 15 |     checkPromptMessageCompleted: vi.fn(),
 16 |   };
 17 | });
 18 | 
 19 | describe("parsePromptMessage", () => {
 20 |   test("will not parse invalid messages", () => {
 21 |     expect(parsePromptMessage(undefined)).toBeUndefined();
 22 |     expect(parsePromptMessage("invalid")).toBeUndefined();
 23 |     expect(
 24 |       parsePromptMessage({ showAfter: Date.now(), showBefore: Date.now() }),
 25 |     ).toBeUndefined();
 26 |     expect(
 27 |       parsePromptMessage({
 28 |         question: "",
 29 |         showAfter: Date.now(),
 30 |         showBefore: Date.now(),
 31 |       }),
 32 |     ).toBeUndefined();
 33 |     expect(
 34 |       parsePromptMessage({
 35 |         question: "hello?",
 36 |         showBefore: Date.now(),
 37 |         promptId: "123",
 38 |         featureId: "123",
 39 |       }),
 40 |     ).toBeUndefined();
 41 |     expect(
 42 |       parsePromptMessage({
 43 |         question: "hello?",
 44 |         showAfter: Date.now(),
 45 |         promptId: "123",
 46 |         featureId: "123",
 47 |       }),
 48 |     ).toBeUndefined();
 49 |     expect(
 50 |       parsePromptMessage({
 51 |         question: "hello?",
 52 |         showAfter: Date.now(),
 53 |         showBefore: Date.now(),
 54 |       }),
 55 |     ).toBeUndefined();
 56 |     expect(
 57 |       parsePromptMessage({
 58 |         question: "hello?",
 59 |         showAfter: Date.now(),
 60 |         showBefore: Date.now(),
 61 |         promptId: "123",
 62 |       }),
 63 |     ).toBeUndefined();
 64 |     expect(
 65 |       parsePromptMessage({
 66 |         question: "hello?",
 67 |         showAfter: Date.now(),
 68 |         showBefore: Date.now(),
 69 |         featureId: "123",
 70 |       }),
 71 |     ).toBeUndefined();
 72 |   });
 73 | 
 74 |   test("will parse valid messages", () => {
 75 |     const start = Date.parse("2021-01-01T00:00:00.000Z");
 76 |     const end = Date.parse("2021-01-01T10:00:00.000Z");
 77 | 
 78 |     expect(
 79 |       parsePromptMessage({
 80 |         question: "hello?",
 81 |         showAfter: start,
 82 |         showBefore: end,
 83 |         promptId: "123",
 84 |         featureId: "456",
 85 |       }),
 86 |     ).toEqual({
 87 |       question: "hello?",
 88 |       showAfter: new Date(start),
 89 |       showBefore: new Date(end),
 90 |       promptId: "123",
 91 |       featureId: "456",
 92 |     });
 93 |   });
 94 | });
 95 | 
 96 | describe("processPromptMessage", () => {
 97 |   const now = Date.now();
 98 |   const promptTemplate = {
 99 |     question: "hello?",
100 |     promptId: "123",
101 |     featureId: "456",
102 |   };
103 | 
104 |   beforeEach(() => {
105 |     vi.mocked(checkPromptMessageCompleted).mockReturnValue(false);
106 |     vi.useFakeTimers();
107 |   });
108 | 
109 |   afterEach(() => {
110 |     vi.useRealTimers();
111 |     vi.clearAllMocks();
112 |   });
113 | 
114 |   test("will not process seen prompts", () => {
115 |     vi.mocked(checkPromptMessageCompleted).mockReturnValue(true);
116 | 
117 |     const prompt = {
118 |       ...promptTemplate,
119 |       showAfter: new Date(now - 10000),
120 |       showBefore: new Date(now + 10000),
121 |     };
122 | 
123 |     const showCallback = vi.fn();
124 | 
125 |     expect(processPromptMessage("user", prompt, showCallback)).toBe(false);
126 | 
127 |     expect(showCallback).not.toHaveBeenCalled();
128 |     expect(markPromptMessageCompleted).not.toHaveBeenCalled();
129 |   });
130 | 
131 |   test("will not process expired prompts", () => {
132 |     const prompt = {
133 |       ...promptTemplate,
134 |       showAfter: new Date(now - 10000),
135 |       showBefore: new Date(now - 5000),
136 |     };
137 | 
138 |     const showCallback = vi.fn();
139 | 
140 |     expect(processPromptMessage("user", prompt, showCallback)).toBe(false);
141 | 
142 |     expect(showCallback).not.toHaveBeenCalled();
143 | 
144 |     expect(markPromptMessageCompleted).not.toHaveBeenCalled();
145 |   });
146 | 
147 |   test("will process prompts that are ready to be shown", () => {
148 |     const prompt = {
149 |       ...promptTemplate,
150 |       showAfter: new Date(now - 10000),
151 |       showBefore: new Date(now + 10000),
152 |     };
153 | 
154 |     const showCallback = vi
155 |       .fn()
156 |       .mockImplementation((_a, _b, actionedCallback) => {
157 |         actionedCallback();
158 |       });
159 | 
160 |     expect(processPromptMessage("user", prompt, showCallback)).toBe(true);
161 |     expect(showCallback).toHaveBeenCalledWith(
162 |       "user",
163 |       prompt,
164 |       expect.any(Function),
165 |     );
166 | 
167 |     expect(markPromptMessageCompleted).toHaveBeenCalledOnce();
168 |     expect(markPromptMessageCompleted).toBeCalledWith(
169 |       "user",
170 |       "123",
171 |       prompt.showBefore,
172 |     );
173 |   });
174 | 
175 |   test("will process and delay prompts that are not yet ready to be shown", () => {
176 |     const prompt = {
177 |       ...promptTemplate,
178 |       showAfter: new Date(now + 5000),
179 |       showBefore: new Date(now + 10000),
180 |     };
181 | 
182 |     const showCallback = vi
183 |       .fn()
184 |       .mockImplementation((_a, _b, actionedCallback) => {
185 |         actionedCallback();
186 |       });
187 | 
188 |     expect(processPromptMessage("user", prompt, showCallback)).toBe(true);
189 |     expect(showCallback).not.toHaveBeenCalled();
190 |     expect(localStorage.getItem("prompt-user")).toBeNull();
191 | 
192 |     vi.runAllTimers();
193 |     expect(showCallback).toHaveBeenCalledWith(
194 |       "user",
195 |       prompt,
196 |       expect.any(Function),
197 |     );
198 | 
199 |     expect(markPromptMessageCompleted).toHaveBeenCalledOnce();
200 |     expect(markPromptMessageCompleted).toBeCalledWith(
201 |       "user",
202 |       "123",
203 |       prompt.showBefore,
204 |     );
205 |   });
206 | });
207 | 
```

--------------------------------------------------------------------------------
/packages/cli/commands/mcp.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { select } from "@inquirer/prompts";
  2 | import chalk from "chalk";
  3 | import { Command } from "commander";
  4 | import { parse as parseJSON, stringify as stringifyJSON } from "comment-json";
  5 | import { mkdir, readFile, writeFile } from "node:fs/promises";
  6 | import { dirname, join, relative } from "node:path";
  7 | import ora, { type Ora } from "ora";
  8 | 
  9 | import {
 10 |   ConfigPaths,
 11 |   getServersConfig,
 12 |   resolveConfigPath,
 13 |   SupportedEditor,
 14 |   SupportedEditors,
 15 | } from "../services/mcp.js";
 16 | import { configStore } from "../stores/config.js";
 17 | import { handleError } from "../utils/errors.js";
 18 | import { fileExists } from "../utils/file.js";
 19 | import { configScopeOption, editorOption } from "../utils/options.js";
 20 | 
 21 | export const mcpAction = async (options: {
 22 |   editor?: SupportedEditor;
 23 |   scope?: "local" | "global";
 24 | }) => {
 25 |   const config = configStore.getConfig();
 26 |   let spinner: Ora | undefined;
 27 |   let selectedEditor = options.editor;
 28 | 
 29 |   // Select Editor/Client
 30 |   if (!selectedEditor) {
 31 |     selectedEditor = await select<SupportedEditor>({
 32 |       message: "Which editor do you want to configure?",
 33 |       choices: SupportedEditors.map((value) => ({
 34 |         value,
 35 |         name: ConfigPaths[value].name,
 36 |       })),
 37 |     });
 38 |   }
 39 | 
 40 |   // Determine Config Path
 41 |   const projectPath = configStore.getProjectPath();
 42 |   const globalPath = resolveConfigPath(selectedEditor, false);
 43 |   const localPath = resolveConfigPath(selectedEditor, true);
 44 |   const fullLocalPath = localPath ? join(projectPath, localPath) : undefined;
 45 | 
 46 |   if (!globalPath) {
 47 |     throw new Error(`Unsupported platform for editor: ${selectedEditor}`);
 48 |   }
 49 | 
 50 |   let configPathType: "global" | "local" = "global";
 51 |   if (fullLocalPath) {
 52 |     if (options.scope) {
 53 |       configPathType = options.scope;
 54 |     } else {
 55 |       configPathType = await select<"global" | "local">({
 56 |         message: "Configure global or project-local settings?",
 57 |         choices: [
 58 |           {
 59 |             name: `Local (${relative(projectPath, fullLocalPath)})`,
 60 |             value: "local",
 61 |           },
 62 |           { name: `Global (${globalPath})`, value: "global" },
 63 |         ],
 64 |       });
 65 |     }
 66 |   }
 67 |   const configPath = configPathType === "local" ? fullLocalPath! : globalPath;
 68 |   const displayConfigPath =
 69 |     configPathType === "local" ? relative(projectPath, configPath) : configPath;
 70 | 
 71 |   // Read/Parse Config File
 72 |   spinner = ora(
 73 |     `Reading configuration file: ${chalk.cyan(displayConfigPath)}...`,
 74 |   ).start();
 75 | 
 76 |   let editorConfig: any = {};
 77 |   if (await fileExists(configPath)) {
 78 |     const content = await readFile(configPath, "utf-8");
 79 |     // Attempt to parse JSON, handle potential comments if needed
 80 |     try {
 81 |       editorConfig = parseJSON(content);
 82 |     } catch {
 83 |       spinner.fail(
 84 |         `Failed to parse configuration file ${chalk.cyan(displayConfigPath)}.`,
 85 |       );
 86 |     }
 87 |     spinner.succeed(
 88 |       `Read configuration file ${chalk.cyan(displayConfigPath)}.`,
 89 |     );
 90 |   } else {
 91 |     spinner.info("Configuration file not found, will create a new one.");
 92 |     editorConfig = {}; // Initialize empty config if file doesn't exist
 93 |   }
 94 | 
 95 |   // Ensure MCP servers object exists
 96 |   const serversConfig = getServersConfig(
 97 |     editorConfig,
 98 |     selectedEditor,
 99 |     configPathType,
100 |   );
101 | 
102 |   // Check for existing Reflag servers
103 |   const existingReflagEntries = Object.keys(serversConfig).filter((key) =>
104 |     /reflag/i.test(key),
105 |   );
106 | 
107 |   // Prompt for Add/Update
108 |   let targetEntryKey: string;
109 |   const defaultNewKey = `Reflag`;
110 | 
111 |   if (existingReflagEntries.length === 0) {
112 |     targetEntryKey = defaultNewKey;
113 |     console.log(`Adding new MCP server entry: ${chalk.cyan(targetEntryKey)}`);
114 |   } else {
115 |     const choices = [
116 |       { name: `Add: ${defaultNewKey}`, value: "add_new" },
117 |       ...existingReflagEntries.map((key) => ({
118 |         name: `Update: ${key}`,
119 |         value: key,
120 |       })),
121 |     ];
122 | 
123 |     const choice = await select({
124 |       message: "Add a new MCP server or update an existing one?",
125 |       choices,
126 |     });
127 | 
128 |     if (choice === "add_new") {
129 |       targetEntryKey = defaultNewKey;
130 |       console.log(`Adding new MCP server entry: ${chalk.cyan(targetEntryKey)}`);
131 |     } else {
132 |       targetEntryKey = choice;
133 |       console.log(
134 |         `Updating existing MCP server entry: ${chalk.cyan(targetEntryKey)}`,
135 |       );
136 |     }
137 |   }
138 | 
139 |   // Construct the MCP endpoint URL
140 |   const newEntryValue = {
141 |     url: config.apiUrl + "/mcp",
142 |   };
143 | 
144 |   // Update Config Object
145 |   serversConfig[targetEntryKey] = newEntryValue;
146 | 
147 |   // Write Config File
148 |   spinner = ora(
149 |     `Writing configuration to ${chalk.cyan(displayConfigPath)}...`,
150 |   ).start();
151 | 
152 |   try {
153 |     // Ensure the directory exists before writing
154 |     await mkdir(dirname(configPath), { recursive: true });
155 |     const configString = stringifyJSON(editorConfig, null, 2);
156 | 
157 |     await writeFile(configPath, configString);
158 |     spinner.succeed(
159 |       `Configuration updated successfully in ${chalk.cyan(displayConfigPath)}.`,
160 |     );
161 | 
162 |     console.log(
163 |       chalk.grey(
164 |         "You may need to restart your editor for changes to take effect.",
165 |       ),
166 |     );
167 |   } catch (error) {
168 |     spinner.fail(
169 |       `Failed to write configuration file ${chalk.cyan(displayConfigPath)}.`,
170 |     );
171 | 
172 |     handleError(error, "MCP Configuration");
173 |   }
174 | };
175 | 
176 | export function registerMcpCommand(cli: Command) {
177 |   cli
178 |     .command("mcp")
179 |     .description("Configure Reflag's remote MCP server for your AI assistant.")
180 |     .addOption(editorOption)
181 |     .addOption(configScopeOption)
182 |     .action(mcpAction);
183 | }
184 | 
```

--------------------------------------------------------------------------------
/packages/react-sdk/dev/nextjs-flag-demo/app/page.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import Image from "next/image";
  2 | import { Flags } from "@/components/Flags";
  3 | 
  4 | export default function Home() {
  5 |   return (
  6 |     <main className="flex min-h-screen flex-col items-center justify-between p-24">
  7 |       <div className="z-10 w-full max-w-5xl items-center justify-between font-mono text-sm lg:flex">
  8 |         <div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:size-auto lg:bg-none">
  9 |           <a
 10 |             className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
 11 |             href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
 12 |             target="_blank"
 13 |             rel="noopener noreferrer"
 14 |           >
 15 |             By{" "}
 16 |             <Image
 17 |               src="/vercel.svg"
 18 |               alt="Vercel Logo"
 19 |               className="dark:invert"
 20 |               width={100}
 21 |               height={24}
 22 |               priority
 23 |             />
 24 |           </a>
 25 |         </div>
 26 |       </div>
 27 | 
 28 |       <div className="relative z-[-1] flex place-items-center before:absolute before:h-[300px] before:w-full before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-full after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 sm:before:w-[480px] sm:after:w-[240px] before:lg:h-[360px]">
 29 |         <Image
 30 |           className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
 31 |           src="/next.svg"
 32 |           alt="Next.js Logo"
 33 |           width={180}
 34 |           height={37}
 35 |           priority
 36 |         />
 37 |       </div>
 38 | 
 39 |       <Flags />
 40 | 
 41 |       <div className="mb-32 grid text-center lg:mb-0 lg:w-full lg:max-w-5xl lg:grid-cols-4 lg:text-left">
 42 |         <a
 43 |           href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
 44 |           className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
 45 |           target="_blank"
 46 |           rel="noopener noreferrer"
 47 |         >
 48 |           <h2 className="mb-3 text-2xl font-semibold">
 49 |             Docs{" "}
 50 |             <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
 51 |               -&gt;
 52 |             </span>
 53 |           </h2>
 54 |           <p className="m-0 max-w-[30ch] text-sm opacity-50">
 55 |             Find in-depth information about Next.js features and API.
 56 |           </p>
 57 |         </a>
 58 | 
 59 |         <a
 60 |           href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
 61 |           className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
 62 |           target="_blank"
 63 |           rel="noopener noreferrer"
 64 |         >
 65 |           <h2 className="mb-3 text-2xl font-semibold">
 66 |             Learn{" "}
 67 |             <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
 68 |               -&gt;
 69 |             </span>
 70 |           </h2>
 71 |           <p className="m-0 max-w-[30ch] text-sm opacity-50">
 72 |             Learn about Next.js in an interactive course with&nbsp;quizzes!
 73 |           </p>
 74 |         </a>
 75 | 
 76 |         <a
 77 |           href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
 78 |           className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
 79 |           target="_blank"
 80 |           rel="noopener noreferrer"
 81 |         >
 82 |           <h2 className="mb-3 text-2xl font-semibold">
 83 |             Templates{" "}
 84 |             <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
 85 |               -&gt;
 86 |             </span>
 87 |           </h2>
 88 |           <p className="m-0 max-w-[30ch] text-sm opacity-50">
 89 |             Explore starter templates for Next.js.
 90 |           </p>
 91 |         </a>
 92 | 
 93 |         <a
 94 |           href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
 95 |           className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
 96 |           target="_blank"
 97 |           rel="noopener noreferrer"
 98 |         >
 99 |           <h2 className="mb-3 text-2xl font-semibold">
100 |             Deploy{" "}
101 |             <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
102 |               -&gt;
103 |             </span>
104 |           </h2>
105 |           <p className="m-0 max-w-[30ch] text-balance text-sm opacity-50">
106 |             Instantly deploy your Next.js site to a shareable URL with Vercel.
107 |           </p>
108 |         </a>
109 |       </div>
110 |     </main>
111 |   );
112 | }
113 | 
```

--------------------------------------------------------------------------------
/packages/react-sdk/dev/nextjs-bootstrap-demo/app/page.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import Image from "next/image";
  2 | import { Flags } from "@/components/Flags";
  3 | 
  4 | export default async function Home() {
  5 |   return (
  6 |     <main className="flex min-h-screen flex-col items-center justify-between p-24">
  7 |       <div className="z-10 w-full max-w-5xl items-center justify-between font-mono text-sm lg:flex">
  8 |         <div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:size-auto lg:bg-none">
  9 |           <a
 10 |             className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
 11 |             href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
 12 |             target="_blank"
 13 |             rel="noopener noreferrer"
 14 |           >
 15 |             By{" "}
 16 |             <Image
 17 |               src="/vercel.svg"
 18 |               alt="Vercel Logo"
 19 |               className="dark:invert w-auto h-auto"
 20 |               width={100}
 21 |               height={24}
 22 |               priority
 23 |             />
 24 |           </a>
 25 |         </div>
 26 |       </div>
 27 | 
 28 |       <div className="relative z-[-1] flex place-items-center before:absolute before:h-[300px] before:w-full before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-full after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 sm:before:w-[480px] sm:after:w-[240px] before:lg:h-[360px]">
 29 |         <Image
 30 |           className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
 31 |           src="/next.svg"
 32 |           alt="Next.js Logo"
 33 |           width={180}
 34 |           height={37}
 35 |           priority
 36 |         />
 37 |       </div>
 38 | 
 39 |       <Flags />
 40 | 
 41 |       <div className="mb-32 grid text-center lg:mb-0 lg:w-full lg:max-w-5xl lg:grid-cols-4 lg:text-left">
 42 |         <a
 43 |           href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
 44 |           className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
 45 |           target="_blank"
 46 |           rel="noopener noreferrer"
 47 |         >
 48 |           <h2 className="mb-3 text-2xl font-semibold">
 49 |             Docs{" "}
 50 |             <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
 51 |               -&gt;
 52 |             </span>
 53 |           </h2>
 54 |           <p className="m-0 max-w-[30ch] text-sm opacity-50">
 55 |             Find in-depth information about Next.js features and API.
 56 |           </p>
 57 |         </a>
 58 | 
 59 |         <a
 60 |           href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
 61 |           className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
 62 |           target="_blank"
 63 |           rel="noopener noreferrer"
 64 |         >
 65 |           <h2 className="mb-3 text-2xl font-semibold">
 66 |             Learn{" "}
 67 |             <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
 68 |               -&gt;
 69 |             </span>
 70 |           </h2>
 71 |           <p className="m-0 max-w-[30ch] text-sm opacity-50">
 72 |             Learn about Next.js in an interactive course with&nbsp;quizzes!
 73 |           </p>
 74 |         </a>
 75 | 
 76 |         <a
 77 |           href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
 78 |           className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
 79 |           target="_blank"
 80 |           rel="noopener noreferrer"
 81 |         >
 82 |           <h2 className="mb-3 text-2xl font-semibold">
 83 |             Templates{" "}
 84 |             <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
 85 |               -&gt;
 86 |             </span>
 87 |           </h2>
 88 |           <p className="m-0 max-w-[30ch] text-sm opacity-50">
 89 |             Explore starter templates for Next.js.
 90 |           </p>
 91 |         </a>
 92 | 
 93 |         <a
 94 |           href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
 95 |           className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
 96 |           target="_blank"
 97 |           rel="noopener noreferrer"
 98 |         >
 99 |           <h2 className="mb-3 text-2xl font-semibold">
100 |             Deploy{" "}
101 |             <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
102 |               -&gt;
103 |             </span>
104 |           </h2>
105 |           <p className="m-0 max-w-[30ch] text-balance text-sm opacity-50">
106 |             Instantly deploy your Next.js site to a shareable URL with Vercel.
107 |           </p>
108 |         </a>
109 |       </div>
110 |     </main>
111 |   );
112 | }
113 | 
```

--------------------------------------------------------------------------------
/packages/node-sdk/test/flusher.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { constants } from "os";
  2 | import {
  3 |   afterEach,
  4 |   beforeEach,
  5 |   describe,
  6 |   expect,
  7 |   it,
  8 |   MockInstance,
  9 |   vi,
 10 | } from "vitest";
 11 | 
 12 | import { subscribe } from "../src/flusher";
 13 | 
 14 | describe("flusher", () => {
 15 |   const mockExit = vi
 16 |     .spyOn(process, "exit")
 17 |     .mockImplementation((() => undefined) as any);
 18 | 
 19 |   const mockConsoleError = vi
 20 |     .spyOn(console, "error")
 21 |     .mockImplementation(() => undefined);
 22 | 
 23 |   const mockProcessOn = vi
 24 |     .spyOn(process, "on")
 25 |     .mockImplementation((_, __) => process);
 26 | 
 27 |   const mockProcessPrependListener = (
 28 |     vi.spyOn(process, "prependListener") as unknown as MockInstance<
 29 |       [event: NodeJS.Signals, listener: NodeJS.SignalsListener],
 30 |       NodeJS.Process
 31 |     >
 32 |   ).mockImplementation((_, __) => process);
 33 | 
 34 |   const mockListenerCount = vi
 35 |     .spyOn(process, "listenerCount")
 36 |     .mockReturnValue(0);
 37 | 
 38 |   function timedCallback(ms: number) {
 39 |     return vi.fn().mockImplementation(
 40 |       () =>
 41 |         new Promise((resolve) => {
 42 |           setTimeout(resolve, ms);
 43 |         }),
 44 |     );
 45 |   }
 46 | 
 47 |   function getHandler(eventName: string, prepended = false) {
 48 |     return prepended
 49 |       ? mockProcessPrependListener.mock.calls.filter(
 50 |           ([evt]) => evt === eventName,
 51 |         )[0][1]
 52 |       : mockProcessOn.mock.calls.filter(([evt]) => evt === eventName)[0][1];
 53 |   }
 54 | 
 55 |   beforeEach(() => {
 56 |     vi.useFakeTimers();
 57 |   });
 58 | 
 59 |   afterEach(() => {
 60 |     vi.useRealTimers();
 61 |     vi.resetAllMocks();
 62 |   });
 63 | 
 64 |   describe("signal handling", () => {
 65 |     const signals = ["SIGINT", "SIGTERM", "SIGHUP", "SIGBREAK"] as const;
 66 | 
 67 |     describe.each(signals)("signal %s", (signal) => {
 68 |       it("should handle signal with no existing listeners", async () => {
 69 |         mockListenerCount.mockReturnValue(0);
 70 |         const callback = vi.fn().mockResolvedValue(undefined);
 71 | 
 72 |         subscribe(callback);
 73 |         expect(mockProcessOn).toHaveBeenCalledWith(
 74 |           signal,
 75 |           expect.any(Function),
 76 |         );
 77 | 
 78 |         getHandler(signal)(signal);
 79 |         await vi.runAllTimersAsync();
 80 | 
 81 |         expect(callback).toHaveBeenCalledTimes(1);
 82 |         expect(mockExit).toHaveBeenCalledWith(0x80 + constants.signals[signal]);
 83 |       });
 84 | 
 85 |       it("should prepend handler when listeners exist", async () => {
 86 |         mockListenerCount.mockReturnValue(1);
 87 |         const callback = vi.fn().mockResolvedValue(undefined);
 88 | 
 89 |         subscribe(callback);
 90 | 
 91 |         expect(mockProcessPrependListener).toHaveBeenCalledWith(
 92 |           signal,
 93 |           expect.any(Function),
 94 |         );
 95 | 
 96 |         getHandler(signal, true)(signal);
 97 | 
 98 |         expect(callback).toHaveBeenCalledTimes(1);
 99 |         expect(mockExit).not.toHaveBeenCalled();
100 |       });
101 |     });
102 |   });
103 | 
104 |   describe("beforeExit handling", () => {
105 |     it("should call callback on beforeExit", async () => {
106 |       const callback = vi.fn().mockResolvedValue(undefined);
107 | 
108 |       subscribe(callback);
109 | 
110 |       getHandler("beforeExit")();
111 | 
112 |       expect(callback).toHaveBeenCalledTimes(1);
113 |     });
114 | 
115 |     it("should not call callback multiple times", async () => {
116 |       const callback = vi.fn().mockResolvedValue(undefined);
117 | 
118 |       subscribe(callback);
119 | 
120 |       getHandler("beforeExit")();
121 |       getHandler("beforeExit")();
122 | 
123 |       expect(callback).toHaveBeenCalledTimes(1);
124 |     });
125 |   });
126 | 
127 |   describe("timeout handling", () => {
128 |     it("should handle timeout when callback takes too long", async () => {
129 |       subscribe(timedCallback(2000), 1000);
130 | 
131 |       getHandler("beforeExit")();
132 | 
133 |       await vi.advanceTimersByTimeAsync(1000);
134 | 
135 |       expect(mockConsoleError).toHaveBeenCalledWith(
136 |         "[Reflag SDK] Timeout while flushing events on process exit.",
137 |       );
138 |     });
139 | 
140 |     it("should not timeout when callback completes in time", async () => {
141 |       subscribe(timedCallback(500), 1000);
142 | 
143 |       getHandler("beforeExit")();
144 |       await vi.advanceTimersByTimeAsync(500);
145 | 
146 |       expect(mockConsoleError).not.toHaveBeenCalled();
147 |     });
148 |   });
149 | 
150 |   describe("exit state handling", () => {
151 |     it("should log error if exit occurs before flushing starts", () => {
152 |       subscribe(timedCallback(0));
153 | 
154 |       getHandler("exit")();
155 | 
156 |       expect(mockConsoleError).toHaveBeenCalledWith(
157 |         "[Reflag SDK] Failed to finalize the flushing of events on process exit.",
158 |       );
159 |     });
160 | 
161 |     it("should log error if exit occurs before flushing completes", async () => {
162 |       subscribe(timedCallback(2000));
163 |       getHandler("beforeExit")();
164 | 
165 |       await vi.advanceTimersByTimeAsync(1000);
166 | 
167 |       getHandler("exit")();
168 | 
169 |       expect(mockConsoleError).toHaveBeenCalledWith(
170 |         "[Reflag SDK] Failed to finalize the flushing of events on process exit.",
171 |       );
172 |     });
173 | 
174 |     it("should not log error if flushing completes before exit", async () => {
175 |       subscribe(timedCallback(500));
176 | 
177 |       getHandler("beforeExit")();
178 |       await vi.advanceTimersByTimeAsync(500);
179 | 
180 |       getHandler("exit")();
181 | 
182 |       expect(mockConsoleError).not.toHaveBeenCalled();
183 |     });
184 | 
185 |     it("should handle callback errors gracefully", async () => {
186 |       subscribe(vi.fn().mockRejectedValue(new Error("Test error")));
187 | 
188 |       getHandler("beforeExit")();
189 |       await vi.runAllTimersAsync();
190 | 
191 |       expect(mockConsoleError).toHaveBeenCalledWith(
192 |         "[Reflag SDK] An error occurred while flushing events on process exit.",
193 |         expect.any(Error),
194 |       );
195 |     });
196 |   });
197 | 
198 |   it("should run the callback only once", async () => {
199 |     const callback = vi.fn().mockResolvedValue(undefined);
200 | 
201 |     subscribe(callback);
202 | 
203 |     getHandler("SIGINT")("SIGINT");
204 |     getHandler("beforeExit")();
205 | 
206 |     await vi.runAllTimersAsync();
207 | 
208 |     expect(callback).toHaveBeenCalledTimes(1);
209 |   });
210 | });
211 | 
```

--------------------------------------------------------------------------------
/packages/cli/stores/config.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Ajv, ValidateFunction } from "ajv";
  2 | import {
  3 |   assign as assignJSON,
  4 |   parse as parseJSON,
  5 |   stringify as stringifyJSON,
  6 | } from "comment-json";
  7 | import equal from "fast-deep-equal";
  8 | import { findUp } from "find-up";
  9 | import { readFile, writeFile } from "node:fs/promises";
 10 | import { dirname, join } from "node:path";
 11 | 
 12 | import {
 13 |   CONFIG_FILE_NAME,
 14 |   DEFAULT_API_URL,
 15 |   DEFAULT_BASE_URL,
 16 |   DEFAULT_TYPES_OUTPUT,
 17 |   MODULE_ROOT,
 18 |   SCHEMA_URL,
 19 | } from "../utils/constants.js";
 20 | import { ConfigValidationError, handleError } from "../utils/errors.js";
 21 | import { stripTrailingSlash } from "../utils/urls.js";
 22 | import { current as currentVersion } from "../utils/version.js";
 23 | 
 24 | export const typeFormats = ["react", "node"] as const;
 25 | export type TypeFormat = (typeof typeFormats)[number];
 26 | 
 27 | export type TypesOutput = {
 28 |   path: string;
 29 |   format: TypeFormat;
 30 | };
 31 | 
 32 | type Config = {
 33 |   $schema: string;
 34 |   baseUrl: string;
 35 |   apiUrl: string;
 36 |   appId: string | undefined;
 37 |   typesOutput: TypesOutput[];
 38 | };
 39 | 
 40 | const defaultConfig: Config = {
 41 |   $schema: SCHEMA_URL,
 42 |   baseUrl: DEFAULT_BASE_URL,
 43 |   apiUrl: DEFAULT_API_URL,
 44 |   appId: undefined,
 45 |   typesOutput: [{ path: DEFAULT_TYPES_OUTPUT, format: "react" }],
 46 | };
 47 | 
 48 | // Helper to normalize typesOutput to array format
 49 | export function normalizeTypesOutput(
 50 |   output?: string | TypesOutput[],
 51 | ): TypesOutput[] | undefined {
 52 |   if (!output) return undefined;
 53 |   if (typeof output === "string") {
 54 |     return [{ path: output, format: "react" }];
 55 |   }
 56 |   return output;
 57 | }
 58 | 
 59 | class ConfigStore {
 60 |   protected config: Config = { ...defaultConfig };
 61 |   protected configPath: string | undefined;
 62 |   protected projectPath: string | undefined;
 63 |   protected clientVersion: string | undefined;
 64 |   protected validateConfig: ValidateFunction | undefined;
 65 | 
 66 |   async initialize() {
 67 |     await this.createValidator();
 68 |     await this.loadConfigFile();
 69 |   }
 70 | 
 71 |   protected async createValidator() {
 72 |     try {
 73 |       // Using current config store file, resolve the schema.json path
 74 |       const schemaPath = join(MODULE_ROOT, "schema.json");
 75 |       const content = await readFile(schemaPath, "utf-8");
 76 |       const parsed = parseJSON(content) as unknown as Config;
 77 | 
 78 |       const ajv = new Ajv();
 79 |       this.validateConfig = ajv.compile(parsed);
 80 |     } catch {
 81 |       handleError(new Error("Failed to load the config schema"), "Config");
 82 |     }
 83 |   }
 84 | 
 85 |   protected async loadConfigFile() {
 86 |     if (!this.validateConfig) {
 87 |       handleError(new Error("Failed to load the config schema"), "Config");
 88 |     }
 89 | 
 90 |     // Load the client version from the module's package.json metadata
 91 |     try {
 92 |       const { version } = await currentVersion();
 93 |       this.clientVersion = version;
 94 |     } catch {
 95 |       // Should not be the case, but ignore if no package.json is found
 96 |     }
 97 | 
 98 |     try {
 99 |       const projectMetadataPath = await findUp("package.json");
100 |       this.configPath = await findUp(CONFIG_FILE_NAME);
101 |       this.projectPath = dirname(
102 |         this.configPath ?? projectMetadataPath ?? process.cwd(),
103 |       );
104 | 
105 |       if (!this.configPath) return;
106 | 
107 |       const content = await readFile(this.configPath, "utf-8");
108 |       const parsed = parseJSON(content) as unknown as Partial<Config>;
109 | 
110 |       // Normalize values
111 |       if (parsed.baseUrl)
112 |         parsed.baseUrl = stripTrailingSlash(parsed.baseUrl.trim());
113 |       if (parsed.apiUrl)
114 |         parsed.apiUrl = stripTrailingSlash(parsed.apiUrl.trim());
115 |       if (parsed.typesOutput?.length)
116 |         parsed.typesOutput = normalizeTypesOutput(parsed.typesOutput);
117 | 
118 |       if (!this.validateConfig!(parsed)) {
119 |         handleError(
120 |           new ConfigValidationError(this.validateConfig!.errors),
121 |           "Config",
122 |         );
123 |       }
124 | 
125 |       this.config = assignJSON(this.config, parsed);
126 |     } catch {
127 |       // No config file found
128 |     }
129 |   }
130 | 
131 |   /**
132 |    * Create a new config file with initial values.
133 |    * @param overwrite If true, overwrites existing config file. Defaults to false
134 |    */
135 |   async saveConfigFile(overwrite = false) {
136 |     const configWithoutDefaults: Partial<Config> = assignJSON({}, this.config);
137 | 
138 |     // Only include non-default values and $schema
139 |     for (const untypedKey in configWithoutDefaults) {
140 |       const key = untypedKey as keyof Config;
141 |       if (
142 |         !["$schema"].includes(key) &&
143 |         equal(configWithoutDefaults[key], defaultConfig[key])
144 |       ) {
145 |         delete configWithoutDefaults[key];
146 |       }
147 |     }
148 | 
149 |     const configJSON = stringifyJSON(configWithoutDefaults, null, 2);
150 | 
151 |     if (this.configPath && !overwrite) {
152 |       throw new Error("Config file already exists");
153 |     }
154 | 
155 |     if (this.configPath) {
156 |       await writeFile(this.configPath, configJSON);
157 |     } else {
158 |       // Write to the project path
159 |       const packageJSONPath = await findUp("package.json");
160 |       this.projectPath = dirname(packageJSONPath ?? process.cwd());
161 |       this.configPath = join(this.projectPath, CONFIG_FILE_NAME);
162 |       await writeFile(this.configPath, configJSON);
163 |     }
164 |   }
165 | 
166 |   getConfig(): Config;
167 |   getConfig<K extends keyof Config>(key: K): Config[K];
168 |   getConfig<K extends keyof Config>(key?: K) {
169 |     return key ? this.config?.[key] : this.config;
170 |   }
171 | 
172 |   getConfigPath() {
173 |     return this.configPath;
174 |   }
175 | 
176 |   getClientVersion() {
177 |     return this.clientVersion;
178 |   }
179 | 
180 |   getProjectPath() {
181 |     return this.projectPath ?? process.cwd();
182 |   }
183 | 
184 |   setConfig(newConfig: Partial<Config>) {
185 |     // Update the config with new values skipping undefined values
186 |     for (const untypedKey in newConfig) {
187 |       const key = untypedKey as keyof Config;
188 |       if (newConfig[key] === undefined) continue;
189 |       (this.config as any)[key] = newConfig[key];
190 |     }
191 |   }
192 | }
193 | 
194 | export const configStore = new ConfigStore();
195 | 
```

--------------------------------------------------------------------------------
/packages/openfeature-browser-provider/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import {
  2 |   ErrorCode,
  3 |   EvaluationContext,
  4 |   JsonValue,
  5 |   OpenFeatureEventEmitter,
  6 |   Provider,
  7 |   ProviderMetadata,
  8 |   ProviderStatus,
  9 |   ResolutionDetails,
 10 |   StandardResolutionReasons,
 11 |   TrackingEventDetails,
 12 | } from "@openfeature/web-sdk";
 13 | 
 14 | import { Flag, InitOptions, ReflagClient } from "@reflag/browser-sdk";
 15 | 
 16 | export type ContextTranslationFn = (
 17 |   context?: EvaluationContext,
 18 | ) => Record<string, any>;
 19 | 
 20 | export function defaultContextTranslator(
 21 |   context?: EvaluationContext,
 22 | ): Record<string, any> {
 23 |   if (!context) return {};
 24 |   return {
 25 |     user: {
 26 |       id: context.targetingKey ?? context["userId"]?.toString(),
 27 |       email: context["email"]?.toString(),
 28 |       name: context["name"]?.toString(),
 29 |       avatar: context["avatar"]?.toString(),
 30 |       country: context["country"]?.toString(),
 31 |     },
 32 |     company: {
 33 |       id: context["companyId"]?.toString(),
 34 |       name: context["companyName"]?.toString(),
 35 |       plan: context["companyPlan"]?.toString(),
 36 |       avatar: context["companyAvatar"]?.toString(),
 37 |     },
 38 |   };
 39 | }
 40 | 
 41 | export class ReflagBrowserSDKProvider implements Provider {
 42 |   readonly metadata: ProviderMetadata = {
 43 |     name: "reflag-browser-provider",
 44 |   };
 45 | 
 46 |   private _client?: ReflagClient;
 47 | 
 48 |   private readonly _clientOptions: InitOptions;
 49 |   private readonly _contextTranslator: ContextTranslationFn;
 50 | 
 51 |   public events = new OpenFeatureEventEmitter();
 52 | 
 53 |   private _status: ProviderStatus = ProviderStatus.NOT_READY;
 54 | 
 55 |   set status(status: ProviderStatus) {
 56 |     this._status = status;
 57 |   }
 58 | 
 59 |   get status() {
 60 |     return this._status;
 61 |   }
 62 | 
 63 |   get client() {
 64 |     return this._client;
 65 |   }
 66 | 
 67 |   constructor({
 68 |     contextTranslator,
 69 |     ...opts
 70 |   }: InitOptions & { contextTranslator?: ContextTranslationFn }) {
 71 |     this._clientOptions = opts;
 72 |     this._contextTranslator = contextTranslator || defaultContextTranslator;
 73 |   }
 74 | 
 75 |   async initialize(context?: EvaluationContext): Promise<void> {
 76 |     const client = new ReflagClient({
 77 |       ...this._clientOptions,
 78 |       ...this._contextTranslator(context),
 79 |     });
 80 | 
 81 |     try {
 82 |       await client.initialize();
 83 |       this.status = ProviderStatus.READY;
 84 |       this._client = client;
 85 |     } catch {
 86 |       this.status = ProviderStatus.ERROR;
 87 |     }
 88 |   }
 89 | 
 90 |   onClose(): Promise<void> {
 91 |     if (this._client) {
 92 |       return this._client?.stop();
 93 |     }
 94 |     return Promise.resolve();
 95 |   }
 96 | 
 97 |   async onContextChange(
 98 |     _oldContext: EvaluationContext,
 99 |     newContext: EvaluationContext,
100 |   ): Promise<void> {
101 |     await this.initialize(newContext);
102 |   }
103 | 
104 |   private resolveFlag<T extends JsonValue>(
105 |     flagKey: string,
106 |     defaultValue: T,
107 |     resolveFn: (feature: Flag) => ResolutionDetails<T>,
108 |   ): ResolutionDetails<T> {
109 |     if (!this._client) {
110 |       return {
111 |         value: defaultValue,
112 |         reason: StandardResolutionReasons.DEFAULT,
113 |         errorCode: ErrorCode.PROVIDER_NOT_READY,
114 |         errorMessage: "Reflag client not initialized",
115 |       } satisfies ResolutionDetails<T>;
116 |     }
117 | 
118 |     const features = this._client.getFlags();
119 |     if (flagKey in features) {
120 |       return resolveFn(this._client.getFlag(flagKey));
121 |     }
122 | 
123 |     return {
124 |       value: defaultValue,
125 |       reason: StandardResolutionReasons.DEFAULT,
126 |       errorCode: ErrorCode.FLAG_NOT_FOUND,
127 |       errorMessage: `Flag ${flagKey} not found`,
128 |     };
129 |   }
130 | 
131 |   resolveBooleanEvaluation(flagKey: string, defaultValue: boolean) {
132 |     return this.resolveFlag(flagKey, defaultValue, (feature) => {
133 |       return {
134 |         value: feature.isEnabled,
135 |         variant: feature.config.key,
136 |         reason: StandardResolutionReasons.TARGETING_MATCH,
137 |       };
138 |     });
139 |   }
140 | 
141 |   resolveNumberEvaluation(_flagKey: string, defaultValue: number) {
142 |     return {
143 |       value: defaultValue,
144 |       reason: StandardResolutionReasons.ERROR,
145 |       errorCode: ErrorCode.GENERAL,
146 |       errorMessage:
147 |         "Reflag doesn't support this method. Use `resolveObjectEvaluation` instead.",
148 |     };
149 |   }
150 | 
151 |   resolveStringEvaluation(
152 |     flagKey: string,
153 |     defaultValue: string,
154 |   ): ResolutionDetails<string> {
155 |     return this.resolveFlag(flagKey, defaultValue, (feature) => {
156 |       if (!feature.config.key) {
157 |         return {
158 |           value: defaultValue,
159 |           reason: StandardResolutionReasons.DEFAULT,
160 |         };
161 |       }
162 | 
163 |       return {
164 |         value: feature.config.key as string,
165 |         variant: feature.config.key,
166 |         reason: StandardResolutionReasons.TARGETING_MATCH,
167 |       };
168 |     });
169 |   }
170 | 
171 |   resolveObjectEvaluation<T extends JsonValue>(
172 |     flagKey: string,
173 |     defaultValue: T,
174 |   ) {
175 |     return this.resolveFlag(flagKey, defaultValue, (feature) => {
176 |       const expType = typeof defaultValue;
177 | 
178 |       const payloadType = typeof feature.config.payload;
179 | 
180 |       if (
181 |         feature.config.payload === undefined ||
182 |         feature.config.payload === null ||
183 |         payloadType !== expType
184 |       ) {
185 |         return {
186 |           value: defaultValue,
187 |           reason: StandardResolutionReasons.ERROR,
188 |           variant: feature.config.key,
189 |           errorCode: ErrorCode.TYPE_MISMATCH,
190 |           errorMessage: `Expected remote config payload of type \`${expType}\` but got \`${payloadType}\`.`,
191 |         };
192 |       }
193 | 
194 |       return {
195 |         value: feature.config.payload,
196 |         variant: feature.config.key,
197 |         reason: StandardResolutionReasons.TARGETING_MATCH,
198 |       };
199 |     });
200 |   }
201 | 
202 |   track(
203 |     trackingEventName: string,
204 |     _context?: EvaluationContext,
205 |     trackingEventDetails?: TrackingEventDetails,
206 |   ): void {
207 |     if (!this._client) {
208 |       this._clientOptions.logger?.error("client not initialized");
209 |     }
210 | 
211 |     this._client
212 |       ?.track(trackingEventName, trackingEventDetails)
213 |       .catch((e: any) => {
214 |         this._clientOptions.logger?.error("error tracking event", e);
215 |       });
216 |   }
217 | }
218 | 
```

--------------------------------------------------------------------------------
/packages/node-sdk/src/utils.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { createHash, Hash } from "crypto";
  2 | 
  3 | import { IdType, Logger, LogLevel } from "./types";
  4 | 
  5 | /**
  6 |  * Assert that the given condition is `true`.
  7 |  *
  8 |  * @param condition - The condition to check.
  9 |  * @param message - The error message to throw.
 10 |  **/
 11 | export function ok(condition: boolean, message: string): asserts condition {
 12 |   if (!condition) {
 13 |     throw new Error(`validation failed: ${message}`);
 14 |   }
 15 | }
 16 | /**
 17 |  * Assert that the given values is a valid user/company id
 18 |  **/
 19 | export function idOk(id: IdType, entity: string) {
 20 |   ok(
 21 |     (typeof id === "string" && id.length > 0) || typeof id === "number",
 22 |     `${entity} must be a string or number if given`,
 23 |   );
 24 | }
 25 | 
 26 | /**
 27 |  * Check if the given item is an object.
 28 |  *
 29 |  * @param item - The item to check.
 30 |  * @returns `true` if the item is an object, `false` otherwise.
 31 |  **/
 32 | export function isObject(item: any): item is Record<string, any> {
 33 |   return (item && typeof item === "object" && !Array.isArray(item)) || false;
 34 | }
 35 | 
 36 | export type LogFn = (message: string, ...args: any[]) => void;
 37 | export function decorate(prefix: string, fn: LogFn): LogFn {
 38 |   return (message: string, ...args: any[]) => {
 39 |     fn(`${prefix} ${message}`, ...args);
 40 |   };
 41 | }
 42 | 
 43 | export function applyLogLevel(logger: Logger, logLevel: LogLevel) {
 44 |   switch (logLevel?.toLocaleUpperCase()) {
 45 |     case "DEBUG":
 46 |       return {
 47 |         debug: decorate("[debug]", logger.debug),
 48 |         info: decorate("[info]", logger.info),
 49 |         warn: decorate("[warn]", logger.warn),
 50 |         error: decorate("[error]", logger.error),
 51 |       };
 52 |     case "INFO":
 53 |       return {
 54 |         debug: () => void 0,
 55 |         info: decorate("[info]", logger.info),
 56 |         warn: decorate("[warn]", logger.warn),
 57 |         error: decorate("[error]", logger.error),
 58 |       };
 59 |     case "WARN":
 60 |       return {
 61 |         debug: () => void 0,
 62 |         info: () => void 0,
 63 |         warn: decorate("[warn]", logger.warn),
 64 |         error: decorate("[error]", logger.error),
 65 |       };
 66 |     case "ERROR":
 67 |       return {
 68 |         debug: () => void 0,
 69 |         info: () => void 0,
 70 |         warn: () => void 0,
 71 |         error: decorate("[error]", logger.error),
 72 |       };
 73 |     default:
 74 |       throw new Error(`invalid log level: ${logLevel}`);
 75 |   }
 76 | }
 77 | 
 78 | /**
 79 |  * Decorate the messages of a given logger with the given prefix.
 80 |  *
 81 |  * @param prefix - The prefix to add to log messages.
 82 |  * @param logger - The logger to decorate.
 83 |  * @returns The decorated logger.
 84 |  **/
 85 | export function decorateLogger(prefix: string, logger: Logger): Logger {
 86 |   ok(typeof prefix === "string", "prefix must be a string");
 87 |   ok(typeof logger === "object", "logger must be an object");
 88 | 
 89 |   return {
 90 |     debug: decorate(prefix, logger.debug),
 91 |     info: decorate(prefix, logger.info),
 92 |     warn: decorate(prefix, logger.warn),
 93 |     error: decorate(prefix, logger.error),
 94 |   };
 95 | }
 96 | 
 97 | /** Merge two objects, skipping `undefined` values.
 98 |  *
 99 |  * @param target - The target object.
100 |  * @param source - The source object.
101 |  * @returns The merged object.
102 |  **/
103 | export function mergeSkipUndefined<T extends object, U extends object>(
104 |   target: T,
105 |   source: U,
106 | ): T & U {
107 |   const newTarget = { ...target };
108 |   for (const key in source) {
109 |     if (source[key] === undefined) {
110 |       continue;
111 |     }
112 |     (newTarget as any)[key] = source[key];
113 |   }
114 |   return newTarget as T & U;
115 | }
116 | 
117 | function updateSha1Hash(hash: Hash, value: any) {
118 |   if (value === null) {
119 |     hash.update("null");
120 |   } else {
121 |     switch (typeof value) {
122 |       case "object":
123 |         if (Array.isArray(value)) {
124 |           for (const item of value) {
125 |             updateSha1Hash(hash, item);
126 |           }
127 |         } else {
128 |           for (const key of Object.keys(value).sort()) {
129 |             hash.update(key);
130 |             updateSha1Hash(hash, value[key]);
131 |           }
132 |         }
133 |         break;
134 |       case "string":
135 |         hash.update(value);
136 |         break;
137 |       case "number":
138 |       case "boolean":
139 |       case "symbol":
140 |       case "bigint":
141 |       case "function":
142 |         hash.update(value.toString());
143 |         break;
144 |       case "undefined":
145 |       default:
146 |         break;
147 |     }
148 |   }
149 | }
150 | 
151 | /** Hash an object using SHA1.
152 |  *
153 |  * @param obj - The object to hash.
154 |  *
155 |  * @returns The SHA1 hash of the object.
156 |  **/
157 | export function hashObject(obj: Record<string, any>): string {
158 |   ok(isObject(obj), "obj must be an object");
159 | 
160 |   const hash = createHash("sha1");
161 |   updateSha1Hash(hash, obj);
162 | 
163 |   return hash.digest("base64");
164 | }
165 | 
166 | export function once<T extends () => ReturnType<T>>(
167 |   fn: T,
168 | ): () => ReturnType<T> {
169 |   let called = false;
170 |   let returned: ReturnType<T> | undefined;
171 |   return function (): ReturnType<T> {
172 |     if (called) {
173 |       return returned!;
174 |     }
175 |     returned = fn();
176 |     called = true;
177 |     return returned;
178 |   };
179 | }
180 | 
181 | export class TimeoutError extends Error {
182 |   constructor(timeoutMs: number) {
183 |     super(`Operation timed out after ${timeoutMs}ms`);
184 |     this.name = "TimeoutError";
185 |   }
186 | }
187 | 
188 | /**
189 |  * Wraps a promise with a timeout. If the promise doesn't resolve within the specified
190 |  * timeout, it will reject with a timeout error. The original promise will still
191 |  * continue to execute but its result will be ignored.
192 |  *
193 |  * @param promise - The promise to wrap with a timeout
194 |  * @param timeoutMs - The timeout in milliseconds
195 |  * @returns A promise that resolves with the original promise result or rejects with a timeout error
196 |  * @throws {Error} If the timeout is reached before the promise resolves
197 |  **/
198 | export function withTimeout<T>(
199 |   promise: Promise<T>,
200 |   timeoutMs: number,
201 | ): Promise<T> {
202 |   ok(timeoutMs > 0, "timeout must be a positive number");
203 | 
204 |   return new Promise((resolve, reject) => {
205 |     const timeoutId = setTimeout(() => {
206 |       reject(new TimeoutError(timeoutMs));
207 |     }, timeoutMs);
208 | 
209 |     promise
210 |       .then((result) => {
211 |         resolve(result);
212 |       })
213 |       .catch((error) => {
214 |         reject(error);
215 |       })
216 |       .finally(() => {
217 |         clearTimeout(timeoutId);
218 |       });
219 |   });
220 | }
221 | 
```

--------------------------------------------------------------------------------
/packages/cli/commands/flags.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { input } from "@inquirer/prompts";
  2 | import chalk from "chalk";
  3 | import { Command } from "commander";
  4 | import { relative } from "node:path";
  5 | import ora, { Ora } from "ora";
  6 | 
  7 | import { App, getApp, getOrg } from "../services/bootstrap.js";
  8 | import { createFlag, Flag, listFlags } from "../services/flags.js";
  9 | import { configStore } from "../stores/config.js";
 10 | import {
 11 |   handleError,
 12 |   MissingAppIdError,
 13 |   MissingEnvIdError,
 14 | } from "../utils/errors.js";
 15 | import {
 16 |   genFlagKey,
 17 |   genTypes,
 18 |   indentLines,
 19 |   KeyFormatPatterns,
 20 |   writeTypesToFile,
 21 | } from "../utils/gen.js";
 22 | import {
 23 |   appIdOption,
 24 |   flagKeyOption,
 25 |   flagNameArgument,
 26 |   typesFormatOption,
 27 |   typesOutOption,
 28 | } from "../utils/options.js";
 29 | import { baseUrlSuffix, featureUrl } from "../utils/urls.js";
 30 | 
 31 | type CreateFlagOptions = {
 32 |   key?: string;
 33 | };
 34 | 
 35 | export const createFlagAction = async (
 36 |   name: string | undefined,
 37 |   { key }: CreateFlagOptions,
 38 | ) => {
 39 |   const { baseUrl, appId } = configStore.getConfig();
 40 |   let spinner: Ora | undefined;
 41 | 
 42 |   if (!appId) {
 43 |     handleError(new MissingAppIdError(), "Flags Create");
 44 |   }
 45 | 
 46 |   let app: App;
 47 |   try {
 48 |     app = getApp(appId);
 49 |   } catch (error) {
 50 |     handleError(error, "Flags Create");
 51 |   }
 52 | 
 53 |   const production = app.environments.find((e) => e.isProduction);
 54 | 
 55 |   try {
 56 |     const org = getOrg();
 57 |     console.log(
 58 |       `Creating flag for app ${chalk.cyan(app.name)}${baseUrlSuffix(baseUrl)}.`,
 59 |     );
 60 | 
 61 |     if (!name) {
 62 |       name = await input({
 63 |         message: "New flag name:",
 64 |         validate: (text) => text.length > 0 || "Name is required.",
 65 |       });
 66 |     }
 67 | 
 68 |     if (!key) {
 69 |       const keyFormat = org.featureKeyFormat;
 70 |       const keyValidator = KeyFormatPatterns[keyFormat];
 71 |       key = await input({
 72 |         message: "New flag key:",
 73 |         default: genFlagKey(name, keyFormat),
 74 |         validate: (str) => keyValidator.regex.test(str) || keyValidator.message,
 75 |       });
 76 |     }
 77 | 
 78 |     spinner = ora(`Creating flag...`).start();
 79 |     const flag = await createFlag(appId, { name, key });
 80 | 
 81 |     spinner.succeed(
 82 |       `Created flag ${chalk.cyan(flag.name)} with key ${chalk.cyan(flag.key)}:`,
 83 |     );
 84 |     if (production) {
 85 |       console.log(
 86 |         indentLines(chalk.magenta(featureUrl(baseUrl, production, flag))),
 87 |       );
 88 |     }
 89 |   } catch (error) {
 90 |     spinner?.fail("Flag creation failed.");
 91 |     handleError(error, "Flags Create");
 92 |   }
 93 | };
 94 | 
 95 | export const listFlagsAction = async () => {
 96 |   const { baseUrl, appId } = configStore.getConfig();
 97 |   let spinner: Ora | undefined;
 98 | 
 99 |   if (!appId) {
100 |     handleError(new MissingAppIdError(), "Flags Create");
101 |   }
102 | 
103 |   try {
104 |     const app = getApp(appId);
105 |     const production = app.environments.find((e) => e.isProduction);
106 |     if (!production) {
107 |       handleError(new MissingEnvIdError(), "Flags Types");
108 |     }
109 | 
110 |     spinner = ora(
111 |       `Loading flags of app ${chalk.cyan(app.name)}${baseUrlSuffix(baseUrl)}...`,
112 |     ).start();
113 | 
114 |     const flagsResponse = await listFlags(appId, {
115 |       envId: production.id,
116 |     });
117 | 
118 |     spinner.succeed(
119 |       `Loaded flags of app ${chalk.cyan(app.name)}${baseUrlSuffix(baseUrl)}.`,
120 |     );
121 | 
122 |     console.table(
123 |       flagsResponse.data.map(({ key, name, stage }) => ({
124 |         name,
125 |         key,
126 |         stage: stage?.name,
127 |       })),
128 |     );
129 |   } catch (error) {
130 |     spinner?.fail("Loading flags failed.");
131 |     handleError(error, "Flags List");
132 |   }
133 | };
134 | 
135 | export const generateTypesAction = async () => {
136 |   const { baseUrl, appId } = configStore.getConfig();
137 |   const typesOutput = configStore.getConfig("typesOutput");
138 | 
139 |   let spinner: Ora | undefined;
140 |   let flags: Flag[] = [];
141 | 
142 |   if (!appId) {
143 |     handleError(new MissingAppIdError(), "Flags Types");
144 |   }
145 | 
146 |   let app: App;
147 |   try {
148 |     app = getApp(appId);
149 |   } catch (error) {
150 |     handleError(error, "Flags Types");
151 |   }
152 | 
153 |   const production = app.environments.find((e) => e.isProduction);
154 |   if (!production) {
155 |     handleError(new MissingEnvIdError(), "Flags Types");
156 |   }
157 | 
158 |   try {
159 |     spinner = ora(
160 |       `Loading flags of app ${chalk.cyan(app.name)}${baseUrlSuffix(baseUrl)}...`,
161 |     ).start();
162 | 
163 |     flags = await listFlags(appId, {
164 |       envId: production.id,
165 |       includeRemoteConfigs: true,
166 |     }).then((res) => res.data);
167 | 
168 |     spinner.succeed(
169 |       `Loaded flags of app ${chalk.cyan(app.name)}${baseUrlSuffix(baseUrl)}.`,
170 |     );
171 |   } catch (error) {
172 |     spinner?.fail("Loading flags failed.");
173 |     handleError(error, "Flags Types");
174 |   }
175 | 
176 |   try {
177 |     spinner = ora(`Generating flag types...`).start();
178 |     const projectPath = configStore.getProjectPath();
179 | 
180 |     // Generate types for each output configuration
181 |     for (const output of typesOutput) {
182 |       const types = genTypes(flags, output.format);
183 | 
184 |       const outPath = await writeTypesToFile(types, output.path, projectPath);
185 |       spinner.succeed(
186 |         `Generated ${output.format} types in ${chalk.cyan(relative(projectPath, outPath))}.`,
187 |       );
188 |     }
189 |   } catch (error) {
190 |     spinner?.fail("Type generation failed.");
191 |     handleError(error, "Flags Types");
192 |   }
193 | };
194 | 
195 | export function registerFlagCommands(cli: Command) {
196 |   const flagsCommand = new Command("flags").description("Manage flags.");
197 | 
198 |   flagsCommand
199 |     .command("create")
200 |     .description("Create a new flag.")
201 |     .addOption(appIdOption)
202 |     .addOption(flagKeyOption)
203 |     .addArgument(flagNameArgument)
204 |     .action(createFlagAction);
205 | 
206 |   flagsCommand
207 |     .command("list")
208 |     .alias("ls")
209 |     .description("List all flags.")
210 |     .addOption(appIdOption)
211 |     .action(listFlagsAction);
212 | 
213 |   flagsCommand
214 |     .command("types")
215 |     .description("Generate flag types.")
216 |     .addOption(appIdOption)
217 |     .addOption(typesOutOption)
218 |     .addOption(typesFormatOption)
219 |     .action(generateTypesAction);
220 | 
221 |   // Update the config with the cli override values
222 |   flagsCommand.hook("preAction", (_, command) => {
223 |     const { appId, out, format } = command.opts();
224 |     configStore.setConfig({
225 |       appId,
226 |       typesOutput: out ? [{ path: out, format: format || "react" }] : undefined,
227 |     });
228 |   });
229 | 
230 |   cli.addCommand(flagsCommand);
231 | }
232 | 
```

--------------------------------------------------------------------------------
/packages/cli/utils/json.ts:
--------------------------------------------------------------------------------

```typescript
  1 | export type JSONPrimitive =
  2 |   | number
  3 |   | string
  4 |   | boolean
  5 |   | null
  6 |   | JSONPrimitive[]
  7 |   | { [key: string]: JSONPrimitive };
  8 | 
  9 | export type PrimitiveAST = { kind: "primitive"; type: string };
 10 | export type ArrayAST = { kind: "array"; elementType: TypeAST };
 11 | export type ObjectAST = {
 12 |   kind: "object";
 13 |   properties: { key: string; type: TypeAST; optional: boolean }[];
 14 | };
 15 | export type UnionAST = { kind: "union"; types: TypeAST[] };
 16 | 
 17 | // Type AST to represent TypeScript types
 18 | export type TypeAST = PrimitiveAST | ArrayAST | ObjectAST | UnionAST;
 19 | 
 20 | // Convert JSON value to TypeAST
 21 | export function toTypeAST(value: JSONPrimitive, path: string[] = []): TypeAST {
 22 |   if (value === null) return { kind: "primitive", type: "null" };
 23 | 
 24 |   if (Array.isArray(value)) {
 25 |     if (value.length === 0) {
 26 |       return {
 27 |         kind: "array",
 28 |         elementType: { kind: "primitive", type: "any" },
 29 |       };
 30 |     }
 31 | 
 32 |     // Process all elements in the array instead of just the first one
 33 |     const elementTypes = value.map((item, index) =>
 34 |       toTypeAST(item, [...path, index.toString()]),
 35 |     );
 36 | 
 37 |     return {
 38 |       kind: "array",
 39 |       elementType: mergeTypeASTs(elementTypes),
 40 |     };
 41 |   }
 42 | 
 43 |   if (typeof value === "object") {
 44 |     return {
 45 |       kind: "object",
 46 |       properties: Object.entries(value).map(([key, val]) => ({
 47 |         key,
 48 |         type: toTypeAST(val, [...path, key]),
 49 |         optional: false,
 50 |       })),
 51 |     };
 52 |   }
 53 | 
 54 |   return { kind: "primitive", type: typeof value };
 55 | }
 56 | 
 57 | // Merge multiple TypeASTs into one
 58 | export function mergeTypeASTs(types: TypeAST[]): TypeAST {
 59 |   if (types.length === 0) return { kind: "primitive", type: "any" };
 60 |   if (types.length === 1) return types[0];
 61 | 
 62 |   // Group ASTs by kind
 63 |   const byKind = {
 64 |     union: types.filter((t) => t.kind === "union"),
 65 |     primitive: types.filter((t) => t.kind === "primitive"),
 66 |     array: types.filter((t) => t.kind === "array"),
 67 |     object: types.filter((t) => t.kind === "object"),
 68 |   };
 69 | 
 70 |   // Create a union for mixed kinds
 71 |   const hasMixedKinds =
 72 |     byKind.union.length > 0 || // If we have any unions, treat it as mixed kinds
 73 |     (byKind.primitive.length > 0 &&
 74 |       (byKind.array.length > 0 || byKind.object.length > 0)) ||
 75 |     (byKind.array.length > 0 && byKind.object.length > 0);
 76 | 
 77 |   if (hasMixedKinds) {
 78 |     // If there are existing unions, flatten them into the current union
 79 |     if (byKind.union.length > 0) {
 80 |       // Flatten existing unions and collect types by category
 81 |       const flattenedTypes: TypeAST[] = [];
 82 |       const objectsToMerge: ObjectAST[] = [...byKind.object];
 83 |       const arraysToMerge: ArrayAST[] = [...byKind.array];
 84 | 
 85 |       // Add primitives directly
 86 |       flattenedTypes.push(...byKind.primitive);
 87 | 
 88 |       // Process union types
 89 |       for (const unionType of byKind.union) {
 90 |         for (const type of unionType.types) {
 91 |           if (type.kind === "object") {
 92 |             objectsToMerge.push(type);
 93 |           } else if (type.kind === "array") {
 94 |             arraysToMerge.push(type);
 95 |           } else {
 96 |             flattenedTypes.push(type);
 97 |           }
 98 |         }
 99 |       }
100 | 
101 |       // Merge objects and arrays if they exist
102 |       if (objectsToMerge.length > 0) {
103 |         flattenedTypes.push(mergeTypeASTs(objectsToMerge));
104 |       }
105 | 
106 |       if (arraysToMerge.length > 0) {
107 |         flattenedTypes.push(mergeTypeASTs(arraysToMerge));
108 |       }
109 | 
110 |       return { kind: "union", types: flattenedTypes };
111 |     }
112 | 
113 |     return { kind: "union", types };
114 |   }
115 | 
116 |   // Handle primitives
117 |   if (byKind.primitive.length === types.length) {
118 |     const uniqueTypes = [...new Set(byKind.primitive.map((p) => p.type))];
119 |     return uniqueTypes.length === 1
120 |       ? { kind: "primitive", type: uniqueTypes[0] }
121 |       : {
122 |           kind: "union",
123 |           types: uniqueTypes.map((type) => ({ kind: "primitive", type })),
124 |         };
125 |   }
126 | 
127 |   // Merge arrays
128 |   if (byKind.array.length === types.length) {
129 |     return {
130 |       kind: "array",
131 |       elementType: mergeTypeASTs(byKind.array.map((a) => a.elementType)),
132 |     };
133 |   }
134 | 
135 |   // Merge objects
136 |   if (byKind.object.length === types.length) {
137 |     // Get all unique property keys
138 |     const allKeys = [
139 |       ...new Set(
140 |         byKind.object.flatMap((obj) => obj.properties.map((p) => p.key)),
141 |       ),
142 |     ];
143 | 
144 |     // Merge properties with same keys
145 |     const mergedProperties = allKeys.map((key) => {
146 |       const props = byKind.object
147 |         .map((obj) => obj.properties.find((p) => p.key === key))
148 |         .filter((obj) => !!obj);
149 | 
150 |       return {
151 |         key,
152 |         type: mergeTypeASTs(props.map((p) => p.type)),
153 |         optional: byKind.object.some(
154 |           (obj) => !obj.properties.some((p) => p.key === key),
155 |         ),
156 |       };
157 |     });
158 | 
159 |     return { kind: "object", properties: mergedProperties };
160 |   }
161 | 
162 |   // Fallback
163 |   return { kind: "primitive", type: "any" };
164 | }
165 | 
166 | // Stringify TypeAST to TypeScript type declaration
167 | export function stringifyTypeAST(ast: TypeAST, nestLevel = 0): string {
168 |   const indent = " ".repeat(nestLevel * 2);
169 |   const nextIndent = " ".repeat((nestLevel + 1) * 2);
170 | 
171 |   switch (ast.kind) {
172 |     case "primitive":
173 |       return ast.type;
174 | 
175 |     case "array":
176 |       return `(${stringifyTypeAST(ast.elementType, nestLevel)})[]`;
177 | 
178 |     case "object":
179 |       if (ast.properties.length === 0) return "{}";
180 | 
181 |       return `{\n${ast.properties
182 |         .map(({ key, optional, type }) => {
183 |           return `${nextIndent}${quoteKey(key)}${optional ? "?" : ""}: ${stringifyTypeAST(
184 |             type,
185 |             nestLevel + 1,
186 |           )}`;
187 |         })
188 |         .join(",\n")}\n${indent}}`;
189 | 
190 |     case "union":
191 |       if (ast.types.length === 0) return "any";
192 |       if (ast.types.length === 1)
193 |         return stringifyTypeAST(ast.types[0], nestLevel);
194 |       return ast.types
195 |         .map((type) => stringifyTypeAST(type, nestLevel))
196 |         .join(" | ");
197 |   }
198 | }
199 | 
200 | export function quoteKey(key: string): string {
201 |   return /[^a-zA-Z0-9_]/.test(key) || /^[0-9]/.test(key) ? `"${key}"` : key;
202 | }
203 | 
204 | // Convert JSON array to TypeScript type
205 | export function JSONToType(json: JSONPrimitive[]): string | null {
206 |   if (!json.length) return null;
207 | 
208 |   return stringifyTypeAST(mergeTypeASTs(json.map((item) => toTypeAST(item))));
209 | }
210 | 
```

--------------------------------------------------------------------------------
/packages/node-sdk/test/batch-buffer.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import {
  2 |   afterAll,
  3 |   beforeAll,
  4 |   beforeEach,
  5 |   describe,
  6 |   expect,
  7 |   it,
  8 |   vi,
  9 | } from "vitest";
 10 | 
 11 | import BatchBuffer from "../src/batch-buffer";
 12 | import { BATCH_INTERVAL_MS, BATCH_MAX_SIZE } from "../src/config";
 13 | import { Logger } from "../src/types";
 14 | 
 15 | describe("BatchBuffer", () => {
 16 |   const mockFlushHandler = vi.fn();
 17 | 
 18 |   const mockLogger: Logger = {
 19 |     debug: vi.fn(),
 20 |     info: vi.fn(),
 21 |     warn: vi.fn(),
 22 |     error: vi.fn(),
 23 |   };
 24 | 
 25 |   beforeEach(() => {
 26 |     vi.clearAllMocks();
 27 |   });
 28 | 
 29 |   describe("constructor", () => {
 30 |     it("should throw an error if options are invalid", () => {
 31 |       expect(() => new BatchBuffer(null as any)).toThrow(
 32 |         "options must be an object",
 33 |       );
 34 | 
 35 |       expect(() => new BatchBuffer("bad" as any)).toThrow(
 36 |         "options must be an object",
 37 |       );
 38 | 
 39 |       expect(
 40 |         () => new BatchBuffer({ flushHandler: null as any } as any),
 41 |       ).toThrow("flushHandler must be a function");
 42 | 
 43 |       expect(
 44 |         () => new BatchBuffer({ flushHandler: "not a function" } as any),
 45 |       ).toThrow("flushHandler must be a function");
 46 | 
 47 |       expect(
 48 |         () =>
 49 |           new BatchBuffer({
 50 |             flushHandler: mockFlushHandler,
 51 |             logger: "string",
 52 |           } as any),
 53 |       ).toThrow("logger must be an object");
 54 | 
 55 |       expect(
 56 |         () =>
 57 |           new BatchBuffer({
 58 |             flushHandler: mockFlushHandler,
 59 |             maxSize: -1,
 60 |           } as any),
 61 |       ).toThrow("maxSize must be greater than 0");
 62 |     });
 63 | 
 64 |     it("should initialize with specified values", () => {
 65 |       const buffer = new BatchBuffer({
 66 |         flushHandler: mockFlushHandler,
 67 |         maxSize: 22,
 68 |         intervalMs: 33,
 69 |       });
 70 | 
 71 |       expect(buffer).toEqual({
 72 |         buffer: [],
 73 |         flushHandler: mockFlushHandler,
 74 |         timer: null,
 75 |         intervalMs: 33,
 76 |         logger: undefined,
 77 |         maxSize: 22,
 78 |       });
 79 |     });
 80 | 
 81 |     it("should initialize with default values if not provided", () => {
 82 |       const buffer = new BatchBuffer({ flushHandler: mockFlushHandler });
 83 |       expect(buffer).toEqual({
 84 |         buffer: [],
 85 |         flushHandler: mockFlushHandler,
 86 |         intervalMs: BATCH_INTERVAL_MS,
 87 |         maxSize: BATCH_MAX_SIZE,
 88 |         timer: null,
 89 |       });
 90 |     });
 91 |   });
 92 | 
 93 |   describe("add", () => {
 94 |     it("should add item to the buffer and flush immediately if maxSize is reached", async () => {
 95 |       const buffer = new BatchBuffer({
 96 |         flushHandler: mockFlushHandler,
 97 |         maxSize: 1,
 98 |       });
 99 | 
100 |       await buffer.add("item1");
101 | 
102 |       expect(mockFlushHandler).toHaveBeenCalledWith(["item1"]);
103 |       expect(mockFlushHandler).toHaveBeenCalledTimes(1);
104 |     });
105 | 
106 |     it("should set a flush timer if buffer does not reach maxSize", async () => {
107 |       vi.useFakeTimers();
108 | 
109 |       const buffer = new BatchBuffer({
110 |         flushHandler: mockFlushHandler,
111 |         maxSize: 2,
112 |         intervalMs: 1000,
113 |       });
114 | 
115 |       await buffer.add("item1");
116 |       expect(mockFlushHandler).not.toHaveBeenCalled();
117 | 
118 |       vi.advanceTimersByTime(1000);
119 |       expect(mockFlushHandler).toHaveBeenCalledWith(["item1"]);
120 |       expect(mockFlushHandler).toHaveBeenCalledTimes(1);
121 | 
122 |       vi.useRealTimers();
123 |     });
124 |   });
125 | 
126 |   describe("flush", () => {
127 |     it("should not do anything if there are no items to flush", async () => {
128 |       const buffer = new BatchBuffer({
129 |         flushHandler: mockFlushHandler,
130 |         logger: mockLogger,
131 |       });
132 | 
133 |       await buffer.flush();
134 | 
135 |       expect(mockFlushHandler).not.toHaveBeenCalled();
136 | 
137 |       expect(mockLogger.debug).toHaveBeenCalledWith(
138 |         "buffer is empty. nothing to flush",
139 |       );
140 |     });
141 | 
142 |     it("calling flush simultaneously should only flush data once", async () => {
143 |       let itemsFlushed = 0;
144 |       const buffer = new BatchBuffer({
145 |         flushHandler: async (items) => {
146 |           itemsFlushed += items.length;
147 |           await new Promise((resolve) => setTimeout(resolve, 100));
148 |           mockFlushHandler();
149 |         },
150 |         logger: mockLogger,
151 |       });
152 | 
153 |       await buffer.add("item1");
154 |       await Promise.all([buffer.flush(), buffer.flush()]);
155 |       expect(itemsFlushed).toBe(1);
156 |     });
157 | 
158 |     it("should flush buffer", async () => {
159 |       const buffer = new BatchBuffer({
160 |         flushHandler: mockFlushHandler,
161 |         logger: mockLogger,
162 |       });
163 | 
164 |       await buffer.add("item1");
165 |       await buffer.flush();
166 | 
167 |       expect(mockFlushHandler).toHaveBeenCalledWith(["item1"]);
168 |       await buffer.flush();
169 | 
170 |       expect(mockFlushHandler).toHaveBeenCalledTimes(1);
171 |     });
172 | 
173 |     it("should log correctly during flush", async () => {
174 |       const buffer = new BatchBuffer({
175 |         flushHandler: mockFlushHandler,
176 |         logger: mockLogger,
177 |       });
178 | 
179 |       await buffer.add("item1");
180 |       await buffer.flush();
181 | 
182 |       expect(mockLogger.info).toHaveBeenCalledWith("flushed buffered items", {
183 |         count: 1,
184 |       });
185 |     });
186 |   });
187 | 
188 |   describe("timer logic", () => {
189 |     beforeAll(() => {
190 |       vi.useFakeTimers();
191 |     });
192 | 
193 |     afterAll(() => {
194 |       vi.useRealTimers();
195 |     });
196 | 
197 |     beforeEach(() => {
198 |       vi.clearAllTimers();
199 |       mockFlushHandler.mockReset();
200 |     });
201 | 
202 |     it("should start the normal timer when adding first item", async () => {
203 |       const buffer = new BatchBuffer({
204 |         flushHandler: mockFlushHandler,
205 |         logger: mockLogger,
206 |         intervalMs: 100,
207 |       });
208 | 
209 |       expect(buffer["timer"]).toBeNull();
210 |       await buffer.add("item1");
211 | 
212 |       expect(buffer["timer"]).toBeDefined();
213 | 
214 |       await vi.advanceTimersByTimeAsync(100);
215 |       expect(mockFlushHandler).toHaveBeenCalledTimes(1);
216 | 
217 |       expect(buffer["timer"]).toBeNull();
218 | 
219 |       expect(mockLogger.info).toHaveBeenCalledWith("flushed buffered items", {
220 |         count: 1,
221 |       });
222 |     });
223 | 
224 |     it("should stop the normal timer if flushed manually", async () => {
225 |       const buffer = new BatchBuffer({
226 |         flushHandler: mockFlushHandler,
227 |         logger: mockLogger,
228 |         intervalMs: 100,
229 |         maxSize: 2,
230 |       });
231 | 
232 |       await buffer.add("item1");
233 |       await buffer.add("item2");
234 | 
235 |       expect(buffer["timer"]).toBeNull();
236 | 
237 |       expect(mockLogger.info).toHaveBeenCalledWith("flushed buffered items", {
238 |         count: 2,
239 |       });
240 |     });
241 |   });
242 | });
243 | 
```

--------------------------------------------------------------------------------
/packages/openfeature-node-provider/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import {
  2 |   ErrorCode,
  3 |   EvaluationContext,
  4 |   JsonValue,
  5 |   OpenFeatureEventEmitter,
  6 |   Paradigm,
  7 |   Provider,
  8 |   ResolutionDetails,
  9 |   ServerProviderStatus,
 10 |   StandardResolutionReasons,
 11 |   TrackingEventDetails,
 12 | } from "@openfeature/server-sdk";
 13 | 
 14 | import {
 15 |   ClientOptions,
 16 |   Context as ReflagContext,
 17 |   ReflagClient,
 18 | } from "@reflag/node-sdk";
 19 | 
 20 | type ProviderOptions = ClientOptions & {
 21 |   contextTranslator?: (context: EvaluationContext) => ReflagContext;
 22 | };
 23 | 
 24 | export const defaultContextTranslator = (
 25 |   context: EvaluationContext,
 26 | ): ReflagContext => {
 27 |   const user = {
 28 |     id: context.targetingKey ?? context["userId"]?.toString(),
 29 |     name: context["name"]?.toString(),
 30 |     email: context["email"]?.toString(),
 31 |     avatar: context["avatar"]?.toString(),
 32 |     country: context["country"]?.toString(),
 33 |   };
 34 | 
 35 |   const company = {
 36 |     id: context["companyId"]?.toString(),
 37 |     name: context["companyName"]?.toString(),
 38 |     avatar: context["companyAvatar"]?.toString(),
 39 |     plan: context["companyPlan"]?.toString(),
 40 |   };
 41 | 
 42 |   return {
 43 |     user,
 44 |     company,
 45 |   };
 46 | };
 47 | 
 48 | export class ReflagNodeProvider implements Provider {
 49 |   public readonly events = new OpenFeatureEventEmitter();
 50 | 
 51 |   private _client: ReflagClient;
 52 | 
 53 |   private contextTranslator: (context: EvaluationContext) => ReflagContext;
 54 | 
 55 |   public runsOn: Paradigm = "server";
 56 | 
 57 |   public status: ServerProviderStatus = ServerProviderStatus.NOT_READY;
 58 | 
 59 |   public metadata = {
 60 |     name: "reflag-node",
 61 |   };
 62 | 
 63 |   get client() {
 64 |     return this._client;
 65 |   }
 66 | 
 67 |   constructor({ contextTranslator, ...opts }: ProviderOptions) {
 68 |     this._client = new ReflagClient(opts);
 69 |     this.contextTranslator = contextTranslator ?? defaultContextTranslator;
 70 |   }
 71 | 
 72 |   public async initialize(): Promise<void> {
 73 |     await this._client.initialize();
 74 |     this.status = ServerProviderStatus.READY;
 75 |   }
 76 | 
 77 |   private resolveFlag<T extends JsonValue>(
 78 |     flagKey: string,
 79 |     defaultValue: T,
 80 |     context: ReflagContext,
 81 |     resolveFn: (
 82 |       feature: ReturnType<typeof this._client.getFlag>,
 83 |     ) => Promise<ResolutionDetails<T>>,
 84 |   ): Promise<ResolutionDetails<T>> {
 85 |     if (this.status !== ServerProviderStatus.READY) {
 86 |       return Promise.resolve({
 87 |         value: defaultValue,
 88 |         reason: StandardResolutionReasons.ERROR,
 89 |         errorCode: ErrorCode.PROVIDER_NOT_READY,
 90 |         errorMessage: "Reflag client not initialized",
 91 |       });
 92 |     }
 93 | 
 94 |     if (!context.user?.id) {
 95 |       return Promise.resolve({
 96 |         value: defaultValue,
 97 |         reason: StandardResolutionReasons.ERROR,
 98 |         errorCode: ErrorCode.INVALID_CONTEXT,
 99 |         errorMessage: "At least a user ID is required",
100 |       });
101 |     }
102 | 
103 |     const featureDefs = this._client.getFlagDefinitions();
104 |     if (featureDefs.some(({ key }) => key === flagKey)) {
105 |       return resolveFn(this._client.getFlag(context, flagKey));
106 |     }
107 | 
108 |     return Promise.resolve({
109 |       value: defaultValue,
110 |       reason: StandardResolutionReasons.ERROR,
111 |       errorCode: ErrorCode.FLAG_NOT_FOUND,
112 |       errorMessage: `Flag ${flagKey} not found`,
113 |     });
114 |   }
115 | 
116 |   resolveBooleanEvaluation(
117 |     flagKey: string,
118 |     defaultValue: boolean,
119 |     context: EvaluationContext,
120 |   ): Promise<ResolutionDetails<boolean>> {
121 |     return this.resolveFlag(
122 |       flagKey,
123 |       defaultValue,
124 |       this.contextTranslator(context),
125 |       (feature) => {
126 |         return Promise.resolve({
127 |           value: feature.isEnabled,
128 |           variant: feature.config?.key,
129 |           reason: StandardResolutionReasons.TARGETING_MATCH,
130 |         });
131 |       },
132 |     );
133 |   }
134 | 
135 |   resolveStringEvaluation(
136 |     flagKey: string,
137 |     defaultValue: string,
138 |     context: EvaluationContext,
139 |   ): Promise<ResolutionDetails<string>> {
140 |     return this.resolveFlag(
141 |       flagKey,
142 |       defaultValue,
143 |       this.contextTranslator(context),
144 |       (feature) => {
145 |         if (!feature.config.key) {
146 |           return Promise.resolve({
147 |             value: defaultValue,
148 |             reason: StandardResolutionReasons.DEFAULT,
149 |           });
150 |         }
151 | 
152 |         return Promise.resolve({
153 |           value: feature.config.key as string,
154 |           variant: feature.config.key,
155 |           reason: StandardResolutionReasons.TARGETING_MATCH,
156 |         });
157 |       },
158 |     );
159 |   }
160 | 
161 |   resolveNumberEvaluation(
162 |     _flagKey: string,
163 |     defaultValue: number,
164 |   ): Promise<ResolutionDetails<number>> {
165 |     return Promise.resolve({
166 |       value: defaultValue,
167 |       reason: StandardResolutionReasons.ERROR,
168 |       errorCode: ErrorCode.GENERAL,
169 |       errorMessage:
170 |         "Reflag doesn't support this method. Use `resolveObjectEvaluation` instead.",
171 |     });
172 |   }
173 | 
174 |   resolveObjectEvaluation<T extends JsonValue>(
175 |     flagKey: string,
176 |     defaultValue: T,
177 |     context: EvaluationContext,
178 |   ): Promise<ResolutionDetails<T>> {
179 |     return this.resolveFlag(
180 |       flagKey,
181 |       defaultValue,
182 |       this.contextTranslator(context),
183 |       (feature) => {
184 |         const expType = typeof defaultValue;
185 |         const payloadType = typeof feature.config.payload;
186 | 
187 |         if (
188 |           feature.config.payload === undefined ||
189 |           feature.config.payload === null ||
190 |           payloadType !== expType
191 |         ) {
192 |           return Promise.resolve({
193 |             value: defaultValue,
194 |             variant: feature.config.key,
195 |             reason: StandardResolutionReasons.ERROR,
196 |             errorCode: ErrorCode.TYPE_MISMATCH,
197 |             errorMessage: `Expected remote config payload of type \`${expType}\` but got \`${payloadType}\`.`,
198 |           });
199 |         }
200 | 
201 |         return Promise.resolve({
202 |           value: feature.config.payload,
203 |           variant: feature.config.key,
204 |           reason: StandardResolutionReasons.TARGETING_MATCH,
205 |         });
206 |       },
207 |     );
208 |   }
209 | 
210 |   track(
211 |     trackingEventName: string,
212 |     context?: EvaluationContext,
213 |     trackingEventDetails?: TrackingEventDetails,
214 |   ): void {
215 |     const translatedContext = context
216 |       ? this.contextTranslator(context)
217 |       : undefined;
218 | 
219 |     const userId = translatedContext?.user?.id;
220 |     if (!userId) {
221 |       this._client.logger?.warn("No user ID provided for tracking event");
222 |       return;
223 |     }
224 | 
225 |     void this._client.track(String(userId), trackingEventName, {
226 |       attributes: trackingEventDetails,
227 |       companyId: translatedContext?.company?.id?.toString(),
228 |     });
229 |   }
230 | 
231 |   public async onClose(): Promise<void> {
232 |     await this._client.flush();
233 |   }
234 | }
235 | 
```
Page 4/9FirstPrevNextLast