@lunora/cli 1.0.0-alpha.402 → 1.0.0-alpha.404

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/cli",
3
- "version": "1.0.0-alpha.402",
3
+ "version": "1.0.0-alpha.404",
4
4
  "description": "The Lunora CLI: init, dev, deploy, codegen, migrate, seed, doctor, insights, logs, registry, and the rest of the project commands",
5
5
  "keywords": [
6
6
  "agent-skills",
@@ -52,16 +52,16 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "@bomb.sh/tab": "0.0.22",
55
- "@lunora/advisor": "1.0.0-alpha.236",
56
- "@lunora/bindings": "1.0.0-alpha.123",
57
- "@lunora/codegen": "1.0.0-alpha.328",
58
- "@lunora/config": "1.0.0-alpha.378",
59
- "@lunora/container": "1.0.0-alpha.82",
60
- "@lunora/d1": "1.0.0-alpha.190",
55
+ "@lunora/advisor": "1.0.0-alpha.238",
56
+ "@lunora/bindings": "1.0.0-alpha.124",
57
+ "@lunora/codegen": "1.0.0-alpha.330",
58
+ "@lunora/config": "1.0.0-alpha.380",
59
+ "@lunora/container": "1.0.0-alpha.83",
60
+ "@lunora/d1": "1.0.0-alpha.191",
61
61
  "@lunora/errors": ">=1.0.0-alpha.51 <2.0.0-0",
62
- "@lunora/mcp": "1.0.0-alpha.243",
63
- "@lunora/runtime": "1.0.0-alpha.219",
64
- "@lunora/seed": "1.0.0-alpha.238",
62
+ "@lunora/mcp": "1.0.0-alpha.244",
63
+ "@lunora/runtime": "1.0.0-alpha.220",
64
+ "@lunora/seed": "1.0.0-alpha.239",
65
65
  "@visulima/cerebro": "3.1.2",
66
66
  "@visulima/error": "6.1.0",
67
67
  "@visulima/fs": "6.0.29",
package/skills/README.md CHANGED
@@ -25,6 +25,7 @@ own directory:
25
25
  | `lunora-migration-helper` | Schema/data migrations, `.global()` DDL flow, the drift gate. |
26
26
  | `lunora-deploy` | Deploying to Cloudflare — wrangler bindings, secrets, the gate. |
27
27
  | `lunora-performance-audit` | `lunora insights`, scans, indexes, OCC conflicts, sharding/`.global()`. |
28
+ | `lunora-cloudflare-audit` | Launch/bill audit: doctor + advisor signal, fan-out, cron, public-path cost. |
28
29
 
29
30
  These are mirrored (via symlinks) into `.agents/skills/` and `.claude/skills/`,
30
31
  so agents working inside this repo pick them up directly, and into
@@ -58,6 +58,7 @@ Scaffolded projects gitignore `lunora/_generated/`; never hand-edit it.
58
58
  - Schema/data migrations: `lunora-migration-helper`
59
59
  - Deploying (wrangler, bindings, secrets, the drift gate): `lunora-deploy`
60
60
  - Slow queries, scans, write conflicts: `lunora-performance-audit`
61
+ - Launch readiness, surprise Cloudflare bills, public routes reaching paid products: `lunora-cloudflare-audit`
61
62
 
62
63
  ### Reviewing existing `lunora/` code
63
64
 
@@ -0,0 +1,163 @@
1
+ ---
2
+ name: lunora-cloudflare-audit
3
+ description: Audits a Lunora app for Cloudflare cost and reliability risk before launch or after a surprise bill. Chains the read-only `lunora doctor`, `lunora advisor` and `lunora insights` signals, then traces what they cannot see — retry and fan-out loops, cron cadence, public routes that reach a metered product (Workers AI, Browser, Containers, Vectorize, R2, D1), preview environments with paid bindings, and missing usage alerts (`lunora cloudflare alerts`). Reports each finding with the billing meter it moves, the smallest fix, and how to verify. Use when the user asks to "audit before launch", "what could blow up our Cloudflare bill", "why did the bill spike", "is this safe to put on the public internet", or "review our cron / queue / AI / workflow cost". For a slow query or OCC conflict use `lunora-performance-audit`; for a failing deploy use `lunora-deploy`.
4
+ ---
5
+
6
+ # Lunora Cloudflare Audit
7
+
8
+ Diagnose from evidence: name the mechanism that spends money or breaks, point at
9
+ the line, and prescribe the smallest fix. Lunora already ships three scanners,
10
+ so this skill is mostly the order to run them in and the judgment they lack.
11
+
12
+ Recurring work (crons, queue consumers, workflows) has no public entry point, so
13
+ check it before concluding costs are modest. If nothing metered is reachable
14
+ from outside, traffic is modest and nothing recurs, say so and stop. Do not
15
+ manufacture findings to fill a report.
16
+
17
+ ## Ground rules
18
+
19
+ - **Evidence first.** Every finding needs a file and line (or command output)
20
+ and a concrete Cloudflare mechanism. "Could be expensive" is not a finding.
21
+ - **Prices and limits come from current docs, never memory.** Name the meter
22
+ (DO requests, DO duration, SQLite rows read, D1 rows written, R2 Class A ops,
23
+ Workers AI neurons, Browser duration, Vectorize queried dimensions, …) and
24
+ fetch the current Cloudflare pricing/limits page before quoting a unit, an
25
+ allowance or a cap. If you could not fetch it, say the claim is unverified.
26
+ - **Do not infer account state from the repo.** Alerts, WAF, cache rules, Access
27
+ and billing live in the dashboard. Without an export or screenshot, list that
28
+ scope as _not inspected_.
29
+ - **Confirmed vs suspected.** A scanner hit is a lead. Confirm it against the
30
+ code path before it becomes a finding.
31
+ - **Read-only.** Never run `lunora doctor --fix`, `deploy`, or `cloudflare
32
+ alerts setup` from an audit. Show the change and get explicit approval first.
33
+
34
+ ## Workflow
35
+
36
+ 1. **Scope.** Which wrangler environments (read each `env.<name>` block in
37
+ `wrangler.jsonc` or `wrangler.json`, the two files Lunora reads; the scan
38
+ commands below take no `--env`), and which paths
39
+ are hot: public queries/mutations, HTTP actions, crons, queue consumers,
40
+ workflows.
41
+ 2. **Preflight signal** (read-only; never pass `--fix`):
42
+
43
+ ```bash
44
+ lunora doctor --format json # wrangler bindings, paid products, sampling, DO class budget
45
+ lunora advisor --format json --no-write # static lints, scored per procedure
46
+ ```
47
+
48
+ `lunora doctor` reports `paid-product-bound` (a metered binding is declared:
49
+ Workers AI, Browser, Images), `observability-full-sampling`,
50
+ `r2-lifecycle-unset`, `cpu-limit-missing` and `do-class-budget-*`.
51
+ `lunora advisor` lint ids are listed below.
52
+
53
+ 3. **Runtime signal, only if the user points you at a worker that has served
54
+ traffic.** `lunora insights` (local by default). Production needs `--prod` and
55
+ the user's admin token, plus `--url …` when the checkout has no production
56
+ link; do not go looking for a token.
57
+ 4. **Account signal.** `lunora cloudflare alerts` (status) reads last month's
58
+ usage and which products have a usage alert. Needs Cloudflare credentials;
59
+ ask first. Budget alerts have no API, so they are always _not inspected_.
60
+ 5. **Trace the gaps** in the next section by reading code.
61
+ 6. **Report** in the format at the end.
62
+
63
+ ## What each signal already covers
64
+
65
+ Check this table before reading code by hand. Only the last column is yours.
66
+
67
+ | Risk | Already flagged by | Still read by hand |
68
+ | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
69
+ | Full-table reads re-sent to every subscriber | `unbounded_collect`, `unbounded_collect_on_open_range`, `filter_without_index`, `count_without_index`, `filter_after_index_scan` | Whether the table will actually grow; `.global()` D1 cost |
70
+ | Runaway loops and re-arming schedules | `unbounded_loop`, `self_reschedule_without_progress`, `dispatch_cycle`, `queue_dispatch_cycle` | Loops spanning a Queue → Worker → DO hop the graph cannot see |
71
+ | Sweeps that re-read the same rows | `unbounded_collect_on_open_range` | — |
72
+ | Cron cadence | `cron_every_minute` (sees `cronJobs()` registrations only) | Every-minute schedules the lint cannot see: `createWorker({ crons })` handlers and `backupCron` in the worker entry; what any tick does: rows read, calls made per fire |
73
+ | Queue retries losing messages | `queue_without_dlq` | Retry delay and `maxRetries` against the downstream's own rate limit |
74
+ | Hot shard / wide fan-out | `privileged_fanout_from_public_procedure`; `hot_shard` and `fan_out_breadth` are runtime lints, shown in the Studio Advisors tab against a live worker, not by `lunora advisor` | Shard traffic on a live worker (Studio Advisors); singleton shard keys (`"global"`, an env var) on `.shardBy()` |
75
+ | Unbounded AI use | `ai_unbounded_generation_public`, `ai_run_without_logging` | Output and usage bounds on every raw `ctx.ai.run()` (`ai_raw_run_escape_hatch` only checks where the model id comes from, so a fixed-model call escapes it); a `ctx.ai` call inside a retry or `Promise.all` over user-sized input |
76
+ | Browser / Containers left running | `browser_user_url_without_allowlist`, `container_long_sleep_after`, `container_oversized_instance` (the container lints read configuration, not lifecycle) | A browser session opened without a close path in an `action`; named container starts that are never stopped or destroyed once their work finishes, within the configured idle window |
77
+ | Public mutations with no throttle | `public_mutation_without_ratelimit`, `ratelimit_default_memory_store`, `ratelimit_key_spoofable_or_global` | Public _reads_ that reach a metered product |
78
+ | Upload size and type | `storage_upload_without_max_size`, `storage_upload_without_content_type_allowlist` | Egress and lifecycle on the bucket (`r2-lifecycle-unset`) |
79
+ | Secrets in the repo | `hardcoded_secret`, `plaintext_secret_in_wrangler_vars` | — |
80
+ | Log volume | `observability-full-sampling` (doctor) | Retention and plan |
81
+ | Handled by construction | Codegen reconciles bindings, DO migrations and the compatibility date; `ShardDO` uses the WebSocket Hibernation API | Nothing, unless `wrangler.jsonc` was hand-edited |
82
+
83
+ ## Gaps to trace by hand
84
+
85
+ For each, find the call site, then count the billed units per user action.
86
+
87
+ - **Fan-out without a cap.** `Promise.all(items.map(...))` or a loop that calls
88
+ `ctx.db`, `ctx.runMutation`, `ctx.scheduler.runAfter`, a queue send
89
+ (`ctx.queues.<name>.send` / `sendBatch`) or `ctx.ai` once per element of user-sized input. Cap the batch or hand the list
90
+ to a Queue/workpool with a concurrency limit.
91
+ - **Retry storms.** A retry (hand-rolled, queue `maxRetries`, workflow step
92
+ retry) around a call that is itself billed or rate-limited, with no backoff or
93
+ circuit breaker. A workflow step that retries a paid `ctx.ai` call bills each
94
+ attempt.
95
+ - **Webhooks with side effects and no dedupe.** An `httpAction` for a provider
96
+ webhook that writes or sends mail without keying on the event id. Providers
97
+ redeliver. (Payment webhooks have their own lints; this is every other
98
+ provider.)
99
+ - **Preview / staging with paid bindings.** An `env.<name>` block that binds
100
+ Workers AI, Browser, Containers or the production R2 bucket and is reachable
101
+ without auth.
102
+ - **Enumeration on hot paths.** Storage enumeration through
103
+ `ctx.db.system.query("_storage")` or a Vectorize query (`ctx.vectors.query`)
104
+ inside a `query` that every subscriber re-runs. (`ctx.storage.list` is
105
+ action-only.)
106
+ - **Third-party origins.** An action that fetches a public serverless URL on
107
+ every request instead of caching, queueing, or using a service binding.
108
+
109
+ ## Public-path cost gate
110
+
111
+ Run this whenever an anonymous caller (an unauthenticated `query`, a public
112
+ `httpAction`, a crawlable GET) can reach a metered product. The caller does not
113
+ have to be a crawler: any script can repeat the request.
114
+
115
+ 1. List the public entry points (`lunora advisor` flags `http_action_missing_auth_guard`;
116
+ read `lunora/http.ts` and the public builders). Also read the custom routes
117
+ registered on the app in the worker entry (`defineApp()`): they run outside
118
+ the HTTP-action router, so no lint covers their use of paid `env` bindings.
119
+ 2. For each, trace arbitrary, missing and malformed inputs. Where is the first
120
+ rejection, and does it happen **before** the metered call?
121
+ 3. State the per-hit unit (one DO request? N rows read? one AI inference? one
122
+ browser session?) and the exposure: `anonymous requests × units per request`.
123
+ Only count a cache if it sits in front of the work and its misses, expiry and
124
+ concurrent fills are also bounded.
125
+ 4. Verdict: **BLOCK** when evidence shows repeated anonymous requests run
126
+ corpus-scale or high-unit work before validation; **CONDITIONAL** when the
127
+ shape may exist but a query plan, measurement or config is missing (never
128
+ assume safe); **PASS** only when the expensive path is prevented before it
129
+ executes.
130
+
131
+ A rate limit or alert that fires _after_ the metered work started is defense in
132
+ depth, not a fix. The fix is to reject early, bound the work, or move it behind
133
+ auth.
134
+
135
+ ## Report
136
+
137
+ Lead with scope, then one card per finding, most severe first.
138
+
139
+ ```markdown
140
+ ## Lunora Cloudflare audit
141
+
142
+ Scope inspected: <paths, env, commands run>
143
+ Scope not inspected: <dashboard / alerts / billing / prod>
144
+ Docs refreshed: <URLs fetched, or "none — claims marked unverified">
145
+
146
+ ### <critical|high|medium|low> — <title>
147
+
148
+ - Evidence: <file:line or command output; lint id / doctor code if one fired>
149
+ - Mechanism: <the billed meter or failure, in Cloudflare terms>
150
+ - Fix: <smallest change; the better primitive if the primitive is wrong>
151
+ - Cost / trade-off: <meter moved, effort, downside>
152
+ - Verify: <command or check that proves it>
153
+ - Confidence: <high|medium|low>
154
+ ```
155
+
156
+ Severity: **critical** = exposed secret, public data exposure or an unbounded
157
+ billed loop; **high** = anonymous traffic reaches high-unit work, or a retry
158
+ path bills each attempt; **medium** = a cost or reliability gap that grows with
159
+ usage; **low** = hygiene. If nothing is supported, write `No confirmed findings.`
160
+ and list what was not inspected.
161
+
162
+ End with the proposed next steps, and offer `lunora cloudflare alerts setup
163
+ --email <address> --dry-run` if no usage alert exists. Do not run it unasked.
@@ -28,8 +28,9 @@ shards. Reach past it only for the rest.
28
28
  | A dedicated socket at your own path | `defineWebSocket` + `client.connectWebSocket(path)` |
29
29
  | Who is online | `usePresence` |
30
30
 
31
- Live topics are the untyped one: the topic is a string and the payload is
32
- `unknown`. Full guide: the "Choosing a data channel" docs page.
31
+ Live topics are typed from their declaration: each topic's key and payload are
32
+ validators on the stream, and codegen emits `_generated/live.ts` with a typed
33
+ reference per topic. Full guide: the "Choosing a data channel" docs page.
33
34
 
34
35
  ## When Not to Use
35
36
 
@@ -194,9 +195,14 @@ TanStack-style alternative.
194
195
  ## Live Topics
195
196
 
196
197
  For an event with no query behind it ("a comment was posted"), the server
197
- publishes to a named topic and the browser follows topics over one shared SSE
198
- connection (shared when the connection options match). Declare the stream once, mount its two handlers on the HTTP router,
199
- and follow topics with the adapter's hook (all return `{ lastEvent, error }`):
198
+ publishes to a declared topic and the browser follows topics over one shared SSE
199
+ connection (shared when the connection options match). Declare the stream and its
200
+ topics once (`defineLiveStream({ id, topics: { post: { key: { postId: v.id("posts") },
201
+ payload: v.object({...}) } }, onSubscribe })`), mount its two handlers on the HTTP
202
+ router, and follow topics with the adapter's hook (all return `{ lastEvent, error }`).
203
+ `onSubscribe` receives the parsed `{ ctx, topic, key, raw }`; do not split a string.
204
+ A subscribe for an undeclared name, wrong arity or a non-canonical spelling is denied
205
+ before `onSubscribe` runs. Codegen writes `_generated/live.ts`:
200
206
 
201
207
  | Package | Primitive |
202
208
  | ---------------------- | ------------------------- |
@@ -207,14 +213,24 @@ and follow topics with the adapter's hook (all return `{ lastEvent, error }`):
207
213
  | `@lunora/react-native` | re-exports `useLiveTopic` |
208
214
 
209
215
  ```tsx
210
- const { lastEvent, error } = useLiveTopic({ url: "/live/posts/connect", topic: `post:${postId}`, onEvent: (payload) => refresh(payload) });
216
+ import { live } from "../lunora/_generated/live";
217
+
218
+ // the route comes from the client in <LunoraProvider>; lastEvent / onEvent are typed by the payload
219
+ const { lastEvent, error } = useLiveTopic(live.posts.post({ postId }), { onEvent: (payload) => refresh(payload) });
211
220
  ```
212
221
 
222
+ Svelte's `liveTopic` and Angular's `liveTopic` read the client when called: call
223
+ them during component init (Angular: or pass `client` and `destroyRef`). Pass
224
+ `shardKey` in the options to follow a `.shardBy` shard.
225
+
213
226
  - Several hooks with the same URL, credentials, headers, `retryDelay`, `maxDelayMs` and
214
227
  `sessionKey` share a single connection; the last to unmount closes it. On a
215
228
  cookie-authenticated stream pass `sessionKey` (the user id), or the connection
216
229
  outlives a sign-out and keeps the previous user's topics.
217
- - Publish from server code with `ctx.live.publish(topic, payload, { stream })`.
230
+ - Publish from server code with `ctx.live.publish(live.posts.post({ postId }), payload)`:
231
+ the ref names the stream, the payload is typed, and the shard refuses a payload the
232
+ topic's validator rejects (`BAD_REQUEST`). Topic names and key fields are plain
233
+ identifiers, key validators are string-valued, and a topic named `room` is refused.
218
234
  `ctx.live` exists only on actions, HTTP actions, queue consumers and scheduled
219
235
  actions, NOT on a query or mutation. It is always present on an HTTP action
220
236
  but typed optional on an `action` and a queue consumer, so write