docs: stdlib envelope/replace notes + dev-insecure-key (S2, S3, O1)
- S2: document that the response envelope is statusCode-gated — a returned
map is only unwrapped into {statusCode, headers, body} when it contains
statusCode, else the whole map silently becomes the 200 body.
- S3: note that Rhai String.replace() mutates in place and returns (),
with the sub_string bearer-parsing idiom.
- Document ctx.request fields including the new `method`.
- O1: document PICLOUD_DEV_INSECURE_KEY next to PICLOUD_DEV_MODE in
CLAUDE.md (the acknowledgement var was previously only in the startup
error).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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`. |
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user