diff --git a/CLAUDE.md b/CLAUDE.md index 1163224..b2e6e25 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -116,6 +116,9 @@ Environment variables consumed by the `picloud` binary: | `PICLOUD_BIND` | `0.0.0.0:8080` | HTTP listen address. Port 8080 is owned by another process on this host — override locally. | | `PICLOUD_MAX_CONCURRENT_EXECUTIONS` | `32` | Global concurrency cap on data-plane script executions. Overflow returns HTTP 503 with `Retry-After: 1` immediately (no queue). | | `DATABASE_URL` | — | Required. Postgres connection string. | +| `PICLOUD_SECRET_KEY` | — | Master encryption key (base64). Required at startup unless dev mode is acknowledged (below). | +| `PICLOUD_DEV_MODE` | `false` | `true` enables local-dev conveniences. Without `PICLOUD_SECRET_KEY` it ALSO requires the acknowledgement var below — `PICLOUD_DEV_MODE=true` alone aborts at startup. | +| `PICLOUD_DEV_INSECURE_KEY` | — | Set to the literal `i-understand-this-is-insecure` to let dev mode boot without `PICLOUD_SECRET_KEY`, using a deterministic, world-known dev master key. Never set in production — it would encrypt everything with a public value. | | `PICLOUD_DB_MAX_CONNECTIONS` | `32` | Postgres pool size. Matched to `PICLOUD_MAX_CONCURRENT_EXECUTIONS` so the data plane can't starve background workers. | | `PICLOUD_SESSION_TTL_HOURS` | `24` | Sliding-window session lifetime. | | `PICLOUD_SANDBOX_MAX_*` | conservative defaults | Per-knob admin ceilings on Rhai sandbox overrides. See `manager-core::sandbox::SandboxCeiling`. | diff --git a/docs/stdlib-reference.md b/docs/stdlib-reference.md index ce371dc..ec3ba0c 100644 --- a/docs/stdlib-reference.md +++ b/docs/stdlib-reference.md @@ -37,6 +37,17 @@ These come with the Rhai engine itself. See the `index_of`, `split`, `trim`, `to_lower`, `to_upper`, `replace`, `chars`, `pad`, `sub_string`, `crop`, `+` (concatenation). +> **Footgun — `replace` mutates in place and returns `()`.** Rhai's +> `String.replace(from, to)` edits the receiver and returns unit, *not* +> a new string. `let t = auth.replace("Bearer ", "")` sets `t` to `()`, +> so a downstream `users::verify(t)` fails with +> `Function not found: users::verify (())`. To strip a known prefix, +> slice instead: +> ```rhai +> let token = if auth.starts_with("Bearer ") { auth.sub_string(7) } else { auth }; +> ``` +> Or call `replace` purely for its side effect: `let t = auth; t.replace("Bearer ", "");`. + **Array:** `push`, `pop`, `shift`, `insert`, `remove`, `len`, `clear`, `truncate`, `extend`, `filter`, `map`, `reduce`, `reduce_rev`, `find`, `find_map`, `any`, `all`, `index_of`, `contains`, `sort`, `reverse`, @@ -203,6 +214,47 @@ let qs = url::encode_query(#{ q: "rust regex", page: 2 }); --- +## Request context (`ctx`) and the response envelope + +Every script sees a `ctx` map describing the invocation. The request +fields under `ctx.request` are: + +| Field | Type | Notes | +|---|---|---| +| `ctx.request.path` | `String` | Request path. | +| `ctx.request.method` | `String` | Uppercased HTTP verb (`"GET"`, `"POST"`, …). Empty for non-HTTP triggers. Branch on it to serve several verbs from one script. | +| `ctx.request.headers` | `Map` | Lowercased header names → values. | +| `ctx.request.body` | parsed | Already-parsed JSON when `Content-Type: application/json`; otherwise a string. | +| `ctx.request.params` | `Map` | Captures from `:name` route segments. | +| `ctx.request.query` | `Map` | Query-string parameters. | +| `ctx.request.rest` | `String` | Suffix captured by a `prefix` (`/*`) route. | + +```rhai +// One script, many verbs: +let m = ctx.request.method; +if m == "GET" { /* list */ } +else if m == "POST" { /* create */ } +else { return #{ statusCode: 405, body: #{ error: "method not allowed" } }; } +``` + +### The response envelope is `statusCode`-gated + +A returned map is unwrapped into `{statusCode, headers, body}` **only when +it contains a `statusCode` key**. Without `statusCode`, the *entire* +returned map becomes the response body, with an implicit `200`: + +```rhai +// WRONG — no statusCode, so the whole map is the body: +#{ body: #{ token: t } } // → HTTP 200 {"body":{"token":"…"}} (double-nested!) + +// RIGHT — statusCode present, so it's treated as the envelope: +#{ statusCode: 200, body: #{ token: t } } // → HTTP 200 {"token":"…"} +``` + +Always include `statusCode` when you mean to set headers or a specific +body shape. Returning a bare value (`42`, `#{ ok: true }`) is fine — it +becomes a `200` body as-is. + ## What's not here - **Crypto** (sha256/hmac/argon2/encryption) — deferred to a focused