From 11b3635cbf5f8a3da8270788fc256b503623ea31 Mon Sep 17 00:00:00 2001 From: Harsh-2002 Date: Thu, 4 Jun 2026 11:33:48 +0000 Subject: [PATCH 1/2] feat(runtime)!: collapse to two runtimes (node, python); fix nsjail invocation on hardened/constrained hosts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING: the four versioned runtime ids (node22/node24/python313/python314) are replaced by two generic, latest-stable-only ids — `node` (Node.js 24) and `python` (Python 3.14). The version is now an implementation detail surfaced only as a display label. Runtime collapse - sandbox.Language: Node="node", Python="python"; IsNode/IsPython retargeted. - validRuntimes + MCP validRuntimesSet + GET /runtimes catalog → {node, python}; /runtimes gains a display-only `version` field. Legacy ids are REJECTED on input (strict cutover) across REST/CLI/MCP. - DB migration collapses stored values in place (node20|node22|node24→node, python312|python313|python314→python) so existing functions keep loading; added TestCollapseRuntimes. builder pythonVersionFor → 3.14. - runtimes/ dirs collapsed to node/ + python/; Makefile adapters-embed, Dockerfile (two rootfs stages), scripts/{build-rootfs,entrypoint,install}.sh, and release.yml rootfs matrix now build only node/python. UI build toolchain bumped node 22→24 (ci.yml/release.yml/Dockerfile). - CLI --runtime help/examples, frontend (Editor/Docs/templates/aiPrompts), AI system prompt, and all docs/* updated; make docs-embed synced. e2e updated + a legacy-id-rejected case added (test_functions). nsjail invocation fixes (pre-existing, surfaced during live testing — function invocation crashed on the old binary too on affected hosts) - sandbox cgroup false-positive: cgroupv2Delegate enabled nsjail cgroup limits whenever it could mkdir a child cgroup, even when the controllers weren't delegated (cgroup.subtree_control empty under the cgroup-v2 "no internal processes" rule). nsjail's memory.max write then failed and every worker crashed. Now verify a probe child exposes memory/pids/cpu; otherwise fall back to rlimit-only (functions run without per-sandbox cgroup caps) instead of crashing. - systemd unit /proc overmount: ProtectKernelTunables=true overmounts /proc/sys, which blocks nsjail's procfs mount inside its user namespace ("Failed to mount mandatory point: /proc"). Dropped from the install.sh systemd unit; nsjail still isolates via userns + seccomp + chroot. Validated: go build/vet/test -race; isolated e2e (fresh image, node24/python3.14 rootfs only) all modules green; live systemd — migrated + fresh node/python functions invoke 200 (Node 24 / Python 3.14), legacy ids rejected. --- .github/workflows/ci.yml | 4 +- .github/workflows/release.yml | 18 +- CLAUDE.md | 4 +- Dockerfile | 39 +- Makefile | 24 +- README.md | 4 +- .../orva/adapters/{node24 => node}/adapter.js | 2 +- .../orva/adapters/{node22 => node}/orva.d.ts | 0 .../orva/adapters/{node22 => node}/orva.js | 0 .../adapters/{node22 => node}/package.json | 0 backend/cmd/orva/adapters/node22/adapter.js | 582 -------- .../orva/adapters/python}/adapter.py | 2 +- .../adapters/{python313 => python}/orva.py | 0 .../adapters/{python313 => python}/py.typed | 0 .../cmd/orva/adapters/python313/adapter.py | 707 --------- backend/cmd/orva/init_cmd.go | 2 +- backend/cmd/orva/setup.go | 10 +- backend/internal/ai/manager.go | 7 +- backend/internal/backup/backup_test.go | 2 +- backend/internal/builder/builder.go | 30 +- backend/internal/builder/builder_test.go | 10 +- backend/internal/database/database_test.go | 12 +- .../database/migrate_to_uuidv7_test.go | 2 +- backend/internal/database/migrations.go | 54 +- .../database/runtime_collapse_test.go | 59 + backend/internal/mcp/reference.md | 27 +- backend/internal/mcp/tools_functions.go | 14 +- backend/internal/mcp/tools_system.go | 7 +- backend/internal/registry/registry_test.go | 18 +- backend/internal/sandbox/sandbox.go | 69 +- backend/internal/sandbox/sandbox_test.go | 6 +- backend/internal/server/channel_e2e_test.go | 8 +- backend/internal/server/diff_test.go | 4 +- backend/internal/server/handlers/functions.go | 16 +- backend/internal/server/handlers/runtimes.go | 35 +- backend/internal/server/server_test.go | 30 +- .../assets/{AI-DnN0qj60.js => AI-CRINc2rq.js} | 2 +- ...ivity-ELKGWazx.js => Activity-CUYXvjyS.js} | 2 +- ...piKeys-DRo8HmPM.js => ApiKeys-D_0i_tEn.js} | 2 +- ...nnels-CnxWaSZb.js => Channels-BVCLEepx.js} | 2 +- ...tor-Davda7hv.js => CodeEditor-1BBBEH1J.js} | 2 +- ...nJobs-KVuRxoUt.js => CronJobs-C1Lm7OXf.js} | 2 +- ...oard-mDlU2mXy.js => Dashboard-DdCcITCh.js} | 2 +- ...ts-DK8gYrYx.js => Deployments-HGMJdPkO.js} | 2 +- .../server/ui_dist/assets/Docs-D8EdWzOh.js | 327 +++++ .../server/ui_dist/assets/Docs-DVZ33BCt.js | 327 ----- ...{Drawer-B_L-gxK5.js => Drawer-C3AFLOZb.js} | 2 +- .../server/ui_dist/assets/Editor-BX4mg0fo.js | 1296 ----------------- .../server/ui_dist/assets/Editor-CGt5HsZM.js | 1296 +++++++++++++++++ ...ditor-ChfgDDB5.css => Editor-Ci5_23J6.css} | 2 +- ...ewall-DfqrkCX_.js => Firewall-BXu8MFSv.js} | 2 +- ...f-uI1awBzr.js => FunctionDiff-B5S0Jfo_.js} | 2 +- ...-Cw2Du-lJ.js => FunctionsList-ClWQkTrs.js} | 2 +- ...ton-Pc0_zskr.js => IconButton-BgeMzwXv.js} | 2 +- ...rY_p4Y6.js => InboundWebhooks-CeKosJjA.js} | 2 +- .../{Input-Bbxjlz9n.js => Input-i1hDoPmt.js} | 2 +- ...Pek-OrhD.js => InvocationsLog-TKQA0zvQ.js} | 2 +- .../{Jobs-DM5XNZr_.js => Jobs-BuEpd4mz.js} | 2 +- ...VStore-DPRpTn_9.js => KVStore-BJOrSxjD.js} | 2 +- .../{Login-eFz4LWYc.js => Login-YGsZM8PV.js} | 2 +- .../{Modal-C-qDVd6z.js => Modal-jEhKmxZK.js} | 2 +- ...Found-CcAhjlCy.js => NotFound-BmRAxlxU.js} | 2 +- ...ing-BLkFUZSu.js => Onboarding-DOb5mnY3.js} | 2 +- ...tings-DXEJ_UsM.js => Settings-DhV460vi.js} | 2 +- ...ge-DoungZTd.js => StatusBadge-Cj_PlPFZ.js} | 2 +- ...il-DNh9rJaF.js => TraceDetail-DlY0pIfy.js} | 2 +- ...{Traces-Dscswtpu.js => Traces-Cr9wsEkz.js} | 2 +- ...hooks-C3cxBoSz.js => Webhooks-DwuKsY7O.js} | 2 +- .../assets/{ai-BsHVzyl5.js => ai-DmyZUAtW.js} | 2 +- ...mpts-Dgb3jxRL.js => aiPrompts-DGZ6L7ag.js} | 14 +- ...eft-gKa5_kvg.js => arrow-left-C4STwFAY.js} | 2 +- ...open-2x4dJwqD.js => book-open-CAmAR_fB.js} | 2 +- .../server/ui_dist/assets/check-BkPCgKSu.js | 1 - .../server/ui_dist/assets/check-C4wzjDZN.js | 1 + .../ui_dist/assets/chevron-down-BTZfO5Md.js | 1 + .../ui_dist/assets/chevron-down-Trjh5P5D.js | 1 - .../ui_dist/assets/chevron-right-CC7vhgfo.js | 1 - .../ui_dist/assets/chevron-right-OdWgNfOU.js | 1 + ...{circle-BHWwGwr8.js => circle-DJWJGpv0.js} | 2 +- ...t-BmP6pM7E.js => circle-alert-DJMgVejj.js} | 2 +- .../{clock-ARFGKas-.js => clock-BWp9w4xs.js} | 2 +- .../{copy-Gc8n9M6v.js => copy-CTb6u-fx.js} | 2 +- .../{flag-CdXTOLpP.js => flag-BkRqUrT5.js} | 2 +- ...re-CXuIlVPE.js => git-compare-omnJl6y2.js} | 2 +- .../{globe-BSxGWG92.js => globe-CR2M7Azm.js} | 2 +- .../{index-D5cO6vit.js => index-BMkkwZ9q.js} | 4 +- ...ound-D643nzM3.js => key-round-BccKiRw7.js} | 2 +- .../{lock-C90zRIcI.js => lock-Dpr2FIZ9.js} | 2 +- ...{pencil-CkSYbb1r.js => pencil-DTkm5-NQ.js} | 2 +- .../{play-BfsTXwNm.js => play-CPjfKIOc.js} | 2 +- ...-cw-BYFoMG0c.js => refresh-cw-C7sR7ShF.js} | 2 +- ...ccw-CeRUwJZR.js => rotate-ccw-CsgWy1Bs.js} | 2 +- ...s-2-BhRqOkbZ.js => settings-2-CcqGdzLw.js} | 2 +- ...k-BFAT8DDU.js => shield-check-sW6QCkG0.js} | 2 +- ...rkles-DV3RWxRD.js => sparkles-BVQ_t_Q_.js} | 2 +- ...pen-WhijiULo.js => square-pen-CsqFW8Ka.js} | 2 +- ...minal-CR-RL0k9.js => terminal-DAVNGL0P.js} | 2 +- ...rash-2-sSCgxcxW.js => trash-2-BXf2uqQH.js} | 2 +- ...iable-kwh9BTXT.js => variable-b2EnW52t.js} | 2 +- .../{zap-CTttJ1hV.js => zap-DvhWYa2n.js} | 2 +- backend/internal/server/ui_dist/docs.md | 27 +- backend/internal/server/ui_dist/index.html | 2 +- backend/internal/server/uuid_e2e_test.go | 4 +- backend/runtimes/{node24 => node}/adapter.js | 2 +- .../node24 => runtimes/node}/orva.d.ts | 0 .../adapters/node24 => runtimes/node}/orva.js | 0 .../node24 => runtimes/node}/package.json | 0 backend/runtimes/node22/adapter.js | 582 -------- backend/runtimes/node22/orva.d.ts | 189 --- backend/runtimes/node22/orva.js | 579 -------- backend/runtimes/node22/package.json | 8 - backend/runtimes/node24/orva.d.ts | 189 --- backend/runtimes/node24/orva.js | 579 -------- backend/runtimes/node24/package.json | 8 - .../python314 => runtimes/python}/adapter.py | 2 +- .../python314 => runtimes/python}/orva.py | 0 .../python314 => runtimes/python}/py.typed | 0 backend/runtimes/python313/adapter.py | 707 --------- backend/runtimes/python313/orva.py | 678 --------- backend/runtimes/python313/py.typed | 0 backend/runtimes/python314/orva.py | 678 --------- backend/runtimes/python314/py.typed | 0 cli/commands/deploy.go | 6 +- cli/commands/functions.go | 6 +- cli/commands/reference.md | 27 +- docs/API.md | 2 +- docs/ARCHITECTURE.md | 10 +- docs/CAPACITY.md | 16 +- docs/CLI.md | 14 +- docs/CONTRIBUTING.md | 6 +- docs/GVISOR.md | 4 +- docs/RUNTIMES.md | 14 +- docs/SECURITY.md | 2 +- docs/reference.md | 27 +- frontend/public/docs.md | 27 +- frontend/src/components/common/CodeEditor.vue | 2 +- frontend/src/templates/index.js | 12 +- frontend/src/utils/aiPrompts.js | 28 +- frontend/src/views/Docs.vue | 12 +- frontend/src/views/Editor.vue | 43 +- scripts/build-rootfs.sh | 14 +- scripts/entrypoint.sh | 30 +- scripts/install.sh | 8 +- test/api-smoke.sh | 2 +- test/atscale.sh | 16 +- test/auth-test.sh | 10 +- test/e2e/CHECKLIST.md | 16 +- test/e2e/tests/test_ai_chat.py | 2 +- test/e2e/tests/test_ai_perms.py | 4 +- test/e2e/tests/test_channels.py | 2 +- test/e2e/tests/test_cli.py | 8 +- test/e2e/tests/test_cli_chat.py | 2 +- test/e2e/tests/test_cron.py | 2 +- test/e2e/tests/test_deploy_invoke.py | 6 +- test/e2e/tests/test_fixtures.py | 2 +- test/e2e/tests/test_functions.py | 11 +- test/e2e/tests/test_inbound_webhooks.py | 2 +- test/e2e/tests/test_jobs.py | 2 +- test/e2e/tests/test_keys.py | 2 +- test/e2e/tests/test_kv.py | 2 +- test/e2e/tests/test_routes.py | 2 +- test/e2e/tests/test_secrets.py | 2 +- test/e2e/tests/test_traces.py | 2 +- test/egress-test.sh | 2 +- test/errors-test.sh | 10 +- test/heavy-deploy-test.sh | 2 +- test/install/failure-modes.sh | 2 +- test/install/kata-flow.sh | 2 +- test/install/smoke-flow.sh | 4 +- test/kata-bench/extended-functional.sh | 6 +- test/kata-bench/run.sh | 2 +- test/loadtest.sh | 12 +- test/rollback-test.sh | 2 +- test/routes-test.sh | 2 +- test/sdk-test.sh | 10 +- test/secrets-test.sh | 2 +- test/tracing-test.sh | 8 +- 177 files changed, 2233 insertions(+), 7656 deletions(-) rename backend/cmd/orva/adapters/{node24 => node}/adapter.js (99%) rename backend/cmd/orva/adapters/{node22 => node}/orva.d.ts (100%) rename backend/cmd/orva/adapters/{node22 => node}/orva.js (100%) rename backend/cmd/orva/adapters/{node22 => node}/package.json (100%) delete mode 100644 backend/cmd/orva/adapters/node22/adapter.js rename backend/{runtimes/python314 => cmd/orva/adapters/python}/adapter.py (99%) rename backend/cmd/orva/adapters/{python313 => python}/orva.py (100%) rename backend/cmd/orva/adapters/{python313 => python}/py.typed (100%) delete mode 100644 backend/cmd/orva/adapters/python313/adapter.py create mode 100644 backend/internal/database/runtime_collapse_test.go rename backend/internal/server/ui_dist/assets/{AI-DnN0qj60.js => AI-CRINc2rq.js} (99%) rename backend/internal/server/ui_dist/assets/{Activity-ELKGWazx.js => Activity-CUYXvjyS.js} (97%) rename backend/internal/server/ui_dist/assets/{ApiKeys-DRo8HmPM.js => ApiKeys-D_0i_tEn.js} (96%) rename backend/internal/server/ui_dist/assets/{Channels-CnxWaSZb.js => Channels-BVCLEepx.js} (97%) rename backend/internal/server/ui_dist/assets/{CodeEditor-Davda7hv.js => CodeEditor-1BBBEH1J.js} (58%) rename backend/internal/server/ui_dist/assets/{CronJobs-KVuRxoUt.js => CronJobs-C1Lm7OXf.js} (98%) rename backend/internal/server/ui_dist/assets/{Dashboard-mDlU2mXy.js => Dashboard-DdCcITCh.js} (99%) rename backend/internal/server/ui_dist/assets/{Deployments-DK8gYrYx.js => Deployments-HGMJdPkO.js} (95%) create mode 100644 backend/internal/server/ui_dist/assets/Docs-D8EdWzOh.js delete mode 100644 backend/internal/server/ui_dist/assets/Docs-DVZ33BCt.js rename backend/internal/server/ui_dist/assets/{Drawer-B_L-gxK5.js => Drawer-C3AFLOZb.js} (96%) delete mode 100644 backend/internal/server/ui_dist/assets/Editor-BX4mg0fo.js create mode 100644 backend/internal/server/ui_dist/assets/Editor-CGt5HsZM.js rename backend/internal/server/ui_dist/assets/{Editor-ChfgDDB5.css => Editor-Ci5_23J6.css} (52%) rename backend/internal/server/ui_dist/assets/{Firewall-DfqrkCX_.js => Firewall-BXu8MFSv.js} (98%) rename backend/internal/server/ui_dist/assets/{FunctionDiff-uI1awBzr.js => FunctionDiff-B5S0Jfo_.js} (99%) rename backend/internal/server/ui_dist/assets/{FunctionsList-Cw2Du-lJ.js => FunctionsList-ClWQkTrs.js} (96%) rename backend/internal/server/ui_dist/assets/{IconButton-Pc0_zskr.js => IconButton-BgeMzwXv.js} (94%) rename backend/internal/server/ui_dist/assets/{InboundWebhooks-BrY_p4Y6.js => InboundWebhooks-CeKosJjA.js} (98%) rename backend/internal/server/ui_dist/assets/{Input-Bbxjlz9n.js => Input-i1hDoPmt.js} (95%) rename backend/internal/server/ui_dist/assets/{InvocationsLog-Pek-OrhD.js => InvocationsLog-TKQA0zvQ.js} (97%) rename backend/internal/server/ui_dist/assets/{Jobs-DM5XNZr_.js => Jobs-BuEpd4mz.js} (96%) rename backend/internal/server/ui_dist/assets/{KVStore-DPRpTn_9.js => KVStore-BJOrSxjD.js} (98%) rename backend/internal/server/ui_dist/assets/{Login-eFz4LWYc.js => Login-YGsZM8PV.js} (95%) rename backend/internal/server/ui_dist/assets/{Modal-C-qDVd6z.js => Modal-jEhKmxZK.js} (97%) rename backend/internal/server/ui_dist/assets/{NotFound-CcAhjlCy.js => NotFound-BmRAxlxU.js} (89%) rename backend/internal/server/ui_dist/assets/{Onboarding-BLkFUZSu.js => Onboarding-DOb5mnY3.js} (98%) rename backend/internal/server/ui_dist/assets/{Settings-DXEJ_UsM.js => Settings-DhV460vi.js} (98%) rename backend/internal/server/ui_dist/assets/{StatusBadge-DoungZTd.js => StatusBadge-Cj_PlPFZ.js} (79%) rename backend/internal/server/ui_dist/assets/{TraceDetail-DNh9rJaF.js => TraceDetail-DlY0pIfy.js} (95%) rename backend/internal/server/ui_dist/assets/{Traces-Dscswtpu.js => Traces-Cr9wsEkz.js} (95%) rename backend/internal/server/ui_dist/assets/{Webhooks-C3cxBoSz.js => Webhooks-DwuKsY7O.js} (96%) rename backend/internal/server/ui_dist/assets/{ai-BsHVzyl5.js => ai-DmyZUAtW.js} (98%) rename backend/internal/server/ui_dist/assets/{aiPrompts-Dgb3jxRL.js => aiPrompts-DGZ6L7ag.js} (94%) rename backend/internal/server/ui_dist/assets/{arrow-left-gKa5_kvg.js => arrow-left-C4STwFAY.js} (60%) rename backend/internal/server/ui_dist/assets/{book-open-2x4dJwqD.js => book-open-CAmAR_fB.js} (76%) delete mode 100644 backend/internal/server/ui_dist/assets/check-BkPCgKSu.js create mode 100644 backend/internal/server/ui_dist/assets/check-C4wzjDZN.js create mode 100644 backend/internal/server/ui_dist/assets/chevron-down-BTZfO5Md.js delete mode 100644 backend/internal/server/ui_dist/assets/chevron-down-Trjh5P5D.js delete mode 100644 backend/internal/server/ui_dist/assets/chevron-right-CC7vhgfo.js create mode 100644 backend/internal/server/ui_dist/assets/chevron-right-OdWgNfOU.js rename backend/internal/server/ui_dist/assets/{circle-BHWwGwr8.js => circle-DJWJGpv0.js} (76%) rename backend/internal/server/ui_dist/assets/{circle-alert-BmP6pM7E.js => circle-alert-DJMgVejj.js} (74%) rename backend/internal/server/ui_dist/assets/{clock-ARFGKas-.js => clock-BWp9w4xs.js} (77%) rename backend/internal/server/ui_dist/assets/{copy-Gc8n9M6v.js => copy-CTb6u-fx.js} (71%) rename backend/internal/server/ui_dist/assets/{flag-CdXTOLpP.js => flag-BkRqUrT5.js} (74%) rename backend/internal/server/ui_dist/assets/{git-compare-CXuIlVPE.js => git-compare-omnJl6y2.js} (86%) rename backend/internal/server/ui_dist/assets/{globe-BSxGWG92.js => globe-CR2M7Azm.js} (73%) rename backend/internal/server/ui_dist/assets/{index-D5cO6vit.js => index-BMkkwZ9q.js} (98%) rename backend/internal/server/ui_dist/assets/{key-round-D643nzM3.js => key-round-BccKiRw7.js} (81%) rename backend/internal/server/ui_dist/assets/{lock-C90zRIcI.js => lock-Dpr2FIZ9.js} (68%) rename backend/internal/server/ui_dist/assets/{pencil-CkSYbb1r.js => pencil-DTkm5-NQ.js} (76%) rename backend/internal/server/ui_dist/assets/{play-BfsTXwNm.js => play-CPjfKIOc.js} (65%) rename backend/internal/server/ui_dist/assets/{refresh-cw-BYFoMG0c.js => refresh-cw-C7sR7ShF.js} (79%) rename backend/internal/server/ui_dist/assets/{rotate-ccw-CeRUwJZR.js => rotate-ccw-CsgWy1Bs.js} (67%) rename backend/internal/server/ui_dist/assets/{settings-2-BhRqOkbZ.js => settings-2-CcqGdzLw.js} (92%) rename backend/internal/server/ui_dist/assets/{shield-check-BFAT8DDU.js => shield-check-sW6QCkG0.js} (79%) rename backend/internal/server/ui_dist/assets/{sparkles-DV3RWxRD.js => sparkles-BVQ_t_Q_.js} (86%) rename backend/internal/server/ui_dist/assets/{square-pen-WhijiULo.js => square-pen-CsqFW8Ka.js} (79%) rename backend/internal/server/ui_dist/assets/{terminal-CR-RL0k9.js => terminal-DAVNGL0P.js} (59%) rename backend/internal/server/ui_dist/assets/{trash-2-sSCgxcxW.js => trash-2-BXf2uqQH.js} (80%) rename backend/internal/server/ui_dist/assets/{variable-kwh9BTXT.js => variable-b2EnW52t.js} (77%) rename backend/internal/server/ui_dist/assets/{zap-CTttJ1hV.js => zap-DvhWYa2n.js} (75%) rename backend/runtimes/{node24 => node}/adapter.js (99%) rename backend/{cmd/orva/adapters/node24 => runtimes/node}/orva.d.ts (100%) rename backend/{cmd/orva/adapters/node24 => runtimes/node}/orva.js (100%) rename backend/{cmd/orva/adapters/node24 => runtimes/node}/package.json (100%) delete mode 100644 backend/runtimes/node22/adapter.js delete mode 100644 backend/runtimes/node22/orva.d.ts delete mode 100644 backend/runtimes/node22/orva.js delete mode 100644 backend/runtimes/node22/package.json delete mode 100644 backend/runtimes/node24/orva.d.ts delete mode 100644 backend/runtimes/node24/orva.js delete mode 100644 backend/runtimes/node24/package.json rename backend/{cmd/orva/adapters/python314 => runtimes/python}/adapter.py (99%) rename backend/{cmd/orva/adapters/python314 => runtimes/python}/orva.py (100%) rename backend/{cmd/orva/adapters/python314 => runtimes/python}/py.typed (100%) delete mode 100644 backend/runtimes/python313/adapter.py delete mode 100644 backend/runtimes/python313/orva.py delete mode 100644 backend/runtimes/python313/py.typed delete mode 100644 backend/runtimes/python314/orva.py delete mode 100644 backend/runtimes/python314/py.typed diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2cc6fe6..a137c5f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,7 +75,7 @@ jobs: - uses: actions/setup-node@v6 with: - node-version: "22" + node-version: "24" # No npm cache — every CI run starts from scratch so we # never pick up a stale lockfile or dependency tree. cache: "" @@ -117,7 +117,7 @@ jobs: - uses: actions/setup-node@v6 with: - node-version: "22" + node-version: "24" cache: "" - name: npm ci diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a6607a0..cfd7af4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,7 @@ name: Release # Triggers on `v*` tag pushes. Produces: # - orva-linux-{amd64,arm64} (Go binary, static) # - nsjail-linux-{amd64,arm64} (built from source, stripped) -# - rootfs-{node22,node24,python313,python314}-{amd64,arm64}.tar.zst +# - rootfs-{node,python}-{amd64,arm64}.tar.zst # (extracted from official Docker slim # images + adapter pre-installed) # - checksums.txt (sha256 of every asset) @@ -49,7 +49,7 @@ jobs: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: - node-version: "22" + node-version: "24" cache: "" - uses: actions/setup-go@v6 with: @@ -85,7 +85,7 @@ jobs: - uses: actions/setup-node@v6 with: - node-version: "22" + node-version: "24" # No npm cache — fresh install on every run. cache: "" @@ -283,7 +283,7 @@ jobs: strategy: fail-fast: false matrix: - runtime: [node22, node24, python313, python314] + runtime: [node, python] goarch: [amd64, arm64] include: - goarch: amd64 @@ -303,11 +303,11 @@ jobs: # Pre-install the adapter so install.sh doesn't need the source tree. mkdir -p "$GITHUB_WORKSPACE/rootfs/opt/orva" - if [ -f "runtimes/${{ matrix.runtime }}/adapter.js" ]; then - cp "runtimes/${{ matrix.runtime }}/adapter.js" "$GITHUB_WORKSPACE/rootfs/opt/orva/adapter.js" + if [ -f "backend/runtimes/${{ matrix.runtime }}/adapter.js" ]; then + cp "backend/runtimes/${{ matrix.runtime }}/adapter.js" "$GITHUB_WORKSPACE/rootfs/opt/orva/adapter.js" fi - if [ -f "runtimes/${{ matrix.runtime }}/adapter.py" ]; then - cp "runtimes/${{ matrix.runtime }}/adapter.py" "$GITHUB_WORKSPACE/rootfs/opt/orva/adapter.py" + if [ -f "backend/runtimes/${{ matrix.runtime }}/adapter.py" ]; then + cp "backend/runtimes/${{ matrix.runtime }}/adapter.py" "$GITHUB_WORKSPACE/rootfs/opt/orva/adapter.py" fi - name: tar + zstd compress @@ -411,7 +411,7 @@ jobs: | `orva-cli-darwin-{amd64,arm64}` | Slim CLI (macOS Intel + Apple Silicon) | | `orva-cli-windows-{amd64,arm64}.exe` | Slim CLI (Windows) — used by `install-cli.ps1` | | `nsjail-linux-{amd64,arm64}` | Pre-built nsjail (downloaded by install.sh) | - | `rootfs-{node22,node24,python313,python314}-{amd64,arm64}.tar.zst` | Language rootfs | + | `rootfs-{node,python}-{amd64,arm64}.tar.zst` | Language rootfs | | `install.sh`, `install-cli.sh`, `install-cli.ps1` | One-line installers | | `checksums.txt` | sha256 of every asset above | diff --git a/CLAUDE.md b/CLAUDE.md index 5f0ce1c..c609ad2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # Orva -Self-hosted Function-as-a-Service (FaaS) for homelab and on-premises use. Users write JavaScript (Node 22/24), Python (3.13/3.14), or TypeScript functions; Orva deploys them into nsjail sandboxes and exposes them over HTTP with a built-in dashboard, CLI, MCP server, and an in-product AI chat assistant (the **AI** sidebar section) that operates the instance end-to-end via in-process tool calling (BYO provider keys, embedded Bifrost gateway). +Self-hosted Function-as-a-Service (FaaS) for homelab and on-premises use. Users write JavaScript (Node.js 24), Python (3.14), or TypeScript functions — two generic runtimes, `node` and `python`, latest-stable only; Orva deploys them into nsjail sandboxes and exposes them over HTTP with a built-in dashboard, CLI, MCP server, and an in-product AI chat assistant (the **AI** sidebar section) that operates the instance end-to-end via in-process tool calling (BYO provider keys, embedded Bifrost gateway). ## Quick Start @@ -36,7 +36,7 @@ go.mod, go.sum Single Go module rooted at the repo (covers backend/ + cli/ + backend/ Go server (see backend/CLAUDE.md) cmd/orva/ Server entry: registers commands.NewRoot() + serve/setup/init internal/ Server packages (config, database, pool, proxy, mcp, …) - runtimes/ Runtime adapter source: node22, node24, python313, python314 + runtimes/ Runtime adapter source: node, python cli/ Slim standalone CLI codebase (see cli/CLAUDE.md) cmd/orva/ Slim CLI entry point (no server packages — ~12 MB binary) commands/ Cobra subcommand library — single source of truth for diff --git a/Dockerfile b/Dockerfile index 70e167f..c4522a6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ ARG VERSION=dev ARG COMMIT=unknown ARG BUILD_TIME=unknown -FROM node:22-alpine AS ui +FROM node:24-alpine AS ui WORKDIR /ui COPY frontend/package.json frontend/package-lock.json ./ RUN npm ci --no-audit --no-fund @@ -42,39 +42,24 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ RUN git clone --depth 1 https://github.com/google/nsjail.git /nsjail \ && cd /nsjail && make -j"$(nproc)" && strip nsjail -FROM node:22-slim AS rootfs-node22 +# Orva offers two runtimes, latest-stable only: node (Node.js 24) and +# python (Python 3.14). Bump the base image here to track a newer stable. +FROM node:24-slim AS rootfs-node RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \ && rm -rf /var/lib/apt/lists/* /usr/share/doc /usr/share/man /usr/share/locale \ && mkdir -p /opt/orva /opt/orva/node_modules/orva /code -COPY backend/runtimes/node22/adapter.js /opt/orva/adapter.js -COPY backend/runtimes/node22/orva.js /opt/orva/node_modules/orva/index.js +COPY backend/runtimes/node/adapter.js /opt/orva/adapter.js +COPY backend/runtimes/node/orva.js /opt/orva/node_modules/orva/index.js RUN echo '{"name":"orva","version":"0.2.0","main":"index.js"}' > /opt/orva/node_modules/orva/package.json -FROM node:24-slim AS rootfs-node24 -RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \ - && rm -rf /var/lib/apt/lists/* /usr/share/doc /usr/share/man /usr/share/locale \ - && mkdir -p /opt/orva /opt/orva/node_modules/orva /code -COPY backend/runtimes/node24/adapter.js /opt/orva/adapter.js -COPY backend/runtimes/node24/orva.js /opt/orva/node_modules/orva/index.js -RUN echo '{"name":"orva","version":"0.2.0","main":"index.js"}' > /opt/orva/node_modules/orva/package.json - -FROM python:3.13-slim AS rootfs-python313 -RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \ - && rm -rf /var/lib/apt/lists/* /usr/share/doc /usr/share/man /usr/share/locale \ - && find /usr/local/lib/python3.13 -depth -type d -name __pycache__ -exec rm -rf {} + \ - && find /usr/local/lib/python3.13 -depth -type d -name tests -exec rm -rf {} + \ - && mkdir -p /opt/orva /code -COPY backend/runtimes/python313/adapter.py /opt/orva/adapter.py -COPY backend/runtimes/python313/orva.py /opt/orva/orva.py - -FROM python:3.14-slim AS rootfs-python314 +FROM python:3.14-slim AS rootfs-python RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \ && rm -rf /var/lib/apt/lists/* /usr/share/doc /usr/share/man /usr/share/locale \ && find /usr/local/lib/python3.14 -depth -type d -name __pycache__ -exec rm -rf {} + \ && find /usr/local/lib/python3.14 -depth -type d -name tests -exec rm -rf {} + \ && mkdir -p /opt/orva /code -COPY backend/runtimes/python314/adapter.py /opt/orva/adapter.py -COPY backend/runtimes/python314/orva.py /opt/orva/orva.py +COPY backend/runtimes/python/adapter.py /opt/orva/adapter.py +COPY backend/runtimes/python/orva.py /opt/orva/orva.py FROM debian:bookworm-slim ARG VERSION @@ -102,10 +87,8 @@ COPY --from=nsjail /nsjail/nsjail /usr/local/bin/nsjail # uses the same binary; the entrypoint pre-writes ~/.orva/config.yaml so # common commands work without re-passing --endpoint / --api-key. COPY --from=go /out/orva /usr/local/bin/orva -COPY --from=rootfs-node22 / /opt/orva/rootfs/node22/ -COPY --from=rootfs-node24 / /opt/orva/rootfs/node24/ -COPY --from=rootfs-python313 / /opt/orva/rootfs/python313/ -COPY --from=rootfs-python314 / /opt/orva/rootfs/python314/ +COPY --from=rootfs-node / /opt/orva/rootfs/node/ +COPY --from=rootfs-python / /opt/orva/rootfs/python/ COPY scripts/entrypoint.sh /usr/local/bin/orva-entrypoint RUN chmod +x /usr/local/bin/orva-entrypoint diff --git a/Makefile b/Makefile index 230e57e..fa370d0 100644 --- a/Makefile +++ b/Makefile @@ -44,24 +44,16 @@ docs-embed: # Also copies the v0.2 orva SDK module (kv / invoke / jobs). adapters-embed: @rm -rf backend/cmd/orva/adapters - @mkdir -p backend/cmd/orva/adapters/node22 backend/cmd/orva/adapters/node24 \ - backend/cmd/orva/adapters/python313 backend/cmd/orva/adapters/python314 - @cp backend/runtimes/node22/adapter.js backend/cmd/orva/adapters/node22/adapter.js - @cp backend/runtimes/node24/adapter.js backend/cmd/orva/adapters/node24/adapter.js - @cp backend/runtimes/python313/adapter.py backend/cmd/orva/adapters/python313/adapter.py - @cp backend/runtimes/python314/adapter.py backend/cmd/orva/adapters/python314/adapter.py - @cp backend/runtimes/node22/orva.js backend/cmd/orva/adapters/node22/orva.js - @cp backend/runtimes/node24/orva.js backend/cmd/orva/adapters/node24/orva.js - @cp backend/runtimes/python313/orva.py backend/cmd/orva/adapters/python313/orva.py - @cp backend/runtimes/python314/orva.py backend/cmd/orva/adapters/python314/orva.py + @mkdir -p backend/cmd/orva/adapters/node backend/cmd/orva/adapters/python + @cp backend/runtimes/node/adapter.js backend/cmd/orva/adapters/node/adapter.js + @cp backend/runtimes/python/adapter.py backend/cmd/orva/adapters/python/adapter.py + @cp backend/runtimes/node/orva.js backend/cmd/orva/adapters/node/orva.js + @cp backend/runtimes/python/orva.py backend/cmd/orva/adapters/python/orva.py @# v0.6 SDK: ship .d.ts + package.json so TS handlers get types; @# py.typed marks the Python module as fully typed for static checkers. - @cp backend/runtimes/node22/orva.d.ts backend/cmd/orva/adapters/node22/orva.d.ts - @cp backend/runtimes/node22/package.json backend/cmd/orva/adapters/node22/package.json - @cp backend/runtimes/node24/orva.d.ts backend/cmd/orva/adapters/node24/orva.d.ts - @cp backend/runtimes/node24/package.json backend/cmd/orva/adapters/node24/package.json - @cp backend/runtimes/python313/py.typed backend/cmd/orva/adapters/python313/py.typed - @cp backend/runtimes/python314/py.typed backend/cmd/orva/adapters/python314/py.typed + @cp backend/runtimes/node/orva.d.ts backend/cmd/orva/adapters/node/orva.d.ts + @cp backend/runtimes/node/package.json backend/cmd/orva/adapters/node/package.json + @cp backend/runtimes/python/py.typed backend/cmd/orva/adapters/python/py.typed build: adapters-embed docs-embed @mkdir -p $(BUILD) diff --git a/README.md b/README.md index a5c31bb..131358e 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,7 @@ The Build info card at the top of Settings shows the running release's version, | Feature | Detail | |---|---| -| **Runtimes** | Node.js 22, Node.js 24, Python 3.13, Python 3.14, TypeScript (via Node) | +| **Runtimes** | `node` (Node.js 24, also runs TypeScript) and `python` (Python 3.14) — two runtimes, latest-stable only | | **Isolation** | Every invocation runs in a fresh nsjail sandbox — user namespace, chroot, cgroup v2, seccomp filter | | **Warm pools** | One pool per function; idle workers stay resident between calls so repeated invocations skip the spawn cost entirely. Pool size is configurable per function. | | **KV store** | Per-function key-value storage backed by SQLite. Use it as a cache, a counter, a session store, or lightweight persistent state. `kv.put / kv.get / kv.delete / kv.list` with optional TTL. Browsable and editable from the dashboard. | @@ -322,7 +322,7 @@ curl -fsSL https://github.com/Harsh-2002/Orva/releases/latest/download/orva-cli- orva login --endpoint https://orva.example.com --api-key orva functions list -orva deploy ./src --name my-fn --runtime node24 +orva deploy ./src --name my-fn --runtime node orva invoke my-fn --body '{"name":"world"}' orva logs my-fn --follow ``` diff --git a/backend/cmd/orva/adapters/node24/adapter.js b/backend/cmd/orva/adapters/node/adapter.js similarity index 99% rename from backend/cmd/orva/adapters/node24/adapter.js rename to backend/cmd/orva/adapters/node/adapter.js index e57d5b7..efcd486 100644 --- a/backend/cmd/orva/adapters/node24/adapter.js +++ b/backend/cmd/orva/adapters/node/adapter.js @@ -484,7 +484,7 @@ async function readFrame() { if (_depth) process.env.ORVA_CALL_DEPTH = _depth; else delete process.env.ORVA_CALL_DEPTH; - // v0.5 trace context — see node22 adapter for the rationale. + // v0.5 trace context — propagate the inbound trace/span ids to F2F calls. const _tID = _hdrs['x-orva-trace-id'] || _hdrs['X-Orva-Trace-Id'] || ''; const _sID = _hdrs['x-orva-span-id'] || _hdrs['X-Orva-Span-Id'] || ''; if (_tID) process.env.ORVA_TRACE_ID = _tID; else delete process.env.ORVA_TRACE_ID; diff --git a/backend/cmd/orva/adapters/node22/orva.d.ts b/backend/cmd/orva/adapters/node/orva.d.ts similarity index 100% rename from backend/cmd/orva/adapters/node22/orva.d.ts rename to backend/cmd/orva/adapters/node/orva.d.ts diff --git a/backend/cmd/orva/adapters/node22/orva.js b/backend/cmd/orva/adapters/node/orva.js similarity index 100% rename from backend/cmd/orva/adapters/node22/orva.js rename to backend/cmd/orva/adapters/node/orva.js diff --git a/backend/cmd/orva/adapters/node22/package.json b/backend/cmd/orva/adapters/node/package.json similarity index 100% rename from backend/cmd/orva/adapters/node22/package.json rename to backend/cmd/orva/adapters/node/package.json diff --git a/backend/cmd/orva/adapters/node22/adapter.js b/backend/cmd/orva/adapters/node22/adapter.js deleted file mode 100644 index f9fb924..0000000 --- a/backend/cmd/orva/adapters/node22/adapter.js +++ /dev/null @@ -1,582 +0,0 @@ -// Orva Node.js adapter — universal handler loader. -// -// Accepts a wide range of export conventions so existing code from AWS -// Lambda, Cloudflare Workers, Vercel, Next.js, Netlify, and generic -// Node/Express style deploys runs with zero changes: -// -// AWS Lambda : exports.handler = async (event, context) => ... -// exports.lambda_handler = async (event, context) => ... -// Cloudflare Worker : export default { fetch(request, env, ctx) { ... } } -// addEventListener('fetch', e => e.respondWith(...)) -// Vercel / Next API : export default async function handler(req, res) { ... } -// Netlify / generic : exports.handler = async (event) => ... -// Plain function : module.exports = async (event) => ... -// -// The adapter normalises all of them to a { statusCode, headers, body } -// response envelope, the native Orva protocol. - -const path = require('path'); -const Module = require('module'); - -const FUNCTION_DIR = '/code'; -const entrypoint = process.env.ORVA_ENTRYPOINT || 'handler.js'; -const handlerPath = path.join(FUNCTION_DIR, entrypoint); - -// Make the bundled `orva` SDK module (kv / invoke / jobs) resolvable -// from user code via `require('orva')`. The package lives at -// /opt/orva/node_modules/orva/; injecting that dir at the front of -// Module._nodeModulePaths ensures user modules can find it without -// the user having to install it. User-installed deps still resolve -// normally via /code/node_modules. -const _origNodeModulePaths = Module._nodeModulePaths; -Module._nodeModulePaths = function (from) { - return ['/opt/orva/node_modules', ...(_origNodeModulePaths.call(this, from) || [])]; -}; - -// Preserve stdout for the protocol response; reroute user output to stderr. -const originalStdoutWrite = process.stdout.write.bind(process.stdout); -const writeProtocol = (s) => originalStdoutWrite(s); -process.stdout.write = process.stderr.write.bind(process.stderr); -console.log = (...a) => process.stderr.write(a.map(String).join(' ') + '\n'); -console.info = console.log; -console.debug = console.log; -console.warn = console.log; -console.error = console.log; - -let mod; -try { - mod = require(handlerPath); -} catch (err) { - process.stderr.write(`Failed to load handler from ${handlerPath}: ${err.message}\n`); - process.exit(1); -} - -// Unwrap ESM default export shim (Babel/TS sometimes emit { default: fn }). -if (mod && typeof mod === 'object' && mod.__esModule && mod.default) { - mod = mod.default; -} - -// Resolve a callable from whatever shape the user exported. -let handler = null; -let style = null; // "lambda" | "worker" | "vercel" | "plain" - -if (typeof mod === 'function') { - handler = mod; - // A plain function could be Lambda-style or Vercel-style — decide by arity. - style = mod.length >= 2 ? 'vercel-or-lambda' : 'plain'; -} else if (mod && typeof mod === 'object') { - // Cloudflare Worker: { fetch(request, env, ctx) } - if (typeof mod.fetch === 'function') { - handler = mod.fetch; - style = 'worker'; - } else if (typeof mod.handler === 'function') { - handler = mod.handler; - style = 'lambda'; - } else if (typeof mod.lambda_handler === 'function') { - handler = mod.lambda_handler; - style = 'lambda'; - } else if (typeof mod.main === 'function') { - handler = mod.main; - style = 'lambda'; - } else if (typeof mod.default === 'function') { - handler = mod.default; - style = 'plain'; - } -} - -if (!handler) { - process.stderr.write( - `Module at ${handlerPath} does not export a usable handler. ` + - `Expected one of: handler, lambda_handler, main, fetch, default, ` + - `or a default function export.\n` - ); - process.exit(1); -} - -// ── Helpers to bridge calling conventions ────────────────────────────── - -function buildLambdaContext(event) { - const hdrs = (event && event.headers) || {}; - return { - functionName: process.env.ORVA_FUNCTION_NAME || '', - awsRequestId: hdrs['x-orva-execution-id'] || '', - invokedFunctionArn: '', - memoryLimitInMB: process.env.ORVA_MEMORY_MB || '', - logGroupName: 'orva', - logStreamName: hdrs['x-orva-execution-id'] || '', - getRemainingTimeInMillis: () => Number(process.env.ORVA_TIMEOUT_MS || 30000), - }; -} - -// Minimal Request/Response polyfills for Cloudflare Worker style. If the -// runtime provides them natively (Node 18+ has global fetch), prefer those. -function buildWorkerRequest(event) { - const url = `http://localhost${event.path || '/'}`; - if (typeof Request === 'function') { - try { - return new Request(url, { - method: event.method || 'GET', - headers: event.headers || {}, - body: ['GET', 'HEAD'].includes((event.method || 'GET').toUpperCase()) - ? undefined - : (event.body || ''), - }); - } catch { /* fall through to shim */ } - } - return { - method: event.method || 'GET', - url, - headers: new Map(Object.entries(event.headers || {})), - body: event.body || '', - async text() { return this.body; }, - async json() { return JSON.parse(this.body || '{}'); }, - async arrayBuffer() { return Buffer.from(this.body || '').buffer; }, - }; -} - -async function normaliseResponse(ret) { - // Already in Orva envelope. - if (ret && typeof ret === 'object' && 'statusCode' in ret) { - return { - statusCode: ret.statusCode || 200, - headers: ret.headers || { 'Content-Type': 'application/json' }, - body: typeof ret.body === 'string' ? ret.body : JSON.stringify(ret.body || {}), - }; - } - // Fetch API Response. - if (ret && typeof ret === 'object' && typeof ret.status === 'number' && typeof ret.text === 'function') { - const body = await ret.text(); - const headers = {}; - if (ret.headers && typeof ret.headers.forEach === 'function') { - ret.headers.forEach((v, k) => { headers[k] = v; }); - } - return { statusCode: ret.status, headers, body }; - } - // Anything else → JSON-encode as the body. - return { - statusCode: 200, - headers: { 'Content-Type': 'application/json' }, - body: typeof ret === 'string' ? ret : JSON.stringify(ret ?? null), - }; -} - -// Vercel/Next-style (req, res) detection: wrap to capture the response. -function invokeVercelStyle(fn, event) { - return new Promise((resolve, reject) => { - // Parse query string out of event.path. Vercel/Next handlers - // expect req.query to be populated; previously hardcoded {} so any - // ?k=v on the URL was invisible to the handler. URLSearchParams - // gives us decode + multi-value handling for free. - const rawPath = event.path || '/'; - const qIdx = rawPath.indexOf('?'); - const query = {}; - if (qIdx >= 0) { - for (const [k, v] of new URLSearchParams(rawPath.slice(qIdx + 1))) { - query[k] = v; - } - } - const req = { - method: event.method || 'GET', - url: rawPath, - headers: event.headers || {}, - body: (() => { - try { return JSON.parse(event.body || 'null'); } catch { return event.body; } - })(), - query, - }; - let statusCode = 200; - const headers = {}; - let body = ''; - const res = { - status(c) { statusCode = c; return this; }, - setHeader(k, v) { headers[k] = v; return this; }, - getHeader(k) { return headers[k]; }, - json(o) { headers['Content-Type'] = 'application/json'; body = JSON.stringify(o); this.end(); }, - send(x) { body = typeof x === 'string' ? x : JSON.stringify(x); this.end(); }, - write(x) { body += typeof x === 'string' ? x : String(x); }, - end(x) { - if (x !== undefined) body = typeof x === 'string' ? x : String(x); - resolve({ statusCode, headers, body }); - }, - }; - Promise.resolve() - .then(() => fn(req, res)) - .catch(reject); - }); -} - -// ── Framed stdio protocol ────────────────────────────────────────────── -// Wire format: 4-byte big-endian uint32 length, then N bytes of UTF-8 JSON. -// Applied symmetrically to stdin (proxy → adapter) and stdout (adapter → -// proxy). Length-prefix chosen over JSONL because it is binary-safe and -// handles the full 6 MB MaxBodyBytes without escape gymnastics. - -async function dispatch(event) { - if (style === 'worker') { - const req = buildWorkerRequest(event); - const env = { ...process.env }; - const ctx = { waitUntil: () => {}, passThroughOnException: () => {} }; - return await handler(req, env, ctx); - } - if (style === 'vercel-or-lambda') { - // Two-arg function: try Lambda first (returns a value). If it returns - // undefined OR throws a TypeError trying to treat ctx as `res`, assume - // Vercel (req, res) style and replay via invokeVercelStyle. - const ctx = buildLambdaContext(event); - let lambdaResult; - let lambdaErr; - try { - lambdaResult = await handler(event, ctx); - } catch (e) { - lambdaErr = e; - } - const looksLikeVercelMiss = - lambdaErr && - (lambdaErr instanceof TypeError) && - /is not a function|Cannot read propert/i.test(String(lambdaErr.message)); - if (lambdaResult === undefined || looksLikeVercelMiss) { - return await invokeVercelStyle(handler, event); - } - if (lambdaErr) throw lambdaErr; - return lambdaResult; - } - if (style === 'lambda') return await handler(event, buildLambdaContext(event)); - return await handler(event); -} - -function writeFrame(obj) { - const body = Buffer.from(JSON.stringify(obj), 'utf-8'); - const hdr = Buffer.alloc(4); - hdr.writeUInt32BE(body.length, 0); - // Node's process.stdout.write returns false if the kernel pipe buffer - // is full (backpressure from the proxy). We ignore the return — the - // next write blocks naturally until drain. EPIPE is rethrown to the - // caller so streaming loops can stop iterating on client disconnect. - originalStdoutWrite(hdr); - originalStdoutWrite(body); -} - -// v0.4 C1: streaming helpers — translate user-yielded values into -// `chunk` frames. data may be Buffer | string | Uint8Array | object. -function streamChunk(data) { - let buf; - if (data == null) { - buf = Buffer.alloc(0); - } else if (Buffer.isBuffer(data)) { - buf = data; - } else if (data instanceof Uint8Array) { - buf = Buffer.from(data.buffer, data.byteOffset, data.byteLength); - } else if (typeof data === 'string') { - buf = Buffer.from(data, 'utf-8'); - } else { - buf = Buffer.from(JSON.stringify(data), 'utf-8'); - } - writeFrame({ type: 'chunk', data: buf.length === 0 ? '' : buf.toString('base64') }); -} - -function looksLikeHead(item) { - if (!item || typeof item !== 'object') return null; - if (!('statusCode' in item)) return null; - return { - status: item.statusCode || 200, - headers: item.headers || { 'Content-Type': 'text/plain' }, - body: 'body' in item ? item.body : null, - }; -} - -// streamIterable consumes any sync/async iterable and emits the -// streaming protocol exchange. When streamingEnabled is false we -// buffer everything into a single response frame for back-compat — -// operators flipping the system_config flag get pre-C1 behaviour -// without redeploying. -async function streamIterable(iterable, streamingEnabled, keepaliveMs) { - if (!streamingEnabled) { - let head = null; - const parts = []; - const collect = (item) => { - if (head === null) { - const detected = looksLikeHead(item); - if (detected) { - head = { status: detected.status, headers: detected.headers }; - if (detected.body != null) parts.push(typeof detected.body === 'string' ? detected.body : String(detected.body)); - return; - } - head = { status: 200, headers: { 'Content-Type': 'text/plain' } }; - } - if (Buffer.isBuffer(item)) parts.push(item.toString('utf-8')); - else if (item instanceof Uint8Array) parts.push(Buffer.from(item).toString('utf-8')); - else if (typeof item === 'string') parts.push(item); - else parts.push(String(item)); - }; - for await (const item of iterable) collect(item); - if (head === null) head = { status: 200, headers: { 'Content-Type': 'text/plain' } }; - writeFrame({ - type: 'response', statusCode: head.status, - headers: head.headers, body: parts.join(''), - }); - return; - } - - let headSent = false; - let lastEmit = Date.now(); - let hbTimer = null; - - const sendHead = (status, headers) => { - if (headSent) return; - writeFrame({ type: 'response_start', statusCode: status, headers }); - headSent = true; - }; - - const startHeartbeat = () => { - if (hbTimer) return; - // setInterval fires regardless of how busy the loop is. The check - // against lastEmit prevents an empty chunk from racing a real one; - // worst case we emit one extra empty chunk per period, which is - // harmless on the wire. - hbTimer = setInterval(() => { - if (Date.now() - lastEmit >= keepaliveMs) { - try { - writeFrame({ type: 'chunk', data: '' }); - lastEmit = Date.now(); - } catch { /* pipe closed — clearInterval below */ } - } - }, keepaliveMs); - }; - - try { - for await (const item of iterable) { - if (!headSent) { - const detected = looksLikeHead(item); - if (detected) { - sendHead(detected.status, detected.headers); - startHeartbeat(); - if (detected.body != null && detected.body !== '') { - streamChunk(detected.body); - lastEmit = Date.now(); - } - continue; - } - sendHead(200, { 'Content-Type': 'text/plain; charset=utf-8' }); - startHeartbeat(); - } - streamChunk(item); - lastEmit = Date.now(); - } - if (!headSent) sendHead(200, { 'Content-Type': 'text/plain; charset=utf-8' }); - writeFrame({ type: 'response_end' }); - } catch (err) { - // EPIPE = client disconnected mid-stream. Stop iterating; the - // worker continues serving subsequent requests if its stdin is - // still open. Anything else we re-raise so the outer try/catch - // emits an error frame BEFORE the head went out. - if (err && err.code !== 'EPIPE') { - if (!headSent) throw err; - // Head already flew — nothing useful to surface. Best-effort end. - try { writeFrame({ type: 'response_end' }); } catch {} - } - } finally { - if (hbTimer) clearInterval(hbTimer); - } -} - -// drainReadableStream yields chunks from a fetch-API ReadableStream -// (e.g. `new Response(stream).body`). Used when the handler returns a -// Response whose body is a stream — we surface it as if the user had -// written an async generator yielding bytes. -async function* drainReadableStream(readable) { - const reader = readable.getReader(); - try { - for (;;) { - const { done, value } = await reader.read(); - if (done) return; - if (value) yield value; - } - } finally { - try { reader.releaseLock(); } catch {} - } -} - -// readExactly reads exactly n bytes from process.stdin, resolving with null -// on EOF. Works by concatenating the available readable chunks. -function readExactly(n) { - return new Promise((resolve, reject) => { - const out = []; - let have = 0; - const stdin = process.stdin; - - const onReadable = () => { - let chunk; - while ((chunk = stdin.read(Math.min(n - have, 65536))) !== null) { - out.push(chunk); - have += chunk.length; - if (have >= n) { - cleanup(); - return resolve(Buffer.concat(out, have)); - } - } - }; - const onEnd = () => { cleanup(); resolve(null); }; - const onError = (err) => { cleanup(); reject(err); }; - const cleanup = () => { - stdin.removeListener('readable', onReadable); - stdin.removeListener('end', onEnd); - stdin.removeListener('error', onError); - }; - - stdin.on('readable', onReadable); - stdin.on('end', onEnd); - stdin.on('error', onError); - // In case data is already buffered. - onReadable(); - }); -} - -async function readFrame() { - const header = await readExactly(4); - if (!header) return null; - const len = header.readUInt32BE(0); - if (len === 0) return {}; - const payload = await readExactly(len); - if (!payload) return null; - try { - return JSON.parse(payload.toString('utf-8')); - } catch (err) { - return { type: 'request', event: { method: 'POST', path: '/', headers: {}, body: '' } }; - } -} - -(async () => { - // Optional recycle cap: after MAX_REQUESTS dispatches, exit so the pool - // respawns and we avoid any slow memory creep in user code. - const maxReqs = Number(process.env.ORVA_MAX_REQUESTS || 0); - let served = 0; - - for (;;) { - const frame = await readFrame(); - if (!frame) process.exit(0); // stdin EOF = clean shutdown - if (frame.type === 'quit') { - writeFrame({ type: 'bye' }); - process.exit(0); - } - if (frame.type !== 'request') continue; - - const event = frame.event || { method: 'POST', path: '/', headers: {}, body: '' }; - // Enrich event.query from event.path. The proxy now passes the raw - // path including ?foo=bar; both Lambda-style handlers (event.query) - // and Vercel/Worker-style handlers (req.query) expect this to be - // already parsed, so we do it once here at the frame boundary. - { - const _p = event.path || '/'; - const _qIdx = _p.indexOf('?'); - const _q = {}; - if (_qIdx >= 0) { - for (const [k, v] of new URLSearchParams(_p.slice(_qIdx + 1))) { - _q[k] = v; - } - } - event.query = _q; - } - // Propagate call depth into env so orva.invoke()'s SDK can forward - // it on outbound nested calls. Otherwise the host's depth guard - // never trips on recursion. - const _hdrs = event.headers || {}; - const _depth = _hdrs['x-orva-call-depth'] || _hdrs['X-Orva-Call-Depth'] || ''; - if (_depth) process.env.ORVA_CALL_DEPTH = _depth; - else delete process.env.ORVA_CALL_DEPTH; - - // v0.5 trace context. Each event carries the trace_id + span_id of - // this invocation; the SDK reads them from env when issuing nested - // F2F calls or job enqueues so causal chains stay linked. - const _tID = _hdrs['x-orva-trace-id'] || _hdrs['X-Orva-Trace-Id'] || ''; - const _sID = _hdrs['x-orva-span-id'] || _hdrs['X-Orva-Span-Id'] || ''; - if (_tID) process.env.ORVA_TRACE_ID = _tID; else delete process.env.ORVA_TRACE_ID; - if (_sID) process.env.ORVA_SPAN_ID = _sID; else delete process.env.ORVA_SPAN_ID; - - // v0.6 SDK: trace.span() / log.* need the execution id. - const _eID = _hdrs['x-orva-execution-id'] || _hdrs['X-Orva-Execution-Id'] || ''; - if (_eID) process.env.ORVA_EXECUTION_ID = _eID; - else delete process.env.ORVA_EXECUTION_ID; - - // v0.4 C1: streaming flag + heartbeat interval ride on per-request - // headers so the proxy can flip them at runtime without redeploying - // the worker. Defaults match the system_config seed values. - const streamingOn = (_hdrs['x-orva-streaming-enabled'] ?? '1') !== '0'; - const keepaliveS = Math.max(1, Number(_hdrs['x-orva-stream-keepalive-seconds'] ?? '15') || 15); - const keepaliveMs = keepaliveS * 1000; - - try { - const result = await dispatch(event); - - // Streaming detection — async iterables, sync iterables, or a - // Response whose body is a ReadableStream. Order matters: - // Response objects are also iterable (ReadableStream is async- - // iterable in Node 18+) so we MUST check the Response branch - // first to extract status + headers, then drain the body stream. - if ( - result && - typeof result === 'object' && - typeof result.status === 'number' && - result.body && typeof result.body.getReader === 'function' - ) { - const headers = {}; - if (result.headers && typeof result.headers.forEach === 'function') { - result.headers.forEach((v, k) => { headers[k] = v; }); - } - // Wrap in a generator that prepends the head, then yields chunks. - async function* withHead() { - yield { statusCode: result.status, headers, body: '' }; - for await (const chunk of drainReadableStream(result.body)) yield chunk; - } - await streamIterable(withHead(), streamingOn, keepaliveMs); - } else if ( - result != null && - typeof result === 'object' && - (typeof result[Symbol.asyncIterator] === 'function' || - (typeof result[Symbol.iterator] === 'function' && typeof result !== 'string')) - ) { - // Exclude strings, Buffers, and arrays from being treated as - // streaming iterables — those are perfectly valid response - // bodies and shouldn't surprise the user. - const isExcluded = - typeof result === 'string' || - Buffer.isBuffer(result) || - Array.isArray(result) || - result instanceof Uint8Array; - if (isExcluded) { - const out = await normaliseResponse(result); - writeFrame({ type: 'response', statusCode: out.statusCode, headers: out.headers, body: out.body }); - } else { - await streamIterable(result, streamingOn, keepaliveMs); - } - } else { - const out = await normaliseResponse(result); - writeFrame({ type: 'response', statusCode: out.statusCode, headers: out.headers, body: out.body }); - } - } catch (err) { - process.stderr.write(`Handler error: ${err.stack || err.message}\n`); - // If we already started streaming the response head, the proxy is - // mid-loop and writeFrame on a fresh "response" envelope would - // confuse it. Best-effort: just close. The proxy will surface the - // truncation via the connection close. - try { - writeFrame({ - type: 'response', - statusCode: 500, - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ error: 'Internal function error', message: err.message }), - }); - } catch {} - } - - served++; - if (maxReqs > 0 && served >= maxReqs) { - writeFrame({ type: 'bye' }); - process.exit(0); - } - } -})().catch((err) => { - try { - writeFrame({ type: 'error', fatal: true, message: String(err && err.stack || err) }); - } catch {} - process.exit(1); -}); diff --git a/backend/runtimes/python314/adapter.py b/backend/cmd/orva/adapters/python/adapter.py similarity index 99% rename from backend/runtimes/python314/adapter.py rename to backend/cmd/orva/adapters/python/adapter.py index 4abc472..8eb61d0 100644 --- a/backend/runtimes/python314/adapter.py +++ b/backend/cmd/orva/adapters/python/adapter.py @@ -648,7 +648,7 @@ def _dispatch_and_emit(event, streaming_enabled, keepalive_s): else: os.environ.pop("ORVA_CALL_DEPTH", None) - # v0.5 trace context — see python313 adapter for the rationale. + # v0.5 trace context — propagate the inbound trace/span ids to F2F calls. _tid = _hdrs.get("x-orva-trace-id") or _hdrs.get("X-Orva-Trace-Id") or "" _sid = _hdrs.get("x-orva-span-id") or _hdrs.get("X-Orva-Span-Id") or "" if _tid: os.environ["ORVA_TRACE_ID"] = _tid diff --git a/backend/cmd/orva/adapters/python313/orva.py b/backend/cmd/orva/adapters/python/orva.py similarity index 100% rename from backend/cmd/orva/adapters/python313/orva.py rename to backend/cmd/orva/adapters/python/orva.py diff --git a/backend/cmd/orva/adapters/python313/py.typed b/backend/cmd/orva/adapters/python/py.typed similarity index 100% rename from backend/cmd/orva/adapters/python313/py.typed rename to backend/cmd/orva/adapters/python/py.typed diff --git a/backend/cmd/orva/adapters/python313/adapter.py b/backend/cmd/orva/adapters/python313/adapter.py deleted file mode 100644 index e789c2a..0000000 --- a/backend/cmd/orva/adapters/python313/adapter.py +++ /dev/null @@ -1,707 +0,0 @@ -"""Orva Python adapter — universal handler loader. - -Accepts a wide range of conventions so existing code from AWS Lambda, -Google Cloud Functions, Azure, FastAPI, Flask, Django, Starlette, and -generic Python deployments runs with zero changes: - - AWS Lambda : def lambda_handler(event, context): ... - def handler(event, context): ... - GCP Functions : def main(request): ... (Flask Request) - Azure Functions : def main(req): ... (HttpRequest) - ASGI (FastAPI, - Starlette) : app = FastAPI() / app = Starlette() - WSGI (Flask, - Django) : app = Flask(__name__) / app = ... (WSGI callable) - Plain : def handler(event): ... - -Response normalisation: the adapter accepts dicts in the Orva envelope -({statusCode, headers, body}), Starlette/FastAPI Response objects, Flask -Response objects, plain strings/dicts, or any ASGI app response. Everything -ends up as a single {statusCode, headers, body} JSON payload written to -stdout for the Orva proxy. -""" - -import asyncio -import base64 -import importlib.util -import inspect -import json -import os -import sys -import threading -import time -import traceback - -FUNCTION_DIR = "/code" -entrypoint = os.environ.get("ORVA_ENTRYPOINT", "handler.py") -handler_path = os.path.join(FUNCTION_DIR, entrypoint) - -# Preserve the real stdout for the protocol response; reroute user print(). -protocol_stdout = sys.stdout -sys.stdout = sys.stderr - -if FUNCTION_DIR not in sys.path: - sys.path.insert(0, FUNCTION_DIR) - -# Make the bundled `orva` SDK (kv / invoke / jobs) importable from user -# code as `from orva import kv, invoke, jobs`. /opt/orva is the dir -# adapter.py itself runs from, but Python only auto-adds it to sys.path -# when invoked as `python /opt/orva/adapter.py` AND nothing has -# rewritten sys.path[0]. Insert explicitly so the import works -# regardless of how the adapter was invoked. -if "/opt/orva" not in sys.path: - sys.path.insert(0, "/opt/orva") - - -class _Context: - """Minimal AWS-Lambda-like context object.""" - - def __init__(self, event): - hdrs = event.get("headers", {}) if isinstance(event, dict) else {} - self.function_name = os.environ.get("ORVA_FUNCTION_NAME", "") - self.aws_request_id = hdrs.get("x-orva-execution-id", "") - self.invoked_function_arn = "" - self.memory_limit_in_mb = os.environ.get("ORVA_MEMORY_MB", "") - self.log_group_name = "orva" - self.log_stream_name = hdrs.get("x-orva-execution-id", "") - - def get_remaining_time_in_millis(self): - return int(os.environ.get("ORVA_TIMEOUT_MS", "30000")) - - -def _load_module(): - if not os.path.exists(handler_path): - print(f"Handler not found at {handler_path}", file=sys.stderr) - sys.exit(1) - spec = importlib.util.spec_from_file_location("user_handler", handler_path) - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - return mod - - -def _resolve(mod): - """Return (callable, style) where style is one of: - 'lambda', 'asgi', 'wsgi', 'gcp_flask_request', 'plain'. - """ - # ASGI frameworks (FastAPI, Starlette, Quart) — `app` is an ASGI callable. - for name in ("app", "application"): - app = getattr(mod, name, None) - if app is None: - continue - # ASGI apps have a __call__(scope, receive, send) signature and are - # usually async. Detect by presence of `__call__` accepting 3 args. - if callable(app): - sig = None - try: - sig = inspect.signature(app.__call__ if hasattr(app, "__call__") else app) - except (TypeError, ValueError): - pass - if sig and len(sig.parameters) == 3: - return (app, "asgi") - # Fall through — treat as WSGI (Flask, Django WSGI, etc.). - return (app, "wsgi") - - # Function-style exports, in priority order. - for name in ("handler", "lambda_handler", "main"): - fn = getattr(mod, name, None) - if callable(fn): - return (fn, "lambda") - - return (None, None) - - -mod = _load_module() -handler, style = _resolve(mod) - -if handler is None: - print( - f"Module at {handler_path} does not export a usable handler. " - f"Expected one of: handler, lambda_handler, main, or an ASGI/WSGI `app`.", - file=sys.stderr, - ) - sys.exit(1) - - -# ── Response normalisation ───────────────────────────────────────────── - -def _normalise_response(ret): - """Convert whatever the handler returned into (status, headers, body).""" - # Already an Orva envelope. - if isinstance(ret, dict) and "statusCode" in ret: - status = ret.get("statusCode", 200) - headers = ret.get("headers", {"Content-Type": "application/json"}) - body = ret.get("body", "") - if not isinstance(body, str): - body = json.dumps(body) - return (status, headers, body) - - # Starlette / FastAPI Response objects. - if hasattr(ret, "status_code") and hasattr(ret, "body"): - body = ret.body - if isinstance(body, (bytes, bytearray)): - body = body.decode("utf-8", errors="replace") - headers = {} - if hasattr(ret, "headers"): - try: - for k, v in ret.headers.items(): - headers[k] = v - except Exception: - pass - return (ret.status_code, headers, body or "") - - # Flask Response objects. - if hasattr(ret, "status_code") and hasattr(ret, "get_data"): - body = ret.get_data(as_text=True) - headers = dict(ret.headers) if hasattr(ret, "headers") else {} - return (ret.status_code, headers, body) - - # Plain string, dict, or anything else. - if isinstance(ret, str): - return (200, {"Content-Type": "text/plain"}, ret) - return (200, {"Content-Type": "application/json"}, json.dumps(ret)) - - -# ── Invocation bridges ───────────────────────────────────────────────── - -def _build_flask_request(event): - """Build a werkzeug Request (for GCP Functions / flask-style `main(request)`).""" - try: - from werkzeug.wrappers import Request - from werkzeug.test import EnvironBuilder - except ImportError: - return None - body = event.get("body") or "" - builder = EnvironBuilder( - method=event.get("method", "POST"), - path=event.get("path", "/"), - headers=event.get("headers", {}), - data=body.encode("utf-8") if isinstance(body, str) else body, - ) - return Request(builder.get_environ()) - - -async def _call_asgi(app, event): - """Drive an ASGI app through one request/response cycle.""" - body_bytes = (event.get("body") or "").encode("utf-8") - headers_list = [ - (k.lower().encode(), str(v).encode()) - for k, v in (event.get("headers") or {}).items() - ] - path = event.get("path", "/") or "/" - query = "" - if "?" in path: - path, query = path.split("?", 1) - - scope = { - "type": "http", - "asgi": {"version": "3.0", "spec_version": "2.3"}, - "http_version": "1.1", - "method": event.get("method", "GET"), - "scheme": "http", - "path": path, - "raw_path": path.encode(), - "query_string": query.encode(), - "headers": headers_list, - "server": ("orva", 8443), - "client": ("127.0.0.1", 0), - } - - body_sent = False - - async def receive(): - nonlocal body_sent - if body_sent: - return {"type": "http.disconnect"} - body_sent = True - return {"type": "http.request", "body": body_bytes, "more_body": False} - - response = {"status": 200, "headers": {}, "body": b""} - - async def send(message): - if message["type"] == "http.response.start": - response["status"] = message["status"] - for k, v in message.get("headers", []): - response["headers"][k.decode()] = v.decode() - elif message["type"] == "http.response.body": - response["body"] += message.get("body", b"") - - await app(scope, receive, send) - return ( - response["status"], - response["headers"], - response["body"].decode("utf-8", errors="replace"), - ) - - -def _call_wsgi(app, event): - """Drive a WSGI app (Flask, Django WSGI) through one request cycle.""" - from io import BytesIO - - body = event.get("body") or "" - body_bytes = body.encode("utf-8") if isinstance(body, str) else body - path = event.get("path", "/") or "/" - query = "" - if "?" in path: - path, query = path.split("?", 1) - - environ = { - "REQUEST_METHOD": event.get("method", "GET"), - "SCRIPT_NAME": "", - "PATH_INFO": path, - "QUERY_STRING": query, - "SERVER_NAME": "orva", - "SERVER_PORT": "8443", - "SERVER_PROTOCOL": "HTTP/1.1", - "wsgi.version": (1, 0), - "wsgi.url_scheme": "http", - "wsgi.input": BytesIO(body_bytes), - "wsgi.errors": sys.stderr, - "wsgi.multithread": False, - "wsgi.multiprocess": False, - "wsgi.run_once": True, - "CONTENT_LENGTH": str(len(body_bytes)), - } - for k, v in (event.get("headers") or {}).items(): - key = "HTTP_" + k.upper().replace("-", "_") - environ[key] = str(v) - if k.lower() == "content-type": - environ["CONTENT_TYPE"] = str(v) - - captured = {"status": "200 OK", "headers": []} - - def start_response(status, headers, exc_info=None): - captured["status"] = status - captured["headers"] = headers - return lambda x: None - - chunks = app(environ, start_response) - body_out = b"".join(chunks if not isinstance(chunks, (bytes, str)) else [chunks]) - if isinstance(body_out, str): - body_out = body_out.encode("utf-8") - - status_code = int(captured["status"].split(" ", 1)[0]) - headers_dict = {k: v for k, v in captured["headers"]} - return (status_code, headers_dict, body_out.decode("utf-8", errors="replace")) - - -# ── Framed stdio protocol ────────────────────────────────────────────── -# Wire format: 4-byte big-endian uint32 length, then N bytes UTF-8 JSON. -# Same on stdin (proxy → adapter) and stdout (adapter → proxy). - -import struct - -_stdin = sys.stdin.buffer -_stdout = protocol_stdout.buffer if hasattr(protocol_stdout, "buffer") else protocol_stdout - - -def _read_exact(n): - buf = bytearray() - while len(buf) < n: - chunk = _stdin.read(n - len(buf)) - if not chunk: - return None - buf.extend(chunk) - return bytes(buf) - - -def _read_frame(): - header = _read_exact(4) - if header is None: - return None - (length,) = struct.unpack(">I", header) - if length == 0: - return {} - payload = _read_exact(length) - if payload is None: - return None - try: - return json.loads(payload.decode("utf-8")) - except Exception: - return {"type": "request", "event": {"method": "POST", "path": "/", "headers": {}, "body": ""}} - - -# Stdout writes must be serialised — the heartbeat thread (sync generators) -# and the foreground yield-loop both write frames. JSON+length-prefix means -# any interleave corrupts the wire. The lock is uncontended on the hot -# path (one writer at a time when no heartbeat is firing). -_stdout_lock = threading.Lock() - - -def _write_frame(obj): - body = json.dumps(obj).encode("utf-8") - with _stdout_lock: - try: - _stdout.write(struct.pack(">I", len(body))) - _stdout.write(body) - _stdout.flush() - except (BrokenPipeError, OSError): - # The proxy went away — typically because the HTTP client - # disconnected mid-stream. Re-raise so the caller can stop - # iterating; the worker process exit then unblocks the pool. - raise - - -def _stream_chunk(data): - """Send a single chunk frame. data may be bytes / bytearray / str.""" - if isinstance(data, (bytes, bytearray)): - b = bytes(data) - elif isinstance(data, str): - b = data.encode("utf-8") - elif data is None: - b = b"" - else: - # Any other type: best-effort JSON-encode then utf-8. - b = json.dumps(data).encode("utf-8") - encoded = base64.b64encode(b).decode("ascii") if b else "" - _write_frame({"type": "chunk", "data": encoded}) - - -def _looks_like_head(item): - """First yield can carry the response head if it's an Orva-shaped dict - with statusCode (and no body, or with body that we treat as the first - chunk). Returns (status, headers, leftover_body_or_None) on match, - None otherwise.""" - if not isinstance(item, dict): - return None - if "statusCode" not in item: - return None - status = item.get("statusCode", 200) - headers = item.get("headers", {"Content-Type": "text/plain"}) - body = item.get("body", None) - return (status, headers, body) - - -def _stream_iterable(iterable, streaming_enabled, keepalive_s): - """Drive a sync iterable / generator through the streaming protocol. - - If streaming_enabled is False we buffer everything into a single - response frame for back-compat — operators flipping the system_config - flag get the pre-C1 single-shot behaviour without redeploying. - - A separate thread fires an empty chunk every keepalive_s seconds if - no real chunk has flown in that window, so intermediate proxies / LBs - don't kill the connection during slow phases (LLM token generation, - DB cursor walks). The thread reads last_emit under no lock — the - timestamp is a single 64-bit float so torn reads aren't a concern. - """ - if not streaming_enabled: - # Fallback: buffer the entire generator output into a single - # response. Tries to honor an Orva-shaped first item as the head; - # otherwise wraps everything into text/plain. - head = None - body_parts = [] - for item in iterable: - if head is None: - detected = _looks_like_head(item) - if detected is not None: - status, headers, body = detected - head = (status, headers) - if body is not None: - body_parts.append(body if isinstance(body, str) else str(body)) - continue - head = (200, {"Content-Type": "text/plain"}) - if isinstance(item, (bytes, bytearray)): - body_parts.append(item.decode("utf-8", errors="replace")) - else: - body_parts.append(item if isinstance(item, str) else str(item)) - if head is None: - head = (200, {"Content-Type": "text/plain"}) - status, headers = head - _write_frame({ - "type": "response", "statusCode": status, - "headers": headers, "body": "".join(body_parts), - }) - return - - # Streaming path. - head_sent = False - last_emit = [time.monotonic()] - stop_evt = threading.Event() - - def _heartbeat(): - while not stop_evt.wait(keepalive_s): - if time.monotonic() - last_emit[0] >= keepalive_s: - try: - _write_frame({"type": "chunk", "data": ""}) - last_emit[0] = time.monotonic() - except Exception: - return - - hb = None - - def _send_head(status, headers): - nonlocal head_sent - if head_sent: - return - _write_frame({ - "type": "response_start", - "statusCode": status, - "headers": headers, - }) - head_sent = True - - try: - for item in iterable: - if not head_sent: - detected = _looks_like_head(item) - if detected is not None: - status, headers, body = detected - _send_head(status, headers) - # Start the heartbeat AFTER the head so the empty - # chunk frames never precede the response_start. - hb = threading.Thread(target=_heartbeat, daemon=True) - hb.start() - if body is not None and body != "": - _stream_chunk(body) - last_emit[0] = time.monotonic() - continue - _send_head(200, {"Content-Type": "text/plain; charset=utf-8"}) - hb = threading.Thread(target=_heartbeat, daemon=True) - hb.start() - _stream_chunk(item) - last_emit[0] = time.monotonic() - if not head_sent: - _send_head(200, {"Content-Type": "text/plain; charset=utf-8"}) - _write_frame({"type": "response_end"}) - except (BrokenPipeError, OSError): - # Client disconnected. Stop iterating; the worker continues - # serving subsequent requests if its stdin is still open. - pass - finally: - stop_evt.set() - - -async def _stream_async_iterable(aiterable, streaming_enabled, keepalive_s): - """Async-gen variant. Uses an asyncio Task as the heartbeat instead - of a thread so it cooperates with the same event loop as the user - code (no GIL contention, no thread-safe-stdout double-locking). - """ - if not streaming_enabled: - head = None - body_parts = [] - async for item in aiterable: - if head is None: - detected = _looks_like_head(item) - if detected is not None: - status, headers, body = detected - head = (status, headers) - if body is not None: - body_parts.append(body if isinstance(body, str) else str(body)) - continue - head = (200, {"Content-Type": "text/plain"}) - if isinstance(item, (bytes, bytearray)): - body_parts.append(item.decode("utf-8", errors="replace")) - else: - body_parts.append(item if isinstance(item, str) else str(item)) - if head is None: - head = (200, {"Content-Type": "text/plain"}) - status, headers = head - _write_frame({ - "type": "response", "statusCode": status, - "headers": headers, "body": "".join(body_parts), - }) - return - - head_sent = [False] - last_emit = [time.monotonic()] - - def _send_head(status, headers): - if head_sent[0]: - return - _write_frame({ - "type": "response_start", - "statusCode": status, - "headers": headers, - }) - head_sent[0] = True - - async def _heartbeat(): - try: - while True: - await asyncio.sleep(keepalive_s) - if time.monotonic() - last_emit[0] >= keepalive_s: - _write_frame({"type": "chunk", "data": ""}) - last_emit[0] = time.monotonic() - except asyncio.CancelledError: - return - - hb_task = None - try: - async for item in aiterable: - if not head_sent[0]: - detected = _looks_like_head(item) - if detected is not None: - status, headers, body = detected - _send_head(status, headers) - hb_task = asyncio.create_task(_heartbeat()) - if body is not None and body != "": - _stream_chunk(body) - last_emit[0] = time.monotonic() - continue - _send_head(200, {"Content-Type": "text/plain; charset=utf-8"}) - hb_task = asyncio.create_task(_heartbeat()) - _stream_chunk(item) - last_emit[0] = time.monotonic() - if not head_sent[0]: - _send_head(200, {"Content-Type": "text/plain; charset=utf-8"}) - _write_frame({"type": "response_end"}) - except (BrokenPipeError, OSError): - pass - finally: - if hb_task is not None: - hb_task.cancel() - try: - await hb_task - except (asyncio.CancelledError, Exception): - pass - - -def _call_handler(event): - """Invoke the user handler and return the raw return value (NOT - normalised). Splits dispatch from normalisation so the caller can - detect generators / async iterables before we collapse them into a - single response.""" - if style == "asgi": - return ("normal", asyncio.run(_call_asgi(handler, event))) - if style == "wsgi": - return ("wsgi-tuple", _call_wsgi(handler, event)) - - # Lambda / plain style. - try: - result = handler(event, _Context(event)) - except TypeError as te: - msg = str(te) - if "positional argument" in msg or "takes" in msg or "missing" in msg: - try: - result = handler(event) - except TypeError: - req = _build_flask_request(event) - if req is not None: - result = handler(req) - else: - raise - else: - raise - - if inspect.iscoroutine(result): - result = asyncio.run(result) - - return ("raw", result) - - -def _dispatch_and_emit(event, streaming_enabled, keepalive_s): - """End-to-end dispatch path: invoke the handler and either emit a - streaming protocol exchange (response_start + chunks + response_end) - or a single response frame, depending on the handler's return value. - - Returns nothing — frames go straight out via _write_frame. - """ - kind, result = _call_handler(event) - if kind == "normal": - # ASGI tuple (status, headers, body) - status, headers, body = result - _write_frame({"type": "response", "statusCode": status, "headers": headers, "body": body}) - return - if kind == "wsgi-tuple": - status, headers, body = result - _write_frame({"type": "response", "statusCode": status, "headers": headers, "body": body}) - return - - # Streaming detection. Async generators take precedence because - # inspect.isgenerator is False for them. - if inspect.isasyncgen(result): - asyncio.run(_stream_async_iterable(result, streaming_enabled, keepalive_s)) - return - if inspect.isgenerator(result): - _stream_iterable(result, streaming_enabled, keepalive_s) - return - - status, headers, body = _normalise_response(result) - _write_frame({"type": "response", "statusCode": status, "headers": headers, "body": body}) - - -# ── Main loop ────────────────────────────────────────────────────────── - -max_reqs = int(os.environ.get("ORVA_MAX_REQUESTS", "0") or 0) -served = 0 - -try: - while True: - frame = _read_frame() - if frame is None: - sys.exit(0) # stdin EOF - ftype = frame.get("type") - if ftype == "quit": - _write_frame({"type": "bye"}) - sys.exit(0) - if ftype != "request": - continue - - event = frame.get("event") or {"method": "POST", "path": "/", "headers": {}, "body": ""} - # Propagate call depth into the env so orva.invoke()'s SDK can - # forward it on outbound nested calls. Without this each recursion - # level would see depth="" and the host's depth guard never trips. - _hdrs = (event.get("headers") or {}) if isinstance(event, dict) else {} - _depth = _hdrs.get("x-orva-call-depth") or _hdrs.get("X-Orva-Call-Depth") or "" - if _depth: - os.environ["ORVA_CALL_DEPTH"] = _depth - else: - os.environ.pop("ORVA_CALL_DEPTH", None) - - # v0.5 trace context. Each event carries the trace_id + span_id of - # this invocation; the SDK reads them from env when issuing nested - # F2F calls or job enqueues so causal chains stay linked. - _tid = _hdrs.get("x-orva-trace-id") or _hdrs.get("X-Orva-Trace-Id") or "" - _sid = _hdrs.get("x-orva-span-id") or _hdrs.get("X-Orva-Span-Id") or "" - if _tid: os.environ["ORVA_TRACE_ID"] = _tid - else: os.environ.pop("ORVA_TRACE_ID", None) - if _sid: os.environ["ORVA_SPAN_ID"] = _sid - else: os.environ.pop("ORVA_SPAN_ID", None) - - # v0.6 SDK: trace.span() / log.* need the execution id. - _eid = _hdrs.get("x-orva-execution-id") or _hdrs.get("X-Orva-Execution-Id") or "" - if _eid: os.environ["ORVA_EXECUTION_ID"] = _eid - else: os.environ.pop("ORVA_EXECUTION_ID", None) - - # v0.4 C1: streaming flag + heartbeat interval ride on per-request - # headers so the proxy can flip them at runtime without redeploying - # the worker. Defaults match the system_config seed values. - _streaming_on = (_hdrs.get("x-orva-streaming-enabled") or "1") != "0" - try: - _keepalive = max(1, int(_hdrs.get("x-orva-stream-keepalive-seconds") or "15")) - except (TypeError, ValueError): - _keepalive = 15 - - try: - _dispatch_and_emit(event, _streaming_on, _keepalive) - except Exception: - traceback.print_exc() - # If we already started streaming we can't undo the head; emit - # response_end and let the proxy close out. Otherwise emit a - # plain 500 response. - try: - _write_frame({ - "type": "response", "statusCode": 500, - "headers": {"Content-Type": "application/json"}, - "body": json.dumps({"error": "Internal function error"}), - }) - except Exception: - # Best-effort terminator — the foreground proxy frame loop - # will see EOF on the next read if even this fails. - try: - _write_frame({"type": "response_end"}) - except Exception: - pass - - served += 1 - if max_reqs > 0 and served >= max_reqs: - _write_frame({"type": "bye"}) - sys.exit(0) -except SystemExit: - raise -except Exception as exc: - try: - _write_frame({"type": "error", "fatal": True, "message": f"{type(exc).__name__}: {exc}"}) - except Exception: - pass - sys.exit(1) diff --git a/backend/cmd/orva/init_cmd.go b/backend/cmd/orva/init_cmd.go index 3ada224..ee9acfc 100644 --- a/backend/cmd/orva/init_cmd.go +++ b/backend/cmd/orva/init_cmd.go @@ -19,7 +19,7 @@ func newInitCmd() *cobra.Command { } const orvaYAMLTemplate = `# Orva configuration -# Supported runtimes: node22 (Node.js 20), python313 (Python 3.12) +# Supported runtimes: node (Node.js 24), python (Python 3.14) server: host: "0.0.0.0" diff --git a/backend/cmd/orva/setup.go b/backend/cmd/orva/setup.go index e831c32..b524ce3 100644 --- a/backend/cmd/orva/setup.go +++ b/backend/cmd/orva/setup.go @@ -80,7 +80,7 @@ func runSetup(cmd *cobra.Command, args []string) error { // Rootfs. if !skipRootfs { - for _, rt := range []string{"node22", "node24", "python313", "python314"} { + for _, rt := range []string{"node", "python"} { target := filepath.Join(cfg.Sandbox.RootfsDir, rt) if err := ensureRootfs(target, rt, rootfsURL); err != nil { return err @@ -217,9 +217,9 @@ func goArchToRelease() string { func installAdapter(rootfs, runtime string) error { name := "" switch runtime { - case "node22", "node24": + case "node": name = "adapter.js" - case "python313", "python314": + case "python": name = "adapter.py" default: return fmt.Errorf("unknown runtime: %s", runtime) @@ -272,10 +272,10 @@ func installSDK(rootfs, runtime string) error { var files []string var destSub string switch runtime { - case "node22", "node24": + case "node": files = []string{"orva.js", "orva.d.ts", "package.json"} destSub = "opt/orva/node_modules/orva" - case "python313", "python314": + case "python": files = []string{"orva.py", "py.typed"} destSub = "opt/orva" default: diff --git a/backend/internal/ai/manager.go b/backend/internal/ai/manager.go index 4f291f8..fcf9e9b 100644 --- a/backend/internal/ai/manager.go +++ b/backend/internal/ai/manager.go @@ -523,8 +523,9 @@ the symptom: - Only ask the user a question when a choice genuinely can't be inferred. # Orva essentials (quick reference; use get_orva_docs for the full spec) -- Runtimes: node22, node24, python313, python314 (TypeScript runs on the node - runtime). Entrypoint defaults: handler.js (JS/TS) or handler.py (Python). +- Runtimes: node (Node.js 24) and python (Python 3.14) — those exact ids, + nothing versioned. TypeScript runs on the node runtime. Entrypoint defaults: + handler.js (JS/TS) or handler.py (Python). - Handler contract: the handler receives an event (method, path, headers, body, query) and returns a response (statusCode, headers, body). Exact shape per runtime is in get_orva_docs. @@ -554,7 +555,7 @@ the symptom: cards and large code auto-collapses, so don't re-print raw tool output or apologize for length. Default to prose and bullet lists — including for multi-attribute entity listings (functions, jobs, executions, secrets, cron): - give each item ONE bullet with its details inline (e.g. "hello: node24, active, + give each item ONE bullet with its details inline (e.g. "hello: node, active, egress"). Do NOT render a routine listing as a Markdown table. Use a table ONLY when the user explicitly asks to compare items side by side. - Surface execution/trace/job ids in prose only when the user is debugging a diff --git a/backend/internal/backup/backup_test.go b/backend/internal/backup/backup_test.go index 37fa1f0..e00cff4 100644 --- a/backend/internal/backup/backup_test.go +++ b/backend/internal/backup/backup_test.go @@ -37,7 +37,7 @@ func TestSnapshotArchiveRestoreRoundtrip(t *testing.T) { fn := &database.Function{ ID: "fn_backup_test", Name: "backup-test", - Runtime: "node22", + Runtime: "node", Entrypoint: "handler.js", TimeoutMS: 30000, MemoryMB: 64, diff --git a/backend/internal/builder/builder.go b/backend/internal/builder/builder.go index 99f1b04..90476dd 100644 --- a/backend/internal/builder/builder.go +++ b/backend/internal/builder/builder.go @@ -282,23 +282,17 @@ exec(open("/opt/orva/adapter.py").read()) } } -// isNodeRuntime / isPythonRuntime / pythonVersion are thin helpers so -// version bumps only need to add new strings in one place. Latest two -// stable LTS / stable only. -func isNodeRuntime(r string) bool { return r == "node22" || r == "node24" } -func isPythonRuntime(r string) bool { return r == "python313" || r == "python314" } - -// pythonVersionFor returns the pip --python-version flag value for the -// runtime. Used so wheels resolve for the right interpreter. +// isNodeRuntime / isPythonRuntime / pythonVersionFor are thin helpers so a +// version bump only changes one place. Orva offers two generic runtimes +// (node = Node.js 24, python = Python 3.14). +func isNodeRuntime(r string) bool { return r == "node" } +func isPythonRuntime(r string) bool { return r == "python" } + +// pythonVersionFor returns the pip --python-version flag value for the python +// runtime. Used so wheels resolve for the right interpreter; bump alongside the +// python rootfs base image. func pythonVersionFor(r string) string { - switch r { - case "python313": - return "3.13" - case "python314": - return "3.14" - default: - return "3.13" // fallback — should never hit because validation rejects it - } + return "3.14" } // hashFile computes the SHA256 hash of a file. @@ -381,7 +375,7 @@ func extractTarGz(archivePath, destDir string) error { // the resolved `outDir`; for everything else it's the unchanged // `entrypoint` argument. // -// - node22 / node24: if package.json is present, runs `npm install --prefix +// - node: if package.json is present, runs `npm install --prefix // `. node_modules/ lands at /code/node_modules and require() // finds it automatically. If a tsconfig.json is *also* present, runs // `npx --no-install tsc --project tsconfig.json` after the install @@ -389,7 +383,7 @@ func extractTarGz(archivePath, destDir string) error { // dependencies / devDependencies. The resolved entrypoint becomes // `/.js`. // -// - python313: if requirements.txt is present, runs `pip install -t `. +// - python: if requirements.txt is present, runs `pip install -t `. // Packages land at /code/ and the Python adapter adds /code to sys.path. // // Both commands run on the host (not inside nsjail) during the build phase. diff --git a/backend/internal/builder/builder_test.go b/backend/internal/builder/builder_test.go index d1d0be0..53a6160 100644 --- a/backend/internal/builder/builder_test.go +++ b/backend/internal/builder/builder_test.go @@ -14,7 +14,7 @@ func TestValidateArchive_Valid(t *testing.T) { dir := t.TempDir() os.WriteFile(filepath.Join(dir, "handler.js"), []byte("module.exports = {}"), 0644) - if err := ValidateArchive(dir, "node22", "handler.js"); err != nil { + if err := ValidateArchive(dir, "node", "handler.js"); err != nil { t.Errorf("expected valid archive, got: %v", err) } } @@ -22,7 +22,7 @@ func TestValidateArchive_Valid(t *testing.T) { func TestValidateArchive_MissingEntrypoint(t *testing.T) { dir := t.TempDir() - err := ValidateArchive(dir, "node22", "handler.js") + err := ValidateArchive(dir, "node", "handler.js") if err == nil { t.Error("expected error for missing entrypoint") } @@ -32,7 +32,7 @@ func TestValidateArchive_ELFBinary(t *testing.T) { dir := t.TempDir() os.WriteFile(filepath.Join(dir, "handler.js"), []byte{0x7f, 'E', 'L', 'F', 0, 0, 0, 0}, 0644) - err := ValidateArchive(dir, "node22", "handler.js") + err := ValidateArchive(dir, "node", "handler.js") if err == nil { t.Error("expected ELF binary to be rejected") } @@ -43,7 +43,7 @@ func TestValidateArchive_SymlinkEscape(t *testing.T) { os.WriteFile(filepath.Join(dir, "handler.js"), []byte("ok"), 0644) os.Symlink("/etc/passwd", filepath.Join(dir, "escape")) - err := ValidateArchive(dir, "node22", "handler.js") + err := ValidateArchive(dir, "node", "handler.js") if err == nil { t.Error("expected symlink escape to be rejected") } @@ -58,7 +58,7 @@ func TestBuild_ExtractAndValidate(t *testing.T) { fn := &database.Function{ ID: "fn_test123", Name: "test-fn", - Runtime: "python313", + Runtime: "python", Entrypoint: "handler.py", } diff --git a/backend/internal/database/database_test.go b/backend/internal/database/database_test.go index 8ac6e9c..832d06d 100644 --- a/backend/internal/database/database_test.go +++ b/backend/internal/database/database_test.go @@ -48,7 +48,7 @@ func TestFunctionCRUD(t *testing.T) { fn := &Function{ ID: "fn_test123456", Name: "hello-world", - Runtime: "node22", + Runtime: "node", Entrypoint: "handler.js", TimeoutMS: 30000, MemoryMB: 128, @@ -119,7 +119,7 @@ func TestExecutionCRUD(t *testing.T) { db := newTestDB(t) fn := &Function{ - ID: "fn_test123456", Name: "test-fn", Runtime: "node22", + ID: "fn_test123456", Name: "test-fn", Runtime: "node", Entrypoint: "handler.js", TimeoutMS: 30000, MemoryMB: 128, CPUs: 0.5, EnvVars: map[string]string{}, NetworkMode: "none", Status: "active", } @@ -207,7 +207,7 @@ func TestFixtureCRUD(t *testing.T) { db := newTestDB(t) fn := &Function{ - ID: "fn_fixt12345678", Name: "fixture-test", Runtime: "node22", + ID: "fn_fixt12345678", Name: "fixture-test", Runtime: "node", Entrypoint: "handler.js", TimeoutMS: 30000, MemoryMB: 128, CPUs: 0.5, EnvVars: map[string]string{}, NetworkMode: "none", Status: "active", } @@ -305,7 +305,7 @@ func TestInboundWebhookCRUD(t *testing.T) { db := newTestDB(t) fn := &Function{ - ID: "fn_inb12345678", Name: "inbound-test", Runtime: "node22", + ID: "fn_inb12345678", Name: "inbound-test", Runtime: "node", Entrypoint: "handler.js", TimeoutMS: 30000, MemoryMB: 128, CPUs: 0.5, EnvVars: map[string]string{}, NetworkMode: "none", Status: "active", } @@ -386,7 +386,7 @@ func TestJobScheduledAtFiltering(t *testing.T) { db := newTestDB(t) fn := &Function{ - ID: "fn_jobsched12345", Name: "job-sched-test", Runtime: "node22", + ID: "fn_jobsched12345", Name: "job-sched-test", Runtime: "node", Entrypoint: "handler.js", TimeoutMS: 30000, MemoryMB: 64, CPUs: 0.5, EnvVars: map[string]string{}, NetworkMode: "none", Status: "active", } @@ -437,7 +437,7 @@ func TestCascadeDelete(t *testing.T) { db := newTestDB(t) fn := &Function{ - ID: "fn_cascade1234", Name: "cascade-test", Runtime: "node22", + ID: "fn_cascade1234", Name: "cascade-test", Runtime: "node", Entrypoint: "handler.js", TimeoutMS: 30000, MemoryMB: 128, CPUs: 0.5, EnvVars: map[string]string{}, NetworkMode: "none", Status: "active", } diff --git a/backend/internal/database/migrate_to_uuidv7_test.go b/backend/internal/database/migrate_to_uuidv7_test.go index 10e8606..01f1509 100644 --- a/backend/internal/database/migrate_to_uuidv7_test.go +++ b/backend/internal/database/migrate_to_uuidv7_test.go @@ -75,7 +75,7 @@ func TestMigrationPopulatedDB(t *testing.T) { } mustExec(`INSERT INTO functions (id, name, runtime, entrypoint) VALUES (?, ?, ?, ?)`, - legacyFnID, "test-func", "python314", "handler.py") + legacyFnID, "test-func", "python", "handler.py") mustExec(`INSERT INTO deployments (id, function_id, version, status, phase) VALUES (?, ?, ?, ?, ?)`, legacyDepID, legacyFnID, 1, "succeeded", "complete") mustExec(`INSERT INTO build_logs (deployment_id, seq, line, stream) VALUES (?, ?, ?, ?)`, diff --git a/backend/internal/database/migrations.go b/backend/internal/database/migrations.go index 10be9cc..78351ae 100644 --- a/backend/internal/database/migrations.go +++ b/backend/internal/database/migrations.go @@ -826,28 +826,7 @@ PRAGMA foreign_keys = ON; } } - // Runtime refresh: bump EOL runtimes to the nearest supported version. - // This is a one-shot, idempotent update — on subsequent boots there - // are no rows to migrate. - runtimeMigrations := []struct { - from, to string - }{ - {"node20", "node22"}, - {"python312", "python313"}, - } - for _, m := range runtimeMigrations { - res, err := db.write.Exec( - "UPDATE functions SET runtime = ? WHERE runtime = ?", - m.to, m.from, - ) - if err != nil { - slog.Warn("runtime migration failed", "from", m.from, "to", m.to, "err", err) - continue - } - if n, _ := res.RowsAffected(); n > 0 { - slog.Info("runtime migrated", "from", m.from, "to", m.to, "functions", n) - } - } + db.collapseRuntimes() // One-shot rewrite of every prefix-typed storage ID (fn_, key_, // oat_, etc.) to UUIDv7. Idempotent — guarded by a marker row in @@ -923,3 +902,34 @@ func dropExecutionRequestsFK(db *Database) error { slog.Info("execution_requests FK dropped") return nil } + +// collapseRuntimes rewrites every legacy versioned runtime id to one of Orva's +// two generic runtimes — `node` (latest Node.js) and `python` (latest Python). +// Functions deployed before the runtime collapse keep loading and invoking; +// node22/python313 get bumped to the latest major (native/ABI deps may need a +// redeploy). One-shot and idempotent: on later boots there are no rows to +// migrate. Best-effort — a failure here is logged, never fatal. +func (db *Database) collapseRuntimes() { + collapse := []struct { + to string + from []string + }{ + {"node", []string{"node20", "node22", "node24"}}, + {"python", []string{"python312", "python313", "python314"}}, + } + for _, m := range collapse { + for _, from := range m.from { + res, err := db.write.Exec( + "UPDATE functions SET runtime = ? WHERE runtime = ?", + m.to, from, + ) + if err != nil { + slog.Warn("runtime collapse failed", "from", from, "to", m.to, "err", err) + continue + } + if n, _ := res.RowsAffected(); n > 0 { + slog.Info("runtime collapsed", "from", from, "to", m.to, "functions", n) + } + } + } +} diff --git a/backend/internal/database/runtime_collapse_test.go b/backend/internal/database/runtime_collapse_test.go new file mode 100644 index 0000000..b8e0ea9 --- /dev/null +++ b/backend/internal/database/runtime_collapse_test.go @@ -0,0 +1,59 @@ +package database + +import ( + "path/filepath" + "testing" +) + +// TestCollapseRuntimes verifies the legacy-runtime → generic-runtime migration: +// functions stored under versioned ids (node20/node22/node24, python312/313/314) +// are rewritten to `node` / `python`, and rows already on the generic ids are +// left untouched. Idempotent on a second pass. +func TestCollapseRuntimes(t *testing.T) { + dir := t.TempDir() + db, err := New(filepath.Join(dir, "test.db")) + if err != nil { + t.Fatalf("open db: %v", err) + } + defer db.Close() + if err := db.Migrate(); err != nil { + t.Fatalf("migrate: %v", err) + } + + cases := []struct { + name, stored, want string + }{ + {"legacy-node22", "node22", "node"}, + {"legacy-node24", "node24", "node"}, + {"legacy-node20", "node20", "node"}, + {"legacy-py313", "python313", "python"}, + {"legacy-py314", "python314", "python"}, + {"already-node", "node", "node"}, + {"already-python", "python", "python"}, + } + + // Seed rows directly with the (possibly legacy) runtime value — the DB layer + // stores the string verbatim; validation lives at the handler/MCP layer. + for _, c := range cases { + if _, err := db.write.Exec( + "INSERT INTO functions (id, name, runtime, entrypoint, status) VALUES (?, ?, ?, ?, ?)", + c.name, c.name, c.stored, "handler.js", "created", + ); err != nil { + t.Fatalf("seed %s: %v", c.name, err) + } + } + + // Run twice to prove idempotency. + db.collapseRuntimes() + db.collapseRuntimes() + + for _, c := range cases { + var got string + if err := db.read.QueryRow("SELECT runtime FROM functions WHERE id = ?", c.name).Scan(&got); err != nil { + t.Fatalf("read %s: %v", c.name, err) + } + if got != c.want { + t.Errorf("%s: runtime = %q, want %q", c.name, got, c.want) + } + } +} diff --git a/backend/internal/mcp/reference.md b/backend/internal/mcp/reference.md index 9d1227d..3cd7889 100644 --- a/backend/internal/mcp/reference.md +++ b/backend/internal/mcp/reference.md @@ -57,12 +57,13 @@ JSON-encoded by the adapter. **Runtime env:** env vars and secrets land in `process.env` (Node) / `os.environ` (Python). -| Runtime | ID | Entrypoint | Dependencies | -|---|---|---|---| -| Python 3.14 | `python314` | `handler.py` | `requirements.txt` | -| Python 3.13 | `python313` | `handler.py` | `requirements.txt` | -| Node.js 24 | `node24` | `handler.js` | `package.json` | -| Node.js 22 | `node22` | `handler.js` | `package.json` | +Orva offers two runtimes, latest-stable only. The ID is generic (`node` / +`python`); the version column shows what they currently track. + +| Runtime | ID | Version | Entrypoint | Dependencies | +|---|---|---|---|---| +| Python | `python` | 3.14 | `handler.py` | `requirements.txt` | +| Node.js | `node` | 24 | `handler.js` | `package.json` | --- @@ -78,7 +79,7 @@ stream `/api/v1/deployments//stream` until `phase: done`. curl -X POST {{ORIGIN}}/api/v1/functions \ -H 'X-Orva-API-Key: ' \ -H 'Content-Type: application/json' \ - -d '{"name":"hello","runtime":"python314","memory_mb":128,"cpus":0.5}' + -d '{"name":"hello","runtime":"python","memory_mb":128,"cpus":0.5}' ``` ### 2. Upload code @@ -966,10 +967,10 @@ Orva is a self-hosted serverless platform — think Cloudflare Workers / Vercel -Pick exactly one — Orva has no Docker, no buildpacks, no per-function Python/Node version pinning beyond this: -- python314 (default) or python313 — entry: handler.py — deps: requirements.txt -- node24 (default) or node22 — entry: handler.js — deps: package.json -Older minor versions auto-migrate to the latest patch on next deploy. Native modules (psycopg2-binary, sharp, bcrypt, etc.) are supported via prebuilt wheels / npm prebuilts; if a dep needs a system library not present in the runtime image, the build will fail with a clear error. +Pick exactly one — Orva has no Docker, no buildpacks, no per-function version pinning. Two runtimes, generic ids, latest-stable only: +- python (Python 3.14) — entry: handler.py — deps: requirements.txt +- node (Node.js 24, also runs TypeScript) — entry: handler.js — deps: package.json +Native modules (psycopg2-binary, sharp, bcrypt, etc.) are supported via prebuilt wheels / npm prebuilts; if a dep needs a system library not present in the runtime image, the build will fail with a clear error. @@ -1596,10 +1597,10 @@ orva init # is present; else uses the runtime default (handler.js / handler.py). orva deploy ./my-fn \ --name resize-image \ - --runtime node24 + --runtime node # Override the entrypoint explicitly: -orva deploy ./my-fn --name api --runtime python314 --entrypoint app.py +orva deploy ./my-fn --name api --runtime python --entrypoint app.py ``` #### Invoke + tail logs diff --git a/backend/internal/mcp/tools_functions.go b/backend/internal/mcp/tools_functions.go index 9f2c029..356da63 100644 --- a/backend/internal/mcp/tools_functions.go +++ b/backend/internal/mcp/tools_functions.go @@ -124,7 +124,7 @@ func resolveFunction(deps Deps, idOrName string) (*database.Function, error) { // ─── list_functions ──────────────────────────────────────────────── type ListFunctionsInput struct { - Runtime string `json:"runtime,omitempty" jsonschema:"filter to one runtime (node22|node24|python313|python314)"` + Runtime string `json:"runtime,omitempty" jsonschema:"filter to one runtime (node|python)"` Status string `json:"status,omitempty" jsonschema:"filter by status (active|inactive|created|building|error)"` Limit int `json:"limit,omitempty" jsonschema:"page size, default 50, max 200"` Offset int `json:"offset,omitempty" jsonschema:"skip this many items, default 0"` @@ -149,7 +149,7 @@ type GetFunctionInput struct { type CreateFunctionInput struct { Name string `json:"name" jsonschema:"unique function name (lowercase, dash-separated, URL-safe — appears in invoke_url and logs)"` Description string `json:"description" jsonschema:"REQUIRED — one-sentence summary of what the function does (e.g. 'resize uploaded images to webp thumbnails'). Surfaces in list_functions, the dashboard's function card, and channel-mode tool descriptions exposed to other agents — so this is how a future operator or LLM identifies what this function is for. Empty / placeholder values rejected."` - Runtime string `json:"runtime" jsonschema:"one of node22 node24 python313 python314"` + Runtime string `json:"runtime" jsonschema:"one of node (Node.js 24) or python (Python 3.14)"` Entrypoint string `json:"entrypoint" jsonschema:"REQUIRED — handler file path relative to deploy dir (e.g. 'handler.js' for Node, 'handler.py' for Python, 'src/index.ts' for TypeScript). Set explicitly so the runtime+entrypoint pairing is intentional; mismatched values silently fail to spawn."` TimeoutMS int64 `json:"timeout_ms" jsonschema:"REQUIRED — per-invocation timeout in ms. Cap on how long any single request can run before the sandbox is killed. Pick from the handler's expected work: a quick CRUD endpoint can use 5000-10000; an LLM/AI call usually 30000-60000; a heavy report 120000+. Must be > 0."` MemoryMB int64 `json:"memory_mb" jsonschema:"REQUIRED — sandbox RAM in MB. Hard cap; a handler that exceeds it gets OOM-killed. Pick from runtime baseline + working set: tiny Node/Python with no deps ~64; with frameworks ~128-256; image/PDF/ML work 512+. Must be > 0."` @@ -208,11 +208,11 @@ type GetFunctionSourceOutput struct { // ─── helpers ─────────────────────────────────────────────────────── var validRuntimesSet = map[string]bool{ - "node22": true, "node24": true, "python313": true, "python314": true, + "node": true, "python": true, } -func runtimeIsNode(r string) bool { return r == "node22" || r == "node24" } -func runtimeIsPython(r string) bool { return r == "python313" || r == "python314" } +func runtimeIsNode(r string) bool { return r == "node" } +func runtimeIsPython(r string) bool { return r == "python" } var userSettableStatuses = map[string]bool{"active": true, "inactive": true} @@ -299,7 +299,7 @@ func registerFunctionTools(rc *regCtx) { "Most fields are REQUIRED so the function record carries explicit intent rather than silent defaults. Specifically you MUST provide: " + "`name` (URL-safe identifier), " + "`description` (one-sentence summary of what the function does — visible in list_functions and the dashboard), " + - "`runtime` (node22 / node24 / python313 / python314), " + + "`runtime` (node or python), " + "`entrypoint` (handler file path; e.g. handler.js / handler.py / src/index.ts), " + "`timeout_ms` (per-invocation cap; pick from your handler's expected work — fast CRUD ~5000-10000, AI/LLM ~30000-60000, heavy reports 120000+), " + "`memory_mb` (RAM cap; tiny handlers 64, with frameworks 128-256, image/PDF/ML 512+), " + @@ -395,7 +395,7 @@ func createFunction(deps Deps, in CreateFunctionInput) (*database.Function, erro ) } if !validRuntimesSet[in.Runtime] { - return nil, fmt.Errorf("unsupported runtime: %s (one of node22, node24, python313, python314)", in.Runtime) + return nil, fmt.Errorf("unsupported runtime: %s (one of node, python)", in.Runtime) } if strings.TrimSpace(in.Entrypoint) == "" { return nil, errors.New( diff --git a/backend/internal/mcp/tools_system.go b/backend/internal/mcp/tools_system.go index 5aad14d..edb2d7e 100644 --- a/backend/internal/mcp/tools_system.go +++ b/backend/internal/mcp/tools_system.go @@ -207,6 +207,7 @@ func buildSystemMetrics(deps Deps) SystemMetricsOutput { type RuntimeInfo struct { ID string `json:"id"` Name string `json:"name"` + Version string `json:"version"` Language string `json:"language"` DefaultHandler string `json:"default_handler"` Extensions []string `json:"extensions"` @@ -218,10 +219,8 @@ type ListRuntimesOutput struct { func supportedRuntimes() []RuntimeInfo { return []RuntimeInfo{ - {ID: "node22", Name: "Node.js 22 (Active LTS)", Language: "javascript", DefaultHandler: "handler.js", Extensions: []string{".js", ".mjs", ".cjs"}}, - {ID: "node24", Name: "Node.js 24 (Current LTS)", Language: "javascript", DefaultHandler: "handler.js", Extensions: []string{".js", ".mjs", ".cjs"}}, - {ID: "python313", Name: "Python 3.13", Language: "python", DefaultHandler: "handler.py", Extensions: []string{".py"}}, - {ID: "python314", Name: "Python 3.14", Language: "python", DefaultHandler: "handler.py", Extensions: []string{".py"}}, + {ID: "node", Name: "Node.js 24 (current)", Version: "24", Language: "javascript", DefaultHandler: "handler.js", Extensions: []string{".js", ".mjs", ".cjs", ".ts"}}, + {ID: "python", Name: "Python 3.14 (current)", Version: "3.14", Language: "python", DefaultHandler: "handler.py", Extensions: []string{".py"}}, } } diff --git a/backend/internal/registry/registry_test.go b/backend/internal/registry/registry_test.go index 2c54035..72d4122 100644 --- a/backend/internal/registry/registry_test.go +++ b/backend/internal/registry/registry_test.go @@ -44,7 +44,7 @@ func TestSetAndGetWarmCache(t *testing.T) { fn := &database.Function{ Name: "hello", - Runtime: "node22", + Runtime: "node", Entrypoint: "handler.js", Status: "active", } @@ -107,7 +107,7 @@ func TestCacheInvalidationOnSet(t *testing.T) { fn := &database.Function{ Name: "invalidate-test", - Runtime: "node22", + Runtime: "node", Entrypoint: "handler.js", Status: "active", } @@ -115,8 +115,8 @@ func TestCacheInvalidationOnSet(t *testing.T) { t.Fatal(err) } - // Update the function - fn.Runtime = "node22" + // Update the function (switch runtime to verify the cache invalidates) + fn.Runtime = "python" if err := reg.Set(fn); err != nil { t.Fatal(err) } @@ -125,8 +125,8 @@ func TestCacheInvalidationOnSet(t *testing.T) { if err != nil { t.Fatal(err) } - if got.Runtime != "node22" { - t.Fatalf("expected node22 after update, got %s", got.Runtime) + if got.Runtime != "python" { + t.Fatalf("expected python after update, got %s", got.Runtime) } } @@ -162,7 +162,7 @@ func TestList(t *testing.T) { for i := 0; i < 3; i++ { fn := &database.Function{ Name: "list-fn-" + string(rune('A'+i)), - Runtime: "node22", + Runtime: "node", Entrypoint: "handler.js", Status: "active", } @@ -186,7 +186,7 @@ func TestLoadAll(t *testing.T) { fn := &database.Function{ Name: "loadall-test", - Runtime: "node22", + Runtime: "node", Entrypoint: "handler.js", Status: "active", } @@ -219,7 +219,7 @@ func TestConcurrentAccess(t *testing.T) { for i := 0; i < 10; i++ { fn := &database.Function{ Name: "conc-" + string(rune('A'+i)), - Runtime: "node22", + Runtime: "node", Entrypoint: "handler.js", Status: "active", } diff --git a/backend/internal/sandbox/sandbox.go b/backend/internal/sandbox/sandbox.go index a24468d..a401cc1 100644 --- a/backend/internal/sandbox/sandbox.go +++ b/backend/internal/sandbox/sandbox.go @@ -17,20 +17,21 @@ import ( type Language string const ( - // Supported runtimes: latest two stable majors for each language. - // node20 / python312 are dropped (Node 20 EOL 2026-04-30). Existing - // functions on those are auto-migrated at startup — see migrations.go. - Node22 Language = "node22" - Node24 Language = "node24" - Python313 Language = "python313" - Python314 Language = "python314" + // Orva offers exactly two runtimes, latest-stable only: `node` (Node.js 24) + // and `python` (Python 3.14). The identifier is generic on purpose — the + // concrete version is an implementation detail that tracks latest-stable and + // is surfaced only as a display label (see handlers/runtimes.go). Legacy + // versioned IDs (node22/node24/python313/python314) are rewritten to these + // at startup — see migrations.go. + Node Language = "node" + Python Language = "python" ) -// IsNode reports whether a language is a Node runtime. -func (l Language) IsNode() bool { return l == Node22 || l == Node24 } +// IsNode reports whether a language is the Node runtime. +func (l Language) IsNode() bool { return l == Node } -// IsPython reports whether a language is a Python runtime. -func (l Language) IsPython() bool { return l == Python313 || l == Python314 } +// IsPython reports whether a language is the Python runtime. +func (l Language) IsPython() bool { return l == Python } // ExecConfig holds everything needed to run user code in nsjail. type ExecConfig struct { @@ -144,10 +145,10 @@ func Execute(ctx context.Context, cfg ExecConfig) *ExecResult { func resolveRuntime(cfg ExecConfig) (rootfs, entrypoint string, err error) { switch cfg.Language { - case Python313, Python314: + case Python: rootfs = filepath.Join(cfg.RootfsDir, string(cfg.Language)) entrypoint = "/usr/local/bin/python3" - case Node22, Node24: + case Node: rootfs = filepath.Join(cfg.RootfsDir, string(cfg.Language)) entrypoint = "/usr/local/bin/node" default: @@ -331,6 +332,11 @@ func getenvOr(m map[string]string, k, def string) string { return def } +// cgroupNeededControllers are the controllers nsjail writes into the sandbox's +// child cgroup (memory.max / pids.max / cpu.max). They only appear in a child +// when the parent delegates them via cgroup.subtree_control. +var cgroupNeededControllers = []string{"memory", "pids", "cpu"} + func isWritableDir(p string) bool { // cgroupfs does not allow creating regular files — probe by creating a // child cgroup directory and checking the kernel auto-creates cgroup.procs. @@ -338,9 +344,40 @@ func isWritableDir(p string) bool { if err := os.Mkdir(probe, 0o755); err != nil { return false } - _, statErr := os.Stat(filepath.Join(probe, "cgroup.procs")) - os.Remove(probe) - return statErr == nil + defer os.Remove(probe) + if _, err := os.Stat(filepath.Join(probe, "cgroup.procs")); err != nil { + return false + } + // Being able to mkdir a child is NOT enough: nsjail will write memory.max / + // pids.max / cpu.max into its own child cgroup, and those knobs exist only + // when the parent delegated the controllers (cgroup.subtree_control). On + // hosts where the controllers aren't delegated to children — e.g. the + // cgroup-v2 "no internal processes" rule blocks it because the server + // process sits directly in this cgroup — the child exposes no controllers, + // nsjail's cgroup write fails, and every worker crashes. Verify the child + // actually carries the controllers we need; if not, report no-delegate so + // the caller falls back to rlimit-only (functions run, just without the + // per-sandbox cgroup memory/pid caps) instead of crashing. + return childHasControllers(probe) +} + +// childHasControllers reports whether a freshly-created child cgroup exposes all +// the controllers Orva's sandbox needs. +func childHasControllers(child string) bool { + data, err := os.ReadFile(filepath.Join(child, "cgroup.controllers")) + if err != nil { + return false + } + have := make(map[string]bool) + for _, c := range strings.Fields(string(data)) { + have[c] = true + } + for _, need := range cgroupNeededControllers { + if !have[need] { + return false + } + } + return true } // CgroupV2Mount returns the cgroup v2 path nsjail cgroups are created under, diff --git a/backend/internal/sandbox/sandbox_test.go b/backend/internal/sandbox/sandbox_test.go index 22b22ac..e8938c0 100644 --- a/backend/internal/sandbox/sandbox_test.go +++ b/backend/internal/sandbox/sandbox_test.go @@ -30,7 +30,7 @@ func TestBuildArgs_NoEgressByDefault(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { cfg := ExecConfig{ - Language: Python314, + Language: Python, CodeDir: "/tmp/code", NetworkMode: tc.mode, } @@ -48,7 +48,7 @@ func TestBuildArgs_NoEgressByDefault(t *testing.T) { // stack and lets the sandbox reach external APIs. func TestBuildArgs_EgressAddsUsePasta(t *testing.T) { cfg := ExecConfig{ - Language: Node24, + Language: Node, CodeDir: "/tmp/code", NetworkMode: "egress", } @@ -63,7 +63,7 @@ func TestBuildArgs_EgressAddsUsePasta(t *testing.T) { // didn't accidentally drop the -Mo flag (one-shot mode is mandatory). func TestBuildArgs_AlwaysHasOnceMode(t *testing.T) { cfg := ExecConfig{ - Language: Python314, + Language: Python, CodeDir: "/tmp/code", } args := buildArgs(cfg, "/tmp/rootfs", "/tmp/code/handler.py") diff --git a/backend/internal/server/channel_e2e_test.go b/backend/internal/server/channel_e2e_test.go index 3879ed5..5a19afb 100644 --- a/backend/internal/server/channel_e2e_test.go +++ b/backend/internal/server/channel_e2e_test.go @@ -47,7 +47,7 @@ func TestChannelE2E_FullFlow(t *testing.T) { mustFn := func(name string) string { t.Helper() fn := &database.Function{ - Name: name, Runtime: "python314", Entrypoint: "handler.py", + Name: name, Runtime: "python", Entrypoint: "handler.py", MemoryMB: 64, CPUs: 0.5, Status: "active", AuthMode: "none", NetworkMode: "none", ConcurrencyPolicy: "queue", } @@ -142,12 +142,12 @@ func TestChannelE2E_ToolNameCollisionRejected(t *testing.T) { tc := newTestServer(t) a := &database.Function{ - Name: "stripe-charge", Runtime: "python314", Entrypoint: "handler.py", + Name: "stripe-charge", Runtime: "python", Entrypoint: "handler.py", MemoryMB: 64, CPUs: 0.5, Status: "active", AuthMode: "none", NetworkMode: "none", ConcurrencyPolicy: "queue", } b := &database.Function{ - Name: "stripe_charge", Runtime: "python314", Entrypoint: "handler.py", + Name: "stripe_charge", Runtime: "python", Entrypoint: "handler.py", MemoryMB: 64, CPUs: 0.5, Status: "active", AuthMode: "none", NetworkMode: "none", ConcurrencyPolicy: "queue", } @@ -183,7 +183,7 @@ func TestChannelE2E_RotateInvalidatesOldToken(t *testing.T) { tc := newTestServer(t) fn := &database.Function{ - Name: "echo", Runtime: "python314", Entrypoint: "handler.py", + Name: "echo", Runtime: "python", Entrypoint: "handler.py", MemoryMB: 64, CPUs: 0.5, Status: "active", AuthMode: "none", NetworkMode: "none", ConcurrencyPolicy: "queue", } diff --git a/backend/internal/server/diff_test.go b/backend/internal/server/diff_test.go index 848b28a..6dabb85 100644 --- a/backend/internal/server/diff_test.go +++ b/backend/internal/server/diff_test.go @@ -20,7 +20,7 @@ func seedDiffFunction(t *testing.T, tc *testContext) (fnID, fromID, toID, fromHa t.Helper() // Create the function via the API. - body := `{"name":"diff-demo","runtime":"node22","entrypoint":"handler.js"}` + body := `{"name":"diff-demo","runtime":"node","entrypoint":"handler.js"}` req := httptest.NewRequest("POST", "/api/v1/functions", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") @@ -216,7 +216,7 @@ func TestDiff_RejectsCrossFunction(t *testing.T) { fnID, fromID, _, _, _ := seedDiffFunction(t, tc) // Insert a second function + a deployment on it. - body := `{"name":"diff-other","runtime":"node22","entrypoint":"handler.js"}` + body := `{"name":"diff-other","runtime":"node","entrypoint":"handler.js"}` req := httptest.NewRequest("POST", "/api/v1/functions", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") diff --git a/backend/internal/server/handlers/functions.go b/backend/internal/server/handlers/functions.go index 23b2600..ef5a586 100644 --- a/backend/internal/server/handlers/functions.go +++ b/backend/internal/server/handlers/functions.go @@ -120,18 +120,18 @@ var userSettableStatus = map[string]bool{ "inactive": true, } +// validRuntimes is the exact set of runtime IDs Orva accepts. Two, generic, +// latest-stable only (node = Node.js 24, python = Python 3.14). Legacy versioned +// IDs are rejected on input (existing rows are migrated — see migrations.go). var validRuntimes = map[string]bool{ - "node22": true, - "node24": true, - "python313": true, - "python314": true, + "node": true, + "python": true, } // runtimeIsNode / runtimeIsPython centralise the "what kind of runtime is -// this string" decision so handler switches don't need to be updated -// every time we bump language versions. -func runtimeIsNode(r string) bool { return r == "node22" || r == "node24" } -func runtimeIsPython(r string) bool { return r == "python313" || r == "python314" } +// this string" decision so handler switches stay version-agnostic. +func runtimeIsNode(r string) bool { return r == "node" } +func runtimeIsPython(r string) bool { return r == "python" } // Create handles POST /api/v1/functions. func (h *FunctionHandler) Create(w http.ResponseWriter, r *http.Request) { diff --git a/backend/internal/server/handlers/runtimes.go b/backend/internal/server/handlers/runtimes.go index a044a17..732c31f 100644 --- a/backend/internal/server/handlers/runtimes.go +++ b/backend/internal/server/handlers/runtimes.go @@ -9,40 +9,35 @@ import ( // RuntimeHandler handles runtime listing endpoints. type RuntimeHandler struct{} -// runtimeInfo describes a supported runtime. +// runtimeInfo describes a supported runtime. The ID is the generic, stable +// identifier callers use (`node` / `python`); Name + Version are display-only +// labels that reveal the concrete latest-stable version the runtime currently +// tracks (clients should not parse them). type runtimeInfo struct { ID string `json:"id"` Name string `json:"name"` + Version string `json:"version"` Language string `json:"language"` DefaultHandler string `json:"default_handler"` Extensions []string `json:"extensions"` } +// supportedRuntimes is Orva's runtime catalog: exactly two, latest-stable only. +// The generic IDs never change; the Name/Version labels move as we bump the +// underlying major. var supportedRuntimes = []runtimeInfo{ { - ID: "node22", - Name: "Node.js 22 (Active LTS)", + ID: "node", + Name: "Node.js 24 (current)", + Version: "24", Language: "javascript", DefaultHandler: "handler.js", - Extensions: []string{".js", ".mjs", ".cjs"}, + Extensions: []string{".js", ".mjs", ".cjs", ".ts"}, }, { - ID: "node24", - Name: "Node.js 24 (Current LTS)", - Language: "javascript", - DefaultHandler: "handler.js", - Extensions: []string{".js", ".mjs", ".cjs"}, - }, - { - ID: "python313", - Name: "Python 3.13", - Language: "python", - DefaultHandler: "handler.py", - Extensions: []string{".py"}, - }, - { - ID: "python314", - Name: "Python 3.14", + ID: "python", + Name: "Python 3.14 (current)", + Version: "3.14", Language: "python", DefaultHandler: "handler.py", Extensions: []string{".py"}, diff --git a/backend/internal/server/server_test.go b/backend/internal/server/server_test.go index 97035a4..e665e6c 100644 --- a/backend/internal/server/server_test.go +++ b/backend/internal/server/server_test.go @@ -136,7 +136,7 @@ func Test404(t *testing.T) { func TestCreateAndGetFunction(t *testing.T) { tc := newTestServer(t) - body := `{"name":"test-hello","runtime":"node22","entrypoint":"handler.js"}` + body := `{"name":"test-hello","runtime":"node","entrypoint":"handler.js"}` req := httptest.NewRequest("POST", "/api/v1/functions", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") tc.setAuth(req) @@ -180,7 +180,7 @@ func TestCreateAndGetFunction(t *testing.T) { func TestListFunctions(t *testing.T) { tc := newTestServer(t) - body := `{"name":"list-test","runtime":"python313"}` + body := `{"name":"list-test","runtime":"python"}` req := httptest.NewRequest("POST", "/api/v1/functions", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") tc.setAuth(req) @@ -212,7 +212,7 @@ func TestListFunctions(t *testing.T) { func TestDeleteFunction(t *testing.T) { tc := newTestServer(t) - body := `{"name":"delete-me","runtime":"node22"}` + body := `{"name":"delete-me","runtime":"node"}` req := httptest.NewRequest("POST", "/api/v1/functions", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") tc.setAuth(req) @@ -285,7 +285,7 @@ func TestAuthMiddleware_InsufficientPermissions(t *testing.T) { } // Try to write with a read-only key. - body := `{"name":"test","runtime":"node22"}` + body := `{"name":"test","runtime":"node"}` req := httptest.NewRequest("POST", "/api/v1/functions", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Orva-API-Key", readOnlyKey) @@ -314,8 +314,8 @@ func TestRuntimesEndpoint(t *testing.T) { t.Fatal(err) } runtimes, ok := resp["runtimes"].([]any) - if !ok || len(runtimes) != 4 { - t.Errorf("expected 4 runtimes (node22/node24/python313/python314), got %v", resp["runtimes"]) + if !ok || len(runtimes) != 2 { + t.Errorf("expected 2 runtimes (node/python), got %v", resp["runtimes"]) } } @@ -406,7 +406,7 @@ func TestUpdateFunction_PartialUpdate(t *testing.T) { tc := newTestServer(t) // Create a function first. - body := `{"name":"update-partial","runtime":"node22","entrypoint":"handler.js"}` + body := `{"name":"update-partial","runtime":"node","entrypoint":"handler.js"}` req := httptest.NewRequest("POST", "/api/v1/functions", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") tc.setAuth(req) @@ -436,8 +436,8 @@ func TestUpdateFunction_PartialUpdate(t *testing.T) { if updated.Name != "updated-name" { t.Errorf("expected name updated-name, got %s", updated.Name) } - if updated.Runtime != "node22" { - t.Errorf("expected runtime preserved as node22, got %s", updated.Runtime) + if updated.Runtime != "node" { + t.Errorf("expected runtime preserved as node, got %s", updated.Runtime) } if updated.Version != fn.Version+1 { t.Errorf("expected version %d, got %d", fn.Version+1, updated.Version) @@ -447,7 +447,7 @@ func TestUpdateFunction_PartialUpdate(t *testing.T) { func TestCreateFunction_DuplicateName(t *testing.T) { tc := newTestServer(t) - body := `{"name":"dup-name","runtime":"node22"}` + body := `{"name":"dup-name","runtime":"node"}` req := httptest.NewRequest("POST", "/api/v1/functions", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") tc.setAuth(req) @@ -490,7 +490,7 @@ func TestCreateFunction_InvalidRuntime(t *testing.T) { func TestCreateFunction_RejectsInvalidNetworkMode(t *testing.T) { tc := newTestServer(t) - body := `{"name":"bad-net","runtime":"node22","network_mode":"wat"}` + body := `{"name":"bad-net","runtime":"node","network_mode":"wat"}` req := httptest.NewRequest("POST", "/api/v1/functions", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") tc.setAuth(req) @@ -513,9 +513,9 @@ func TestCreateFunction_DefaultNetworkMode(t *testing.T) { body string want string }{ - {"omitted", `{"name":"net-default","runtime":"node22"}`, "none"}, - {"explicit-none", `{"name":"net-none","runtime":"node22","network_mode":"none"}`, "none"}, - {"egress", `{"name":"net-egress","runtime":"node22","network_mode":"egress"}`, "egress"}, + {"omitted", `{"name":"net-default","runtime":"node"}`, "none"}, + {"explicit-none", `{"name":"net-none","runtime":"node","network_mode":"none"}`, "none"}, + {"egress", `{"name":"net-egress","runtime":"node","network_mode":"egress"}`, "egress"}, } { t.Run(tc2.name, func(t *testing.T) { req := httptest.NewRequest("POST", "/api/v1/functions", bytes.NewBufferString(tc2.body)) @@ -542,7 +542,7 @@ func TestCreateFunction_DefaultNetworkMode(t *testing.T) { func TestUpdateFunction_TogglesNetworkMode(t *testing.T) { tc := newTestServer(t) - body := `{"name":"net-toggle","runtime":"node22"}` + body := `{"name":"net-toggle","runtime":"node"}` req := httptest.NewRequest("POST", "/api/v1/functions", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") tc.setAuth(req) diff --git a/backend/internal/server/ui_dist/assets/AI-DnN0qj60.js b/backend/internal/server/ui_dist/assets/AI-CRINc2rq.js similarity index 99% rename from backend/internal/server/ui_dist/assets/AI-DnN0qj60.js rename to backend/internal/server/ui_dist/assets/AI-CRINc2rq.js index ed2d527..161fe66 100644 --- a/backend/internal/server/ui_dist/assets/AI-DnN0qj60.js +++ b/backend/internal/server/ui_dist/assets/AI-CRINc2rq.js @@ -1,4 +1,4 @@ -import{c as V,j as g,a as y,b as k,d as D,f as x,t as N,g as F,C as Uu,h as $,P as gu,k as q,_ as oe,F as O,p as ue,s as T,w as xu,r as S,M as Bt,D as Oe,E as ce,G as ju,n as I,H as _e,q as z,o as be,y as iu,I as le,T as Lt,J as Hu,K as We,L as Ot,N as $t,Q as Pt,S as qt,e as Zu,v as Gu,R as Ut,i as jt}from"./index-D5cO6vit.js";import{D as Vu}from"./Drawer-B_L-gxK5.js";import{D as Ht,u as $e}from"./ai-BsHVzyl5.js";import{P as Wu}from"./pencil-CkSYbb1r.js";import{T as tu}from"./trash-2-sSCgxcxW.js";import{H as Z,j as Zt,p as Gt,a as Vt,b as Wt}from"./github-dark-BrynTfs3.js";import{c as Ku}from"./clipboard-CmSw2rR-.js";import{C as ye}from"./check-BkPCgKSu.js";import{C as Ju}from"./copy-Gc8n9M6v.js";import{C as au}from"./chevron-down-Trjh5P5D.js";import{R as Qu}from"./rotate-ccw-CeRUwJZR.js";import{Z as Kt}from"./zap-CTttJ1hV.js";import{S as Jt}from"./sparkles-DV3RWxRD.js";const Qt=V("arrow-down",[["path",{d:"M12 5v14",key:"s699le"}],["path",{d:"m19 12-7 7-7-7",key:"1idqje"}]]);const Xt=V("arrow-up",[["path",{d:"m5 12 7-7 7 7",key:"hav0vg"}],["path",{d:"M12 19V5",key:"x0mq9r"}]]);const Yt=V("ban",[["path",{d:"M4.929 4.929 19.07 19.071",key:"196cmz"}],["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]]);const Xu=V("brain",[["path",{d:"M12 18V5",key:"adv99a"}],["path",{d:"M15 13a4.17 4.17 0 0 1-3-4 4.17 4.17 0 0 1-3 4",key:"1e3is1"}],["path",{d:"M17.598 6.5A3 3 0 1 0 12 5a3 3 0 1 0-5.598 1.5",key:"1gqd8o"}],["path",{d:"M17.997 5.125a4 4 0 0 1 2.526 5.77",key:"iwvgf7"}],["path",{d:"M18 18a4 4 0 0 0 2-7.464",key:"efp6ie"}],["path",{d:"M19.967 17.483A4 4 0 1 1 12 18a4 4 0 1 1-7.967-.517",key:"1gq6am"}],["path",{d:"M6 18a4 4 0 0 1-2-7.464",key:"k1g0md"}],["path",{d:"M6.003 5.125a4 4 0 0 0-2.526 5.77",key:"q97ue3"}]]);const en=V("chevrons-down-up",[["path",{d:"m7 20 5-5 5 5",key:"13a0gw"}],["path",{d:"m7 4 5 5 5-5",key:"1kwcof"}]]);const un=V("chevrons-up-down",[["path",{d:"m7 15 5 5 5-5",key:"1hf1tw"}],["path",{d:"m7 9 5-5 5 5",key:"sgt6xg"}]]);const tn=V("cpu",[["path",{d:"M12 20v2",key:"1lh1kg"}],["path",{d:"M12 2v2",key:"tus03m"}],["path",{d:"M17 20v2",key:"1rnc9c"}],["path",{d:"M17 2v2",key:"11trls"}],["path",{d:"M2 12h2",key:"1t8f8n"}],["path",{d:"M2 17h2",key:"7oei6x"}],["path",{d:"M2 7h2",key:"asdhe0"}],["path",{d:"M20 12h2",key:"1q8mjw"}],["path",{d:"M20 17h2",key:"1fpfkl"}],["path",{d:"M20 7h2",key:"1o8tra"}],["path",{d:"M7 20v2",key:"4gnj0m"}],["path",{d:"M7 2v2",key:"1i4yhu"}],["rect",{x:"4",y:"4",width:"16",height:"16",rx:"2",key:"1vbyd7"}],["rect",{x:"8",y:"8",width:"8",height:"8",rx:"1",key:"z9xiuo"}]]);const Yu=V("message-square",[["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z",key:"18887p"}]]);const nn=V("panel-left",[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",key:"afitv7"}],["path",{d:"M9 3v18",key:"fh3hqa"}]]);const rn=V("square",[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",key:"afitv7"}]]);const on=V("wrench",[["path",{d:"M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z",key:"1ngwbx"}]]),an={class:"flex h-16 shrink-0 items-center justify-between gap-3 border-b border-border px-4"},sn={class:"flex min-w-0 items-center gap-2"},cn={class:"truncate text-sm font-semibold tracking-tight text-white"},ln={class:"flex items-center gap-0.5"},dn={__name:"ChatHeader",props:{title:{type:String,default:"Assistant"},canExport:{type:Boolean,default:!1}},emits:["toggle-rail","export"],setup(e){return(u,t)=>(g(),y("header",an,[k("div",sn,[k("button",{class:"touch-expand-iconbtn -ml-1 rounded-md p-2 text-foreground-muted transition-colors hover:bg-surface-hover hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background md:hidden","aria-label":"Conversations",onClick:t[0]||(t[0]=n=>u.$emit("toggle-rail"))},[D(x(nn),{class:"h-4 w-4"})]),D(x(Yu),{class:"hidden h-4 w-4 shrink-0 text-foreground-muted md:block"}),k("h1",cn,N(e.title),1)]),k("div",ln,[e.canExport?(g(),y("button",{key:0,class:"touch-expand-iconbtn -mr-1 rounded-md p-2 text-foreground-muted transition-colors hover:bg-surface-hover hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background","aria-label":"Export conversation as Markdown",title:"Export conversation",onClick:t[1]||(t[1]=n=>u.$emit("export"))},[D(x(Ht),{class:"h-4 w-4"})])):F("",!0)])]))}},fn={class:"flex h-full flex-col"},bn={key:0,class:"flex h-16 shrink-0 items-center justify-between px-4 border-b border-border"},pn={class:"flex-1 overflow-y-auto scrollable p-2 space-y-0.5"},hn=["aria-current","onClick"],mn={class:"flex-1 truncate"},gn=["onClick"],xn=["onClick"],kn={key:1,class:"px-2.5 py-2 text-xs text-foreground-muted"},ku={__name:"ConversationRail",props:{embedded:{type:Boolean,default:!1}},emits:["select"],setup(e,{emit:u}){const t=$e(),n=Uu();async function r(c){const d=await n.prompt({title:"Rename conversation",defaultValue:c.title||"",placeholder:"Conversation name",confirmLabel:"Rename"});d!=null&&d.trim()&&t.renameConversation(c.id,d.trim())}async function o(c){await n.ask({title:"Delete conversation?",message:"This permanently deletes the conversation and all its messages.",danger:!0,confirmLabel:"Delete"})&&t.deleteConversation(c)}const a=u;function i(){t.newConversation(),a("select")}function s(c){t.openConversation(c),a("select")}return(c,d)=>(g(),y("div",fn,[e.embedded?F("",!0):(g(),y("div",bn,[d[1]||(d[1]=k("span",{class:"text-sm font-semibold tracking-tight text-white"},"Conversations",-1)),D(oe,{size:"xs",variant:"secondary",onClick:i},{default:$(()=>[D(x(gu),{class:"h-3.5 w-3.5"}),d[0]||(d[0]=q(" New ",-1))]),_:1})])),k("div",pn,[e.embedded?(g(),y("button",{key:0,class:"touch-expand-sm mb-1 flex w-full items-center gap-2 rounded-md border border-border px-2.5 py-2 text-left text-sm text-foreground transition-colors hover:bg-surface-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary",onClick:i},[D(x(gu),{class:"h-3.5 w-3.5 shrink-0"}),d[2]||(d[2]=q(" New conversation ",-1))])):F("",!0),(g(!0),y(O,null,ue(x(t).conversations,f=>(g(),y("div",{key:f.id,class:T(["group flex w-full items-center gap-0.5 rounded-md pr-1 transition-colors",f.id===x(t).activeId?"bg-primary/15":"hover:bg-surface-hover"])},[k("button",{class:T(["touch-expand-sm flex min-w-0 flex-1 items-center gap-2 rounded-md px-2.5 py-2 text-left text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary",f.id===x(t).activeId?"text-white":"text-foreground-muted group-hover:text-white"]),"aria-current":f.id===x(t).activeId?"page":void 0,onClick:b=>s(f.id)},[D(x(Yu),{class:"h-3.5 w-3.5 shrink-0 opacity-70"}),k("span",mn,N(f.title||"New conversation"),1)],10,hn),k("button",{type:"button",class:"touch-expand-xs shrink-0 rounded-md p-2 text-foreground-muted opacity-0 transition-opacity hover:bg-surface-hover hover:text-white focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary group-hover:opacity-100 max-md:opacity-100",title:"Rename conversation","aria-label":"Rename conversation",onClick:xu(b=>r(f),["stop"])},[D(x(Wu),{class:"h-3.5 w-3.5"})],8,gn),k("button",{type:"button",class:"touch-expand-xs shrink-0 rounded-md p-2 text-foreground-muted opacity-0 transition-opacity hover:bg-surface-hover hover:text-danger-fg focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary group-hover:opacity-100 max-md:opacity-100",title:"Delete conversation","aria-label":"Delete conversation",onClick:xu(b=>o(f.id),["stop"])},[D(x(tu),{class:"h-3.5 w-3.5"})],8,xn)],2))),128)),x(t).conversations.length?F("",!0):(g(),y("p",kn," No conversations yet. "))])]))}},_n={class:"text-center"},yn={class:"mx-auto mb-4 flex h-11 w-11 items-center justify-center rounded-xl bg-primary/15 text-primary"},vn={class:"mx-auto mt-6 grid max-w-xl gap-2 sm:grid-cols-2"},En=["onClick"],Cn={class:"line-clamp-2"},Dn={__name:"EmptyState",emits:["pick"],setup(e){const u=["How many functions do I have?","Which function ran most recently?","Any errors in the last 24 hours?","Show my most recent executions.","List my deployed functions.","Summarize today’s invocation errors.","Show failed deployments and why.","What’s my system health right now?","Show storage usage for my instance.","List my cron schedules.","Are any background jobs failing?","Check for failed webhook deliveries.","Which runtimes are available?","Show my slowest functions by duration.","Which functions have egress enabled?","List my secrets by name only.","Write a Python function that returns the current UTC time.","Write a Node function that echoes the request body.","Create an hourly cron schedule for a function.","Walk me through deploying a new function."];function t(r,o){const a=[...r];for(let i=a.length-1;i>0;i--){const s=Math.floor(Math.random()*(i+1));[a[i],a[s]]=[a[s],a[i]]}return a.slice(0,o)}const n=S(t(u,4));return(r,o)=>(g(),y("div",_n,[k("div",yn,[D(x(Bt),{class:"h-5 w-5"})]),o[0]||(o[0]=k("h2",{class:"text-lg font-semibold tracking-tight text-white"}," What would you like to do? ",-1)),o[1]||(o[1]=k("p",{class:"mx-auto mt-1.5 max-w-md text-sm leading-relaxed text-foreground-muted"}," Ask about your functions, logs, deployments, and operations. Or have me create, deploy, and invoke functions for you. ",-1)),k("div",vn,[(g(!0),y(O,null,ue(n.value,(a,i)=>(g(),y("button",{key:i,type:"button",class:"flex min-h-[4.25rem] items-center rounded-lg border border-border bg-surface/50 px-3.5 py-3 text-left text-[13px] leading-snug text-foreground-muted transition-colors hover:border-foreground-muted/40 hover:bg-surface-hover hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background",onClick:s=>r.$emit("pick",a)},[k("span",Cn,N(a),1)],8,En))),128))])]))}},_u={};function An(e){let u=_u[e];if(u)return u;u=_u[e]=[];for(let t=0;t<128;t++){const n=String.fromCharCode(t);u.push(n)}for(let t=0;t=55296&&d<=57343?r+="���":r+=String.fromCharCode(d),o+=6;continue}}if((i&248)===240&&o+91114111?r+="����":(f-=65536,r+=String.fromCharCode(55296+(f>>10),56320+(f&1023))),o+=9;continue}}r+="�"}return r})}pe.defaultChars=";/?:@&=+$,#";pe.componentChars="";const yu={};function wn(e){let u=yu[e];if(u)return u;u=yu[e]=[];for(let t=0;t<128;t++){const n=String.fromCharCode(t);/^[0-9a-z]$/i.test(n)?u.push(n):u.push("%"+("0"+t.toString(16).toUpperCase()).slice(-2))}for(let t=0;t"u"&&(t=!0);const n=wn(u);let r="";for(let o=0,a=e.length;o=55296&&i<=57343){if(i>=55296&&i<=56319&&o+1=56320&&s<=57343){r+=encodeURIComponent(e[o]+e[o+1]),o++;continue}}r+="%EF%BF%BD";continue}r+=encodeURIComponent(e[o])}return r}we.defaultChars=";/?:@&=+$,-_.!~*'()#";we.componentChars="-_.!~*'()";function su(e){let u="";return u+=e.protocol||"",u+=e.slashes?"//":"",u+=e.auth?e.auth+"@":"",e.hostname&&e.hostname.indexOf(":")!==-1?u+="["+e.hostname+"]":u+=e.hostname||"",u+=e.port?":"+e.port:"",u+=e.pathname||"",u+=e.search||"",u+=e.hash||"",u}function ze(){this.protocol=null,this.slashes=null,this.auth=null,this.port=null,this.hostname=null,this.hash=null,this.search=null,this.pathname=null}const Fn=/^([a-z0-9.+-]+:)/i,Sn=/:[0-9]*$/,Tn=/^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/,Mn=["<",">",'"',"`"," ","\r",` +import{c as V,j as g,a as y,b as k,d as D,f as x,t as N,g as F,C as Uu,h as $,P as gu,k as q,_ as oe,F as O,p as ue,s as T,w as xu,r as S,M as Bt,D as Oe,E as ce,G as ju,n as I,H as _e,q as z,o as be,y as iu,I as le,T as Lt,J as Hu,K as We,L as Ot,N as $t,Q as Pt,S as qt,e as Zu,v as Gu,R as Ut,i as jt}from"./index-BMkkwZ9q.js";import{D as Vu}from"./Drawer-C3AFLOZb.js";import{D as Ht,u as $e}from"./ai-DmyZUAtW.js";import{P as Wu}from"./pencil-DTkm5-NQ.js";import{T as tu}from"./trash-2-BXf2uqQH.js";import{H as Z,j as Zt,p as Gt,a as Vt,b as Wt}from"./github-dark-BrynTfs3.js";import{c as Ku}from"./clipboard-CmSw2rR-.js";import{C as ye}from"./check-C4wzjDZN.js";import{C as Ju}from"./copy-CTb6u-fx.js";import{C as au}from"./chevron-down-BTZfO5Md.js";import{R as Qu}from"./rotate-ccw-CsgWy1Bs.js";import{Z as Kt}from"./zap-DvhWYa2n.js";import{S as Jt}from"./sparkles-BVQ_t_Q_.js";const Qt=V("arrow-down",[["path",{d:"M12 5v14",key:"s699le"}],["path",{d:"m19 12-7 7-7-7",key:"1idqje"}]]);const Xt=V("arrow-up",[["path",{d:"m5 12 7-7 7 7",key:"hav0vg"}],["path",{d:"M12 19V5",key:"x0mq9r"}]]);const Yt=V("ban",[["path",{d:"M4.929 4.929 19.07 19.071",key:"196cmz"}],["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]]);const Xu=V("brain",[["path",{d:"M12 18V5",key:"adv99a"}],["path",{d:"M15 13a4.17 4.17 0 0 1-3-4 4.17 4.17 0 0 1-3 4",key:"1e3is1"}],["path",{d:"M17.598 6.5A3 3 0 1 0 12 5a3 3 0 1 0-5.598 1.5",key:"1gqd8o"}],["path",{d:"M17.997 5.125a4 4 0 0 1 2.526 5.77",key:"iwvgf7"}],["path",{d:"M18 18a4 4 0 0 0 2-7.464",key:"efp6ie"}],["path",{d:"M19.967 17.483A4 4 0 1 1 12 18a4 4 0 1 1-7.967-.517",key:"1gq6am"}],["path",{d:"M6 18a4 4 0 0 1-2-7.464",key:"k1g0md"}],["path",{d:"M6.003 5.125a4 4 0 0 0-2.526 5.77",key:"q97ue3"}]]);const en=V("chevrons-down-up",[["path",{d:"m7 20 5-5 5 5",key:"13a0gw"}],["path",{d:"m7 4 5 5 5-5",key:"1kwcof"}]]);const un=V("chevrons-up-down",[["path",{d:"m7 15 5 5 5-5",key:"1hf1tw"}],["path",{d:"m7 9 5-5 5 5",key:"sgt6xg"}]]);const tn=V("cpu",[["path",{d:"M12 20v2",key:"1lh1kg"}],["path",{d:"M12 2v2",key:"tus03m"}],["path",{d:"M17 20v2",key:"1rnc9c"}],["path",{d:"M17 2v2",key:"11trls"}],["path",{d:"M2 12h2",key:"1t8f8n"}],["path",{d:"M2 17h2",key:"7oei6x"}],["path",{d:"M2 7h2",key:"asdhe0"}],["path",{d:"M20 12h2",key:"1q8mjw"}],["path",{d:"M20 17h2",key:"1fpfkl"}],["path",{d:"M20 7h2",key:"1o8tra"}],["path",{d:"M7 20v2",key:"4gnj0m"}],["path",{d:"M7 2v2",key:"1i4yhu"}],["rect",{x:"4",y:"4",width:"16",height:"16",rx:"2",key:"1vbyd7"}],["rect",{x:"8",y:"8",width:"8",height:"8",rx:"1",key:"z9xiuo"}]]);const Yu=V("message-square",[["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z",key:"18887p"}]]);const nn=V("panel-left",[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",key:"afitv7"}],["path",{d:"M9 3v18",key:"fh3hqa"}]]);const rn=V("square",[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",key:"afitv7"}]]);const on=V("wrench",[["path",{d:"M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z",key:"1ngwbx"}]]),an={class:"flex h-16 shrink-0 items-center justify-between gap-3 border-b border-border px-4"},sn={class:"flex min-w-0 items-center gap-2"},cn={class:"truncate text-sm font-semibold tracking-tight text-white"},ln={class:"flex items-center gap-0.5"},dn={__name:"ChatHeader",props:{title:{type:String,default:"Assistant"},canExport:{type:Boolean,default:!1}},emits:["toggle-rail","export"],setup(e){return(u,t)=>(g(),y("header",an,[k("div",sn,[k("button",{class:"touch-expand-iconbtn -ml-1 rounded-md p-2 text-foreground-muted transition-colors hover:bg-surface-hover hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background md:hidden","aria-label":"Conversations",onClick:t[0]||(t[0]=n=>u.$emit("toggle-rail"))},[D(x(nn),{class:"h-4 w-4"})]),D(x(Yu),{class:"hidden h-4 w-4 shrink-0 text-foreground-muted md:block"}),k("h1",cn,N(e.title),1)]),k("div",ln,[e.canExport?(g(),y("button",{key:0,class:"touch-expand-iconbtn -mr-1 rounded-md p-2 text-foreground-muted transition-colors hover:bg-surface-hover hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background","aria-label":"Export conversation as Markdown",title:"Export conversation",onClick:t[1]||(t[1]=n=>u.$emit("export"))},[D(x(Ht),{class:"h-4 w-4"})])):F("",!0)])]))}},fn={class:"flex h-full flex-col"},bn={key:0,class:"flex h-16 shrink-0 items-center justify-between px-4 border-b border-border"},pn={class:"flex-1 overflow-y-auto scrollable p-2 space-y-0.5"},hn=["aria-current","onClick"],mn={class:"flex-1 truncate"},gn=["onClick"],xn=["onClick"],kn={key:1,class:"px-2.5 py-2 text-xs text-foreground-muted"},ku={__name:"ConversationRail",props:{embedded:{type:Boolean,default:!1}},emits:["select"],setup(e,{emit:u}){const t=$e(),n=Uu();async function r(c){const d=await n.prompt({title:"Rename conversation",defaultValue:c.title||"",placeholder:"Conversation name",confirmLabel:"Rename"});d!=null&&d.trim()&&t.renameConversation(c.id,d.trim())}async function o(c){await n.ask({title:"Delete conversation?",message:"This permanently deletes the conversation and all its messages.",danger:!0,confirmLabel:"Delete"})&&t.deleteConversation(c)}const a=u;function i(){t.newConversation(),a("select")}function s(c){t.openConversation(c),a("select")}return(c,d)=>(g(),y("div",fn,[e.embedded?F("",!0):(g(),y("div",bn,[d[1]||(d[1]=k("span",{class:"text-sm font-semibold tracking-tight text-white"},"Conversations",-1)),D(oe,{size:"xs",variant:"secondary",onClick:i},{default:$(()=>[D(x(gu),{class:"h-3.5 w-3.5"}),d[0]||(d[0]=q(" New ",-1))]),_:1})])),k("div",pn,[e.embedded?(g(),y("button",{key:0,class:"touch-expand-sm mb-1 flex w-full items-center gap-2 rounded-md border border-border px-2.5 py-2 text-left text-sm text-foreground transition-colors hover:bg-surface-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary",onClick:i},[D(x(gu),{class:"h-3.5 w-3.5 shrink-0"}),d[2]||(d[2]=q(" New conversation ",-1))])):F("",!0),(g(!0),y(O,null,ue(x(t).conversations,f=>(g(),y("div",{key:f.id,class:T(["group flex w-full items-center gap-0.5 rounded-md pr-1 transition-colors",f.id===x(t).activeId?"bg-primary/15":"hover:bg-surface-hover"])},[k("button",{class:T(["touch-expand-sm flex min-w-0 flex-1 items-center gap-2 rounded-md px-2.5 py-2 text-left text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary",f.id===x(t).activeId?"text-white":"text-foreground-muted group-hover:text-white"]),"aria-current":f.id===x(t).activeId?"page":void 0,onClick:b=>s(f.id)},[D(x(Yu),{class:"h-3.5 w-3.5 shrink-0 opacity-70"}),k("span",mn,N(f.title||"New conversation"),1)],10,hn),k("button",{type:"button",class:"touch-expand-xs shrink-0 rounded-md p-2 text-foreground-muted opacity-0 transition-opacity hover:bg-surface-hover hover:text-white focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary group-hover:opacity-100 max-md:opacity-100",title:"Rename conversation","aria-label":"Rename conversation",onClick:xu(b=>r(f),["stop"])},[D(x(Wu),{class:"h-3.5 w-3.5"})],8,gn),k("button",{type:"button",class:"touch-expand-xs shrink-0 rounded-md p-2 text-foreground-muted opacity-0 transition-opacity hover:bg-surface-hover hover:text-danger-fg focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary group-hover:opacity-100 max-md:opacity-100",title:"Delete conversation","aria-label":"Delete conversation",onClick:xu(b=>o(f.id),["stop"])},[D(x(tu),{class:"h-3.5 w-3.5"})],8,xn)],2))),128)),x(t).conversations.length?F("",!0):(g(),y("p",kn," No conversations yet. "))])]))}},_n={class:"text-center"},yn={class:"mx-auto mb-4 flex h-11 w-11 items-center justify-center rounded-xl bg-primary/15 text-primary"},vn={class:"mx-auto mt-6 grid max-w-xl gap-2 sm:grid-cols-2"},En=["onClick"],Cn={class:"line-clamp-2"},Dn={__name:"EmptyState",emits:["pick"],setup(e){const u=["How many functions do I have?","Which function ran most recently?","Any errors in the last 24 hours?","Show my most recent executions.","List my deployed functions.","Summarize today’s invocation errors.","Show failed deployments and why.","What’s my system health right now?","Show storage usage for my instance.","List my cron schedules.","Are any background jobs failing?","Check for failed webhook deliveries.","Which runtimes are available?","Show my slowest functions by duration.","Which functions have egress enabled?","List my secrets by name only.","Write a Python function that returns the current UTC time.","Write a Node function that echoes the request body.","Create an hourly cron schedule for a function.","Walk me through deploying a new function."];function t(r,o){const a=[...r];for(let i=a.length-1;i>0;i--){const s=Math.floor(Math.random()*(i+1));[a[i],a[s]]=[a[s],a[i]]}return a.slice(0,o)}const n=S(t(u,4));return(r,o)=>(g(),y("div",_n,[k("div",yn,[D(x(Bt),{class:"h-5 w-5"})]),o[0]||(o[0]=k("h2",{class:"text-lg font-semibold tracking-tight text-white"}," What would you like to do? ",-1)),o[1]||(o[1]=k("p",{class:"mx-auto mt-1.5 max-w-md text-sm leading-relaxed text-foreground-muted"}," Ask about your functions, logs, deployments, and operations. Or have me create, deploy, and invoke functions for you. ",-1)),k("div",vn,[(g(!0),y(O,null,ue(n.value,(a,i)=>(g(),y("button",{key:i,type:"button",class:"flex min-h-[4.25rem] items-center rounded-lg border border-border bg-surface/50 px-3.5 py-3 text-left text-[13px] leading-snug text-foreground-muted transition-colors hover:border-foreground-muted/40 hover:bg-surface-hover hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background",onClick:s=>r.$emit("pick",a)},[k("span",Cn,N(a),1)],8,En))),128))])]))}},_u={};function An(e){let u=_u[e];if(u)return u;u=_u[e]=[];for(let t=0;t<128;t++){const n=String.fromCharCode(t);u.push(n)}for(let t=0;t=55296&&d<=57343?r+="���":r+=String.fromCharCode(d),o+=6;continue}}if((i&248)===240&&o+91114111?r+="����":(f-=65536,r+=String.fromCharCode(55296+(f>>10),56320+(f&1023))),o+=9;continue}}r+="�"}return r})}pe.defaultChars=";/?:@&=+$,#";pe.componentChars="";const yu={};function wn(e){let u=yu[e];if(u)return u;u=yu[e]=[];for(let t=0;t<128;t++){const n=String.fromCharCode(t);/^[0-9a-z]$/i.test(n)?u.push(n):u.push("%"+("0"+t.toString(16).toUpperCase()).slice(-2))}for(let t=0;t"u"&&(t=!0);const n=wn(u);let r="";for(let o=0,a=e.length;o=55296&&i<=57343){if(i>=55296&&i<=56319&&o+1=56320&&s<=57343){r+=encodeURIComponent(e[o]+e[o+1]),o++;continue}}r+="%EF%BF%BD";continue}r+=encodeURIComponent(e[o])}return r}we.defaultChars=";/?:@&=+$,-_.!~*'()#";we.componentChars="-_.!~*'()";function su(e){let u="";return u+=e.protocol||"",u+=e.slashes?"//":"",u+=e.auth?e.auth+"@":"",e.hostname&&e.hostname.indexOf(":")!==-1?u+="["+e.hostname+"]":u+=e.hostname||"",u+=e.port?":"+e.port:"",u+=e.pathname||"",u+=e.search||"",u+=e.hash||"",u}function ze(){this.protocol=null,this.slashes=null,this.auth=null,this.port=null,this.hostname=null,this.hash=null,this.search=null,this.pathname=null}const Fn=/^([a-z0-9.+-]+:)/i,Sn=/:[0-9]*$/,Tn=/^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/,Mn=["<",">",'"',"`"," ","\r",` `," "],Rn=["{","}","|","\\","^","`"].concat(Mn),Nn=["'"].concat(Rn),vu=["%","/","?",";","#"].concat(Nn),Eu=["/","?","#"],In=255,Cu=/^[+a-z0-9A-Z_-]{0,63}$/,zn=/^([+a-z0-9A-Z_-]{0,63})(.*)$/,Du={javascript:!0,"javascript:":!0},Au={http:!0,https:!0,ftp:!0,gopher:!0,file:!0,"http:":!0,"https:":!0,"ftp:":!0,"gopher:":!0,"file:":!0};function cu(e,u){if(e&&e instanceof ze)return e;const t=new ze;return t.parse(e,u),t}ze.prototype.parse=function(e,u){let t,n,r,o=e;if(o=o.trim(),!u&&e.split("#").length===1){const c=Tn.exec(o);if(c)return this.pathname=c[1],c[2]&&(this.search=c[2]),this}let a=Fn.exec(o);if(a&&(a=a[0],t=a.toLowerCase(),this.protocol=a,o=o.substr(a.length)),(u||a||o.match(/^\/\/[^@\/]+@[^@\/]+/))&&(r=o.substr(0,2)==="//",r&&!(a&&Du[a])&&(o=o.substr(2),this.slashes=!0)),!Du[a]&&(r||a&&!Au[a])){let c=-1;for(let l=0;l127?_+="x":_+=E[v];if(!_.match(Cu)){const v=l.slice(0,m),C=l.slice(m+1),A=E.match(zn);A&&(v.push(A[1]),C.unshift(A[2])),C.length&&(o=C.join(".")+o),this.hostname=v.join(".");break}}}}this.hostname.length>In&&(this.hostname=""),p&&(this.hostname=this.hostname.substr(1,this.hostname.length-2))}const i=o.indexOf("#");i!==-1&&(this.hash=o.substr(i),o=o.slice(0,i));const s=o.indexOf("?");return s!==-1&&(this.search=o.substr(s),o=o.slice(0,s)),o&&(this.pathname=o),Au[t]&&this.hostname&&!this.pathname&&(this.pathname=""),this};ze.prototype.parseHost=function(e){let u=Sn.exec(e);u&&(u=u[0],u!==":"&&(this.port=u.substr(1)),e=e.substr(0,e.length-u.length)),e&&(this.hostname=e)};const Bn=Object.freeze(Object.defineProperty({__proto__:null,decode:pe,encode:we,format:su,parse:cu},Symbol.toStringTag,{value:"Module"})),et=/[\0-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/,ut=/[\0-\x1F\x7F-\x9F]/,Ln=/[\xAD\u0600-\u0605\u061C\u06DD\u070F\u0890\u0891\u08E2\u180E\u200B-\u200F\u202A-\u202E\u2060-\u2064\u2066-\u206F\uFEFF\uFFF9-\uFFFB]|\uD804[\uDCBD\uDCCD]|\uD80D[\uDC30-\uDC3F]|\uD82F[\uDCA0-\uDCA3]|\uD834[\uDD73-\uDD7A]|\uDB40[\uDC01\uDC20-\uDC7F]/,lu=/[!-#%-\*,-\/:;\?@\[-\]_\{\}\xA1\xA7\xAB\xB6\xB7\xBB\xBF\u037E\u0387\u055A-\u055F\u0589\u058A\u05BE\u05C0\u05C3\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061D-\u061F\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u09FD\u0A76\u0AF0\u0C77\u0C84\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F14\u0F3A-\u0F3D\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1360-\u1368\u1400\u166E\u169B\u169C\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1B7D\u1B7E\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CC0-\u1CC7\u1CD3\u2010-\u2027\u2030-\u2043\u2045-\u2051\u2053-\u205E\u207D\u207E\u208D\u208E\u2308-\u230B\u2329\u232A\u2768-\u2775\u27C5\u27C6\u27E6-\u27EF\u2983-\u2998\u29D8-\u29DB\u29FC\u29FD\u2CF9-\u2CFC\u2CFE\u2CFF\u2D70\u2E00-\u2E2E\u2E30-\u2E4F\u2E52-\u2E5D\u3001-\u3003\u3008-\u3011\u3014-\u301F\u3030\u303D\u30A0\u30FB\uA4FE\uA4FF\uA60D-\uA60F\uA673\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA8FC\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uAAF0\uAAF1\uABEB\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE61\uFE63\uFE68\uFE6A\uFE6B\uFF01-\uFF03\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B-\uFF3D\uFF3F\uFF5B\uFF5D\uFF5F-\uFF65]|\uD800[\uDD00-\uDD02\uDF9F\uDFD0]|\uD801\uDD6F|\uD802[\uDC57\uDD1F\uDD3F\uDE50-\uDE58\uDE7F\uDEF0-\uDEF6\uDF39-\uDF3F\uDF99-\uDF9C]|\uD803[\uDEAD\uDF55-\uDF59\uDF86-\uDF89]|\uD804[\uDC47-\uDC4D\uDCBB\uDCBC\uDCBE-\uDCC1\uDD40-\uDD43\uDD74\uDD75\uDDC5-\uDDC8\uDDCD\uDDDB\uDDDD-\uDDDF\uDE38-\uDE3D\uDEA9]|\uD805[\uDC4B-\uDC4F\uDC5A\uDC5B\uDC5D\uDCC6\uDDC1-\uDDD7\uDE41-\uDE43\uDE60-\uDE6C\uDEB9\uDF3C-\uDF3E]|\uD806[\uDC3B\uDD44-\uDD46\uDDE2\uDE3F-\uDE46\uDE9A-\uDE9C\uDE9E-\uDEA2\uDF00-\uDF09]|\uD807[\uDC41-\uDC45\uDC70\uDC71\uDEF7\uDEF8\uDF43-\uDF4F\uDFFF]|\uD809[\uDC70-\uDC74]|\uD80B[\uDFF1\uDFF2]|\uD81A[\uDE6E\uDE6F\uDEF5\uDF37-\uDF3B\uDF44]|\uD81B[\uDE97-\uDE9A\uDFE2]|\uD82F\uDC9F|\uD836[\uDE87-\uDE8B]|\uD83A[\uDD5E\uDD5F]/,tt=/[\$\+<->\^`\|~\xA2-\xA6\xA8\xA9\xAC\xAE-\xB1\xB4\xB8\xD7\xF7\u02C2-\u02C5\u02D2-\u02DF\u02E5-\u02EB\u02ED\u02EF-\u02FF\u0375\u0384\u0385\u03F6\u0482\u058D-\u058F\u0606-\u0608\u060B\u060E\u060F\u06DE\u06E9\u06FD\u06FE\u07F6\u07FE\u07FF\u0888\u09F2\u09F3\u09FA\u09FB\u0AF1\u0B70\u0BF3-\u0BFA\u0C7F\u0D4F\u0D79\u0E3F\u0F01-\u0F03\u0F13\u0F15-\u0F17\u0F1A-\u0F1F\u0F34\u0F36\u0F38\u0FBE-\u0FC5\u0FC7-\u0FCC\u0FCE\u0FCF\u0FD5-\u0FD8\u109E\u109F\u1390-\u1399\u166D\u17DB\u1940\u19DE-\u19FF\u1B61-\u1B6A\u1B74-\u1B7C\u1FBD\u1FBF-\u1FC1\u1FCD-\u1FCF\u1FDD-\u1FDF\u1FED-\u1FEF\u1FFD\u1FFE\u2044\u2052\u207A-\u207C\u208A-\u208C\u20A0-\u20C0\u2100\u2101\u2103-\u2106\u2108\u2109\u2114\u2116-\u2118\u211E-\u2123\u2125\u2127\u2129\u212E\u213A\u213B\u2140-\u2144\u214A-\u214D\u214F\u218A\u218B\u2190-\u2307\u230C-\u2328\u232B-\u2426\u2440-\u244A\u249C-\u24E9\u2500-\u2767\u2794-\u27C4\u27C7-\u27E5\u27F0-\u2982\u2999-\u29D7\u29DC-\u29FB\u29FE-\u2B73\u2B76-\u2B95\u2B97-\u2BFF\u2CE5-\u2CEA\u2E50\u2E51\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u2FF0-\u2FFF\u3004\u3012\u3013\u3020\u3036\u3037\u303E\u303F\u309B\u309C\u3190\u3191\u3196-\u319F\u31C0-\u31E3\u31EF\u3200-\u321E\u322A-\u3247\u3250\u3260-\u327F\u328A-\u32B0\u32C0-\u33FF\u4DC0-\u4DFF\uA490-\uA4C6\uA700-\uA716\uA720\uA721\uA789\uA78A\uA828-\uA82B\uA836-\uA839\uAA77-\uAA79\uAB5B\uAB6A\uAB6B\uFB29\uFBB2-\uFBC2\uFD40-\uFD4F\uFDCF\uFDFC-\uFDFF\uFE62\uFE64-\uFE66\uFE69\uFF04\uFF0B\uFF1C-\uFF1E\uFF3E\uFF40\uFF5C\uFF5E\uFFE0-\uFFE6\uFFE8-\uFFEE\uFFFC\uFFFD]|\uD800[\uDD37-\uDD3F\uDD79-\uDD89\uDD8C-\uDD8E\uDD90-\uDD9C\uDDA0\uDDD0-\uDDFC]|\uD802[\uDC77\uDC78\uDEC8]|\uD805\uDF3F|\uD807[\uDFD5-\uDFF1]|\uD81A[\uDF3C-\uDF3F\uDF45]|\uD82F\uDC9C|\uD833[\uDF50-\uDFC3]|\uD834[\uDC00-\uDCF5\uDD00-\uDD26\uDD29-\uDD64\uDD6A-\uDD6C\uDD83\uDD84\uDD8C-\uDDA9\uDDAE-\uDDEA\uDE00-\uDE41\uDE45\uDF00-\uDF56]|\uD835[\uDEC1\uDEDB\uDEFB\uDF15\uDF35\uDF4F\uDF6F\uDF89\uDFA9\uDFC3]|\uD836[\uDC00-\uDDFF\uDE37-\uDE3A\uDE6D-\uDE74\uDE76-\uDE83\uDE85\uDE86]|\uD838[\uDD4F\uDEFF]|\uD83B[\uDCAC\uDCB0\uDD2E\uDEF0\uDEF1]|\uD83C[\uDC00-\uDC2B\uDC30-\uDC93\uDCA0-\uDCAE\uDCB1-\uDCBF\uDCC1-\uDCCF\uDCD1-\uDCF5\uDD0D-\uDDAD\uDDE6-\uDE02\uDE10-\uDE3B\uDE40-\uDE48\uDE50\uDE51\uDE60-\uDE65\uDF00-\uDFFF]|\uD83D[\uDC00-\uDED7\uDEDC-\uDEEC\uDEF0-\uDEFC\uDF00-\uDF76\uDF7B-\uDFD9\uDFE0-\uDFEB\uDFF0]|\uD83E[\uDC00-\uDC0B\uDC10-\uDC47\uDC50-\uDC59\uDC60-\uDC87\uDC90-\uDCAD\uDCB0\uDCB1\uDD00-\uDE53\uDE60-\uDE6D\uDE70-\uDE7C\uDE80-\uDE88\uDE90-\uDEBD\uDEBF-\uDEC5\uDECE-\uDEDB\uDEE0-\uDEE8\uDEF0-\uDEF8\uDF00-\uDF92\uDF94-\uDFCA]/,nt=/[ \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]/,On=Object.freeze(Object.defineProperty({__proto__:null,Any:et,Cc:ut,Cf:Ln,P:lu,S:tt,Z:nt},Symbol.toStringTag,{value:"Module"})),$n=new Uint16Array('ᵁ<Õıʊҝջאٵ۞ޢߖࠏ੊ઑඡ๭༉༦჊ረዡᐕᒝᓃᓟᔥ\0\0\0\0\0\0ᕫᛍᦍᰒᷝ὾⁠↰⊍⏀⏻⑂⠤⤒ⴈ⹈⿎〖㊺㘹㞬㣾㨨㩱㫠㬮ࠀEMabcfglmnoprstu\\bfms„‹•˜¦³¹ÈÏlig耻Æ䃆P耻&䀦cute耻Á䃁reve;䄂Āiyx}rc耻Â䃂;䐐r;쀀𝔄rave耻À䃀pha;䎑acr;䄀d;橓Āgp¡on;䄄f;쀀𝔸plyFunction;恡ing耻Å䃅Ācs¾Ãr;쀀𝒜ign;扔ilde耻Ã䃃ml耻Ä䃄ЀaceforsuåûþėĜĢħĪĀcrêòkslash;或Ŷöø;櫧ed;挆y;䐑ƀcrtąċĔause;戵noullis;愬a;䎒r;쀀𝔅pf;쀀𝔹eve;䋘còēmpeq;扎܀HOacdefhilorsuōőŖƀƞƢƵƷƺǜȕɳɸɾcy;䐧PY耻©䂩ƀcpyŝŢźute;䄆Ā;iŧŨ拒talDifferentialD;慅leys;愭ȀaeioƉƎƔƘron;䄌dil耻Ç䃇rc;䄈nint;戰ot;䄊ĀdnƧƭilla;䂸terDot;䂷òſi;䎧rcleȀDMPTLJNjǑǖot;抙inus;抖lus;投imes;抗oĀcsǢǸkwiseContourIntegral;戲eCurlyĀDQȃȏoubleQuote;思uote;怙ȀlnpuȞȨɇɕonĀ;eȥȦ户;橴ƀgitȯȶȺruent;扡nt;戯ourIntegral;戮ĀfrɌɎ;愂oduct;成nterClockwiseContourIntegral;戳oss;樯cr;쀀𝒞pĀ;Cʄʅ拓ap;才րDJSZacefiosʠʬʰʴʸˋ˗ˡ˦̳ҍĀ;oŹʥtrahd;椑cy;䐂cy;䐅cy;䐏ƀgrsʿ˄ˇger;怡r;憡hv;櫤Āayː˕ron;䄎;䐔lĀ;t˝˞戇a;䎔r;쀀𝔇Āaf˫̧Ācm˰̢riticalȀADGT̖̜̀̆cute;䂴oŴ̋̍;䋙bleAcute;䋝rave;䁠ilde;䋜ond;拄ferentialD;慆Ѱ̽\0\0\0͔͂\0Ѕf;쀀𝔻ƀ;DE͈͉͍䂨ot;惜qual;扐blèCDLRUVͣͲ΂ϏϢϸontourIntegraìȹoɴ͹\0\0ͻ»͉nArrow;懓Āeo·ΤftƀARTΐΖΡrrow;懐ightArrow;懔eåˊngĀLRΫτeftĀARγιrrow;柸ightArrow;柺ightArrow;柹ightĀATϘϞrrow;懒ee;抨pɁϩ\0\0ϯrrow;懑ownArrow;懕erticalBar;戥ǹABLRTaВЪаўѿͼrrowƀ;BUНОТ憓ar;椓pArrow;懵reve;䌑eft˒к\0ц\0ѐightVector;楐eeVector;楞ectorĀ;Bљњ憽ar;楖ightǔѧ\0ѱeeVector;楟ectorĀ;BѺѻ懁ar;楗eeĀ;A҆҇护rrow;憧ĀctҒҗr;쀀𝒟rok;䄐ࠀNTacdfglmopqstuxҽӀӄӋӞӢӧӮӵԡԯԶՒ՝ՠեG;䅊H耻Ð䃐cute耻É䃉ƀaiyӒӗӜron;䄚rc耻Ê䃊;䐭ot;䄖r;쀀𝔈rave耻È䃈ement;戈ĀapӺӾcr;䄒tyɓԆ\0\0ԒmallSquare;旻erySmallSquare;斫ĀgpԦԪon;䄘f;쀀𝔼silon;䎕uĀaiԼՉlĀ;TՂՃ橵ilde;扂librium;懌Āci՗՚r;愰m;橳a;䎗ml耻Ë䃋Āipժկsts;戃onentialE;慇ʀcfiosօֈ֍ֲ׌y;䐤r;쀀𝔉lledɓ֗\0\0֣mallSquare;旼erySmallSquare;斪Ͱֺ\0ֿ\0\0ׄf;쀀𝔽All;戀riertrf;愱cò׋؀JTabcdfgorstר׬ׯ׺؀ؒؖ؛؝أ٬ٲcy;䐃耻>䀾mmaĀ;d׷׸䎓;䏜reve;䄞ƀeiy؇،ؐdil;䄢rc;䄜;䐓ot;䄠r;쀀𝔊;拙pf;쀀𝔾eater̀EFGLSTصلَٖٛ٦qualĀ;Lؾؿ扥ess;招ullEqual;执reater;檢ess;扷lantEqual;橾ilde;扳cr;쀀𝒢;扫ЀAacfiosuڅڋږڛڞڪھۊRDcy;䐪Āctڐڔek;䋇;䁞irc;䄤r;愌lbertSpace;愋ǰگ\0ڲf;愍izontalLine;攀Āctۃۅòکrok;䄦mpńېۘownHumðįqual;扏܀EJOacdfgmnostuۺ۾܃܇܎ܚܞܡܨ݄ݸދޏޕcy;䐕lig;䄲cy;䐁cute耻Í䃍Āiyܓܘrc耻Î䃎;䐘ot;䄰r;愑rave耻Ì䃌ƀ;apܠܯܿĀcgܴܷr;䄪inaryI;慈lieóϝǴ݉\0ݢĀ;eݍݎ戬Āgrݓݘral;戫section;拂isibleĀCTݬݲomma;恣imes;恢ƀgptݿރވon;䄮f;쀀𝕀a;䎙cr;愐ilde;䄨ǫޚ\0ޞcy;䐆l耻Ï䃏ʀcfosuެ޷޼߂ߐĀiyޱ޵rc;䄴;䐙r;쀀𝔍pf;쀀𝕁ǣ߇\0ߌr;쀀𝒥rcy;䐈kcy;䐄΀HJacfosߤߨ߽߬߱ࠂࠈcy;䐥cy;䐌ppa;䎚Āey߶߻dil;䄶;䐚r;쀀𝔎pf;쀀𝕂cr;쀀𝒦րJTaceflmostࠥࠩࠬࡐࡣ঳সে্਷ੇcy;䐉耻<䀼ʀcmnpr࠷࠼ࡁࡄࡍute;䄹bda;䎛g;柪lacetrf;愒r;憞ƀaeyࡗ࡜ࡡron;䄽dil;䄻;䐛Āfsࡨ॰tԀACDFRTUVarࡾࢩࢱࣦ࣠ࣼयज़ΐ४Ānrࢃ࢏gleBracket;柨rowƀ;BR࢙࢚࢞憐ar;懤ightArrow;懆eiling;挈oǵࢷ\0ࣃbleBracket;柦nǔࣈ\0࣒eeVector;楡ectorĀ;Bࣛࣜ懃ar;楙loor;挊ightĀAV࣯ࣵrrow;憔ector;楎Āerँगeƀ;AVउऊऐ抣rrow;憤ector;楚iangleƀ;BEतथऩ抲ar;槏qual;抴pƀDTVषूौownVector;楑eeVector;楠ectorĀ;Bॖॗ憿ar;楘ectorĀ;B॥०憼ar;楒ightáΜs̀EFGLSTॾঋকঝঢভqualGreater;拚ullEqual;扦reater;扶ess;檡lantEqual;橽ilde;扲r;쀀𝔏Ā;eঽা拘ftarrow;懚idot;䄿ƀnpw৔ਖਛgȀLRlr৞৷ਂਐeftĀAR০৬rrow;柵ightArrow;柷ightArrow;柶eftĀarγਊightáοightáϊf;쀀𝕃erĀLRਢਬeftArrow;憙ightArrow;憘ƀchtਾੀੂòࡌ;憰rok;䅁;扪Ѐacefiosuਗ਼੝੠੷੼અઋ઎p;椅y;䐜Ādl੥੯iumSpace;恟lintrf;愳r;쀀𝔐nusPlus;戓pf;쀀𝕄cò੶;䎜ҀJacefostuણધભીଔଙඑ඗ඞcy;䐊cute;䅃ƀaey઴હાron;䅇dil;䅅;䐝ƀgswે૰଎ativeƀMTV૓૟૨ediumSpace;怋hiĀcn૦૘ë૙eryThiî૙tedĀGL૸ଆreaterGreateòٳessLesóੈLine;䀊r;쀀𝔑ȀBnptଢନଷ଺reak;恠BreakingSpace;䂠f;愕ڀ;CDEGHLNPRSTV୕ୖ୪୼஡௫ఄ౞಄ದ೘ൡඅ櫬Āou୛୤ngruent;扢pCap;扭oubleVerticalBar;戦ƀlqxஃஊ஛ement;戉ualĀ;Tஒஓ扠ilde;쀀≂̸ists;戄reater΀;EFGLSTஶஷ஽௉௓௘௥扯qual;扱ullEqual;쀀≧̸reater;쀀≫̸ess;批lantEqual;쀀⩾̸ilde;扵umpń௲௽ownHump;쀀≎̸qual;쀀≏̸eĀfsఊధtTriangleƀ;BEచఛడ拪ar;쀀⧏̸qual;括s̀;EGLSTవశ఼ౄోౘ扮qual;扰reater;扸ess;쀀≪̸lantEqual;쀀⩽̸ilde;扴estedĀGL౨౹reaterGreater;쀀⪢̸essLess;쀀⪡̸recedesƀ;ESಒಓಛ技qual;쀀⪯̸lantEqual;拠ĀeiಫಹverseElement;戌ghtTriangleƀ;BEೋೌ೒拫ar;쀀⧐̸qual;拭ĀquೝഌuareSuĀbp೨೹setĀ;E೰ೳ쀀⊏̸qual;拢ersetĀ;Eഃആ쀀⊐̸qual;拣ƀbcpഓതൎsetĀ;Eഛഞ쀀⊂⃒qual;抈ceedsȀ;ESTലള഻െ抁qual;쀀⪰̸lantEqual;拡ilde;쀀≿̸ersetĀ;E൘൛쀀⊃⃒qual;抉ildeȀ;EFT൮൯൵ൿ扁qual;扄ullEqual;扇ilde;扉erticalBar;戤cr;쀀𝒩ilde耻Ñ䃑;䎝܀Eacdfgmoprstuvලෂ෉෕ෛ෠෧෼ขภยา฿ไlig;䅒cute耻Ó䃓Āiy෎ීrc耻Ô䃔;䐞blac;䅐r;쀀𝔒rave耻Ò䃒ƀaei෮ෲ෶cr;䅌ga;䎩cron;䎟pf;쀀𝕆enCurlyĀDQฎบoubleQuote;怜uote;怘;橔Āclวฬr;쀀𝒪ash耻Ø䃘iŬื฼de耻Õ䃕es;樷ml耻Ö䃖erĀBP๋๠Āar๐๓r;怾acĀek๚๜;揞et;掴arenthesis;揜Ҁacfhilors๿ງຊຏຒດຝະ໼rtialD;戂y;䐟r;쀀𝔓i;䎦;䎠usMinus;䂱Āipຢອncareplanåڝf;愙Ȁ;eio຺ູ໠໤檻cedesȀ;EST່້໏໚扺qual;檯lantEqual;扼ilde;找me;怳Ādp໩໮uct;戏ortionĀ;aȥ໹l;戝Āci༁༆r;쀀𝒫;䎨ȀUfos༑༖༛༟OT耻"䀢r;쀀𝔔pf;愚cr;쀀𝒬؀BEacefhiorsu༾གྷཇའཱིྦྷྪྭ႖ႩႴႾarr;椐G耻®䂮ƀcnrཎནབute;䅔g;柫rĀ;tཛྷཝ憠l;椖ƀaeyཧཬཱron;䅘dil;䅖;䐠Ā;vླྀཹ愜erseĀEUྂྙĀlq྇ྎement;戋uilibrium;懋pEquilibrium;楯r»ཹo;䎡ghtЀACDFTUVa࿁࿫࿳ဢဨၛႇϘĀnr࿆࿒gleBracket;柩rowƀ;BL࿜࿝࿡憒ar;懥eftArrow;懄eiling;按oǵ࿹\0စbleBracket;柧nǔည\0နeeVector;楝ectorĀ;Bဝသ懂ar;楕loor;挋Āerိ၃eƀ;AVဵံြ抢rrow;憦ector;楛iangleƀ;BEၐၑၕ抳ar;槐qual;抵pƀDTVၣၮၸownVector;楏eeVector;楜ectorĀ;Bႂႃ憾ar;楔ectorĀ;B႑႒懀ar;楓Āpuႛ႞f;愝ndImplies;楰ightarrow;懛ĀchႹႼr;愛;憱leDelayed;槴ڀHOacfhimoqstuფჱჷჽᄙᄞᅑᅖᅡᅧᆵᆻᆿĀCcჩხHcy;䐩y;䐨FTcy;䐬cute;䅚ʀ;aeiyᄈᄉᄎᄓᄗ檼ron;䅠dil;䅞rc;䅜;䐡r;쀀𝔖ortȀDLRUᄪᄴᄾᅉownArrow»ОeftArrow»࢚ightArrow»࿝pArrow;憑gma;䎣allCircle;战pf;쀀𝕊ɲᅭ\0\0ᅰt;戚areȀ;ISUᅻᅼᆉᆯ斡ntersection;抓uĀbpᆏᆞsetĀ;Eᆗᆘ抏qual;抑ersetĀ;Eᆨᆩ抐qual;抒nion;抔cr;쀀𝒮ar;拆ȀbcmpᇈᇛሉላĀ;sᇍᇎ拐etĀ;Eᇍᇕqual;抆ĀchᇠህeedsȀ;ESTᇭᇮᇴᇿ扻qual;檰lantEqual;扽ilde;承Tháྌ;我ƀ;esሒሓሣ拑rsetĀ;Eሜም抃qual;抇et»ሓրHRSacfhiorsሾቄ቉ቕ቞ቱቶኟዂወዑORN耻Þ䃞ADE;愢ĀHc቎ቒcy;䐋y;䐦Ābuቚቜ;䀉;䎤ƀaeyብቪቯron;䅤dil;䅢;䐢r;쀀𝔗Āeiቻ኉Dzኀ\0ኇefore;戴a;䎘Ācn኎ኘkSpace;쀀  Space;怉ldeȀ;EFTካኬኲኼ戼qual;扃ullEqual;扅ilde;扈pf;쀀𝕋ipleDot;惛Āctዖዛr;쀀𝒯rok;䅦ૡዷጎጚጦ\0ጬጱ\0\0\0\0\0ጸጽ፷ᎅ\0᏿ᐄᐊᐐĀcrዻጁute耻Ú䃚rĀ;oጇገ憟cir;楉rǣጓ\0጖y;䐎ve;䅬Āiyጞጣrc耻Û䃛;䐣blac;䅰r;쀀𝔘rave耻Ù䃙acr;䅪Ādiፁ፩erĀBPፈ፝Āarፍፐr;䁟acĀekፗፙ;揟et;掵arenthesis;揝onĀ;P፰፱拃lus;抎Āgp፻፿on;䅲f;쀀𝕌ЀADETadps᎕ᎮᎸᏄϨᏒᏗᏳrrowƀ;BDᅐᎠᎤar;椒ownArrow;懅ownArrow;憕quilibrium;楮eeĀ;AᏋᏌ报rrow;憥ownáϳerĀLRᏞᏨeftArrow;憖ightArrow;憗iĀ;lᏹᏺ䏒on;䎥ing;䅮cr;쀀𝒰ilde;䅨ml耻Ü䃜ҀDbcdefosvᐧᐬᐰᐳᐾᒅᒊᒐᒖash;披ar;櫫y;䐒ashĀ;lᐻᐼ抩;櫦Āerᑃᑅ;拁ƀbtyᑌᑐᑺar;怖Ā;iᑏᑕcalȀBLSTᑡᑥᑪᑴar;戣ine;䁼eparator;杘ilde;所ThinSpace;怊r;쀀𝔙pf;쀀𝕍cr;쀀𝒱dash;抪ʀcefosᒧᒬᒱᒶᒼirc;䅴dge;拀r;쀀𝔚pf;쀀𝕎cr;쀀𝒲Ȁfiosᓋᓐᓒᓘr;쀀𝔛;䎞pf;쀀𝕏cr;쀀𝒳ҀAIUacfosuᓱᓵᓹᓽᔄᔏᔔᔚᔠcy;䐯cy;䐇cy;䐮cute耻Ý䃝Āiyᔉᔍrc;䅶;䐫r;쀀𝔜pf;쀀𝕐cr;쀀𝒴ml;䅸ЀHacdefosᔵᔹᔿᕋᕏᕝᕠᕤcy;䐖cute;䅹Āayᕄᕉron;䅽;䐗ot;䅻Dzᕔ\0ᕛoWidtè૙a;䎖r;愨pf;愤cr;쀀𝒵௡ᖃᖊᖐ\0ᖰᖶᖿ\0\0\0\0ᗆᗛᗫᙟ᙭\0ᚕ᚛ᚲᚹ\0ᚾcute耻á䃡reve;䄃̀;Ediuyᖜᖝᖡᖣᖨᖭ戾;쀀∾̳;房rc耻â䃢te肻´̆;䐰lig耻æ䃦Ā;r²ᖺ;쀀𝔞rave耻à䃠ĀepᗊᗖĀfpᗏᗔsym;愵èᗓha;䎱ĀapᗟcĀclᗤᗧr;䄁g;樿ɤᗰ\0\0ᘊʀ;adsvᗺᗻᗿᘁᘇ戧nd;橕;橜lope;橘;橚΀;elmrszᘘᘙᘛᘞᘿᙏᙙ戠;榤e»ᘙsdĀ;aᘥᘦ戡ѡᘰᘲᘴᘶᘸᘺᘼᘾ;榨;榩;榪;榫;榬;榭;榮;榯tĀ;vᙅᙆ戟bĀ;dᙌᙍ抾;榝Āptᙔᙗh;戢»¹arr;捼Āgpᙣᙧon;䄅f;쀀𝕒΀;Eaeiop዁ᙻᙽᚂᚄᚇᚊ;橰cir;橯;扊d;手s;䀧roxĀ;e዁ᚒñᚃing耻å䃥ƀctyᚡᚦᚨr;쀀𝒶;䀪mpĀ;e዁ᚯñʈilde耻ã䃣ml耻ä䃤Āciᛂᛈoninôɲnt;樑ࠀNabcdefiklnoprsu᛭ᛱᜰ᜼ᝃᝈ᝸᝽០៦ᠹᡐᜍ᤽᥈ᥰot;櫭Ācrᛶ᜞kȀcepsᜀᜅᜍᜓong;扌psilon;䏶rime;怵imĀ;e᜚᜛戽q;拍Ŷᜢᜦee;抽edĀ;gᜬᜭ挅e»ᜭrkĀ;t፜᜷brk;掶Āoyᜁᝁ;䐱quo;怞ʀcmprtᝓ᝛ᝡᝤᝨausĀ;eĊĉptyv;榰séᜌnoõēƀahwᝯ᝱ᝳ;䎲;愶een;扬r;쀀𝔟g΀costuvwឍឝឳេ៕៛៞ƀaiuបពរðݠrc;旯p»፱ƀdptឤឨឭot;樀lus;樁imes;樂ɱឹ\0\0ើcup;樆ar;昅riangleĀdu៍្own;施p;斳plus;樄eåᑄåᒭarow;植ƀako៭ᠦᠵĀcn៲ᠣkƀlst៺֫᠂ozenge;槫riangleȀ;dlr᠒᠓᠘᠝斴own;斾eft;旂ight;斸k;搣Ʊᠫ\0ᠳƲᠯ\0ᠱ;斒;斑4;斓ck;斈ĀeoᠾᡍĀ;qᡃᡆ쀀=⃥uiv;쀀≡⃥t;挐Ȁptwxᡙᡞᡧᡬf;쀀𝕓Ā;tᏋᡣom»Ꮜtie;拈؀DHUVbdhmptuvᢅᢖᢪᢻᣗᣛᣬ᣿ᤅᤊᤐᤡȀLRlrᢎᢐᢒᢔ;敗;敔;敖;敓ʀ;DUduᢡᢢᢤᢦᢨ敐;敦;敩;敤;敧ȀLRlrᢳᢵᢷᢹ;敝;敚;敜;教΀;HLRhlrᣊᣋᣍᣏᣑᣓᣕ救;敬;散;敠;敫;敢;敟ox;槉ȀLRlrᣤᣦᣨᣪ;敕;敒;攐;攌ʀ;DUduڽ᣷᣹᣻᣽;敥;敨;攬;攴inus;抟lus;択imes;抠ȀLRlrᤙᤛᤝ᤟;敛;敘;攘;攔΀;HLRhlrᤰᤱᤳᤵᤷ᤻᤹攂;敪;敡;敞;攼;攤;攜Āevģ᥂bar耻¦䂦Ȁceioᥑᥖᥚᥠr;쀀𝒷mi;恏mĀ;e᜚᜜lƀ;bhᥨᥩᥫ䁜;槅sub;柈Ŭᥴ᥾lĀ;e᥹᥺怢t»᥺pƀ;Eeįᦅᦇ;檮Ā;qۜۛೡᦧ\0᧨ᨑᨕᨲ\0ᨷᩐ\0\0᪴\0\0᫁\0\0ᬡᬮ᭍᭒\0᯽\0ᰌƀcpr᦭ᦲ᧝ute;䄇̀;abcdsᦿᧀᧄ᧊᧕᧙戩nd;橄rcup;橉Āau᧏᧒p;橋p;橇ot;橀;쀀∩︀Āeo᧢᧥t;恁îړȀaeiu᧰᧻ᨁᨅǰ᧵\0᧸s;橍on;䄍dil耻ç䃧rc;䄉psĀ;sᨌᨍ橌m;橐ot;䄋ƀdmnᨛᨠᨦil肻¸ƭptyv;榲t脀¢;eᨭᨮ䂢räƲr;쀀𝔠ƀceiᨽᩀᩍy;䑇ckĀ;mᩇᩈ朓ark»ᩈ;䏇r΀;Ecefms᩟᩠ᩢᩫ᪤᪪᪮旋;槃ƀ;elᩩᩪᩭ䋆q;扗eɡᩴ\0\0᪈rrowĀlr᩼᪁eft;憺ight;憻ʀRSacd᪒᪔᪖᪚᪟»ཇ;擈st;抛irc;抚ash;抝nint;樐id;櫯cir;槂ubsĀ;u᪻᪼晣it»᪼ˬ᫇᫔᫺\0ᬊonĀ;eᫍᫎ䀺Ā;qÇÆɭ᫙\0\0᫢aĀ;t᫞᫟䀬;䁀ƀ;fl᫨᫩᫫戁îᅠeĀmx᫱᫶ent»᫩eóɍǧ᫾\0ᬇĀ;dኻᬂot;橭nôɆƀfryᬐᬔᬗ;쀀𝕔oäɔ脀©;sŕᬝr;愗Āaoᬥᬩrr;憵ss;朗Ācuᬲᬷr;쀀𝒸Ābpᬼ᭄Ā;eᭁᭂ櫏;櫑Ā;eᭉᭊ櫐;櫒dot;拯΀delprvw᭠᭬᭷ᮂᮬᯔ᯹arrĀlr᭨᭪;椸;椵ɰ᭲\0\0᭵r;拞c;拟arrĀ;p᭿ᮀ憶;椽̀;bcdosᮏᮐᮖᮡᮥᮨ截rcap;橈Āauᮛᮞp;橆p;橊ot;抍r;橅;쀀∪︀Ȁalrv᮵ᮿᯞᯣrrĀ;mᮼᮽ憷;椼yƀevwᯇᯔᯘqɰᯎ\0\0ᯒreã᭳uã᭵ee;拎edge;拏en耻¤䂤earrowĀlrᯮ᯳eft»ᮀight»ᮽeäᯝĀciᰁᰇoninôǷnt;戱lcty;挭ঀAHabcdefhijlorstuwz᰸᰻᰿ᱝᱩᱵᲊᲞᲬᲷ᳻᳿ᴍᵻᶑᶫᶻ᷆᷍rò΁ar;楥Ȁglrs᱈ᱍ᱒᱔ger;怠eth;愸òᄳhĀ;vᱚᱛ怐»ऊūᱡᱧarow;椏aã̕Āayᱮᱳron;䄏;䐴ƀ;ao̲ᱼᲄĀgrʿᲁr;懊tseq;橷ƀglmᲑᲔᲘ耻°䂰ta;䎴ptyv;榱ĀirᲣᲨsht;楿;쀀𝔡arĀlrᲳᲵ»ࣜ»သʀaegsv᳂͸᳖᳜᳠mƀ;oș᳊᳔ndĀ;ș᳑uit;晦amma;䏝in;拲ƀ;io᳧᳨᳸䃷de脀÷;o᳧ᳰntimes;拇nø᳷cy;䑒cɯᴆ\0\0ᴊrn;挞op;挍ʀlptuwᴘᴝᴢᵉᵕlar;䀤f;쀀𝕕ʀ;emps̋ᴭᴷᴽᵂqĀ;d͒ᴳot;扑inus;戸lus;戔quare;抡blebarwedgåúnƀadhᄮᵝᵧownarrowóᲃarpoonĀlrᵲᵶefôᲴighôᲶŢᵿᶅkaro÷གɯᶊ\0\0ᶎrn;挟op;挌ƀcotᶘᶣᶦĀryᶝᶡ;쀀𝒹;䑕l;槶rok;䄑Ādrᶰᶴot;拱iĀ;fᶺ᠖斿Āah᷀᷃ròЩaòྦangle;榦Āci᷒ᷕy;䑟grarr;柿ऀDacdefglmnopqrstuxḁḉḙḸոḼṉṡṾấắẽỡἪἷὄ὎὚ĀDoḆᴴoôᲉĀcsḎḔute耻é䃩ter;橮ȀaioyḢḧḱḶron;䄛rĀ;cḭḮ扖耻ê䃪lon;払;䑍ot;䄗ĀDrṁṅot;扒;쀀𝔢ƀ;rsṐṑṗ檚ave耻è䃨Ā;dṜṝ檖ot;檘Ȁ;ilsṪṫṲṴ檙nters;揧;愓Ā;dṹṺ檕ot;檗ƀapsẅẉẗcr;䄓tyƀ;svẒẓẕ戅et»ẓpĀ1;ẝẤijạả;怄;怅怃ĀgsẪẬ;䅋p;怂ĀgpẴẸon;䄙f;쀀𝕖ƀalsỄỎỒrĀ;sỊị拕l;槣us;橱iƀ;lvỚớở䎵on»ớ;䏵ȀcsuvỪỳἋἣĀioữḱrc»Ḯɩỹ\0\0ỻíՈantĀglἂἆtr»ṝess»Ṻƀaeiἒ἖Ἒls;䀽st;扟vĀ;DȵἠD;橸parsl;槥ĀDaἯἳot;打rr;楱ƀcdiἾὁỸr;愯oô͒ĀahὉὋ;䎷耻ð䃰Āmrὓὗl耻ë䃫o;悬ƀcipὡὤὧl;䀡sôծĀeoὬὴctatioîՙnentialåչৡᾒ\0ᾞ\0ᾡᾧ\0\0ῆῌ\0ΐ\0ῦῪ \0 ⁚llingdotseñṄy;䑄male;晀ƀilrᾭᾳ῁lig;耀ffiɩᾹ\0\0᾽g;耀ffig;耀ffl;쀀𝔣lig;耀filig;쀀fjƀaltῙ῜ῡt;晭ig;耀flns;斱of;䆒ǰ΅\0ῳf;쀀𝕗ĀakֿῷĀ;vῼ´拔;櫙artint;樍Āao‌⁕Ācs‑⁒ႉ‸⁅⁈\0⁐β•‥‧‪‬\0‮耻½䂽;慓耻¼䂼;慕;慙;慛Ƴ‴\0‶;慔;慖ʴ‾⁁\0\0⁃耻¾䂾;慗;慜5;慘ƶ⁌\0⁎;慚;慝8;慞l;恄wn;挢cr;쀀𝒻ࢀEabcdefgijlnorstv₂₉₟₥₰₴⃰⃵⃺⃿℃ℒℸ̗ℾ⅒↞Ā;lٍ₇;檌ƀcmpₐₕ₝ute;䇵maĀ;dₜ᳚䎳;檆reve;䄟Āiy₪₮rc;䄝;䐳ot;䄡Ȁ;lqsؾق₽⃉ƀ;qsؾٌ⃄lanô٥Ȁ;cdl٥⃒⃥⃕c;檩otĀ;o⃜⃝檀Ā;l⃢⃣檂;檄Ā;e⃪⃭쀀⋛︀s;檔r;쀀𝔤Ā;gٳ؛mel;愷cy;䑓Ȁ;Eajٚℌℎℐ;檒;檥;檤ȀEaesℛℝ℩ℴ;扩pĀ;p℣ℤ檊rox»ℤĀ;q℮ℯ檈Ā;q℮ℛim;拧pf;쀀𝕘Āci⅃ⅆr;愊mƀ;el٫ⅎ⅐;檎;檐茀>;cdlqr׮ⅠⅪⅮⅳⅹĀciⅥⅧ;檧r;橺ot;拗Par;榕uest;橼ʀadelsↄⅪ←ٖ↛ǰ↉\0↎proø₞r;楸qĀlqؿ↖lesó₈ií٫Āen↣↭rtneqq;쀀≩︀Å↪ԀAabcefkosy⇄⇇⇱⇵⇺∘∝∯≨≽ròΠȀilmr⇐⇔⇗⇛rsðᒄf»․ilôکĀdr⇠⇤cy;䑊ƀ;cwࣴ⇫⇯ir;楈;憭ar;意irc;䄥ƀalr∁∎∓rtsĀ;u∉∊晥it»∊lip;怦con;抹r;쀀𝔥sĀew∣∩arow;椥arow;椦ʀamopr∺∾≃≞≣rr;懿tht;戻kĀlr≉≓eftarrow;憩ightarrow;憪f;쀀𝕙bar;怕ƀclt≯≴≸r;쀀𝒽asè⇴rok;䄧Ābp⊂⊇ull;恃hen»ᱛૡ⊣\0⊪\0⊸⋅⋎\0⋕⋳\0\0⋸⌢⍧⍢⍿\0⎆⎪⎴cute耻í䃭ƀ;iyݱ⊰⊵rc耻î䃮;䐸Ācx⊼⊿y;䐵cl耻¡䂡ĀfrΟ⋉;쀀𝔦rave耻ì䃬Ȁ;inoܾ⋝⋩⋮Āin⋢⋦nt;樌t;戭fin;槜ta;愩lig;䄳ƀaop⋾⌚⌝ƀcgt⌅⌈⌗r;䄫ƀelpܟ⌏⌓inåގarôܠh;䄱f;抷ed;䆵ʀ;cfotӴ⌬⌱⌽⍁are;愅inĀ;t⌸⌹戞ie;槝doô⌙ʀ;celpݗ⍌⍐⍛⍡al;抺Āgr⍕⍙eróᕣã⍍arhk;樗rod;樼Ȁcgpt⍯⍲⍶⍻y;䑑on;䄯f;쀀𝕚a;䎹uest耻¿䂿Āci⎊⎏r;쀀𝒾nʀ;EdsvӴ⎛⎝⎡ӳ;拹ot;拵Ā;v⎦⎧拴;拳Ā;iݷ⎮lde;䄩ǫ⎸\0⎼cy;䑖l耻ï䃯̀cfmosu⏌⏗⏜⏡⏧⏵Āiy⏑⏕rc;䄵;䐹r;쀀𝔧ath;䈷pf;쀀𝕛ǣ⏬\0⏱r;쀀𝒿rcy;䑘kcy;䑔Ѐacfghjos␋␖␢␧␭␱␵␻ppaĀ;v␓␔䎺;䏰Āey␛␠dil;䄷;䐺r;쀀𝔨reen;䄸cy;䑅cy;䑜pf;쀀𝕜cr;쀀𝓀஀ABEHabcdefghjlmnoprstuv⑰⒁⒆⒍⒑┎┽╚▀♎♞♥♹♽⚚⚲⛘❝❨➋⟀⠁⠒ƀart⑷⑺⑼rò৆òΕail;椛arr;椎Ā;gঔ⒋;檋ar;楢ॣ⒥\0⒪\0⒱\0\0\0\0\0⒵Ⓔ\0ⓆⓈⓍ\0⓹ute;䄺mptyv;榴raîࡌbda;䎻gƀ;dlࢎⓁⓃ;榑åࢎ;檅uo耻«䂫rЀ;bfhlpst࢙ⓞⓦⓩ⓫⓮⓱⓵Ā;f࢝ⓣs;椟s;椝ë≒p;憫l;椹im;楳l;憢ƀ;ae⓿─┄檫il;椙Ā;s┉┊檭;쀀⪭︀ƀabr┕┙┝rr;椌rk;杲Āak┢┬cĀek┨┪;䁻;䁛Āes┱┳;榋lĀdu┹┻;榏;榍Ȁaeuy╆╋╖╘ron;䄾Ādi═╔il;䄼ìࢰâ┩;䐻Ȁcqrs╣╦╭╽a;椶uoĀ;rนᝆĀdu╲╷har;楧shar;楋h;憲ʀ;fgqs▋▌উ◳◿扤tʀahlrt▘▤▷◂◨rrowĀ;t࢙□aé⓶arpoonĀdu▯▴own»њp»०eftarrows;懇ightƀahs◍◖◞rrowĀ;sࣴࢧarpoonó྘quigarro÷⇰hreetimes;拋ƀ;qs▋ও◺lanôবʀ;cdgsব☊☍☝☨c;檨otĀ;o☔☕橿Ā;r☚☛檁;檃Ā;e☢☥쀀⋚︀s;檓ʀadegs☳☹☽♉♋pproøⓆot;拖qĀgq♃♅ôউgtò⒌ôছiíলƀilr♕࣡♚sht;楼;쀀𝔩Ā;Eজ♣;檑š♩♶rĀdu▲♮Ā;l॥♳;楪lk;斄cy;䑙ʀ;achtੈ⚈⚋⚑⚖rò◁orneòᴈard;楫ri;旺Āio⚟⚤dot;䅀ustĀ;a⚬⚭掰che»⚭ȀEaes⚻⚽⛉⛔;扨pĀ;p⛃⛄檉rox»⛄Ā;q⛎⛏檇Ā;q⛎⚻im;拦Ѐabnoptwz⛩⛴⛷✚✯❁❇❐Ānr⛮⛱g;柬r;懽rëࣁgƀlmr⛿✍✔eftĀar০✇ightá৲apsto;柼ightá৽parrowĀlr✥✩efô⓭ight;憬ƀafl✶✹✽r;榅;쀀𝕝us;樭imes;樴š❋❏st;戗áፎƀ;ef❗❘᠀旊nge»❘arĀ;l❤❥䀨t;榓ʀachmt❳❶❼➅➇ròࢨorneòᶌarĀ;d྘➃;業;怎ri;抿̀achiqt➘➝ੀ➢➮➻quo;怹r;쀀𝓁mƀ;egল➪➬;檍;檏Ābu┪➳oĀ;rฟ➹;怚rok;䅂萀<;cdhilqrࠫ⟒☹⟜⟠⟥⟪⟰Āci⟗⟙;檦r;橹reå◲mes;拉arr;楶uest;橻ĀPi⟵⟹ar;榖ƀ;ef⠀भ᠛旃rĀdu⠇⠍shar;楊har;楦Āen⠗⠡rtneqq;쀀≨︀Å⠞܀Dacdefhilnopsu⡀⡅⢂⢎⢓⢠⢥⢨⣚⣢⣤ઃ⣳⤂Dot;戺Ȁclpr⡎⡒⡣⡽r耻¯䂯Āet⡗⡙;時Ā;e⡞⡟朠se»⡟Ā;sျ⡨toȀ;dluျ⡳⡷⡻owîҌefôएðᏑker;斮Āoy⢇⢌mma;権;䐼ash;怔asuredangle»ᘦr;쀀𝔪o;愧ƀcdn⢯⢴⣉ro耻µ䂵Ȁ;acdᑤ⢽⣀⣄sôᚧir;櫰ot肻·Ƶusƀ;bd⣒ᤃ⣓戒Ā;uᴼ⣘;横ţ⣞⣡p;櫛ò−ðઁĀdp⣩⣮els;抧f;쀀𝕞Āct⣸⣽r;쀀𝓂pos»ᖝƀ;lm⤉⤊⤍䎼timap;抸ఀGLRVabcdefghijlmoprstuvw⥂⥓⥾⦉⦘⧚⧩⨕⨚⩘⩝⪃⪕⪤⪨⬄⬇⭄⭿⮮ⰴⱧⱼ⳩Āgt⥇⥋;쀀⋙̸Ā;v⥐௏쀀≫⃒ƀelt⥚⥲⥶ftĀar⥡⥧rrow;懍ightarrow;懎;쀀⋘̸Ā;v⥻ే쀀≪⃒ightarrow;懏ĀDd⦎⦓ash;抯ash;抮ʀbcnpt⦣⦧⦬⦱⧌la»˞ute;䅄g;쀀∠⃒ʀ;Eiop඄⦼⧀⧅⧈;쀀⩰̸d;쀀≋̸s;䅉roø඄urĀ;a⧓⧔普lĀ;s⧓ସdz⧟\0⧣p肻 ଷmpĀ;e௹ఀʀaeouy⧴⧾⨃⨐⨓ǰ⧹\0⧻;橃on;䅈dil;䅆ngĀ;dൾ⨊ot;쀀⩭̸p;橂;䐽ash;怓΀;Aadqsxஒ⨩⨭⨻⩁⩅⩐rr;懗rĀhr⨳⨶k;椤Ā;oᏲᏰot;쀀≐̸uiöୣĀei⩊⩎ar;椨í஘istĀ;s஠டr;쀀𝔫ȀEest௅⩦⩹⩼ƀ;qs஼⩭௡ƀ;qs஼௅⩴lanô௢ií௪Ā;rஶ⪁»ஷƀAap⪊⪍⪑rò⥱rr;憮ar;櫲ƀ;svྍ⪜ྌĀ;d⪡⪢拼;拺cy;䑚΀AEadest⪷⪺⪾⫂⫅⫶⫹rò⥦;쀀≦̸rr;憚r;急Ȁ;fqs఻⫎⫣⫯tĀar⫔⫙rro÷⫁ightarro÷⪐ƀ;qs఻⪺⫪lanôౕĀ;sౕ⫴»శiíౝĀ;rవ⫾iĀ;eచథiäඐĀpt⬌⬑f;쀀𝕟膀¬;in⬙⬚⬶䂬nȀ;Edvஉ⬤⬨⬮;쀀⋹̸ot;쀀⋵̸ǡஉ⬳⬵;拷;拶iĀ;vಸ⬼ǡಸ⭁⭃;拾;拽ƀaor⭋⭣⭩rȀ;ast୻⭕⭚⭟lleì୻l;쀀⫽⃥;쀀∂̸lint;樔ƀ;ceಒ⭰⭳uåಥĀ;cಘ⭸Ā;eಒ⭽ñಘȀAait⮈⮋⮝⮧rò⦈rrƀ;cw⮔⮕⮙憛;쀀⤳̸;쀀↝̸ghtarrow»⮕riĀ;eೋೖ΀chimpqu⮽⯍⯙⬄୸⯤⯯Ȁ;cerല⯆ഷ⯉uå൅;쀀𝓃ortɭ⬅\0\0⯖ará⭖mĀ;e൮⯟Ā;q൴൳suĀbp⯫⯭å೸åഋƀbcp⯶ⰑⰙȀ;Ees⯿ⰀഢⰄ抄;쀀⫅̸etĀ;eഛⰋqĀ;qണⰀcĀ;eലⰗñസȀ;EesⰢⰣൟⰧ抅;쀀⫆̸etĀ;e൘ⰮqĀ;qൠⰣȀgilrⰽⰿⱅⱇìௗlde耻ñ䃱çృiangleĀlrⱒⱜeftĀ;eచⱚñదightĀ;eೋⱥñ೗Ā;mⱬⱭ䎽ƀ;esⱴⱵⱹ䀣ro;愖p;怇ҀDHadgilrsⲏⲔⲙⲞⲣⲰⲶⳓⳣash;抭arr;椄p;쀀≍⃒ash;抬ĀetⲨⲬ;쀀≥⃒;쀀>⃒nfin;槞ƀAetⲽⳁⳅrr;椂;쀀≤⃒Ā;rⳊⳍ쀀<⃒ie;쀀⊴⃒ĀAtⳘⳜrr;椃rie;쀀⊵⃒im;쀀∼⃒ƀAan⳰⳴ⴂrr;懖rĀhr⳺⳽k;椣Ā;oᏧᏥear;椧ቓ᪕\0\0\0\0\0\0\0\0\0\0\0\0\0ⴭ\0ⴸⵈⵠⵥ⵲ⶄᬇ\0\0ⶍⶫ\0ⷈⷎ\0ⷜ⸙⸫⸾⹃Ācsⴱ᪗ute耻ó䃳ĀiyⴼⵅrĀ;c᪞ⵂ耻ô䃴;䐾ʀabios᪠ⵒⵗLjⵚlac;䅑v;樸old;榼lig;䅓Ācr⵩⵭ir;榿;쀀𝔬ͯ⵹\0\0⵼\0ⶂn;䋛ave耻ò䃲;槁Ābmⶈ෴ar;榵Ȁacitⶕ⶘ⶥⶨrò᪀Āir⶝ⶠr;榾oss;榻nå๒;槀ƀaeiⶱⶵⶹcr;䅍ga;䏉ƀcdnⷀⷅǍron;䎿;榶pf;쀀𝕠ƀaelⷔ⷗ǒr;榷rp;榹΀;adiosvⷪⷫⷮ⸈⸍⸐⸖戨rò᪆Ȁ;efmⷷⷸ⸂⸅橝rĀ;oⷾⷿ愴f»ⷿ耻ª䂪耻º䂺gof;抶r;橖lope;橗;橛ƀclo⸟⸡⸧ò⸁ash耻ø䃸l;折iŬⸯ⸴de耻õ䃵esĀ;aǛ⸺s;樶ml耻ö䃶bar;挽ૡ⹞\0⹽\0⺀⺝\0⺢⺹\0\0⻋ຜ\0⼓\0\0⼫⾼\0⿈rȀ;astЃ⹧⹲຅脀¶;l⹭⹮䂶leìЃɩ⹸\0\0⹻m;櫳;櫽y;䐿rʀcimpt⺋⺏⺓ᡥ⺗nt;䀥od;䀮il;怰enk;怱r;쀀𝔭ƀimo⺨⺰⺴Ā;v⺭⺮䏆;䏕maô੶ne;明ƀ;tv⺿⻀⻈䏀chfork»´;䏖Āau⻏⻟nĀck⻕⻝kĀ;h⇴⻛;愎ö⇴sҀ;abcdemst⻳⻴ᤈ⻹⻽⼄⼆⼊⼎䀫cir;樣ir;樢Āouᵀ⼂;樥;橲n肻±ຝim;樦wo;樧ƀipu⼙⼠⼥ntint;樕f;쀀𝕡nd耻£䂣Ԁ;Eaceinosu່⼿⽁⽄⽇⾁⾉⾒⽾⾶;檳p;檷uå໙Ā;c໎⽌̀;acens່⽙⽟⽦⽨⽾pproø⽃urlyeñ໙ñ໎ƀaes⽯⽶⽺pprox;檹qq;檵im;拨iíໟmeĀ;s⾈ຮ怲ƀEas⽸⾐⽺ð⽵ƀdfp໬⾙⾯ƀals⾠⾥⾪lar;挮ine;挒urf;挓Ā;t໻⾴ï໻rel;抰Āci⿀⿅r;쀀𝓅;䏈ncsp;怈̀fiopsu⿚⋢⿟⿥⿫⿱r;쀀𝔮pf;쀀𝕢rime;恗cr;쀀𝓆ƀaeo⿸〉〓tĀei⿾々rnionóڰnt;樖stĀ;e【】䀿ñἙô༔઀ABHabcdefhilmnoprstux぀けさすムㄎㄫㅇㅢㅲㆎ㈆㈕㈤㈩㉘㉮㉲㊐㊰㊷ƀartぇおがròႳòϝail;検aròᱥar;楤΀cdenqrtとふへみわゔヌĀeuねぱ;쀀∽̱te;䅕iãᅮmptyv;榳gȀ;del࿑らるろ;榒;榥å࿑uo耻»䂻rր;abcfhlpstw࿜ガクシスゼゾダッデナp;極Ā;f࿠ゴs;椠;椳s;椞ë≝ð✮l;楅im;楴l;憣;憝Āaiパフil;椚oĀ;nホボ戶aló༞ƀabrョリヮrò៥rk;杳ĀakンヽcĀekヹ・;䁽;䁝Āes㄂㄄;榌lĀduㄊㄌ;榎;榐Ȁaeuyㄗㄜㄧㄩron;䅙Ādiㄡㄥil;䅗ì࿲âヺ;䑀Ȁclqsㄴㄷㄽㅄa;椷dhar;楩uoĀ;rȎȍh;憳ƀacgㅎㅟངlȀ;ipsླྀㅘㅛႜnåႻarôྩt;断ƀilrㅩဣㅮsht;楽;쀀𝔯ĀaoㅷㆆrĀduㅽㅿ»ѻĀ;l႑ㆄ;楬Ā;vㆋㆌ䏁;䏱ƀgns㆕ㇹㇼht̀ahlrstㆤㆰ㇂㇘㇤㇮rrowĀ;t࿜ㆭaéトarpoonĀduㆻㆿowîㅾp»႒eftĀah㇊㇐rrowó࿪arpoonóՑightarrows;應quigarro÷ニhreetimes;拌g;䋚ingdotseñἲƀahm㈍㈐㈓rò࿪aòՑ;怏oustĀ;a㈞㈟掱che»㈟mid;櫮Ȁabpt㈲㈽㉀㉒Ānr㈷㈺g;柭r;懾rëဃƀafl㉇㉊㉎r;榆;쀀𝕣us;樮imes;樵Āap㉝㉧rĀ;g㉣㉤䀩t;榔olint;樒arò㇣Ȁachq㉻㊀Ⴜ㊅quo;怺r;쀀𝓇Ābu・㊊oĀ;rȔȓƀhir㊗㊛㊠reåㇸmes;拊iȀ;efl㊪ၙᠡ㊫方tri;槎luhar;楨;愞ൡ㋕㋛㋟㌬㌸㍱\0㍺㎤\0\0㏬㏰\0㐨㑈㑚㒭㒱㓊㓱\0㘖\0\0㘳cute;䅛quï➺Ԁ;Eaceinpsyᇭ㋳㋵㋿㌂㌋㌏㌟㌦㌩;檴ǰ㋺\0㋼;檸on;䅡uåᇾĀ;dᇳ㌇il;䅟rc;䅝ƀEas㌖㌘㌛;檶p;檺im;择olint;樓iíሄ;䑁otƀ;be㌴ᵇ㌵担;橦΀Aacmstx㍆㍊㍗㍛㍞㍣㍭rr;懘rĀhr㍐㍒ë∨Ā;oਸ਼਴t耻§䂧i;䀻war;椩mĀin㍩ðnuóñt;朶rĀ;o㍶⁕쀀𝔰Ȁacoy㎂㎆㎑㎠rp;景Āhy㎋㎏cy;䑉;䑈rtɭ㎙\0\0㎜iäᑤaraì⹯耻­䂭Āgm㎨㎴maƀ;fv㎱㎲㎲䏃;䏂Ѐ;deglnprካ㏅㏉㏎㏖㏞㏡㏦ot;橪Ā;q኱ኰĀ;E㏓㏔檞;檠Ā;E㏛㏜檝;檟e;扆lus;樤arr;楲aròᄽȀaeit㏸㐈㐏㐗Āls㏽㐄lsetmé㍪hp;樳parsl;槤Ādlᑣ㐔e;挣Ā;e㐜㐝檪Ā;s㐢㐣檬;쀀⪬︀ƀflp㐮㐳㑂tcy;䑌Ā;b㐸㐹䀯Ā;a㐾㐿槄r;挿f;쀀𝕤aĀdr㑍ЂesĀ;u㑔㑕晠it»㑕ƀcsu㑠㑹㒟Āau㑥㑯pĀ;sᆈ㑫;쀀⊓︀pĀ;sᆴ㑵;쀀⊔︀uĀbp㑿㒏ƀ;esᆗᆜ㒆etĀ;eᆗ㒍ñᆝƀ;esᆨᆭ㒖etĀ;eᆨ㒝ñᆮƀ;afᅻ㒦ְrť㒫ֱ»ᅼaròᅈȀcemt㒹㒾㓂㓅r;쀀𝓈tmîñiì㐕aræᆾĀar㓎㓕rĀ;f㓔ឿ昆Āan㓚㓭ightĀep㓣㓪psiloîỠhé⺯s»⡒ʀbcmnp㓻㕞ሉ㖋㖎Ҁ;Edemnprs㔎㔏㔑㔕㔞㔣㔬㔱㔶抂;櫅ot;檽Ā;dᇚ㔚ot;櫃ult;櫁ĀEe㔨㔪;櫋;把lus;檿arr;楹ƀeiu㔽㕒㕕tƀ;en㔎㕅㕋qĀ;qᇚ㔏eqĀ;q㔫㔨m;櫇Ābp㕚㕜;櫕;櫓c̀;acensᇭ㕬㕲㕹㕻㌦pproø㋺urlyeñᇾñᇳƀaes㖂㖈㌛pproø㌚qñ㌗g;晪ڀ123;Edehlmnps㖩㖬㖯ሜ㖲㖴㗀㗉㗕㗚㗟㗨㗭耻¹䂹耻²䂲耻³䂳;櫆Āos㖹㖼t;檾ub;櫘Ā;dሢ㗅ot;櫄sĀou㗏㗒l;柉b;櫗arr;楻ult;櫂ĀEe㗤㗦;櫌;抋lus;櫀ƀeiu㗴㘉㘌tƀ;enሜ㗼㘂qĀ;qሢ㖲eqĀ;q㗧㗤m;櫈Ābp㘑㘓;櫔;櫖ƀAan㘜㘠㘭rr;懙rĀhr㘦㘨ë∮Ā;oਫ਩war;椪lig耻ß䃟௡㙑㙝㙠ዎ㙳㙹\0㙾㛂\0\0\0\0\0㛛㜃\0㜉㝬\0\0\0㞇ɲ㙖\0\0㙛get;挖;䏄rë๟ƀaey㙦㙫㙰ron;䅥dil;䅣;䑂lrec;挕r;쀀𝔱Ȁeiko㚆㚝㚵㚼Dz㚋\0㚑eĀ4fኄኁaƀ;sv㚘㚙㚛䎸ym;䏑Ācn㚢㚲kĀas㚨㚮pproø዁im»ኬsðኞĀas㚺㚮ð዁rn耻þ䃾Ǭ̟㛆⋧es膀×;bd㛏㛐㛘䃗Ā;aᤏ㛕r;樱;樰ƀeps㛡㛣㜀á⩍Ȁ;bcf҆㛬㛰㛴ot;挶ir;櫱Ā;o㛹㛼쀀𝕥rk;櫚á㍢rime;怴ƀaip㜏㜒㝤dåቈ΀adempst㜡㝍㝀㝑㝗㝜㝟ngleʀ;dlqr㜰㜱㜶㝀㝂斵own»ᶻeftĀ;e⠀㜾ñम;扜ightĀ;e㊪㝋ñၚot;旬inus;樺lus;樹b;槍ime;樻ezium;揢ƀcht㝲㝽㞁Āry㝷㝻;쀀𝓉;䑆cy;䑛rok;䅧Āio㞋㞎xô᝷headĀlr㞗㞠eftarro÷ࡏightarrow»ཝऀAHabcdfghlmoprstuw㟐㟓㟗㟤㟰㟼㠎㠜㠣㠴㡑㡝㡫㢩㣌㣒㣪㣶ròϭar;楣Ācr㟜㟢ute耻ú䃺òᅐrǣ㟪\0㟭y;䑞ve;䅭Āiy㟵㟺rc耻û䃻;䑃ƀabh㠃㠆㠋ròᎭlac;䅱aòᏃĀir㠓㠘sht;楾;쀀𝔲rave耻ù䃹š㠧㠱rĀlr㠬㠮»ॗ»ႃlk;斀Āct㠹㡍ɯ㠿\0\0㡊rnĀ;e㡅㡆挜r»㡆op;挏ri;旸Āal㡖㡚cr;䅫肻¨͉Āgp㡢㡦on;䅳f;쀀𝕦̀adhlsuᅋ㡸㡽፲㢑㢠ownáᎳarpoonĀlr㢈㢌efô㠭ighô㠯iƀ;hl㢙㢚㢜䏅»ᏺon»㢚parrows;懈ƀcit㢰㣄㣈ɯ㢶\0\0㣁rnĀ;e㢼㢽挝r»㢽op;挎ng;䅯ri;旹cr;쀀𝓊ƀdir㣙㣝㣢ot;拰lde;䅩iĀ;f㜰㣨»᠓Āam㣯㣲rò㢨l耻ü䃼angle;榧ހABDacdeflnoprsz㤜㤟㤩㤭㦵㦸㦽㧟㧤㧨㧳㧹㧽㨁㨠ròϷarĀ;v㤦㤧櫨;櫩asèϡĀnr㤲㤷grt;榜΀eknprst㓣㥆㥋㥒㥝㥤㦖appá␕othinçẖƀhir㓫⻈㥙opô⾵Ā;hᎷ㥢ïㆍĀiu㥩㥭gmá㎳Ābp㥲㦄setneqĀ;q㥽㦀쀀⊊︀;쀀⫋︀setneqĀ;q㦏㦒쀀⊋︀;쀀⫌︀Āhr㦛㦟etá㚜iangleĀlr㦪㦯eft»थight»ၑy;䐲ash»ံƀelr㧄㧒㧗ƀ;beⷪ㧋㧏ar;抻q;扚lip;拮Ābt㧜ᑨaòᑩr;쀀𝔳tré㦮suĀbp㧯㧱»ജ»൙pf;쀀𝕧roð໻tré㦴Ācu㨆㨋r;쀀𝓋Ābp㨐㨘nĀEe㦀㨖»㥾nĀEe㦒㨞»㦐igzag;榚΀cefoprs㨶㨻㩖㩛㩔㩡㩪irc;䅵Ādi㩀㩑Ābg㩅㩉ar;機eĀ;qᗺ㩏;扙erp;愘r;쀀𝔴pf;쀀𝕨Ā;eᑹ㩦atèᑹcr;쀀𝓌ૣណ㪇\0㪋\0㪐㪛\0\0㪝㪨㪫㪯\0\0㫃㫎\0㫘ៜ៟tré៑r;쀀𝔵ĀAa㪔㪗ròσrò৶;䎾ĀAa㪡㪤ròθrò৫að✓is;拻ƀdptឤ㪵㪾Āfl㪺ឩ;쀀𝕩imåឲĀAa㫇㫊ròώròਁĀcq㫒ីr;쀀𝓍Āpt៖㫜ré។Ѐacefiosu㫰㫽㬈㬌㬑㬕㬛㬡cĀuy㫶㫻te耻ý䃽;䑏Āiy㬂㬆rc;䅷;䑋n耻¥䂥r;쀀𝔶cy;䑗pf;쀀𝕪cr;쀀𝓎Ācm㬦㬩y;䑎l耻ÿ䃿Ԁacdefhiosw㭂㭈㭔㭘㭤㭩㭭㭴㭺㮀cute;䅺Āay㭍㭒ron;䅾;䐷ot;䅼Āet㭝㭡træᕟa;䎶r;쀀𝔷cy;䐶grarr;懝pf;쀀𝕫cr;쀀𝓏Ājn㮅㮇;怍j;怌'.split("").map(e=>e.charCodeAt(0))),Pn=new Uint16Array("Ȁaglq \x1Bɭ\0\0p;䀦os;䀧t;䀾t;䀼uot;䀢".split("").map(e=>e.charCodeAt(0)));var Ke;const qn=new Map([[0,65533],[128,8364],[130,8218],[131,402],[132,8222],[133,8230],[134,8224],[135,8225],[136,710],[137,8240],[138,352],[139,8249],[140,338],[142,381],[145,8216],[146,8217],[147,8220],[148,8221],[149,8226],[150,8211],[151,8212],[152,732],[153,8482],[154,353],[155,8250],[156,339],[158,382],[159,376]]),Un=(Ke=String.fromCodePoint)!==null&&Ke!==void 0?Ke:function(e){let u="";return e>65535&&(e-=65536,u+=String.fromCharCode(e>>>10&1023|55296),e=56320|e&1023),u+=String.fromCharCode(e),u};function jn(e){var u;return e>=55296&&e<=57343||e>1114111?65533:(u=qn.get(e))!==null&&u!==void 0?u:e}var L;(function(e){e[e.NUM=35]="NUM",e[e.SEMI=59]="SEMI",e[e.EQUALS=61]="EQUALS",e[e.ZERO=48]="ZERO",e[e.NINE=57]="NINE",e[e.LOWER_A=97]="LOWER_A",e[e.LOWER_F=102]="LOWER_F",e[e.LOWER_X=120]="LOWER_X",e[e.LOWER_Z=122]="LOWER_Z",e[e.UPPER_A=65]="UPPER_A",e[e.UPPER_F=70]="UPPER_F",e[e.UPPER_Z=90]="UPPER_Z"})(L||(L={}));const Hn=32;var re;(function(e){e[e.VALUE_LENGTH=49152]="VALUE_LENGTH",e[e.BRANCH_LENGTH=16256]="BRANCH_LENGTH",e[e.JUMP_TABLE=127]="JUMP_TABLE"})(re||(re={}));function nu(e){return e>=L.ZERO&&e<=L.NINE}function Zn(e){return e>=L.UPPER_A&&e<=L.UPPER_F||e>=L.LOWER_A&&e<=L.LOWER_F}function Gn(e){return e>=L.UPPER_A&&e<=L.UPPER_Z||e>=L.LOWER_A&&e<=L.LOWER_Z||nu(e)}function Vn(e){return e===L.EQUALS||Gn(e)}var B;(function(e){e[e.EntityStart=0]="EntityStart",e[e.NumericStart=1]="NumericStart",e[e.NumericDecimal=2]="NumericDecimal",e[e.NumericHex=3]="NumericHex",e[e.NamedEntity=4]="NamedEntity"})(B||(B={}));var ee;(function(e){e[e.Legacy=0]="Legacy",e[e.Strict=1]="Strict",e[e.Attribute=2]="Attribute"})(ee||(ee={}));class Wn{constructor(u,t,n){this.decodeTree=u,this.emitCodePoint=t,this.errors=n,this.state=B.EntityStart,this.consumed=1,this.result=0,this.treeIndex=0,this.excess=1,this.decodeMode=ee.Strict}startEntity(u){this.decodeMode=u,this.state=B.EntityStart,this.result=0,this.treeIndex=0,this.excess=1,this.consumed=1}write(u,t){switch(this.state){case B.EntityStart:return u.charCodeAt(t)===L.NUM?(this.state=B.NumericStart,this.consumed+=1,this.stateNumericStart(u,t+1)):(this.state=B.NamedEntity,this.stateNamedEntity(u,t));case B.NumericStart:return this.stateNumericStart(u,t);case B.NumericDecimal:return this.stateNumericDecimal(u,t);case B.NumericHex:return this.stateNumericHex(u,t);case B.NamedEntity:return this.stateNamedEntity(u,t)}}stateNumericStart(u,t){return t>=u.length?-1:(u.charCodeAt(t)|Hn)===L.LOWER_X?(this.state=B.NumericHex,this.consumed+=1,this.stateNumericHex(u,t+1)):(this.state=B.NumericDecimal,this.stateNumericDecimal(u,t))}addToNumericResult(u,t,n,r){if(t!==n){const o=n-t;this.result=this.result*Math.pow(r,o)+parseInt(u.substr(t,o),r),this.consumed+=o}}stateNumericHex(u,t){const n=t;for(;t>14;for(;t>14,o!==0){if(a===L.SEMI)return this.emitNamedEntityData(this.treeIndex,o,this.consumed+this.excess);this.decodeMode!==ee.Strict&&(this.result=this.treeIndex,this.consumed+=this.excess,this.excess=0)}}return-1}emitNotTerminatedNamedEntity(){var u;const{result:t,decodeTree:n}=this,r=(n[t]&re.VALUE_LENGTH)>>14;return this.emitNamedEntityData(t,r,this.consumed),(u=this.errors)===null||u===void 0||u.missingSemicolonAfterCharacterReference(),this.consumed}emitNamedEntityData(u,t,n){const{decodeTree:r}=this;return this.emitCodePoint(t===1?r[u]&~re.VALUE_LENGTH:r[u+1],n),t===3&&this.emitCodePoint(r[u+2],n),n}end(){var u;switch(this.state){case B.NamedEntity:return this.result!==0&&(this.decodeMode!==ee.Attribute||this.result===this.treeIndex)?this.emitNotTerminatedNamedEntity():0;case B.NumericDecimal:return this.emitNumericEntity(0,2);case B.NumericHex:return this.emitNumericEntity(0,3);case B.NumericStart:return(u=this.errors)===null||u===void 0||u.absenceOfDigitsInNumericCharacterReference(this.consumed),0;case B.EntityStart:return 0}}}function rt(e){let u="";const t=new Wn(e,n=>u+=Un(n));return function(r,o){let a=0,i=0;for(;(i=r.indexOf("&",i))>=0;){u+=r.slice(a,i),t.startEntity(o);const c=t.write(r,i+1);if(c<0){a=i+t.end();break}a=i+c,i=c===0?a+1:a}const s=u+r.slice(a);return u="",s}}function Kn(e,u,t,n){const r=(u&re.BRANCH_LENGTH)>>7,o=u&re.JUMP_TABLE;if(r===0)return o!==0&&n===o?t:-1;if(o){const s=n-o;return s<0||s>=r?-1:e[t+s]-1}let a=t,i=a+r-1;for(;a<=i;){const s=a+i>>>1,c=e[s];if(cn)i=s-1;else return e[s+r]}return-1}const ot=rt($n);rt(Pn);function Jn(e,u=ee.Legacy){return ot(e,u)}function Qn(e){return ot(e,ee.Strict)}function Xn(e){return Object.prototype.toString.call(e)}function du(e){return Xn(e)==="[object String]"}const Yn=Object.prototype.hasOwnProperty;function e0(e,u){return Yn.call(e,u)}function Pe(e){return Array.prototype.slice.call(arguments,1).forEach(function(t){if(t){if(typeof t!="object")throw new TypeError(t+"must be object");Object.keys(t).forEach(function(n){e[n]=t[n]})}}),e}function it(e,u,t){return[].concat(e.slice(0,u),t,e.slice(u+1))}function fu(e){return!(e>=55296&&e<=57343||e>=64976&&e<=65007||(e&65535)===65535||(e&65535)===65534||e>=0&&e<=8||e===11||e>=14&&e<=31||e>=127&&e<=159||e>1114111)}function ve(e){if(e>65535){e-=65536;const u=55296+(e>>10),t=56320+(e&1023);return String.fromCharCode(u,t)}return String.fromCharCode(e)}const at=/\\([!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~])/g,u0=/&([a-z#][a-z0-9]{1,31});/gi,t0=new RegExp(at.source+"|"+u0.source,"gi"),n0=/^#((?:x[a-f0-9]{1,8}|[0-9]{1,8}))$/i;function r0(e,u){if(u.charCodeAt(0)===35&&n0.test(u)){const n=u[1].toLowerCase()==="x"?parseInt(u.slice(2),16):parseInt(u.slice(1),10);return fu(n)?ve(n):e}const t=Jn(e);return t!==e?t:e}function o0(e){return e.indexOf("\\")<0?e:e.replace(at,"$1")}function he(e){return e.indexOf("\\")<0&&e.indexOf("&")<0?e:e.replace(t0,function(u,t,n){return t||r0(u,n)})}const i0=/[&<>"]/,a0=/[&<>"]/g,s0={"&":"&","<":"<",">":">",'"':"""};function c0(e){return s0[e]}function ie(e){return i0.test(e)?e.replace(a0,c0):e}const l0=/[.?*+^$[\]\\(){}|-]/g;function d0(e){return e.replace(l0,"\\$&")}function M(e){switch(e){case 9:case 32:return!0}return!1}function Ee(e){if(e>=8192&&e<=8202)return!0;switch(e){case 9:case 10:case 11:case 12:case 13:case 32:case 160:case 5760:case 8239:case 8287:case 12288:return!0}return!1}function st(e){return lu.test(e)||tt.test(e)}function Ce(e){return st(ve(e))}function De(e){switch(e){case 33:case 34:case 35:case 36:case 37:case 38:case 39:case 40:case 41:case 42:case 43:case 44:case 45:case 46:case 47:case 58:case 59:case 60:case 61:case 62:case 63:case 64:case 91:case 92:case 93:case 94:case 95:case 96:case 123:case 124:case 125:case 126:return!0;default:return!1}}function qe(e){return e=e.trim().replace(/\s+/g," "),"ẞ".toLowerCase()==="Ṿ"&&(e=e.replace(/ẞ/g,"ß")),e.toLowerCase().toUpperCase()}function wu(e){return e===32||e===9||e===10||e===13}function Ue(e){let u=0;for(;u=u&&wu(e.charCodeAt(t));t--);return e.slice(u,t+1)}const f0={mdurl:Bn,ucmicro:On},b0=Object.freeze(Object.defineProperty({__proto__:null,arrayReplaceAt:it,asciiTrim:Ue,assign:Pe,escapeHtml:ie,escapeRE:d0,fromCodePoint:ve,has:e0,isMdAsciiPunct:De,isPunctChar:st,isPunctCharCode:Ce,isSpace:M,isString:du,isValidEntityCode:fu,isWhiteSpace:Ee,lib:f0,normalizeReference:qe,unescapeAll:he,unescapeMd:o0},Symbol.toStringTag,{value:"Module"}));function p0(e,u,t){let n,r,o,a;const i=e.posMax,s=e.pos;for(e.pos=u+1,n=1;e.pos32))return o;if(n===41){if(a===0)break;a--}r++}return u===r||a!==0||(o.str=he(e.slice(u,r)),o.pos=r,o.ok=!0),o}function m0(e,u,t,n){let r,o=u;const a={ok:!1,can_continue:!1,pos:0,str:"",marker:0};if(n)a.str=n.str,a.marker=n.marker;else{if(o>=t)return a;let i=e.charCodeAt(o);if(i!==34&&i!==39&&i!==40)return a;u++,o++,i===40&&(i=41),a.marker=i}for(;o"+ie(o.content)+""};X.code_block=function(e,u,t,n,r){const o=e[u];return""+ie(e[u].content)+` `};X.fence=function(e,u,t,n,r){const o=e[u],a=o.info?he(o.info).trim():"";let i="",s="";if(a){const d=a.split(/(\s+)/g);i=d[0],s=d.slice(2).join("")}let c;if(t.highlight?c=t.highlight(o.content,i,s)||ie(o.content):c=ie(o.content),c.indexOf("${c} diff --git a/backend/internal/server/ui_dist/assets/Activity-ELKGWazx.js b/backend/internal/server/ui_dist/assets/Activity-CUYXvjyS.js similarity index 97% rename from backend/internal/server/ui_dist/assets/Activity-ELKGWazx.js rename to backend/internal/server/ui_dist/assets/Activity-CUYXvjyS.js index 0ca00f4..17bf349 100644 --- a/backend/internal/server/ui_dist/assets/Activity-ELKGWazx.js +++ b/backend/internal/server/ui_dist/assets/Activity-CUYXvjyS.js @@ -1 +1 @@ -import{c as ue,j as o,a as l,t as r,f as c,s as ie,q as _,U as de,o as ce,y as ve,R as xe,V as me,b as t,d as f,S as fe,e as pe,v as be,F as k,p as w,g as d,h,_ as C,r as p,aB as he,k as S,n as q}from"./index-D5cO6vit.js";import{E as v}from"./format-CsU4_SPu.js";import{D as _e}from"./Drawer-B_L-gxK5.js";import{_ as N}from"./StatusBadge-DoungZTd.js";import{C as ge}from"./chevron-right-CC7vhgfo.js";import"./circle-BHWwGwr8.js";import"./clock-ARFGKas-.js";const ye=ue("chevron-left",[["path",{d:"m15 18-6-6 6-6",key:"1wnfg3"}]]),F={__name:"SourceTag",props:{source:{type:String,default:""}},setup(A){const P=A,L=_(()=>{switch(P.source){case"web":return"text-indigo-300 border-indigo-900/40";case"api":return"text-sky-300 border-sky-900/40";case"mcp":return"text-violet-300 border-violet-900/40";case"sdk":return"text-teal-300 border-teal-900/40";case"webhook":return"text-amber-300 border-amber-900/40";case"cron":return"text-emerald-300 border-emerald-900/40";case"internal":return"text-foreground-muted border-border";default:return"text-foreground-muted border-border"}});return(O,R)=>(o(),l("span",{class:ie(["inline-flex items-center px-2 py-0.5 rounded text-xs border bg-background font-mono uppercase tracking-wide",L.value])},r(A.source||c(v)),3))}},ke={class:"space-y-6"},we={class:"flex flex-col sm:flex-row sm:items-center gap-2 sm:flex-wrap"},Ce={class:"relative w-full sm:flex-1 sm:min-w-[260px] sm:max-w-[420px]"},Se={class:"flex items-center gap-2 sm:flex-wrap overflow-x-auto sm:overflow-visible scrollable snap-x min-w-0"},Te={key:0,class:"ml-1 opacity-60 tabular-nums"},Pe={class:"bg-background border border-border rounded-lg overflow-x-auto"},De={class:"sm:hidden divide-y divide-border"},Me=["onClick"],$e={class:"flex items-start justify-between gap-2"},qe={class:"min-w-0 flex-1"},Ae={class:"flex items-center gap-2 flex-wrap"},Le={class:"mt-1 text-xs font-mono text-white break-all"},Be={key:0,class:"mt-1 text-[11px] text-foreground-muted break-words"},Ee={class:"mt-1 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[11px] text-foreground-muted font-mono"},Ie={key:0},Ve={key:1,class:"break-all"},Ne={key:0,class:"px-6 py-12 text-center text-sm text-foreground-muted"},Fe={class:"hidden sm:table w-full text-sm text-left"},Oe={class:"divide-y divide-border"},Re=["onClick"],ze={class:"px-4 py-2.5 font-mono text-xs text-foreground-muted"},Ue={class:"px-4 py-2.5"},Ke={class:"px-4 py-2.5 hidden md:table-cell"},je={class:"text-xs text-white truncate max-w-[200px]"},Je={key:0,class:"text-[10px] text-foreground-muted/70 font-mono truncate"},We={class:"px-4 py-2.5 text-xs font-mono text-foreground-muted hidden sm:table-cell"},Ge={class:"px-4 py-2.5 text-xs font-mono text-white truncate max-w-[440px]"},He={class:"px-4 py-2.5 hidden sm:table-cell"},Xe={key:1,class:"text-foreground-muted text-xs"},Ye={class:"px-4 py-2.5 text-xs font-mono text-foreground-muted hidden lg:table-cell"},Ze={class:"px-4 py-2.5 text-xs text-foreground-muted truncate max-w-[280px] hidden xl:table-cell"},Qe={key:0},et={key:0,class:"flex items-center justify-between text-xs"},tt={class:"text-foreground-muted"},st={class:"flex items-center gap-1"},at={key:0,class:"p-5 space-y-5 text-sm"},rt={class:"grid grid-cols-2 gap-3"},ot={class:"bg-surface border border-border rounded p-3 min-w-0"},lt={class:"text-xs text-white font-mono truncate"},nt={class:"bg-surface border border-border rounded p-3 min-w-0"},ut={class:"bg-surface border border-border rounded p-3 min-w-0"},it={class:"text-sm text-white truncate"},dt={key:0,class:"text-[11px] text-foreground-muted font-mono truncate mt-0.5"},ct={class:"bg-surface border border-border rounded p-3 min-w-0"},vt={class:"flex items-center gap-2"},xt={key:1,class:"text-foreground-muted text-xs"},mt={key:2,class:"text-xs text-foreground-muted font-mono"},ft={class:"bg-surface border border-border rounded p-3 min-w-0"},pt={class:"text-xs text-white font-mono truncate"},bt={class:"bg-surface border border-border rounded p-3 min-w-0"},ht={class:"text-xs text-white font-mono"},_t={class:"bg-surface border border-border rounded p-3 text-xs text-white font-mono whitespace-pre-wrap break-all"},gt={class:"text-foreground break-words"},yt={key:0},kt={class:"bg-surface border border-border rounded p-3 text-xs text-foreground-muted font-mono whitespace-pre-wrap break-all"},wt={key:1},Ct={class:"bg-surface border border-border rounded p-3 text-xs text-foreground font-mono overflow-auto max-h-72 whitespace-pre-wrap break-words"},Y=100,Z=200,At={__name:"Activity",setup(A){const P=de(),L=[{label:"All",value:""},{label:"Web",value:"web"},{label:"API",value:"api"},{label:"MCP",value:"mcp"},{label:"SDK",value:"sdk"},{label:"Webhook",value:"webhook"},{label:"Internal",value:"internal"}],O=[{label:"All",value:""},{label:"Success",value:"ok"},{label:"Errors",value:"err"}],R=[{label:"5m",value:"5m"},{label:"1h",value:"1h"},{label:"24h",value:"24h"},{label:"7d",value:"7d"}],u=p({q:"",source:"",statusBucket:"",range:"24h"}),D=p([]),b=p([]),B=p(!1),n=p(null),z=p(0),E=p(!1),x=p(1),m=p([{since:void 0,until:void 0}]),g=_(()=>x.value===1?[...b.value,...D.value]:D.value),U=_(()=>{const a={};for(const s of g.value)a[s.source]=(a[s.source]||0)+1;return a[""]=g.value.length,a}),M=_(()=>Math.max(m.value.length,x.value)),Q=_(()=>{const a=M.value,s=x.value;return[...new Set([1,a,s-1,s,s+1])].filter(i=>i>=1&&i<=a).sort((i,ne)=>i-ne)}),ee=a=>{switch(a){case"5m":return 5*6e4;case"1h":return 60*6e4;case"24h":return 1440*6e4;case"7d":return 10080*6e4;default:return 0}},te=(a={})=>{const s={limit:Y};u.value.source&&(s.source=u.value.source),u.value.statusBucket==="err"&&(s.status_min=400),u.value.q&&(s.q=u.value.q);const e=ee(u.value.range);return e&&(s.since=Date.now()-e),Object.assign(s,a)},$=async a=>{if(a<1||a>m.value.length+1)return;const s=m.value[a-1]?.cursor,e=await he(te(s?{cursor:s}:{}));D.value=e.data?.rows||[];const i=e.data?.next_cursor||0;E.value=i>0,m.value[a-1]||(m.value[a-1]={}),m.value[a-1].cursor=s,i?(m.value[a]||(m.value[a]={}),m.value[a].cursor=i):m.value=m.value.slice(0,a),z.value=(a-1)*Y+D.value.length,x.value=a,a>1&&(b.value=[])},y=async()=>{m.value=[{since:void 0,until:void 0}],b.value=[],x.value=1,await $(1)};let T=null;const se=a=>{if(u.value.source&&a.source!==u.value.source||u.value.statusBucket==="err"&&(a.status||0)<400)return!1;if(u.value.q){const s=u.value.q.toLowerCase();if(!(a.path+" "+a.summary+" "+a.actor_label).toLowerCase().includes(s))return!1}return!0},ae=a=>{se(a)&&x.value===1&&(b.value.unshift(a),b.value.length>Z&&(b.value=b.value.slice(0,Z)))};let K=null;const re=()=>{clearTimeout(K),K=setTimeout(y,250)},j=a=>{n.value=a,B.value=!0},oe=_(()=>n.value?n.value.summary||n.value.method+" "+n.value.path:"Activity"),J=_(()=>{if(!n.value?.metadata)return"";try{return JSON.stringify(JSON.parse(n.value.metadata),null,2)}catch{return n.value.metadata}}),W=a=>a?new Date(a).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:!1}):v,le=a=>a?new Date(a).toLocaleString():v,I=a=>a==null?v:a<1?"<1ms":a<1e3?a+"ms":(a/1e3).toFixed(2)+"s",V=a=>a?a>=500?"error":a>=400?"failed":a>=200?"success":"pending":"",G=a=>a.id?`db-${a.id}`:`live-${a.ts}-${a.request_id}-${a.path}`,H=()=>{T||(T=P.subscribe("activity",ae),P.connect())},X=()=>{T&&(T(),T=null)};return ce(()=>{H(),y()}),ve(X),xe(()=>{H(),y()}),me(X),(a,s)=>(o(),l("div",ke,[s[20]||(s[20]=t("div",null,[t("h1",{class:"text-xl font-semibold text-white tracking-tight"}," Activity "),t("p",{class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"}," Live feed of every API call hitting Orva: UI clicks, REST/SDK, MCP tools, webhook deliveries. ")],-1)),t("div",we,[t("div",Ce,[f(c(fe),{class:"w-3.5 h-3.5 absolute left-2.5 top-1/2 -translate-y-1/2 text-foreground-muted/60 pointer-events-none"}),pe(t("input",{"onUpdate:modelValue":s[0]||(s[0]=e=>u.value.q=e),"aria-label":"Search activity by path, summary, or actor",placeholder:"Search path, summary, actor…",class:"w-full bg-background border border-border rounded-md pl-8 pr-3 py-1.5 text-base sm:text-xs text-foreground placeholder-foreground-muted/60 focus:outline-none focus:ring-1 focus:ring-white focus:border-white",onInput:re},null,544),[[be,u.value.q]])]),t("div",Se,[(o(),l(k,null,w(L,e=>f(C,{key:e.value,variant:"chip",size:"xs",active:u.value.source===e.value,class:"shrink-0 snap-start",onClick:i=>{u.value.source=e.value,y()}},{default:h(()=>[S(r(e.label)+" ",1),U.value[e.value]!=null&&e.value!==""?(o(),l("span",Te,r(U.value[e.value]),1)):d("",!0)]),_:2},1032,["active","onClick"])),64)),s[4]||(s[4]=t("span",{class:"text-foreground-muted/40 shrink-0"},"·",-1)),(o(),l(k,null,w(O,e=>f(C,{key:e.value,variant:"chip",size:"xs",active:u.value.statusBucket===e.value,class:"shrink-0 snap-start",onClick:i=>{u.value.statusBucket=e.value,y()}},{default:h(()=>[S(r(e.label),1)]),_:2},1032,["active","onClick"])),64)),s[5]||(s[5]=t("span",{class:"text-foreground-muted/40 shrink-0"},"·",-1)),(o(),l(k,null,w(R,e=>f(C,{key:e.value,variant:"chip",size:"xs",active:u.value.range===e.value,class:"shrink-0 snap-start",onClick:i=>{u.value.range=e.value,y()}},{default:h(()=>[S(r(e.label),1)]),_:2},1032,["active","onClick"])),64))])]),t("div",Pe,[t("ul",De,[(o(!0),l(k,null,w(g.value,e=>(o(),l("li",{key:G(e),class:"px-4 py-3 cursor-pointer hover:bg-surface-hover transition-colors",onClick:i=>j(e)},[t("div",$e,[t("div",qe,[t("div",Ae,[f(F,{source:e.source},null,8,["source"]),e.status?(o(),q(N,{key:0,status:V(e.status)},null,8,["status"])):d("",!0)]),t("div",Le,r(e.method?e.method+" ":"")+r(e.path||c(v)),1),e.summary?(o(),l("div",Be,r(e.summary),1)):d("",!0),t("div",Ee,[t("span",null,r(W(e.ts)),1),e.duration_ms!=null?(o(),l("span",Ie,r(I(e.duration_ms)),1)):d("",!0),e.actor_label||e.actor_id?(o(),l("span",Ve,r(e.actor_label||e.actor_id),1)):d("",!0)])])])],8,Me))),128)),g.value.length?d("",!0):(o(),l("li",Ne," No activity yet. Drive any action (open the dashboard, call a function, fire an MCP tool) and rows will land here. "))]),t("table",Fe,[s[7]||(s[7]=t("thead",{class:"text-xs text-foreground-muted uppercase bg-surface border-b border-border"},[t("tr",null,[t("th",{class:"px-4 py-3 w-32"},"Time"),t("th",{class:"px-4 py-3 w-24"},"Source"),t("th",{class:"px-4 py-3 w-40 hidden md:table-cell"},"Actor"),t("th",{class:"px-4 py-3 w-20 hidden sm:table-cell"},"Method"),t("th",{class:"px-4 py-3"},"Path / Tool"),t("th",{class:"px-4 py-3 w-16 hidden sm:table-cell"},"Status"),t("th",{class:"px-4 py-3 w-20 hidden lg:table-cell"},"Duration"),t("th",{class:"px-4 py-3 hidden xl:table-cell"},"Summary")])],-1)),t("tbody",Oe,[(o(!0),l(k,null,w(g.value,e=>(o(),l("tr",{key:G(e),class:"hover:bg-surface-hover cursor-pointer transition-colors",onClick:i=>j(e)},[t("td",ze,r(W(e.ts)),1),t("td",Ue,[f(F,{source:e.source},null,8,["source"])]),t("td",Ke,[t("div",je,r(e.actor_label||e.actor_id||c(v)),1),e.actor_label&&e.actor_id&&e.actor_label!==e.actor_id?(o(),l("div",Je,r(e.actor_id),1)):d("",!0)]),t("td",We,r(e.method||c(v)),1),t("td",Ge,r(e.path||c(v)),1),t("td",He,[e.status?(o(),q(N,{key:0,status:V(e.status)},null,8,["status"])):(o(),l("span",Xe,r(c(v)),1))]),t("td",Ye,r(I(e.duration_ms)),1),t("td",Ze,r(e.summary),1)],8,Re))),128)),g.value.length?d("",!0):(o(),l("tr",Qe,[...s[6]||(s[6]=[t("td",{colspan:"8",class:"px-4 py-12 text-center text-foreground-muted text-sm"}," No activity yet. Drive any action (open the dashboard, call a function, fire an MCP tool) and rows will land here. ",-1)])]))])])]),M.value>1?(o(),l("div",et,[t("div",tt," Page "+r(x.value)+" of "+r(M.value)+" · "+r(z.value)+r(E.value?"+":"")+" rows ",1),t("div",st,[f(C,{variant:"secondary",size:"xs",disabled:x.value<=1,onClick:s[1]||(s[1]=e=>$(x.value-1))},{default:h(()=>[f(c(ye),{class:"w-3.5 h-3.5"}),s[8]||(s[8]=S(" Prev ",-1))]),_:1},8,["disabled"]),(o(!0),l(k,null,w(Q.value,e=>(o(),q(C,{key:e,variant:e===x.value?"primary":"secondary",size:"xs",onClick:i=>$(e)},{default:h(()=>[S(r(e),1)]),_:2},1032,["variant","onClick"]))),128)),f(C,{variant:"secondary",size:"xs",disabled:x.value>=M.value&&!E.value,onClick:s[2]||(s[2]=e=>$(x.value+1))},{default:h(()=>[s[9]||(s[9]=S(" Next ",-1)),f(c(ge),{class:"w-3.5 h-3.5"})]),_:1},8,["disabled"])])])):d("",!0),f(_e,{modelValue:B.value,"onUpdate:modelValue":s[3]||(s[3]=e=>B.value=e),title:oe.value,width:"640px"},{default:h(()=>[n.value?(o(),l("div",at,[t("div",rt,[t("div",ot,[s[10]||(s[10]=t("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},"Time",-1)),t("div",lt,r(le(n.value.ts)),1)]),t("div",nt,[s[11]||(s[11]=t("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},"Source",-1)),f(F,{source:n.value.source},null,8,["source"])]),t("div",ut,[s[12]||(s[12]=t("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},"Actor",-1)),t("div",it,r(n.value.actor_label||c(v)),1),n.value.actor_id?(o(),l("div",dt,r(n.value.actor_id),1)):d("",!0)]),t("div",ct,[s[13]||(s[13]=t("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},"Status",-1)),t("div",vt,[n.value.status?(o(),q(N,{key:0,status:V(n.value.status)},null,8,["status"])):(o(),l("span",xt,r(c(v)),1)),n.value.status?(o(),l("span",mt,"HTTP "+r(n.value.status),1)):d("",!0)])]),t("div",ft,[s[14]||(s[14]=t("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},"Method",-1)),t("div",pt,r(n.value.method||c(v)),1)]),t("div",bt,[s[15]||(s[15]=t("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},"Duration",-1)),t("div",ht,r(I(n.value.duration_ms)),1)])]),t("div",null,[s[16]||(s[16]=t("h3",{class:"text-xs uppercase tracking-wider text-foreground-muted mb-2"},"Path / Tool",-1)),t("pre",_t,r(n.value.path||c(v)),1)]),t("div",null,[s[17]||(s[17]=t("h3",{class:"text-xs uppercase tracking-wider text-foreground-muted mb-2"},"Summary",-1)),t("div",gt,r(n.value.summary||c(v)),1)]),n.value.request_id?(o(),l("div",yt,[s[18]||(s[18]=t("h3",{class:"text-xs uppercase tracking-wider text-foreground-muted mb-2"},"Request ID",-1)),t("pre",kt,r(n.value.request_id),1)])):d("",!0),J.value?(o(),l("div",wt,[s[19]||(s[19]=t("h3",{class:"text-xs uppercase tracking-wider text-foreground-muted mb-2"},"Metadata",-1)),t("pre",Ct,r(J.value),1)])):d("",!0)])):d("",!0)]),_:1},8,["modelValue","title"])]))}};export{At as default}; +import{c as ue,j as o,a as l,t as r,f as c,s as ie,q as _,U as de,o as ce,y as ve,R as xe,V as me,b as t,d as f,S as fe,e as pe,v as be,F as k,p as w,g as d,h,_ as C,r as p,aB as he,k as S,n as q}from"./index-BMkkwZ9q.js";import{E as v}from"./format-CsU4_SPu.js";import{D as _e}from"./Drawer-C3AFLOZb.js";import{_ as N}from"./StatusBadge-Cj_PlPFZ.js";import{C as ge}from"./chevron-right-OdWgNfOU.js";import"./circle-DJWJGpv0.js";import"./clock-BWp9w4xs.js";const ye=ue("chevron-left",[["path",{d:"m15 18-6-6 6-6",key:"1wnfg3"}]]),F={__name:"SourceTag",props:{source:{type:String,default:""}},setup(A){const P=A,L=_(()=>{switch(P.source){case"web":return"text-indigo-300 border-indigo-900/40";case"api":return"text-sky-300 border-sky-900/40";case"mcp":return"text-violet-300 border-violet-900/40";case"sdk":return"text-teal-300 border-teal-900/40";case"webhook":return"text-amber-300 border-amber-900/40";case"cron":return"text-emerald-300 border-emerald-900/40";case"internal":return"text-foreground-muted border-border";default:return"text-foreground-muted border-border"}});return(O,R)=>(o(),l("span",{class:ie(["inline-flex items-center px-2 py-0.5 rounded text-xs border bg-background font-mono uppercase tracking-wide",L.value])},r(A.source||c(v)),3))}},ke={class:"space-y-6"},we={class:"flex flex-col sm:flex-row sm:items-center gap-2 sm:flex-wrap"},Ce={class:"relative w-full sm:flex-1 sm:min-w-[260px] sm:max-w-[420px]"},Se={class:"flex items-center gap-2 sm:flex-wrap overflow-x-auto sm:overflow-visible scrollable snap-x min-w-0"},Te={key:0,class:"ml-1 opacity-60 tabular-nums"},Pe={class:"bg-background border border-border rounded-lg overflow-x-auto"},De={class:"sm:hidden divide-y divide-border"},Me=["onClick"],$e={class:"flex items-start justify-between gap-2"},qe={class:"min-w-0 flex-1"},Ae={class:"flex items-center gap-2 flex-wrap"},Le={class:"mt-1 text-xs font-mono text-white break-all"},Be={key:0,class:"mt-1 text-[11px] text-foreground-muted break-words"},Ee={class:"mt-1 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[11px] text-foreground-muted font-mono"},Ie={key:0},Ve={key:1,class:"break-all"},Ne={key:0,class:"px-6 py-12 text-center text-sm text-foreground-muted"},Fe={class:"hidden sm:table w-full text-sm text-left"},Oe={class:"divide-y divide-border"},Re=["onClick"],ze={class:"px-4 py-2.5 font-mono text-xs text-foreground-muted"},Ue={class:"px-4 py-2.5"},Ke={class:"px-4 py-2.5 hidden md:table-cell"},je={class:"text-xs text-white truncate max-w-[200px]"},Je={key:0,class:"text-[10px] text-foreground-muted/70 font-mono truncate"},We={class:"px-4 py-2.5 text-xs font-mono text-foreground-muted hidden sm:table-cell"},Ge={class:"px-4 py-2.5 text-xs font-mono text-white truncate max-w-[440px]"},He={class:"px-4 py-2.5 hidden sm:table-cell"},Xe={key:1,class:"text-foreground-muted text-xs"},Ye={class:"px-4 py-2.5 text-xs font-mono text-foreground-muted hidden lg:table-cell"},Ze={class:"px-4 py-2.5 text-xs text-foreground-muted truncate max-w-[280px] hidden xl:table-cell"},Qe={key:0},et={key:0,class:"flex items-center justify-between text-xs"},tt={class:"text-foreground-muted"},st={class:"flex items-center gap-1"},at={key:0,class:"p-5 space-y-5 text-sm"},rt={class:"grid grid-cols-2 gap-3"},ot={class:"bg-surface border border-border rounded p-3 min-w-0"},lt={class:"text-xs text-white font-mono truncate"},nt={class:"bg-surface border border-border rounded p-3 min-w-0"},ut={class:"bg-surface border border-border rounded p-3 min-w-0"},it={class:"text-sm text-white truncate"},dt={key:0,class:"text-[11px] text-foreground-muted font-mono truncate mt-0.5"},ct={class:"bg-surface border border-border rounded p-3 min-w-0"},vt={class:"flex items-center gap-2"},xt={key:1,class:"text-foreground-muted text-xs"},mt={key:2,class:"text-xs text-foreground-muted font-mono"},ft={class:"bg-surface border border-border rounded p-3 min-w-0"},pt={class:"text-xs text-white font-mono truncate"},bt={class:"bg-surface border border-border rounded p-3 min-w-0"},ht={class:"text-xs text-white font-mono"},_t={class:"bg-surface border border-border rounded p-3 text-xs text-white font-mono whitespace-pre-wrap break-all"},gt={class:"text-foreground break-words"},yt={key:0},kt={class:"bg-surface border border-border rounded p-3 text-xs text-foreground-muted font-mono whitespace-pre-wrap break-all"},wt={key:1},Ct={class:"bg-surface border border-border rounded p-3 text-xs text-foreground font-mono overflow-auto max-h-72 whitespace-pre-wrap break-words"},Y=100,Z=200,At={__name:"Activity",setup(A){const P=de(),L=[{label:"All",value:""},{label:"Web",value:"web"},{label:"API",value:"api"},{label:"MCP",value:"mcp"},{label:"SDK",value:"sdk"},{label:"Webhook",value:"webhook"},{label:"Internal",value:"internal"}],O=[{label:"All",value:""},{label:"Success",value:"ok"},{label:"Errors",value:"err"}],R=[{label:"5m",value:"5m"},{label:"1h",value:"1h"},{label:"24h",value:"24h"},{label:"7d",value:"7d"}],u=p({q:"",source:"",statusBucket:"",range:"24h"}),D=p([]),b=p([]),B=p(!1),n=p(null),z=p(0),E=p(!1),x=p(1),m=p([{since:void 0,until:void 0}]),g=_(()=>x.value===1?[...b.value,...D.value]:D.value),U=_(()=>{const a={};for(const s of g.value)a[s.source]=(a[s.source]||0)+1;return a[""]=g.value.length,a}),M=_(()=>Math.max(m.value.length,x.value)),Q=_(()=>{const a=M.value,s=x.value;return[...new Set([1,a,s-1,s,s+1])].filter(i=>i>=1&&i<=a).sort((i,ne)=>i-ne)}),ee=a=>{switch(a){case"5m":return 5*6e4;case"1h":return 60*6e4;case"24h":return 1440*6e4;case"7d":return 10080*6e4;default:return 0}},te=(a={})=>{const s={limit:Y};u.value.source&&(s.source=u.value.source),u.value.statusBucket==="err"&&(s.status_min=400),u.value.q&&(s.q=u.value.q);const e=ee(u.value.range);return e&&(s.since=Date.now()-e),Object.assign(s,a)},$=async a=>{if(a<1||a>m.value.length+1)return;const s=m.value[a-1]?.cursor,e=await he(te(s?{cursor:s}:{}));D.value=e.data?.rows||[];const i=e.data?.next_cursor||0;E.value=i>0,m.value[a-1]||(m.value[a-1]={}),m.value[a-1].cursor=s,i?(m.value[a]||(m.value[a]={}),m.value[a].cursor=i):m.value=m.value.slice(0,a),z.value=(a-1)*Y+D.value.length,x.value=a,a>1&&(b.value=[])},y=async()=>{m.value=[{since:void 0,until:void 0}],b.value=[],x.value=1,await $(1)};let T=null;const se=a=>{if(u.value.source&&a.source!==u.value.source||u.value.statusBucket==="err"&&(a.status||0)<400)return!1;if(u.value.q){const s=u.value.q.toLowerCase();if(!(a.path+" "+a.summary+" "+a.actor_label).toLowerCase().includes(s))return!1}return!0},ae=a=>{se(a)&&x.value===1&&(b.value.unshift(a),b.value.length>Z&&(b.value=b.value.slice(0,Z)))};let K=null;const re=()=>{clearTimeout(K),K=setTimeout(y,250)},j=a=>{n.value=a,B.value=!0},oe=_(()=>n.value?n.value.summary||n.value.method+" "+n.value.path:"Activity"),J=_(()=>{if(!n.value?.metadata)return"";try{return JSON.stringify(JSON.parse(n.value.metadata),null,2)}catch{return n.value.metadata}}),W=a=>a?new Date(a).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:!1}):v,le=a=>a?new Date(a).toLocaleString():v,I=a=>a==null?v:a<1?"<1ms":a<1e3?a+"ms":(a/1e3).toFixed(2)+"s",V=a=>a?a>=500?"error":a>=400?"failed":a>=200?"success":"pending":"",G=a=>a.id?`db-${a.id}`:`live-${a.ts}-${a.request_id}-${a.path}`,H=()=>{T||(T=P.subscribe("activity",ae),P.connect())},X=()=>{T&&(T(),T=null)};return ce(()=>{H(),y()}),ve(X),xe(()=>{H(),y()}),me(X),(a,s)=>(o(),l("div",ke,[s[20]||(s[20]=t("div",null,[t("h1",{class:"text-xl font-semibold text-white tracking-tight"}," Activity "),t("p",{class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"}," Live feed of every API call hitting Orva: UI clicks, REST/SDK, MCP tools, webhook deliveries. ")],-1)),t("div",we,[t("div",Ce,[f(c(fe),{class:"w-3.5 h-3.5 absolute left-2.5 top-1/2 -translate-y-1/2 text-foreground-muted/60 pointer-events-none"}),pe(t("input",{"onUpdate:modelValue":s[0]||(s[0]=e=>u.value.q=e),"aria-label":"Search activity by path, summary, or actor",placeholder:"Search path, summary, actor…",class:"w-full bg-background border border-border rounded-md pl-8 pr-3 py-1.5 text-base sm:text-xs text-foreground placeholder-foreground-muted/60 focus:outline-none focus:ring-1 focus:ring-white focus:border-white",onInput:re},null,544),[[be,u.value.q]])]),t("div",Se,[(o(),l(k,null,w(L,e=>f(C,{key:e.value,variant:"chip",size:"xs",active:u.value.source===e.value,class:"shrink-0 snap-start",onClick:i=>{u.value.source=e.value,y()}},{default:h(()=>[S(r(e.label)+" ",1),U.value[e.value]!=null&&e.value!==""?(o(),l("span",Te,r(U.value[e.value]),1)):d("",!0)]),_:2},1032,["active","onClick"])),64)),s[4]||(s[4]=t("span",{class:"text-foreground-muted/40 shrink-0"},"·",-1)),(o(),l(k,null,w(O,e=>f(C,{key:e.value,variant:"chip",size:"xs",active:u.value.statusBucket===e.value,class:"shrink-0 snap-start",onClick:i=>{u.value.statusBucket=e.value,y()}},{default:h(()=>[S(r(e.label),1)]),_:2},1032,["active","onClick"])),64)),s[5]||(s[5]=t("span",{class:"text-foreground-muted/40 shrink-0"},"·",-1)),(o(),l(k,null,w(R,e=>f(C,{key:e.value,variant:"chip",size:"xs",active:u.value.range===e.value,class:"shrink-0 snap-start",onClick:i=>{u.value.range=e.value,y()}},{default:h(()=>[S(r(e.label),1)]),_:2},1032,["active","onClick"])),64))])]),t("div",Pe,[t("ul",De,[(o(!0),l(k,null,w(g.value,e=>(o(),l("li",{key:G(e),class:"px-4 py-3 cursor-pointer hover:bg-surface-hover transition-colors",onClick:i=>j(e)},[t("div",$e,[t("div",qe,[t("div",Ae,[f(F,{source:e.source},null,8,["source"]),e.status?(o(),q(N,{key:0,status:V(e.status)},null,8,["status"])):d("",!0)]),t("div",Le,r(e.method?e.method+" ":"")+r(e.path||c(v)),1),e.summary?(o(),l("div",Be,r(e.summary),1)):d("",!0),t("div",Ee,[t("span",null,r(W(e.ts)),1),e.duration_ms!=null?(o(),l("span",Ie,r(I(e.duration_ms)),1)):d("",!0),e.actor_label||e.actor_id?(o(),l("span",Ve,r(e.actor_label||e.actor_id),1)):d("",!0)])])])],8,Me))),128)),g.value.length?d("",!0):(o(),l("li",Ne," No activity yet. Drive any action (open the dashboard, call a function, fire an MCP tool) and rows will land here. "))]),t("table",Fe,[s[7]||(s[7]=t("thead",{class:"text-xs text-foreground-muted uppercase bg-surface border-b border-border"},[t("tr",null,[t("th",{class:"px-4 py-3 w-32"},"Time"),t("th",{class:"px-4 py-3 w-24"},"Source"),t("th",{class:"px-4 py-3 w-40 hidden md:table-cell"},"Actor"),t("th",{class:"px-4 py-3 w-20 hidden sm:table-cell"},"Method"),t("th",{class:"px-4 py-3"},"Path / Tool"),t("th",{class:"px-4 py-3 w-16 hidden sm:table-cell"},"Status"),t("th",{class:"px-4 py-3 w-20 hidden lg:table-cell"},"Duration"),t("th",{class:"px-4 py-3 hidden xl:table-cell"},"Summary")])],-1)),t("tbody",Oe,[(o(!0),l(k,null,w(g.value,e=>(o(),l("tr",{key:G(e),class:"hover:bg-surface-hover cursor-pointer transition-colors",onClick:i=>j(e)},[t("td",ze,r(W(e.ts)),1),t("td",Ue,[f(F,{source:e.source},null,8,["source"])]),t("td",Ke,[t("div",je,r(e.actor_label||e.actor_id||c(v)),1),e.actor_label&&e.actor_id&&e.actor_label!==e.actor_id?(o(),l("div",Je,r(e.actor_id),1)):d("",!0)]),t("td",We,r(e.method||c(v)),1),t("td",Ge,r(e.path||c(v)),1),t("td",He,[e.status?(o(),q(N,{key:0,status:V(e.status)},null,8,["status"])):(o(),l("span",Xe,r(c(v)),1))]),t("td",Ye,r(I(e.duration_ms)),1),t("td",Ze,r(e.summary),1)],8,Re))),128)),g.value.length?d("",!0):(o(),l("tr",Qe,[...s[6]||(s[6]=[t("td",{colspan:"8",class:"px-4 py-12 text-center text-foreground-muted text-sm"}," No activity yet. Drive any action (open the dashboard, call a function, fire an MCP tool) and rows will land here. ",-1)])]))])])]),M.value>1?(o(),l("div",et,[t("div",tt," Page "+r(x.value)+" of "+r(M.value)+" · "+r(z.value)+r(E.value?"+":"")+" rows ",1),t("div",st,[f(C,{variant:"secondary",size:"xs",disabled:x.value<=1,onClick:s[1]||(s[1]=e=>$(x.value-1))},{default:h(()=>[f(c(ye),{class:"w-3.5 h-3.5"}),s[8]||(s[8]=S(" Prev ",-1))]),_:1},8,["disabled"]),(o(!0),l(k,null,w(Q.value,e=>(o(),q(C,{key:e,variant:e===x.value?"primary":"secondary",size:"xs",onClick:i=>$(e)},{default:h(()=>[S(r(e),1)]),_:2},1032,["variant","onClick"]))),128)),f(C,{variant:"secondary",size:"xs",disabled:x.value>=M.value&&!E.value,onClick:s[2]||(s[2]=e=>$(x.value+1))},{default:h(()=>[s[9]||(s[9]=S(" Next ",-1)),f(c(ge),{class:"w-3.5 h-3.5"})]),_:1},8,["disabled"])])])):d("",!0),f(_e,{modelValue:B.value,"onUpdate:modelValue":s[3]||(s[3]=e=>B.value=e),title:oe.value,width:"640px"},{default:h(()=>[n.value?(o(),l("div",at,[t("div",rt,[t("div",ot,[s[10]||(s[10]=t("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},"Time",-1)),t("div",lt,r(le(n.value.ts)),1)]),t("div",nt,[s[11]||(s[11]=t("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},"Source",-1)),f(F,{source:n.value.source},null,8,["source"])]),t("div",ut,[s[12]||(s[12]=t("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},"Actor",-1)),t("div",it,r(n.value.actor_label||c(v)),1),n.value.actor_id?(o(),l("div",dt,r(n.value.actor_id),1)):d("",!0)]),t("div",ct,[s[13]||(s[13]=t("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},"Status",-1)),t("div",vt,[n.value.status?(o(),q(N,{key:0,status:V(n.value.status)},null,8,["status"])):(o(),l("span",xt,r(c(v)),1)),n.value.status?(o(),l("span",mt,"HTTP "+r(n.value.status),1)):d("",!0)])]),t("div",ft,[s[14]||(s[14]=t("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},"Method",-1)),t("div",pt,r(n.value.method||c(v)),1)]),t("div",bt,[s[15]||(s[15]=t("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},"Duration",-1)),t("div",ht,r(I(n.value.duration_ms)),1)])]),t("div",null,[s[16]||(s[16]=t("h3",{class:"text-xs uppercase tracking-wider text-foreground-muted mb-2"},"Path / Tool",-1)),t("pre",_t,r(n.value.path||c(v)),1)]),t("div",null,[s[17]||(s[17]=t("h3",{class:"text-xs uppercase tracking-wider text-foreground-muted mb-2"},"Summary",-1)),t("div",gt,r(n.value.summary||c(v)),1)]),n.value.request_id?(o(),l("div",yt,[s[18]||(s[18]=t("h3",{class:"text-xs uppercase tracking-wider text-foreground-muted mb-2"},"Request ID",-1)),t("pre",kt,r(n.value.request_id),1)])):d("",!0),J.value?(o(),l("div",wt,[s[19]||(s[19]=t("h3",{class:"text-xs uppercase tracking-wider text-foreground-muted mb-2"},"Metadata",-1)),t("pre",Ct,r(J.value),1)])):d("",!0)])):d("",!0)]),_:1},8,["modelValue","title"])]))}};export{At as default}; diff --git a/backend/internal/server/ui_dist/assets/ApiKeys-DRo8HmPM.js b/backend/internal/server/ui_dist/assets/ApiKeys-D_0i_tEn.js similarity index 96% rename from backend/internal/server/ui_dist/assets/ApiKeys-DRo8HmPM.js rename to backend/internal/server/ui_dist/assets/ApiKeys-D_0i_tEn.js index d75bf8d..317de89 100644 --- a/backend/internal/server/ui_dist/assets/ApiKeys-DRo8HmPM.js +++ b/backend/internal/server/ui_dist/assets/ApiKeys-D_0i_tEn.js @@ -1,3 +1,3 @@ -import{C as L,o as M,a,b as e,d as c,h,_ as w,f as n,aa as j,t as l,n as C,k as i,g as y,e as D,v as B,a1 as F,F as K,p as N,aN as R,r as p,j as o,aO as G,aP as H}from"./index-D5cO6vit.js";import{E as O}from"./format-CsU4_SPu.js";import{_ as I}from"./IconButton-Pc0_zskr.js";import{c as X}from"./clipboard-CmSw2rR-.js";import{f as x,i as A}from"./time-Cfu9zNbw.js";import{C as Y}from"./check-BkPCgKSu.js";import{C as q}from"./copy-Gc8n9M6v.js";import{K as z}from"./key-round-D643nzM3.js";import{T as P}from"./trash-2-sSCgxcxW.js";const J={class:"space-y-6"},Q={class:"flex items-start justify-between gap-4"},W={key:0,class:"bg-background border border-amber-700/40 rounded-lg p-4 space-y-2"},Z={class:"flex items-start justify-between gap-3"},ee={class:"flex items-center gap-2"},te={class:"flex-1 font-mono text-sm text-white break-all bg-surface px-3 py-2 rounded border border-border"},se={key:1,class:"bg-background border border-border rounded-lg p-5 space-y-4"},oe={class:"grid grid-cols-1 md:grid-cols-2 gap-3"},ae={class:"flex gap-2 pt-1"},re={class:"bg-background border border-border rounded-lg overflow-x-auto"},ne={class:"sm:hidden divide-y divide-border"},le={class:"flex items-start justify-between gap-2"},de={class:"min-w-0 flex-1"},ie={class:"flex items-center gap-2 flex-wrap"},ce={class:"font-medium text-white truncate"},ue={key:0,class:"text-[11px] font-mono text-foreground-muted bg-surface px-1.5 py-0.5 rounded"},pe={class:"mt-1 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[11px] text-foreground-muted"},xe={key:0},me={key:1,class:"text-amber-400/80"},fe={key:2},ye={key:3,class:"text-red-400"},ve={key:4},ge={key:0,class:"px-6 py-8 text-center text-sm text-foreground-muted"},be={class:"hidden sm:table w-full text-sm text-left"},_e={class:"divide-y divide-border"},he={class:"px-6 py-4 text-white font-medium"},we={class:"px-6 py-4 text-foreground-muted font-mono text-xs hidden sm:table-cell"},ke={class:"px-6 py-4 text-foreground-muted hidden xl:table-cell"},Ce={class:"px-6 py-4 hidden md:table-cell"},De={key:0,class:"text-foreground-muted"},Ke={key:1,class:"text-amber-400/70 text-xs"},Ne={class:"px-6 py-4 hidden lg:table-cell"},Ie={key:0,class:"text-foreground-muted"},Ae={key:1,class:"text-red-400 text-xs"},Pe={key:2,class:"text-foreground-muted"},Te={class:"px-6 py-4 text-right"},Ee={key:0},Ge={__name:"ApiKeys",setup(Se){const v=L(),m=p([]),u=p(""),f=p(!1),g=p(!1),b=p(!1),d=p({name:"",expiresInDays:0}),_=async()=>{const r=await R();m.value=r.data.keys||[]},T=()=>{d.value={name:"",expiresInDays:0},g.value=!0},E=()=>{g.value=!1,d.value={name:"",expiresInDays:0}},S=async()=>{b.value=!0;try{const r={name:d.value.name.trim()};d.value.expiresInDays>0&&(r.expires_in_days=d.value.expiresInDays);const t=await G(r);u.value=t.data.key,f.value=!1,g.value=!1,await _()}catch(r){console.error(r),v.notify({title:"Failed to create key",message:r?.response?.data?.error?.message||"Unknown error",danger:!0})}finally{b.value=!1}},U=async()=>{await X(u.value)?(f.value=!0,setTimeout(()=>{f.value=!1},1500)):v.notify({title:"Copy failed",message:`Could not copy to clipboard. Select the key manually: +import{C as L,o as M,a,b as e,d as c,h,_ as w,f as n,aa as j,t as l,n as C,k as i,g as y,e as D,v as B,a1 as F,F as K,p as N,aN as R,r as p,j as o,aO as G,aP as H}from"./index-BMkkwZ9q.js";import{E as O}from"./format-CsU4_SPu.js";import{_ as I}from"./IconButton-BgeMzwXv.js";import{c as X}from"./clipboard-CmSw2rR-.js";import{f as x,i as A}from"./time-Cfu9zNbw.js";import{C as Y}from"./check-C4wzjDZN.js";import{C as q}from"./copy-CTb6u-fx.js";import{K as z}from"./key-round-BccKiRw7.js";import{T as P}from"./trash-2-BXf2uqQH.js";const J={class:"space-y-6"},Q={class:"flex items-start justify-between gap-4"},W={key:0,class:"bg-background border border-amber-700/40 rounded-lg p-4 space-y-2"},Z={class:"flex items-start justify-between gap-3"},ee={class:"flex items-center gap-2"},te={class:"flex-1 font-mono text-sm text-white break-all bg-surface px-3 py-2 rounded border border-border"},se={key:1,class:"bg-background border border-border rounded-lg p-5 space-y-4"},oe={class:"grid grid-cols-1 md:grid-cols-2 gap-3"},ae={class:"flex gap-2 pt-1"},re={class:"bg-background border border-border rounded-lg overflow-x-auto"},ne={class:"sm:hidden divide-y divide-border"},le={class:"flex items-start justify-between gap-2"},de={class:"min-w-0 flex-1"},ie={class:"flex items-center gap-2 flex-wrap"},ce={class:"font-medium text-white truncate"},ue={key:0,class:"text-[11px] font-mono text-foreground-muted bg-surface px-1.5 py-0.5 rounded"},pe={class:"mt-1 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[11px] text-foreground-muted"},xe={key:0},me={key:1,class:"text-amber-400/80"},fe={key:2},ye={key:3,class:"text-red-400"},ve={key:4},ge={key:0,class:"px-6 py-8 text-center text-sm text-foreground-muted"},be={class:"hidden sm:table w-full text-sm text-left"},_e={class:"divide-y divide-border"},he={class:"px-6 py-4 text-white font-medium"},we={class:"px-6 py-4 text-foreground-muted font-mono text-xs hidden sm:table-cell"},ke={class:"px-6 py-4 text-foreground-muted hidden xl:table-cell"},Ce={class:"px-6 py-4 hidden md:table-cell"},De={key:0,class:"text-foreground-muted"},Ke={key:1,class:"text-amber-400/70 text-xs"},Ne={class:"px-6 py-4 hidden lg:table-cell"},Ie={key:0,class:"text-foreground-muted"},Ae={key:1,class:"text-red-400 text-xs"},Pe={key:2,class:"text-foreground-muted"},Te={class:"px-6 py-4 text-right"},Ee={key:0},Ge={__name:"ApiKeys",setup(Se){const v=L(),m=p([]),u=p(""),f=p(!1),g=p(!1),b=p(!1),d=p({name:"",expiresInDays:0}),_=async()=>{const r=await R();m.value=r.data.keys||[]},T=()=>{d.value={name:"",expiresInDays:0},g.value=!0},E=()=>{g.value=!1,d.value={name:"",expiresInDays:0}},S=async()=>{b.value=!0;try{const r={name:d.value.name.trim()};d.value.expiresInDays>0&&(r.expires_in_days=d.value.expiresInDays);const t=await G(r);u.value=t.data.key,f.value=!1,g.value=!1,await _()}catch(r){console.error(r),v.notify({title:"Failed to create key",message:r?.response?.data?.error?.message||"Unknown error",danger:!0})}finally{b.value=!1}},U=async()=>{await X(u.value)?(f.value=!0,setTimeout(()=>{f.value=!1},1500)):v.notify({title:"Copy failed",message:`Could not copy to clipboard. Select the key manually: `+u.value})},k=async r=>{if(await v.ask({title:"Delete API key?",message:`"${r.name||r.id}" will stop working immediately. This cannot be undone.`,confirmLabel:"Delete",danger:!0}))try{await H(r.id),await _()}catch(s){console.error(s),v.notify({title:"Failed to delete key",message:s?.response?.data?.error?.message||"Unknown error",danger:!0})}},V=r=>new Date(r).toLocaleString();return M(_),(r,t)=>(o(),a("div",J,[e("div",Q,[t[4]||(t[4]=e("div",null,[e("h1",{class:"text-xl font-semibold text-white tracking-tight"}," API Keys "),e("p",{class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"}," Long-lived bearer tokens that authorise REST and MCP calls from CI, scripts, and external services. Plaintext is shown once at creation; the server keeps only a SHA-256 hash. ")],-1)),c(w,{onClick:T},{default:h(()=>[c(n(z),{class:"w-4 h-4"}),t[3]||(t[3]=i(" New Key ",-1))]),_:1})]),u.value?(o(),a("div",W,[e("div",Z,[t[5]||(t[5]=e("div",null,[e("h2",{class:"text-xs font-bold text-amber-300 uppercase tracking-wider"}," Copy this key now "),e("div",{class:"text-xs text-foreground-muted mt-0.5"}," It will not be shown again. Anyone with this key can invoke your functions. ")],-1)),e("button",{class:"text-foreground-muted hover:text-white",title:"Dismiss",onClick:t[0]||(t[0]=s=>u.value="")},[c(n(j),{class:"w-4 h-4"})])]),e("div",ee,[e("code",te,l(u.value),1),e("button",{class:"px-3 py-2 rounded-md border border-border bg-surface-hover hover:bg-surface text-foreground-muted hover:text-white transition-colors flex items-center gap-1.5 text-xs",onClick:U},[f.value?(o(),C(n(Y),{key:0,class:"w-3.5 h-3.5 text-success"})):(o(),C(n(q),{key:1,class:"w-3.5 h-3.5"})),i(" "+l(f.value?"Copied":"Copy"),1)])])])):y("",!0),g.value?(o(),a("div",se,[t[11]||(t[11]=e("div",{class:"text-sm font-semibold text-white"}," New API Key ",-1)),e("div",oe,[e("div",null,[t[6]||(t[6]=e("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-1.5"},"Name",-1)),D(e("input",{"onUpdate:modelValue":t[1]||(t[1]=s=>d.value.name=s),placeholder:"e.g. ci-deployer",class:"w-full bg-surface-hover border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:border-white"},null,512),[[B,d.value.name]])]),e("div",null,[t[8]||(t[8]=e("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-1.5"},"Expires in",-1)),D(e("select",{"onUpdate:modelValue":t[2]||(t[2]=s=>d.value.expiresInDays=s),class:"w-full bg-surface-hover border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:border-white"},[...t[7]||(t[7]=[e("option",{value:0}," Never ",-1),e("option",{value:1}," 1 day ",-1),e("option",{value:7}," 7 days ",-1),e("option",{value:30}," 30 days ",-1),e("option",{value:90}," 90 days ",-1),e("option",{value:365}," 1 year ",-1)])],512),[[F,d.value.expiresInDays]])])]),e("div",ae,[c(w,{disabled:!d.value.name.trim()||b.value,loading:b.value,onClick:S},{default:h(()=>[...t[9]||(t[9]=[i(" Generate Key ",-1)])]),_:1},8,["disabled","loading"]),c(w,{variant:"secondary",onClick:E},{default:h(()=>[...t[10]||(t[10]=[i(" Cancel ",-1)])]),_:1})])])):y("",!0),e("div",re,[e("ul",ne,[(o(!0),a(K,null,N(m.value,s=>(o(),a("li",{key:s.id,class:"px-4 py-3"},[e("div",le,[e("div",de,[e("div",ie,[e("span",ce,l(s.name||"Unnamed"),1),s.prefix?(o(),a("code",ue,l(s.prefix)+"…",1)):y("",!0)]),e("div",pe,[s.last_used_at?(o(),a("span",xe,"used "+l(n(x)(s.last_used_at)),1)):(o(),a("span",me,"never used")),s.expires_at?n(A)(s.expires_at)?(o(),a("span",ye,"expired "+l(n(x)(s.expires_at)),1)):(o(),a("span",ve,"expires "+l(n(x)(s.expires_at)),1)):(o(),a("span",fe,"no expiry"))])]),c(I,{icon:n(P),variant:"danger",title:"Delete key",onClick:$=>k(s)},null,8,["icon","onClick"])])]))),128)),m.value.length===0?(o(),a("li",ge,[...t[12]||(t[12]=[i(" No API keys yet. Tap ",-1),e("span",{class:"text-white"},"New Key",-1),i(" to generate one. ",-1)])])):y("",!0)]),e("table",be,[t[14]||(t[14]=e("thead",{class:"text-xs text-foreground-muted uppercase bg-surface border-b border-border"},[e("tr",null,[e("th",{scope:"col",class:"px-6 py-3 font-medium"}," Name "),e("th",{scope:"col",class:"px-6 py-3 font-medium hidden sm:table-cell"}," Prefix "),e("th",{scope:"col",class:"px-6 py-3 font-medium hidden xl:table-cell"}," Created "),e("th",{scope:"col",class:"px-6 py-3 font-medium hidden md:table-cell"}," Last Used "),e("th",{scope:"col",class:"px-6 py-3 font-medium hidden lg:table-cell"}," Expires "),e("th",{scope:"col",class:"px-6 py-3 font-medium text-right"}," Actions ")])],-1)),e("tbody",_e,[(o(!0),a(K,null,N(m.value,s=>(o(),a("tr",{key:s.id,class:"hover:bg-surface/50 transition-colors"},[e("td",he,l(s.name||"Unnamed"),1),e("td",we,l(s.prefix?s.prefix+"…":n(O)),1),e("td",ke,l(V(s.created_at)),1),e("td",Ce,[s.last_used_at?(o(),a("span",De,l(n(x)(s.last_used_at)),1)):(o(),a("span",Ke,"Never used"))]),e("td",Ne,[s.expires_at?n(A)(s.expires_at)?(o(),a("span",Ae,"Expired "+l(n(x)(s.expires_at)),1)):(o(),a("span",Pe,l(n(x)(s.expires_at)),1)):(o(),a("span",Ie,"Never"))]),e("td",Te,[c(I,{icon:n(P),variant:"danger",title:"Delete key",onClick:$=>k(s)},null,8,["icon","onClick"])])]))),128)),m.value.length===0?(o(),a("tr",Ee,[...t[13]||(t[13]=[e("td",{colspan:"6",class:"px-6 py-8 text-center text-foreground-muted"},[i(" No API keys yet. Click "),e("span",{class:"text-white"},"New Key"),i(" to generate one. ")],-1)])])):y("",!0)])])])]))}};export{Ge as default}; diff --git a/backend/internal/server/ui_dist/assets/Channels-CnxWaSZb.js b/backend/internal/server/ui_dist/assets/Channels-BVCLEepx.js similarity index 97% rename from backend/internal/server/ui_dist/assets/Channels-CnxWaSZb.js rename to backend/internal/server/ui_dist/assets/Channels-BVCLEepx.js index 84bb26f..7fd4598 100644 --- a/backend/internal/server/ui_dist/assets/Channels-CnxWaSZb.js +++ b/backend/internal/server/ui_dist/assets/Channels-BVCLEepx.js @@ -1 +1 @@ -import{r as f,o as G,W as Z,j as o,a as n,b as e,k as u,d,f as r,aa as H,S as U,e as L,v as E,F as N,t as i,p as F,s as J,w as z,g as v,h as w,_ as C,q as M,C as Y,n as P,a1 as ee,aQ as te,Z as se,B as j,aR as oe,aS as ne,aT as ae}from"./index-D5cO6vit.js";import{_ as I}from"./IconButton-Pc0_zskr.js";import{c as ie}from"./clipboard-CmSw2rR-.js";import{f as $,i as q}from"./time-Cfu9zNbw.js";import{C as le}from"./check-BkPCgKSu.js";import{C as re}from"./copy-Gc8n9M6v.js";import{C as de}from"./circle-alert-BmP6pM7E.js";import{R as O}from"./rotate-ccw-CeRUwJZR.js";import{T as W}from"./trash-2-sSCgxcxW.js";const ue={class:"w-full max-w-2xl bg-background border border-border rounded-lg shadow-lg flex flex-col max-h-[80vh]"},ce={class:"px-5 py-4 border-b border-border flex items-start justify-between gap-3"},pe={class:"px-5 py-3 border-b border-border flex items-center gap-2"},me={class:"flex-1 overflow-y-auto"},xe={key:0,class:"px-5 py-10 text-center text-xs text-foreground-muted italic"},fe={key:1,class:"px-5 py-10 text-center"},ve={class:"text-xs text-foreground-muted"},ge={key:2,class:"divide-y divide-border"},he=["onClick"],ye=["checked","onClick"],be={class:"flex-1 min-w-0"},_e={class:"text-sm font-medium text-white truncate"},ke={key:0,class:"text-xs text-foreground-muted mt-0.5 line-clamp-1"},we={class:"text-[11px] text-foreground-muted font-mono shrink-0"},Ce={class:"px-5 py-3 border-t border-border flex items-center justify-between gap-3"},$e={class:"text-xs text-foreground-muted tabular-nums"},Ne={class:"flex gap-2"},De={__name:"FunctionPickerModal",props:{selected:{type:Array,default:()=>[]}},emits:["close","apply"],setup(T,{emit:D}){const b=T,h=D,g=f([]),_=f(!0),y=f(""),x=f(new Set(b.selected)),k=c=>{const a=new Set(x.value);a.has(c)?a.delete(c):a.add(c),x.value=a},l=M(()=>{const c=y.value.trim().toLowerCase();return c?g.value.filter(a=>a.name.toLowerCase().includes(c)||(a.description||"").toLowerCase().includes(c)||(a.runtime||"").toLowerCase().includes(c)):g.value}),R=()=>{h("apply",Array.from(x.value))};return G(async()=>{try{const c=await Z({limit:200});g.value=c.data.functions||[]}finally{_.value=!1}}),(c,a)=>(o(),n("div",{class:"fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm",onClick:a[3]||(a[3]=z(p=>c.$emit("close"),["self"]))},[e("div",ue,[e("div",ce,[a[4]||(a[4]=e("div",null,[e("div",{class:"text-sm font-semibold text-white"}," Pick functions "),e("div",{class:"text-xs text-foreground-muted mt-0.5 max-w-prose leading-relaxed"},[u(" Each selected function becomes one MCP tool in this channel. Names with dashes are converted to snake_case (e.g. "),e("code",{class:"text-foreground"},"stripe-charge"),u(" → "),e("code",{class:"text-foreground"},"stripe_charge"),u("). ")])],-1)),e("button",{class:"text-foreground-muted hover:text-white transition-colors",title:"Dismiss",onClick:a[0]||(a[0]=p=>c.$emit("close"))},[d(r(H),{class:"w-4 h-4"})])]),e("div",pe,[d(r(U),{class:"w-4 h-4 text-foreground-muted shrink-0"}),L(e("input",{"onUpdate:modelValue":a[1]||(a[1]=p=>y.value=p),type:"text",placeholder:"Filter by name, description, or runtime",class:"flex-1 bg-transparent text-sm text-foreground placeholder-foreground-muted focus:outline-none"},null,512),[[E,y.value]])]),e("div",me,[_.value?(o(),n("div",xe," Loading functions… ")):l.value.length===0?(o(),n("div",fe,[d(r(U),{class:"w-8 h-8 text-foreground-muted mx-auto mb-2 opacity-30"}),e("p",ve,[g.value.length===0?(o(),n(N,{key:0},[u(" No functions deployed yet. ")],64)):(o(),n(N,{key:1},[u(' No functions match "'+i(y.value)+'". ',1)],64))])])):(o(),n("ul",ge,[(o(!0),n(N,null,F(l.value,p=>(o(),n("li",{key:p.id,class:J(["px-5 py-3 flex items-center gap-3 cursor-pointer transition-colors",x.value.has(p.id)?"bg-surface/30 hover:bg-surface/50":"hover:bg-surface/40"]),onClick:S=>k(p.id)},[e("input",{type:"checkbox",checked:x.value.has(p.id),class:"accent-primary cursor-pointer",onClick:z(S=>k(p.id),["stop"])},null,8,ye),e("div",be,[e("div",_e,i(p.name),1),p.description?(o(),n("div",ke,i(p.description),1)):v("",!0)]),e("code",we,i(p.runtime),1)],10,he))),128))]))]),e("div",Ce,[e("div",$e,i(x.value.size)+" of "+i(g.value.length)+" selected ",1),e("div",Ne,[d(C,{variant:"secondary",onClick:a[2]||(a[2]=p=>c.$emit("close"))},{default:w(()=>[...a[5]||(a[5]=[u(" Cancel ",-1)])]),_:1}),d(C,{disabled:x.value.size===0,onClick:R},{default:w(()=>[...a[6]||(a[6]=[u(" Apply ",-1)])]),_:1},8,["disabled"])])])])]))}},Ie={class:"space-y-6"},Le={class:"flex items-center justify-between gap-4"},Re={key:0,class:"bg-background border border-warning-ring rounded-lg p-4 space-y-3"},Se={class:"flex items-start justify-between gap-3"},Ae={class:"flex items-center gap-2"},Pe={class:"flex-1 font-mono text-sm text-white break-all bg-surface px-3 py-2 rounded border border-border"},je={class:"text-[11px] text-foreground-muted flex flex-wrap items-center gap-x-3 gap-y-1"},Ee={class:"text-foreground bg-surface px-1.5 py-0.5 rounded"},Fe={key:1,class:"bg-background border border-border rounded-lg p-5 space-y-4"},Me={class:"grid grid-cols-1 md:grid-cols-2 gap-3"},Te={class:"flex items-center justify-between mb-1.5"},Be={key:0,class:"text-[11px] text-foreground-muted"},Ve={key:0,class:"rounded-md border border-red-700/40 bg-red-950/30 p-3 text-xs text-red-200 flex items-start gap-2"},Ue={class:"flex gap-2 pt-1"},ze={class:"bg-background border border-border rounded-lg overflow-x-auto"},qe={class:"sm:hidden divide-y divide-border"},Oe={class:"flex items-start justify-between gap-2"},We={class:"min-w-0 flex-1"},Ge={class:"flex items-center gap-2 flex-wrap"},He={class:"font-medium text-white truncate"},Ke={class:"inline-flex items-center gap-1 text-[11px] text-foreground-muted"},Qe={key:0,class:"mt-1 text-xs text-foreground-muted line-clamp-2"},Xe={class:"mt-1 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[11px] text-foreground-muted"},Ze={class:"font-mono"},Je={key:0},Ye={key:1,class:"text-amber-400/80"},et={key:2,class:"text-red-400"},tt={key:3},st={class:"flex items-center gap-1 shrink-0"},ot={key:0,class:"px-6 py-8 text-center text-sm text-foreground-muted"},nt={class:"hidden sm:table w-full text-sm text-left"},at={class:"divide-y divide-border"},it={class:"px-6 py-4"},lt={class:"font-medium text-white"},rt={key:0,class:"text-xs text-foreground-muted mt-0.5 line-clamp-1 max-w-md"},dt={class:"px-6 py-4"},ut={class:"inline-flex items-center gap-1.5 text-foreground-muted"},ct={class:"tabular-nums"},pt={class:"px-6 py-4 hidden sm:table-cell"},mt={class:"text-foreground-muted font-mono text-xs"},xt={class:"px-6 py-4 hidden md:table-cell"},ft={key:0,class:"text-foreground-muted"},vt={key:1,class:"text-amber-400/70 text-xs"},gt={class:"px-6 py-4 hidden lg:table-cell"},ht={key:0,class:"text-foreground-muted"},yt={key:1,class:"text-red-400 text-xs"},bt={key:2,class:"text-foreground-muted"},_t={class:"px-6 py-4 text-right"},kt={class:"inline-flex justify-end gap-1"},wt={key:0},Pt={__name:"Channels",setup(T){const D=Y(),b=f([]),h=f(""),g=f(!1),_=f(!1),y=f(!1),x=f(""),k=f(!1),l=f({name:"",description:"",expiresInDays:0,functionIds:[]}),R=M(()=>`${window.location.origin}/mcp`),c=M(()=>l.value.name.trim()&&l.value.functionIds.length>0),a=async()=>{const m=await te();b.value=m.data.channels||[]},p=()=>{l.value={name:"",description:"",expiresInDays:0,functionIds:[]},x.value="",_.value=!0},S=()=>{_.value=!1},K=m=>{l.value.functionIds=m,k.value=!1},Q=async()=>{y.value=!0,x.value="";try{const m={name:l.value.name.trim(),description:l.value.description.trim(),function_ids:l.value.functionIds};l.value.expiresInDays>0&&(m.expires_in_days=l.value.expiresInDays);const t=await oe(m);h.value=t.data.token,_.value=!1,await a()}catch(m){x.value=m?.response?.data?.error?.message||"Failed to create channel."}finally{y.value=!1}},X=async()=>{h.value&&await ie(h.value)&&(g.value=!0,setTimeout(()=>{g.value=!1},1500))},B=async m=>{if(!await D.ask({title:`Rotate ${m.name}?`,message:"A new token will be issued. The previous token stops working immediately. Agents using it will need the new value.",confirmLabel:"Rotate",danger:!0}))return;const s=await ne(m.id);h.value=s.data.token,await a()},V=async m=>{await D.ask({title:`Delete ${m.name}?`,message:`${m.name} will lose MCP access immediately. Functions inside are not affected. Re-create the channel if you need it again.`,confirmLabel:"Delete",danger:!0})&&(await ae(m.id),await a())};return G(a),(m,t)=>(o(),n("div",Ie,[e("div",Le,[t[7]||(t[7]=e("div",null,[e("h1",{class:"text-xl font-semibold text-white tracking-tight"}," Channels "),e("p",{class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"}," Bundle deployed functions and expose them as MCP tools to a third-party agent. Each channel has its own bearer token that grants invoke-only access to its functions and nothing else on Orva, but the bundled functions themselves remain as powerful as you've configured them, including any in-sandbox SDK calls they make. ")],-1)),d(C,{onClick:p},{default:w(()=>[d(r(se),{class:"w-4 h-4"}),t[6]||(t[6]=u(" New channel ",-1))]),_:1})]),h.value?(o(),n("div",Re,[e("div",Se,[t[8]||(t[8]=e("div",null,[e("h2",{class:"text-xs font-bold text-warning-fg uppercase tracking-wider"}," Copy this token now "),e("div",{class:"text-xs text-foreground-muted mt-0.5"}," It will not be shown again. Configure it in your agent's MCP client. ")],-1)),e("button",{class:"text-foreground-muted hover:text-white transition-colors",title:"Dismiss",onClick:t[0]||(t[0]=s=>h.value="")},[d(r(H),{class:"w-4 h-4"})])]),e("div",Ae,[e("code",Pe,i(h.value),1),e("button",{class:"px-3 py-2 rounded-md border border-border bg-surface-hover hover:bg-surface text-foreground-muted hover:text-white transition-colors flex items-center gap-1.5 text-xs",onClick:X},[g.value?(o(),P(r(le),{key:0,class:"w-3.5 h-3.5 text-success"})):(o(),P(r(re),{key:1,class:"w-3.5 h-3.5"})),u(" "+i(g.value?"Copied":"Copy"),1)])]),e("div",je,[e("span",null,[t[9]||(t[9]=u("URL ",-1)),e("code",Ee,i(R.value),1)]),t[10]||(t[10]=e("span",null,[u("Header "),e("code",{class:"text-foreground bg-surface px-1.5 py-0.5 rounded"},"Authorization: Bearer ")],-1))])])):v("",!0),_.value?(o(),n("div",Fe,[t[18]||(t[18]=e("div",{class:"text-sm font-semibold text-white"}," New channel ",-1)),e("div",Me,[e("div",null,[t[11]||(t[11]=e("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-1.5"},"Name",-1)),L(e("input",{"onUpdate:modelValue":t[1]||(t[1]=s=>l.value.name=s),placeholder:"e.g. support-bot",class:"w-full bg-surface-hover border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-primary transition-colors"},null,512),[[E,l.value.name]])]),e("div",null,[t[13]||(t[13]=e("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-1.5"},"Expires in",-1)),L(e("select",{"onUpdate:modelValue":t[2]||(t[2]=s=>l.value.expiresInDays=s),class:"w-full bg-surface-hover border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-primary transition-colors"},[...t[12]||(t[12]=[e("option",{value:0}," Never ",-1),e("option",{value:7}," 7 days ",-1),e("option",{value:30}," 30 days ",-1),e("option",{value:90}," 90 days ",-1)])],512),[[ee,l.value.expiresInDays]])])]),e("div",null,[t[14]||(t[14]=e("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-1.5"},"Description (optional)",-1)),L(e("input",{"onUpdate:modelValue":t[3]||(t[3]=s=>l.value.description=s),placeholder:"What this channel is for",class:"w-full bg-surface-hover border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-primary transition-colors"},null,512),[[E,l.value.description]])]),e("div",null,[e("div",Te,[t[15]||(t[15]=e("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide"},"Functions",-1)),l.value.functionIds.length>0?(o(),n("span",Be,i(l.value.functionIds.length)+" selected",1)):v("",!0)]),d(C,{variant:"secondary",onClick:t[4]||(t[4]=s=>k.value=!0)},{default:w(()=>[d(r(j),{class:"w-4 h-4"}),u(" "+i(l.value.functionIds.length===0?"Pick functions":"Edit selection"),1)]),_:1})]),x.value?(o(),n("div",Ve,[d(r(de),{class:"w-4 h-4 text-red-400 shrink-0 mt-0.5"}),e("span",null,i(x.value),1)])):v("",!0),e("div",Ue,[d(C,{disabled:!c.value||y.value,loading:y.value,onClick:Q},{default:w(()=>[...t[16]||(t[16]=[u(" Generate token ",-1)])]),_:1},8,["disabled","loading"]),d(C,{variant:"secondary",onClick:S},{default:w(()=>[...t[17]||(t[17]=[u(" Cancel ",-1)])]),_:1})])])):v("",!0),e("div",ze,[e("ul",qe,[(o(!0),n(N,null,F(b.value,s=>(o(),n("li",{key:s.id,class:"px-4 py-3"},[e("div",Oe,[e("div",We,[e("div",Ge,[e("span",He,i(s.name),1),e("span",Ke,[d(r(j),{class:"w-3 h-3"}),u(" "+i(s.function_count),1)])]),s.description?(o(),n("div",Qe,i(s.description),1)):v("",!0),e("div",Xe,[e("code",Ze,i(s.prefix)+"…",1),s.last_used_at?(o(),n("span",Je,"used "+i(r($)(s.last_used_at)),1)):(o(),n("span",Ye,"never used")),s.expires_at&&r(q)(s.expires_at)?(o(),n("span",et,"expired")):s.expires_at?(o(),n("span",tt,"expires "+i(r($)(s.expires_at)),1)):v("",!0)])]),e("div",st,[d(I,{icon:r(O),title:"Rotate token",onClick:A=>B(s)},null,8,["icon","onClick"]),d(I,{icon:r(W),variant:"danger",title:"Delete channel",onClick:A=>V(s)},null,8,["icon","onClick"])])])]))),128)),b.value.length===0?(o(),n("li",ot," No channels yet. ")):v("",!0)]),e("table",nt,[t[20]||(t[20]=e("thead",{class:"text-xs text-foreground-muted uppercase bg-surface border-b border-border"},[e("tr",null,[e("th",{class:"px-6 py-3 font-medium"}," Name "),e("th",{class:"px-6 py-3 font-medium"}," Functions "),e("th",{class:"px-6 py-3 font-medium hidden sm:table-cell"}," Prefix "),e("th",{class:"px-6 py-3 font-medium hidden md:table-cell"}," Last used "),e("th",{class:"px-6 py-3 font-medium hidden lg:table-cell"}," Expires "),e("th",{class:"px-6 py-3 font-medium text-right"}," Actions ")])],-1)),e("tbody",at,[(o(!0),n(N,null,F(b.value,s=>(o(),n("tr",{key:s.id,class:"hover:bg-surface/50 transition-colors"},[e("td",it,[e("div",lt,i(s.name),1),s.description?(o(),n("div",rt,i(s.description),1)):v("",!0)]),e("td",dt,[e("span",ut,[d(r(j),{class:"w-3.5 h-3.5"}),e("span",ct,i(s.function_count),1)])]),e("td",pt,[e("code",mt,i(s.prefix)+"…",1)]),e("td",xt,[s.last_used_at?(o(),n("span",ft,i(r($)(s.last_used_at)),1)):(o(),n("span",vt,"Never used"))]),e("td",gt,[s.expires_at?r(q)(s.expires_at)?(o(),n("span",yt,"Expired "+i(r($)(s.expires_at)),1)):(o(),n("span",bt,i(r($)(s.expires_at)),1)):(o(),n("span",ht,"Never"))]),e("td",_t,[e("div",kt,[d(I,{icon:r(O),title:"Rotate token",onClick:A=>B(s)},null,8,["icon","onClick"]),d(I,{icon:r(W),variant:"danger",title:"Delete channel",onClick:A=>V(s)},null,8,["icon","onClick"])])])]))),128)),b.value.length===0?(o(),n("tr",wt,[...t[19]||(t[19]=[e("td",{colspan:"6",class:"px-6 py-8 text-center text-foreground-muted"},[u(" No channels yet. Click "),e("span",{class:"text-white"},"New channel"),u(" to bundle functions for an agent. ")],-1)])])):v("",!0)])])]),k.value?(o(),P(De,{key:2,selected:l.value.functionIds,onClose:t[5]||(t[5]=s=>k.value=!1),onApply:K},null,8,["selected"])):v("",!0)]))}};export{Pt as default}; +import{r as f,o as G,W as Z,j as o,a as n,b as e,k as u,d,f as r,aa as H,S as U,e as L,v as E,F as N,t as i,p as F,s as J,w as z,g as v,h as w,_ as C,q as M,C as Y,n as P,a1 as ee,aQ as te,Z as se,B as j,aR as oe,aS as ne,aT as ae}from"./index-BMkkwZ9q.js";import{_ as I}from"./IconButton-BgeMzwXv.js";import{c as ie}from"./clipboard-CmSw2rR-.js";import{f as $,i as q}from"./time-Cfu9zNbw.js";import{C as le}from"./check-C4wzjDZN.js";import{C as re}from"./copy-CTb6u-fx.js";import{C as de}from"./circle-alert-DJMgVejj.js";import{R as O}from"./rotate-ccw-CsgWy1Bs.js";import{T as W}from"./trash-2-BXf2uqQH.js";const ue={class:"w-full max-w-2xl bg-background border border-border rounded-lg shadow-lg flex flex-col max-h-[80vh]"},ce={class:"px-5 py-4 border-b border-border flex items-start justify-between gap-3"},pe={class:"px-5 py-3 border-b border-border flex items-center gap-2"},me={class:"flex-1 overflow-y-auto"},xe={key:0,class:"px-5 py-10 text-center text-xs text-foreground-muted italic"},fe={key:1,class:"px-5 py-10 text-center"},ve={class:"text-xs text-foreground-muted"},ge={key:2,class:"divide-y divide-border"},he=["onClick"],ye=["checked","onClick"],be={class:"flex-1 min-w-0"},_e={class:"text-sm font-medium text-white truncate"},ke={key:0,class:"text-xs text-foreground-muted mt-0.5 line-clamp-1"},we={class:"text-[11px] text-foreground-muted font-mono shrink-0"},Ce={class:"px-5 py-3 border-t border-border flex items-center justify-between gap-3"},$e={class:"text-xs text-foreground-muted tabular-nums"},Ne={class:"flex gap-2"},De={__name:"FunctionPickerModal",props:{selected:{type:Array,default:()=>[]}},emits:["close","apply"],setup(T,{emit:D}){const b=T,h=D,g=f([]),_=f(!0),y=f(""),x=f(new Set(b.selected)),k=c=>{const a=new Set(x.value);a.has(c)?a.delete(c):a.add(c),x.value=a},l=M(()=>{const c=y.value.trim().toLowerCase();return c?g.value.filter(a=>a.name.toLowerCase().includes(c)||(a.description||"").toLowerCase().includes(c)||(a.runtime||"").toLowerCase().includes(c)):g.value}),R=()=>{h("apply",Array.from(x.value))};return G(async()=>{try{const c=await Z({limit:200});g.value=c.data.functions||[]}finally{_.value=!1}}),(c,a)=>(o(),n("div",{class:"fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm",onClick:a[3]||(a[3]=z(p=>c.$emit("close"),["self"]))},[e("div",ue,[e("div",ce,[a[4]||(a[4]=e("div",null,[e("div",{class:"text-sm font-semibold text-white"}," Pick functions "),e("div",{class:"text-xs text-foreground-muted mt-0.5 max-w-prose leading-relaxed"},[u(" Each selected function becomes one MCP tool in this channel. Names with dashes are converted to snake_case (e.g. "),e("code",{class:"text-foreground"},"stripe-charge"),u(" → "),e("code",{class:"text-foreground"},"stripe_charge"),u("). ")])],-1)),e("button",{class:"text-foreground-muted hover:text-white transition-colors",title:"Dismiss",onClick:a[0]||(a[0]=p=>c.$emit("close"))},[d(r(H),{class:"w-4 h-4"})])]),e("div",pe,[d(r(U),{class:"w-4 h-4 text-foreground-muted shrink-0"}),L(e("input",{"onUpdate:modelValue":a[1]||(a[1]=p=>y.value=p),type:"text",placeholder:"Filter by name, description, or runtime",class:"flex-1 bg-transparent text-sm text-foreground placeholder-foreground-muted focus:outline-none"},null,512),[[E,y.value]])]),e("div",me,[_.value?(o(),n("div",xe," Loading functions… ")):l.value.length===0?(o(),n("div",fe,[d(r(U),{class:"w-8 h-8 text-foreground-muted mx-auto mb-2 opacity-30"}),e("p",ve,[g.value.length===0?(o(),n(N,{key:0},[u(" No functions deployed yet. ")],64)):(o(),n(N,{key:1},[u(' No functions match "'+i(y.value)+'". ',1)],64))])])):(o(),n("ul",ge,[(o(!0),n(N,null,F(l.value,p=>(o(),n("li",{key:p.id,class:J(["px-5 py-3 flex items-center gap-3 cursor-pointer transition-colors",x.value.has(p.id)?"bg-surface/30 hover:bg-surface/50":"hover:bg-surface/40"]),onClick:S=>k(p.id)},[e("input",{type:"checkbox",checked:x.value.has(p.id),class:"accent-primary cursor-pointer",onClick:z(S=>k(p.id),["stop"])},null,8,ye),e("div",be,[e("div",_e,i(p.name),1),p.description?(o(),n("div",ke,i(p.description),1)):v("",!0)]),e("code",we,i(p.runtime),1)],10,he))),128))]))]),e("div",Ce,[e("div",$e,i(x.value.size)+" of "+i(g.value.length)+" selected ",1),e("div",Ne,[d(C,{variant:"secondary",onClick:a[2]||(a[2]=p=>c.$emit("close"))},{default:w(()=>[...a[5]||(a[5]=[u(" Cancel ",-1)])]),_:1}),d(C,{disabled:x.value.size===0,onClick:R},{default:w(()=>[...a[6]||(a[6]=[u(" Apply ",-1)])]),_:1},8,["disabled"])])])])]))}},Ie={class:"space-y-6"},Le={class:"flex items-center justify-between gap-4"},Re={key:0,class:"bg-background border border-warning-ring rounded-lg p-4 space-y-3"},Se={class:"flex items-start justify-between gap-3"},Ae={class:"flex items-center gap-2"},Pe={class:"flex-1 font-mono text-sm text-white break-all bg-surface px-3 py-2 rounded border border-border"},je={class:"text-[11px] text-foreground-muted flex flex-wrap items-center gap-x-3 gap-y-1"},Ee={class:"text-foreground bg-surface px-1.5 py-0.5 rounded"},Fe={key:1,class:"bg-background border border-border rounded-lg p-5 space-y-4"},Me={class:"grid grid-cols-1 md:grid-cols-2 gap-3"},Te={class:"flex items-center justify-between mb-1.5"},Be={key:0,class:"text-[11px] text-foreground-muted"},Ve={key:0,class:"rounded-md border border-red-700/40 bg-red-950/30 p-3 text-xs text-red-200 flex items-start gap-2"},Ue={class:"flex gap-2 pt-1"},ze={class:"bg-background border border-border rounded-lg overflow-x-auto"},qe={class:"sm:hidden divide-y divide-border"},Oe={class:"flex items-start justify-between gap-2"},We={class:"min-w-0 flex-1"},Ge={class:"flex items-center gap-2 flex-wrap"},He={class:"font-medium text-white truncate"},Ke={class:"inline-flex items-center gap-1 text-[11px] text-foreground-muted"},Qe={key:0,class:"mt-1 text-xs text-foreground-muted line-clamp-2"},Xe={class:"mt-1 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[11px] text-foreground-muted"},Ze={class:"font-mono"},Je={key:0},Ye={key:1,class:"text-amber-400/80"},et={key:2,class:"text-red-400"},tt={key:3},st={class:"flex items-center gap-1 shrink-0"},ot={key:0,class:"px-6 py-8 text-center text-sm text-foreground-muted"},nt={class:"hidden sm:table w-full text-sm text-left"},at={class:"divide-y divide-border"},it={class:"px-6 py-4"},lt={class:"font-medium text-white"},rt={key:0,class:"text-xs text-foreground-muted mt-0.5 line-clamp-1 max-w-md"},dt={class:"px-6 py-4"},ut={class:"inline-flex items-center gap-1.5 text-foreground-muted"},ct={class:"tabular-nums"},pt={class:"px-6 py-4 hidden sm:table-cell"},mt={class:"text-foreground-muted font-mono text-xs"},xt={class:"px-6 py-4 hidden md:table-cell"},ft={key:0,class:"text-foreground-muted"},vt={key:1,class:"text-amber-400/70 text-xs"},gt={class:"px-6 py-4 hidden lg:table-cell"},ht={key:0,class:"text-foreground-muted"},yt={key:1,class:"text-red-400 text-xs"},bt={key:2,class:"text-foreground-muted"},_t={class:"px-6 py-4 text-right"},kt={class:"inline-flex justify-end gap-1"},wt={key:0},Pt={__name:"Channels",setup(T){const D=Y(),b=f([]),h=f(""),g=f(!1),_=f(!1),y=f(!1),x=f(""),k=f(!1),l=f({name:"",description:"",expiresInDays:0,functionIds:[]}),R=M(()=>`${window.location.origin}/mcp`),c=M(()=>l.value.name.trim()&&l.value.functionIds.length>0),a=async()=>{const m=await te();b.value=m.data.channels||[]},p=()=>{l.value={name:"",description:"",expiresInDays:0,functionIds:[]},x.value="",_.value=!0},S=()=>{_.value=!1},K=m=>{l.value.functionIds=m,k.value=!1},Q=async()=>{y.value=!0,x.value="";try{const m={name:l.value.name.trim(),description:l.value.description.trim(),function_ids:l.value.functionIds};l.value.expiresInDays>0&&(m.expires_in_days=l.value.expiresInDays);const t=await oe(m);h.value=t.data.token,_.value=!1,await a()}catch(m){x.value=m?.response?.data?.error?.message||"Failed to create channel."}finally{y.value=!1}},X=async()=>{h.value&&await ie(h.value)&&(g.value=!0,setTimeout(()=>{g.value=!1},1500))},B=async m=>{if(!await D.ask({title:`Rotate ${m.name}?`,message:"A new token will be issued. The previous token stops working immediately. Agents using it will need the new value.",confirmLabel:"Rotate",danger:!0}))return;const s=await ne(m.id);h.value=s.data.token,await a()},V=async m=>{await D.ask({title:`Delete ${m.name}?`,message:`${m.name} will lose MCP access immediately. Functions inside are not affected. Re-create the channel if you need it again.`,confirmLabel:"Delete",danger:!0})&&(await ae(m.id),await a())};return G(a),(m,t)=>(o(),n("div",Ie,[e("div",Le,[t[7]||(t[7]=e("div",null,[e("h1",{class:"text-xl font-semibold text-white tracking-tight"}," Channels "),e("p",{class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"}," Bundle deployed functions and expose them as MCP tools to a third-party agent. Each channel has its own bearer token that grants invoke-only access to its functions and nothing else on Orva, but the bundled functions themselves remain as powerful as you've configured them, including any in-sandbox SDK calls they make. ")],-1)),d(C,{onClick:p},{default:w(()=>[d(r(se),{class:"w-4 h-4"}),t[6]||(t[6]=u(" New channel ",-1))]),_:1})]),h.value?(o(),n("div",Re,[e("div",Se,[t[8]||(t[8]=e("div",null,[e("h2",{class:"text-xs font-bold text-warning-fg uppercase tracking-wider"}," Copy this token now "),e("div",{class:"text-xs text-foreground-muted mt-0.5"}," It will not be shown again. Configure it in your agent's MCP client. ")],-1)),e("button",{class:"text-foreground-muted hover:text-white transition-colors",title:"Dismiss",onClick:t[0]||(t[0]=s=>h.value="")},[d(r(H),{class:"w-4 h-4"})])]),e("div",Ae,[e("code",Pe,i(h.value),1),e("button",{class:"px-3 py-2 rounded-md border border-border bg-surface-hover hover:bg-surface text-foreground-muted hover:text-white transition-colors flex items-center gap-1.5 text-xs",onClick:X},[g.value?(o(),P(r(le),{key:0,class:"w-3.5 h-3.5 text-success"})):(o(),P(r(re),{key:1,class:"w-3.5 h-3.5"})),u(" "+i(g.value?"Copied":"Copy"),1)])]),e("div",je,[e("span",null,[t[9]||(t[9]=u("URL ",-1)),e("code",Ee,i(R.value),1)]),t[10]||(t[10]=e("span",null,[u("Header "),e("code",{class:"text-foreground bg-surface px-1.5 py-0.5 rounded"},"Authorization: Bearer ")],-1))])])):v("",!0),_.value?(o(),n("div",Fe,[t[18]||(t[18]=e("div",{class:"text-sm font-semibold text-white"}," New channel ",-1)),e("div",Me,[e("div",null,[t[11]||(t[11]=e("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-1.5"},"Name",-1)),L(e("input",{"onUpdate:modelValue":t[1]||(t[1]=s=>l.value.name=s),placeholder:"e.g. support-bot",class:"w-full bg-surface-hover border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-primary transition-colors"},null,512),[[E,l.value.name]])]),e("div",null,[t[13]||(t[13]=e("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-1.5"},"Expires in",-1)),L(e("select",{"onUpdate:modelValue":t[2]||(t[2]=s=>l.value.expiresInDays=s),class:"w-full bg-surface-hover border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-primary transition-colors"},[...t[12]||(t[12]=[e("option",{value:0}," Never ",-1),e("option",{value:7}," 7 days ",-1),e("option",{value:30}," 30 days ",-1),e("option",{value:90}," 90 days ",-1)])],512),[[ee,l.value.expiresInDays]])])]),e("div",null,[t[14]||(t[14]=e("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-1.5"},"Description (optional)",-1)),L(e("input",{"onUpdate:modelValue":t[3]||(t[3]=s=>l.value.description=s),placeholder:"What this channel is for",class:"w-full bg-surface-hover border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-primary transition-colors"},null,512),[[E,l.value.description]])]),e("div",null,[e("div",Te,[t[15]||(t[15]=e("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide"},"Functions",-1)),l.value.functionIds.length>0?(o(),n("span",Be,i(l.value.functionIds.length)+" selected",1)):v("",!0)]),d(C,{variant:"secondary",onClick:t[4]||(t[4]=s=>k.value=!0)},{default:w(()=>[d(r(j),{class:"w-4 h-4"}),u(" "+i(l.value.functionIds.length===0?"Pick functions":"Edit selection"),1)]),_:1})]),x.value?(o(),n("div",Ve,[d(r(de),{class:"w-4 h-4 text-red-400 shrink-0 mt-0.5"}),e("span",null,i(x.value),1)])):v("",!0),e("div",Ue,[d(C,{disabled:!c.value||y.value,loading:y.value,onClick:Q},{default:w(()=>[...t[16]||(t[16]=[u(" Generate token ",-1)])]),_:1},8,["disabled","loading"]),d(C,{variant:"secondary",onClick:S},{default:w(()=>[...t[17]||(t[17]=[u(" Cancel ",-1)])]),_:1})])])):v("",!0),e("div",ze,[e("ul",qe,[(o(!0),n(N,null,F(b.value,s=>(o(),n("li",{key:s.id,class:"px-4 py-3"},[e("div",Oe,[e("div",We,[e("div",Ge,[e("span",He,i(s.name),1),e("span",Ke,[d(r(j),{class:"w-3 h-3"}),u(" "+i(s.function_count),1)])]),s.description?(o(),n("div",Qe,i(s.description),1)):v("",!0),e("div",Xe,[e("code",Ze,i(s.prefix)+"…",1),s.last_used_at?(o(),n("span",Je,"used "+i(r($)(s.last_used_at)),1)):(o(),n("span",Ye,"never used")),s.expires_at&&r(q)(s.expires_at)?(o(),n("span",et,"expired")):s.expires_at?(o(),n("span",tt,"expires "+i(r($)(s.expires_at)),1)):v("",!0)])]),e("div",st,[d(I,{icon:r(O),title:"Rotate token",onClick:A=>B(s)},null,8,["icon","onClick"]),d(I,{icon:r(W),variant:"danger",title:"Delete channel",onClick:A=>V(s)},null,8,["icon","onClick"])])])]))),128)),b.value.length===0?(o(),n("li",ot," No channels yet. ")):v("",!0)]),e("table",nt,[t[20]||(t[20]=e("thead",{class:"text-xs text-foreground-muted uppercase bg-surface border-b border-border"},[e("tr",null,[e("th",{class:"px-6 py-3 font-medium"}," Name "),e("th",{class:"px-6 py-3 font-medium"}," Functions "),e("th",{class:"px-6 py-3 font-medium hidden sm:table-cell"}," Prefix "),e("th",{class:"px-6 py-3 font-medium hidden md:table-cell"}," Last used "),e("th",{class:"px-6 py-3 font-medium hidden lg:table-cell"}," Expires "),e("th",{class:"px-6 py-3 font-medium text-right"}," Actions ")])],-1)),e("tbody",at,[(o(!0),n(N,null,F(b.value,s=>(o(),n("tr",{key:s.id,class:"hover:bg-surface/50 transition-colors"},[e("td",it,[e("div",lt,i(s.name),1),s.description?(o(),n("div",rt,i(s.description),1)):v("",!0)]),e("td",dt,[e("span",ut,[d(r(j),{class:"w-3.5 h-3.5"}),e("span",ct,i(s.function_count),1)])]),e("td",pt,[e("code",mt,i(s.prefix)+"…",1)]),e("td",xt,[s.last_used_at?(o(),n("span",ft,i(r($)(s.last_used_at)),1)):(o(),n("span",vt,"Never used"))]),e("td",gt,[s.expires_at?r(q)(s.expires_at)?(o(),n("span",yt,"Expired "+i(r($)(s.expires_at)),1)):(o(),n("span",bt,i(r($)(s.expires_at)),1)):(o(),n("span",ht,"Never"))]),e("td",_t,[e("div",kt,[d(I,{icon:r(O),title:"Rotate token",onClick:A=>B(s)},null,8,["icon","onClick"]),d(I,{icon:r(W),variant:"danger",title:"Delete channel",onClick:A=>V(s)},null,8,["icon","onClick"])])])]))),128)),b.value.length===0?(o(),n("tr",wt,[...t[19]||(t[19]=[e("td",{colspan:"6",class:"px-6 py-8 text-center text-foreground-muted"},[u(" No channels yet. Click "),e("span",{class:"text-white"},"New channel"),u(" to bundle functions for an agent. ")],-1)])])):v("",!0)])])]),k.value?(o(),P(De,{key:2,selected:l.value.functionIds,onClose:t[5]||(t[5]=s=>k.value=!1),onApply:K},null,8,["selected"])):v("",!0)]))}};export{Pt as default}; diff --git a/backend/internal/server/ui_dist/assets/CodeEditor-Davda7hv.js b/backend/internal/server/ui_dist/assets/CodeEditor-1BBBEH1J.js similarity index 58% rename from backend/internal/server/ui_dist/assets/CodeEditor-Davda7hv.js rename to backend/internal/server/ui_dist/assets/CodeEditor-1BBBEH1J.js index 0fca784..190b930 100644 --- a/backend/internal/server/ui_dist/assets/CodeEditor-Davda7hv.js +++ b/backend/internal/server/ui_dist/assets/CodeEditor-1BBBEH1J.js @@ -1 +1 @@ -import{a as d,p as m,q as g,C as h,E as n,r as y,u as l}from"./index-BOWx3BJu.js";import{D as _,o as x,y as S,E as c,j as E,a as v,r as w}from"./index-D5cO6vit.js";const C={__name:"CodeEditor",props:{modelValue:{type:String,default:""},language:{type:String,default:"javascript"},readOnly:{type:Boolean,default:!1}},emits:["update:modelValue"],setup(p,{emit:f}){const a=p,u=f,s=w(null);let t=null;const r=new h,i=e=>e?.startsWith("python")?y():e?.startsWith("node")||e==="javascript"?l():l();return x(()=>{const e=d.create({doc:a.modelValue,extensions:[m,r.of(i(a.language)),g,n.updateListener.of(o=>{o.docChanged&&u("update:modelValue",o.state.doc.toString())}),n.theme({"&":{fontSize:"16px",height:"100%"},"@media (min-width: 640px)":{"&":{fontSize:"14px"}},".cm-scroller":{fontFamily:"JetBrains Mono, monospace",lineHeight:"1.6"},".cm-content":{padding:"16px 0"},".cm-line":{padding:"0 16px"}}),d.readOnly.of(a.readOnly)]});t=new n({state:e,parent:s.value})}),S(()=>{t&&t.destroy()}),c(()=>a.modelValue,e=>{t&&e!==t.state.doc.toString()&&t.dispatch({changes:{from:0,to:t.state.doc.length,insert:e}})}),c(()=>a.language,e=>{t&&t.dispatch({effects:r.reconfigure(i(e))})}),(e,o)=>(E(),v("div",{ref_key:"editorRef",ref:s,class:"h-full w-full"},null,512))}},k=_(C,[["__scopeId","data-v-7991b3c8"]]);export{k as default}; +import{a as c,p as m,q as g,C as h,E as n,r as y,u as d}from"./index-BOWx3BJu.js";import{D as _,o as x,y as S,E as l,j as E,a as v,r as w}from"./index-BMkkwZ9q.js";const C={__name:"CodeEditor",props:{modelValue:{type:String,default:""},language:{type:String,default:"javascript"},readOnly:{type:Boolean,default:!1}},emits:["update:modelValue"],setup(p,{emit:f}){const a=p,u=f,s=w(null);let t=null;const r=new h,i=e=>e?.startsWith("python")?y():e?.startsWith("node")||e==="javascript"?d():d();return x(()=>{const e=c.create({doc:a.modelValue,extensions:[m,r.of(i(a.language)),g,n.updateListener.of(o=>{o.docChanged&&u("update:modelValue",o.state.doc.toString())}),n.theme({"&":{fontSize:"16px",height:"100%"},"@media (min-width: 640px)":{"&":{fontSize:"14px"}},".cm-scroller":{fontFamily:"JetBrains Mono, monospace",lineHeight:"1.6"},".cm-content":{padding:"16px 0"},".cm-line":{padding:"0 16px"}}),c.readOnly.of(a.readOnly)]});t=new n({state:e,parent:s.value})}),S(()=>{t&&t.destroy()}),l(()=>a.modelValue,e=>{t&&e!==t.state.doc.toString()&&t.dispatch({changes:{from:0,to:t.state.doc.length,insert:e}})}),l(()=>a.language,e=>{t&&t.dispatch({effects:r.reconfigure(i(e))})}),(e,o)=>(E(),v("div",{ref_key:"editorRef",ref:s,class:"h-full w-full"},null,512))}},k=_(C,[["__scopeId","data-v-ccca551c"]]);export{k as default}; diff --git a/backend/internal/server/ui_dist/assets/CronJobs-KVuRxoUt.js b/backend/internal/server/ui_dist/assets/CronJobs-C1Lm7OXf.js similarity index 98% rename from backend/internal/server/ui_dist/assets/CronJobs-KVuRxoUt.js rename to backend/internal/server/ui_dist/assets/CronJobs-C1Lm7OXf.js index c7c9941..4b75404 100644 --- a/backend/internal/server/ui_dist/assets/CronJobs-KVuRxoUt.js +++ b/backend/internal/server/ui_dist/assets/CronJobs-C1Lm7OXf.js @@ -1 +1 @@ -import{c as X,as as oe,C as ne,o as se,a as l,b as e,d as c,h as _,_ as O,F as C,p as S,f as a,g as x,at as re,W as ae,r as v,j as d,k as m,t as r,s as q,n as J,H as R,e as f,a1 as F,v as U,ab as de,au as H,av as le,aw as ue}from"./index-D5cO6vit.js";import{E as P}from"./format-CsU4_SPu.js";import{_ as y}from"./IconButton-Pc0_zskr.js";import{_ as ie}from"./Modal-C-qDVd6z.js";import{a as D,C as Y}from"./clock-ARFGKas-.js";import{P as G}from"./play-BfsTXwNm.js";import{S as K}from"./square-pen-WhijiULo.js";import{T as Z}from"./trash-2-sSCgxcxW.js";const ce=X("circle-plus",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M8 12h8",key:"1wcyev"}],["path",{d:"M12 8v8",key:"napkw2"}]]);const Q=X("pause",[["rect",{x:"14",y:"3",width:"5",height:"18",rx:"1",key:"kaeet6"}],["rect",{x:"5",y:"3",width:"5",height:"18",rx:"1",key:"1wsw3u"}]]),me={class:"space-y-6"},fe={class:"flex items-center justify-between"},pe={class:"bg-background border border-border rounded-lg overflow-x-auto"},xe={class:"sm:hidden divide-y divide-border"},ge={class:"flex items-start justify-between gap-2"},ve={class:"min-w-0 flex-1"},be={class:"flex items-center gap-2 flex-wrap"},ye={class:"font-medium text-foreground truncate"},he={class:"mt-1 text-[11px] text-foreground font-mono break-all"},ke={class:"mt-0.5 text-[11px] text-foreground-muted"},we={class:"text-foreground-muted/70"},_e={class:"mt-1 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[11px] text-foreground-muted"},Ce={class:"flex items-center gap-1 shrink-0"},Se={key:0,class:"px-6 py-12 text-center"},$e={class:"hidden sm:table w-full text-sm text-left"},Ae={class:"divide-y divide-border"},Me={class:"px-6 py-4 font-medium text-foreground max-w-[16rem] truncate"},Te={class:"px-6 py-4"},Ee={class:"flex flex-col gap-1"},ze={class:"text-foreground font-mono text-xs break-all"},Fe={class:"text-foreground-muted text-[10px]"},Ue={class:"text-foreground-muted/70"},Pe={class:"px-6 py-4 hidden sm:table-cell"},De={class:"px-6 py-4 text-foreground-muted text-xs hidden md:table-cell"},Ve={class:"px-6 py-4 text-foreground-muted text-xs hidden lg:table-cell"},Oe={class:"px-6 py-4 text-right"},qe={class:"inline-flex items-center gap-1"},Ne={key:0},We={colspan:"6",class:"px-6 py-12 text-center"},Le={class:"space-y-5"},Ie=["disabled"],Be=["value"],Je={class:"flex gap-2 bg-background rounded-lg p-1 border border-border"},Re=["onClick"],He={key:0,class:"space-y-4"},Ye={class:"grid grid-cols-3 gap-3"},Ge={key:0},Ke={key:1},Ze={key:2},Qe={key:3},Xe={class:"bg-background border border-border rounded-lg p-4"},je={class:"font-mono text-sm text-foreground"},et={class:"text-xs text-foreground-muted mt-1"},tt={key:1,class:"space-y-3"},ot={class:"bg-background border border-border rounded-lg p-4"},nt={class:"text-xs text-foreground"},st=["value"],rt={class:"text-xs text-foreground-muted mt-1.5"},at={class:"bg-surface px-1 rounded"},dt={class:"flex items-center gap-3"},bt={__name:"CronJobs",setup(lt){const $=oe(),j=(()=>{const n=["UTC","America/Los_Angeles","America/New_York","America/Chicago","America/Denver","America/Sao_Paulo","Europe/London","Europe/Berlin","Europe/Paris","Europe/Moscow","Africa/Lagos","Africa/Cairo","Africa/Johannesburg","Asia/Dubai","Asia/Kolkata","Asia/Singapore","Asia/Shanghai","Asia/Tokyo","Australia/Sydney","Pacific/Auckland"];return[...new Set([$,...n])]})(),N=ne(),h=v([]),W=v([]),A=v(!1),p=v(null),k=v("simple"),s=v({function_name:"",cron:"0 0 * * *",timezone:$,enabled:!0}),u=v({frequency:"day",minute:0,hour:0,dayOfWeek:1,dayOfMonth:1}),M=async()=>{try{const n=await re();h.value=n.data.schedules||[]}catch(n){console.error("Failed to load cron jobs",n)}},ee=async()=>{try{const n=await ae();W.value=n.data.functions||[]}catch(n){console.error("Failed to load functions",n)}},b=()=>{const{frequency:n,minute:t,hour:o,dayOfWeek:i,dayOfMonth:g}=u.value;switch(n){case"minute":s.value.cron="* * * * *";break;case"hour":s.value.cron=`${t} * * * *`;break;case"day":s.value.cron=`${t} ${o} * * *`;break;case"week":s.value.cron=`${t} ${o} * * ${i}`;break;case"month":s.value.cron=`${t} ${o} ${g} * *`;break}},T=n=>{if(!n)return"Invalid expression";const t=n.trim().split(/\s+/);if(t.length!==5)return"Invalid format (use 5 fields)";const[o,i,g,z,w]=t;return n==="* * * * *"?"Every minute":o!=="*"&&i==="*"&&g==="*"&&z==="*"&&w==="*"?`Every hour at minute ${o}`:o!=="*"&&i!=="*"&&g==="*"&&z==="*"&&w==="*"?`Every day at ${i.padStart(2,"0")}:${o.padStart(2,"0")}`:o!=="*"&&i!=="*"&&g==="*"&&z==="*"&&w!=="*"?`Every ${["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"][w]} at ${i.padStart(2,"0")}:${o.padStart(2,"0")}`:o!=="*"&&i!=="*"&&g!=="*"&&z==="*"&&w==="*"?`On day ${g} of every month at ${i.padStart(2,"0")}:${o.padStart(2,"0")}`:`Custom: ${n}`},E=n=>new Date(n).toLocaleString("en-US",{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"}),te=async()=>{try{p.value?await H(p.value.id,{function_id:p.value.function_id,cron:s.value.cron,timezone:s.value.timezone,enabled:s.value.enabled}):await ue(s.value.function_name,{cron:s.value.cron,timezone:s.value.timezone,enabled:s.value.enabled}),await M(),V()}catch(n){console.error("Failed to save schedule",n),N.notify({title:"Failed to save schedule",danger:!0})}},L=n=>{p.value=n,s.value={function_name:n.function_name,cron:n.cron_expression,timezone:n.timezone||"UTC",enabled:n.enabled},A.value=!0},I=async n=>{try{await H(n.id,{function_id:n.function_id,enabled:!n.enabled}),await M()}catch(t){console.error("Failed to toggle schedule",t)}},B=async n=>{if(await N.ask({title:"Delete schedule?",message:`Cron schedule for "${n.function_name}" will be removed.`,confirmLabel:"Delete",danger:!0}))try{await le(n.id,n.function_id),await M()}catch(o){console.error("Failed to delete schedule",o)}},V=()=>{A.value=!1,p.value=null,s.value={function_name:"",cron:"0 0 * * *",timezone:$,enabled:!0},u.value={frequency:"day",minute:0,hour:0,dayOfWeek:1,dayOfMonth:1},k.value="simple"};return se(()=>{M(),ee(),b()}),(n,t)=>(d(),l("div",me,[e("div",fe,[t[12]||(t[12]=e("div",null,[e("h1",{class:"text-xl font-semibold text-white tracking-tight"}," Scheduled Jobs "),e("p",{class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"}," Cron-driven triggers that fire any deployed function on a schedule. Use them for periodic cleanup, daily reports, polling external APIs, anything you'd normally pin to a server's crontab. Orva runs the schedule, captures stdout/stderr, and surfaces failures in the activity feed. ")],-1)),c(O,{onClick:t[0]||(t[0]=o=>A.value=!0)},{default:_(()=>[c(a(ce),{class:"w-4 h-4"}),t[11]||(t[11]=m(" New Schedule ",-1))]),_:1})]),e("div",pe,[e("ul",xe,[(d(!0),l(C,null,S(h.value,o=>(d(),l("li",{key:o.id,class:"px-4 py-3"},[e("div",ge,[e("div",ve,[e("div",be,[e("span",ye,r(o.function_name),1),e("span",{class:q(["inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-medium border",o.enabled?"bg-success-tint text-success-fg border-success-ring":"bg-warning-tint text-warning-fg border-warning-ring"])},[(d(),J(R(o.enabled?a(Y):a(D)),{class:"h-3 w-3 shrink-0","aria-hidden":"true"})),m(" "+r(o.enabled?"Active":"Paused"),1)],2)]),e("div",he,r(o.cron_expression),1),e("div",ke,[m(r(T(o.cron_expression))+" ",1),e("span",we,"· "+r(o.timezone||"UTC"),1)]),e("div",_e,[e("span",null,"last "+r(o.last_run_at?E(o.last_run_at):a(P)),1),e("span",null,"next "+r(o.next_run_at?E(o.next_run_at):a(P)),1)])]),e("div",Ce,[c(y,{icon:o.enabled?a(Q):a(G),title:o.enabled?"Pause":"Resume",onClick:i=>I(o)},null,8,["icon","title","onClick"]),c(y,{icon:a(K),title:"Edit",onClick:i=>L(o)},null,8,["icon","onClick"]),c(y,{icon:a(Z),variant:"danger",title:"Delete",onClick:i=>B(o)},null,8,["icon","onClick"])])])]))),128)),h.value.length===0?(d(),l("li",Se,[c(a(D),{class:"w-12 h-12 text-foreground-muted mx-auto mb-3 opacity-60"}),t[13]||(t[13]=e("p",{class:"text-foreground-muted"}," No scheduled jobs yet. ",-1)),t[14]||(t[14]=e("p",{class:"text-foreground-muted text-xs mt-1"}," Create your first schedule to automate function execution. ",-1))])):x("",!0)]),e("table",$e,[t[17]||(t[17]=e("thead",{class:"text-xs text-foreground-muted uppercase bg-surface border-b border-border"},[e("tr",null,[e("th",{class:"px-6 py-3 font-medium"}," Function "),e("th",{class:"px-6 py-3 font-medium"}," Schedule "),e("th",{class:"px-6 py-3 font-medium hidden sm:table-cell"}," Status "),e("th",{class:"px-6 py-3 font-medium hidden md:table-cell"}," Last Run "),e("th",{class:"px-6 py-3 font-medium hidden lg:table-cell"}," Next Run "),e("th",{class:"px-6 py-3 font-medium text-right"}," Actions ")])],-1)),e("tbody",Ae,[(d(!0),l(C,null,S(h.value,o=>(d(),l("tr",{key:o.id,class:"hover:bg-surface-hover transition-colors"},[e("td",Me,r(o.function_name),1),e("td",Te,[e("div",Ee,[e("span",ze,r(o.cron_expression),1),e("span",Fe,[m(r(T(o.cron_expression))+" ",1),e("span",Ue,"· "+r(o.timezone||"UTC"),1)])])]),e("td",Pe,[e("span",{class:q(["inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium border",o.enabled?"bg-success-tint text-success-fg border-success-ring":"bg-warning-tint text-warning-fg border-warning-ring"])},[(d(),J(R(o.enabled?a(Y):a(D)),{class:"h-3 w-3 shrink-0","aria-hidden":"true"})),m(" "+r(o.enabled?"Active":"Paused"),1)],2)]),e("td",De,r(o.last_run_at?E(o.last_run_at):a(P)),1),e("td",Ve,r(o.next_run_at?E(o.next_run_at):a(P)),1),e("td",Oe,[e("div",qe,[c(y,{icon:o.enabled?a(Q):a(G),title:o.enabled?"Pause":"Resume",onClick:i=>I(o)},null,8,["icon","title","onClick"]),c(y,{icon:a(K),title:"Edit",onClick:i=>L(o)},null,8,["icon","onClick"]),c(y,{icon:a(Z),variant:"danger",title:"Delete",onClick:i=>B(o)},null,8,["icon","onClick"])])])]))),128)),h.value.length===0?(d(),l("tr",Ne,[e("td",We,[c(a(D),{class:"w-12 h-12 text-foreground-muted mx-auto mb-3 opacity-60"}),t[15]||(t[15]=e("p",{class:"text-foreground-muted"}," No scheduled jobs yet. ",-1)),t[16]||(t[16]=e("p",{class:"text-foreground-muted text-xs mt-1"}," Create your first schedule to automate function execution. ",-1))])])):x("",!0)])])]),c(ie,{"model-value":A.value,title:p.value?"Edit Schedule":"Create Schedule",size:"lg","onUpdate:modelValue":t[10]||(t[10]=o=>{o||V()})},{footer:_(()=>[c(O,{variant:"ghost",onClick:V},{default:_(()=>[...t[38]||(t[38]=[m(" Cancel ",-1)])]),_:1}),c(O,{disabled:!s.value.function_name||!s.value.cron,onClick:te},{default:_(()=>[m(r(p.value?"Update":"Create")+" Schedule ",1)]),_:1},8,["disabled"])]),default:_(()=>[e("div",Le,[e("div",null,[t[19]||(t[19]=e("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-2"},"Function",-1)),f(e("select",{"onUpdate:modelValue":t[1]||(t[1]=o=>s.value.function_name=o),class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white",disabled:!!p.value},[t[18]||(t[18]=e("option",{value:""}," Select a function ",-1)),(d(!0),l(C,null,S(W.value,o=>(d(),l("option",{key:o.name,value:o.name},r(o.name)+" ("+r(o.runtime)+") ",9,Be))),128))],8,Ie),[[F,s.value.function_name]])]),e("div",null,[t[20]||(t[20]=e("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-2"},"Schedule Type",-1)),e("div",Je,[(d(),l(C,null,S(["simple","advanced"],o=>e("button",{key:o,class:q(["flex-1 py-2 px-3 text-sm font-medium rounded transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background",k.value===o?"bg-primary text-primary-foreground shadow-sm":"text-foreground-muted hover:text-foreground"]),onClick:i=>k.value=o},r(o==="simple"?"Natural Language":"Cron Expression"),11,Re)),64))])]),k.value==="simple"?(d(),l("div",He,[e("div",Ye,[e("div",null,[t[22]||(t[22]=e("label",{class:"text-xs font-medium text-foreground-muted block mb-1.5"},"Frequency",-1)),f(e("select",{"onUpdate:modelValue":t[2]||(t[2]=o=>u.value.frequency=o),class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white",onChange:b},[...t[21]||(t[21]=[e("option",{value:"minute"}," Every Minute ",-1),e("option",{value:"hour"}," Hourly ",-1),e("option",{value:"day"}," Daily ",-1),e("option",{value:"week"}," Weekly ",-1),e("option",{value:"month"}," Monthly ",-1)])],544),[[F,u.value.frequency]])]),["hour","day","week","month"].includes(u.value.frequency)?(d(),l("div",Ge,[t[23]||(t[23]=e("label",{class:"text-xs font-medium text-foreground-muted block mb-1.5"},"At Minute",-1)),f(e("input",{"onUpdate:modelValue":t[3]||(t[3]=o=>u.value.minute=o),type:"number",min:"0",max:"59",class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white",onInput:b},null,544),[[U,u.value.minute,void 0,{number:!0}]])])):x("",!0),["day","week","month"].includes(u.value.frequency)?(d(),l("div",Ke,[t[24]||(t[24]=e("label",{class:"text-xs font-medium text-foreground-muted block mb-1.5"},"At Hour",-1)),f(e("input",{"onUpdate:modelValue":t[4]||(t[4]=o=>u.value.hour=o),type:"number",min:"0",max:"23",class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white",onInput:b},null,544),[[U,u.value.hour,void 0,{number:!0}]])])):x("",!0),u.value.frequency==="week"?(d(),l("div",Ze,[t[26]||(t[26]=e("label",{class:"text-xs font-medium text-foreground-muted block mb-1.5"},"Day of Week",-1)),f(e("select",{"onUpdate:modelValue":t[5]||(t[5]=o=>u.value.dayOfWeek=o),class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white",onChange:b},[...t[25]||(t[25]=[e("option",{value:"0"}," Sunday ",-1),e("option",{value:"1"}," Monday ",-1),e("option",{value:"2"}," Tuesday ",-1),e("option",{value:"3"}," Wednesday ",-1),e("option",{value:"4"}," Thursday ",-1),e("option",{value:"5"}," Friday ",-1),e("option",{value:"6"}," Saturday ",-1)])],544),[[F,u.value.dayOfWeek]])])):x("",!0),u.value.frequency==="month"?(d(),l("div",Qe,[t[27]||(t[27]=e("label",{class:"text-xs font-medium text-foreground-muted block mb-1.5"},"Day of Month",-1)),f(e("input",{"onUpdate:modelValue":t[6]||(t[6]=o=>u.value.dayOfMonth=o),type:"number",min:"1",max:"31",class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white",onInput:b},null,544),[[U,u.value.dayOfMonth,void 0,{number:!0}]])])):x("",!0)]),e("div",Xe,[t[28]||(t[28]=e("div",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide mb-2"}," Generated Expression ",-1)),e("div",je,r(s.value.cron),1),e("div",et,r(T(s.value.cron)),1)])])):x("",!0),k.value==="advanced"?(d(),l("div",tt,[e("div",null,[t[29]||(t[29]=e("label",{class:"text-xs font-medium text-foreground-muted block mb-1.5"},"Cron Expression",-1)),f(e("input",{"onUpdate:modelValue":t[7]||(t[7]=o=>s.value.cron=o),placeholder:"* * * * *",class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm font-mono text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white"},null,512),[[U,s.value.cron]]),t[30]||(t[30]=e("p",{class:"text-xs text-foreground-muted mt-1.5"}," Format: minute hour day month weekday ",-1))]),e("div",ot,[t[31]||(t[31]=e("div",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide mb-2"}," Preview ",-1)),e("div",nt,r(T(s.value.cron)),1)])])):x("",!0),e("div",null,[t[36]||(t[36]=e("label",{class:"block text-xs font-medium text-foreground-muted uppercase tracking-wide mb-1.5"}," Timezone ",-1)),f(e("select",{"onUpdate:modelValue":t[8]||(t[8]=o=>s.value.timezone=o),class:"w-full bg-surface-hover border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:border-white"},[(d(!0),l(C,null,S(a(j),o=>(d(),l("option",{key:o,value:o},r(o)+r(o===a($)?" (your browser)":""),9,st))),128))],512),[[F,s.value.timezone]]),e("div",rt,[t[32]||(t[32]=m(" The cron expression is interpreted in this zone (e.g. ",-1)),t[33]||(t[33]=e("code",{class:"bg-surface px-1 rounded"},"0 9 * * *",-1)),t[34]||(t[34]=m(" with timezone ",-1)),e("code",at,r(s.value.timezone),1),t[35]||(t[35]=m(" fires at 9 AM local time every day. ",-1))])]),e("div",dt,[f(e("input",{id:"enabled-toggle","onUpdate:modelValue":t[9]||(t[9]=o=>s.value.enabled=o),type:"checkbox",class:"w-4 h-4 text-primary bg-background border-border rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background"},null,512),[[de,s.value.enabled]]),t[37]||(t[37]=e("label",{for:"enabled-toggle",class:"text-sm font-medium text-foreground cursor-pointer"}," Enable schedule immediately ",-1))])])]),_:1},8,["model-value","title"])]))}};export{bt as default}; +import{c as X,as as oe,C as ne,o as se,a as l,b as e,d as c,h as _,_ as O,F as C,p as S,f as a,g as x,at as re,W as ae,r as v,j as d,k as m,t as r,s as q,n as J,H as R,e as f,a1 as F,v as U,ab as de,au as H,av as le,aw as ue}from"./index-BMkkwZ9q.js";import{E as P}from"./format-CsU4_SPu.js";import{_ as y}from"./IconButton-BgeMzwXv.js";import{_ as ie}from"./Modal-jEhKmxZK.js";import{a as D,C as Y}from"./clock-BWp9w4xs.js";import{P as G}from"./play-CPjfKIOc.js";import{S as K}from"./square-pen-CsqFW8Ka.js";import{T as Z}from"./trash-2-BXf2uqQH.js";const ce=X("circle-plus",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M8 12h8",key:"1wcyev"}],["path",{d:"M12 8v8",key:"napkw2"}]]);const Q=X("pause",[["rect",{x:"14",y:"3",width:"5",height:"18",rx:"1",key:"kaeet6"}],["rect",{x:"5",y:"3",width:"5",height:"18",rx:"1",key:"1wsw3u"}]]),me={class:"space-y-6"},fe={class:"flex items-center justify-between"},pe={class:"bg-background border border-border rounded-lg overflow-x-auto"},xe={class:"sm:hidden divide-y divide-border"},ge={class:"flex items-start justify-between gap-2"},ve={class:"min-w-0 flex-1"},be={class:"flex items-center gap-2 flex-wrap"},ye={class:"font-medium text-foreground truncate"},he={class:"mt-1 text-[11px] text-foreground font-mono break-all"},ke={class:"mt-0.5 text-[11px] text-foreground-muted"},we={class:"text-foreground-muted/70"},_e={class:"mt-1 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[11px] text-foreground-muted"},Ce={class:"flex items-center gap-1 shrink-0"},Se={key:0,class:"px-6 py-12 text-center"},$e={class:"hidden sm:table w-full text-sm text-left"},Ae={class:"divide-y divide-border"},Me={class:"px-6 py-4 font-medium text-foreground max-w-[16rem] truncate"},Te={class:"px-6 py-4"},Ee={class:"flex flex-col gap-1"},ze={class:"text-foreground font-mono text-xs break-all"},Fe={class:"text-foreground-muted text-[10px]"},Ue={class:"text-foreground-muted/70"},Pe={class:"px-6 py-4 hidden sm:table-cell"},De={class:"px-6 py-4 text-foreground-muted text-xs hidden md:table-cell"},Ve={class:"px-6 py-4 text-foreground-muted text-xs hidden lg:table-cell"},Oe={class:"px-6 py-4 text-right"},qe={class:"inline-flex items-center gap-1"},Ne={key:0},We={colspan:"6",class:"px-6 py-12 text-center"},Le={class:"space-y-5"},Ie=["disabled"],Be=["value"],Je={class:"flex gap-2 bg-background rounded-lg p-1 border border-border"},Re=["onClick"],He={key:0,class:"space-y-4"},Ye={class:"grid grid-cols-3 gap-3"},Ge={key:0},Ke={key:1},Ze={key:2},Qe={key:3},Xe={class:"bg-background border border-border rounded-lg p-4"},je={class:"font-mono text-sm text-foreground"},et={class:"text-xs text-foreground-muted mt-1"},tt={key:1,class:"space-y-3"},ot={class:"bg-background border border-border rounded-lg p-4"},nt={class:"text-xs text-foreground"},st=["value"],rt={class:"text-xs text-foreground-muted mt-1.5"},at={class:"bg-surface px-1 rounded"},dt={class:"flex items-center gap-3"},bt={__name:"CronJobs",setup(lt){const $=oe(),j=(()=>{const n=["UTC","America/Los_Angeles","America/New_York","America/Chicago","America/Denver","America/Sao_Paulo","Europe/London","Europe/Berlin","Europe/Paris","Europe/Moscow","Africa/Lagos","Africa/Cairo","Africa/Johannesburg","Asia/Dubai","Asia/Kolkata","Asia/Singapore","Asia/Shanghai","Asia/Tokyo","Australia/Sydney","Pacific/Auckland"];return[...new Set([$,...n])]})(),N=ne(),h=v([]),W=v([]),A=v(!1),p=v(null),k=v("simple"),s=v({function_name:"",cron:"0 0 * * *",timezone:$,enabled:!0}),u=v({frequency:"day",minute:0,hour:0,dayOfWeek:1,dayOfMonth:1}),M=async()=>{try{const n=await re();h.value=n.data.schedules||[]}catch(n){console.error("Failed to load cron jobs",n)}},ee=async()=>{try{const n=await ae();W.value=n.data.functions||[]}catch(n){console.error("Failed to load functions",n)}},b=()=>{const{frequency:n,minute:t,hour:o,dayOfWeek:i,dayOfMonth:g}=u.value;switch(n){case"minute":s.value.cron="* * * * *";break;case"hour":s.value.cron=`${t} * * * *`;break;case"day":s.value.cron=`${t} ${o} * * *`;break;case"week":s.value.cron=`${t} ${o} * * ${i}`;break;case"month":s.value.cron=`${t} ${o} ${g} * *`;break}},T=n=>{if(!n)return"Invalid expression";const t=n.trim().split(/\s+/);if(t.length!==5)return"Invalid format (use 5 fields)";const[o,i,g,z,w]=t;return n==="* * * * *"?"Every minute":o!=="*"&&i==="*"&&g==="*"&&z==="*"&&w==="*"?`Every hour at minute ${o}`:o!=="*"&&i!=="*"&&g==="*"&&z==="*"&&w==="*"?`Every day at ${i.padStart(2,"0")}:${o.padStart(2,"0")}`:o!=="*"&&i!=="*"&&g==="*"&&z==="*"&&w!=="*"?`Every ${["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"][w]} at ${i.padStart(2,"0")}:${o.padStart(2,"0")}`:o!=="*"&&i!=="*"&&g!=="*"&&z==="*"&&w==="*"?`On day ${g} of every month at ${i.padStart(2,"0")}:${o.padStart(2,"0")}`:`Custom: ${n}`},E=n=>new Date(n).toLocaleString("en-US",{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"}),te=async()=>{try{p.value?await H(p.value.id,{function_id:p.value.function_id,cron:s.value.cron,timezone:s.value.timezone,enabled:s.value.enabled}):await ue(s.value.function_name,{cron:s.value.cron,timezone:s.value.timezone,enabled:s.value.enabled}),await M(),V()}catch(n){console.error("Failed to save schedule",n),N.notify({title:"Failed to save schedule",danger:!0})}},L=n=>{p.value=n,s.value={function_name:n.function_name,cron:n.cron_expression,timezone:n.timezone||"UTC",enabled:n.enabled},A.value=!0},I=async n=>{try{await H(n.id,{function_id:n.function_id,enabled:!n.enabled}),await M()}catch(t){console.error("Failed to toggle schedule",t)}},B=async n=>{if(await N.ask({title:"Delete schedule?",message:`Cron schedule for "${n.function_name}" will be removed.`,confirmLabel:"Delete",danger:!0}))try{await le(n.id,n.function_id),await M()}catch(o){console.error("Failed to delete schedule",o)}},V=()=>{A.value=!1,p.value=null,s.value={function_name:"",cron:"0 0 * * *",timezone:$,enabled:!0},u.value={frequency:"day",minute:0,hour:0,dayOfWeek:1,dayOfMonth:1},k.value="simple"};return se(()=>{M(),ee(),b()}),(n,t)=>(d(),l("div",me,[e("div",fe,[t[12]||(t[12]=e("div",null,[e("h1",{class:"text-xl font-semibold text-white tracking-tight"}," Scheduled Jobs "),e("p",{class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"}," Cron-driven triggers that fire any deployed function on a schedule. Use them for periodic cleanup, daily reports, polling external APIs, anything you'd normally pin to a server's crontab. Orva runs the schedule, captures stdout/stderr, and surfaces failures in the activity feed. ")],-1)),c(O,{onClick:t[0]||(t[0]=o=>A.value=!0)},{default:_(()=>[c(a(ce),{class:"w-4 h-4"}),t[11]||(t[11]=m(" New Schedule ",-1))]),_:1})]),e("div",pe,[e("ul",xe,[(d(!0),l(C,null,S(h.value,o=>(d(),l("li",{key:o.id,class:"px-4 py-3"},[e("div",ge,[e("div",ve,[e("div",be,[e("span",ye,r(o.function_name),1),e("span",{class:q(["inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-medium border",o.enabled?"bg-success-tint text-success-fg border-success-ring":"bg-warning-tint text-warning-fg border-warning-ring"])},[(d(),J(R(o.enabled?a(Y):a(D)),{class:"h-3 w-3 shrink-0","aria-hidden":"true"})),m(" "+r(o.enabled?"Active":"Paused"),1)],2)]),e("div",he,r(o.cron_expression),1),e("div",ke,[m(r(T(o.cron_expression))+" ",1),e("span",we,"· "+r(o.timezone||"UTC"),1)]),e("div",_e,[e("span",null,"last "+r(o.last_run_at?E(o.last_run_at):a(P)),1),e("span",null,"next "+r(o.next_run_at?E(o.next_run_at):a(P)),1)])]),e("div",Ce,[c(y,{icon:o.enabled?a(Q):a(G),title:o.enabled?"Pause":"Resume",onClick:i=>I(o)},null,8,["icon","title","onClick"]),c(y,{icon:a(K),title:"Edit",onClick:i=>L(o)},null,8,["icon","onClick"]),c(y,{icon:a(Z),variant:"danger",title:"Delete",onClick:i=>B(o)},null,8,["icon","onClick"])])])]))),128)),h.value.length===0?(d(),l("li",Se,[c(a(D),{class:"w-12 h-12 text-foreground-muted mx-auto mb-3 opacity-60"}),t[13]||(t[13]=e("p",{class:"text-foreground-muted"}," No scheduled jobs yet. ",-1)),t[14]||(t[14]=e("p",{class:"text-foreground-muted text-xs mt-1"}," Create your first schedule to automate function execution. ",-1))])):x("",!0)]),e("table",$e,[t[17]||(t[17]=e("thead",{class:"text-xs text-foreground-muted uppercase bg-surface border-b border-border"},[e("tr",null,[e("th",{class:"px-6 py-3 font-medium"}," Function "),e("th",{class:"px-6 py-3 font-medium"}," Schedule "),e("th",{class:"px-6 py-3 font-medium hidden sm:table-cell"}," Status "),e("th",{class:"px-6 py-3 font-medium hidden md:table-cell"}," Last Run "),e("th",{class:"px-6 py-3 font-medium hidden lg:table-cell"}," Next Run "),e("th",{class:"px-6 py-3 font-medium text-right"}," Actions ")])],-1)),e("tbody",Ae,[(d(!0),l(C,null,S(h.value,o=>(d(),l("tr",{key:o.id,class:"hover:bg-surface-hover transition-colors"},[e("td",Me,r(o.function_name),1),e("td",Te,[e("div",Ee,[e("span",ze,r(o.cron_expression),1),e("span",Fe,[m(r(T(o.cron_expression))+" ",1),e("span",Ue,"· "+r(o.timezone||"UTC"),1)])])]),e("td",Pe,[e("span",{class:q(["inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium border",o.enabled?"bg-success-tint text-success-fg border-success-ring":"bg-warning-tint text-warning-fg border-warning-ring"])},[(d(),J(R(o.enabled?a(Y):a(D)),{class:"h-3 w-3 shrink-0","aria-hidden":"true"})),m(" "+r(o.enabled?"Active":"Paused"),1)],2)]),e("td",De,r(o.last_run_at?E(o.last_run_at):a(P)),1),e("td",Ve,r(o.next_run_at?E(o.next_run_at):a(P)),1),e("td",Oe,[e("div",qe,[c(y,{icon:o.enabled?a(Q):a(G),title:o.enabled?"Pause":"Resume",onClick:i=>I(o)},null,8,["icon","title","onClick"]),c(y,{icon:a(K),title:"Edit",onClick:i=>L(o)},null,8,["icon","onClick"]),c(y,{icon:a(Z),variant:"danger",title:"Delete",onClick:i=>B(o)},null,8,["icon","onClick"])])])]))),128)),h.value.length===0?(d(),l("tr",Ne,[e("td",We,[c(a(D),{class:"w-12 h-12 text-foreground-muted mx-auto mb-3 opacity-60"}),t[15]||(t[15]=e("p",{class:"text-foreground-muted"}," No scheduled jobs yet. ",-1)),t[16]||(t[16]=e("p",{class:"text-foreground-muted text-xs mt-1"}," Create your first schedule to automate function execution. ",-1))])])):x("",!0)])])]),c(ie,{"model-value":A.value,title:p.value?"Edit Schedule":"Create Schedule",size:"lg","onUpdate:modelValue":t[10]||(t[10]=o=>{o||V()})},{footer:_(()=>[c(O,{variant:"ghost",onClick:V},{default:_(()=>[...t[38]||(t[38]=[m(" Cancel ",-1)])]),_:1}),c(O,{disabled:!s.value.function_name||!s.value.cron,onClick:te},{default:_(()=>[m(r(p.value?"Update":"Create")+" Schedule ",1)]),_:1},8,["disabled"])]),default:_(()=>[e("div",Le,[e("div",null,[t[19]||(t[19]=e("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-2"},"Function",-1)),f(e("select",{"onUpdate:modelValue":t[1]||(t[1]=o=>s.value.function_name=o),class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white",disabled:!!p.value},[t[18]||(t[18]=e("option",{value:""}," Select a function ",-1)),(d(!0),l(C,null,S(W.value,o=>(d(),l("option",{key:o.name,value:o.name},r(o.name)+" ("+r(o.runtime)+") ",9,Be))),128))],8,Ie),[[F,s.value.function_name]])]),e("div",null,[t[20]||(t[20]=e("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-2"},"Schedule Type",-1)),e("div",Je,[(d(),l(C,null,S(["simple","advanced"],o=>e("button",{key:o,class:q(["flex-1 py-2 px-3 text-sm font-medium rounded transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background",k.value===o?"bg-primary text-primary-foreground shadow-sm":"text-foreground-muted hover:text-foreground"]),onClick:i=>k.value=o},r(o==="simple"?"Natural Language":"Cron Expression"),11,Re)),64))])]),k.value==="simple"?(d(),l("div",He,[e("div",Ye,[e("div",null,[t[22]||(t[22]=e("label",{class:"text-xs font-medium text-foreground-muted block mb-1.5"},"Frequency",-1)),f(e("select",{"onUpdate:modelValue":t[2]||(t[2]=o=>u.value.frequency=o),class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white",onChange:b},[...t[21]||(t[21]=[e("option",{value:"minute"}," Every Minute ",-1),e("option",{value:"hour"}," Hourly ",-1),e("option",{value:"day"}," Daily ",-1),e("option",{value:"week"}," Weekly ",-1),e("option",{value:"month"}," Monthly ",-1)])],544),[[F,u.value.frequency]])]),["hour","day","week","month"].includes(u.value.frequency)?(d(),l("div",Ge,[t[23]||(t[23]=e("label",{class:"text-xs font-medium text-foreground-muted block mb-1.5"},"At Minute",-1)),f(e("input",{"onUpdate:modelValue":t[3]||(t[3]=o=>u.value.minute=o),type:"number",min:"0",max:"59",class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white",onInput:b},null,544),[[U,u.value.minute,void 0,{number:!0}]])])):x("",!0),["day","week","month"].includes(u.value.frequency)?(d(),l("div",Ke,[t[24]||(t[24]=e("label",{class:"text-xs font-medium text-foreground-muted block mb-1.5"},"At Hour",-1)),f(e("input",{"onUpdate:modelValue":t[4]||(t[4]=o=>u.value.hour=o),type:"number",min:"0",max:"23",class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white",onInput:b},null,544),[[U,u.value.hour,void 0,{number:!0}]])])):x("",!0),u.value.frequency==="week"?(d(),l("div",Ze,[t[26]||(t[26]=e("label",{class:"text-xs font-medium text-foreground-muted block mb-1.5"},"Day of Week",-1)),f(e("select",{"onUpdate:modelValue":t[5]||(t[5]=o=>u.value.dayOfWeek=o),class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white",onChange:b},[...t[25]||(t[25]=[e("option",{value:"0"}," Sunday ",-1),e("option",{value:"1"}," Monday ",-1),e("option",{value:"2"}," Tuesday ",-1),e("option",{value:"3"}," Wednesday ",-1),e("option",{value:"4"}," Thursday ",-1),e("option",{value:"5"}," Friday ",-1),e("option",{value:"6"}," Saturday ",-1)])],544),[[F,u.value.dayOfWeek]])])):x("",!0),u.value.frequency==="month"?(d(),l("div",Qe,[t[27]||(t[27]=e("label",{class:"text-xs font-medium text-foreground-muted block mb-1.5"},"Day of Month",-1)),f(e("input",{"onUpdate:modelValue":t[6]||(t[6]=o=>u.value.dayOfMonth=o),type:"number",min:"1",max:"31",class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white",onInput:b},null,544),[[U,u.value.dayOfMonth,void 0,{number:!0}]])])):x("",!0)]),e("div",Xe,[t[28]||(t[28]=e("div",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide mb-2"}," Generated Expression ",-1)),e("div",je,r(s.value.cron),1),e("div",et,r(T(s.value.cron)),1)])])):x("",!0),k.value==="advanced"?(d(),l("div",tt,[e("div",null,[t[29]||(t[29]=e("label",{class:"text-xs font-medium text-foreground-muted block mb-1.5"},"Cron Expression",-1)),f(e("input",{"onUpdate:modelValue":t[7]||(t[7]=o=>s.value.cron=o),placeholder:"* * * * *",class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm font-mono text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white"},null,512),[[U,s.value.cron]]),t[30]||(t[30]=e("p",{class:"text-xs text-foreground-muted mt-1.5"}," Format: minute hour day month weekday ",-1))]),e("div",ot,[t[31]||(t[31]=e("div",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide mb-2"}," Preview ",-1)),e("div",nt,r(T(s.value.cron)),1)])])):x("",!0),e("div",null,[t[36]||(t[36]=e("label",{class:"block text-xs font-medium text-foreground-muted uppercase tracking-wide mb-1.5"}," Timezone ",-1)),f(e("select",{"onUpdate:modelValue":t[8]||(t[8]=o=>s.value.timezone=o),class:"w-full bg-surface-hover border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:border-white"},[(d(!0),l(C,null,S(a(j),o=>(d(),l("option",{key:o,value:o},r(o)+r(o===a($)?" (your browser)":""),9,st))),128))],512),[[F,s.value.timezone]]),e("div",rt,[t[32]||(t[32]=m(" The cron expression is interpreted in this zone (e.g. ",-1)),t[33]||(t[33]=e("code",{class:"bg-surface px-1 rounded"},"0 9 * * *",-1)),t[34]||(t[34]=m(" with timezone ",-1)),e("code",at,r(s.value.timezone),1),t[35]||(t[35]=m(" fires at 9 AM local time every day. ",-1))])]),e("div",dt,[f(e("input",{id:"enabled-toggle","onUpdate:modelValue":t[9]||(t[9]=o=>s.value.enabled=o),type:"checkbox",class:"w-4 h-4 text-primary bg-background border-border rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background"},null,512),[[de,s.value.enabled]]),t[37]||(t[37]=e("label",{for:"enabled-toggle",class:"text-sm font-medium text-foreground cursor-pointer"}," Enable schedule immediately ",-1))])])]),_:1},8,["model-value","title"])]))}};export{bt as default}; diff --git a/backend/internal/server/ui_dist/assets/Dashboard-mDlU2mXy.js b/backend/internal/server/ui_dist/assets/Dashboard-DdCcITCh.js similarity index 99% rename from backend/internal/server/ui_dist/assets/Dashboard-mDlU2mXy.js rename to backend/internal/server/ui_dist/assets/Dashboard-DdCcITCh.js index b007341..fe72de3 100644 --- a/backend/internal/server/ui_dist/assets/Dashboard-mDlU2mXy.js +++ b/backend/internal/server/ui_dist/assets/Dashboard-DdCcITCh.js @@ -1 +1 @@ -import{E as y}from"./format-CsU4_SPu.js";import{c as B,x as O,o as V,y as z,a as v,b as e,d as r,f as c,B as W,A as G,t as n,k as i,g as M,F as Y,p as J,h as K,_ as Q,q as m,j as p,P as X,z as l}from"./index-D5cO6vit.js";const Z=B("snowflake",[["path",{d:"m10 20-1.25-2.5L6 18",key:"18frcb"}],["path",{d:"M10 4 8.75 6.5 6 6",key:"7mghy3"}],["path",{d:"m14 20 1.25-2.5L18 18",key:"1chtki"}],["path",{d:"m14 4 1.25 2.5L18 6",key:"1b4wsy"}],["path",{d:"m17 21-3-6h-4",key:"15hhxa"}],["path",{d:"m17 3-3 6 1.5 3",key:"11697g"}],["path",{d:"M2 12h6.5L10 9",key:"kv9z4n"}],["path",{d:"m20 10-1.5 2 1.5 2",key:"1swlpi"}],["path",{d:"M22 12h-6.5L14 15",key:"1mxi28"}],["path",{d:"m4 10 1.5 2L4 14",key:"k9enpj"}],["path",{d:"m7 21 3-6-1.5-3",key:"j8hb9u"}],["path",{d:"m7 3 3 6h4",key:"1otusx"}]]);const ee=B("trending-up",[["path",{d:"M16 7h6v6",key:"box55l"}],["path",{d:"m22 7-8.5 8.5-5-5L2 17",key:"1t1m79"}]]),te={class:"space-y-6"},se={class:"grid grid-cols-2 md:grid-cols-4 gap-4"},oe={class:"grid grid-cols-1 lg:grid-cols-3 gap-4"},le={class:"bg-background border border-border rounded-lg p-5 lg:col-span-1"},ne={class:"bg-background border border-border rounded-lg p-5 lg:col-span-2 space-y-5"},ae={class:"grid grid-cols-2 gap-4 text-sm"},re={class:"text-lg font-mono text-white mt-0.5"},de={class:"text-lg font-mono text-white mt-0.5"},ie={class:"text-foreground-muted text-sm"},ue={class:"text-[11px] text-foreground-muted mt-0.5"},ce={class:"space-y-2"},me={class:"flex flex-wrap items-center gap-x-4 gap-y-1 text-[11px] text-foreground-muted"},ve={class:"flex items-center gap-1.5"},pe={class:"flex items-center gap-1.5"},ge={class:"grid grid-cols-1 md:grid-cols-2 gap-4"},xe={class:"bg-background border border-border rounded-lg p-5 space-y-3"},be={class:"grid grid-cols-3 gap-3"},he={key:0,class:"text-xs text-red-400 flex items-center gap-1.5 pt-1"},fe={class:"bg-background border border-border rounded-lg p-5 space-y-3"},_e={class:"grid grid-cols-3 gap-3"},we={key:0},ye={class:"flex items-baseline justify-between mb-3"},ke={class:"text-xs font-bold text-white uppercase tracking-wider"},Se={class:"grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4"},$e={class:"flex items-start justify-between gap-2"},Fe={class:"min-w-0"},Me={class:"text-sm font-medium text-white truncate"},Be={class:"text-[10px] text-foreground-muted font-mono truncate"},Ne={class:"text-right shrink-0"},Ae={class:"text-xs font-mono text-white"},Ce={class:"grid grid-cols-3 gap-2"},Le={class:"border-t border-border pt-3 grid grid-cols-2 gap-3 text-[11px]"},je={class:"font-mono text-white"},Pe={class:"font-mono text-white"},Re={class:"font-mono text-white",title:"Average memory used per invocation vs allocated limit"},qe={class:"text-foreground-muted"},Te={class:"font-mono text-white",title:"Average CPU cores consumed per invocation vs allocated"},Ue={key:0,class:"text-foreground-muted"},De={key:1,class:"bg-background border border-border rounded-lg p-8 text-center space-y-4"},Oe={__name:"Dashboard",setup(Ie){const b=O(),a=m(()=>b.metrics||{}),N=s=>b.poolHistory[s]||[],A=s=>s==null?y:`${s.toFixed(1)}%`,C=s=>s==null?"0":s.toFixed(1),g=s=>{const t=s||0;return t>=1024?`${(t/1024).toFixed(1)} GB`:`${Math.round(t)} MB`},_=s=>{const t=Number(s)||0;return t>=1e6?`${(t/1e6).toFixed(1)}M`:t>=1e3?`${(t/1e3).toFixed(1)}k`:String(t)},u=m(()=>a.value.host?.mem_total_mb??0),k=m(()=>a.value.host?.mem_reserved_mb??0),L=m(()=>a.value.host?.mem_available_mb??0),h=m(()=>Math.max(0,u.value-L.value)),$=m(()=>Math.max(0,u.value-h.value)),j=m(()=>u.value>0?h.value/u.value*100:0),P=m(()=>u.value>0?k.value/u.value*100:0);V(()=>b.connect()),z(()=>b.disconnect());const w={props:{label:String,value:[String,Number],icon:Object,hint:String},setup(s){return()=>l("div",{class:"bg-background border border-border rounded-lg p-5 flex flex-col h-full hover:border-primary/50 transition-colors group"},[l("div",{class:"flex items-center justify-between mb-3"},[l("span",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide"},s.label),s.icon?l(s.icon,{class:"w-4 h-4 text-foreground-muted group-hover:text-primary"}):null]),l("div",{class:"text-2xl font-mono text-foreground leading-none"},String(s.value)),s.hint?l("div",{class:"text-[11px] text-foreground-muted mt-auto pt-3 leading-snug"},s.hint):null])}},R={props:{p50:Number,p95:Number,p99:Number},setup(s){return()=>{const t=[{label:"p50",ms:s.p50,color:"bg-success/70"},{label:"p95",ms:s.p95,color:"bg-warning/70"},{label:"p99",ms:s.p99,color:"bg-danger/70"}],o=Math.max(s.p50||0,s.p95||0,s.p99||0,1);return l("div",{class:"space-y-2.5"},t.map(d=>{const f=d.ms==null?0:d.ms/o*100;return l("div",{class:"space-y-1"},[l("div",{class:"flex items-baseline justify-between text-[11px]"},[l("span",{class:"font-mono uppercase text-foreground-muted tracking-wider"},d.label),l("span",{class:"font-mono text-white"},d.ms==null?y:`${d.ms}ms`)]),l("div",{class:"h-1.5 bg-surface rounded overflow-hidden"},[l("div",{class:`h-full ${d.color} transition-[width] duration-500 ease-out`,style:{width:`${f.toFixed(1)}%`}})])])}))}}},x={props:{label:String,value:[String,Number],hint:String},setup(s){return()=>l("div",{class:"bg-surface border border-border rounded p-3 flex flex-col h-full"},[l("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted"},s.label),l("div",{class:"text-lg font-mono text-white mt-0.5"},String(s.value??0)),s.hint?l("div",{class:"text-[10px] text-foreground-muted mt-auto pt-1.5 leading-snug"},s.hint):null])}},S={props:{label:String,value:[String,Number],hint:String},setup(s){return()=>l("div",{class:"bg-surface border border-border rounded p-2.5 flex flex-col h-full"},[l("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted"},s.label),l("div",{class:"text-base font-mono text-white mt-0.5 leading-none"},String(s.value??0)),s.hint?l("div",{class:"text-[10px] text-foreground-muted mt-auto pt-1.5"},s.hint):null])}},q={props:{total:{type:Number,required:!0},segments:{type:Array,required:!0}},setup(s){return()=>{const t=s.total>0?s.total:1;return l("div",{class:"h-2.5 bg-surface rounded overflow-hidden flex",role:"img","aria-label":s.segments.map(o=>`${o.label}: ${o.value} of ${s.total}`).join("; ")},s.segments.map(o=>l("div",{class:`h-full ${o.color}`,style:{width:`${(o.value/t*100).toFixed(2)}%`},title:`${o.label}: ${o.value}`})))}}},T={props:{points:{type:Array,default:()=>[]}},setup(s){return()=>{const t=s.points||[];if(t.length<2)return l("div",{class:"h-8 flex items-center text-[10px] text-foreground-muted"},"(collecting samples…)");const o=Math.max(...t,1),d=100,f=32,U=d/(t.length-1),D=t.map((I,F)=>{const E=(F*U).toFixed(2),H=(f-I/o*f).toFixed(2);return`${F===0?"M":"L"}${E},${H}`}).join(" ");return l("svg",{viewBox:`0 0 ${d} ${f}`,class:"w-full h-8 text-blue-400",preserveAspectRatio:"none"},[l("path",{d:D,fill:"none",stroke:"currentColor","stroke-width":"1.5"})])}}};return(s,t)=>(p(),v("div",te,[t[21]||(t[21]=e("div",null,[e("h1",{class:"text-xl font-semibold text-white tracking-tight"},"System Overview"),e("p",{class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"},"Live snapshot of what your platform is doing right now.")],-1)),e("div",se,[r(w,{label:"Functions",hint:"Deployed in this workspace",value:c(b).functionsCount,icon:c(W)},null,8,["value","icon"]),r(w,{label:"In flight",hint:"Requests being handled right now",value:a.value.active_requests??0,icon:c(G)},null,8,["value","icon"]),r(w,{label:"Invocations",hint:"Total calls served since the platform started",value:_(a.value.totals?.invocations??0),icon:c(ee)},null,8,["value","icon"]),r(w,{label:"Cold starts",hint:"Calls that had to spawn a fresh sandbox",value:A(a.value.rates?.cold_start_pct),icon:c(Z)},null,8,["value","icon"])]),e("div",oe,[e("div",le,[t[1]||(t[1]=e("div",{class:"mb-3"},[e("h2",{class:"text-xs font-bold text-white uppercase tracking-wider"}," Response time "),e("div",{class:"text-[11px] text-foreground-muted mt-1"}," How long calls take to come back. p99 is the worst-case 1-in-100. ")],-1)),r(R,{p50:a.value.latency_ms?.p50,p95:a.value.latency_ms?.p95,p99:a.value.latency_ms?.p99},null,8,["p50","p95","p99"])]),e("div",ne,[t[7]||(t[7]=e("div",null,[e("h2",{class:"text-xs font-bold text-white uppercase tracking-wider"}," Host machine "),e("div",{class:"text-[11px] text-foreground-muted mt-1"}," The server Orva is running on: how much of its RAM is actually in use, and how much your warm pools reserve as headroom. ")],-1)),e("div",ae,[e("div",null,[t[2]||(t[2]=e("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted"},"CPU cores",-1)),e("div",re,n(a.value.host?.num_cpu??"?"),1),t[3]||(t[3]=e("div",{class:"text-[11px] text-foreground-muted mt-0.5"}," available to functions on this host ",-1))]),e("div",null,[t[4]||(t[4]=e("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted"},"Memory in use",-1)),e("div",de,[i(n(g(h.value))+" ",1),e("span",ie,"/ "+n(g(u.value)),1)]),e("div",ue,n(j.value.toFixed(1))+"% used · "+n(g(k.value))+" reserved by warm pools ",1)])]),e("div",ce,[r(q,{total:u.value,segments:[{label:"In use",value:h.value,color:"bg-info/70"},{label:"Free",value:$.value,color:"bg-success/40"}]},null,8,["total","segments"]),e("div",me,[e("span",ve,[t[5]||(t[5]=e("span",{class:"w-2 h-2 rounded-full bg-info/70"},null,-1)),i(" "+n(g(h.value))+" in use ",1)]),e("span",pe,[t[6]||(t[6]=e("span",{class:"w-2 h-2 rounded-full bg-success/40"},null,-1)),i(" "+n(g($.value))+" free ",1)]),e("span",null,n(g(k.value))+" reserved by warm pools ("+n(P.value.toFixed(1))+"% — held ready, not all in use) ",1)])])])]),e("div",ge,[e("div",xe,[t[9]||(t[9]=e("div",null,[e("h2",{class:"text-xs font-bold text-white uppercase tracking-wider"}," Builds "),e("div",{class:"text-[11px] text-foreground-muted mt-1"}," Where deploys go: extracted, dependencies installed, then activated. ")],-1)),e("div",be,[r(x,{label:"In queue",value:a.value.build_queue?.pending??0,hint:"waiting to start"},null,8,["value"]),r(x,{label:"Build workers",value:a.value.build_queue?.workers??0,hint:"parallel slots"},null,8,["value"]),r(x,{label:"Built so far",value:_(a.value.totals?.builds??0),hint:"lifetime total"},null,8,["value"])]),(a.value.totals?.build_errors??0)>0?(p(),v("div",he,[t[8]||(t[8]=e("span",{class:"w-1.5 h-1.5 rounded-full bg-red-400"},null,-1)),i(" "+n(a.value.totals.build_errors)+" build"+n(a.value.totals.build_errors===1?" has":"s have")+" failed since start ",1)])):M("",!0)]),e("div",fe,[t[10]||(t[10]=e("div",null,[e("h2",{class:"text-xs font-bold text-white uppercase tracking-wider"}," Sandbox activity "),e("div",{class:"text-[11px] text-foreground-muted mt-1"}," Each invocation runs inside an isolated nsjail sandbox process. ")],-1)),e("div",_e,[r(x,{label:"Running now",value:a.value.sandbox?.active??0,hint:"serving a request"},null,8,["value"]),r(x,{label:"Reused",value:_(a.value.totals?.warm_hits??0),hint:"warm-pool hits"},null,8,["value"]),r(x,{label:"Spawned fresh",value:_(a.value.totals?.cold_starts??0),hint:"cold starts"},null,8,["value"])])])]),(a.value.pools||[]).length?(p(),v("div",we,[e("div",ye,[e("div",null,[e("h2",ke," Warm pools ("+n(a.value.pools.length)+") ",1),t[11]||(t[11]=e("div",{class:"text-[11px] text-foreground-muted mt-1"}," One pool per active function. Sandboxes stay ready so the next call doesn't pay a cold start. ",-1))])]),e("div",Se,[(p(!0),v(Y,null,J(a.value.pools,o=>(p(),v("div",{key:o.function_id,class:"bg-background border border-border rounded-lg p-4 space-y-3"},[e("div",$e,[e("div",Fe,[e("div",Me,n(o.function_name||o.function_id),1),e("div",Be,n(o.function_id),1)]),e("div",Ne,[t[13]||(t[13]=e("div",{class:"text-[10px] text-foreground-muted"},"Target / cap",-1)),e("div",Ae,[i(n(o.target)+" ",1),t[12]||(t[12]=e("span",{class:"text-foreground-muted"},"/",-1)),i(" "+n(o.dynamic_max),1)])])]),e("div",Ce,[r(S,{label:"Ready",value:o.idle,hint:"idle workers"},null,8,["value"]),r(S,{label:"Busy",value:o.busy,hint:"serving now"},null,8,["value"]),r(S,{label:"Calls / sec",value:C(o.rate_ewma),hint:"recent rate"},null,8,["value"])]),e("div",null,[r(T,{points:N(o.function_id)},null,8,["points"]),t[14]||(t[14]=e("div",{class:"text-[10px] text-foreground-muted mt-1"}," Recent calls per second (last 5 min) ",-1))]),e("div",Le,[e("div",null,[t[15]||(t[15]=e("div",{class:"text-foreground-muted"},"Spawned · killed",-1)),e("div",je,n(o.spawned)+" · "+n(o.killed),1)]),e("div",null,[t[16]||(t[16]=e("div",{class:"text-foreground-muted"},"Avg latency",-1)),e("div",Pe,n(o.latency_ewma_ms?.toFixed?.(1)??0)+" ms",1)]),e("div",null,[t[17]||(t[17]=e("div",{class:"text-foreground-muted"},"Avg memory",-1)),e("div",Re,[i(n(o.mem_used_avg_mb>0?"~"+Math.round(o.mem_used_avg_mb):c(y))+" ",1),e("span",qe,"/ "+n(o.mem_limit_mb)+" MB",1)])]),e("div",null,[t[18]||(t[18]=e("div",{class:"text-foreground-muted"},"Avg CPU",-1)),e("div",Te,[i(n(o.cpu_frac_avg>0&&o.cpu_limit>0?(o.cpu_frac_avg*o.cpu_limit).toFixed(2):c(y))+" ",1),o.cpu_limit>0?(p(),v("span",Ue,"/ "+n(o.cpu_limit)+" CPU",1)):M("",!0)])])])]))),128))])])):(p(),v("div",De,[t[20]||(t[20]=e("div",null,[e("div",{class:"text-sm text-white"},"No warm pools yet"),e("div",{class:"text-xs text-foreground-muted mt-1 max-w-prose mx-auto leading-body"}," Deploy your first function to see live worker pools, latency, and cold-start rate land in the tiles above. ")],-1)),e("div",null,[r(Q,{onClick:t[0]||(t[0]=o=>s.$router.push("/functions/new"))},{default:K(()=>[r(c(X),{class:"w-4 h-4"}),t[19]||(t[19]=i(" Deploy your first function ",-1))]),_:1})])]))]))}};export{Oe as default}; +import{E as y}from"./format-CsU4_SPu.js";import{c as B,x as O,o as V,y as z,a as v,b as e,d as r,f as c,B as W,A as G,t as n,k as i,g as M,F as Y,p as J,h as K,_ as Q,q as m,j as p,P as X,z as l}from"./index-BMkkwZ9q.js";const Z=B("snowflake",[["path",{d:"m10 20-1.25-2.5L6 18",key:"18frcb"}],["path",{d:"M10 4 8.75 6.5 6 6",key:"7mghy3"}],["path",{d:"m14 20 1.25-2.5L18 18",key:"1chtki"}],["path",{d:"m14 4 1.25 2.5L18 6",key:"1b4wsy"}],["path",{d:"m17 21-3-6h-4",key:"15hhxa"}],["path",{d:"m17 3-3 6 1.5 3",key:"11697g"}],["path",{d:"M2 12h6.5L10 9",key:"kv9z4n"}],["path",{d:"m20 10-1.5 2 1.5 2",key:"1swlpi"}],["path",{d:"M22 12h-6.5L14 15",key:"1mxi28"}],["path",{d:"m4 10 1.5 2L4 14",key:"k9enpj"}],["path",{d:"m7 21 3-6-1.5-3",key:"j8hb9u"}],["path",{d:"m7 3 3 6h4",key:"1otusx"}]]);const ee=B("trending-up",[["path",{d:"M16 7h6v6",key:"box55l"}],["path",{d:"m22 7-8.5 8.5-5-5L2 17",key:"1t1m79"}]]),te={class:"space-y-6"},se={class:"grid grid-cols-2 md:grid-cols-4 gap-4"},oe={class:"grid grid-cols-1 lg:grid-cols-3 gap-4"},le={class:"bg-background border border-border rounded-lg p-5 lg:col-span-1"},ne={class:"bg-background border border-border rounded-lg p-5 lg:col-span-2 space-y-5"},ae={class:"grid grid-cols-2 gap-4 text-sm"},re={class:"text-lg font-mono text-white mt-0.5"},de={class:"text-lg font-mono text-white mt-0.5"},ie={class:"text-foreground-muted text-sm"},ue={class:"text-[11px] text-foreground-muted mt-0.5"},ce={class:"space-y-2"},me={class:"flex flex-wrap items-center gap-x-4 gap-y-1 text-[11px] text-foreground-muted"},ve={class:"flex items-center gap-1.5"},pe={class:"flex items-center gap-1.5"},ge={class:"grid grid-cols-1 md:grid-cols-2 gap-4"},xe={class:"bg-background border border-border rounded-lg p-5 space-y-3"},be={class:"grid grid-cols-3 gap-3"},he={key:0,class:"text-xs text-red-400 flex items-center gap-1.5 pt-1"},fe={class:"bg-background border border-border rounded-lg p-5 space-y-3"},_e={class:"grid grid-cols-3 gap-3"},we={key:0},ye={class:"flex items-baseline justify-between mb-3"},ke={class:"text-xs font-bold text-white uppercase tracking-wider"},Se={class:"grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4"},$e={class:"flex items-start justify-between gap-2"},Fe={class:"min-w-0"},Me={class:"text-sm font-medium text-white truncate"},Be={class:"text-[10px] text-foreground-muted font-mono truncate"},Ne={class:"text-right shrink-0"},Ae={class:"text-xs font-mono text-white"},Ce={class:"grid grid-cols-3 gap-2"},Le={class:"border-t border-border pt-3 grid grid-cols-2 gap-3 text-[11px]"},je={class:"font-mono text-white"},Pe={class:"font-mono text-white"},Re={class:"font-mono text-white",title:"Average memory used per invocation vs allocated limit"},qe={class:"text-foreground-muted"},Te={class:"font-mono text-white",title:"Average CPU cores consumed per invocation vs allocated"},Ue={key:0,class:"text-foreground-muted"},De={key:1,class:"bg-background border border-border rounded-lg p-8 text-center space-y-4"},Oe={__name:"Dashboard",setup(Ie){const b=O(),a=m(()=>b.metrics||{}),N=s=>b.poolHistory[s]||[],A=s=>s==null?y:`${s.toFixed(1)}%`,C=s=>s==null?"0":s.toFixed(1),g=s=>{const t=s||0;return t>=1024?`${(t/1024).toFixed(1)} GB`:`${Math.round(t)} MB`},_=s=>{const t=Number(s)||0;return t>=1e6?`${(t/1e6).toFixed(1)}M`:t>=1e3?`${(t/1e3).toFixed(1)}k`:String(t)},u=m(()=>a.value.host?.mem_total_mb??0),k=m(()=>a.value.host?.mem_reserved_mb??0),L=m(()=>a.value.host?.mem_available_mb??0),h=m(()=>Math.max(0,u.value-L.value)),$=m(()=>Math.max(0,u.value-h.value)),j=m(()=>u.value>0?h.value/u.value*100:0),P=m(()=>u.value>0?k.value/u.value*100:0);V(()=>b.connect()),z(()=>b.disconnect());const w={props:{label:String,value:[String,Number],icon:Object,hint:String},setup(s){return()=>l("div",{class:"bg-background border border-border rounded-lg p-5 flex flex-col h-full hover:border-primary/50 transition-colors group"},[l("div",{class:"flex items-center justify-between mb-3"},[l("span",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide"},s.label),s.icon?l(s.icon,{class:"w-4 h-4 text-foreground-muted group-hover:text-primary"}):null]),l("div",{class:"text-2xl font-mono text-foreground leading-none"},String(s.value)),s.hint?l("div",{class:"text-[11px] text-foreground-muted mt-auto pt-3 leading-snug"},s.hint):null])}},R={props:{p50:Number,p95:Number,p99:Number},setup(s){return()=>{const t=[{label:"p50",ms:s.p50,color:"bg-success/70"},{label:"p95",ms:s.p95,color:"bg-warning/70"},{label:"p99",ms:s.p99,color:"bg-danger/70"}],o=Math.max(s.p50||0,s.p95||0,s.p99||0,1);return l("div",{class:"space-y-2.5"},t.map(d=>{const f=d.ms==null?0:d.ms/o*100;return l("div",{class:"space-y-1"},[l("div",{class:"flex items-baseline justify-between text-[11px]"},[l("span",{class:"font-mono uppercase text-foreground-muted tracking-wider"},d.label),l("span",{class:"font-mono text-white"},d.ms==null?y:`${d.ms}ms`)]),l("div",{class:"h-1.5 bg-surface rounded overflow-hidden"},[l("div",{class:`h-full ${d.color} transition-[width] duration-500 ease-out`,style:{width:`${f.toFixed(1)}%`}})])])}))}}},x={props:{label:String,value:[String,Number],hint:String},setup(s){return()=>l("div",{class:"bg-surface border border-border rounded p-3 flex flex-col h-full"},[l("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted"},s.label),l("div",{class:"text-lg font-mono text-white mt-0.5"},String(s.value??0)),s.hint?l("div",{class:"text-[10px] text-foreground-muted mt-auto pt-1.5 leading-snug"},s.hint):null])}},S={props:{label:String,value:[String,Number],hint:String},setup(s){return()=>l("div",{class:"bg-surface border border-border rounded p-2.5 flex flex-col h-full"},[l("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted"},s.label),l("div",{class:"text-base font-mono text-white mt-0.5 leading-none"},String(s.value??0)),s.hint?l("div",{class:"text-[10px] text-foreground-muted mt-auto pt-1.5"},s.hint):null])}},q={props:{total:{type:Number,required:!0},segments:{type:Array,required:!0}},setup(s){return()=>{const t=s.total>0?s.total:1;return l("div",{class:"h-2.5 bg-surface rounded overflow-hidden flex",role:"img","aria-label":s.segments.map(o=>`${o.label}: ${o.value} of ${s.total}`).join("; ")},s.segments.map(o=>l("div",{class:`h-full ${o.color}`,style:{width:`${(o.value/t*100).toFixed(2)}%`},title:`${o.label}: ${o.value}`})))}}},T={props:{points:{type:Array,default:()=>[]}},setup(s){return()=>{const t=s.points||[];if(t.length<2)return l("div",{class:"h-8 flex items-center text-[10px] text-foreground-muted"},"(collecting samples…)");const o=Math.max(...t,1),d=100,f=32,U=d/(t.length-1),D=t.map((I,F)=>{const E=(F*U).toFixed(2),H=(f-I/o*f).toFixed(2);return`${F===0?"M":"L"}${E},${H}`}).join(" ");return l("svg",{viewBox:`0 0 ${d} ${f}`,class:"w-full h-8 text-blue-400",preserveAspectRatio:"none"},[l("path",{d:D,fill:"none",stroke:"currentColor","stroke-width":"1.5"})])}}};return(s,t)=>(p(),v("div",te,[t[21]||(t[21]=e("div",null,[e("h1",{class:"text-xl font-semibold text-white tracking-tight"},"System Overview"),e("p",{class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"},"Live snapshot of what your platform is doing right now.")],-1)),e("div",se,[r(w,{label:"Functions",hint:"Deployed in this workspace",value:c(b).functionsCount,icon:c(W)},null,8,["value","icon"]),r(w,{label:"In flight",hint:"Requests being handled right now",value:a.value.active_requests??0,icon:c(G)},null,8,["value","icon"]),r(w,{label:"Invocations",hint:"Total calls served since the platform started",value:_(a.value.totals?.invocations??0),icon:c(ee)},null,8,["value","icon"]),r(w,{label:"Cold starts",hint:"Calls that had to spawn a fresh sandbox",value:A(a.value.rates?.cold_start_pct),icon:c(Z)},null,8,["value","icon"])]),e("div",oe,[e("div",le,[t[1]||(t[1]=e("div",{class:"mb-3"},[e("h2",{class:"text-xs font-bold text-white uppercase tracking-wider"}," Response time "),e("div",{class:"text-[11px] text-foreground-muted mt-1"}," How long calls take to come back. p99 is the worst-case 1-in-100. ")],-1)),r(R,{p50:a.value.latency_ms?.p50,p95:a.value.latency_ms?.p95,p99:a.value.latency_ms?.p99},null,8,["p50","p95","p99"])]),e("div",ne,[t[7]||(t[7]=e("div",null,[e("h2",{class:"text-xs font-bold text-white uppercase tracking-wider"}," Host machine "),e("div",{class:"text-[11px] text-foreground-muted mt-1"}," The server Orva is running on: how much of its RAM is actually in use, and how much your warm pools reserve as headroom. ")],-1)),e("div",ae,[e("div",null,[t[2]||(t[2]=e("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted"},"CPU cores",-1)),e("div",re,n(a.value.host?.num_cpu??"?"),1),t[3]||(t[3]=e("div",{class:"text-[11px] text-foreground-muted mt-0.5"}," available to functions on this host ",-1))]),e("div",null,[t[4]||(t[4]=e("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted"},"Memory in use",-1)),e("div",de,[i(n(g(h.value))+" ",1),e("span",ie,"/ "+n(g(u.value)),1)]),e("div",ue,n(j.value.toFixed(1))+"% used · "+n(g(k.value))+" reserved by warm pools ",1)])]),e("div",ce,[r(q,{total:u.value,segments:[{label:"In use",value:h.value,color:"bg-info/70"},{label:"Free",value:$.value,color:"bg-success/40"}]},null,8,["total","segments"]),e("div",me,[e("span",ve,[t[5]||(t[5]=e("span",{class:"w-2 h-2 rounded-full bg-info/70"},null,-1)),i(" "+n(g(h.value))+" in use ",1)]),e("span",pe,[t[6]||(t[6]=e("span",{class:"w-2 h-2 rounded-full bg-success/40"},null,-1)),i(" "+n(g($.value))+" free ",1)]),e("span",null,n(g(k.value))+" reserved by warm pools ("+n(P.value.toFixed(1))+"% — held ready, not all in use) ",1)])])])]),e("div",ge,[e("div",xe,[t[9]||(t[9]=e("div",null,[e("h2",{class:"text-xs font-bold text-white uppercase tracking-wider"}," Builds "),e("div",{class:"text-[11px] text-foreground-muted mt-1"}," Where deploys go: extracted, dependencies installed, then activated. ")],-1)),e("div",be,[r(x,{label:"In queue",value:a.value.build_queue?.pending??0,hint:"waiting to start"},null,8,["value"]),r(x,{label:"Build workers",value:a.value.build_queue?.workers??0,hint:"parallel slots"},null,8,["value"]),r(x,{label:"Built so far",value:_(a.value.totals?.builds??0),hint:"lifetime total"},null,8,["value"])]),(a.value.totals?.build_errors??0)>0?(p(),v("div",he,[t[8]||(t[8]=e("span",{class:"w-1.5 h-1.5 rounded-full bg-red-400"},null,-1)),i(" "+n(a.value.totals.build_errors)+" build"+n(a.value.totals.build_errors===1?" has":"s have")+" failed since start ",1)])):M("",!0)]),e("div",fe,[t[10]||(t[10]=e("div",null,[e("h2",{class:"text-xs font-bold text-white uppercase tracking-wider"}," Sandbox activity "),e("div",{class:"text-[11px] text-foreground-muted mt-1"}," Each invocation runs inside an isolated nsjail sandbox process. ")],-1)),e("div",_e,[r(x,{label:"Running now",value:a.value.sandbox?.active??0,hint:"serving a request"},null,8,["value"]),r(x,{label:"Reused",value:_(a.value.totals?.warm_hits??0),hint:"warm-pool hits"},null,8,["value"]),r(x,{label:"Spawned fresh",value:_(a.value.totals?.cold_starts??0),hint:"cold starts"},null,8,["value"])])])]),(a.value.pools||[]).length?(p(),v("div",we,[e("div",ye,[e("div",null,[e("h2",ke," Warm pools ("+n(a.value.pools.length)+") ",1),t[11]||(t[11]=e("div",{class:"text-[11px] text-foreground-muted mt-1"}," One pool per active function. Sandboxes stay ready so the next call doesn't pay a cold start. ",-1))])]),e("div",Se,[(p(!0),v(Y,null,J(a.value.pools,o=>(p(),v("div",{key:o.function_id,class:"bg-background border border-border rounded-lg p-4 space-y-3"},[e("div",$e,[e("div",Fe,[e("div",Me,n(o.function_name||o.function_id),1),e("div",Be,n(o.function_id),1)]),e("div",Ne,[t[13]||(t[13]=e("div",{class:"text-[10px] text-foreground-muted"},"Target / cap",-1)),e("div",Ae,[i(n(o.target)+" ",1),t[12]||(t[12]=e("span",{class:"text-foreground-muted"},"/",-1)),i(" "+n(o.dynamic_max),1)])])]),e("div",Ce,[r(S,{label:"Ready",value:o.idle,hint:"idle workers"},null,8,["value"]),r(S,{label:"Busy",value:o.busy,hint:"serving now"},null,8,["value"]),r(S,{label:"Calls / sec",value:C(o.rate_ewma),hint:"recent rate"},null,8,["value"])]),e("div",null,[r(T,{points:N(o.function_id)},null,8,["points"]),t[14]||(t[14]=e("div",{class:"text-[10px] text-foreground-muted mt-1"}," Recent calls per second (last 5 min) ",-1))]),e("div",Le,[e("div",null,[t[15]||(t[15]=e("div",{class:"text-foreground-muted"},"Spawned · killed",-1)),e("div",je,n(o.spawned)+" · "+n(o.killed),1)]),e("div",null,[t[16]||(t[16]=e("div",{class:"text-foreground-muted"},"Avg latency",-1)),e("div",Pe,n(o.latency_ewma_ms?.toFixed?.(1)??0)+" ms",1)]),e("div",null,[t[17]||(t[17]=e("div",{class:"text-foreground-muted"},"Avg memory",-1)),e("div",Re,[i(n(o.mem_used_avg_mb>0?"~"+Math.round(o.mem_used_avg_mb):c(y))+" ",1),e("span",qe,"/ "+n(o.mem_limit_mb)+" MB",1)])]),e("div",null,[t[18]||(t[18]=e("div",{class:"text-foreground-muted"},"Avg CPU",-1)),e("div",Te,[i(n(o.cpu_frac_avg>0&&o.cpu_limit>0?(o.cpu_frac_avg*o.cpu_limit).toFixed(2):c(y))+" ",1),o.cpu_limit>0?(p(),v("span",Ue,"/ "+n(o.cpu_limit)+" CPU",1)):M("",!0)])])])]))),128))])])):(p(),v("div",De,[t[20]||(t[20]=e("div",null,[e("div",{class:"text-sm text-white"},"No warm pools yet"),e("div",{class:"text-xs text-foreground-muted mt-1 max-w-prose mx-auto leading-body"}," Deploy your first function to see live worker pools, latency, and cold-start rate land in the tiles above. ")],-1)),e("div",null,[r(Q,{onClick:t[0]||(t[0]=o=>s.$router.push("/functions/new"))},{default:K(()=>[r(c(X),{class:"w-4 h-4"}),t[19]||(t[19]=i(" Deploy your first function ",-1))]),_:1})])]))]))}};export{Oe as default}; diff --git a/backend/internal/server/ui_dist/assets/Deployments-DK8gYrYx.js b/backend/internal/server/ui_dist/assets/Deployments-HGMJdPkO.js similarity index 95% rename from backend/internal/server/ui_dist/assets/Deployments-DK8gYrYx.js rename to backend/internal/server/ui_dist/assets/Deployments-HGMJdPkO.js index 7d695d3..84fae08 100644 --- a/backend/internal/server/ui_dist/assets/Deployments-DK8gYrYx.js +++ b/backend/internal/server/ui_dist/assets/Deployments-HGMJdPkO.js @@ -1,4 +1,4 @@ -import{C as de,E as ce,U as me,o as pe,G as ve,a as n,b as s,k as h,d as i,h as b,_ as I,f as c,t as r,g as u,F as ee,p as te,ah as fe,ai as P,q as M,a2 as xe,r as p,W as he,a9 as be,j as a,s as E,w as se,n as O,aj as ge,z as J,af as _e}from"./index-D5cO6vit.js";import{E as g}from"./format-CsU4_SPu.js";import{D as ye}from"./Drawer-B_L-gxK5.js";import{_ as W}from"./StatusBadge-DoungZTd.js";import{d as ke}from"./rollbackDiff-Cvt2Ss82.js";import{C as we}from"./clock-ARFGKas-.js";import{C as Ce,G as oe}from"./git-compare-CXuIlVPE.js";import{R as $e}from"./refresh-cw-BYFoMG0c.js";import{R as ae}from"./rotate-ccw-CeRUwJZR.js";import"./circle-BHWwGwr8.js";const Se={class:"space-y-6"},De={class:"flex items-center justify-between"},Re={class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"},Ee={class:"flex items-center gap-2"},Ne={key:0,class:"bg-background border border-border rounded-lg p-4 flex items-center gap-4"},Ve={class:"w-10 h-10 rounded-md bg-success/15 border border-success/30 flex items-center justify-center shrink-0"},Fe={class:"flex-1 min-w-0"},Le={class:"flex items-center gap-2 flex-wrap"},Te={class:"text-xs px-2 py-0.5 rounded bg-success/15 text-success border border-success/30 font-mono"},je={key:0,class:"text-xs px-2 py-0.5 rounded bg-amber-500/15 text-amber-400 border border-amber-500/30"},Be={class:"text-xs text-foreground-muted mt-1 font-mono truncate"},qe={key:1,class:"bg-red-950/30 border border-red-900/40 rounded p-3 text-xs text-red-300"},ze={class:"bg-background border border-border rounded-lg overflow-x-auto"},Ae={class:"sm:hidden divide-y divide-border"},Ge=["onClick"],Ie={class:"flex items-start justify-between gap-2"},Me={class:"min-w-0 flex-1"},Oe={class:"flex items-center gap-2 flex-wrap"},Ue={key:0,class:"px-1.5 py-0.5 rounded text-[10px] bg-success/15 text-success border border-success/30"},He={class:"mt-1 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[11px] text-foreground-muted"},Pe={key:0,class:"font-mono"},Je={key:1},We={key:0,class:"px-6 py-8 text-center text-sm text-foreground-muted"},Ye={class:"hidden sm:table w-full text-sm text-left"},Ke={class:"divide-y divide-border"},Qe=["onClick"],Xe={class:"px-6 py-4 font-mono text-xs"},Ze={class:"flex items-center gap-2"},et={key:0,class:"px-1.5 py-0.5 rounded text-[10px] bg-success/15 text-success border border-success/30 normal-case"},tt={class:"px-6 py-4 text-foreground"},st={class:"px-6 py-4"},ot={class:"px-6 py-4 text-foreground-muted text-xs hidden md:table-cell"},at={class:"px-6 py-4 text-foreground-muted font-mono text-xs hidden sm:table-cell"},nt={class:"px-6 py-4 text-foreground-muted font-mono text-xs hidden xl:table-cell"},lt={class:"inline-flex items-center gap-2 justify-end"},rt={key:2,class:"text-foreground-muted/50"},it={key:3,class:"text-foreground-muted/30"},ut={key:0},dt={key:0,class:"p-6 text-sm text-foreground-muted"},ct={key:1,class:"p-5 space-y-4"},mt={class:"flex items-center gap-2 flex-wrap"},pt={key:0,class:"inline-flex items-center px-2.5 py-1 rounded text-xs border bg-background font-mono text-foreground-muted"},vt={class:"grid grid-cols-2 gap-3 text-sm"},ft={key:0},xt={class:"bg-red-950/30 border border-red-900/40 rounded p-3 text-xs text-red-300 font-mono whitespace-pre-wrap break-words"},ht={class:"flex items-center justify-between mb-2"},bt={key:0,class:"text-[10px] text-green-400"},gt={class:"bg-surface border border-border rounded p-3 text-xs text-foreground font-mono overflow-auto max-h-96 whitespace-pre-wrap break-words"},Vt={__name:"Deployments",setup(_t){const U=de(),ne=be(),x=M(()=>ne.params.name),N=p(null),d=p(null),_=p([]),C=p(!1),V=p(""),$=p(!1),F=t=>t&&t.status==="succeeded"&&t.code_hash&&!v(t),y=M(()=>_.value.find(e=>v(e))?.id||null),H=t=>t&&t.status==="succeeded"&&t.code_hash&&!v(t)&&y.value,Y=async t=>{if(!N.value||!t?.id||$.value)return;const e=(t.code_hash||"").slice(0,12);let m=`Code hash ${e}. Current ${d.value?"v"+d.value.version:"version"} stays in history.`;try{const G=(await P(t.id))?.data?.snapshot;if(G&&d.value){const R=ke(d.value,G);R.length?m=`Rolling back to v${t.version} (code ${e}) will also change: +import{C as de,E as ce,U as me,o as pe,G as ve,a as n,b as s,k as h,d as i,h as b,_ as I,f as c,t as r,g as u,F as ee,p as te,ah as fe,ai as P,q as M,a2 as xe,r as p,W as he,a9 as be,j as a,s as E,w as se,n as O,aj as ge,z as J,af as _e}from"./index-BMkkwZ9q.js";import{E as g}from"./format-CsU4_SPu.js";import{D as ye}from"./Drawer-C3AFLOZb.js";import{_ as W}from"./StatusBadge-Cj_PlPFZ.js";import{d as ke}from"./rollbackDiff-Cvt2Ss82.js";import{C as we}from"./clock-BWp9w4xs.js";import{C as Ce,G as oe}from"./git-compare-omnJl6y2.js";import{R as $e}from"./refresh-cw-C7sR7ShF.js";import{R as ae}from"./rotate-ccw-CsgWy1Bs.js";import"./circle-DJWJGpv0.js";const Se={class:"space-y-6"},De={class:"flex items-center justify-between"},Re={class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"},Ee={class:"flex items-center gap-2"},Ne={key:0,class:"bg-background border border-border rounded-lg p-4 flex items-center gap-4"},Ve={class:"w-10 h-10 rounded-md bg-success/15 border border-success/30 flex items-center justify-center shrink-0"},Fe={class:"flex-1 min-w-0"},Le={class:"flex items-center gap-2 flex-wrap"},Te={class:"text-xs px-2 py-0.5 rounded bg-success/15 text-success border border-success/30 font-mono"},je={key:0,class:"text-xs px-2 py-0.5 rounded bg-amber-500/15 text-amber-400 border border-amber-500/30"},Be={class:"text-xs text-foreground-muted mt-1 font-mono truncate"},qe={key:1,class:"bg-red-950/30 border border-red-900/40 rounded p-3 text-xs text-red-300"},ze={class:"bg-background border border-border rounded-lg overflow-x-auto"},Ae={class:"sm:hidden divide-y divide-border"},Ge=["onClick"],Ie={class:"flex items-start justify-between gap-2"},Me={class:"min-w-0 flex-1"},Oe={class:"flex items-center gap-2 flex-wrap"},Ue={key:0,class:"px-1.5 py-0.5 rounded text-[10px] bg-success/15 text-success border border-success/30"},He={class:"mt-1 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[11px] text-foreground-muted"},Pe={key:0,class:"font-mono"},Je={key:1},We={key:0,class:"px-6 py-8 text-center text-sm text-foreground-muted"},Ye={class:"hidden sm:table w-full text-sm text-left"},Ke={class:"divide-y divide-border"},Qe=["onClick"],Xe={class:"px-6 py-4 font-mono text-xs"},Ze={class:"flex items-center gap-2"},et={key:0,class:"px-1.5 py-0.5 rounded text-[10px] bg-success/15 text-success border border-success/30 normal-case"},tt={class:"px-6 py-4 text-foreground"},st={class:"px-6 py-4"},ot={class:"px-6 py-4 text-foreground-muted text-xs hidden md:table-cell"},at={class:"px-6 py-4 text-foreground-muted font-mono text-xs hidden sm:table-cell"},nt={class:"px-6 py-4 text-foreground-muted font-mono text-xs hidden xl:table-cell"},lt={class:"inline-flex items-center gap-2 justify-end"},rt={key:2,class:"text-foreground-muted/50"},it={key:3,class:"text-foreground-muted/30"},ut={key:0},dt={key:0,class:"p-6 text-sm text-foreground-muted"},ct={key:1,class:"p-5 space-y-4"},mt={class:"flex items-center gap-2 flex-wrap"},pt={key:0,class:"inline-flex items-center px-2.5 py-1 rounded text-xs border bg-background font-mono text-foreground-muted"},vt={class:"grid grid-cols-2 gap-3 text-sm"},ft={key:0},xt={class:"bg-red-950/30 border border-red-900/40 rounded p-3 text-xs text-red-300 font-mono whitespace-pre-wrap break-words"},ht={class:"flex items-center justify-between mb-2"},bt={key:0,class:"text-[10px] text-green-400"},gt={class:"bg-surface border border-border rounded p-3 text-xs text-foreground font-mono overflow-auto max-h-96 whitespace-pre-wrap break-words"},Vt={__name:"Deployments",setup(_t){const U=de(),ne=be(),x=M(()=>ne.params.name),N=p(null),d=p(null),_=p([]),C=p(!1),V=p(""),$=p(!1),F=t=>t&&t.status==="succeeded"&&t.code_hash&&!v(t),y=M(()=>_.value.find(e=>v(e))?.id||null),H=t=>t&&t.status==="succeeded"&&t.code_hash&&!v(t)&&y.value,Y=async t=>{if(!N.value||!t?.id||$.value)return;const e=(t.code_hash||"").slice(0,12);let m=`Code hash ${e}. Current ${d.value?"v"+d.value.version:"version"} stays in history.`;try{const G=(await P(t.id))?.data?.snapshot;if(G&&d.value){const R=ke(d.value,G);R.length?m=`Rolling back to v${t.version} (code ${e}) will also change: ${R.join(` `)} diff --git a/backend/internal/server/ui_dist/assets/Docs-D8EdWzOh.js b/backend/internal/server/ui_dist/assets/Docs-D8EdWzOh.js new file mode 100644 index 0000000..b505a02 --- /dev/null +++ b/backend/internal/server/ui_dist/assets/Docs-D8EdWzOh.js @@ -0,0 +1,327 @@ +import{E as V}from"./format-CsU4_SPu.js";import{c as G}from"./clipboard-CmSw2rR-.js";import{a as Ee,b as De}from"./aiPrompts-DGZ6L7ag.js";import{C as Re,o as W,G as He,a as g,b as e,s as P,n as I,f as n,F as _,p as A,d,l as b,k as a,h as C,bb as $e,t as l,g as Me,r as k,q as v,aD as x,z as s,a2 as Le,Y as qe,j as u,H as Y,X as Be}from"./index-BMkkwZ9q.js";import{H as f,p as Ne,j as J,a as ze,b as B}from"./github-dark-BrynTfs3.js";import{C as N}from"./check-C4wzjDZN.js";import{C as z}from"./copy-CTb6u-fx.js";import{G as K}from"./globe-CR2M7Azm.js";import{C as Z}from"./chevron-right-OdWgNfOU.js";import{K as M}from"./key-round-BccKiRw7.js";import{C as Ke}from"./chevron-down-BTZfO5Md.js";import{V as Ue}from"./variable-b2EnW52t.js";import{L as Xe}from"./lock-Dpr2FIZ9.js";function Fe(L){const q=L.regex,r="HTTP/([32]|1\\.[01])",j=/[A-Za-z][A-Za-z0-9-]*/,E={className:"attribute",begin:q.concat("^",j,"(?=\\:\\s)"),starts:{contains:[{className:"punctuation",begin:/: /,relevance:0,starts:{end:"$",relevance:0}}]}},T=[E,{begin:"\\n\\n",starts:{subLanguage:[],endsWithParent:!0}}];return{name:"HTTP",aliases:["https"],illegal:/\S/,contains:[{begin:"^(?="+r+" \\d{3})",end:/$/,contains:[{className:"meta",begin:r},{className:"number",begin:"\\b\\d{3}\\b"}],starts:{end:/\b\B/,illegal:/\S/,contains:T}},{begin:"(?=^[A-Z]+ (.*?) "+r+"$)",end:/$/,contains:[{className:"string",begin:" ",end:" ",excludeBegin:!0,excludeEnd:!0},{className:"meta",begin:r},{className:"keyword",begin:"[A-Z]+"}],starts:{end:/\b\B/,illegal:/\S/,contains:T}},L.inherit(E,{relevance:0})]}}const Ve={class:"space-y-12 pb-16"},Ge={class:"docs-hero"},We={class:"docs-hero-content"},Ye={class:"docs-hero-row"},Je={class:"docs-hero-actions"},Ze=["title","aria-label"],Qe={class:"docs-hero-toc","aria-label":"Jump to docs section"},eo=["href"],oo={class:"docs-hero-toc-num"},so={id:"handler",class:"space-y-5 scroll-mt-6"},to={class:"doc-table-wrap"},ao={class:"doc-table"},no={class:"doc-cell-key"},co={class:"doc-cell-mono"},ro={class:"doc-cell-mono hidden sm:table-cell"},io={class:"doc-cell-mono hidden md:table-cell"},lo={id:"deploy",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},po={class:"grid grid-cols-1 lg:grid-cols-2 gap-3"},uo={class:"space-y-2"},ho={class:"space-y-2"},vo={class:"space-y-2"},mo={id:"config",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},bo={class:"doc-table-wrap"},go={class:"doc-table"},yo={class:"doc-cell-key whitespace-nowrap"},fo={class:"doc-cell-mono hidden sm:table-cell whitespace-nowrap"},ko={class:"doc-cell-body"},wo={class:"space-y-2"},Co={class:"doc-details group"},xo={class:"doc-details-summary"},To={class:"doc-details-body"},So={id:"sdk",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},_o={class:"space-y-2"},Ao={class:"space-y-2"},Oo={class:"space-y-2"},Po={id:"schedules",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},Io={class:"doc-section-head"},jo={class:"doc-lede"},Eo={id:"webhooks",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},Do={class:"doc-section-head"},Ro={class:"doc-lede"},Ho={class:"doc-table-wrap"},$o={class:"doc-table"},Mo={class:"doc-cell-key whitespace-nowrap"},Lo={class:"doc-cell-body"},qo={class:"space-y-2"},Bo={id:"mcp",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},No={class:"grid grid-cols-1 md:grid-cols-3 gap-3"},zo={class:"doc-card"},Ko={class:"doc-card-body"},Uo={class:"doc-chip break-all"},Xo={class:"doc-token-bar"},Fo={class:"flex items-center gap-2 min-w-0 flex-1"},Vo={key:0,class:"text-sm text-foreground-muted truncate"},Go={key:1,class:"text-sm text-success truncate"},Wo={class:"doc-chip"},Yo=["disabled"],Jo={class:"doc-details group"},Zo={class:"doc-details-summary"},Qo={class:"doc-details-body space-y-4"},es={id:"generate",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},os={class:"ai-prompt-actions"},ss={key:0,class:"prompt-collapse-fade","aria-hidden":"true"},ts=["aria-expanded"],as={id:"tracing",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},ns={class:"doc-table-wrap"},cs={class:"doc-table"},ds={class:"doc-cell-key whitespace-nowrap"},rs={class:"doc-cell-body"},is={id:"errors",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},ls={class:"doc-table-wrap"},ps={class:"doc-table"},us={class:"doc-cell-key whitespace-nowrap"},hs={class:"doc-cell-body"},vs={id:"cli",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},ms={class:"doc-prose"},bs={class:"doc-table-wrap"},gs={class:"doc-table"},ys={class:"doc-cell-key whitespace-nowrap"},fs={class:"doc-cell-mono"},ks={class:"doc-cell-body hidden md:table-cell"},ws={class:"space-y-2"},Cs={class:"space-y-2"},xs={class:"space-y-2"},Ts={class:"space-y-2"},Ss={class:"space-y-2"},_s=`# Available inside every running function — refresh per-invocation: +ORVA_TRACE_ID=tr_3e39f6991c66f140577c6021da7dd13b # one per causal chain +ORVA_SPAN_ID=sp_4ceba57f6b1c982e # this execution + +# Python: os.environ["ORVA_TRACE_ID"] +# Node.js: process.env.ORVA_TRACE_ID +# Reading them is optional — the platform records the trace for you.`,As=`// Function A — calls B via the SDK. Trace context flows automatically. +const { invoke, jobs } = require('orva') + +module.exports.handler = async (event) => { + // F2F call — B becomes a child span under A. + const result = await invoke('send_email', { to: event.email }) + + // Job enqueue — when this job runs (now or in 6 hours), the resulting + // execution lands in the SAME trace as A. + await jobs.enqueue('audit_log', { action: 'sent', to: event.email }) + + return { statusCode: 200, body: 'ok' } +}`,Os=`# Send the W3C traceparent header — Orva will adopt it as the trace root. +curl -H "traceparent: 00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01" \\ + https://orva.example.com/fn/myfn/ + +# Response always echoes: +# X-Trace-Id: tr_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`,Ps=`{ + "error": { + "code": "VALIDATION", + "message": "name must be lowercase and dash-separated", + "request_id": "req_abc123" + } +}`,Is=`# 1. Generate an API key in the dashboard (Keys page) or via the API +# 2. Tell the CLI where to find your Orva and which key to use +orva login \\ + --endpoint https://orva.example.com \\ + --api-key orva_xxx_your_key_here + +# Writes ~/.orva/config.yaml. Subsequent commands need no flags. +orva system health # smoke test`,js=`# Init a project in cwd (creates orva.yaml + handler stub) +orva init + +# Deploy from a directory. Auto-detects handler.ts when tsconfig.json +# is present; else uses the runtime default (handler.js / handler.py). +orva deploy ./my-fn \\ + --name resize-image \\ + --runtime node + +# Override the entrypoint explicitly: +orva deploy ./my-fn --name api --runtime python --entrypoint app.py`,Es=`# Invoke a function by name or fn_: +orva invoke resize-image --data '{"url":"https://example.com/cat.jpg"}' + +# Recent executions: +orva logs resize-image + +# Single execution, with stdout/stderr: +orva logs resize-image --exec-id exec_abc123 + +# Live tail — SSE stream, Ctrl-C to stop: +orva logs resize-image --tail`,Ds=`# List keys (optionally by prefix) +orva kv list resize-image +orva kv list resize-image --prefix user: + +# Read / write / delete +orva kv get resize-image cache:home +orva kv put resize-image cache:home '{"hits":42}' --ttl 3600 +orva kv delete resize-image cache:home`,Rs=`# Secrets — encrypted at rest, injected as env vars at spawn: +orva secrets set resize-image S3_BUCKET my-bucket +orva secrets list resize-image +orva secrets delete resize-image S3_BUCKET + +# Cron — fire a function on a schedule: +orva cron create --fn daily-report --expr '0 9 * * *' --tz Asia/Kolkata +orva cron list +orva cron update --enabled false # pause +orva cron delete + +# Jobs — fire-and-forget background queue: +orva jobs enqueue --fn send-email --data '{"to":"a@b.c"}' +orva jobs list --status pending +orva jobs retry +orva jobs delete + +# Outbound webhooks (system events): +orva webhooks create --url https://hooks.slack.com/... --events deployment.failed,job.failed +orva webhooks test + +# Inbound webhook triggers (external POST → function): +orva webhooks inbound create --fn order-handler --signature stripe`,Hs=`orva system health # daemon up + DB ok +orva system metrics # JSON metrics snapshot +orva system db-stats # on-disk breakdown (orva.db, WAL, functions/) +orva system vacuum # rewrite SQLite to reclaim freelist pages + +orva activity # last 50 activity rows +orva activity --tail # live feed (Ctrl-C) +orva activity --source mcp --limit 200 # MCP-only, last 200`,Q="",Ys={__name:"Docs",setup(L){const q=Re();f.registerLanguage("python",Ne),f.registerLanguage("javascript",J),f.registerLanguage("js",J),f.registerLanguage("json",ze),f.registerLanguage("bash",B),f.registerLanguage("shell",B),f.registerLanguage("sh",B),f.registerLanguage("http",Fe);const r=v(()=>window.location.origin),j=[{id:"handler",num:"01",label:"Handler"},{id:"deploy",num:"02",label:"Deploy"},{id:"config",num:"03",label:"Config"},{id:"sdk",num:"04",label:"SDK"},{id:"schedules",num:"05",label:"Schedules"},{id:"webhooks",num:"06",label:"Webhooks"},{id:"mcp",num:"07",label:"MCP"},{id:"generate",num:"08",label:"AI prompt"},{id:"tracing",num:"09",label:"Tracing"},{id:"errors",num:"10",label:"Errors"},{id:"cli",num:"11",label:"CLI"}],E=k("handler");let T=null;W(()=>{if(typeof IntersectionObserver>"u")return;const c=new Set;T=new IntersectionObserver(o=>{for(const i of o)i.isIntersecting?c.add(i.target.id):c.delete(i.target.id);for(const i of j)if(c.has(i.id)){E.value=i.id;break}},{rootMargin:"-20% 0px -70% 0px",threshold:0});for(const o of j){const i=document.getElementById(o.id);i&&T.observe(i)}}),He(()=>{T&&T.disconnect()});const ee=De(),D=k(!1);let U=null;const oe=async()=>{await Ee()&&(D.value=!0,clearTimeout(U),U=setTimeout(()=>{D.value=!1},1500))},se=x({setup(){return()=>s("svg",{viewBox:"0 0 256 255",width:"14",height:"14",xmlns:"http://www.w3.org/2000/svg"},[s("defs",null,[s("linearGradient",{id:"pyg1",x1:"0",y1:"0",x2:"1",y2:"1"},[s("stop",{offset:"0","stop-color":"#387EB8"}),s("stop",{offset:"1","stop-color":"#366994"})]),s("linearGradient",{id:"pyg2",x1:"0",y1:"0",x2:"1",y2:"1"},[s("stop",{offset:"0","stop-color":"#FFE052"}),s("stop",{offset:"1","stop-color":"#FFC331"})])]),s("path",{fill:"url(#pyg1)",d:"M126.9 12c-58.3 0-54.7 25.3-54.7 25.3l.1 26.2H128v8H50.5S12 67.2 12 126.1c0 58.9 33.6 56.8 33.6 56.8h19.4v-27.4s-1-33.6 33.1-33.6h55.9s32 .5 32-30.9V43.5S191.7 12 126.9 12zM95.7 29.9a10 10 0 0 1 0 20 10 10 0 0 1 0-20z"}),s("path",{fill:"url(#pyg2)",d:"M129.1 243c58.3 0 54.7-25.3 54.7-25.3l-.1-26.2H128v-8h77.5s38.5 4.4 38.5-54.5c0-58.9-33.6-56.8-33.6-56.8h-19.4v27.4s1 33.6-33.1 33.6H102s-32-.5-32 30.9v52S64.3 243 129.1 243zm30.4-17.9a10 10 0 0 1 0-20 10 10 0 0 1 0 20z"})])}}),te=x({setup(){return()=>s("svg",{viewBox:"0 0 256 280",width:"14",height:"14",xmlns:"http://www.w3.org/2000/svg"},[s("path",{fill:"#3F873F",d:"M128 0 12 67v146l116 67 116-67V67L128 0zm0 24.6 95 54.8v121.2l-95 54.8-95-54.8V79.4l95-54.8z"}),s("path",{fill:"#3F873F",d:"M128 64c-3 0-5.7.7-8 2.3L73 92c-5 2.7-8 8-8 13.6V169c0 5.6 3 10.7 8 13.5l13 7.4c6.3 3.1 8.5 3.1 11.4 3.1 9.4 0 14.8-5.7 14.8-15.6V117c0-1-.7-1.7-1.7-1.7H103c-1 0-1.7.7-1.7 1.7v60.2c0 4.4-4.5 8.7-11.8 5.1l-13.7-7.9a1.6 1.6 0 0 1-.8-1.4v-63.4c0-.6.3-1 .8-1.4l46.8-26.9c.4-.3 1-.3 1.4 0L171 110c.5.4.8.8.8 1.4V174a1.7 1.7 0 0 1-.8 1.4l-46.8 27c-.4.2-1 .2-1.4 0l-12-7.2c-.4-.2-.8-.2-1.2 0-3.4 1.9-4 2.2-7.2 3.3-.8.3-2 .7.4 2.1l15.7 9.3c2.5 1.4 5.3 2.2 8.2 2.2 2.9 0 5.7-.8 8.2-2.2L181 184c5-2.8 8-7.9 8-13.5V107c0-5.6-3-10.7-8-13.5l-46.7-26.7a17 17 0 0 0-6.3-2.8z"})])}}),ae=x({name:"DeployPipelineDiagram",setup(){const c=[{glyph:"▣",label:"Tarball",sub:"POST /deploy"},{glyph:"⟜",label:"Extract",sub:"untar → scratch dir"},{glyph:"◍",label:"Install",sub:"npm / pip"},{glyph:"⟐",label:"Compile",sub:"tsc (TypeScript)"},{glyph:"◉",label:"Activate",sub:"rename → current"},{glyph:"✦",label:"Warm pool",sub:"pre-spawn N workers"}];return()=>s("figure",{class:"doc-diagram"},[s("figcaption",{class:"doc-diagram-cap"},"Deploy pipeline"),s("div",{class:"doc-pipeline"},c.flatMap((o,i)=>{const t=s("div",{key:`s${i}`,class:"doc-pipeline-stage"},[s("div",{class:"doc-pipeline-glyph"},o.glyph),s("div",{class:"doc-pipeline-label"},[s("span",{class:"doc-pipeline-name"},o.label),s("span",{class:"doc-pipeline-sub"},o.sub)])]),p=it/220*100;return()=>s("figure",{class:"doc-diagram"},[s("figcaption",{class:"doc-diagram-cap"},"Causal trace, one HTTP request and three spans"),s("div",{class:"doc-trace"},[s("div",{class:"doc-trace-axis"},[s("span",null,"0 ms"),s("span",null,"220 ms")]),...o.map(t=>s("div",{key:t.fn,class:["doc-trace-row",`is-${t.klass}`]},[s("div",{class:"doc-trace-label"},[s("span",{class:"doc-trace-fn"},t.fn),s("span",{class:"doc-trace-trigger"},t.trigger)]),s("div",{class:"doc-trace-track"},[s("div",{class:"doc-trace-bar",style:{left:`${i(t.start)}%`,width:`${i(t.dur)}%`},title:`+${t.start}ms · ${t.dur}ms`})]),s("div",{class:"doc-trace-dur"},`${t.dur}ms`)])),s("div",{class:"doc-trace-legend"},[s("span",null,"Same "),s("code",{class:"doc-chip"},"trace_id"),s("span",null," across all spans · "),s("code",{class:"doc-chip"},"parent_span_id"),s("span",null," chains them into a tree.")])])])}}),ce=x({name:"WebhookDeliveryDiagram",setup(){return()=>s("figure",{class:"doc-diagram"},[s("figcaption",{class:"doc-diagram-cap"},"Signed webhook delivery"),s("div",{class:"doc-webhook"},[s("div",{class:"doc-webhook-actor"},[s("div",{class:"doc-webhook-actor-head"},"orvad"),s("div",{class:"doc-webhook-actor-body"},[s("span",null,"event fires"),s("code",{class:"doc-chip"},"deployment.succeeded")])]),s("div",{class:"doc-webhook-wire"},[s("div",{class:"doc-webhook-wire-line","aria-hidden":"true"}),s("div",{class:"doc-webhook-wire-payload"},[s("div",{class:"doc-webhook-wire-method"},"POST"),s("div",{class:"doc-webhook-wire-headers"},[s("code",null,"X-Orva-Event"),s("code",null,"X-Orva-Timestamp"),s("code",null,"X-Orva-Signature")]),s("div",{class:"doc-webhook-wire-sig"},"sha256=hex(hmac(secret, ts.body))")])]),s("div",{class:"doc-webhook-actor"},[s("div",{class:"doc-webhook-actor-head"},"your receiver"),s("div",{class:"doc-webhook-actor-body"},[s("span",null,"verify HMAC"),s("span",null,"→ 2xx within 15s or get retried")])])])])}}),de=v(()=>[{label:"Python",lang:"python",code:`def handler(event): + body = event.get("body") or {} + return { + "statusCode": 200, + "headers": {"Content-Type": "application/json"}, + "body": {"hello": body.get("name", "world")}, + }`},{label:"Node.js",lang:"js",code:`exports.handler = async (event) => { + const body = event.body || {}; + return { + statusCode: 200, + headers: { 'Content-Type': 'application/json' }, + body: { hello: body.name || 'world' }, + }; +};`}]),re=v(()=>[{label:"curl",lang:"bash",code:`curl -X POST ${r.value}/fn/ \\ + -H 'Content-Type: application/json' \\ + -d '{"name": "Orva"}'`},{label:"fetch",lang:"js",code:`const res = await fetch('${r.value}/fn/', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: 'Orva' }), +}); +console.log(await res.json());`},{label:"Python",lang:"python",code:`import httpx + +r = httpx.post( + "${r.value}/fn/", + json={"name": "Orva"}, +) +print(r.json())`}]),ie=[{id:"python",name:"Python 3.14",entry:"handler.py",deps:"requirements.txt",icon:se},{id:"node",name:"Node.js 24",entry:"handler.js",deps:"package.json",icon:te}],le=[{field:"env_vars",purpose:"Plain config",body:"Plaintext config stored on the function record. Use for feature flags and non-secret settings.",icon:Ue,iconClass:"text-violet-300"},{field:"/secrets",purpose:"Encrypted",body:"AES-256-GCM at rest. Values decrypt only into the worker environment at spawn time.",icon:M,iconClass:"text-emerald-300"},{field:"network_mode",purpose:"Egress control",body:"none = isolated loopback. egress = outbound HTTPS allowed; firewall blocklist applies.",icon:K,iconClass:"text-sky-300"},{field:"auth_mode",purpose:"Invoke gate",body:"none = public. platform_key = require Orva API key. signed = require HMAC.",icon:Xe,iconClass:"text-violet-300"},{field:"rate_limit_per_min",purpose:"Per-IP throttle",body:"Optional cap for public or webhook-facing functions. Exceeding it returns 429.",icon:Be,iconClass:"text-amber-300"}],pe=v(()=>`curl -X POST ${r.value}/api/v1/functions \\ + -H 'X-Orva-API-Key: ' \\ + -H 'Content-Type: application/json' \\ + -d '{"name":"hello","runtime":"python","memory_mb":128,"cpus":0.5}'`),ue=v(()=>`tar czf code.tar.gz handler.py requirements.txt +curl -X POST ${r.value}/api/v1/functions//deploy \\ + -H 'X-Orva-API-Key: ' \\ + -F code=@code.tar.gz`),he=v(()=>`curl -X POST ${r.value}/api/v1/functions//secrets \\ + -H 'X-Orva-API-Key: ' \\ + -H 'Content-Type: application/json' \\ + -d '{"key":"DATABASE_URL","value":"postgres://..."}'`),ve=v(()=>`# generate signature +SECRET='your-shared-secret-stored-in-function-secrets' +TS=$(date +%s) +BODY='{"hello":"world"}' +SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $2}') + +curl -X POST ${r.value}/fn/ \\ + -H "X-Orva-Timestamp: $TS" \\ + -H "X-Orva-Signature: sha256=$SIG" \\ + -H 'Content-Type: application/json' \\ + -d "$BODY"`),me=v(()=>[{label:"curl",lang:"bash",note:"Create a daily-9am schedule for an existing function. payload is delivered as the invoke body.",code:`curl -X POST ${r.value}/api/v1/functions//cron \\ + -H 'X-Orva-API-Key: ' \\ + -H 'Content-Type: application/json' \\ + -d '{ + "cron_expr": "0 9 * * *", + "enabled": true, + "payload": {"task": "daily-summary"} + }'`},{label:"Toggle / edit",lang:"bash",note:"PUT accepts any subset of {cron_expr, enabled, payload}; omitted fields keep their previous value. next_run_at is recomputed on expr changes.",code:`# pause +curl -X PUT ${r.value}/api/v1/functions//cron/ \\ + -H 'X-Orva-API-Key: ' \\ + -H 'Content-Type: application/json' \\ + -d '{"enabled": false}' + +# change schedule +curl -X PUT ${r.value}/api/v1/functions//cron/ \\ + -H 'X-Orva-API-Key: ' \\ + -H 'Content-Type: application/json' \\ + -d '{"cron_expr": "*/15 * * * *"}'`},{label:"List & delete",lang:"bash",note:"GET /api/v1/cron lists every schedule across functions (with function_name JOIN); per-function uses the nested route.",code:`# all schedules +curl ${r.value}/api/v1/cron \\ + -H 'X-Orva-API-Key: ' + +# delete one +curl -X DELETE ${r.value}/api/v1/functions//cron/ \\ + -H 'X-Orva-API-Key: '`}]),be=[{label:"Python",lang:"python",code:`from orva import kv + +def handler(event): + # Store with optional TTL (seconds). 0 = no expiry. + kv.put("user:42", {"name": "Ada", "tier": "pro"}, ttl_seconds=3600) + + # Read; default returned if missing or expired. + user = kv.get("user:42", default=None) + + # List by prefix. + pages = kv.list(prefix="page:", limit=50) + + # Delete is idempotent. + kv.delete("user:42") + + return {"statusCode": 200, "body": str(user)}`},{label:"Node.js",lang:"js",code:`const { kv } = require('orva') + +exports.handler = async (event) => { + await kv.put('user:42', { name: 'Ada', tier: 'pro' }, { ttlSeconds: 3600 }) + + const user = await kv.get('user:42', null) + + const pages = await kv.list({ prefix: 'page:', limit: 50 }) + + await kv.delete('user:42') + + return { statusCode: 200, body: JSON.stringify(user) } +}`}],ge=[{label:"Python",lang:"python",code:`from orva import invoke, OrvaError + +def handler(event): + try: + # invoke() returns the downstream {statusCode, headers, body}. + # body is JSON-decoded when possible. + result = invoke("resize-image", {"url": event["body"]["url"]}) + return {"statusCode": 200, "body": result["body"]} + except OrvaError as e: + # 404 = function not found, 507 = call depth exceeded. + return {"statusCode": e.status or 502, "body": str(e)}`},{label:"Node.js",lang:"js",code:`const { invoke, OrvaError } = require('orva') + +exports.handler = async (event) => { + try { + const result = await invoke('resize-image', { url: event.body.url }) + return { statusCode: 200, body: result.body } + } catch (e) { + if (e instanceof OrvaError) { + return { statusCode: e.status || 502, body: e.message } + } + throw e + } +}`}],ye=[{label:"Python",lang:"python",code:`from orva import jobs + +def handler(event): + # Fire-and-forget. Returns the job id immediately; the function + # body runs later via the scheduler. max_attempts retries with + # exponential backoff on 5xx / exception. + job_id = jobs.enqueue( + "send-welcome-email", + {"to": event["body"]["email"]}, + max_attempts=3, + ) + return {"statusCode": 202, "body": job_id}`},{label:"Node.js",lang:"js",code:`const { jobs } = require('orva') + +exports.handler = async (event) => { + const jobId = await jobs.enqueue( + 'send-welcome-email', + { to: event.body.email }, + { maxAttempts: 3 } + ) + return { statusCode: 202, body: jobId } +}`}],fe=[{name:"deployment.succeeded",when:"A function build finished and the new version is active."},{name:"deployment.failed",when:"A build failed or was rejected."},{name:"function.created",when:"A new function row was created via POST /api/v1/functions."},{name:"function.updated",when:"A function config was edited via PUT /api/v1/functions/{id} (status flips during a deploy do NOT fire this; see deployment.*)."},{name:"function.deleted",when:"A function was removed."},{name:"execution.error",when:"An invocation finished with status=error or 5xx."},{name:"cron.failed",when:"A scheduled run failed (bad expr, missing fn, dispatch error, or 5xx)."},{name:"job.succeeded",when:"A queued background job finished successfully."},{name:"job.failed",when:"A queued job exhausted its retries (terminal failure)."}],ke=[{label:"Python",lang:"python",note:"Run on the receiver. Reject anything that fails verification. The signature ensures the request really came from this Orva instance.",code:`import hmac, hashlib, time + +def verify(secret: str, ts: str, body: bytes, sig_header: str) -> bool: + if abs(time.time() - int(ts)) > 300: # 5-min skew window + return False + mac = hmac.new(secret.encode(), f"{ts}.".encode() + body, hashlib.sha256) + expected = "sha256=" + mac.hexdigest() + return hmac.compare_digest(expected, sig_header) + +# In your Flask/FastAPI/etc. handler: +ts = request.headers["X-Orva-Timestamp"] +sig = request.headers["X-Orva-Signature"] +if not verify(WEBHOOK_SECRET, ts, request.get_data(), sig): + return "bad signature", 401`},{label:"Node.js",lang:"js",note:"Same shape as Stripe. Use timingSafeEqual to avoid sig-leak via timing.",code:`const crypto = require('crypto') + +function verify(secret, ts, body, sigHeader) { + if (Math.abs(Date.now() / 1000 - parseInt(ts, 10)) > 300) return false + const mac = crypto.createHmac('sha256', secret) + mac.update(ts + '.') + mac.update(body) + const expected = 'sha256=' + mac.digest('hex') + if (expected.length !== sigHeader.length) return false + return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sigHeader)) +} + +// In an express handler with raw body middleware: +app.post('/webhooks/orva', (req, res) => { + const ok = verify( + process.env.WEBHOOK_SECRET, + req.headers['x-orva-timestamp'], + req.body, // raw bytes — NOT parsed JSON + req.headers['x-orva-signature'] + ) + if (!ok) return res.status(401).send('bad signature') + res.sendStatus(200) +})`}],we=[{name:"http",desc:"Public HTTP request hit /fn//. Almost always a root span."},{name:"f2f",desc:"Another function called this one via orva.invoke(). Has a parent_span_id."},{name:"job",desc:"Background job runner picked up an enqueued job. Parent_span_id is whoever enqueued it."},{name:"cron",desc:"Scheduler fired a cron entry. Always a root span."},{name:"inbound",desc:"External webhook hit /webhook/{id}. Always a root span."},{name:"replay",desc:"Operator clicked Replay on a captured execution. Fresh trace, no link to original."},{name:"mcp",desc:"AI agent invoked the function via MCP invoke_function. Fresh trace."}],Ce=[{code:"VALIDATION",when:"Bad request body or path parameter."},{code:"UNAUTHORIZED",when:"Missing or invalid API key / session cookie."},{code:"NOT_FOUND",when:"Function, deployment, or secret doesn't exist."},{code:"RATE_LIMITED",when:"Too many requests; check the Retry-After header."},{code:"VERSION_GCD",when:"Rollback target was garbage-collected."},{code:"INSUFFICIENT_DISK",when:"Host is below min_free_disk_mb."}],xe=[{cmd:"login",subs:V,purpose:"Save endpoint + API key to ~/.orva/config.yaml"},{cmd:"init",subs:V,purpose:"Scaffold an orva.yaml in the current directory"},{cmd:"deploy",subs:"[path]",purpose:"Package a directory and deploy as a function"},{cmd:"invoke",subs:"[name|id]",purpose:"POST to /fn// and print the response"},{cmd:"logs",subs:"[name|id] [--tail]",purpose:"List recent executions; --tail follows live via SSE"},{cmd:"functions",subs:"list / get / create / delete",purpose:"CRUD for the function registry"},{cmd:"cron",subs:"list / create / update / delete",purpose:"Manage cron schedules attached to functions"},{cmd:"jobs",subs:"list / enqueue / retry / delete",purpose:"Background queue management"},{cmd:"kv",subs:"list / get / put / delete",purpose:"Browse a function’s key/value store"},{cmd:"secrets",subs:"list / set / delete",purpose:"AES-256-GCM secrets per function"},{cmd:"webhooks",subs:"list / create / test / delete / inbound",purpose:"System-event subscribers + inbound triggers"},{cmd:"routes",subs:"list / set / delete",purpose:"Custom URL → function path mappings"},{cmd:"keys",subs:"list / create / revoke",purpose:"Manage API keys"},{cmd:"activity",subs:"[--tail] [--source web|api|...]",purpose:"Paginated activity rows; live SSE with --tail"},{cmd:"system",subs:"health / metrics / db-stats / vacuum",purpose:"Server diagnostics"},{cmd:"setup",subs:"[--skip-nsjail] [--skip-rootfs]",purpose:"Install nsjail + rootfs on a bare host"},{cmd:"serve",subs:"[--port N]",purpose:"Run as the server daemon (not the CLI client)"},{cmd:"completion",subs:"bash / zsh / fish / powershell",purpose:"Emit shell completion script"}],X=k("");W(async()=>{try{const c=await fetch("/web/docs.md",{cache:"no-cache"});c.ok&&(X.value=await c.text())}catch{}});const Te=v(()=>X.value.replaceAll("{{ORIGIN}}",window.location.origin)),O=k(!1);let F=null;const Se=async()=>{await G(Te.value)&&(O.value=!0,clearTimeout(F),F=setTimeout(()=>{O.value=!1},1500))},S=k(!1),R=k(""),H=k(!1),_e=v(()=>R.value.slice(0,12)),m=v(()=>R.value||Q),Ae=async()=>{if(!H.value){H.value=!0;try{const c=new Date().toISOString().slice(0,16).replace("T"," "),o=await qe.post("/keys",{name:"MCP: "+c,permissions:["invoke","read","write","admin"]});R.value=o.data.key}catch(c){console.error("mint mcp key failed",c),q.notify({title:"Could not mint key",message:c?.response?.data?.error?.message||c.message||"Unknown error",danger:!0})}finally{H.value=!1}}},Oe=v(()=>[{label:"Claude Code",lang:"bash",note:"Anthropic's `claude` CLI. Restart Claude Code afterwards; `/mcp` lists Orva's 70 tools.",code:`claude mcp add --transport http --scope user orva ${r.value}/mcp --header "Authorization: Bearer ${m.value}"`},{label:"curl",lang:"bash",note:"Talk to MCP directly. Step 1 returns a session id (Mcp-Session-Id) that Step 2 references.",code:`curl -sD - -X POST ${r.value}/mcp \\ + -H 'Authorization: Bearer ${m.value}' \\ + -H 'Content-Type: application/json' \\ + -H 'Accept: application/json, text/event-stream' \\ + -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}' + +curl -sX POST ${r.value}/mcp \\ + -H 'Authorization: Bearer ${m.value}' \\ + -H 'Content-Type: application/json' \\ + -H 'Accept: application/json, text/event-stream' \\ + -H 'Mcp-Session-Id: ' \\ + -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'`}]),Pe=v(()=>[{label:"Claude Desktop",lang:"json",note:"Paste into ~/Library/Application Support/Claude/claude_desktop_config.json (macOS), %APPDATA%\\Claude\\claude_desktop_config.json (Windows), or ~/.config/Claude/claude_desktop_config.json (Linux). Restart Claude Desktop.",code:`{ + "mcpServers": { + "orva": { + "url": "${r.value}/mcp", + "headers": { + "Authorization": "Bearer ${m.value}" + } + } + } +}`},{label:"Cursor",lang:"bash",note:"Open the link in your browser. Cursor pops an approval dialog and writes ~/.cursor/mcp.json.",code:`cursor://anysphere.cursor-deeplink/mcp/install?name=orva&config=${Ie.value}`},{label:"VS Code",lang:"bash",note:'User-scoped install via the Copilot-MCP `code --add-mcp` flag. Pick "Workspace" at the prompt to write .vscode/mcp.json instead.',code:`code --add-mcp '{"name":"orva","type":"http","url":"${r.value}/mcp","headers":{"Authorization":"Bearer ${m.value}"}}'`},{label:"Codex CLI",lang:"bash",note:"OpenAI's `codex` CLI. Writes to ~/.codex/config.toml.",code:`codex mcp add --transport streamable-http orva ${r.value}/mcp --header "Authorization: Bearer ${m.value}"`},{label:"OpenCode",lang:"bash",note:`Interactive add. Pick "Remote", paste ${r.value}/mcp, then add the header Authorization: Bearer ${m.value}.`,code:"opencode mcp add"},{label:"Zed",lang:"json",note:"Zed runs MCP as stdio subprocesses, so use the `mcp-remote` bridge. Paste under context_servers in ~/.config/zed/settings.json. Restart Zed.",code:`{ + "context_servers": { + "orva": { + "source": "custom", + "command": "npx", + "args": [ + "-y", "mcp-remote", + "${r.value}/mcp", + "--header", "Authorization:Bearer ${m.value}" + ] + } + } +}`},{label:"Windsurf",lang:"json",note:"Paste into ~/.codeium/windsurf/mcp_config.json and reload Windsurf.",code:`{ + "mcpServers": { + "orva": { + "serverUrl": "${r.value}/mcp", + "headers": { + "Authorization": "Bearer ${m.value}" + } + } + } +}`},{label:"claude.ai web",lang:"text",note:"UI-only flow. Settings → Connectors → Add custom connector. claude.ai opens an Orva login + consent popup and issues an OAuth 2.1 token automatically; no token paste required.",code:`URL: ${r.value}/mcp +Auth: OAuth (auto-discovered)`},{label:"ChatGPT",lang:"text",note:"UI-only flow. Settings → Apps & Connectors → Developer mode → Add new connector. ChatGPT discovers OIDC metadata, performs Dynamic Client Registration, and pops the Orva consent screen. No token paste required.",code:`URL: ${r.value}/mcp +Auth: OAuth (auto-discovered)`}]),Ie=v(()=>{const c=JSON.stringify({url:r.value+"/mcp",headers:{Authorization:"Bearer "+m.value}});return typeof window.btoa=="function"?window.btoa(c):c}),je=v(()=>[{label:"Cursor (global)",lang:"json",note:"Paste into ~/.cursor/mcp.json, or .cursor/mcp.json in your project root for a per-workspace install.",code:`{ + "mcpServers": { + "orva": { + "url": "${r.value}/mcp", + "headers": { + "Authorization": "Bearer ${m.value}" + } + } + } +}`},{label:"Cline",lang:"json",note:"In VS Code: open Cline → MCP icon → Configure MCP Servers. Cline writes cline_mcp_settings.json.",code:`{ + "mcpServers": { + "orva": { + "url": "${r.value}/mcp", + "headers": { + "Authorization": "Bearer ${m.value}" + }, + "disabled": false + } + } +}`}]),h=x({name:"CodeBlock",props:{code:{type:String,required:!0},lang:{type:String,default:""}},setup(c){const o=k(!1),i=async()=>{await G(c.code)&&(o.value=!0,setTimeout(()=>{o.value=!1},1200))},t=v(()=>{const p=(c.lang||"").toLowerCase();if(p&&f.getLanguage(p))try{return f.highlight(c.code,{language:p,ignoreIllegals:!0}).value}catch{}return c.code.replace(/&/g,"&").replace(//g,">")});return()=>s("div",{class:"codeblock"},[s("div",{class:"codeblock-bar"},[s("span",{class:"codeblock-lang"},c.lang||""),s("button",{class:"codeblock-copy",onClick:i,title:"Copy code"},[o.value?s(N,{class:"w-3 h-3"}):s(z,{class:"w-3 h-3"}),o.value?"Copied":"Copy"])]),s("pre",{class:"codeblock-pre"},[s("code",{class:`hljs language-${(c.lang||"text").toLowerCase()}`,innerHTML:t.value})])])}}),y=x({name:"TabbedCode",props:{tabs:{type:Array,required:!0},storageKey:{type:String,default:""}},setup(c){const o=(()=>{try{if(c.storageKey){const p=localStorage.getItem(c.storageKey);if(p&&c.tabs.some(w=>w.label===p))return p}}catch{}return c.tabs[0]?.label})(),i=k(o),t=p=>{i.value=p;try{c.storageKey&&localStorage.setItem(c.storageKey,p)}catch{}};return()=>{const p=c.tabs.find(w=>w.label===i.value)||c.tabs[0];return s("div",{class:"tabbed"},[s("div",{class:"tabbed-tabs"},c.tabs.map(w=>s("button",{key:w.label,class:["tabbed-tab",{active:w.label===i.value}],onClick:()=>t(w.label)},w.label))),p.note?s("div",{class:"tabbed-note"},p.note):null,s(h,{code:p.code,lang:p.lang})])}}}),$=x({name:"Callout",props:{title:{type:String,default:""},icon:{type:[Object,Function],default:null}},setup(c,{slots:o}){return()=>s("div",{class:"callout"},[s("div",{class:"callout-head"},[c.icon?s(c.icon,{class:"callout-icon"}):null,c.title?s("span",null,c.title):null]),s("div",{class:"callout-body"},o.default?.())])}});return(c,o)=>{const i=Le("router-link");return u(),g("div",Ve,[e("header",Ge,[o[3]||(o[3]=e("div",{class:"docs-hero-bg","aria-hidden":"true"},null,-1)),e("div",We,[e("div",Ye,[o[1]||(o[1]=e("div",{class:"docs-hero-text"},[e("h1",{class:"docs-hero-title"}," Documentation "),e("p",{class:"docs-hero-sub"}," Everything you need to write, deploy, and operate functions on Orva. Handler contract, deploy + invoke, SDK, MCP, tracing, error taxonomy. ")],-1)),e("div",Je,[e("button",{class:P(["docs-hero-copy-icon",{copied:O.value}]),title:O.value?"Copied":"Copy entire docs page as Markdown","aria-label":O.value?"Markdown copied to clipboard":"Copy entire docs page as Markdown",onClick:Se},[O.value?(u(),I(n(N),{key:0,class:"w-4 h-4"})):(u(),I(n(z),{key:1,class:"w-4 h-4"}))],10,Ze)])]),e("nav",Qe,[o[2]||(o[2]=e("span",{class:"docs-hero-toc-label"},"Jump to",-1)),(u(),g(_,null,A(j,t=>e("a",{key:t.id,href:`#${t.id}`,class:P(["docs-hero-toc-link",{active:E.value===t.id}])},[e("span",oo,l(t.num),1),e("span",null,l(t.label),1)],10,eo)),64))])])]),e("section",so,[o[5]||(o[5]=e("div",{class:"doc-section-head"},[e("span",{class:"doc-section-num"},"01"),e("div",null,[e("h2",{class:"doc-section-title"}," Handler contract "),e("p",{class:"doc-lede"}," One exported function receives the inbound HTTP event and returns an HTTP-shaped response. The adapter handles serialization and headers. ")])],-1)),d(n(y),{tabs:de.value,"storage-key":"docs.handler"},null,8,["tabs"]),o[6]||(o[6]=b('
Event shape
methodpathheadersquerybody
Response
{ statusCode, headers, body }

Non-string bodies are JSON-encoded by the adapter.

Runtime env
Env vars and secrets land in process.env / os.environ.
',1)),e("div",to,[e("table",ao,[o[4]||(o[4]=e("thead",null,[e("tr",null,[e("th",null,"Runtime"),e("th",null,"ID"),e("th",{class:"hidden sm:table-cell"}," Entrypoint "),e("th",{class:"hidden md:table-cell"}," Dependencies ")])],-1)),e("tbody",null,[(u(),g(_,null,A(ie,t=>e("tr",{key:t.id},[e("td",no,[(u(),I(Y(t.icon),{class:"shrink-0"})),a(" "+l(t.name),1)]),e("td",co,l(t.id),1),e("td",ro,l(t.entry),1),e("td",io,l(t.deps),1)])),64))])])])]),e("section",lo,[o[11]||(o[11]=b('
02

Deploy & invoke

The dashboard handles day-to-day work; these calls are for CI and automation. Builds run async; poll /api/v1/deployments/<id> or stream /api/v1/deployments/<id>/stream until phase: done.

',1)),d(n(ae)),e("div",po,[e("div",uo,[o[7]||(o[7]=e("div",{class:"doc-step-label"},[e("span",{class:"doc-step-num"},"1"),a(" Create the function row ")],-1)),d(n(h),{code:pe.value,lang:"bash"},null,8,["code"])]),e("div",ho,[o[8]||(o[8]=e("div",{class:"doc-step-label"},[e("span",{class:"doc-step-num"},"2"),a(" Upload code ")],-1)),d(n(h),{code:ue.value,lang:"bash"},null,8,["code"])])]),e("div",vo,[o[9]||(o[9]=e("div",{class:"doc-microlabel"}," Invoke ",-1)),d(n(y),{tabs:re.value,"storage-key":"docs.invoke"},null,8,["tabs"])]),d(n($),{icon:n(K),title:"Custom routes"},{default:C(()=>[...o[10]||(o[10]=[a(" Attach a friendly path with ",-1),e("code",{class:"doc-chip"},"POST /api/v1/routes",-1),a(". Reserved prefixes: ",-1),e("code",{class:"doc-chip"},"/api/",-1),e("code",{class:"doc-chip"},"/fn/",-1),e("code",{class:"doc-chip"},"/mcp/",-1),e("code",{class:"doc-chip"},"/web/",-1),e("code",{class:"doc-chip"},"/_orva/",-1),a(". ",-1)])]),_:1},8,["icon"])]),e("section",mo,[o[15]||(o[15]=e("div",{class:"doc-section-head"},[e("span",{class:"doc-section-num"},"03"),e("div",null,[e("h2",{class:"doc-section-title"}," Configuration reference "),e("p",{class:"doc-lede"}," Everything below lives on the function record. Secrets are stored encrypted and only decrypt into the worker environment at spawn time. ")])],-1)),e("div",bo,[e("table",go,[o[12]||(o[12]=e("thead",null,[e("tr",null,[e("th",null,"Field"),e("th",{class:"hidden sm:table-cell"}," Purpose "),e("th",null,"Behaviour")])],-1)),e("tbody",null,[(u(),g(_,null,A(le,t=>e("tr",{key:t.field,class:"align-top"},[e("td",yo,[(u(),I(Y(t.icon),{class:P(["w-3.5 h-3.5 shrink-0",t.iconClass])},null,8,["class"])),e("code",null,l(t.field),1)]),e("td",fo,l(t.purpose),1),e("td",ko,l(t.body),1)])),64))])])]),e("div",wo,[o[13]||(o[13]=e("div",{class:"doc-microlabel"}," Set a secret ",-1)),d(n(h),{code:he.value,lang:"bash"},null,8,["code"])]),e("details",Co,[e("summary",xo,[d(n(Z),{class:"w-3.5 h-3.5 transition-transform group-open:rotate-90 text-foreground-muted"}),o[14]||(o[14]=a(" Signed-invoke recipe (HMAC, opt-in) ",-1))]),e("div",To,[d(n(h),{code:ve.value,lang:"bash"},null,8,["code"])])])]),e("section",So,[o[21]||(o[21]=b('
04

SDK from inside a function

The bundled orva module exposes three primitives every function can use without extra dependencies: a per-function key/value store, in-process calls to other Orva functions, and a fire-and-forget background job queue. Routes through the per-process internal token injected at worker spawn time.

orva.kv
put / get / delete / list

Per-function namespace on SQLite, optional TTL.

orva.invoke
invoke(name, payload)

In-process call to another function. 8-deep call cap.

orva.jobs
jobs.enqueue(name, payload)

Fire-and-forget; persisted; retried with exp backoff.

',2)),e("div",_o,[o[16]||(o[16]=e("div",{class:"doc-microlabel"}," KV: get/put with TTL ",-1)),d(n(y),{tabs:be,"storage-key":"docs.sdk.kv"}),o[17]||(o[17]=b('

Browse / inspect / edit / delete / set keys without leaving the dashboard at /web/functions/<name>/kv (or click the KV button in the editor's action bar). REST mirror at GET/PUT/DELETE /api/v1/functions/<id>/kv[/<key>]; MCP tools kv_list / kv_get / kv_put / kv_delete for agents.

',1))]),e("div",Ao,[o[18]||(o[18]=e("div",{class:"doc-microlabel"}," Function-to-function: invoke() ",-1)),d(n(y),{tabs:ge,"storage-key":"docs.sdk.invoke"})]),e("div",Oo,[o[19]||(o[19]=e("div",{class:"doc-microlabel"}," Background jobs: jobs.enqueue() ",-1)),d(n(y),{tabs:ye,"storage-key":"docs.sdk.jobs"})]),d(n($),{icon:n(K),title:"Network mode"},{default:C(()=>[...o[20]||(o[20]=[a(" The SDK reaches orvad over loopback through the host gateway, so the function needs ",-1),e("code",{class:"doc-chip"},'network_mode: "egress"',-1),a(". On the default ",-1),e("code",{class:"doc-chip"},'"none"',-1),a(" the SDK throws ",-1),e("code",{class:"doc-chip"},"OrvaUnavailableError",-1),a(" with a clear hint. ",-1)])]),_:1},8,["icon"])]),e("section",Po,[e("div",Io,[o[32]||(o[32]=e("span",{class:"doc-section-num"},"05",-1)),e("div",null,[o[31]||(o[31]=e("h2",{class:"doc-section-title"}," Schedules ",-1)),e("p",jo,[o[23]||(o[23]=a(" Fire any function on a cron expression. The scheduler runs as part of the orvad process; no external service. Manage from the ",-1)),d(i,{to:"/cron",class:"text-foreground hover:text-white underline decoration-dotted underline-offset-4"},{default:C(()=>[...o[22]||(o[22]=[a("Schedules page",-1)])]),_:1}),o[24]||(o[24]=a(" or via the API. Standard 5-field cron with the usual shorthands (",-1)),o[25]||(o[25]=e("code",{class:"doc-chip"},"@daily",-1)),o[26]||(o[26]=a(", ",-1)),o[27]||(o[27]=e("code",{class:"doc-chip"},"@hourly",-1)),o[28]||(o[28]=a(", ",-1)),o[29]||(o[29]=e("code",{class:"doc-chip"},"*/5 * * * *",-1)),o[30]||(o[30]=a("). ",-1))])])]),d(n(y),{tabs:me.value,"storage-key":"docs.cron"},null,8,["tabs"]),d(n($),{icon:n($e),title:"Cron-fired headers"},{default:C(()=>[...o[33]||(o[33]=[a(" Every cron-triggered invocation arrives at the function with ",-1),e("code",{class:"doc-chip"},"x-orva-trigger: cron",-1),a(" and ",-1),e("code",{class:"doc-chip"},"x-orva-cron-id: cron_…",-1),a(" on the event headers, so user code can branch on origin. ",-1)])]),_:1},8,["icon"])]),e("section",Eo,[e("div",Do,[o[38]||(o[38]=e("span",{class:"doc-section-num"},"06",-1)),e("div",null,[o[37]||(o[37]=e("h2",{class:"doc-section-title"}," Webhooks ",-1)),e("p",Ro,[o[35]||(o[35]=a(" Operator-managed subscriptions for system events. Configure URLs from the ",-1)),d(i,{to:"/webhooks",class:"text-foreground hover:text-white underline decoration-dotted underline-offset-4"},{default:C(()=>[...o[34]||(o[34]=[a("Webhooks page",-1)])]),_:1}),o[36]||(o[36]=a("; Orva delivers signed POSTs to them when matching events fire (deployments, function lifecycle, cron failures, job outcomes). Subscriptions are global, not per-function. ",-1))])])]),d(n(ce)),o[41]||(o[41]=b('
Headers
X-Orva-EventX-Orva-Delivery-IdX-Orva-TimestampX-Orva-Signature
Signature
sha256=hex(hmac(secret, ts.body))

Same shape as Stripe / signed-invoke. Receivers verify with the secret returned at create time.

Retries
5 attemptsexp backoff (≤ 1h)

Receiver must 2xx within 15s.

',1)),e("div",Ho,[e("table",$o,[o[39]||(o[39]=e("thead",null,[e("tr",null,[e("th",null,"Event"),e("th",null,"When it fires")])],-1)),e("tbody",null,[(u(),g(_,null,A(fe,t=>e("tr",{key:t.name},[e("td",Mo,[e("code",null,l(t.name),1)]),e("td",Lo,l(t.when),1)])),64))])])]),e("div",qo,[o[40]||(o[40]=e("div",{class:"doc-microlabel"}," Verify a delivery ",-1)),d(n(y),{tabs:ke,"storage-key":"docs.webhooks.verify"})])]),e("section",Bo,[o[51]||(o[51]=e("div",{class:"doc-section-head"},[e("span",{class:"doc-section-num"},"07"),e("div",null,[e("h2",{class:"doc-section-title"}," MCP: Model Context Protocol "),e("p",{class:"doc-lede"}," Same API surface the dashboard uses, exposed as 70 tools an agent can call directly. API key permissions scope the available tool set. ")])],-1)),e("div",No,[e("div",zo,[o[42]||(o[42]=e("div",{class:"doc-microlabel"}," Endpoint ",-1)),e("div",Ko,[e("code",Uo,l(r.value)+"/mcp",1)])]),o[43]||(o[43]=b('
Auth header
Authorization: Bearer <token>

Or as a fallback: X-Orva-API-Key: <token>

Transport
Streamable HTTPMCP 2025-11-25
',2))]),d(n($),{icon:n(M),title:"Two header formats; same auth"},{default:C(()=>[...o[44]||(o[44]=[a(" Either header works against the same API key store with identical permission gating. ",-1),e("code",{class:"doc-chip"},"Authorization: Bearer",-1),a(" is the MCP / OAuth 2 spec form; every MCP SDK (Claude Code, Claude Desktop, Cursor, mcp-remote, Python ",-1),e("code",{class:"doc-chip"},"mcp",-1),a(") configures it natively, so prefer it for new setups. ",-1),e("code",{class:"doc-chip"},"X-Orva-API-Key",-1),a(" is the same header the REST API accepts; useful when a tool reuses an existing Orva REST integration. Internally both paths SHA-256-hash the token and look it up against the same ",-1),e("code",{class:"doc-chip"},"api_keys",-1),a(" table. ",-1)])]),_:1},8,["icon"]),e("div",Xo,[e("div",Fo,[d(n(M),{class:"w-4 h-4 shrink-0 text-foreground-muted"}),R.value?(u(),g("span",Go,[o[47]||(o[47]=a(" Token minted: ",-1)),e("code",Wo,l(_e.value)+"…",1),o[48]||(o[48]=a(" Shown once, copy now. ",-1))])):(u(),g("span",Vo,[o[45]||(o[45]=a(" Snippets show ",-1)),e("code",{class:"doc-chip"},l(Q)),o[46]||(o[46]=a(". Mint a token to substitute it everywhere. ",-1))]))]),e("button",{class:"doc-token-btn",disabled:H.value,onClick:Ae},[d(n(M),{class:"w-3.5 h-3.5"}),a(" "+l(R.value?"Mint another":H.value?"Minting…":"Generate token"),1)],8,Yo)]),d(n(y),{tabs:Oe.value,"storage-key":"docs.mcp.install"},null,8,["tabs"]),e("details",Jo,[e("summary",Zo,[d(n(Z),{class:"w-3.5 h-3.5 transition-transform group-open:rotate-90 text-foreground-muted"}),o[49]||(o[49]=a(" More clients (Cursor, VS Code, Codex CLI, OpenCode, Zed, Windsurf, ChatGPT, manual config) ",-1))]),e("div",Qo,[d(n(y),{tabs:Pe.value,"storage-key":"docs.mcp.install.more"},null,8,["tabs"]),o[50]||(o[50]=e("div",{class:"doc-microlabel pt-1"}," Hand-edited config files ",-1)),d(n(y),{tabs:je.value,"storage-key":"docs.mcp.manual"},null,8,["tabs"])])])]),e("section",es,[o[52]||(o[52]=b('
08

System prompt for AI assistants

Paste the prompt below into ChatGPT, Claude, Gemini, Cursor, Copilot, or any other AI tool to teach it Orva's full surface Handler contract, runtimes, sandbox limits, the in-sandbox orva SDK (kv / invoke / jobs), cron triggers, system-event webhooks, auth modes, and production patterns. The model then turns "describe what I want" into a pasteable handler on the first try.

',1)),e("div",os,[e("button",{class:P(["ai-copy-btn",{copied:D.value}]),onClick:oe},[D.value?(u(),I(n(N),{key:0,class:"w-3.5 h-3.5"})):(u(),I(n(z),{key:1,class:"w-3.5 h-3.5"})),a(" "+l(D.value?"Copied":"Copy system prompt"),1)],2)]),e("div",{class:P(["prompt-collapse",{expanded:S.value}])},[d(n(h),{code:n(ee),lang:"text"},null,8,["code"]),S.value?Me("",!0):(u(),g("div",ss))],2),e("button",{class:"prompt-expand-btn","aria-expanded":S.value,onClick:o[0]||(o[0]=t=>S.value=!S.value)},[d(n(Ke),{class:P(["w-3.5 h-3.5 transition-transform",{"rotate-180":S.value}])},null,8,["class"]),a(" "+l(S.value?"Collapse system prompt":"Expand full system prompt (~400 lines)"),1)],8,ts)]),e("section",as,[o[54]||(o[54]=b('
09

Tracing

Every invocation chain is recorded as a causal trace. automatically, with zero changes to your function code. HTTP requests, F2F invokes, jobs, cron, inbound webhooks, and replays all stitch into the same tree. The dashboard renders it as a waterfall at /traces.

Each execution row IS a span. Spans share a trace_id; child spans point at their parent via parent_span_id. You don't instantiate spans, you don't import a tracer; you just write your handler and the platform plumbs IDs through every internal hop.

',2)),d(n(ne)),o[55]||(o[55]=e("h3",{class:"doc-h3"},"What user code sees",-1)),o[56]||(o[56]=e("p",{class:"doc-prose"}," Two env vars are stamped per invocation. Read them only if you want to log the trace_id alongside your own messages; they're optional. ",-1)),d(n(h),{code:_s,lang:"text"}),o[57]||(o[57]=e("h3",{class:"doc-h3"},"Automatic propagation",-1)),o[58]||(o[58]=e("p",{class:"doc-prose"},[a(" When a function calls another via the SDK, the trace context flows through automatically. The called function becomes a child span of the caller; both share the same "),e("code",{class:"doc-chip"},"trace_id"),a(". ")],-1)),d(n(h),{code:As,lang:"js"}),o[59]||(o[59]=b('

Job enqueues work the same way: orva.jobs.enqueue() records the trace context on the job row. When the scheduler picks the job up later, the resulting execution lands in the same trace as the function that enqueued it, even if the gap is hours or days.

Triggers

Each span carries a trigger label so the UI can show how the chain started.

',3)),e("div",ns,[e("table",cs,[o[53]||(o[53]=e("thead",null,[e("tr",null,[e("th",null,"Trigger"),e("th",null,"Meaning")])],-1)),e("tbody",null,[(u(),g(_,null,A(we,t=>e("tr",{key:t.name},[e("td",ds,[e("code",null,l(t.name),1)]),e("td",rs,l(t.desc),1)])),64))])])]),o[60]||(o[60]=e("h3",{class:"doc-h3"},"External correlation (W3C traceparent)",-1)),o[61]||(o[61]=e("p",{class:"doc-prose"},[a(" Send a standard "),e("code",{class:"doc-chip"},"traceparent"),a(" header on the inbound HTTP request and Orva makes its trace a child of yours. The same trace_id is echoed back as "),e("code",{class:"doc-chip"},"X-Trace-Id"),a(" on every response, so external systems can correlate without parsing bodies. ")],-1)),d(n(h),{code:Os,lang:"bash"}),o[62]||(o[62]=b('

Outlier detection

Each function maintains an in-memory rolling P95 baseline over its last 100 successful warm executions. An invocation is flagged as an outlier when it has at least 20 baseline samples AND its duration exceeds P95 × 2. Cold starts and errors are excluded from the baseline so a flapping function can't drag it down. The flag and baseline P95 are stored on the execution row and rendered as an amber flag icon next to the span.

Where to look

  • /traces: list of recent traces, filterable by function / status / outlier-only.
  • /traces/:id: waterfall + per-span detail. Click a span to jump to its execution in the Invocations log.
  • GET /api/v1/traces/{id}: full span tree as JSON. Pair with list_traces / get_trace MCP tools for AI agents.
  • GET /api/v1/functions/{id}/baseline: current P95/P99/mean for a function.
',4))]),e("section",is,[o[64]||(o[64]=b('
10

Errors & recovery

Every error response uses the same envelope so log scrapers and retries can match on code. Deploys are content-addressed; rollback retargets the active version pointer and refreshes warm workers.

',1)),d(n(h),{code:Ps,lang:"json"}),e("div",ls,[e("table",ps,[o[63]||(o[63]=e("thead",null,[e("tr",null,[e("th",null,"Code"),e("th",null,"When you see it")])],-1)),e("tbody",null,[(u(),g(_,null,A(Ce,t=>e("tr",{key:t.code},[e("td",us,[e("code",null,l(t.code),1)]),e("td",hs,l(t.when),1)])),64))])])])]),e("section",vs,[o[81]||(o[81]=b('
11

CLI

orva is a single static binary that talks to a remote (or local) Orva server over HTTPS. Same binary as the daemon, orva serve starts a server, every other subcommand is a CLI client. Drop it on operator laptops, CI runners, or anywhere bash runs.

Install (server included)
curl … install.sh | sh

Full install: daemon + nsjail + rootfs + CLI.

Install (CLI only)
install.sh --cli-only

~10 MB binary at /usr/local/bin/orva. No service.

Inside Docker
docker exec orva orva …

CLI auto-authed via ~/.orva/config.yaml.

Authenticate

',3)),e("p",ms,[o[66]||(o[66]=a(" The CLI reads ",-1)),o[67]||(o[67]=e("code",{class:"doc-chip"},"~/.orva/config.yaml",-1)),o[68]||(o[68]=a(" for ",-1)),o[69]||(o[69]=e("code",{class:"doc-chip"},"endpoint",-1)),o[70]||(o[70]=a(" + ",-1)),o[71]||(o[71]=e("code",{class:"doc-chip"},"api_key",-1)),o[72]||(o[72]=a(". Generate a key from ",-1)),d(i,{to:"/api-keys",class:"text-foreground hover:text-white underline decoration-dotted underline-offset-4"},{default:C(()=>[...o[65]||(o[65]=[a("Keys",-1)])]),_:1}),o[73]||(o[73]=a(" in the dashboard, then: ",-1))]),d(n(h),{code:Is,lang:"bash"}),o[82]||(o[82]=e("h3",{class:"doc-h3"},"Command index",-1)),e("div",bs,[e("table",gs,[o[74]||(o[74]=e("thead",null,[e("tr",null,[e("th",null,"Command"),e("th",null,"Subcommands"),e("th",{class:"hidden md:table-cell"},"Purpose")])],-1)),e("tbody",null,[(u(),g(_,null,A(xe,t=>e("tr",{key:t.cmd},[e("td",ys,[e("code",null,"orva "+l(t.cmd),1)]),e("td",fs,l(t.subs),1),e("td",ks,l(t.purpose),1)])),64))])])]),o[83]||(o[83]=e("h3",{class:"doc-h3"},"Common recipes",-1)),e("div",ws,[o[75]||(o[75]=e("div",{class:"doc-microlabel"},"Deploy a function from a directory",-1)),d(n(h),{code:js,lang:"bash"})]),e("div",Cs,[o[76]||(o[76]=e("div",{class:"doc-microlabel"},"Invoke + tail logs",-1)),d(n(h),{code:Es,lang:"bash"})]),e("div",xs,[o[77]||(o[77]=e("div",{class:"doc-microlabel"},"Manage KV state",-1)),d(n(h),{code:Ds,lang:"bash"})]),e("div",Ts,[o[78]||(o[78]=e("div",{class:"doc-microlabel"},"Secrets, cron, jobs, webhooks",-1)),d(n(h),{code:Rs,lang:"bash"})]),e("div",Ss,[o[79]||(o[79]=e("div",{class:"doc-microlabel"},"System health, metrics, vacuum",-1)),d(n(h),{code:Hs,lang:"bash"})]),d(n($),{icon:n(M),title:"Shell completion"},{default:C(()=>[...o[80]||(o[80]=[a(" Generate completion for your shell: ",-1),e("code",{class:"doc-chip"},"orva completion bash | sudo tee /etc/bash_completion.d/orva",-1),a(", or ",-1),e("code",{class:"doc-chip"},"zsh",-1),a(" / ",-1),e("code",{class:"doc-chip"},"fish",-1),a(" / ",-1),e("code",{class:"doc-chip"},"powershell",-1),a(". Tab-completes commands, subcommands, and flags. ",-1)])]),_:1},8,["icon"])])])}}};export{Ys as default}; diff --git a/backend/internal/server/ui_dist/assets/Docs-DVZ33BCt.js b/backend/internal/server/ui_dist/assets/Docs-DVZ33BCt.js deleted file mode 100644 index e6723b9..0000000 --- a/backend/internal/server/ui_dist/assets/Docs-DVZ33BCt.js +++ /dev/null @@ -1,327 +0,0 @@ -import{E as W}from"./format-CsU4_SPu.js";import{c as Y}from"./clipboard-CmSw2rR-.js";import{a as Ee,b as De}from"./aiPrompts-Dgb3jxRL.js";import{C as Re,o as J,G as He,a as g,b as e,s as P,n as j,f as n,F as _,p as A,d,l as b,k as a,h as C,bb as $e,t as l,g as Me,r as k,q as v,aD as x,z as s,a2 as Le,Y as qe,j as u,H as Z,X as Ne}from"./index-D5cO6vit.js";import{H as f,p as Be,j as Q,a as ze,b as N}from"./github-dark-BrynTfs3.js";import{C as B}from"./check-BkPCgKSu.js";import{C as z}from"./copy-Gc8n9M6v.js";import{G as K}from"./globe-BSxGWG92.js";import{C as ee}from"./chevron-right-CC7vhgfo.js";import{K as M}from"./key-round-D643nzM3.js";import{C as Ke}from"./chevron-down-Trjh5P5D.js";import{V as Ue}from"./variable-kwh9BTXT.js";import{L as Xe}from"./lock-C90zRIcI.js";function Fe(L){const q=L.regex,r="HTTP/([32]|1\\.[01])",I=/[A-Za-z][A-Za-z0-9-]*/,E={className:"attribute",begin:q.concat("^",I,"(?=\\:\\s)"),starts:{contains:[{className:"punctuation",begin:/: /,relevance:0,starts:{end:"$",relevance:0}}]}},T=[E,{begin:"\\n\\n",starts:{subLanguage:[],endsWithParent:!0}}];return{name:"HTTP",aliases:["https"],illegal:/\S/,contains:[{begin:"^(?="+r+" \\d{3})",end:/$/,contains:[{className:"meta",begin:r},{className:"number",begin:"\\b\\d{3}\\b"}],starts:{end:/\b\B/,illegal:/\S/,contains:T}},{begin:"(?=^[A-Z]+ (.*?) "+r+"$)",end:/$/,contains:[{className:"string",begin:" ",end:" ",excludeBegin:!0,excludeEnd:!0},{className:"meta",begin:r},{className:"keyword",begin:"[A-Z]+"}],starts:{end:/\b\B/,illegal:/\S/,contains:T}},L.inherit(E,{relevance:0})]}}const Ve={class:"space-y-12 pb-16"},Ge={class:"docs-hero"},We={class:"docs-hero-content"},Ye={class:"docs-hero-row"},Je={class:"docs-hero-actions"},Ze=["title","aria-label"],Qe={class:"docs-hero-toc","aria-label":"Jump to docs section"},eo=["href"],oo={class:"docs-hero-toc-num"},so={id:"handler",class:"space-y-5 scroll-mt-6"},to={class:"doc-table-wrap"},ao={class:"doc-table"},no={class:"doc-cell-key"},co={class:"doc-cell-mono"},ro={class:"doc-cell-mono hidden sm:table-cell"},io={class:"doc-cell-mono hidden md:table-cell"},lo={id:"deploy",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},po={class:"grid grid-cols-1 lg:grid-cols-2 gap-3"},uo={class:"space-y-2"},ho={class:"space-y-2"},vo={class:"space-y-2"},mo={id:"config",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},bo={class:"doc-table-wrap"},go={class:"doc-table"},yo={class:"doc-cell-key whitespace-nowrap"},fo={class:"doc-cell-mono hidden sm:table-cell whitespace-nowrap"},ko={class:"doc-cell-body"},wo={class:"space-y-2"},Co={class:"doc-details group"},xo={class:"doc-details-summary"},To={class:"doc-details-body"},So={id:"sdk",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},_o={class:"space-y-2"},Ao={class:"space-y-2"},Oo={class:"space-y-2"},Po={id:"schedules",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},jo={class:"doc-section-head"},Io={class:"doc-lede"},Eo={id:"webhooks",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},Do={class:"doc-section-head"},Ro={class:"doc-lede"},Ho={class:"doc-table-wrap"},$o={class:"doc-table"},Mo={class:"doc-cell-key whitespace-nowrap"},Lo={class:"doc-cell-body"},qo={class:"space-y-2"},No={id:"mcp",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},Bo={class:"grid grid-cols-1 md:grid-cols-3 gap-3"},zo={class:"doc-card"},Ko={class:"doc-card-body"},Uo={class:"doc-chip break-all"},Xo={class:"doc-token-bar"},Fo={class:"flex items-center gap-2 min-w-0 flex-1"},Vo={key:0,class:"text-sm text-foreground-muted truncate"},Go={key:1,class:"text-sm text-success truncate"},Wo={class:"doc-chip"},Yo=["disabled"],Jo={class:"doc-details group"},Zo={class:"doc-details-summary"},Qo={class:"doc-details-body space-y-4"},es={id:"generate",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},os={class:"ai-prompt-actions"},ss={key:0,class:"prompt-collapse-fade","aria-hidden":"true"},ts=["aria-expanded"],as={id:"tracing",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},ns={class:"doc-table-wrap"},cs={class:"doc-table"},ds={class:"doc-cell-key whitespace-nowrap"},rs={class:"doc-cell-body"},is={id:"errors",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},ls={class:"doc-table-wrap"},ps={class:"doc-table"},us={class:"doc-cell-key whitespace-nowrap"},hs={class:"doc-cell-body"},vs={id:"cli",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},ms={class:"doc-prose"},bs={class:"doc-table-wrap"},gs={class:"doc-table"},ys={class:"doc-cell-key whitespace-nowrap"},fs={class:"doc-cell-mono"},ks={class:"doc-cell-body hidden md:table-cell"},ws={class:"space-y-2"},Cs={class:"space-y-2"},xs={class:"space-y-2"},Ts={class:"space-y-2"},Ss={class:"space-y-2"},_s=`# Available inside every running function — refresh per-invocation: -ORVA_TRACE_ID=tr_3e39f6991c66f140577c6021da7dd13b # one per causal chain -ORVA_SPAN_ID=sp_4ceba57f6b1c982e # this execution - -# Python: os.environ["ORVA_TRACE_ID"] -# Node.js: process.env.ORVA_TRACE_ID -# Reading them is optional — the platform records the trace for you.`,As=`// Function A — calls B via the SDK. Trace context flows automatically. -const { invoke, jobs } = require('orva') - -module.exports.handler = async (event) => { - // F2F call — B becomes a child span under A. - const result = await invoke('send_email', { to: event.email }) - - // Job enqueue — when this job runs (now or in 6 hours), the resulting - // execution lands in the SAME trace as A. - await jobs.enqueue('audit_log', { action: 'sent', to: event.email }) - - return { statusCode: 200, body: 'ok' } -}`,Os=`# Send the W3C traceparent header — Orva will adopt it as the trace root. -curl -H "traceparent: 00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01" \\ - https://orva.example.com/fn/myfn/ - -# Response always echoes: -# X-Trace-Id: tr_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`,Ps=`{ - "error": { - "code": "VALIDATION", - "message": "name must be lowercase and dash-separated", - "request_id": "req_abc123" - } -}`,js=`# 1. Generate an API key in the dashboard (Keys page) or via the API -# 2. Tell the CLI where to find your Orva and which key to use -orva login \\ - --endpoint https://orva.example.com \\ - --api-key orva_xxx_your_key_here - -# Writes ~/.orva/config.yaml. Subsequent commands need no flags. -orva system health # smoke test`,Is=`# Init a project in cwd (creates orva.yaml + handler stub) -orva init - -# Deploy from a directory. Auto-detects handler.ts when tsconfig.json -# is present; else uses the runtime default (handler.js / handler.py). -orva deploy ./my-fn \\ - --name resize-image \\ - --runtime node24 - -# Override the entrypoint explicitly: -orva deploy ./my-fn --name api --runtime python314 --entrypoint app.py`,Es=`# Invoke a function by name or fn_: -orva invoke resize-image --data '{"url":"https://example.com/cat.jpg"}' - -# Recent executions: -orva logs resize-image - -# Single execution, with stdout/stderr: -orva logs resize-image --exec-id exec_abc123 - -# Live tail — SSE stream, Ctrl-C to stop: -orva logs resize-image --tail`,Ds=`# List keys (optionally by prefix) -orva kv list resize-image -orva kv list resize-image --prefix user: - -# Read / write / delete -orva kv get resize-image cache:home -orva kv put resize-image cache:home '{"hits":42}' --ttl 3600 -orva kv delete resize-image cache:home`,Rs=`# Secrets — encrypted at rest, injected as env vars at spawn: -orva secrets set resize-image S3_BUCKET my-bucket -orva secrets list resize-image -orva secrets delete resize-image S3_BUCKET - -# Cron — fire a function on a schedule: -orva cron create --fn daily-report --expr '0 9 * * *' --tz Asia/Kolkata -orva cron list -orva cron update --enabled false # pause -orva cron delete - -# Jobs — fire-and-forget background queue: -orva jobs enqueue --fn send-email --data '{"to":"a@b.c"}' -orva jobs list --status pending -orva jobs retry -orva jobs delete - -# Outbound webhooks (system events): -orva webhooks create --url https://hooks.slack.com/... --events deployment.failed,job.failed -orva webhooks test - -# Inbound webhook triggers (external POST → function): -orva webhooks inbound create --fn order-handler --signature stripe`,Hs=`orva system health # daemon up + DB ok -orva system metrics # JSON metrics snapshot -orva system db-stats # on-disk breakdown (orva.db, WAL, functions/) -orva system vacuum # rewrite SQLite to reclaim freelist pages - -orva activity # last 50 activity rows -orva activity --tail # live feed (Ctrl-C) -orva activity --source mcp --limit 200 # MCP-only, last 200`,oe="",Ys={__name:"Docs",setup(L){const q=Re();f.registerLanguage("python",Be),f.registerLanguage("javascript",Q),f.registerLanguage("js",Q),f.registerLanguage("json",ze),f.registerLanguage("bash",N),f.registerLanguage("shell",N),f.registerLanguage("sh",N),f.registerLanguage("http",Fe);const r=v(()=>window.location.origin),I=[{id:"handler",num:"01",label:"Handler"},{id:"deploy",num:"02",label:"Deploy"},{id:"config",num:"03",label:"Config"},{id:"sdk",num:"04",label:"SDK"},{id:"schedules",num:"05",label:"Schedules"},{id:"webhooks",num:"06",label:"Webhooks"},{id:"mcp",num:"07",label:"MCP"},{id:"generate",num:"08",label:"AI prompt"},{id:"tracing",num:"09",label:"Tracing"},{id:"errors",num:"10",label:"Errors"},{id:"cli",num:"11",label:"CLI"}],E=k("handler");let T=null;J(()=>{if(typeof IntersectionObserver>"u")return;const c=new Set;T=new IntersectionObserver(o=>{for(const i of o)i.isIntersecting?c.add(i.target.id):c.delete(i.target.id);for(const i of I)if(c.has(i.id)){E.value=i.id;break}},{rootMargin:"-20% 0px -70% 0px",threshold:0});for(const o of I){const i=document.getElementById(o.id);i&&T.observe(i)}}),He(()=>{T&&T.disconnect()});const se=De(),D=k(!1);let U=null;const te=async()=>{await Ee()&&(D.value=!0,clearTimeout(U),U=setTimeout(()=>{D.value=!1},1500))},X=x({setup(){return()=>s("svg",{viewBox:"0 0 256 255",width:"14",height:"14",xmlns:"http://www.w3.org/2000/svg"},[s("defs",null,[s("linearGradient",{id:"pyg1",x1:"0",y1:"0",x2:"1",y2:"1"},[s("stop",{offset:"0","stop-color":"#387EB8"}),s("stop",{offset:"1","stop-color":"#366994"})]),s("linearGradient",{id:"pyg2",x1:"0",y1:"0",x2:"1",y2:"1"},[s("stop",{offset:"0","stop-color":"#FFE052"}),s("stop",{offset:"1","stop-color":"#FFC331"})])]),s("path",{fill:"url(#pyg1)",d:"M126.9 12c-58.3 0-54.7 25.3-54.7 25.3l.1 26.2H128v8H50.5S12 67.2 12 126.1c0 58.9 33.6 56.8 33.6 56.8h19.4v-27.4s-1-33.6 33.1-33.6h55.9s32 .5 32-30.9V43.5S191.7 12 126.9 12zM95.7 29.9a10 10 0 0 1 0 20 10 10 0 0 1 0-20z"}),s("path",{fill:"url(#pyg2)",d:"M129.1 243c58.3 0 54.7-25.3 54.7-25.3l-.1-26.2H128v-8h77.5s38.5 4.4 38.5-54.5c0-58.9-33.6-56.8-33.6-56.8h-19.4v27.4s1 33.6-33.1 33.6H102s-32-.5-32 30.9v52S64.3 243 129.1 243zm30.4-17.9a10 10 0 0 1 0-20 10 10 0 0 1 0 20z"})])}}),F=x({setup(){return()=>s("svg",{viewBox:"0 0 256 280",width:"14",height:"14",xmlns:"http://www.w3.org/2000/svg"},[s("path",{fill:"#3F873F",d:"M128 0 12 67v146l116 67 116-67V67L128 0zm0 24.6 95 54.8v121.2l-95 54.8-95-54.8V79.4l95-54.8z"}),s("path",{fill:"#3F873F",d:"M128 64c-3 0-5.7.7-8 2.3L73 92c-5 2.7-8 8-8 13.6V169c0 5.6 3 10.7 8 13.5l13 7.4c6.3 3.1 8.5 3.1 11.4 3.1 9.4 0 14.8-5.7 14.8-15.6V117c0-1-.7-1.7-1.7-1.7H103c-1 0-1.7.7-1.7 1.7v60.2c0 4.4-4.5 8.7-11.8 5.1l-13.7-7.9a1.6 1.6 0 0 1-.8-1.4v-63.4c0-.6.3-1 .8-1.4l46.8-26.9c.4-.3 1-.3 1.4 0L171 110c.5.4.8.8.8 1.4V174a1.7 1.7 0 0 1-.8 1.4l-46.8 27c-.4.2-1 .2-1.4 0l-12-7.2c-.4-.2-.8-.2-1.2 0-3.4 1.9-4 2.2-7.2 3.3-.8.3-2 .7.4 2.1l15.7 9.3c2.5 1.4 5.3 2.2 8.2 2.2 2.9 0 5.7-.8 8.2-2.2L181 184c5-2.8 8-7.9 8-13.5V107c0-5.6-3-10.7-8-13.5l-46.7-26.7a17 17 0 0 0-6.3-2.8z"})])}}),ae=x({name:"DeployPipelineDiagram",setup(){const c=[{glyph:"▣",label:"Tarball",sub:"POST /deploy"},{glyph:"⟜",label:"Extract",sub:"untar → scratch dir"},{glyph:"◍",label:"Install",sub:"npm / pip"},{glyph:"⟐",label:"Compile",sub:"tsc (TypeScript)"},{glyph:"◉",label:"Activate",sub:"rename → current"},{glyph:"✦",label:"Warm pool",sub:"pre-spawn N workers"}];return()=>s("figure",{class:"doc-diagram"},[s("figcaption",{class:"doc-diagram-cap"},"Deploy pipeline"),s("div",{class:"doc-pipeline"},c.flatMap((o,i)=>{const t=s("div",{key:`s${i}`,class:"doc-pipeline-stage"},[s("div",{class:"doc-pipeline-glyph"},o.glyph),s("div",{class:"doc-pipeline-label"},[s("span",{class:"doc-pipeline-name"},o.label),s("span",{class:"doc-pipeline-sub"},o.sub)])]),p=it/220*100;return()=>s("figure",{class:"doc-diagram"},[s("figcaption",{class:"doc-diagram-cap"},"Causal trace, one HTTP request and three spans"),s("div",{class:"doc-trace"},[s("div",{class:"doc-trace-axis"},[s("span",null,"0 ms"),s("span",null,"220 ms")]),...o.map(t=>s("div",{key:t.fn,class:["doc-trace-row",`is-${t.klass}`]},[s("div",{class:"doc-trace-label"},[s("span",{class:"doc-trace-fn"},t.fn),s("span",{class:"doc-trace-trigger"},t.trigger)]),s("div",{class:"doc-trace-track"},[s("div",{class:"doc-trace-bar",style:{left:`${i(t.start)}%`,width:`${i(t.dur)}%`},title:`+${t.start}ms · ${t.dur}ms`})]),s("div",{class:"doc-trace-dur"},`${t.dur}ms`)])),s("div",{class:"doc-trace-legend"},[s("span",null,"Same "),s("code",{class:"doc-chip"},"trace_id"),s("span",null," across all spans · "),s("code",{class:"doc-chip"},"parent_span_id"),s("span",null," chains them into a tree.")])])])}}),ce=x({name:"WebhookDeliveryDiagram",setup(){return()=>s("figure",{class:"doc-diagram"},[s("figcaption",{class:"doc-diagram-cap"},"Signed webhook delivery"),s("div",{class:"doc-webhook"},[s("div",{class:"doc-webhook-actor"},[s("div",{class:"doc-webhook-actor-head"},"orvad"),s("div",{class:"doc-webhook-actor-body"},[s("span",null,"event fires"),s("code",{class:"doc-chip"},"deployment.succeeded")])]),s("div",{class:"doc-webhook-wire"},[s("div",{class:"doc-webhook-wire-line","aria-hidden":"true"}),s("div",{class:"doc-webhook-wire-payload"},[s("div",{class:"doc-webhook-wire-method"},"POST"),s("div",{class:"doc-webhook-wire-headers"},[s("code",null,"X-Orva-Event"),s("code",null,"X-Orva-Timestamp"),s("code",null,"X-Orva-Signature")]),s("div",{class:"doc-webhook-wire-sig"},"sha256=hex(hmac(secret, ts.body))")])]),s("div",{class:"doc-webhook-actor"},[s("div",{class:"doc-webhook-actor-head"},"your receiver"),s("div",{class:"doc-webhook-actor-body"},[s("span",null,"verify HMAC"),s("span",null,"→ 2xx within 15s or get retried")])])])])}}),de=v(()=>[{label:"Python",lang:"python",code:`def handler(event): - body = event.get("body") or {} - return { - "statusCode": 200, - "headers": {"Content-Type": "application/json"}, - "body": {"hello": body.get("name", "world")}, - }`},{label:"Node.js",lang:"js",code:`exports.handler = async (event) => { - const body = event.body || {}; - return { - statusCode: 200, - headers: { 'Content-Type': 'application/json' }, - body: { hello: body.name || 'world' }, - }; -};`}]),re=v(()=>[{label:"curl",lang:"bash",code:`curl -X POST ${r.value}/fn/ \\ - -H 'Content-Type: application/json' \\ - -d '{"name": "Orva"}'`},{label:"fetch",lang:"js",code:`const res = await fetch('${r.value}/fn/', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name: 'Orva' }), -}); -console.log(await res.json());`},{label:"Python",lang:"python",code:`import httpx - -r = httpx.post( - "${r.value}/fn/", - json={"name": "Orva"}, -) -print(r.json())`}]),ie=[{id:"python314",name:"Python 3.14",entry:"handler.py",deps:"requirements.txt",icon:X},{id:"python313",name:"Python 3.13",entry:"handler.py",deps:"requirements.txt",icon:X},{id:"node24",name:"Node.js 24",entry:"handler.js",deps:"package.json",icon:F},{id:"node22",name:"Node.js 22",entry:"handler.js",deps:"package.json",icon:F}],le=[{field:"env_vars",purpose:"Plain config",body:"Plaintext config stored on the function record. Use for feature flags and non-secret settings.",icon:Ue,iconClass:"text-violet-300"},{field:"/secrets",purpose:"Encrypted",body:"AES-256-GCM at rest. Values decrypt only into the worker environment at spawn time.",icon:M,iconClass:"text-emerald-300"},{field:"network_mode",purpose:"Egress control",body:"none = isolated loopback. egress = outbound HTTPS allowed; firewall blocklist applies.",icon:K,iconClass:"text-sky-300"},{field:"auth_mode",purpose:"Invoke gate",body:"none = public. platform_key = require Orva API key. signed = require HMAC.",icon:Xe,iconClass:"text-violet-300"},{field:"rate_limit_per_min",purpose:"Per-IP throttle",body:"Optional cap for public or webhook-facing functions. Exceeding it returns 429.",icon:Ne,iconClass:"text-amber-300"}],pe=v(()=>`curl -X POST ${r.value}/api/v1/functions \\ - -H 'X-Orva-API-Key: ' \\ - -H 'Content-Type: application/json' \\ - -d '{"name":"hello","runtime":"python314","memory_mb":128,"cpus":0.5}'`),ue=v(()=>`tar czf code.tar.gz handler.py requirements.txt -curl -X POST ${r.value}/api/v1/functions//deploy \\ - -H 'X-Orva-API-Key: ' \\ - -F code=@code.tar.gz`),he=v(()=>`curl -X POST ${r.value}/api/v1/functions//secrets \\ - -H 'X-Orva-API-Key: ' \\ - -H 'Content-Type: application/json' \\ - -d '{"key":"DATABASE_URL","value":"postgres://..."}'`),ve=v(()=>`# generate signature -SECRET='your-shared-secret-stored-in-function-secrets' -TS=$(date +%s) -BODY='{"hello":"world"}' -SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $2}') - -curl -X POST ${r.value}/fn/ \\ - -H "X-Orva-Timestamp: $TS" \\ - -H "X-Orva-Signature: sha256=$SIG" \\ - -H 'Content-Type: application/json' \\ - -d "$BODY"`),me=v(()=>[{label:"curl",lang:"bash",note:"Create a daily-9am schedule for an existing function. payload is delivered as the invoke body.",code:`curl -X POST ${r.value}/api/v1/functions//cron \\ - -H 'X-Orva-API-Key: ' \\ - -H 'Content-Type: application/json' \\ - -d '{ - "cron_expr": "0 9 * * *", - "enabled": true, - "payload": {"task": "daily-summary"} - }'`},{label:"Toggle / edit",lang:"bash",note:"PUT accepts any subset of {cron_expr, enabled, payload}; omitted fields keep their previous value. next_run_at is recomputed on expr changes.",code:`# pause -curl -X PUT ${r.value}/api/v1/functions//cron/ \\ - -H 'X-Orva-API-Key: ' \\ - -H 'Content-Type: application/json' \\ - -d '{"enabled": false}' - -# change schedule -curl -X PUT ${r.value}/api/v1/functions//cron/ \\ - -H 'X-Orva-API-Key: ' \\ - -H 'Content-Type: application/json' \\ - -d '{"cron_expr": "*/15 * * * *"}'`},{label:"List & delete",lang:"bash",note:"GET /api/v1/cron lists every schedule across functions (with function_name JOIN); per-function uses the nested route.",code:`# all schedules -curl ${r.value}/api/v1/cron \\ - -H 'X-Orva-API-Key: ' - -# delete one -curl -X DELETE ${r.value}/api/v1/functions//cron/ \\ - -H 'X-Orva-API-Key: '`}]),be=[{label:"Python",lang:"python",code:`from orva import kv - -def handler(event): - # Store with optional TTL (seconds). 0 = no expiry. - kv.put("user:42", {"name": "Ada", "tier": "pro"}, ttl_seconds=3600) - - # Read; default returned if missing or expired. - user = kv.get("user:42", default=None) - - # List by prefix. - pages = kv.list(prefix="page:", limit=50) - - # Delete is idempotent. - kv.delete("user:42") - - return {"statusCode": 200, "body": str(user)}`},{label:"Node.js",lang:"js",code:`const { kv } = require('orva') - -exports.handler = async (event) => { - await kv.put('user:42', { name: 'Ada', tier: 'pro' }, { ttlSeconds: 3600 }) - - const user = await kv.get('user:42', null) - - const pages = await kv.list({ prefix: 'page:', limit: 50 }) - - await kv.delete('user:42') - - return { statusCode: 200, body: JSON.stringify(user) } -}`}],ge=[{label:"Python",lang:"python",code:`from orva import invoke, OrvaError - -def handler(event): - try: - # invoke() returns the downstream {statusCode, headers, body}. - # body is JSON-decoded when possible. - result = invoke("resize-image", {"url": event["body"]["url"]}) - return {"statusCode": 200, "body": result["body"]} - except OrvaError as e: - # 404 = function not found, 507 = call depth exceeded. - return {"statusCode": e.status or 502, "body": str(e)}`},{label:"Node.js",lang:"js",code:`const { invoke, OrvaError } = require('orva') - -exports.handler = async (event) => { - try { - const result = await invoke('resize-image', { url: event.body.url }) - return { statusCode: 200, body: result.body } - } catch (e) { - if (e instanceof OrvaError) { - return { statusCode: e.status || 502, body: e.message } - } - throw e - } -}`}],ye=[{label:"Python",lang:"python",code:`from orva import jobs - -def handler(event): - # Fire-and-forget. Returns the job id immediately; the function - # body runs later via the scheduler. max_attempts retries with - # exponential backoff on 5xx / exception. - job_id = jobs.enqueue( - "send-welcome-email", - {"to": event["body"]["email"]}, - max_attempts=3, - ) - return {"statusCode": 202, "body": job_id}`},{label:"Node.js",lang:"js",code:`const { jobs } = require('orva') - -exports.handler = async (event) => { - const jobId = await jobs.enqueue( - 'send-welcome-email', - { to: event.body.email }, - { maxAttempts: 3 } - ) - return { statusCode: 202, body: jobId } -}`}],fe=[{name:"deployment.succeeded",when:"A function build finished and the new version is active."},{name:"deployment.failed",when:"A build failed or was rejected."},{name:"function.created",when:"A new function row was created via POST /api/v1/functions."},{name:"function.updated",when:"A function config was edited via PUT /api/v1/functions/{id} (status flips during a deploy do NOT fire this; see deployment.*)."},{name:"function.deleted",when:"A function was removed."},{name:"execution.error",when:"An invocation finished with status=error or 5xx."},{name:"cron.failed",when:"A scheduled run failed (bad expr, missing fn, dispatch error, or 5xx)."},{name:"job.succeeded",when:"A queued background job finished successfully."},{name:"job.failed",when:"A queued job exhausted its retries (terminal failure)."}],ke=[{label:"Python",lang:"python",note:"Run on the receiver. Reject anything that fails verification. The signature ensures the request really came from this Orva instance.",code:`import hmac, hashlib, time - -def verify(secret: str, ts: str, body: bytes, sig_header: str) -> bool: - if abs(time.time() - int(ts)) > 300: # 5-min skew window - return False - mac = hmac.new(secret.encode(), f"{ts}.".encode() + body, hashlib.sha256) - expected = "sha256=" + mac.hexdigest() - return hmac.compare_digest(expected, sig_header) - -# In your Flask/FastAPI/etc. handler: -ts = request.headers["X-Orva-Timestamp"] -sig = request.headers["X-Orva-Signature"] -if not verify(WEBHOOK_SECRET, ts, request.get_data(), sig): - return "bad signature", 401`},{label:"Node.js",lang:"js",note:"Same shape as Stripe. Use timingSafeEqual to avoid sig-leak via timing.",code:`const crypto = require('crypto') - -function verify(secret, ts, body, sigHeader) { - if (Math.abs(Date.now() / 1000 - parseInt(ts, 10)) > 300) return false - const mac = crypto.createHmac('sha256', secret) - mac.update(ts + '.') - mac.update(body) - const expected = 'sha256=' + mac.digest('hex') - if (expected.length !== sigHeader.length) return false - return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sigHeader)) -} - -// In an express handler with raw body middleware: -app.post('/webhooks/orva', (req, res) => { - const ok = verify( - process.env.WEBHOOK_SECRET, - req.headers['x-orva-timestamp'], - req.body, // raw bytes — NOT parsed JSON - req.headers['x-orva-signature'] - ) - if (!ok) return res.status(401).send('bad signature') - res.sendStatus(200) -})`}],we=[{name:"http",desc:"Public HTTP request hit /fn//. Almost always a root span."},{name:"f2f",desc:"Another function called this one via orva.invoke(). Has a parent_span_id."},{name:"job",desc:"Background job runner picked up an enqueued job. Parent_span_id is whoever enqueued it."},{name:"cron",desc:"Scheduler fired a cron entry. Always a root span."},{name:"inbound",desc:"External webhook hit /webhook/{id}. Always a root span."},{name:"replay",desc:"Operator clicked Replay on a captured execution. Fresh trace, no link to original."},{name:"mcp",desc:"AI agent invoked the function via MCP invoke_function. Fresh trace."}],Ce=[{code:"VALIDATION",when:"Bad request body or path parameter."},{code:"UNAUTHORIZED",when:"Missing or invalid API key / session cookie."},{code:"NOT_FOUND",when:"Function, deployment, or secret doesn't exist."},{code:"RATE_LIMITED",when:"Too many requests; check the Retry-After header."},{code:"VERSION_GCD",when:"Rollback target was garbage-collected."},{code:"INSUFFICIENT_DISK",when:"Host is below min_free_disk_mb."}],xe=[{cmd:"login",subs:W,purpose:"Save endpoint + API key to ~/.orva/config.yaml"},{cmd:"init",subs:W,purpose:"Scaffold an orva.yaml in the current directory"},{cmd:"deploy",subs:"[path]",purpose:"Package a directory and deploy as a function"},{cmd:"invoke",subs:"[name|id]",purpose:"POST to /fn// and print the response"},{cmd:"logs",subs:"[name|id] [--tail]",purpose:"List recent executions; --tail follows live via SSE"},{cmd:"functions",subs:"list / get / create / delete",purpose:"CRUD for the function registry"},{cmd:"cron",subs:"list / create / update / delete",purpose:"Manage cron schedules attached to functions"},{cmd:"jobs",subs:"list / enqueue / retry / delete",purpose:"Background queue management"},{cmd:"kv",subs:"list / get / put / delete",purpose:"Browse a function’s key/value store"},{cmd:"secrets",subs:"list / set / delete",purpose:"AES-256-GCM secrets per function"},{cmd:"webhooks",subs:"list / create / test / delete / inbound",purpose:"System-event subscribers + inbound triggers"},{cmd:"routes",subs:"list / set / delete",purpose:"Custom URL → function path mappings"},{cmd:"keys",subs:"list / create / revoke",purpose:"Manage API keys"},{cmd:"activity",subs:"[--tail] [--source web|api|...]",purpose:"Paginated activity rows; live SSE with --tail"},{cmd:"system",subs:"health / metrics / db-stats / vacuum",purpose:"Server diagnostics"},{cmd:"setup",subs:"[--skip-nsjail] [--skip-rootfs]",purpose:"Install nsjail + rootfs on a bare host"},{cmd:"serve",subs:"[--port N]",purpose:"Run as the server daemon (not the CLI client)"},{cmd:"completion",subs:"bash / zsh / fish / powershell",purpose:"Emit shell completion script"}],V=k("");J(async()=>{try{const c=await fetch("/web/docs.md",{cache:"no-cache"});c.ok&&(V.value=await c.text())}catch{}});const Te=v(()=>V.value.replaceAll("{{ORIGIN}}",window.location.origin)),O=k(!1);let G=null;const Se=async()=>{await Y(Te.value)&&(O.value=!0,clearTimeout(G),G=setTimeout(()=>{O.value=!1},1500))},S=k(!1),R=k(""),H=k(!1),_e=v(()=>R.value.slice(0,12)),m=v(()=>R.value||oe),Ae=async()=>{if(!H.value){H.value=!0;try{const c=new Date().toISOString().slice(0,16).replace("T"," "),o=await qe.post("/keys",{name:"MCP: "+c,permissions:["invoke","read","write","admin"]});R.value=o.data.key}catch(c){console.error("mint mcp key failed",c),q.notify({title:"Could not mint key",message:c?.response?.data?.error?.message||c.message||"Unknown error",danger:!0})}finally{H.value=!1}}},Oe=v(()=>[{label:"Claude Code",lang:"bash",note:"Anthropic's `claude` CLI. Restart Claude Code afterwards; `/mcp` lists Orva's 70 tools.",code:`claude mcp add --transport http --scope user orva ${r.value}/mcp --header "Authorization: Bearer ${m.value}"`},{label:"curl",lang:"bash",note:"Talk to MCP directly. Step 1 returns a session id (Mcp-Session-Id) that Step 2 references.",code:`curl -sD - -X POST ${r.value}/mcp \\ - -H 'Authorization: Bearer ${m.value}' \\ - -H 'Content-Type: application/json' \\ - -H 'Accept: application/json, text/event-stream' \\ - -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}' - -curl -sX POST ${r.value}/mcp \\ - -H 'Authorization: Bearer ${m.value}' \\ - -H 'Content-Type: application/json' \\ - -H 'Accept: application/json, text/event-stream' \\ - -H 'Mcp-Session-Id: ' \\ - -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'`}]),Pe=v(()=>[{label:"Claude Desktop",lang:"json",note:"Paste into ~/Library/Application Support/Claude/claude_desktop_config.json (macOS), %APPDATA%\\Claude\\claude_desktop_config.json (Windows), or ~/.config/Claude/claude_desktop_config.json (Linux). Restart Claude Desktop.",code:`{ - "mcpServers": { - "orva": { - "url": "${r.value}/mcp", - "headers": { - "Authorization": "Bearer ${m.value}" - } - } - } -}`},{label:"Cursor",lang:"bash",note:"Open the link in your browser. Cursor pops an approval dialog and writes ~/.cursor/mcp.json.",code:`cursor://anysphere.cursor-deeplink/mcp/install?name=orva&config=${je.value}`},{label:"VS Code",lang:"bash",note:'User-scoped install via the Copilot-MCP `code --add-mcp` flag. Pick "Workspace" at the prompt to write .vscode/mcp.json instead.',code:`code --add-mcp '{"name":"orva","type":"http","url":"${r.value}/mcp","headers":{"Authorization":"Bearer ${m.value}"}}'`},{label:"Codex CLI",lang:"bash",note:"OpenAI's `codex` CLI. Writes to ~/.codex/config.toml.",code:`codex mcp add --transport streamable-http orva ${r.value}/mcp --header "Authorization: Bearer ${m.value}"`},{label:"OpenCode",lang:"bash",note:`Interactive add. Pick "Remote", paste ${r.value}/mcp, then add the header Authorization: Bearer ${m.value}.`,code:"opencode mcp add"},{label:"Zed",lang:"json",note:"Zed runs MCP as stdio subprocesses, so use the `mcp-remote` bridge. Paste under context_servers in ~/.config/zed/settings.json. Restart Zed.",code:`{ - "context_servers": { - "orva": { - "source": "custom", - "command": "npx", - "args": [ - "-y", "mcp-remote", - "${r.value}/mcp", - "--header", "Authorization:Bearer ${m.value}" - ] - } - } -}`},{label:"Windsurf",lang:"json",note:"Paste into ~/.codeium/windsurf/mcp_config.json and reload Windsurf.",code:`{ - "mcpServers": { - "orva": { - "serverUrl": "${r.value}/mcp", - "headers": { - "Authorization": "Bearer ${m.value}" - } - } - } -}`},{label:"claude.ai web",lang:"text",note:"UI-only flow. Settings → Connectors → Add custom connector. claude.ai opens an Orva login + consent popup and issues an OAuth 2.1 token automatically; no token paste required.",code:`URL: ${r.value}/mcp -Auth: OAuth (auto-discovered)`},{label:"ChatGPT",lang:"text",note:"UI-only flow. Settings → Apps & Connectors → Developer mode → Add new connector. ChatGPT discovers OIDC metadata, performs Dynamic Client Registration, and pops the Orva consent screen. No token paste required.",code:`URL: ${r.value}/mcp -Auth: OAuth (auto-discovered)`}]),je=v(()=>{const c=JSON.stringify({url:r.value+"/mcp",headers:{Authorization:"Bearer "+m.value}});return typeof window.btoa=="function"?window.btoa(c):c}),Ie=v(()=>[{label:"Cursor (global)",lang:"json",note:"Paste into ~/.cursor/mcp.json, or .cursor/mcp.json in your project root for a per-workspace install.",code:`{ - "mcpServers": { - "orva": { - "url": "${r.value}/mcp", - "headers": { - "Authorization": "Bearer ${m.value}" - } - } - } -}`},{label:"Cline",lang:"json",note:"In VS Code: open Cline → MCP icon → Configure MCP Servers. Cline writes cline_mcp_settings.json.",code:`{ - "mcpServers": { - "orva": { - "url": "${r.value}/mcp", - "headers": { - "Authorization": "Bearer ${m.value}" - }, - "disabled": false - } - } -}`}]),h=x({name:"CodeBlock",props:{code:{type:String,required:!0},lang:{type:String,default:""}},setup(c){const o=k(!1),i=async()=>{await Y(c.code)&&(o.value=!0,setTimeout(()=>{o.value=!1},1200))},t=v(()=>{const p=(c.lang||"").toLowerCase();if(p&&f.getLanguage(p))try{return f.highlight(c.code,{language:p,ignoreIllegals:!0}).value}catch{}return c.code.replace(/&/g,"&").replace(//g,">")});return()=>s("div",{class:"codeblock"},[s("div",{class:"codeblock-bar"},[s("span",{class:"codeblock-lang"},c.lang||""),s("button",{class:"codeblock-copy",onClick:i,title:"Copy code"},[o.value?s(B,{class:"w-3 h-3"}):s(z,{class:"w-3 h-3"}),o.value?"Copied":"Copy"])]),s("pre",{class:"codeblock-pre"},[s("code",{class:`hljs language-${(c.lang||"text").toLowerCase()}`,innerHTML:t.value})])])}}),y=x({name:"TabbedCode",props:{tabs:{type:Array,required:!0},storageKey:{type:String,default:""}},setup(c){const o=(()=>{try{if(c.storageKey){const p=localStorage.getItem(c.storageKey);if(p&&c.tabs.some(w=>w.label===p))return p}}catch{}return c.tabs[0]?.label})(),i=k(o),t=p=>{i.value=p;try{c.storageKey&&localStorage.setItem(c.storageKey,p)}catch{}};return()=>{const p=c.tabs.find(w=>w.label===i.value)||c.tabs[0];return s("div",{class:"tabbed"},[s("div",{class:"tabbed-tabs"},c.tabs.map(w=>s("button",{key:w.label,class:["tabbed-tab",{active:w.label===i.value}],onClick:()=>t(w.label)},w.label))),p.note?s("div",{class:"tabbed-note"},p.note):null,s(h,{code:p.code,lang:p.lang})])}}}),$=x({name:"Callout",props:{title:{type:String,default:""},icon:{type:[Object,Function],default:null}},setup(c,{slots:o}){return()=>s("div",{class:"callout"},[s("div",{class:"callout-head"},[c.icon?s(c.icon,{class:"callout-icon"}):null,c.title?s("span",null,c.title):null]),s("div",{class:"callout-body"},o.default?.())])}});return(c,o)=>{const i=Le("router-link");return u(),g("div",Ve,[e("header",Ge,[o[3]||(o[3]=e("div",{class:"docs-hero-bg","aria-hidden":"true"},null,-1)),e("div",We,[e("div",Ye,[o[1]||(o[1]=e("div",{class:"docs-hero-text"},[e("h1",{class:"docs-hero-title"}," Documentation "),e("p",{class:"docs-hero-sub"}," Everything you need to write, deploy, and operate functions on Orva. Handler contract, deploy + invoke, SDK, MCP, tracing, error taxonomy. ")],-1)),e("div",Je,[e("button",{class:P(["docs-hero-copy-icon",{copied:O.value}]),title:O.value?"Copied":"Copy entire docs page as Markdown","aria-label":O.value?"Markdown copied to clipboard":"Copy entire docs page as Markdown",onClick:Se},[O.value?(u(),j(n(B),{key:0,class:"w-4 h-4"})):(u(),j(n(z),{key:1,class:"w-4 h-4"}))],10,Ze)])]),e("nav",Qe,[o[2]||(o[2]=e("span",{class:"docs-hero-toc-label"},"Jump to",-1)),(u(),g(_,null,A(I,t=>e("a",{key:t.id,href:`#${t.id}`,class:P(["docs-hero-toc-link",{active:E.value===t.id}])},[e("span",oo,l(t.num),1),e("span",null,l(t.label),1)],10,eo)),64))])])]),e("section",so,[o[5]||(o[5]=e("div",{class:"doc-section-head"},[e("span",{class:"doc-section-num"},"01"),e("div",null,[e("h2",{class:"doc-section-title"}," Handler contract "),e("p",{class:"doc-lede"}," One exported function receives the inbound HTTP event and returns an HTTP-shaped response. The adapter handles serialization and headers. ")])],-1)),d(n(y),{tabs:de.value,"storage-key":"docs.handler"},null,8,["tabs"]),o[6]||(o[6]=b('
Event shape
methodpathheadersquerybody
Response
{ statusCode, headers, body }

Non-string bodies are JSON-encoded by the adapter.

Runtime env
Env vars and secrets land in process.env / os.environ.
',1)),e("div",to,[e("table",ao,[o[4]||(o[4]=e("thead",null,[e("tr",null,[e("th",null,"Runtime"),e("th",null,"ID"),e("th",{class:"hidden sm:table-cell"}," Entrypoint "),e("th",{class:"hidden md:table-cell"}," Dependencies ")])],-1)),e("tbody",null,[(u(),g(_,null,A(ie,t=>e("tr",{key:t.id},[e("td",no,[(u(),j(Z(t.icon),{class:"shrink-0"})),a(" "+l(t.name),1)]),e("td",co,l(t.id),1),e("td",ro,l(t.entry),1),e("td",io,l(t.deps),1)])),64))])])])]),e("section",lo,[o[11]||(o[11]=b('
02

Deploy & invoke

The dashboard handles day-to-day work; these calls are for CI and automation. Builds run async; poll /api/v1/deployments/<id> or stream /api/v1/deployments/<id>/stream until phase: done.

',1)),d(n(ae)),e("div",po,[e("div",uo,[o[7]||(o[7]=e("div",{class:"doc-step-label"},[e("span",{class:"doc-step-num"},"1"),a(" Create the function row ")],-1)),d(n(h),{code:pe.value,lang:"bash"},null,8,["code"])]),e("div",ho,[o[8]||(o[8]=e("div",{class:"doc-step-label"},[e("span",{class:"doc-step-num"},"2"),a(" Upload code ")],-1)),d(n(h),{code:ue.value,lang:"bash"},null,8,["code"])])]),e("div",vo,[o[9]||(o[9]=e("div",{class:"doc-microlabel"}," Invoke ",-1)),d(n(y),{tabs:re.value,"storage-key":"docs.invoke"},null,8,["tabs"])]),d(n($),{icon:n(K),title:"Custom routes"},{default:C(()=>[...o[10]||(o[10]=[a(" Attach a friendly path with ",-1),e("code",{class:"doc-chip"},"POST /api/v1/routes",-1),a(". Reserved prefixes: ",-1),e("code",{class:"doc-chip"},"/api/",-1),e("code",{class:"doc-chip"},"/fn/",-1),e("code",{class:"doc-chip"},"/mcp/",-1),e("code",{class:"doc-chip"},"/web/",-1),e("code",{class:"doc-chip"},"/_orva/",-1),a(". ",-1)])]),_:1},8,["icon"])]),e("section",mo,[o[15]||(o[15]=e("div",{class:"doc-section-head"},[e("span",{class:"doc-section-num"},"03"),e("div",null,[e("h2",{class:"doc-section-title"}," Configuration reference "),e("p",{class:"doc-lede"}," Everything below lives on the function record. Secrets are stored encrypted and only decrypt into the worker environment at spawn time. ")])],-1)),e("div",bo,[e("table",go,[o[12]||(o[12]=e("thead",null,[e("tr",null,[e("th",null,"Field"),e("th",{class:"hidden sm:table-cell"}," Purpose "),e("th",null,"Behaviour")])],-1)),e("tbody",null,[(u(),g(_,null,A(le,t=>e("tr",{key:t.field,class:"align-top"},[e("td",yo,[(u(),j(Z(t.icon),{class:P(["w-3.5 h-3.5 shrink-0",t.iconClass])},null,8,["class"])),e("code",null,l(t.field),1)]),e("td",fo,l(t.purpose),1),e("td",ko,l(t.body),1)])),64))])])]),e("div",wo,[o[13]||(o[13]=e("div",{class:"doc-microlabel"}," Set a secret ",-1)),d(n(h),{code:he.value,lang:"bash"},null,8,["code"])]),e("details",Co,[e("summary",xo,[d(n(ee),{class:"w-3.5 h-3.5 transition-transform group-open:rotate-90 text-foreground-muted"}),o[14]||(o[14]=a(" Signed-invoke recipe (HMAC, opt-in) ",-1))]),e("div",To,[d(n(h),{code:ve.value,lang:"bash"},null,8,["code"])])])]),e("section",So,[o[21]||(o[21]=b('
04

SDK from inside a function

The bundled orva module exposes three primitives every function can use without extra dependencies: a per-function key/value store, in-process calls to other Orva functions, and a fire-and-forget background job queue. Routes through the per-process internal token injected at worker spawn time.

orva.kv
put / get / delete / list

Per-function namespace on SQLite, optional TTL.

orva.invoke
invoke(name, payload)

In-process call to another function. 8-deep call cap.

orva.jobs
jobs.enqueue(name, payload)

Fire-and-forget; persisted; retried with exp backoff.

',2)),e("div",_o,[o[16]||(o[16]=e("div",{class:"doc-microlabel"}," KV: get/put with TTL ",-1)),d(n(y),{tabs:be,"storage-key":"docs.sdk.kv"}),o[17]||(o[17]=b('

Browse / inspect / edit / delete / set keys without leaving the dashboard at /web/functions/<name>/kv (or click the KV button in the editor's action bar). REST mirror at GET/PUT/DELETE /api/v1/functions/<id>/kv[/<key>]; MCP tools kv_list / kv_get / kv_put / kv_delete for agents.

',1))]),e("div",Ao,[o[18]||(o[18]=e("div",{class:"doc-microlabel"}," Function-to-function: invoke() ",-1)),d(n(y),{tabs:ge,"storage-key":"docs.sdk.invoke"})]),e("div",Oo,[o[19]||(o[19]=e("div",{class:"doc-microlabel"}," Background jobs: jobs.enqueue() ",-1)),d(n(y),{tabs:ye,"storage-key":"docs.sdk.jobs"})]),d(n($),{icon:n(K),title:"Network mode"},{default:C(()=>[...o[20]||(o[20]=[a(" The SDK reaches orvad over loopback through the host gateway, so the function needs ",-1),e("code",{class:"doc-chip"},'network_mode: "egress"',-1),a(". On the default ",-1),e("code",{class:"doc-chip"},'"none"',-1),a(" the SDK throws ",-1),e("code",{class:"doc-chip"},"OrvaUnavailableError",-1),a(" with a clear hint. ",-1)])]),_:1},8,["icon"])]),e("section",Po,[e("div",jo,[o[32]||(o[32]=e("span",{class:"doc-section-num"},"05",-1)),e("div",null,[o[31]||(o[31]=e("h2",{class:"doc-section-title"}," Schedules ",-1)),e("p",Io,[o[23]||(o[23]=a(" Fire any function on a cron expression. The scheduler runs as part of the orvad process; no external service. Manage from the ",-1)),d(i,{to:"/cron",class:"text-foreground hover:text-white underline decoration-dotted underline-offset-4"},{default:C(()=>[...o[22]||(o[22]=[a("Schedules page",-1)])]),_:1}),o[24]||(o[24]=a(" or via the API. Standard 5-field cron with the usual shorthands (",-1)),o[25]||(o[25]=e("code",{class:"doc-chip"},"@daily",-1)),o[26]||(o[26]=a(", ",-1)),o[27]||(o[27]=e("code",{class:"doc-chip"},"@hourly",-1)),o[28]||(o[28]=a(", ",-1)),o[29]||(o[29]=e("code",{class:"doc-chip"},"*/5 * * * *",-1)),o[30]||(o[30]=a("). ",-1))])])]),d(n(y),{tabs:me.value,"storage-key":"docs.cron"},null,8,["tabs"]),d(n($),{icon:n($e),title:"Cron-fired headers"},{default:C(()=>[...o[33]||(o[33]=[a(" Every cron-triggered invocation arrives at the function with ",-1),e("code",{class:"doc-chip"},"x-orva-trigger: cron",-1),a(" and ",-1),e("code",{class:"doc-chip"},"x-orva-cron-id: cron_…",-1),a(" on the event headers, so user code can branch on origin. ",-1)])]),_:1},8,["icon"])]),e("section",Eo,[e("div",Do,[o[38]||(o[38]=e("span",{class:"doc-section-num"},"06",-1)),e("div",null,[o[37]||(o[37]=e("h2",{class:"doc-section-title"}," Webhooks ",-1)),e("p",Ro,[o[35]||(o[35]=a(" Operator-managed subscriptions for system events. Configure URLs from the ",-1)),d(i,{to:"/webhooks",class:"text-foreground hover:text-white underline decoration-dotted underline-offset-4"},{default:C(()=>[...o[34]||(o[34]=[a("Webhooks page",-1)])]),_:1}),o[36]||(o[36]=a("; Orva delivers signed POSTs to them when matching events fire (deployments, function lifecycle, cron failures, job outcomes). Subscriptions are global, not per-function. ",-1))])])]),d(n(ce)),o[41]||(o[41]=b('
Headers
X-Orva-EventX-Orva-Delivery-IdX-Orva-TimestampX-Orva-Signature
Signature
sha256=hex(hmac(secret, ts.body))

Same shape as Stripe / signed-invoke. Receivers verify with the secret returned at create time.

Retries
5 attemptsexp backoff (≤ 1h)

Receiver must 2xx within 15s.

',1)),e("div",Ho,[e("table",$o,[o[39]||(o[39]=e("thead",null,[e("tr",null,[e("th",null,"Event"),e("th",null,"When it fires")])],-1)),e("tbody",null,[(u(),g(_,null,A(fe,t=>e("tr",{key:t.name},[e("td",Mo,[e("code",null,l(t.name),1)]),e("td",Lo,l(t.when),1)])),64))])])]),e("div",qo,[o[40]||(o[40]=e("div",{class:"doc-microlabel"}," Verify a delivery ",-1)),d(n(y),{tabs:ke,"storage-key":"docs.webhooks.verify"})])]),e("section",No,[o[51]||(o[51]=e("div",{class:"doc-section-head"},[e("span",{class:"doc-section-num"},"07"),e("div",null,[e("h2",{class:"doc-section-title"}," MCP: Model Context Protocol "),e("p",{class:"doc-lede"}," Same API surface the dashboard uses, exposed as 70 tools an agent can call directly. API key permissions scope the available tool set. ")])],-1)),e("div",Bo,[e("div",zo,[o[42]||(o[42]=e("div",{class:"doc-microlabel"}," Endpoint ",-1)),e("div",Ko,[e("code",Uo,l(r.value)+"/mcp",1)])]),o[43]||(o[43]=b('
Auth header
Authorization: Bearer <token>

Or as a fallback: X-Orva-API-Key: <token>

Transport
Streamable HTTPMCP 2025-11-25
',2))]),d(n($),{icon:n(M),title:"Two header formats; same auth"},{default:C(()=>[...o[44]||(o[44]=[a(" Either header works against the same API key store with identical permission gating. ",-1),e("code",{class:"doc-chip"},"Authorization: Bearer",-1),a(" is the MCP / OAuth 2 spec form; every MCP SDK (Claude Code, Claude Desktop, Cursor, mcp-remote, Python ",-1),e("code",{class:"doc-chip"},"mcp",-1),a(") configures it natively, so prefer it for new setups. ",-1),e("code",{class:"doc-chip"},"X-Orva-API-Key",-1),a(" is the same header the REST API accepts; useful when a tool reuses an existing Orva REST integration. Internally both paths SHA-256-hash the token and look it up against the same ",-1),e("code",{class:"doc-chip"},"api_keys",-1),a(" table. ",-1)])]),_:1},8,["icon"]),e("div",Xo,[e("div",Fo,[d(n(M),{class:"w-4 h-4 shrink-0 text-foreground-muted"}),R.value?(u(),g("span",Go,[o[47]||(o[47]=a(" Token minted: ",-1)),e("code",Wo,l(_e.value)+"…",1),o[48]||(o[48]=a(" Shown once, copy now. ",-1))])):(u(),g("span",Vo,[o[45]||(o[45]=a(" Snippets show ",-1)),e("code",{class:"doc-chip"},l(oe)),o[46]||(o[46]=a(". Mint a token to substitute it everywhere. ",-1))]))]),e("button",{class:"doc-token-btn",disabled:H.value,onClick:Ae},[d(n(M),{class:"w-3.5 h-3.5"}),a(" "+l(R.value?"Mint another":H.value?"Minting…":"Generate token"),1)],8,Yo)]),d(n(y),{tabs:Oe.value,"storage-key":"docs.mcp.install"},null,8,["tabs"]),e("details",Jo,[e("summary",Zo,[d(n(ee),{class:"w-3.5 h-3.5 transition-transform group-open:rotate-90 text-foreground-muted"}),o[49]||(o[49]=a(" More clients (Cursor, VS Code, Codex CLI, OpenCode, Zed, Windsurf, ChatGPT, manual config) ",-1))]),e("div",Qo,[d(n(y),{tabs:Pe.value,"storage-key":"docs.mcp.install.more"},null,8,["tabs"]),o[50]||(o[50]=e("div",{class:"doc-microlabel pt-1"}," Hand-edited config files ",-1)),d(n(y),{tabs:Ie.value,"storage-key":"docs.mcp.manual"},null,8,["tabs"])])])]),e("section",es,[o[52]||(o[52]=b('
08

System prompt for AI assistants

Paste the prompt below into ChatGPT, Claude, Gemini, Cursor, Copilot, or any other AI tool to teach it Orva's full surface Handler contract, runtimes, sandbox limits, the in-sandbox orva SDK (kv / invoke / jobs), cron triggers, system-event webhooks, auth modes, and production patterns. The model then turns "describe what I want" into a pasteable handler on the first try.

',1)),e("div",os,[e("button",{class:P(["ai-copy-btn",{copied:D.value}]),onClick:te},[D.value?(u(),j(n(B),{key:0,class:"w-3.5 h-3.5"})):(u(),j(n(z),{key:1,class:"w-3.5 h-3.5"})),a(" "+l(D.value?"Copied":"Copy system prompt"),1)],2)]),e("div",{class:P(["prompt-collapse",{expanded:S.value}])},[d(n(h),{code:n(se),lang:"text"},null,8,["code"]),S.value?Me("",!0):(u(),g("div",ss))],2),e("button",{class:"prompt-expand-btn","aria-expanded":S.value,onClick:o[0]||(o[0]=t=>S.value=!S.value)},[d(n(Ke),{class:P(["w-3.5 h-3.5 transition-transform",{"rotate-180":S.value}])},null,8,["class"]),a(" "+l(S.value?"Collapse system prompt":"Expand full system prompt (~400 lines)"),1)],8,ts)]),e("section",as,[o[54]||(o[54]=b('
09

Tracing

Every invocation chain is recorded as a causal trace. automatically, with zero changes to your function code. HTTP requests, F2F invokes, jobs, cron, inbound webhooks, and replays all stitch into the same tree. The dashboard renders it as a waterfall at /traces.

Each execution row IS a span. Spans share a trace_id; child spans point at their parent via parent_span_id. You don't instantiate spans, you don't import a tracer; you just write your handler and the platform plumbs IDs through every internal hop.

',2)),d(n(ne)),o[55]||(o[55]=e("h3",{class:"doc-h3"},"What user code sees",-1)),o[56]||(o[56]=e("p",{class:"doc-prose"}," Two env vars are stamped per invocation. Read them only if you want to log the trace_id alongside your own messages; they're optional. ",-1)),d(n(h),{code:_s,lang:"text"}),o[57]||(o[57]=e("h3",{class:"doc-h3"},"Automatic propagation",-1)),o[58]||(o[58]=e("p",{class:"doc-prose"},[a(" When a function calls another via the SDK, the trace context flows through automatically. The called function becomes a child span of the caller; both share the same "),e("code",{class:"doc-chip"},"trace_id"),a(". ")],-1)),d(n(h),{code:As,lang:"js"}),o[59]||(o[59]=b('

Job enqueues work the same way: orva.jobs.enqueue() records the trace context on the job row. When the scheduler picks the job up later, the resulting execution lands in the same trace as the function that enqueued it, even if the gap is hours or days.

Triggers

Each span carries a trigger label so the UI can show how the chain started.

',3)),e("div",ns,[e("table",cs,[o[53]||(o[53]=e("thead",null,[e("tr",null,[e("th",null,"Trigger"),e("th",null,"Meaning")])],-1)),e("tbody",null,[(u(),g(_,null,A(we,t=>e("tr",{key:t.name},[e("td",ds,[e("code",null,l(t.name),1)]),e("td",rs,l(t.desc),1)])),64))])])]),o[60]||(o[60]=e("h3",{class:"doc-h3"},"External correlation (W3C traceparent)",-1)),o[61]||(o[61]=e("p",{class:"doc-prose"},[a(" Send a standard "),e("code",{class:"doc-chip"},"traceparent"),a(" header on the inbound HTTP request and Orva makes its trace a child of yours. The same trace_id is echoed back as "),e("code",{class:"doc-chip"},"X-Trace-Id"),a(" on every response, so external systems can correlate without parsing bodies. ")],-1)),d(n(h),{code:Os,lang:"bash"}),o[62]||(o[62]=b('

Outlier detection

Each function maintains an in-memory rolling P95 baseline over its last 100 successful warm executions. An invocation is flagged as an outlier when it has at least 20 baseline samples AND its duration exceeds P95 × 2. Cold starts and errors are excluded from the baseline so a flapping function can't drag it down. The flag and baseline P95 are stored on the execution row and rendered as an amber flag icon next to the span.

Where to look

  • /traces: list of recent traces, filterable by function / status / outlier-only.
  • /traces/:id: waterfall + per-span detail. Click a span to jump to its execution in the Invocations log.
  • GET /api/v1/traces/{id}: full span tree as JSON. Pair with list_traces / get_trace MCP tools for AI agents.
  • GET /api/v1/functions/{id}/baseline: current P95/P99/mean for a function.
',4))]),e("section",is,[o[64]||(o[64]=b('
10

Errors & recovery

Every error response uses the same envelope so log scrapers and retries can match on code. Deploys are content-addressed; rollback retargets the active version pointer and refreshes warm workers.

',1)),d(n(h),{code:Ps,lang:"json"}),e("div",ls,[e("table",ps,[o[63]||(o[63]=e("thead",null,[e("tr",null,[e("th",null,"Code"),e("th",null,"When you see it")])],-1)),e("tbody",null,[(u(),g(_,null,A(Ce,t=>e("tr",{key:t.code},[e("td",us,[e("code",null,l(t.code),1)]),e("td",hs,l(t.when),1)])),64))])])])]),e("section",vs,[o[81]||(o[81]=b('
11

CLI

orva is a single static binary that talks to a remote (or local) Orva server over HTTPS. Same binary as the daemon, orva serve starts a server, every other subcommand is a CLI client. Drop it on operator laptops, CI runners, or anywhere bash runs.

Install (server included)
curl … install.sh | sh

Full install: daemon + nsjail + rootfs + CLI.

Install (CLI only)
install.sh --cli-only

~10 MB binary at /usr/local/bin/orva. No service.

Inside Docker
docker exec orva orva …

CLI auto-authed via ~/.orva/config.yaml.

Authenticate

',3)),e("p",ms,[o[66]||(o[66]=a(" The CLI reads ",-1)),o[67]||(o[67]=e("code",{class:"doc-chip"},"~/.orva/config.yaml",-1)),o[68]||(o[68]=a(" for ",-1)),o[69]||(o[69]=e("code",{class:"doc-chip"},"endpoint",-1)),o[70]||(o[70]=a(" + ",-1)),o[71]||(o[71]=e("code",{class:"doc-chip"},"api_key",-1)),o[72]||(o[72]=a(". Generate a key from ",-1)),d(i,{to:"/api-keys",class:"text-foreground hover:text-white underline decoration-dotted underline-offset-4"},{default:C(()=>[...o[65]||(o[65]=[a("Keys",-1)])]),_:1}),o[73]||(o[73]=a(" in the dashboard, then: ",-1))]),d(n(h),{code:js,lang:"bash"}),o[82]||(o[82]=e("h3",{class:"doc-h3"},"Command index",-1)),e("div",bs,[e("table",gs,[o[74]||(o[74]=e("thead",null,[e("tr",null,[e("th",null,"Command"),e("th",null,"Subcommands"),e("th",{class:"hidden md:table-cell"},"Purpose")])],-1)),e("tbody",null,[(u(),g(_,null,A(xe,t=>e("tr",{key:t.cmd},[e("td",ys,[e("code",null,"orva "+l(t.cmd),1)]),e("td",fs,l(t.subs),1),e("td",ks,l(t.purpose),1)])),64))])])]),o[83]||(o[83]=e("h3",{class:"doc-h3"},"Common recipes",-1)),e("div",ws,[o[75]||(o[75]=e("div",{class:"doc-microlabel"},"Deploy a function from a directory",-1)),d(n(h),{code:Is,lang:"bash"})]),e("div",Cs,[o[76]||(o[76]=e("div",{class:"doc-microlabel"},"Invoke + tail logs",-1)),d(n(h),{code:Es,lang:"bash"})]),e("div",xs,[o[77]||(o[77]=e("div",{class:"doc-microlabel"},"Manage KV state",-1)),d(n(h),{code:Ds,lang:"bash"})]),e("div",Ts,[o[78]||(o[78]=e("div",{class:"doc-microlabel"},"Secrets, cron, jobs, webhooks",-1)),d(n(h),{code:Rs,lang:"bash"})]),e("div",Ss,[o[79]||(o[79]=e("div",{class:"doc-microlabel"},"System health, metrics, vacuum",-1)),d(n(h),{code:Hs,lang:"bash"})]),d(n($),{icon:n(M),title:"Shell completion"},{default:C(()=>[...o[80]||(o[80]=[a(" Generate completion for your shell: ",-1),e("code",{class:"doc-chip"},"orva completion bash | sudo tee /etc/bash_completion.d/orva",-1),a(", or ",-1),e("code",{class:"doc-chip"},"zsh",-1),a(" / ",-1),e("code",{class:"doc-chip"},"fish",-1),a(" / ",-1),e("code",{class:"doc-chip"},"powershell",-1),a(". Tab-completes commands, subcommands, and flags. ",-1)])]),_:1},8,["icon"])])])}}};export{Ys as default}; diff --git a/backend/internal/server/ui_dist/assets/Drawer-B_L-gxK5.js b/backend/internal/server/ui_dist/assets/Drawer-C3AFLOZb.js similarity index 96% rename from backend/internal/server/ui_dist/assets/Drawer-B_L-gxK5.js rename to backend/internal/server/ui_dist/assets/Drawer-C3AFLOZb.js index 88a89eb..cfe56f2 100644 --- a/backend/internal/server/ui_dist/assets/Drawer-B_L-gxK5.js +++ b/backend/internal/server/ui_dist/assets/Drawer-C3AFLOZb.js @@ -1 +1 @@ -import{D as h,E as p,o as y,y as x,j as a,n as k,d as r,h as m,a as n,b as o,J as f,L as v,N as V,K as d,f as _,aa as g,g as l,Q as B,r as C,k as D,t as E,I as N}from"./index-D5cO6vit.js";const S={key:0,class:"fixed inset-0 z-50 pointer-events-none"},K={class:"px-5 py-3 border-b border-border flex items-center justify-between shrink-0"},T={class:"text-sm font-medium text-white truncate"},$={class:"flex-1 overflow-y-auto scrollable"},L={key:0,class:"px-5 py-3 border-t border-border shrink-0"},j={__name:"Drawer",props:{modelValue:{type:Boolean,default:!1},title:{type:String,default:""},width:{type:String,default:"560px"}},emits:["update:modelValue"],setup(t,{emit:b}){const i=t,w=b,c=C(null),s=()=>w("update:modelValue",!1);p(()=>i.modelValue,async e=>{e&&(await N(),c.value?.focus?.())});const u=e=>{e.key==="Escape"&&i.modelValue&&s()};return y(()=>window.addEventListener("keydown",u)),x(()=>window.removeEventListener("keydown",u)),(e,z)=>(a(),k(B,{to:"body"},[r(f,{name:"drawer-fade"},{default:m(()=>[t.modelValue?(a(),n("div",S,[o("div",{class:"absolute inset-0 pointer-events-auto",onClick:s}),r(f,{name:"drawer-slide"},{default:m(()=>[t.modelValue?(a(),n("div",{key:0,class:"absolute pointer-events-auto bg-background flex flex-col inset-x-0 bottom-0 max-h-[85dvh] border-t border-border rounded-t-lg pb-safe sm:inset-x-auto sm:right-0 sm:top-0 sm:bottom-0 sm:max-h-none sm:border-t-0 sm:border-l sm:rounded-none sm:pb-0 sm:w-[var(--drawer-w,560px)]",style:V({"--drawer-w":t.width}),onKeydown:v(s,["esc"]),tabindex:"-1",ref_key:"root",ref:c},[o("header",K,[o("div",T,[d(e.$slots,"title",{},()=>[D(E(t.title),1)],!0)]),o("button",{class:"text-foreground-muted hover:text-white transition-colors touch-expand-iconbtn -mr-1",onClick:s,"aria-label":"Close"},[r(_(g),{class:"w-4 h-4"})])]),o("div",$,[d(e.$slots,"default",{},void 0,!0)]),e.$slots.footer?(a(),n("footer",L,[d(e.$slots,"footer",{},void 0,!0)])):l("",!0)],36)):l("",!0)]),_:3})])):l("",!0)]),_:3})]))}},J=h(j,[["__scopeId","data-v-bda4ef6c"]]);export{J as D}; +import{D as h,E as p,o as y,y as x,j as a,n as k,d as r,h as m,a as n,b as o,J as f,L as v,N as V,K as d,f as _,aa as g,g as l,Q as B,r as C,k as D,t as E,I as N}from"./index-BMkkwZ9q.js";const S={key:0,class:"fixed inset-0 z-50 pointer-events-none"},K={class:"px-5 py-3 border-b border-border flex items-center justify-between shrink-0"},T={class:"text-sm font-medium text-white truncate"},$={class:"flex-1 overflow-y-auto scrollable"},L={key:0,class:"px-5 py-3 border-t border-border shrink-0"},j={__name:"Drawer",props:{modelValue:{type:Boolean,default:!1},title:{type:String,default:""},width:{type:String,default:"560px"}},emits:["update:modelValue"],setup(t,{emit:b}){const i=t,w=b,c=C(null),s=()=>w("update:modelValue",!1);p(()=>i.modelValue,async e=>{e&&(await N(),c.value?.focus?.())});const u=e=>{e.key==="Escape"&&i.modelValue&&s()};return y(()=>window.addEventListener("keydown",u)),x(()=>window.removeEventListener("keydown",u)),(e,z)=>(a(),k(B,{to:"body"},[r(f,{name:"drawer-fade"},{default:m(()=>[t.modelValue?(a(),n("div",S,[o("div",{class:"absolute inset-0 pointer-events-auto",onClick:s}),r(f,{name:"drawer-slide"},{default:m(()=>[t.modelValue?(a(),n("div",{key:0,class:"absolute pointer-events-auto bg-background flex flex-col inset-x-0 bottom-0 max-h-[85dvh] border-t border-border rounded-t-lg pb-safe sm:inset-x-auto sm:right-0 sm:top-0 sm:bottom-0 sm:max-h-none sm:border-t-0 sm:border-l sm:rounded-none sm:pb-0 sm:w-[var(--drawer-w,560px)]",style:V({"--drawer-w":t.width}),onKeydown:v(s,["esc"]),tabindex:"-1",ref_key:"root",ref:c},[o("header",K,[o("div",T,[d(e.$slots,"title",{},()=>[D(E(t.title),1)],!0)]),o("button",{class:"text-foreground-muted hover:text-white transition-colors touch-expand-iconbtn -mr-1",onClick:s,"aria-label":"Close"},[r(_(g),{class:"w-4 h-4"})])]),o("div",$,[d(e.$slots,"default",{},void 0,!0)]),e.$slots.footer?(a(),n("footer",L,[d(e.$slots,"footer",{},void 0,!0)])):l("",!0)],36)):l("",!0)]),_:3})])):l("",!0)]),_:3})]))}},J=h(j,[["__scopeId","data-v-bda4ef6c"]]);export{J as D}; diff --git a/backend/internal/server/ui_dist/assets/Editor-BX4mg0fo.js b/backend/internal/server/ui_dist/assets/Editor-BX4mg0fo.js deleted file mode 100644 index 956cfa9..0000000 --- a/backend/internal/server/ui_dist/assets/Editor-BX4mg0fo.js +++ /dev/null @@ -1,1296 +0,0 @@ -const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/CodeEditor-Davda7hv.js","assets/index-BOWx3BJu.js","assets/index-D5cO6vit.js","assets/index-De6hiwes.css","assets/CodeEditor-tn0RQdqM.css"])))=>i.map(i=>d[i]); -import{c as Je,D as po,C as fo,E as he,o as vo,G as gt,a as l,b as o,t as p,d,f,e as _,v as O,g as h,k as u,Z as ho,$ as go,h as g,_ as M,n as le,F as S,p as C,s as oe,a0 as bo,a1 as Oe,Y as T,r as v,q as E,a2 as yo,a3 as xo,a4 as _o,a5 as wo,a6 as ko,i as So,a7 as Co,a8 as To,a9 as Eo,j as i,H as Oo,w as Ro,aa as $e,ab as jo,L as No,ac as Do,ad as Ao,ae as Po,af as Io,ag as Mo,z as ge}from"./index-D5cO6vit.js";import{_ as de}from"./Input-Bbxjlz9n.js";import{_ as re}from"./Modal-C-qDVd6z.js";import{c as Vo}from"./clipboard-CmSw2rR-.js";import{c as Lo}from"./aiPrompts-Dgb3jxRL.js";import{d as $o}from"./rollbackDiff-Cvt2Ss82.js";import{F as bt,S as Ue,P as yt}from"./settings-2-BhRqOkbZ.js";import{C as be}from"./chevron-down-Trjh5P5D.js";import{V as xt}from"./variable-kwh9BTXT.js";import{K as _t}from"./key-round-D643nzM3.js";import{B as wt}from"./book-open-2x4dJwqD.js";import{C as Uo}from"./check-BkPCgKSu.js";import{C as qo}from"./copy-Gc8n9M6v.js";import{P as kt}from"./play-BfsTXwNm.js";import{S as Bo}from"./sparkles-DV3RWxRD.js";import{C as qe,G as zo}from"./git-compare-CXuIlVPE.js";import{T as Be}from"./trash-2-sSCgxcxW.js";import{G as St}from"./globe-BSxGWG92.js";import{L as Fo}from"./lock-C90zRIcI.js";import{S as Ho}from"./shield-check-BFAT8DDU.js";import{R as Go}from"./rotate-ccw-CeRUwJZR.js";import{T as Jo}from"./terminal-CR-RL0k9.js";const Ko=Je("database",[["ellipse",{cx:"12",cy:"5",rx:"9",ry:"3",key:"msslwz"}],["path",{d:"M3 5V19A9 3 0 0 0 21 19V5",key:"1wlel7"}],["path",{d:"M3 12A9 3 0 0 0 21 12",key:"mv7ke4"}]]);const Ct=Je("layers",[["path",{d:"M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z",key:"zw3jo"}],["path",{d:"M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12",key:"1wduqc"}],["path",{d:"M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17",key:"kqbvx6"}]]);const Tt=Je("shuffle",[["path",{d:"m18 14 4 4-4 4",key:"10pe0f"}],["path",{d:"m18 2 4 4-4 4",key:"pucp1d"}],["path",{d:"M2 18h1.973a4 4 0 0 0 3.3-1.7l5.454-8.6a4 4 0 0 1 3.3-1.7H22",key:"1ailkh"}],["path",{d:"M2 6h1.972a4 4 0 0 1 3.6 2.2",key:"km57vx"}],["path",{d:"M22 18h-6.041a4 4 0 0 1-3.3-1.8l-.359-.45",key:"os18l9"}]]),Wo=["amber","arctic","aurora","bold","brave","breezy","bright","brisk","calm","celestial","cobalt","cosmic","crimson","crisp","crystal","dapper","dazzling","deep","eager","ember","fearless","feisty","fierce","flaming","fluent","fluorescent","frosty","gentle","glacial","golden","graceful","happy","hazy","icy","indigo","jade","jolly","jovial","keen","kindred","lavender","lively","lucent","lunar","magenta","magnetic","merry","midnight","mighty","mellow","mossy","mystic","neon","nimble","noble","obsidian","opal","pearl","peppy","pixel","plucky","plush","polar","prime","quartz","quick","quiet","radiant","rapid","rare","roaming","rosy","royal","rugged","runic","rustic","sapphire","scarlet","sharp","silent","silken","silver","sleek","smooth","snowy","snug","solar","sonic","spry","starlit","stellar","sturdy","sublime","sunny","svelte","swift","tame","tender","thunder","tidal","topaz","tropic","turquoise","twilight","urban","velvet","verdant","vibrant","violet","vivid","warm","whisper","wild","wise","witty","woven","zesty","zen"],Yo=["albatross","amber","antler","apricot","archer","arrow","atlas","aurora","badger","bayou","beacon","bison","blossom","bramble","breeze","cactus","canyon","caravan","cedar","cliff","comet","compass","coral","cosmos","cypress","dawn","delta","dolphin","drift","dune","eagle","ember","fable","falcon","fern","fjord","flame","flint","forest","galaxy","garnet","geyser","glacier","glade","glint","gorge","gull","harbor","haven","horizon","iceberg","iris","jaguar","jetty","jungle","kelp","kestrel","kettle","kraken","lagoon","lantern","lark","ledge","lily","lighthouse","lupine","lynx","maple","meadow","meridian","meteor","mirage","mistral","monsoon","moon","moss","mountain","nebula","oak","oasis","ocean","orchid","osprey","otter","panda","panther","parrot","pebble","phoenix","pine","pinion","pixel","planet","plume","pond","poppy","prairie","puffin","puma","quartz","quasar","quill","rapids","raven","reef","ridge","river","robin","rune","sage","satellite","savanna","sequoia","shadow","signal","silo","sky","sloth","snow","sparrow","spire","star","stream","summit","swan","tempest","thicket","thistle","thunder","tide","tiger","totem","tower","tundra","twilight","twister","valley","vortex","walnut","wave","whale","whisper","wildflower","willow","wolf","wren","zenith","zephyr"],Et=Re=>Re[Math.floor(Math.random()*Re.length)];function Ot(){return`${Et(Wo)}-${Et(Yo)}`}const He=`import json - - -def handler(event): - body = event.get("body") or {} - if isinstance(body, str): - body = json.loads(body) if body else {} - name = body.get("name", "World") if isinstance(body, dict) else "World" - - return { - "statusCode": 200, - "headers": {"Content-Type": "application/json"}, - "body": json.dumps({"message": f"Hello {name}!", "language": "Python"}), - } -`,Xo=`# Verify a Stripe webhook and branch on event type. -# Set the secret as STRIPE_WEBHOOK_SECRET in the function's secrets so -# rotation doesn't require a redeploy. -import hashlib -import hmac -import json -import os -import time - - -def _verify(payload: bytes, header: str, secret: str, tolerance: int = 300) -> bool: - # Stripe sends a header like: t=1614265978,v1=abcdef... - parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p) - ts = int(parts.get("t", "0")) - sig = parts.get("v1", "") - if abs(time.time() - ts) > tolerance: - return False - signed = f"{ts}.{payload.decode('utf-8')}".encode() - expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest() - return hmac.compare_digest(expected, sig) - - -def handler(event): - headers = {k.lower(): v for k, v in (event.get("headers") or {}).items()} - sig_header = headers.get("stripe-signature", "") - secret = os.environ.get("STRIPE_WEBHOOK_SECRET", "") - raw = (event.get("body") or "").encode("utf-8") if isinstance(event.get("body"), str) else json.dumps(event.get("body") or {}).encode() - - if not _verify(raw, sig_header, secret): - return {"statusCode": 401, "body": "invalid signature"} - - payload = json.loads(raw) - event_type = payload.get("type", "unknown") - - # Branch on the event types you care about. Anything else is ack'd 200 - # so Stripe stops retrying. - if event_type == "checkout.session.completed": - session_id = payload["data"]["object"]["id"] - # TODO: fulfil the order — record session_id in your DB. - print(f"checkout completed: {session_id}") - elif event_type == "customer.subscription.deleted": - sub_id = payload["data"]["object"]["id"] - print(f"subscription cancelled: {sub_id}") - - return {"statusCode": 200, "body": json.dumps({"received": True, "type": event_type})} -`,Qo=`# Verify a GitHub webhook (HMAC-SHA256) and route on X-GitHub-Event. -# Set the secret as GITHUB_WEBHOOK_SECRET in the function's secrets. -import hashlib -import hmac -import json -import os - - -def handler(event): - headers = {k.lower(): v for k, v in (event.get("headers") or {}).items()} - sig = headers.get("x-hub-signature-256", "") - if not sig.startswith("sha256="): - return {"statusCode": 401, "body": "missing signature"} - - secret = os.environ.get("GITHUB_WEBHOOK_SECRET", "") - raw = event.get("body") or "" - if not isinstance(raw, str): - raw = json.dumps(raw) - expected = "sha256=" + hmac.new(secret.encode(), raw.encode(), hashlib.sha256).hexdigest() - if not hmac.compare_digest(expected, sig): - return {"statusCode": 401, "body": "bad signature"} - - gh_event = headers.get("x-github-event", "ping") - payload = json.loads(raw) if raw else {} - - if gh_event == "ping": - return {"statusCode": 200, "body": json.dumps({"pong": True})} - if gh_event == "push": - ref = payload.get("ref", "") - commits = len(payload.get("commits", [])) - print(f"push to {ref}: {commits} commit(s)") - elif gh_event == "pull_request": - action = payload.get("action", "") - pr_num = payload.get("number", 0) - print(f"PR #{pr_num} {action}") - - return {"statusCode": 200, "body": json.dumps({"event": gh_event})} -`,Zo=`# Slack slash command handler. Slack POSTs application/x-www-form-urlencoded -# to the request URL and expects a JSON response within 3 seconds. Set -# SLACK_SIGNING_SECRET in the function's secrets to verify requests. -import hashlib -import hmac -import json -import os -import time -import urllib.parse - - -def _verify(ts: str, body: str, sig: str, secret: str) -> bool: - if abs(time.time() - int(ts or "0")) > 300: - return False - base = f"v0:{ts}:{body}".encode() - expected = "v0=" + hmac.new(secret.encode(), base, hashlib.sha256).hexdigest() - return hmac.compare_digest(expected, sig) - - -def handler(event): - headers = {k.lower(): v for k, v in (event.get("headers") or {}).items()} - body = event.get("body") or "" - if not isinstance(body, str): - body = urllib.parse.urlencode(body) - - secret = os.environ.get("SLACK_SIGNING_SECRET", "") - if not _verify(headers.get("x-slack-request-timestamp", ""), - body, - headers.get("x-slack-signature", ""), - secret): - return {"statusCode": 401, "body": "invalid signature"} - - form = dict(urllib.parse.parse_qsl(body)) - user = form.get("user_name", "?") - text = form.get("text", "").strip() - - return { - "statusCode": 200, - "headers": {"Content-Type": "application/json"}, - "body": json.dumps({ - "response_type": "in_channel", - "text": f":wave: Hello @{user}! You said: \\"{text or '(nothing)'}\\"", - }), - } -`,es=`# Convert a CSV upload to JSON. Accepts plain text body OR a multipart -# upload with field name "file". Stream-parses with csv.reader so a 5MB -# CSV doesn't blow the 6MB request body cap with intermediate copies. -import csv -import io -import json - - -def handler(event): - body = event.get("body") or "" - if not isinstance(body, str): - body = json.dumps(body) - - headers = {k.lower(): v for k, v in (event.get("headers") or {}).items()} - ctype = headers.get("content-type", "") - - # Strip multipart wrapper if present. Real multipart parsing is more - # involved; this covers the common "single file, plain CSV body" - # case that simple uploaders produce. - if ctype.startswith("multipart/"): - marker = "\\r\\n\\r\\n" - if marker in body: - body = body.split(marker, 1)[1] - tail = body.rfind("\\r\\n--") - if tail > 0: - body = body[:tail] - - reader = csv.reader(io.StringIO(body)) - rows = list(reader) - if not rows: - return {"statusCode": 400, "body": "empty CSV"} - - header_row, *data_rows = rows - out = [dict(zip(header_row, r)) for r in data_rows] - - return { - "statusCode": 200, - "headers": {"Content-Type": "application/json"}, - "body": json.dumps({"count": len(out), "rows": out}), - } -`,ts=`# Scheduled DB cleanup: delete rows older than RETENTION_DAYS. -# Pair this with a Schedules entry like "0 2 * * *" (daily at 02:00). -# Configure DB_DSN as a secret. The handler is idempotent so multiple -# fires within the same window are safe. -import json -import os -from datetime import datetime, timedelta, timezone - -# Replace this with your actual DB driver; psycopg2/asyncpg/etc. -# import psycopg2 - - -def handler(event): - retention_days = int(os.environ.get("RETENTION_DAYS", "30")) - cutoff = datetime.now(timezone.utc) - timedelta(days=retention_days) - - # Example DB call (commented — drop your driver in): - # conn = psycopg2.connect(os.environ["DB_DSN"]) - # cur = conn.cursor() - # cur.execute("DELETE FROM events WHERE created_at < %s", (cutoff,)) - # deleted = cur.rowcount - # conn.commit() - # conn.close() - deleted = 0 # replace with real count - - headers = {k.lower(): v for k, v in (event.get("headers") or {}).items()} - triggered_by = headers.get("x-orva-trigger", "manual") - - return { - "statusCode": 200, - "headers": {"Content-Type": "application/json"}, - "body": json.dumps({ - "trigger": triggered_by, - "cutoff": cutoff.isoformat(), - "deleted_rows": deleted, - }), - } -`,os=`# RSS / Atom summarizer. Fetches a feed and returns the latest N items -# as JSON. Works for both RSS 2.0 and Atom. Requires network_mode=egress -# on the function settings to reach the upstream feed. -import json -import os -import urllib.request -import xml.etree.ElementTree as ET - - -def _strip_ns(tag: str) -> str: - return tag.split("}", 1)[1] if "}" in tag else tag - - -def handler(event): - body = event.get("body") or {} - if isinstance(body, str): - body = json.loads(body) if body else {} - - feed_url = body.get("url") or os.environ.get("FEED_URL", "") - if not feed_url: - return {"statusCode": 400, "body": "url is required"} - - limit = int(body.get("limit", 10)) - - with urllib.request.urlopen(feed_url, timeout=10) as resp: - xml_bytes = resp.read() - - root = ET.fromstring(xml_bytes) - items = [] - # RSS: ...; Atom: ... - for el in root.iter(): - tag = _strip_ns(el.tag) - if tag in ("item", "entry"): - entry = {_strip_ns(c.tag): (c.text or "").strip() for c in el} - items.append(entry) - if len(items) >= limit: - break - - return { - "statusCode": 200, - "headers": {"Content-Type": "application/json"}, - "body": json.dumps({"feed": feed_url, "count": len(items), "items": items}), - } -`,ss=`# Resize an image to a thumbnail. Accepts {"image_b64": "...", "width": 200} -# and returns base64 PNG. Requires Pillow (add "Pillow" to requirements.txt). -import base64 -import io -import json - -from PIL import Image - - -def handler(event): - body = event.get("body") or {} - if isinstance(body, str): - body = json.loads(body) if body else {} - - img_b64 = body.get("image_b64") or "" - if not img_b64: - return {"statusCode": 400, "body": "image_b64 is required"} - - width = int(body.get("width", 200)) - height = int(body.get("height", 0)) or None - - raw = base64.b64decode(img_b64) - img = Image.open(io.BytesIO(raw)) - - # Preserve aspect ratio when only width is given. - if height is None: - ratio = width / img.width - height = int(img.height * ratio) - img.thumbnail((width, height)) - - out = io.BytesIO() - img.save(out, format="PNG", optimize=True) - out_b64 = base64.b64encode(out.getvalue()).decode() - - return { - "statusCode": 200, - "headers": {"Content-Type": "application/json"}, - "body": json.dumps({"thumbnail_b64": out_b64, "width": width, "height": height}), - } -`,ns=`Pillow>=10.0 -`,rs=`""" -guestbook — Orva showcase template - -A single function that demonstrates EVERY core Orva surface: - - • Server-rendered dark-mode HTML page with form + feed - • JSON API with pagination, validation, and CORS - • orva.kv for durable per-function state - • orva.jobs to enqueue post-submit enrichment work back to itself - • Cron-trigger handling for periodic cleanup of stale entries - • Admin auth via a function secret (ADMIN_TOKEN) on destructive ops - • Custom route mount under /guestbook/* - -Setup steps after deploy: - 1. Set the ADMIN_TOKEN secret (Settings → Secrets) to enable DELETE. - 2. Create a custom route /guestbook/* → this function (Routes API). - 3. (optional) Schedule a daily cron via the Schedules page; the - handler reads x-orva-trigger and runs the cleanup branch. - -Endpoints (after the route is mounted): - GET /guestbook/ → render the page - POST /guestbook/ → form submission, 303 back - GET /guestbook/api/submissions → list with ?limit=&cursor= - GET /guestbook/api/submissions/ → one entry - POST /guestbook/api/submissions → JSON body, 201 - DELETE /guestbook/api/submissions/ → admin only (Bearer ADMIN_TOKEN) - GET /guestbook/api/stats → totals + last-24h count -""" - -import html as _html -import json -import os -import re -import secrets -import time -from urllib.parse import parse_qs - -from orva import kv, jobs - -ROUTE_BASE = "/guestbook" -NAME_RE = re.compile(r"^[\\w \\-\\.\\'\\"@]{1,40}$") -MAX_MSG = 280 -LIST_LIMIT = 200 -RETENTION_DAYS = int(os.environ.get("RETENTION_DAYS", "30")) - - -# ── helpers ────────────────────────────────────────────────────────── - -def _new_id(): - return f"sub:{time.time_ns()}:{secrets.token_hex(2)}" - - -def _now_iso(): - return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) - - -def _now_unix(): - return int(time.time()) - - -def _json_response(status, body, headers=None): - h = {"Content-Type": "application/json", "Cache-Control": "no-store", - "Access-Control-Allow-Origin": "*"} - if headers: - h.update(headers) - return {"statusCode": status, "headers": h, "body": body} - - -def _html_response(body, status=200): - return { - "statusCode": status, - "headers": {"Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store"}, - "body": body, - } - - -def _read_body(event): - body = event.get("body") - ct = (event.get("headers") or {}).get("content-type", "").lower() - if isinstance(body, dict): - return body - if not body: - return {} - if "application/json" in ct: - try: - return json.loads(body) if isinstance(body, str) else body - except json.JSONDecodeError: - return {} - if "application/x-www-form-urlencoded" in ct or "multipart/form-data" in ct: - if isinstance(body, str): - return {k: v[0] for k, v in parse_qs(body, keep_blank_values=True).items()} - if isinstance(body, str): - try: - return json.loads(body) - except json.JSONDecodeError: - return {} - return {} - - -def _validate(data): - name = (data.get("name") or "").strip() - message = (data.get("message") or "").strip() - if not name: - return "", "", "name is required" - if not NAME_RE.match(name): - return "", "", "name must be <= 40 chars; letters / digits / @ . - _ ' \\" space only" - if not message: - return "", "", "message is required" - if len(message) > MAX_MSG: - return "", "", f"message must be <= {MAX_MSG} chars" - return name, message, "" - - -def _save(name, message): - sid = _new_id() - record = { - "id": sid, - "name": name, - "message": message, - "ts": _now_iso(), - "ts_unix": _now_unix(), - } - kv.put(sid, record) - counter = (kv.get("meta:counter", default=0) or 0) + 1 - kv.put("meta:counter", counter) - - # Hand off enrichment to the background queue. The same function - # picks up the job (see _on_job_trigger) — no separate worker - # function. The user's POST returns immediately. - try: - jobs.enqueue("guestbook", {"action": "enrich", "id": sid}) - except Exception as e: - _log("warn", "jobs.enqueue failed", error=str(e)) - - return record - - -def _unwrap(row): - """orva.kv.list returns either a parsed dict OR a {key, value: } envelope depending on runtime. Handle both.""" - if isinstance(row, dict) and "key" in row and "value" in row: - v = row["value"] - if isinstance(v, str): - try: - return json.loads(v) - except json.JSONDecodeError: - return None - return v - return row - - -def _list(limit=LIST_LIMIT, cursor=None): - rows = kv.list(prefix="sub:", limit=limit) or [] - parsed = [_unwrap(r) for r in rows] - parsed = [r for r in parsed if isinstance(r, dict)] - parsed = list(reversed(parsed)) # newest first - if cursor: - try: - i = next(idx for idx, r in enumerate(parsed) if r.get("id") == cursor) - parsed = parsed[i + 1:] - except StopIteration: - pass - return parsed - - -def _log(level, msg, **fields): - fields.update({"level": level, "msg": msg}) - print(json.dumps(fields)) - - -# ── HTML page (inline CSS, no external deps) ──────────────────────── - -PAGE = """\\ - - - - - -guestbook · {count} - - - -
-
-

guestbook.

-

Drop a note — say hi, share a link, leave a thought.

-
- -
-
- - -
-
- - -
- {error_html} -
- 0 / {max_msg} - -
-
- - {entries_html} - -
- {count} {entry_word} - · JSON at {base}/api/submissions - · Stats at {base}/api/stats -
-
- + diff --git a/backend/internal/server/uuid_e2e_test.go b/backend/internal/server/uuid_e2e_test.go index effb506..6267007 100644 --- a/backend/internal/server/uuid_e2e_test.go +++ b/backend/internal/server/uuid_e2e_test.go @@ -21,7 +21,7 @@ func TestE2E_FunctionsHaveUUIDIDs(t *testing.T) { tc := newTestServer(t) // Create a function via REST. - body := `{"name":"hello-uuid","runtime":"python314"}` + body := `{"name":"hello-uuid","runtime":"python"}` req := httptest.NewRequest("POST", "/api/v1/functions", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") tc.setAuth(req) @@ -200,7 +200,7 @@ func TestE2E_MCPListFunctionsIncludesInvokeURL(t *testing.T) { cookie := onboardAndLogin(t, tc, "alice", "supersecret123") // Create a function so list_functions has something to return. - body := `{"name":"e2e-mcp-test","runtime":"python314"}` + body := `{"name":"e2e-mcp-test","runtime":"python"}` req := httptest.NewRequest("POST", "/api/v1/functions", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") tc.setAuth(req) diff --git a/backend/runtimes/node24/adapter.js b/backend/runtimes/node/adapter.js similarity index 99% rename from backend/runtimes/node24/adapter.js rename to backend/runtimes/node/adapter.js index e57d5b7..efcd486 100644 --- a/backend/runtimes/node24/adapter.js +++ b/backend/runtimes/node/adapter.js @@ -484,7 +484,7 @@ async function readFrame() { if (_depth) process.env.ORVA_CALL_DEPTH = _depth; else delete process.env.ORVA_CALL_DEPTH; - // v0.5 trace context — see node22 adapter for the rationale. + // v0.5 trace context — propagate the inbound trace/span ids to F2F calls. const _tID = _hdrs['x-orva-trace-id'] || _hdrs['X-Orva-Trace-Id'] || ''; const _sID = _hdrs['x-orva-span-id'] || _hdrs['X-Orva-Span-Id'] || ''; if (_tID) process.env.ORVA_TRACE_ID = _tID; else delete process.env.ORVA_TRACE_ID; diff --git a/backend/cmd/orva/adapters/node24/orva.d.ts b/backend/runtimes/node/orva.d.ts similarity index 100% rename from backend/cmd/orva/adapters/node24/orva.d.ts rename to backend/runtimes/node/orva.d.ts diff --git a/backend/cmd/orva/adapters/node24/orva.js b/backend/runtimes/node/orva.js similarity index 100% rename from backend/cmd/orva/adapters/node24/orva.js rename to backend/runtimes/node/orva.js diff --git a/backend/cmd/orva/adapters/node24/package.json b/backend/runtimes/node/package.json similarity index 100% rename from backend/cmd/orva/adapters/node24/package.json rename to backend/runtimes/node/package.json diff --git a/backend/runtimes/node22/adapter.js b/backend/runtimes/node22/adapter.js deleted file mode 100644 index f9fb924..0000000 --- a/backend/runtimes/node22/adapter.js +++ /dev/null @@ -1,582 +0,0 @@ -// Orva Node.js adapter — universal handler loader. -// -// Accepts a wide range of export conventions so existing code from AWS -// Lambda, Cloudflare Workers, Vercel, Next.js, Netlify, and generic -// Node/Express style deploys runs with zero changes: -// -// AWS Lambda : exports.handler = async (event, context) => ... -// exports.lambda_handler = async (event, context) => ... -// Cloudflare Worker : export default { fetch(request, env, ctx) { ... } } -// addEventListener('fetch', e => e.respondWith(...)) -// Vercel / Next API : export default async function handler(req, res) { ... } -// Netlify / generic : exports.handler = async (event) => ... -// Plain function : module.exports = async (event) => ... -// -// The adapter normalises all of them to a { statusCode, headers, body } -// response envelope, the native Orva protocol. - -const path = require('path'); -const Module = require('module'); - -const FUNCTION_DIR = '/code'; -const entrypoint = process.env.ORVA_ENTRYPOINT || 'handler.js'; -const handlerPath = path.join(FUNCTION_DIR, entrypoint); - -// Make the bundled `orva` SDK module (kv / invoke / jobs) resolvable -// from user code via `require('orva')`. The package lives at -// /opt/orva/node_modules/orva/; injecting that dir at the front of -// Module._nodeModulePaths ensures user modules can find it without -// the user having to install it. User-installed deps still resolve -// normally via /code/node_modules. -const _origNodeModulePaths = Module._nodeModulePaths; -Module._nodeModulePaths = function (from) { - return ['/opt/orva/node_modules', ...(_origNodeModulePaths.call(this, from) || [])]; -}; - -// Preserve stdout for the protocol response; reroute user output to stderr. -const originalStdoutWrite = process.stdout.write.bind(process.stdout); -const writeProtocol = (s) => originalStdoutWrite(s); -process.stdout.write = process.stderr.write.bind(process.stderr); -console.log = (...a) => process.stderr.write(a.map(String).join(' ') + '\n'); -console.info = console.log; -console.debug = console.log; -console.warn = console.log; -console.error = console.log; - -let mod; -try { - mod = require(handlerPath); -} catch (err) { - process.stderr.write(`Failed to load handler from ${handlerPath}: ${err.message}\n`); - process.exit(1); -} - -// Unwrap ESM default export shim (Babel/TS sometimes emit { default: fn }). -if (mod && typeof mod === 'object' && mod.__esModule && mod.default) { - mod = mod.default; -} - -// Resolve a callable from whatever shape the user exported. -let handler = null; -let style = null; // "lambda" | "worker" | "vercel" | "plain" - -if (typeof mod === 'function') { - handler = mod; - // A plain function could be Lambda-style or Vercel-style — decide by arity. - style = mod.length >= 2 ? 'vercel-or-lambda' : 'plain'; -} else if (mod && typeof mod === 'object') { - // Cloudflare Worker: { fetch(request, env, ctx) } - if (typeof mod.fetch === 'function') { - handler = mod.fetch; - style = 'worker'; - } else if (typeof mod.handler === 'function') { - handler = mod.handler; - style = 'lambda'; - } else if (typeof mod.lambda_handler === 'function') { - handler = mod.lambda_handler; - style = 'lambda'; - } else if (typeof mod.main === 'function') { - handler = mod.main; - style = 'lambda'; - } else if (typeof mod.default === 'function') { - handler = mod.default; - style = 'plain'; - } -} - -if (!handler) { - process.stderr.write( - `Module at ${handlerPath} does not export a usable handler. ` + - `Expected one of: handler, lambda_handler, main, fetch, default, ` + - `or a default function export.\n` - ); - process.exit(1); -} - -// ── Helpers to bridge calling conventions ────────────────────────────── - -function buildLambdaContext(event) { - const hdrs = (event && event.headers) || {}; - return { - functionName: process.env.ORVA_FUNCTION_NAME || '', - awsRequestId: hdrs['x-orva-execution-id'] || '', - invokedFunctionArn: '', - memoryLimitInMB: process.env.ORVA_MEMORY_MB || '', - logGroupName: 'orva', - logStreamName: hdrs['x-orva-execution-id'] || '', - getRemainingTimeInMillis: () => Number(process.env.ORVA_TIMEOUT_MS || 30000), - }; -} - -// Minimal Request/Response polyfills for Cloudflare Worker style. If the -// runtime provides them natively (Node 18+ has global fetch), prefer those. -function buildWorkerRequest(event) { - const url = `http://localhost${event.path || '/'}`; - if (typeof Request === 'function') { - try { - return new Request(url, { - method: event.method || 'GET', - headers: event.headers || {}, - body: ['GET', 'HEAD'].includes((event.method || 'GET').toUpperCase()) - ? undefined - : (event.body || ''), - }); - } catch { /* fall through to shim */ } - } - return { - method: event.method || 'GET', - url, - headers: new Map(Object.entries(event.headers || {})), - body: event.body || '', - async text() { return this.body; }, - async json() { return JSON.parse(this.body || '{}'); }, - async arrayBuffer() { return Buffer.from(this.body || '').buffer; }, - }; -} - -async function normaliseResponse(ret) { - // Already in Orva envelope. - if (ret && typeof ret === 'object' && 'statusCode' in ret) { - return { - statusCode: ret.statusCode || 200, - headers: ret.headers || { 'Content-Type': 'application/json' }, - body: typeof ret.body === 'string' ? ret.body : JSON.stringify(ret.body || {}), - }; - } - // Fetch API Response. - if (ret && typeof ret === 'object' && typeof ret.status === 'number' && typeof ret.text === 'function') { - const body = await ret.text(); - const headers = {}; - if (ret.headers && typeof ret.headers.forEach === 'function') { - ret.headers.forEach((v, k) => { headers[k] = v; }); - } - return { statusCode: ret.status, headers, body }; - } - // Anything else → JSON-encode as the body. - return { - statusCode: 200, - headers: { 'Content-Type': 'application/json' }, - body: typeof ret === 'string' ? ret : JSON.stringify(ret ?? null), - }; -} - -// Vercel/Next-style (req, res) detection: wrap to capture the response. -function invokeVercelStyle(fn, event) { - return new Promise((resolve, reject) => { - // Parse query string out of event.path. Vercel/Next handlers - // expect req.query to be populated; previously hardcoded {} so any - // ?k=v on the URL was invisible to the handler. URLSearchParams - // gives us decode + multi-value handling for free. - const rawPath = event.path || '/'; - const qIdx = rawPath.indexOf('?'); - const query = {}; - if (qIdx >= 0) { - for (const [k, v] of new URLSearchParams(rawPath.slice(qIdx + 1))) { - query[k] = v; - } - } - const req = { - method: event.method || 'GET', - url: rawPath, - headers: event.headers || {}, - body: (() => { - try { return JSON.parse(event.body || 'null'); } catch { return event.body; } - })(), - query, - }; - let statusCode = 200; - const headers = {}; - let body = ''; - const res = { - status(c) { statusCode = c; return this; }, - setHeader(k, v) { headers[k] = v; return this; }, - getHeader(k) { return headers[k]; }, - json(o) { headers['Content-Type'] = 'application/json'; body = JSON.stringify(o); this.end(); }, - send(x) { body = typeof x === 'string' ? x : JSON.stringify(x); this.end(); }, - write(x) { body += typeof x === 'string' ? x : String(x); }, - end(x) { - if (x !== undefined) body = typeof x === 'string' ? x : String(x); - resolve({ statusCode, headers, body }); - }, - }; - Promise.resolve() - .then(() => fn(req, res)) - .catch(reject); - }); -} - -// ── Framed stdio protocol ────────────────────────────────────────────── -// Wire format: 4-byte big-endian uint32 length, then N bytes of UTF-8 JSON. -// Applied symmetrically to stdin (proxy → adapter) and stdout (adapter → -// proxy). Length-prefix chosen over JSONL because it is binary-safe and -// handles the full 6 MB MaxBodyBytes without escape gymnastics. - -async function dispatch(event) { - if (style === 'worker') { - const req = buildWorkerRequest(event); - const env = { ...process.env }; - const ctx = { waitUntil: () => {}, passThroughOnException: () => {} }; - return await handler(req, env, ctx); - } - if (style === 'vercel-or-lambda') { - // Two-arg function: try Lambda first (returns a value). If it returns - // undefined OR throws a TypeError trying to treat ctx as `res`, assume - // Vercel (req, res) style and replay via invokeVercelStyle. - const ctx = buildLambdaContext(event); - let lambdaResult; - let lambdaErr; - try { - lambdaResult = await handler(event, ctx); - } catch (e) { - lambdaErr = e; - } - const looksLikeVercelMiss = - lambdaErr && - (lambdaErr instanceof TypeError) && - /is not a function|Cannot read propert/i.test(String(lambdaErr.message)); - if (lambdaResult === undefined || looksLikeVercelMiss) { - return await invokeVercelStyle(handler, event); - } - if (lambdaErr) throw lambdaErr; - return lambdaResult; - } - if (style === 'lambda') return await handler(event, buildLambdaContext(event)); - return await handler(event); -} - -function writeFrame(obj) { - const body = Buffer.from(JSON.stringify(obj), 'utf-8'); - const hdr = Buffer.alloc(4); - hdr.writeUInt32BE(body.length, 0); - // Node's process.stdout.write returns false if the kernel pipe buffer - // is full (backpressure from the proxy). We ignore the return — the - // next write blocks naturally until drain. EPIPE is rethrown to the - // caller so streaming loops can stop iterating on client disconnect. - originalStdoutWrite(hdr); - originalStdoutWrite(body); -} - -// v0.4 C1: streaming helpers — translate user-yielded values into -// `chunk` frames. data may be Buffer | string | Uint8Array | object. -function streamChunk(data) { - let buf; - if (data == null) { - buf = Buffer.alloc(0); - } else if (Buffer.isBuffer(data)) { - buf = data; - } else if (data instanceof Uint8Array) { - buf = Buffer.from(data.buffer, data.byteOffset, data.byteLength); - } else if (typeof data === 'string') { - buf = Buffer.from(data, 'utf-8'); - } else { - buf = Buffer.from(JSON.stringify(data), 'utf-8'); - } - writeFrame({ type: 'chunk', data: buf.length === 0 ? '' : buf.toString('base64') }); -} - -function looksLikeHead(item) { - if (!item || typeof item !== 'object') return null; - if (!('statusCode' in item)) return null; - return { - status: item.statusCode || 200, - headers: item.headers || { 'Content-Type': 'text/plain' }, - body: 'body' in item ? item.body : null, - }; -} - -// streamIterable consumes any sync/async iterable and emits the -// streaming protocol exchange. When streamingEnabled is false we -// buffer everything into a single response frame for back-compat — -// operators flipping the system_config flag get pre-C1 behaviour -// without redeploying. -async function streamIterable(iterable, streamingEnabled, keepaliveMs) { - if (!streamingEnabled) { - let head = null; - const parts = []; - const collect = (item) => { - if (head === null) { - const detected = looksLikeHead(item); - if (detected) { - head = { status: detected.status, headers: detected.headers }; - if (detected.body != null) parts.push(typeof detected.body === 'string' ? detected.body : String(detected.body)); - return; - } - head = { status: 200, headers: { 'Content-Type': 'text/plain' } }; - } - if (Buffer.isBuffer(item)) parts.push(item.toString('utf-8')); - else if (item instanceof Uint8Array) parts.push(Buffer.from(item).toString('utf-8')); - else if (typeof item === 'string') parts.push(item); - else parts.push(String(item)); - }; - for await (const item of iterable) collect(item); - if (head === null) head = { status: 200, headers: { 'Content-Type': 'text/plain' } }; - writeFrame({ - type: 'response', statusCode: head.status, - headers: head.headers, body: parts.join(''), - }); - return; - } - - let headSent = false; - let lastEmit = Date.now(); - let hbTimer = null; - - const sendHead = (status, headers) => { - if (headSent) return; - writeFrame({ type: 'response_start', statusCode: status, headers }); - headSent = true; - }; - - const startHeartbeat = () => { - if (hbTimer) return; - // setInterval fires regardless of how busy the loop is. The check - // against lastEmit prevents an empty chunk from racing a real one; - // worst case we emit one extra empty chunk per period, which is - // harmless on the wire. - hbTimer = setInterval(() => { - if (Date.now() - lastEmit >= keepaliveMs) { - try { - writeFrame({ type: 'chunk', data: '' }); - lastEmit = Date.now(); - } catch { /* pipe closed — clearInterval below */ } - } - }, keepaliveMs); - }; - - try { - for await (const item of iterable) { - if (!headSent) { - const detected = looksLikeHead(item); - if (detected) { - sendHead(detected.status, detected.headers); - startHeartbeat(); - if (detected.body != null && detected.body !== '') { - streamChunk(detected.body); - lastEmit = Date.now(); - } - continue; - } - sendHead(200, { 'Content-Type': 'text/plain; charset=utf-8' }); - startHeartbeat(); - } - streamChunk(item); - lastEmit = Date.now(); - } - if (!headSent) sendHead(200, { 'Content-Type': 'text/plain; charset=utf-8' }); - writeFrame({ type: 'response_end' }); - } catch (err) { - // EPIPE = client disconnected mid-stream. Stop iterating; the - // worker continues serving subsequent requests if its stdin is - // still open. Anything else we re-raise so the outer try/catch - // emits an error frame BEFORE the head went out. - if (err && err.code !== 'EPIPE') { - if (!headSent) throw err; - // Head already flew — nothing useful to surface. Best-effort end. - try { writeFrame({ type: 'response_end' }); } catch {} - } - } finally { - if (hbTimer) clearInterval(hbTimer); - } -} - -// drainReadableStream yields chunks from a fetch-API ReadableStream -// (e.g. `new Response(stream).body`). Used when the handler returns a -// Response whose body is a stream — we surface it as if the user had -// written an async generator yielding bytes. -async function* drainReadableStream(readable) { - const reader = readable.getReader(); - try { - for (;;) { - const { done, value } = await reader.read(); - if (done) return; - if (value) yield value; - } - } finally { - try { reader.releaseLock(); } catch {} - } -} - -// readExactly reads exactly n bytes from process.stdin, resolving with null -// on EOF. Works by concatenating the available readable chunks. -function readExactly(n) { - return new Promise((resolve, reject) => { - const out = []; - let have = 0; - const stdin = process.stdin; - - const onReadable = () => { - let chunk; - while ((chunk = stdin.read(Math.min(n - have, 65536))) !== null) { - out.push(chunk); - have += chunk.length; - if (have >= n) { - cleanup(); - return resolve(Buffer.concat(out, have)); - } - } - }; - const onEnd = () => { cleanup(); resolve(null); }; - const onError = (err) => { cleanup(); reject(err); }; - const cleanup = () => { - stdin.removeListener('readable', onReadable); - stdin.removeListener('end', onEnd); - stdin.removeListener('error', onError); - }; - - stdin.on('readable', onReadable); - stdin.on('end', onEnd); - stdin.on('error', onError); - // In case data is already buffered. - onReadable(); - }); -} - -async function readFrame() { - const header = await readExactly(4); - if (!header) return null; - const len = header.readUInt32BE(0); - if (len === 0) return {}; - const payload = await readExactly(len); - if (!payload) return null; - try { - return JSON.parse(payload.toString('utf-8')); - } catch (err) { - return { type: 'request', event: { method: 'POST', path: '/', headers: {}, body: '' } }; - } -} - -(async () => { - // Optional recycle cap: after MAX_REQUESTS dispatches, exit so the pool - // respawns and we avoid any slow memory creep in user code. - const maxReqs = Number(process.env.ORVA_MAX_REQUESTS || 0); - let served = 0; - - for (;;) { - const frame = await readFrame(); - if (!frame) process.exit(0); // stdin EOF = clean shutdown - if (frame.type === 'quit') { - writeFrame({ type: 'bye' }); - process.exit(0); - } - if (frame.type !== 'request') continue; - - const event = frame.event || { method: 'POST', path: '/', headers: {}, body: '' }; - // Enrich event.query from event.path. The proxy now passes the raw - // path including ?foo=bar; both Lambda-style handlers (event.query) - // and Vercel/Worker-style handlers (req.query) expect this to be - // already parsed, so we do it once here at the frame boundary. - { - const _p = event.path || '/'; - const _qIdx = _p.indexOf('?'); - const _q = {}; - if (_qIdx >= 0) { - for (const [k, v] of new URLSearchParams(_p.slice(_qIdx + 1))) { - _q[k] = v; - } - } - event.query = _q; - } - // Propagate call depth into env so orva.invoke()'s SDK can forward - // it on outbound nested calls. Otherwise the host's depth guard - // never trips on recursion. - const _hdrs = event.headers || {}; - const _depth = _hdrs['x-orva-call-depth'] || _hdrs['X-Orva-Call-Depth'] || ''; - if (_depth) process.env.ORVA_CALL_DEPTH = _depth; - else delete process.env.ORVA_CALL_DEPTH; - - // v0.5 trace context. Each event carries the trace_id + span_id of - // this invocation; the SDK reads them from env when issuing nested - // F2F calls or job enqueues so causal chains stay linked. - const _tID = _hdrs['x-orva-trace-id'] || _hdrs['X-Orva-Trace-Id'] || ''; - const _sID = _hdrs['x-orva-span-id'] || _hdrs['X-Orva-Span-Id'] || ''; - if (_tID) process.env.ORVA_TRACE_ID = _tID; else delete process.env.ORVA_TRACE_ID; - if (_sID) process.env.ORVA_SPAN_ID = _sID; else delete process.env.ORVA_SPAN_ID; - - // v0.6 SDK: trace.span() / log.* need the execution id. - const _eID = _hdrs['x-orva-execution-id'] || _hdrs['X-Orva-Execution-Id'] || ''; - if (_eID) process.env.ORVA_EXECUTION_ID = _eID; - else delete process.env.ORVA_EXECUTION_ID; - - // v0.4 C1: streaming flag + heartbeat interval ride on per-request - // headers so the proxy can flip them at runtime without redeploying - // the worker. Defaults match the system_config seed values. - const streamingOn = (_hdrs['x-orva-streaming-enabled'] ?? '1') !== '0'; - const keepaliveS = Math.max(1, Number(_hdrs['x-orva-stream-keepalive-seconds'] ?? '15') || 15); - const keepaliveMs = keepaliveS * 1000; - - try { - const result = await dispatch(event); - - // Streaming detection — async iterables, sync iterables, or a - // Response whose body is a ReadableStream. Order matters: - // Response objects are also iterable (ReadableStream is async- - // iterable in Node 18+) so we MUST check the Response branch - // first to extract status + headers, then drain the body stream. - if ( - result && - typeof result === 'object' && - typeof result.status === 'number' && - result.body && typeof result.body.getReader === 'function' - ) { - const headers = {}; - if (result.headers && typeof result.headers.forEach === 'function') { - result.headers.forEach((v, k) => { headers[k] = v; }); - } - // Wrap in a generator that prepends the head, then yields chunks. - async function* withHead() { - yield { statusCode: result.status, headers, body: '' }; - for await (const chunk of drainReadableStream(result.body)) yield chunk; - } - await streamIterable(withHead(), streamingOn, keepaliveMs); - } else if ( - result != null && - typeof result === 'object' && - (typeof result[Symbol.asyncIterator] === 'function' || - (typeof result[Symbol.iterator] === 'function' && typeof result !== 'string')) - ) { - // Exclude strings, Buffers, and arrays from being treated as - // streaming iterables — those are perfectly valid response - // bodies and shouldn't surprise the user. - const isExcluded = - typeof result === 'string' || - Buffer.isBuffer(result) || - Array.isArray(result) || - result instanceof Uint8Array; - if (isExcluded) { - const out = await normaliseResponse(result); - writeFrame({ type: 'response', statusCode: out.statusCode, headers: out.headers, body: out.body }); - } else { - await streamIterable(result, streamingOn, keepaliveMs); - } - } else { - const out = await normaliseResponse(result); - writeFrame({ type: 'response', statusCode: out.statusCode, headers: out.headers, body: out.body }); - } - } catch (err) { - process.stderr.write(`Handler error: ${err.stack || err.message}\n`); - // If we already started streaming the response head, the proxy is - // mid-loop and writeFrame on a fresh "response" envelope would - // confuse it. Best-effort: just close. The proxy will surface the - // truncation via the connection close. - try { - writeFrame({ - type: 'response', - statusCode: 500, - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ error: 'Internal function error', message: err.message }), - }); - } catch {} - } - - served++; - if (maxReqs > 0 && served >= maxReqs) { - writeFrame({ type: 'bye' }); - process.exit(0); - } - } -})().catch((err) => { - try { - writeFrame({ type: 'error', fatal: true, message: String(err && err.stack || err) }); - } catch {} - process.exit(1); -}); diff --git a/backend/runtimes/node22/orva.d.ts b/backend/runtimes/node22/orva.d.ts deleted file mode 100644 index 781c24b..0000000 --- a/backend/runtimes/node22/orva.d.ts +++ /dev/null @@ -1,189 +0,0 @@ -// Orva Node.js SDK — TypeScript declarations. -// Ships beside orva.js in the runtime bundle so TS handlers get full -// IntelliSense without an external @types package. - -export const SDK_VERSION: string - -export class OrvaError extends Error { - constructor(message: string, status?: number) - status: number -} - -export class OrvaUnavailableError extends OrvaError {} - -export class OrvaCASMismatch extends OrvaError { - currentValue: unknown -} - -export interface KVListEntry { - key: string - value: T - expires_at?: string -} - -export interface KVListResult { - keys: KVListEntry[] - nextCursor: string -} - -export interface KVPutEntry { - key: string - value: T - ttlSeconds?: number -} - -export interface KVOptions { - ttlSeconds?: number -} - -export interface KV { - get(key: string, defaultValue?: T | null): Promise - put(key: string, value: T, opts?: KVOptions): Promise - delete(key: string): Promise - list(opts?: { - prefix?: string - limit?: number - cursor?: string - }): Promise> - getMany(keys: string[]): Promise> - putMany(entries: KVPutEntry[]): Promise - deleteMany(keys: string[]): Promise - incr(key: string, delta?: number, opts?: KVOptions): Promise - cas( - key: string, - expected: T | null, - next: T, - opts?: KVOptions - ): Promise -} - -export const kv: KV - -export interface InvokeEnvelope { - statusCode: number - headers: Record - body: T -} - -export interface InvokeOptions { - timeoutMs?: number -} - -export function invoke( - name: string, - payload?: Req, - opts?: InvokeOptions -): Promise> - -export function invokeStream( - name: string, - payload?: unknown, - opts?: InvokeOptions -): AsyncIterable - -export interface EnqueueOptions { - maxAttempts?: number - scheduledAt?: string - idempotencyKey?: string - idempotencyWindowSeconds?: number -} - -export interface EnqueueResult { - id: string - replayed: boolean -} - -export const jobs: { - enqueue( - name: string, - payload?: unknown, - opts?: EnqueueOptions - ): Promise -} - -export interface CronUpsertOptions { - payload?: unknown - timezone?: string - enabled?: boolean -} - -export interface CronUpsertResult { - id: string - function_id: string - name: string - schedule: string - timezone: string - enabled: boolean -} - -export const crons: { - upsert( - name: string, - schedule: string, - opts?: CronUpsertOptions - ): Promise -} - -export const trace: { - span( - name: string, - fn: () => Promise | T, - attrs?: Record - ): Promise -} - -export type LogFields = Record - -export const log: { - debug(msg: string, fields?: LogFields): void - info(msg: string, fields?: LogFields): void - warn(msg: string, fields?: LogFields): void - error(msg: string, fields?: LogFields): void -} - -export const secrets: { get(name: string): string | undefined } - -export type WebhookSource = 'github' | 'stripe' | 'slack' | 'hmac' | 'unknown' - -export interface ParsedWebhook { - verified: boolean - source: WebhookSource - eventType?: string - webhookId?: string - payload: T - headers: Record -} - -export const webhook: { - parse(event: { - headers: Record - body: string - }): ParsedWebhook -} - -export interface OrvaContext { - functionId: string - executionId: string - traceId: string - spanId: string - callDepth: number - timeoutMs: number - memoryMb: number - sdkVersion: string -} - -export const context: OrvaContext - -export interface TestImpl { - request?: ( - method: string, - path: string, - opts?: { - body?: unknown - headers?: Record - timeoutMs?: number - } - ) => Promise<{ status: number; body: string; headers?: any }> -} - -export function __test_mode__(impl: TestImpl | null): void diff --git a/backend/runtimes/node22/orva.js b/backend/runtimes/node22/orva.js deleted file mode 100644 index 7125460..0000000 --- a/backend/runtimes/node22/orva.js +++ /dev/null @@ -1,579 +0,0 @@ -// Orva Node.js SDK — kv, invoke, jobs, crons, trace, log, context. -// -// Routes through ORVA_API_BASE (loopback) using ORVA_INTERNAL_TOKEN that -// was injected at worker spawn. Both env vars must be present in -// production; absent in tests where the SDK throws OrvaUnavailableError -// (unless __test_mode__ has supplied an override implementation). -// -// One-file design: no build step, no deps beyond the Node standard -// library. The shape mirrors the Python SDK byte-for-byte on the wire so -// parity tests can deploy the same payload across both runtimes. - -'use strict' - -// SDK version baked at adapter-embed time. Bumped in lockstep with the -// server. The string is sent on every internal-token call so operators -// can see drift in deployment logs. -const SDK_VERSION = '0.6.0' - -const COMMON_HEADERS = { 'Content-Type': 'application/json' } - -// ── Errors ────────────────────────────────────────────────────────── - -class OrvaError extends Error { - constructor(message, status = 0) { - super(message) - this.name = 'OrvaError' - this.status = status - } -} - -class OrvaUnavailableError extends OrvaError { - constructor(message) { - super(message, 0) - this.name = 'OrvaUnavailableError' - } -} - -class OrvaCASMismatch extends OrvaError { - constructor(currentValue) { - super('kv.cas: precondition failed', 409) - this.name = 'OrvaCASMismatch' - this.currentValue = currentValue - } -} - -// ── Test-mode hook ────────────────────────────────────────────────── -// -// __test_mode__ swaps the entire internal request implementation. Used -// by user tests that exercise their handler without standing up Orva. -// Pass `null` to restore the real transport. -let _testImpl = null -function __test_mode__(impl) { - _testImpl = impl -} - -// ── Environment accessors ─────────────────────────────────────────── - -const _apiBase = () => process.env.ORVA_API_BASE || '' -const _token = () => process.env.ORVA_INTERNAL_TOKEN || '' -const _fnID = () => process.env.ORVA_FUNCTION_ID || '' -const _execID = () => process.env.ORVA_EXECUTION_ID || '' -const _traceID = () => process.env.ORVA_TRACE_ID || '' -const _spanID = () => process.env.ORVA_SPAN_ID || '' -const _callDepth = () => parseInt(process.env.ORVA_CALL_DEPTH || '0', 10) || 0 -const _timeoutMs = () => parseInt(process.env.ORVA_TIMEOUT_MS || '30000', 10) || 30000 -const _memoryMb = () => parseInt(process.env.ORVA_MEMORY_MB || '64', 10) || 64 - -function _traceHeaders() { - const h = {} - const trace = _traceID() - const span = _spanID() - const fn = _fnID() - const exec = _execID() - if (trace) h['X-Orva-Trace-Id'] = trace - if (span) h['X-Orva-Span-Id'] = span - if (fn) h['X-Orva-Caller-Function'] = fn - if (fn) h['X-Orva-Function-Id'] = fn - if (exec) h['X-Orva-Execution-Id'] = exec - h['X-Orva-SDK-Version'] = SDK_VERSION - return h -} - -// ── HTTP transport ────────────────────────────────────────────────── - -const DEFAULT_TIMEOUT_MS = 30000 - -async function _request(method, path, opts = {}) { - if (_testImpl && _testImpl.request) { - return _testImpl.request(method, path, opts) - } - const base = _apiBase() - const token = _token() - if (!base || !token) { - throw new OrvaUnavailableError( - 'Orva SDK not available (missing ORVA_API_BASE or ORVA_INTERNAL_TOKEN)' - ) - } - const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS - const controller = - typeof AbortController === 'function' ? new AbortController() : null - const tid = controller - ? setTimeout(() => controller.abort(new Error('timeout')), timeoutMs) - : null - const init = { - method, - headers: { - 'X-Orva-Internal-Token': token, - ...COMMON_HEADERS, - ..._traceHeaders(), - ...(opts.headers || {}), - }, - signal: controller ? controller.signal : undefined, - } - if (opts.body != null) { - init.body = - typeof opts.body === 'string' ? opts.body : JSON.stringify(opts.body) - } - let res - try { - res = await fetch(base + path, init) - } catch (err) { - if (tid) clearTimeout(tid) - if (err && err.name === 'AbortError') { - throw new OrvaError(`request timed out after ${timeoutMs}ms`, 0) - } - throw new OrvaError(`request failed: ${err.message || err}`, 0) - } - if (tid) clearTimeout(tid) - const text = await res.text() - return { status: res.status, body: text, headers: res.headers } -} - -// ── KV ────────────────────────────────────────────────────────────── - -const kv = { - /** Read a JSON-decoded value. Returns defaultValue on missing/expired. */ - async get(key, defaultValue = null) { - const fn = _fnID() - const { status, body } = await _request( - 'GET', - `/api/v1/_kv/${fn}/${encodeURIComponent(key)}` - ) - if (status === 404) return defaultValue - if (status >= 400) throw new OrvaError(`kv.get(${key}) failed: ${body}`, status) - const data = JSON.parse(body) - return data.value != null ? data.value : defaultValue - }, - - /** Upsert a value. ttlSeconds=0 disables expiry. */ - async put(key, value, { ttlSeconds = 0 } = {}) { - const fn = _fnID() - const { status, body } = await _request( - 'PUT', - `/api/v1/_kv/${fn}/${encodeURIComponent(key)}`, - { body: { value, ttl_seconds: ttlSeconds | 0 } } - ) - if (status >= 400) throw new OrvaError(`kv.put(${key}) failed: ${body}`, status) - }, - - async delete(key) { - const fn = _fnID() - const { status, body } = await _request( - 'DELETE', - `/api/v1/_kv/${fn}/${encodeURIComponent(key)}` - ) - if (status >= 400 && status !== 404) { - throw new OrvaError(`kv.delete(${key}) failed: ${body}`, status) - } - }, - - /** - * List entries. Pass `cursor` to resume from a previous page; the - * response's nextCursor is the cursor for the page after this one - * (empty string when there are no more rows). - */ - async list({ prefix = '', limit = 100, cursor = '' } = {}) { - const fn = _fnID() - const qs = new URLSearchParams() - qs.set('limit', String(limit)) - if (prefix) qs.set('prefix', prefix) - if (cursor) qs.set('cursor', cursor) - const { status, body } = await _request('GET', `/api/v1/_kv/${fn}?${qs}`) - if (status >= 400) throw new OrvaError(`kv.list failed: ${body}`, status) - const data = JSON.parse(body) - return { - keys: data.keys || [], - nextCursor: data.next_cursor || '', - } - }, - - /** Read N keys in one round trip. Missing keys map to null. */ - async getMany(keys) { - if (!keys || keys.length === 0) return {} - const fn = _fnID() - const ops = keys.map((k) => ({ op: 'get', key: k })) - const { status, body } = await _request('POST', `/api/v1/_kv/${fn}/batch`, { - body: { ops }, - }) - if (status >= 400) throw new OrvaError(`kv.getMany failed: ${body}`, status) - const data = JSON.parse(body) - const out = {} - for (const r of data.results || []) { - out[r.key] = r.found ? r.value : null - } - return out - }, - - /** Write N entries in one transaction. */ - async putMany(entries) { - if (!entries || entries.length === 0) return - const fn = _fnID() - const ops = entries.map((e) => ({ - op: 'put', - key: e.key, - value: e.value, - ttl_seconds: (e.ttlSeconds | 0) || 0, - })) - const { status, body } = await _request('POST', `/api/v1/_kv/${fn}/batch`, { - body: { ops }, - }) - if (status >= 400) throw new OrvaError(`kv.putMany failed: ${body}`, status) - }, - - /** Delete N keys in one transaction. Returns the number removed. */ - async deleteMany(keys) { - if (!keys || keys.length === 0) return 0 - const fn = _fnID() - const ops = keys.map((k) => ({ op: 'delete', key: k })) - const { status, body } = await _request('POST', `/api/v1/_kv/${fn}/batch`, { - body: { ops }, - }) - if (status >= 400) throw new OrvaError(`kv.deleteMany failed: ${body}`, status) - const data = JSON.parse(body) - return (data.results || []).filter((r) => r.found).length - }, - - /** Atomic increment. Missing keys are treated as 0. Returns new value. */ - async incr(key, delta = 1, { ttlSeconds = 0 } = {}) { - const fn = _fnID() - const { status, body } = await _request( - 'POST', - `/api/v1/_kv/${fn}/${encodeURIComponent(key)}/incr`, - { body: { delta, ttl_seconds: ttlSeconds | 0 } } - ) - if (status >= 400) throw new OrvaError(`kv.incr(${key}) failed: ${body}`, status) - return JSON.parse(body).value - }, - - /** - * Compare-and-swap. `expected===null` means "key must not exist". - * Returns true on success; on mismatch, throws OrvaCASMismatch carrying - * the current value so callers can retry. - */ - async cas(key, expected, newValue, { ttlSeconds = 0 } = {}) { - const fn = _fnID() - const { status, body } = await _request( - 'POST', - `/api/v1/_kv/${fn}/${encodeURIComponent(key)}/cas`, - { - body: { - expected: expected === null ? null : expected, - new: newValue, - ttl_seconds: ttlSeconds | 0, - }, - } - ) - if (status >= 400) throw new OrvaError(`kv.cas(${key}) failed: ${body}`, status) - const data = JSON.parse(body) - if (!data.ok) throw new OrvaCASMismatch(data.current ?? null) - return true - }, -} - -// ── Function-to-function invoke ───────────────────────────────────── - -async function invoke(functionName, payload = {}, { timeoutMs = DEFAULT_TIMEOUT_MS } = {}) { - const headers = {} - const incoming = process.env.ORVA_CALL_DEPTH - if (incoming) headers['X-Orva-Call-Depth'] = incoming - - const { status, body } = await _request( - 'POST', - `/api/v1/_internal/invoke/${functionName}`, - { body: payload, headers, timeoutMs } - ) - - if (status === 404) throw new OrvaError(`function not found: ${functionName}`, 404) - if (status === 507) throw new OrvaError('call depth exceeded', 507) - if (status >= 400) throw new OrvaError(`invoke(${functionName}) failed: ${body}`, status) - - const env = JSON.parse(body) - if (typeof env.body === 'string') { - try { - env.body = JSON.parse(env.body) - } catch { - // leave as string - } - } - return env -} - -/** - * Streaming variant. Returns an async iterable of Uint8Array chunks. The - * server's chunked-transfer response is piped straight through; the - * inner statusCode arrives via the response's HTTP status, inner headers - * via X-Orva-Inner-* response headers (available on the iterable's - * `.headers` after the first chunk). - */ -function invokeStream(functionName, payload = {}, { timeoutMs = DEFAULT_TIMEOUT_MS } = {}) { - return { - [Symbol.asyncIterator]() { - let reader = null - let started = false - return { - async next() { - if (!started) { - started = true - const base = _apiBase() - const token = _token() - if (!base || !token) { - throw new OrvaUnavailableError( - 'Orva SDK not available (missing ORVA_API_BASE or ORVA_INTERNAL_TOKEN)' - ) - } - const controller = new AbortController() - const tid = setTimeout(() => controller.abort(new Error('timeout')), timeoutMs) - let res - try { - res = await fetch(`${base}/api/v1/_internal/invoke/${functionName}/stream`, { - method: 'POST', - headers: { - 'X-Orva-Internal-Token': token, - ...COMMON_HEADERS, - ..._traceHeaders(), - 'X-Orva-Call-Depth': String(_callDepth()), - }, - body: JSON.stringify(payload), - signal: controller.signal, - }) - } catch (err) { - clearTimeout(tid) - throw new OrvaError(`invokeStream(${functionName}) failed: ${err.message || err}`, 0) - } - clearTimeout(tid) - if (res.status === 404) throw new OrvaError(`function not found: ${functionName}`, 404) - if (res.status === 507) throw new OrvaError('call depth exceeded', 507) - if (res.status >= 400) { - const text = await res.text() - throw new OrvaError(`invokeStream(${functionName}) failed: ${text}`, res.status) - } - reader = res.body.getReader() - } - const { value, done } = await reader.read() - if (done) return { value: undefined, done: true } - return { value, done: false } - }, - async return() { - if (reader) await reader.cancel().catch(() => {}) - return { value: undefined, done: true } - }, - } - }, - } -} - -// ── Background jobs ───────────────────────────────────────────────── - -const jobs = { - async enqueue(functionName, payload = {}, opts = {}) { - const bodyObj = { - function_name: functionName, - payload, - max_attempts: (opts.maxAttempts | 0) || 3, - } - if (opts.scheduledAt) bodyObj.scheduled_at = opts.scheduledAt - if (opts.idempotencyKey) bodyObj.idempotency_key = opts.idempotencyKey - if (opts.idempotencyWindowSeconds) { - bodyObj.idempotency_window_seconds = opts.idempotencyWindowSeconds | 0 - } - - const { status, body, headers } = await _request('POST', '/api/v1/jobs', { body: bodyObj }) - if (status >= 400) throw new OrvaError(`jobs.enqueue failed: ${body}`, status) - const parsed = JSON.parse(body) - const replayed = - (headers && headers.get && headers.get('x-idempotency-replayed') === 'true') || - parsed.replayed === true - return { id: parsed.id, replayed } - }, -} - -// ── Cron-from-code ────────────────────────────────────────────────── - -const crons = { - /** - * Idempotent upsert of a cron schedule attached to this function. - * Identifies the row by (function_id, name) — subsequent calls with - * the same name update the existing schedule in place. - */ - async upsert(name, schedule, opts = {}) { - const bodyObj = { name, schedule } - if (opts.payload !== undefined) bodyObj.payload = opts.payload - if (opts.timezone) bodyObj.timezone = opts.timezone - if (opts.enabled !== undefined) bodyObj.enabled = !!opts.enabled - const { status, body } = await _request('POST', '/api/v1/_internal/crons', { - body: bodyObj, - }) - if (status >= 400) throw new OrvaError(`crons.upsert(${name}) failed: ${body}`, status) - return JSON.parse(body) - }, -} - -// ── User-defined spans ────────────────────────────────────────────── - -const trace = { - /** - * Wrap `fn` in a child span. The span's duration is the wall-clock - * time of the awaited fn. Errors are recorded with status="error" and - * rethrown unchanged so callers can choose their own handling. - */ - async span(name, fn, attrs = undefined) { - const startedAt = new Date() - const t0 = Date.now() - let ok = true - let errMsg = '' - try { - return await fn() - } catch (e) { - ok = false - errMsg = e && e.message ? e.message : String(e) - throw e - } finally { - const durationMs = Date.now() - t0 - // Fire-and-forget — never block the user's code waiting on span - // ingestion. We deliberately swallow errors so a flaky loopback - // never breaks the handler. - _request('POST', '/api/v1/_internal/spans', { - body: { - name, - started_at: startedAt.toISOString(), - duration_ms: durationMs, - status: ok ? 'ok' : 'error', - error_message: errMsg, - attributes: attrs, - }, - }).catch(() => {}) - } - }, -} - -// ── Structured logging ────────────────────────────────────────────── - -function _emitLog(level, msg, fields) { - const rec = { - ts: new Date().toISOString(), - level, - message: typeof msg === 'string' ? msg : JSON.stringify(msg), - } - if (fields && typeof fields === 'object') rec.fields = fields - const span = _spanID() - if (span) rec.span_id = span - // Magic-prefix line on stderr — parsed by the server in proxy.Forward. - // Wrapped in a try since process.stderr.write can fail mid-shutdown. - try { - process.stderr.write('__ORVA_LOG_JSON__' + JSON.stringify(rec) + '\n') - } catch { - // ignore - } -} - -const log = { - debug(msg, fields) { _emitLog('debug', msg, fields) }, - info(msg, fields) { _emitLog('info', msg, fields) }, - warn(msg, fields) { _emitLog('warn', msg, fields) }, - error(msg, fields) { _emitLog('error', msg, fields) }, -} - -// ── Secrets accessor ──────────────────────────────────────────────── -// -// Currently a thin wrapper over process.env. The indirection lets us add -// per-secret access auditing later without changing user code. - -const secrets = { - get(name) { - return process.env[name] - }, -} - -// ── Webhook helper ────────────────────────────────────────────────── -// -// Pure local parsing of the event Orva passes to a function whose entry -// is an inbound webhook trigger. The server has already verified the -// HMAC before calling the handler — verified is always true here unless -// the function is called directly without a webhook trigger. - -function _firstHeader(event, ...names) { - if (!event || !event.headers) return '' - for (const n of names) { - const v = - event.headers[n] ?? - event.headers[n.toLowerCase()] ?? - event.headers[n.toUpperCase()] - if (v) return v - } - return '' -} - -const webhook = { - parse(event) { - const headers = event && event.headers ? event.headers : {} - const trigger = _firstHeader(event, 'x-orva-trigger') - const webhookId = _firstHeader(event, 'x-orva-inbound-webhook-id') - let source = 'unknown' - let eventType = '' - if (_firstHeader(event, 'X-GitHub-Event')) { - source = 'github' - eventType = _firstHeader(event, 'X-GitHub-Event') - } else if (_firstHeader(event, 'Stripe-Signature')) { - source = 'stripe' - eventType = _firstHeader(event, 'Stripe-Event-Type') - } else if (_firstHeader(event, 'X-Slack-Signature')) { - source = 'slack' - } else if ( - _firstHeader(event, 'X-Hub-Signature-256') || - _firstHeader(event, 'X-Signature') - ) { - source = 'hmac' - } - let payload = event && event.body !== undefined ? event.body : null - if (typeof payload === 'string' && payload.length > 0) { - try { - payload = JSON.parse(payload) - } catch { - // leave as string - } - } - return { - verified: trigger === 'inbound_webhook', - source, - eventType, - webhookId, - payload, - headers, - } - }, -} - -// ── Context (env-var snapshot exposed as an object) ───────────────── -// -// Lazy getters so tests can mutate process.env at runtime without -// recomputing the SDK. - -const context = { - get functionId() { return _fnID() }, - get executionId() { return _execID() }, - get traceId() { return _traceID() }, - get spanId() { return _spanID() }, - get callDepth() { return _callDepth() }, - get timeoutMs() { return _timeoutMs() }, - get memoryMb() { return _memoryMb() }, - get sdkVersion() { return SDK_VERSION }, -} - -module.exports = { - kv, - invoke, - invokeStream, - jobs, - crons, - trace, - log, - secrets, - webhook, - context, - OrvaError, - OrvaUnavailableError, - OrvaCASMismatch, - __test_mode__, - SDK_VERSION, -} diff --git a/backend/runtimes/node22/package.json b/backend/runtimes/node22/package.json deleted file mode 100644 index 4c1d5af..0000000 --- a/backend/runtimes/node22/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "orva", - "version": "0.6.0", - "main": "./orva.js", - "types": "./orva.d.ts", - "private": true, - "description": "Orva runtime SDK (in-sandbox). Shipped with the Node 24 adapter — not published to npm." -} diff --git a/backend/runtimes/node24/orva.d.ts b/backend/runtimes/node24/orva.d.ts deleted file mode 100644 index 781c24b..0000000 --- a/backend/runtimes/node24/orva.d.ts +++ /dev/null @@ -1,189 +0,0 @@ -// Orva Node.js SDK — TypeScript declarations. -// Ships beside orva.js in the runtime bundle so TS handlers get full -// IntelliSense without an external @types package. - -export const SDK_VERSION: string - -export class OrvaError extends Error { - constructor(message: string, status?: number) - status: number -} - -export class OrvaUnavailableError extends OrvaError {} - -export class OrvaCASMismatch extends OrvaError { - currentValue: unknown -} - -export interface KVListEntry { - key: string - value: T - expires_at?: string -} - -export interface KVListResult { - keys: KVListEntry[] - nextCursor: string -} - -export interface KVPutEntry { - key: string - value: T - ttlSeconds?: number -} - -export interface KVOptions { - ttlSeconds?: number -} - -export interface KV { - get(key: string, defaultValue?: T | null): Promise - put(key: string, value: T, opts?: KVOptions): Promise - delete(key: string): Promise - list(opts?: { - prefix?: string - limit?: number - cursor?: string - }): Promise> - getMany(keys: string[]): Promise> - putMany(entries: KVPutEntry[]): Promise - deleteMany(keys: string[]): Promise - incr(key: string, delta?: number, opts?: KVOptions): Promise - cas( - key: string, - expected: T | null, - next: T, - opts?: KVOptions - ): Promise -} - -export const kv: KV - -export interface InvokeEnvelope { - statusCode: number - headers: Record - body: T -} - -export interface InvokeOptions { - timeoutMs?: number -} - -export function invoke( - name: string, - payload?: Req, - opts?: InvokeOptions -): Promise> - -export function invokeStream( - name: string, - payload?: unknown, - opts?: InvokeOptions -): AsyncIterable - -export interface EnqueueOptions { - maxAttempts?: number - scheduledAt?: string - idempotencyKey?: string - idempotencyWindowSeconds?: number -} - -export interface EnqueueResult { - id: string - replayed: boolean -} - -export const jobs: { - enqueue( - name: string, - payload?: unknown, - opts?: EnqueueOptions - ): Promise -} - -export interface CronUpsertOptions { - payload?: unknown - timezone?: string - enabled?: boolean -} - -export interface CronUpsertResult { - id: string - function_id: string - name: string - schedule: string - timezone: string - enabled: boolean -} - -export const crons: { - upsert( - name: string, - schedule: string, - opts?: CronUpsertOptions - ): Promise -} - -export const trace: { - span( - name: string, - fn: () => Promise | T, - attrs?: Record - ): Promise -} - -export type LogFields = Record - -export const log: { - debug(msg: string, fields?: LogFields): void - info(msg: string, fields?: LogFields): void - warn(msg: string, fields?: LogFields): void - error(msg: string, fields?: LogFields): void -} - -export const secrets: { get(name: string): string | undefined } - -export type WebhookSource = 'github' | 'stripe' | 'slack' | 'hmac' | 'unknown' - -export interface ParsedWebhook { - verified: boolean - source: WebhookSource - eventType?: string - webhookId?: string - payload: T - headers: Record -} - -export const webhook: { - parse(event: { - headers: Record - body: string - }): ParsedWebhook -} - -export interface OrvaContext { - functionId: string - executionId: string - traceId: string - spanId: string - callDepth: number - timeoutMs: number - memoryMb: number - sdkVersion: string -} - -export const context: OrvaContext - -export interface TestImpl { - request?: ( - method: string, - path: string, - opts?: { - body?: unknown - headers?: Record - timeoutMs?: number - } - ) => Promise<{ status: number; body: string; headers?: any }> -} - -export function __test_mode__(impl: TestImpl | null): void diff --git a/backend/runtimes/node24/orva.js b/backend/runtimes/node24/orva.js deleted file mode 100644 index 7125460..0000000 --- a/backend/runtimes/node24/orva.js +++ /dev/null @@ -1,579 +0,0 @@ -// Orva Node.js SDK — kv, invoke, jobs, crons, trace, log, context. -// -// Routes through ORVA_API_BASE (loopback) using ORVA_INTERNAL_TOKEN that -// was injected at worker spawn. Both env vars must be present in -// production; absent in tests where the SDK throws OrvaUnavailableError -// (unless __test_mode__ has supplied an override implementation). -// -// One-file design: no build step, no deps beyond the Node standard -// library. The shape mirrors the Python SDK byte-for-byte on the wire so -// parity tests can deploy the same payload across both runtimes. - -'use strict' - -// SDK version baked at adapter-embed time. Bumped in lockstep with the -// server. The string is sent on every internal-token call so operators -// can see drift in deployment logs. -const SDK_VERSION = '0.6.0' - -const COMMON_HEADERS = { 'Content-Type': 'application/json' } - -// ── Errors ────────────────────────────────────────────────────────── - -class OrvaError extends Error { - constructor(message, status = 0) { - super(message) - this.name = 'OrvaError' - this.status = status - } -} - -class OrvaUnavailableError extends OrvaError { - constructor(message) { - super(message, 0) - this.name = 'OrvaUnavailableError' - } -} - -class OrvaCASMismatch extends OrvaError { - constructor(currentValue) { - super('kv.cas: precondition failed', 409) - this.name = 'OrvaCASMismatch' - this.currentValue = currentValue - } -} - -// ── Test-mode hook ────────────────────────────────────────────────── -// -// __test_mode__ swaps the entire internal request implementation. Used -// by user tests that exercise their handler without standing up Orva. -// Pass `null` to restore the real transport. -let _testImpl = null -function __test_mode__(impl) { - _testImpl = impl -} - -// ── Environment accessors ─────────────────────────────────────────── - -const _apiBase = () => process.env.ORVA_API_BASE || '' -const _token = () => process.env.ORVA_INTERNAL_TOKEN || '' -const _fnID = () => process.env.ORVA_FUNCTION_ID || '' -const _execID = () => process.env.ORVA_EXECUTION_ID || '' -const _traceID = () => process.env.ORVA_TRACE_ID || '' -const _spanID = () => process.env.ORVA_SPAN_ID || '' -const _callDepth = () => parseInt(process.env.ORVA_CALL_DEPTH || '0', 10) || 0 -const _timeoutMs = () => parseInt(process.env.ORVA_TIMEOUT_MS || '30000', 10) || 30000 -const _memoryMb = () => parseInt(process.env.ORVA_MEMORY_MB || '64', 10) || 64 - -function _traceHeaders() { - const h = {} - const trace = _traceID() - const span = _spanID() - const fn = _fnID() - const exec = _execID() - if (trace) h['X-Orva-Trace-Id'] = trace - if (span) h['X-Orva-Span-Id'] = span - if (fn) h['X-Orva-Caller-Function'] = fn - if (fn) h['X-Orva-Function-Id'] = fn - if (exec) h['X-Orva-Execution-Id'] = exec - h['X-Orva-SDK-Version'] = SDK_VERSION - return h -} - -// ── HTTP transport ────────────────────────────────────────────────── - -const DEFAULT_TIMEOUT_MS = 30000 - -async function _request(method, path, opts = {}) { - if (_testImpl && _testImpl.request) { - return _testImpl.request(method, path, opts) - } - const base = _apiBase() - const token = _token() - if (!base || !token) { - throw new OrvaUnavailableError( - 'Orva SDK not available (missing ORVA_API_BASE or ORVA_INTERNAL_TOKEN)' - ) - } - const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS - const controller = - typeof AbortController === 'function' ? new AbortController() : null - const tid = controller - ? setTimeout(() => controller.abort(new Error('timeout')), timeoutMs) - : null - const init = { - method, - headers: { - 'X-Orva-Internal-Token': token, - ...COMMON_HEADERS, - ..._traceHeaders(), - ...(opts.headers || {}), - }, - signal: controller ? controller.signal : undefined, - } - if (opts.body != null) { - init.body = - typeof opts.body === 'string' ? opts.body : JSON.stringify(opts.body) - } - let res - try { - res = await fetch(base + path, init) - } catch (err) { - if (tid) clearTimeout(tid) - if (err && err.name === 'AbortError') { - throw new OrvaError(`request timed out after ${timeoutMs}ms`, 0) - } - throw new OrvaError(`request failed: ${err.message || err}`, 0) - } - if (tid) clearTimeout(tid) - const text = await res.text() - return { status: res.status, body: text, headers: res.headers } -} - -// ── KV ────────────────────────────────────────────────────────────── - -const kv = { - /** Read a JSON-decoded value. Returns defaultValue on missing/expired. */ - async get(key, defaultValue = null) { - const fn = _fnID() - const { status, body } = await _request( - 'GET', - `/api/v1/_kv/${fn}/${encodeURIComponent(key)}` - ) - if (status === 404) return defaultValue - if (status >= 400) throw new OrvaError(`kv.get(${key}) failed: ${body}`, status) - const data = JSON.parse(body) - return data.value != null ? data.value : defaultValue - }, - - /** Upsert a value. ttlSeconds=0 disables expiry. */ - async put(key, value, { ttlSeconds = 0 } = {}) { - const fn = _fnID() - const { status, body } = await _request( - 'PUT', - `/api/v1/_kv/${fn}/${encodeURIComponent(key)}`, - { body: { value, ttl_seconds: ttlSeconds | 0 } } - ) - if (status >= 400) throw new OrvaError(`kv.put(${key}) failed: ${body}`, status) - }, - - async delete(key) { - const fn = _fnID() - const { status, body } = await _request( - 'DELETE', - `/api/v1/_kv/${fn}/${encodeURIComponent(key)}` - ) - if (status >= 400 && status !== 404) { - throw new OrvaError(`kv.delete(${key}) failed: ${body}`, status) - } - }, - - /** - * List entries. Pass `cursor` to resume from a previous page; the - * response's nextCursor is the cursor for the page after this one - * (empty string when there are no more rows). - */ - async list({ prefix = '', limit = 100, cursor = '' } = {}) { - const fn = _fnID() - const qs = new URLSearchParams() - qs.set('limit', String(limit)) - if (prefix) qs.set('prefix', prefix) - if (cursor) qs.set('cursor', cursor) - const { status, body } = await _request('GET', `/api/v1/_kv/${fn}?${qs}`) - if (status >= 400) throw new OrvaError(`kv.list failed: ${body}`, status) - const data = JSON.parse(body) - return { - keys: data.keys || [], - nextCursor: data.next_cursor || '', - } - }, - - /** Read N keys in one round trip. Missing keys map to null. */ - async getMany(keys) { - if (!keys || keys.length === 0) return {} - const fn = _fnID() - const ops = keys.map((k) => ({ op: 'get', key: k })) - const { status, body } = await _request('POST', `/api/v1/_kv/${fn}/batch`, { - body: { ops }, - }) - if (status >= 400) throw new OrvaError(`kv.getMany failed: ${body}`, status) - const data = JSON.parse(body) - const out = {} - for (const r of data.results || []) { - out[r.key] = r.found ? r.value : null - } - return out - }, - - /** Write N entries in one transaction. */ - async putMany(entries) { - if (!entries || entries.length === 0) return - const fn = _fnID() - const ops = entries.map((e) => ({ - op: 'put', - key: e.key, - value: e.value, - ttl_seconds: (e.ttlSeconds | 0) || 0, - })) - const { status, body } = await _request('POST', `/api/v1/_kv/${fn}/batch`, { - body: { ops }, - }) - if (status >= 400) throw new OrvaError(`kv.putMany failed: ${body}`, status) - }, - - /** Delete N keys in one transaction. Returns the number removed. */ - async deleteMany(keys) { - if (!keys || keys.length === 0) return 0 - const fn = _fnID() - const ops = keys.map((k) => ({ op: 'delete', key: k })) - const { status, body } = await _request('POST', `/api/v1/_kv/${fn}/batch`, { - body: { ops }, - }) - if (status >= 400) throw new OrvaError(`kv.deleteMany failed: ${body}`, status) - const data = JSON.parse(body) - return (data.results || []).filter((r) => r.found).length - }, - - /** Atomic increment. Missing keys are treated as 0. Returns new value. */ - async incr(key, delta = 1, { ttlSeconds = 0 } = {}) { - const fn = _fnID() - const { status, body } = await _request( - 'POST', - `/api/v1/_kv/${fn}/${encodeURIComponent(key)}/incr`, - { body: { delta, ttl_seconds: ttlSeconds | 0 } } - ) - if (status >= 400) throw new OrvaError(`kv.incr(${key}) failed: ${body}`, status) - return JSON.parse(body).value - }, - - /** - * Compare-and-swap. `expected===null` means "key must not exist". - * Returns true on success; on mismatch, throws OrvaCASMismatch carrying - * the current value so callers can retry. - */ - async cas(key, expected, newValue, { ttlSeconds = 0 } = {}) { - const fn = _fnID() - const { status, body } = await _request( - 'POST', - `/api/v1/_kv/${fn}/${encodeURIComponent(key)}/cas`, - { - body: { - expected: expected === null ? null : expected, - new: newValue, - ttl_seconds: ttlSeconds | 0, - }, - } - ) - if (status >= 400) throw new OrvaError(`kv.cas(${key}) failed: ${body}`, status) - const data = JSON.parse(body) - if (!data.ok) throw new OrvaCASMismatch(data.current ?? null) - return true - }, -} - -// ── Function-to-function invoke ───────────────────────────────────── - -async function invoke(functionName, payload = {}, { timeoutMs = DEFAULT_TIMEOUT_MS } = {}) { - const headers = {} - const incoming = process.env.ORVA_CALL_DEPTH - if (incoming) headers['X-Orva-Call-Depth'] = incoming - - const { status, body } = await _request( - 'POST', - `/api/v1/_internal/invoke/${functionName}`, - { body: payload, headers, timeoutMs } - ) - - if (status === 404) throw new OrvaError(`function not found: ${functionName}`, 404) - if (status === 507) throw new OrvaError('call depth exceeded', 507) - if (status >= 400) throw new OrvaError(`invoke(${functionName}) failed: ${body}`, status) - - const env = JSON.parse(body) - if (typeof env.body === 'string') { - try { - env.body = JSON.parse(env.body) - } catch { - // leave as string - } - } - return env -} - -/** - * Streaming variant. Returns an async iterable of Uint8Array chunks. The - * server's chunked-transfer response is piped straight through; the - * inner statusCode arrives via the response's HTTP status, inner headers - * via X-Orva-Inner-* response headers (available on the iterable's - * `.headers` after the first chunk). - */ -function invokeStream(functionName, payload = {}, { timeoutMs = DEFAULT_TIMEOUT_MS } = {}) { - return { - [Symbol.asyncIterator]() { - let reader = null - let started = false - return { - async next() { - if (!started) { - started = true - const base = _apiBase() - const token = _token() - if (!base || !token) { - throw new OrvaUnavailableError( - 'Orva SDK not available (missing ORVA_API_BASE or ORVA_INTERNAL_TOKEN)' - ) - } - const controller = new AbortController() - const tid = setTimeout(() => controller.abort(new Error('timeout')), timeoutMs) - let res - try { - res = await fetch(`${base}/api/v1/_internal/invoke/${functionName}/stream`, { - method: 'POST', - headers: { - 'X-Orva-Internal-Token': token, - ...COMMON_HEADERS, - ..._traceHeaders(), - 'X-Orva-Call-Depth': String(_callDepth()), - }, - body: JSON.stringify(payload), - signal: controller.signal, - }) - } catch (err) { - clearTimeout(tid) - throw new OrvaError(`invokeStream(${functionName}) failed: ${err.message || err}`, 0) - } - clearTimeout(tid) - if (res.status === 404) throw new OrvaError(`function not found: ${functionName}`, 404) - if (res.status === 507) throw new OrvaError('call depth exceeded', 507) - if (res.status >= 400) { - const text = await res.text() - throw new OrvaError(`invokeStream(${functionName}) failed: ${text}`, res.status) - } - reader = res.body.getReader() - } - const { value, done } = await reader.read() - if (done) return { value: undefined, done: true } - return { value, done: false } - }, - async return() { - if (reader) await reader.cancel().catch(() => {}) - return { value: undefined, done: true } - }, - } - }, - } -} - -// ── Background jobs ───────────────────────────────────────────────── - -const jobs = { - async enqueue(functionName, payload = {}, opts = {}) { - const bodyObj = { - function_name: functionName, - payload, - max_attempts: (opts.maxAttempts | 0) || 3, - } - if (opts.scheduledAt) bodyObj.scheduled_at = opts.scheduledAt - if (opts.idempotencyKey) bodyObj.idempotency_key = opts.idempotencyKey - if (opts.idempotencyWindowSeconds) { - bodyObj.idempotency_window_seconds = opts.idempotencyWindowSeconds | 0 - } - - const { status, body, headers } = await _request('POST', '/api/v1/jobs', { body: bodyObj }) - if (status >= 400) throw new OrvaError(`jobs.enqueue failed: ${body}`, status) - const parsed = JSON.parse(body) - const replayed = - (headers && headers.get && headers.get('x-idempotency-replayed') === 'true') || - parsed.replayed === true - return { id: parsed.id, replayed } - }, -} - -// ── Cron-from-code ────────────────────────────────────────────────── - -const crons = { - /** - * Idempotent upsert of a cron schedule attached to this function. - * Identifies the row by (function_id, name) — subsequent calls with - * the same name update the existing schedule in place. - */ - async upsert(name, schedule, opts = {}) { - const bodyObj = { name, schedule } - if (opts.payload !== undefined) bodyObj.payload = opts.payload - if (opts.timezone) bodyObj.timezone = opts.timezone - if (opts.enabled !== undefined) bodyObj.enabled = !!opts.enabled - const { status, body } = await _request('POST', '/api/v1/_internal/crons', { - body: bodyObj, - }) - if (status >= 400) throw new OrvaError(`crons.upsert(${name}) failed: ${body}`, status) - return JSON.parse(body) - }, -} - -// ── User-defined spans ────────────────────────────────────────────── - -const trace = { - /** - * Wrap `fn` in a child span. The span's duration is the wall-clock - * time of the awaited fn. Errors are recorded with status="error" and - * rethrown unchanged so callers can choose their own handling. - */ - async span(name, fn, attrs = undefined) { - const startedAt = new Date() - const t0 = Date.now() - let ok = true - let errMsg = '' - try { - return await fn() - } catch (e) { - ok = false - errMsg = e && e.message ? e.message : String(e) - throw e - } finally { - const durationMs = Date.now() - t0 - // Fire-and-forget — never block the user's code waiting on span - // ingestion. We deliberately swallow errors so a flaky loopback - // never breaks the handler. - _request('POST', '/api/v1/_internal/spans', { - body: { - name, - started_at: startedAt.toISOString(), - duration_ms: durationMs, - status: ok ? 'ok' : 'error', - error_message: errMsg, - attributes: attrs, - }, - }).catch(() => {}) - } - }, -} - -// ── Structured logging ────────────────────────────────────────────── - -function _emitLog(level, msg, fields) { - const rec = { - ts: new Date().toISOString(), - level, - message: typeof msg === 'string' ? msg : JSON.stringify(msg), - } - if (fields && typeof fields === 'object') rec.fields = fields - const span = _spanID() - if (span) rec.span_id = span - // Magic-prefix line on stderr — parsed by the server in proxy.Forward. - // Wrapped in a try since process.stderr.write can fail mid-shutdown. - try { - process.stderr.write('__ORVA_LOG_JSON__' + JSON.stringify(rec) + '\n') - } catch { - // ignore - } -} - -const log = { - debug(msg, fields) { _emitLog('debug', msg, fields) }, - info(msg, fields) { _emitLog('info', msg, fields) }, - warn(msg, fields) { _emitLog('warn', msg, fields) }, - error(msg, fields) { _emitLog('error', msg, fields) }, -} - -// ── Secrets accessor ──────────────────────────────────────────────── -// -// Currently a thin wrapper over process.env. The indirection lets us add -// per-secret access auditing later without changing user code. - -const secrets = { - get(name) { - return process.env[name] - }, -} - -// ── Webhook helper ────────────────────────────────────────────────── -// -// Pure local parsing of the event Orva passes to a function whose entry -// is an inbound webhook trigger. The server has already verified the -// HMAC before calling the handler — verified is always true here unless -// the function is called directly without a webhook trigger. - -function _firstHeader(event, ...names) { - if (!event || !event.headers) return '' - for (const n of names) { - const v = - event.headers[n] ?? - event.headers[n.toLowerCase()] ?? - event.headers[n.toUpperCase()] - if (v) return v - } - return '' -} - -const webhook = { - parse(event) { - const headers = event && event.headers ? event.headers : {} - const trigger = _firstHeader(event, 'x-orva-trigger') - const webhookId = _firstHeader(event, 'x-orva-inbound-webhook-id') - let source = 'unknown' - let eventType = '' - if (_firstHeader(event, 'X-GitHub-Event')) { - source = 'github' - eventType = _firstHeader(event, 'X-GitHub-Event') - } else if (_firstHeader(event, 'Stripe-Signature')) { - source = 'stripe' - eventType = _firstHeader(event, 'Stripe-Event-Type') - } else if (_firstHeader(event, 'X-Slack-Signature')) { - source = 'slack' - } else if ( - _firstHeader(event, 'X-Hub-Signature-256') || - _firstHeader(event, 'X-Signature') - ) { - source = 'hmac' - } - let payload = event && event.body !== undefined ? event.body : null - if (typeof payload === 'string' && payload.length > 0) { - try { - payload = JSON.parse(payload) - } catch { - // leave as string - } - } - return { - verified: trigger === 'inbound_webhook', - source, - eventType, - webhookId, - payload, - headers, - } - }, -} - -// ── Context (env-var snapshot exposed as an object) ───────────────── -// -// Lazy getters so tests can mutate process.env at runtime without -// recomputing the SDK. - -const context = { - get functionId() { return _fnID() }, - get executionId() { return _execID() }, - get traceId() { return _traceID() }, - get spanId() { return _spanID() }, - get callDepth() { return _callDepth() }, - get timeoutMs() { return _timeoutMs() }, - get memoryMb() { return _memoryMb() }, - get sdkVersion() { return SDK_VERSION }, -} - -module.exports = { - kv, - invoke, - invokeStream, - jobs, - crons, - trace, - log, - secrets, - webhook, - context, - OrvaError, - OrvaUnavailableError, - OrvaCASMismatch, - __test_mode__, - SDK_VERSION, -} diff --git a/backend/runtimes/node24/package.json b/backend/runtimes/node24/package.json deleted file mode 100644 index 4c1d5af..0000000 --- a/backend/runtimes/node24/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "orva", - "version": "0.6.0", - "main": "./orva.js", - "types": "./orva.d.ts", - "private": true, - "description": "Orva runtime SDK (in-sandbox). Shipped with the Node 24 adapter — not published to npm." -} diff --git a/backend/cmd/orva/adapters/python314/adapter.py b/backend/runtimes/python/adapter.py similarity index 99% rename from backend/cmd/orva/adapters/python314/adapter.py rename to backend/runtimes/python/adapter.py index 4abc472..8eb61d0 100644 --- a/backend/cmd/orva/adapters/python314/adapter.py +++ b/backend/runtimes/python/adapter.py @@ -648,7 +648,7 @@ def _dispatch_and_emit(event, streaming_enabled, keepalive_s): else: os.environ.pop("ORVA_CALL_DEPTH", None) - # v0.5 trace context — see python313 adapter for the rationale. + # v0.5 trace context — propagate the inbound trace/span ids to F2F calls. _tid = _hdrs.get("x-orva-trace-id") or _hdrs.get("X-Orva-Trace-Id") or "" _sid = _hdrs.get("x-orva-span-id") or _hdrs.get("X-Orva-Span-Id") or "" if _tid: os.environ["ORVA_TRACE_ID"] = _tid diff --git a/backend/cmd/orva/adapters/python314/orva.py b/backend/runtimes/python/orva.py similarity index 100% rename from backend/cmd/orva/adapters/python314/orva.py rename to backend/runtimes/python/orva.py diff --git a/backend/cmd/orva/adapters/python314/py.typed b/backend/runtimes/python/py.typed similarity index 100% rename from backend/cmd/orva/adapters/python314/py.typed rename to backend/runtimes/python/py.typed diff --git a/backend/runtimes/python313/adapter.py b/backend/runtimes/python313/adapter.py deleted file mode 100644 index e789c2a..0000000 --- a/backend/runtimes/python313/adapter.py +++ /dev/null @@ -1,707 +0,0 @@ -"""Orva Python adapter — universal handler loader. - -Accepts a wide range of conventions so existing code from AWS Lambda, -Google Cloud Functions, Azure, FastAPI, Flask, Django, Starlette, and -generic Python deployments runs with zero changes: - - AWS Lambda : def lambda_handler(event, context): ... - def handler(event, context): ... - GCP Functions : def main(request): ... (Flask Request) - Azure Functions : def main(req): ... (HttpRequest) - ASGI (FastAPI, - Starlette) : app = FastAPI() / app = Starlette() - WSGI (Flask, - Django) : app = Flask(__name__) / app = ... (WSGI callable) - Plain : def handler(event): ... - -Response normalisation: the adapter accepts dicts in the Orva envelope -({statusCode, headers, body}), Starlette/FastAPI Response objects, Flask -Response objects, plain strings/dicts, or any ASGI app response. Everything -ends up as a single {statusCode, headers, body} JSON payload written to -stdout for the Orva proxy. -""" - -import asyncio -import base64 -import importlib.util -import inspect -import json -import os -import sys -import threading -import time -import traceback - -FUNCTION_DIR = "/code" -entrypoint = os.environ.get("ORVA_ENTRYPOINT", "handler.py") -handler_path = os.path.join(FUNCTION_DIR, entrypoint) - -# Preserve the real stdout for the protocol response; reroute user print(). -protocol_stdout = sys.stdout -sys.stdout = sys.stderr - -if FUNCTION_DIR not in sys.path: - sys.path.insert(0, FUNCTION_DIR) - -# Make the bundled `orva` SDK (kv / invoke / jobs) importable from user -# code as `from orva import kv, invoke, jobs`. /opt/orva is the dir -# adapter.py itself runs from, but Python only auto-adds it to sys.path -# when invoked as `python /opt/orva/adapter.py` AND nothing has -# rewritten sys.path[0]. Insert explicitly so the import works -# regardless of how the adapter was invoked. -if "/opt/orva" not in sys.path: - sys.path.insert(0, "/opt/orva") - - -class _Context: - """Minimal AWS-Lambda-like context object.""" - - def __init__(self, event): - hdrs = event.get("headers", {}) if isinstance(event, dict) else {} - self.function_name = os.environ.get("ORVA_FUNCTION_NAME", "") - self.aws_request_id = hdrs.get("x-orva-execution-id", "") - self.invoked_function_arn = "" - self.memory_limit_in_mb = os.environ.get("ORVA_MEMORY_MB", "") - self.log_group_name = "orva" - self.log_stream_name = hdrs.get("x-orva-execution-id", "") - - def get_remaining_time_in_millis(self): - return int(os.environ.get("ORVA_TIMEOUT_MS", "30000")) - - -def _load_module(): - if not os.path.exists(handler_path): - print(f"Handler not found at {handler_path}", file=sys.stderr) - sys.exit(1) - spec = importlib.util.spec_from_file_location("user_handler", handler_path) - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - return mod - - -def _resolve(mod): - """Return (callable, style) where style is one of: - 'lambda', 'asgi', 'wsgi', 'gcp_flask_request', 'plain'. - """ - # ASGI frameworks (FastAPI, Starlette, Quart) — `app` is an ASGI callable. - for name in ("app", "application"): - app = getattr(mod, name, None) - if app is None: - continue - # ASGI apps have a __call__(scope, receive, send) signature and are - # usually async. Detect by presence of `__call__` accepting 3 args. - if callable(app): - sig = None - try: - sig = inspect.signature(app.__call__ if hasattr(app, "__call__") else app) - except (TypeError, ValueError): - pass - if sig and len(sig.parameters) == 3: - return (app, "asgi") - # Fall through — treat as WSGI (Flask, Django WSGI, etc.). - return (app, "wsgi") - - # Function-style exports, in priority order. - for name in ("handler", "lambda_handler", "main"): - fn = getattr(mod, name, None) - if callable(fn): - return (fn, "lambda") - - return (None, None) - - -mod = _load_module() -handler, style = _resolve(mod) - -if handler is None: - print( - f"Module at {handler_path} does not export a usable handler. " - f"Expected one of: handler, lambda_handler, main, or an ASGI/WSGI `app`.", - file=sys.stderr, - ) - sys.exit(1) - - -# ── Response normalisation ───────────────────────────────────────────── - -def _normalise_response(ret): - """Convert whatever the handler returned into (status, headers, body).""" - # Already an Orva envelope. - if isinstance(ret, dict) and "statusCode" in ret: - status = ret.get("statusCode", 200) - headers = ret.get("headers", {"Content-Type": "application/json"}) - body = ret.get("body", "") - if not isinstance(body, str): - body = json.dumps(body) - return (status, headers, body) - - # Starlette / FastAPI Response objects. - if hasattr(ret, "status_code") and hasattr(ret, "body"): - body = ret.body - if isinstance(body, (bytes, bytearray)): - body = body.decode("utf-8", errors="replace") - headers = {} - if hasattr(ret, "headers"): - try: - for k, v in ret.headers.items(): - headers[k] = v - except Exception: - pass - return (ret.status_code, headers, body or "") - - # Flask Response objects. - if hasattr(ret, "status_code") and hasattr(ret, "get_data"): - body = ret.get_data(as_text=True) - headers = dict(ret.headers) if hasattr(ret, "headers") else {} - return (ret.status_code, headers, body) - - # Plain string, dict, or anything else. - if isinstance(ret, str): - return (200, {"Content-Type": "text/plain"}, ret) - return (200, {"Content-Type": "application/json"}, json.dumps(ret)) - - -# ── Invocation bridges ───────────────────────────────────────────────── - -def _build_flask_request(event): - """Build a werkzeug Request (for GCP Functions / flask-style `main(request)`).""" - try: - from werkzeug.wrappers import Request - from werkzeug.test import EnvironBuilder - except ImportError: - return None - body = event.get("body") or "" - builder = EnvironBuilder( - method=event.get("method", "POST"), - path=event.get("path", "/"), - headers=event.get("headers", {}), - data=body.encode("utf-8") if isinstance(body, str) else body, - ) - return Request(builder.get_environ()) - - -async def _call_asgi(app, event): - """Drive an ASGI app through one request/response cycle.""" - body_bytes = (event.get("body") or "").encode("utf-8") - headers_list = [ - (k.lower().encode(), str(v).encode()) - for k, v in (event.get("headers") or {}).items() - ] - path = event.get("path", "/") or "/" - query = "" - if "?" in path: - path, query = path.split("?", 1) - - scope = { - "type": "http", - "asgi": {"version": "3.0", "spec_version": "2.3"}, - "http_version": "1.1", - "method": event.get("method", "GET"), - "scheme": "http", - "path": path, - "raw_path": path.encode(), - "query_string": query.encode(), - "headers": headers_list, - "server": ("orva", 8443), - "client": ("127.0.0.1", 0), - } - - body_sent = False - - async def receive(): - nonlocal body_sent - if body_sent: - return {"type": "http.disconnect"} - body_sent = True - return {"type": "http.request", "body": body_bytes, "more_body": False} - - response = {"status": 200, "headers": {}, "body": b""} - - async def send(message): - if message["type"] == "http.response.start": - response["status"] = message["status"] - for k, v in message.get("headers", []): - response["headers"][k.decode()] = v.decode() - elif message["type"] == "http.response.body": - response["body"] += message.get("body", b"") - - await app(scope, receive, send) - return ( - response["status"], - response["headers"], - response["body"].decode("utf-8", errors="replace"), - ) - - -def _call_wsgi(app, event): - """Drive a WSGI app (Flask, Django WSGI) through one request cycle.""" - from io import BytesIO - - body = event.get("body") or "" - body_bytes = body.encode("utf-8") if isinstance(body, str) else body - path = event.get("path", "/") or "/" - query = "" - if "?" in path: - path, query = path.split("?", 1) - - environ = { - "REQUEST_METHOD": event.get("method", "GET"), - "SCRIPT_NAME": "", - "PATH_INFO": path, - "QUERY_STRING": query, - "SERVER_NAME": "orva", - "SERVER_PORT": "8443", - "SERVER_PROTOCOL": "HTTP/1.1", - "wsgi.version": (1, 0), - "wsgi.url_scheme": "http", - "wsgi.input": BytesIO(body_bytes), - "wsgi.errors": sys.stderr, - "wsgi.multithread": False, - "wsgi.multiprocess": False, - "wsgi.run_once": True, - "CONTENT_LENGTH": str(len(body_bytes)), - } - for k, v in (event.get("headers") or {}).items(): - key = "HTTP_" + k.upper().replace("-", "_") - environ[key] = str(v) - if k.lower() == "content-type": - environ["CONTENT_TYPE"] = str(v) - - captured = {"status": "200 OK", "headers": []} - - def start_response(status, headers, exc_info=None): - captured["status"] = status - captured["headers"] = headers - return lambda x: None - - chunks = app(environ, start_response) - body_out = b"".join(chunks if not isinstance(chunks, (bytes, str)) else [chunks]) - if isinstance(body_out, str): - body_out = body_out.encode("utf-8") - - status_code = int(captured["status"].split(" ", 1)[0]) - headers_dict = {k: v for k, v in captured["headers"]} - return (status_code, headers_dict, body_out.decode("utf-8", errors="replace")) - - -# ── Framed stdio protocol ────────────────────────────────────────────── -# Wire format: 4-byte big-endian uint32 length, then N bytes UTF-8 JSON. -# Same on stdin (proxy → adapter) and stdout (adapter → proxy). - -import struct - -_stdin = sys.stdin.buffer -_stdout = protocol_stdout.buffer if hasattr(protocol_stdout, "buffer") else protocol_stdout - - -def _read_exact(n): - buf = bytearray() - while len(buf) < n: - chunk = _stdin.read(n - len(buf)) - if not chunk: - return None - buf.extend(chunk) - return bytes(buf) - - -def _read_frame(): - header = _read_exact(4) - if header is None: - return None - (length,) = struct.unpack(">I", header) - if length == 0: - return {} - payload = _read_exact(length) - if payload is None: - return None - try: - return json.loads(payload.decode("utf-8")) - except Exception: - return {"type": "request", "event": {"method": "POST", "path": "/", "headers": {}, "body": ""}} - - -# Stdout writes must be serialised — the heartbeat thread (sync generators) -# and the foreground yield-loop both write frames. JSON+length-prefix means -# any interleave corrupts the wire. The lock is uncontended on the hot -# path (one writer at a time when no heartbeat is firing). -_stdout_lock = threading.Lock() - - -def _write_frame(obj): - body = json.dumps(obj).encode("utf-8") - with _stdout_lock: - try: - _stdout.write(struct.pack(">I", len(body))) - _stdout.write(body) - _stdout.flush() - except (BrokenPipeError, OSError): - # The proxy went away — typically because the HTTP client - # disconnected mid-stream. Re-raise so the caller can stop - # iterating; the worker process exit then unblocks the pool. - raise - - -def _stream_chunk(data): - """Send a single chunk frame. data may be bytes / bytearray / str.""" - if isinstance(data, (bytes, bytearray)): - b = bytes(data) - elif isinstance(data, str): - b = data.encode("utf-8") - elif data is None: - b = b"" - else: - # Any other type: best-effort JSON-encode then utf-8. - b = json.dumps(data).encode("utf-8") - encoded = base64.b64encode(b).decode("ascii") if b else "" - _write_frame({"type": "chunk", "data": encoded}) - - -def _looks_like_head(item): - """First yield can carry the response head if it's an Orva-shaped dict - with statusCode (and no body, or with body that we treat as the first - chunk). Returns (status, headers, leftover_body_or_None) on match, - None otherwise.""" - if not isinstance(item, dict): - return None - if "statusCode" not in item: - return None - status = item.get("statusCode", 200) - headers = item.get("headers", {"Content-Type": "text/plain"}) - body = item.get("body", None) - return (status, headers, body) - - -def _stream_iterable(iterable, streaming_enabled, keepalive_s): - """Drive a sync iterable / generator through the streaming protocol. - - If streaming_enabled is False we buffer everything into a single - response frame for back-compat — operators flipping the system_config - flag get the pre-C1 single-shot behaviour without redeploying. - - A separate thread fires an empty chunk every keepalive_s seconds if - no real chunk has flown in that window, so intermediate proxies / LBs - don't kill the connection during slow phases (LLM token generation, - DB cursor walks). The thread reads last_emit under no lock — the - timestamp is a single 64-bit float so torn reads aren't a concern. - """ - if not streaming_enabled: - # Fallback: buffer the entire generator output into a single - # response. Tries to honor an Orva-shaped first item as the head; - # otherwise wraps everything into text/plain. - head = None - body_parts = [] - for item in iterable: - if head is None: - detected = _looks_like_head(item) - if detected is not None: - status, headers, body = detected - head = (status, headers) - if body is not None: - body_parts.append(body if isinstance(body, str) else str(body)) - continue - head = (200, {"Content-Type": "text/plain"}) - if isinstance(item, (bytes, bytearray)): - body_parts.append(item.decode("utf-8", errors="replace")) - else: - body_parts.append(item if isinstance(item, str) else str(item)) - if head is None: - head = (200, {"Content-Type": "text/plain"}) - status, headers = head - _write_frame({ - "type": "response", "statusCode": status, - "headers": headers, "body": "".join(body_parts), - }) - return - - # Streaming path. - head_sent = False - last_emit = [time.monotonic()] - stop_evt = threading.Event() - - def _heartbeat(): - while not stop_evt.wait(keepalive_s): - if time.monotonic() - last_emit[0] >= keepalive_s: - try: - _write_frame({"type": "chunk", "data": ""}) - last_emit[0] = time.monotonic() - except Exception: - return - - hb = None - - def _send_head(status, headers): - nonlocal head_sent - if head_sent: - return - _write_frame({ - "type": "response_start", - "statusCode": status, - "headers": headers, - }) - head_sent = True - - try: - for item in iterable: - if not head_sent: - detected = _looks_like_head(item) - if detected is not None: - status, headers, body = detected - _send_head(status, headers) - # Start the heartbeat AFTER the head so the empty - # chunk frames never precede the response_start. - hb = threading.Thread(target=_heartbeat, daemon=True) - hb.start() - if body is not None and body != "": - _stream_chunk(body) - last_emit[0] = time.monotonic() - continue - _send_head(200, {"Content-Type": "text/plain; charset=utf-8"}) - hb = threading.Thread(target=_heartbeat, daemon=True) - hb.start() - _stream_chunk(item) - last_emit[0] = time.monotonic() - if not head_sent: - _send_head(200, {"Content-Type": "text/plain; charset=utf-8"}) - _write_frame({"type": "response_end"}) - except (BrokenPipeError, OSError): - # Client disconnected. Stop iterating; the worker continues - # serving subsequent requests if its stdin is still open. - pass - finally: - stop_evt.set() - - -async def _stream_async_iterable(aiterable, streaming_enabled, keepalive_s): - """Async-gen variant. Uses an asyncio Task as the heartbeat instead - of a thread so it cooperates with the same event loop as the user - code (no GIL contention, no thread-safe-stdout double-locking). - """ - if not streaming_enabled: - head = None - body_parts = [] - async for item in aiterable: - if head is None: - detected = _looks_like_head(item) - if detected is not None: - status, headers, body = detected - head = (status, headers) - if body is not None: - body_parts.append(body if isinstance(body, str) else str(body)) - continue - head = (200, {"Content-Type": "text/plain"}) - if isinstance(item, (bytes, bytearray)): - body_parts.append(item.decode("utf-8", errors="replace")) - else: - body_parts.append(item if isinstance(item, str) else str(item)) - if head is None: - head = (200, {"Content-Type": "text/plain"}) - status, headers = head - _write_frame({ - "type": "response", "statusCode": status, - "headers": headers, "body": "".join(body_parts), - }) - return - - head_sent = [False] - last_emit = [time.monotonic()] - - def _send_head(status, headers): - if head_sent[0]: - return - _write_frame({ - "type": "response_start", - "statusCode": status, - "headers": headers, - }) - head_sent[0] = True - - async def _heartbeat(): - try: - while True: - await asyncio.sleep(keepalive_s) - if time.monotonic() - last_emit[0] >= keepalive_s: - _write_frame({"type": "chunk", "data": ""}) - last_emit[0] = time.monotonic() - except asyncio.CancelledError: - return - - hb_task = None - try: - async for item in aiterable: - if not head_sent[0]: - detected = _looks_like_head(item) - if detected is not None: - status, headers, body = detected - _send_head(status, headers) - hb_task = asyncio.create_task(_heartbeat()) - if body is not None and body != "": - _stream_chunk(body) - last_emit[0] = time.monotonic() - continue - _send_head(200, {"Content-Type": "text/plain; charset=utf-8"}) - hb_task = asyncio.create_task(_heartbeat()) - _stream_chunk(item) - last_emit[0] = time.monotonic() - if not head_sent[0]: - _send_head(200, {"Content-Type": "text/plain; charset=utf-8"}) - _write_frame({"type": "response_end"}) - except (BrokenPipeError, OSError): - pass - finally: - if hb_task is not None: - hb_task.cancel() - try: - await hb_task - except (asyncio.CancelledError, Exception): - pass - - -def _call_handler(event): - """Invoke the user handler and return the raw return value (NOT - normalised). Splits dispatch from normalisation so the caller can - detect generators / async iterables before we collapse them into a - single response.""" - if style == "asgi": - return ("normal", asyncio.run(_call_asgi(handler, event))) - if style == "wsgi": - return ("wsgi-tuple", _call_wsgi(handler, event)) - - # Lambda / plain style. - try: - result = handler(event, _Context(event)) - except TypeError as te: - msg = str(te) - if "positional argument" in msg or "takes" in msg or "missing" in msg: - try: - result = handler(event) - except TypeError: - req = _build_flask_request(event) - if req is not None: - result = handler(req) - else: - raise - else: - raise - - if inspect.iscoroutine(result): - result = asyncio.run(result) - - return ("raw", result) - - -def _dispatch_and_emit(event, streaming_enabled, keepalive_s): - """End-to-end dispatch path: invoke the handler and either emit a - streaming protocol exchange (response_start + chunks + response_end) - or a single response frame, depending on the handler's return value. - - Returns nothing — frames go straight out via _write_frame. - """ - kind, result = _call_handler(event) - if kind == "normal": - # ASGI tuple (status, headers, body) - status, headers, body = result - _write_frame({"type": "response", "statusCode": status, "headers": headers, "body": body}) - return - if kind == "wsgi-tuple": - status, headers, body = result - _write_frame({"type": "response", "statusCode": status, "headers": headers, "body": body}) - return - - # Streaming detection. Async generators take precedence because - # inspect.isgenerator is False for them. - if inspect.isasyncgen(result): - asyncio.run(_stream_async_iterable(result, streaming_enabled, keepalive_s)) - return - if inspect.isgenerator(result): - _stream_iterable(result, streaming_enabled, keepalive_s) - return - - status, headers, body = _normalise_response(result) - _write_frame({"type": "response", "statusCode": status, "headers": headers, "body": body}) - - -# ── Main loop ────────────────────────────────────────────────────────── - -max_reqs = int(os.environ.get("ORVA_MAX_REQUESTS", "0") or 0) -served = 0 - -try: - while True: - frame = _read_frame() - if frame is None: - sys.exit(0) # stdin EOF - ftype = frame.get("type") - if ftype == "quit": - _write_frame({"type": "bye"}) - sys.exit(0) - if ftype != "request": - continue - - event = frame.get("event") or {"method": "POST", "path": "/", "headers": {}, "body": ""} - # Propagate call depth into the env so orva.invoke()'s SDK can - # forward it on outbound nested calls. Without this each recursion - # level would see depth="" and the host's depth guard never trips. - _hdrs = (event.get("headers") or {}) if isinstance(event, dict) else {} - _depth = _hdrs.get("x-orva-call-depth") or _hdrs.get("X-Orva-Call-Depth") or "" - if _depth: - os.environ["ORVA_CALL_DEPTH"] = _depth - else: - os.environ.pop("ORVA_CALL_DEPTH", None) - - # v0.5 trace context. Each event carries the trace_id + span_id of - # this invocation; the SDK reads them from env when issuing nested - # F2F calls or job enqueues so causal chains stay linked. - _tid = _hdrs.get("x-orva-trace-id") or _hdrs.get("X-Orva-Trace-Id") or "" - _sid = _hdrs.get("x-orva-span-id") or _hdrs.get("X-Orva-Span-Id") or "" - if _tid: os.environ["ORVA_TRACE_ID"] = _tid - else: os.environ.pop("ORVA_TRACE_ID", None) - if _sid: os.environ["ORVA_SPAN_ID"] = _sid - else: os.environ.pop("ORVA_SPAN_ID", None) - - # v0.6 SDK: trace.span() / log.* need the execution id. - _eid = _hdrs.get("x-orva-execution-id") or _hdrs.get("X-Orva-Execution-Id") or "" - if _eid: os.environ["ORVA_EXECUTION_ID"] = _eid - else: os.environ.pop("ORVA_EXECUTION_ID", None) - - # v0.4 C1: streaming flag + heartbeat interval ride on per-request - # headers so the proxy can flip them at runtime without redeploying - # the worker. Defaults match the system_config seed values. - _streaming_on = (_hdrs.get("x-orva-streaming-enabled") or "1") != "0" - try: - _keepalive = max(1, int(_hdrs.get("x-orva-stream-keepalive-seconds") or "15")) - except (TypeError, ValueError): - _keepalive = 15 - - try: - _dispatch_and_emit(event, _streaming_on, _keepalive) - except Exception: - traceback.print_exc() - # If we already started streaming we can't undo the head; emit - # response_end and let the proxy close out. Otherwise emit a - # plain 500 response. - try: - _write_frame({ - "type": "response", "statusCode": 500, - "headers": {"Content-Type": "application/json"}, - "body": json.dumps({"error": "Internal function error"}), - }) - except Exception: - # Best-effort terminator — the foreground proxy frame loop - # will see EOF on the next read if even this fails. - try: - _write_frame({"type": "response_end"}) - except Exception: - pass - - served += 1 - if max_reqs > 0 and served >= max_reqs: - _write_frame({"type": "bye"}) - sys.exit(0) -except SystemExit: - raise -except Exception as exc: - try: - _write_frame({"type": "error", "fatal": True, "message": f"{type(exc).__name__}: {exc}"}) - except Exception: - pass - sys.exit(1) diff --git a/backend/runtimes/python313/orva.py b/backend/runtimes/python313/orva.py deleted file mode 100644 index 448bcda..0000000 --- a/backend/runtimes/python313/orva.py +++ /dev/null @@ -1,678 +0,0 @@ -"""Orva Python SDK — kv, invoke, jobs, crons, trace, log, context. - -Available inside any function running on Orva. Routes through the -ORVA_API_BASE loopback URL using the per-process ORVA_INTERNAL_TOKEN -that the worker received at spawn time. Both env vars are present in -production and absent in tests; helpers raise OrvaUnavailableError when -the SDK can't reach the host unless __test_mode__ has installed an -override. - -Stdlib-only (urllib, json, time, os, sys) — no third-party deps and no -build step. -""" - -from __future__ import annotations - -import json -import os -import sys -import time -import urllib.error -import urllib.parse -import urllib.request -from contextlib import contextmanager -from datetime import datetime, timezone -from typing import Any, Awaitable, Callable, Dict, Iterator, List, Optional, TypeVar -from urllib.parse import quote - - -# SDK version baked at adapter-embed time. Sent on every internal call. -SDK_VERSION = "0.6.0" - -T = TypeVar("T") - -# ── Errors ────────────────────────────────────────────────────────── - - -class OrvaError(RuntimeError): - """Base class for SDK errors. `status` carries the upstream HTTP code - so callers can branch.""" - - def __init__(self, message: str, status: int = 0): - super().__init__(message) - self.status = status - - -class OrvaUnavailableError(OrvaError): - """ORVA_API_BASE / ORVA_INTERNAL_TOKEN are missing — typically only - happens in tests or when running the handler outside of Orva's - sandbox.""" - - -class OrvaCASMismatch(OrvaError): - """kv.cas() precondition failed. `current_value` carries the value - that was actually present so callers can retry with a fresh - expectation.""" - - def __init__(self, current_value: Any): - super().__init__("kv.cas: precondition failed", status=409) - self.current_value = current_value - - -# ── Test-mode hook ────────────────────────────────────────────────── - - -_test_impl: Optional[Dict[str, Callable]] = None - - -def __test_mode__(impl: Optional[Dict[str, Callable]]) -> None: - """Swap the SDK transport for unit tests. `impl` is a dict with an - optional `request` callable matching the (method, path, opts) → - (status, body) shape of the internal `_request` function.""" - global _test_impl - _test_impl = impl - - -# ── Environment accessors ─────────────────────────────────────────── - - -def _api_base() -> str: - return os.environ.get("ORVA_API_BASE", "") - - -def _token() -> str: - return os.environ.get("ORVA_INTERNAL_TOKEN", "") - - -def _function_id() -> str: - return os.environ.get("ORVA_FUNCTION_ID", "") - - -def _execution_id() -> str: - return os.environ.get("ORVA_EXECUTION_ID", "") - - -def _trace_id() -> str: - return os.environ.get("ORVA_TRACE_ID", "") - - -def _span_id() -> str: - return os.environ.get("ORVA_SPAN_ID", "") - - -def _call_depth() -> int: - try: - return int(os.environ.get("ORVA_CALL_DEPTH", "0")) - except ValueError: - return 0 - - -def _timeout_ms() -> int: - try: - return int(os.environ.get("ORVA_TIMEOUT_MS", "30000")) - except ValueError: - return 30000 - - -def _memory_mb() -> int: - try: - return int(os.environ.get("ORVA_MEMORY_MB", "64")) - except ValueError: - return 64 - - -def _trace_headers() -> Dict[str, str]: - """Forward trace context on every internal call so F2F / job enqueues - stay linked into the same trace as the caller.""" - h: Dict[str, str] = {"X-Orva-SDK-Version": SDK_VERSION} - if v := _trace_id(): - h["X-Orva-Trace-Id"] = v - if v := _span_id(): - h["X-Orva-Span-Id"] = v - if v := _function_id(): - h["X-Orva-Caller-Function"] = v - h["X-Orva-Function-Id"] = v - if v := _execution_id(): - h["X-Orva-Execution-Id"] = v - return h - - -# ── HTTP transport ────────────────────────────────────────────────── - - -DEFAULT_TIMEOUT_S = 30.0 - -# A module-level opener with HTTP keep-alive enabled. urllib's default -# `urlopen` opens a fresh TCP connection every call; the opener pools -# connections so the loopback handshake amortises across the function's -# SDK calls. The HTTPHandler is the stock one — keep-alive support comes -# from Python 3.12+'s default `Connection: keep-alive` behaviour on -# urllib's HTTPConnection underneath. -_opener = urllib.request.build_opener() - - -def _request( - method: str, - path: str, - *, - body: bytes = b"", - headers: Optional[Dict[str, str]] = None, - timeout_s: float = DEFAULT_TIMEOUT_S, -) -> tuple[int, bytes, Dict[str, str]]: - if _test_impl and "request" in _test_impl: - out = _test_impl["request"]( - method, path, {"body": body, "headers": headers or {}} - ) - if isinstance(out, tuple) and len(out) == 2: - return out[0], out[1], {} - return out - base = _api_base() - token = _token() - if not base or not token: - raise OrvaUnavailableError( - "Orva SDK not available (missing ORVA_API_BASE or ORVA_INTERNAL_TOKEN)" - ) - h: Dict[str, str] = { - "X-Orva-Internal-Token": token, - "Content-Type": "application/json", - } - h.update(_trace_headers()) - if headers: - h.update(headers) - req = urllib.request.Request(base + path, data=body or None, headers=h, method=method) - try: - with _opener.open(req, timeout=timeout_s) as resp: - return ( - resp.getcode(), - resp.read(), - {k.lower(): v for k, v in resp.headers.items()}, - ) - except urllib.error.HTTPError as e: - return e.code, e.read(), {k.lower(): v for k, v in (e.headers or {}).items()} - except (urllib.error.URLError, TimeoutError) as e: - raise OrvaError(f"request failed: {e}", status=0) - - -# ── KV ────────────────────────────────────────────────────────────── - - -class _KV: - """Per-function key/value store backed by SQLite on the host.""" - - @staticmethod - def get(key: str, default: Any = None) -> Any: - fn = _function_id() - status, body, _ = _request("GET", f"/api/v1/_kv/{fn}/{quote(key, safe='')}") - if status == 404: - return default - if status >= 400: - raise OrvaError(f"kv.get({key!r}) failed: {body!r}", status=status) - data = json.loads(body) - return data["value"] if data.get("value") is not None else default - - @staticmethod - def put(key: str, value: Any, *, ttl_seconds: int = 0) -> None: - fn = _function_id() - payload = json.dumps({"value": value, "ttl_seconds": int(ttl_seconds)}).encode("utf-8") - status, body, _ = _request( - "PUT", f"/api/v1/_kv/{fn}/{quote(key, safe='')}", body=payload - ) - if status >= 400: - raise OrvaError(f"kv.put({key!r}) failed: {body!r}", status=status) - - @staticmethod - def delete(key: str) -> None: - fn = _function_id() - status, body, _ = _request("DELETE", f"/api/v1/_kv/{fn}/{quote(key, safe='')}") - if status >= 400 and status != 404: - raise OrvaError(f"kv.delete({key!r}) failed: {body!r}", status=status) - - @staticmethod - def list( - prefix: str = "", limit: int = 100, cursor: str = "" - ) -> Dict[str, Any]: - fn = _function_id() - qs = [f"limit={int(limit)}"] - if prefix: - qs.append("prefix=" + urllib.parse.quote(prefix, safe="")) - if cursor: - qs.append("cursor=" + urllib.parse.quote(cursor, safe="")) - status, body, _ = _request("GET", f"/api/v1/_kv/{fn}?" + "&".join(qs)) - if status >= 400: - raise OrvaError(f"kv.list failed: {body!r}", status=status) - data = json.loads(body) - return {"keys": list(data.get("keys") or []), "next_cursor": data.get("next_cursor", "")} - - @staticmethod - def get_many(keys: List[str]) -> Dict[str, Any]: - if not keys: - return {} - fn = _function_id() - payload = json.dumps({"ops": [{"op": "get", "key": k} for k in keys]}).encode("utf-8") - status, body, _ = _request("POST", f"/api/v1/_kv/{fn}/batch", body=payload) - if status >= 400: - raise OrvaError(f"kv.get_many failed: {body!r}", status=status) - data = json.loads(body) - out: Dict[str, Any] = {} - for r in data.get("results") or []: - out[r["key"]] = r.get("value") if r.get("found") else None - return out - - @staticmethod - def put_many(entries: List[Dict[str, Any]]) -> None: - if not entries: - return - fn = _function_id() - ops = [ - { - "op": "put", - "key": e["key"], - "value": e["value"], - "ttl_seconds": int(e.get("ttl_seconds", 0)), - } - for e in entries - ] - payload = json.dumps({"ops": ops}).encode("utf-8") - status, body, _ = _request("POST", f"/api/v1/_kv/{fn}/batch", body=payload) - if status >= 400: - raise OrvaError(f"kv.put_many failed: {body!r}", status=status) - - @staticmethod - def delete_many(keys: List[str]) -> int: - if not keys: - return 0 - fn = _function_id() - payload = json.dumps({"ops": [{"op": "delete", "key": k} for k in keys]}).encode("utf-8") - status, body, _ = _request("POST", f"/api/v1/_kv/{fn}/batch", body=payload) - if status >= 400: - raise OrvaError(f"kv.delete_many failed: {body!r}", status=status) - data = json.loads(body) - return sum(1 for r in data.get("results") or [] if r.get("found")) - - @staticmethod - def incr(key: str, delta: int = 1, *, ttl_seconds: int = 0) -> int: - fn = _function_id() - payload = json.dumps({"delta": int(delta), "ttl_seconds": int(ttl_seconds)}).encode("utf-8") - status, body, _ = _request( - "POST", f"/api/v1/_kv/{fn}/{quote(key, safe='')}/incr", body=payload - ) - if status >= 400: - raise OrvaError(f"kv.incr({key!r}) failed: {body!r}", status=status) - return int(json.loads(body)["value"]) - - @staticmethod - def cas(key: str, expected: Any, new: Any, *, ttl_seconds: int = 0) -> bool: - fn = _function_id() - payload = json.dumps( - {"expected": expected, "new": new, "ttl_seconds": int(ttl_seconds)} - ).encode("utf-8") - status, body, _ = _request( - "POST", f"/api/v1/_kv/{fn}/{quote(key, safe='')}/cas", body=payload - ) - if status >= 400: - raise OrvaError(f"kv.cas({key!r}) failed: {body!r}", status=status) - data = json.loads(body) - if not data.get("ok"): - raise OrvaCASMismatch(data.get("current")) - return True - - -kv = _KV() - - -# ── Function-to-function invoke ───────────────────────────────────── - - -def invoke( - function_name: str, - payload: Any = None, - *, - timeout_ms: int = 30000, -) -> Dict[str, Any]: - """Invoke another Orva function by friendly name. Returns the parsed - {statusCode, headers, body} envelope; body is JSON-decoded when - possible.""" - body = json.dumps(payload if payload is not None else {}).encode("utf-8") - headers: Dict[str, str] = {} - incoming = os.environ.get("ORVA_CALL_DEPTH", "") - if incoming: - headers["X-Orva-Call-Depth"] = incoming - status, raw, _ = _request( - "POST", - f"/api/v1/_internal/invoke/{function_name}", - body=body, - headers=headers, - timeout_s=max(timeout_ms / 1000.0, 1.0), - ) - if status == 404: - raise OrvaError(f"function not found: {function_name}", status=404) - if status == 507: - raise OrvaError("call depth exceeded", status=507) - if status >= 400: - raise OrvaError(f"invoke({function_name!r}) failed: {raw!r}", status=status) - envelope = json.loads(raw) - body_str = envelope.get("body", "") - if isinstance(body_str, str): - try: - envelope["body"] = json.loads(body_str) - except (ValueError, TypeError): - pass - return envelope - - -def invoke_stream( - function_name: str, - payload: Any = None, - *, - timeout_ms: int = 30000, -) -> Iterator[bytes]: - """Stream-invoke variant. Yields response body chunks as bytes. Use - when the callee returns SSE / async-iterator / generator output and - you want to consume frames as they arrive instead of buffering.""" - base = _api_base() - token = _token() - if not base or not token: - raise OrvaUnavailableError( - "Orva SDK not available (missing ORVA_API_BASE or ORVA_INTERNAL_TOKEN)" - ) - body = json.dumps(payload if payload is not None else {}).encode("utf-8") - h: Dict[str, str] = { - "X-Orva-Internal-Token": token, - "Content-Type": "application/json", - "X-Orva-Call-Depth": str(_call_depth()), - } - h.update(_trace_headers()) - req = urllib.request.Request( - base + f"/api/v1/_internal/invoke/{function_name}/stream", - data=body, - headers=h, - method="POST", - ) - try: - resp = _opener.open(req, timeout=max(timeout_ms / 1000.0, 1.0)) - except urllib.error.HTTPError as e: - if e.code == 404: - raise OrvaError(f"function not found: {function_name}", status=404) - if e.code == 507: - raise OrvaError("call depth exceeded", status=507) - raise OrvaError(f"invoke_stream({function_name!r}) failed: {e.read()!r}", status=e.code) - except (urllib.error.URLError, TimeoutError) as e: - raise OrvaError(f"invoke_stream request failed: {e}", status=0) - try: - while True: - chunk = resp.read(64 * 1024) - if not chunk: - return - yield chunk - finally: - resp.close() - - -# ── Background jobs ───────────────────────────────────────────────── - - -class _Jobs: - @staticmethod - def enqueue( - function_name: str, - payload: Any = None, - *, - max_attempts: int = 3, - scheduled_at: Optional[str] = None, - idempotency_key: Optional[str] = None, - idempotency_window_seconds: int = 0, - ) -> Dict[str, Any]: - body_obj: Dict[str, Any] = { - "function_name": function_name, - "payload": payload if payload is not None else {}, - "max_attempts": int(max_attempts), - } - if scheduled_at is not None: - body_obj["scheduled_at"] = scheduled_at - if idempotency_key: - body_obj["idempotency_key"] = idempotency_key - if idempotency_window_seconds: - body_obj["idempotency_window_seconds"] = int(idempotency_window_seconds) - status, raw, headers = _request( - "POST", "/api/v1/jobs", body=json.dumps(body_obj).encode("utf-8") - ) - if status >= 400: - raise OrvaError(f"jobs.enqueue failed: {raw!r}", status=status) - data = json.loads(raw) - replayed = headers.get("x-idempotency-replayed") == "true" or data.get("replayed") is True - return {"id": data["id"], "replayed": bool(replayed)} - - -jobs = _Jobs() - - -# ── Cron-from-code ────────────────────────────────────────────────── - - -class _Crons: - @staticmethod - def upsert( - name: str, - schedule: str, - *, - payload: Any = None, - timezone: Optional[str] = None, - enabled: Optional[bool] = None, - ) -> Dict[str, Any]: - body_obj: Dict[str, Any] = {"name": name, "schedule": schedule} - if payload is not None: - body_obj["payload"] = payload - if timezone: - body_obj["timezone"] = timezone - if enabled is not None: - body_obj["enabled"] = bool(enabled) - status, raw, _ = _request( - "POST", "/api/v1/_internal/crons", body=json.dumps(body_obj).encode("utf-8") - ) - if status >= 400: - raise OrvaError(f"crons.upsert({name!r}) failed: {raw!r}", status=status) - return json.loads(raw) - - -crons = _Crons() - - -# ── User-defined spans ────────────────────────────────────────────── - - -class _Trace: - @staticmethod - @contextmanager - def span(name: str, attributes: Optional[Dict[str, Any]] = None) -> Iterator[None]: - """Context manager wrapping a code block in a child span. Errors - inside the block are recorded as status="error" and re-raised.""" - started_at = datetime.now(timezone.utc) - t0 = time.monotonic() - ok = True - err_msg = "" - try: - yield - except BaseException as e: # capture every kind of exit - ok = False - err_msg = str(e) - raise - finally: - duration_ms = int((time.monotonic() - t0) * 1000) - body_obj: Dict[str, Any] = { - "name": name, - "started_at": started_at.isoformat(), - "duration_ms": duration_ms, - "status": "ok" if ok else "error", - } - if err_msg: - body_obj["error_message"] = err_msg - if attributes is not None: - body_obj["attributes"] = attributes - try: - _request( - "POST", - "/api/v1/_internal/spans", - body=json.dumps(body_obj).encode("utf-8"), - ) - except Exception: - # Fire-and-forget — never let span ingestion break user code. - pass - - -trace = _Trace() - - -# ── Structured logging ────────────────────────────────────────────── - - -def _emit_log(level: str, msg: str, fields: Optional[Dict[str, Any]]) -> None: - rec: Dict[str, Any] = { - "ts": datetime.now(timezone.utc).isoformat(), - "level": level, - "message": msg if isinstance(msg, str) else json.dumps(msg), - } - if fields: - rec["fields"] = fields - if span := _span_id(): - rec["span_id"] = span - try: - sys.stderr.write("__ORVA_LOG_JSON__" + json.dumps(rec) + "\n") - sys.stderr.flush() - except Exception: - pass - - -class _Log: - @staticmethod - def debug(msg: str, fields: Optional[Dict[str, Any]] = None) -> None: - _emit_log("debug", msg, fields) - - @staticmethod - def info(msg: str, fields: Optional[Dict[str, Any]] = None) -> None: - _emit_log("info", msg, fields) - - @staticmethod - def warn(msg: str, fields: Optional[Dict[str, Any]] = None) -> None: - _emit_log("warn", msg, fields) - - @staticmethod - def error(msg: str, fields: Optional[Dict[str, Any]] = None) -> None: - _emit_log("error", msg, fields) - - -log = _Log() - - -# ── Secrets ───────────────────────────────────────────────────────── - - -class _Secrets: - @staticmethod - def get(name: str) -> Optional[str]: - return os.environ.get(name) - - -secrets = _Secrets() - - -# ── Webhook helper ────────────────────────────────────────────────── - - -def _first_header(event: Dict[str, Any], *names: str) -> str: - h = event.get("headers") or {} if event else {} - for n in names: - if n in h: - return str(h[n]) - if n.lower() in h: - return str(h[n.lower()]) - if n.upper() in h: - return str(h[n.upper()]) - return "" - - -class _Webhook: - @staticmethod - def parse(event: Dict[str, Any]) -> Dict[str, Any]: - headers = (event.get("headers") or {}) if event else {} - trigger = _first_header(event, "x-orva-trigger") - webhook_id = _first_header(event, "x-orva-inbound-webhook-id") - source = "unknown" - event_type = "" - if _first_header(event, "X-GitHub-Event"): - source = "github" - event_type = _first_header(event, "X-GitHub-Event") - elif _first_header(event, "Stripe-Signature"): - source = "stripe" - event_type = _first_header(event, "Stripe-Event-Type") - elif _first_header(event, "X-Slack-Signature"): - source = "slack" - elif _first_header(event, "X-Hub-Signature-256") or _first_header(event, "X-Signature"): - source = "hmac" - payload: Any = event.get("body") if event else None - if isinstance(payload, str) and payload: - try: - payload = json.loads(payload) - except (ValueError, TypeError): - pass - return { - "verified": trigger == "inbound_webhook", - "source": source, - "event_type": event_type, - "webhook_id": webhook_id, - "payload": payload, - "headers": headers, - } - - -webhook = _Webhook() - - -# ── Context (env-var snapshot, lazy) ──────────────────────────────── - - -class _Context: - @property - def function_id(self) -> str: return _function_id() - - @property - def execution_id(self) -> str: return _execution_id() - - @property - def trace_id(self) -> str: return _trace_id() - - @property - def span_id(self) -> str: return _span_id() - - @property - def call_depth(self) -> int: return _call_depth() - - @property - def timeout_ms(self) -> int: return _timeout_ms() - - @property - def memory_mb(self) -> int: return _memory_mb() - - @property - def sdk_version(self) -> str: return SDK_VERSION - - -context = _Context() - - -__all__ = [ - "kv", - "invoke", - "invoke_stream", - "jobs", - "crons", - "trace", - "log", - "secrets", - "webhook", - "context", - "OrvaError", - "OrvaUnavailableError", - "OrvaCASMismatch", - "__test_mode__", - "SDK_VERSION", -] diff --git a/backend/runtimes/python313/py.typed b/backend/runtimes/python313/py.typed deleted file mode 100644 index e69de29..0000000 diff --git a/backend/runtimes/python314/orva.py b/backend/runtimes/python314/orva.py deleted file mode 100644 index 448bcda..0000000 --- a/backend/runtimes/python314/orva.py +++ /dev/null @@ -1,678 +0,0 @@ -"""Orva Python SDK — kv, invoke, jobs, crons, trace, log, context. - -Available inside any function running on Orva. Routes through the -ORVA_API_BASE loopback URL using the per-process ORVA_INTERNAL_TOKEN -that the worker received at spawn time. Both env vars are present in -production and absent in tests; helpers raise OrvaUnavailableError when -the SDK can't reach the host unless __test_mode__ has installed an -override. - -Stdlib-only (urllib, json, time, os, sys) — no third-party deps and no -build step. -""" - -from __future__ import annotations - -import json -import os -import sys -import time -import urllib.error -import urllib.parse -import urllib.request -from contextlib import contextmanager -from datetime import datetime, timezone -from typing import Any, Awaitable, Callable, Dict, Iterator, List, Optional, TypeVar -from urllib.parse import quote - - -# SDK version baked at adapter-embed time. Sent on every internal call. -SDK_VERSION = "0.6.0" - -T = TypeVar("T") - -# ── Errors ────────────────────────────────────────────────────────── - - -class OrvaError(RuntimeError): - """Base class for SDK errors. `status` carries the upstream HTTP code - so callers can branch.""" - - def __init__(self, message: str, status: int = 0): - super().__init__(message) - self.status = status - - -class OrvaUnavailableError(OrvaError): - """ORVA_API_BASE / ORVA_INTERNAL_TOKEN are missing — typically only - happens in tests or when running the handler outside of Orva's - sandbox.""" - - -class OrvaCASMismatch(OrvaError): - """kv.cas() precondition failed. `current_value` carries the value - that was actually present so callers can retry with a fresh - expectation.""" - - def __init__(self, current_value: Any): - super().__init__("kv.cas: precondition failed", status=409) - self.current_value = current_value - - -# ── Test-mode hook ────────────────────────────────────────────────── - - -_test_impl: Optional[Dict[str, Callable]] = None - - -def __test_mode__(impl: Optional[Dict[str, Callable]]) -> None: - """Swap the SDK transport for unit tests. `impl` is a dict with an - optional `request` callable matching the (method, path, opts) → - (status, body) shape of the internal `_request` function.""" - global _test_impl - _test_impl = impl - - -# ── Environment accessors ─────────────────────────────────────────── - - -def _api_base() -> str: - return os.environ.get("ORVA_API_BASE", "") - - -def _token() -> str: - return os.environ.get("ORVA_INTERNAL_TOKEN", "") - - -def _function_id() -> str: - return os.environ.get("ORVA_FUNCTION_ID", "") - - -def _execution_id() -> str: - return os.environ.get("ORVA_EXECUTION_ID", "") - - -def _trace_id() -> str: - return os.environ.get("ORVA_TRACE_ID", "") - - -def _span_id() -> str: - return os.environ.get("ORVA_SPAN_ID", "") - - -def _call_depth() -> int: - try: - return int(os.environ.get("ORVA_CALL_DEPTH", "0")) - except ValueError: - return 0 - - -def _timeout_ms() -> int: - try: - return int(os.environ.get("ORVA_TIMEOUT_MS", "30000")) - except ValueError: - return 30000 - - -def _memory_mb() -> int: - try: - return int(os.environ.get("ORVA_MEMORY_MB", "64")) - except ValueError: - return 64 - - -def _trace_headers() -> Dict[str, str]: - """Forward trace context on every internal call so F2F / job enqueues - stay linked into the same trace as the caller.""" - h: Dict[str, str] = {"X-Orva-SDK-Version": SDK_VERSION} - if v := _trace_id(): - h["X-Orva-Trace-Id"] = v - if v := _span_id(): - h["X-Orva-Span-Id"] = v - if v := _function_id(): - h["X-Orva-Caller-Function"] = v - h["X-Orva-Function-Id"] = v - if v := _execution_id(): - h["X-Orva-Execution-Id"] = v - return h - - -# ── HTTP transport ────────────────────────────────────────────────── - - -DEFAULT_TIMEOUT_S = 30.0 - -# A module-level opener with HTTP keep-alive enabled. urllib's default -# `urlopen` opens a fresh TCP connection every call; the opener pools -# connections so the loopback handshake amortises across the function's -# SDK calls. The HTTPHandler is the stock one — keep-alive support comes -# from Python 3.12+'s default `Connection: keep-alive` behaviour on -# urllib's HTTPConnection underneath. -_opener = urllib.request.build_opener() - - -def _request( - method: str, - path: str, - *, - body: bytes = b"", - headers: Optional[Dict[str, str]] = None, - timeout_s: float = DEFAULT_TIMEOUT_S, -) -> tuple[int, bytes, Dict[str, str]]: - if _test_impl and "request" in _test_impl: - out = _test_impl["request"]( - method, path, {"body": body, "headers": headers or {}} - ) - if isinstance(out, tuple) and len(out) == 2: - return out[0], out[1], {} - return out - base = _api_base() - token = _token() - if not base or not token: - raise OrvaUnavailableError( - "Orva SDK not available (missing ORVA_API_BASE or ORVA_INTERNAL_TOKEN)" - ) - h: Dict[str, str] = { - "X-Orva-Internal-Token": token, - "Content-Type": "application/json", - } - h.update(_trace_headers()) - if headers: - h.update(headers) - req = urllib.request.Request(base + path, data=body or None, headers=h, method=method) - try: - with _opener.open(req, timeout=timeout_s) as resp: - return ( - resp.getcode(), - resp.read(), - {k.lower(): v for k, v in resp.headers.items()}, - ) - except urllib.error.HTTPError as e: - return e.code, e.read(), {k.lower(): v for k, v in (e.headers or {}).items()} - except (urllib.error.URLError, TimeoutError) as e: - raise OrvaError(f"request failed: {e}", status=0) - - -# ── KV ────────────────────────────────────────────────────────────── - - -class _KV: - """Per-function key/value store backed by SQLite on the host.""" - - @staticmethod - def get(key: str, default: Any = None) -> Any: - fn = _function_id() - status, body, _ = _request("GET", f"/api/v1/_kv/{fn}/{quote(key, safe='')}") - if status == 404: - return default - if status >= 400: - raise OrvaError(f"kv.get({key!r}) failed: {body!r}", status=status) - data = json.loads(body) - return data["value"] if data.get("value") is not None else default - - @staticmethod - def put(key: str, value: Any, *, ttl_seconds: int = 0) -> None: - fn = _function_id() - payload = json.dumps({"value": value, "ttl_seconds": int(ttl_seconds)}).encode("utf-8") - status, body, _ = _request( - "PUT", f"/api/v1/_kv/{fn}/{quote(key, safe='')}", body=payload - ) - if status >= 400: - raise OrvaError(f"kv.put({key!r}) failed: {body!r}", status=status) - - @staticmethod - def delete(key: str) -> None: - fn = _function_id() - status, body, _ = _request("DELETE", f"/api/v1/_kv/{fn}/{quote(key, safe='')}") - if status >= 400 and status != 404: - raise OrvaError(f"kv.delete({key!r}) failed: {body!r}", status=status) - - @staticmethod - def list( - prefix: str = "", limit: int = 100, cursor: str = "" - ) -> Dict[str, Any]: - fn = _function_id() - qs = [f"limit={int(limit)}"] - if prefix: - qs.append("prefix=" + urllib.parse.quote(prefix, safe="")) - if cursor: - qs.append("cursor=" + urllib.parse.quote(cursor, safe="")) - status, body, _ = _request("GET", f"/api/v1/_kv/{fn}?" + "&".join(qs)) - if status >= 400: - raise OrvaError(f"kv.list failed: {body!r}", status=status) - data = json.loads(body) - return {"keys": list(data.get("keys") or []), "next_cursor": data.get("next_cursor", "")} - - @staticmethod - def get_many(keys: List[str]) -> Dict[str, Any]: - if not keys: - return {} - fn = _function_id() - payload = json.dumps({"ops": [{"op": "get", "key": k} for k in keys]}).encode("utf-8") - status, body, _ = _request("POST", f"/api/v1/_kv/{fn}/batch", body=payload) - if status >= 400: - raise OrvaError(f"kv.get_many failed: {body!r}", status=status) - data = json.loads(body) - out: Dict[str, Any] = {} - for r in data.get("results") or []: - out[r["key"]] = r.get("value") if r.get("found") else None - return out - - @staticmethod - def put_many(entries: List[Dict[str, Any]]) -> None: - if not entries: - return - fn = _function_id() - ops = [ - { - "op": "put", - "key": e["key"], - "value": e["value"], - "ttl_seconds": int(e.get("ttl_seconds", 0)), - } - for e in entries - ] - payload = json.dumps({"ops": ops}).encode("utf-8") - status, body, _ = _request("POST", f"/api/v1/_kv/{fn}/batch", body=payload) - if status >= 400: - raise OrvaError(f"kv.put_many failed: {body!r}", status=status) - - @staticmethod - def delete_many(keys: List[str]) -> int: - if not keys: - return 0 - fn = _function_id() - payload = json.dumps({"ops": [{"op": "delete", "key": k} for k in keys]}).encode("utf-8") - status, body, _ = _request("POST", f"/api/v1/_kv/{fn}/batch", body=payload) - if status >= 400: - raise OrvaError(f"kv.delete_many failed: {body!r}", status=status) - data = json.loads(body) - return sum(1 for r in data.get("results") or [] if r.get("found")) - - @staticmethod - def incr(key: str, delta: int = 1, *, ttl_seconds: int = 0) -> int: - fn = _function_id() - payload = json.dumps({"delta": int(delta), "ttl_seconds": int(ttl_seconds)}).encode("utf-8") - status, body, _ = _request( - "POST", f"/api/v1/_kv/{fn}/{quote(key, safe='')}/incr", body=payload - ) - if status >= 400: - raise OrvaError(f"kv.incr({key!r}) failed: {body!r}", status=status) - return int(json.loads(body)["value"]) - - @staticmethod - def cas(key: str, expected: Any, new: Any, *, ttl_seconds: int = 0) -> bool: - fn = _function_id() - payload = json.dumps( - {"expected": expected, "new": new, "ttl_seconds": int(ttl_seconds)} - ).encode("utf-8") - status, body, _ = _request( - "POST", f"/api/v1/_kv/{fn}/{quote(key, safe='')}/cas", body=payload - ) - if status >= 400: - raise OrvaError(f"kv.cas({key!r}) failed: {body!r}", status=status) - data = json.loads(body) - if not data.get("ok"): - raise OrvaCASMismatch(data.get("current")) - return True - - -kv = _KV() - - -# ── Function-to-function invoke ───────────────────────────────────── - - -def invoke( - function_name: str, - payload: Any = None, - *, - timeout_ms: int = 30000, -) -> Dict[str, Any]: - """Invoke another Orva function by friendly name. Returns the parsed - {statusCode, headers, body} envelope; body is JSON-decoded when - possible.""" - body = json.dumps(payload if payload is not None else {}).encode("utf-8") - headers: Dict[str, str] = {} - incoming = os.environ.get("ORVA_CALL_DEPTH", "") - if incoming: - headers["X-Orva-Call-Depth"] = incoming - status, raw, _ = _request( - "POST", - f"/api/v1/_internal/invoke/{function_name}", - body=body, - headers=headers, - timeout_s=max(timeout_ms / 1000.0, 1.0), - ) - if status == 404: - raise OrvaError(f"function not found: {function_name}", status=404) - if status == 507: - raise OrvaError("call depth exceeded", status=507) - if status >= 400: - raise OrvaError(f"invoke({function_name!r}) failed: {raw!r}", status=status) - envelope = json.loads(raw) - body_str = envelope.get("body", "") - if isinstance(body_str, str): - try: - envelope["body"] = json.loads(body_str) - except (ValueError, TypeError): - pass - return envelope - - -def invoke_stream( - function_name: str, - payload: Any = None, - *, - timeout_ms: int = 30000, -) -> Iterator[bytes]: - """Stream-invoke variant. Yields response body chunks as bytes. Use - when the callee returns SSE / async-iterator / generator output and - you want to consume frames as they arrive instead of buffering.""" - base = _api_base() - token = _token() - if not base or not token: - raise OrvaUnavailableError( - "Orva SDK not available (missing ORVA_API_BASE or ORVA_INTERNAL_TOKEN)" - ) - body = json.dumps(payload if payload is not None else {}).encode("utf-8") - h: Dict[str, str] = { - "X-Orva-Internal-Token": token, - "Content-Type": "application/json", - "X-Orva-Call-Depth": str(_call_depth()), - } - h.update(_trace_headers()) - req = urllib.request.Request( - base + f"/api/v1/_internal/invoke/{function_name}/stream", - data=body, - headers=h, - method="POST", - ) - try: - resp = _opener.open(req, timeout=max(timeout_ms / 1000.0, 1.0)) - except urllib.error.HTTPError as e: - if e.code == 404: - raise OrvaError(f"function not found: {function_name}", status=404) - if e.code == 507: - raise OrvaError("call depth exceeded", status=507) - raise OrvaError(f"invoke_stream({function_name!r}) failed: {e.read()!r}", status=e.code) - except (urllib.error.URLError, TimeoutError) as e: - raise OrvaError(f"invoke_stream request failed: {e}", status=0) - try: - while True: - chunk = resp.read(64 * 1024) - if not chunk: - return - yield chunk - finally: - resp.close() - - -# ── Background jobs ───────────────────────────────────────────────── - - -class _Jobs: - @staticmethod - def enqueue( - function_name: str, - payload: Any = None, - *, - max_attempts: int = 3, - scheduled_at: Optional[str] = None, - idempotency_key: Optional[str] = None, - idempotency_window_seconds: int = 0, - ) -> Dict[str, Any]: - body_obj: Dict[str, Any] = { - "function_name": function_name, - "payload": payload if payload is not None else {}, - "max_attempts": int(max_attempts), - } - if scheduled_at is not None: - body_obj["scheduled_at"] = scheduled_at - if idempotency_key: - body_obj["idempotency_key"] = idempotency_key - if idempotency_window_seconds: - body_obj["idempotency_window_seconds"] = int(idempotency_window_seconds) - status, raw, headers = _request( - "POST", "/api/v1/jobs", body=json.dumps(body_obj).encode("utf-8") - ) - if status >= 400: - raise OrvaError(f"jobs.enqueue failed: {raw!r}", status=status) - data = json.loads(raw) - replayed = headers.get("x-idempotency-replayed") == "true" or data.get("replayed") is True - return {"id": data["id"], "replayed": bool(replayed)} - - -jobs = _Jobs() - - -# ── Cron-from-code ────────────────────────────────────────────────── - - -class _Crons: - @staticmethod - def upsert( - name: str, - schedule: str, - *, - payload: Any = None, - timezone: Optional[str] = None, - enabled: Optional[bool] = None, - ) -> Dict[str, Any]: - body_obj: Dict[str, Any] = {"name": name, "schedule": schedule} - if payload is not None: - body_obj["payload"] = payload - if timezone: - body_obj["timezone"] = timezone - if enabled is not None: - body_obj["enabled"] = bool(enabled) - status, raw, _ = _request( - "POST", "/api/v1/_internal/crons", body=json.dumps(body_obj).encode("utf-8") - ) - if status >= 400: - raise OrvaError(f"crons.upsert({name!r}) failed: {raw!r}", status=status) - return json.loads(raw) - - -crons = _Crons() - - -# ── User-defined spans ────────────────────────────────────────────── - - -class _Trace: - @staticmethod - @contextmanager - def span(name: str, attributes: Optional[Dict[str, Any]] = None) -> Iterator[None]: - """Context manager wrapping a code block in a child span. Errors - inside the block are recorded as status="error" and re-raised.""" - started_at = datetime.now(timezone.utc) - t0 = time.monotonic() - ok = True - err_msg = "" - try: - yield - except BaseException as e: # capture every kind of exit - ok = False - err_msg = str(e) - raise - finally: - duration_ms = int((time.monotonic() - t0) * 1000) - body_obj: Dict[str, Any] = { - "name": name, - "started_at": started_at.isoformat(), - "duration_ms": duration_ms, - "status": "ok" if ok else "error", - } - if err_msg: - body_obj["error_message"] = err_msg - if attributes is not None: - body_obj["attributes"] = attributes - try: - _request( - "POST", - "/api/v1/_internal/spans", - body=json.dumps(body_obj).encode("utf-8"), - ) - except Exception: - # Fire-and-forget — never let span ingestion break user code. - pass - - -trace = _Trace() - - -# ── Structured logging ────────────────────────────────────────────── - - -def _emit_log(level: str, msg: str, fields: Optional[Dict[str, Any]]) -> None: - rec: Dict[str, Any] = { - "ts": datetime.now(timezone.utc).isoformat(), - "level": level, - "message": msg if isinstance(msg, str) else json.dumps(msg), - } - if fields: - rec["fields"] = fields - if span := _span_id(): - rec["span_id"] = span - try: - sys.stderr.write("__ORVA_LOG_JSON__" + json.dumps(rec) + "\n") - sys.stderr.flush() - except Exception: - pass - - -class _Log: - @staticmethod - def debug(msg: str, fields: Optional[Dict[str, Any]] = None) -> None: - _emit_log("debug", msg, fields) - - @staticmethod - def info(msg: str, fields: Optional[Dict[str, Any]] = None) -> None: - _emit_log("info", msg, fields) - - @staticmethod - def warn(msg: str, fields: Optional[Dict[str, Any]] = None) -> None: - _emit_log("warn", msg, fields) - - @staticmethod - def error(msg: str, fields: Optional[Dict[str, Any]] = None) -> None: - _emit_log("error", msg, fields) - - -log = _Log() - - -# ── Secrets ───────────────────────────────────────────────────────── - - -class _Secrets: - @staticmethod - def get(name: str) -> Optional[str]: - return os.environ.get(name) - - -secrets = _Secrets() - - -# ── Webhook helper ────────────────────────────────────────────────── - - -def _first_header(event: Dict[str, Any], *names: str) -> str: - h = event.get("headers") or {} if event else {} - for n in names: - if n in h: - return str(h[n]) - if n.lower() in h: - return str(h[n.lower()]) - if n.upper() in h: - return str(h[n.upper()]) - return "" - - -class _Webhook: - @staticmethod - def parse(event: Dict[str, Any]) -> Dict[str, Any]: - headers = (event.get("headers") or {}) if event else {} - trigger = _first_header(event, "x-orva-trigger") - webhook_id = _first_header(event, "x-orva-inbound-webhook-id") - source = "unknown" - event_type = "" - if _first_header(event, "X-GitHub-Event"): - source = "github" - event_type = _first_header(event, "X-GitHub-Event") - elif _first_header(event, "Stripe-Signature"): - source = "stripe" - event_type = _first_header(event, "Stripe-Event-Type") - elif _first_header(event, "X-Slack-Signature"): - source = "slack" - elif _first_header(event, "X-Hub-Signature-256") or _first_header(event, "X-Signature"): - source = "hmac" - payload: Any = event.get("body") if event else None - if isinstance(payload, str) and payload: - try: - payload = json.loads(payload) - except (ValueError, TypeError): - pass - return { - "verified": trigger == "inbound_webhook", - "source": source, - "event_type": event_type, - "webhook_id": webhook_id, - "payload": payload, - "headers": headers, - } - - -webhook = _Webhook() - - -# ── Context (env-var snapshot, lazy) ──────────────────────────────── - - -class _Context: - @property - def function_id(self) -> str: return _function_id() - - @property - def execution_id(self) -> str: return _execution_id() - - @property - def trace_id(self) -> str: return _trace_id() - - @property - def span_id(self) -> str: return _span_id() - - @property - def call_depth(self) -> int: return _call_depth() - - @property - def timeout_ms(self) -> int: return _timeout_ms() - - @property - def memory_mb(self) -> int: return _memory_mb() - - @property - def sdk_version(self) -> str: return SDK_VERSION - - -context = _Context() - - -__all__ = [ - "kv", - "invoke", - "invoke_stream", - "jobs", - "crons", - "trace", - "log", - "secrets", - "webhook", - "context", - "OrvaError", - "OrvaUnavailableError", - "OrvaCASMismatch", - "__test_mode__", - "SDK_VERSION", -] diff --git a/backend/runtimes/python314/py.typed b/backend/runtimes/python314/py.typed deleted file mode 100644 index e69de29..0000000 diff --git a/cli/commands/deploy.go b/cli/commands/deploy.go index 751cbdc..9bcedb3 100644 --- a/cli/commands/deploy.go +++ b/cli/commands/deploy.go @@ -29,15 +29,15 @@ auto-detects the entrypoint: - Otherwise the server's runtime default is used (handler.js / handler.py). Examples: - orva deploy ./src --name greeter --runtime node24 - orva deploy ./src --name greeter --runtime node24 --watch # stream build logs`, + orva deploy ./src --name greeter --runtime node + orva deploy ./src --name greeter --runtime node --watch # stream build logs`, Args: cobra.ExactArgs(1), RunE: runDeploy, } func init() { deployCmd.Flags().String("name", "", "function name (required)") - deployCmd.Flags().String("runtime", "", "runtime (node24, node22, python314, python313) (required)") + deployCmd.Flags().String("runtime", "", "runtime: node or python (required)") deployCmd.Flags().String("entrypoint", "", "entrypoint file (optional; auto-detects handler.ts when tsconfig.json + handler.ts present)") deployCmd.Flags().Bool("watch", false, "stream build logs and wait for the deploy to finish (non-zero exit on build failure)") deployCmd.MarkFlagRequired("name") diff --git a/cli/commands/functions.go b/cli/commands/functions.go index d00711b..72b1f95 100644 --- a/cli/commands/functions.go +++ b/cli/commands/functions.go @@ -32,8 +32,8 @@ var functionsCreateCmd = &cobra.Command{ Long: `Create a new function. Name and runtime are required; resource limits and network mode are optional and fall back to server defaults when omitted. - orva functions create --name greeter --runtime node24 - orva functions create --name fetcher --runtime python314 \ + orva functions create --name greeter --runtime node + orva functions create --name fetcher --runtime python \ --memory-mb 256 --timeout-ms 60000 --network-mode egress`, Args: cobra.NoArgs, RunE: runFunctionsCreate, @@ -64,7 +64,7 @@ confirmation unless --yes is set. func init() { functionsCreateCmd.Flags().String("name", "", "function name (required)") - functionsCreateCmd.Flags().String("runtime", "", "runtime (node24, node22, python314, python313) (required)") + functionsCreateCmd.Flags().String("runtime", "", "runtime: node or python (required)") functionsCreateCmd.Flags().Int("memory-mb", 0, "memory limit in MB (0 = server default)") functionsCreateCmd.Flags().Int("timeout-ms", 0, "invocation timeout in ms (0 = server default)") functionsCreateCmd.Flags().String("network-mode", "", "network mode: none | egress (empty = server default)") diff --git a/cli/commands/reference.md b/cli/commands/reference.md index 9d1227d..3cd7889 100644 --- a/cli/commands/reference.md +++ b/cli/commands/reference.md @@ -57,12 +57,13 @@ JSON-encoded by the adapter. **Runtime env:** env vars and secrets land in `process.env` (Node) / `os.environ` (Python). -| Runtime | ID | Entrypoint | Dependencies | -|---|---|---|---| -| Python 3.14 | `python314` | `handler.py` | `requirements.txt` | -| Python 3.13 | `python313` | `handler.py` | `requirements.txt` | -| Node.js 24 | `node24` | `handler.js` | `package.json` | -| Node.js 22 | `node22` | `handler.js` | `package.json` | +Orva offers two runtimes, latest-stable only. The ID is generic (`node` / +`python`); the version column shows what they currently track. + +| Runtime | ID | Version | Entrypoint | Dependencies | +|---|---|---|---|---| +| Python | `python` | 3.14 | `handler.py` | `requirements.txt` | +| Node.js | `node` | 24 | `handler.js` | `package.json` | --- @@ -78,7 +79,7 @@ stream `/api/v1/deployments//stream` until `phase: done`. curl -X POST {{ORIGIN}}/api/v1/functions \ -H 'X-Orva-API-Key: ' \ -H 'Content-Type: application/json' \ - -d '{"name":"hello","runtime":"python314","memory_mb":128,"cpus":0.5}' + -d '{"name":"hello","runtime":"python","memory_mb":128,"cpus":0.5}' ``` ### 2. Upload code @@ -966,10 +967,10 @@ Orva is a self-hosted serverless platform — think Cloudflare Workers / Vercel -Pick exactly one — Orva has no Docker, no buildpacks, no per-function Python/Node version pinning beyond this: -- python314 (default) or python313 — entry: handler.py — deps: requirements.txt -- node24 (default) or node22 — entry: handler.js — deps: package.json -Older minor versions auto-migrate to the latest patch on next deploy. Native modules (psycopg2-binary, sharp, bcrypt, etc.) are supported via prebuilt wheels / npm prebuilts; if a dep needs a system library not present in the runtime image, the build will fail with a clear error. +Pick exactly one — Orva has no Docker, no buildpacks, no per-function version pinning. Two runtimes, generic ids, latest-stable only: +- python (Python 3.14) — entry: handler.py — deps: requirements.txt +- node (Node.js 24, also runs TypeScript) — entry: handler.js — deps: package.json +Native modules (psycopg2-binary, sharp, bcrypt, etc.) are supported via prebuilt wheels / npm prebuilts; if a dep needs a system library not present in the runtime image, the build will fail with a clear error. @@ -1596,10 +1597,10 @@ orva init # is present; else uses the runtime default (handler.js / handler.py). orva deploy ./my-fn \ --name resize-image \ - --runtime node24 + --runtime node # Override the entrypoint explicitly: -orva deploy ./my-fn --name api --runtime python314 --entrypoint app.py +orva deploy ./my-fn --name api --runtime python --entrypoint app.py ``` #### Invoke + tail logs diff --git a/docs/API.md b/docs/API.md index d7d9667..5363e48 100644 --- a/docs/API.md +++ b/docs/API.md @@ -69,7 +69,7 @@ Create a function record. ```json { "name": "my-fn", - "runtime": "node22", // node22|node24|python313|python314 + "runtime": "node", // node|node|python|python "entrypoint": "handler.js", // optional, defaults match the runtime "memory_mb": 128, "cpus": 1, diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 13b9de7..cdfe883 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -185,10 +185,8 @@ picks up the new target. │ │ └── main.js Orva's adapter wrapper │ └── def5678.../ └── rootfs/ - ├── node22/ debian-slim with /usr/bin/node - ├── node24/ - ├── python313/ - └── python314/ + ├── node/ slim image with /usr/local/bin/node (Node.js 24) + └── python/ slim image with /usr/local/bin/python3 (Python 3.14) ``` ## Database schema @@ -346,8 +344,8 @@ Per-runtime adapter scripts. The adapter is the entrypoint nsjail exec's into; it reads request frames from stdin, calls the user's exported handler, and writes response frames back. -- `node22/adapter.js` — also used for node24 -- `python313/adapter.py` — also used for python314 +- `node/adapter.js` — also used for node +- `python/adapter.py` — also used for python ### `frontend/` diff --git a/docs/CAPACITY.md b/docs/CAPACITY.md index b959024..5fb65ae 100644 --- a/docs/CAPACITY.md +++ b/docs/CAPACITY.md @@ -12,12 +12,10 @@ This doc reports what was measured, not what was hoped for. 20 functions, mixed runtime: -| runtime | count | shape | -|------------|-------|------------------------------| -| node22 | 5 | trivial echo handler | -| node24 | 5 | trivial echo handler | -| python3.13 | 5 | trivial dict-return handler | -| python3.14 | 5 | trivial dict-return handler | +| runtime | count | shape | +|----------|-------|------------------------------| +| node | 10 | trivial echo handler | +| python | 10 | trivial dict-return handler | All 20 inline-deployed (no requirements.txt / package.json — those exercise the longer build path; covered separately in plan §B). @@ -61,9 +59,9 @@ Per-pool snapshot at end of run: | function | idle | busy | scale_ups | scale_downs | |-------------------|-----:|-----:|----------:|------------:| -| ascale-node22-1 | 25 | 0 | 0 | 10 | -| ascale-node22-3 | 25 | 0 | 0 | 11 | -| ascale-node24-1 | 25 | 0 | 0 | 9 | +| ascale-node-1 | 25 | 0 | 0 | 10 | +| ascale-node-3 | 25 | 0 | 0 | 11 | +| ascale-node-1 | 25 | 0 | 0 | 9 | | ascale-py313-1 | 14 | 0 | 14 | 0 | | ascale-py314-1 | 14 | 0 | 14 | 0 | | (15 idle fns) | – | – | – | – | diff --git a/docs/CLI.md b/docs/CLI.md index 833d079..fff8528 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -108,7 +108,7 @@ orva login --endpoint https://orva.example.com --api-key orva_… orva system health # 3. Deploy + invoke a function. -orva deploy ./my-fn --name my-fn --runtime node24 +orva deploy ./my-fn --name my-fn --runtime node orva invoke my-fn --body '{"hello":"world"}' # 4. See what happened. @@ -224,18 +224,18 @@ orva login --endpoint https://orva.example.com --api-key orva_… --test ```bash # Default: handler.js / handler.py based on runtime. -orva deploy ./my-fn --name greeter --runtime node24 +orva deploy ./my-fn --name greeter --runtime node # TypeScript: server compiles at deploy time. CLI auto-detects when # both tsconfig.json and a .ts file are present. -orva deploy ./my-fn-ts --name greeter --runtime node24 +orva deploy ./my-fn-ts --name greeter --runtime node # Python: pick the runtime explicitly. -orva deploy ./py-fn --name greeter --runtime python314 +orva deploy ./py-fn --name greeter --runtime python # Watch the build: stream build logs over SSE, wait for completion, and # exit non-zero if the build fails (great for CI gates). -orva deploy ./src --name greeter --runtime node24 --watch +orva deploy ./src --name greeter --runtime node --watch ``` ### Invoke @@ -642,7 +642,7 @@ inline: ```bash orva backup download -f /tmp/pre-deploy.tar.gz \ - && orva deploy ./big-refactor --name greeter --runtime node24 + && orva deploy ./big-refactor --name greeter --runtime node # If the deploy goes sideways: orva backup restore /tmp/pre-deploy.tar.gz --yes ``` @@ -657,7 +657,7 @@ to leak between jobs: ORVA_API_KEY: ${{ secrets.ORVA_API_KEY }} run: | orva --endpoint https://orva.example.com deploy ./fn \ - --name greeter --runtime node24 + --name greeter --runtime node ``` **Backup retention.** A typical homelab keeps 7 daily + 4 weekly + diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 8bf945c..83e9e25 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -49,10 +49,8 @@ backend/ handlers/ one file per concern (functions, invoke, secrets, ...) handlers/respond/ error envelope + Retry-After helpers runtimes/ - node22/adapter.js - node24/adapter.js - python313/adapter.py - python314/adapter.py + node/adapter.js + python/adapter.py frontend/ src/ diff --git a/docs/GVISOR.md b/docs/GVISOR.md index 284cf84..19cb758 100644 --- a/docs/GVISOR.md +++ b/docs/GVISOR.md @@ -82,7 +82,7 @@ flags the daemon would use shows the exact cause: ``` $ docker exec orva-under-runsc \ - /usr/local/bin/nsjail -v -Mo --chroot /var/lib/orva/rootfs/node24 \ + /usr/local/bin/nsjail -v -Mo --chroot /var/lib/orva/rootfs/node \ -T /tmp -- /usr/local/bin/node --version [D] runChild():535 Creating new process with clone flags: @@ -189,7 +189,7 @@ until curl -fsS http://localhost:28443/api/v1/system/health >/dev/null; do sleep # 4. Reproduce the clone() rejection directly with nsjail: docker exec orva-gvisor-test \ /usr/local/bin/nsjail -v -Mo \ - --chroot /var/lib/orva/rootfs/node24 \ + --chroot /var/lib/orva/rootfs/node \ -T /tmp -- /usr/local/bin/node --version 2>&1 | tail -5 # Expected: # clone(flags=CLONE_NEWNS|CLONE_NEWCGROUP|CLONE_NEWUTS|CLONE_NEWIPC| diff --git a/docs/RUNTIMES.md b/docs/RUNTIMES.md index ab3082b..0f17472 100644 --- a/docs/RUNTIMES.md +++ b/docs/RUNTIMES.md @@ -6,12 +6,10 @@ the sandbox your function file is at `/code/` (default adapter wraps your handler and speaks a JSON frame protocol over stdin/stdout to the parent `orvad` process. -| runtime | base image | entrypoint default | dependency file | -|------------|----------------|--------------------|--------------------| -| `node22` | node:22-slim | `handler.js` | `package.json` | -| `node24` | node:24-slim | `handler.js` | `package.json` | -| `python313`| python:3.13-slim | `handler.py` | `requirements.txt` | -| `python314`| python:3.14-slim | `handler.py` | `requirements.txt` | +| runtime | base image | entrypoint default | dependency file | +|----------|------------------|--------------------|--------------------| +| `node` | node:24-slim | `handler.js` | `package.json` | +| `python` | python:3.14-slim | `handler.py` | `requirements.txt` | ## The `event` object @@ -93,7 +91,7 @@ def handler(req): ``` The adapter looks for a top-level `handler` callable. Async (`async def`) -is supported on Python 3.13+; the adapter awaits if needed. +is supported on the python runtime (Python 3.14); the adapter awaits if needed. ## Dependencies @@ -192,7 +190,7 @@ See [`docs/CONFIG.md`](CONFIG.md) for everything tunable. # 1. Create the function curl -X POST -H "X-Orva-API-Key: $KEY" -H 'content-type: application/json' \ http://localhost:8443/api/v1/functions \ - -d '{"name":"my-fn","runtime":"node22","memory_mb":128,"cpus":1}' + -d '{"name":"my-fn","runtime":"node","memory_mb":128,"cpus":1}' # 2. Deploy code (inline) curl -X POST -H "X-Orva-API-Key: $KEY" -H 'content-type: application/json' \ diff --git a/docs/SECURITY.md b/docs/SECURITY.md index c51542e..5f503d5 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -69,7 +69,7 @@ KEY=$(docker exec orva cat /var/lib/orva/.admin-key) curl -X POST -H "X-Orva-API-Key: $KEY" -H 'content-type: application/json' \ http://localhost:8443/api/v1/functions \ - -d '{"name":"whoami","runtime":"node22","memory_mb":128,"cpus":1}' + -d '{"name":"whoami","runtime":"node","memory_mb":128,"cpus":1}' FID=$(curl -s -H "X-Orva-API-Key: $KEY" http://localhost:8443/api/v1/functions \ | jq -r '.functions[] | select(.name=="whoami") | .id') diff --git a/docs/reference.md b/docs/reference.md index 9d1227d..3cd7889 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -57,12 +57,13 @@ JSON-encoded by the adapter. **Runtime env:** env vars and secrets land in `process.env` (Node) / `os.environ` (Python). -| Runtime | ID | Entrypoint | Dependencies | -|---|---|---|---| -| Python 3.14 | `python314` | `handler.py` | `requirements.txt` | -| Python 3.13 | `python313` | `handler.py` | `requirements.txt` | -| Node.js 24 | `node24` | `handler.js` | `package.json` | -| Node.js 22 | `node22` | `handler.js` | `package.json` | +Orva offers two runtimes, latest-stable only. The ID is generic (`node` / +`python`); the version column shows what they currently track. + +| Runtime | ID | Version | Entrypoint | Dependencies | +|---|---|---|---|---| +| Python | `python` | 3.14 | `handler.py` | `requirements.txt` | +| Node.js | `node` | 24 | `handler.js` | `package.json` | --- @@ -78,7 +79,7 @@ stream `/api/v1/deployments//stream` until `phase: done`. curl -X POST {{ORIGIN}}/api/v1/functions \ -H 'X-Orva-API-Key: ' \ -H 'Content-Type: application/json' \ - -d '{"name":"hello","runtime":"python314","memory_mb":128,"cpus":0.5}' + -d '{"name":"hello","runtime":"python","memory_mb":128,"cpus":0.5}' ``` ### 2. Upload code @@ -966,10 +967,10 @@ Orva is a self-hosted serverless platform — think Cloudflare Workers / Vercel -Pick exactly one — Orva has no Docker, no buildpacks, no per-function Python/Node version pinning beyond this: -- python314 (default) or python313 — entry: handler.py — deps: requirements.txt -- node24 (default) or node22 — entry: handler.js — deps: package.json -Older minor versions auto-migrate to the latest patch on next deploy. Native modules (psycopg2-binary, sharp, bcrypt, etc.) are supported via prebuilt wheels / npm prebuilts; if a dep needs a system library not present in the runtime image, the build will fail with a clear error. +Pick exactly one — Orva has no Docker, no buildpacks, no per-function version pinning. Two runtimes, generic ids, latest-stable only: +- python (Python 3.14) — entry: handler.py — deps: requirements.txt +- node (Node.js 24, also runs TypeScript) — entry: handler.js — deps: package.json +Native modules (psycopg2-binary, sharp, bcrypt, etc.) are supported via prebuilt wheels / npm prebuilts; if a dep needs a system library not present in the runtime image, the build will fail with a clear error. @@ -1596,10 +1597,10 @@ orva init # is present; else uses the runtime default (handler.js / handler.py). orva deploy ./my-fn \ --name resize-image \ - --runtime node24 + --runtime node # Override the entrypoint explicitly: -orva deploy ./my-fn --name api --runtime python314 --entrypoint app.py +orva deploy ./my-fn --name api --runtime python --entrypoint app.py ``` #### Invoke + tail logs diff --git a/frontend/public/docs.md b/frontend/public/docs.md index 9d1227d..3cd7889 100644 --- a/frontend/public/docs.md +++ b/frontend/public/docs.md @@ -57,12 +57,13 @@ JSON-encoded by the adapter. **Runtime env:** env vars and secrets land in `process.env` (Node) / `os.environ` (Python). -| Runtime | ID | Entrypoint | Dependencies | -|---|---|---|---| -| Python 3.14 | `python314` | `handler.py` | `requirements.txt` | -| Python 3.13 | `python313` | `handler.py` | `requirements.txt` | -| Node.js 24 | `node24` | `handler.js` | `package.json` | -| Node.js 22 | `node22` | `handler.js` | `package.json` | +Orva offers two runtimes, latest-stable only. The ID is generic (`node` / +`python`); the version column shows what they currently track. + +| Runtime | ID | Version | Entrypoint | Dependencies | +|---|---|---|---|---| +| Python | `python` | 3.14 | `handler.py` | `requirements.txt` | +| Node.js | `node` | 24 | `handler.js` | `package.json` | --- @@ -78,7 +79,7 @@ stream `/api/v1/deployments//stream` until `phase: done`. curl -X POST {{ORIGIN}}/api/v1/functions \ -H 'X-Orva-API-Key: ' \ -H 'Content-Type: application/json' \ - -d '{"name":"hello","runtime":"python314","memory_mb":128,"cpus":0.5}' + -d '{"name":"hello","runtime":"python","memory_mb":128,"cpus":0.5}' ``` ### 2. Upload code @@ -966,10 +967,10 @@ Orva is a self-hosted serverless platform — think Cloudflare Workers / Vercel -Pick exactly one — Orva has no Docker, no buildpacks, no per-function Python/Node version pinning beyond this: -- python314 (default) or python313 — entry: handler.py — deps: requirements.txt -- node24 (default) or node22 — entry: handler.js — deps: package.json -Older minor versions auto-migrate to the latest patch on next deploy. Native modules (psycopg2-binary, sharp, bcrypt, etc.) are supported via prebuilt wheels / npm prebuilts; if a dep needs a system library not present in the runtime image, the build will fail with a clear error. +Pick exactly one — Orva has no Docker, no buildpacks, no per-function version pinning. Two runtimes, generic ids, latest-stable only: +- python (Python 3.14) — entry: handler.py — deps: requirements.txt +- node (Node.js 24, also runs TypeScript) — entry: handler.js — deps: package.json +Native modules (psycopg2-binary, sharp, bcrypt, etc.) are supported via prebuilt wheels / npm prebuilts; if a dep needs a system library not present in the runtime image, the build will fail with a clear error. @@ -1596,10 +1597,10 @@ orva init # is present; else uses the runtime default (handler.js / handler.py). orva deploy ./my-fn \ --name resize-image \ - --runtime node24 + --runtime node # Override the entrypoint explicitly: -orva deploy ./my-fn --name api --runtime python314 --entrypoint app.py +orva deploy ./my-fn --name api --runtime python --entrypoint app.py ``` #### Invoke + tail logs diff --git a/frontend/src/components/common/CodeEditor.vue b/frontend/src/components/common/CodeEditor.vue index a248ce9..57a6150 100644 --- a/frontend/src/components/common/CodeEditor.vue +++ b/frontend/src/components/common/CodeEditor.vue @@ -19,7 +19,7 @@ const props = defineProps({ default: '' }, // Accepts either the codemirror language id (`javascript`, `python`) or - // an Orva runtime id (`node22`, `python314`, ...) — the editor maps both + // an Orva runtime id (`node`, `python`) — the editor maps both // shapes onto the same CM language extension. language: { type: String, diff --git a/frontend/src/templates/index.js b/frontend/src/templates/index.js index dcfc03a..a8a596d 100644 --- a/frontend/src/templates/index.js +++ b/frontend/src/templates/index.js @@ -1473,19 +1473,15 @@ const nodeTemplates = [ // Indexed by runtime — Editor.vue uses this directly. export const templates = { - python313: pythonTemplates, - python314: pythonTemplates, - node22: nodeTemplates, - node24: nodeTemplates, + python: pythonTemplates, + node: nodeTemplates, } // Default code (the "HTTP Hello" template) per runtime — used when the // Editor picks a runtime and the editor is empty. export const defaultCode = { - python313: py_http_hello, - python314: py_http_hello, - node22: node_http_hello, - node24: node_http_hello, + python: py_http_hello, + node: node_http_hello, } // Categories in display order — used by the picker UX to group entries. diff --git a/frontend/src/utils/aiPrompts.js b/frontend/src/utils/aiPrompts.js index 8951d85..73923b3 100644 --- a/frontend/src/utils/aiPrompts.js +++ b/frontend/src/utils/aiPrompts.js @@ -20,10 +20,10 @@ Orva is a self-hosted serverless platform — think Cloudflare Workers / Vercel -Pick exactly one — Orva has no Docker, no buildpacks, no per-function Python/Node version pinning beyond this: -- python314 (default) or python313 — entry: handler.py — deps: requirements.txt -- node24 (default) or node22 — entry: handler.js — deps: package.json -Older minor versions auto-migrate to the latest patch on next deploy. Native modules (psycopg2-binary, sharp, bcrypt, etc.) are supported via prebuilt wheels / npm prebuilts; if a dep needs a system library not present in the runtime image, the build will fail with a clear error. +Pick exactly one — Orva has no Docker, no buildpacks, no per-function version pinning. Two runtimes, generic ids, latest-stable only: +- python (Python 3.14) — entry: handler.py — deps: requirements.txt +- node (Node.js 24, also runs TypeScript) — entry: handler.js — deps: package.json +Native modules (psycopg2-binary, sharp, bcrypt, etc.) are supported via prebuilt wheels / npm prebuilts; if a dep needs a system library not present in the runtime image, the build will fail with a clear error. @@ -500,9 +500,9 @@ const truncateStderr = (s) => { return `[truncated — original was ${bytes.length} bytes; showing first ${STDERR_CAP_BYTES}]\n${head}` } -// Map runtime → language attr on the tag. The runtime string -// arrives as the platform's canonical form (python314, node24, …); we -// collapse to the broad family the model recognises. +// Map runtime → language attr on the tag. The runtime string is the +// platform's canonical id (python, node); collapse to the broad family the +// model recognises. const sourceLanguageFor = (runtime) => { if (!runtime) return 'text' const r = String(runtime).toLowerCase() @@ -512,17 +512,13 @@ const sourceLanguageFor = (runtime) => { return 'text' } -// Pretty-print a runtime tag for the blurb. python314 → -// "Python 3.14"; node24 → "Node.js 24"; falls back to the raw id. -// Python tags pack {major}{minor:2}, Node tags pack {major:2} — so -// Python splits at first/rest, Node uses the whole numeric tail. +// Pretty-print a runtime tag for the blurb. node → "Node.js 24", +// python → "Python 3.14"; falls back to the raw id for anything unexpected. const formatRuntime = (runtime) => { if (!runtime) return 'unknown runtime' const r = String(runtime).toLowerCase() - let m = r.match(/^python(\d)(\d+)$/) - if (m) return `Python ${m[1]}.${m[2]}` - m = r.match(/^node(\d+)$/) - if (m) return `Node.js ${m[1]}` + if (r === 'node') return 'Node.js 24' + if (r === 'python') return 'Python 3.14' return runtime } @@ -559,7 +555,7 @@ const formatRequest = (req) => { export const buildFixSuggestionPrompt = ({ source = '', language, // optional — falls back to detection from runtime - runtime = '', // canonical Orva runtime id (python314 / node24 / …) + runtime = '', // canonical Orva runtime id (node / python) stderr = '', requestPreview = null, errorMessage = '', diff --git a/frontend/src/views/Docs.vue b/frontend/src/views/Docs.vue index c035d44..c6186bf 100644 --- a/frontend/src/views/Docs.vue +++ b/frontend/src/views/Docs.vue @@ -1441,10 +1441,8 @@ print(r.json())`, ]) const runtimes = [ - { id: 'python314', name: 'Python 3.14', entry: 'handler.py', deps: 'requirements.txt', icon: PythonGlyph }, - { id: 'python313', name: 'Python 3.13', entry: 'handler.py', deps: 'requirements.txt', icon: PythonGlyph }, - { id: 'node24', name: 'Node.js 24', entry: 'handler.js', deps: 'package.json', icon: NodeGlyph }, - { id: 'node22', name: 'Node.js 22', entry: 'handler.js', deps: 'package.json', icon: NodeGlyph }, + { id: 'python', name: 'Python 3.14', entry: 'handler.py', deps: 'requirements.txt', icon: PythonGlyph }, + { id: 'node', name: 'Node.js 24', entry: 'handler.js', deps: 'package.json', icon: NodeGlyph }, ] const configRows = [ @@ -1458,7 +1456,7 @@ const configRows = [ const curlCreate = computed(() => `curl -X POST ${origin.value}/api/v1/functions \\ -H 'X-Orva-API-Key: ' \\ -H 'Content-Type: application/json' \\ - -d '{"name":"hello","runtime":"python314","memory_mb":128,"cpus":0.5}'`) + -d '{"name":"hello","runtime":"python","memory_mb":128,"cpus":0.5}'`) const curlDeploy = computed(() => `tar czf code.tar.gz handler.py requirements.txt curl -X POST ${origin.value}/api/v1/functions//deploy \\ @@ -1800,10 +1798,10 @@ orva init # is present; else uses the runtime default (handler.js / handler.py). orva deploy ./my-fn \\ --name resize-image \\ - --runtime node24 + --runtime node # Override the entrypoint explicitly: -orva deploy ./my-fn --name api --runtime python314 --entrypoint app.py` +orva deploy ./my-fn --name api --runtime python --entrypoint app.py` const cliInvokeLogs = `# Invoke a function by name or fn_: orva invoke resize-image --data '{"url":"https://example.com/cat.jpg"}' diff --git a/frontend/src/views/Editor.vue b/frontend/src/views/Editor.vue index 3f0cb90..8b04adf 100644 --- a/frontend/src/views/Editor.vue +++ b/frontend/src/views/Editor.vue @@ -1292,17 +1292,14 @@ const terminalTabs = computed(() => [ { id: 'test', label: 'Test', icon: Play, badge: invokeLogs.value.length || null }, ]) -// Pretty runtime label for the editor strip — "python314" → "Python 3.14", -// "node24" → "Node.js 24". Anything we don't recognize falls back to the -// raw id so an unknown runtime still surfaces something visible. +// Pretty runtime label for the editor strip. Orva exposes two generic +// runtimes — "node" → "Node.js 24", "python" → "Python 3.14" — where the +// concrete version is a display detail. Unknown ids fall back to the raw value. const runtimeShort = (rt) => { if (!rt) return '' - const m = /^(python|node)(\d)(\d+)$/.exec(rt) - if (!m) return rt - const family = m[1] === 'python' ? 'Python' : 'Node.js' - const major = m[2] - const minor = m[3] - return m[1] === 'python' ? `${family} ${major}.${minor}` : `${family} ${major}` + if (rt === 'node') return 'Node.js 24' + if (rt === 'python') return 'Python 3.14' + return rt } const envVarCount = computed(() => envVars.value.filter((p) => p.key.trim()).length) @@ -1310,7 +1307,7 @@ const code = ref('') const form = ref({ name: '', description: '', // surfaces in list_functions, get_function, the channel picker, and as the MCP tool description - runtime: 'python314', + runtime: 'python', memory_mb: 64, cpus: 0.5, network_mode: 'none', // 'none' | 'egress' @@ -1407,18 +1404,16 @@ const isEditing = computed(() => !!route.params.name) const isDeployed = computed(() => isEditing.value || deployedThisSession.value) const canTest = computed(() => isDeployed.value && !deploying.value) -// Supported runtimes: latest two stable majors per language. The user -// picks version explicitly; existing functions on EOL runtimes (node20, -// python312) are auto-migrated one step up on server startup. +// Orva offers two runtimes, latest-stable only. The id is generic (node / +// python); the version shown here is a display label that tracks latest-stable. +// Functions on legacy versioned ids are migrated to these on server startup. const runtimes = [ - { id: 'python314', label: 'Python 3.14' }, - { id: 'python313', label: 'Python 3.13' }, - { id: 'node24', label: 'Node.js 24 (LTS)' }, - { id: 'node22', label: 'Node.js 22 (LTS)' }, + { id: 'python', label: 'Python 3.14' }, + { id: 'node', label: 'Node.js 24' }, ] -const isPythonRuntime = (rt) => rt === 'python313' || rt === 'python314' -const isNodeRuntime = (rt) => rt === 'node22' || rt === 'node24' +const isPythonRuntime = (rt) => rt === 'python' +const isNodeRuntime = (rt) => rt === 'node' const fileName = computed(() => { if (isPythonRuntime(form.value.runtime)) return 'handler.py' @@ -1512,10 +1507,10 @@ const scheduleDetect = (src) => { const isPy = isPythonRuntime(form.value.runtime) const isNode = isNodeRuntime(form.value.runtime) if (lang === 'python' && !isPy) { - form.value.runtime = 'python314' + form.value.runtime = 'python' autoDetected.value = true } else if (lang === 'node' && !isNode) { - form.value.runtime = 'node24' + form.value.runtime = 'node' autoDetected.value = true } }, 400) @@ -1693,7 +1688,7 @@ const resetEditorState = () => { form.value = { name: '', description: '', - runtime: 'python314', + runtime: 'python', memory_mb: 64, cpus: 0.5, network_mode: 'none', @@ -1775,7 +1770,7 @@ const loadRouteData = async () => { // Create mode (/functions/new). Seed a friendly auto-generated // name so the field isn't empty. The user can edit it, clear it, // or hit the re-roll button next to it. - setRuntime('python314') + setRuntime('python') form.value.name = generateFunctionName() } } @@ -2581,7 +2576,7 @@ const resetForm = async () => { deployedThisSession.value = false output.value = null error.value = null - setRuntime('python314') + setRuntime('python') } diff --git a/scripts/build-rootfs.sh b/scripts/build-rootfs.sh index 1299c18..4512927 100755 --- a/scripts/build-rootfs.sh +++ b/scripts/build-rootfs.sh @@ -2,7 +2,7 @@ # build-rootfs.sh — extract a minimal language rootfs from an official image. # # Usage: build-rootfs.sh -# runtime: node22 | node24 | python313 | python314 +# runtime: node | python (latest-stable only: Node.js 24 / Python 3.14) # # The resulting rootfs is a directory usable as an nsjail --chroot target. # It contains /usr/local/bin/node (or python3), all required shared libs, @@ -14,15 +14,13 @@ set -euo pipefail out="${1:-}" runtime="${2:-}" if [[ -z "$out" || -z "$runtime" ]]; then - echo "usage: $0 " >&2 + echo "usage: $0 " >&2 exit 2 fi case "$runtime" in - node22) image="node:22-slim" ;; - node24) image="node:24-slim" ;; - python313) image="python:3.13-slim" ;; - python314) image="python:3.14-slim" ;; + node) image="node:24-slim" ;; + python) image="python:3.14-slim" ;; *) echo "unsupported runtime: $runtime" >&2; exit 2 ;; esac @@ -46,12 +44,12 @@ docker export "$tmp" | tar -xf - -C "$out" # nsjail expects the language binary at /usr/local/bin/. case "$runtime" in - node22|node24) + node) if [[ ! -x "$out/usr/local/bin/node" ]]; then ln -sf "$(readlink -f "$out/usr/local/bin/node" 2>/dev/null || echo /usr/local/bin/node)" "$out/usr/local/bin/node" || true fi ;; - python313|python314) + python) if [[ ! -x "$out/usr/local/bin/python3" ]]; then (cd "$out/usr/local/bin" && ln -sf python python3) || true fi diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh index f14ff82..048b9f3 100644 --- a/scripts/entrypoint.sh +++ b/scripts/entrypoint.sh @@ -12,7 +12,7 @@ set -e IMAGE_ROOTFS=/opt/orva/rootfs VOLUME_ROOTFS=/var/lib/orva/rootfs -RUNTIMES="node22 node24 python313 python314" +RUNTIMES="node python" for rt in $RUNTIMES; do # If the volume's rootfs for this runtime is empty, seed it from the image. @@ -26,21 +26,19 @@ done # Always refresh the adapters + bundled SDK so image upgrades roll out # even when the user has an existing volume. Includes the orva module # (kv / invoke / jobs) introduced in v0.2. -for rt in node22 node24; do - mkdir -p "$VOLUME_ROOTFS/$rt/opt/orva" - cp "$IMAGE_ROOTFS/$rt/opt/orva/adapter.js" "$VOLUME_ROOTFS/$rt/opt/orva/adapter.js" - if [ -d "$IMAGE_ROOTFS/$rt/opt/orva/node_modules/orva" ]; then - mkdir -p "$VOLUME_ROOTFS/$rt/opt/orva/node_modules/orva" - cp -a "$IMAGE_ROOTFS/$rt/opt/orva/node_modules/orva/." "$VOLUME_ROOTFS/$rt/opt/orva/node_modules/orva/" - fi -done -for rt in python313 python314; do - mkdir -p "$VOLUME_ROOTFS/$rt/opt/orva" - cp "$IMAGE_ROOTFS/$rt/opt/orva/adapter.py" "$VOLUME_ROOTFS/$rt/opt/orva/adapter.py" - if [ -f "$IMAGE_ROOTFS/$rt/opt/orva/orva.py" ]; then - cp "$IMAGE_ROOTFS/$rt/opt/orva/orva.py" "$VOLUME_ROOTFS/$rt/opt/orva/orva.py" - fi -done +rt=node +mkdir -p "$VOLUME_ROOTFS/$rt/opt/orva" +cp "$IMAGE_ROOTFS/$rt/opt/orva/adapter.js" "$VOLUME_ROOTFS/$rt/opt/orva/adapter.js" +if [ -d "$IMAGE_ROOTFS/$rt/opt/orva/node_modules/orva" ]; then + mkdir -p "$VOLUME_ROOTFS/$rt/opt/orva/node_modules/orva" + cp -a "$IMAGE_ROOTFS/$rt/opt/orva/node_modules/orva/." "$VOLUME_ROOTFS/$rt/opt/orva/node_modules/orva/" +fi +rt=python +mkdir -p "$VOLUME_ROOTFS/$rt/opt/orva" +cp "$IMAGE_ROOTFS/$rt/opt/orva/adapter.py" "$VOLUME_ROOTFS/$rt/opt/orva/adapter.py" +if [ -f "$IMAGE_ROOTFS/$rt/opt/orva/orva.py" ]; then + cp "$IMAGE_ROOTFS/$rt/opt/orva/orva.py" "$VOLUME_ROOTFS/$rt/opt/orva/orva.py" +fi mkdir -p /var/lib/orva/functions diff --git a/scripts/install.sh b/scripts/install.sh index 017f5b3..a78f5bc 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -652,7 +652,7 @@ download_rootfs() { [ "$DRYRUN" = "1" ] && { log "(dryrun) would download runtime rootfs tarballs"; return; } base="https://github.com/${REPO}/releases/download/${VERSION}" install -d -m 0755 "$DATA_DIR/rootfs" - for dr_rt in node22 node24 python313 python314; do + for dr_rt in node python; do dr_target="$DATA_DIR/rootfs/$dr_rt" if [ -f "$dr_target/.orva-rootfs-version" ] && [ "$(cat "$dr_target/.orva-rootfs-version" 2>/dev/null)" = "$VERSION" ]; then @@ -717,7 +717,11 @@ ReadWritePaths=${DATA_DIR} ProtectSystem=strict ProtectHome=true PrivateTmp=true -ProtectKernelTunables=true +# ProtectKernelTunables is intentionally OMITTED: it overmounts /proc/sys, +# which prevents nsjail from mounting a fresh procfs inside its user +# namespace (functions would crash with 'Failed to mount mandatory point +# /proc'). nsjail already isolates each function heavily (userns + seccomp +# + chroot), so the marginal loss is acceptable. ProtectKernelModules=true LimitNOFILE=65536 LimitNPROC=8192 diff --git a/test/api-smoke.sh b/test/api-smoke.sh index f137ca9..2df1204 100755 --- a/test/api-smoke.sh +++ b/test/api-smoke.sh @@ -41,7 +41,7 @@ expect_code "GET /api/v1/auth/status" 200 "$code" fn_name="smoke-$$" create=$("${CURL[@]}" -X POST "$BASE/api/v1/functions" \ -H "Content-Type: application/json" \ - -d "{\"name\":\"$fn_name\",\"runtime\":\"node24\",\"network_mode\":\"egress\"}") + -d "{\"name\":\"$fn_name\",\"runtime\":\"node\",\"network_mode\":\"egress\"}") fid=$(echo "$create" | jq -r '.id') mode=$(echo "$create" | jq -r '.network_mode') [ "$mode" = "egress" ] && expect_code "POST /functions {network_mode:egress}" 201 201 \ diff --git a/test/atscale.sh b/test/atscale.sh index d5a7b47..097806e 100755 --- a/test/atscale.sh +++ b/test/atscale.sh @@ -40,23 +40,23 @@ trap 'rm -rf "$TMPDIR"' EXIT # 20 fixtures: 5 of each runtime mix. FN_NAMES=() -for i in $(seq 1 5); do FN_NAMES+=("ascale-node22-$i"); done -for i in $(seq 1 5); do FN_NAMES+=("ascale-node24-$i"); done +for i in $(seq 1 5); do FN_NAMES+=("ascale-node-$i"); done +for i in $(seq 1 5); do FN_NAMES+=("ascale-node-$i"); done for i in $(seq 1 5); do FN_NAMES+=("ascale-py313-$i"); done for i in $(seq 1 5); do FN_NAMES+=("ascale-py314-$i"); done runtime_for() { case "$1" in - ascale-node22-*) echo node22 ;; - ascale-node24-*) echo node24 ;; - ascale-py313-*) echo python313 ;; - ascale-py314-*) echo python314 ;; + ascale-node-*) echo node ;; + ascale-node-*) echo node ;; + ascale-py313-*) echo python ;; + ascale-py314-*) echo python ;; esac } handler_for() { case "$1" in - ascale-node22-*|ascale-node24-*) + ascale-node-*|ascale-node-*) cat <<'EOF' exports.handler = async (event) => ({ statusCode: 200, @@ -81,7 +81,7 @@ EOF filename_for() { case "$1" in - ascale-node22-*|ascale-node24-*) echo handler.js ;; + ascale-node-*|ascale-node-*) echo handler.js ;; ascale-py313-*|ascale-py314-*) echo handler.py ;; esac } diff --git a/test/auth-test.sh b/test/auth-test.sh index cad652c..2ba88de 100755 --- a/test/auth-test.sh +++ b/test/auth-test.sh @@ -31,7 +31,7 @@ code='exports.handler = async (event) => ({ statusCode: 200, headers: {"Content- status=$("${CURL_AUTH[@]}" -o /dev/null -w '%{http_code}' \ -X POST "$BASE/api/v1/functions" \ -H 'Content-Type: application/json' \ - -d '{"name":"auth-bogus-'$$'","runtime":"node24","auth_mode":"wat"}') + -d '{"name":"auth-bogus-'$$'","runtime":"node","auth_mode":"wat"}') check "reject auth_mode=wat" "$([ "$status" = 400 ] && echo ok || echo fail)" "status=$status" # --------------------------------------------------------------- @@ -40,7 +40,7 @@ check "reject auth_mode=wat" "$([ "$status" = 400 ] && echo ok || echo fail)" "s fn_none="auth-none-$$" fid_none=$("${CURL_AUTH[@]}" -X POST "$BASE/api/v1/functions" \ -H 'Content-Type: application/json' \ - -d "{\"name\":\"$fn_none\",\"runtime\":\"node24\",\"auth_mode\":\"none\"}" | jq -r '.id') + -d "{\"name\":\"$fn_none\",\"runtime\":\"node\",\"auth_mode\":\"none\"}" | jq -r '.id') "${CURL_AUTH[@]}" -X POST "$BASE/api/v1/functions/$fid_none/deploy-inline" \ -H 'Content-Type: application/json' \ -d "$(jq -n --arg c "$code" '{code:$c, filename:"handler.js"}')" > /dev/null @@ -63,7 +63,7 @@ check "auth=none allows public invoke" "$([ "$status" = 200 ] && echo ok || echo fn_key="auth-key-$$" fid_key=$("${CURL_AUTH[@]}" -X POST "$BASE/api/v1/functions" \ -H 'Content-Type: application/json' \ - -d "{\"name\":\"$fn_key\",\"runtime\":\"node24\",\"auth_mode\":\"platform_key\"}" | jq -r '.id') + -d "{\"name\":\"$fn_key\",\"runtime\":\"node\",\"auth_mode\":\"platform_key\"}" | jq -r '.id') "${CURL_AUTH[@]}" -X POST "$BASE/api/v1/functions/$fid_key/deploy-inline" \ -H 'Content-Type: application/json' \ -d "$(jq -n --arg c "$code" '{code:$c, filename:"handler.js"}')" > /dev/null @@ -93,7 +93,7 @@ check "auth=platform_key rejects bad key" "$([ "$status" = 401 ] && echo ok || e fn_sig="auth-signed-$$" fid_sig=$("${CURL_AUTH[@]}" -X POST "$BASE/api/v1/functions" \ -H 'Content-Type: application/json' \ - -d "{\"name\":\"$fn_sig\",\"runtime\":\"node24\",\"auth_mode\":\"signed\"}" | jq -r '.id') + -d "{\"name\":\"$fn_sig\",\"runtime\":\"node\",\"auth_mode\":\"signed\"}" | jq -r '.id') # Set the signing secret BEFORE first deploy. SIGNING_SECRET="test-secret-$(date +%s)" @@ -150,7 +150,7 @@ check "auth=signed rejects stale timestamp" "$([ "$status" = 401 ] && echo ok || fn_rl="auth-rl-$$" fid_rl=$("${CURL_AUTH[@]}" -X POST "$BASE/api/v1/functions" \ -H 'Content-Type: application/json' \ - -d "{\"name\":\"$fn_rl\",\"runtime\":\"node24\",\"auth_mode\":\"none\",\"rate_limit_per_min\":5}" | jq -r '.id') + -d "{\"name\":\"$fn_rl\",\"runtime\":\"node\",\"auth_mode\":\"none\",\"rate_limit_per_min\":5}" | jq -r '.id') "${CURL_AUTH[@]}" -X POST "$BASE/api/v1/functions/$fid_rl/deploy-inline" \ -H 'Content-Type: application/json' \ -d "$(jq -n --arg c "$code" '{code:$c, filename:"handler.js"}')" > /dev/null diff --git a/test/e2e/CHECKLIST.md b/test/e2e/CHECKLIST.md index ca84bee..f302fda 100644 --- a/test/e2e/CHECKLIST.md +++ b/test/e2e/CHECKLIST.md @@ -4,28 +4,30 @@ > overwrites it. When a module FAILs, fix the code or harden the test; > when you add a feature, add a module here. -- **Last run:** 2026-06-01 14:15:36Z +- **Last run:** 2026-06-04 10:47:18Z - **Target:** isolated Docker image `orva:e2e` (http://127.0.0.1:8455) -- **Modules:** 23 passed, 0 failed, 1 skipped -- **Checks:** 451 passed, 0 failed +- **Modules:** 25 passed, 0 failed, 1 skipped +- **Checks:** 512 passed, 0 failed | Module | Status | Checks (pass/fail) | |---|---|---| | `test_ai_advanced.py` | ✅ PASS | 27/0 | -| `test_ai_chat.py` | ✅ PASS | 20/0 | +| `test_ai_chat.py` | ✅ PASS | 23/0 | | `test_ai_conversations.py` | ✅ PASS | 26/0 | -| `test_ai_perms.py` | ✅ PASS | 7/0 | +| `test_ai_edit.py` | ✅ PASS | 23/0 | +| `test_ai_perms.py` | ✅ PASS | 9/0 | | `test_ai_providers.py` | ✅ PASS | 22/0 | | `test_ai_settings.py` | ✅ PASS | 15/0 | | `test_auth.py` | ✅ PASS | 15/0 | | `test_backup.py` | ✅ PASS | 11/0 | | `test_channels.py` | ✅ PASS | 21/0 | -| `test_cli.py` | ✅ PASS | 21/0 | +| `test_cli.py` | ✅ PASS | 39/0 | +| `test_cli_chat.py` | ✅ PASS | 11/0 | | `test_cron.py` | ✅ PASS | 19/0 | | `test_deploy_invoke.py` | ⚠️ SKIP | 0/0 | | `test_firewall.py` | ✅ PASS | 30/0 | | `test_fixtures.py` | ✅ PASS | 29/0 | -| `test_functions.py` | ✅ PASS | 9/0 | +| `test_functions.py` | ✅ PASS | 13/0 | | `test_inbound_webhooks.py` | ✅ PASS | 23/0 | | `test_jobs.py` | ✅ PASS | 21/0 | | `test_keys.py` | ✅ PASS | 17/0 | diff --git a/test/e2e/tests/test_ai_chat.py b/test/e2e/tests/test_ai_chat.py index 5d5f376..775fa40 100644 --- a/test/e2e/tests/test_ai_chat.py +++ b/test/e2e/tests/test_ai_chat.py @@ -61,7 +61,7 @@ def main(): # ── 2. write tool pauses for approval, then approve runs it ─────── section("write tool requires approval, then approve (create_function)") cleanup_fn(c) - args = ('{"name":"%s","description":"e2e mock","runtime":"node24",' + args = ('{"name":"%s","description":"e2e mock","runtime":"node",' '"entrypoint":"handler.js","timeout_ms":30000,"memory_mb":128,' '"cpus":1,"network_mode":"none","auth_mode":"none"}') % TEST_FN frames, conv = c.chat("CALL create_function " + args) diff --git a/test/e2e/tests/test_ai_perms.py b/test/e2e/tests/test_ai_perms.py index 9683292..19e1afc 100644 --- a/test/e2e/tests/test_ai_perms.py +++ b/test/e2e/tests/test_ai_perms.py @@ -37,9 +37,9 @@ RO_KEY_NAME = "e2e-perm-readonly" PERM_FN = "e2e-perm-fn" -# create_function args used to probe the write path. node24 / minimal limits. +# create_function args used to probe the write path. node / minimal limits. CREATE_ARGS = ( - '{"name":"%s","description":"x","runtime":"node24",' + '{"name":"%s","description":"x","runtime":"node",' '"entrypoint":"handler.js","timeout_ms":30000,"memory_mb":128,' '"cpus":1,"network_mode":"none","auth_mode":"none"}' ) % PERM_FN diff --git a/test/e2e/tests/test_channels.py b/test/e2e/tests/test_channels.py index 8230031..11f602c 100644 --- a/test/e2e/tests/test_channels.py +++ b/test/e2e/tests/test_channels.py @@ -28,7 +28,7 @@ def main(): cid = None try: section("setup function") - fbody = {"name": FN, "description": "channel member", "runtime": "node24", + fbody = {"name": FN, "description": "channel member", "runtime": "node", "entrypoint": "handler.js", "timeout_ms": 30000, "memory_mb": 128, "cpus": 1, "network_mode": "none", "auth_mode": "none"} fc, fn = c.req("POST", "/api/v1/functions", fbody, expect=range(200, 599)) diff --git a/test/e2e/tests/test_cli.py b/test/e2e/tests/test_cli.py index 080e475..07d3910 100644 --- a/test/e2e/tests/test_cli.py +++ b/test/e2e/tests/test_cli.py @@ -58,7 +58,7 @@ def main(): section("CLI sees server state (created via REST)") # Create a function through the REST client, then assert the CLI lists it. - body = {"name": NAME, "description": "cli parity", "runtime": "node24", + body = {"name": NAME, "description": "cli parity", "runtime": "node", "entrypoint": "handler.js", "timeout_ms": 30000, "memory_mb": 128, "cpus": 1, "network_mode": "none", "auth_mode": "none"} code, created = c.req("POST", "/api/v1/functions", body, expect=range(200, 599)) @@ -144,7 +144,7 @@ def main(): check("completion script references orva", "orva" in out, out.strip()[:120]) section("orva deploy + invoke (nsjail-dependent; skips if unavailable)") - # Build a trivial node24 source dir and try a real deploy via the CLI. + # Build a trivial node source dir and try a real deploy via the CLI. # If the build sandbox (nsjail) isn't available the deploy won't succeed; # we treat that as a skip of just these two checks, not a failure. deploy_ok = _try_deploy_invoke(cli) @@ -173,7 +173,7 @@ def main(): def _try_deploy_invoke(cli): - """Best-effort: deploy a tiny node24 function to its OWN name via the CLI and + """Best-effort: deploy a tiny node function to its OWN name via the CLI and invoke it. Returns None when the deploy clearly couldn't build (sandbox missing), else a dict describing what happened. Uses a distinct name so it never collides with the REST-created NAME used elsewhere.""" @@ -185,7 +185,7 @@ def _try_deploy_invoke(cli): f.write("export default async function () { return { ok: true }; }\n") rc, out, err = cli.run("deploy", src, "--name", dep_name, - "--runtime", "node24", timeout=120) + "--runtime", "node", timeout=120) blob = (out + err) deployed = ('"status": "succeeded"' in blob or '"status": "deployed"' in blob or '"status": "active"' in blob) diff --git a/test/e2e/tests/test_cli_chat.py b/test/e2e/tests/test_cli_chat.py index 4f1edca..006dc2f 100644 --- a/test/e2e/tests/test_cli_chat.py +++ b/test/e2e/tests/test_cli_chat.py @@ -19,7 +19,7 @@ CREATE_ARGS = ( 'CALL create_function {"name":"%s","description":"e2e cli chat",' - '"runtime":"node24","entrypoint":"handler.js","timeout_ms":30000,' + '"runtime":"node","entrypoint":"handler.js","timeout_ms":30000,' '"memory_mb":128,"cpus":1,"network_mode":"none","auth_mode":"none"}' ) % TEST_FN diff --git a/test/e2e/tests/test_cron.py b/test/e2e/tests/test_cron.py index da1de74..3c792a8 100644 --- a/test/e2e/tests/test_cron.py +++ b/test/e2e/tests/test_cron.py @@ -48,7 +48,7 @@ def main(): sid = None try: section("setup function") - body = {"name": FN_NAME, "description": "cron owner", "runtime": "node24", + body = {"name": FN_NAME, "description": "cron owner", "runtime": "node", "entrypoint": "handler.js", "timeout_ms": 30000, "memory_mb": 128, "cpus": 1, "network_mode": "none", "auth_mode": "none"} fc, fn = c.req("POST", "/api/v1/functions", body, expect=range(200, 599)) diff --git a/test/e2e/tests/test_deploy_invoke.py b/test/e2e/tests/test_deploy_invoke.py index 27eddc6..ddc5cb3 100644 --- a/test/e2e/tests/test_deploy_invoke.py +++ b/test/e2e/tests/test_deploy_invoke.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Full sandboxed loop: create node24 fn -> deploy-inline -> wait active -> invoke -> executions. +"""Full sandboxed loop: create node fn -> deploy-inline -> wait active -> invoke -> executions. Needs nsjail/sandbox. If the build never reaches active (no nested sandboxing in this env), the whole module skips. REST resolves functions by UUID only, so the @@ -59,8 +59,8 @@ def main(): cleanup(c) fid = None try: - section("create node24 function") - body = {"name": NAME, "description": "deploy+invoke e2e", "runtime": "node24", + section("create node function") + body = {"name": NAME, "description": "deploy+invoke e2e", "runtime": "node", "entrypoint": "handler.js", "timeout_ms": 30000, "memory_mb": 128, "cpus": 1, "network_mode": "none", "auth_mode": "none"} code, created = c.req("POST", "/api/v1/functions", body, expect=range(200, 599)) diff --git a/test/e2e/tests/test_fixtures.py b/test/e2e/tests/test_fixtures.py index 877baeb..0d76672 100644 --- a/test/e2e/tests/test_fixtures.py +++ b/test/e2e/tests/test_fixtures.py @@ -37,7 +37,7 @@ def main(): fid = None try: section("setup function") - fnbody = {"name": FN_NAME, "description": "fixture host", "runtime": "node24", + fnbody = {"name": FN_NAME, "description": "fixture host", "runtime": "node", "entrypoint": "handler.js", "timeout_ms": 30000, "memory_mb": 128, "cpus": 1, "network_mode": "none", "auth_mode": "none"} code, created = c.req("POST", "/api/v1/functions", fnbody, expect=range(200, 599)) diff --git a/test/e2e/tests/test_functions.py b/test/e2e/tests/test_functions.py index 6066051..2fa4c51 100644 --- a/test/e2e/tests/test_functions.py +++ b/test/e2e/tests/test_functions.py @@ -23,7 +23,7 @@ def main(): fid = None try: section("create function") - body = {"name": NAME, "description": "crud", "runtime": "node24", + body = {"name": NAME, "description": "crud", "runtime": "node", "entrypoint": "handler.js", "timeout_ms": 30000, "memory_mb": 128, "cpus": 1, "network_mode": "none", "auth_mode": "none"} code, created = c.req("POST", "/api/v1/functions", body, expect=range(200, 599)) @@ -49,6 +49,15 @@ def main(): bc, _ = c.req("POST", "/api/v1/functions", bad, expect=range(200, 599)) check("invalid runtime rejected (4xx)", 400 <= bc < 500, f"status {bc}") + # Strict cutover: the legacy versioned ids are no longer valid runtimes — + # only the generic `node` / `python` are accepted. + for legacy in ("node24", "node22", "python314", "python313"): + lb = {"name": f"e2e-legacy-{legacy}", "description": "x", "runtime": legacy, + "entrypoint": "handler.js", "timeout_ms": 30000, "memory_mb": 128, + "cpus": 1, "network_mode": "none", "auth_mode": "none"} + lc, _ = c.req("POST", "/api/v1/functions", lb, expect=range(200, 599)) + check(f"legacy runtime {legacy} rejected (4xx)", 400 <= lc < 500, f"status {lc}") + section("delete") if fid: dc, _ = c.req("DELETE", f"/api/v1/functions/{fid}", expect=range(200, 599)) diff --git a/test/e2e/tests/test_inbound_webhooks.py b/test/e2e/tests/test_inbound_webhooks.py index 313747f..6069d47 100644 --- a/test/e2e/tests/test_inbound_webhooks.py +++ b/test/e2e/tests/test_inbound_webhooks.py @@ -42,7 +42,7 @@ def main(): try: section("setup function") fbody = {"name": FN_NAME, "description": "inbound webhook host", - "runtime": "node24", "entrypoint": "handler.js", + "runtime": "node", "entrypoint": "handler.js", "timeout_ms": 30000, "memory_mb": 128, "cpus": 1, "network_mode": "none", "auth_mode": "none"} fc, fn = c.req("POST", "/api/v1/functions", fbody, expect=range(200, 599)) diff --git a/test/e2e/tests/test_jobs.py b/test/e2e/tests/test_jobs.py index 991f60d..71e9121 100644 --- a/test/e2e/tests/test_jobs.py +++ b/test/e2e/tests/test_jobs.py @@ -41,7 +41,7 @@ def main(): try: section("setup: create owning function") fnbody = {"name": FN_NAME, "description": "jobs queue target", - "runtime": "node24", "entrypoint": "handler.js", + "runtime": "node", "entrypoint": "handler.js", "timeout_ms": 30000, "memory_mb": 128, "cpus": 1, "network_mode": "none", "auth_mode": "none"} fc, fn = c.req("POST", "/api/v1/functions", fnbody, expect=range(200, 599)) diff --git a/test/e2e/tests/test_keys.py b/test/e2e/tests/test_keys.py index 2c0f6bd..0b078e6 100644 --- a/test/e2e/tests/test_keys.py +++ b/test/e2e/tests/test_keys.py @@ -67,7 +67,7 @@ def main(): ro = OrvaClient(api_key=plaintext) rc = ro.status("GET", "/api/v1/functions") check("read-only key: GET /functions allowed (2xx)", 200 <= rc < 300, f"status {rc}") - wbody = {"name": "e2e-keys-ro-fn", "runtime": "node24", + wbody = {"name": "e2e-keys-ro-fn", "runtime": "node", "entrypoint": "handler.js", "timeout_ms": 30000, "memory_mb": 128, "cpus": 1, "network_mode": "none", "auth_mode": "none"} diff --git a/test/e2e/tests/test_kv.py b/test/e2e/tests/test_kv.py index f401240..b989c00 100644 --- a/test/e2e/tests/test_kv.py +++ b/test/e2e/tests/test_kv.py @@ -28,7 +28,7 @@ def main(): fid = None try: section("setup function") - body = {"name": FN_NAME, "description": "kv host", "runtime": "node24", + body = {"name": FN_NAME, "description": "kv host", "runtime": "node", "entrypoint": "handler.js", "timeout_ms": 30000, "memory_mb": 128, "cpus": 1, "network_mode": "none", "auth_mode": "none"} code, created = c.req("POST", "/api/v1/functions", body, expect=range(200, 599)) diff --git a/test/e2e/tests/test_routes.py b/test/e2e/tests/test_routes.py index a47e146..caf4ea6 100644 --- a/test/e2e/tests/test_routes.py +++ b/test/e2e/tests/test_routes.py @@ -44,7 +44,7 @@ def main(): fid = None try: section("setup: create mapping-target function") - body = {"name": FN_NAME, "description": "route target", "runtime": "node24", + body = {"name": FN_NAME, "description": "route target", "runtime": "node", "entrypoint": "handler.js", "timeout_ms": 30000, "memory_mb": 128, "cpus": 1, "network_mode": "none", "auth_mode": "none"} code, created = c.req("POST", "/api/v1/functions", body, expect=range(200, 599)) diff --git a/test/e2e/tests/test_secrets.py b/test/e2e/tests/test_secrets.py index 0545501..9693db1 100644 --- a/test/e2e/tests/test_secrets.py +++ b/test/e2e/tests/test_secrets.py @@ -33,7 +33,7 @@ def main(): fid = None try: section("create owning function") - body = {"name": FN_NAME, "description": "secrets host", "runtime": "node24", + body = {"name": FN_NAME, "description": "secrets host", "runtime": "node", "entrypoint": "handler.js", "timeout_ms": 30000, "memory_mb": 128, "cpus": 1, "network_mode": "none", "auth_mode": "none"} code, created = c.req("POST", "/api/v1/functions", body, expect=range(200, 599)) diff --git a/test/e2e/tests/test_traces.py b/test/e2e/tests/test_traces.py index be0010b..619bc5e 100644 --- a/test/e2e/tests/test_traces.py +++ b/test/e2e/tests/test_traces.py @@ -74,7 +74,7 @@ def main(): check("no traces on fresh instance (expected)", True) section("create function for baseline") - body = {"name": NAME, "description": "baseline", "runtime": "node24", + body = {"name": NAME, "description": "baseline", "runtime": "node", "entrypoint": "handler.js", "timeout_ms": 30000, "memory_mb": 128, "cpus": 1, "network_mode": "none", "auth_mode": "none"} cc, created = c.req("POST", "/api/v1/functions", body, expect=range(200, 599)) diff --git a/test/egress-test.sh b/test/egress-test.sh index f0c035f..ed67532 100755 --- a/test/egress-test.sh +++ b/test/egress-test.sh @@ -29,7 +29,7 @@ check() { fn_name="egress-test-$$" create=$("${CURL[@]}" -X POST "$BASE/api/v1/functions" \ -H "Content-Type: application/json" \ - -d "{\"name\":\"$fn_name\",\"runtime\":\"node24\",\"memory_mb\":128,\"cpus\":1}") + -d "{\"name\":\"$fn_name\",\"runtime\":\"node\",\"memory_mb\":128,\"cpus\":1}") fid=$(echo "$create" | jq -r '.id') default_net=$(echo "$create" | jq -r '.network_mode') check "default network_mode == none" \ diff --git a/test/errors-test.sh b/test/errors-test.sh index c868db2..a0e3c30 100755 --- a/test/errors-test.sh +++ b/test/errors-test.sh @@ -68,7 +68,7 @@ http_assert "PAYLOAD_TOO_LARGE 413" "413" "PAYLOAD_TOO_LARGE" "" "$resp" echo "# 2: WORKER_CRASHED" fn_name="errs-crash-$$" "${CURL[@]}" -X POST "$BASE/api/v1/functions" -H "Content-Type: application/json" \ - -d "{\"name\":\"$fn_name\",\"runtime\":\"node24\",\"memory_mb\":128,\"cpus\":1}" >/dev/null + -d "{\"name\":\"$fn_name\",\"runtime\":\"node\",\"memory_mb\":128,\"cpus\":1}" >/dev/null fid=$("${CURL[@]}" "$BASE/api/v1/functions" | jq -r --arg n "$fn_name" '.functions[] | select(.name==$n) | .id') crash_code='exports.handler = async () => { process.exit(1); };' @@ -91,7 +91,7 @@ http_assert "WORKER_CRASHED 502" "502" "WORKER_CRASHED" "" "$resp" echo "# 3: TIMEOUT" fn_name="errs-timeout-$$" "${CURL[@]}" -X POST "$BASE/api/v1/functions" -H "Content-Type: application/json" \ - -d "{\"name\":\"$fn_name\",\"runtime\":\"node24\",\"memory_mb\":128,\"cpus\":1,\"timeout_ms\":1000}" >/dev/null + -d "{\"name\":\"$fn_name\",\"runtime\":\"node\",\"memory_mb\":128,\"cpus\":1,\"timeout_ms\":1000}" >/dev/null fid=$("${CURL[@]}" "$BASE/api/v1/functions" | jq -r --arg n "$fn_name" '.functions[] | select(.name==$n) | .id') slow='exports.handler = async () => { await new Promise(r => setTimeout(r, 5000)); return { statusCode: 200, body: "" }; };' @@ -118,7 +118,7 @@ http_assert "NOT_FOUND 404" "404" "NOT_FOUND" "" "$resp" echo "# 5: METHOD_NOT_ALLOWED via custom route" fn_name="errs-method-$$" "${CURL[@]}" -X POST "$BASE/api/v1/functions" -H "Content-Type: application/json" \ - -d "{\"name\":\"$fn_name\",\"runtime\":\"node24\",\"memory_mb\":128,\"cpus\":1}" >/dev/null + -d "{\"name\":\"$fn_name\",\"runtime\":\"node\",\"memory_mb\":128,\"cpus\":1}" >/dev/null fid=$("${CURL[@]}" "$BASE/api/v1/functions" | jq -r --arg n "$fn_name" '.functions[] | select(.name==$n) | .id') ok_code='exports.handler = async () => ({ statusCode: 200, body: "{}" });' @@ -143,7 +143,7 @@ http_assert "METHOD_NOT_ALLOWED 405" "405" "METHOD_NOT_ALLOWED" "" "$resp" echo "# 6: POOL_AT_CAPACITY" fn_name="errs-pool-$$" "${CURL[@]}" -X POST "$BASE/api/v1/functions" -H "Content-Type: application/json" \ - -d "{\"name\":\"$fn_name\",\"runtime\":\"node24\",\"memory_mb\":128,\"cpus\":1,\"timeout_ms\":2000}" >/dev/null + -d "{\"name\":\"$fn_name\",\"runtime\":\"node\",\"memory_mb\":128,\"cpus\":1,\"timeout_ms\":2000}" >/dev/null fid=$("${CURL[@]}" "$BASE/api/v1/functions" | jq -r --arg n "$fn_name" '.functions[] | select(.name==$n) | .id') slow_ok='exports.handler = async () => { await new Promise(r => setTimeout(r, 1500)); return { statusCode: 200, body: "{}" }; };' @@ -191,7 +191,7 @@ fi echo "# 7: NOT_ACTIVE" fn_name="errs-inactive-$$" "${CURL[@]}" -X POST "$BASE/api/v1/functions" -H "Content-Type: application/json" \ - -d "{\"name\":\"$fn_name\",\"runtime\":\"node22\"}" >/dev/null + -d "{\"name\":\"$fn_name\",\"runtime\":\"node\"}" >/dev/null fid=$("${CURL[@]}" "$BASE/api/v1/functions" | jq -r --arg n "$fn_name" '.functions[] | select(.name==$n) | .id') "${CURL[@]}" -X POST "$BASE/api/v1/functions/$fid/deploy-inline" \ -H "Content-Type: application/json" \ diff --git a/test/heavy-deploy-test.sh b/test/heavy-deploy-test.sh index 01af90e..9e019c2 100755 --- a/test/heavy-deploy-test.sh +++ b/test/heavy-deploy-test.sh @@ -21,7 +21,7 @@ check() { fn_name="heavy-test-$$" create=$("${CURL[@]}" -X POST "$BASE/api/v1/functions" \ -H "Content-Type: application/json" \ - -d "{\"name\":\"$fn_name\",\"runtime\":\"python313\",\"memory_mb\":256,\"cpus\":1,\"timeout_ms\":30000}") + -d "{\"name\":\"$fn_name\",\"runtime\":\"python\",\"memory_mb\":256,\"cpus\":1,\"timeout_ms\":30000}") fid=$(echo "$create" | jq -r '.id') # 1. First deploy: a trivial handler so we have a known-good baseline. diff --git a/test/install/failure-modes.sh b/test/install/failure-modes.sh index 37331f3..203d9cd 100755 --- a/test/install/failure-modes.sh +++ b/test/install/failure-modes.sh @@ -93,7 +93,7 @@ curl -s -o /dev/null -X POST "$BASE/api/v1/auth/onboard" \ create=$(curl -sf -H "X-Orva-API-Key: $ADMIN_KEY" -X POST "$BASE/api/v1/functions" \ -H 'Content-Type: application/json' \ - -d '{"name":"fail-idem","runtime":"node24"}') + -d '{"name":"fail-idem","runtime":"node"}') fid=$(echo "$create" | jq -r '.id // empty') [[ -n "$fid" ]] || die "could not create marker function" ok "created marker function fail-idem ($fid)" diff --git a/test/install/kata-flow.sh b/test/install/kata-flow.sh index 7954220..b09d4d4 100755 --- a/test/install/kata-flow.sh +++ b/test/install/kata-flow.sh @@ -110,7 +110,7 @@ curl -s -o /dev/null -X POST "$BASE/api/v1/auth/onboard" \ CURL=(curl -sf -H "X-Orva-API-Key: $ADMIN_KEY") create=$("${CURL[@]}" -X POST "$BASE/api/v1/functions" \ -H 'Content-Type: application/json' \ - -d '{"name":"hello-kata","runtime":"node24","memory_mb":128}') + -d '{"name":"hello-kata","runtime":"node","memory_mb":128}') fid=$(echo "$create" | jq -r '.id // empty') [[ -n "$fid" ]] || { fail "could not create function under $RUNTIME"; exit 1; } diff --git a/test/install/smoke-flow.sh b/test/install/smoke-flow.sh index 360e174..5bacfa5 100755 --- a/test/install/smoke-flow.sh +++ b/test/install/smoke-flow.sh @@ -82,7 +82,7 @@ CURL=(curl -sf -H "X-Orva-API-Key: $API_KEY") # Create hello-api function. create=$("${CURL[@]}" -X POST "$BASE/api/v1/functions" \ -H 'Content-Type: application/json' \ - -d '{"name":"hello-api","runtime":"node24","memory_mb":128}') \ + -d '{"name":"hello-api","runtime":"node","memory_mb":128}') \ || { fail "POST /functions failed"; FAIL=$((FAIL+1)); } fid=$(echo "$create" | jq -r '.id // empty') if [[ -n "$fid" ]]; then @@ -163,7 +163,7 @@ EOF ' if docker exec "$CONTAINER" /usr/local/bin/orva deploy /tmp/hello-cli \ - --name hello-cli --runtime node24 >>/tmp/cli-deploy.log 2>&1; then + --name hello-cli --runtime node >>/tmp/cli-deploy.log 2>&1; then ok "orva deploy hello-cli"; PASS=$((PASS+1)) else fail "orva deploy hello-cli failed"; FAIL=$((FAIL+1)) diff --git a/test/kata-bench/extended-functional.sh b/test/kata-bench/extended-functional.sh index 2b6b23a..29afae6 100755 --- a/test/kata-bench/extended-functional.sh +++ b/test/kata-bench/extended-functional.sh @@ -109,7 +109,7 @@ PASS=0; FAIL=0 # ── 1. Baseline deploy + invoke ─────────────────────────────────────────── log "1. baseline deploy + invoke" -if fid=$(deploy_fn "baseline" node24 none \ +if fid=$(deploy_fn "baseline" node none \ 'exports.handler = async () => ({statusCode:200, body:"baseline"});'); then body=$(invoke "$fid") if [[ "$body" == *"baseline"* ]]; then @@ -123,7 +123,7 @@ fi # ── 2. Egress (highest-risk) ────────────────────────────────────────────── log "2. egress (network_mode=egress, fetch https://example.com)" -if fid=$(deploy_fn "egress-test" node24 egress \ +if fid=$(deploy_fn "egress-test" node egress \ 'exports.handler = async () => { const r = await fetch("https://example.com", {method:"GET"}); return { statusCode: r.status, body: "egress-status-" + r.status }; @@ -140,7 +140,7 @@ fi # ── 3. Secrets (env injection) ──────────────────────────────────────────── log "3. secrets (FOO=bar env injection)" -if fid=$(deploy_fn "secrets-test" node24 none \ +if fid=$(deploy_fn "secrets-test" node none \ 'exports.handler = async () => ({statusCode:200, body:"FOO="+(process.env.FOO||"unset")});'); then curl -sf -H "X-Orva-API-Key: $C" -X POST "$BASE/api/v1/functions/$fid/secrets" \ -H 'Content-Type: application/json' \ diff --git a/test/kata-bench/run.sh b/test/kata-bench/run.sh index ff587d1..3528417 100755 --- a/test/kata-bench/run.sh +++ b/test/kata-bench/run.sh @@ -110,7 +110,7 @@ run_one() { local create create=$(curl -sf -H "X-Orva-API-Key: $admin_key" -X POST "$base/api/v1/functions" \ -H 'Content-Type: application/json' \ - -d '{"name":"hello","runtime":"node24","memory_mb":128,"network_mode":"none"}') + -d '{"name":"hello","runtime":"node","memory_mb":128,"network_mode":"none"}') fid=$(echo "$create" | jq -r '.id // empty') if [[ -z "$fid" ]]; then fail "[$runtime] could not create function"; echo "status: failed" > "$out/result.txt"; kill $stats_pid 2>/dev/null; return 1 diff --git a/test/loadtest.sh b/test/loadtest.sh index cef48e7..e65aa67 100755 --- a/test/loadtest.sh +++ b/test/loadtest.sh @@ -77,12 +77,12 @@ EP="--endpoint http://localhost:8443 --api-key $API_KEY" # Deploy all functions echo "Deploying functions..." FUNCTIONS=( - "node-api:node22:test/fixtures/node-api" - "node-cpu:node22:test/fixtures/node-cpu" - "node-slow:node22:test/fixtures/node-slow" - "python-data:python313:test/fixtures/python-data" - "python-compute:python313:test/fixtures/python-compute" - "python-error:python313:test/fixtures/python-error" + "node-api:node:test/fixtures/node-api" + "node-cpu:node:test/fixtures/node-cpu" + "node-slow:node:test/fixtures/node-slow" + "python-data:python:test/fixtures/python-data" + "python-compute:python:test/fixtures/python-compute" + "python-error:python:test/fixtures/python-error" ) for fn_spec in "${FUNCTIONS[@]}"; do diff --git a/test/rollback-test.sh b/test/rollback-test.sh index cbdbf44..ff91053 100755 --- a/test/rollback-test.sh +++ b/test/rollback-test.sh @@ -25,7 +25,7 @@ check() { fn_name="rb-$$" echo "# 0: setup function $fn_name" "${CURL[@]}" -X POST "$BASE/api/v1/functions" -H "Content-Type: application/json" \ - -d "{\"name\":\"$fn_name\",\"runtime\":\"node22\",\"memory_mb\":128,\"cpus\":1}" >/dev/null + -d "{\"name\":\"$fn_name\",\"runtime\":\"node\",\"memory_mb\":128,\"cpus\":1}" >/dev/null fid=$("${CURL[@]}" "$BASE/api/v1/functions" | jq -r --arg n "$fn_name" '.functions[] | select(.name==$n) | .id') [ -z "$fid" ] && { echo "fail create fn"; exit 1; } diff --git a/test/routes-test.sh b/test/routes-test.sh index c4e47e5..7a7c0dc 100755 --- a/test/routes-test.sh +++ b/test/routes-test.sh @@ -20,7 +20,7 @@ check() { fn_name="routes-test-$$" create=$("${CURL[@]}" -X POST "$BASE/api/v1/functions" \ -H "Content-Type: application/json" \ - -d "{\"name\":\"$fn_name\",\"runtime\":\"node24\",\"memory_mb\":128,\"cpus\":1}") + -d "{\"name\":\"$fn_name\",\"runtime\":\"node\",\"memory_mb\":128,\"cpus\":1}") fid=$(echo "$create" | jq -r '.id') code='exports.handler = async (event) => ({ statusCode: 200, headers: {"Content-Type":"application/json"}, body: JSON.stringify({path: event.path, method: event.method}) });' diff --git a/test/sdk-test.sh b/test/sdk-test.sh index 602d710..1067d25 100755 --- a/test/sdk-test.sh +++ b/test/sdk-test.sh @@ -75,7 +75,7 @@ wait_active() { NOOP_FN_NAME="sdk-test-noop" noop_create=$("${CURL[@]}" -X POST "$BASE/api/v1/functions" \ -H "Content-Type: application/json" \ - -d "{\"name\":\"$NOOP_FN_NAME\",\"runtime\":\"python314\",\"network_mode\":\"none\",\"entrypoint\":\"handler.py\"}" 2>/dev/null || true) + -d "{\"name\":\"$NOOP_FN_NAME\",\"runtime\":\"python\",\"network_mode\":\"none\",\"entrypoint\":\"handler.py\"}" 2>/dev/null || true) noop_id=$(echo "$noop_create" | jq -r '.id // empty') if [ -z "$noop_id" ]; then noop_id=$(curl -s -H "X-Orva-API-Key: $KEY" "$BASE/api/v1/functions?limit=200" \ @@ -117,9 +117,9 @@ EOF py_create=$("${CURL[@]}" -X POST "$BASE/api/v1/functions" \ -H "Content-Type: application/json" \ - -d "{\"name\":\"$PY_FN\",\"runtime\":\"python314\",\"network_mode\":\"egress\",\"entrypoint\":\"handler.py\"}") + -d "{\"name\":\"$PY_FN\",\"runtime\":\"python\",\"network_mode\":\"egress\",\"entrypoint\":\"handler.py\"}") py_id=$(echo "$py_create" | jq -r '.id') -assert_nonempty "POST /functions (python314)" "$py_id" +assert_nonempty "POST /functions (python)" "$py_id" py_deploy=$(jq -nc --arg src "$PY_SRC" '{code:$src,filename:"handler.py"}') "${CURL[@]}" -X POST "$BASE/api/v1/functions/$py_id/deploy-inline" \ @@ -215,9 +215,9 @@ EOF node_create=$("${CURL[@]}" -X POST "$BASE/api/v1/functions" \ -H "Content-Type: application/json" \ - -d "{\"name\":\"$NODE_FN\",\"runtime\":\"node24\",\"network_mode\":\"egress\",\"entrypoint\":\"handler.js\"}") + -d "{\"name\":\"$NODE_FN\",\"runtime\":\"node\",\"network_mode\":\"egress\",\"entrypoint\":\"handler.js\"}") node_id=$(echo "$node_create" | jq -r '.id') -assert_nonempty "POST /functions (node24)" "$node_id" +assert_nonempty "POST /functions (node)" "$node_id" node_deploy=$(jq -nc --arg src "$NODE_SRC" '{code:$src,filename:"handler.js"}') "${CURL[@]}" -X POST "$BASE/api/v1/functions/$node_id/deploy-inline" \ diff --git a/test/secrets-test.sh b/test/secrets-test.sh index 013f440..e321a13 100755 --- a/test/secrets-test.sh +++ b/test/secrets-test.sh @@ -26,7 +26,7 @@ check() { fn_name="secrets-test-$$" create=$("${CURL[@]}" -X POST "$BASE/api/v1/functions" \ -H "Content-Type: application/json" \ - -d "{\"name\":\"$fn_name\",\"runtime\":\"node24\",\"memory_mb\":128,\"cpus\":1}") + -d "{\"name\":\"$fn_name\",\"runtime\":\"node\",\"memory_mb\":128,\"cpus\":1}") fid=$(echo "$create" | jq -r '.id') code='exports.handler = async () => ({ statusCode: 200, headers: {"Content-Type":"application/json"}, body: JSON.stringify({STRIPE_SECRET: process.env.STRIPE_SECRET || null, STRIPE_WEBHOOK: process.env.STRIPE_WEBHOOK || null, STRIPE_PUBLIC: process.env.STRIPE_PUBLIC || null}) });' diff --git a/test/tracing-test.sh b/test/tracing-test.sh index 0116ef4..768ca96 100755 --- a/test/tracing-test.sh +++ b/test/tracing-test.sh @@ -93,7 +93,7 @@ cleanup # also wipe leftovers from a previous interrupted run section "T1: HTTP root span" # Tiny function that echoes back its trace context env vars. -fid_c=$(deploy trace_chain_c node22 'module.exports.handler = async () => ({ +fid_c=$(deploy trace_chain_c node 'module.exports.handler = async () => ({ statusCode: 200, headers: {"content-type":"application/json"}, body: JSON.stringify({ @@ -116,13 +116,13 @@ trigger=$(echo "$http_trace" | jq -r '.spans[0].trigger') section "T2: F2F propagation" -deploy trace_chain_b node22 'module.exports.handler = async () => ({ +deploy trace_chain_b node 'module.exports.handler = async () => ({ statusCode: 200, headers: {"content-type":"application/json"}, body: JSON.stringify({ from: "B" }) })' >/dev/null -fid_a=$(deploy trace_chain_a node22 'const { invoke } = require("orva") +fid_a=$(deploy trace_chain_a node 'const { invoke } = require("orva") module.exports.handler = async () => { const r = await invoke("trace_chain_b", {}) return { statusCode: 200, headers: {"content-type":"application/json"}, @@ -175,7 +175,7 @@ section "T7: Outlier detection" # Deploy a function whose duration we can swap. We feed the baseline with # fast calls, then make it slow. -fid_o=$(deploy trace_outlier python313 ' +fid_o=$(deploy trace_outlier python ' import os, time def handler(event): if os.environ.get("SLOW") == "1": From 145104768566872a7086a122d6a42eb92fe95c64 Mon Sep 17 00:00:00 2001 From: Harsh-2002 Date: Thu, 4 Jun 2026 11:42:59 +0000 Subject: [PATCH 2/2] fix(review): atscale name collision, API.md/CAPACITY.md docs, log cgroup rlimit fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address PR #10 review: - test/atscale.sh: the two node loops produced duplicate names (ascale-node-$i twice → 5 dup create 409s). Now 10 node + 10 python with distinct prefixes. - docs/API.md: fix garbled runtime comment (node|node|python|python → node | python). - docs/CAPACITY.md: refresh stale py313/py314 + duplicate node snapshot rows. - sandbox.go: log a one-time WARN when no cgroup-v2 delegate is usable so operators know per-sandbox memory/pid/cpu caps fell back to rlimit-only. --- backend/internal/sandbox/sandbox.go | 17 ++++++++++++++++- docs/API.md | 2 +- docs/CAPACITY.md | 6 +++--- test/atscale.sh | 20 ++++++++------------ 4 files changed, 28 insertions(+), 17 deletions(-) diff --git a/backend/internal/sandbox/sandbox.go b/backend/internal/sandbox/sandbox.go index a401cc1..d854fb9 100644 --- a/backend/internal/sandbox/sandbox.go +++ b/backend/internal/sandbox/sandbox.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "log/slog" "os" "os/exec" "path/filepath" @@ -312,7 +313,8 @@ func cgroupv2Delegate() string { if rel == "" { return } - // Walk up from the current cgroup until we find a writable dir. + // Walk up from the current cgroup until we find a writable dir that + // actually has the controllers we need delegated to children. p := filepath.Join("/sys/fs/cgroup", rel) for p != "/sys/fs/cgroup" && p != "/" { if isWritableDir(p) { @@ -322,9 +324,22 @@ func cgroupv2Delegate() string { p = filepath.Dir(p) } }) + if cgroupMount == "" { + // One-time heads-up: no usable cgroup-v2 delegate, so per-sandbox + // memory/pid/cpu caps are NOT enforced (we fall back to rlimits). This + // is normal on hosts where systemd doesn't delegate controllers to the + // service (e.g. constrained VMs/containers); functions still run fully + // isolated via nsjail. Set Delegate=yes + delegate the controllers, or + // ORVA_CGROUPV2_MOUNT, to enable hard caps. + cgroupWarnOnce.Do(func() { + slog.Warn("cgroup v2 controllers not delegated; per-sandbox memory/pid/cpu caps disabled (rlimit-only fallback)") + }) + } return cgroupMount } +var cgroupWarnOnce sync.Once + func getenvOr(m map[string]string, k, def string) string { if v, ok := m[k]; ok { return v diff --git a/docs/API.md b/docs/API.md index 5363e48..f01ccfc 100644 --- a/docs/API.md +++ b/docs/API.md @@ -69,7 +69,7 @@ Create a function record. ```json { "name": "my-fn", - "runtime": "node", // node|node|python|python + "runtime": "node", // node | python "entrypoint": "handler.js", // optional, defaults match the runtime "memory_mb": 128, "cpus": 1, diff --git a/docs/CAPACITY.md b/docs/CAPACITY.md index 5fb65ae..274c0ee 100644 --- a/docs/CAPACITY.md +++ b/docs/CAPACITY.md @@ -61,9 +61,9 @@ Per-pool snapshot at end of run: |-------------------|-----:|-----:|----------:|------------:| | ascale-node-1 | 25 | 0 | 0 | 10 | | ascale-node-3 | 25 | 0 | 0 | 11 | -| ascale-node-1 | 25 | 0 | 0 | 9 | -| ascale-py313-1 | 14 | 0 | 14 | 0 | -| ascale-py314-1 | 14 | 0 | 14 | 0 | +| ascale-node-5 | 25 | 0 | 0 | 9 | +| ascale-py-1 | 14 | 0 | 14 | 0 | +| ascale-py-2 | 14 | 0 | 14 | 0 | | (15 idle fns) | – | – | – | – | **Only 5 pools exist.** The 15 untouched functions never spawned a worker — confirming pool isolation under the per-function `functionPool` design. A function under heavy load cannot starve another function's resources. diff --git a/test/atscale.sh b/test/atscale.sh index 097806e..b6bdc67 100755 --- a/test/atscale.sh +++ b/test/atscale.sh @@ -38,25 +38,21 @@ trap 'rm -rf "$TMPDIR"' EXIT # Each function is a hello-world handler in its target runtime; the deps # field tests the async build queue without taking minutes per fn. -# 20 fixtures: 5 of each runtime mix. +# 20 fixtures: 10 node + 10 python (Orva offers two runtimes). FN_NAMES=() -for i in $(seq 1 5); do FN_NAMES+=("ascale-node-$i"); done -for i in $(seq 1 5); do FN_NAMES+=("ascale-node-$i"); done -for i in $(seq 1 5); do FN_NAMES+=("ascale-py313-$i"); done -for i in $(seq 1 5); do FN_NAMES+=("ascale-py314-$i"); done +for i in $(seq 1 10); do FN_NAMES+=("ascale-node-$i"); done +for i in $(seq 1 10); do FN_NAMES+=("ascale-py-$i"); done runtime_for() { case "$1" in ascale-node-*) echo node ;; - ascale-node-*) echo node ;; - ascale-py313-*) echo python ;; - ascale-py314-*) echo python ;; + ascale-py-*) echo python ;; esac } handler_for() { case "$1" in - ascale-node-*|ascale-node-*) + ascale-node-*) cat <<'EOF' exports.handler = async (event) => ({ statusCode: 200, @@ -65,7 +61,7 @@ exports.handler = async (event) => ({ }); EOF ;; - ascale-py313-*|ascale-py314-*) + ascale-py-*) cat <<'EOF' import json, os def handler(event, context): @@ -81,8 +77,8 @@ EOF filename_for() { case "$1" in - ascale-node-*|ascale-node-*) echo handler.js ;; - ascale-py313-*|ascale-py314-*) echo handler.py ;; + ascale-node-*) echo handler.js ;; + ascale-py-*) echo handler.py ;; esac }