@xano/sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +788 -0
- package/LICENSE +21 -0
- package/README.md +541 -0
- package/dist/.build-fingerprint +1 -0
- package/dist/agent-file-refresh-ZEMK33AX.js +38 -0
- package/dist/answer-shape-K67TAAPY.js +33 -0
- package/dist/backend-dir-2U53CIAL.js +39 -0
- package/dist/backend-slot-7QXW2QYC.js +28 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +255 -0
- package/dist/branch-NCRNWQBQ.js +61 -0
- package/dist/branch-commands-ACIRCEQJ.js +79 -0
- package/dist/bundle.d.ts +223 -0
- package/dist/bundle.js +153 -0
- package/dist/capture-WAXPD7DR.js +41 -0
- package/dist/chunk-27GWNP2I.js +23 -0
- package/dist/chunk-2CMJZDCL.js +177 -0
- package/dist/chunk-2EZ3ZJZZ.js +302 -0
- package/dist/chunk-2GNFDZA2.js +390 -0
- package/dist/chunk-2MNLRIOX.js +11 -0
- package/dist/chunk-2TFRR6JV.js +104 -0
- package/dist/chunk-2ZUEE4EK.js +408 -0
- package/dist/chunk-36JUQZVQ.js +22 -0
- package/dist/chunk-3DRXWLJ5.js +83 -0
- package/dist/chunk-3JDU4VRL.js +137 -0
- package/dist/chunk-3KZCLN4Z.js +241 -0
- package/dist/chunk-3LDULPRE.js +168 -0
- package/dist/chunk-3M4LFYNQ.js +111 -0
- package/dist/chunk-3XI5CTUE.js +22 -0
- package/dist/chunk-3YW23UFH.js +50 -0
- package/dist/chunk-45UIT5T2.js +862 -0
- package/dist/chunk-4BW2SG2N.js +46 -0
- package/dist/chunk-4KHEMC32.js +79 -0
- package/dist/chunk-4POWHT2K.js +321 -0
- package/dist/chunk-4Q2GN7NW.js +192 -0
- package/dist/chunk-5A2PLKZB.js +70 -0
- package/dist/chunk-5FBYXK5P.js +159 -0
- package/dist/chunk-5HVXPTU4.js +323 -0
- package/dist/chunk-5QU2P5CA.js +304 -0
- package/dist/chunk-5V5XU32D.js +191 -0
- package/dist/chunk-5ZSO4T2G.js +10 -0
- package/dist/chunk-622HUCZT.js +10 -0
- package/dist/chunk-65HWOMGV.js +177 -0
- package/dist/chunk-6ELFRN3P.js +365 -0
- package/dist/chunk-6HO3NCSJ.js +1055 -0
- package/dist/chunk-6IUMKYWI.js +457 -0
- package/dist/chunk-6KEB3C3U.js +65 -0
- package/dist/chunk-6MR5TI5V.js +987 -0
- package/dist/chunk-6NNTNXN7.js +157 -0
- package/dist/chunk-6PBR37LN.js +97 -0
- package/dist/chunk-6QXLMMT3.js +155 -0
- package/dist/chunk-6ZGJCBZS.js +224 -0
- package/dist/chunk-73IVBKMA.js +250 -0
- package/dist/chunk-7G3CTU7I.js +286 -0
- package/dist/chunk-A3UDOZB6.js +222 -0
- package/dist/chunk-A6E77GDW.js +85 -0
- package/dist/chunk-AHJWCYQO.js +660 -0
- package/dist/chunk-AKNCFVKZ.js +36 -0
- package/dist/chunk-AM4FKL4J.js +237 -0
- package/dist/chunk-AN6BL47T.js +26 -0
- package/dist/chunk-AOSYVMUS.js +147 -0
- package/dist/chunk-APWXY5SL.js +182 -0
- package/dist/chunk-AWEIIR2U.js +218 -0
- package/dist/chunk-AWQ7BQTQ.js +73 -0
- package/dist/chunk-BAUZOWGX.js +393 -0
- package/dist/chunk-BHACRS6R.js +940 -0
- package/dist/chunk-BWPNT55T.js +680 -0
- package/dist/chunk-C6RIMRU4.js +187 -0
- package/dist/chunk-CGVQZXYG.js +263 -0
- package/dist/chunk-CIMQZRRE.js +162 -0
- package/dist/chunk-CJHQKJU4.js +113 -0
- package/dist/chunk-CTMO46PK.js +1897 -0
- package/dist/chunk-D53J55C5.js +106 -0
- package/dist/chunk-D7VGG3WS.js +90 -0
- package/dist/chunk-DAD3ONIS.js +245 -0
- package/dist/chunk-DGWHAVRG.js +162 -0
- package/dist/chunk-DX4QR4B2.js +483 -0
- package/dist/chunk-EHP3WPEG.js +21 -0
- package/dist/chunk-ELP2HMXC.js +432 -0
- package/dist/chunk-ERVIASGL.js +21 -0
- package/dist/chunk-ET4Y4XCB.js +865 -0
- package/dist/chunk-ETO6LGLD.js +4750 -0
- package/dist/chunk-EUUUMY7J.js +24 -0
- package/dist/chunk-FQJ2TTHP.js +218 -0
- package/dist/chunk-FQOF7E2E.js +133 -0
- package/dist/chunk-FWD7AL6N.js +21 -0
- package/dist/chunk-FXFV3KU6.js +138 -0
- package/dist/chunk-FXRQOWTE.js +171 -0
- package/dist/chunk-G554NZNI.js +210 -0
- package/dist/chunk-GSGDWJB5.js +13 -0
- package/dist/chunk-H2V3N23D.js +66 -0
- package/dist/chunk-H5C7MBCR.js +159 -0
- package/dist/chunk-HHN734BU.js +371 -0
- package/dist/chunk-HRVBLCVJ.js +243 -0
- package/dist/chunk-HVRUQXGD.js +1264 -0
- package/dist/chunk-I4F5TCJH.js +130 -0
- package/dist/chunk-IDOTCMDV.js +57 -0
- package/dist/chunk-IGOLHKWF.js +427 -0
- package/dist/chunk-JGE7CAWB.js +148 -0
- package/dist/chunk-JTUKLCXS.js +92 -0
- package/dist/chunk-JZGVVJSI.js +66 -0
- package/dist/chunk-K4WX6UUU.js +923 -0
- package/dist/chunk-K5BBGTYZ.js +217 -0
- package/dist/chunk-K6RXBGUB.js +427 -0
- package/dist/chunk-KTSDSD36.js +165 -0
- package/dist/chunk-L5JYN3NM.js +1038 -0
- package/dist/chunk-LIEOKXKD.js +354 -0
- package/dist/chunk-LLNZGEFK.js +51 -0
- package/dist/chunk-LPECSJ2J.js +55 -0
- package/dist/chunk-LQCMKAJ7.js +413 -0
- package/dist/chunk-LRN4ZJCQ.js +1145 -0
- package/dist/chunk-MJ4ZVHKN.js +17 -0
- package/dist/chunk-MXHSQOC4.js +3061 -0
- package/dist/chunk-N5GKCWE5.js +367 -0
- package/dist/chunk-NA3PXMEF.js +10 -0
- package/dist/chunk-NAQDMEHV.js +76 -0
- package/dist/chunk-NWKXM5RD.js +109 -0
- package/dist/chunk-NZOWWHU5.js +196 -0
- package/dist/chunk-OHX6MIUZ.js +184 -0
- package/dist/chunk-OOXWYAA4.js +15 -0
- package/dist/chunk-OPHB5ORL.js +1990 -0
- package/dist/chunk-OXA6PSZT.js +1725 -0
- package/dist/chunk-P2HAGF6H.js +73 -0
- package/dist/chunk-PTHVMXZI.js +3036 -0
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/chunk-Q325JKYB.js +189 -0
- package/dist/chunk-Q7DGTQ5G.js +255 -0
- package/dist/chunk-QCNYHBL7.js +601 -0
- package/dist/chunk-QO6BVGS5.js +145 -0
- package/dist/chunk-QXTKSOFI.js +76 -0
- package/dist/chunk-ROY4YAEQ.js +66 -0
- package/dist/chunk-RTJRZFFY.js +447 -0
- package/dist/chunk-SAVJUS2X.js +128 -0
- package/dist/chunk-SFLHVCKX.js +173 -0
- package/dist/chunk-STSFNCY4.js +127 -0
- package/dist/chunk-SWBF2LTK.js +170 -0
- package/dist/chunk-T5KELPAR.js +109 -0
- package/dist/chunk-TFFGOFVZ.js +1481 -0
- package/dist/chunk-TLHIFK4M.js +256 -0
- package/dist/chunk-TLQE7ZN3.js +156 -0
- package/dist/chunk-TPHWJ7UG.js +1981 -0
- package/dist/chunk-TRJCALW3.js +291 -0
- package/dist/chunk-TTEIYD5P.js +133 -0
- package/dist/chunk-U64O2MXY.js +407 -0
- package/dist/chunk-UCQ2VUTG.js +630 -0
- package/dist/chunk-UTI5G6OJ.js +44 -0
- package/dist/chunk-UVHDMMIP.js +188 -0
- package/dist/chunk-UZ6QQWJT.js +120 -0
- package/dist/chunk-VAX3UZOY.js +686 -0
- package/dist/chunk-W3HBUKFO.js +57 -0
- package/dist/chunk-WKQLGKCL.js +2823 -0
- package/dist/chunk-WQFNASK4.js +560 -0
- package/dist/chunk-WRBKLYSJ.js +224 -0
- package/dist/chunk-WTPZRR4I.js +17 -0
- package/dist/chunk-WZDJ2LLX.js +2385 -0
- package/dist/chunk-X2NL4KUO.js +14 -0
- package/dist/chunk-X4DVXBFY.js +38 -0
- package/dist/chunk-X677DMZM.js +7050 -0
- package/dist/chunk-XBKMWY7F.js +612 -0
- package/dist/chunk-XNCP4KYO.js +78 -0
- package/dist/chunk-XVBGLISP.js +35 -0
- package/dist/chunk-XW6KISBB.js +1192 -0
- package/dist/chunk-YGZHHAAB.js +8156 -0
- package/dist/chunk-YHEFQUQB.js +186 -0
- package/dist/chunk-YLDALTWZ.js +653 -0
- package/dist/chunk-YU4HSQW7.js +229 -0
- package/dist/chunk-ZDPP6RKB.js +27 -0
- package/dist/chunk-ZFDZKNGG.js +194 -0
- package/dist/chunk-ZIY7RNUT.js +49 -0
- package/dist/chunk-ZYJP6BOO.js +30 -0
- package/dist/cli.d.ts +1111 -0
- package/dist/cli.js +165 -0
- package/dist/codegen-command-VR2GWR6C.js +173 -0
- package/dist/codegen.d.ts +115 -0
- package/dist/codegen.js +19 -0
- package/dist/commands-GJMJ4XYZ.js +76 -0
- package/dist/completion-ZQLO6PB6.js +738 -0
- package/dist/config-MBYGCAFK.js +136 -0
- package/dist/deploy-command-JVZPWHMK.js +180 -0
- package/dist/deploy-source-3W45CXMP.js +81 -0
- package/dist/dev-env-sync-HN44K4D5.js +188 -0
- package/dist/diagnostics-UEAAHWVF.js +30 -0
- package/dist/display-name-hint-ABBPVQIR.js +46 -0
- package/dist/emit-CyhBDxli.d.ts +7007 -0
- package/dist/env-example-refresh-ET5HMMDW.js +126 -0
- package/dist/env-pull-command-TNMNB3CV.js +323 -0
- package/dist/env-target-O4HRKTGZ.js +40 -0
- package/dist/env-var-command-ZIRDCAZA.js +427 -0
- package/dist/ephemeral-P7F3PMAD.js +54 -0
- package/dist/ephemeral-command-CLJJKOFT.js +592 -0
- package/dist/ephemeral-state-PYCBSOGX.js +61 -0
- package/dist/file-lock-PT74GWYW.js +10 -0
- package/dist/generate-command-AYEOLA26.js +510 -0
- package/dist/gitignore-3SJ3WNRG.js +51 -0
- package/dist/guards-4EBNCZDC.js +162 -0
- package/dist/hosted-file-PSHJB3HX.js +28 -0
- package/dist/impersonate-command-5JAS3WSV.js +152 -0
- package/dist/import-EQSFU6JC.js +19 -0
- package/dist/index.d.ts +5470 -0
- package/dist/index.js +371 -0
- package/dist/init-command-X2FR4UAK.js +125 -0
- package/dist/init-modules-RY7UHAOL.js +50 -0
- package/dist/init-web-M4YZSZK6.js +42 -0
- package/dist/internal.d.ts +179 -0
- package/dist/internal.js +2634 -0
- package/dist/io-RC56QSN5.js +26 -0
- package/dist/keep-data-merge-7MPLVAVI.js +1119 -0
- package/dist/lambda-config-reconcile-OUZ4OYAB.js +116 -0
- package/dist/landed-753YLJ5H.js +40 -0
- package/dist/landing-record-SU52SKXU.js +61 -0
- package/dist/live-diff-YONW2VI3.js +65 -0
- package/dist/local-engine-choice-CH5X3W7E.js +79 -0
- package/dist/local-engine-command-Z3OL7VOD.js +766 -0
- package/dist/local-engine-config-5U3SC434.js +39 -0
- package/dist/local-engine-deploy-PCQ4SNPB.js +39 -0
- package/dist/local-engine-process-EHTE5LLU.js +63 -0
- package/dist/local-engine-state-A4FTRFAF.js +32 -0
- package/dist/local-engine-update-check-6L645VBL.js +162 -0
- package/dist/lock-MRRHW5GS.js +87 -0
- package/dist/lock-commands-2QG25HL7.js +963 -0
- package/dist/login-command-QPYRWOXZ.js +538 -0
- package/dist/logout-command-WOIQLDP6.js +56 -0
- package/dist/loop-ALIZLOSX.js +191 -0
- package/dist/marketplace-command-K5YL42NO.js +977 -0
- package/dist/marketplace-resolve-LY32JES5.js +40 -0
- package/dist/meta-client-YXGPW53C.js +244 -0
- package/dist/module-manifest-JFGHU2TC.js +38 -0
- package/dist/near-key.d.ts +13 -0
- package/dist/near-key.js +8 -0
- package/dist/node.d.ts +95 -0
- package/dist/node.js +969 -0
- package/dist/operation-outcome-TZSESNC7.js +51 -0
- package/dist/output-target-AL3I44GG.js +47 -0
- package/dist/package-manager-PRAHUSCN.js +34 -0
- package/dist/plan-disclosure-6G4MLZKD.js +114 -0
- package/dist/plugin-contributions-WRNY5ABG.js +28 -0
- package/dist/plugin-prompt-4VA3B4FF.js +20 -0
- package/dist/plugin-questions-JTSYA74Q.js +24 -0
- package/dist/plugin.d.ts +396 -0
- package/dist/plugin.js +1 -0
- package/dist/preflight-command-2T5Z36MI.js +321 -0
- package/dist/profile-command-AH3PIM4X.js +800 -0
- package/dist/profile-pointer-GRWJU3YD.js +31 -0
- package/dist/profile-select-74TRDEPD.js +23 -0
- package/dist/project-dependency-KSH63ODT.js +35 -0
- package/dist/project-sdk-REPPOIBD.js +24 -0
- package/dist/promote-command-PCWT5N6S.js +140 -0
- package/dist/prompt-II3F3LND.js +27 -0
- package/dist/publish-command-TTDUUJI5.js +414 -0
- package/dist/pull-command-VBO7C4S3.js +161 -0
- package/dist/pull-superseded-GJWLG7XO.js +20 -0
- package/dist/release-command-N4ZPKMHR.js +146 -0
- package/dist/release-ns-command-3KFDMZ6G.js +1745 -0
- package/dist/response-HlXahv4K.d.ts +925 -0
- package/dist/retry-command-AS5EVREX.js +34 -0
- package/dist/routes-manifest-SNWUVBT6.js +43 -0
- package/dist/runtime-XETHVF33.js +14 -0
- package/dist/scaffold.d.ts +1911 -0
- package/dist/scaffold.js +107 -0
- package/dist/secrets-file-LVZD6IIM.js +56 -0
- package/dist/secrets-fill-command-TTCPSVXN.js +218 -0
- package/dist/seed-VU7C3QYS.js +46 -0
- package/dist/sent-writes-N7X4OVXT.js +22 -0
- package/dist/source-resolve-S2XA3XZF.js +83 -0
- package/dist/static-host-B2Y6AG3W.js +352 -0
- package/dist/status-command-RUDXTF7I.js +554 -0
- package/dist/store-6VNCD4D7.js +20 -0
- package/dist/store-ARM6VMO5.js +98 -0
- package/dist/store-DuYfzKlW.d.ts +3144 -0
- package/dist/suggest-6FOOYKHG.js +12 -0
- package/dist/sveltekit.d.ts +154 -0
- package/dist/sveltekit.js +109 -0
- package/dist/sync-contributions-UCEROO26.js +415 -0
- package/dist/table-ENAVMITR.js +42 -0
- package/dist/tables-command-GUPXXSB5.js +156 -0
- package/dist/tenant-IIHPOHGP.js +42 -0
- package/dist/tenant-command-D5HGI2X4.js +965 -0
- package/dist/test-command-TIKZMDPZ.js +476 -0
- package/dist/tests-BGGIOK3L.js +26 -0
- package/dist/token-NICJUTMT.js +72 -0
- package/dist/toolchain-hooks-GHKLUJOH.js +96 -0
- package/dist/toolchain-modules-5EAZUONB.js +24 -0
- package/dist/tracked-backend-EIBUMDZA.js +80 -0
- package/dist/update-check-2W2NLDTC.js +136 -0
- package/dist/upgrade-command-QHNFWM7O.js +531 -0
- package/dist/verify-rollout-DRCK5TDD.js +61 -0
- package/dist/whoami-command-STGINTXH.js +51 -0
- package/dist/workspace-2CLHU2LK.js +93 -0
- package/dist/workspace-binding-56PIIPCP.js +54 -0
- package/dist/workspace-command-JEXQETJD.js +535 -0
- package/dist/workspace-env-F2RFMSC5.js +70 -0
- package/dist/workspace-export-OJSH2HIW.js +21 -0
- package/dist/writable-VA7OISSK.js +25 -0
- package/dist/xano-ICFUSPFR.js +42 -0
- package/dist/xanosdk-import-GN42TO2B.js +56 -0
- package/dist/xanosdk-project-S6DXJONQ.js +41 -0
- package/dist/xdo-CxDr2Q1C.d.ts +1371 -0
- package/guides/README.md +33 -0
- package/guides/authoring.md +921 -0
- package/guides/cli.md +401 -0
- package/guides/codegen.md +177 -0
- package/guides/codes.md +404 -0
- package/guides/coverage.md +74 -0
- package/guides/deploying.md +1525 -0
- package/guides/environment.md +400 -0
- package/guides/marketplace.md +415 -0
- package/guides/module-contract.md +558 -0
- package/guides/object-kinds.md +433 -0
- package/guides/project-structure.md +43 -0
- package/guides/scaffold.md +351 -0
- package/guides/typed-frontend.md +242 -0
- package/llms/client.md +63 -0
- package/llms/errors.md +56 -0
- package/llms/fields.md +176 -0
- package/llms/filters.md +183 -0
- package/llms/kinds-agent-mcp.md +30 -0
- package/llms/kinds-core.md +100 -0
- package/llms/kinds-knowledge.md +20 -0
- package/llms/kinds-realtime.md +76 -0
- package/llms/lambda.md +94 -0
- package/llms/legacy.md +44 -0
- package/llms/lock.md +40 -0
- package/llms/object-kinds.md +34 -0
- package/llms/statements-calls.md +62 -0
- package/llms/statements-catalog.md +320 -0
- package/llms/statements-data.md +66 -0
- package/llms/statements-runtime.md +68 -0
- package/llms/tests.md +59 -0
- package/llms/toolchain.md +16 -0
- package/llms/triggers.md +36 -0
- package/llms/values.md +29 -0
- package/llms-full.txt +2085 -0
- package/llms.txt +493 -0
- package/manifest.json +11224 -0
- package/package.json +162 -0
- package/types/lambda-globals.d.ts +129 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Knowledge def shape
|
|
2
|
+
|
|
3
|
+
> Read when the workspace defines a `knowledge()` item for its AI agents.
|
|
4
|
+
|
|
5
|
+
- `knowledge({ name, description?, type?, mode?, enabled?, body, refs?, guid?, tags? })` — markdown Xano's own AI reads before it acts — the workspace's builder agent, and outside agents reading the workspace through the Meta API or MCP. It does NOT reach an `agent()` run by `s.ai.agent.run`, whatever its `type` or `mode`: put what such an agent must know in its `llm.systemPrompt`. Takes `registerKnowledge`.
|
|
6
|
+
- `body` (REQUIRED): `knowledgeFile("./runbook.md", import.meta.url)` — a path to a real markdown FILE. There is no inline-string form. The path resolves relative to the MODULE THAT DECLARES the item, not the process working directory and not the workspace entry; `import.meta.url` is required and is what makes that true.
|
|
7
|
+
- `refs?`: `knowledgeDir("./runbook", import.meta.url)` — a directory whose WHOLE tree ships, recursively, paths preserved: UTF-8 text only (else refused; binary `.DS_Store` skipped), symlinks not followed. Keep the body OUT of it, or it ships as a reference file.
|
|
8
|
+
- `type?`: `"skill"` (default) | `"doc"` | `"agents.md"`. `skill` and `doc` differ in how they are filed, not in what an agent receives.
|
|
9
|
+
- `mode?`: `"auto"` (default) | `"always"` | `"referenced"`.
|
|
10
|
+
- `enabled?` defaults `true`. A disabled item is stored and never reaches an agent.
|
|
11
|
+
|
|
12
|
+
### What reaches the agent
|
|
13
|
+
|
|
14
|
+
- `type: "agents.md"` — full body on EVERY turn, whatever `mode` says. At most ONE per workspace; a second is an export error. Setting `mode` on one warns, because nothing reads it.
|
|
15
|
+
- `mode: "always"` — full body on every turn.
|
|
16
|
+
- `mode: "referenced"` — full body only on turns whose message names the item (bare name or `@name`).
|
|
17
|
+
- `mode: "auto"` — name and `description` only; the agent loads the body when a request matches.
|
|
18
|
+
- Reference files are never injected wholesale — an agent searches them on demand. That is what makes a large `mode: "auto"` item cheap.
|
|
19
|
+
- ⚠ `mode: "always"` on a long body spends its whole length on every request, and nothing in the types says so. `"auto"` is the default for that reason. For an `auto` item `description` is ALL the agent sees until it decides to load the body, so write it to be matched against a request rather than as a title.
|
|
20
|
+
- `name` takes letters, digits, and `/ _ - { } . ` or a space; anything else is an export error.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Realtime def shapes
|
|
2
|
+
|
|
3
|
+
> Read when the workspace defines a `realtimeServer()`, `realtimeChannel()`, or `realtimeMessage()` handler.
|
|
4
|
+
|
|
5
|
+
- **Realtime** — the only three-level chain: `realtimeServer` owns `realtimeChannel`s, which own `realtimeMessage` handlers. Pass the HANDLE, not a name (a channel path is unique only within its server).
|
|
6
|
+
- `realtimeServer({ name, guid?, description?, enabled?, canonical?, tags?, history? , middleware? })` — the container.
|
|
7
|
+
- `enabled` defaults to **false** — the one `enabled` in the SDK that does.
|
|
8
|
+
- An enabled server with no active channel still refuses the handshake.
|
|
9
|
+
- `realtimeChannel({ name, server, guid?, description?, active?, input?, anonymousClients?, presence?, publish?, conversation?, delivery?, rateLimit?, tags?, history?, middleware? })`
|
|
10
|
+
- `name` is a PATH (`"lobby"`, `"rooms/{room_id}"`); `input` types its `{param}` segments, NOT the payload. Every `{param}` MUST have a matching input, and that input must be a SCALAR and not a list (`json`/`object`/`array: true` have no URL form), or `realtimeChannel()` THROWS. `required: true` is NOT checked and is not needed — segment counts must match, so the segment is always present at join. Name charset as query (`A-Za-z0-9_-/{}`, max 200), and so is `tool`; `realtimeMessage` is NARROWER — no `/` or `{}`.
|
|
11
|
+
- Matching is STRICT: a literal segment beats a param (`rooms/lobby` and `rooms/{room_id}` coexist); segment counts must be EQUAL (`rooms/{room_id}` does NOT match `rooms/42/edit`); literals are CASE-SENSITIVE; an empty segment is REJECTED, not collapsed (a leading/trailing/doubled `/` matches nothing). `realtimeChannel()` refuses it and a partial-segment `{param}`; `getChannel()` a value outside `[A-Za-z0-9_-]`.
|
|
12
|
+
- An INACTIVE channel reports the same error as a nonexistent one — deactivating leaks nothing.
|
|
13
|
+
- `middleware?: { pre?, post? }` is the CONTAINER tier, on the channel and on the server alike — the default chain every message inherits, the same shape an api group has. Full chain: message -> channel -> server -> branch/workspace, mirroring `history`. Put a realtime auth or rate-limit guard here rather than repeating it on every `realtimeMessage`.
|
|
14
|
+
- `anonymousClients` also gates CONNECT: a tokenless socket connects only if an active channel of its server sets it (no server flag); each channel admits its own join.
|
|
15
|
+
- `publish?: { who?: "nobody"|"anyone"|"authenticated", direct? }` — `who` defaults to `nobody`: nobody can publish until you set it. `direct` (default false) allows DIRECT messages — a frame whose `options.to = { dbo_id, row_id }` addresses one USER (`dbo_id` = the auth table's id as `s.realtime.get_session` reports it, `row_id` = the user's row id) and reaches every socket of theirs on the channel, no one else; the sender gets a receipt `{ delivered_local, direct: true }`. `direct` REPLACES `who` for such a frame rather than adding to it (`direct: true` with `who: "nobody"` delivers direct messages and refuses broadcasts); an anonymous sender is refused. A direct message is never stored in the transcript or the `at_least_once` stream, so an offline user never gets it. ⚠ An `options.to` naming no usable identity is sent as an ordinary broadcast to the whole channel where `who` admits the sender.
|
|
16
|
+
- `conversation?: { enabled?, limit?, ttl? }` — the client-visible TRANSCRIPT replayed to a joiner (distinct from `history`, which is execution history). ⚠ `limit` DEFAULTS TO 0 AND 0 MEANS OFF: `{ enabled: true }` alone records nothing and replays nothing, silently. `ttl` is an IDLE expiry of the WHOLE transcript, refreshed by every write (an active channel never ages out; a silent one loses all of it at once) — NOT a per-message age cap.
|
|
17
|
+
- `delivery?: { guarantee?: "at_most_once"|"at_least_once", perRecipient? }` — `perRecipient` is independent of the guarantee, is a NO-OP unless the channel declares a `deliver` trigger, and costs a stack PER RECIPIENT PER MESSAGE. Per-viewer redaction needs BOTH HALVES — this flag AND an active `deliver` trigger bound to the channel — and with either missing the payload is delivered UNCHANGED to everyone; `export()` warns on each half alone.
|
|
18
|
+
- `rateLimit?: { messagesPerMinute? }` — 0 = unlimited, checked BEFORE the handler runs. A COST guardrail, not a security control: an anonymous client is bucketed per CONNECTION (reconnecting resets it), and it fails OPEN when its store is down.
|
|
19
|
+
- `realtimeMessage({ name, channel, server?, guid?, description?, active?, auth?, deliverTo?, input?, middleware?, stack?, response?, responseShape?, history?, disabled?, tags? })` — the invocable unit (the realtime analogue of a query).
|
|
20
|
+
- `input` types the message PAYLOAD. `server` is required only when `channel` is a bare path.
|
|
21
|
+
- `deliverTo?`: `"channel"` (default) | `"sender"` | `"others"` | `"explicit"`. ⚠ `"explicit"` still delivers to NOBODY — nothing selects recipients from inside a handler, and `s.realtime.publish` (which originates an event INTO a channel) is not a substitute.
|
|
22
|
+
- Only `"channel"`/`"others"` fan out AND are RECORDED in the `conversation` transcript — a `"sender"` response is invisible to every future joiner.
|
|
23
|
+
- **Both input surfaces read as ordinary inputs:** `inp("body")` for a payload field, `inp("room_id")` for the channel's `{room_id}`. No session lookup, no frame parsing.
|
|
24
|
+
- A path param is bound ONCE at join and read from the connection thereafter, never from the frame — a sender cannot claim a room it did not join.
|
|
25
|
+
- `s.realtime.get_session({ as })` — the CALLER's realtime session for the current frame. FLAT shape:
|
|
26
|
+
- `authenticated` bool · `client_id` text (the AUTHED ROW ID as text, `""` anonymous) · `dbo_id` int (the auth TABLE's id — NOT the user's row id; `0` anonymous — to look the caller up use `client_id`. `dbo_id` is an int in the same position and typechecks, so a gate that keys on it finds no user and refuses EVERYONE) · `socket_id` int (transport id) · `channel` text (resolved path, `""` in a server trigger) · `params` object (bound path params, `{}` when none — `ref("session.params.room_id")`) · `extras` object · `opened_at` decimal.
|
|
27
|
+
- Works in a realtime MESSAGE stack and in CHANNEL and SERVER trigger stacks; off that path it degrades to an anonymous session.
|
|
28
|
+
- For a path param prefer `inp("room_id")` in a MESSAGE; a lifecycle TRIGGER has only the session. Reach for the session when you need the CONNECTION (identity/extras) — "who is this sender" on an anonymous-client channel.
|
|
29
|
+
- ⚠ THREE UNRELATED THINGS ARE CALLED A CLIENT ID: `session.client_id` (app-facing identity), `session.socket_id` (transport), and a frame's `options.client_id` (the at_least_once CURSOR handle). Conflating the first and last breaks at_least_once for anonymous clients.
|
|
30
|
+
- `s.realtime.publish({ server, channel, data, message?, authTable?, authId? })` — the PUSH direction: originate a server-authored event onto a channel from ANY stack, no client frame first.
|
|
31
|
+
- `server` is the handle or its NAME (resolved by name, not guid); `channel` is the FILLED-IN path (`channel.getChannel({ room_id: 42 })`), never the template — a constant still carrying `{param}` THROWS at author time, and a constant `server`/`channel` naming nothing this workspace registers WARNS at export.
|
|
32
|
+
- A PER-ROW path whose id is only known at runtime is built as a value, not with `getChannel()` (which needs the id at author time): `withFilters(c.text("rooms/"), fl.concat(ref("room.id")))`, or `s.set_var` + `s.text.prepend`. A computed `channel`/`server` — a `ref`/`inp`, or a constant carrying a filter chain — is left alone by the export check.
|
|
33
|
+
- DELIVERY-ONLY — fanned out as-is; does NOT invoke a `realtimeMessage()` handler even when `message` names one (a channel `deliver` trigger still runs).
|
|
34
|
+
- SERVER-AUTHORITATIVE — bypasses `publish.who`, which governs CLIENTS. Authorize in your own stack.
|
|
35
|
+
- ⚠ FAIL-SOFT — a missing/disabled server or dead bus is swallowed engine-side, so a mis-targeted publish is SILENT with no result to check.
|
|
36
|
+
- `authTable`/`authId` are ASSERTED attribution on the frame — not a credential, nothing validates them.
|
|
37
|
+
- **Client recipe (derive, never hardcode):**
|
|
38
|
+
- `server.getUrl(baseUrl)` → `wss://<host>/ws/<canonical>` — accepts the `https://…` instance base URL and normalizes the scheme. `channel.getChannel({ room_id: 42 })` → `"rooms/42"`, the path that goes in a frame's `channel` field. Both throw rather than guess. A canonical is minted by `xanosdk export <entry> --lock`.
|
|
39
|
+
- Auth is a bearer token passed as the websocket SUBPROTOCOL: `new WebSocket(url, token)`. No token = an anonymous client, admitted only where `anonymousClients: true`.
|
|
40
|
+
- Frames are JSON `{ action: "join"|"leave"|"broadcast"|"ack"|"ping"|"presence", channel, type?, payload?, options?, id? }`. `broadcast` REQUIRES `type`: a `realtimeMessage` of that channel, else refused `Unknown message type`. You must `join` before you may `broadcast`, and the server's context is ready only a moment after `open` — an immediate first frame is refused.
|
|
41
|
+
- `options` is `{ to?, client_id?, channel? }` — `to: { dbo_id, row_id }` makes the frame a direct message (needs `publish.direct`), `client_id` is the at-least-once cursor handle, and `options.channel` WINS over a top-level `channel`.
|
|
42
|
+
- ⚠ KEEP THE SOCKET ALIVE: an idle connection is REAPED after ~10 minutes. A LISTEN-ONLY client (a feed or dashboard that joins and rarely publishes) MUST send `{ action: "ping" }` (answered `pong`) or any frame periodically or it silently drops.
|
|
43
|
+
- Server frames: `join` (ack `{ joined: true, params }`, + `cursor`/`resumed` on at_least_once) · `message` · `replay` · `broadcast` · `presence_full`|`presence_join`|`presence_leave` · `conversation_start`|`conversation_end` (replayed frames flagged `conversation: true`) · `pong` · `ack` · `error`.
|
|
44
|
+
- ⚠ `broadcast` is a RECEIPT to the sender, not a delivery confirmation: `payload.delivered_local` counts recipients on the ANSWERING NODE ONLY, not the channel. It also carries `id` on at_least_once and `dropped: true` when the handler returned null.
|
|
45
|
+
- `error` carries `payload.message`, plus `code`/`limit`/`retry_after` when rate limited. `rate_limited` is the ONLY code — do NOT switch on `code`.
|
|
46
|
+
- An `error` is a per-frame refusal, NOT a disconnect — EXCEPT a failed handshake and a REFUSED `connect` trigger, which each send one and then CLOSE with code 4401.
|
|
47
|
+
- **Tenant instances (isolated DB):** a tenant's realtime objects live in the TENANT's database, so BOTH halves of a client must name the tenant.
|
|
48
|
+
- Socket: `server.getUrl(base, { tenant })` → `/ws/<tenant>:<canonical>`. ⚠ A bare canonical on a tenant host resolves against the INSTANCE workspace instead.
|
|
49
|
+
- That colon form is PECULIAR TO THE SOCKET. Every other tenant URL gives the tenant its OWN segment — the HTTP half of the same client is `https://<host>/tenant/<tenant>/api:<canonical>/…`. NO request header is required for either.
|
|
50
|
+
- Because the shapes differ, `getUrl` TRANSLATES a tenant base URL instead of concatenating: pass the `https://<host>/tenant/<name>` that `xanosdk status` prints (and that deploy injects as `window.XANO_HOST`) and the tenant is LIFTED into the socket form. So `getUrl(window.XANO_HOST)` needs no `{ tenant }`, and a CONFLICTING `{ tenant }` alongside it throws.
|
|
51
|
+
- ⚠ `getUrl`/`socketUrl` are NOT idempotent — a `baseUrl` that already carries a `/ws/<…>` path THROWS. Resolve ONCE from the http(s) base; never feed a result back in.
|
|
52
|
+
- Still pass `{ tenant }` explicitly for a tenant on its OWN DOMAIN — the hostname carries it for HTTP, but there is nothing in the URL for the socket to lift.
|
|
53
|
+
- ⚠ Tokens are tenant-scoped (audience `<tenant>:<license>`, not the bare license), so one minted through the instance workspace is REJECTED by a tenant's realtime server — authenticate and dial through the same tenant.
|
|
54
|
+
- **Presence frames** (a `presence: true` channel only):
|
|
55
|
+
- `presence_full` carries `payload.members`, an ARRAY: at join it EXCLUDES the receiver (first joiner: `[]`), on a re-request it includes it. `presence_join`/`presence_leave` carry a single `payload.member`.
|
|
56
|
+
- A member is `{ id, dbo_id, authenticated, extras, joined_at }`: `id` the auth row id as a string (`"0"` anonymous), `dbo_id` the auth table's id (`0` anonymous), `extras` the connection's extras (`[]` if none), `joined_at` epoch SECONDS.
|
|
57
|
+
- Render `presence_full`, then apply deltas. One member per authenticated identity (a second tab fires no `presence_join`); anonymous ones all have `id` `"0"`, never collapsed.
|
|
58
|
+
- Join order: `join` ack → `presence_full` → (others get `presence_join`) → conversation replay → `replay` frames.
|
|
59
|
+
- A joined client re-requests it with `{ action: "presence", channel }`, answered to the SENDER only; a socket that never joined is REFUSED.
|
|
60
|
+
- **Conversation frames — the transcript hydrates the client, so DO NOT build a hydration endpoint.**
|
|
61
|
+
- On a `conversation` channel the replay is PUSHED automatically at join, unasked: `conversation_start` (`payload.count`) → the last `limit` messages, each a normal `action: "message"` frame carrying its ORIGINAL `type` and `payload` plus `conversation: true` and the original `ts` → `conversation_end`.
|
|
62
|
+
- Render `message` frames identically either way: no fetch or table read is needed to paint the initial view.
|
|
63
|
+
- ⚠ `{ enabled: true }` alone is a no-op: `limit` defaults to 0 and 0 means RETAIN NONE (see `conversation` above) — always pass `limit`.
|
|
64
|
+
- The POST-HANDLER broadcast payload IS the stored transcript row — a handler must broadcast everything the UI needs to render a past message (author name, id, `created_at`). Nothing else is replayed.
|
|
65
|
+
- The transcript is a capped ring (`limit`, `ttl`), not storage: persist to a table only for durability, search, or reads beyond it.
|
|
66
|
+
- **`delivery.guarantee: "at_least_once"` is a CLIENT CONTRACT, not just a channel setting.**
|
|
67
|
+
- The client must ACK what it receives — `{ action: "ack", channel, id }`, confirmed by `{ action: "ack", channel, payload: { cursor } }`.
|
|
68
|
+
- ⚠ An ANONYMOUS client must ALSO send a durable `options.client_id` in its JOIN frame (once; later acks need not repeat it). WITHOUT one it has no cursor, its acks are SILENTLY IGNORED, and it degrades to at_most_once. An AUTHENTICATED client is keyed by identity and needs no `client_id`.
|
|
69
|
+
- The missed gap arrives after join as `replay` frames, oldest-first, each with an `id` to ack. ⚠ A first join gets `cursor: "0"`: the WHOLE retained stream replays, repeating a `conversation` transcript.
|
|
70
|
+
- DISTINCT from the conversation transcript: `conversation_*` is the SHARED "what was said before I arrived", `replay` is the PER-CLIENT "what I missed while disconnected". Both may be on.
|
|
71
|
+
- How far back `replay` reaches is sized by `conversation.ttl` (here a REAL per-message age cut, and it BEATS `limit`), else `conversation.limit`, else 1000 — even on a channel with no transcript enabled.
|
|
72
|
+
- **What a message handler RETURNS decides delivery, and the failure directions are NOT symmetric.**
|
|
73
|
+
- A returned value fans out per `deliverTo` and becomes the transcript row.
|
|
74
|
+
- Returning NULL delivers NOTHING — the supported way to veto a message (the sender is told `dropped: true`).
|
|
75
|
+
- A payload REJECTED by the declared `input` also delivers nothing; the detail goes ONLY to the sender.
|
|
76
|
+
- ⚠ But a handler that CRASHES FAILS OPEN: the sender's ORIGINAL, UNVALIDATED payload is broadcast to the channel unchanged. A handler doing redaction or authorization must NOT be the only thing between client input and subscribers.
|
package/llms/lambda.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Lambda bodies (JavaScript)
|
|
2
|
+
|
|
3
|
+
> Read when writing a JavaScript body, or weighing whether to reach for one at all — `lam.fn`, `fl.lambda`, `fl.reduce`, `s.lambda`; each surface binds a different set of identifiers.
|
|
4
|
+
|
|
5
|
+
**A lambda is an escape hatch, not a default.** The body runs outside the request's own
|
|
6
|
+
runtime, and a workspace has a BOUNDED pool of lambda workers every lambda in it shares
|
|
7
|
+
— so a call both crosses a process boundary and draws on a workspace-wide resource.
|
|
8
|
+
Reach for one only when the typed surface cannot express the work: if a native filter,
|
|
9
|
+
an `expr(...)`/`obj(...)` expression, or a plain statement can, use that. The crossing
|
|
10
|
+
is per CALL, not per element — an iterating filter sends the body ONCE and loops on the
|
|
11
|
+
other side, so one body over a whole list beats one called from inside a stack loop.
|
|
12
|
+
|
|
13
|
+
The lambda statement (`s.lambda({ as, code, timeout? })`) and eight filters run a
|
|
14
|
+
JavaScript body. **Write the body as a FUNCTION, not a `c.text` string** — the
|
|
15
|
+
bindings are its parameters, so the editor supplies them and a wrong name is a
|
|
16
|
+
compile error instead of a wrong value at runtime. Write it inline and the surface
|
|
17
|
+
is implied by where it sits; nothing names one:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
fl.map(({ $this }) => $this * 2) // map's bindings, typed from the position
|
|
21
|
+
fl.reduce({ initial_value: 0, code: ({ $result, $this }) => $result + $this })
|
|
22
|
+
s.lambda({ as: "total", code: ({ $var }) => $var.subtotal * 1.2 }) // ambient only — $this is a compile error
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The parameters are a fiction — only the BODY is sent, and the engine injects the
|
|
26
|
+
bindings as free identifiers — so DESTRUCTURE them as named: `(b) => b.$this`,
|
|
27
|
+
`{ $this: x }`, nesting, a default or rest are undefined there (refused).
|
|
28
|
+
|
|
29
|
+
⚠ An inline `code:` arrow receives BINDINGS ONLY. `capture` is an option of
|
|
30
|
+
`lam.fn`, not a field of `s.lambda` or a filter, so `capture:` beside `code:` is a type
|
|
31
|
+
error — the fix is to relocate the body, not to drop the field: `lam.fn(fn, { capture })` (below).
|
|
32
|
+
|
|
33
|
+
For a body built away from its call site:
|
|
34
|
+
|
|
35
|
+
- `lam.fn(({ $result, $this }) => $result + $this, { surface?, capture? })` — name a `surface` to check it here, or omit it and the call site checks it.
|
|
36
|
+
- `lam.raw("return 1", { surface })` — text, same validation.
|
|
37
|
+
- `lam.file("./lambdas/total.ts")` — a default-exported function in its own module (path relative to the caller), read as text at build time. NODE ONLY: `import { lam } from "@xano/sdk/node"` (isomorphic `lam` has no `file`). Only the default export's BODY is sent, so a value import, a second export or a top-level helper is refused — move helpers inside; `import type` / `import { type X }` are free. `@xano/sdk/lambda-globals` types the globals below program-wide: keep modules in `xano/lambdas/` (its scaffold tsconfig loads it) or `import type {} from` it under their own tsconfig.
|
|
38
|
+
|
|
39
|
+
Nothing from the enclosing scope crosses: the body is sent as TEXT, so a closed-over
|
|
40
|
+
`const rate` is undefined there (a wrong VALUE at HTTP 200, not an error). Put what the
|
|
41
|
+
body needs in `capture`; it arrives as the SECOND parameter, emitted as a `const` prelude:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
lam.fn(({ $this }, { capturedRate }) => $this * capturedRate, { surface: "map", capture: { capturedRate: rate } })
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
⚠ A capture key must NOT share its name with a module-scope binding: a `.ts` loader
|
|
48
|
+
renames one of two same-named bindings, so the body reads `rate2` while the prelude
|
|
49
|
+
declares `rate` (refused at build time). `capture: { capturedRate: rate }` is the safe form.
|
|
50
|
+
|
|
51
|
+
Capture JSON data only: a function, `NaN`, a sparse/typed array or class instance (`Date`, `Map`…)
|
|
52
|
+
is refused at build time, at any depth. Capture the plain form and rebuild in
|
|
53
|
+
the body (`d.getTime()` → `new Date(d)`). The captured type flows into the second parameter.
|
|
54
|
+
|
|
55
|
+
A body is a FUNCTION BODY: it must `return` its value. Bindings by surface — an
|
|
56
|
+
identifier outside its surface's set is undefined at runtime, and the SDK refuses
|
|
57
|
+
it at build time whichever spelling you use:
|
|
58
|
+
|
|
59
|
+
- every surface: `$env` · `$input` · `$var` · `$auth` (+ the `console` / `crypto` globals)
|
|
60
|
+
- `fl.lambda`: + `$this`
|
|
61
|
+
- `fl.map` · `fl.filter` · `fl.some` · `fl.every` · `fl.find` · `fl.findIndex`: + `$this` · `$index` · `$parent`
|
|
62
|
+
- `fl.reduce`: + `$this` · `$index` · `$parent` · `$result`
|
|
63
|
+
- `s.lambda`: ambient only — no `$this`, no `$parent`, no `$result`.
|
|
64
|
+
|
|
65
|
+
`$result` is `reduce`'s ACCUMULATOR (there is no `$acc`). `$this` is the element in
|
|
66
|
+
an iterating filter and the piped value in `fl.lambda`; `$parent` is the whole array
|
|
67
|
+
and exists only on the iterating filters. A stack variable is reached as
|
|
68
|
+
`$var.name` — it is NOT also injected as a bare `$name`.
|
|
69
|
+
|
|
70
|
+
Four hazards and the dependency route, all live-verified:
|
|
71
|
+
|
|
72
|
+
- ⚠ A body that THROWS does not fail the request: the engine returns its diagnostic
|
|
73
|
+
TEXT as the value with HTTP 200, so the failure reads as bad data. Validate before
|
|
74
|
+
consuming a lambda result numerically, and prefer a `lam.*` body, which cannot fail
|
|
75
|
+
this way for a binding reason.
|
|
76
|
+
- ⚠ `timeout` is COOPERATIVE — observed only at an `await` — so it bounds WAITING (a slow
|
|
77
|
+
`fetch`), not compute: a 1s `timeout` over a 3s busy-loop runs all 3s and returns
|
|
78
|
+
normally. Bound a loop that could run away inside the body.
|
|
79
|
+
- ⚠ A top-level `import`/`export` is a syntax error — the body is a function body. Reach
|
|
80
|
+
a dependency through the PRELOADED globals below. A dynamic `import("…")`/`require("…")`
|
|
81
|
+
with a LITERAL specifier is not portable: an instance that bundles the body first
|
|
82
|
+
returns the TEXT `Could not resolve "node:crypto"` with HTTP 200.
|
|
83
|
+
- Preloaded globals, live-probed — no specifier, so these work everywhere:
|
|
84
|
+
`_` · `aws4` · `axios` · `cryptojs` · `DateTime` · `ethers` · `fastXmlParser` ·
|
|
85
|
+
`jose` · `luxon` · `mailparser` · `math` · `moment` · `nodemailer` · `socks` ·
|
|
86
|
+
`uuid` · `utils`
|
|
87
|
+
…plus `fetch`, `Buffer`, `TextEncoder`/`TextDecoder`, and the `crypto` above
|
|
88
|
+
(`randomUUID`, `createHmac`, `createHash`, `subtle`). `Object.keys(globalThis)`
|
|
89
|
+
inside a body lists whatever else a given instance carries.
|
|
90
|
+
- ⚠ `console` output goes to the request LOG, not stdout. `log` · `error` · `warn` ·
|
|
91
|
+
`info` · `debug` · `trace` all route there; any other `console` method is undefined,
|
|
92
|
+
and CALLING it throws (error text as the value, HTTP 200).
|
|
93
|
+
|
|
94
|
+
TypeScript annotations survive in the body, and top-level `await` works.
|
package/llms/legacy.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Legacy paradigms and retired statements
|
|
2
|
+
|
|
3
|
+
> Read when the code was PULLED from an existing Xano instance — how a codegen'd tree reads, the shapes in it that must not be "fixed", and any `raw({ name: "mvp:…" })`, `realtimeTrigger()`, or name the catalogs do not list.
|
|
4
|
+
|
|
5
|
+
Older paradigms this SDK still supports and still emits when it decodes an existing
|
|
6
|
+
workspace. **Do not author these.** They are listed by name only so you recognize them
|
|
7
|
+
in pulled code rather than "fixing" them; each line names what to use instead.
|
|
8
|
+
|
|
9
|
+
Names overlap across the split deliberately — the engine reused words like
|
|
10
|
+
"realtime" and "channel" for both generations. A name matching is NOT evidence that
|
|
11
|
+
two things are the same object; check which list it came from.
|
|
12
|
+
|
|
13
|
+
- `c.expressionLegacy` — the older `const:expr` expression form, emitted by codegen for workspaces that still hold one — author `c.expression` instead.
|
|
14
|
+
- `realtimeTrigger()` — the SUPERSEDED realtime trigger, against the workspace-global realtime layer — a different object from the current `channel`, despite the similar name. For a join hook use `realtimeChannelTrigger({ actions: { join: true } })`; for message handling use a `realtimeMessage()` handler, which is the current equivalent of its `message` action (a message is an authored unit now, not a trigger action).
|
|
15
|
+
- `s.api.realtime_event` — publishes to the SUPERSEDED workspace-global realtime layer, NOT to a `realtimeChannel()` — its `channel` is a string against that layer, so pointing it at a current-layer channel path publishes into the void. Use `s.realtime.publish` instead: it names the owning `realtimeServer()`, so it addresses a real `realtimeChannel()`.
|
|
16
|
+
|
|
17
|
+
Retired statement VERSIONS — no `s.` surface exists. Pulled code shows them as
|
|
18
|
+
`raw({ name: "…" })` and they keep running as stored, so leave them; author the
|
|
19
|
+
replacement only for NEW code. Never swap one for the other — each version broke the last.
|
|
20
|
+
|
|
21
|
+
- `mvp:crypto_jwe_decode` → `mvp:crypto_jwe_decode2`
|
|
22
|
+
- `mvp:crypto_jwe_encode` → `mvp:crypto_jwe_encode3`
|
|
23
|
+
- `mvp:crypto_jwe_encode2` → `mvp:crypto_jwe_encode3`
|
|
24
|
+
- `mvp:crypto_jws_decode` → `mvp:crypto_jws_decode2`
|
|
25
|
+
- `mvp:crypto_jws_encode` → `mvp:crypto_jws_encode2`
|
|
26
|
+
- `mvp:connect_ncscale_send_log` — retired, no replacement
|
|
27
|
+
|
|
28
|
+
Statements the engine writes but will NOT import back — no `s.` surface exists, and
|
|
29
|
+
unlike the retired versions above these must be FIXED, not left alone. Pulled code shows
|
|
30
|
+
them as `raw({ name: "…" })`; `export()` refuses any bundle that still contains one.
|
|
31
|
+
|
|
32
|
+
- `mvp:placeholder` — an unconfigured statement slot the engine writes in place of a statement it could not resolve, so an export stays well-formed. There is no statement class behind it: importing a workspace that contains one fails outright with "Missing statement: mvp:placeholder". Replace it with the statement it stands in for before deploying — there is no destination where it runs.
|
|
33
|
+
|
|
34
|
+
## Reading a pulled tree
|
|
35
|
+
|
|
36
|
+
`codegen` emits objects as FACTORY calls (`table({...})`, `query({...})`, …) — the form
|
|
37
|
+
you author by hand — so inference survives the round trip.
|
|
38
|
+
Three shapes read differently and must not be "fixed": a trigger its factory cannot
|
|
39
|
+
reproduce (its target outside the pulled tree, or `meta` it cannot write) stays
|
|
40
|
+
`{...} satisfies TriggerDef`; a
|
|
41
|
+
statement the catalog cannot model round-trips verbatim through `raw()`
|
|
42
|
+
(`@xano/sdk/codegen`); and an object ALREADY EMPTY upstream decodes to a def with
|
|
43
|
+
no `stack`, reported as `empty-source` — faithful, not a decode failure. Workspace env
|
|
44
|
+
var VALUES go to `xano/.env` (owner-only, gitignored); `xano/workspace.ts` declares each as `""`.
|
package/llms/lock.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Lock file
|
|
2
|
+
|
|
3
|
+
> Read when a `xano.lock` exists or should — renaming/pruning/importing identities, seeding the lock programmatically, or asking which commands write it.
|
|
4
|
+
|
|
5
|
+
`xano.lock` pins each object's guid and each api-group/toolset canonical, so renames
|
|
6
|
+
stay renames (guids otherwise derive from `(type, name)`; a query's from `(api group,
|
|
7
|
+
verb, name)`). Committed. A composed entry is keyed by that identity —
|
|
8
|
+
`query:<group>|<verb>|<name>`, `channel:<server>|<path>`, `message:<server>|<path>|<name>`
|
|
9
|
+
— its guid's seed; a legacy `query:<name>` is migrated by the next locked export.
|
|
10
|
+
|
|
11
|
+
- `xanosdk lock rename <kind> <old> <new>` — `kind` is the SDK kind (`table`, `apiGroup`) or stored key (`dbo`).
|
|
12
|
+
Run it when `export` names it after a rename in code (it prints the command); the next export emits the original guid under the new name.
|
|
13
|
+
Reads the `index.ts` beside the lock (or `--entry`); refused while `<old>` is still exported. Composed names go in full; renaming a parent moves them.
|
|
14
|
+
- `xanosdk lock prune <entry-file> [keys…] --yes` — drops orphaned entries. Finding orphans
|
|
15
|
+
RUNS the entry's module scope (env assertions included); `--identity-only --yes <key>…`
|
|
16
|
+
prunes named keys (`table:users`) with no evaluation and no orphan check.
|
|
17
|
+
- `xanosdk lock import <live-bundle.json> [--yes]` — seed the lock from a `workspace export`
|
|
18
|
+
to take over an existing workspace (a compiled bundle is refused).
|
|
19
|
+
- Every lock subcommand accepts `--lock=<path>`; from outside the lock's directory pass it.
|
|
20
|
+
- Merge conflict: keep both sides; two keys on one guid → `lock rename` onto the kept
|
|
21
|
+
name, or `lock prune --identity-only --yes` the other.
|
|
22
|
+
- Programmatic use: call `seedLockOverrides(readLockFile(path))` BEFORE importing any def
|
|
23
|
+
module — references bake guids at import time; `writeBundle` refuses an unseeded lock.
|
|
24
|
+
|
|
25
|
+
What writes the lock: `export`/`deploy`/`release`/`preflight` of an ENTRY FILE update it;
|
|
26
|
+
`compile`/`paths` only READ. `--no-lock` builds
|
|
27
|
+
without one (names derive guids, the instance invents public URLs, no `--prune`);
|
|
28
|
+
REFUSED over a parseable lock. A DEPLOY or a RELEASE writes back only `landed`: what
|
|
29
|
+
this project LANDED on each destination (ephemerals: `.xano/ephemeral.json`). `--prune` deletes only that — never a merely
|
|
30
|
+
exported name; no record there, no prune until one landing (commit it). A `deploy` (`--replace`
|
|
31
|
+
too) PRESERVES the archive's guids, so the lock still describes the rebuilt
|
|
32
|
+
workspace. CI: `--frozen-lock` fails (`SDK_DRIFT`) instead of changing the lock, and fails while the lock
|
|
33
|
+
carries an entry no exported object matches — a plain `export` only warns and writes that
|
|
34
|
+
entry down, after which nothing tells a rename from a deletion: resolve it with
|
|
35
|
+
`lock rename` or `lock prune`, or accept them with `--allow-lock-orphans` when they are the
|
|
36
|
+
not-yet-ported half of an adopted workspace.
|
|
37
|
+
`export --check` implies `--frozen-lock` and writes nothing — no bundle, no lock — so it
|
|
38
|
+
reads no `xano/.env` or `.secrets.json` and refuses the flags that supply them; this is
|
|
39
|
+
what `npm run xano:check` runs, so CI needs no secrets. With no lock at all, either flag
|
|
40
|
+
fails only when the source has identities to record (an empty workspace passes).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Object kinds
|
|
2
|
+
|
|
3
|
+
> Read when deciding WHAT to build — every authorable primitive, what each is, and which factory + register method builds it. Also when `register*` will not typecheck an array built by `.flatMap()`/`.concat()`.
|
|
4
|
+
|
|
5
|
+
Author with the factory, register on the Xano instance, lands under the payload key. Each line ends with a one-liner on what the primitive is.
|
|
6
|
+
|
|
7
|
+
⚠ Composing a workspace from independently-authored modules: `register*` takes its defs however they arrive, but `modules.flatMap((m) => m.tables)` does NOT typecheck. `Array.prototype.flatMap` binds its element type to the FIRST element, so every later module's tables are checked against that one table's schema — the error compares two unrelated column names and mentions neither `flatMap` nor the cause. `.concat()` collapses the same way. Two spellings work: pass an array LITERAL (`registerTables([...a.tables, ...b.tables])` — spreads are fine, TypeScript infers the union across every element at once), or annotate the module array with the wide aliases `AnyTableDef` / `AnyQueryDef` / `AnyFunctionDef` / `AnyAddonDef`, exported for exactly the four def types whose generics can collapse this way. Widening the array costs nothing: the typing you need lives on the `table()`/`query()` handle you hold and pass to `s.db.*`, and is never read back off the registered array.
|
|
8
|
+
|
|
9
|
+
- function: `defineFunction` → `Xano.registerFunctions` → payload `function` — Reusable server-side logic (a custom function) callable from any stack via `s.function.run`.
|
|
10
|
+
- table: `table` → `Xano.registerTables` → payload `dbo` — A database table: typed columns (`f.*`), indexes, and views; the schema other kinds read and write.
|
|
11
|
+
- query: `query` → `Xano.registerQueries` → payload `query` — An HTTP API endpoint (verb + path) bound to an API group; the main request/response surface.
|
|
12
|
+
- api_group: `apiGroup` → `Xano.registerApiGroups` → payload `app` — A container that groups queries under a shared base path, CORS, and swagger config.
|
|
13
|
+
- trigger (database): `tableTrigger` → `Xano.registerTriggers` → payload `trigger` — Fires when rows change on a bound table (insert/update/delete/truncate). The changed row is exposed as `t.new`/`t.old`, typed to the table when a `table()` handle is bound. Config-only (no response). `search` filters rows in the DATABASE, so it uses `col("NEW.x")`/`col("OLD.x")`, not `t`; invalid with `truncate`, and insert/delete cannot read the absent side.
|
|
14
|
+
- trigger (realtime_server): `realtimeServerTrigger` → `Xano.registerTriggers` → payload `trigger` — Fires when a client connects to or disconnects from a realtime server; inspect the connecting client and its permissions via `t`. Bind with `realtimeServer`. Response-bearing.
|
|
15
|
+
- trigger (channel): `realtimeChannelTrigger` → `Xano.registerTriggers` → payload `trigger` — Fires when a client joins or leaves a channel; inspect the addressed channel path and the client via `t`. Bind with a `realtimeChannel()` handle (a bare path is ambiguous across servers). Response-bearing.
|
|
16
|
+
- trigger (toolset): `mcpServerTrigger` → `Xano.registerTriggers` → payload `trigger` — Fires when an MCP client connects to a bound MCP server; filter its tools/prompts/resources via `t`. Response-bearing.
|
|
17
|
+
- trigger (toolset): `agentTrigger` → `Xano.registerTriggers` → payload `trigger` — Fires when a client connects to a bound agent; gate or annotate its toolset via `t.toolset`/`t.tools`. Response-bearing.
|
|
18
|
+
- trigger (workspace): `workspaceTrigger` → `Xano.registerTriggers` → payload `trigger` — Fires on branch lifecycle events (branch new/merge/live); inspect the from/to branch and action via `t`. Config-only.
|
|
19
|
+
- trigger (error): `errorTrigger` → `Xano.registerTriggers` → payload `trigger` — Fires when an error signature is first seen, regresses, or is marked fixed; inspect the error, caller, statement, and occurrence counts via `t`. Config-only.
|
|
20
|
+
- tool: `tool` → `Xano.registerTools` → payload `tool` — An agent/MCP tool: a callable capability with typed inputs an AI agent can invoke.
|
|
21
|
+
- prompt: `prompt` → `Xano.registerPrompts` → payload `prompt` — An MCP prompt: typed arguments plus a stack that builds the messages an MCP client receives. Exposed by an `mcpServer`.
|
|
22
|
+
- resource: `resource` → `Xano.registerResources` → payload `resource` — An MCP resource: contents an MCP client reads by URI (a literal or a `{variable}` template), produced by a stack. Exposed by an `mcpServer`.
|
|
23
|
+
- mcp_server: `mcpServer` → `Xano.registerMcpServers` → payload `toolset` — An MCP server exposing tools, prompts and resources to external MCP clients.
|
|
24
|
+
- agent: `agent` → `Xano.registerAgents` → payload `toolset` — An AI agent: an LLM configuration plus the tools it can call. Invoke it from any stack (query/function/task/tool/trigger) with `s.ai.agent.run` — no public endpoint; the result is a rich envelope whose completion text is at `.result`.
|
|
25
|
+
- task: `task` → `Xano.registerTasks` → payload `task` — A scheduled background job (cron/interval) that runs a stack on a timer.
|
|
26
|
+
- workflow_test: `workflowTest` → `Xano.registerWorkflowTests` → payload `workflow_test` — An end-to-end test: a named stack with NO input and NO response that invokes other objects (`s.function.call`, `s.task.call`, `s.api.call`) and asserts on what they bind with `s.expect.*`. `datasource` defaults to `""` (an EMPTY datasource, recommended); naming one CLONES it before every run — see `llms/kinds-core.md`.
|
|
27
|
+
- middleware: `middleware` → `Xano.registerMiddleware` → payload `middleware` — A reusable pre/post stack attached to a query/function/task/tool/API group to run before or after its own logic.
|
|
28
|
+
- addon: `addon` → `Xano.registerAddons` → payload `addon` — A reusable read fragment that enriches a query result by joining related table data.
|
|
29
|
+
- realtime_server: `realtimeServer` → `Xano.registerRealtimeServers` → payload `realtime_server` — A realtime (websocket) server: the canonical-addressed container that owns realtime channels. Off until `enabled: true`. Returns a handle with `getUrl(baseUrl)`/`getPath()` for the client's socket URL (`wss://<host>/ws/<canonical>`).
|
|
30
|
+
- channel: `realtimeChannel` → `Xano.registerRealtimeChannels` → payload `channel` — A realtime channel: a joinable path on a realtime server (`rooms/{room_id}`) with typed path params, join/publish policy, a client-visible conversation transcript, and delivery semantics. Owns message handlers. Returns a handle with `getChannel(params)` for the path a client joins.
|
|
31
|
+
- message: `realtimeMessage` → `Xano.registerRealtimeMessages` → payload `message` — A realtime message handler: a named message type on a channel with its own typed payload and stack — the realtime analogue of a query. Pass the `realtimeChannel()` handle as `channel` and the owning server comes with it.
|
|
32
|
+
- microservice: `microservice` → `Xano.registerMicroservices` → payload `microservice` — A container workload deployed alongside the workspace, called from a stack with `s.microservice.request`. Def shapes (`builtin` containers vs a `helm` chart) and the SECRET-handling rules its config carries: `llms/statements-calls.md`.
|
|
33
|
+
- knowledge: `knowledge` → `Xano.registerKnowledge` → payload `knowledge` — Markdown Xano's own AI (the builder agent, and Meta API / MCP readers) reads before it acts — never an `agent()` run — an `agents.md` of standing instructions, a `skill`, or a `doc`. The body is a real markdown FILE via `knowledgeFile("./x.md", import.meta.url)`, and `mode` decides how much of it reaches the agent.
|
|
34
|
+
- workspace: `workspaceConfig` → `Xano.registerWorkspace` → payload `workspace` — Workspace-level configuration such as default middleware chains and request-history defaults per host kind.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Auth, cross-object calls, and microservices
|
|
2
|
+
|
|
3
|
+
> Read when the stack authenticates or verifies a webhook (recipes here), calls another object (`s.function.run`, `s.api.request`, `s.tool.call`), sends email (`s.util.send_email`), or reaches a microservice.
|
|
4
|
+
|
|
5
|
+
Auth & calls:
|
|
6
|
+
|
|
7
|
+
- `s.security.create_auth_token({ table, id, extras?, expiration?, as? })` — `extras` defaults to `{}`, `expiration` (seconds, a `Value`: `c.int(3600)`) to 86400 (`c.int(0)` = never).
|
|
8
|
+
- **Marketplace first:** `@xano-sdk/auth` registers a complete auth surface — signup/login/me endpoints plus their `user`/`account`/`event_log` tables — via `registerAuth(ws, { canonical: "authn" })`. Authentication only, not authorization. Inspect with `xanosdk marketplace details @xano-sdk/auth --prompt`.
|
|
9
|
+
- **The auth loop end to end** — what that module implements; author it directly for a custom flow. An auth TABLE (`table({ auth: true })`) backs identity; a PUBLIC login query verifies and mints a token; every protected query names that table as `auth:` and reads the caller with `auth(...)`.
|
|
10
|
+
- Signup: `s.db.add({ table: users, row: { email: inp("email"), password: inp("password") } })` — the `f.password()` column hashes on write; take the submission as `input.text()` on signup AND login (see Gotchas on double-hashing).
|
|
11
|
+
- Login: `s.db.get({ table: users, fieldName: "email", fieldValue: inp("email"), output: ["id", "password"], as: "u" })` — the `output` naming `password` is REQUIRED (the column is `access: "internal"` and absent from the row otherwise) → `s.precondition({ expr: expr(ref("u", { safe: true }), "!=", c.null()), error: c.text("No such user."), error_type: "notfound" })` → `s.security.check_password({ text_password: inp("password"), hash_password: ref("u.password"), as: "ok" })` → precondition on `ok` → `s.security.create_auth_token({ table: users, id: ref("u.id"), as: "token" })` → `response: { token: ref("token") }`.
|
|
12
|
+
- Client: send it as an `Authorization: Bearer <token>` header. A `query({ auth: users })` refuses a request without a valid one before its stack runs; inside the stack `auth("id")` is the caller's row id (`response: { id: auth("id") }` is the whole `me` endpoint).
|
|
13
|
+
- **Authorization, on top of that.** `guard.role(users, "admin")` and `guard.owner("note", "user_id")` each return a fixed-arity TUPLE of statements — spread them (`stack: [...guard.role(users, "admin"), …]`); a helper returning `Statement[]` widens the stack and kills `InferResponse`.
|
|
14
|
+
- `guard.role(table, role | roles[], { field?, as?, errorType?, message?, missingErrorType?, missingMessage? })` — fetches the caller's own row by `auth("id")` into `__actor`, asserts it EXISTS (401), then asserts its `role` is in the set (403). Roles type from the enum column; an unknown role or `field` throws. The existence step matters: a role check on a null actor row is not reliably false.
|
|
15
|
+
- `guard.owner(rowVar, ownerField = "user_id", { … })` — asserts the row bound to `rowVar` is non-null (404), THEN compares its owner column to `auth("id")` (also 404). Drilling before the existence check is a 500, not a denial: `db.get` binds `null` on a miss and `ref("note.user_id")` against it raises `Unable to locate var`. Takes the var NAME and the column separately for exactly that reason; a dotted path is refused.
|
|
16
|
+
- `guard.found(rowVar, { errorType?, message? })` — ONE statement: asserts the row bound to `rowVar` is non-null (404, `"Not found."`), and NARROWS it — after it, `InferResponse` drops the `| null` from `rowVar`. `guard.owner`'s existence step is this statement.
|
|
17
|
+
- `guard.require(condition, { errorType?, message? })` for anything else — same shape, and it defaults to 403 where a bare `s.precondition` defaults to `standard`, i.e. **500**.
|
|
18
|
+
- `s.security.create_uuid({ as? })` — bind a fresh UUID (v4). Takes nothing else. There is NO `create_guid` (internal, unpushable).
|
|
19
|
+
- **Receiving a webhook** — no `auth`, no declared `input` (the signature covers the raw bytes): `s.util.get_raw_input({ encoding: "none", as: "raw" })` → `s.set_var("mac", withFilters(ref("raw"), fl.hmac_sha256(env("WH_SECRET"))))` → `s.set_var("sig", withFilters(sys.httpHeaders(), fl.get("X-Signature")))` (Title-Cased) → an `s.precondition` (`error_type: "unauthorized"`) comparing the two TIMING-SAFELY by HMACing both again — `expr(withFilters(ref("sig"), fl.hmac_sha256(env("WH_SECRET"))), "=", withFilters(ref("mac"), fl.hmac_sha256(env("WH_SECRET"))))` — BEFORE `fl.json_decode()` reads `raw`. Claim the event id with `s.redis.set({ …, create_only: c.bool(true) })` and answer 200 to a replay.
|
|
20
|
+
- `s.function.run({ fn, input?, as?, runtime? })` / `s.function.call({ fn, input?, as? })` — run another function; `input` is keyed by the target's input names (an undeclared one is a type and export error; generic over `fn`, pass `Record<string, Value>`; `2` binds an int — `c.decimal(2)` for a decimal). ⚠ `s.function.run` binds an input it OMITS from the CALLING def's same-named declared input — an endpoint declaring `discount` hands `?discount=100` through, or `0` when the client sends none. An endpoint/tool/prompt declaring NO inputs binds it from the raw request. `ignored()` always takes the target's default. Pass inputs explicitly (`statement.omitted-input` warns).
|
|
21
|
+
- `runtime?` runs it in the BACKGROUND: `{ mode: "async-shared" }` or `{ mode: "async-dedicated", cpu?, memory?, timeout?, maxRetry? }` (resources read at dedicated only). An async call DOES NOT return the result — it dispatches and continues, and `as` binds the job id (a UUID `string`); collect with `s.await({ ids, timeout, as })`, binding `{ "<jobId>": result }`; a too-short `timeout` or unknown id is a 500 `Async timeout`, no partial result. Omit for a normal call; same on `s.ai.agent.run`.
|
|
22
|
+
- `s.api.call({ api, input?, headers?, auth?, as? })` — invoke an endpoint. WORKFLOW-TEST ONLY (see Gotchas in `llms.txt`).
|
|
23
|
+
- `api` takes the `query()` def HANDLE (or a `{ name, guid }` pair) — a bare name is refused, because a query's identity is composed from its api group, verb, and name.
|
|
24
|
+
- `headers` REPLACES the request headers the callee sees and takes the same shapes as `s.api.request`'s, or one `Value`.
|
|
25
|
+
- `auth` is `{ token, ignoreExpiration? }`: `token` must be a BARE STRING — a tagged `Value` deploys clean and then fails the run with `Param: token - Text filter requires an integer, float, string or boolean value`, since the engine stores that slot as plain text and never evaluates it.
|
|
26
|
+
- Neither slot authenticates the call today; see `llms/tests.md` for what a workflow-test run actually sees.
|
|
27
|
+
- `s.api.request({ url, method?, params?, headers?, timeout?, follow_location?, verify_host?, verify_peer?, ca_certificate?, certificate?, certificate_pass?, private_key?, private_key_pass?, description?, output?, as? })` — external HTTP request (`mvp:api_request`). Ergonomic types, each also accepting a dynamic `Value`:
|
|
28
|
+
- `method` suggests the 7 verbs (GET/POST/PUT/DELETE/HEAD/OPTIONS/PATCH).
|
|
29
|
+
- `params` a plain JSON object **or** a FLAT record whose values are tagged `Value`s (`{ count: ref("count") }`, each lifted via a `set` filter — the same record-of-values shape `response: { key: value }` takes); a tagged value NESTED inside an object or array THROWS at encode, so wrap a structured body in `obj({...})`, which encodes any depth as one `const:expr2` (→ query string for GET/HEAD/OPTIONS, body otherwise).
|
|
30
|
+
- `headers` a `{ "Name": value }` record whose values may be tagged (`{ "x-api-key": env("KEY") }`, each pair joined to a `"Name: value"` line) **or** a `string[]` of full header lines — prefer a header over a `?key=` query param for a credential — a URL reaches access logs and proxies.
|
|
31
|
+
- ⚠ Neither spelling is envelope-safe: the `as` envelope's `request` half mirrors `url`, `params` AND `headers`, so never return it raw from a credentialed request — read `response.result`.
|
|
32
|
+
- A NAME outside the header-token charset is refused, and a LITERAL value (string or `c.text`) carrying CR/LF/NUL is refused; a value may hold `:` and spaces (`Bearer a: b` is a valid value). A TAGGED value cannot be checked at build time, so strip CRLF from caller-controlled input before this slot. Literal pairs are emitted before computed ones, whatever the key order.
|
|
33
|
+
- No `User-Agent` is sent unless `headers` carries one. An API requiring one answers **403** as ordinary `response.result` (no throw; every mapped field reads `null`). Send one (`headers: { "User-Agent": c.text("my-app") }`) and gate on `ref("<var>.response.status")` = 200 with `s.precondition`.
|
|
34
|
+
- A transport failure (timeout, DNS) does not throw — `s.try_catch` never catches it: `response.status` is `0`, with `response.error` set. Retry on `status == 0 || status >= 500`; `status < 500` passes a 0.
|
|
35
|
+
- `timeout` a `number` in seconds (1–86400), and `follow_location`/`verify_host`/`verify_peer` booleans.
|
|
36
|
+
- `description` and `output` filters ride the envelope. Cert pairings (certificate↔private_key, ca_certificate→verify_peer) are checked at build time when provable.
|
|
37
|
+
- The `as` result is typed as the `{request, response}` envelope (`response.status: number`, `response.result: unknown`), so `InferResponse` resolves a `ref` to it. Same typed result on `webflow.request` and `microservice.request`.
|
|
38
|
+
- `s.stream.from_request({ url, method?, …tls, as? })` — streaming external HTTP request (`mvp:streaming_api_request`); same typed field surface as `s.api.request` (no description/output envelope). `url` is REQUIRED.
|
|
39
|
+
- `s.util.send_email({ to, subject, message, from?, cc?, bcc?, reply_to?, service_provider?, api_key?, scheduled_at?, as? })` — send email from the stack. `service_provider` is `"xano"` (the built-in mailer — needs NO `api_key` and no configuration, and does not require a verified sender) or `"resend"` (pass the key as `api_key: env("RESEND_API_KEY")`). Prefer this over hand-rolling `s.api.request` against a mail provider.
|
|
40
|
+
- `s.webflow.request({ path, method?, …tls, as? })` — Webflow API request (`mvp:connect_webflow_api_request`); like `s.api.request` but addressed by `path` (host is engine-supplied), and `path` is REQUIRED — the engine rejects an empty one. No `headers`: the engine builds its own from the workspace's Webflow connection and ignores an authored value.
|
|
41
|
+
- `s.task.call` / `s.tool.call` / `s.trigger.call` / `s.middleware.call` / `s.addon.call` — same `{ <target>, input?, as? }` shape against the named kind. `s.task.call` and `s.trigger.call` are WORKFLOW-TEST ONLY (see Gotchas in `llms.txt`); `s.tool.call`, `s.middleware.call` and `s.addon.call` run from any stack.
|
|
42
|
+
- `s.action.call({ actionId, input?, registry?, as? })` / `s.action.package.call({ traceId, versionId, slug, input?, registry?, as? })` — invoke an installed action. Ids are SUPPLIED, never derived from a name: an action is installed onto the instance, so its identity is assigned at install and differs per instance — read them off a call in a pulled workspace. The package form needs all three parts; they are one composite and two of them address nothing. `registry` is the action's own settings, `input` the per-call arguments.
|
|
43
|
+
- `s.cloud.job({ image, command, args?, secret?, template?, await?, as? })` — launch a containerized job. `image`/`command` are REQUIRED; omitted → `Missing param`. ⚠ `await` is SECONDS (default 60), not a boolean.
|
|
44
|
+
- `s.cloud.job.status({ id, as? })` · `s.cloud.job.await({ ids, timeout, as? })` — poll one job, or block on several (`ids` a list `Value`, `timeout` in seconds; both required).
|
|
45
|
+
|
|
46
|
+
Microservices (the `microservice()` def and the statement that calls it):
|
|
47
|
+
|
|
48
|
+
- `microservice({ name, kind, … })` — two mutually exclusive shapes via `kind`: `builtin` declares containers (image/ports/resources/env/command/args) plus optional `ingresses`, and `helm` points at a chart and its `values` — passing both throws.
|
|
49
|
+
- EARLY SURFACE, expected to change — every export of a workspace declaring one prints a notice saying so.
|
|
50
|
+
- `configs`/`volumes` are typed and `@deprecated` but NOT deployable: the engine rejects an import carrying either, so `export()` fails the build rather than letting the deploy fatal.
|
|
51
|
+
- Put a value the workload reads in a container's `env`, and storage in a container's own `volumes` (`emptyDir`/`persistent`/`config`).
|
|
52
|
+
- Container names are free-form — they need not match the microservice name, which is what a stack addresses.
|
|
53
|
+
- SECRETS RIDE ALONG — `chart.values` and `registryAuth.dockerconfigjson` are carried into the bundle, and into a pulled tree, verbatim. `process.env.X` in the def is NOT a way out: it resolves at EXPORT and writes the literal in.
|
|
54
|
+
- Reference a `workspaceConfig({ env })` var instead and only the NAME travels — the engine substitutes at DEPLOY. Two spellings: `env: [{ name: "API_KEY", fromEnv: "STRIPE_SECRET" }]` on a builtin container (`value`/`fromEnv` exclusive; both → build error), or `${env.NAME}` inside a helm `chart.values` (a VALUE at any depth, never a key). A name the workspace does not define FAILS the deploy rather than resolving blank.
|
|
55
|
+
- `registryAuth.dockerconfigjson` has NO such form: either leave it unset and pull a public image, or treat the bundle and any tree pulled from it as secret material — out of git, or rotate after. Export prints a notice per microservice carrying a literal; `--strict` does not promote it.
|
|
56
|
+
- `s.microservice.request({ host, path, port?, method?, params?, headers?, timeout?, follow_location?, as? })` — in-cluster microservice call (`mvp:microservice_request`); no TLS fields.
|
|
57
|
+
- ONLY `host`+`path` required; the rest default to the engine's values (`GET`/`{}`/`[]`/`10`/`true`), always emitted.
|
|
58
|
+
- Pass the `microservice()` DEF as `host` — it binds by NAME (how the engine resolves it), so a rename fixes every call site and the port is checked before deploy.
|
|
59
|
+
- `port?` folds into `host` as `"name:port"`: a def exposing ONE `servicePort` resolves automatically, SEVERAL requires it.
|
|
60
|
+
- A raw `"name:port"` string works, unvalidated, and is the only way to reach an instance-level microservice.
|
|
61
|
+
- `tenantDeploy: "manual"` on the def imports the row without starting the workload.
|
|
62
|
+
- `s.workflow_test.call({ workflowTest, datasource?, as? })` — run another workflow test from inside one, which is the only place it runs (see Gotchas in `llms.txt`). The odd one out: NO `input` (a workflow test takes none), and it carries `datasource?` instead — same clone caveat as the kind's own field.
|