@getxflow/cli 0.3.0 → 0.3.1

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.
@@ -49,6 +49,17 @@ async function functionsInvoke(args) {
49
49
  const data = (0, args_1.flagString)(args, 'data');
50
50
  const method = ((0, args_1.flagString)(args, 'method') ?? (data ? 'POST' : 'GET')).toUpperCase();
51
51
  const sendsBody = method !== 'GET' && method !== 'HEAD';
52
+ // A pass for the person who owns the key: the project token alone is not an
53
+ // identity, and a project built with the current template refuses without one.
54
+ // A read-only key cannot get a pass, so the call still goes out without it.
55
+ let pass = '';
56
+ try {
57
+ const issued = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/pass`, { method: 'POST' });
58
+ pass = issued.pass;
59
+ }
60
+ catch {
61
+ (0, ui_1.note)((0, ui_1.dim)(' Could not get a visitor pass: calling with the project token only'));
62
+ }
52
63
  const started = Date.now();
53
64
  let response;
54
65
  try {
@@ -57,6 +68,7 @@ async function functionsInvoke(args) {
57
68
  headers: {
58
69
  'Content-Type': 'application/json',
59
70
  'X-Project-Token': card.project_token ?? '',
71
+ ...(pass ? { 'X-Project-Pass': pass } : {}),
60
72
  },
61
73
  body: sendsBody ? (data ?? '{}') : undefined,
62
74
  // Wait past the function's own 90 s cap to see its timeout, not ours.
package/dist/help.js CHANGED
@@ -150,8 +150,9 @@ A failed run shows up in ${(0, ui_1.bold)('xflow functions logs')}: nobody is wa
150
150
  function, so the wrapper reports a crash the same way it does on an ordinary call.`,
151
151
  invoke: `${(0, ui_1.bold)('xflow functions invoke')} <name>: call a function
152
152
 
153
- Calls it exactly the way the application does, with the X-Project-Token header. The
154
- token is taken from the project card on the platform, no local .env is needed.
153
+ Calls it exactly the way the application does: the project token plus a visitor pass
154
+ for the person who owns the key, so the function sees a real caller and its role.
155
+ Both are taken from the platform, no local .env is needed.
155
156
 
156
157
  --data '{"a":1}' request body (the method becomes POST by default)
157
158
  --method GET a different method
package/dist/version.js CHANGED
@@ -2,6 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DEFAULT_API_URL = exports.CLI_VERSION = void 0;
4
4
  /** Keep in sync with cli/package.json. */
5
- exports.CLI_VERSION = '0.3.0';
5
+ exports.CLI_VERSION = '0.3.1';
6
6
  /** Overridden by XFLOW_API_URL or the `api` field in xflow.json. */
7
7
  exports.DEFAULT_API_URL = 'https://app.getxflow.com';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getxflow/cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "CLI for the XFlow platform: source sync, deployment and publishing of applications",
5
5
  "license": "UNLICENSED",
6
6
  "engines": {
@@ -146,13 +146,53 @@ output of that call. Only failed calls are logged, so an empty output means the
146
146
  never crashed, not that logging is broken.
147
147
 
148
148
  From the app, call a function through `src/lib/xflow.ts`:
149
- `await xflow.functions.invoke('send-mail', { body: { to } })`. It carries the project
150
- token for you. Addresses are baked into the build, which is why the functions go out first:
149
+ `await xflow.functions.invoke('send-mail', { body: { to } })`. It carries the credentials
150
+ for you. Addresses are baked into the build, which is why the functions go out first:
151
151
  by the time the bundle is built they already exist, and a new function is never missing
152
152
  from the application that calls it.
153
153
 
154
- Treat a function as a public API: the token ships inside the frontend bundle, so anyone
155
- who opens the app can call it.
154
+ ### Who is calling
155
+
156
+ A function answers only to a member of the organization who has access to that project.
157
+ The platform issues a short-lived pass when it opens the application, the wrapper checks it
158
+ with the platform on every call, and the handler receives the answer in `event.xflow`:
159
+
160
+ ```js
161
+ exports.handler = async (event) => {
162
+ const { caller, user } = event.xflow
163
+ // caller: 'visitor' (a person), 'service' (another function of this project),
164
+ // 'external' (an outside service with a key), 'schedule' (a timer run)
165
+ // user: { id, role } for a visitor, null for everything else
166
+ }
167
+ ```
168
+
169
+ Never trust an identity that arrives in the body or in a header of the request: those are
170
+ written by the page, which lives on someone else's computer. `event.xflow` is the only
171
+ identity the platform stands behind, and `usePlatformAuth()` in the frontend is a hint for
172
+ the interface, not a check.
173
+
174
+ A function that changes data should say so instead of checking the role by hand:
175
+
176
+ ```js
177
+ exports.minRole = 'admin' // 'member' | 'developer' | 'admin' | 'owner'
178
+ ```
179
+
180
+ The wrapper refuses anything below that role before your code runs. Without the line every
181
+ member of the project can call the function, including the ones who may only look at apps.
182
+
183
+ Losing access closes the function within a minute, so a removed member cannot keep calling it.
184
+ Opening the deployed address directly does not work either: there is no pass outside the
185
+ platform.
186
+
187
+ Calling a function from another function is a server call and uses the server key of the
188
+ project: send `process.env.XFLOW_SERVER_KEY` in the `X-Server-Key` header.
189
+
190
+ An outside service (a webhook from a payment provider, a bot, a CRM) has no person behind it
191
+ and needs a key of that one function. Keys are not issued by default and the CLI cannot
192
+ create one: a human opens the function in the web interface, section "Внешний доступ", and
193
+ issues it there. Ask the user to do that and to paste the address back to you — never invent
194
+ another way in. A function holds at most two keys, and the second one exists to replace the
195
+ first without downtime, not to serve a second consumer.
156
196
 
157
197
  Keys and passwords live on the platform, not in the repository: `xflow env set SMTP_PASSWORD=…`
158
198
  writes one, `xflow env` lists the names, `xflow env check` tells you which variables your
@@ -160,8 +200,9 @@ functions read but the platform does not have. Values never come back out — th
160
200
  they exist is inside the running function.
161
201
 
162
202
  A function receives only the variables it mentions by name via `process.env.NAME`, so never
163
- assemble a variable name from an expression. New values arrive on the next `xflow deploy`,
164
- not at the moment they are written.
203
+ assemble a variable name from an expression and never destructure the environment
204
+ (`const { API_KEY } = process.env` reads as no mention at all, and the variable arrives
205
+ empty). New values arrive on the next `xflow deploy`, not at the moment they are written.
165
206
 
166
207
  To run a function on a timer: `xflow schedules set report "0 3 ? * * *"` (daily at 03:00).
167
208
  Six fields, UTC, and exactly one of day-of-month / day-of-week must be `?` — that is
@@ -184,7 +225,9 @@ const db = new Client({ connectionString: process.env.DATABASE_URL })
184
225
 
185
226
  The platform passes `DATABASE_URL` only to functions that mention it, and sets the project
186
227
  schema on every connection, so plain table names (`select * from tasks`) hit your project.
187
- You never write that variable yourself: `xflow env set DATABASE_URL=...` is refused.
228
+ You never write that variable yourself: `xflow env set DATABASE_URL=...` is refused. The same
229
+ goes for every name starting with `XFLOW`: the platform fills those in itself, and your value
230
+ under one of them would shadow the real one.
188
231
 
189
232
  The platform keeps no database history and no backups. Anything that destroys data
190
233
  (`DROP TABLE`, `DROP COLUMN`, `TRUNCATE`, `DELETE FROM` without a condition) is refused
@@ -195,6 +238,35 @@ One logical database can be shared by several projects, so your migration can br
195
238
  you do not see. `xflow db status` lists applied migrations that have no file in your
196
239
  repository: that is what someone else's project did.
197
240
 
241
+ ## File storage
242
+
243
+ The project has file storage, and the browser cannot reach it. Those endpoints take only the
244
+ server key of the project, and the platform puts it into the environment of your cloud
245
+ functions as `XFLOW_SERVER_KEY`. Nothing else holds it: not the bundle, not `.env`, not
246
+ `xflow env`.
247
+
248
+ So uploading is a function of your own. It asks the platform for a one-time link, the browser
249
+ then sends the bytes straight to storage, and a second call records the file:
250
+
251
+ ```js
252
+ const link = await fetch(`${process.env.XFLOW_API_URL}/api/storage/project/upload-url`, {
253
+ method: 'POST',
254
+ headers: {
255
+ 'Content-Type': 'application/json',
256
+ 'X-Server-Key': process.env.XFLOW_SERVER_KEY,
257
+ },
258
+ body: JSON.stringify({ fileName, fileSize, contentType, folderPath: 'invoices' }),
259
+ }).then((r) => r.json())
260
+ ```
261
+
262
+ `confirm` takes the same fields plus the returned `s3Key` and makes the file visible to the
263
+ app; `delete` takes the file `url`. Never pipe the bytes through the function itself.
264
+
265
+ What the app may do with files is decided inside that function, because the page in the
266
+ browser can be edited by whoever opened it. Never write the key into the sources and never
267
+ send it to the frontend: the build gate stops on a key found in the application code, and a
268
+ key that reached a visitor lets them delete every file of the project.
269
+
198
270
  ## Syncing code
199
271
 
200
272
  `xflow status` shows how the local copy differs from the server revision.
@@ -244,7 +316,9 @@ the design system sits in the project.
244
316
 
245
317
  The app runs inside the platform in an iframe and receives the theme and the current
246
318
  user from it. The `usePlatformAuth()` hook gives the name, role, permissions and the
247
- list of organization members.
319
+ list of organization members. Use it to draw the interface, never to guard data: the
320
+ value lives on the page and is edited from the console. Guard data in the function, by
321
+ `event.xflow`.
248
322
 
249
323
  ## Errors from a deployed app
250
324
 
@@ -256,6 +330,10 @@ records per project are kept.
256
330
  Local `npm run dev` does not report anything: these logs exist for what you cannot open
257
331
  in your own devtools.
258
332
 
333
+ Cloud functions do work under `npm run dev`, and nothing has to be configured for that:
334
+ the dev server swaps the access key of the logged-in developer for the same narrow pass and
335
+ forwards the call. If it answers that the key is missing, the fix is `xflow login`.
336
+
259
337
  ## Environment
260
338
 
261
339
  `VITE_XFLOW_PROJECT_TOKEN` and `VITE_XFLOW_API_URL` are written by the CLI when the