diff --git a/.atlas b/.atlas index f63852e..13f5ff3 100644 --- a/.atlas +++ b/.atlas @@ -1,5 +1,5 @@ { "type": "workspace", - "description": "Environment variable management tool: onenv-api REST service + onenv-manager admin frontend.", + "description": "1Password-backed environment variable manager. Single CLI (onenv) that replaces .env files.", "archived": false } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d14b3be..7428da6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,19 +21,3 @@ jobs: - run: bun run biome check src/ - run: bunx tsc --noEmit - run: bun run test run - - api: - name: onenv-api - runs-on: ubuntu-latest - defaults: - run: - working-directory: onenv-api - steps: - - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - run: bun install --frozen-lockfile - - run: bun run biome check src/ - - run: bunx tsc --noEmit - - run: bun run test run diff --git a/AGENTS.md b/AGENTS.md index 3a39826..d38b35e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,17 +4,12 @@ This file provides guidance to Codex (Codex.ai/code) when working with code in t ## Overview -Monorepo with two independent TypeScript sub-projects that manage secrets stored in 1Password via the `op` CLI. A shared `install.ts` bootstraps both. +A single TypeScript CLI that stores environment variables in 1Password via the `op` CLI. A root `install.ts` script bootstraps the project (deps, build, link, optional `.env` migration). - **onenv-manager** — CLI + interactive TUI for managing secrets (Commander, @clack/prompts) -- **onenv-api** — HTTP API (Express) for agent-driven secret access with permission brokering - -Both have a parallel `onenv-client.ts` / `manager-service.ts` / `state-store.ts` architecture (duplicated, not shared as a package — copies have diverged). ## Commands -### onenv-manager - ```bash cd onenv-manager just check # loc-check + lint + typecheck + test (preferred) @@ -27,19 +22,6 @@ bun run build # tsc → dist/ Single test: `bun run vitest run src/lib/state-store.test.ts` -### onenv-api - -```bash -cd onenv-api -bun run biome check --write src/ # lint + format -bunx tsc --noEmit # typecheck -bun run test # tests (vitest) -bun run dev # dev with watch -bun run build # tsc → dist/ -``` - -Single test: `bun run vitest run src/lib/config.test.ts` - ### Installer ```bash @@ -52,7 +34,7 @@ bun run install.ts # interactive setup wizard (requires op CLI) onenv-client.ts → spawns `op` CLI, CRUD on 1Password items ↓ manager-service.ts → orchestration: combines onenv-client + state-store - ↓ (exported functions consumed by CLI/API routes) + ↓ state-store.ts → disabled-key tracking (~/.config/onenv-manager/state.json) ``` @@ -65,20 +47,13 @@ state-store.ts → disabled-key tracking (~/.config/onenv-manager/state.j - Secret stored in the `credential` field - Imported JSON files (via `onenv import`) carry extra STRING fields `group`, `path`, `type` per leaf for round-trip via `onenv build-file` -### onenv-api specifics - -- `src/server.ts` — Express routes, Zod validation, auth middleware (`x-onenv-token` header), pre-auth (per-IP, all methods) and post-auth (per-token, mutating only) rate limiters -- `src/lib/permission.ts` — macOS AppleScript permission dialog -- `src/lib/config.ts` — loads env vars with defaults; `AGENT_API_TOKEN` is required -- All mutating endpoints require permission approval before executing - ### onenv-manager specifics - `src/cli.ts` — Commander-based CLI entry point (set, edit, unset, list [--groups], disable, enable, init, run [--file], export, import, build-file, prime, tui) - `src/commands/tui.ts` — interactive @clack/prompts TUI loop -- `src/commands/prime.ts` — agent primer; XML by default, JSON when `--json` or piped -- `src/commands/prime-data.ts` + `prime-data-cli.ts` + `prime-data-api.ts` — primer data shape -- `src/commands/prime-xml.ts` — flat XML renderer (top-level tags with prose bodies) +- `src/commands/prime.ts` — agent primer dispatcher (XML on TTY, Markdown when piped, JSON with `--json`) +- `src/commands/prime-data.ts` + `prime-data-cli.ts` — primer data shape +- `src/commands/prime-xml.ts` / `prime-md.ts` — primer renderers - `src/commands/import.ts` — flatten a JSON file into onenv keys with `group`/`path`/`type` metadata - `src/commands/build-file.ts` — reassemble a grouped JSON file from stored leaves - `src/commands/run-files.ts` — `--file group:VAR` materialization + child-process cleanup helpers @@ -87,6 +62,7 @@ state-store.ts → disabled-key tracking (~/.config/onenv-manager/state.j - `src/lib/project-config.ts` — `.onenv.json` read/write for per-project setup - `src/lib/errors.ts` — structured CLI error types - `src/lib/output.ts` — JSON/text output formatting +- `src/lib/permission.ts` — opt-in AppleScript dialog gating mutations when `ONENV_CONFIRM_MUTATIONS=1` - `src/lib/ref-store.ts` — @-ref shorthand storage (positional: `@1`, `@2`, `@last`) - `src/lib/op-token.ts` — `op://` reference resolution + cache at `~/.config/onenv-manager/op-token` - `src/lib/validation.ts` — namespace + key regex/length validation @@ -99,5 +75,4 @@ state-store.ts → disabled-key tracking (~/.config/onenv-manager/state.j - Biome for linting/formatting (not ESLint/Prettier) - Vitest for testing, co-located test files (`*.test.ts`) - Strict TypeScript, `.js` extensions in imports -- `onenv-client.ts` is duplicated across both projects (not a shared package; copies have diverged) - State file path: `~/.config/onenv-manager/state.json` (or `$XDG_CONFIG_HOME`) diff --git a/INSTALL.md b/INSTALL.md index 4b1fff8..cb07283 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -7,23 +7,21 @@ | [Bun](https://bun.sh) | Yes | `curl -fsSL https://bun.sh/install \| bash` | | [1Password CLI](https://developer.1password.com/docs/cli/) | Yes | `brew install 1password-cli` | | [1Password desktop app](https://1password.com/downloads) | Yes | Required for biometric unlock / CLI integration | -| [just](https://github.com/casey/just) | Optional | `brew install just` (task runner for manager) | -| macOS | Yes | Desktop permission dialog uses AppleScript | +| [just](https://github.com/casey/just) | Optional | `brew install just` (task runner) | +| macOS | Optional | Only required to use the `ONENV_CONFIRM_MUTATIONS` desktop dialog | ## Clone & install ```bash git clone onenv && cd onenv - -cd onenv-manager && bun install && cd .. -cd onenv-api && bun install && cd .. +bun install.ts ``` -## Build +The installer walks through dependency install, build, vault creation, and +linking `onenv` system-wide. If you prefer manual steps: ```bash -cd onenv-manager && bun run build && cd .. -cd onenv-api && bun run build && cd .. +cd onenv-manager && bun install && bun run build && bun link && cd .. ``` ## 1Password setup @@ -35,36 +33,27 @@ cd onenv-api && bun run build && cd .. op vault create onenv ``` -## Configure — agent-api - -Copy the example env file and fill in values: - -```bash -cp onenv-api/.env.example onenv-api/.env -``` +## Configure | Variable | Required | Default | Description | |----------|----------|---------|-------------| -| `AGENT_API_TOKEN` | Yes | — | API authentication token | -| `API_HOST` | No | `127.0.0.1` | Server bind address | -| `API_PORT` | No | `4317` | Server port | -| `PERMISSION_TIMEOUT_MS` | No | `120000` | Permission request timeout (ms) | | `ONENV_VAULT` | No | `onenv` | 1Password vault name | | `ONENV_CATEGORY` | No | `API Credential` | 1Password item category | +| `ONENV_CONFIRM_MUTATIONS` | No | unset | Set to `1` on macOS to gate `set` / `edit` / `unset` / `import` behind an AppleScript desktop dialog | +| `PERMISSION_TIMEOUT_MS` | No | `120000` | Confirm-dialog timeout in ms (only used when `ONENV_CONFIRM_MUTATIONS=1`) | | `OP_SERVICE_ACCOUNT_TOKEN` | No | — | Skips biometric prompts (see below) | ### Avoiding constant 1Password approvals By default `op` requires biometric unlock (Touch ID / desktop app) for every -secret read. Agents calling `onenv-api` repeatedly — and any process invoking -the `onenv` CLI — will trigger prompt spam. +secret read. Any process invoking the `onenv` CLI will trigger prompt spam. To run headless / no-prompts, create a **1Password service account**: 1. 1Password web → **Developer** → **Service Accounts** → **Create**. 2. Grant **read + write** access to the `onenv` vault. -3. Set `OP_SERVICE_ACCOUNT_TOKEN` for both the API (`onenv-api/.env`) and the - shell that runs the `onenv` CLI. Two forms accepted: +3. Set `OP_SERVICE_ACCOUNT_TOKEN` in the shell that runs the `onenv` CLI. + Two forms accepted: ``` # literal — fastest, but plaintext on disk @@ -75,8 +64,7 @@ To run headless / no-prompts, create a **1Password service account**: OP_SERVICE_ACCOUNT_TOKEN=op://Personal//credential ``` -4. Restart the API. Startup log should show `1password auth: service-account`. - For the CLI, the first command resolves the reference; subsequent commands +4. The first `onenv` command resolves the reference; subsequent commands read the cached literal at `~/.config/onenv-manager/op-token` (mode 0600). Tradeoffs: @@ -87,24 +75,12 @@ Tradeoffs: let the next failed `op` call self-invalidate the cache) so the CLI re-resolves the new value. -> **Note:** The code reads env vars via `process.env` directly — there is no built-in dotenv loader. Use Bun's `--env-file .env` flag or Node 20+'s `--env-file` to load `.env` files: -> -> ```bash -> bun --env-file .env run start -> ``` - -## Configure — manager (optional) - -```bash -cp onenv-manager/.env.example onenv-manager/.env -``` - -| Variable | Required | Default | Description | -|----------|----------|---------|-------------| -| `ONENV_VAULT` | No | `onenv` | 1Password vault name | -| `ONENV_CATEGORY` | No | `API Credential` | 1Password item category | +> **Note:** The code reads env vars via `process.env` directly — there is no +> built-in dotenv loader. If you keep settings in a `.env`, source it from +> your shell or use `onenv export -- ` to inject them into the +> child process. -## Link CLI globally (manager) +## Link CLI globally ```bash cd onenv-manager && bun link @@ -114,8 +90,6 @@ This makes `onenv` available system-wide. ## Run & verify -### Manager - ```bash # Development mode (with watch) cd onenv-manager && bun run dev @@ -126,55 +100,12 @@ just dev onenv list ``` -### Agent API - -```bash -cd onenv-api - -# Development -bun run dev - -# Production -bun run start - -# Verify -curl http://127.0.0.1:4317/health -``` - -### Auth headers (agent-api) - -All endpoints except `/health` require: - -``` -x-onenv-token: -``` - -Optional agent identifier for permission prompts: - -``` -x-agent-name: my-agent -``` - ## State file location -The manager stores disabled-key state at: +The CLI stores disabled-key state at: ``` ~/.config/onenv-manager/state.json ``` Or `$XDG_CONFIG_HOME/onenv-manager/state.json` if `XDG_CONFIG_HOME` is set. - -## API endpoints - -| Method | Path | Auth | Permission | -|--------|------|------|------------| -| GET | `/health` | No | No | -| GET | `/v1/namespaces` | Yes | No | -| GET | `/v1/namespaces/:namespace/vars` | Yes | No | -| POST | `/v1/vars/set` | Yes | Yes | -| POST | `/v1/vars/edit` | Yes | Yes | -| POST | `/v1/vars/unset` | Yes | Yes | -| POST | `/v1/vars/disable` | Yes | Yes | -| POST | `/v1/vars/enable` | Yes | Yes | -| POST | `/v1/env/export` | Yes | Yes | diff --git a/README.md b/README.md index 7bd3c23..25d7b2a 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,7 @@ > 1Password-backed environment variable manager. Replaces `.env` files. -Two pieces, sharing a 1Password vault: - -- **`onenv-manager`** — CLI + interactive TUI for humans -- **`onenv-api`** — local HTTP API for agents, with permission brokering - -Secrets live in a 1Password vault as `namespace/KEY` items. No plaintext on disk. +Single CLI binary, one 1Password vault. Secrets live as `namespace/KEY` items. No plaintext on disk. ## Features @@ -18,8 +13,8 @@ Secrets live in a 1Password vault as `namespace/KEY` items. No plaintext on disk - **Per-project namespaces** — `.onenv.json` declares which namespaces a project uses; no global blast radius - **Disable without deleting** — `onenv disable ` keeps the secret in 1Password but excludes it from `run`/`export`; `enable` restores it - **Interactive TUI** — `@clack/prompts`-based menu for browsing/editing without typing commands -- **Agent HTTP API** — `onenv-api` exposes the same surface over HTTP, gating every mutation behind a macOS AppleScript desktop dialog -- **Agent primer** — `onenv prime` emits a complete spec (commands, errors, state files, HTTP endpoints) as XML or JSON for dropping into agent context +- **Optional macOS permission gate** — set `ONENV_CONFIRM_MUTATIONS=1` and `set` / `edit` / `unset` / `import` prompt a desktop AppleScript dialog before writing +- **Agent primer** — `onenv prime` emits a complete spec (commands, errors, state files) as XML, Markdown, or JSON for dropping into agent context - **`@`-refs** — positional shorthand against the last namespace list (`@1`, `@2`, `@last`) for fast repeat work - **Service-account ready** — `OP_SERVICE_ACCOUNT_TOKEN` accepts a literal `ops_eyJ...` token *or* an `op://...` reference; resolved once and cached at `~/.config/onenv-manager/op-token` (mode 0600), self-heals on auth failure - **Grouped listing** — `onenv list --groups` buckets keys by their reassembly group, with `(ungrouped)` for flat secrets @@ -35,24 +30,19 @@ onenv keeps the same `KEY=value` ergonomics but the values live in your 1Passwor - **Nothing on disk** — `onenv run` injects secrets at process start; they exist in env memory only - **Real auth** — biometric unlock or a scoped service account, not a chmod 600 hope - **Audit + rotate** — 1Password tracks every read; rotate by overwriting the item -- **Agent-safe** — the HTTP API gates every mutation behind a macOS AppleScript desktop dialog - **Per-project namespaces** — `.onenv.json` declares which namespaces a project uses; no global blast radius ## How it works ``` - ┌─ onenv-manager (CLI/TUI) ─┐ - │ │ - │ onenv set / list / run │──┐ - │ │ │ - └───────────────────────────┘ │ ┌──────────────┐ ┌───────────────┐ - ├──▶ │ op CLI │ ──▶ │ 1Password │ - ┌─ onenv-api (HTTP, :4317) ─┐ │ │ (biometric / │ │ vault │ - │ │ │ │ service-acct│ │ "onenv" │ - │ POST /v1/vars/set │──┘ └──────────────┘ └───────────────┘ - │ ↳ desktop dialog │ ▲ - │ (AppleScript) │ │ - └───────────────────────────┘ │ + ┌─ onenv (CLI/TUI) ─────────┐ + │ │ ┌──────────────┐ ┌───────────────┐ + │ onenv set / list / run │──▶ │ op CLI │ ──▶ │ 1Password │ + │ │ │ (biometric / │ │ vault │ + └───────────────────────────┘ │ service-acct│ │ "onenv" │ + └──────────────┘ └───────────────┘ + ▲ + │ reads `namespace/KEY` items tagged `onenv:`, field `credential` = value @@ -103,12 +93,12 @@ eu-west-1 Output is JSON by default (one shape per command — see `onenv prime`). For interactive browsing/editing, run `onenv tui`. -`onenv prime` is a complete agent primer — every command with output shape, every error code with retryable flag, every state-file path, and every HTTP endpoint with request/response shape. XML by default, structured JSON with `--json` or when piped. +`onenv prime` is a complete agent primer — every command with output shape, every error code with retryable flag, every state-file path. XML on a TTY, Markdown when piped, structured JSON with `--json`. ## Usage ```bash -onenv prime # XML primer of the CLI + API surface +onenv prime # XML primer of the CLI surface onenv list # all namespaces / keys you have access to onenv list aws # keys in one namespace onenv list aws --groups # bucket keys by reassembly group @@ -151,25 +141,11 @@ onenv run \ -- python3 mail_sync.py ``` -## Agent API - -For headless / agent use, run `onenv-api` and call it over HTTP. Mutations require human approval via a macOS AppleScript desktop dialog. - -```bash -curl -H "x-onenv-token: $AGENT_API_TOKEN" \ - http://127.0.0.1:4317/v1/namespaces -``` +## Confirm-on-write (optional) -| Method | Path | Auth | Permission | -|--------|-----------------------------------|------|------------| -| GET | `/health` | No | No | -| GET | `/v1/namespaces` | Yes | No | -| GET | `/v1/namespaces/:namespace/vars` | Yes | No | -| POST | `/v1/vars/{set,edit,unset}` | Yes | Yes | -| POST | `/v1/vars/{disable,enable}` | Yes | Yes | -| POST | `/v1/env/export` | Yes | Yes | +Set `ONENV_CONFIRM_MUTATIONS=1` and `set` / `edit` / `unset` / `import` will pop a macOS AppleScript dialog before writing to 1Password. Deny (or timeout — see `PERMISSION_TIMEOUT_MS`, default 120000ms) → the command throws `PERMISSION_DENIED` and nothing changes. Useful when handing the CLI to a long-running agent that should pause on every mutation. -Full endpoint + body docs in [`onenv-api/README.md`](onenv-api/README.md). +The flag has no effect on read commands (`list`, `export`, `run`, `build-file`) or on local-only state changes (`disable`, `enable`). ## Avoiding biometric prompts @@ -184,7 +160,7 @@ OP_SERVICE_ACCOUNT_TOKEN=ops_eyJ... # literal OP_SERVICE_ACCOUNT_TOKEN=op://Personal//credential # resolved on first use ``` -Both `onenv-api` (resolves at boot) and the `onenv` CLI (resolves on first command, caches the literal at `~/.config/onenv-manager/op-token` mode 0600) accept either form. After the first resolution all subsequent calls are silent. +The `onenv` CLI resolves the reference on first command and caches the literal at `~/.config/onenv-manager/op-token` (mode 0600). After the first resolution all subsequent calls are silent. Tradeoff: token compromise = vault compromise. Keep the `onenv` vault scoped to non-personal secrets. Full guide: [`docs/guides/service-account-setup.md`](docs/guides/service-account-setup.md). diff --git a/docs/CLAUDE-onenv.md b/docs/CLAUDE-onenv.md index 86ad2f9..528fd90 100644 --- a/docs/CLAUDE-onenv.md +++ b/docs/CLAUDE-onenv.md @@ -1,12 +1,13 @@ -Secrets via 1Password vault `onenv`. CLI: `onenv`. HTTP API: `onenv-api`. Replaces `.env` files. +Secrets via 1Password vault `onenv`. CLI: `onenv`. Replaces `.env` files. -First step in any onenv project: `onenv prime`. Returns the full CLI + API spec (commands, error envelope, state files, HTTP endpoints) — treat as ground truth. Use `onenv list` (no args) for namespaces, `onenv list ` for keys in one. Don't guess key names. +First step in any onenv project: `onenv prime`. Returns the full CLI spec (commands, error envelope, state files) — treat as ground truth. Use `onenv list` (no args) for namespaces, `onenv list ` for keys in one. Don't guess key names. NEVER create/read/reference `.env`. NEVER paste secret literals in code/configs/commits/messages. Add secret: `onenv set ` (interactive). Never write secrets to files. Run cmd needing secrets: `onenv run -- `. Not manual `export`, not shell injection. Rotate: `onenv set` overwrites. Retire: `onenv disable ` (preserves history) over `unset` (destructive). +Optional confirm-on-write: export `ONENV_CONFIRM_MUTATIONS=1` to gate `set` / `edit` / `unset` / `import` behind a macOS AppleScript dialog. Deny → `PERMISSION_DENIED`. @@ -21,21 +22,14 @@ prime · list [--groups] · set · edit · unset · disable · enable · run [-- Single-ns fetch: `onenv list `. All ns: `onenv list`. Group view: `onenv list --groups`. - -`onenv-api` (Express, default :4317). Auth header `x-onenv-token`. Optional `x-agent-name`. -GET (read): no approval. POST (mutate/export): require human approval via a macOS AppleScript desktop dialog. -Use API only when agent w/o TTY. Else CLI. - - Read on demand when task matches. Path `docs/guides/`: - migrating-from-dotenv.md — bulk import `.env`, retire it - per-project-setup.md — `.onenv.json`, ns conventions, `onenv init` - running-commands.md — `onenv run --`, `export`, shell integration -- agent-api.md — when HTTP API, auth, permission flow - service-account-setup.md — headless/no-prompt for daemons - rotation-and-disable.md — expiry, `disable` vs `unset`, key hygiene - multi-env-namespaces.md — dev/staging/prod patterns, `@refs` - ci-and-deploys.md — non-interactive, service account tokens - \ No newline at end of file + diff --git a/docs/examples/onenv-api.plist b/docs/examples/onenv-api.plist deleted file mode 100644 index df04633..0000000 --- a/docs/examples/onenv-api.plist +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - Label - com.doublej.onenv-api - - ProgramArguments - - /opt/homebrew/bin/bun - /Users/YOUR_USER/path/to/onenv/onenv-api/dist/index.js - - - WorkingDirectory - /Users/YOUR_USER/path/to/onenv/onenv-api - - EnvironmentVariables - - PATH - /opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin - AGENT_API_TOKEN - REPLACE_WITH_TOKEN - OP_SERVICE_ACCOUNT_TOKEN - ops_REPLACE_WITH_SA_TOKEN - PERMISSION_MODE - desktop - ONENV_VAULT - onenv - - - RunAtLoad - - - KeepAlive - - - StandardOutPath - /Users/YOUR_USER/Library/Logs/onenv-api.log - - StandardErrorPath - /Users/YOUR_USER/Library/Logs/onenv-api.err - - ProcessType - Background - - diff --git a/docs/guides/agent-api.md b/docs/guides/agent-api.md deleted file mode 100644 index e341952..0000000 --- a/docs/guides/agent-api.md +++ /dev/null @@ -1,91 +0,0 @@ -# Agent API (`onenv-api`) - -HTTP API: use when can't shell `onenv` — agents, daemons, runtimes without TTY. - -Human/interactive: prefer CLI. - -## Start the server - -```bash -cd onenv-api -bun run start # production (compiled) -bun run dev # watch mode -``` - -Default: `127.0.0.1:4317`. Override: `API_HOST` / `API_PORT`. - -## Auth - -All except `/health` require: - -``` -x-onenv-token: -``` - -Optional, approval prompts: - -``` -x-agent-name: my-agent -``` - -## Endpoints - -| Method | Path | Approval | -|--------|------|----------| -| GET | `/health` | — | -| GET | `/v1/namespaces` | No | -| GET | `/v1/namespaces/:ns/vars` | No | -| POST | `/v1/vars/set` | Yes | -| POST | `/v1/vars/edit` | Yes | -| POST | `/v1/vars/unset` | Yes | -| POST | `/v1/vars/disable` | Yes | -| POST | `/v1/vars/enable` | Yes | -| POST | `/v1/env/export` | Yes | - -## Permission flow - -POST endpoints block until the user approves via a macOS AppleScript desktop dialog. Timeout: `PERMISSION_TIMEOUT_MS` (default 120s). - -## Read example - -```bash -curl -s -H "x-onenv-token: $AGENT_API_TOKEN" \ - http://127.0.0.1:4317/v1/namespaces/aws/vars -# {"vars":[{"key":"AWS_KEY","disabled":false},...]} -``` - -## Mutate example - -```bash -curl -s -X POST \ - -H "x-onenv-token: $AGENT_API_TOKEN" \ - -H "x-agent-name: my-bot" \ - -H "Content-Type: application/json" \ - -d '{"namespace":"aws","key":"AWS_KEY","value":"AKIA..."}' \ - http://127.0.0.1:4317/v1/vars/set -``` - -Shows desktop prompt. Blocks until approved or timed out. - -## Rate limiting - -Two layers, both 60s windows: - -- **Pre-auth** (per IP, all methods): 120 req/min -- **Post-auth** (per `x-onenv-token`, mutating only): 60 req/min - -Exceeded → 429 + `Retry-After`. - -## Logging - -JSON to stdout per request: - -```json -{"ts":"2026-04-30T15:43:15.113Z","method":"GET","path":"/v1/namespaces","status":200,"ms":1488,"agent":"my-bot"} -``` - -Pipe `jq` for filtering, ship to log aggregator. - -## Headless / no prompts - -Set service account token (see [`service-account-setup.md`](service-account-setup.md)). API never triggers biometric prompts calling `op`. \ No newline at end of file diff --git a/docs/guides/ci-and-deploys.md b/docs/guides/ci-and-deploys.md index 74c487c..f25fd7d 100644 --- a/docs/guides/ci-and-deploys.md +++ b/docs/guides/ci-and-deploys.md @@ -51,13 +51,6 @@ Pass the SA token via: Never bake the token into the image. Don't commit it to a Helm chart. -## Server daemons (launchd / systemd) - -For `onenv-api` running as a long-lived service: - -- macOS: see `docs/examples/onenv-api.plist`. -- Linux: drop-in unit file with `Environment="OP_SERVICE_ACCOUNT_TOKEN=..."` or `EnvironmentFile=/etc/onenv-api.env` (root-only mode 0600). - ## Best practices - Scope the SA narrowly: read-only if the runner only consumes secrets; write only for rotation jobs. diff --git a/docs/guides/running-commands.md b/docs/guides/running-commands.md index 1773dfa..c18ce5b 100644 --- a/docs/guides/running-commands.md +++ b/docs/guides/running-commands.md @@ -29,8 +29,6 @@ Use for: debug and one-off namespace selection. Prefer `run` when a project has Best: `onenv run --` from parent shell, read via `process.env.MY_KEY`. Don't shell out to `onenv export` from app code. -If must, use HTTP API (`onenv-api`) — see [`agent-api.md`](agent-api.md). - ## Disabled keys `onenv run` and `onenv export` skip disabled keys. Re-enable with `onenv enable`. diff --git a/docs/guides/service-account-setup.md b/docs/guides/service-account-setup.md index 09c0923..71afde3 100644 --- a/docs/guides/service-account-setup.md +++ b/docs/guides/service-account-setup.md @@ -1,42 +1,33 @@ # Service Account setup -Without: `op` triggers biometric/desktop unlock per secret read. Agents calling `onenv-api` repeatedly — and any process invoking the `onenv` CLI — spam approvals. +Without: `op` triggers biometric/desktop unlock per secret read. Any process invoking the `onenv` CLI spams approvals. 1Password Service Account = non-human identity, own token. `op` skips biometric entirely. ## Create the SA 1. 1Password web → **Developer** → **Service Accounts** → **Create**. -2. Name it (e.g. "onenv-api-prod"). +2. Name it (e.g. "onenv-prod"). 3. Grant **read + write** on `onenv` vault. No other vaults. 4. Copy token (`ops_eyJ...`). Shown once. ## Wire it up -In `onenv-api/.env`: +Export in the shell that runs `onenv`: ``` OP_SERVICE_ACCOUNT_TOKEN=ops_eyJ... ``` -Restart. Startup log shows: - -``` -1password auth: service-account -``` - ## Reference form (optional) -Store token in 1Password, reference instead of plaintext: +Store the token in 1Password and pass a reference instead of a plaintext literal: ``` OP_SERVICE_ACCOUNT_TOKEN=op://Personal//credential ``` -Both surfaces resolve the reference automatically: - -- `onenv-api` resolves at startup via one `op read` call (one biometric on boot, zero during operation). -- The `onenv` CLI resolves on first invocation and caches the literal at `~/.config/onenv-manager/op-token` (mode 0600). Subsequent CLI runs read the cache — zero prompts. +The `onenv` CLI resolves the reference on first invocation and caches the literal at `~/.config/onenv-manager/op-token` (mode 0600). Subsequent CLI runs read the cache — zero prompts. After rotating the SA token, delete `~/.config/onenv-manager/op-token` so the CLI re-resolves the new value. (The cache also self-invalidates when `op` reports the cached token as invalid/expired.) @@ -60,8 +51,6 @@ Environment="OP_SERVICE_ACCOUNT_TOKEN=ops_eyJ..." ## Rotation 1. Create new SA, grant same vault access. -2. Update `OP_SERVICE_ACCOUNT_TOKEN` in env. -3. Restart `onenv-api`. +2. Update `OP_SERVICE_ACCOUNT_TOKEN` in the consuming shell / service. +3. Restart the consuming process (or delete `~/.config/onenv-manager/op-token` for ad-hoc CLI shells). 4. Delete old SA in 1Password web. - -Zero-downtime: run two API instances, rotate one at a time behind load balancer. \ No newline at end of file diff --git a/docs/src/routes/+page.svelte b/docs/src/routes/+page.svelte index 38ae262..f88b575 100644 --- a/docs/src/routes/+page.svelte +++ b/docs/src/routes/+page.svelte @@ -29,14 +29,14 @@ 'onenv disable hides a key from run/export but keeps it in 1Password. enable restores. State lives in ~/.config/onenv-manager/state.json.', }, { - title: 'Agent HTTP API', + title: 'Confirm-on-write', description: - 'onenv-api exposes the same surface over HTTP — every mutation gates behind a macOS AppleScript desktop dialog before executing.', + 'Set ONENV_CONFIRM_MUTATIONS=1 and set / edit / unset / import pause on a macOS AppleScript dialog before writing. Deny → the command throws PERMISSION_DENIED and nothing changes.', }, { title: 'Agent primer', description: - 'onenv prime emits the full CLI + API spec — every command shape, every error code, every state file, every endpoint — as XML or JSON. Drop straight into agent context.', + 'onenv prime emits the full CLI spec — every command shape, every error code, every state file — as XML, Markdown, or JSON. Drop straight into agent context.', }, { title: '@-refs', @@ -109,7 +109,7 @@

onenv

- A 1Password-backed environment variable manager. Replaces .env files with per-key 1Password items, a CLI for humans, and an HTTP API for agents with permission brokering. + A 1Password-backed environment variable manager. Replaces .env files with per-key 1Password items and a single CLI for humans and scripts.

View on GitHub @@ -171,7 +171,7 @@ .env files leak. They sit unencrypted next to source, drift between machines, get pasted into chat, end up in git status more often than they should, and nobody ever rotates the keys. Sharing them means Slack DMs and stale copies on three laptops.

- onenv keeps the same KEY=value ergonomics, but the values live in your 1Password vault — biometric or service-account auth, full audit log, atomic rotation, and an HTTP-API surface that gates every mutation behind explicit human approval. + onenv keeps the same KEY=value ergonomics, but the values live in your 1Password vault — biometric or service-account auth, full audit log, atomic rotation, and an opt-in macOS dialog that gates every write behind explicit human approval.

diff --git a/install.ts b/install.ts index ad2bfcd..e0f70ae 100644 --- a/install.ts +++ b/install.ts @@ -3,7 +3,6 @@ import { basename, dirname, join, relative } from 'node:path' const ROOT = import.meta.dir const MANAGER = `${ROOT}/onenv-manager` -const API = `${ROOT}/onenv-api` const CLACK_PATH = `${MANAGER}/node_modules/@clack/prompts/dist/index.mjs` const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', '.next', '.nuxt', '__pycache__', '.venv', 'venv']) const ONENV_VAULT = 'onenv' @@ -89,36 +88,6 @@ async function ensureVault(): Promise { }) } -async function configureEnv(): Promise { - p.log.step('Configure onenv-api') - const envPath = `${API}/.env` - if (await Bun.file(envPath).exists()) { - const overwrite = guard(await p.confirm({ message: '.env already exists — overwrite?', initialValue: false })) - if (!overwrite) return - } - - const token = guard(await p.text({ - message: 'Set an API token for authenticating requests to the agent API', - placeholder: 'a-secret-string-of-your-choice', - validate: (v) => (v.trim().length > 0 ? undefined : 'Token is required'), - })) - - const lines = [ - `AGENT_API_TOKEN=${token}`, - '', - '# API_HOST=127.0.0.1', - '# API_PORT=4317', - '# PERMISSION_TIMEOUT_MS=120000', - '', - `ONENV_VAULT=${ONENV_VAULT}`, - `ONENV_CATEGORY=${ONENV_CATEGORY}`, - '# ONENV_SERVICE_ACCOUNT_TOKEN=', - '', - ] - await Bun.write(envPath, lines.join('\n')) - p.log.success('.env written') -} - // --- env scanning & migration --- function findEnvFiles(root: string, maxDepth = 4): string[] { @@ -252,10 +221,7 @@ if (!Bun.which('just')) p.log.info('just not found — optional but recommended: await ensureVault() await installDeps('onenv-manager', MANAGER) -await installDeps('onenv-api', API) await build('onenv-manager', MANAGER) -await build('onenv-api', API) -await configureEnv() await spin('Linking onenv-manager CLI', () => run(['bun', 'link'], MANAGER)) try { @@ -272,7 +238,6 @@ const wantScan = guard(await p.confirm({ })) if (wantScan) await migrateEnvFiles() -p.log.info('Start the agent API:\n cd onenv-api && bun run --env-file .env start') -p.log.info('Verify:\n curl http://127.0.0.1:4317/health') +p.log.info('Optional: gate writes behind a macOS permission dialog by exporting\n ONENV_CONFIRM_MUTATIONS=1 (applies to set / edit / unset / import).') p.outro('Installation complete') diff --git a/onenv-api/.env.example b/onenv-api/.env.example deleted file mode 100644 index 00c6949..0000000 --- a/onenv-api/.env.example +++ /dev/null @@ -1,20 +0,0 @@ -# Required — API authentication token -AGENT_API_TOKEN=replace-me - -# Optional — Server bind address and port -# API_HOST=127.0.0.1 -# API_PORT=4317 - -# Optional — Permission request timeout in ms -# PERMISSION_TIMEOUT_MS=120000 - -# Optional — 1Password vault and category -ONENV_VAULT=onenv -ONENV_CATEGORY=API Credential - -# Optional — 1Password service account token. When set, `op` skips biometric/desktop -# auth so agents stop triggering Touch ID prompts. Required for headless/daemon use. -# Create in 1Password web → Developer → Service Accounts. Grant read+write on the -# vault above. Accepts either a literal `ops_...` token or an `op://...` reference -# (resolved at startup via one biometric prompt). Token compromise = vault compromise. -# OP_SERVICE_ACCOUNT_TOKEN=op://Personal//credential diff --git a/onenv-api/README.md b/onenv-api/README.md deleted file mode 100644 index db8c267..0000000 --- a/onenv-api/README.md +++ /dev/null @@ -1,71 +0,0 @@ -# onenv-api - -Safe local API for agent access to 1Password-managed environment variables. - -## Safety Model - -- Binds to `127.0.0.1` by default -- Requires API token on all endpoints except `/health` -- Mutating operations and env export require explicit user permission via a macOS AppleScript desktop dialog - -## Requirements - -- Bun -- `op` CLI available in `PATH` (1Password CLI) -- 1Password desktop app (for biometric unlock) - -## Install - -```bash -bun install -``` - -## Configure - -```bash -export AGENT_API_TOKEN='replace-me' - -# 1Password config -export ONENV_VAULT='onenv' # default -export ONENV_CATEGORY='API Credential' # default -``` - -Optional: -- `API_HOST` (default `127.0.0.1`) -- `API_PORT` (default `4317`) -- `PERMISSION_TIMEOUT_MS` (default `120000`) - -## Run - -```bash -bun run dev -# or -bun run build && bun run start -``` - -## Endpoints - -- `GET /health` -- `GET /v1/namespaces` -- `GET /v1/namespaces/:namespace/vars` -- `POST /v1/vars/set` -- `POST /v1/vars/edit` -- `POST /v1/vars/unset` -- `POST /v1/vars/disable` -- `POST /v1/vars/enable` -- `POST /v1/env/export` - -### Auth Headers - -- `x-onenv-token: ` -- optional `x-agent-name: ` (for permission prompts) - -### Body Examples - -```json -{ "namespace": "aws", "key": "AWS_SECRET_ACCESS_KEY", "value": "..." } -``` - -```json -{ "namespaces": ["aws", "project"] } -``` diff --git a/onenv-api/biome.json b/onenv-api/biome.json deleted file mode 100644 index 3351ac8..0000000 --- a/onenv-api/biome.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "$schema": "https://biomejs.dev/schemas/2.4.14/schema.json", - "assist": { "actions": { "source": { "organizeImports": "on" } } }, - "linter": { - "enabled": true, - "rules": { - "recommended": true, - "complexity": { - "noExcessiveCognitiveComplexity": { - "level": "error", - "options": { "maxAllowedComplexity": 10 } - }, - "noForEach": "warn" - }, - "style": { - "noNonNullAssertion": "warn", - "useConst": "error" - }, - "suspicious": { - "noExplicitAny": "warn" - } - } - }, - "formatter": { - "enabled": true, - "indentStyle": "space", - "indentWidth": 2, - "lineWidth": 100 - }, - "javascript": { - "formatter": { - "quoteStyle": "single", - "semicolons": "asNeeded" - } - } -} diff --git a/onenv-api/bun.lock b/onenv-api/bun.lock deleted file mode 100644 index 000471d..0000000 --- a/onenv-api/bun.lock +++ /dev/null @@ -1,418 +0,0 @@ -{ - "lockfileVersion": 1, - "configVersion": 1, - "workspaces": { - "": { - "name": "onenv-api", - "dependencies": { - "express": "latest", - "zod": "latest", - }, - "devDependencies": { - "@biomejs/biome": "latest", - "@types/express": "latest", - "@types/node": "latest", - "tsx": "latest", - "typescript": "latest", - "vitest": "latest", - }, - }, - }, - "packages": { - "@biomejs/biome": ["@biomejs/biome@2.4.14", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.14", "@biomejs/cli-darwin-x64": "2.4.14", "@biomejs/cli-linux-arm64": "2.4.14", "@biomejs/cli-linux-arm64-musl": "2.4.14", "@biomejs/cli-linux-x64": "2.4.14", "@biomejs/cli-linux-x64-musl": "2.4.14", "@biomejs/cli-win32-arm64": "2.4.14", "@biomejs/cli-win32-x64": "2.4.14" }, "bin": { "biome": "bin/biome" } }, "sha512-TmAvxOEgrpLypzVGJ8FulIZnlyA9TxrO1hyqYrCz9r+bwma9xXxuLA5IuYnj55XQneFx460KjRbx6SWGLkg3bQ=="], - - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XvgoE9XOawUOQPdmvs4J7wPhi/DLwSCGks3AlPJDmh34O0awRTqCED1HRcRDdpf1Zrp4us4MGOOdIxNpbqNF5Q=="], - - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-jE7hKBCFhOx3uUh+ZkWBfOHxAcILPfhFplNkuID/eZeSTLHzfZzoZxW8fbqY9xXRnPi7jGNAf1iPVR+0yWsM/Q=="], - - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-2TELhZnW5RSLL063l9rc5xLpA0ZIw0Ccwy/0q384rvNAgFw3yI76bd59547yxowdQr5MNPET/xDLrLuvgSeeWQ=="], - - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-/z+6gqAqqUQTHazwStxSXKHg9b8UvqBmDFRp+c4wYbq2KXhELQDon9EoC9RpmQ8JWkqQx/lIUy/cs+MhzDZp6A=="], - - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.14", "", { "os": "linux", "cpu": "x64" }, "sha512-zHrlQZDBDUz4OLAraYpWKcnLS6HOewBFWYOzY91d1ZjdqZwibOyb6BEu6WuWLugyo0P3riCmsbV9UqV1cSXwQg=="], - - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.14", "", { "os": "linux", "cpu": "x64" }, "sha512-R6BWgJdQOwW9ulJatuTVrQkjnODjqHZkKNOqb1sz++3Noe5LYd0i3PchnOBUCYAPHoPWHhjJqbdZlHEu0hpjdA=="], - - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-M3EH5hqOI/F/FUA2u4xcLoUgmxd218mvuj/6JL7Hv2toQvr2/AdOvKSpGkoRuWFCtQPVa+ZqkEV3Q5xBA9+XSA=="], - - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.14", "", { "os": "win32", "cpu": "x64" }, "sha512-WL0EG5qE+EAKomGXbf2g6VnSKJhTL3tXC0QRzWRwA5VpjxNYa6H4P7ZWfymbGE4IhZZQi1KXQ2R0YjwInmz2fA=="], - - "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], - - "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], - - "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], - - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], - - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], - - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], - - "@oxc-project/types": ["@oxc-project/types@0.128.0", "", {}, "sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ=="], - - "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.18", "", { "os": "android", "cpu": "arm64" }, "sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ=="], - - "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-apJq2ktnGp27nSInMR5Vcj8kY6xJzDAvfdIFlpDcAK/w4cDO58qVoi1YQsES/SKiFNge/6e4CUzgjfHduYqWpQ=="], - - "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-5Ofot8xbs+pxRHJqm9/9N/4sTQOvdrwEsmPE9pdLEEoAbdZtG6F2LMDfO1sp6ZAtXJuJV/21ew2srq3W8NXB5g=="], - - "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.18", "", { "os": "freebsd", "cpu": "x64" }, "sha512-7h8eeOTT1eyqJyx64BFCnWZpNm486hGWt2sqeLLgDxA0xI1oGZ9H7gK1S85uNGmBhkdPwa/6reTxfFFKvIsebw=="], - - "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.18", "", { "os": "linux", "cpu": "arm" }, "sha512-eRcm/HVt9U/JFu5RKAEKwGQYtDCKWLiaH6wOnsSEp6NMBb/3Os8LgHZlNyzMpFVNmiiMFlfb2zEnebfzJrHFmg=="], - - "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-SOrT/cT4ukTmgnrEz/Hg3m7LBnuCLW9psDeMKrimRWY4I8DmnO7Lco8W2vtqPmMkbVu8iJ+g4GFLVLLOVjJ9DQ=="], - - "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-QWjdxN1HJCpBTAcZ5N5F7wju3gVPzRzSpmGzx7na0c/1qpN9CFil+xt+l9lV/1M6/gqHSNXCiqPfwhVJPeLnug=="], - - "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.18", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ugCOyj7a4d9h3q9B+wXmf6g3a68UsjGh6dob5DHevHGMwDUbhsYNbSPxJsENcIttJZ9jv7qGM2UesLw5jqIhdg=="], - - "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.18", "", { "os": "linux", "cpu": "s390x" }, "sha512-kKWRhbsotpXkGbcd5dllUWg5gEXcDAa8u5YnP9AV5DYNbvJHGzzuwv7dpmhc8NqKMJldl0a+x76IHbspEpEmdA=="], - - "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.18", "", { "os": "linux", "cpu": "x64" }, "sha512-uCo8ElcCIAMyYAZyuIZ81oFkhTSIllNvUCHCAlbhlN4ji3uC28h7IIdlXyIvGO7HsuqnV9p3rD/bpH7XhIyhRw=="], - - "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.18", "", { "os": "linux", "cpu": "x64" }, "sha512-XNOQZtuE6yUIvx4rwGemwh8kpL1xvU41FXy/s9K7T/3JVcqGzo3NfKM2HrbrGgfPYGFW42f07Wk++aOC6B9NWA=="], - - "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.18", "", { "os": "none", "cpu": "arm64" }, "sha512-tSn/kzrfa7tNOXr7sEacDBN4YsIqTyLqh45IO0nHDwtpKIDNDJr+VFojt+4klSpChxB29JLyduSsE0MKEwa65A=="], - - "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.18", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-+J9YGmc+czgqlhYmwun3S3O0FIZhsH8ep2456xwjAdIOmuJxM7xz4P4PtrxU+Bz17a/5bqPA8o3HAAoX0teUdg=="], - - "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-zsu47DgU0FQzSwi6sU9dZoEdUv7pc1AptSEz/Z8HBg54sV0Pbs3N0+CrIbTsgiu6EyoaNN9CHboqbLaz9lhOyQ=="], - - "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.18", "", { "os": "win32", "cpu": "x64" }, "sha512-7H+3yqGgmnlDTRRhw/xpYY9J1kf4GC681nVc4GqKhExZTDrVVrV2tsOR9kso0fvgBdcTCcQShx4SLLoHgaLwhg=="], - - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.18", "", {}, "sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw=="], - - "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], - - "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], - - "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], - - "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], - - "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], - - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - - "@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="], - - "@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.1", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A=="], - - "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], - - "@types/node": ["@types/node@25.6.1", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-coJCN8O1q4AGyyqCAUSP06P+SrMTu18BkEj3NVAK07q6QUneD2wzj3CLv9+yP+BMeZQlMvneXqqvDe3w+xcq7g=="], - - "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], - - "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], - - "@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="], - - "@types/serve-static": ["@types/serve-static@2.2.0", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*" } }, "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ=="], - - "@vitest/expect": ["@vitest/expect@4.1.5", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.5", "@vitest/utils": "4.1.5", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw=="], - - "@vitest/mocker": ["@vitest/mocker@4.1.5", "", { "dependencies": { "@vitest/spy": "4.1.5", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw=="], - - "@vitest/pretty-format": ["@vitest/pretty-format@4.1.5", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g=="], - - "@vitest/runner": ["@vitest/runner@4.1.5", "", { "dependencies": { "@vitest/utils": "4.1.5", "pathe": "^2.0.3" } }, "sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ=="], - - "@vitest/snapshot": ["@vitest/snapshot@4.1.5", "", { "dependencies": { "@vitest/pretty-format": "4.1.5", "@vitest/utils": "4.1.5", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ=="], - - "@vitest/spy": ["@vitest/spy@4.1.5", "", {}, "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ=="], - - "@vitest/utils": ["@vitest/utils@4.1.5", "", { "dependencies": { "@vitest/pretty-format": "4.1.5", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug=="], - - "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - - "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], - - "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], - - "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], - - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], - - "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - - "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], - - "content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], - - "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], - - "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - - "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - - "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], - - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], - - "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - - "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - - "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - - "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], - - "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - - "es-module-lexer": ["es-module-lexer@2.1.0", "", {}, "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="], - - "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], - - "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], - - "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], - - "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - - "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], - - "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], - - "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], - - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - - "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], - - "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - - "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], - - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - - "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], - - "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - - "get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="], - - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], - - "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], - - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - - "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], - - "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], - - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], - - "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], - - "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], - - "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], - - "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], - - "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], - - "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], - - "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], - - "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], - - "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], - - "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], - - "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], - - "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], - - "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], - - "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], - - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - - "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], - - "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], - - "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], - - "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - - "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - - "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - - "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], - - "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], - - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - - "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], - - "path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], - - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], - - "postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="], - - "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], - - "qs": ["qs@6.14.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q=="], - - "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - - "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], - - "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], - - "rolldown": ["rolldown@1.0.0-rc.18", "", { "dependencies": { "@oxc-project/types": "=0.128.0", "@rolldown/pluginutils": "1.0.0-rc.18" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.18", "@rolldown/binding-darwin-arm64": "1.0.0-rc.18", "@rolldown/binding-darwin-x64": "1.0.0-rc.18", "@rolldown/binding-freebsd-x64": "1.0.0-rc.18", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.18", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.18", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.18", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.18", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.18", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.18", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.18", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.18", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.18", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.18", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.18" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg=="], - - "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], - - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - - "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], - - "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], - - "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - - "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], - - "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], - - "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], - - "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - - "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - - "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], - - "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], - - "std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="], - - "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], - - "tinyexec": ["tinyexec@1.1.2", "", {}, "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA=="], - - "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], - - "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], - - "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], - - "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], - - "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], - - "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], - - "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], - - "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - - "vite": ["vite@8.0.11", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.14", "rolldown": "1.0.0-rc.18", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow=="], - - "vitest": ["vitest@4.1.5", "", { "dependencies": { "@vitest/expect": "4.1.5", "@vitest/mocker": "4.1.5", "@vitest/pretty-format": "4.1.5", "@vitest/runner": "4.1.5", "@vitest/snapshot": "4.1.5", "@vitest/spy": "4.1.5", "@vitest/utils": "4.1.5", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.5", "@vitest/browser-preview": "4.1.5", "@vitest/browser-webdriverio": "4.1.5", "@vitest/coverage-istanbul": "4.1.5", "@vitest/coverage-v8": "4.1.5", "@vitest/ui": "4.1.5", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg=="], - - "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], - - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - - "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - - "@types/body-parser/@types/node": ["@types/node@22.19.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w=="], - - "@types/connect/@types/node": ["@types/node@22.19.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w=="], - - "@types/send/@types/node": ["@types/node@22.19.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w=="], - - "@types/body-parser/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "@types/connect/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "@types/send/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - } -} diff --git a/onenv-api/package.json b/onenv-api/package.json deleted file mode 100644 index bed5e09..0000000 --- a/onenv-api/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "onenv-api", - "version": "0.6.0", - "description": "Safe local API for agent-driven 1Password operations", - "author": "Jurre-Jan Smit", - "type": "module", - "scripts": { - "build": "tsc", - "dev": "tsx watch src/index.ts", - "start": "node dist/index.js", - "test": "vitest run src", - "lint": "biome check src/", - "lint:fix": "biome check --write src/" - }, - "dependencies": { - "express": "^5.2.1", - "zod": "^4.4.3" - }, - "devDependencies": { - "@biomejs/biome": "^2.4.14", - "@types/express": "^5.0.6", - "@types/node": "^25.6.1", - "tsx": "^4.21.0", - "typescript": "^6.0.3", - "vitest": "^4.1.5" - } -} diff --git a/onenv-api/src/index.ts b/onenv-api/src/index.ts deleted file mode 100644 index 79c66c0..0000000 --- a/onenv-api/src/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { loadConfig } from './lib/config.js' -import { resolveServiceAccountToken } from './lib/op-token.js' -import { createApp } from './server.js' - -async function main(): Promise { - await resolveServiceAccountToken() - - const config = loadConfig() - const app = createApp(config) - - const opAuth = process.env.OP_SERVICE_ACCOUNT_TOKEN ? 'service-account' : 'biometric' - - const server = app.listen(config.port, config.host, () => { - console.log(`onenv-api listening on http://${config.host}:${config.port}`) - console.log(`1password auth: ${opAuth}`) - }) - - const shutdown = (signal: string) => { - console.log(`received ${signal}, shutting down`) - server.close(() => process.exit(0)) - setTimeout(() => process.exit(1), 5000).unref() - } - process.on('SIGTERM', () => shutdown('SIGTERM')) - process.on('SIGINT', () => shutdown('SIGINT')) -} - -main().catch((err) => { - console.error(err) - process.exit(1) -}) diff --git a/onenv-api/src/lib/config.test.ts b/onenv-api/src/lib/config.test.ts deleted file mode 100644 index 6d29fea..0000000 --- a/onenv-api/src/lib/config.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { afterEach, describe, expect, it } from 'vitest' -import { loadConfig } from './config.js' - -const ENV_KEYS = [ - 'AGENT_API_TOKEN', - 'PERMISSION_TIMEOUT_MS', - 'API_HOST', - 'API_PORT', - 'ONENV_VAULT', - 'ONENV_CATEGORY', -] as const - -function clearEnv(): void { - for (const key of ENV_KEYS) delete process.env[key] -} - -afterEach(clearEnv) - -describe('loadConfig', () => { - it('loads defaults with required token', () => { - process.env.AGENT_API_TOKEN = 'token' - - const config = loadConfig() - - expect(config.host).toBe('127.0.0.1') - expect(config.port).toBe(4317) - expect(config.onenvVault).toBe('onenv') - expect(config.onenvCategory).toBe('API Credential') - }) - - it('reads custom ONENV_VAULT and ONENV_CATEGORY', () => { - process.env.AGENT_API_TOKEN = 'token' - process.env.ONENV_VAULT = 'my-vault' - process.env.ONENV_CATEGORY = 'Login' - - const config = loadConfig() - - expect(config.onenvVault).toBe('my-vault') - expect(config.onenvCategory).toBe('Login') - }) - - it('throws for invalid numeric config', () => { - process.env.AGENT_API_TOKEN = 'token' - process.env.API_PORT = 'nope' - - expect(() => loadConfig()).toThrow('Invalid API_PORT: nope') - }) -}) diff --git a/onenv-api/src/lib/config.ts b/onenv-api/src/lib/config.ts deleted file mode 100644 index 621cf3e..0000000 --- a/onenv-api/src/lib/config.ts +++ /dev/null @@ -1,36 +0,0 @@ -export interface ApiConfig { - host: string - port: number - authToken: string - permissionTimeoutMs: number - onenvVault: string - onenvCategory: string -} - -function parsePositiveInteger(name: string, raw: string | undefined, fallback: number): number { - const value = Number(raw ?? String(fallback)) - if (!Number.isInteger(value) || value <= 0) { - throw new Error(`Invalid ${name}: ${raw}`) - } - return value -} - -export function loadConfig(): ApiConfig { - const authToken = process.env.AGENT_API_TOKEN - if (!authToken) { - throw new Error('AGENT_API_TOKEN is required') - } - - return { - host: process.env.API_HOST ?? '127.0.0.1', - port: parsePositiveInteger('API_PORT', process.env.API_PORT, 4317), - authToken, - permissionTimeoutMs: parsePositiveInteger( - 'PERMISSION_TIMEOUT_MS', - process.env.PERMISSION_TIMEOUT_MS, - 120000, - ), - onenvVault: process.env.ONENV_VAULT ?? 'onenv', - onenvCategory: process.env.ONENV_CATEGORY ?? 'API Credential', - } -} diff --git a/onenv-api/src/lib/logging.ts b/onenv-api/src/lib/logging.ts deleted file mode 100644 index 6b7f907..0000000 --- a/onenv-api/src/lib/logging.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { NextFunction, Request, Response } from 'express' - -export function requestLogger() { - return (req: Request, res: Response, next: NextFunction) => { - if (req.path === '/health') { - next() - return - } - const start = Date.now() - res.on('finish', () => { - const entry = { - ts: new Date().toISOString(), - method: req.method, - path: req.path, - status: res.statusCode, - ms: Date.now() - start, - agent: req.header('x-agent-name') ?? null, - } - console.log(JSON.stringify(entry)) - }) - next() - } -} diff --git a/onenv-api/src/lib/manager-service.ts b/onenv-api/src/lib/manager-service.ts deleted file mode 100644 index b4b1fef..0000000 --- a/onenv-api/src/lib/manager-service.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { listKeys, listNamespaces, listValues, setValue, unsetValue } from './onenv-client.js' -import { getDisabledMap, setDisabled } from './state-store.js' - -export interface NamespaceVarSummary { - key: string - disabled: boolean -} - -export async function getNamespaces(): Promise { - return await listNamespaces() -} - -export async function getNamespaceVars(namespace: string): Promise { - const keys = await listKeys(namespace) - const disabledMap = await getDisabledMap() - const disabled = new Set(disabledMap[namespace] ?? []) - - return keys.map((key) => ({ - key, - disabled: disabled.has(key), - })) -} - -export async function setVar(namespace: string, key: string, value: string): Promise { - await setValue(namespace, key, value) - await setDisabled(namespace, key, false) -} - -export async function editVar(namespace: string, key: string, value: string): Promise { - await setValue(namespace, key, value) -} - -export async function unsetVar(namespace: string, key: string): Promise { - await unsetValue(namespace, key) - await setDisabled(namespace, key, false) -} - -export async function disableVar(namespace: string, key: string): Promise { - await setDisabled(namespace, key, true) -} - -export async function enableVar(namespace: string, key: string): Promise { - await setDisabled(namespace, key, false) -} - -export async function exportEnabledValues(namespaces: string[]): Promise> { - const disabledMap = await getDisabledMap() - - const perNamespace = await Promise.all( - namespaces.map(async (namespace) => { - const values = await listValues(namespace) - const disabled = new Set(disabledMap[namespace] ?? []) - return Object.entries(values).filter(([key]) => !disabled.has(key)) - }), - ) - - return Object.fromEntries(perNamespace.flat()) -} diff --git a/onenv-api/src/lib/onenv-client.test.ts b/onenv-api/src/lib/onenv-client.test.ts deleted file mode 100644 index 854157b..0000000 --- a/onenv-api/src/lib/onenv-client.test.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { EventEmitter } from 'node:events' -import { readFileSync } from 'node:fs' -import { beforeEach, describe, expect, it, vi } from 'vitest' - -vi.mock('node:child_process', () => ({ spawn: vi.fn() })) - -const cp = await import('node:child_process') - -interface FakeChild extends EventEmitter { - stdout: EventEmitter - stderr: EventEmitter - stdin: { end: ReturnType; write: ReturnType } -} - -interface OpField { - id: string - type: string - value: string -} - -function fakeChild(stdout = '', stderr = '', code = 0): FakeChild { - const child = new EventEmitter() as FakeChild - child.stdout = new EventEmitter() - child.stderr = new EventEmitter() - child.stdin = { end: vi.fn(), write: vi.fn() } - setTimeout(() => { - if (stdout) child.stdout.emit('data', Buffer.from(stdout)) - if (stderr) child.stderr.emit('data', Buffer.from(stderr)) - child.emit('close', code) - }, 0) - return child -} - -function lastStdin(child: FakeChild): string { - const calls = child.stdin.end.mock.calls - return calls.length === 0 ? '' : String(calls[calls.length - 1][0] ?? '') -} - -const sampleItems = [ - { id: 'a1', title: 'aws/AWS_KEY', tags: ['aws'] }, - { id: 'a2', title: 'aws/AWS_SECRET', tags: ['aws'] }, - { id: 'g1', title: 'github/GH_TOKEN', tags: ['github'] }, -] - -describe('onenv-client', () => { - beforeEach(() => { - vi.mocked(cp.spawn).mockReset() - }) - - it('listNamespaces returns sorted unique namespaces', async () => { - vi.mocked(cp.spawn).mockImplementationOnce((() => - fakeChild(JSON.stringify(sampleItems))) as never) - const { listNamespaces } = await import('./onenv-client.js') - expect(await listNamespaces()).toEqual(['aws', 'github']) - }) - - it('listKeys filters and sorts by namespace', async () => { - vi.mocked(cp.spawn).mockImplementationOnce((() => - fakeChild(JSON.stringify(sampleItems.filter((i) => i.tags.includes('aws'))))) as never) - const { listKeys } = await import('./onenv-client.js') - expect(await listKeys('aws')).toEqual(['AWS_KEY', 'AWS_SECRET']) - }) - - it('listValues batches via op inject (one inject call, multiple keys)', async () => { - let injectCalls = 0 - vi.mocked(cp.spawn) - .mockImplementationOnce((() => - fakeChild(JSON.stringify(sampleItems.filter((i) => i.tags.includes('aws'))))) as never) - .mockImplementationOnce(((_cmd: string, args: readonly string[]) => { - injectCalls += 1 - expect(args[0]).toBe('inject') - expect(args[1]).toBe('-i') - const template = readFileSync(args[2], 'utf8') - const sep = template.match(/<>/)?.[0] - expect(sep).toBeTruthy() - const substituted = template.replace( - /\{\{ op:\/\/[^/]+\/([^/]+)\/credential \}\}/g, - 'mock_$1', - ) - return fakeChild(substituted) as never - }) as never) - - const { listValues } = await import('./onenv-client.js') - const result = await listValues('aws') - expect(injectCalls).toBe(1) - expect(Object.keys(result).sort()).toEqual(['AWS_KEY', 'AWS_SECRET']) - expect(result.AWS_KEY).toBe('mock_a1') - expect(result.AWS_SECRET).toBe('mock_a2') - }) - - it('does not interpolate key names into op inject templates', async () => { - const key = 'BAD{{ op://Personal/other/credential }}' - vi.mocked(cp.spawn) - .mockImplementationOnce((() => - fakeChild(JSON.stringify([{ id: 'evil', title: `aws/${key}` }]))) as never) - .mockImplementationOnce(((_cmd: string, args: readonly string[]) => { - const template = readFileSync(args[2], 'utf8') - expect(template).not.toContain('Personal/other') - return fakeChild( - template.replace(/\{\{ op:\/\/[^/]+\/([^/]+)\/credential \}\}/g, 'mock_$1'), - ) as never - }) as never) - - const { listValues } = await import('./onenv-client.js') - expect(await listValues('aws')).toEqual({ [key]: 'mock_evil' }) - }) - - it('listValues returns empty when namespace has no matching items', async () => { - vi.mocked(cp.spawn).mockImplementationOnce((() => fakeChild('[]')) as never) - const { listValues } = await import('./onenv-client.js') - expect(await listValues('missing')).toEqual({}) - }) - - it('setValue creates a new item with secret piped via stdin', async () => { - let createArgs: readonly string[] = [] - let createChild: FakeChild | null = null - vi.mocked(cp.spawn) - .mockImplementationOnce((() => fakeChild('', 'item not found', 1)) as never) - .mockImplementationOnce(((_cmd: string, args: readonly string[]) => { - createArgs = args - createChild = fakeChild('ok') - return createChild as never - }) as never) - const { setValue } = await import('./onenv-client.js') - await setValue('aws', 'AWS_KEY', 'secret') - expect(createArgs).toContain('create') - expect(createArgs).toContain('-') - expect(createArgs).not.toContain('credential=secret') - const stdin = JSON.parse(lastStdin(createChild as unknown as FakeChild)) - expect(stdin.title).toBe('aws/AWS_KEY') - expect(stdin.fields.find((f: OpField) => f.id === 'credential').value).toBe('secret') - }) - - it('setValue edits existing item via stdin-piped JSON', async () => { - let editArgs: readonly string[] = [] - let editChild: FakeChild | null = null - const existing = { - id: 'x', - title: 'aws/AWS_KEY', - fields: [{ id: 'credential', type: 'CONCEALED', value: 'old' }], - } - vi.mocked(cp.spawn) - .mockImplementationOnce((() => fakeChild(JSON.stringify(existing))) as never) - .mockImplementationOnce(((_cmd: string, args: readonly string[]) => { - editArgs = args - editChild = fakeChild('ok') - return editChild as never - }) as never) - const { setValue } = await import('./onenv-client.js') - await setValue('aws', 'AWS_KEY', 'newsecret') - expect(editArgs).toContain('edit') - expect(editArgs).toContain('aws/AWS_KEY') - expect(editArgs).not.toContain('credential=newsecret') - const stdin = JSON.parse(lastStdin(editChild as unknown as FakeChild)) - expect(stdin.fields.find((f: OpField) => f.id === 'credential').value).toBe('newsecret') - }) - - it('unsetValue deletes the item', async () => { - let deleteArgs: readonly string[] = [] - vi.mocked(cp.spawn).mockImplementationOnce(((_cmd: string, args: readonly string[]) => { - deleteArgs = args - return fakeChild('') as never - }) as never) - const { unsetValue } = await import('./onenv-client.js') - await unsetValue('aws', 'AWS_KEY') - expect(deleteArgs).toContain('item') - expect(deleteArgs).toContain('delete') - expect(deleteArgs).toContain('aws/AWS_KEY') - }) -}) diff --git a/onenv-api/src/lib/onenv-client.ts b/onenv-api/src/lib/onenv-client.ts deleted file mode 100644 index e2fa060..0000000 --- a/onenv-api/src/lib/onenv-client.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { spawn } from 'node:child_process' -import { randomBytes } from 'node:crypto' -import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' -import { tmpdir } from 'node:os' -import { join } from 'node:path' - -const ONENV_VAULT = process.env.ONENV_VAULT ?? 'onenv' -const ONENV_CATEGORY = process.env.ONENV_CATEGORY ?? 'API Credential' - -interface ExecResult { - code: number - stdout: string - stderr: string -} - -async function execOp(args: string[], stdin?: string): Promise { - return await new Promise((resolve, reject) => { - const child = spawn('op', args, { stdio: ['pipe', 'pipe', 'pipe'], env: process.env }) - - let stdout = '' - let stderr = '' - - child.stdout.on('data', (chunk) => { - stdout += String(chunk) - }) - child.stderr.on('data', (chunk) => { - stderr += String(chunk) - }) - - child.on('error', (error) => { - reject(new Error(`Failed to start op CLI: ${error.message}`)) - }) - - child.on('close', (code) => { - resolve({ code: code ?? 1, stdout, stderr }) - }) - - child.stdin.end(stdin ?? '') - }) -} - -async function runOp(args: string[], stdin?: string): Promise { - const result = await execOp(args, stdin) - if (result.code !== 0) { - const err = result.stderr.trim() || result.stdout.trim() || 'Unknown op error' - throw new Error(`op ${args.join(' ')} failed: ${err}`) - } - return result.stdout -} - -async function runOpInject(template: string): Promise { - const dir = mkdtempSync(join(tmpdir(), 'onenv-inject-')) - const file = join(dir, 'template') - writeFileSync(file, template) - try { - return await runOp(['inject', '-i', file]) - } finally { - rmSync(dir, { recursive: true, force: true }) - } -} - -interface OpItem { - id: string - title: string - tags?: string[] -} - -function parseTitle(title: string): { namespace: string; key: string } | null { - const sep = title.indexOf('/') - if (sep <= 0) return null - return { namespace: title.slice(0, sep), key: title.slice(sep + 1) } -} - -async function listItems(tags?: string): Promise { - const args = [ - 'item', - 'list', - '--vault', - ONENV_VAULT, - '--categories', - ONENV_CATEGORY, - '--format', - 'json', - ] - if (tags) args.push('--tags', tags) - - const result = await execOp(args) - if (result.code !== 0 && result.stderr.includes('no items found')) return [] - if (result.code !== 0) { - throw new Error(`op ${args.join(' ')} failed: ${result.stderr.trim()}`) - } - - const raw = result.stdout.trim() - if (!raw || raw === '[]') return [] - return JSON.parse(raw) as OpItem[] -} - -export async function listNamespaces(): Promise { - const items = await listItems() - const namespaces = new Set( - items.map((i) => parseTitle(i.title)?.namespace).filter(Boolean) as string[], - ) - return [...namespaces].sort() -} - -export async function listKeys(namespace: string): Promise { - const items = await listItems(namespace) - const keys: string[] = [] - for (const item of items) { - const parsed = parseTitle(item.title) - if (parsed?.namespace === namespace) keys.push(parsed.key) - } - return keys.sort() -} - -export async function listValues(namespace: string): Promise> { - const items = await listItems(namespace) - const matched: { id: string; key: string; index: number }[] = [] - for (const item of items) { - const parsed = parseTitle(item.title) - if (parsed?.namespace === namespace) { - matched.push({ id: item.id, key: parsed.key, index: matched.length }) - } - } - if (matched.length === 0) return {} - - const sep = `<>` - const template = `${matched - .map(({ id, index }) => `${sep}${index}${sep}{{ op://${ONENV_VAULT}/${id}/credential }}`) - .join('\n')}\n` - - const output = await runOpInject(template) - return parseInject( - output, - sep, - matched.map(({ key }) => key), - ) -} - -function parseInject(output: string, sep: string, keys: string[]): Record { - const result: Record = {} - const parts = output.split(sep) - for (let i = 1; i + 1 < parts.length; i += 2) { - const index = Number.parseInt(parts[i], 10) - const key = Number.isInteger(index) ? keys[index] : undefined - if (!key) continue - let value = parts[i + 1] - if (value.endsWith('\n')) value = value.slice(0, -1) - result[key] = value - } - return result -} - -interface OpField { - id: string - type: string - value: string - label?: string - purpose?: string -} - -interface OpItemDetail { - fields?: OpField[] - [key: string]: unknown -} - -function upsertField(item: OpItemDetail, id: string, type: string, value: string): void { - if (!item.fields) item.fields = [] - const existing = item.fields.find((f) => f.id === id) - if (existing) { - existing.value = value - existing.type = type - return - } - item.fields.push({ id, type, value, label: id }) -} - -export async function setValue(namespace: string, key: string, value: string): Promise { - const title = `${namespace}/${key}` - const check = await execOp(['item', 'get', title, '--vault', ONENV_VAULT, '--format', 'json']) - - if (check.code === 0) { - const item = JSON.parse(check.stdout) as OpItemDetail - upsertField(item, 'credential', 'CONCEALED', value) - await runOp(['item', 'edit', title, '--vault', ONENV_VAULT], JSON.stringify(item)) - return - } - - const template: OpItemDetail = { - title, - category: 'API_CREDENTIAL', - vault: { name: ONENV_VAULT }, - tags: [namespace], - fields: [{ id: 'credential', type: 'CONCEALED', value, label: 'credential' }], - } - await runOp( - ['item', 'create', '-', '--vault', ONENV_VAULT, '--category', ONENV_CATEGORY], - JSON.stringify(template), - ) -} - -export async function unsetValue(namespace: string, key: string): Promise { - await runOp(['item', 'delete', `${namespace}/${key}`, '--vault', ONENV_VAULT]) -} diff --git a/onenv-api/src/lib/op-token.ts b/onenv-api/src/lib/op-token.ts deleted file mode 100644 index e49dcde..0000000 --- a/onenv-api/src/lib/op-token.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { spawn } from 'node:child_process' - -export async function resolveServiceAccountToken(): Promise { - const raw = process.env.OP_SERVICE_ACCOUNT_TOKEN - if (!raw?.startsWith('op://')) return - - const resolved = await opRead(raw) - process.env.OP_SERVICE_ACCOUNT_TOKEN = resolved -} - -async function opRead(reference: string): Promise { - const { OP_SERVICE_ACCOUNT_TOKEN: _omit, ...env } = process.env - return await new Promise((resolve, reject) => { - const child = spawn('op', ['read', reference], { stdio: ['pipe', 'pipe', 'pipe'], env }) - let stdout = '' - let stderr = '' - child.stdout.on('data', (chunk) => { - stdout += String(chunk) - }) - child.stderr.on('data', (chunk) => { - stderr += String(chunk) - }) - child.on('error', (err) => reject(err)) - child.on('close', (code) => { - if (code !== 0) { - reject(new Error(`op read ${reference} failed: ${stderr.trim()}`)) - return - } - resolve(stdout.trimEnd()) - }) - child.stdin.end() - }) -} diff --git a/onenv-api/src/lib/permission.ts b/onenv-api/src/lib/permission.ts deleted file mode 100644 index 7a4c386..0000000 --- a/onenv-api/src/lib/permission.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { execFile } from 'node:child_process' -import type { ApiConfig } from './config.js' - -export interface PermissionRequest { - action: 'set' | 'edit' | 'unset' | 'disable' | 'enable' | 'export' - namespace?: string - key?: string - requestedBy: string - details?: string -} - -async function execFileAsync( - file: string, - args: string[], -): Promise<{ stdout: string; stderr: string }> { - return await new Promise((resolve, reject) => { - execFile(file, args, (error, stdout, stderr) => { - if (error) { - reject(new Error(stderr || error.message)) - return - } - resolve({ stdout, stderr }) - }) - }) -} - -async function showDesktopDialog(request: PermissionRequest, timeoutMs: number): Promise { - if (process.platform !== 'darwin') { - return false - } - - const timeoutSeconds = Math.max(10, Math.floor(timeoutMs / 1000)) - const summary = [ - `Agent request: ${request.action}`, - request.namespace ? `Namespace: ${request.namespace}` : null, - request.key ? `Key: ${request.key}` : null, - `Requester: ${request.requestedBy}`, - request.details ? `Details: ${request.details}` : null, - 'Allow this operation?', - ] - .filter(Boolean) - .join('\\n') - - const script = `display dialog ${JSON.stringify(summary)} buttons {"Deny", "Allow"} default button "Deny" with title "onenv-api" giving up after ${timeoutSeconds}` - - try { - const result = await execFileAsync('osascript', ['-e', script]) - return ( - result.stdout.includes('button returned:Allow') && !result.stdout.includes('gave up:true') - ) - } catch { - return false - } -} - -function parseRequestedBy(headerValue: string | undefined): string { - if (!headerValue) { - return 'unknown-agent' - } - - return headerValue.trim().slice(0, 128) || 'unknown-agent' -} - -export class PermissionService { - private readonly timeoutMs: number - - constructor(config: ApiConfig) { - this.timeoutMs = config.permissionTimeoutMs - } - - async request( - request: Omit, - requestedByHeader: string | undefined, - ): Promise { - const fullRequest: PermissionRequest = { - ...request, - requestedBy: parseRequestedBy(requestedByHeader), - } - return await showDesktopDialog(fullRequest, this.timeoutMs) - } -} diff --git a/onenv-api/src/lib/rate-limit.ts b/onenv-api/src/lib/rate-limit.ts deleted file mode 100644 index 648e3fb..0000000 --- a/onenv-api/src/lib/rate-limit.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { NextFunction, Request, Response } from 'express' - -interface Bucket { - count: number - resetAt: number -} - -const WINDOW_MS = 60_000 -const MAX_PER_WINDOW = 60 - -interface RateLimiterOptions { - key?: (req: Request) => string - maxPerWindow?: number - methods?: 'all' | 'mutating' - windowMs?: number -} - -export function rateLimiter(options: RateLimiterOptions = {}) { - const buckets = new Map() - const maxPerWindow = options.maxPerWindow ?? MAX_PER_WINDOW - const methods = options.methods ?? 'mutating' - const windowMs = options.windowMs ?? WINDOW_MS - - return (req: Request, res: Response, next: NextFunction) => { - if (methods === 'mutating' && req.method === 'GET') { - next() - return - } - const key = options.key?.(req) ?? req.header('x-onenv-token') ?? req.ip ?? 'unknown' - const now = Date.now() - const bucket = buckets.get(key) - - if (!bucket || bucket.resetAt <= now) { - buckets.set(key, { count: 1, resetAt: now + windowMs }) - next() - return - } - - if (bucket.count >= maxPerWindow) { - const retryAfter = Math.ceil((bucket.resetAt - now) / 1000) - res.setHeader('Retry-After', String(retryAfter)) - res.status(429).json({ error: 'rate limit exceeded' }) - return - } - - bucket.count += 1 - next() - } -} diff --git a/onenv-api/src/lib/state-store.ts b/onenv-api/src/lib/state-store.ts deleted file mode 100644 index 353dd7e..0000000 --- a/onenv-api/src/lib/state-store.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises' -import { homedir } from 'node:os' -import { dirname, join } from 'node:path' - -interface StateFile { - version: 1 - disabled: Record -} - -const DEFAULT_STATE: StateFile = { - version: 1, - disabled: {}, -} - -function getConfigRoot(): string { - return process.env.XDG_CONFIG_HOME ?? join(homedir(), '.config') -} - -function getStatePath(): string { - return join(getConfigRoot(), 'onenv-manager', 'state.json') -} - -async function readState(): Promise { - try { - const raw = await readFile(getStatePath(), 'utf-8') - const parsed = JSON.parse(raw) as Partial - return { - version: 1, - disabled: parsed.disabled ?? {}, - } - } catch { - return { ...DEFAULT_STATE } - } -} - -async function writeState(state: StateFile): Promise { - const statePath = getStatePath() - await mkdir(dirname(statePath), { recursive: true }) - await chmod(dirname(statePath), 0o700) - await writeFile(statePath, JSON.stringify(state, null, 2), 'utf-8') - await chmod(statePath, 0o600) -} - -export async function getDisabledMap(): Promise> { - const state = await readState() - return state.disabled -} - -export async function setDisabled( - namespace: string, - key: string, - disabled: boolean, -): Promise { - const state = await readState() - const keys = new Set(state.disabled[namespace] ?? []) - - if (disabled) { - keys.add(key) - } else { - keys.delete(key) - } - - if (keys.size === 0) { - delete state.disabled[namespace] - } else { - state.disabled[namespace] = [...keys].sort() - } - - await writeState(state) -} diff --git a/onenv-api/src/server.ts b/onenv-api/src/server.ts deleted file mode 100644 index 5e250de..0000000 --- a/onenv-api/src/server.ts +++ /dev/null @@ -1,236 +0,0 @@ -import { createHash, timingSafeEqual } from 'node:crypto' -import express, { type NextFunction, type Request, type Response } from 'express' -import { z } from 'zod' -import type { ApiConfig } from './lib/config.js' -import { requestLogger } from './lib/logging.js' -import { - disableVar, - editVar, - enableVar, - exportEnabledValues, - getNamespaces, - getNamespaceVars, - setVar, - unsetVar, -} from './lib/manager-service.js' -import { PermissionService } from './lib/permission.js' -import { rateLimiter } from './lib/rate-limit.js' - -const namespaceSchema = z - .string() - .trim() - .min(1) - .max(128) - .regex(/^[A-Za-z0-9][A-Za-z0-9_.-]*$/, 'invalid namespace') - -const keySchema = z - .string() - .trim() - .min(1) - .max(128) - .regex(/^[A-Za-z_][A-Za-z0-9_]*$/, 'invalid key') - -const setSchema = z.object({ - namespace: namespaceSchema, - key: keySchema, - value: z.string(), -}) - -const nsKeySchema = z.object({ - namespace: namespaceSchema, - key: keySchema, -}) - -const exportSchema = z.object({ - namespaces: z.array(namespaceSchema).min(1), -}) - -function errorResponse(res: Response, code: number, message: string): void { - res.status(code).json({ error: message }) -} - -function tokenDigest(token: string | undefined): Buffer { - return createHash('sha256') - .update(token ?? '') - .digest() -} - -function tokenMatches(actual: string | undefined, expected: string): boolean { - return timingSafeEqual(tokenDigest(actual), tokenDigest(expected)) -} - -function authMiddleware(config: ApiConfig) { - return (req: Request, res: Response, next: NextFunction) => { - if (req.path === '/health') { - next() - return - } - - const token = req.header('x-onenv-token') - if (!tokenMatches(token, config.authToken)) { - errorResponse(res, 401, 'unauthorized') - return - } - - next() - } -} - -type PermissionAction = 'set' | 'edit' | 'unset' | 'disable' | 'enable' | 'export' - -interface PermissionOptions { - action: PermissionAction - namespace?: string - key?: string - details?: string -} - -async function withPermission( - permissionService: PermissionService, - req: Request, - res: Response, - opts: PermissionOptions, -): Promise { - const allowed = await permissionService.request( - { - action: opts.action, - namespace: opts.namespace, - key: opts.key, - details: opts.details ?? `${req.method} ${req.path}`, - }, - req.header('x-agent-name') ?? undefined, - ) - - if (!allowed) { - errorResponse(res, 403, 'permission denied') - return false - } - - return true -} - -export function createApp(config: ApiConfig): express.Express { - const app = express() - const permissionService = new PermissionService(config) - - app.use(requestLogger()) - app.use(express.json({ limit: '32kb' })) - app.use(rateLimiter({ key: (req) => req.ip ?? 'unknown', maxPerWindow: 120, methods: 'all' })) - app.use(authMiddleware(config)) - app.use(rateLimiter()) - - app.get('/health', (_req, res) => { - res.status(200).json({ ok: true }) - }) - - app.get('/v1/namespaces', async (_req, res, next) => { - try { - const namespaces = await getNamespaces() - res.json({ namespaces }) - } catch (error) { - next(error) - } - }) - - app.get('/v1/namespaces/:namespace/vars', async (req, res, next) => { - try { - const vars = await getNamespaceVars(req.params.namespace) - res.json({ vars }) - } catch (error) { - next(error) - } - }) - - app.post('/v1/vars/set', async (req, res, next) => { - try { - const input = setSchema.parse(req.body) - const opts = { action: 'set' as const, namespace: input.namespace, key: input.key } - if (!(await withPermission(permissionService, req, res, opts))) return - - await setVar(input.namespace, input.key, input.value) - res.json({ ok: true }) - } catch (error) { - next(error) - } - }) - - app.post('/v1/vars/edit', async (req, res, next) => { - try { - const input = setSchema.parse(req.body) - const opts = { action: 'edit' as const, namespace: input.namespace, key: input.key } - if (!(await withPermission(permissionService, req, res, opts))) return - - await editVar(input.namespace, input.key, input.value) - res.json({ ok: true }) - } catch (error) { - next(error) - } - }) - - app.post('/v1/vars/unset', async (req, res, next) => { - try { - const input = nsKeySchema.parse(req.body) - const opts = { action: 'unset' as const, namespace: input.namespace, key: input.key } - if (!(await withPermission(permissionService, req, res, opts))) return - - await unsetVar(input.namespace, input.key) - res.json({ ok: true }) - } catch (error) { - next(error) - } - }) - - app.post('/v1/vars/disable', async (req, res, next) => { - try { - const input = nsKeySchema.parse(req.body) - const opts = { action: 'disable' as const, namespace: input.namespace, key: input.key } - if (!(await withPermission(permissionService, req, res, opts))) return - - await disableVar(input.namespace, input.key) - res.json({ ok: true }) - } catch (error) { - next(error) - } - }) - - app.post('/v1/vars/enable', async (req, res, next) => { - try { - const input = nsKeySchema.parse(req.body) - const opts = { action: 'enable' as const, namespace: input.namespace, key: input.key } - if (!(await withPermission(permissionService, req, res, opts))) return - - await enableVar(input.namespace, input.key) - res.json({ ok: true }) - } catch (error) { - next(error) - } - }) - - app.post('/v1/env/export', async (req, res, next) => { - try { - const input = exportSchema.parse(req.body) - const opts = { - action: 'export' as const, - details: `${req.method} ${req.path} namespaces=${input.namespaces.join(',')}`, - } - if (!(await withPermission(permissionService, req, res, opts))) return - - const env = await exportEnabledValues(input.namespaces) - res.json({ env }) - } catch (error) { - next(error) - } - }) - - app.use((error: unknown, _req: Request, res: Response, _next: NextFunction) => { - if (error instanceof z.ZodError) { - errorResponse(res, 400, error.issues.map((issue) => issue.message).join('; ')) - return - } - - const message = error instanceof Error ? error.message : 'Internal server error' - errorResponse(res, 500, message) - }) - - return app -} diff --git a/onenv-api/tsconfig.json b/onenv-api/tsconfig.json deleted file mode 100644 index 21b5c30..0000000 --- a/onenv-api/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "outDir": "dist", - "rootDir": "src", - "strict": true, - "types": ["node"], - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true - }, - "include": ["src"] -} diff --git a/onenv-manager/CHANGELOG.md b/onenv-manager/CHANGELOG.md index e454a1f..c5dc530 100644 --- a/onenv-manager/CHANGELOG.md +++ b/onenv-manager/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## [0.7.0] — 2026-05-22 + +### Added + +- `ONENV_CONFIRM_MUTATIONS=1` — opt-in macOS dialog that gates every mutating CLI path (`set`, `edit`, `unset`, `import`) with an Allow/Deny prompt naming the action, namespace, and key(s). Deny → `PERMISSION_DENIED`. Non-macOS errors with `PERMISSION_UNSUPPORTED` so the guard fails loud. Optional `PERMISSION_TIMEOUT_MS` (default 120000) caps the dialog wait. + +### Removed (breaking) + +- The `onenv-api` package is gone. It served HTTP at port 47891 with `x-onenv-token` auth and brokered mutations through an AppleScript dialog. A consumer audit turned up no callers — the agent surface is the CLI. The permission-brokering feature it uniquely offered now lives on the CLI as `ONENV_CONFIRM_MUTATIONS=1` (above). If you were running the api, switch your callers to invoking `onenv` directly with that env set. +- `prime-data-api.ts` and the `` section of the primer are gone; replaced by ``. + +### Changed + +- Single-package repo: install.ts no longer prompts for `AGENT_API_TOKEN` or bootstraps the api. + ## [0.6.0] — 2026-05-21 ### Added diff --git a/onenv-manager/package.json b/onenv-manager/package.json index 1895897..0f612a8 100644 --- a/onenv-manager/package.json +++ b/onenv-manager/package.json @@ -1,6 +1,6 @@ { "name": "onenv-manager", - "version": "0.6.0", + "version": "0.7.0", "description": "A command-line tool", "author": "Jurre-Jan Smit", "type": "module", diff --git a/onenv-manager/src/commands/edit.ts b/onenv-manager/src/commands/edit.ts index fd962f5..c872f6f 100644 --- a/onenv-manager/src/commands/edit.ts +++ b/onenv-manager/src/commands/edit.ts @@ -2,6 +2,7 @@ import type { Command } from 'commander' import { editVar } from '../index.js' import { validationError } from '../lib/errors.js' import { success } from '../lib/output.js' +import { confirmMutation } from '../lib/permission.js' import { resolveRef, storeRefs } from '../lib/ref-store.js' import { validateKey, validateNamespace } from '../lib/validation.js' import { readValueFromPrompt, readValueFromStdin } from './_prompt.js' @@ -29,6 +30,7 @@ export async function editAction( const namespace = validateNamespace(await resolveRef(rawNamespace)) const safeKey = validateKey(key) const next = await resolveValue(namespace, safeKey, options) + await confirmMutation({ action: 'edit', namespace, details: `key=${safeKey}` }) await editVar(namespace, safeKey, next) await storeRefs([namespace]) success(`Updated ${namespace}.${safeKey}`, { namespace, key: safeKey }) diff --git a/onenv-manager/src/commands/import.ts b/onenv-manager/src/commands/import.ts index b933d6b..e316aa6 100644 --- a/onenv-manager/src/commands/import.ts +++ b/onenv-manager/src/commands/import.ts @@ -11,6 +11,7 @@ import { unsetValue, } from '../lib/onenv-client.js' import { output, success } from '../lib/output.js' +import { confirmMutation } from '../lib/permission.js' import { resolveRef, storeRefs } from '../lib/ref-store.js' import type { JsonLeafType } from '../lib/types.js' import { validateKey, validateNamespace } from '../lib/validation.js' @@ -68,6 +69,12 @@ export async function importJsonFile( const snapshot = await fetchSnapshot(namespace, keys) validateAgainstSnapshot(snapshot, namespace, opts) + await confirmMutation({ + action: 'import', + namespace, + details: `group=${opts.group} keys=${keys.length}`, + }) + const written: string[] = [] try { for (let i = 0; i < entries.length; i++) { diff --git a/onenv-manager/src/commands/prime-data-api.ts b/onenv-manager/src/commands/prime-data-api.ts deleted file mode 100644 index 207e52b..0000000 --- a/onenv-manager/src/commands/prime-data-api.ts +++ /dev/null @@ -1,126 +0,0 @@ -import type { ApiSection } from './prime-data.js' - -export function buildApi(): ApiSection { - return { - description: - 'Local HTTP server (onenv-api) for agent-driven access. Mutating endpoints block on a macOS desktop permission dialog before executing.', - default_url: 'http://127.0.0.1:4317', - auth: 'Header: x-onenv-token: . Compared with sha256 + timing-safe equality.', - audit_header: 'Optional x-agent-name (≤128 chars) is logged + shown in permission prompts.', - rate_limit: - '120 req/IP/min pre-auth (all methods); 60 req/token/min post-auth (mutating only). 429 + Retry-After on exceed.', - config_env: [ - { - name: 'AGENT_API_TOKEN', - required: true, - description: 'Bearer token clients send via x-onenv-token.', - }, - { name: 'API_HOST', required: false, default: '127.0.0.1', description: 'Bind address.' }, - { name: 'API_PORT', required: false, default: '4317', description: 'Bind port.' }, - { - name: 'PERMISSION_TIMEOUT_MS', - required: false, - default: '120000', - description: 'How long to wait for user approval before denying.', - }, - { - name: 'ONENV_VAULT', - required: false, - default: 'onenv', - description: '1Password vault name.', - }, - { - name: 'ONENV_CATEGORY', - required: false, - default: 'API Credential', - description: '1Password item category.', - }, - ], - endpoints: [ - { - method: 'GET', - path: '/health', - permission: 'none', - response: '{ok:true}', - notes: 'Auth not required.', - }, - { - method: 'GET', - path: '/v1/namespaces', - permission: 'none', - response: '{namespaces:string[]}', - }, - { - method: 'GET', - path: '/v1/namespaces/:namespace/vars', - permission: 'none', - response: '{vars:[{key:string,disabled:boolean}]}', - }, - { - method: 'POST', - path: '/v1/vars/set', - permission: 'required', - body: '{namespace, key, value}', - response: '{ok:true}', - }, - { - method: 'POST', - path: '/v1/vars/edit', - permission: 'required', - body: '{namespace, key, value}', - response: '{ok:true}', - }, - { - method: 'POST', - path: '/v1/vars/unset', - permission: 'required', - body: '{namespace, key}', - response: '{ok:true}', - }, - { - method: 'POST', - path: '/v1/vars/disable', - permission: 'required', - body: '{namespace, key}', - response: '{ok:true}', - }, - { - method: 'POST', - path: '/v1/vars/enable', - permission: 'required', - body: '{namespace, key}', - response: '{ok:true}', - }, - { - method: 'POST', - path: '/v1/env/export', - permission: 'required', - body: '{namespaces:string[]}', - response: '{env:{KEY:"value",...}}', - notes: 'Same bare-key collision rule as the CLI export.', - }, - ], - error_responses: [ - { - status: 401, - body: '{error:"unauthorized"}', - when: 'Missing or wrong x-onenv-token.', - }, - { - status: 403, - body: '{error:"permission denied"}', - when: 'User denied or timed out the permission prompt.', - }, - { - status: 400, - body: '{error:""}', - when: 'Request body failed schema validation.', - }, - { - status: 500, - body: '{error:""}', - when: 'Unexpected internal failure (e.g. op CLI errors).', - }, - ], - } -} diff --git a/onenv-manager/src/commands/prime-data.ts b/onenv-manager/src/commands/prime-data.ts index dd57a81..f82d1e9 100644 --- a/onenv-manager/src/commands/prime-data.ts +++ b/onenv-manager/src/commands/prime-data.ts @@ -1,4 +1,3 @@ -import { buildApi } from './prime-data-api.js' import { buildCommands, buildErrors, buildState } from './prime-data-cli.js' export interface PrimerData { @@ -12,7 +11,7 @@ export interface PrimerData { state: StateEntry[] errors: ErrorsSection output_contract: OutputContract - api: ApiSection + permission: PermissionSection } export interface SetupSection { @@ -74,44 +73,19 @@ export interface OutputContract { json_mode_trigger: string } -export interface ApiEnv { - name: string - required: boolean - default?: string +export interface PermissionSection { description: string -} - -export interface ApiEndpoint { - method: string - path: string - permission: 'none' | 'required' - body?: string - response: string - notes?: string -} - -export interface ApiErrorResponse { - status: number - body: string - when: string -} - -export interface ApiSection { - description: string - default_url: string - auth: string - audit_header: string - rate_limit: string - config_env: ApiEnv[] - endpoints: ApiEndpoint[] - error_responses: ApiErrorResponse[] + trigger_env: string + timeout_env: string + gated_commands: string[] + error_code: string } export function buildPrimer(version: string): PrimerData { return { version, summary: - '1Password-backed secret management. CLI (onenv) for humans + scripts; HTTP API (onenv-api) for agents with permission brokering. All output is JSON when piped or --json is set.', + '1Password-backed secret management. Single CLI (onenv) for humans, scripts, and agents. All output is JSON when piped or --json is set. Set ONENV_CONFIRM_MUTATIONS=1 to gate writes behind a macOS desktop dialog.', setup: { description: 'Authenticate via service account (headless) or interactive op signin. The CLI auto-resolves op:// references on first use.', @@ -167,6 +141,17 @@ export function buildPrimer(version: string): PrimerData { json_mode_trigger: 'Pass --json explicitly, OR run with stdout not a TTY (pipes, scripts, CI). Either triggers JSON mode globally.', }, - api: buildApi(), + permission: buildPermission(), + } +} + +function buildPermission(): PermissionSection { + return { + description: + 'Opt-in confirm-on-write. When enabled, the listed commands display a macOS AppleScript dialog before mutating 1Password. Deny → the command throws PERMISSION_DENIED and nothing changes. macOS only; the CLI errors if the flag is set on any other platform. Useful when an agent or long-running script should pause on every write.', + trigger_env: 'ONENV_CONFIRM_MUTATIONS=1 (any other value, including unset, disables it)', + timeout_env: 'PERMISSION_TIMEOUT_MS (optional, default 120000ms; minimum 10s)', + gated_commands: ['set', 'edit', 'unset', 'import'], + error_code: 'PERMISSION_DENIED on deny / timeout (category: user_error, exit code 1)', } } diff --git a/onenv-manager/src/commands/prime-md.ts b/onenv-manager/src/commands/prime-md.ts index 11298e1..76edd1e 100644 --- a/onenv-manager/src/commands/prime-md.ts +++ b/onenv-manager/src/commands/prime-md.ts @@ -1,9 +1,9 @@ import type { - ApiSection, CommandSpec, ErrorsSection, NamingSection, OutputContract, + PermissionSection, PrimerData, RefsSection, SetupSection, @@ -20,7 +20,7 @@ export function renderMarkdown(data: PrimerData): string { renderWorkflow(data.workflow), renderErrors(data.errors), renderOutputContract(data.output_contract), - renderApi(data.api), + renderPermission(data.permission), ] return `${sections.join('\n\n')}\n` } @@ -139,45 +139,13 @@ function renderOutputContract(c: OutputContract): string { ].join('\n') } -function renderApi(api: ApiSection): string { +function renderPermission(perm: PermissionSection): string { return [ - '## API', - api.description, - `URL: ${api.default_url} (default)`, - `Auth: ${api.auth}`, - `Audit: ${api.audit_header}`, - `Rate limit: ${api.rate_limit}`, - '### Config env', - renderApiEnv(api), - '### Endpoints', - renderApiEndpoints(api), - '### Errors', - renderApiErrors(api), - ].join('\n\n') -} - -function renderApiEnv(api: ApiSection): string { - return api.config_env - .map((e) => { - const flags = [e.required ? 'required' : 'optional'] - if (e.default !== undefined) flags.push(`default ${e.default}`) - return `- \`${e.name}\` (${flags.join(', ')}) — ${e.description}` - }) - .join('\n') -} - -function renderApiEndpoints(api: ApiSection): string { - return api.endpoints - .map((e) => { - const parts = [`\`${e.method} ${e.path}\``, `[${e.permission}]`] - if (e.body) parts.push(`body: ${e.body}`) - parts.push(`→ ${e.response}`) - const line = `- ${parts.join(' ')}` - return e.notes ? `${line}\n - notes: ${e.notes}` : line - }) - .join('\n') -} - -function renderApiErrors(api: ApiSection): string { - return api.error_responses.map((e) => `- ${e.status} \`${e.body}\` — ${e.when}`).join('\n') + '## Confirm-on-write (opt-in)', + perm.description, + `- Trigger: \`${perm.trigger_env}\``, + `- Timeout: \`${perm.timeout_env}\``, + `- Gated commands: ${perm.gated_commands.map((c) => `\`${c}\``).join(', ')}`, + `- On deny / timeout: ${perm.error_code}`, + ].join('\n') } diff --git a/onenv-manager/src/commands/prime-xml.ts b/onenv-manager/src/commands/prime-xml.ts index 4ef6f6f..8aa2483 100644 --- a/onenv-manager/src/commands/prime-xml.ts +++ b/onenv-manager/src/commands/prime-xml.ts @@ -1,9 +1,9 @@ import type { - ApiSection, CommandSpec, ErrorsSection, NamingSection, OutputContract, + PermissionSection, PrimerData, RefsSection, SetupSection, @@ -19,7 +19,7 @@ export function renderXml(data: PrimerData): string { tag('workflow', renderWorkflow(data.workflow), { strict: 'true' }), tag('errors', renderErrors(data.errors)), tag('output', renderOutputContract(data.output_contract)), - tag('api', renderApi(data.api)), + tag('permission', renderPermission(data.permission)), ] return `\n\n${sections.join('\n\n')}\n\n` } @@ -143,48 +143,13 @@ function renderOutputContract(c: OutputContract): string { ].join('\n') } -function renderApi(api: ApiSection): string { +function renderPermission(perm: PermissionSection): string { return [ - api.description, + perm.description, '', - `URL: ${api.default_url} (default)`, - `Auth: ${api.auth}`, - `Audit: ${api.audit_header}`, - `Rate limit: ${api.rate_limit}`, - '', - 'CONFIG ENV:', - renderApiEnv(api), - '', - 'ENDPOINTS:', - renderApiEndpoints(api), - '', - 'ERRORS:', - renderApiErrors(api), + `Trigger: ${perm.trigger_env}`, + `Timeout: ${perm.timeout_env}`, + `Gated commands: ${perm.gated_commands.join(', ')}`, + `On deny / timeout: ${perm.error_code}`, ].join('\n') } - -function renderApiEnv(api: ApiSection): string { - return api.config_env - .map((e) => { - const flags = [e.required ? 'required' : 'optional'] - if (e.default !== undefined) flags.push(`default ${e.default}`) - return `- ${e.name} (${flags.join(', ')}) — ${e.description}` - }) - .join('\n') -} - -function renderApiEndpoints(api: ApiSection): string { - return api.endpoints - .map((e) => { - const parts = [`${e.method.padEnd(4)} ${e.path}`, `[${e.permission}]`] - if (e.body) parts.push(`body: ${e.body}`) - parts.push(`→ ${e.response}`) - const line = ` ${parts.join(' ')}` - return e.notes ? `${line}\n notes: ${e.notes}` : line - }) - .join('\n') -} - -function renderApiErrors(api: ApiSection): string { - return api.error_responses.map((e) => `- ${e.status} ${e.body} — ${e.when}`).join('\n') -} diff --git a/onenv-manager/src/commands/prime.ts b/onenv-manager/src/commands/prime.ts index 95329ec..886b934 100644 --- a/onenv-manager/src/commands/prime.ts +++ b/onenv-manager/src/commands/prime.ts @@ -4,7 +4,7 @@ import { LOGO } from './prime-logo.js' import { renderMarkdown } from './prime-md.js' import { renderXml } from './prime-xml.js' -export type PrimeFormat = 'xml' | 'json' | 'md' +export type PrimeFormat = 'xml' | 'md' | 'json' export function printPrime(format: PrimeFormat): void { const data = buildPrimer(getPackageVersion()) diff --git a/onenv-manager/src/commands/set.ts b/onenv-manager/src/commands/set.ts index a785969..aba983d 100644 --- a/onenv-manager/src/commands/set.ts +++ b/onenv-manager/src/commands/set.ts @@ -2,6 +2,7 @@ import type { Command } from 'commander' import { createOrUpdateVar } from '../index.js' import { validationError } from '../lib/errors.js' import { success } from '../lib/output.js' +import { confirmMutation } from '../lib/permission.js' import { resolveRef, storeRefs } from '../lib/ref-store.js' import { validateKey, validateNamespace } from '../lib/validation.js' import { readValueFromPrompt, readValueFromStdin } from './_prompt.js' @@ -29,6 +30,7 @@ export async function setAction( const namespace = validateNamespace(await resolveRef(rawNamespace)) const safeKey = validateKey(key) const next = await resolveValue(namespace, safeKey, options) + await confirmMutation({ action: 'set', namespace, details: `key=${safeKey}` }) await createOrUpdateVar(namespace, safeKey, next) await storeRefs([namespace]) success(`Saved ${namespace}.${safeKey}`, { namespace, key: safeKey }) diff --git a/onenv-manager/src/commands/unset.ts b/onenv-manager/src/commands/unset.ts index 11b16ac..63adce4 100644 --- a/onenv-manager/src/commands/unset.ts +++ b/onenv-manager/src/commands/unset.ts @@ -3,6 +3,7 @@ import type { Command } from 'commander' import { removeVar } from '../index.js' import { validationError } from '../lib/errors.js' import { success } from '../lib/output.js' +import { confirmMutation } from '../lib/permission.js' import { resolveRef, storeRefs } from '../lib/ref-store.js' import { validateKey, validateNamespace } from '../lib/validation.js' @@ -49,6 +50,11 @@ export async function unsetAction( const proceed = await confirmOrAbort(namespace, safeKeys) if (!proceed) return } + await confirmMutation({ + action: 'unset', + namespace, + details: `keys=${safeKeys.join(',')}`, + }) for (const key of safeKeys) { await removeVar(namespace, key) } diff --git a/onenv-manager/src/lib/permission.test.ts b/onenv-manager/src/lib/permission.test.ts new file mode 100644 index 0000000..816e993 --- /dev/null +++ b/onenv-manager/src/lib/permission.test.ts @@ -0,0 +1,36 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { CliError } from './errors.js' +import { confirmMutation } from './permission.js' + +const originalFlag = process.env.ONENV_CONFIRM_MUTATIONS + +describe('confirmMutation', () => { + beforeEach(() => { + delete process.env.ONENV_CONFIRM_MUTATIONS + }) + + afterEach(() => { + if (originalFlag === undefined) { + delete process.env.ONENV_CONFIRM_MUTATIONS + } else { + process.env.ONENV_CONFIRM_MUTATIONS = originalFlag + } + }) + + it('is a no-op when ONENV_CONFIRM_MUTATIONS is unset', async () => { + await expect(confirmMutation({ action: 'set', namespace: 'demo' })).resolves.toBeUndefined() + }) + + it('is a no-op when ONENV_CONFIRM_MUTATIONS is set to anything other than "1"', async () => { + process.env.ONENV_CONFIRM_MUTATIONS = 'true' + await expect(confirmMutation({ action: 'edit', namespace: 'demo' })).resolves.toBeUndefined() + }) + + it('throws PERMISSION_UNSUPPORTED on non-darwin when enabled', async () => { + if (process.platform === 'darwin') return + process.env.ONENV_CONFIRM_MUTATIONS = '1' + await expect(confirmMutation({ action: 'unset', namespace: 'demo' })).rejects.toBeInstanceOf( + CliError, + ) + }) +}) diff --git a/onenv-manager/src/lib/permission.ts b/onenv-manager/src/lib/permission.ts new file mode 100644 index 0000000..1122859 --- /dev/null +++ b/onenv-manager/src/lib/permission.ts @@ -0,0 +1,73 @@ +import { execFile } from 'node:child_process' +import { CliError } from './errors.js' + +export type MutationAction = 'set' | 'edit' | 'unset' | 'import' + +export interface ConfirmOptions { + action: MutationAction + namespace: string + details?: string +} + +const DEFAULT_TIMEOUT_MS = 120_000 + +export async function confirmMutation(opts: ConfirmOptions): Promise { + if (process.env.ONENV_CONFIRM_MUTATIONS !== '1') return + if (process.platform !== 'darwin') { + throw new CliError( + 'PERMISSION_UNSUPPORTED', + 'ONENV_CONFIRM_MUTATIONS is set but only macOS is supported', + 'user_error', + 'Unset ONENV_CONFIRM_MUTATIONS or run on macOS', + ) + } + const allowed = await showDialog(opts, parseTimeoutMs()) + if (!allowed) { + throw new CliError( + 'PERMISSION_DENIED', + `Permission denied for ${opts.action} on ${opts.namespace}`, + 'user_error', + 'Click Allow in the system dialog, or unset ONENV_CONFIRM_MUTATIONS to skip the prompt', + ) + } +} + +function parseTimeoutMs(): number { + const raw = process.env.PERMISSION_TIMEOUT_MS + if (!raw) return DEFAULT_TIMEOUT_MS + const value = Number(raw) + return Number.isInteger(value) && value > 0 ? value : DEFAULT_TIMEOUT_MS +} + +async function showDialog(opts: ConfirmOptions, timeoutMs: number): Promise { + const timeoutSeconds = Math.max(10, Math.floor(timeoutMs / 1000)) + const summary = [ + `Action: ${opts.action}`, + `Namespace: ${opts.namespace}`, + opts.details ? `Details: ${opts.details}` : null, + 'Allow this operation?', + ] + .filter(Boolean) + .join('\\n') + + const script = `display dialog ${JSON.stringify(summary)} buttons {"Deny", "Allow"} default button "Deny" with title "onenv" giving up after ${timeoutSeconds}` + + try { + const { stdout } = await execFileAsync('osascript', ['-e', script]) + return stdout.includes('button returned:Allow') && !stdout.includes('gave up:true') + } catch { + return false + } +} + +function execFileAsync(file: string, args: string[]): Promise<{ stdout: string; stderr: string }> { + return new Promise((resolve, reject) => { + execFile(file, args, (error, stdout, stderr) => { + if (error) { + reject(new Error(stderr || error.message)) + return + } + resolve({ stdout, stderr }) + }) + }) +}