{
    "count": 189,
    "resources": [
        {
            "name": "active",
            "summary": "Build active class attribute for UI state",
            "advice": "Writes the whole class attribute, or nothing at all when there is nothing to say, so a link needs no ternary in the view. Because it emits the attribute rather than a class name, it goes into the tag itself and not inside another class attribute.",
            "tags": [
                "active",
                "class",
                "ui",
                "view",
                "html"
            ],
            "package": "view",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/active.phlo"
        },
        {
            "name": "age.human",
            "summary": "Convert age in seconds to human readable text",
            "advice": "The reader-facing side of age(): give it an age in seconds and it gives a label like 3 hours. Pass an age here and a timestamp to time_human; the two are easy to swap and both give an answer, only the wrong one.",
            "tags": [
                "age",
                "human",
                "time",
                "format"
            ],
            "package": "time",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/age.human.phlo"
        },
        {
            "name": "age",
            "summary": "Get age in seconds since a given timestamp",
            "advice": "Seconds since a timestamp, so an expiry or a cache check reads as a comparison. Nothing more than that: use time_human for something a reader sees.",
            "tags": [
                "age",
                "time",
                "timestamp"
            ],
            "package": "time",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/age.phlo"
        },
        {
            "name": "apcu",
            "summary": "Cache callback results in APCu",
            "advice": "Wraps a callback in APCu with one line, and the callback only runs when the value is not there. The cache lives in one server's shared memory and is gone after a restart, and a CLI run has its own unless apc.enable_cli is on, which makes it right for something expensive and reproducible and wrong for something authoritative. Key it on everything the answer depends on, or two different questions get the same answer.",
            "tags": [
                "cache",
                "apcu",
                "performance"
            ],
            "package": "cache",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/apcu.phlo"
        },
        {
            "name": "await",
            "summary": "Run app targets in parallel, via the daemon pool or one-shot CLI processes",
            "advice": "Runs several app targets at once and returns their results in the order you asked, so three slow calls take as long as the slowest instead of their sum. Under the daemon they run in the pool; without it each becomes a CLI process. The whole wait is bounded, five minutes unless await_timeout says otherwise, and a child still running is killed rather than waited for, so one hung job cannot hold a request.",
            "tags": [
                "await",
                "parallel",
                "cli",
                "process",
                "daemon"
            ],
            "package": "runtime",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/await.phlo"
        },
        {
            "name": "camel",
            "summary": "Convert text to camelCase",
            "advice": "Turns a sentence into camelCase, for making a property or key name out of a title. It splits on spaces, so a hyphen or an underscore survives into the result.",
            "tags": [
                "camelcase",
                "string",
                "format"
            ],
            "package": "string",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/camel.phlo"
        },
        {
            "name": "chunk",
            "summary": "Stream JSON chunks over CLI or Server-Sent Events",
            "advice": "Sends a piece of a response now instead of at the end, so long work reports as it goes. The first call switches the response to a stream and fixes the content type, so anything printed after it is part of that stream and a normal return can no longer be made. In the browser these arrive as commands; on the CLI they are plain lines.",
            "tags": [
                "chunk",
                "stream",
                "sse",
                "cli",
                "async"
            ],
            "package": "runtime",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/chunk.phlo"
        },
        {
            "name": "cookies",
            "summary": "Cookies data object",
            "advice": "Reads and writes a cookie as a property. Every cookie it sets is httponly, samesite Lax and secure on an https request, so a script cannot read it and it does not travel to another site. That also means a value you need in the browser does not belong here. lifetimeDays sets how long they last; unsetting the property removes the cookie on the visitor's side too.",
            "tags": [
                "cookies",
                "session",
                "browser",
                "web"
            ],
            "package": "web",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/cookies.phlo"
        },
        {
            "name": "create",
            "summary": "Create associative array from iterable using callbacks",
            "advice": "Turns a list into a keyed array in one pass: the first callback makes the key, the optional second makes the value. Keys that repeat overwrite each other, so this doubles as a way to fold a list into its unique entries.",
            "tags": [
                "create",
                "array",
                "iterable",
                "callback"
            ],
            "package": "array",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/create.phlo"
        },
        {
            "name": "exec.stream",
            "summary": "Stream shell command output via yielding",
            "advice": "Yields a shell command's output line by line, with error true on the lines that came from stderr, so a failure can be shown as it happens instead of after. It reads stdout and stderr together, which is what keeps a command that writes a lot to one of them from deadlocking. Pass a timeout for anything that could hang; zero waits forever.",
            "tags": [
                "stream",
                "shell",
                "cli",
                "process",
                "yield"
            ],
            "package": "runtime",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/exec.stream.phlo"
        },
        {
            "name": "HTTP",
            "summary": "HTTP request helper via cURL",
            "advice": "One function for a whole request: a URL and headers is a GET, and POST, PUT, PATCH, QUERY or DELETE named as an argument decides the rest. Pass an array with JSON true and it is sent as JSON. The timeout is fifteen seconds and it is a real limit, so raise it deliberately for a slow API rather than by accident. Pass response by reference to see the status and the headers, which is the only way to tell an empty answer from a failure.",
            "tags": [
                "http",
                "curl",
                "request",
                "api"
            ],
            "package": "network",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/HTTP.phlo"
        },
        {
            "name": "lang",
            "summary": "Language and translation resource",
            "advice": "%lang prints the current app language through its view, so it drops straight into a link or an attribute. Beyond that it is the translation layer behind nl() and en(): a phrase is hashed, looked up in langs/<lang>.ini, and translated by the AI in the background when it is missing, so the first visitor reads the source text and the next one reads the translation. Editing a source phrase changes its hash and orphans the old translation, so a rewrite costs a re-translation.",
            "tags": [
                "lang",
                "translation",
                "i18n",
                "locale",
                "ai"
            ],
            "package": "i18n",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/lang.phlo"
        },
        {
            "name": "lastmod",
            "summary": "Build-time stamp of when each page's source last changed, read at runtime for sitemap lastmod and for showing a date to the reader",
            "advice": "Call lastmod::stamp from a build hook, so the dates are read where the sources are. Resolving them at runtime does not work on a release node, which carries no .phlo sources, and a deploy rewrites every mtime, so every page would claim to have changed on the day it was deployed. Pages in %app->pages resolve by convention; declare prop %lastmod.sources as uri => file path for the rest. A page that resolves to nothing simply gets no lastmod, which is the right answer: a crawler that catches a site inventing them stops trusting the field domain-wide.",
            "tags": [
                "seo",
                "sitemap",
                "lastmod",
                "build",
                "stamp"
            ],
            "package": "seo",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/lastmod.phlo"
        },
        {
            "name": "manifest",
            "summary": "PWA web app manifest: declare the body, get the manifest.json route, head link and correct serving",
            "advice": "Set the body from your app (prop %manifest.body => arr(...)) and put %manifest->head in your head view, because that head view is the only thing that links the manifest from a page. %manifest itself is the manifest document, the way %seo is the sitemap. Apps with multiple manifest variants keep their own routes and serve each body through manifest::output().",
            "tags": [
                "manifest",
                "pwa",
                "webmanifest",
                "install",
                "standalone"
            ],
            "package": "web",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/manifest.phlo"
        },
        {
            "name": "manual",
            "summary": "Self-writing manual at /manual: app description, source reflection and recent commits, plus an optional AI summary",
            "advice": "Nothing to configure: include the resource and the page describes the app it runs in. It reads data/app.md, reflects the live source and reads git log, so it never goes stale. A product layer under paths.resources appears as its own section once it carries a layer.json in its repo root and a heading in its README; without that file a layer stays out of sight, which is how the framework itself stays out. The page is standalone (inline css, no javascript, no layout) and every render that changes the content is stored as data/manual.html without its session token, so the last state survives without the app. The markdown of data/app.md is rendered server-side, so the page needs no runtime and no namespace configuration. The AI summary is optional and keyed on data/app.md, so it costs nothing per visit; without the AI resource or a key the rest of the page still works. Put the route behind your auth gate and add manual to release.exclude, since a manual carrying the source does not belong on a customer server.",
            "tags": [
                "manual",
                "docs",
                "reflection",
                "ai"
            ],
            "package": "docs",
            "frontend": true,
            "backend": true,
            "file": "/phlo/resources/manual.phlo"
        },
        {
            "name": "n8n",
            "summary": "Call n8n webhook endpoint",
            "advice": "Fires an n8n workflow from your app, with test true to hit the test URL that only listens while the editor is open. It posts and returns what came back; n8n itself decides whether that is the result or just an acknowledgement, so a long workflow is better answered by a callback than by waiting here.",
            "tags": [
                "n8n",
                "webhook",
                "http",
                "automation"
            ],
            "package": "network",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/n8n.phlo"
        },
        {
            "name": "notify",
            "summary": "Notification to the central hub: POST to [notify].url (secret header) via Phlo's HTTP() function. No-op without [notify] config.",
            "advice": "One line to send a notification to the central hub, and quietly nothing at all when no hub is configured, so the same code runs on a laptop and on a server. That silence cuts both ways: a missing config gives no error, so check the hub itself when a message fails to arrive.",
            "tags": [],
            "package": "fleet",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/notify.phlo"
        },
        {
            "name": "payload",
            "summary": "POST, PUT, PATCH, QUERY and file-upload data object",
            "advice": "Everything a request carried in its body, whichever way it was sent: JSON, a form, or multipart with files, including PUT and PATCH, which PHP itself leaves for you to unpack. An uploaded file arrives as a file resource rather than a temp path. It is raw input from outside, so read it, never trust it: what you save belongs behind field validation.",
            "tags": [
                "payload",
                "request",
                "upload",
                "post",
                "put",
                "patch",
                "query"
            ],
            "package": "web",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/payload.phlo"
        },
        {
            "name": "phlo.async",
            "summary": "Run an app target in the background, via the daemon pool or a one-shot CLI process",
            "advice": "Fire and forget: it starts an app target and returns at once, so nothing that follows can see the result or whether it went well. Use it for work a visitor should not wait on, and let the job write its own outcome somewhere you can read back. Use phlo_sync when you need the answer, await when you need several.",
            "tags": [
                "async",
                "cli",
                "process",
                "background",
                "app",
                "daemon"
            ],
            "package": "runtime",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/phlo.async.phlo"
        },
        {
            "name": "phlo.exists",
            "summary": "Check if compiled Phlo class exists",
            "advice": "Asks whether a Phlo object was built, by looking for the generated PHP rather than by loading anything, so it costs nothing and triggers no autoload. Use it to make a resource optional: check first, then call. This is a build question, not a runtime one, so a resource added after the last build answers no until it is rebuilt.",
            "tags": [
                "phlo",
                "exists",
                "class",
                "build",
                "runtime"
            ],
            "package": "build",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/phlo.exists.phlo"
        },
        {
            "name": "phlo.stream",
            "summary": "Stream an app target's output line by line, via the daemon pool or a one-shot CLI process",
            "advice": "For a job whose output you want while it runs rather than at the end: it yields line by line, so a foreach can pass every line straight to chunk() and a page fills up as the work happens. The generator only advances while you read it, so a caller that stops reading stops the job.",
            "tags": [
                "stream",
                "phlo",
                "cli",
                "process",
                "yield",
                "daemon"
            ],
            "package": "runtime",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/phlo.stream.phlo"
        },
        {
            "name": "phlo.sync",
            "summary": "Run an app target synchronously, via the daemon pool or a one-shot CLI process",
            "advice": "Runs an app target and gives you its return value, in the pool under the daemon and as a CLI process without one. JSON output is decoded, anything else comes back as text, and a job that answers with an error field raises it here, so a failure surfaces in the caller rather than being swallowed.",
            "tags": [
                "sync",
                "cli",
                "process",
                "app",
                "daemon"
            ],
            "package": "runtime",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/phlo.sync.phlo"
        },
        {
            "name": "seo",
            "summary": "Multilingual SEO: sitemap.xml, robots.txt, hreflang + head meta (description/OG/Twitter/canonical)",
            "advice": "Serves sitemap.xml and robots.txt itself and writes the head meta, so a page needs no SEO markup of its own. It reads from the app: pages for the sitemap, slugs for translated URLs, description, image and structuredData for the head, so filling those is the whole job. Every language of a page points at the others with hreflang, and a lastmod resource, when there is one, dates the sitemap.",
            "tags": [
                "seo",
                "sitemap",
                "robots",
                "hreflang",
                "opengraph",
                "multilingual"
            ],
            "package": "seo",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/seo.phlo"
        },
        {
            "name": "session",
            "summary": "Session data object",
            "advice": "The session as an object: read a key, write a key, done. Call objRegenerateId() the moment someone logs in or changes role, so a token from before cannot be reused. Sessions are per browser, so anything that must survive a device belongs in a model. Override options to steer the cookie: a flow that returns to you with a cross-site POST, such as an OIDC provider posting its callback, needs cookie_samesite None with cookie_secure true, because a Lax cookie is not sent on that request and the state you stored is then unreadable.",
            "tags": [
                "session",
                "web",
                "state"
            ],
            "package": "web",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/session.phlo"
        },
        {
            "name": "settings",
            "summary": "Persistent app settings in data/settings.json: setting() lists everything, setting(key) reads one value or null, setting(key, value) writes",
            "advice": "Three shapes in one function: setting() gives everything, setting(key) reads one value or null, setting(key, value) writes and saves at once. It is meant for the handful of choices an app keeps, in data/settings.json, not for data with a life of its own; anything you would want to query or relate belongs in a model.",
            "tags": [
                "settings",
                "config",
                "storage",
                "json"
            ],
            "package": "files",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/settings.phlo"
        },
        {
            "name": "slug",
            "summary": "Convert text to URL slug",
            "advice": "Makes a URL slug: accents are folded to their ascii letter, everything else becomes a dash. Two different titles can produce the same slug, so check for a collision before you use one as an id.",
            "tags": [
                "slug",
                "url",
                "string",
                "format"
            ],
            "package": "string",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/slug.phlo"
        },
        {
            "name": "stream",
            "summary": "Raw data stream beside the JSON command channel: stream() emits text or binary chunks under any content type, app.stream() consumes them via fetch dispatching on the response type",
            "advice": "The channel beside the command stream, for what is not JSON: an image, a PDF, a zip, a file to download. Give it a content type, and a name when it should arrive as a download rather than be shown. On the page app.stream() picks its handling from the type that came back, so a caller does not have to know in advance what it is getting.",
            "tags": [
                "stream",
                "binary",
                "raw",
                "data",
                "download"
            ],
            "package": "runtime",
            "frontend": true,
            "backend": true,
            "file": "/phlo/resources/stream.phlo"
        },
        {
            "name": "tag",
            "summary": "Generate HTML tag string with attributes",
            "advice": "One function for every element: attributes are named arguments, an underscore becomes a dash, so data_id turns into data-id, and true gives a bare attribute like required. Values are escaped, and an attribute with null is left out, which is what lets an optional attribute be written without a condition around it. Pass no inner content and you get a single tag without a closing one.",
            "tags": [
                "tag",
                "html",
                "render",
                "view"
            ],
            "package": "view",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/tag.phlo"
        },
        {
            "name": "tags.form",
            "summary": "DOM form tags for button, input, select and textarea",
            "advice": "button(), input(), select() and textarea() are tag() with the name filled in, so they take the same named arguments and the same escaping. They exist because these four appear so often that the name is noise.",
            "tags": [
                "form",
                "tags",
                "html",
                "view"
            ],
            "package": "view",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/tags.form.phlo"
        },
        {
            "name": "tasks",
            "summary": "Cron runner for %app->tasks. One cron entry per app triggers this every minute.",
            "advice": "Everything scheduled in one place: %app->tasks holds what runs and when, and one cron entry per app triggers this every minute. A task takes a lock, so a run that lasts longer than its interval does not start over itself. Timing is per minute, so anything finer belongs in a daemon rather than here.",
            "tags": [
                "cron",
                "schedule",
                "tasks",
                "scheduler"
            ],
            "package": "scheduling",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/tasks.phlo"
        },
        {
            "name": "time.human",
            "summary": "Convert timestamp age to human label",
            "advice": "Rounds a timestamp to one unit that reads well, so a moment ago and last year both come out short. Define tsLabels to say it in another language; the frontend has the same trick with app.tsLabels, so both sides can speak the visitor's language.",
            "tags": [
                "time",
                "human",
                "age",
                "format"
            ],
            "package": "time",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/time.human.phlo"
        },
        {
            "name": "useragent",
            "summary": "User agent information",
            "advice": "Reads operating system, browser and device out of the user agent string. That string is a claim, not a fact: it can be turned off, faked or shortened, so use it for statistics and never as a condition for something that matters. What a browser can do is worth asking the browser itself.",
            "tags": [
                "useragent",
                "browser",
                "os",
                "device",
                "web"
            ],
            "package": "web",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/useragent.phlo"
        },
        {
            "name": "visitors",
            "summary": "Visitor tracking via heartbeat",
            "advice": "Counts visitors from the page itself with a heartbeat, so time on page and who is online now are real rather than guessed from page loads. It keeps a token instead of an IP for recognition, and bots are filtered out before anything is written. A heartbeat every few seconds is a write per visitor, so watch the table on a busy site and prune it.",
            "tags": [
                "visitors",
                "analytics",
                "heartbeat",
                "tracking"
            ],
            "package": "analytics",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/visitors.phlo"
        },
        {
            "name": "websocket",
            "summary": "Server-side WebSocket handler via phloWS",
            "advice": "Enable this class only when websockets are configured for the host",
            "tags": [
                "websocket",
                "realtime",
                "ws",
                "server"
            ],
            "package": "realtime",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/websocket.phlo"
        },
        {
            "name": "WhatsApp",
            "summary": "WhatsApp client for phloWA using whatsapp-web.js",
            "advice": "Speaks to a phloWA instance, which holds the actual WhatsApp session, so this resource is a client and not a connection: without that service running, nothing is sent. A contact is a full WhatsApp address, and one ending in @g is a group, which number() and isGroup() sort out. It is an unofficial route, so treat volume and content with the care an account you cannot replace deserves.",
            "tags": [
                "whatsapp",
                "messaging",
                "api"
            ],
            "package": "messaging",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/WhatsApp.phlo"
        },
        {
            "name": "wsCast",
            "summary": "Broadcast a message to WebSocket clients via the daemon's cast bridge",
            "advice": "Sends a message to connected clients from anywhere in the backend, without a socket of your own: it goes over the daemon's bridge. wsTarget picks who, and wsExcept leaves one connection out, which is how you avoid echoing an action back to the person who caused it. It needs a running daemon, so it does nothing on a plain web process.",
            "tags": [
                "websocket",
                "cast",
                "realtime",
                "http",
                "daemon"
            ],
            "package": "realtime",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/wsCast.phlo"
        },
        {
            "name": "AI/AI",
            "summary": "Unified AI facade with engine auto-detect",
            "advice": "One door for every engine: %AI->chat reads the engine from the model name, so gpt goes to OpenAI, claude to Claude and gemini to Gemini, while via names one outright. Every engine answers in the same shape, with answer, model, finish and a token count, so swapping models is a one-word change. A model no rule matches falls back to OpenAI, and each engine still needs credentials of its own.",
            "tags": [
                "ai",
                "facade",
                "llm",
                "streaming",
                "tools",
                "embeddings"
            ],
            "package": "ai",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/AI/AI.phlo"
        },
        {
            "name": "AI/answer",
            "summary": "Simple AI answering helper",
            "advice": "For a question with one short answer rather than a conversation: answer('...') gives a bare line and answer('...', 'yes', 'no') forces the reply to be exactly one of the options. Nothing fitting gives null instead of an invented answer, so treat null as a real outcome. It runs at temperature .1, so the same question tends to give the same answer.",
            "tags": [
                "ai",
                "answer",
                "question",
                "llm"
            ],
            "package": "ai",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/AI/answer.phlo"
        },
        {
            "name": "AI/Claude",
            "summary": "Anthropic Claude API",
            "advice": "Anthropic has no embedding endpoint, so embedding() quietly goes out through OpenAI and needs that key too. system travels as its own field here rather than as a first message, which context() settles, so the same call works on either engine. vision() fetches an image URL itself and sends it inline as base64, so a large photo becomes a large request.",
            "tags": [
                "ai",
                "claude",
                "anthropic",
                "chat",
                "vision",
                "embeddings"
            ],
            "package": "ai",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/AI/Claude.phlo"
        },
        {
            "name": "AI/DeepSeek",
            "summary": "DeepSeek API (OpenAI-compatible, extends OpenAI)",
            "advice": "OpenAI-compatible, so it inherits the whole OpenAI resource and only swaps endpoint, model and credentials; anything that works there works here unless DeepSeek itself lacks it. Embeddings are such a gap: they are routed to OpenAI, so that key has to be present as well.",
            "tags": [
                "ai",
                "deepseek",
                "chat",
                "embeddings"
            ],
            "package": "ai",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/AI/DeepSeek.phlo"
        },
        {
            "name": "AI/Gemini",
            "summary": "Google Gemini API",
            "advice": "Google puts the model in the path rather than in the body, so a wrong model name reads as a wrong URL. Its embeddings come from text-embedding-004 with a different vector length than OpenAI's, so a collection filled by one engine cannot be searched with the other.",
            "tags": [
                "ai",
                "gemini",
                "google",
                "chat",
                "vision",
                "embeddings"
            ],
            "package": "ai",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/AI/Gemini.phlo"
        },
        {
            "name": "AI/Grok",
            "summary": "xAI Grok API (OpenAI-compatible, extends OpenAI)",
            "advice": "OpenAI-compatible, so it inherits the whole OpenAI resource and only swaps endpoint, model and credentials. Embeddings are not part of the deal and go out through OpenAI, so that key has to be present as well.",
            "tags": [
                "ai",
                "grok",
                "xai",
                "chat",
                "vision",
                "embeddings"
            ],
            "package": "ai",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/AI/Grok.phlo"
        },
        {
            "name": "AI/OpenAI",
            "summary": "Basic OpenAI functions",
            "advice": "Write a conversation as system, user and assistant instead of assembling messages yourself; context() folds them into the right order. Each answer carries tokens_in and tokens_out, so a run can be metered or capped without reading the raw response. A refused request is raised as an error rather than returned, which is the opposite of how the connectors behave, so wrap a call you cannot afford to lose. Pass token to use a key other than the configured one.",
            "tags": [
                "ai",
                "openai",
                "llm",
                "chat",
                "embeddings",
                "audio",
                "vision"
            ],
            "package": "ai",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/AI/OpenAI.phlo"
        },
        {
            "name": "connectors/Connector",
            "summary": "Base class for API connectors: credentials, JSON requests, retries, pagination and a normalized result contract",
            "advice": "Make one with Connector::make(); it reads its own section from %creds, so keys live in data/creds.ini or in PHLO__Section__key in the environment and never in your code. Every call answers in the same shape, ok with status and data or ok false with error, and nothing is thrown, so test ->ok rather than catching. Raise retries above zero to let GET, HEAD and QUERY back off and try again on 429 and 5xx; writes are never retried, because a repeated POST would book twice.",
            "tags": [
                "api",
                "connector",
                "http",
                "rest",
                "base"
            ],
            "package": "connectors",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/connectors/Connector.phlo"
        },
        {
            "name": "connectors/OAuthConnector",
            "summary": "Base class for OAuth2 connectors: stored, auto-refreshed bearer access tokens via TokenStore, on the OAuth2 primitive",
            "advice": "Use this instead of Connector when the provider hands out access tokens that expire. Give it client_id, client_secret and a refresh_token and TokenStore keeps the access token and renews it in time; a subclass only names its section and tokenUrl.",
            "tags": [
                "oauth",
                "oauth2",
                "connector",
                "base",
                "token",
                "refresh"
            ],
            "package": "connectors",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/connectors/OAuthConnector.phlo"
        },
        {
            "name": "connectors/TokenStore",
            "summary": "Persisted OAuth2 token store with automatic refresh via the OAuth2 resource",
            "advice": "Tokens are kept as one file per key under data/tokens with 0600 rights, so a refresh survives a restart and every worker shares it. A refresh takes an exclusive lock, which matters with providers that rotate the refresh token: without it the losing request is left holding a dead one.",
            "tags": [
                "oauth",
                "oauth2",
                "token",
                "refresh",
                "store",
                "credentials"
            ],
            "package": "connectors",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/connectors/TokenStore.phlo"
        },
        {
            "name": "connectors/chat/MessageBird",
            "summary": "MessageBird connector: send SMS",
            "advice": "Needs access_key and an originator, the sender shown to the recipient; some countries refuse an alphanumeric originator, so a real number is the safer choice. Pass an array as the recipient to send one message to several numbers at once.",
            "tags": [
                "messagebird",
                "sms",
                "messaging",
                "connector"
            ],
            "package": "connectors",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/connectors/chat/MessageBird.phlo"
        },
        {
            "name": "connectors/chat/Resend",
            "summary": "Resend connector: send transactional email via the HTTP API",
            "advice": "Needs api_key and a from_email on a domain you verified with Resend, otherwise the send is refused. send() takes HTML; leave it out for a plain text mail and add anything else Resend accepts through extra.",
            "tags": [
                "resend",
                "email",
                "transactional",
                "messaging",
                "connector"
            ],
            "package": "connectors",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/connectors/chat/Resend.phlo"
        },
        {
            "name": "connectors/chat/Slack",
            "summary": "Slack connector: post messages, read channel history and list channels",
            "advice": "Needs a bot token, and the bot has to be invited into the channel it posts in, else the call comes back ok false with not_in_channel. Slack answers HTTP 200 even when it refuses, so read the outcome from the result, never from the status.",
            "tags": [
                "slack",
                "messaging",
                "chat",
                "connector"
            ],
            "package": "connectors",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/connectors/chat/Slack.phlo"
        },
        {
            "name": "connectors/chat/Telegram",
            "summary": "Telegram Bot API connector: send messages, photos and documents; poll updates",
            "advice": "Needs a bot token from BotFather and a chat_id, and a person has to have written to the bot before it can write to them. Like Slack, the API answers 200 on refusal, which the connector already translates into ok false. photo() and document() take a URL or an existing file_id, not raw bytes.",
            "tags": [
                "telegram",
                "bot",
                "messaging",
                "chat",
                "connector"
            ],
            "package": "connectors",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/connectors/chat/Telegram.phlo"
        },
        {
            "name": "connectors/chat/Twilio",
            "summary": "Twilio connector: send SMS and read message status",
            "advice": "Needs account_sid and auth_token, plus either a from_number in E.164 or a messaging_service_sid; without one of the two the send is refused before it leaves. Twilio speaks form encoding rather than JSON, which sms() handles, so pass extra fields in Twilio's own capitalized names. A returned sid says it was accepted, not delivered; message() tells you what became of it.",
            "tags": [
                "twilio",
                "sms",
                "messaging",
                "connector"
            ],
            "package": "connectors",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/connectors/chat/Twilio.phlo"
        },
        {
            "name": "connectors/cloud/GoogleCalendar",
            "summary": "Google Calendar connector (OAuth2): read events and create events",
            "advice": "Shares the Google section with Sheets, so one refresh token has to carry both scopes. calendarId defaults to primary, which is the mailbox of the account that authorized, not a shared agenda; give a calendar address for those. Google returns times as RFC3339 with an offset, so keep the timezone rather than casting to a local timestamp.",
            "tags": [
                "google",
                "calendar",
                "events",
                "oauth",
                "connector"
            ],
            "package": "connectors",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/connectors/cloud/GoogleCalendar.phlo"
        },
        {
            "name": "connectors/cloud/GoogleSheets",
            "summary": "Google Sheets connector (OAuth2): read ranges and append rows",
            "advice": "Shares the Google section with Calendar, so one refresh token has to carry both scopes. A range is A1 notation including the tab name, e.g. Sheet1!A:D. append() writes with USER_ENTERED, so the sheet parses what you send just as a typist would and a leading zero or a date-like string is reinterpreted; pass RAW when the value has to stay untouched.",
            "tags": [
                "google",
                "sheets",
                "spreadsheet",
                "oauth",
                "connector"
            ],
            "package": "connectors",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/connectors/cloud/GoogleSheets.phlo"
        },
        {
            "name": "connectors/cloud/MicrosoftGraph",
            "summary": "Microsoft Graph connector (app-only client credentials): read users and calendars, send mail, create events",
            "advice": "This is the app-only flow: the token belongs to the registration, not to a person, so it needs admin-consented application permissions and every call names the mailbox it acts on. Tokens are cached in APCu for their lifetime, so a run without APCu fetches one per request. Set mailbox to spare yourself passing a user each time.",
            "tags": [
                "microsoft",
                "graph",
                "office365",
                "calendar",
                "mail",
                "connector"
            ],
            "package": "connectors",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/connectors/cloud/MicrosoftGraph.phlo"
        },
        {
            "name": "connectors/finance/EBoekhouden",
            "summary": "e-Boekhouden.nl connector: session auth from an API token, relations and sales invoices",
            "advice": "Authenticates with a session rather than a token per call: the first call trades api_token for one, later calls in the same request reuse it, and a new request starts over. Sessions are limited, so gather the work you need per request instead of making a connector per call.",
            "tags": [
                "eboekhouden",
                "accounting",
                "invoices",
                "relations",
                "connector"
            ],
            "package": "connectors",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/connectors/finance/EBoekhouden.phlo"
        },
        {
            "name": "connectors/finance/ExactOnline",
            "summary": "Exact Online connector (OAuth2): read sales invoices and accounts, create sales invoices",
            "advice": "The division number sits in the base URL, so a connector speaks to exactly one administration and a second administration needs a second connector with its own config. Exact rotates the refresh token on every refresh, which is why the token store locks; running two apps on one refresh token logs both out.",
            "tags": [
                "exact",
                "exactonline",
                "accounting",
                "invoices",
                "oauth",
                "connector"
            ],
            "package": "connectors",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/connectors/finance/ExactOnline.phlo"
        },
        {
            "name": "connectors/finance/Moneybird",
            "summary": "Moneybird connector: read contacts and invoices, create sales invoices",
            "advice": "Needs an administration_id and a personal access token with rights for contacts and invoices. findContact() searches on one match, so use it to look up rather than to list. An invoice is created as a draft: sending or booking it is a separate step in Moneybird.",
            "tags": [
                "moneybird",
                "accounting",
                "invoices",
                "contacts",
                "connector"
            ],
            "package": "connectors",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/connectors/finance/Moneybird.phlo"
        },
        {
            "name": "connectors/shops/Lightspeed",
            "summary": "Lightspeed Retail (V3) connector: read customers and sales, create customers",
            "advice": "Retail V3, so cluster_id names the account and the key and secret authenticate. Lightspeed rate limits per account with a leaky bucket, so raise retries rather than firing calls in a loop. findCustomer() picks its search field from what you pass: an address with an at sign searches on email, anything else on phone.",
            "tags": [
                "lightspeed",
                "webshop",
                "retail",
                "pos",
                "customers",
                "connector"
            ],
            "package": "connectors",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/connectors/shops/Lightspeed.phlo"
        },
        {
            "name": "connectors/shops/Shopify",
            "summary": "Shopify Admin API connector: read customers, orders and products; create draft orders and products; update inventory",
            "advice": "Needs shop_domain including myshopify.com and an admin access token; api_version defaults to 2024-01 and a Shopify version is supported for a limited time, so set the one you tested against rather than leaning on the default. setInventory() writes an absolute level, not a difference, so read the current one first when you mean to add. A draft order is not an order until it is completed in Shopify.",
            "tags": [
                "shopify",
                "webshop",
                "ecommerce",
                "orders",
                "products",
                "connector"
            ],
            "package": "connectors",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/connectors/shops/Shopify.phlo"
        },
        {
            "name": "DB/DB",
            "summary": "Database engine class",
            "advice": "The shape every driver fills in, and the reason a model can move between MySQL, PostgreSQL, SQLite and a JSON file untouched. Reads come in named flavours, so ask for what you want back: record for one, records keyed by id, rows in order, column, pair and item. Anything you pass as an argument is bound, never pasted into the SQL, so a value from a visitor is safe by construction. A connection that went away is reconnected and the query is tried once more, which is what keeps a long-lived worker alive.",
            "tags": [
                "database",
                "pdo",
                "sql"
            ],
            "package": "database",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/DB/DB.phlo"
        },
        {
            "name": "DB/JSON.result",
            "summary": "Minimal PDOStatement-like wrapper for JSONDB result arrays",
            "advice": "Exists so JSONDB can hand back something that behaves like a PDO statement, which is what lets the ORM stay unaware of which driver answered. You do not build one yourself; you meet it as the return value of a JSONDB query.",
            "tags": [
                "json",
                "database",
                "result"
            ],
            "package": "database",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/DB/JSON.result.phlo"
        },
        {
            "name": "DB/JSONDB",
            "summary": "JSON file database driver. One JSONDB instance = one JSON file = one model table. No joins, no transactions, no schema introspection.",
            "advice": "A model in a JSON file, for a set of records that stays small and readable: no joins, no transactions, and every write rewrites the whole file. It understands only equality and IN in a where, and raw SQL is refused outright, so keep the model plain. Move to SQLite the moment the file grows or two processes start writing.",
            "tags": [
                "json",
                "database",
                "file",
                "storage"
            ],
            "package": "database",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/DB/JSONDB.phlo"
        },
        {
            "name": "DB/model",
            "summary": "Phlo ORM class with unified columns and schema",
            "advice": "A model is a class with fields, and everything else follows from that: columns, form, validation and relations. Named arguments in a read are equality, so records(active: 1) is a where and nothing more; reach for query() when you need a comparison, a LIKE or an IN. A parent field gives back the record itself rather than an id, and children and many-relations are fetched once for a whole result set instead of per row. Set objValidate to have field rules enforced on save, objAudit to log every change, and objCache to keep reads in APCu.",
            "tags": [
                "orm",
                "model",
                "database",
                "records",
                "schema"
            ],
            "package": "database",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/DB/model.phlo"
        },
        {
            "name": "DB/MySQL",
            "summary": "MySQL handler via DB class",
            "advice": "Reads host, database, user and password from the mysql section of %creds. It is the default assumption of the ORM, so a model that names no engine of its own ends up here.",
            "tags": [
                "mysql",
                "pdo",
                "database",
                "sql"
            ],
            "package": "database",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/DB/MySQL.phlo"
        },
        {
            "name": "DB/PostgreSQL",
            "summary": "PostgreSQL resource",
            "advice": "Quotes identifiers with double quotes rather than backticks and has no INSERT IGNORE, so a duplicate is skipped with ON CONFLICT DO NOTHING. Postgres folds an unquoted name to lower case, so a column called createdAt in a schema is not the same one you get back unquoted.",
            "tags": [
                "postgresql",
                "pdo",
                "database",
                "sql"
            ],
            "package": "database",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/DB/PostgreSQL.phlo"
        },
        {
            "name": "DB/Qdrant",
            "summary": "Embeddings resource with Qdrant",
            "advice": "Embeddings are cached in APCu for four weeks per input, so repeating a search costs nothing at the AI end. create() opens a collection at 1536 dimensions, the length of an OpenAI vector, so state the size yourself when another engine fills it. search() without input sends a zero vector, which lists a collection rather than searching it.",
            "tags": [
                "qdrant",
                "embeddings",
                "vector",
                "search",
                "ai"
            ],
            "package": "ai",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/DB/Qdrant.phlo"
        },
        {
            "name": "DB/query",
            "summary": "Fluent query builder for Phlo ORM",
            "advice": "For everything named arguments cannot say: eq, gt, like, in, isNull, order, limit and offset, chained and closed with records, record, column, item or count. Column names are quoted for the driver you are on, so the same chain works on MySQL and Postgres. Values go in as bindings, so a search box can be passed straight through.",
            "tags": [
                "query",
                "builder",
                "orm",
                "database",
                "sql"
            ],
            "package": "database",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/DB/query.phlo"
        },
        {
            "name": "DB/SQLite",
            "summary": "SQLite resource",
            "advice": "One file, one database, given as a path: %SQLite('/path/db.sqlite'). It writes with a lock over the whole file, so it fits a single site or a worker but not a set of processes writing at once. Perfect where you want the ORM without a server.",
            "tags": [
                "sqlite",
                "pdo",
                "database",
                "sql"
            ],
            "package": "database",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/DB/SQLite.phlo"
        },
        {
            "name": "DOM/charts",
            "summary": "Lightweight dependency-free SVG charts: sparkline, bars and donut. Use charts::spark/bars/donut.",
            "advice": "Three small SVG charts rendered on the server, without a library and without a script, so they show up in a mail, a PDF and a page with a strict policy alike. They are meant for a number in context, not for exploring data: no axes, no legend, no tooltips.",
            "tags": [
                "chart",
                "svg",
                "sparkline",
                "bars",
                "donut",
                "visualization"
            ],
            "package": "dom",
            "frontend": true,
            "backend": true,
            "file": "/phlo/resources/DOM/charts.phlo"
        },
        {
            "name": "DOM/cookiewall",
            "summary": "Subtle GDPR cookie-consent banner. English by default; auto-translates when the lang system (en()) is loaded. Override prop labels for a fixed language, or prop translate to force it on/off.",
            "advice": "Asks once and keeps the answer in a cookie, so it stays out of the way afterwards. canTrack and canAnalytics are what the rest of the app should ask before it loads anything; the banner itself blocks nothing. It translates itself when the language system is loaded, so it speaks the visitor's language without a second set of texts.",
            "tags": [
                "gdpr",
                "consent",
                "cookies",
                "privacy"
            ],
            "package": "privacy",
            "frontend": true,
            "backend": true,
            "file": "/phlo/resources/DOM/cookiewall.phlo"
        },
        {
            "name": "DOM/CSS.fixes",
            "summary": "Single Page App basic CSS boilerplate fixes",
            "advice": "The handful of corrections nearly every app makes anyway: border-box sizing, no tap delay on anything clickable, no spinners on a number field, collapsed table borders and a [hidden] that actually hides. Note that it also clears the focus outline on inputs and buttons, so give focus a visible state of your own or keyboard users lose their place.",
            "tags": [
                "css",
                "fixes",
                "boilerplate",
                "reset"
            ],
            "package": "css",
            "frontend": true,
            "backend": false,
            "file": "/phlo/resources/DOM/CSS.fixes.phlo"
        },
        {
            "name": "DOM/CSS.var",
            "summary": "CSS variable proxy via app.var",
            "advice": "app.var reads and writes CSS custom properties as if they were an object, so the server can change a colour, a size or a spacing with a command instead of a stylesheet swap. It writes on the root element, so what you set applies everywhere that inherits it.",
            "tags": [
                "css",
                "variables",
                "app.var",
                "frontend"
            ],
            "package": "css",
            "frontend": true,
            "backend": false,
            "file": "/phlo/resources/DOM/CSS.var.phlo"
        },
        {
            "name": "DOM/datatags",
            "summary": "Single Page App datatag plugin",
            "advice": "Turn any element into a request without writing a handler: data-get, data-post, data-put, data-patch or data-delete holds the path, and with post, put and patch every other data attribute travels along as a field. That is why an element that also carries data-confirm is left alone here: the dialog resource asks first and clicks it again afterwards. Attribute names arrive dash-lowered as the browser gives them, so keep them one word.",
            "tags": [
                "dom",
                "datatag",
                "dataset",
                "spa",
                "events"
            ],
            "package": "dom",
            "frontend": true,
            "backend": false,
            "file": "/phlo/resources/DOM/datatags.phlo"
        },
        {
            "name": "DOM/dialog",
            "summary": "Single Page App dialog resource",
            "advice": "Replaces alert, confirm and prompt with a real dialog element, so they no longer block the page and no longer break an automated session. They answer a promise, so await them. Put data-confirm on a link or a button to ask before it does anything: the question is asked once, then the original action runs.",
            "tags": [
                "dom",
                "dialog",
                "modal",
                "confirm",
                "prompt",
                "alert"
            ],
            "package": "dom",
            "frontend": true,
            "backend": false,
            "file": "/phlo/resources/DOM/dialog.phlo"
        },
        {
            "name": "DOM/exists",
            "summary": "onExist helper for dynamic SPA elements",
            "advice": "on() binds to what is there now, which is why it does not survive a page swap; onExist runs your callback the first time an element appears and only then, however it got there. That makes it the right hook for anything a plugin has to prepare once, and it is what the store and the numpad use themselves.",
            "tags": [
                "dom",
                "onexist",
                "spa",
                "lifecycle"
            ],
            "package": "dom",
            "frontend": true,
            "backend": false,
            "file": "/phlo/resources/DOM/exists.phlo"
        },
        {
            "name": "DOM/ffmpeg",
            "summary": "ffmpeg-wasm for the DOM: encode a canvas timeline to MP4, decode source frames via WebCodecs (seek fallback), transcode/run arbitrary ffmpeg. Exposes the ready singleton `ffmpeg` (and class `Ffmpeg`).",
            "advice": "Encodes and converts video in the browser through ffmpeg-wasm, so no file has to leave the machine and no server has to be equipped for it. That comes at a price: several megabytes of wasm on first use, and encoding costs real time and memory, so it fits a clip rather than an hour of video. It loads the multithreaded core only on a cross-origin isolated page and falls back to a single-threaded one otherwise, which still works but is markedly slower.",
            "tags": [
                "video",
                "ffmpeg",
                "wasm",
                "canvas",
                "encode",
                "transcode",
                "webcodecs",
                "mp4",
                "render"
            ],
            "package": "dom",
            "frontend": true,
            "backend": false,
            "file": "/phlo/resources/DOM/ffmpeg.phlo"
        },
        {
            "name": "DOM/form",
            "summary": "Single Page App form handler and input state saver",
            "advice": "A form with class async submits over the same channel as the rest and answers with commands rather than a new page, using its own method attribute. Apart from that, this keeps the DOM honest: what a visitor types is written back into the attributes, so the state that is saved and restored on a back button matches what is on screen.",
            "tags": [
                "dom",
                "form",
                "input",
                "state",
                "spa"
            ],
            "package": "dom",
            "frontend": true,
            "backend": false,
            "file": "/phlo/resources/DOM/form.phlo"
        },
        {
            "name": "DOM/image.resizer",
            "summary": "Client-side file upload image resizer",
            "advice": "Scales a picked file down in the browser before it is uploaded, so a phone photo of several megabytes leaves as a few hundred kilobytes. It only shrinks, keeps the aspect ratio, and gives a data URL back through the callback, which can go straight into a preview or a form field. The image field uses it, so an upload through the CMS is already covered.",
            "tags": [
                "dom",
                "image",
                "resize",
                "upload",
                "canvas"
            ],
            "package": "dom",
            "frontend": true,
            "backend": false,
            "file": "/phlo/resources/DOM/image.resizer.phlo"
        },
        {
            "name": "DOM/keyboard",
            "summary": "On-screen keyboard for touch devices, with selectable layout",
            "advice": "Put data-keyboard on a field to open a keyboard on focus; data-keyboard=\"azerty\" picks the layout. Inside a dialog the keys mount in that dialog; data-keyboard-dock on an element there places them in the flow. phlo.keyboard.layouts takes extra layouts.",
            "tags": [
                "dom",
                "keyboard",
                "onscreen",
                "touch",
                "input",
                "frontend"
            ],
            "package": "dom",
            "frontend": true,
            "backend": false,
            "file": "/phlo/resources/DOM/keyboard.phlo"
        },
        {
            "name": "DOM/link",
            "summary": "Single Page App async link handler",
            "advice": "A link with class async is fetched and swapped in instead of loading the page, and everything else keeps working: a target, a modifier click and an outside link go to the browser untouched. An anchor is remembered across the swap, so a deep link scrolls to the right place after the new content has arrived.",
            "tags": [
                "dom",
                "link",
                "async",
                "navigation",
                "spa"
            ],
            "package": "dom",
            "frontend": true,
            "backend": false,
            "file": "/phlo/resources/DOM/link.phlo"
        },
        {
            "name": "DOM/markdown",
            "summary": "Client-side markdown parser",
            "advice": "Parses markdown in the browser, for text that arrives after the page: a chat message, a preview while typing, an answer streaming in. It renders what it is given, so escape or clean anything a visitor wrote before showing it to someone else. Markdown that is already known at render time is cheaper to parse on the server.",
            "tags": [
                "dom",
                "markdown",
                "parser",
                "frontend"
            ],
            "package": "dom",
            "frontend": true,
            "backend": false,
            "file": "/phlo/resources/DOM/markdown.phlo"
        },
        {
            "name": "DOM/numpad",
            "summary": "On-screen numeric keypad for touch input, bound to a field",
            "advice": "Put data-numpad=\"<selector>\" on a container; empty containers get the standard keys, containers with their own data-numpad-key buttons keep them. Without a selector the pad writes to the first field of its own form.",
            "tags": [
                "dom",
                "numpad",
                "keypad",
                "touch",
                "input",
                "pos",
                "frontend"
            ],
            "package": "dom",
            "frontend": true,
            "backend": false,
            "file": "/phlo/resources/DOM/numpad.phlo"
        },
        {
            "name": "DOM/presentation",
            "summary": "Timeline presentation player for the DOM: timed image/video layers, transitions, subtitles and language alternatives from a presentation.json payload. Exposes the class `PresentationPlayer`; each transition carries its own pp-* CSS animation and the matching canvas curve for deterministic export rendering. boot() wires every .pp-embed, from an inline JSON script child or a data-src payload URL. A document keymap steers the fullscreen, focused or only player: space/k toggles, arrows seek and set volume, m mutes, c toggles subtitles, f fullscreen, home/end jump.",
            "advice": "Plays a presentation from one JSON payload against its own clock, and seeks the audio and video to match it, so a slow machine drops frames rather than drifting out of step. That also means a presentation without audio runs perfectly well. Alternative languages live in the same payload, so switching language reloads nothing.",
            "tags": [
                "presentation",
                "player",
                "timeline",
                "audio",
                "video",
                "subtitles",
                "transitions",
                "canvas",
                "render",
                "keyboard"
            ],
            "package": "dom",
            "frontend": true,
            "backend": false,
            "file": "/phlo/resources/DOM/presentation.phlo"
        },
        {
            "name": "DOM/recorder",
            "summary": "Record the screen, a camera/mic or a canvas via MediaRecorder -> Blob. Optionally transcodes to MP4 with the DOM/ffmpeg resource when it is loaded. Exposes the ready singleton `recorder` (and class `Recorder`).",
            "advice": "Records the screen, a camera or a canvas through MediaRecorder and answers with a blob. The browser decides the container, which is usually WebM and on Safari is not, so transcode when the file has to be played anywhere; loading the ffmpeg resource gives you MP4. Recording needs a real gesture from the visitor and a secure origin, so it cannot be started from a script alone.",
            "tags": [
                "video",
                "recorder",
                "mediarecorder",
                "screen",
                "capture",
                "getdisplaymedia",
                "webcam",
                "canvas"
            ],
            "package": "dom",
            "frontend": true,
            "backend": false,
            "file": "/phlo/resources/DOM/recorder.phlo"
        },
        {
            "name": "DOM/shorthands",
            "summary": "onChange, onClick and onInput event shorthands",
            "advice": "Nothing more than onClick, onChange and onInput for the three most common cases of on(). Same behaviour and the same limitation: they bind to what exists at that moment, so use onExist for anything that appears later.",
            "tags": [
                "dom",
                "events",
                "shorthand",
                "frontend"
            ],
            "package": "dom",
            "frontend": true,
            "backend": false,
            "file": "/phlo/resources/DOM/shorthands.phlo"
        },
        {
            "name": "DOM/store",
            "summary": "Stateful binding engine",
            "advice": "Bind an element to a value with data-bind and it follows every change, in both directions on an input. data-bind-attr does the same for an attribute, data-each repeats a template over a list, and app.calc holds values derived from others and recalculated on their own. app.persist keeps a path across a reload and app.sync keeps it equal across tabs or over a websocket. On a first render the DOM wins over an empty store, so server-rendered content is not blanked before the store has been filled.",
            "tags": [
                "dom",
                "store",
                "binding",
                "state",
                "signals",
                "calc",
                "reactive",
                "each",
                "persist",
                "websocket",
                "sync"
            ],
            "package": "dom",
            "frontend": true,
            "backend": false,
            "file": "/phlo/resources/DOM/store.phlo"
        },
        {
            "name": "DOM/template",
            "summary": "Single Page App client-side templating",
            "advice": "Add cb's to the templates object and output via apply(template: [$name => $rows, $name2 => $rows2, etc])",
            "tags": [
                "dom",
                "template",
                "spa",
                "frontend",
                "render"
            ],
            "package": "dom",
            "frontend": true,
            "backend": false,
            "file": "/phlo/resources/DOM/template.phlo"
        },
        {
            "name": "DOM/timestamps",
            "summary": "DOM live timestamps",
            "advice": "Create an app.tsLabels array to overwrite the tsBase labels in any language",
            "tags": [
                "dom",
                "timestamps",
                "time",
                "live",
                "frontend"
            ],
            "package": "dom",
            "frontend": true,
            "backend": false,
            "file": "/phlo/resources/DOM/timestamps.phlo"
        },
        {
            "name": "DOM/toasts",
            "summary": "Simple toast resource",
            "advice": "app.mod.toast(msg) for a short message that needs no answer, gone after four seconds or on a click. Because it is a command, the server can raise one from a route without a line of frontend code. Anything a visitor must confirm belongs in the dialog resource instead.",
            "tags": [
                "dom",
                "toast",
                "notification",
                "frontend"
            ],
            "package": "dom",
            "frontend": true,
            "backend": false,
            "file": "/phlo/resources/DOM/toasts.phlo"
        },
        {
            "name": "DOM/visible",
            "summary": "onVisible and onVisibleIn helpers for DOM visibility",
            "advice": "Reacts to an element entering or leaving the viewport at ten percent visible, which is what you want for lazy loading, counting a view or starting an animation at the right moment. Give only cbOut and it fires once and stops watching, so a one-off costs nothing afterwards. onVisibleIn watches inside a scrolling container rather than the window.",
            "tags": [
                "dom",
                "visible",
                "intersection",
                "observer",
                "frontend"
            ],
            "package": "dom",
            "frontend": true,
            "backend": false,
            "file": "/phlo/resources/DOM/visible.phlo"
        },
        {
            "name": "DOM/websocket",
            "summary": "Client-side WebSocket handler",
            "advice": "Opens the connection and keeps it open, reconnecting on its own, and treats what arrives as commands, so the server can update a page with the same instructions a route uses. It survives a page swap, which a hand-bound on() does not. A token belongs in a cookie rather than in the URL, since a URL ends up in logs.",
            "tags": [
                "websocket",
                "realtime",
                "frontend",
                "dom"
            ],
            "package": "realtime",
            "frontend": true,
            "backend": false,
            "file": "/phlo/resources/DOM/websocket.phlo"
        },
        {
            "name": "fields/bool",
            "summary": "Boolean field",
            "advice": "Stores 1 or 0 and never null, so a checkbox that is left alone still saves a value. Set true and false to change the two symbols a list shows.",
            "tags": [
                "field",
                "boolean",
                "input"
            ],
            "package": "fields",
            "frontend": true,
            "backend": true,
            "file": "/phlo/resources/fields/bool.phlo"
        },
        {
            "name": "fields/child",
            "summary": "Child relation field",
            "advice": "The mirror of a parent field elsewhere: it reads the records that point back at this one and owns no column itself. It cannot be edited here, only followed, so change a child from its own record. Set key when the foreign key is not named after the parent model.",
            "tags": [
                "field",
                "relation",
                "child"
            ],
            "package": "fields",
            "frontend": true,
            "backend": true,
            "file": "/phlo/resources/fields/child.phlo"
        },
        {
            "name": "fields/date",
            "summary": "Date field",
            "advice": "Renders in the reader's language through IntlDateFormatter, so a Dutch visitor sees a Dutch date without any work. Without the intl extension it falls back to a built-in Dutch and English month list, so any other language reads as English there. Set format to a date() pattern when the notation has to be fixed rather than local.",
            "tags": [
                "field",
                "date"
            ],
            "package": "fields",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/fields/date.phlo"
        },
        {
            "name": "fields/datetime",
            "summary": "Date-time field",
            "advice": "Stores a unix timestamp rather than a formatted string, and shows an age icon that runs blue under an hour, yellow under a day and red beyond. Fields called created and changed are kept out of the form because the model writes those itself.",
            "tags": [
                "field",
                "datetime"
            ],
            "package": "fields",
            "frontend": true,
            "backend": true,
            "file": "/phlo/resources/fields/datetime.phlo"
        },
        {
            "name": "fields/email",
            "summary": "Email field",
            "advice": "Only changes how a stored address is shown, as a mailto link. It validates nothing, so add pattern or required when the address has to be real.",
            "tags": [
                "field",
                "email"
            ],
            "package": "fields",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/fields/email.phlo"
        },
        {
            "name": "fields/field",
            "summary": "Base ORM field",
            "advice": "Every field type extends this one and is made with field(type: 'x'), which resolves to the resource field_x; anything else you pass becomes a property, so title, required, length, pattern and enum need no declaration. Override input() for the form and label() for the list, and let objColumns name the columns the field owns, so a field that stores nothing returns an empty array.",
            "tags": [
                "field",
                "orm"
            ],
            "package": "fields",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/fields/field.phlo"
        },
        {
            "name": "fields/file",
            "summary": "File field",
            "advice": "An upload is stored under a random token instead of its own name, so two people can send the same filename and no visitor can guess a neighbouring URL. That takes two columns, name and name_token, which objColumns already claims. Set accept to narrow the picker, and path and uri when the files live somewhere other than the default.",
            "tags": [
                "field",
                "file",
                "upload"
            ],
            "package": "fields",
            "frontend": true,
            "backend": true,
            "file": "/phlo/resources/fields/file.phlo"
        },
        {
            "name": "fields/image",
            "summary": "Image field",
            "advice": "A file field that also writes a thumbnail, and the browser scales the picture down before it is sent, so a phone photo does not travel at full size. thumbSize is the thumbnail edge in pixels; a list shows the thumbnail and a record shows the full image.",
            "tags": [
                "field",
                "image",
                "upload"
            ],
            "package": "fields",
            "frontend": true,
            "backend": true,
            "file": "/phlo/resources/fields/image.phlo"
        },
        {
            "name": "fields/many",
            "summary": "Many-to-many relation field",
            "advice": "Editing is opt-in: a model that sets create/change on a many field gets the checkbox picker from input(); CMS.API::syncMany() then rewrites the pivot table on save.",
            "tags": [
                "field",
                "relation",
                "many"
            ],
            "package": "fields",
            "frontend": true,
            "backend": true,
            "file": "/phlo/resources/fields/many.phlo"
        },
        {
            "name": "fields/multiselect",
            "summary": "Multi-select via checkboxes; stores the choice as CSV in one hidden field (no save change needed).",
            "advice": "Joins the checked boxes into one comma separated column, so it needs no pivot table and no extra save logic. Reach for many instead when the choices are records with a life of their own.",
            "tags": [
                "field",
                "select",
                "multi"
            ],
            "package": "fields",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/fields/multiselect.phlo"
        },
        {
            "name": "fields/number",
            "summary": "Number field",
            "advice": "decimals sets both the step of the input and the formatting of the list. min is 0 out of the box, so state it yourself when negative values are allowed.",
            "tags": [
                "field",
                "number"
            ],
            "package": "fields",
            "frontend": true,
            "backend": true,
            "file": "/phlo/resources/fields/number.phlo"
        },
        {
            "name": "fields/parent",
            "summary": "Parent relation field",
            "advice": "Points at another model through obj and offers every record of it in one select, so keep it to sets a person can still scroll. Reading the field gives you the record itself, not the id.",
            "tags": [
                "field",
                "relation",
                "parent"
            ],
            "package": "fields",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/fields/parent.phlo"
        },
        {
            "name": "fields/password",
            "summary": "Password field",
            "advice": "Never gives the stored value back: a list prints dots and an empty form field leaves the current password untouched. It hashes with bcrypt on save, so keep the plain value nowhere.",
            "tags": [
                "field",
                "password"
            ],
            "package": "fields",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/fields/password.phlo"
        },
        {
            "name": "fields/price",
            "summary": "Price field",
            "advice": "A number field fixed at two decimals. Store the amount in the unit you bill in and leave the formatting to the field.",
            "tags": [
                "field",
                "price",
                "money"
            ],
            "package": "fields",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/fields/price.phlo"
        },
        {
            "name": "fields/select",
            "summary": "Select field",
            "advice": "Stores the chosen option itself rather than a key, so renaming an option later leaves older records pointing at wording you no longer offer. Pass the accepted values as options.",
            "tags": [
                "field",
                "select"
            ],
            "package": "fields",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/fields/select.phlo"
        },
        {
            "name": "fields/text",
            "summary": "Text field",
            "advice": "length is both the limit and the choice of input: over 250 characters the field renders a textarea instead of a single line.",
            "tags": [
                "field",
                "text"
            ],
            "package": "fields",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/fields/text.phlo"
        },
        {
            "name": "fields/token",
            "summary": "Token field",
            "advice": "Fills itself with a random token when a record is made and refuses to change afterwards, which makes it the id to use for anything that ends up in a URL. handle is true, so a record can be looked up by this column instead of by its id.",
            "tags": [
                "field",
                "token"
            ],
            "package": "fields",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/fields/token.phlo"
        },
        {
            "name": "fields/virtual",
            "summary": "Virtual field",
            "advice": "Owns no column and is never saved, so use it to show something derived next to the real fields. The record supplies the value through a prop or method of the same name.",
            "tags": [
                "field",
                "virtual"
            ],
            "package": "fields",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/fields/virtual.phlo"
        },
        {
            "name": "fields/wysiwyg",
            "summary": "WYSIWYG field",
            "advice": "Stores whatever HTML the editor produces, including anything a user pasted in, so clean or escape it before it reaches a public page.",
            "tags": [
                "field",
                "wysiwyg",
                "editor"
            ],
            "package": "fields",
            "frontend": true,
            "backend": true,
            "file": "/phlo/resources/fields/wysiwyg.phlo"
        },
        {
            "name": "files/CSV",
            "summary": "CSV reader resource",
            "advice": "Reads the first line as the header and picks its own separator by counting: more commas than semicolons and it is a comma, otherwise a semicolon. Every row comes back keyed by header name, so a file with duplicate or empty headers loses columns. It reads, it does not write.",
            "tags": [
                "file",
                "csv",
                "reader",
                "import"
            ],
            "package": "files",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/files/CSV.phlo"
        },
        {
            "name": "files/DOCX",
            "summary": "DOCX reader resource",
            "advice": "Pulls the plain text out of a .docx and nothing more: no styling, no tables as tables, no images. Paragraphs come out as a list, which is what makes it usable for search and for feeding a model, and DOCX::toText() is the one-liner for that.",
            "tags": [
                "file",
                "docx",
                "word",
                "reader"
            ],
            "package": "files",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/files/DOCX.phlo"
        },
        {
            "name": "files/file",
            "summary": "File resource",
            "advice": "Wrap a path in %file and everything about it is one call away: contents, size, mime, hashes, human dates, and a token() derived from the file's sha1, so the same content always yields the same token. Watch the difference between file and name: file is where it sits, name is what it is called, and ext and mime read the name. An upload therefore keeps its extension while the temp path has none.",
            "tags": [
                "file",
                "filesystem",
                "io"
            ],
            "package": "files",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/files/file.phlo"
        },
        {
            "name": "files/img",
            "summary": "GD image resource",
            "advice": "Only scales down, never up: a request larger than the original returns the image untouched, so a thumbnail never looks stretched. The output format follows the extension you save to, so saving a .jpg writes JPEG at quality 85 whatever came in. Pass crop with a width and a height to fill the frame instead of fitting inside it, and top or bottom to choose which part survives.",
            "tags": [
                "image",
                "gd",
                "file",
                "graphics"
            ],
            "package": "files",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/files/img.phlo"
        },
        {
            "name": "files/INI",
            "summary": "Generic INI resource",
            "advice": "The same shape as the JSON resource, saved when the object goes out of scope, but writing flattens the file: comments and section headers do not survive a round trip. Keep it to values a program owns; a file a person edits by hand deserves to be read rather than rewritten.",
            "tags": [
                "file",
                "ini",
                "config",
                "parser"
            ],
            "package": "files",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/files/INI.phlo"
        },
        {
            "name": "files/JSON",
            "summary": "Generic JSON resource",
            "advice": "The file behaves as an object: read a key, write a key, and the file is saved when the object goes out of scope. That last part is the trap: nothing is written until then, so call objWrite() yourself when the request may end otherwise. Names are read against data/ and a slash in the name becomes a dot, so no name can escape the directory.",
            "tags": [
                "file",
                "json",
                "storage",
                "parser"
            ],
            "package": "files",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/files/JSON.phlo"
        },
        {
            "name": "files/PDF",
            "summary": "PDF generator and reader",
            "advice": "Two halves that share a name. Reading uses the pdftotext binary, so it needs poppler-utils on the machine and gives nothing on a scan without a text layer. Writing renders HTML through mPDF, and mode is the mPDF one: D sends a download, I shows it in the browser, S returns the bytes as a string, so leaving the default on an API route pushes a download at your caller.",
            "tags": [
                "file",
                "pdf",
                "reader",
                "generator"
            ],
            "package": "files",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/files/PDF.phlo"
        },
        {
            "name": "files/UBL",
            "summary": "UBL 2.1 invoice XML (PEPPOL BIS Billing 3.0) from a normalized invoice structure. Use UBL::invoice($data).",
            "advice": "Builds the invoice XML that PEPPOL expects from a plain array with supplier, customer and lines. Tax is grouped by rate and each group gets its own subtotal, so lines at 21 and 9 percent land in the right boxes on their own. It formats and escapes the amounts you hand it but checks nothing: a total that does not match its lines is written out as given, and a receiver will reject it.",
            "tags": [
                "ubl",
                "peppol",
                "invoice",
                "xml",
                "e-invoicing",
                "export"
            ],
            "package": "files",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/files/UBL.phlo"
        },
        {
            "name": "files/XLSX",
            "summary": "XLSX reader resource",
            "advice": "Reads a workbook without a library by unpacking the zip itself, and gives every sheet by name with the first row as the header. Values arrive as the sheet stored them, so a date is the serial number Excel keeps and a percentage is a fraction; convert those yourself. Formulas give the last calculated value, so a sheet that was never opened after an edit hands you the old one.",
            "tags": [
                "file",
                "xlsx",
                "excel",
                "reader"
            ],
            "package": "files",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/files/XLSX.phlo"
        },
        {
            "name": "loaders/binary",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/loaders/binary.phlo"
        },
        {
            "name": "loaders/cassette",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/loaders/cassette.phlo"
        },
        {
            "name": "loaders/dialup",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/loaders/dialup.phlo"
        },
        {
            "name": "loaders/kitchen",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/loaders/kitchen.phlo"
        },
        {
            "name": "loaders/matrixprint",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/loaders/matrixprint.phlo"
        },
        {
            "name": "loaders/terminal",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/loaders/terminal.phlo"
        },
        {
            "name": "loaders/trace",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/loaders/trace.phlo"
        },
        {
            "name": "payments/Stripe",
            "summary": "Thin Stripe wrappers: Checkout, Billing Portal, prices, customers, subscriptions and webhook verification. Call Stripe::boot($secret) first. Requires the Stripe PHP SDK (composer: stripe/stripe-php).",
            "advice": "A thin layer over the Stripe SDK, which has to be installed and booted with Stripe::boot($secret) before anything else. Checkout and the billing portal are hosted by Stripe, so card details never touch your server. Verify a webhook before you read it: an unverified POST is a stranger claiming a payment, and verifyWebhook() is what turns it into a fact.",
            "tags": [
                "stripe",
                "payments",
                "checkout",
                "subscription",
                "billing",
                "webhook"
            ],
            "package": "payments",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/payments/Stripe.phlo"
        },
        {
            "name": "payments/SumUp",
            "summary": "SumUp connector: card-present checkouts on paired Solo readers via the Cloud API, transaction lookup and history",
            "advice": "Cloud-initiated terminal payments: the POS creates a checkout for a paired reader over HTTPS, the reader wakes up with the amount, and the result arrives on the return_url webhook or by polling transaction(). A checkout must start on the device within 60 seconds and only one checkout can be active per reader at a time. Treat the webhook as a wake-up call, never as the verdict: re-fetch the transaction with transaction() before recording a payment, so a forged POST can never book money.",
            "tags": [
                "sumup",
                "payments",
                "terminal",
                "reader",
                "card-present",
                "checkout",
                "connector"
            ],
            "package": "payments",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/payments/SumUp.phlo"
        },
        {
            "name": "security/audit",
            "summary": "Audit log for model mutations (opt-in via static idColumn/objAudit). Schema: resources/security/audit.sql",
            "advice": "Off unless a model sets objAudit, and then every create, change and delete is written: an update as the difference between before and after, a create as the new row, a delete as the row that went. Pass exclude for columns you would rather not keep, a password hash or a token; the log outlives the record, so what goes in is a decision, not a detail. purge() is there because a log nobody prunes eventually costs more than the table it watches.",
            "tags": [
                "audit",
                "log",
                "compliance",
                "traceability"
            ],
            "package": "security",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/security/audit.phlo"
        },
        {
            "name": "security/captcha",
            "summary": "Self-contained interactive slider-puzzle captcha (no external service). The server picks a secret gap position and renders the background plus a loose piece with GD; the client drags the piece into place. verify() checks the end position plus human drag behaviour (time, path, variation). Single-use and session-bound; the gap position never leaves the server.",
            "advice": "Runs entirely on your own server, so no visitor is handed to a third party and nothing has to be disclosed in a privacy statement. The gap position never leaves the server, and it judges the drag as well as the endpoint, so a script that jumps straight to the answer is refused. verify() does not consume the puzzle: call consume() yourself, and only on success, or a failed attempt costs the visitor their challenge. It needs GD.",
            "tags": [
                "captcha",
                "spam",
                "bot",
                "human-verification",
                "security"
            ],
            "package": "security",
            "frontend": true,
            "backend": true,
            "file": "/phlo/resources/security/captcha.phlo"
        },
        {
            "name": "security/creds",
            "summary": "Credentials resolver from env and ini sources",
            "advice": "One place for every secret, filled from data/creds.ini and from the environment, where PHLO__Section__key sets a value and PHLO_<HOST>__Section__key overrides it for one host, with <HOST> the request host uppercased and every other character an underscore. Values are wrapped so a var_dump or an error page shows stars instead of the secret. Keep the ini file out of the repository and let the environment win on a server.",
            "tags": [
                "credentials",
                "env",
                "ini",
                "secrets",
                "configuration"
            ],
            "package": "security",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/security/creds.phlo"
        },
        {
            "name": "security/CSRF",
            "summary": "Rotating async CSRF protection for Phlo requests",
            "advice": "Put %CSRF in your head view and it writes the meta tag the frontend reads from. The rest you wire yourself: verify() checks the X-CSRF-Token header against the session, and update() answers with a fresh token as a command the page applies to its meta tag. A route that does both makes a stolen token worth one request at most, which is the whole point; a route that only verifies keeps one token for the life of the session. It protects a session, so it says nothing about an API authenticated with a bearer token.",
            "tags": [
                "csrf",
                "security",
                "async",
                "forms"
            ],
            "package": "security",
            "frontend": true,
            "backend": true,
            "file": "/phlo/resources/security/CSRF.phlo"
        },
        {
            "name": "security/encryption",
            "summary": "Encrypt and decrypt secretbox payloads using a key",
            "advice": "Authenticated encryption through libsodium: a fresh nonce per call travels with the value, so encrypting the same text twice gives different output and a changed ciphertext refuses to decrypt rather than returning rubbish. decrypt() gives false on a failure, so test with === false and not on falsiness. The key is hashed to the right length, which means any string works, but a short one is still a short secret.",
            "tags": [
                "encrypt",
                "decrypt",
                "encryption",
                "sodium",
                "secretbox",
                "crypto"
            ],
            "package": "security",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/security/encryption.phlo"
        },
        {
            "name": "security/JWT",
            "summary": "Sign and verify compact HS256 JSON Web Tokens (RFC 7519), secure by default",
            "advice": "HS256 only, and a token that claims another algorithm is refused rather than tried, which is the classic way these are broken. The secret must be at least 32 bytes, an expiry is always written, and verify() throws with a 401 instead of returning false, so a route can simply call it. Name an issuer and it is checked as well. A signed token is readable by anyone holding it, so keep secrets out of the claims.",
            "tags": [
                "jwt",
                "jws",
                "hs256",
                "token",
                "auth",
                "security"
            ],
            "package": "security",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/security/JWT.phlo"
        },
        {
            "name": "security/OAuth2",
            "summary": "Stateless OAuth2 client: build the authorize URL and exchange/refresh tokens. Token storage and config are the caller's responsibility. The protocol primitive under TokenStore and OAuthConnector.",
            "advice": "The bare protocol: build an authorize URL, trade a code for tokens, refresh. It keeps nothing and knows nothing about your app, which is what makes it usable for any provider. For a connector you almost never need it directly, since TokenStore and OAuthConnector do the keeping for you; reach for it when you run the login yourself.",
            "tags": [
                "oauth",
                "oauth2",
                "token",
                "authorization",
                "refresh",
                "authentication"
            ],
            "package": "security",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/security/OAuth2.phlo"
        },
        {
            "name": "security/rate",
            "summary": "Rate-limit (fixed window) on the rate_limit table. Schema: resources/security/rate.sql",
            "advice": "A fixed window, counted in one atomic statement, so two requests arriving together cannot both slip past the limit. Storage db survives a restart and is shared across machines; apcu is faster but lives in one server's shared memory, so it is gone after a restart and says nothing about a second machine. Because the window is fixed rather than sliding, a caller can spend a full limit at the end of one window and again at the start of the next.",
            "tags": [
                "rate",
                "limit",
                "throttle",
                "abuse"
            ],
            "package": "security",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/security/rate.phlo"
        },
        {
            "name": "security/security",
            "summary": "Generic security resource",
            "advice": "Pick a profile per app rather than writing headers yourself: strict allows only nonced scripts and styles, basic allows your own files, marketing also allows images from anywhere, and api shuts everything off and marks the response as an API. Async responses get no policy, because the page they land in already has one. Add a CDN or a media host to sources and a domain that may frame you to whitelist; those are the two escape hatches, and everything else stays closed. Under debug, basic and marketing let inline scripts through so the debug console runs, so a production site with debug on is running a weaker policy than it thinks.",
            "tags": [
                "security",
                "csp",
                "nonce",
                "headers"
            ],
            "package": "security",
            "frontend": true,
            "backend": true,
            "file": "/phlo/resources/security/security.phlo"
        },
        {
            "name": "security/social",
            "summary": "Reusable social login (OIDC) on top of OAuth2: build the authorize URL and turn a callback code into a verified profile. Google, Microsoft and Apple. No user, session or route handling - that is the caller's responsibility.",
            "advice": "The id_token is verified against the provider's JWKS (RS256 only, key looked up by kid) before any claim is read, on top of issuer, audience (must equal client_id), expiry and, when supplied, nonce. `verified` reports what the provider actually proved: Microsoft omits email_verified, so it counts only when the optional xms_edov claim states the tenant owns the address. Treat an unverified email as a claim, never as an identity: match users on provider + sub. Apple's client_secret is an ES256 JWT signed with the .p8 key (team_id/key_id/client_id + key from creds).",
            "tags": [
                "oauth",
                "oidc",
                "social",
                "login",
                "google",
                "microsoft",
                "apple",
                "authentication"
            ],
            "package": "security",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/security/social.phlo"
        },
        {
            "name": "security/token",
            "summary": "Generate deterministic or random lowercase token",
            "advice": "Lowercase letters only, so a token survives being read aloud, typed by hand or used in a URL without escaping. Pass an input and the token is derived from it, so the same input always gives the same token: exactly what you want for a file or a record, and exactly what you do not want for a secret. Leave the input out for anything that must be unguessable.",
            "tags": [
                "token",
                "random",
                "deterministic",
                "security"
            ],
            "package": "security",
            "frontend": false,
            "backend": true,
            "file": "/phlo/resources/security/token.phlo"
        },
        {
            "name": "themes/aurasteel",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/themes/aurasteel.phlo"
        },
        {
            "name": "themes/biolux",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/themes/biolux.phlo"
        },
        {
            "name": "themes/chromaforge",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/themes/chromaforge.phlo"
        },
        {
            "name": "themes/cobalt",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/themes/cobalt.phlo"
        },
        {
            "name": "themes/copperflare",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/themes/copperflare.phlo"
        },
        {
            "name": "themes/crimson",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/themes/crimson.phlo"
        },
        {
            "name": "themes/dark",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/themes/dark.phlo"
        },
        {
            "name": "themes/darknova",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/themes/darknova.phlo"
        },
        {
            "name": "themes/deepgrove",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/themes/deepgrove.phlo"
        },
        {
            "name": "themes/elegant",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/themes/elegant.phlo"
        },
        {
            "name": "themes/forestdusk",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/themes/forestdusk.phlo"
        },
        {
            "name": "themes/galaxytwist",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/themes/galaxytwist.phlo"
        },
        {
            "name": "themes/ionstorm",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/themes/ionstorm.phlo"
        },
        {
            "name": "themes/ironspectrum",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/themes/ironspectrum.phlo"
        },
        {
            "name": "themes/jadeember",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/themes/jadeember.phlo"
        },
        {
            "name": "themes/kiro",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/themes/kiro.phlo"
        },
        {
            "name": "themes/light",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/themes/light.phlo"
        },
        {
            "name": "themes/lightgrid",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/themes/lightgrid.phlo"
        },
        {
            "name": "themes/limepulse",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/themes/limepulse.phlo"
        },
        {
            "name": "themes/midnight",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/themes/midnight.phlo"
        },
        {
            "name": "themes/neon",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/themes/neon.phlo"
        },
        {
            "name": "themes/oceandeep",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/themes/oceandeep.phlo"
        },
        {
            "name": "themes/plasmashift",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/themes/plasmashift.phlo"
        },
        {
            "name": "themes/polar",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/themes/polar.phlo"
        },
        {
            "name": "themes/quantumrift",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/themes/quantumrift.phlo"
        },
        {
            "name": "themes/steelwave",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/themes/steelwave.phlo"
        },
        {
            "name": "themes/synthdream",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/themes/synthdream.phlo"
        },
        {
            "name": "themes/voidpulse",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/themes/voidpulse.phlo"
        },
        {
            "name": "themes/whitebolt",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/themes/whitebolt.phlo"
        },
        {
            "name": "themes/zen",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/themes/zen.phlo"
        },
        {
            "name": "themes/zulo",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/themes/zulo.phlo"
        },
        {
            "name": "transitions/cards",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/transitions/cards.phlo"
        },
        {
            "name": "transitions/cube",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/transitions/cube.phlo"
        },
        {
            "name": "transitions/curtain",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/transitions/curtain.phlo"
        },
        {
            "name": "transitions/diagonal",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/transitions/diagonal.phlo"
        },
        {
            "name": "transitions/diamond",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/transitions/diamond.phlo"
        },
        {
            "name": "transitions/diaphragm",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/transitions/diaphragm.phlo"
        },
        {
            "name": "transitions/drop",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/transitions/drop.phlo"
        },
        {
            "name": "transitions/flip",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/transitions/flip.phlo"
        },
        {
            "name": "transitions/glide",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/transitions/glide.phlo"
        },
        {
            "name": "transitions/glitch",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/transitions/glitch.phlo"
        },
        {
            "name": "transitions/ripple",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/transitions/ripple.phlo"
        },
        {
            "name": "transitions/skew",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/transitions/skew.phlo"
        },
        {
            "name": "transitions/slide",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/transitions/slide.phlo"
        },
        {
            "name": "transitions/spiral",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/transitions/spiral.phlo"
        },
        {
            "name": "transitions/spotlight",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/transitions/spotlight.phlo"
        },
        {
            "name": "transitions/tilt",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/transitions/tilt.phlo"
        },
        {
            "name": "transitions/tv",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/transitions/tv.phlo"
        },
        {
            "name": "transitions/wipe",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/transitions/wipe.phlo"
        },
        {
            "name": "transitions/zoom",
            "summary": null,
            "advice": null,
            "tags": [],
            "package": null,
            "frontend": null,
            "backend": null,
            "file": "/phlo/resources/transitions/zoom.phlo"
        }
    ]
}