Files
PiCloud/examples/cms-poc/picloud.toml
MechaCat02 813bc46640
Some checks failed
CI / Rust — fmt, clippy, test (push) Failing after 39m53s
CI / Dashboard — check (push) Successful in 10m15s
docs(examples): add the CMS proof-of-concept dogfooding artifact
A WordPress-inspired CMS whose entire backend is PiCloud Rhai scripts
deployed with pic apply, plus a SvelteKit frontend. Built to dogfood the
project tool, CLI, and SDK; exercises docs/kv/files/users, docs+queue+cron
+pubsub triggers, the transactional-outbox notification chain, a docs
before-interceptor, set_if CAS, SSE, per-app CORS, env overlays, and a
durable Workflow (validate -> enrich || seo -> publish -> finalize) started
with workflow::start and polled with workflow::run_status, visualized live
with Svelte Flow.

FINDINGS.md / SECURITY.md capture every gap, surprise, and security issue
found (each tagged [PiCloud] vs [CMS]); the platform fixes for the
actionable ones ship in the preceding commit.

Also folds in the read-only `pic` allowlist entries added to
.claude/settings.json during the session (fewer-permission-prompts).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 20:15:29 +02:00

527 lines
12 KiB
TOML

## PiCloud CMS PoC — declarative manifest.
## Deploy: pic apply --env dev (from this directory, after `pic login`).
## Everything (scripts, routes, triggers, interceptors, config) lives here.
[project]
slug = "cms"
name = "PiCloud CMS PoC"
[app]
slug = "cms"
name = "PiCloud CMS"
description = "A WordPress-inspired CMS whose backend is 100% PiCloud Rhai scripts."
# ------------------------------------------------------------------ config
# NOTE: env-specific values (e.g. site_base_url) live in the overlay files
# picloud.dev.toml / picloud.prod.toml, NOT in inline [vars.<env>] sections
# (an inline [vars.dev] here would parse as a var literally NAMED "dev"). See
# FINDINGS F-005.
# NOTE: var KEYS must be kebab-case ([a-z0-9-], no underscores) — see FINDINGS F-008.
[vars]
site-title = "My PiCloud Blog"
posts-per-page = "10"
comment-moderation = "manual" # "manual" | "auto"
notify-from-email = "blog@example.com"
signups-open = "true"
# ------------------------------------------------------------------ secrets
# Values are pushed out-of-band via `pic secrets set <name>` (never in the
# manifest). `setup-token` gates the one-time admin bootstrap endpoint.
[secrets]
names = ["setup-token"]
# ------------------------------------------------------------------ scripts
[[scripts]]
name = "hello"
file = "scripts/hello.rhai"
kind = "endpoint"
description = "Skeleton smoke-test endpoint."
# --- auth (shared module + endpoints) ---
[[scripts]]
name = "auth"
file = "scripts/auth/auth.rhai"
kind = "module"
description = "Role-check helper module — the CMS access boundary."
[[scripts]]
name = "auth_register"
file = "scripts/auth/register.rhai"
description = "Reader self-signup."
[[scripts]]
name = "auth_login"
file = "scripts/auth/login.rhai"
description = "Email+password -> session token."
[[scripts]]
name = "auth_logout"
file = "scripts/auth/logout.rhai"
description = "Invalidate the current session."
[[scripts]]
name = "auth_me"
file = "scripts/auth/me.rhai"
description = "Current user + roles."
[[scripts]]
name = "auth_bootstrap"
file = "scripts/auth/bootstrap.rhai"
description = "One-time first-admin creation (setup-token gated)."
[[scripts]]
name = "admin_users_list"
file = "scripts/auth/admin_users_list.rhai"
description = "List app-users + roles (admin)."
[[scripts]]
name = "admin_user_roles"
file = "scripts/auth/admin_user_roles.rhai"
description = "Grant/revoke user roles (admin)."
# --- shared util module ---
[[scripts]]
name = "util"
file = "scripts/util.rhai"
kind = "module"
description = "slugify / doc-shaping / CAS counter helpers."
# --- posts ---
[[scripts]]
name = "posts_list"
file = "scripts/posts/posts_list.rhai"
description = "Public list of published posts."
[[scripts]]
name = "post_get"
file = "scripts/posts/post_get.rhai"
description = "Public single post by slug (+ view counter)."
[[scripts]]
name = "posts_admin_list"
file = "scripts/posts/posts_admin_list.rhai"
description = "Dashboard post list (author+)."
[[scripts]]
name = "post_create"
file = "scripts/posts/post_create.rhai"
description = "Create a post (author+)."
[[scripts]]
name = "post_update"
file = "scripts/posts/post_update.rhai"
description = "Update a post (own-only unless admin)."
[[scripts]]
name = "post_delete"
file = "scripts/posts/post_delete.rhai"
description = "Delete a post (admin)."
# --- pages (one script, method-dispatched) ---
[[scripts]]
name = "pages"
file = "scripts/pages/pages.rhai"
description = "Page CRUD (public reads + admin writes)."
# --- comments ---
[[scripts]]
name = "comment_sanitize"
file = "scripts/comments/comment_sanitize.rhai"
description = "docs before-interceptor: escape HTML + set moderation status."
[[scripts]]
name = "comment_create"
file = "scripts/comments/comment_create.rhai"
description = "Submit a comment (public, rate-limited)."
[[scripts]]
name = "comments_for_post"
file = "scripts/comments/comments_for_post.rhai"
description = "Public approved comments for a post."
[[scripts]]
name = "comments_admin"
file = "scripts/comments/comments_admin.rhai"
description = "Comment moderation (admin)."
[[scripts]]
name = "comment_on_approve"
file = "scripts/comments/comment_on_approve.rhai"
description = "docs trigger: publish approved comment to SSE feed."
# --- notifications ---
[[scripts]]
name = "posts_on_publish"
file = "scripts/posts/posts_on_publish.rhai"
description = "docs trigger: enqueue reader notifications on publish."
[[scripts]]
name = "notify_drain"
file = "scripts/notify/notify_drain.rhai"
description = "queue trigger: send notification emails."
[[scripts]]
name = "posts_scheduled_publish"
file = "scripts/posts/posts_scheduled_publish.rhai"
description = "cron trigger: publish due scheduled posts."
# --- tags ---
[[scripts]]
name = "tags_list"
file = "scripts/tags/tags_list.rhai"
description = "Public tag cloud (distinct tags + counts)."
# --- media ---
[[scripts]]
name = "media_upload"
file = "scripts/media/media_upload.rhai"
description = "Upload an image (author+, base64)."
[[scripts]]
name = "media_get"
file = "scripts/media/media_get.rhai"
description = "Fetch an image as base64 (public)."
# --- workflow: editorial publish pipeline ---
[[scripts]]
name = "wf_validate"
file = "scripts/workflow/wf_validate.rhai"
description = "Workflow step: validate content."
[[scripts]]
name = "wf_enrich"
file = "scripts/workflow/wf_enrich.rhai"
description = "Workflow step: auto-excerpt + reading time."
[[scripts]]
name = "wf_seo"
file = "scripts/workflow/wf_seo.rhai"
description = "Workflow step: SEO scoring."
[[scripts]]
name = "wf_publish"
file = "scripts/workflow/wf_publish.rhai"
description = "Workflow step: publish (chains into notifications)."
[[scripts]]
name = "wf_finalize"
file = "scripts/workflow/wf_finalize.rhai"
description = "Workflow step: finalize run."
[[scripts]]
name = "pipeline_start"
file = "scripts/workflow/pipeline_start.rhai"
description = "Start the editorial pipeline on a post (author+)."
[[scripts]]
name = "pipeline_dag"
file = "scripts/workflow/pipeline_dag.rhai"
description = "Return the pipeline DAG for the visual editor."
[[scripts]]
name = "pipeline_run"
file = "scripts/workflow/pipeline_run.rhai"
description = "Return a run's live per-step status."
# ------------------------------------------------------------------ routes
[[routes]]
script = "hello"
method = "GET"
host_kind = "any"
path_kind = "exact"
path = "/cms/hello"
dispatch_mode = "sync"
[[routes]]
script = "auth_register"
method = "POST"
host_kind = "any"
path_kind = "exact"
path = "/cms/auth/register"
[[routes]]
script = "auth_login"
method = "POST"
host_kind = "any"
path_kind = "exact"
path = "/cms/auth/login"
[[routes]]
script = "auth_logout"
method = "POST"
host_kind = "any"
path_kind = "exact"
path = "/cms/auth/logout"
[[routes]]
script = "auth_me"
method = "GET"
host_kind = "any"
path_kind = "exact"
path = "/cms/auth/me"
[[routes]]
script = "auth_bootstrap"
method = "POST"
host_kind = "any"
path_kind = "exact"
path = "/cms/auth/bootstrap"
# --- admin user management ---
[[routes]]
script = "admin_users_list"
method = "GET"
host_kind = "any"
path_kind = "exact"
path = "/cms/admin/users"
[[routes]]
script = "admin_user_roles"
method = "PUT"
host_kind = "any"
path_kind = "param"
path = "/cms/admin/users/:id/roles"
# --- posts (public) ---
[[routes]]
script = "posts_list"
method = "GET"
host_kind = "any"
path_kind = "exact"
path = "/cms/posts"
[[routes]]
script = "post_get"
method = "GET"
host_kind = "any"
path_kind = "param"
path = "/cms/posts/:slug"
# --- posts (admin) ---
[[routes]]
script = "posts_admin_list"
method = "GET"
host_kind = "any"
path_kind = "exact"
path = "/cms/admin/posts"
[[routes]]
script = "post_create"
method = "POST"
host_kind = "any"
path_kind = "exact"
path = "/cms/admin/posts"
[[routes]]
script = "post_update"
method = "PUT"
host_kind = "any"
path_kind = "param"
path = "/cms/admin/posts/:id"
[[routes]]
script = "post_delete"
method = "DELETE"
host_kind = "any"
path_kind = "param"
path = "/cms/admin/posts/:id"
# --- pages ---
[[routes]]
script = "pages"
method = "GET"
host_kind = "any"
path_kind = "exact"
path = "/cms/pages"
[[routes]]
script = "pages"
method = "GET"
host_kind = "any"
path_kind = "param"
path = "/cms/pages/:slug"
[[routes]]
script = "pages"
method = "GET"
host_kind = "any"
path_kind = "exact"
path = "/cms/admin/pages"
[[routes]]
script = "pages"
method = "POST"
host_kind = "any"
path_kind = "exact"
path = "/cms/admin/pages"
[[routes]]
script = "pages"
method = "PUT"
host_kind = "any"
path_kind = "param"
path = "/cms/admin/pages/:id"
[[routes]]
script = "pages"
method = "DELETE"
host_kind = "any"
path_kind = "param"
path = "/cms/admin/pages/:id"
# --- comments ---
[[routes]]
script = "comment_create"
method = "POST"
host_kind = "any"
path_kind = "param"
path = "/cms/posts/:id/comments"
[[routes]]
script = "comments_for_post"
method = "GET"
host_kind = "any"
path_kind = "param"
path = "/cms/posts/:id/comments"
[[routes]]
script = "comments_admin"
method = "GET"
host_kind = "any"
path_kind = "exact"
path = "/cms/admin/comments"
[[routes]]
script = "comments_admin"
method = "PUT"
host_kind = "any"
path_kind = "param"
path = "/cms/admin/comments/:id"
[[routes]]
script = "comments_admin"
method = "DELETE"
host_kind = "any"
path_kind = "param"
path = "/cms/admin/comments/:id"
# --- tags ---
[[routes]]
script = "tags_list"
method = "GET"
host_kind = "any"
path_kind = "exact"
path = "/cms/tags"
# --- media ---
[[routes]]
script = "media_upload"
method = "POST"
host_kind = "any"
path_kind = "exact"
path = "/cms/admin/media"
[[routes]]
script = "media_get"
method = "GET"
host_kind = "any"
path_kind = "param"
path = "/cms/media/:id"
# --- workflow ---
[[routes]]
script = "pipeline_start"
method = "POST"
host_kind = "any"
path_kind = "param"
path = "/cms/admin/posts/:id/pipeline"
[[routes]]
script = "pipeline_dag"
method = "GET"
host_kind = "any"
path_kind = "exact"
path = "/cms/admin/workflow"
[[routes]]
script = "pipeline_run"
method = "GET"
host_kind = "any"
path_kind = "param"
path = "/cms/admin/workflow/runs/:id"
# ------------------------------------------------------------------ triggers
# docs trigger: fire posts_on_publish when a post is created/updated.
[[triggers.docs]]
script = "posts_on_publish"
collection_glob = "posts"
ops = ["create", "update"]
retry_max_attempts = 3
# docs trigger: publish an approved comment to the SSE feed.
[[triggers.docs]]
script = "comment_on_approve"
collection_glob = "comments"
ops = ["update"]
retry_max_attempts = 2
# queue trigger: drain post-notifications, send emails.
[[triggers.queue]]
script = "notify_drain"
queue_name = "post-notifications"
retry_max_attempts = 5
# cron trigger: publish due scheduled posts every minute (6-field: sec min hr ...).
[[triggers.cron]]
script = "posts_scheduled_publish"
schedule = "0 * * * * *"
timezone = "UTC"
# ------------------------------------------------------------------ interceptors
[[interceptors]]
script = "comment_sanitize"
service = "docs"
ops = ["create"]
phase = "before"
timeout_ms = 500
# ------------------------------------------------------------------ workflows
# Editorial publish pipeline (durable DAG). validate → (enrich ∥ seo) → publish
# → finalize. Each step reads post_id from its input mapping; `when` gates the
# branch on validation passing. Per-step progress is read back via the platform
# `workflow::run_status` SDK (F-038) — no app-side run tracking needed.
[[workflows]]
name = "editorial_pipeline"
[[workflows.steps]]
name = "validate"
function = "wf_validate"
input = { post_id = "{{ input.post_id }}" }
[[workflows.steps]]
name = "enrich"
function = "wf_enrich"
depends_on = ["validate"]
when = "steps.validate.output.ok == true"
input = { post_id = "{{ input.post_id }}" }
[[workflows.steps]]
name = "seo"
function = "wf_seo"
depends_on = ["validate"]
when = "steps.validate.output.ok == true"
input = { post_id = "{{ input.post_id }}" }
[[workflows.steps]]
name = "publish"
function = "wf_publish"
depends_on = ["enrich", "seo"]
when = "steps.validate.output.ok == true"
input = { post_id = "{{ input.post_id }}" }
[[workflows.steps]]
name = "finalize"
function = "wf_finalize"
depends_on = ["publish"]
input = { post_id = "{{ input.post_id }}" }