fix(cms-poc): remediate findings surfaced by the CMS proof-of-concept

Additive fixes surfaced by building a full CMS on PiCloud
(examples/cms-poc/). One migration (0077_apps_cors.sql); no destructive
changes.

Security:
- Close the 502 info-leak on user routes: an uncaught script error (incl.
  a throw) leaked the app UUID + script fn names + source line/col. The
  user-route (inbox) path now shares scrub_runtime_detail with the
  execute-by-id path — raw detail is logged under a correlation id and the
  client sees only "script execution error (ref: <uuid>)".

Added:
- docs::find $contains operator (array membership via JSONB @>), per-app
  and group-shared — the inverse of $in.
- App-user role management from API/CLI: pic users add-role / rm-role,
  backed by the apps/{id}/users/{user_id}/roles endpoints (AppUsersAdmin).
- Per-app CORS: pic apps cors set, apps.cors_allowed_origins; the
  orchestrator echoes an allowed Origin and answers OPTIONS preflight.
- Non-JSON request bodies: form-urlencoded -> object, text/* -> string,
  other -> base64; ctx.request.content_type added. Malformed JSON still 400s.
- Binary responses via #{ body_base64 } with a script-set Content-Type.
- workflow::run_status(run_id) SDK (F-038): the starting script can poll a
  run — #{ status, output, error, steps } or () when no such run belongs to
  the app (app_id is the isolation boundary); same AppInvoke gate as start.

Fixed:
- pic apply "app not found" is now actionable (points at pic apps create).
- Interceptor- (F-021) and workflow-step-bound (F-037) scripts no longer
  warn "no route or trigger" at plan time — both count as reachability.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-19 20:15:17 +02:00
parent dbdd398d90
commit 5682be366c
32 changed files with 1361 additions and 92 deletions

View File

@@ -1,5 +1,62 @@
# PiCloud Changelog
## Unreleased — CMS proof-of-concept remediation
Fixes surfaced by building a full CMS on PiCloud (`examples/cms-poc/`). Pure
additive surface + one migration (`0077_apps_cors.sql`), no destructive changes.
### Security
- **502 info-leak closed on user routes.** An uncaught script error (incl. a
`throw`) on a user route returned HTTP 502 whose body embedded the app UUID,
script function names, and source line/col. The 2026-06-11 audit scrub had
only covered the execute-by-id path; the user-route (inbox) path still leaked.
Both paths now share `scrub_runtime_detail` — the raw detail is logged
server-side under a correlation id and the client sees only
`script execution error (ref: <uuid>)`.
### Added
- **`docs::find` `$contains` operator** — array-membership via JSONB `@>`, e.g.
`docs::find("posts", #{ tags: #{ "$contains": "rust" } })` finds every post
tagged `rust`. The inverse of `$in`. Covers per-app and group-shared docs.
- **App-user role management from the API/CLI** — `pic users add-role --app <app>
<user_id> <role>` / `rm-role`, backed by `POST`/`DELETE
/api/v1/admin/apps/{id}/users/{user_id}/roles[/{role}]` (`AppUsersAdmin`).
Role assignment was previously reachable only from a Rhai script.
- **Per-app CORS** — `pic apps cors set <app> <origins…>` (or `*`), stored on
`apps.cors_allowed_origins`. The orchestrator echoes an allowed `Origin` on
every response and answers the `OPTIONS` preflight. Empty = off (unchanged).
- **Non-JSON request bodies** — a user route no longer 400s on a non-JSON body.
`application/x-www-form-urlencoded` → `ctx.request.body` object, `text/*` →
string, other types → base64 string; `ctx.request.content_type` added as a
convenience. JSON (or unspecified) is unchanged; malformed JSON still 400s.
- **Binary responses** — a script may return `#{ statusCode, headers,
body_base64 }`; the platform decodes the base64 and returns raw bytes with the
script-set `Content-Type` (default `application/octet-stream`).
- **`workflow::run_status(run_id)` SDK** (F-038) — the script that started a run
can now poll it: returns `#{ status, output, error, steps: #{ name: status } }`
or `()` when no such run belongs to the app. Read-only, same-app scoped (the
run's `app_id` is the isolation boundary), same `AppInvoke` gate as
`workflow::start`. Closes the gap where run status was platform-admin-API only.
### Fixed
- **`pic apply` "app not found" is now actionable** — points at
`pic apps create <slug>` (apply reconciles groups but never creates an app).
- **Interceptor- and workflow-step-bound scripts no longer warned "no route or
trigger"** at plan time — an `[[interceptors]]` binding (F-021) *and* a
`[[workflows]]` step `function` (F-037) now count as reachability.
### Notes
- **Workflow `skipped` ≠ `failed` (F-039, by design).** A step whose `when`
evaluates false is `skipped` and counts as *satisfied-but-empty* for its
dependents — so a step downstream of a skipped one still runs. To gate a
dependent on an upstream actually having run, give it its own `when`
(e.g. `when = "steps.publish.output.published == true"`), or have the upstream
step return an explicit outcome the dependent checks.
## v1.1.9 — Durable Queues & Function Composition (unreleased)
Per-app durable queues + same-app function composition + caller-