# Phlo Manual

Auto-generated reference of every built-in object and function in the Phlo runtime. Source: `/srv/control/phlo/resources/` plus core `/srv/control/phlo/phlo.php`.

## Core

### %cookies

Cookies data object

- File: `resources/cookies.phlo`
- `cookies->controller` (line 9)
- `cookies->lifetimeDays` (line 11)
- `cookies->objSet($key, $value, array $options = [])` (line 13)
- `cookies->__unset($key)` (line 21)

### %lang

Language and translation resource

- File: `resources/lang.phlo`
- `function nl($text, ...$args)` (line 11)
- `function en($text, ...$args)` (line 12)
- `lang::asyncBatch($from, $to, $json)` (line 14)
- `lang->view` (line 21)
- `lang->model` (line 23)
- `lang->instructions` (line 24)
- `lang::fileCache` (line 25)
- `lang->file($lang)` (line 27)
- `lang->escape($value)` (line 29)
- `lang->unescape($value)` (line 30)
- `lang->lineValue($line, $eq)` (line 32)
- `lang->readAll($file)` (line 38)
- `lang->search($file, $hash)` (line 49)
- `lang->lookup($hash)` (line 95)
- `lang->save($lang, $pairs)` (line 106)
- `lang->transContext` (line 120)
- `lang->browser` (line 122)
- `lang->cookie` (line 123)
- `lang->detect($text, $fallback = 'en')` (line 124)
- `lang->hash($from, $text)` (line 133)
- `lang->translation($from, $text, ...$args)` (line 134)
- `lang->translate($from, $to, $text)` (line 153)
- `lang->translateBatch($from, $to, $texts)` (line 162)

### %payload

POST, PUT, PATCH, QUERY and file-upload data object

- File: `resources/payload.phlo`
- `payload->controller` (line 10)

### %seo

Multilingual SEO: sitemap.xml, robots.txt, hreflang + head meta (description/OG/Twitter/canonical)

- File: `resources/seo.phlo`
- `route GET sitemap.xml` (line 10)
- `route GET robots.txt` (line 12)
- `seo->robots` (line 14)
- `seo->intl($uri)` (line 22)
- `seo->locale` (line 24)
- `seo->ogTitle` (line 30)
- `seo->ogDescr` (line 31)
- `seo->ogImage` (line 32)
- `seo->canonical` (line 33)
- `seo->ogType` (line 34)
- `seo->siteName` (line 35)
- `seo->twitterCard` (line 36)
- `seo->noIndex` (line 37)
- `seo->view` (line 39)
- `seo->page($uri)` (line 47)
- `seo->xlink($lang, $uri)` (line 58)
- `seo->link($lang, $uri)` (line 59)
- `seo->head` (line 61)

### %session

Session data object

- File: `resources/session.phlo`
- `session->controller` (line 9)
- `session->__set($key, $value)` (line 12)
- `session->__unset($key)` (line 13)
- `session->__isset($key)` (line 14)
- `session->objRegenerateId($deleteOld = true)` (line 16)

### %stream

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

- File: `resources/stream.phlo`
- `function stream($data = null, string $type = 'application/octet-stream', ?string $name = null):void` (line 10)

### %tasks

Cron runner for %app->tasks. One cron entry per app triggers this every minute.

- File: `resources/tasks.phlo`
- `tasks::dir` (line 9)
- `tasks::run` (line 11)
- `tasks::saveRun($name, $do, $schedule, $return)` (line 26)
- `tasks::due($name, $task, $now)` (line 28)
- `tasks::fire($do)` (line 46)
- `tasks::lastRun($name)` (line 56)
- `tasks::markRun($name, $ts)` (line 61)
- `tasks::lock($name)` (line 63)
- `tasks::unlock($name)` (line 70)

### %useragent

User agent information

- File: `resources/useragent.phlo`
- `useragent->source` (line 9)
- `useragent->os` (line 11)
- `useragent->osV` (line 27)
- `useragent->osFull` (line 38)
- `useragent->name` (line 48)
- `useragent->version` (line 65)
- `useragent->full` (line 76)
- `useragent->device` (line 85)

### %visitors

Visitor tracking via heartbeat

- File: `resources/visitors.phlo`
- `visitors::table` (line 11)
- `visitors::columns` (line 12)
- `visitors::history` (line 14)
- `visitors::online` (line 15)
- `visitors::lastHour` (line 16)
- `visitors::isBot(?string $ua):bool` (line 18)
- `visitors::parseReferrer(string $url):string` (line 23)
- `route PUT heartbeat` (line 30)

### %websocket

Server-side WebSocket handler via phloWS

- File: `resources/websocket.phlo`
- `websocket::connect($wsHost, $wsToken, $wsSocket)` (line 10)
- `websocket::auth($wsHost, $wsToken, $wsSocket)` (line 11)
- `websocket::receive($wsHost, $wsToken, $wsSocket, $data)` (line 12)
- `websocket::close($wsHost, $wsToken, $wsSocket)` (line 13)

### %WhatsApp

WhatsApp client for phloWA using whatsapp-web.js

- File: `resources/WhatsApp.phlo`
- `WhatsApp->__construct(public string $url, public string $secret)` (line 10)
- `WhatsApp::channel($channel)` (line 12)
- `WhatsApp->number($contact)` (line 14)
- `WhatsApp->isGroup($contact)` (line 15)
- `WhatsApp->status` (line 17)
- `WhatsApp->health` (line 18)
- `WhatsApp->qr` (line 19)
- `WhatsApp->disconnect` (line 20)
- `WhatsApp->read($chat)` (line 22)
- `WhatsApp->reaction($msg, $emoji)` (line 23)
- `WhatsApp->text($to, $text)` (line 25)
- `WhatsApp->image($to, file $file, $text = void)` (line 26)
- `WhatsApp->location($to, $lat, $lon, $text)` (line 27)
- `WhatsApp->document($to, file $file, $text = void)` (line 28)
- `WhatsApp->audio($to, file $file)` (line 30)
- `WhatsApp->voice($to, file $file)` (line 31)
- `WhatsApp->poll($to, $name, array $options, bool $multi = false)` (line 33)
- `WhatsApp->startTyping($to)` (line 35)
- `WhatsApp->stopTyping($to)` (line 36)
- `WhatsApp->request($action, ...$data)` (line 38)

### Functions

- `active(bool $cond, string $classList = void)`
- `age(int $time)`
- `age_human(int $age)`
- `apcu($key, $cb, int $duration = 3600, bool $log = true)`
- `await(...$jobs)`
- `button(...$args):string`
- `camel(string $text)`
- `chunk(...$cmds):void`
- `create(iterable $items, \Closure $keyCb, ?\Closure $valueCb = null)`
- `en($text, ...$args)`
- `exec_stream(string $cmd, ?int $timeoutSec = 0)`
- `HTTP(string $url, array $headers = [], bool $JSON = false, $POST = null, $PUT = null, $PATCH = null, $QUERY = null, bool $DELETE = false, string|bool|null $agent = null, string|bool $cookies = false, int $timeout = 15, &$response = null)`
- `input(...$args):string`
- `n8n($webhook, ?array $data = null, $test = false)`
- `nl($text, ...$args)`
- `notify(string $title, string $body = void, string $type = 'info', string $level = 'info', ?string $user = null):void`
- `phlo(?string $phloName = null, ...$args):mixed`
- `phlo_app(...$args):void`
- `phlo_async(string $cb, ...$args)`
- `phlo_cli(array $args):void`
- `phlo_dispatch(string $target, array $args = []):mixed`
- `phlo_exception(Throwable $e):void`
- `phlo_exists(string $obj)`
- `phlo_load(bool $http):void`
- `phlo_serve():void`
- `phlo_stream(string $cb, ...$args)`
- `phlo_sync(string $cb, ...$args)`
- `phlo_thread():void`
- `select(...$args):string`
- `setting(?string $key = null, $value = null)`
- `slug(string $text)`
- `stream($data = null, string $type = 'application/octet-stream', ?string $name = null):void`
- `tag(string $tagName, ?string $inner = null, ...$args)`
- `textarea(...$args):string`
- `time_human(?int $time = null)`
- `wsCast($wsTarget = 'all', $wsHost = host, $wsPort = daemon, ...$data)`

## AI

### %AI

Unified AI facade with engine auto-detect

- File: `resources/AI/AI.phlo`
- `AI->model` (line 10)
- `AI::engines` (line 11)
- `AI::http(string $url, array $headers, bool $json = true, mixed $post = null):string` (line 12)
- `AI->resolve(...$args):array` (line 13)
- `AI->chat(...$args)` (line 21)
- `AI->stream(...$args):Generator` (line 25)
- `AI->embedding(...$args)` (line 29)
- `AI->vision(...$args)` (line 33)
- `AI->transcribe(...$args)` (line 37)

### %Claude

Anthropic Claude API

- File: `resources/AI/Claude.phlo`
- `Claude::model` (line 10)
- `Claude::context(...$args)` (line 12)
- `Claude::tool($tool):array` (line 22)
- `Claude->embedding($input, $model = 'text-embedding-3-small')` (line 32)
- `Claude->vision($text, $image, $stream = false, ...$args)` (line 34)
- `Claude->chat(...$args)` (line 41)
- `Claude->parseSSE(string $url, array $headers, array $payload):Generator` (line 58)
- `Claude->stream(...$args):Generator` (line 90)
- `Claude->request($uri, ...$args)` (line 103)

### %DeepSeek

DeepSeek API (OpenAI-compatible, extends OpenAI)

- File: `resources/AI/DeepSeek.phlo`
- `DeepSeek::model` (line 11)
- `DeepSeek::endpoint` (line 12)
- `DeepSeek::cred` (line 13)
- `DeepSeek::label` (line 14)
- `DeepSeek->embedding($input, $model = 'text-embedding-3-small')` (line 16)

### %Gemini

Google Gemini API

- File: `resources/AI/Gemini.phlo`
- `Gemini::model` (line 10)
- `Gemini::endpoint` (line 11)
- `Gemini::context(...$args)` (line 13)
- `Gemini::tool($tool):array` (line 24)
- `Gemini->embedding($input, $model = 'text-embedding-004')` (line 34)
- `Gemini->vision($text, $image, $stream = false, ...$args)` (line 36)
- `Gemini->chat(...$args)` (line 44)
- `Gemini->parseSSE(string $url, array $headers, array $payload):Generator` (line 59)
- `Gemini->stream(...$args):Generator` (line 90)
- `Gemini->request($path, ...$args)` (line 98)

### %Grok

xAI Grok API (OpenAI-compatible, extends OpenAI)

- File: `resources/AI/Grok.phlo`
- `Grok::model` (line 11)
- `Grok::endpoint` (line 12)
- `Grok::cred` (line 13)
- `Grok::label` (line 14)
- `Grok->embedding($input, $model = 'text-embedding-3-small')` (line 16)

### %OpenAI

Basic OpenAI functions

- File: `resources/AI/OpenAI.phlo`
- `OpenAI::model` (line 10)
- `OpenAI::endpoint` (line 11)
- `OpenAI::cred` (line 12)
- `OpenAI::label` (line 13)
- `OpenAI::voices` (line 14)
- `OpenAI::context(...$args):array` (line 15)
- `OpenAI::tool($tool):array` (line 22)
- `OpenAI->chat(...$args):obj` (line 36)
- `OpenAI->embedding($input, $model = 'text-embedding-3-small')` (line 46)
- `OpenAI->parseSSE(string $url, array $headers, array $payload):Generator` (line 47)
- `OpenAI->stream(...$args):Generator` (line 78)
- `OpenAI->transcribe($file, $model = 'whisper-1', ...$args):obj` (line 88)
- `OpenAI->vision($text, $image, $stream = false, ...$args):obj` (line 99)
- `OpenAI->request($uri, $JSON = true, $token = null, ...$args)` (line 105)

### Functions

- `answer($question, ...$options)`

## connectors

### %Connector

Base class for API connectors: credentials, JSON requests, retries, pagination and a normalized result contract

- File: `resources/connectors/Connector.phlo`
- `Connector::section` (line 10)
- `Connector::api` (line 11)
- `Connector->__construct(?array $config = null)` (line 13)
- `Connector::make(?array $config = null):static` (line 24)
- `Connector->base` (line 26)
- `Connector->headers` (line 27)
- `Connector::fields` (line 29)
- `Connector->configured(...$keys):bool` (line 31)
- `Connector->missing(...$keys):?obj` (line 38)
- `Connector::bearer($token):string` (line 42)
- `Connector::basic($user, $pass):string` (line 43)
- `Connector::build(string $method, string $url, ?array $query = null, array $headers = [], mixed $json = null, mixed $form = null):array` (line 45)
- `Connector::ok($data, int $status = 200):obj` (line 60)
- `Connector::fail($error, int $status = 0):obj` (line 61)
- `Connector::errorMessage($data, string $raw, int $status):string` (line 63)
- `Connector::parse($raw, int $status = 200):obj` (line 78)
- `Connector::retryable($method, int $status):bool` (line 85)
- `Connector::backoff(int $attempt, $response):int` (line 87)
- `Connector->dispatch(array $req):obj` (line 92)
- `Connector->request(string $method, string $url, ?array $query = null, array $headers = [], mixed $json = null, mixed $form = null):obj` (line 118)
- `Connector->get(string $url, ?array $query = null, array $headers = []):obj` (line 124)
- `Connector->post(string $url, mixed $json = null, array $headers = []):obj` (line 125)
- `Connector->put(string $url, mixed $json = null, array $headers = []):obj` (line 126)
- `Connector->patch(string $url, mixed $json = null, array $headers = []):obj` (line 127)
- `Connector->query(string $url, mixed $json = null, array $headers = []):obj` (line 128)
- `Connector->del(string $url, array $headers = []):obj` (line 129)
- `Connector->form(string $url, array $fields, array $headers = []):obj` (line 130)
- `Connector->paginate(string $url, callable $extract, ?array $query = null, string $param = 'page', int $start = 1, int $max = 0):array` (line 132)

### %EBoekhouden

e-Boekhouden.nl connector: session auth from an API token, relations and sales invoices

- File: `resources/connectors/finance/EBoekhouden.phlo`
- `EBoekhouden::section` (line 11)
- `EBoekhouden::api` (line 12)
- `EBoekhouden->sessionToken` (line 14)
- `EBoekhouden->headers` (line 16)
- `EBoekhouden::fields` (line 18)
- `EBoekhouden->session:obj` (line 25)
- `EBoekhouden->guard:?obj` (line 36)
- `EBoekhouden->relations(array $query = []):obj` (line 41)
- `EBoekhouden->createRelation(array $relation):obj` (line 46)
- `EBoekhouden->invoices(array $query = []):obj` (line 51)
- `EBoekhouden->createInvoice(array $invoice):obj` (line 56)

### %ExactOnline

Exact Online connector (OAuth2): read sales invoices and accounts, create sales invoices

- File: `resources/connectors/finance/ExactOnline.phlo`
- `ExactOnline::section` (line 11)
- `ExactOnline::tokenUrl` (line 12)
- `ExactOnline->base` (line 14)
- `ExactOnline::fields` (line 16)
- `ExactOnline->guard` (line 27)
- `ExactOnline->invoices(array $query = []):obj` (line 29)
- `ExactOnline->accounts(array $query = []):obj` (line 34)
- `ExactOnline->createInvoice(array $invoice):obj` (line 39)

### %GoogleCalendar

Google Calendar connector (OAuth2): read events and create events

- File: `resources/connectors/cloud/GoogleCalendar.phlo`
- `GoogleCalendar::section` (line 11)
- `GoogleCalendar::tokenUrl` (line 12)
- `GoogleCalendar->base` (line 14)
- `GoogleCalendar::fields` (line 16)
- `GoogleCalendar->guard` (line 26)
- `GoogleCalendar->events(string $calendarId = 'primary', array $query = []):obj` (line 28)
- `GoogleCalendar->createEvent(array $event, string $calendarId = 'primary'):obj` (line 33)

### %GoogleSheets

Google Sheets connector (OAuth2): read ranges and append rows

- File: `resources/connectors/cloud/GoogleSheets.phlo`
- `GoogleSheets::section` (line 11)
- `GoogleSheets::tokenUrl` (line 12)
- `GoogleSheets->base` (line 14)
- `GoogleSheets::fields` (line 16)
- `GoogleSheets->guard` (line 26)
- `GoogleSheets->values($spreadsheetId, string $range):obj` (line 28)
- `GoogleSheets->append($spreadsheetId, string $range, array $rows, string $valueInputOption = 'USER_ENTERED'):obj` (line 33)

### %Lightspeed

Lightspeed Retail (V3) connector: read customers and sales, create customers

- File: `resources/connectors/shops/Lightspeed.phlo`
- `Lightspeed::section` (line 11)
- `Lightspeed->base` (line 13)
- `Lightspeed->headers` (line 15)
- `Lightspeed::fields` (line 17)
- `Lightspeed->customers(array $query = []):obj` (line 30)
- `Lightspeed->findCustomer($participant):obj` (line 35)
- `Lightspeed->customer($id):obj` (line 41)
- `Lightspeed->sales(array $query = []):obj` (line 46)
- `Lightspeed->createCustomer(array $customer):obj` (line 51)

### %MessageBird

MessageBird connector: send SMS

- File: `resources/connectors/chat/MessageBird.phlo`
- `MessageBird::section` (line 11)
- `MessageBird->base` (line 13)
- `MessageBird->headers` (line 15)
- `MessageBird::fields` (line 17)
- `MessageBird::errorMessage($data, string $raw, int $status):string` (line 23)
- `MessageBird->sms($to, $body, array $extra = []):obj` (line 28)

### %MicrosoftGraph

Microsoft Graph connector (app-only client credentials): read users and calendars, send mail, create events

- File: `resources/connectors/cloud/MicrosoftGraph.phlo`
- `MicrosoftGraph::section` (line 11)
- `MicrosoftGraph->base` (line 13)
- `MicrosoftGraph->headers` (line 15)
- `MicrosoftGraph::fields` (line 17)
- `MicrosoftGraph->token` (line 28)
- `MicrosoftGraph->fetchToken` (line 30)
- `MicrosoftGraph->mailbox($user = null)` (line 48)
- `MicrosoftGraph->users(array $query = []):obj` (line 50)
- `MicrosoftGraph->user($id):obj` (line 55)
- `MicrosoftGraph->events($user = null, array $query = []):obj` (line 60)
- `MicrosoftGraph->sendMail($message, $user = null, bool $save = true):obj` (line 67)
- `MicrosoftGraph->createEvent(array $event, $user = null):obj` (line 74)

### %Moneybird

Moneybird connector: read contacts and invoices, create sales invoices

- File: `resources/connectors/finance/Moneybird.phlo`
- `Moneybird::section` (line 11)
- `Moneybird->base` (line 13)
- `Moneybird->headers` (line 15)
- `Moneybird::fields` (line 17)
- `Moneybird->contacts(array $query = []):obj` (line 23)
- `Moneybird->findContact($query):obj` (line 28)
- `Moneybird->contact($id):obj` (line 33)
- `Moneybird->invoices(array $query = []):obj` (line 38)
- `Moneybird->createContact(array $contact):obj` (line 43)
- `Moneybird->createInvoice(array $invoice):obj` (line 48)

### %OAuthConnector

Base class for OAuth2 connectors: stored, auto-refreshed bearer access tokens via TokenStore, on the OAuth2 primitive

- File: `resources/connectors/OAuthConnector.phlo`
- `OAuthConnector::tokenUrl` (line 11)
- `OAuthConnector->oauthKey` (line 13)
- `OAuthConnector->token` (line 15)
- `OAuthConnector->headers` (line 17)
- `OAuthConnector->authed` (line 19)

### %Resend

Resend connector: send transactional email via the HTTP API

- File: `resources/connectors/chat/Resend.phlo`
- `Resend::section` (line 11)
- `Resend->base` (line 13)
- `Resend->headers` (line 15)
- `Resend::fields` (line 17)
- `Resend->send($to, $subject, $html = void, array $extra = []):obj` (line 23)

### %Shopify

Shopify Admin API connector: read customers, orders and products; create draft orders and products; update inventory

- File: `resources/connectors/shops/Shopify.phlo`
- `Shopify::section` (line 11)
- `Shopify->base` (line 13)
- `Shopify->headers` (line 15)
- `Shopify::fields` (line 17)
- `Shopify->customers(array $query = []):obj` (line 27)
- `Shopify->searchCustomers($query, int $limit = 10):obj` (line 32)
- `Shopify->customer($id):obj` (line 37)
- `Shopify->orders(array $query = []):obj` (line 42)
- `Shopify->products(array $query = []):obj` (line 47)
- `Shopify->createDraftOrder(array $order):obj` (line 52)
- `Shopify->createProduct(array $product):obj` (line 57)
- `Shopify->setInventory($inventoryItemId, $locationId, int $available):obj` (line 62)

### %Slack

Slack connector: post messages, read channel history and list channels

- File: `resources/connectors/chat/Slack.phlo`
- `Slack::section` (line 11)
- `Slack::api` (line 12)
- `Slack->headers` (line 14)
- `Slack::fields` (line 16)
- `Slack->result(obj $res):obj` (line 25)
- `Slack->send($channel, $text, array $extra = []):obj` (line 32)
- `Slack->history($channel, int $limit = 20):obj` (line 37)
- `Slack->channels(int $limit = 100, string $types = 'public_channel'):obj` (line 42)

### %Telegram

Telegram Bot API connector: send messages, photos and documents; poll updates

- File: `resources/connectors/chat/Telegram.phlo`
- `Telegram::section` (line 11)
- `Telegram->base` (line 13)
- `Telegram::fields` (line 15)
- `Telegram->result(obj $res):obj` (line 24)
- `Telegram->send($chatId, $text, array $extra = []):obj` (line 31)
- `Telegram->photo($chatId, $photo, $caption = void, array $extra = []):obj` (line 36)
- `Telegram->document($chatId, $document, $caption = void, array $extra = []):obj` (line 43)
- `Telegram->updates(int $offset = 0, int $limit = 100):obj` (line 50)

### %TokenStore

Persisted OAuth2 token store with automatic refresh via the OAuth2 resource

- File: `resources/connectors/TokenStore.phlo`
- `TokenStore::path($key):string` (line 10)
- `TokenStore::read($key):array` (line 12)
- `TokenStore::write($key, array $token):void` (line 20)
- `TokenStore::valid(array $token):bool` (line 29)
- `TokenStore::store($res, $refresh):array` (line 31)
- `TokenStore::lock($key)` (line 42)
- `TokenStore::access($key, $tokenUrl, $clientId, $clientSecret, array $seed = []):?string` (line 53)

### %Twilio

Twilio connector: send SMS and read message status

- File: `resources/connectors/chat/Twilio.phlo`
- `Twilio::section` (line 11)
- `Twilio->base` (line 13)
- `Twilio->headers` (line 15)
- `Twilio::fields` (line 17)
- `Twilio->sms($to, $body, array $extra = []):obj` (line 27)
- `Twilio->message($sid):obj` (line 38)

## DB

### %DB

Database engine class

- File: `resources/DB/DB.phlo`
- `DB->PDO` (line 11)
- `DB->fieldQuotes` (line 12)
- `DB->savepoint` (line 13)
- `DB->insertIgnore` (line 14)
- `DB->insertOnConflict` (line 15)
- `DB->load(string $table, string $columns = '*', string $where = void, string $joins = void, string $group = void, string $limit = void, string $order = void, ...$args)` (line 17)
- `DB->query($query, ...$args)` (line 29)
- `DB->queryRun($query, $args, $retry)` (line 35)
- `DB->goneAway($e)` (line 59)
- `DB->column(...$args)` (line 61)
- `DB->item(...$args)` (line 62)
- `DB->pair(...$args)` (line 63)
- `DB->group(...$args)` (line 64)
- `DB->records(...$args)` (line 65)
- `DB->rows(...$args)` (line 66)
- `DB->record(...$args)` (line 67)
- `DB->quoteList(array $ids)` (line 68)
- `DB->quoteId($id)` (line 72)
- `DB->create(string $table, ...$data)` (line 74)
- `DB->lastId` (line 83)
- `DB->change(string $table, string $where, ...$data)` (line 85)
- `DB->delete(string $table, string $where, ...$args)` (line 95)
- `DB->begin` (line 96)
- `DB->commit` (line 97)
- `DB->rollback` (line 98)
- `DB->transaction($callback)` (line 100)

### %JSONDB

JSON file database driver. One JSONDB instance = one JSON file = one model table. No joins, no transactions, no schema introspection.

- File: `resources/DB/JSONDB.phlo`
- `JSONDB::__handle` (line 11)
- `JSONDB->__construct(private string $file)` (line 12)
- `JSONDB->PDO` (line 16)
- `JSONDB->fieldQuotes` (line 17)
- `JSONDB->lastInsertedId` (line 18)
- `JSONDB->quoteList(array $ids)` (line 25)
- `JSONDB->objRead` (line 27)
- `JSONDB->objWrite(array $data)` (line 28)
- `JSONDB->objNextId(array $data)` (line 29)
- `JSONDB->objFilter(array $data, string $where = void, ...$args)` (line 31)
- `JSONDB->objSelect(string $where = void, string $limit = void, string $order = void, ...$args)` (line 55)
- `JSONDB->create(string $table, ...$data)` (line 66)
- `JSONDB->change(string $table, string $where, ...$data)` (line 80)
- `JSONDB->delete(string $table, string $where, ...$args)` (line 97)
- `JSONDB->load(string $table, string $columns = '*', string $where = void, string $joins = void, string $group = void, string $limit = void, string $order = void, ...$args)` (line 106)
- `JSONDB->query($query, ...$args)` (line 111)
- `JSONDB->begin` (line 113)
- `JSONDB->commit` (line 114)
- `JSONDB->rollback` (line 115)

### %JSON_result

Minimal PDOStatement-like wrapper for JSONDB result arrays

- File: `resources/DB/JSON.result.phlo`
- `JSON_result::__handle` (line 9)
- `JSON_result->data` (line 10)
- `JSON_result->__construct(array $data)` (line 11)
- `JSON_result->fetchAll($mode = 2)` (line 13)
- `JSON_result->fetchObject($class = 'obj')` (line 44)
- `JSON_result->fetch($mode = 2)` (line 52)
- `JSON_result->fetchColumn($col = 0)` (line 59)
- `JSON_result->rowCount` (line 66)

### %model

Phlo ORM class with unified columns and schema

- File: `resources/DB/model.phlo`
- `model::DB` (line 11)
- `model::objCache` (line 12)
- `model::objRecordLimit` (line 13)
- `model::objAudit` (line 14)
- `model::objValidate` (line 15)
- `model::idColumn` (line 16)
- `model::idType` (line 17)
- `model::canView` (line 19)
- `model::canCreate` (line 20)
- `model::canChange` (line 21)
- `model::canDelete` (line 22)
- `model::state` (line 24)
- `model::columns` (line 25)
- `model::_columns` (line 32)
- `model::fields` (line 37)
- `model::_fields` (line 42)
- `model::field($name)` (line 48)
- `model::create(...$args)` (line 50)
- `model::objCreateCommit($record, $pk)` (line 60)
- `model::objRunValidation($data)` (line 69)
- `model::objErrors` (line 80)
- `model::createRecord(...$args)` (line 81)
- `model::change($where, ...$args)` (line 82)
- `model::delete($where, ...$args)` (line 97)
- `model::objDeleteCommit($where, $args, $records)` (line 106)
- `model::objLogChange($where, ...$args)` (line 115)
- `model->objSave` (line 117)
- `model->objSaveCreate($pk, $pkValue)` (line 139)
- `model::transaction($callback)` (line 147)
- `model::query` (line 148)
- `model::column(...$args)` (line 150)
- `model::item(...$args)` (line 151)
- `model::pair(...$args)` (line 152)
- `model::records(...$args)` (line 153)
- `model::recordCount(...$args)` (line 154)
- `model::record(...$args)` (line 155)
- `model::recordsLoad($args, $fetch, $fetchMode, $saveRelations = false)` (line 157)
- `model::objRel($key)` (line 182)
- `model->objState` (line 187)
- `model->objGet($key)` (line 188)
- `model->objIn($ids, $db = null)` (line 189)
- `model->objMirror(string $bucket, $key)` (line 194)
- `model->getParent($key)` (line 203)
- `model->getChildren($key)` (line 226)
- `model->getMany($key)` (line 244)
- `model->getCount($key)` (line 268)
- `model->getLast($key)` (line 298)
- `model::objResolveClass($name)` (line 324)
- `model::objShortName($class = null)` (line 325)
- `model::objParents` (line 327)
- `model::objChildren` (line 333)
- `model::objMany` (line 339)

### %MySQL

MySQL handler via DB class

- File: `resources/DB/MySQL.phlo`
- `MySQL->PDO` (line 11)

### %PostgreSQL

PostgreSQL resource

- File: `resources/DB/PostgreSQL.phlo`
- `PostgreSQL->PDO` (line 11)
- `PostgreSQL->fieldQuotes` (line 12)
- `PostgreSQL->insertIgnore` (line 13)
- `PostgreSQL->insertOnConflict` (line 14)
- `PostgreSQL->lastId` (line 16)

### %Qdrant

Embeddings resource with Qdrant

- File: `resources/DB/Qdrant.phlo`
- `Qdrant->get(string $input, ?string $model = null)` (line 10)
- `Qdrant->collections` (line 12)
- `Qdrant->create($collection, $size = 1536, $distance = 'Cosine')` (line 13)
- `Qdrant->upsert($collection, $id, $input, ...$payload)` (line 14)
- `Qdrant->delete($collection, ...$ids)` (line 15)
- `Qdrant->search($collection, $input = null, $top = 100)` (line 16)
- `Qdrant->drop($collection)` (line 17)
- `Qdrant->request($uri, ...$data)` (line 19)

### %query

Fluent query builder for Phlo ORM

- File: `resources/DB/query.phlo`
- `query->class` (line 10)
- `query->conditions` (line 11)
- `query->bindings` (line 12)
- `query->orderBy` (line 13)
- `query->limitVal` (line 14)
- `query->offsetVal` (line 15)
- `query->fq` (line 16)
- `query->q($column)` (line 17)
- `query->eq($column, $value)` (line 23)
- `query->neq($column, $value)` (line 24)
- `query->gt($column, $value)` (line 25)
- `query->gte($column, $value)` (line 26)
- `query->lt($column, $value)` (line 27)
- `query->lte($column, $value)` (line 28)
- `query->like($column, $value)` (line 29)
- `query->in($column, array $values)` (line 30)
- `query->isNull($column)` (line 31)
- `query->notNull($column)` (line 32)
- `query->between($column, $min, $max)` (line 33)
- `query->raw($sql, ...$bindings)` (line 34)
- `query->where($condition, ...$values)` (line 35)
- `query->order($order)` (line 41)
- `query->limit($limit)` (line 46)
- `query->offset($offset)` (line 51)
- `query->build` (line 55)
- `query->records` (line 63)
- `query->record` (line 64)
- `query->column` (line 65)
- `query->item` (line 66)
- `query->count` (line 67)
- `query->delete` (line 68)

### %SQLite

SQLite resource

- File: `resources/DB/SQLite.phlo`
- `SQLite::__handle` (line 11)
- `SQLite->__construct(private string $file)` (line 12)
- `SQLite->PDO` (line 13)
- `SQLite->insertIgnore` (line 14)

## DOM

### %charts

Lightweight dependency-free SVG charts: sparkline, bars and donut. Use charts::spark/bars/donut.

- File: `resources/DOM/charts.phlo`
- `charts::spark($values, $color = '#888', $w = 240, $h = 48, $label = null)` (line 9)
- `charts::bars($values, $color = '#888', $w = 240, $h = 48, $label = null)` (line 29)
- `charts::donut($parts, $colors = null, $size = 120)` (line 47)

### %cookiewall

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.

- File: `resources/DOM/cookiewall.phlo`
- `cookiewall::__handle` (line 10)
- `cookiewall->choice` (line 12)
- `cookiewall->hasChosen` (line 13)
- `cookiewall->canTrack` (line 14)
- `cookiewall->canAnalytics` (line 15)
- `cookiewall->translate` (line 17)
- `cookiewall->labels` (line 18)
- `cookiewall->label($key)` (line 24)
- `route async POST cookiewall accept all` (line 26)
- `route async POST cookiewall accept essential` (line 31)
- `cookiewall->banner` (line 36)

### %CSS_fixes

Single Page App basic CSS boilerplate fixes

- File: `resources/DOM/CSS.fixes.phlo`

### %CSS_var

CSS variable proxy via app.var

- File: `resources/DOM/CSS.var.phlo`

### %datatags

Single Page App datatag plugin

- File: `resources/DOM/datatags.phlo`

### %dialog

Single Page App dialog resource

- File: `resources/DOM/dialog.phlo`

### %exists

onExist helper for dynamic SPA elements

- File: `resources/DOM/exists.phlo`

### %ffmpeg

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`).

- File: `resources/DOM/ffmpeg.phlo`

### %form

Single Page App form handler and input state saver

- File: `resources/DOM/form.phlo`

### %image_resizer

Client-side file upload image resizer

- File: `resources/DOM/image.resizer.phlo`

### %link

Single Page App async link handler

- File: `resources/DOM/link.phlo`

### %markdown

Client-side markdown parser

- File: `resources/DOM/markdown.phlo`

### %presentation

Audio-clock 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 #pp-data + #pp-root when present.

- File: `resources/DOM/presentation.phlo`

### %recorder

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`).

- File: `resources/DOM/recorder.phlo`

### %shorthands

onChange, onClick and onInput event shorthands

- File: `resources/DOM/shorthands.phlo`

### %store

Stateful binding engine

- File: `resources/DOM/store.phlo`

### %template

Single Page App client-side templating

- File: `resources/DOM/template.phlo`

### %timestamps

DOM live timestamps

- File: `resources/DOM/timestamps.phlo`

### %toasts

Simple toast resource

- File: `resources/DOM/toasts.phlo`

### %visible

onVisible and onVisibleIn helpers for DOM visibility

- File: `resources/DOM/visible.phlo`

### %websocket

Client-side WebSocket handler

- File: `resources/DOM/websocket.phlo`

## fields

### %field

Base ORM field

- File: `resources/fields/field.phlo`
- `function field($type, ...$args)` (line 10)
- `field::__handle` (line 12)
- `field->title` (line 14)
- `field->input($record)` (line 16)
- `field->label($record)` (line 17)
- `field->objColumns` (line 19)
- `field->objValidate($value)` (line 21)

### %field_bool

Boolean field

- File: `resources/fields/bool.phlo`
- `field_bool->true` (line 11)
- `field_bool->false` (line 12)
- `field_bool->label($record)` (line 14)
- `field_bool->input($record)` (line 15)
- `field_bool->parse($record)` (line 16)
- `field_bool->nullable` (line 17)
- `field_bool->objColumns` (line 19)

### %field_child

Child relation field

- File: `resources/fields/child.phlo`
- `field_child->list` (line 11)
- `field_child->change` (line 12)
- `field_child->create` (line 13)
- `field_child->record` (line 14)
- `field_child->count($record)` (line 16)
- `field_child->last($record)` (line 17)
- `field_child->label($record)` (line 18)
- `field_child->input($record)` (line 19)
- `field_child->link($record)` (line 20)
- `field_child->objKey($parentModel)` (line 22)
- `field_child->objOwns($record, $parentId, $parentModel):bool` (line 24)

### %field_date

Date field

- File: `resources/fields/date.phlo`
- `field_date->handle` (line 11)
- `field_date->format` (line 12)
- `field_date->months` (line 13)
- `field_date->label($record)` (line 18)
- `field_date->objColumns` (line 29)

### %field_datetime

Date-time field

- File: `resources/fields/datetime.phlo`
- `field_datetime->handle` (line 11)
- `field_datetime->change` (line 12)
- `field_datetime->create` (line 13)
- `field_datetime->label($record)` (line 15)
- `field_datetime->labelIconClass($value)` (line 16)
- `field_datetime->input($record)` (line 17)
- `field_datetime->parse($record)` (line 18)
- `field_datetime->objColumns` (line 24)

### %field_email

Email field

- File: `resources/fields/email.phlo`
- `field_email->label($record)` (line 12)

### %field_file

File field

- File: `resources/fields/file.phlo`
- `field_file->canDelete` (line 11)
- `field_file->delete` (line 12)
- `field_file->path` (line 13)
- `field_file->uri` (line 14)
- `field_file->length` (line 15)
- `field_file->accept` (line 16)
- `field_file->label($record)` (line 18)
- `field_file->input($record)` (line 19)
- `field_file->parse($record)` (line 28)
- `field_file->read($record, $path = null)` (line 40)
- `field_file->write($file)` (line 46)
- `field_file->writePath($file, $path = null)` (line 47)
- `field_file->objColumns` (line 54)

### %field_image

Image field

- File: `resources/fields/image.phlo`
- `field_image->delete` (line 11)
- `field_image->uri` (line 12)
- `field_image->path` (line 13)
- `field_image->thumbPath` (line 14)
- `field_image->thumbSize` (line 15)
- `field_image->thumbUri` (line 16)
- `field_image->placeholder` (line 17)
- `field_image->record` (line 19)
- `field_image->label($record)` (line 21)
- `field_image->preview($record)` (line 22)
- `field_image->input($record)` (line 23)
- `field_image->write($file)` (line 32)
- `field_image->objColumns` (line 34)

### %field_many

Many-to-many relation field

- File: `resources/fields/many.phlo`
- `field_many->list` (line 12)
- `field_many->record` (line 13)
- `field_many->create` (line 14)
- `field_many->change` (line 15)
- `field_many->count($record)` (line 17)
- `field_many->label($record)` (line 18)
- `field_many->link($record)` (line 19)
- `field_many->input($record)` (line 21)
- `field_many->sync($model, $parentId)` (line 28)
- `field_many->objOwns($record, $parentId, $parentModel):bool` (line 36)
- `field_many->objColumns` (line 41)

### %field_multiselect

Multi-select via checkboxes; stores the choice as CSV in one hidden field (no save change needed).

- File: `resources/fields/multiselect.phlo`
- `field_multiselect->label($record)` (line 11)
- `field_multiselect->input($record)` (line 16)
- `field_multiselect->objColumns` (line 23)

### %field_number

Number field

- File: `resources/fields/number.phlo`
- `field_number->decimals` (line 11)
- `field_number->length` (line 12)
- `field_number->min` (line 13)
- `field_number->label($record)` (line 15)
- `field_number->input($record)` (line 16)
- `field_number->objColumns` (line 18)

### %field_parent

Parent relation field

- File: `resources/fields/parent.phlo`
- `field_parent->label($record)` (line 11)
- `field_parent->input($record)` (line 12)
- `field_parent->link($record, $content = null)` (line 13)
- `field_parent->options` (line 14)
- `field_parent->objColumns` (line 16)

### %field_password

Password field

- File: `resources/fields/password.phlo`
- `field_password->list` (line 11)
- `field_password->required` (line 12)
- `field_password->minlength` (line 13)
- `field_password->placeholder` (line 14)
- `field_password->input($record)` (line 16)
- `field_password->label($record)` (line 17)
- `field_password->parse($record)` (line 18)
- `field_password->objColumns` (line 20)

### %field_price

Price field

- File: `resources/fields/price.phlo`
- `field_price->decimals` (line 11)

### %field_select

Select field

- File: `resources/fields/select.phlo`
- `field_select->input($record)` (line 11)
- `field_select->objColumns` (line 13)

### %field_text

Text field

- File: `resources/fields/text.phlo`
- `field_text->length` (line 11)
- `field_text->multiline` (line 12)
- `field_text->label($record)` (line 14)
- `field_text->input($record)` (line 15)
- `field_text->inputField($record)` (line 16)
- `field_text->inputMulti($record)` (line 17)
- `field_text->objColumns` (line 19)

### %field_token

Token field

- File: `resources/fields/token.phlo`
- `field_token->length` (line 11)
- `field_token->default` (line 12)
- `field_token->create` (line 13)
- `field_token->change` (line 14)
- `field_token->search` (line 15)
- `field_token->handle` (line 16)
- `field_token->label($record)` (line 18)
- `field_token->parse($record)` (line 19)
- `field_token->objColumns` (line 21)

### %field_virtual

Virtual field

- File: `resources/fields/virtual.phlo`
- `field_virtual->create` (line 11)
- `field_virtual->change` (line 12)
- `field_virtual->objColumns` (line 14)

### %field_wysiwyg

WYSIWYG field

- File: `resources/fields/wysiwyg.phlo`
- `field_wysiwyg->input($record)` (line 11)
- `field_wysiwyg->objColumns` (line 19)

### Functions

- `field($type, ...$args)`

## files

### %CSV

CSV reader resource

- File: `resources/files/CSV.phlo`
- `CSV::__handle` (line 9)
- `CSV->__construct(string $filename, ?string $path = null)` (line 10)
- `CSV->objFile:string` (line 16)
- `CSV->objRead` (line 18)

### %DOCX

DOCX reader resource

- File: `resources/files/DOCX.phlo`
- `DOCX->__construct(string $file)` (line 10)
- `DOCX::toText(string $file):string` (line 23)

### %file

File resource

- File: `resources/files/file.phlo`
- `file::__handle` (line 9)
- `file->__construct(public string $file, ?string $name = null, $contents = null, ...$args)` (line 10)
- `file->append(string $data)` (line 16)
- `file->basename` (line 17)
- `file->base64` (line 18)
- `file->contents` (line 19)
- `file->contentsINI(bool $parse = true)` (line 20)
- `file->contentsJSON($assoc = null)` (line 21)
- `file->copy($to)` (line 22)
- `file->created` (line 23)
- `file->createdAge` (line 24)
- `file->createdHuman` (line 25)
- `file->curl($type = null, $filename = null)` (line 26)
- `file->delete` (line 27)
- `file->exists` (line 28)
- `file->ext` (line 29)
- `file->filename` (line 30)
- `file->getLine` (line 31)
- `file->getLength(int $length)` (line 32)
- `file->is(string $file)` (line 33)
- `file->md5` (line 34)
- `file->mime` (line 35)
- `file->modified` (line 36)
- `file->modifiedAge` (line 37)
- `file->modifiedHuman` (line 38)
- `file->move($to)` (line 39)
- `file->name` (line 40)
- `file->output($download = false)` (line 41)
- `file->path` (line 42)
- `file->pathRel` (line 43)
- `file->pointer` (line 44)
- `file->readable` (line 45)
- `file->src` (line 46)
- `file->size` (line 47)
- `file->sizeHuman(int $precision = 0)` (line 48)
- `file->sha1` (line 49)
- `file->shortenTo(int $length)` (line 50)
- `file->title` (line 51)
- `file->token($length = 20)` (line 52)
- `file->type` (line 53)
- `file->touch` (line 54)
- `file->writable` (line 55)
- `file->writeINI($data, bool $deleteEmpty = false)` (line 56)
- `file->writeJSON($data, bool $deleteEmpty = false)` (line 57)
- `file->writeJSONplain($data, bool $deleteEmpty = false)` (line 58)
- `file->write(string $data, bool $deleteEmpty = false)` (line 59)
- `file->objInfo` (line 66)

### %img

GD image resource

- File: `resources/files/img.phlo`
- `img::detect($data)` (line 9)
- `img::__handle` (line 19)
- `img->__construct(public string $file)` (line 20)
- `img->src:GdImage` (line 22)
- `img->width` (line 23)
- `img->height` (line 24)
- `img->scale($width = null, $height = null, $crop = false)` (line 26)
- `img->ext($file = null)` (line 57)
- `img->source($format = null)` (line 59)
- `img->save($file = null)` (line 65)
- `img->write($format = null, $file = null)` (line 70)

### %INI

Generic INI resource

- File: `resources/files/INI.phlo`
- `INI->objFile:string` (line 9)
- `INI::__handle` (line 11)
- `INI->__construct(string $filename, ?string $path = null, bool $parse = true)` (line 12)
- `INI->objRead($parse = true)` (line 18)
- `INI->objWrite` (line 19)
- `INI->__destruct` (line 21)

### %JSON

Generic JSON resource

- File: `resources/files/JSON.phlo`
- `JSON::__handle` (line 10)
- `JSON->__construct(string $filename, ?string $path = null, $assoc = null)` (line 11)
- `JSON->objFile:string` (line 17)
- `JSON->objTouch` (line 19)
- `JSON->objRead($assoc = null)` (line 20)
- `JSON->objWrite($data, $flags = null)` (line 21)
- `JSON->__destruct` (line 23)

### %PDF

PDF generator and reader

- File: `resources/files/PDF.phlo`
- `PDF::toText(string $file):string` (line 9)
- `PDF->title` (line 21)
- `PDF->author` (line 22)
- `PDF->subject` (line 23)
- `PDF->keywords` (line 24)
- `PDF->creator` (line 25)
- `PDF->filename` (line 27)
- `PDF->mode` (line 28)
- `PDF->fromHTML($HTML)` (line 30)

### %UBL

UBL 2.1 invoice XML (PEPPOL BIS Billing 3.0) from a normalized invoice structure. Use UBL::invoice($data).

- File: `resources/files/UBL.phlo`
- `UBL::invoice(array $data)` (line 9)
- `UBL::xmlLine($idx, $line, $qty, $net, $rate, $currency = 'EUR')` (line 66)
- `UBL::partyXml($wrapper, $name, $info, $vatNumber)` (line 81)
- `UBL::n($v)` (line 102)
- `UBL::esc($v)` (line 103)

### %XLSX

XLSX reader resource

- File: `resources/files/XLSX.phlo`
- `XLSX->__construct(string $file)` (line 10)

## payments

### %Stripe

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).

- File: `resources/payments/Stripe.phlo`
- `Stripe::boot($secret, $version = '2025-09-30.clover')` (line 9)
- `Stripe::price($lookupKey)` (line 16)
- `Stripe::customer($id)` (line 21)
- `Stripe::createCustomer(array $data)` (line 31)
- `Stripe::checkout(array $params)` (line 33)
- `Stripe::portal($customerId, $returnUrl)` (line 35)
- `Stripe::verifyWebhook($payload, $sigHeader, $secret)` (line 37)
- `Stripe::subscriptions($customerId, $status = 'all', $limit = 10)` (line 43)
- `Stripe::subscription($id)` (line 45)
- `Stripe::product($id)` (line 47)

## security

### %audit

Audit log for model mutations (opt-in via static idColumn/objAudit). Schema: resources/security/audit.sql

- File: `resources/security/audit.phlo`
- `audit::log($model, $action, $before = [], $after = [], $exclude = [])` (line 9)
- `audit::diff($before, $after)` (line 30)
- `audit::history($model, $recordId, $limit = 50)` (line 39)
- `audit::byUser($model, $userId, $fromTs = 0, $limit = 100)` (line 47)
- `audit::purge($model, $olderThanSeconds = 31536000)` (line 54)

### %captcha

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.

- File: `resources/security/captcha.phlo`
- `captcha::W` (line 10)
- `captcha::H` (line 11)
- `captcha::P` (line 12)
- `captcha::tol` (line 13)
- `captcha::ttl` (line 14)
- `captcha::issue` (line 16)
- `captcha::images($gapX, $gapY)` (line 23)
- `captcha::verify($x, $telemetry)` (line 55)
- `captcha::consume` (line 76)
- `captcha->widget` (line 80)
- `captcha->field($bg, $piece, $gapY, $w, $h, $p)` (line 86)

### %creds

Credentials resolver from env and ini sources

- File: `resources/security/creds.phlo`
- `creds->__construct(?array $values = null)` (line 9)
- `creds->resolve` (line 16)
- `creds->loadINI(string $file):array` (line 24)
- `creds->envValues(bool $hostScoped = false):array` (line 30)
- `creds->hostKey` (line 49)
- `creds->envAssign(array &$target, array $parts, string $value):void` (line 55)
- `creds->merge(array &$base, array $add):void` (line 70)
- `creds->objGet($key)` (line 80)
- `creds->objInfo` (line 85)

### %CSRF

Rotating async CSRF protection for Phlo requests

- File: `resources/security/CSRF.phlo`
- `CSRF->view` (line 11)
- `CSRF->token` (line 12)
- `CSRF->verify` (line 13)
- `CSRF->update` (line 14)

### %JWT

Sign and verify compact HS256 JSON Web Tokens (RFC 7519), secure by default

- File: `resources/security/JWT.phlo`
- `JWT->__construct(public string $secret, public string $issuer = void, public int $leeway = 30)` (line 10)
- `JWT->sign(array $claims, int $ttl = 3600):string` (line 12)
- `JWT->verify(string $token):array` (line 21)
- `JWT->sig(string $body):string` (line 37)
- `JWT->encode($data):string` (line 38)
- `JWT->decode(string $data):string` (line 39)

### %OAuth2

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.

- File: `resources/security/OAuth2.phlo`
- `OAuth2::authorizeUrl($endpoint, array $params)` (line 10)
- `OAuth2::token($tokenUrl, $clientId, $clientSecret, $grantType, array $extra = [])` (line 12)
- `OAuth2::exchangeCode($tokenUrl, $clientId, $clientSecret, $code, $redirectUri = null, array $extra = [])` (line 26)
- `OAuth2::refresh($tokenUrl, $clientId, $clientSecret, $refreshToken, array $extra = [])` (line 28)

### %rate

Rate-limit (fixed window) on the rate_limit table. Schema: resources/security/rate.sql

- File: `resources/security/rate.phlo`
- `rate::check($key, $limit, $windowSeconds, $storage = 'db')` (line 10)
- `rate::checkApcu($key, $limit, $windowSeconds)` (line 20)
- `rate::status($key, $limit, $windowSeconds)` (line 27)
- `rate::reset($key)` (line 34)
- `rate::purge($olderThanSeconds = 604800)` (line 35)

### %security

Generic security resource

- File: `resources/security/security.phlo`
- `security->whitelist` (line 10)
- `security->setNonce` (line 12)
- `security->frameProtect($mode = 'DENY')` (line 14)
- `security->frameWhitelist` (line 15)
- `security->strict` (line 17)
- `security->basic` (line 26)
- `security->marketing` (line 31)
- `security->api` (line 36)
- `security->base` (line 43)

### %social

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.

- File: `resources/security/social.phlo`
- `social::providers` (line 11)
- `social::config($provider)` (line 38)
- `social::configured($provider)` (line 50)
- `social::authUrl($provider, $state, $nonce = void)` (line 55)
- `social::profile($provider, $code, $nonce = void)` (line 69)
- `social::decodeIdToken($jwt)` (line 86)
- `social::algs` (line 93)
- `social::verifySignature($provider, $jwt)` (line 95)
- `social::jwks($provider)` (line 108)
- `social::key($provider, $kid)` (line 122)
- `social::jwkToPem(array $jwk)` (line 129)
- `social::der($tag, $content)` (line 141)
- `social::derInt($bytes)` (line 148)
- `social::b64urlDecode($data)` (line 155)
- `social::verifyClaims($provider, array $cfg, array $claims, $nonce = void)` (line 162)
- `social::verifyIssuer($provider, array $claims)` (line 170)
- `social::normalize($provider, array $claims)` (line 181)
- `social::b64url($data)` (line 199)
- `social::appleSecret` (line 201)
- `social::derToJose($der)` (line 219)
- `social::pad32($x)` (line 227)

### Functions

- `decrypt($encrypted, $key):string|false`
- `encrypt($data, $key):string`
- `token(int $length = 8, ?string $input = null)`

