@xanots/sdk 0.0.3 → 0.0.4
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 +80 -0
- package/README.md +34 -1
- package/dist/agent-file-refresh-IJ7GN2FJ.js +17 -0
- package/dist/bin.js +12 -153
- package/dist/{capture-HUV5BNTC.js → capture-7PO6SGB4.js} +2 -2
- package/dist/{chunk-CMZLPTGW.js → chunk-7SISR3CS.js} +2 -2
- package/dist/{chunk-7DKX2SPN.js → chunk-ACQCK4DT.js} +4 -4
- package/dist/{chunk-XQ22GLYS.js → chunk-AJ7JQ4OU.js} +5 -5
- package/dist/{chunk-ZO3HJOCJ.js → chunk-ALNQCWAW.js} +2 -2
- package/dist/{chunk-55VHDNW5.js → chunk-BPEEGJJA.js} +108 -3
- package/dist/{chunk-U3G2UW65.js → chunk-CYJ7R3AD.js} +3 -3
- package/dist/{agent-file-refresh-6M5M7T24.js → chunk-D6OGU2AI.js} +5 -6
- package/dist/{chunk-3NFUWXOC.js → chunk-DUC3PXDG.js} +142 -45
- package/dist/{chunk-HQ2CBRVI.js → chunk-EAIR6VUL.js} +10 -8
- package/dist/chunk-ERQZFWIW.js +22 -0
- package/dist/{chunk-TQUO2OXY.js → chunk-FN4OQT2J.js} +4 -4
- package/dist/{chunk-6USV65XA.js → chunk-KGNJM4LN.js} +2 -2
- package/dist/{chunk-HJPTWBLH.js → chunk-MFIHIS6B.js} +2 -2
- package/dist/{chunk-F6JCFRKO.js → chunk-PXXLBXOP.js} +2 -2
- package/dist/{chunk-YZ4GU6F5.js → chunk-QUUB7HYK.js} +5 -838
- package/dist/{chunk-W2G2WPTB.js → chunk-SWIXWJIY.js} +3 -3
- package/dist/{chunk-76QBEIGO.js → chunk-T4XPCJRF.js} +3 -2
- package/dist/chunk-T6N3VMMO.js +173 -0
- package/dist/{chunk-2V4YE6QC.js → chunk-VKD3EBKG.js} +112 -21
- package/dist/{chunk-BT2CSEC5.js → chunk-VWGJQTNA.js} +3 -3
- package/dist/chunk-WGDAOOXG.js +845 -0
- package/dist/{chunk-4YMD2OOZ.js → chunk-XHEXOES3.js} +1 -1
- package/dist/{chunk-4HT3BNZ7.js → chunk-YUBJLB6G.js} +10 -2
- package/dist/{chunk-5L4X5LS6.js → chunk-YUPQOLFX.js} +77 -2
- package/dist/{chunk-DYCLVQXW.js → chunk-YYSJAAJM.js} +25 -1
- package/dist/{chunk-P3TTMWUP.js → chunk-ZOYMZZ3S.js} +8 -1
- package/dist/chunk-ZSYZTGJH.js +81 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +8 -8
- package/dist/{codegen-command-FIADGXRN.js → codegen-command-Z7WQMOJ7.js} +21 -20
- package/dist/{completion-BKAFCZBE.js → completion-4GYN752B.js} +2 -2
- package/dist/{deploy-command-4R6BYC6G.js → deploy-command-M2S4HHLC.js} +26 -26
- package/dist/{env-target-XWS2ZZ2Y.js → env-target-GSFCYHKZ.js} +6 -6
- package/dist/{ephemeral-command-JL4TPIRQ.js → ephemeral-command-6WJW7LP6.js} +24 -24
- package/dist/index.d.ts +2 -2
- package/dist/index.js +13 -7
- package/dist/init-command-GJUPWTKA.js +30 -0
- package/dist/internal.d.ts +2 -2
- package/dist/internal.js +50 -5
- package/dist/{io-AMIKRLPC.js → io-7VIA5SON.js} +3 -3
- package/dist/{live-diff-RXSJCVJ7.js → live-diff-FP4SFNT4.js} +2 -2
- package/dist/{lock-3CVKALKT.js → lock-KXOJIGCG.js} +2 -2
- package/dist/{lock-commands-XOYS75YQ.js → lock-commands-7IH7RSSP.js} +9 -9
- package/dist/{login-command-ACJF6KWQ.js → login-command-QM5SBBI2.js} +2 -2
- package/dist/{logout-command-MX3MJS5U.js → logout-command-THPASOHM.js} +2 -2
- package/dist/{loop-SAWAOUFO.js → loop-7SAIGRCZ.js} +3 -3
- package/dist/{marketplace-command-UVV3XAOL.js → marketplace-command-463DT7O7.js} +11 -25
- package/dist/meta-client-OW5WKWW7.js +1 -1
- package/dist/node.d.ts +2 -2
- package/dist/node.js +18 -12
- package/dist/onboard-command-EHOHKQQU.js +36 -0
- package/dist/{profile-command-SWJ3SPKR.js → profile-command-EUWPVWED.js} +6 -6
- package/dist/{release-command-HZUY2XZX.js → release-command-M4CVNTWL.js} +25 -25
- package/dist/{sandbox-details-command-HJE5SPVG.js → sandbox-details-command-BRII3QHG.js} +5 -5
- package/dist/{sandbox-export-command-QCJY4GMV.js → sandbox-export-command-PCZ5FL4U.js} +8 -8
- package/dist/scaffold.js +4 -2
- package/dist/{store-g45zwB33.d.ts → store-BJONDJoZ.d.ts} +171 -2
- package/dist/{test-command-72Y5S22H.js → test-command-U4G5Y3UM.js} +10 -10
- package/dist/upgrade-command-CKJ4BS2P.js +177 -0
- package/dist/{validate-command-KDGH537H.js → validate-command-S5N37JRP.js} +12 -12
- package/dist/{workspace-command-ICSI6PKO.js → workspace-command-RALSEJSW.js} +28 -27
- package/dist/workspace-export-AJMGN3CQ.js +1 -1
- package/guides/README.md +30 -0
- package/guides/authoring.md +642 -0
- package/guides/cli.md +191 -0
- package/guides/codegen.md +83 -0
- package/guides/coverage.md +67 -0
- package/guides/deploying.md +340 -0
- package/guides/environment.md +132 -0
- package/guides/object-kinds.md +376 -0
- package/guides/project-structure.md +43 -0
- package/guides/scaffold.md +198 -0
- package/guides/typed-frontend.md +201 -0
- package/llms/kinds-knowledge.md +20 -0
- package/llms/object-kinds.md +1 -0
- package/llms-full.txt +25 -2
- package/llms.txt +3 -2
- package/manifest.json +26 -3
- package/package.json +5 -2
- package/dist/.build-fingerprint +0 -1
- package/dist/chunk-WUSKBXXD.js +0 -25
- package/dist/init-command-LOK75W64.js +0 -29
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
# Signing in & deploying
|
|
2
|
+
|
|
3
|
+
Authentication, ephemeral environments, static hosting, releasing to production, and validating against a live instance.
|
|
4
|
+
|
|
5
|
+
## Signing in & deploying (in depth)
|
|
6
|
+
|
|
7
|
+
**Sign in once** with `xanots login`. It runs the standard authorization-code + PKCE
|
|
8
|
+
browser flow (powered by the OpenID-certified [`openid-client`](https://github.com/panva/openid-client)):
|
|
9
|
+
opens your browser, you approve, and the CLI captures the redirect on a `127.0.0.1`
|
|
10
|
+
callback. On first use it dynamically registers its own OAuth client (RFC 7591) so the
|
|
11
|
+
authorize step never depends on the server tolerating an arbitrary loopback port; the
|
|
12
|
+
registration is cached in `~/.xano/xanots-clients.json`. The instance you're bound to is
|
|
13
|
+
read from the token's own `aud` claim.
|
|
14
|
+
|
|
15
|
+
`login` also **pins the numeric workspace** you consented to into the credential, so every
|
|
16
|
+
later command acts on exactly that workspace without looking it up again. There is **no
|
|
17
|
+
`--workspace` flag** — a credential addresses exactly one instance and one workspace. Run
|
|
18
|
+
`xanots workspace details` to see which.
|
|
19
|
+
|
|
20
|
+
The credential caches by default in a **shared** `~/.xanots/auth.json`, reusable
|
|
21
|
+
from **any** project directory — so a single `xanots login` covers all your projects.
|
|
22
|
+
Override the OAuth host with `--origin`/`$XANO_ORIGIN` and the loopback port with `--port`.
|
|
23
|
+
|
|
24
|
+
Running `login` again when a usable credential is already cached prints what you're
|
|
25
|
+
signed in to and stops, rather than spending another browser round trip — pass `--force`
|
|
26
|
+
to sign in anyway. That guard is also what keeps `login` from silently replacing a
|
|
27
|
+
hand-authored `type: "token"` credential (below).
|
|
28
|
+
|
|
29
|
+
**Credential formats.** `auth.json` holds one credential, discriminated by `type`:
|
|
30
|
+
|
|
31
|
+
```jsonc
|
|
32
|
+
// type: "oauth" — written by `xanots login`. Do not hand-edit.
|
|
33
|
+
{ "type": "oauth", "instance": "https://your-instance.xano.io", "workspace_id": 3, /* …tokens… */ }
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```jsonc
|
|
37
|
+
// type: "token" — WRITE THIS YOURSELF. A meta API bearer token for the same
|
|
38
|
+
// meta APIs, for automation. No login flow, no refresh, no rotation.
|
|
39
|
+
{
|
|
40
|
+
"type": "token",
|
|
41
|
+
"instance_base_url": "https://your-instance.xano.io",
|
|
42
|
+
"workspace_id": 3,
|
|
43
|
+
"meta_api_token": "your-meta-api-token"
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Both formats work at **either** location (project-local `./.xano/auth.json` or global
|
|
48
|
+
`~/.xanots/auth.json`) on the same precedence ladder below, and both determine the same
|
|
49
|
+
thing: one instance, one workspace. A `token` credential is never created, refreshed, or
|
|
50
|
+
revoked by the CLI — `xanots logout` just deletes the file, and the token itself stays
|
|
51
|
+
valid until you revoke it wherever you minted it. Save it with owner-only permissions
|
|
52
|
+
(`chmod 600`) and keep it out of git.
|
|
53
|
+
|
|
54
|
+
> **Upgrading:** this format is a break. An `auth.json` written before it is rejected with a
|
|
55
|
+
> message naming the fix — run `xanots login` again.
|
|
56
|
+
|
|
57
|
+
**Project-local credentials** — pass `--local` to `login` to cache tokens in a
|
|
58
|
+
**project-local** `./.xano/auth.json` instead (which `login` **auto-adds to `.gitignore`**),
|
|
59
|
+
scoping the sign-in to that directory. Every command that **reads** credentials
|
|
60
|
+
(`deploy`/`details`, `profile me`, token refresh) resolves them **project-local first, global
|
|
61
|
+
as a fallback**: it uses `./.xano/auth.json` when present, otherwise `~/.xanots/auth.json` —
|
|
62
|
+
so a `--local` project keeps working without repeating the flag. `login` and `logout` do
|
|
63
|
+
**not** fall back: they target the shared global cache unless you pass `--local`. An explicit
|
|
64
|
+
`--config`/`$XANO_CONFIG` always wins over everything.
|
|
65
|
+
|
|
66
|
+
`deploy <file>` runs the exact same pipeline as `export` (including `xano.lock`
|
|
67
|
+
seeding), then create-or-refreshes the target environment and imports the compiled
|
|
68
|
+
workspace into it as a full replace. `deploy --bundle <path>` skips the compile and uploads a
|
|
69
|
+
bundle a previous `export` wrote (handy in CI). A **projected, secret-free summary** prints to stdout
|
|
70
|
+
as JSON — `baseUrl` plus the workspace `id`/`name`, and the static URL (plus a `verified`
|
|
71
|
+
boolean reporting whether the frontend was confirmed live) when `--static` is used — while the
|
|
72
|
+
human-readable progress (and the live URLs) echoes to stderr. The raw
|
|
73
|
+
workspace blob is deliberately never dumped: it carries per-tenant secrets that must not land
|
|
74
|
+
in shell history or CI logs.
|
|
75
|
+
|
|
76
|
+
**Where it goes** — the instance your **token is bound to** (the token's `aud`), never a
|
|
77
|
+
flag. `xanots deploy` create-or-refreshes an **ephemeral** (default) or resolves your
|
|
78
|
+
throwaway **sandbox** (`--dest sandbox`); production promotion is the separate
|
|
79
|
+
`xanots release` path to your main instance workspace, which merges rather than replacing.
|
|
80
|
+
|
|
81
|
+
**Static host** — `deploy --static <dir>` archives a directory and deploys it to a
|
|
82
|
+
static host after the backend import. Its target follows the destination: with `--dest
|
|
83
|
+
ephemeral` the frontend lands **on the ephemeral itself** (backend + frontend in one
|
|
84
|
+
environment); with `--dest sandbox` it lands on your **own (parent) workspace**, since the
|
|
85
|
+
sandbox tenant does not serve static hosting. For the parent-workspace case the target is the
|
|
86
|
+
workspace your credential is already pinned to, and the CLI uploads the archive to
|
|
87
|
+
`/api:meta/workspace/{id}/static_host/default/build` with your ordinary bearer. That route
|
|
88
|
+
auto-creates the `default` host and **auto-deploys to `dev`**, returning the live URL — so
|
|
89
|
+
the static step is independent of the backend deploy (the backend still runs first because
|
|
90
|
+
it's the primary action).
|
|
91
|
+
|
|
92
|
+
**Liveness verification** — the build endpoint returning `200` only means the archive was
|
|
93
|
+
*accepted*; the edge may still be starting a cold host (which `503`s for tens of seconds) or
|
|
94
|
+
briefly routing the previous build. So after the upload the CLI polls the deployed URL until
|
|
95
|
+
the static server reports it is serving **this** build — its `X-Xano-Canonical` response header
|
|
96
|
+
matches the canonical returned for the build just pushed — then prints `Frontend is live`. It
|
|
97
|
+
polls every second for the first 30s, then every two seconds out to 120s. An unconfirmed poll
|
|
98
|
+
is a **warning, not a failure** (the build uploaded fine and usually comes online moments
|
|
99
|
+
later; the exit code stays `0` and the summary records `"verified": false`). Verification is
|
|
100
|
+
skipped when the response carries no canonical to compare against. Pass **`--no-verify`** to
|
|
101
|
+
skip the wait entirely — useful for fast iterative deploys or when the deployed URL isn't
|
|
102
|
+
reachable from the machine running the CLI.
|
|
103
|
+
|
|
104
|
+
**Config injection** — before archiving, the deploy rewrites EVERY `.html` document in the
|
|
105
|
+
build, inserting an inline `<script>` at the top of `<head>` that assigns each config value to a
|
|
106
|
+
`window.<KEY>` global (so it runs before the app bundle). The backend URL is seeded
|
|
107
|
+
automatically as `window.XANO_HOST` (from the backend deploy's own response), and
|
|
108
|
+
`--static-env KEY=VALUE` (repeatable) merges in extra keys, overriding the seed on a name
|
|
109
|
+
clash. A static host has no server runtime — it serves these files verbatim — so injected
|
|
110
|
+
values are **public**: base URLs and *publishable* keys only, never secrets (those go in
|
|
111
|
+
backend env, read via `env(name)`). Rewriting every document, not just the root, is what a
|
|
112
|
+
prerendered build needs: it serves a different document per route, so a root-only injection
|
|
113
|
+
leaves every deep link and refresh running with the global unset — and the page still renders.
|
|
114
|
+
Injection is skipped (reported as a warning, not a failure) when the archive has no document
|
|
115
|
+
with a `<head>` to anchor to, and any individual document that lacks one is named; values are
|
|
116
|
+
`<`-escaped so one containing `</script>` can't break out of the element. This is why a
|
|
117
|
+
prebuilt `frontend/dist` can retarget any sandbox with no rebuild.
|
|
118
|
+
|
|
119
|
+
> **Caching — verify with a cache buster.** The static host serves `index.html` with
|
|
120
|
+
> `Cache-Control: public, max-age=3600`, so a browser (or CDN) that loaded the page before
|
|
121
|
+
> your latest deploy can hold the old HTML — including a *pre-injection* `<script>`-less
|
|
122
|
+
> version — for up to an hour. If `window.XANO_HOST` looks missing, it's almost always this:
|
|
123
|
+
> hard-reload (Cmd/Ctrl+Shift+R) or open DevTools with "Disable cache" checked. When
|
|
124
|
+
> verifying from a script or agent, append a throwaway query param so you never read a cached
|
|
125
|
+
> copy — `curl -s "$URL/?nocache=$(date +%s)"` — and check the fetched HTML for the injected
|
|
126
|
+
> `window.XANO_HOST` line rather than retrying the same cached URL.
|
|
127
|
+
|
|
128
|
+
`xanots sandbox details` prints the same **sandbox base URL** (`GET /api:meta/sandbox/me`,
|
|
129
|
+
projected to JSON) out of band, for cases where you'd rather bake it in at build time.
|
|
130
|
+
(`xanots profile me` prints the *instance* base URL, i.e. the account's origin rather than
|
|
131
|
+
the sandbox tenant.) A static failure after a committed backend deploy **does not roll
|
|
132
|
+
back**: it exits with code `3` and a resumable message telling you to re-run with `--static`
|
|
133
|
+
to retry just that step.
|
|
134
|
+
|
|
135
|
+
`deploy` reuses cached tokens and **refreshes them automatically** when the access token
|
|
136
|
+
expires (Xano rotates the refresh token on every use; the new one is persisted). A rejected
|
|
137
|
+
refresh (`invalid_grant`) clears the stale cache and tells you to `xanots login` again.
|
|
138
|
+
|
|
139
|
+
**CI & agents** run non-interactively. The credential to reach for is the **meta credential
|
|
140
|
+
as three environment variables** — the `type: "token"` record above, with no file:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
XANO_INSTANCE_URL=https://your-instance.xano.io \
|
|
144
|
+
XANO_WORKSPACE_ID=3 \
|
|
145
|
+
XANO_META_TOKEN=your-meta-api-token \
|
|
146
|
+
npx xanots deploy ./xano/index.ts
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Nothing is read from or written to disk, and nothing rotates, so the same three secrets keep
|
|
150
|
+
working run after run — which is what makes this the right shape for a CI job. It **outranks
|
|
151
|
+
every other credential**, including an explicit `--config` path and `$XANO_REFRESH_TOKEN`;
|
|
152
|
+
whichever it displaces is named on stderr, so it never wins silently.
|
|
153
|
+
|
|
154
|
+
All three are required **together**. Setting some but not others is a hard error naming the
|
|
155
|
+
rest, rather than a quiet fallback to another credential — a workflow with one misspelled
|
|
156
|
+
secret must not deploy against whatever happens to be on the runner.
|
|
157
|
+
|
|
158
|
+
> **Automated agents:** do **not** invoke `xanots login` — it blocks on interactive browser
|
|
159
|
+
> consent. Use the three variables above.
|
|
160
|
+
>
|
|
161
|
+
> The older `$XANO_REFRESH_TOKEN` + `$XANO_CLIENT_ID` pair still works (both copied once
|
|
162
|
+
> from `auth.json` after a local `xanots login`; the target instance is read from the refresh
|
|
163
|
+
> token's `aud` and the workspace resolved per run), but Xano **rotates refresh tokens on
|
|
164
|
+
> use** — a stored one is spent by its first exchange, so a job that runs twice fails the
|
|
165
|
+
> second time. Prefer the meta credential.
|
|
166
|
+
|
|
167
|
+
## Validating against a live instance (xanots validate)
|
|
168
|
+
|
|
169
|
+
`xanots validate` proves your compiled output against a **real, running Xano
|
|
170
|
+
instance** — not a static snapshot. It compiles your workspace, imports it into a
|
|
171
|
+
**fresh ephemeral environment created for that run**, exports it back, and diffs it
|
|
172
|
+
against what you compiled, so you catch three classes of problem a local build can't:
|
|
173
|
+
|
|
174
|
+
1. **Import accepts** — the engine actually accepts the bundle (malformed-but-shaped output is rejected here).
|
|
175
|
+
2. **Round-trip parity** — the workspace the engine stores, re-exported in the same bundle format, matches your compiled JSON after normalization (full object logic included). Every authored kind is diffed — tables, functions, queries, triggers, tasks, and more — each object matched by identity and reported per kind.
|
|
176
|
+
3. **Runtime** (`--runtime`) — each deployed function actually runs on the engine, with logs surfaced on failure.
|
|
177
|
+
|
|
178
|
+
It talks only to public meta API routes — the **same** archive import `xanots
|
|
179
|
+
deploy` uses, plus the workspace export — and never touches XanoScript. There is one
|
|
180
|
+
way into an instance, so a transport bug is one `validate` reproduces rather than
|
|
181
|
+
routes around.
|
|
182
|
+
|
|
183
|
+
It is non-destructive: nothing you own is written to. Each run creates its own
|
|
184
|
+
ephemeral environment, imports into that, and deletes it afterwards — including when
|
|
185
|
+
the import is rejected or a transport error is thrown. The environment carries a
|
|
186
|
+
short expiry, so even a killed process leaves nothing permanent behind. A fresh
|
|
187
|
+
environment per run is also what makes the diff trustworthy: the objects read back
|
|
188
|
+
can only have come from this bundle, never from what a previous run left.
|
|
189
|
+
|
|
190
|
+
**Setup** — copy `.env.example` to `.env` (gitignored) and fill in a base URL +
|
|
191
|
+
token. Switching between a cloud dev instance and a local Docker one is just a
|
|
192
|
+
different `XANO_VALIDATE_INSTANCE`:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
# .env
|
|
196
|
+
XANO_VALIDATE_INSTANCE=https://your-instance.xano.io # or http://localhost:8080 for local Docker
|
|
197
|
+
XANO_VALIDATE_TOKEN=your-meta-bearer-token
|
|
198
|
+
# XANO_VALIDATE_WORKSPACE_ID=… # optional; PARENT workspace the run's env is created under (default 1)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
xanots validate ./xano/index.ts # import + round-trip diff, reports per object (every authored kind)
|
|
203
|
+
xanots validate ./xano/index.ts --runtime # + run each deployed function
|
|
204
|
+
xanots validate ./xano/index.ts --capture # + write fetched JSON to ./validate-out (fixture candidates)
|
|
205
|
+
xanots validate ./xano/index.ts --instance http://localhost:8080 # override the target for one run
|
|
206
|
+
xanots validate --bundle ws.json # validate an already-exported bundle
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Config comes from the environment (a `.env` is autoloaded; a real env var wins),
|
|
210
|
+
`--instance` overrides per run, and the token is env-only — never a flag. This
|
|
211
|
+
harness is deliberately separate from the `auth.json` credential the rest of the
|
|
212
|
+
CLI uses. A non-zero exit means a check failed; `--verbose` prints full diffs and raw
|
|
213
|
+
engine detail instead of a projected summary.
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
## Wiring the frontend to the backend
|
|
217
|
+
|
|
218
|
+
**Wiring the frontend to the backend.** The deploy bakes the environment's backend URL into
|
|
219
|
+
every HTML document in your build automatically, as a `window.XANO_HOST` global evaluated
|
|
220
|
+
*before* your app bundle — every document, so a prerendered build's deep links and refreshes
|
|
221
|
+
boot with the same backend the root does. So read it at runtime with a build-time fallback and you never have to
|
|
222
|
+
know the URL ahead of time:
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
const HOST = (typeof window !== "undefined" && window.XANO_HOST) || import.meta.env.VITE_XANO_HOST;
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
`window.XANO_HOST` is the **sandbox tenant** URL your deployed APIs answer at (the same
|
|
229
|
+
value `xanots sandbox details` prints as `baseUrl`); it is *not* `xanots profile me`,
|
|
230
|
+
which prints your account's instance origin. Because injection happens at deploy time, a
|
|
231
|
+
prebuilt `frontend/dist` retargets any sandbox with **no rebuild** — ideal for headless agents.
|
|
232
|
+
Add your own public config (base URLs, *publishable* keys) with `--static-env KEY=VALUE`
|
|
233
|
+
(repeatable), exposed the same way as `window.<KEY>`. A static host serves these files
|
|
234
|
+
verbatim to the browser, so everything injected is **public** — never put secrets here;
|
|
235
|
+
those belong in backend env, read server-side via `env(name)`.
|
|
236
|
+
|
|
237
|
+
**Showing a stored file.** A file column comes back as `{ path, name, type, size, meta,
|
|
238
|
+
access, url }`. Don't use its `url`: on a tenant-scoped environment that field addresses the
|
|
239
|
+
instance host *without* the `/tenant/<name>` segment and 404s — as a broken `<img>`, while
|
|
240
|
+
every assertion about the response still passes. Build the URL from `path` and the host you
|
|
241
|
+
already have:
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
import { fileUrl } from "@xanots/sdk";
|
|
245
|
+
|
|
246
|
+
<img src={fileUrl(row.avatar, HOST) ?? ""} /> // null for an absent file
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**Verifying the injection:** the served `index.html` writes the global in **bracket
|
|
250
|
+
notation** — `window["XANO_HOST"]="…";` — so grep for the bare token `XANO_HOST`, not the
|
|
251
|
+
exact string `window.XANO_HOST` (the dot form is valid to *read* the global in your app,
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
## Deploy targets, and what a release changes
|
|
255
|
+
|
|
256
|
+
**Two targets**, so the dev loop and the production step stay distinct:
|
|
257
|
+
|
|
258
|
+
| Command | Where it goes |
|
|
259
|
+
|---|---|
|
|
260
|
+
| `xanots deploy` | A disposable **ephemeral** environment (default) — create-or-refreshed each run, auto-expiring, with its own URL. `--dest sandbox` targets your throwaway singleton instead. |
|
|
261
|
+
| `xanots release` | Your **main Xano instance** workspace — the production target. **Merges** by default: objects are updated in place or added, and your table data is never touched unless you ask. |
|
|
262
|
+
| `xanots test` | Nothing — it only reads. Runs the tests an already-deployed environment carries; `--dest` picks which one, `workspace` included. |
|
|
263
|
+
|
|
264
|
+
Not every instance has ephemeral environments enabled. Where they are off, the default
|
|
265
|
+
`deploy` says so and points at `--dest sandbox` — note that `--static` then publishes the
|
|
266
|
+
frontend to your own (parent) workspace, replacing whatever it is hosting.
|
|
267
|
+
|
|
268
|
+
Every `deploy` is a **full replace** of the disposable environment — always fresh, no
|
|
269
|
+
merge mode, no flags to get wrong. A `release` is the opposite by design: it changes what
|
|
270
|
+
your code defines and leaves the rest of the workspace — including every row in every
|
|
271
|
+
table — alone.
|
|
272
|
+
|
|
273
|
+
| `release` flag | What it does to your workspace |
|
|
274
|
+
|---|---|
|
|
275
|
+
| *(none)* | Adds new objects, updates existing ones. Nothing is deleted, no rows are written. |
|
|
276
|
+
| `--dry-run` | Prints the plan and exits without sending anything. |
|
|
277
|
+
| `--prune` | Also deletes objects **this project released** and no longer defines. Requires `xano.lock` — see below. |
|
|
278
|
+
| `--reset-data` | Empties every table the bundle carries. |
|
|
279
|
+
| `--seed` | Writes the bundle's table rows. Combine with `--reset-data` to reset **and** re-seed. |
|
|
280
|
+
| `--replace` | The disposable-environment behavior: wipe the workspace and import in its place. |
|
|
281
|
+
|
|
282
|
+
**An unchanged project is a no-op.** Before importing, the release compares the bundle it is
|
|
283
|
+
about to send against the workspace it is about to send it to, object by object. When every
|
|
284
|
+
object is already there, nothing is sent: the plan prints `no changes`, the JSON summary
|
|
285
|
+
reports `"upToDate": true` with `"operations": 0`, and no `updated_at` moves. That makes
|
|
286
|
+
`release` usable as a reconcile step — safe to run on a schedule, in CI on every merge, or
|
|
287
|
+
behind a "make production match main" button.
|
|
288
|
+
|
|
289
|
+
The comparison is deliberately one-sided: a false "changed" costs one import that was
|
|
290
|
+
happening anyway, while a false "unchanged" would silently skip a real release. So anything
|
|
291
|
+
it cannot prove equal counts as changed, and it is skipped entirely for `--replace` (which
|
|
292
|
+
mints fresh identities) and for `--seed`/`--reset-data` (which write table rows, about which
|
|
293
|
+
the comparison knows nothing). Under `--prune`, an object the workspace holds and the project
|
|
294
|
+
no longer defines is work to do, so that is not a no-op either.
|
|
295
|
+
|
|
296
|
+
Anything destructive is **previewed first** — the CLI fetches the plan, prints what would
|
|
297
|
+
change, and asks. `--yes` skips the prompt for CI but never skips the preview. Start with
|
|
298
|
+
`xanots release ./xano/index.ts --dry-run` to see the plan without committing to it.
|
|
299
|
+
|
|
300
|
+
> ⚠️ `--prune` removes tables this project released and no longer defines, and a removed
|
|
301
|
+
> table takes its rows with it — no flag prevents that. The preview reports it explicitly;
|
|
302
|
+
> read it before confirming.
|
|
303
|
+
|
|
304
|
+
**`--prune` is scoped to what this project released**, and `xano.lock` is what defines that
|
|
305
|
+
scope: it holds an entry for every object the project has ever exported. A planned deletion
|
|
306
|
+
with no lock entry is an object the project never created — a table built in the UI, another
|
|
307
|
+
team's API group — and the release is refused rather than deleting it. A prune with no lock
|
|
308
|
+
at all is refused too: without one there is no record of what belongs to the project, so
|
|
309
|
+
every deletion would be a guess. To delete something that is genuinely yours to remove,
|
|
310
|
+
adopt it first (`xanots lock adopt`). Releasing a pre-exported `--bundle` carries no entry
|
|
311
|
+
file to find a lock beside, so name it with `--lock=<path>`.
|
|
312
|
+
|
|
313
|
+
**A dropped column is destructive, and does not need a flag to happen.** Removing a column
|
|
314
|
+
from a table schema and releasing destroys the column and every value in it. The server's
|
|
315
|
+
plan calls that a routine in-place update, so the release compares your schema against the
|
|
316
|
+
live workspace, names each column that would be dropped, and asks before doing it — on an
|
|
317
|
+
ordinary release, with no destructive flag passed.
|
|
318
|
+
|
|
319
|
+
**Environment variables are add-only on a release.** A merge creates keys that do not exist
|
|
320
|
+
yet and leaves existing ones as they are, so changing a value in code and releasing will not
|
|
321
|
+
change it on the workspace. The preview names any key it will decline to update. To change
|
|
322
|
+
one, set it on the workspace directly, or use `--replace` (which rebuilds the workspace).
|
|
323
|
+
|
|
324
|
+
A merge matches objects by the stable identity your project assigns them, so it only
|
|
325
|
+
recognizes a workspace it has released to before. Releasing into one built by hand — or
|
|
326
|
+
populated by `--replace`, which assigns its own — matches nothing: every object is a
|
|
327
|
+
create, and with `--prune` the workspace is emptied and rebuilt rather than updated. The
|
|
328
|
+
preview says so in as many words when it happens. To adopt objects that are already there,
|
|
329
|
+
pin their `guid` on the matching defs first. Deploys are **authenticated over OAuth** — sign in once,
|
|
330
|
+
and the CLI refreshes tokens automatically. The target instance comes from your token
|
|
331
|
+
(never a stray flag), and the CLI prints what it's about to do before it touches anything.
|
|
332
|
+
|
|
333
|
+
**CI & agents** run fully headless from two env vars — no browser needed:
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
XANO_REFRESH_TOKEN=… XANO_CLIENT_ID=… npx @xanots/sdk deploy --bundle ws.json
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
> ⚠️ A deploy is a full replace of the target environment, including its table records,
|
|
340
|
+
> before importing. The blast radius is your own disposable ephemeral/sandbox — but anything
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Environment & identity
|
|
2
|
+
|
|
3
|
+
The environment variables XanoTS reads, and how `xano.lock` pins object identity across deploys.
|
|
4
|
+
|
|
5
|
+
## Environment variables
|
|
6
|
+
|
|
7
|
+
Every variable the CLI and SDK read. All are optional — the defaults in the right column
|
|
8
|
+
are what you get when the variable is unset.
|
|
9
|
+
|
|
10
|
+
**Authentication** (see [Signing in & deploying](deploying.md) for the full precedence ladder)
|
|
11
|
+
|
|
12
|
+
| Variable | What it does |
|
|
13
|
+
|---|---|
|
|
14
|
+
| `XANO_INSTANCE_URL` | Instance origin for the meta credential (CI, agents), e.g. `https://your-instance.xano.io`. Set with `XANO_WORKSPACE_ID` + `XANO_META_TOKEN` — all three together, or none. |
|
|
15
|
+
| `XANO_WORKSPACE_ID` | Numeric workspace the meta credential acts on. |
|
|
16
|
+
| `XANO_META_TOKEN` | Meta API bearer token. With the two above it forms a complete credential that outranks every other source, reads no file, and never rotates. |
|
|
17
|
+
| `XANO_REFRESH_TOKEN` | OAuth refresh token for non-interactive runs (CI, agents). Paired with `XANO_CLIENT_ID`; the target instance comes from the token's own `aud` claim. Rotates on use — prefer the meta credential above. |
|
|
18
|
+
| `XANO_CLIENT_ID` | OAuth client id that goes with `XANO_REFRESH_TOKEN`. Both are copied once out of `auth.json` after a local `xanots login`. |
|
|
19
|
+
| `XANO_CONFIG` | Explicit path to the credential file. Wins over `--local` and over both default locations — the same thing `--config <path>` does. |
|
|
20
|
+
| `XANO_GLOBAL_CONFIG` | Moves the **shared** credential cache off `~/.xanots/auth.json`. Only changes where the global cache lives; the project-local `./.xano/auth.json` and the `XANO_CONFIG`/`--config` override are unaffected. |
|
|
21
|
+
| `XANO_CLIENT_FILE` | Moves the OAuth **client-registration** cache off `~/.xano/xanots-clients.json`. That file holds the `client_id` minted per auth host + redirect URI, not a credential. |
|
|
22
|
+
| `XANO_ORIGIN` | OAuth host to sign in against, instead of the default — the same thing `--origin` does. |
|
|
23
|
+
| `XANO_NO_BROWSER` | Set to anything non-empty and `xanots login` will **not** launch a browser; it prints the authorize URL to stderr for you to open yourself. The loopback server still runs and still waits for the redirect, so the flow completes once you visit the URL. For a machine that has no browser to launch, or a remote shell. |
|
|
24
|
+
|
|
25
|
+
**`xanots validate`** — its own target, deliberately separate from the deploy login
|
|
26
|
+
|
|
27
|
+
| Variable | What it does |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `XANO_VALIDATE_INSTANCE` | Base URL of the instance to validate against (`https://your-instance.xano.io`, or `http://localhost:8080` for local Docker). Required; `--instance <url>` overrides it. |
|
|
30
|
+
| `XANO_VALIDATE_TOKEN` | Meta API bearer token for that instance. Required. |
|
|
31
|
+
| `XANO_VALIDATE_WORKSPACE_ID` | Parent workspace the run's throwaway environment is created under. Defaults to `1`. |
|
|
32
|
+
|
|
33
|
+
**Output & diagnostics**
|
|
34
|
+
|
|
35
|
+
| Variable | What it does |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `XANOTS_DEBUG` | Appends the untouched underlying error to a failure message, instead of only the mapped explanation. |
|
|
38
|
+
| `NO_COLOR` | Suppresses ANSI color on the stderr progress output ([no-color.org](https://no-color.org)). Color is off by default whenever stderr is not a TTY. |
|
|
39
|
+
| `FORCE_COLOR` | Forces color on even when stderr is not a TTY; `FORCE_COLOR=0` forces it off and beats `NO_COLOR`'s absence either way. |
|
|
40
|
+
|
|
41
|
+
**Update notifier**
|
|
42
|
+
|
|
43
|
+
| Variable | What it does |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `XANOTS_NO_UPDATE_CHECK` | Turns the once-a-day "a newer version is out" notice off. |
|
|
46
|
+
| `NO_UPDATE_NOTIFIER` | The de-facto convention, honored identically. `CI` being set also silences the notice. |
|
|
47
|
+
| `XANOTS_INSTALL_MODE` | `global` or `local` — pins whether the notice suggests `npm i -g` or a project-local upgrade, instead of detecting it. |
|
|
48
|
+
| `XANOTS_UPDATE_REGISTRY` | Registry URL the check reads, instead of the npm endpoint for `@xanots/sdk`. |
|
|
49
|
+
| `XANOTS_UPDATE_CACHE` | Moves the check's cache file off `~/.xanots/update-check.json`. |
|
|
50
|
+
|
|
51
|
+
**Escape hatches**
|
|
52
|
+
|
|
53
|
+
| Variable | What it does |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `XANOTS_MARKETPLACE_URL` | Base URL the `xanots marketplace` reads hit, instead of the published catalogue. Repoints the three read verbs without waiting for a release. |
|
|
56
|
+
| `XANOTS_PROVE_DIFF` | A file path. Codegen appends one JSON line per statement that fell back to `raw()` — the arm that declined and the key paths where the re-encode disagreed. The decline *reason* is on the report either way; this adds the machine-readable detail. |
|
|
57
|
+
|
|
58
|
+
> `process.env` read inside a **workspace definition** is a different thing entirely: it
|
|
59
|
+
> resolves at export time and bakes the literal into the bundle. For a value the deployed
|
|
60
|
+
> stack reads at runtime, use `workspaceConfig({ env })` + `env("NAME")`.
|
|
61
|
+
|
|
62
|
+
## Identity & the xano.lock file
|
|
63
|
+
|
|
64
|
+
Every top-level object carries a stable `guid` — Xano's identity anchor. On a sync import
|
|
65
|
+
the engine matches an incoming object to an existing one **by guid** and updates it in
|
|
66
|
+
place; no match means a new object. So re-running `export`/`deploy` on the same code maps
|
|
67
|
+
cleanly onto the same workspace — **no duplicates**. By default the guid derives from the
|
|
68
|
+
object's `name`; set an explicit `guid` to pin identity across a rename, or to adopt an
|
|
69
|
+
existing workspace object into code.
|
|
70
|
+
|
|
71
|
+
The opt-in **`xano.lock`** freezes the whole workspace's identities at once — every
|
|
72
|
+
auto-derived guid, plus the `canonical` URL tokens of API groups and toolsets (which the
|
|
73
|
+
engine otherwise randomizes, giving the same code different public URLs per environment).
|
|
74
|
+
Create it once with `xanots export ./xano/index.ts --lock`; from then on it's read automatically and
|
|
75
|
+
updated on every export (written atomically before the bundle). **Commit it next to your
|
|
76
|
+
code.** A project from `xanots init` is locked from its first export — both its `xano:export`
|
|
77
|
+
and `xano:deploy` scripts pass `--lock`, and `npm run xano:check` is the `--frozen-lock` CI
|
|
78
|
+
guard. Adopt it early either way: once identities have drifted, the only way back is
|
|
79
|
+
`lock adopt` against the deployed workspace.
|
|
80
|
+
|
|
81
|
+
Precedence at emit is always **explicit in-code value → lock entry → name derivation**.
|
|
82
|
+
|
|
83
|
+
**When the lock is actually load-bearing.** Because the default derivation is deterministic
|
|
84
|
+
— `md5("<type>:<name>")` — a project that created all of its own objects can regenerate a
|
|
85
|
+
byte-identical lock from its own source. Delete that lock, release again, and the same guids
|
|
86
|
+
come back: the objects match and update in place. For that project the lock is a *cache*, and
|
|
87
|
+
losing it costs nothing.
|
|
88
|
+
|
|
89
|
+
The lock is load-bearing exactly where a live guid **diverges** from that derivation, which
|
|
90
|
+
happens two ways:
|
|
91
|
+
|
|
92
|
+
- **Adopted** objects — anything built in the Xano UI first and taken over with `lock adopt`.
|
|
93
|
+
The engine assigned those guids randomly; nothing in your code can re-derive them.
|
|
94
|
+
- **Renamed** objects — `lock rename` pins the original guid under the new name, so the
|
|
95
|
+
derivation no longer reproduces it.
|
|
96
|
+
|
|
97
|
+
For those entries the lock is irreplaceable, and losing it means the next release matches
|
|
98
|
+
nothing and creates a duplicate of every diverged object. A workspace adopted wholesale from
|
|
99
|
+
the UI can be almost entirely divergent, so treat *that* lock as the critical artifact.
|
|
100
|
+
Either way, commit it — the cache is worth having, and you generally will not know which
|
|
101
|
+
entries have diverged without looking.
|
|
102
|
+
|
|
103
|
+
**Renames** — with a lock, a rename in code no longer means delete+create on sync. The
|
|
104
|
+
export warns about the orphaned entry and names the fix-up:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# code: defineFunction({ name: "signup" }) → { name: "register" }
|
|
108
|
+
xanots export ./xano/index.ts # stderr: lock entry "function:signup" matches no exported object…
|
|
109
|
+
xanots lock rename --entry=xano/index.ts function signup register
|
|
110
|
+
xanots export ./xano/index.ts # emits signup's original guid under "register" → engine renames in place
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`rename`/`adopt` take no entry file, so on their own they look for `xano.lock` in the
|
|
114
|
+
**current directory**. Pass `--entry=<path>` to derive it beside the entry the way
|
|
115
|
+
`export`/`deploy`/`prune` do, or `--lock=<path>` to name the file outright. They never
|
|
116
|
+
reach for a lock you did not point them at — when they spot one next door they say so and
|
|
117
|
+
stop, rather than writing a file you did not name.
|
|
118
|
+
|
|
119
|
+
**After `--replace`** — a replace rebuilds the workspace with fresh engine identities, so the
|
|
120
|
+
lock is stale the moment it finishes and the next ordinary release would match nothing and
|
|
121
|
+
try to create everything. `xanots release ./xano/index.ts --replace` now re-pins the lock from the rebuilt
|
|
122
|
+
workspace itself and tells you to commit it. If there is no lock to re-pin, it says so —
|
|
123
|
+
without one, the next release duplicates every object.
|
|
124
|
+
|
|
125
|
+
**Adopting a live workspace** — `xanots lock adopt <bundle.json>` seeds the lock from a
|
|
126
|
+
real engine `packageExport`, capturing the live workspace's random guids by `(type, name)`
|
|
127
|
+
so code takes over an existing workspace and the first sync updates in place instead of
|
|
128
|
+
duplicating.
|
|
129
|
+
|
|
130
|
+
**CI** — `xanots export ./xano/index.ts --frozen-lock` fails instead of changing the lock, so a canonical
|
|
131
|
+
minted in a throwaway container can never silently diverge public URLs. Mint locally, commit
|
|
132
|
+
the lock.
|