diff --git a/CHANGELOG.md b/CHANGELOG.md index aafb300..490873c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,13 +7,13 @@ All notable changes to this project will be documented in this file. Format foll ### Added - (Reserved for next-cycle changes.) -## [2.3.0] - 2026-06-19 +## [2.3.0-rc1] - 2026-06-22 Surfaces the known scope tree through a sanctioned interface so external consumers (the upcoming `luci-app-uapi` LuCI frontend, fleet inventory tools, anything that wants to render a scope picker) can enumerate valid scopes without parsing `src/lib/scope.uc` or hardcoding a copy. Closes [openwrt-iac/uapi#5](https://github.com/openwrt-iac/uapi/issues/5). Also plumbs per-token `rate` / `burst` overrides through the mint surfaces, closing a "planned for v2.x" gap that has been carried in `docs/tokens.md` since 2.0. ### Added -- Commit-confirmed apply (safe apply). A config write can arm a rollback deadline via `?confirm=` (or the `X-Uapi-Confirm` header behind a proxy that forwards it; uhttpd's CGI strips custom headers, so the query form is the portable interface): uapi snapshots the affected uci packages, commits, and returns `202 Accepted` with a `confirm` token; unless the client acks via `POST /confirm/` before the deadline, the pre-change snapshot is restored automatically (surviving reboot / process kill / a dead management path). The ack is client-driven, so a network blip that hides the response also prevents the ack, keeping the auto-revert and the client's view consistent. New endpoints `GET /confirm`, `GET|POST|DELETE /confirm/` and scope `uapi:confirm` (`:ro` for status/list, `:rw` for ack/rollback; arming needs no extra scope). The rollback timer and durable state live in a separate package, `apply-confirm` (uapi invokes its CLI, runs no daemon of its own); the integration is optional and feature-detected, returning `501 confirm_unavailable` when apply-confirm is not installed. See `docs/commit-confirm.md`. (The 2.3.0 tag is held until apply-confirm reaches a stable feed release.) +- Commit-confirmed apply (safe apply). A config write can arm a rollback deadline via `?confirm=` (or the `X-Uapi-Confirm` header behind a proxy that forwards it; uhttpd's CGI strips custom headers, so the query form is the portable interface): uapi snapshots the affected uci packages, commits, and returns `202 Accepted` with a `confirm` token; unless the client acks via `POST /confirm/` before the deadline, the pre-change snapshot is restored automatically (surviving reboot / process kill / a dead management path). The ack is client-driven, so a network blip that hides the response also prevents the ack, keeping the auto-revert and the client's view consistent. New endpoints `GET /confirm`, `GET|POST|DELETE /confirm/` and scope `uapi:confirm` (`:ro` for status/list, `:rw` for ack/rollback; arming needs no extra scope). The rollback timer and durable state live in a separate package, `apply-confirm` (uapi invokes its CLI, runs no daemon of its own); the integration is optional and feature-detected, returning `501 confirm_unavailable` when apply-confirm is not installed. See `docs/commit-confirm.md`. (This RC ships to GitHub Releases only; the stable 2.3.0 tag and apk-feed publication wait until apply-confirm reaches a stable feed release.) - New CLI subcommand `uapi-token scopes` printing one scope path per line (sorted, greppable). Pair with `--json` for a JSON array suitable for piping into `jq` or any other consumer. The CLI is the durable cross-package interface; it works from any shell, Ansible playbook, or fleet inventory tool that can `ssh` to the router. diff --git a/VERSION b/VERSION index 276cbf9..d62e3a5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.3.0 +2.3.0-rc1 diff --git a/build/openapi.json b/build/openapi.json index 9097402..e25a3e3 100644 --- a/build/openapi.json +++ b/build/openapi.json @@ -2,7 +2,7 @@ "openapi": "3.1.0", "info": { "title": "uapi", - "version": "2.3.0", + "version": "2.3.0-rc1", "description": "Native HTTP REST API for OpenWrt. Translates standard REST verbs into ubus/uci operations so edge routers become first-class targets for Infrastructure-as-Code workflows.\n\n## Quickstart\n\nMint a token on the router (one-time):\n\n```sh\nuapi-token create --name terraform_prod --scope '*:rw' --expires-in 90d\n```\n\nThen call the API:\n\n```sh\ncurl -H \"Authorization: Bearer $TOKEN\" https://router/api/v2/firewall/rules\n```\n\n## Two surfaces\n\n- **Curated resources** under `/api/v2//...` - hand-written schemas, stable across the major. Field names are `snake_case`; uci booleans normalize to JSON booleans; uci list options surface as JSON arrays.\n- **Raw passthrough** under `/api/v2/raw//` - generic uci access for the long tail. Same atomic-transaction recipe and same auth model, but payloads follow uci's field names directly (and move when upstream OpenWrt does).\n\n## Resource shape\n\nEvery curated resource carries `id` (stable across uci rewrites) and `managed: bool` at the top level. Server-derived state lives under `runtime: {...}` (computed; clients ignore for drift detection).\n\n## Auth\n\nBearer tokens with hierarchical scopes (e.g. `firewall:rules:rw`, `*:ro`). See the **Auth / Tokens** group for mint/list/revoke and the `/auth/whoami` endpoint for introspection.\n\n## Optimistic concurrency\n\nEvery resource GET and write returns an `ETag` header that is a stable hash of the resource's own body (the `runtime` block is excluded so live ubus state never trips a 412). Honor with `If-Match` on writes (or `?if_match=` query param for clients behind uhttpd's strict CGI env, which drops the header). Conditional GET via `If-None-Match` returns 304 when matching. Sibling sections in the same package do not influence each other's ETags; If-Match fires only when *this* resource has actually changed.\n\n## Idempotency\n\n`Idempotency-Key` on POST caches the response for 24 h; a repeat with the same key replays. Same key with a different body returns `409 idempotency_key_conflict`.\n\n## Sensitive fields (write-only + `has_` presence flag)\n\nFields holding secret material (passphrases, private keys, PSKs, PKCS#12 paths) are write-only on the wire: GET responses omit the value and surface a read-only `has_: bool` companion indicating presence. Examples: `wireless.interfaces.key`/`has_key`, `network.wireguard_peers.private_key`/`has_private_key`, `network.wireguard_peers.preshared_key`/`has_preshared_key`, `openvpn.instances.key`/`has_key`, `openvpn.instances.tls_auth`/`has_tls_auth`, `openvpn.instances.pkcs12`/`has_pkcs12`. PATCH that omits a sensitive field carries the existing value forward; rotation is explicit.\n\n## Atomicity\n\nEvery write is one transaction: snapshot, validate, commit, reload, restore-on-failure. `POST /batch` extends this across N packages under one combined snapshot/restore.\n\n## IMPORTANT - Success != runtime convergence\n\nA 2xx response means the init script's reload action **exited 0**. It does NOT mean the daemon has finished re-converging (`network/interfaces` is the dangerous one: a bad change can drop the management link, and the API has already reported success). The `X-Reload-Status` response header surfaces the reload outcome explicitly:\n\n- `X-Reload-Status: ok` - init script ran and exited 0 (not a convergence promise)\n- `X-Reload-Status: no_reload` - the resource has no reload services\n\nFor high-stakes writes (management interface, firewall defaults, uhttpd itself) verify convergence out-of-band. See [`docs/operations.md`](https://github.com/raspbeguy/uapi/blob/main/docs/operations.md) `Success != converged` for the full contract.\n\n## Compatibility & versioning\n\nA given uapi installation serves exactly one API major. Within a major, additions are backwards-compatible: new endpoints, new optional fields, new error codes, new scopes. Breaking changes require the next major. Operators who need an older major keep that package version installed.\n\n## Schema annotations\n\nProperty schemas under `components.schemas.*.properties` carry two annotations beyond the standard OpenAPI shape:\n\n- **`default`**: the value uapi's `fromUci` synthesizes when the underlying uci option is absent. Standard OpenAPI 3.1 / JSON Schema 2020-12 keyword. The framework does NOT apply this default to incoming requests; it is documentation of the server-side fallback so IaC clients can keep the field sticky (Optional+Computed) instead of mistakenly treating it as caller-owned.\n- **`x-uapi-clear-on-omit`** (vendor extension, boolean): when present and `true`, the field is caller-owned and an IaC client (e.g. the terraform-provider-uapi) can safely send an explicit JSON null on `PUT`/`PATCH` to clear the underlying uci option. Absence of this flag means the field should be treated as sticky. A field with `default:` MUST NOT carry this flag, and vice versa (the framework's `lint-defaults` enforces this).\n\n## More\n\n- **GitHub:** https://github.com/raspbeguy/uapi\n- **Terraform provider:** https://registry.terraform.io/providers/raspbeguy/uapi\n- **APK feed install:** [/install/](../install/)\n- **Architecture, security, migration, release-process docs:** [in repo](https://github.com/raspbeguy/uapi/tree/main/docs)", "contact": { "name": "uapi",