diff --git a/README.md b/README.md index 15448d5..fb5f939 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ import { rep } from '@rep-protocol/sdk'; // PUBLIC vars — synchronous, no async, no loading state const apiUrl = rep.get('API_URL'); -const flags = rep.get('FEATURE_FLAGS'); +const flags = rep.get('FEATURE_FLAGS'); // see rep-protocol.dev/guides/feature-flags/ // SENSITIVE vars — encrypted, decrypted on demand const key = await rep.getSecure('ANALYTICS_KEY'); diff --git a/docs/src/content/docs/agents.mdx b/docs/src/content/docs/agents.mdx index 3b464df..0b7f27c 100644 --- a/docs/src/content/docs/agents.mdx +++ b/docs/src/content/docs/agents.mdx @@ -415,6 +415,7 @@ When this page is not enough, these are the pages to read next — each is avail - [Quick Start](/quick-start.md) — the five-minute path - [How REP Works](/concepts/how-it-works.md) — startup sequence and injection mechanics - [Variable Classification](/concepts/variable-classification.md) — tier rules and guardrail internals +- [Feature Flags](/guides/feature-flags.md) — one `csv` variable, flipped without a rebuild - [Security Model](/concepts/security-model.md) — threat analyses and hardening - [Wire Format](/concepts/wire-format.md) — payload JSON, encrypted blob layout, HMAC - [SDK API](/reference/sdk.md) — full client reference diff --git a/docs/src/content/docs/concepts/variable-classification.mdx b/docs/src/content/docs/concepts/variable-classification.mdx index 75a7d8d..5824ab3 100644 --- a/docs/src/content/docs/concepts/variable-classification.mdx +++ b/docs/src/content/docs/concepts/variable-classification.mdx @@ -70,6 +70,8 @@ At startup, the gateway scans all `REP_PUBLIC_*` values for patterns indicating | **Known key formats** | AWS keys (`AKIA...`), JWTs (`eyJ...`), GitHub tokens (`ghp_...`), Stripe keys (`sk_live_...`), private keys (`-----BEGIN`) | | **Length anomalies** | Strings > 64 characters that may be encoded secrets | +A variable declared `type: csv` in the [manifest](/guides/manifest/) is scanned one element at a time (split on commas, whitespace trimmed) rather than as one string. A long list of short feature flags therefore doesn't trip the length check under `--strict`. A long or secret-shaped token inside the list is still flagged, and the warning names its position (`csv element 3: …` in the message, `csv_element` in the log). Every other value is scanned whole, and so is every value in the dev plugins, which [don't read the manifest](/guides/feature-flags/#dev-plugins). + When a potential misclassification is detected: - The gateway logs a **WARNING** with the variable name (never the value) diff --git a/docs/src/content/docs/guides/development.mdx b/docs/src/content/docs/guides/development.mdx index 9789612..76b0322 100644 --- a/docs/src/content/docs/guides/development.mdx +++ b/docs/src/content/docs/guides/development.mdx @@ -69,7 +69,7 @@ The easiest way to get full REP support in development. The plugin injects the s ## Option B: Default values (simplest) diff --git a/docs/src/content/docs/guides/feature-flags.mdx b/docs/src/content/docs/guides/feature-flags.mdx new file mode 100644 index 0000000..4b85eea --- /dev/null +++ b/docs/src/content/docs/guides/feature-flags.mdx @@ -0,0 +1,88 @@ +--- +title: Feature Flags — One csv Variable, Flipped Without a Rebuild +description: Use a single REP_PUBLIC_FEATURE_FLAGS csv variable for deployment-wide feature flags. Manifest declaration, SDK reads, guardrail behaviour, flipping flags without a rebuild, and what REP flags are not. +--- + +import { Aside } from '@astrojs/starlight/components'; + +REP can carry simple feature flags: switches that are on or off for a whole deployment. Use one `csv` variable that lists the flags that are on. + +## Declare one variable + +```yaml +# .rep.yaml +variables: + FEATURE_FLAGS: + tier: public + type: csv + default: "" + pattern: "([a-z0-9-]+(,[a-z0-9-]+)*)?" # optional: kebab-case names, no spaces + description: "Flags that are on, comma-separated" +``` + +Use one variable for all flags, not one per flag. Adding a flag is then a value change, not a manifest change. + +With `default: ""`, an unset `REP_PUBLIC_FEATURE_FLAGS` becomes an empty list, so every flag is off (see [Defaults](/guides/manifest/#defaults)). The optional `pattern` rejects typos such as stray spaces or capitals at startup. + +## Read it with the SDK + +```typescript +import { rep } from '@rep-protocol/sdk'; + +const enabled = new Set( + rep.get('FEATURE_FLAGS', '').split(',').map((f) => f.trim()).filter(Boolean), +); + +export const isOn = (flag: string) => enabled.has(flag); +``` + +A flag is **off unless it is named**. There is no list of known flags to keep in sync and no "false" value to set. + +The second argument to `rep.get()` covers the case with no gateway at all, such as unit tests or a plain `vite dev`. Behind the gateway, the manifest default already guarantees a string. + +For example, a single image might serve a marketing landing page at `/` on its hosted deployment and redirect `/` straight to sign-in everywhere else: + +```bash +# hosted deployment +REP_PUBLIC_FEATURE_FLAGS=landing-page +# self-hosted deployment: leave it unset — every flag is off +``` + +```typescript +if (location.pathname === '/' && !isOn('landing-page')) location.replace('/login'); +``` + +## Flags are public + +`FEATURE_FLAGS` is a PUBLIC variable, so its value is in the page source of every page the gateway serves. + +- **Never put a secret in a flag name.** That includes a token, a customer name, or an unreleased product's codename you can't disclose. +- **A flag is not access control.** Hiding a button does not stop anyone calling the API behind it, so the server must still check permissions. + +## Guardrails judge each flag, not the list + +The gateway's [guardrails](/concepts/variable-classification/#automatic-secret-detection-guardrails) check a `csv` value one element at a time. A long list of short flags can't make `--strict` refuse to start, and an element that looks like a secret is still flagged. + +### Dev plugins + + + +## Flipping a flag + +You flip a flag by changing the environment, not the image: + +- **Restart:** set `REP_PUBLIC_FEATURE_FLAGS` and restart the container or roll the deployment. +- **Hot reload:** with `--hot-reload` and `--env-file`, the gateway picks up edits to that file without a restart; it does not see changes to its own process environment. Pages using `rep.onChange('FEATURE_FLAGS', …)` or a framework adapter update in place. See [Hot Reload](/concepts/hot-reload/), the [Kubernetes recipe](/deployment/kubernetes/), and the end-to-end [Next.js example](/examples/nextjs-csr-embedded/). + +## What this is not + +A REP flag has one value per deployment, shared by every visitor. That makes it a good fit for environment differences, kill switches and staged launches. It cannot do: + +- per-user or per-account targeting; +- percentage or gradual rollouts; +- A/B tests or experiments; +- an audit trail or a UI for non-engineers. + +If you need any of those, use a dedicated feature-flag service. REP can still deliver that service's public client key as a PUBLIC variable. diff --git a/docs/src/content/docs/guides/manifest.mdx b/docs/src/content/docs/guides/manifest.mdx index d71ce01..31163a3 100644 --- a/docs/src/content/docs/guides/manifest.mdx +++ b/docs/src/content/docs/guides/manifest.mdx @@ -91,7 +91,7 @@ settings: | `url` | Must be a valid URL | | `number` | Must parse as a number | | `boolean` | Must be `true`, `false`, `1`, or `0` | -| `csv` | Comma-separated values | +| `csv` | Comma-separated values. [Guardrails](/concepts/variable-classification/#automatic-secret-detection-guardrails) scan each element separately. See [Feature Flags](/guides/feature-flags/) | | `json` | Must be valid JSON | | `enum` | Must match one of the `values` array entries | diff --git a/docs/src/content/docs/reference/plugins/next.mdx b/docs/src/content/docs/reference/plugins/next.mdx index 16da353..bd5d3d2 100644 --- a/docs/src/content/docs/reference/plugins/next.mdx +++ b/docs/src/content/docs/reference/plugins/next.mdx @@ -65,6 +65,7 @@ Only needed if you read `REP_SENSITIVE_*` variables with `rep.getSecure()` / `us - **Server Component only.** `RepScript` reads the filesystem (`env`) and generates keys server-side; it cannot be used in a Client Component. Consume the injected payload from Client Components via `rep.get()` / `useRep()` as usual — the SDK reads the DOM, not React context. - **Script content is Go-escaped** (`<`, `>`, `&` → `<`, `>`, `&`) before being written via `dangerouslySetInnerHTML`, preventing `` breakout from untrusted env values. The tag still carries `type="application/json"`, so the browser never executes it. - **Ephemeral keys are process-scoped**, held in a module-level singleton (`getOrCreateKeys()`) so the same key survives across route handler invocations within one `next dev` process. +- **Guardrails scan every `REP_PUBLIC_*` value whole**, including `csv` values the gateway scans by element ([details](/guides/feature-flags/#dev-plugins)). - **Byte-identical payload** to the Go gateway and the Vite plugin (sorted keys, same HMAC + SRI format) — `rep.verify()` and `rep.meta()` behave the same regardless of which one produced the payload. ## App Router only diff --git a/docs/src/content/docs/reference/plugins/vite.mdx b/docs/src/content/docs/reference/plugins/vite.mdx index 3617ca2..50e5ca9 100644 --- a/docs/src/content/docs/reference/plugins/vite.mdx +++ b/docs/src/content/docs/reference/plugins/vite.mdx @@ -48,7 +48,7 @@ repPlugin({ - **Injects `