@quonfig/cli 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +157 -75
  2. package/dist/agent/skill-bundle.generated.d.ts +1 -1
  3. package/dist/agent/skill-bundle.generated.js +4 -4
  4. package/dist/codegen/code-generators/base-typescript-generator.d.ts +1 -1
  5. package/dist/codegen/schema-extractor.d.ts +1 -1
  6. package/dist/commands/config-schema.js +9 -2
  7. package/dist/commands/create.d.ts +3 -1
  8. package/dist/commands/create.js +93 -20
  9. package/dist/commands/get.d.ts +1 -0
  10. package/dist/commands/get.js +40 -3
  11. package/dist/commands/info.js +10 -1
  12. package/dist/commands/set-default.d.ts +2 -0
  13. package/dist/commands/set-default.js +70 -18
  14. package/dist/index.d.ts +6 -0
  15. package/dist/index.js +35 -3
  16. package/dist/interactive-prompt.d.ts +18 -0
  17. package/dist/interactive-prompt.js +11 -2
  18. package/dist/migrate/quonfig-target/operators.js +4 -2
  19. package/dist/migrate/quonfig-target/ruleset.d.ts +48 -0
  20. package/dist/migrate/quonfig-target/ruleset.js +132 -8
  21. package/dist/migrate/sources/flagsmith/translate.js +11 -0
  22. package/dist/migrate/sources/launchdarkly/translate.js +19 -16
  23. package/dist/push/git-pack.d.ts +12 -0
  24. package/dist/push/git-pack.js +27 -3
  25. package/dist/push/run-push.d.ts +2 -1
  26. package/dist/push/run-push.js +1 -2
  27. package/dist/ui/get-string.d.ts +18 -1
  28. package/dist/ui/get-string.js +20 -5
  29. package/dist/ui/get-value.d.ts +3 -1
  30. package/dist/ui/get-value.js +7 -7
  31. package/dist/util/clone-and-stack-push.js +8 -3
  32. package/dist/util/encryption.js +11 -20
  33. package/dist/util/git-ops.d.ts +4 -2
  34. package/dist/util/git-ops.js +4 -2
  35. package/dist/util/redact.d.ts +56 -0
  36. package/dist/util/redact.js +98 -0
  37. package/dist/util/value-source.d.ts +30 -0
  38. package/dist/util/value-source.js +81 -0
  39. package/dist/verify/regex-dialect.d.ts +39 -0
  40. package/dist/verify/regex-dialect.js +278 -0
  41. package/dist/verify/standalone.d.ts +3 -1
  42. package/dist/verify/standalone.js +4 -19
  43. package/dist/verify/validate.js +20 -0
  44. package/dist/version.d.ts +1 -1
  45. package/dist/version.js +1 -1
  46. package/oclif.manifest.json +236 -192
  47. package/package.json +18 -16
@@ -1,5 +1,5 @@
1
1
  export declare const SKILL_NAME = "quonfig";
2
- export declare const SKILL_VERSION = "1.0.1";
2
+ export declare const SKILL_VERSION = "1.1.0";
3
3
  export declare const SKILL_FILES: ReadonlyArray<{
4
4
  path: string;
5
5
  content: string;
@@ -3,17 +3,17 @@
3
3
  // then run `node scripts/sync-agent-skill.mjs`.
4
4
  /* eslint-disable */
5
5
  export const SKILL_NAME = "quonfig";
6
- export const SKILL_VERSION = "1.0.1";
6
+ export const SKILL_VERSION = "1.1.0";
7
7
  export const SKILL_FILES = [
8
- { path: "SKILL.md", content: "---\nname: quonfig\ndescription: Use Quonfig feature flags, dynamic config, and log levels correctly from an application codebase. Covers picking and wiring up the right Quonfig SDK (Node, browser JS, React, React Native, Go, Python, Ruby, Java, .NET, Swift), creating flags and configs with the qfg CLI, gating code with the SDK and the right context, rolling out per environment, typed clients, and retiring flags — plus the Quonfig MCP tools when there is no checkout. Use this skill whenever the user wants to add, check, roll out, toggle, read, or remove a feature flag or remote config value, put code behind a flag, add a kill switch, change a log level at runtime, or mentions Quonfig, qfg, @quonfig/*, QUONFIG_BACKEND_SDK_KEY, or a flag key in this repo.\n---\n\n# Quonfig\n\nQuonfig stores feature flags, configs, segments, and log levels as JSON files in\na git repo (the **workspace**). Your application reads them through a Quonfig\nSDK; people and agents change them with the `qfg` CLI, the Quonfig MCP server,\nor the web app. Every change is a git commit.\n\nThis skill is for an **application repo** (code that *reads* flags). If the\ncurrent repo is itself a workspace (it has `quonfig.json` and `feature-flags/`,\n`configs/` directories), follow that repo's `AGENTS.md`/`CLAUDE.md` instead:\nedit the JSON there and run `qfg verify`.\n\n## 1. Orient before writing code\n\n- **Find the SDK.** Look for an existing client before adding one: grep for\n `@quonfig/`, `quonfig` in `go.mod` / `Gemfile` / `pyproject.toml` /\n `requirements.txt` / `pom.xml` / `build.gradle*` / `*.csproj` /\n `Package.swift`, and for where the client is constructed. Reuse it — one\n client per process.\n- **Read the reference for this language** (only the one you need):\n\n | Stack | Reference |\n |-------|-----------|\n | Node.js server (`@quonfig/node`) | [references/sdk-node.md](references/sdk-node.md) |\n | Browser JS, React, React Native | [references/sdk-browser.md](references/sdk-browser.md) |\n | Go | [references/sdk-go.md](references/sdk-go.md) |\n | Python | [references/sdk-python.md](references/sdk-python.md) |\n | Ruby | [references/sdk-ruby.md](references/sdk-ruby.md) |\n | Java / Kotlin | [references/sdk-java.md](references/sdk-java.md) |\n | .NET | [references/sdk-dotnet.md](references/sdk-dotnet.md) |\n | Swift (iOS/macOS) | [references/sdk-swift.md](references/sdk-swift.md) |\n\n- **Pick the surface for changing flags.** You have a shell and a checkout, so\n use `qfg` (check with `qfg whoami`; install with `npm install -g @quonfig/cli`,\n sign in with `qfg login` — the human does the browser step). Use the MCP\n tools only when `qfg` is unavailable or for a quick read of live state.\n Details: [references/writes.md](references/writes.md).\n\n## 2. Adding a feature flag (the core loop)\n\n1. **Name it.** Keys are lowercase, dot-separated, grouped by area:\n `checkout.new-flow`, `search.semantic-ranking`. Check it isn't taken and\n isn't already what you need: `qfg list --feature-flags --json`.\n2. **Create it, off.** `qfg create checkout.new-flow --type boolean-flag --value=false`.\n If it already exists, stop and ask — never invent a variant of the key.\n3. **Gate the code** with the SDK's flag call and a context that identifies\n who is asking (at minimum `user.key`, so rollouts and targeting work):\n - Node: `quonfig.isEnabled(\"checkout.new-flow\", { user: { key: user.id } })`\n - Go: `on, _ := client.FeatureIsOn(\"checkout.new-flow\", ctx)`\n - Python: `client.is_feature_enabled(\"checkout.new-flow\", contexts={\"user\": {\"key\": user.id}})`\n - others: see the language reference.\n Keep the off path identical to today's behavior, so the flag can ship dark.\n4. **Turn it on where it is safe** — normally development first:\n `qfg set-default checkout.new-flow --value=true --environment=development --confirm`.\n Always name the environment. (`--confirm` skips the interactive prompt —\n without a terminal, `set-default` and `set-rollout` refuse to run without it.) Do not change production (or roll out there)\n unless the human asked for it.\n5. **Tell the human** the key, which environments serve what, and the rollout\n command for later (`qfg set-rollout checkout.new-flow --environment=production --true-percent 10 --confirm`).\n\n## 3. Configs and log levels\n\n- A dynamic value instead of a constant: `qfg create checkout.timeout --type int --value=30`,\n then read it with the SDK's typed getter (see the language reference).\n Types: `string`, `int`, `double`, `boolean`, `string-list`, `json`, `duration`.\n- Create a key **before** shipping code that reads it — most server SDKs throw\n on a missing key with no default.\n- Runtime log levels: one `log-level.<service>` key per service, wired into\n the app's logger via the SDK's `loggerKey`; change one logger with\n `qfg log-level log-level.checkout-api --target=Checkout.Payments --value=DEBUG --environment=staging`.\n- TypeScript apps can generate a typed client: `qfg generate --targets node-ts`\n (server) or `--targets react-ts` (React).\n\n## 4. Rules that prevent the classic mistakes\n\n- **Name the environment** in every write and every answer about state. A flag\n is usually on in development and off in production.\n- **Setting a value keeps targeting rules.** `set-default`/`set-rollout` change\n only the fallback rule; `--replace-targeting` deletes the environment's\n targeting and needs a human's agreement first. Read with `qfg info <key>`\n before you write. See [references/writes.md](references/writes.md).\n- **Backend vs frontend keys.** Server SDKs use `QUONFIG_BACKEND_SDK_KEY` and\n evaluate locally; browser/mobile SDKs use a frontend key and get values\n evaluated for one context. Never ship a backend key to a browser or app.\n Never commit any key.\n- **The SDK key decides the environment** in server SDKs. Set\n `QUONFIG_ENVIRONMENT` / `environment` only for local datadir mode.\n\n## 5. More\n\n- Full `qfg` cheat sheet (create types, rollouts, SDK keys, overrides, `qfg run`):\n [references/cli.md](references/cli.md)\n- Quonfig MCP tools (Slack, claude.ai, no checkout; undo with `set_document`):\n [references/mcp.md](references/mcp.md)\n- Removing a flag once its rollout is done (`qfg cleanup`):\n [references/retire-a-flag.md](references/retire-a-flag.md)\n- Docs: https://docs.quonfig.com\n" },
9
- { path: "references/cli.md", content: "# `qfg` command reference (app-developer loop)\n\nInstall: `npm install -g @quonfig/cli`. Every command takes `--json` for\nstructured output; prompts turn off automatically when stdin is not a TTY, so\npass every required flag — including `--confirm` on `set-default` /\n`set-rollout` and `--yes` on `push` / `delete`, which otherwise refuse to run\nwithout a terminal. `qfg <command> --help` is authoritative.\n\n## Auth and workspace\n\n```bash\nqfg login # browser device sign-in, once per machine\nqfg whoami # who am I, which orgs\nqfg workspace # active workspace + everything reachable\nqfg workspace switch acme/production\nqfg list -w acme/production # -w <org>/<workspace> on any API command\n```\n\nCI: set `QUONFIG_API_KEY` **and** `QUONFIG_WORKSPACE=<org>/<workspace>`.\n\n## Look before you create\n\n```bash\nqfg list --feature-flags --json # what exists (also --configs, --log-levels, --segments)\nqfg info checkout.new-flow # every environment's rules + 24h evaluations\nqfg get checkout.timeout --environment production # evaluated value in one env\n```\n\n## Create\n\n```bash\nqfg create checkout.new-flow --type boolean-flag --value=false\nqfg create checkout.banner-text --type string --value=\"Free shipping today\"\nqfg create checkout.timeout --type int --value=30\nqfg create checkout.retry-window --type duration --value PT90S\nqfg create log-level.checkout-api --type log_level --value WARN\n```\n\n- `--type`: `boolean-flag | boolean | string | int | double | string-list | json | duration | log_level`.\n Use `boolean-flag` for a feature flag (it is also sent to client SDKs);\n `boolean` is a plain config.\n- `create` sets the default for all environments; there is no `--environment`.\n- If the key already exists the command fails (\"... already exists\"). Do not\n retry with a tweaked key — use the existing one or agree a new name with a\n human.\n\n## Change what an environment serves\n\n```bash\nqfg set-default checkout.new-flow --value=true --environment=development --confirm\nqfg set-rollout checkout.new-flow --environment=staging --true-percent 20 --confirm\nqfg set-rollout checkout.layout --environment=production --weights \"control:50,treatment:50\" --confirm\nqfg set-default checkout.new-flow --value=false --environment=production --replace-targeting --confirm # kill switch: off for everyone\n```\n\n`--environment` is required when not interactive. These keep targeting rules\nunless you pass `--replace-targeting` — see [writes.md](writes.md).\n\n## Log levels\n\n```bash\nqfg log-level log-level.checkout-api --value=WARN # creates the key (fails if it exists)\nqfg set-default log-level.checkout-api --value=INFO --environment=production --confirm # change an existing default\nqfg log-level log-level.checkout-api --target=Checkout.Payments --value=DEBUG --environment=staging\n```\n\n## SDK keys\n\n```bash\nqfg sdk-key create --environment production --type server # backend: QUONFIG_BACKEND_SDK_KEY\nqfg sdk-key create --environment development --type browser # frontend SDKs\nqfg sdk-key list\n```\n\nNever commit a key; put it in the app's secret store / `.env` (gitignored).\n\n## Typed clients (TypeScript only)\n\n```bash\nqfg generate --targets node-ts -o generated # @quonfig/node\nqfg generate --targets react-ts -o generated # @quonfig/react (default target)\n```\n\n## Run a process with config injected as env vars\n\n```bash\nqfg run --env DATABASE_URL=db.url --environment=staging -- npm run migrate\n```\n\n## Personal override (only you see it)\n\n```bash\nqfg override checkout.new-flow true --env=development\nqfg override checkout.new-flow --remove --env=development\n```\n\nFires only for SDK clients that send the `quonfig-user.email` context.\n\n## Workspace repo (editing JSON directly)\n\n```bash\nqfg pull --dir ./quonfig-workspace # clone/refresh the workspace repo\nqfg verify ./quonfig-workspace # validate (local, no login)\nqfg push --dir ./quonfig-workspace --yes # publish committed edits\nqfg config-schema # operators, field names, value types\n```\n\nInside a workspace repo, follow that repo's `AGENTS.md` / `CLAUDE.md`.\n" },
8
+ { path: "SKILL.md", content: "---\nname: quonfig\ndescription: Use Quonfig feature flags, dynamic config, and log levels correctly from an application codebase. Covers picking and wiring up the right Quonfig SDK (Node, browser JS, React, React Native, Go, Python, Ruby, Java, .NET, Swift), creating flags and configs with the qfg CLI, gating code with the SDK and the right context, rolling out per environment, typed clients, and retiring flags — plus the Quonfig MCP tools when there is no checkout. Use this skill whenever the user wants to add, check, roll out, toggle, read, or remove a feature flag or remote config value, put code behind a flag, add a kill switch, change a log level at runtime, or mentions Quonfig, qfg, @quonfig/*, QUONFIG_BACKEND_SDK_KEY, or a flag key in this repo.\n---\n\n# Quonfig\n\nQuonfig stores feature flags, configs, segments, and log levels as JSON files in\na git repo (the **workspace**). Your application reads them through a Quonfig\nSDK; people and agents change them with the `qfg` CLI, the Quonfig MCP server,\nor the web app. Every change is a git commit.\n\nThis skill is for an **application repo** (code that *reads* flags). If the\ncurrent repo is itself a workspace (it has `quonfig.json` and `feature-flags/`,\n`configs/` directories), follow that repo's `AGENTS.md`/`CLAUDE.md` instead:\nedit the JSON there and run `qfg verify`.\n\n## 1. Orient before writing code\n\n- **Find the SDK.** Look for an existing client before adding one: grep for\n `@quonfig/`, `quonfig` in `go.mod` / `Gemfile` / `pyproject.toml` /\n `requirements.txt` / `pom.xml` / `build.gradle*` / `*.csproj` /\n `Package.swift`, and for where the client is constructed. Reuse it — one\n client per process.\n- **Read the reference for this language** (only the one you need):\n\n | Stack | Reference |\n |-------|-----------|\n | Node.js server (`@quonfig/node`) | [references/sdk-node.md](references/sdk-node.md) |\n | Browser JS, React, React Native | [references/sdk-browser.md](references/sdk-browser.md) |\n | Go | [references/sdk-go.md](references/sdk-go.md) |\n | Python | [references/sdk-python.md](references/sdk-python.md) |\n | Ruby | [references/sdk-ruby.md](references/sdk-ruby.md) |\n | Java / Kotlin | [references/sdk-java.md](references/sdk-java.md) |\n | .NET | [references/sdk-dotnet.md](references/sdk-dotnet.md) |\n | Swift (iOS/macOS) | [references/sdk-swift.md](references/sdk-swift.md) |\n\n- **Pick the surface for changing flags.** You have a shell and a checkout, so\n use `qfg` (check with `qfg whoami`; install with `npm install -g @quonfig/cli`,\n sign in with `qfg login` — the human does the browser step). Use the MCP\n tools only when `qfg` is unavailable or for a quick read of live state.\n Details: [references/writes.md](references/writes.md).\n\n## 2. Adding a feature flag (the core loop)\n\n1. **Name it.** Keys are lowercase, dot-separated, grouped by area:\n `checkout.new-flow`, `search.semantic-ranking`. Check it isn't taken and\n isn't already what you need: `qfg list --feature-flags --json`.\n2. **Create it, off.** `qfg create checkout.new-flow --type boolean-flag --value=false`.\n If it already exists, stop and ask — never invent a variant of the key.\n3. **Gate the code** with the SDK's flag call and a context that identifies\n who is asking (at minimum `user.key`, so rollouts and targeting work):\n - Node: `quonfig.isEnabled(\"checkout.new-flow\", { user: { key: user.id } })`\n - Go: `on, _ := client.FeatureIsOn(\"checkout.new-flow\", ctx)`\n - Python: `client.is_feature_enabled(\"checkout.new-flow\", contexts={\"user\": {\"key\": user.id}})`\n - others: see the language reference.\n Keep the off path identical to today's behavior, so the flag can ship dark.\n4. **Turn it on where it is safe** — normally development first:\n `qfg set-default checkout.new-flow --value=true --environment=development --confirm`.\n Always name the environment. (`--confirm` skips the interactive prompt —\n without a terminal, `set-default` and `set-rollout` refuse to run without it.) Do not change production (or roll out there)\n unless the human asked for it.\n5. **Tell the human** the key, which environments serve what, and the rollout\n command for later (`qfg set-rollout checkout.new-flow --environment=production --true-percent 10 --confirm`).\n\n## 3. Configs and log levels\n\n- A dynamic value instead of a constant: `qfg create checkout.timeout --type int --value=30`,\n then read it with the SDK's typed getter (see the language reference).\n Types: `string`, `int`, `double`, `boolean`, `string-list`, `json`, `duration`.\n- Create a key **before** shipping code that reads it — most server SDKs throw\n on a missing key with no default.\n- Runtime log levels: one `log-level.<service>` key per service, wired into\n the app's logger via the SDK's `loggerKey`; change one logger with\n `qfg log-level log-level.checkout-api --target=Checkout.Payments --value=DEBUG --environment=staging`.\n- TypeScript apps can generate a typed client: `qfg generate --targets node-ts`\n (server) or `--targets react-ts` (React).\n\n## 4. Rules that prevent the classic mistakes\n\n- **Name the environment** in every write and every answer about state. A flag\n is usually on in development and off in production.\n- **Setting a value keeps targeting rules.** `set-default`/`set-rollout` change\n only the fallback rule; `--replace-targeting` deletes the environment's\n targeting and needs a human's agreement first. Read with `qfg info <key>`\n before you write. See [references/writes.md](references/writes.md).\n- **Backend vs frontend keys.** Server SDKs use `QUONFIG_BACKEND_SDK_KEY` and\n evaluate locally; browser/mobile SDKs use a frontend key and get values\n evaluated for one context. Never ship a backend key to a browser or app.\n Never commit any key.\n- **Secrets never enter the transcript.** Create or rotate them with\n `--secret --value-from-env <VAR>` or `--value-stdin`, never `--value=`;\n verify with `qfg get` (redacted fingerprint), never `--reveal`; never `cat`\n a `.env`. Secrets are string-only. See\n [references/cli.md](references/cli.md#secrets-encrypted-configs).\n- **The SDK key decides the environment** in server SDKs. Set\n `QUONFIG_ENVIRONMENT` / `environment` only for local datadir mode.\n\n## 5. More\n\n- Full `qfg` cheat sheet (create types, rollouts, secrets, SDK keys, overrides, `qfg run`):\n [references/cli.md](references/cli.md)\n- Quonfig MCP tools (Slack, claude.ai, no checkout; undo with `set_document`):\n [references/mcp.md](references/mcp.md)\n- Removing a flag once its rollout is done (`qfg cleanup`):\n [references/retire-a-flag.md](references/retire-a-flag.md)\n- Docs: https://docs.quonfig.com\n" },
9
+ { path: "references/cli.md", content: "# `qfg` command reference (app-developer loop)\n\nInstall: `npm install -g @quonfig/cli`. Every command takes `--json` for\nstructured output; prompts turn off automatically when stdin is not a TTY, so\npass every required flag — including `--confirm` on `set-default` /\n`set-rollout` and `--yes` on `push` / `delete`, which otherwise refuse to run\nwithout a terminal. `qfg <command> --help` is authoritative.\n\n## Auth and workspace\n\n```bash\nqfg login # browser device sign-in, once per machine\nqfg whoami # who am I, which orgs\nqfg workspace # active workspace + everything reachable\nqfg workspace switch acme/production\nqfg list -w acme/production # -w <org>/<workspace> on any API command\n```\n\nCI: set `QUONFIG_API_KEY` **and** `QUONFIG_WORKSPACE=<org>/<workspace>`.\n\n## Look before you create\n\n```bash\nqfg list --feature-flags --json # what exists (also --configs, --log-levels, --segments)\nqfg info checkout.new-flow # every environment's rules + 24h evaluations\nqfg get checkout.timeout --environment production # evaluated value in one env\n```\n\n## Create\n\n```bash\nqfg create checkout.new-flow --type boolean-flag --value=false\nqfg create checkout.banner-text --type string --value=\"Free shipping today\"\nqfg create checkout.timeout --type int --value=30\nqfg create checkout.retry-window --type duration --value PT90S\nqfg create log-level.checkout-api --type log_level --value WARN\n```\n\n- `--type`: `boolean-flag | boolean | string | int | double | string-list | json | duration | log_level`.\n Use `boolean-flag` for a feature flag (it is also sent to client SDKs);\n `boolean` is a plain config.\n- `create` sets the default for all environments; there is no `--environment`.\n- If the key already exists the command fails (\"... already exists\"). Do not\n retry with a tweaked key — use the existing one or agree a new name with a\n human.\n\n## Change what an environment serves\n\n```bash\nqfg set-default checkout.new-flow --value=true --environment=development --confirm\nqfg set-rollout checkout.new-flow --environment=staging --true-percent 20 --confirm\nqfg set-rollout checkout.layout --environment=production --weights \"control:50,treatment:50\" --confirm\nqfg set-default checkout.new-flow --value=false --environment=production --replace-targeting --confirm # kill switch: off for everyone\n```\n\n`--environment` is required when not interactive. These keep targeting rules\nunless you pass `--replace-targeting` — see [writes.md](writes.md).\n\n## Secrets (encrypted configs)\n\nYour commands and their output are in a transcript others can read. A secret\nvalue must never appear in either.\n\n```bash\n# Load a .env file the human points you at WITHOUT printing it. `set -a`\n# exports every variable it sets; a plain `source` does not, and\n# --value-from-env only sees exported variables.\nset -a; source ./secrets.env; set +a\n\n# Create: the human puts the value in a local env var first; you name the var.\n# (--secret implies --type string.)\nqfg create payments.stripe.key --secret --value-from-env STRIPE_SECRET_KEY\n# Or pipe it from a file / password-manager CLI the human points you at.\nqfg create payments.stripe.key --secret --value-stdin < ./stripe-key.txt\n\n# Rotate one environment (same flags).\nqfg set-default payments.stripe.key --environment=production --secret \\\n --value-from-env STRIPE_LIVE_KEY --confirm\n\n# Verify: prints a redacted fingerprint (*****<5 hex>, length, last 4), not the value.\nqfg get payments.stripe.key --environment=production\n\n# Check length and last 4 against the source, without printing either:\n[ \"${#STRIPE_LIVE_KEY}\" = 107 ] && [ \"${STRIPE_LIVE_KEY: -4}\" = a1b2 ] && echo match\n```\n\n- **Never** pass a secret as `--value=...`, in an `echo ... |`, or anywhere\n else in a command line. Use `--value-from-env <VAR>` or `--value-stdin`.\n If no such variable or file exists, ask the human to create one; do not\n ask them to paste the secret into chat.\n- **Never** run `qfg get --reveal`, `cat .env*`, `printenv`, or `echo $VAR`\n for a secret or for the encryption-key variables\n (`QUONFIG_SECRET_KEY_*`). Confirm a variable is set with\n `[ -n \"$VAR\" ] && echo set`.\n- **Verify with the fingerprint.** Report the fingerprint, length, and last 4\n to the human; they compare last 4 against the source. A new write always\n gets a new fingerprint (encryption is randomized).\n- **The encryption key.** The CLI finds the workspace key\n (`quonfig.secrets.encryption.key`) on its own and says which env var it\n read it from; that variable must be set in your shell. Do not create a new\n key if one exists (`qfg info quonfig.secrets.encryption.key`).\n- **Environment names.** Every write names one (`--environment=production`).\n A wrong name fails with the list of valid ones; `--environment=\"[Default]\"`\n writes the rules every environment without its own inherits. Rotating an\n environment that had no rules copies the default's rules first (the CLI\n says so).\n- `qfg get` on a secret prints a note on stderr that a human can pass\n `--reveal`. That note is not for you: never pass `--reveal`.\n- **Secrets are string-only.** `create --secret` needs no `--type`. For\n structured data, store the JSON text as a string secret and `JSON.parse`\n it in the app. `--secret` on a `json`/non-string config is refused.\n- `--value-from-env VAR` copies VAR's value **now**. `--env-var=VAR` stores a\n pointer the app resolves from its own environment at runtime. Don't mix\n them up.\n- Need the plaintext in a process (migration, build)? `qfg run`, never print it.\n- `--json` returns `{redacted: true, encrypted: true, fingerprint, length,\n last4?}` for secrets.\n\nDocs: https://docs.quonfig.com/docs/explanations/features/secret-management\n\n## Log levels\n\n```bash\nqfg log-level log-level.checkout-api --value=WARN # creates the key (fails if it exists)\nqfg set-default log-level.checkout-api --value=INFO --environment=production --confirm # change an existing default\nqfg log-level log-level.checkout-api --target=Checkout.Payments --value=DEBUG --environment=staging\n```\n\n## SDK keys\n\n```bash\nqfg sdk-key create --environment production --type server # backend: QUONFIG_BACKEND_SDK_KEY\nqfg sdk-key create --environment development --type browser # frontend SDKs\nqfg sdk-key list\n```\n\nNever commit a key; put it in the app's secret store / `.env` (gitignored).\n\n## Typed clients (TypeScript only)\n\n```bash\nqfg generate --targets node-ts -o generated # @quonfig/node\nqfg generate --targets react-ts -o generated # @quonfig/react (default target)\n```\n\n## Run a process with config injected as env vars\n\n```bash\nqfg run --env DATABASE_URL=db.url --environment=staging -- npm run migrate\n```\n\n## Personal override (only you see it)\n\n```bash\nqfg override checkout.new-flow true --env=development\nqfg override checkout.new-flow --remove --env=development\n```\n\nFires only for SDK clients that send the `quonfig-user.email` context.\n\n## Workspace repo (editing JSON directly)\n\n```bash\nqfg pull --dir ./quonfig-workspace # clone/refresh the workspace repo\nqfg verify ./quonfig-workspace # validate (local, no login)\nqfg push --dir ./quonfig-workspace --yes # publish committed edits\nqfg config-schema # operators, field names, value types\n```\n\nInside a workspace repo, follow that repo's `AGENTS.md` / `CLAUDE.md`.\n" },
10
10
  { path: "references/mcp.md", content: "# Quonfig MCP server\n\n`https://mcp.quonfig.com/mcp` — streamable HTTP. Use it where there is no repo\ncheckout or shell (Slack via Claude Tag, claude.ai), or to answer \"what is\nproduction serving right now?\" from inside a coding session. The plugin\ninstall registers it as `quonfig`; elsewhere add it with\n`claude mcp add --transport http quonfig https://mcp.quonfig.com/mcp`.\nDevelopers sign in through the browser (OAuth); shared bots use a `qf_sa_`\nservice-account key. Either way the caller can only do what its Quonfig roles\nallow.\n\nFourteen read tools and six write tools. Full list:\nhttps://docs.quonfig.com/docs/api/mcp-server#tools\n\n## Answering questions\n\n- Current state of one flag: `get_flag` (returns per-environment rules,\n rollout percentages, and variants). Do not infer state from `list_flags`\n alone.\n- A rule with the `IN_SEG` operator names a segment, it does not list its\n members. Call `get_segment` before claiming who receives the flag.\n- \"Who changed this, and when\": `get_flag_history` / `get_config_history`.\n For workspace-wide questions use `get_recent_changes`. For \"when did this\n last change\" across many items, read `lastModified` off the `list_flags` /\n `list_configs` rows instead of calling a history tool per item.\n- Always name the environment in your answer. A flag is usually on in\n development and off in production; an answer that omits the environment is\n wrong more often than right.\n\n## Changing a flag or config\n\n- `set_flag` and `set_config` are the everyday writes, and each sets the\n scope's FALLBACK: the unconditional rule at the end of its rule list, what\n users receive when no targeting rule matches. Targeting rules above it are\n kept — say so, naming the `preservedTargetingRuleCount` the write returns.\n A scope with no rules of its own is copied from the default rules first, so\n inherited targeting is kept too.\n- Read the item first, then say plainly what will change and in which scope\n before you call it.\n- `replaceTargeting: true` is a different request: it sets the value for\n everyone, deleting the scope's targeting rules. Show the rules and ask a\n human; send it only after someone agrees — neither tool can put those rules\n back afterwards. Report the returned `previousCommitSha` and\n `replacedTargetingRuleCount`.\n- Never guess an environment name. Call `list_environments`. A scope is\n either one of those names or the literal `\"default\"` — the rules every\n environment without its own entry inherits, which for most configs is the\n only place a value is stored. Naming an environment SHADOWS the default\n there; if the request is ambiguous (\"turn it off\"), ask which was meant.\n\n## Log levels\n\n- `set_log_level` is SURGICAL, unlike the two above: it adds or overwrites\n exactly one rule and leaves every sibling in place, so it never needs a\n confirmation to avoid destroying targeting.\n- `key` is the SERVICE (`log-level.api-delivery`) — there is one document\n per service. Pass `target` (a logger path prefix) to change one logger, or\n omit it to set the scope's fallback level. `list_log_levels` shows what is\n stored.\n\n## Creating\n\n- `create_flag` and `create_config` only ADD. A new flag is created OFF in\n every environment — enabling it is a separate `set_flag` call. A new\n config needs an explicit `valueType`.\n- `ALREADY_EXISTS` means the key is taken; read `collidingType` to see by\n what. NEVER retry a create with a mutated key — agree a different key\n with a human instead.\n\n## Raw documents and undo\n\n- `get_document` / `set_document` read and replace the stored JSON for a\n flag, config, or log level (`type`: `flag`, `config`, or `log-level` —\n note the hyphen; `get_recent_changes` spells the same kind `log_level`).\n Use them for edits the write verbs cannot express — multi-rule targeting,\n variants, metadata — and to undo any write.\n- `set_document` is a FULL REPLACEMENT: a field you omit is deleted. Always\n start from a `get_document` result and edit it; never build the document\n by hand.\n- To undo: `get_document` with `at` set to the bad write's\n `previousCommitSha`, then `get_document` again without `at` for the\n current `commitSha`, then `set_document` the old document pinned to that\n CURRENT sha. Say what you are restoring before you write it.\n- A `STALE_COMMIT_SHA` failure means someone else changed the item. Re-read\n and re-apply — never retry the same `set_document` arguments.\n\n## More than one workspace\n\nWith several workspaces, the first call comes back asking which one and\nlisting the options; retry with the workspace the human names.\n" },
11
11
  { path: "references/retire-a-flag.md", content: "# Retiring a flag\n\nFlags are meant to be temporary. When a rollout is finished, take the flag out\nof the code first, then delete its definition. Walk the `qfg cleanup`\nlifecycle instead of jumping to `qfg delete`.\n\n```bash\nqfg cleanup list # flags an owner marked readyForCleanup, with eval volume\nqfg cleanup status my.flag # telemetry + per-environment rules\nqfg cleanup remove my.flag # safety gate (no evals in 2 days), writes .qf/cleanup/my.flag.json\n```\n\nThen remove the call sites, using the payload at `.qf/cleanup/<key>.json`\n(`type`, per-environment rules, `grepPatterns`):\n\n1. **Find every call site.** Grep for the key together with the SDK methods in\n `grepPatterns` (`isEnabled`, `get`, `FeatureIsOn`, ...). Also look for\n wrappers the codebase built around the SDK (a `flagEnabled(\"...\")` helper,\n a `<FlagGate flag=\"...\">` component) and follow them to their callers.\n2. **Ask the engineer once which value wins** — \"keep the true branch and\n delete the false branch?\", or for a variant/config \"which of these values\n wins?\" (list the values per environment from the payload). Do not infer it.\n3. **Apply it mechanically.** The SDK call becomes the literal value, the\n unreachable branch is deleted, a helper that existed only for this flag is\n deleted. Do not refactor unrelated code.\n4. **Run the repo's formatter and tests.**\n5. **Open one PR** for this flag in this repo, naming the flag, the chosen\n direction, and the call sites changed. If the payload says `forced: true`,\n say so. Never auto-merge.\n\nAfter the PR merges and the service redeploys:\n\n```bash\nqfg cleanup verify my.flag # 7-day zero-evaluation gate\nqfg delete my.flag --yes # only after the engineer agrees\n```\n" },
12
12
  { path: "references/sdk-browser.md", content: "# Browser JavaScript, React, React Native\n\nFrontend SDKs **evaluate on the server**: the client sends its context and gets\nback already-evaluated values for that context. Use a **frontend (browser) SDK\nkey** (`qfg sdk-key create --environment <env> --type browser`), never the\nbackend key. Only flags/configs sent to client SDKs are visible here\n(`qfg create --type boolean-flag` sets that by default).\n\nContext is fixed per client: set it at init, then change it with\n`updateContext` (JS) or the `contextAttributes` prop (React). There is no\nper-call context argument.\n\n## Plain JavaScript — `@quonfig/javascript`\n\n```bash\nnpm install @quonfig/javascript\n```\n\n```js\nimport { quonfig } from \"@quonfig/javascript\";\n\nawait quonfig.init({\n sdkKey: \"your-frontend-sdk-key\",\n context: { user: { key: \"user-123\" } }, // required\n});\n\nif (quonfig.isEnabled(\"my-flag\")) {\n // ...\n}\nconst color = quonfig.get(\"button-color\"); // undefined until loaded\n\nawait quonfig.updateContext({ user: { key: \"user-456\" } }); // re-fetches\nawait quonfig.poll({ frequencyInMs: 60000 }); // optional: keep values fresh\n```\n\nBesides `get` and `isEnabled` there are only `getDuration` and `getDetails` —\nno typed getters, and no default argument on `get`. Supply your own fallback (`quonfig.get(\"greeting\") ?? \"Hello\"`).\n\n## React — `@quonfig/react`\n\n```bash\nnpm install @quonfig/react\n```\n\n```jsx\nimport { QuonfigProvider, useQuonfig, useFlag } from \"@quonfig/react\";\n\nconst WrappedApp = () => (\n <QuonfigProvider\n sdkKey={process.env.NEXT_PUBLIC_QUONFIG_API_KEY}\n contextAttributes={{ user: { key: user.id, email: user.email } }}\n onError={(error) => console.error(error)}\n >\n <App />\n </QuonfigProvider>\n);\n\nconst Logo = () => {\n const { isEnabled, get, loading } = useQuonfig();\n return isEnabled(\"new-logo\") ? <NewLogo /> : <OldLogo />;\n};\n\nconst Banner = () => {\n const showBanner = useFlag(\"new-banner\");\n // ...\n};\n```\n\n- Changing `contextAttributes` re-fetches. `pollInterval` (ms) opts into polling.\n- `loading` is true until values arrive; `initialFlags` hydrates SSR without a\n flicker.\n- Tests: wrap components in `QuonfigTestProvider config={{ \"my-flag\": true }}`.\n- Typed hooks: `qfg generate --targets react-ts -o ./generated`, then\n `import { useQuonfig } from \"./generated/quonfig-client\"` and read\n `q.flagSmoke` style accessors.\n\n## React Native — `@quonfig/react-native`\n\n```bash\nnpm install @quonfig/react-native @quonfig/react @quonfig/javascript base-64 react-native-get-random-values\n```\n\nImport `\"@quonfig/react-native\"` first in `index.js` (it installs polyfills),\nthen use `QuonfigProvider` / `useQuonfig` from `@quonfig/react-native` exactly\nas in React above.\n" },
13
13
  { path: "references/sdk-dotnet.md", content: "# .NET — `Quonfig.Sdk`\n\nServer-side SDK; evaluates in-process.\n\n## Install\n\n```bash\ndotnet add package Quonfig.Sdk\n```\n\nCompanions: `Quonfig.Sdk.AspNetCore`, `Quonfig.Sdk.Extensions.Logging`,\n`Quonfig.Sdk.Serilog`.\n\n## Initialize once\n\n`SdkKey` and `Environment` fall back to `QUONFIG_BACKEND_SDK_KEY` and\n`QUONFIG_ENVIRONMENT`.\n\n```csharp\nusing Quonfig.Sdk;\n\nawait using var quonfig = new Quonfig.Sdk.Quonfig(new QuonfigOptions\n{\n SdkKey = Environment.GetEnvironmentVariable(\"QUONFIG_BACKEND_SDK_KEY\"),\n});\nawait quonfig.InitAsync(); // await before relying on the sync getters\n```\n\nLocal / offline mode (the SDK does not read `QUONFIG_DIR`; set it):\n\n```csharp\nvar options = new QuonfigOptions { Datadir = \"./workspace-data\", Environment = \"development\" };\n```\n\nASP.NET Core (`using Quonfig.Sdk.AspNetCore;`): `builder.Services.AddQuonfig(opts => ...)` registers it with a\nhosted service that drives init and shutdown.\n\n## Context\n\n```csharp\n// Context values are string, int, long, double, bool, or string[] (call .ToString() on a Guid).\nvar ctx = new ContextSet\n{\n [\"user\"] = new ContextProperties { [\"key\"] = user.Id, [\"email\"] = user.Email },\n};\nvar bound = quonfig.WithContext(ctx); // optional\n```\n\n## Feature flag\n\n```csharp\nif (quonfig.IsFeatureEnabled(\"new-dashboard\", ctx))\n{\n // new path\n}\n```\n\n## Config values\n\nArgument order is **(key, contexts, defaultValue)** — context before default:\n\n```csharp\nvar s = quonfig.GetString(\"config.string\", ctx);\nvar n = quonfig.GetInt(\"rate-limit\", ctx);\n```\n\nAlso `GetLong`, `GetBool`, `GetDouble`, `GetStringList`, `GetJson`,\n`GetDuration`. With no default, a missing key throws (`OnNoDefault = Throw`).\n\n## Log levels\n\nSet `LoggerKey = \"log-level.my-app\"` on the options, then\n`builder.Logging.AddQuonfigFilter(quonfig);` (`using Quonfig.Sdk.Extensions.Logging;`).\n\nNo `qfg generate` target for .NET.\n" },
14
14
  { path: "references/sdk-go.md", content: "# Go — `github.com/quonfig/sdk-go`\n\nServer-side SDK; evaluates in-process.\n\n## Install\n\n```bash\ngo get github.com/quonfig/sdk-go\n```\n\n## Initialize once\n\n```go\nimport quonfig \"github.com/quonfig/sdk-go\"\n\n// With no WithSdkKey option, the SDK reads $QUONFIG_BACKEND_SDK_KEY.\n// (WithSdkKey(\"\") is an error, so don't pass an unset variable through it.)\nclient, err := quonfig.NewClient()\nif err != nil {\n log.Fatal(err)\n}\ndefer client.Close()\n```\n\n`NewClient` blocks for the first load (up to `WithInitTimeout`, default 10s).\nThe backend key decides the environment.\n\nLocal / offline mode (the SDK does not read `QUONFIG_DIR`; pass it):\n\n```go\nclient, err := quonfig.NewClient(\n quonfig.WithDataDir(\"./workspace-data\"), // or os.Getenv(\"QUONFIG_DIR\"), if set\n quonfig.WithEnvironment(\"development\"), // or $QUONFIG_ENVIRONMENT\n)\n```\n\n## Context\n\n```go\nctx := quonfig.NewContextSet().\n WithNamedContextValues(\"user\", map[string]interface{}{\n \"key\": user.ID,\n \"email\": user.Email,\n })\n```\n\nPass it per call (`nil` = no context), or bind it:\n`bound := client.WithContext(ctx)` gives methods without the ctx argument\n(`bound.FeatureIsOn(key)`, `bound.GetStringValue(key)`).\n\n## Feature flag\n\n```go\non, found := client.FeatureIsOn(\"new-dashboard\", ctx)\nif on {\n // new path\n}\n```\n\nReturns `false` (and `found == false`) when the key does not exist.\n\n## Config values\n\n```go\ns, found, err := client.GetStringValue(\"config.string\", ctx)\nn, found, err := client.GetIntValue(\"rate-limit\", ctx) // int64\n```\n\nAlso `GetBoolValue`, `GetFloatValue`, `GetStringSliceValue`,\n`GetDurationValue`, `GetJSONValue` — all `(key, *ContextSet)` returning\n`(value, found, err)`. No default argument: handle `found == false` yourself.\n\n## Log levels\n\n`quonfig.WithLoggerKey(\"log-level.my-app\")` plus\n`slog.New(quonfig.NewQuonfigHandler(client, inner, loggerPath))`.\n\nNo `qfg generate` target for Go.\n" },
15
15
  { path: "references/sdk-java.md", content: "# Java — `com.quonfig:sdk-java`\n\nServer-side SDK; evaluates in-process.\n\n## Install\n\n```kotlin\nimplementation(\"com.quonfig:sdk-java:1.5.0\")\n```\n\n```xml\n<dependency>\n <groupId>com.quonfig</groupId>\n <artifactId>sdk-java</artifactId>\n <version>1.5.0</version>\n</dependency>\n```\n\nCheck Maven Central for the latest version. Logging add-ons:\n`sdk-java-logback`, `sdk-java-log4j2`.\n\n## Initialize once\n\n`Options` reads `QUONFIG_BACKEND_SDK_KEY` and `QUONFIG_ENVIRONMENT` from the\nenvironment.\n\n```java\nimport com.quonfig.sdk.BoundQuonfig;\nimport com.quonfig.sdk.Options;\nimport com.quonfig.sdk.Quonfig;\nimport com.quonfig.sdk.eval.ContextSet;\n\nOptions opts = Options.builder()\n .sdkKey(System.getenv(\"QUONFIG_BACKEND_SDK_KEY\"))\n .build();\nQuonfig quonfig = new Quonfig(opts);\nquonfig.initFuture().get(); // wait for the first load\n```\n\nLocal / offline mode (the SDK does not read `QUONFIG_DIR`; `environment` is\nrequired):\n\n```java\nOptions opts = Options.builder()\n .datadir(\"./workspace-data\")\n .environment(\"development\")\n .build();\n```\n\n`Quonfig` is `AutoCloseable` — `close()` on shutdown.\n\n## Context\n\n```java\nContextSet ctx = new ContextSet()\n .withNamedContext(\"user\", Map.of(\"key\", user.getId(), \"email\", user.getEmail()));\nBoundQuonfig bound = quonfig.withContext(ctx); // optional\n```\n\n## Feature flag\n\n```java\nif (quonfig.featureIsOn(\"new-dashboard\", ctx)) {\n // new path\n}\n```\n\n## Config values\n\nThe default is a required argument; context is the optional third:\n\n```java\nString s = quonfig.getString(\"config.string\", \"<missing>\", ctx);\nLong n = quonfig.getInt(\"rate-limit\", 100L, ctx);\n```\n\nAlso `getBool`, `getDouble`, `getStringList`, `getDuration`, `getJson`.\n\n## Log levels\n\n`.loggerKey(\"log-level.my-app\")` on the builder, then\n`QuonfigLogbackTurboFilter.install(quonfig)` (`com.quonfig.sdk.logback`) or\n`QuonfigLog4j2Filter.install(quonfig)` (`com.quonfig.sdk.log4j2`).\n\nNo `qfg generate` target for Java.\n" },
16
- { path: "references/sdk-node.md", content: "# Node.js — `@quonfig/node`\n\nServer-side SDK. Downloads the whole workspace and evaluates every check\nin-process (no network call per evaluation).\n\n## Install\n\n```bash\nnpm install @quonfig/node\n```\n\n## Initialize once, at startup\n\n```typescript\nimport { Quonfig } from \"@quonfig/node\";\n\n// sdkKey falls back to process.env.QUONFIG_BACKEND_SDK_KEY when omitted.\nconst quonfig = new Quonfig({ sdkKey: process.env.QUONFIG_BACKEND_SDK_KEY });\nawait quonfig.init();\n```\n\n- The **backend SDK key decides the environment**. Do not also pin\n `environment` in SDK-key mode (the SDK warns and ignores it).\n- Create one client per process and share it (export it from a module). If the\n app already has one, reuse it — grep for `new Quonfig(` first.\n- Local / offline mode reads a workspace checkout instead of the network. Here an\n environment is required (`environment` option or `QUONFIG_ENVIRONMENT`), and\n Node does **not** read `QUONFIG_DIR` itself:\n\n```typescript\nconst quonfig = new Quonfig({\n datadir: \"./workspace-data\",\n environment: \"development\",\n dataDirAutoReload: true, // off by default — must be opted in\n});\nawait quonfig.init();\n```\n\n## Feature flag\n\n```typescript\nif (quonfig.isEnabled(\"new-dashboard\", { user: { key: user.id, email: user.email } })) {\n // new path\n}\n```\n\nSignature: `isEnabled(key, contexts?)`. `isFeatureEnabled` is a deprecated alias\n— write `isEnabled` in new code.\n\n## Config values\n\n```typescript\nconst limit = quonfig.getNumber(\"rate-limit\");\nconst regions = quonfig.getStringList(\"allowed-regions\");\nconst hero = quonfig.get(\"homepage-hero\", { user: { key: \"user-123\", country: \"US\" } });\nconst timeout = quonfig.get(\"api.timeout\", undefined, 30); // only get() takes a default\n```\n\nTyped getters: `getString`, `getNumber`, `getBool`, `getStringList`,\n`getDuration`, `getJSON` — each `(key, contexts?)`, no default argument. With\nthe default `onNoDefault: \"error\"`, reading a key that does not exist (and has\nno default) throws, so create the key before shipping code that reads it.\n\n## Context\n\n```typescript\n// Bound context for repeated lookups (e.g. per request)\nconst userClient = quonfig.withContext({ user: { key: \"user-123\", plan: \"pro\" } });\nuserClient.get(\"feature-x\");\nuserClient.isEnabled(\"beta-feature\");\n```\n\n`inContext` is a deprecated alias of `withContext`. The `globalContext`\nconstructor option applies a context to every evaluation (e.g. service name).\n\n## Typed client (optional)\n\n`qfg generate --targets node-ts -o generated` writes\n`generated/quonfig-server.ts`, a typed wrapper with one accessor per key:\n\n```typescript\nimport { QuonfigTypesafeNode } from \"./generated/quonfig-server\";\n\nconst typed = new QuonfigTypesafeNode(quonfig);\ntyped.flagSmoke({ user: { key: user.id } }); // flag.smoke\n```\n\nIf any config uses templates, the generated code imports `mustache` (qfg\nprints a note); add it as a dependency then.\nRe-run `qfg generate` after creating keys.\n\n## Log levels\n\nPass `loggerKey: \"log-level.my-app\"` to the constructor, then wire the logger:\n`createWinstonFormat` from `@quonfig/node/winston` or `createPinoHooks` from\n`@quonfig/node/pino`. Read the package README for the exact wiring.\n\n## Shutdown\n\n`await quonfig.close()` on shutdown; `await quonfig.flush()` at the end of a\nserverless invocation to send telemetry.\n" },
16
+ { path: "references/sdk-node.md", content: "# Node.js — `@quonfig/node`\n\nServer-side SDK. Downloads the whole workspace and evaluates every check\nin-process (no network call per evaluation).\n\n## Install\n\n```bash\nnpm install @quonfig/node\n```\n\n## Initialize once, at startup\n\n```typescript\nimport { Quonfig } from \"@quonfig/node\";\n\n// sdkKey falls back to process.env.QUONFIG_BACKEND_SDK_KEY when omitted.\nconst quonfig = new Quonfig({ sdkKey: process.env.QUONFIG_BACKEND_SDK_KEY });\nawait quonfig.init();\n```\n\n- The **backend SDK key decides the environment**. Do not also pin\n `environment` in SDK-key mode (the SDK warns and ignores it).\n- Create one client per process and share it (export it from a module). If the\n app already has one, reuse it — grep for `new Quonfig(` first.\n- Local / offline mode reads a workspace checkout instead of the network. Here an\n environment is required (`environment` option or `QUONFIG_ENVIRONMENT`), and\n Node does **not** read `QUONFIG_DIR` itself:\n\n```typescript\nconst quonfig = new Quonfig({\n datadir: \"./workspace-data\",\n environment: \"development\",\n dataDirAutoReload: true, // off by default — must be opted in\n});\nawait quonfig.init();\n```\n\n## Feature flag\n\n```typescript\nif (quonfig.isEnabled(\"new-dashboard\", { user: { key: user.id, email: user.email } })) {\n // new path\n}\n```\n\nSignature: `isEnabled(key, contexts?)`. `isFeatureEnabled` is a deprecated alias\n— write `isEnabled` in new code.\n\n## Config values\n\n```typescript\nconst limit = quonfig.getNumber(\"rate-limit\");\nconst regions = quonfig.getStringList(\"allowed-regions\");\nconst hero = quonfig.get(\"homepage-hero\", { user: { key: \"user-123\", country: \"US\" } });\nconst timeout = quonfig.get(\"api.timeout\", undefined, 30); // only get() takes a default\n```\n\nTyped getters: `getString`, `getNumber`, `getBool`, `getStringList`,\n`getDuration`, `getJSON` — each `(key, contexts?)`, no default argument. With\nthe default `onNoDefault: \"error\"`, reading a key that does not exist (and has\nno default) throws, so create the key before shipping code that reads it.\n\n## Secrets\n\n`get`/`getString` on a secret returns the decrypted string. The app process\nneeds the env var that `quonfig.secrets.encryption.key` is provided by (e.g.\n`QUONFIG_SECRET_KEY_DEFAULT`); without it the read throws. Secrets are\nstring-only: `JSON.parse(quonfig.getString(\"gcp.service-account\"))`.\n\n## Context\n\n```typescript\n// Bound context for repeated lookups (e.g. per request)\nconst userClient = quonfig.withContext({ user: { key: \"user-123\", plan: \"pro\" } });\nuserClient.get(\"feature-x\");\nuserClient.isEnabled(\"beta-feature\");\n```\n\n`inContext` is a deprecated alias of `withContext`. The `globalContext`\nconstructor option applies a context to every evaluation (e.g. service name).\n\n## Typed client (optional)\n\n`qfg generate --targets node-ts -o generated` writes\n`generated/quonfig-server.ts`, a typed wrapper with one accessor per key:\n\n```typescript\nimport { QuonfigTypesafeNode } from \"./generated/quonfig-server\";\n\nconst typed = new QuonfigTypesafeNode(quonfig);\ntyped.flagSmoke({ user: { key: user.id } }); // flag.smoke\n```\n\nIf any config uses templates, the generated code imports `mustache` (qfg\nprints a note); add it as a dependency then.\nRe-run `qfg generate` after creating keys.\n\n## Log levels\n\nPass `loggerKey: \"log-level.my-app\"` to the constructor, then wire the logger:\n`createWinstonFormat` from `@quonfig/node/winston` or `createPinoHooks` from\n`@quonfig/node/pino`. Read the package README for the exact wiring.\n\n## Shutdown\n\n`await quonfig.close()` on shutdown; `await quonfig.flush()` at the end of a\nserverless invocation to send telemetry.\n" },
17
17
  { path: "references/sdk-python.md", content: "# Python — `quonfig`\n\nServer-side SDK; evaluates in-process.\n\n## Install\n\n```bash\npip install quonfig\n```\n\n## Initialize once\n\n```python\nimport os\n\nfrom quonfig import Quonfig\n\nclient = Quonfig(sdk_key=os.environ[\"QUONFIG_BACKEND_SDK_KEY\"])\nclient.init()\n```\n\nEnv fallbacks read by the SDK: `sdk_key` <- `QUONFIG_BACKEND_SDK_KEY`,\n`environment` <- `QUONFIG_ENVIRONMENT`, `datadir` <- `QUONFIG_DIR`.\n\nLocal / offline mode:\n\n```python\nclient = Quonfig(datadir=\"/path/to/workspace\", environment=\"development\")\nclient.init()\n```\n\n## Feature flag\n\n```python\nenabled = client.is_feature_enabled(\"my.flag\", contexts={\"user\": {\"key\": user.id}})\n```\n\nSignature: `is_feature_enabled(key, default=False, contexts=None)`. The second\npositional argument is the **default**, so always pass `contexts=` by keyword.\n\n## Config values\n\n```python\nvalue = client.get_string(\"my.key\", default=\"fallback\")\nlimit = client.get_int(\"rate-limit\", default=100, contexts={\"user\": {\"plan\": \"pro\"}})\n```\n\nGetters: `get`, `get_string`, `get_int`, `get_float`, `get_bool`,\n`get_string_list`, `get_json`, `get_duration` — all\n`(key, default=..., contexts=None)`. With no default, a missing key follows\n`on_no_default` (raises by default).\n\n## Context\n\n```python\n# Bound context (request handlers etc.)\nuser_client = client.with_context({\"user\": {\"key\": \"u123\", \"plan\": \"pro\"}})\nenabled = user_client.is_feature_enabled(\"my.flag\")\n\n# Scoped: per thread / asyncio task, restored on exit\nwith client.scoped_context({\"user\": {\"key\": \"u123\"}}):\n enabled = client.is_feature_enabled(\"my.flag\")\n```\n\n## Log levels\n\n`logger_key=\"log-level.my-app\"` plus `QuonfigLoggerFilter(client)` for stdlib\nlogging or `QuonfigLoggerProcessor` for structlog.\n\nShutdown: `client.close()`; `client.flush()` in serverless. No `qfg generate`\ntarget for Python.\n" },
18
18
  { path: "references/sdk-ruby.md", content: "# Ruby — `quonfig` gem\n\nServer-side SDK; evaluates in-process.\n\n## Install\n\n```ruby\ngem 'quonfig'\n```\n\n## Initialize once\n\n```ruby\nrequire 'quonfig'\n\nclient = Quonfig::Client.new(sdk_key: ENV['QUONFIG_BACKEND_SDK_KEY'])\n```\n\nOr as a process singleton (e.g. a Rails initializer), then `Quonfig.instance`:\n\n```ruby\nQuonfig.init(Quonfig::Options.new(sdk_key: ENV.fetch('QUONFIG_BACKEND_SDK_KEY')))\n```\n\nThe constructor blocks on the first load. The backend key decides the\nenvironment. Local / offline mode reads `QUONFIG_DIR` + `QUONFIG_ENVIRONMENT`:\n\n```ruby\nclient = Quonfig::Client.new(datadir: '/path/to/workspace', environment: 'development')\n```\n\n## Feature flag\n\n```ruby\nif client.enabled?('new-dashboard', user: { key: current_user.id })\n # new path\nend\n```\n\n## Config values\n\n```ruby\nclient.get_string('app.display-name')\nclient.get_int('rate-limit', default: 100, context: { user: { key: 'user-123' } })\nclient.get_bool('flags.new-checkout', default: false)\n```\n\nAlso `get_float`, `get_string_list`, `get_duration` (ms), `get_json`. A missing\nkey returns `default:`; with no default, `on_no_default` (`:raise` by default)\ndecides.\n\n## Context\n\n```ruby\nclient.with_context(user: { key: 'user-123', plan: 'pro' }) do |bound|\n bound.enabled?('beta-feature')\nend\n```\n\n`in_context` is a deprecated alias. `global_context:` is a constructor option.\n\n## Log levels\n\nSemanticLogger: `filter: Quonfig.instance.semantic_logger_filter(config_key: 'log-level.my-app')`.\nStdlib Logger: `logger_key:` option plus `client.stdlib_formatter(logger_name:)`.\n\nShutdown: `client.stop`. No `qfg generate` target for Ruby.\n" },
19
19
  { path: "references/sdk-swift.md", content: "# Swift (iOS 15+ / macOS 12+) — `Quonfig`\n\nA frontend SDK: it evaluates on the server for one context, so use a\n**frontend (browser) SDK key** (`qfg sdk-key create --environment <env> --type browser`),\nnever the backend key.\n\n## Install (SwiftPM)\n\n```swift\ndependencies: [\n .package(url: \"https://github.com/quonfig/sdk-swift.git\", from: \"0.2.0\"),\n],\ntargets: [\n .target(\n name: \"YourApp\",\n dependencies: [\n .product(name: \"Quonfig\", package: \"sdk-swift\"),\n ]\n ),\n]\n```\n\n## Initialize once\n\n```swift\nimport Quonfig\n\nlet quonfig = try await Quonfig.initialize(\n context: QuonfigContext([\n \"user\": [\"key\": .string(\"u_123\"), \"email\": .string(\"a@b.com\")],\n ]),\n options: .init(sdkKey: frontendSdkKey, domain: \"quonfig.com\")\n)\n```\n\nThe SDK reads no environment variables; the key decides the environment.\n\n## Flags and values\n\n```swift\nlet on = quonfig.isEnabled(\"new-checkout\") // Bool\nlet color = quonfig.string(\"button-color\", default: \"blue\") // String\nlet limit = quonfig.int(\"rate-limit\", default: 100) // Int\nlet hosts = quonfig.stringList(\"allowed-hosts\", default: []) // [String]\n```\n\nReads are synchronous and return the default until values arrive.\n\n## Context\n\n```swift\ntry await quonfig.updateContext(\n QuonfigContext([\"user\": [\"key\": .string(\"u_456\")]])\n)\n```\n\n## Log levels and shutdown\n\n`QuonfigLogger(quonfig: quonfig, loggerKey: \"log-level.my-app\")`, or\n`quonfig.shouldLog(.debug, loggerKey: \"log-level.my-app\")`.\n`await quonfig.shutdown()` when done.\n" },
@@ -38,7 +38,7 @@ export declare abstract class BaseTypescriptGenerator extends BaseGenerator {
38
38
  configType: "CONFIG" | "FEATURE_FLAG" | "SCHEMA";
39
39
  hasFunction: boolean;
40
40
  key: string;
41
- schema: z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
41
+ schema: z.ZodTypeAny<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
42
42
  sendToClientSdk: boolean;
43
43
  }[];
44
44
  protected durationTypeMap(): z.ZodTypeAny;
@@ -8,7 +8,7 @@ export declare class SchemaExtractor {
8
8
  config: Config;
9
9
  configFile: ConfigFile;
10
10
  durationTypeMap?: DurationTypeMap;
11
- }): z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
11
+ }): z.ZodTypeAny<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
12
12
  private createZodFunctionFromMustacheStrings;
13
13
  private getAllJsonValues;
14
14
  /**
@@ -1,6 +1,7 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import { BaseCommand } from '../index.js';
3
3
  import { storedConfigJsonSchema } from '../init/schema.js';
4
+ import { REGEX_V1_DOCS_URL } from '../verify/regex-dialect.js';
4
5
  export const REFERENCE = `
5
6
  ────────────────────────────────────────────────────────────
6
7
  FILE LAYOUT
@@ -117,8 +118,8 @@ OPERATOR REFERENCE
117
118
  PROP_SEMVER_GREATER_THAN string semver comparison; same version grammar
118
119
  PROP_BEFORE int or string epoch ms (int) or RFC3339 with Z/offset, e.g. "2025-01-01T00:00:00Z"; no date-only
119
120
  PROP_AFTER int or string same timestamp grammar
120
- PROP_MATCHES string regex match
121
- PROP_DOES_NOT_MATCH string
121
+ PROP_MATCHES string regex search (unanchored); Quonfig regex v1 only, see below
122
+ PROP_DOES_NOT_MATCH string same regex dialect
122
123
  IS_PRESENT (none) propertyName is set on the evaluation context (no valueToMatch)
123
124
  IS_NOT_PRESENT (none) propertyName is missing from the evaluation context (no valueToMatch)
124
125
  IN_SEG string value = segment key (no propertyName needed)
@@ -127,6 +128,12 @@ OPERATOR REFERENCE
127
128
  LOOKUP_KEY_IN string_list matches if SDK lookup key is in the list
128
129
  LOOKUP_KEY_NOT_IN string_list
129
130
 
131
+ Regex (PROP_MATCHES / PROP_DOES_NOT_MATCH) must be Quonfig regex v1, or qfg verify and push reject it:
132
+ literals . ^ $ | ( ) (?: ) [...] [^...], * + ? {n} {n,} {n,m} (<= 1000, optionally lazy), escapes \\<punct> (not \\_) \\t \\n \\r \\f \\x00-\\x7F.
133
+ No lookaround, named groups, (?i), backrefs or \\d \\w \\s \\b (use [0-9], [A-Za-z0-9_], [ \\t\\r\\n\\f\\x0B]);
134
+ no quantified group holding a quantifier or |, at most 2 of * + {n,}, at most 256 chars.
135
+ Docs: ${REGEX_V1_DOCS_URL}
136
+
130
137
  valueToMatch format: { "type": "<type>", "value": <value> }
131
138
  string_list → { "type": "string_list", "value": ["a", "b"] }
132
139
  string → { "type": "string", "value": "beta" }
@@ -9,9 +9,11 @@ export default class Create extends APICommand {
9
9
  static flags: {
10
10
  secret: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
11
11
  'secret-key-name': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
12
+ 'value-from-env': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
13
+ 'value-stdin': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
12
14
  confidential: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
13
15
  'env-var': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
14
- type: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
16
+ type: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
15
17
  value: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
16
18
  };
17
19
  run(): Promise<JsonObj | void>;
@@ -9,7 +9,9 @@ import { makeConfidentialValue } from '../util/encryption.js';
9
9
  import { writeStoredConfigToWorkspace } from '../util/local-config-writer.js';
10
10
  import { LOG_LEVELS, LOG_LEVEL_KEY_PREFIX, isLogLevel } from '../util/log-levels.js';
11
11
  import { policyAKeyError } from '../util/policy-a-key.js';
12
+ import { redactedDisplay, redactedSummary, scrubConfidentialValues } from '../util/redact.js';
12
13
  import secretFlags, { parsedSecretFlags } from '../util/secret-flags.js';
14
+ import { resolveValueSource, valueSourceFlags } from '../util/value-source.js';
13
15
  export default class Create extends APICommand {
14
16
  static args = {
15
17
  name: Args.string({ description: 'name for your new item (e.g. my.new.flag)', required: true }),
@@ -27,6 +29,17 @@ Use --type to specify the kind of item:
27
29
  duration An ISO 8601 duration string, e.g. PT30S, PT5M, PT1H30M (SDK returns milliseconds)
28
30
  log_level A dynamic log level (value must be one of ${LOG_LEVELS.join('/')})
29
31
 
32
+ Secrets (--secret) are encrypted client-side and are STRING-ONLY: --secret
33
+ implies --type string, so you can leave --type off. To store a JSON secret, store the JSON as a string secret and
34
+ JSON.parse it in your app. The CLI never prints a secret's plaintext: it shows
35
+ a redacted fingerprint like [encrypted *****3fa9c, 51 chars, ends …a1b2].
36
+
37
+ Keep the value out of argv / shell history / agent transcripts with:
38
+ --value-stdin read the value from stdin
39
+ --value-from-env=VAR read the value from a local env var, now
40
+ These are different from --env-var, which stores NO value: it tells the SDK to
41
+ read the value from that env var at runtime.
42
+
30
43
  This sets the global default value. Override per-environment with:
31
44
  qfg set-default my.flag --environment production --value true
32
45
 
@@ -49,7 +62,11 @@ When this flag has done its job, mark readyForCleanup: true in the UI and run \`
49
62
  '<%= config.bin %> <%= command.id %> my.new.flag --type boolean-flag',
50
63
  '<%= config.bin %> <%= command.id %> my.new.flag --type boolean-flag --value=true',
51
64
  '<%= config.bin %> <%= command.id %> my.new.string --type string --value="hello world"',
52
- '<%= config.bin %> <%= command.id %> my.new.string --type string --value="hello world" --secret',
65
+ '# Create a secret without the value ever appearing in argv (--secret implies --type string):',
66
+ 'pbpaste | <%= config.bin %> <%= command.id %> stripe.api-key --secret --value-stdin',
67
+ '<%= config.bin %> <%= command.id %> stripe.api-key --secret --value-from-env STRIPE_API_KEY',
68
+ '# A JSON secret is a string secret holding JSON (JSON.parse it in your app):',
69
+ '<%= config.bin %> <%= command.id %> gcp.service-account --secret --value-stdin < sa.json',
53
70
  '<%= config.bin %> <%= command.id %> my.new.string --type string --env-var=MY_ENV_VAR_NAME',
54
71
  '<%= config.bin %> <%= command.id %> my.new.string --type json --value="{\\"key\\": \\"value\\"}"',
55
72
  '<%= config.bin %> <%= command.id %> my.timeout --type duration --value PT90S',
@@ -59,13 +76,19 @@ When this flag has done its job, mark readyForCleanup: true in the UI and run \`
59
76
  ];
60
77
  static flags = {
61
78
  confidential: Flags.boolean({ default: false, description: 'mark the value as confidential' }),
62
- 'env-var': Flags.string({ description: 'environment variable to get value from' }),
79
+ 'env-var': Flags.string({
80
+ description: 'store no value: the SDK reads it from this env var at RUNTIME. To read a value from a local env var NOW, use --value-from-env',
81
+ }),
63
82
  type: Flags.string({
83
+ description: 'kind of item to create (required, except with --secret, which implies string)',
64
84
  options: ['boolean-flag', 'boolean', 'string', 'double', 'int', 'string-list', 'json', 'duration', 'log_level'],
65
- required: true,
66
85
  }),
67
- value: Flags.string({ description: 'default value for your new item', required: false }),
68
- ...secretFlags('encrypt the value of this item'),
86
+ value: Flags.string({
87
+ description: 'default value for your new item (for secrets prefer --value-stdin / --value-from-env)',
88
+ required: false,
89
+ }),
90
+ ...valueSourceFlags,
91
+ ...secretFlags('encrypt the value of this item (string-only; the CLI never prints the plaintext)'),
69
92
  };
70
93
  async run() {
71
94
  const { args, flags } = await this.parse(Create);
@@ -77,28 +100,51 @@ When this flag has done its job, mark readyForCleanup: true in the UI and run \`
77
100
  if (keyError) {
78
101
  return this.err(`Invalid key "${args.name}": ${keyError}`, { key: args.name, phase: 'validation' });
79
102
  }
80
- if (flags.type === 'boolean-flag') {
81
- return this.createBooleanFlag(args, flags.value);
103
+ const secret = parsedSecretFlags(flags);
104
+ // qfg-38lp: secrets are string-only, so --secret implies --type string.
105
+ const type = flags.type ?? (secret.selected ? 'string' : undefined);
106
+ if (type === undefined) {
107
+ return this.err('Missing required flag --type (e.g. --type string, --type boolean-flag). --secret implies --type string.', { key: args.name, phase: 'validation' });
108
+ }
109
+ // Refuse --secret / --confidential on types that cannot carry them BEFORE
110
+ // any value is parsed, so a coercion error can never echo the value.
111
+ if (secret.selected && type !== 'string' && type !== 'log_level') {
112
+ return this.err(`--secret flag only works with string type: secrets are string-only (got --type ${type}). ` +
113
+ 'To store JSON (or any structured value) as a secret, use --type string and JSON.parse it in your app.', { key: args.name, phase: 'validation' });
82
114
  }
83
- if (flags.type === 'log_level') {
84
- return this.createLogLevel(args, flags);
115
+ if (flags.confidential && type === 'boolean-flag') {
116
+ return this.err('--confidential is not supported for boolean-flag', { key: args.name, phase: 'validation' });
85
117
  }
86
- const key = args.name;
87
- const secret = parsedSecretFlags(flags);
88
- if (flags['env-var'] && flags.value) {
118
+ if (flags['env-var'] && flags.value && type !== 'log_level') {
89
119
  return this.err('cannot specify both --env-var and --value');
90
120
  }
121
+ // qfg-38lp: --value / --value-stdin / --value-from-env all resolve here.
122
+ // (log_level rejects --env-var itself, with a more specific message.)
123
+ const valueSource = await resolveValueSource({
124
+ ...flags,
125
+ 'env-var': type === 'log_level' ? undefined : flags['env-var'],
126
+ });
127
+ if (!valueSource.ok) {
128
+ return this.err(valueSource.error ? valueSource.message : 'No value provided', { key: args.name, phase: 'input' });
129
+ }
130
+ const providedValue = valueSource.value;
131
+ if (type === 'boolean-flag') {
132
+ return this.createBooleanFlag(args, providedValue);
133
+ }
134
+ if (type === 'log_level') {
135
+ return this.createLogLevel(args, { ...flags, value: providedValue });
136
+ }
137
+ const key = args.name;
91
138
  if (flags['env-var'] && secret.selected) {
92
139
  return this.err('cannot specify both --env-var and --secret');
93
140
  }
94
141
  if (flags.confidential && secret.selected) {
95
142
  console.warn("Note: --confidential is implied when using --secret, so you don't need to specify both.");
96
143
  }
97
- if (secret.selected && flags.type !== 'string') {
98
- return this.err('--secret flag only works with string type');
99
- }
100
144
  let configValue = {};
101
- let valueType = TYPE_MAPPING[flags.type];
145
+ let valueType = TYPE_MAPPING[type];
146
+ // Set whenever the value must not be printed (secret or confidential).
147
+ let redacted;
102
148
  if (flags['env-var']) {
103
149
  configValue = {
104
150
  provided: {
@@ -109,17 +155,31 @@ When this flag has done its job, mark readyForCleanup: true in the UI and run \`
109
155
  }
110
156
  else {
111
157
  const valueInput = await getValue({
112
- desiredValue: flags.value,
158
+ desiredValue: providedValue,
113
159
  flags,
114
- message: 'Default value',
160
+ mask: secret.selected || flags.confidential,
161
+ message: secret.selected ? 'Secret value (input is masked)' : 'Default value',
115
162
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
116
163
  quonfig: undefined,
117
164
  });
118
165
  if (valueInput.ok) {
119
166
  const rawValue = valueInput.value;
120
- const parsedConfigValue = coerceIntoType(flags.type, rawValue);
167
+ let parsedConfigValue;
168
+ try {
169
+ parsedConfigValue = coerceIntoType(type, rawValue);
170
+ }
171
+ catch (error) {
172
+ // qfg-38lp: coercion errors quote the value; hide it when it is confidential.
173
+ const message = error instanceof Error ? error.message : String(error);
174
+ if (!flags.confidential && !secret.selected)
175
+ throw error;
176
+ return this.err(message.split(rawValue).join(redactedDisplay(redactedSummary({ plaintext: rawValue }))), {
177
+ key,
178
+ phase: 'coercion',
179
+ });
180
+ }
121
181
  if (!parsedConfigValue) {
122
- return this.err(`Failed to coerce value into type: ${flags.type}`, { key, phase: 'coercion' });
182
+ return this.err(`Failed to coerce value into type: ${type}`, { key, phase: 'coercion' });
123
183
  }
124
184
  configValue = parsedConfigValue[0];
125
185
  valueType = parsedConfigValue[1];
@@ -130,6 +190,10 @@ When this flag has done its job, mark readyForCleanup: true in the UI and run \`
130
190
  return;
131
191
  }
132
192
  configValue = confidentialValueResult.value;
193
+ redacted = redactedSummary({ ciphertext: String(confidentialValueResult.value.value), plaintext: rawValue });
194
+ }
195
+ else if (flags.confidential) {
196
+ redacted = redactedSummary({ plaintext: rawValue });
133
197
  }
134
198
  }
135
199
  else if (valueInput.error) {
@@ -163,6 +227,15 @@ When this flag has done its job, mark readyForCleanup: true in the UI and run \`
163
227
  const response = request.json;
164
228
  const confidentialMaybe = flags.confidential ? '(confidential) ' : '';
165
229
  await this.syncConfigToDisk('configs', key, response);
230
+ if (redacted) {
231
+ // qfg-38lp: never echo the plaintext — not on the success line, not in
232
+ // --json (the server response carries confidential values verbatim).
233
+ return this.ok(`${checkmark} Created ${confidentialMaybe}config: ${key} ${redactedDisplay(redacted)}`, {
234
+ key,
235
+ ...scrubConfidentialValues(response),
236
+ value: redacted,
237
+ });
238
+ }
166
239
  return this.ok(`${checkmark} Created ${confidentialMaybe}config: ${key}`, { key, ...response });
167
240
  }
168
241
  async createBooleanFlag(args, rawDefault) {
@@ -8,6 +8,7 @@ export default class Get extends APICommand {
8
8
  static examples: string[];
9
9
  static flags: {
10
10
  environment: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
11
+ reveal: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
11
12
  };
12
13
  run(): Promise<JsonObj | void>;
13
14
  }
@@ -5,15 +5,29 @@ import getEnvironment from '../ui/get-environment.js';
5
5
  import isInteractive from '../util/is-interactive.js';
6
6
  import nameArg from '../util/name-arg.js';
7
7
  import { decrypt } from '../util/encryption.js';
8
+ import { redactedDisplay, redactedSummary } from '../util/redact.js';
8
9
  export default class Get extends APICommand {
9
10
  static args = { ...nameArg };
10
- static description = 'Get the value of a config/feature-flag/etc.';
11
+ static description = `Get the value of a config/feature-flag/etc.
12
+
13
+ Secrets (encrypted) and confidential values print REDACTED by default, as a
14
+ fingerprint plus length (and the last 4 characters for values of 12+ chars):
15
+ [encrypted *****3fa9c, 51 chars, ends …a1b2]
16
+ The fingerprint is the same one SDK telemetry and the Quonfig UI show, so you
17
+ can confirm a rotation landed without printing the secret. Pass --reveal to
18
+ print the plaintext. To inject secrets into a process, prefer \`qfg run\`.`;
11
19
  static examples = [
12
20
  '<%= config.bin %> <%= command.id %> my.config.name',
13
21
  '<%= config.bin %> <%= command.id %> my.config.name --environment=production',
22
+ '<%= config.bin %> <%= command.id %> stripe.api-key --environment=production # redacted fingerprint',
23
+ '<%= config.bin %> <%= command.id %> stripe.api-key --environment=production --reveal # plaintext',
14
24
  ];
15
25
  static flags = {
16
26
  environment: Flags.string({ description: 'environment to evaluate in' }),
27
+ reveal: Flags.boolean({
28
+ default: false,
29
+ description: 'print the plaintext of a secret/confidential value (it is redacted by default)',
30
+ }),
17
31
  };
18
32
  async run() {
19
33
  const { args, flags } = await this.parse(Get);
@@ -69,6 +83,10 @@ export default class Get extends APICommand {
69
83
  return this.err(`${key} could not be evaluated in this environment`);
70
84
  }
71
85
  let value = config.value;
86
+ // The stored ciphertext, kept for the redacted fingerprint (the SDK hashes
87
+ // the ciphertext for encrypted values, qfg-38lp).
88
+ let ciphertext;
89
+ let decrypted = false;
72
90
  const providedByDep = config.dependencies?.find((dep) => dep.dependencyType === 'providedBy');
73
91
  if (providedByDep) {
74
92
  const envVarName = providedByDep.source;
@@ -92,7 +110,11 @@ export default class Get extends APICommand {
92
110
  const keyProvidedByDep = encryptionKeyConfig.dependencies?.find((dep) => dep.dependencyType === 'providedBy');
93
111
  if (keyProvidedByDep) {
94
112
  const envVarName = keyProvidedByDep.source;
95
- this.logToStderr(`This config is encrypted by key '${encryptionKeyConfig.key}' that should be found in env var '${envVarName}'`);
113
+ // qfg-38lp: on a redacted read these diagnostics only add noise (and
114
+ // "Successfully decrypted" reads as if the plaintext was shown), so
115
+ // they print with --reveal, or with --verbose.
116
+ const diagnostic = (message) => (flags.reveal ? this.logToStderr(message) : this.verboseLog(message));
117
+ diagnostic(`This config is encrypted by key '${encryptionKeyConfig.key}' that should be found in env var '${envVarName}'`);
96
118
  const encryptionKey = process.env[envVarName];
97
119
  if (encryptionKey === undefined) {
98
120
  return this.err(`Environment variable '${envVarName}' is not set. Cannot decrypt config '${key}'.`, {
@@ -108,8 +130,10 @@ export default class Get extends APICommand {
108
130
  });
109
131
  }
110
132
  try {
133
+ ciphertext = value;
111
134
  value = decrypt(value, encryptionKey);
112
- this.logToStderr(`Successfully decrypted config '${key}'`);
135
+ decrypted = true;
136
+ diagnostic(`Successfully decrypted config '${key}'`);
113
137
  }
114
138
  catch (error) {
115
139
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -121,6 +145,19 @@ export default class Get extends APICommand {
121
145
  }
122
146
  }
123
147
  }
148
+ const isSecret = Boolean(decryptWithDep) || config.confidential === true;
149
+ if (isSecret && !flags.reveal) {
150
+ // qfg-38lp: never print a secret's plaintext unless asked to.
151
+ const summary = decryptWithDep
152
+ ? redactedSummary({
153
+ ciphertext: ciphertext ?? String(value),
154
+ plaintext: decrypted ? String(value) : undefined,
155
+ })
156
+ : redactedSummary({ plaintext: typeof value === 'string' ? value : JSON.stringify(value) });
157
+ this.logToStderr(`${key} is ${summary.encrypted ? 'a secret' : 'confidential'}: value redacted. ` +
158
+ 'A human can pass --reveal to print the plaintext (never do this in an agent session or CI log).');
159
+ return this.ok(redactedDisplay(summary), { [key]: summary });
160
+ }
124
161
  return this.ok(this.toSuccessJson(value), { [key]: value });
125
162
  }
126
163
  }
@@ -1,6 +1,7 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import { getEnvironments } from '../api/get-environments.js';
3
3
  import { APICommand } from '../index.js';
4
+ import { redactedDisplay, redactedSummary } from '../util/redact.js';
4
5
  import autocomplete from '../util/autocomplete.js';
5
6
  import { getAppUrl } from '../util/domain-urls.js';
6
7
  import isInteractive from '../util/is-interactive.js';
@@ -440,7 +441,15 @@ Related commands:
440
441
  if (envConfig.name === 'Default') {
441
442
  // For Default, show the unconditional fallback value
442
443
  const fallbackRule = envConfig.rules.find((r) => isCatchAllRule(r));
443
- const valueToShow = fallbackRule ? fallbackRule.value.value : envConfig.rules[0].value.value;
444
+ const shownRule = fallbackRule ?? envConfig.rules[0];
445
+ // qfg-38lp: a confidential (unencrypted) value is redacted, as in text mode.
446
+ const valueToShow = shownRule.value?.confidential && !shownRule.value?.decryptWith
447
+ ? redactedDisplay(redactedSummary({
448
+ plaintext: typeof shownRule.value.value === 'string'
449
+ ? shownRule.value.value
450
+ : JSON.stringify(shownRule.value.value),
451
+ }))
452
+ : shownRule.value.value;
444
453
  json[envConfig.name].value = valueToShow;
445
454
  }
446
455
  else {
@@ -11,6 +11,8 @@ export default class SetDefault extends APICommand {
11
11
  secret: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
12
12
  'secret-key-name': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
13
13
  confirm: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
14
+ 'value-from-env': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
15
+ 'value-stdin': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
14
16
  confidential: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
15
17
  'env-var': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
16
18
  environment: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;