@xanots/sdk 0.0.12 → 0.0.13

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +5 -3
  3. package/dist/.build-fingerprint +1 -1
  4. package/dist/bin.js +3 -3
  5. package/dist/{chunk-AG5GCZDD.js → chunk-2AY3PKF4.js} +2 -2
  6. package/dist/{chunk-GSP2BY4F.js → chunk-BYQHCCYU.js} +1 -1
  7. package/dist/{chunk-QYSQ3UDO.js → chunk-MEFMTICH.js} +76 -1
  8. package/dist/{chunk-ELK7UALJ.js → chunk-OONT4ZL4.js} +2 -2
  9. package/dist/{chunk-VRNZ2NVV.js → chunk-PJNWOZMT.js} +2 -2
  10. package/dist/{chunk-XT3XQ4PF.js → chunk-RLI6XD4O.js} +10 -10
  11. package/dist/{chunk-VPAWRBK5.js → chunk-S3DOJOW4.js} +32 -8
  12. package/dist/{chunk-PLE5QQOZ.js → chunk-SS2V2QOG.js} +4 -4
  13. package/dist/cli.js +2 -2
  14. package/dist/{codegen-command-Y2SPMAUW.js → codegen-command-ZDHMGFWZ.js} +6 -6
  15. package/dist/{deploy-command-XHS5PKPV.js → deploy-command-CGVKRWUE.js} +4 -4
  16. package/dist/index.d.ts +2 -2
  17. package/dist/index.js +3 -3
  18. package/dist/{init-command-23FFNUFT.js → init-command-DNDONP3O.js} +4 -4
  19. package/dist/internal.d.ts +2 -2
  20. package/dist/internal.js +11 -11
  21. package/dist/{lock-commands-WCRC56ME.js → lock-commands-6U7UIJGR.js} +3 -3
  22. package/dist/node.d.ts +2 -2
  23. package/dist/node.js +4 -4
  24. package/dist/{preflight-command-TZWPHBXY.js → preflight-command-UYE7SUQV.js} +4 -4
  25. package/dist/{release-command-WEFYRTCI.js → release-command-CMMYT6XK.js} +6 -6
  26. package/dist/{routes-manifest-5ZFKUQWA.js → routes-manifest-MN6XBYRE.js} +24 -13
  27. package/dist/{store-DAnUIi1T.d.ts → store-9Psd0jiF.d.ts} +34 -16
  28. package/dist/{upgrade-command-GYEIMJBG.js → upgrade-command-A75DOIUH.js} +4 -4
  29. package/dist/{workspace-command-2ZTGT26W.js → workspace-command-YELP47SJ.js} +6 -6
  30. package/guides/typed-frontend.md +20 -3
  31. package/llms/kinds-realtime.md +2 -2
  32. package/llms/tests.md +1 -1
  33. package/llms/triggers.md +2 -2
  34. package/llms-full.txt +10 -10
  35. package/llms.txt +5 -5
  36. package/manifest.json +1 -1
  37. package/package.json +1 -1
package/llms-full.txt CHANGED
@@ -1,4 +1,4 @@
1
- # xanots v0.0.12
1
+ # xanots v0.0.13
2
2
 
3
3
  > TypeScript SDK that compiles a typed Xano workspace into the importable packageExport JSON bundle.
4
4
 
@@ -336,10 +336,10 @@ Non-obvious authoring rules:
336
336
  `getPath()`/`verb`/`getUrl()`/`getChannel()` also pulls whatever its `stack` references:
337
337
  the `s.*`/`c.*` factory CALLS run at module load to BUILD it. Types are free.
338
338
  ⚠ A FLOOR — **~267 kB minified (~65 kB gzipped)** for the FIRST def; splitting modules
339
- never removes it. The floor is the RUNTIME, not the def: a second def, or a much richer
340
- one, adds ~2 kB so reducing what a def does will not reduce it.
339
+ never removes it. The floor is the RUNTIME, not the def: a second or much richer def
340
+ adds ~2 kB, so trimming a def does not shrink it.
341
341
  Fix: `xanots routes <entry> --emit xano/routes.gen.ts` (`paths` is an accepted alias) — verbs, paths, and sockets as
342
- plain data importing NOTHING, still compile-checked: `routePath("blog/{slug}", { slug })`,
342
+ plain data importing NOTHING, still compile-checked: `routePath("GET blog/{slug}", { slug })`
343
343
  `channelPath("rooms/{room_id}", { room_id })`, `socketUrl("chat", baseUrl)` (tenant base
344
344
  URLs lifted to `wss://h/ws/<tenant>:<canonical>`). A rename is a type error, not a 404.
345
345
  - **Intra-workspace imports use `.js` specifiers** (`../tables/links.js`), not
@@ -416,7 +416,7 @@ Non-obvious authoring rules:
416
416
  submission on bind, so `s.security.check_password` compares two different hashes
417
417
  and a correct password always fails (`ok:false` on a found row). Take the submitted
418
418
  password as `input.text()` on both signup and login and pass the plaintext straight
419
- to `check_password` (which does the comparison hash itself).
419
+ to `check_password` (which does the comparison hash itself). `export()` warns.
420
420
  - **Agents authenticate with env vars — never `xanots login`.** `login` blocks on a
421
421
  browser consent no agent can complete. Set `$XANO_INSTANCE_URL` + `$XANO_WORKSPACE_ID`
422
422
  + `$XANO_META_TOKEN` and run `deploy`/`release` directly: no disk, no rotation, so it
@@ -628,7 +628,7 @@ The run is isolated in ways that make a correct test fail for reasons the failur
628
628
 
629
629
  - The run uses an EMPTY datasource, so **no `table({ seed })` rows exist while it runs** and every `db` read misses. A test that buys seeded row 1 fails with its own precondition message, which reads as a wrong id rather than an empty database. Create what the test needs INSIDE the test — typically a `defineFunction` fixture the stack calls first.
630
630
  - `s.api.call` does NOT raise when the endpoint answers with an error. It BINDS the error envelope (`{code, message}`) to its `as` and carries on, so a later `s.expect.to_be_defined({ expr: ref("r.field") })` reports the ASSERTION while the real failure was the call, four statements up. Assert on the envelope — `s.expect.to_contain({ expr: ref("r.code"), value: c.text("ERROR_CODE_INPUT_ERROR") })` — when a call may fail. `s.function.run` raises instead; the two disagree.
631
- - `s.expect.to_throw({ body, exception? })` runs `body` in an ISOLATED var stack, so a variable bound EARLIER in the test is not visible inside it — bind what the body needs inside the body. `exception` is a `Value` whose text the raised message must CONTAIN (`c.text("already exists")`, not a bare string); omit it to accept any error.
631
+ - `s.expect.to_throw({ body, exception? })` runs `body` in an ISOLATED var stack, so a variable bound EARLIER in the test is not visible inside it — bind what the body needs inside the body. An outer one raises `Missing var entry: <name>` there, which the test reports as `to_throw` not matching (`export()` warns). `exception` is a `Value` whose text the raised message must CONTAIN (`c.text("already exists")`, not a bare string); omit it to accept any error.
632
632
  - `s.expect.to_throw` catches such a call only when the error carries a MESSAGE. `ERROR_CODE_ACCESS_DENIED` arrives with an empty one, so `to_throw` around an auth-refused call reports `to_throw failed - response is ok` — which reads as a broken auth gate on a gate that works.
633
633
  - An endpoint's `auth` gate is NOT enforced on `s.api.call`. A `query({ auth: users })` runs anyway and fails only where its stack dereferences `auth(...)`. A stack that never touches `auth(...)` runs unauthenticated and passes.
634
634
  - Neither `auth.token` nor an `Authorization` entry in `headers` authenticates the call — a token that answers 200 over real HTTP is refused here. To cover auth-gated logic, move the body into a `defineFunction` taking the user id and `s.function.call` that; the gate itself is not reachable from a workflow test.
@@ -700,11 +700,11 @@ The run is isolated in ways that make a correct test fail for reasons the failur
700
700
  - `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.
701
701
  - Only `"channel"`/`"others"` fan out AND are written to the `conversation` transcript — a `"sender"` response is invisible to every future joiner.
702
702
  - **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.
703
- - 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. The same values reach a channel `join`/`leave` trigger's stack.
703
+ - 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.
704
704
  - `s.realtime.get_session({ as })` — the CALLER's realtime session for the current frame. FLAT shape:
705
705
  - `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.
706
706
  - Works in a realtime MESSAGE stack and in CHANNEL and SERVER trigger stacks; off that path it degrades to an anonymous session.
707
- - For a path param prefer `inp("room_id")`. Reach for the session when you need the CONNECTION (identity/extras) — "who is this sender" on an anonymous-client channel.
707
+ - 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.
708
708
  - ⚠ 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.
709
709
  - `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.
710
710
  - `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.
@@ -772,8 +772,8 @@ realtimeChannelTrigger, mcpServerTrigger, agentTrigger, workspaceTrigger,
772
772
  errorTrigger}({ name, guid?, description?, active?, tags?, ... })`.
773
773
 
774
774
  - `tableTrigger({ name, table?, datasources?, actions?: {insert?,update?,delete?,truncate?}, stack })` — database/table trigger. `t.new` / `t.old` are the row **after** / **before** the change; `t.action` (`insert|update|delete|truncate`), `t.datasource`. Bind `table` to a `table()` handle and `t.new("col")` / `t.old("col")` are typed to that row (misspelled column = compile error). Nullability follows the enabled actions: insert → `old` is null, delete → `new` is null, update → both, truncate → neither. Config-only (no response).
775
- - `realtimeServerTrigger({ name, realtimeServer, actions?: {connect?,disconnect?}, stack?, response?, responseShape? })` — realtime SERVER lifecycle (a client connecting to / disconnecting from the server, not a message). Inputs: `t.action` (`connect|disconnect`), `t.realtime_server`, `t.client`. Bind `realtimeServer` to a `realtimeServer()` handle (or its name). `connect` GATES the connection — a denial sends an `error` and CLOSES the socket with code 4401 before it is ever ready, so it is a real front door, not an observer; same return shape as a channel `join` (`{ allowed: c.bool(true) }` or any truthy value admits, EMPTY/FALSY DENIES — INCLUDING a gating trigger with NO `response`, which returns nothing and so refuses every client). A CRASH DENIES too — the transport seeds a deny and keeps it on a throw. Both failure modes lock the door, so plan for a self-inflicted LOCKOUT (an unguarded drill into a null `db.get` raises → everyone refused), not a breach. Gating is OPT-IN: a server with no `connect` trigger accepts every connection. `disconnect` is OBSERVATIONAL (return ignored, throws swallowed — cleanup must always complete). Both are SERVER-scoped, so `s.realtime.get_session` works but carries no channel path and no bound params.
776
- - `realtimeChannelTrigger({ name, channel, actions?: {join?,leave?,deliver?}, stack?, response?, responseShape? })` — realtime CHANNEL lifecycle. Inputs: `t.action` (`join|leave|deliver`), `t.channel`, `t.client`. Bind `channel` to a `realtimeChannel()` handle — a bare path is NOT accepted (it is unique only within its server). The three actions have DIFFERENT postures, and the posture decides what the stack should return: `join` GATES the join (it runs BEFORE the client becomes a member, so a denial means it never sees a fan-out) — return `{ allowed: c.bool(true) }` (optional `reason` reaches the client) or any truthy value to admit, and an EMPTY OR FALSY RETURN DENIES, so a stack that just falls through — or a gating trigger with NO `response` — refuses everyone, and a CRASH DENIES too. That is the inverse of a normal message (a crashing message still delivers) and of `deliver` below (a gate that fails OPEN). `join`/`leave` bind the channel's typed path params as INPUTS, so `inp("room_id")` resolves and the gate decides per room; a SERVER connect/disconnect has no channel, the one place a path param cannot be read; `leave` is OBSERVATIONAL (return ignored, throws swallowed); `deliver` GATES delivery PER RECIPIENT — the per-viewer redaction tool and the most expensive action here (a stack per recipient per message), and it needs `delivery.perRecipient` on the channel to run at all — BOTH HALVES are required, so a `deliver` trigger on a channel without the flag NEVER RUNS and every subscriber receives the UNREDACTED payload (no error, no log line); `export()` warns on each half alone. **`deliver`'s RETURN VALUES DO NOT READ LIKE A FILTER:** ONLY an explicit NULL drops the message for that recipient; an OBJECT replaces that recipient's payload; ANYTHING ELSE — INCLUDING `false`, `0`, `""` — DELIVERS IT UNCHANGED, as does a crash. So `return false` from a yes/no redaction check SENDS the message it was written to suppress — return null instead. The delivered payload arrives NESTED, so read `inp("payload").<field>`, and `t.client` is the SENDER while `s.realtime.get_session` describes the RECIPIENT this run is for.
775
+ - `realtimeServerTrigger({ name, realtimeServer, actions?: {connect?,disconnect?}, stack?, response?, responseShape? })` — realtime SERVER lifecycle (a client connecting to / disconnecting from the server, not a message). Inputs: `t.action` (`connect|disconnect`), `t.realtime_server`, `t.client`. Bind `realtimeServer` to a `realtimeServer()` handle (or its name). `connect` GATES the connection — a denial sends an `error` and CLOSES the socket with code 4401 before it is ever ready, so it is a real front door, not an observer; same return shape as a channel `join` below (EMPTY/FALSY DENIES — INCLUDING a gating trigger with NO `response`, which returns nothing and so refuses every client). A CRASH DENIES too — a gate that cannot answer must not admit. Both failure modes lock the door, so plan for a self-inflicted LOCKOUT (an unguarded drill into a null `db.get` raises → everyone refused), not a breach. Gating is OPT-IN: a server with no `connect` trigger accepts every connection. `disconnect` is OBSERVATIONAL (return ignored, throws swallowed — cleanup must always complete). Both are SERVER-scoped, so `s.realtime.get_session` works but carries no channel path and no bound params.
776
+ - `realtimeChannelTrigger({ name, channel, actions?: {join?,leave?,deliver?}, stack?, response?, responseShape? })` — realtime CHANNEL lifecycle. Inputs: `t.action` (`join|leave|deliver`), `t.channel`, `t.payload`, `t.client`. Bind `channel` to a `realtimeChannel()` handle — a bare path is NOT accepted (it is unique only within its server). The three actions have DIFFERENT postures, and the posture decides what the stack should return: `join` GATES the join (it runs BEFORE the client becomes a member, so a denial means it never sees a fan-out) — return `{ allowed: c.bool(true) }` (optional `reason` reaches the client) or any truthy value to admit, and an EMPTY OR FALSY RETURN DENIES, so a stack that just falls through — or a gating trigger with NO `response` — refuses everyone, and a CRASH DENIES too. ONCE the object carries an `allowed` key admission needs STRICTLY `true` — a computed `1`/`"yes"` there DENIES. That is the inverse of a crashing message, which still delivers, and of `deliver` below. A lifecycle trigger's inputs are PINNED to those four, so a channel PATH PARAM is NOT among them — `inp("room_id")` RAISES, which crashes the gate and so REFUSES every client; take the param from `s.realtime.get_session` (`ref("session.params.room_id")`). A gate establishes NO auth, so `ref("auth.id")` reads 0 even when authenticated identity is `t.client("permissions.dbo_id")` or the session. A SERVER connect/disconnect has no channel, so no params at all; `leave` is OBSERVATIONAL (return ignored, throws swallowed); `deliver` GATES delivery PER RECIPIENT — the per-viewer redaction tool and the most expensive action here (a stack per recipient per message), and it needs `delivery.perRecipient` on the channel to run at all — BOTH HALVES are required, so a `deliver` trigger on a channel without the flag NEVER RUNS and every subscriber receives the UNREDACTED payload (no error, no log line); `export()` warns on each half alone. **`deliver`'s RETURN VALUES DO NOT READ LIKE A FILTER:** ONLY an explicit NULL drops the message for that recipient; an OBJECT replaces that recipient's payload; ANYTHING ELSE — INCLUDING `false`, `0`, `""` — DELIVERS IT UNCHANGED, as does a crash. So `return false` from a yes/no redaction check SENDS the message it was written to suppress — return null instead. The delivered payload arrives NESTED, so read `t.payload("<field>")`, and `t.client` is the SENDER while `s.realtime.get_session` describes the RECIPIENT this run is for.
777
777
  - `mcpServerTrigger({ name, mcpServer, stack?, response?, responseShape? })` / `agentTrigger({ name, agent, stack?, response?, responseShape? })` — toolset connection. Bind with the `mcpServer()`/`agent()` def handle (or its name) — it resolves to the toolset guid at export. Raw numeric `objId` is the escape hatch, rarely right: ids are assigned at import, so a handle passed to `objId` is a type error, and binding nothing deploys a trigger that never fires. Inputs: `t.toolset` (`t.toolset("name")`), `t.tools`. Response-bearing; the default stack copies `toolset`/`tools` into vars and returns them.
778
778
  - `workspaceTrigger({ name, actions?: {branch_live?,branch_merge?,branch_new?}, stack? })` — branch lifecycle. Inputs: `t.to_branch`, `t.from_branch`, `t.action`. Config-only.
779
779
  - `errorTrigger({ name, stack? })` — error-signature trigger. Inputs: `t.event` (`new|regression|fixed`), `t.id`, `t.signature`, `t.error` (`t.error("code")`/`t.error("message")`), `t.caller`, `t.statement`, `t.actor`, `t.count`, `t.first_seen`, `t.last_seen`, `t.fixed_at`. Config-only.
package/llms.txt CHANGED
@@ -1,4 +1,4 @@
1
- # xanots v0.0.12
1
+ # xanots v0.0.13
2
2
 
3
3
  > TypeScript SDK that compiles a typed Xano workspace into the importable packageExport JSON bundle.
4
4
 
@@ -336,10 +336,10 @@ Non-obvious authoring rules:
336
336
  `getPath()`/`verb`/`getUrl()`/`getChannel()` also pulls whatever its `stack` references:
337
337
  the `s.*`/`c.*` factory CALLS run at module load to BUILD it. Types are free.
338
338
  ⚠ A FLOOR — **~267 kB minified (~65 kB gzipped)** for the FIRST def; splitting modules
339
- never removes it. The floor is the RUNTIME, not the def: a second def, or a much richer
340
- one, adds ~2 kB so reducing what a def does will not reduce it.
339
+ never removes it. The floor is the RUNTIME, not the def: a second or much richer def
340
+ adds ~2 kB, so trimming a def does not shrink it.
341
341
  Fix: `xanots routes <entry> --emit xano/routes.gen.ts` (`paths` is an accepted alias) — verbs, paths, and sockets as
342
- plain data importing NOTHING, still compile-checked: `routePath("blog/{slug}", { slug })`,
342
+ plain data importing NOTHING, still compile-checked: `routePath("GET blog/{slug}", { slug })`
343
343
  `channelPath("rooms/{room_id}", { room_id })`, `socketUrl("chat", baseUrl)` (tenant base
344
344
  URLs lifted to `wss://h/ws/<tenant>:<canonical>`). A rename is a type error, not a 404.
345
345
  - **Intra-workspace imports use `.js` specifiers** (`../tables/links.js`), not
@@ -416,7 +416,7 @@ Non-obvious authoring rules:
416
416
  submission on bind, so `s.security.check_password` compares two different hashes
417
417
  and a correct password always fails (`ok:false` on a found row). Take the submitted
418
418
  password as `input.text()` on both signup and login and pass the plaintext straight
419
- to `check_password` (which does the comparison hash itself).
419
+ to `check_password` (which does the comparison hash itself). `export()` warns.
420
420
  - **Agents authenticate with env vars — never `xanots login`.** `login` blocks on a
421
421
  browser consent no agent can complete. Set `$XANO_INSTANCE_URL` + `$XANO_WORKSPACE_ID`
422
422
  + `$XANO_META_TOKEN` and run `deploy`/`release` directly: no disk, no rotation, so it
package/manifest.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xanots",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "description": "TypeScript SDK that compiles a typed Xano workspace into the importable packageExport JSON bundle.",
5
5
  "coverage": {
6
6
  "objectKinds": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xanots/sdk",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "description": "XanoTS — your Xano backend as TypeScript. `xanots deploy` ships your typed workspace (and an optional static frontend) to a live, auto-expiring ephemeral environment and prints its URL. init → deploy → URL.",
5
5
  "keywords": [
6
6
  "xanots",