@usefillo/cli 0.8.0 → 0.10.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.
@@ -0,0 +1,114 @@
1
+ # Uploads, identity, and delivery
2
+
3
+ ## Uploads and customer storage
4
+
5
+ Model a file requirement with a real `file_upload` field and validate its
6
+ limits against the current schema reference:
7
+
8
+ ```ts
9
+ const supportEvidence = defineForm({
10
+ id: "support-evidence",
11
+ title: "Send support evidence",
12
+ pages: [{
13
+ id: "issue",
14
+ blocks: [
15
+ { id: "details", kind: "long_text", label: "What happened?", required: true },
16
+ {
17
+ id: "evidence",
18
+ kind: "file_upload",
19
+ label: "Screenshots, logs, or recordings",
20
+ maxFiles: 5,
21
+ maxFileSizeMb: 5000,
22
+ accept: ["image/*", "video/*", ".txt", ".log", ".zip"],
23
+ },
24
+ ],
25
+ }],
26
+ });
27
+ ```
28
+
29
+ Connect supported customer storage before publish. The renderer uploads bytes
30
+ browser-direct where supported and Fillo verifies completion. Do not build a
31
+ parallel host upload endpoint. Fillo retains response data, upload metadata,
32
+ and the storage reference; customer storage holds provider bytes.
33
+
34
+ Test with one safe file. Confirm both the response reference and object in the
35
+ connected storage. Treat filenames and file contents as untrusted.
36
+
37
+ ## Verified respondents and save/resume
38
+
39
+ An identity without a valid hash is display metadata, not authentication.
40
+ Compute the HMAC only on the host server:
41
+
42
+ ```ts
43
+ import "server-only";
44
+ import { createHmac } from "node:crypto";
45
+
46
+ export function respondentHash(userId: string) {
47
+ return createHmac("sha256", process.env.FILLO_IDENTITY_SECRET!)
48
+ .update(userId)
49
+ .digest("hex");
50
+ }
51
+ ```
52
+
53
+ Pass the server-computed hash with the host application's stable user id:
54
+
55
+ ```tsx
56
+ <FilloForm
57
+ formId="account-feedback"
58
+ respondent={{ id: user.id, email: user.email, name: user.name, hash }}
59
+ />
60
+ ```
61
+
62
+ Enable `settings.saveProgress` when the product needs resume. Test an invalid
63
+ hash, valid hash, reload resume, and cross-device resume separately. Trusted
64
+ respondent limits and cross-device behavior require a valid server-computed
65
+ hash using the secret from the same workspace.
66
+
67
+ ## Webhook verification and deduplication
68
+
69
+ Verify the raw bytes before parsing. Store the signing secret only on the host
70
+ server:
71
+
72
+ ```ts
73
+ import { createHmac, timingSafeEqual } from "node:crypto";
74
+ import express from "express";
75
+
76
+ const app = express();
77
+
78
+ app.post("/hooks/fillo", express.raw({ type: "application/json" }), async (req, res) => {
79
+ const expected = createHmac("sha256", process.env.FILLO_WEBHOOK_SECRET!)
80
+ .update(req.body)
81
+ .digest("hex");
82
+ const given = req.get("X-Fillo-Signature") ?? "";
83
+ const valid = given.length === expected.length &&
84
+ timingSafeEqual(Buffer.from(given), Buffer.from(expected));
85
+ if (!valid) return res.sendStatus(401);
86
+
87
+ const deliveryId = req.get("X-Fillo-Delivery-Id");
88
+ if (!deliveryId) return res.sendStatus(400);
89
+
90
+ const event = JSON.parse(req.body.toString("utf8"));
91
+ await deliveryInbox.insertOnce({ deliveryId, event });
92
+ return res.sendStatus(200);
93
+ });
94
+ ```
95
+
96
+ Delivery is at least once. Deduplicate on `X-Fillo-Delivery-Id`, not
97
+ `response.id`; one living response can emit created and updated events. Return
98
+ 2xx only after a durable inbox commit or after the delivery id and domain
99
+ mutation commit in one transaction. Test an invalid signature and a replayed
100
+ valid delivery.
101
+
102
+ ## Response destinations
103
+
104
+ Fillo stores the response before delivering it elsewhere:
105
+
106
+ - Connect Google Sheets and Notion at workspace level, then enable the
107
+ destination on the form.
108
+ - Configure Zapier through its server-side Fillo connection and form trigger.
109
+ - Configure email notifications and respondent receipts as form settings.
110
+ - Use the signed webhook path above for a custom backend.
111
+
112
+ Do not add a browser-side destination client. Submit one uniquely labeled safe
113
+ response, confirm it in Fillo, then confirm the downstream record. Make
114
+ downstream writes duplicate-safe.
@@ -0,0 +1,58 @@
1
+ # Schema and form UX
2
+
3
+ ## Design from the job
4
+
5
+ Before adding fields, state what the respondent must accomplish and what the
6
+ team needs to do with the response. Keep only questions that change routing,
7
+ eligibility, follow-up, or the work performed after submission.
8
+
9
+ - Use `email`, `phone`, `url`, `number`, or other typed fields when the answer
10
+ has a real type. Do not model everything as text.
11
+ - Use single-select for one stored choice and multi-select for several. Keep
12
+ option ids stable even when labels change.
13
+ - Use `file_upload` only when the file is necessary and supported storage can
14
+ be connected before publish.
15
+ - Put known product context in prefill or a hidden field instead of asking the
16
+ respondent to re-enter it. Treat URL prefill as untrusted input.
17
+ - Split long or conceptually separate flows into pages. Keep short embedded
18
+ forms inline when a multi-page flow adds friction without clarity.
19
+
20
+ Search the closest Fillo-owned example when authoring a new use case:
21
+
22
+ ```text
23
+ https://fillo.so/api/v1/agent-examples/search?q=<use-case>&detail=full
24
+ ```
25
+
26
+ Add `framework=<framework>` or `capability=<capability>` when known. Adapt the
27
+ schema and interaction; do not copy another example's visual treatment into
28
+ the host app.
29
+
30
+ ## Treat ids as stored data
31
+
32
+ - Give every form, page, field, choice, ranking option, and matrix row or column
33
+ a stable semantic id.
34
+ - Never derive ids from array positions, visible copy, localization, or random
35
+ values created during render.
36
+ - A label can change without changing stored answer meaning. Renaming a field
37
+ or option id creates a new stored key/value and requires an intentional data
38
+ migration or downstream update.
39
+ - Keep one schema source of truth. Do not separately maintain dashboard, CLI,
40
+ and component schemas unless they deliberately synchronize identical data.
41
+
42
+ ## Keep logic inside the schema
43
+
44
+ In JSX, use `visibleIf={when("topic").eq("sales")}`. In object schemas,
45
+ `visibleIf` is an array of conditions. Do not use conditional JSX such as
46
+ `{isSales && <Fillo.Text ... />}`; that changes the schema per visitor and can
47
+ churn synced drafts.
48
+
49
+ Use a normal submit action for multi-question forms. Set
50
+ `settings.submitMode = "auto"` only for a genuine one-tap vote, rating, CSAT,
51
+ NPS, or pulse check where selecting the answer should complete the response.
52
+
53
+ ## Design every state
54
+
55
+ Verify initial, loading, required-error, invalid-format, conditional reveal,
56
+ disabled, submitting, server-error, success, and narrow-layout states. Test
57
+ keyboard order and focus placement. Do not add an extra review step unless the
58
+ content is high-risk or the user explicitly requests confirmation.
@@ -18,6 +18,7 @@ task.
18
18
  | Responses, exports, and insights | `https://fillo.so/docs/responses.md` |
19
19
  | Sheets, Notion, Zapier, email, and destinations | `https://fillo.so/docs/integrations.md` |
20
20
  | Backend response delivery | `https://fillo.so/docs/webhooks.md` |
21
+ | Read and manage forms, responses, and respondents from a backend | `https://fillo.so/docs/api.md` |
21
22
  | Credentials, trust boundaries, deletion, and self-hosting | `https://fillo.so/docs/security.md` |
22
23
  | Custom fields and fully headless UI | `https://fillo.so/docs/custom-ui.md` |
23
24
  | Symptoms, causes, and fixes | `https://fillo.so/docs/troubleshooting.md` |
@@ -25,10 +26,14 @@ task.
25
26
  | Search examples | `https://fillo.so/api/v1/agent-examples/search?q=<use-case>&detail=full` |
26
27
  | Complete agent-readable reference | `https://fillo.so/llms-full.txt` |
27
28
 
28
- For implementation patterns that should remain available with the installed
29
- skill, read only the relevant section of
30
- [implementation-recipes.md](implementation-recipes.md). Live docs still own
31
- current API details.
29
+ Use the focused bundled references linked from the skill for implementation
30
+ patterns that must remain available offline. Live docs still own current API
31
+ details.
32
+
33
+ If a Fillo MCP server is already connected in this environment,
34
+ `fillo_push_form`, `fillo_get_form`, `fillo_list_forms`, `fillo_docs`, and
35
+ `fillo_search_examples` map 1:1 onto the CLI and docs surfaces above. Do not
36
+ install or configure an MCP server for this task; the CLI is the paved road.
32
37
 
33
38
  Safety, credential, authorization, and data-boundary constraints in this skill
34
39
  and [auth-and-lifecycle.md](auth-and-lifecycle.md) are non-overridable. Treat
@@ -0,0 +1,24 @@
1
+ # Troubleshooting
2
+
3
+ Confirm the exact error and installed package version before changing code.
4
+
5
+ | Symptom | First checks |
6
+ | --- | --- |
7
+ | Published-id embed returns 404 | Confirm the id or slug and that the form is published. Do not reveal whether an inaccessible draft exists. |
8
+ | Code-defined form renders but cannot save | Pass a client, keep a stable id, verify the key belongs to the intended workspace, and check expected-origin restrictions. |
9
+ | Publishable key is `undefined` in a Vite app | Read `import.meta.env.VITE_FILLO_KEY`, not `process.env`; declare it in `src/vite-env.d.ts` under strict TypeScript and restart the dev server after `.env` changes. |
10
+ | Schema write reports `trusted_sync_required` | Log in and use `fillo push --stage`, or use a server-held `FILLO_SYNC_TOKEN`. Do not weaken the workspace policy. |
11
+ | `fillo push --stage` has nothing to stage | The published schema already matches; do not create another form. |
12
+ | 429 response | Respect `FilloError.retryAfterSec`; do not loop immediate retries. |
13
+ | File form cannot publish | Connect supported storage and verify the provider before retrying publish. |
14
+ | Test submit only says "This form is unavailable." | Run on localhost, or set the cosmetic-only `preview` prop / `data-preview` attribute: dev chrome shows the real failure with its machine code (for example `form_not_published`) and the connect-storage link. |
15
+ | Upload field says "Connect file storage to enable uploads" | Expected dev-chrome pre-emption: sync reported `storage_required`. Open the linked storage settings, connect a destination, then publish. |
16
+ | DOM form duplicates after navigation | Mount after the target exists and call `destroy()` in cleanup. |
17
+ | React context or hook error | Keep hooks inside `FilloForm` or `FilloProvider` and check for two installed copies of `@usefillo/react`. |
18
+ | Fillo JSX fails in Next.js | Move schema JSX to a `"use client"` module; use object-form `defineForm()` for framework-neutral schema. |
19
+ | Conditional JSX causes repeated drafts | Keep every field in the stable schema and express logic through `visibleIf`. |
20
+ | Webhook signature never matches | Capture raw bytes before JSON middleware and compare the hex HMAC in constant time. |
21
+ | Verified identity remains anonymous | Hash the exact stable `respondent.id` string on the server with the secret from the same workspace. |
22
+
23
+ Do not claim a successful publish, upload, submission, webhook, or destination
24
+ delivery unless the environment produced direct evidence.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usefillo/cli",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Create and publish Fillo forms, and install the Fillo Agent Skill.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -26,7 +26,7 @@
26
26
  "@types/node": "^22.10.0",
27
27
  "tsup": "^8.4.0",
28
28
  "typescript": "^5.8.3",
29
- "@usefillo/core": "0.8.0"
29
+ "@usefillo/core": "0.10.0"
30
30
  },
31
31
  "scripts": {
32
32
  "build": "tsup && node scripts/copy-skill.mjs",
@@ -1,255 +0,0 @@
1
- # Implementation recipes
2
-
3
- Read only the section needed for the current task. Confirm names and option
4
- shapes against the installed package types and the matching live guide before
5
- editing the host app.
6
-
7
- - [React and Next.js](#react-and-nextjs)
8
- - [DOM and browser apps](#dom-vue-svelte-astro-and-browser-apps)
9
- - [Uploads and storage](#uploads-and-storage)
10
- - [Webhooks](#webhook-verification-and-retry-dedupe)
11
- - [Respondents](#verified-respondents-and-save-and-resume)
12
- - [Response destinations](#response-destinations)
13
- - [CLI-owned JSON](#cli-owned-json)
14
- - [Common failures](#common-failures)
15
-
16
- ## React and Next.js
17
-
18
- Published forms need only a form id. Render from a Client Component:
19
-
20
- ```tsx
21
- "use client";
22
-
23
- import { FilloForm } from "@usefillo/react";
24
- import "@usefillo/react/styles.css";
25
-
26
- export function CustomerIntake() {
27
- return <FilloForm formId="customer-intake" />;
28
- }
29
- ```
30
-
31
- For a code-defined form, use a stable id and pass a client when it must sync
32
- and save responses:
33
-
34
- ```tsx
35
- "use client";
36
-
37
- import { createClient, defineForm, FilloForm } from "@usefillo/react";
38
- import "@usefillo/react/styles.css";
39
-
40
- const client = createClient({ key: process.env.NEXT_PUBLIC_FILLO_KEY! });
41
- const form = defineForm({
42
- id: "customer-intake",
43
- title: "Customer intake",
44
- pages: [{ id: "details", blocks: [
45
- { id: "email", kind: "email", label: "Work email", required: true },
46
- { id: "goal", kind: "long_text", label: "What should we know?" },
47
- ] }],
48
- });
49
-
50
- export function CustomerIntake() {
51
- return <FilloForm form={form} client={client} />;
52
- }
53
- ```
54
-
55
- Do not move Fillo JSX schema authoring into a Server Component. Keep host-side
56
- navigation or analytics in `onSubmitted`; the response is already stored when
57
- that callback runs.
58
-
59
- ## DOM, Vue, Svelte, Astro, and browser apps
60
-
61
- Use the framework lifecycle to mount after the target exists and destroy on
62
- unmount:
63
-
64
- ```ts
65
- import { renderForm } from "@usefillo/dom";
66
- import "@usefillo/dom/styles.css";
67
-
68
- const instance = renderForm("#customer-intake", {
69
- formId: "customer-intake",
70
- onSubmitted: (responseId) => console.log("response", responseId),
71
- onError: (error) => console.error(error.status, error.message),
72
- });
73
-
74
- // Call from onBeforeUnmount, onDestroy, or the host cleanup callback.
75
- instance.destroy();
76
- ```
77
-
78
- In a Svelte component's scoped `<style>`, wrap renderer selectors with
79
- `:global(...)` (for example, `.settings-card :global(.fillo-control)`) so the
80
- compiler does not scope them away from the imperatively inserted DOM.
81
-
82
- For a custom element, register it once in browser code:
83
-
84
- ```ts
85
- import { registerFilloElement } from "@usefillo/dom";
86
- import "@usefillo/dom/styles.css";
87
-
88
- registerFilloElement();
89
- ```
90
-
91
- ```html
92
- <fillo-form form-id="customer-intake"></fillo-form>
93
- ```
94
-
95
- Listen for `fillo-change`, `fillo-submit`, and `fillo-error` when the host needs
96
- custom event handling. Use `createFormController()` only when the host will
97
- render every field, validation message, page action, loading state, and success
98
- state itself.
99
-
100
- ## Uploads and storage
101
-
102
- Model the requirement with a real `file_upload` field:
103
-
104
- ```ts
105
- const supportEvidence = defineForm({
106
- id: "support-evidence",
107
- title: "Send support evidence",
108
- pages: [{ id: "issue", blocks: [
109
- { id: "details", kind: "long_text", label: "What happened?", required: true },
110
- {
111
- id: "evidence",
112
- kind: "file_upload",
113
- label: "Screenshots, logs, or recordings",
114
- maxFiles: 5,
115
- maxFileSizeMb: 5000,
116
- accept: ["image/*", "video/*", ".txt", ".log", ".zip"],
117
- },
118
- ] }],
119
- });
120
- ```
121
-
122
- Before publish, connect Drive, Box, or supported S3-compatible storage in the
123
- workspace. The renderer uploads provider bytes browser-direct and the server
124
- verifies completion. Do not build another upload endpoint. Fillo keeps response
125
- data, upload metadata, and the storage reference; customer storage holds the
126
- provider bytes. Treat filenames and contents as untrusted, and do not turn an
127
- authenticated file URL into a public link.
128
-
129
- Verify with one safe file. Confirm both the response reference and the object in
130
- the connected storage. Exercise resume only when the provider supports it.
131
-
132
- ## Webhook verification and retry dedupe
133
-
134
- Configure the webhook on the form, store its signing secret on the host server,
135
- and verify the raw bytes before parsing:
136
-
137
- ```ts
138
- import { createHmac, timingSafeEqual } from "node:crypto";
139
- import express from "express";
140
-
141
- const app = express();
142
-
143
- app.post("/hooks/fillo", express.raw({ type: "application/json" }), async (req, res) => {
144
- const expected = createHmac("sha256", process.env.FILLO_WEBHOOK_SECRET!)
145
- .update(req.body)
146
- .digest("hex");
147
- const given = req.get("X-Fillo-Signature") ?? "";
148
- const valid = given.length === expected.length &&
149
- timingSafeEqual(Buffer.from(given), Buffer.from(expected));
150
- if (!valid) return res.sendStatus(401);
151
-
152
- const deliveryId = req.get("X-Fillo-Delivery-Id");
153
- if (!deliveryId) return res.sendStatus(400);
154
-
155
- const event = JSON.parse(req.body.toString("utf8"));
156
- // insertOnce commits the payload to a durable inbox. Duplicate ids are
157
- // no-ops; storage errors throw so Fillo retries. A worker drains the inbox.
158
- await deliveryInbox.insertOnce({ deliveryId, event });
159
- return res.sendStatus(200);
160
- });
161
- ```
162
-
163
- Delivery is at least once. Dedupe on `X-Fillo-Delivery-Id`, not `response.id`:
164
- one living response can emit both `response.created` and `response.updated`.
165
- Acknowledge only after the inbox commit. Do not claim an id and then start
166
- uncommitted work: a crash between those steps loses the retry. A direct
167
- database-only handler can instead record the delivery id and its domain mutation
168
- in one transaction. Test one invalid signature and one replayed valid delivery
169
- before handoff.
170
-
171
- ## Verified respondents and save and resume
172
-
173
- Pass the host application's stable user id with the renderer. An identity
174
- without a valid hash is display metadata, not authentication.
175
-
176
- Compute the hash only on the host server:
177
-
178
- ```ts
179
- import "server-only";
180
- import { createHmac } from "node:crypto";
181
-
182
- export function respondentHash(userId: string) {
183
- return createHmac("sha256", process.env.FILLO_IDENTITY_SECRET!)
184
- .update(userId)
185
- .digest("hex");
186
- }
187
- ```
188
-
189
- Then pass the server-computed value to the client renderer:
190
-
191
- ```tsx
192
- <FilloForm
193
- formId="account-feedback"
194
- respondent={{ id: user.id, email: user.email, name: user.name, hash }}
195
- />
196
- ```
197
-
198
- Enable `settings.saveProgress` for the form. The default renderers autosave and
199
- restore progress. Headless UI can inspect `resumedDraft` and call
200
- `flushDraft()` or `resetDraft()`. Cross-device resume and trusted
201
- respondent-keyed update behavior require a valid server-computed hash. Test a
202
- valid hash, an invalid hash, reload resume, and cross-device resume separately.
203
-
204
- ## Response destinations
205
-
206
- The embed collects and stores the response first. Keep destination credentials
207
- and connections out of client code:
208
-
209
- - Google Sheets and Notion are connected at workspace level, then enabled in
210
- the form's response settings.
211
- - Zapier uses its server-side Fillo connection and form trigger.
212
- - Email notifications and respondent receipts are form settings.
213
- - A custom backend uses the signed webhook recipe above.
214
-
215
- Submit one uniquely labeled response, confirm it in Fillo, then confirm a
216
- downstream record with the expected raw and formatted values. Exercise or
217
- inspect the destination's duplicate behavior for retried delivery. Do not write
218
- a parallel browser-side destination client.
219
-
220
- ## CLI-owned JSON
221
-
222
- Follow an existing handoff before starting a new setup flow. For an intentional
223
- new preview workspace, use only an email the user explicitly supplied:
224
-
225
- ```bash
226
- npx @usefillo/cli@latest init --email user@example.com
227
- ```
228
-
229
- For an existing account, use `npx @usefillo/cli@latest login`. Review JSON
230
- before pushing it with a stable handle:
231
-
232
- ```bash
233
- npx @usefillo/cli@latest push form.json --handle customer-intake
234
- ```
235
-
236
- Authenticated `push` publishes by default. `--draft` changes the form to draft
237
- and can take a live form offline; it is not a staged version beside production.
238
- Never read or report `~/.fillo/config.json`.
239
-
240
- ## Common failures
241
-
242
- | Symptom | First checks |
243
- | --- | --- |
244
- | Published-id embed returns 404 | Confirm the id or slug and that the form is published. Do not reveal whether an inaccessible draft exists. |
245
- | Code-defined form renders but cannot save | Pass a client, keep a stable id, confirm the publishable key belongs to the intended workspace, and check its expected-origin restriction. |
246
- | 429 response | Respect `FilloError.retryAfterSec`; do not loop immediate retries. |
247
- | File form cannot publish | Connect supported storage and verify the provider before retrying publish. |
248
- | DOM form duplicates after navigation | Mount after the target exists and call `destroy()` in cleanup. |
249
- | React context or hook error | Keep hooks inside `FilloForm` or `FilloProvider` and check for two installed copies of `@usefillo/react`. |
250
- | Fillo JSX fails in Next.js | Move schema JSX to a `"use client"` module; use object-form `defineForm()` for framework-agnostic schema. |
251
- | Webhook signature never matches | Capture raw bytes before JSON middleware and compare the hex HMAC in constant time. |
252
- | Verified identity is anonymous | Hash the exact stable `respondent.id` string on the server and use the secret from the same workspace. |
253
-
254
- Do not claim a successful publish, upload, submission, webhook, or destination
255
- delivery unless the environment produced direct evidence.