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:
57
CHANGELOG.md
57
CHANGELOG.md
@@ -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-
|
||||
|
||||
Reference in New Issue
Block a user