void 0.10.7 → 0.10.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/{auth-cmd-DlgNwByu.mjs → auth-cmd-Cw1edJdZ.mjs} +2 -2
  2. package/dist/{build-cmd-Br8qL0rA.mjs → build-cmd-C6kNDJBv.mjs} +1 -1
  3. package/dist/{cache-wH-mP8UE.mjs → cache-DIUSnIjJ.mjs} +1 -1
  4. package/dist/{cancel-deploy-BEBOEgtu.mjs → cancel-deploy-BosnzEHC.mjs} +1 -1
  5. package/dist/cli/cli.mjs +77 -28
  6. package/dist/{client-Cj96iiBH.mjs → client-C9FG6Vzc.mjs} +8 -0
  7. package/dist/{config-1twldYCW.mjs → config-BcAeIPe3.mjs} +6 -0
  8. package/dist/{create-project-DD9n8Ho-.mjs → create-project-CjYX23M_.mjs} +1 -1
  9. package/dist/{db-DjKE2-A-.mjs → db-BvKV34IK.mjs} +4 -4
  10. package/dist/{delete-D2kr3Kmk.mjs → delete-CThKLXfi.mjs} +1 -1
  11. package/dist/{deploy-C4PbkFyE.mjs → deploy-B02JyKP9.mjs} +59 -16
  12. package/dist/{domain-_luIsM_B.mjs → domain-DMrBabBQ.mjs} +1 -1
  13. package/dist/{env-CO5XAS9t.mjs → env-BfG7O71F.mjs} +1 -1
  14. package/dist/env-raw-Cx8ElDdj.mjs +58 -0
  15. package/dist/{gen-Cf79J4aw.mjs → gen-Bee261SV.mjs} +1 -1
  16. package/dist/{github-cmd-DnqxyOsb.mjs → github-cmd-DfeFEasr.mjs} +178 -45
  17. package/dist/{headers-BwvFGhkx.mjs → headers-CTAjX-UO.mjs} +1 -1
  18. package/dist/index.d.mts +13 -1
  19. package/dist/index.mjs +70 -31
  20. package/dist/{init-KirOzVDs.mjs → init-xfwW7cLp.mjs} +4 -4
  21. package/dist/{link-CUmiosyb.mjs → link-Co136MEs.mjs} +2 -2
  22. package/dist/{list-3GEw7b6m.mjs → list-BKDjA3M5.mjs} +1 -1
  23. package/dist/{login-DJReaT_Q.mjs → login-CrOTnR_6.mjs} +1 -1
  24. package/dist/{logs-D-rQ56Lq.mjs → logs-DlAg25ww.mjs} +1 -1
  25. package/dist/{mcp-D7yc0dXY.mjs → mcp-BNjMGMB0.mjs} +1 -1
  26. package/dist/{node-yFFk626c.mjs → node-CnE-SZCk.mjs} +1 -1
  27. package/dist/pages/index.mjs +2 -2
  28. package/dist/{prepare-C_cVurhP.mjs → prepare-BRyC4TCC.mjs} +1 -1
  29. package/dist/{project-cmd-DnU7u9QF.mjs → project-cmd-B2bcKefD.mjs} +8 -8
  30. package/dist/{rollback-Yh7bCKob.mjs → rollback-9dfmB-YZ.mjs} +1 -1
  31. package/dist/runtime/ai.mjs +113 -15
  32. package/dist/runtime/env-public.mjs +9 -4
  33. package/dist/runtime/env.d.mts +14 -1
  34. package/dist/runtime/env.mjs +24 -10
  35. package/dist/runtime/isr-cache.d.mts +207 -0
  36. package/dist/runtime/isr-cache.mjs +523 -0
  37. package/dist/runtime/isr.d.mts +4 -3
  38. package/dist/runtime/isr.mjs +44 -5
  39. package/dist/runtime/live.mjs +1 -1
  40. package/dist/runtime/sandbox.mjs +1 -1
  41. package/dist/{secret-u7FRvg8d.mjs → secret-DeMnMcV0.mjs} +1 -1
  42. package/package.json +3 -2
  43. package/schema.json +11 -0
  44. package/skills/void/docs/integrations/cloudflare.md +107 -0
  45. package/skills/void/docs/node_modules/void/{AGENTS.md → CLAUDE.md} +14 -0
  46. package/skills/void/docs/reference/cli.md +29 -2
  47. package/dist/env-raw-CoS20LHP.mjs +0 -32
@@ -285,3 +285,110 @@ That's it. The Cloudflare Vite plugin produces a complete build output with a me
285
285
  ### Local development
286
286
 
287
287
  `pnpm dev` continues to work as before -- Miniflare creates local instances of all bindings regardless of the IDs in your `wrangler.jsonc`. Your real resource IDs are only used when you run `wrangler deploy`.
288
+
289
+ ### AI (self-host)
290
+
291
+ `void/ai` works on your own Cloudflare account, along two paths:
292
+
293
+ - **Workers AI** (`ai.run`, `ai.stream`, `ai.image`) works out of the box. When your app imports `void/ai`, `vite build` infers that you need AI and adds a Workers AI binding (`env.AI`) to the generated `wrangler.json` automatically -- you do **not** add it to `wrangler.jsonc`.
294
+ - **Provider models** (`ai.provider("openai").fetch(...)`) route through _your own_ Cloudflare AI Gateway. Set its id in `void.json` and add the provider's API key as a Worker secret.
295
+
296
+ ```jsonc
297
+ // void.json
298
+ {
299
+ "ai": {
300
+ "gateway": "my-gateway", // an AI Gateway in YOUR Cloudflare account
301
+ },
302
+ }
303
+ ```
304
+
305
+ ```bash
306
+ # provider API key, stored as a Worker secret (never committed)
307
+ wrangler secret put OPENAI_API_KEY
308
+ ```
309
+
310
+ ```ts
311
+ // routes/chat.ts
312
+ import { defineHandler } from 'void';
313
+ import { ai } from 'void/ai';
314
+
315
+ export const POST = defineHandler(async (c) => {
316
+ // Workers AI -- uses env.AI directly
317
+ const summary = await ai.run('@cf/meta/llama-3.1-8b-instruct', {
318
+ prompt: 'Summarize the changelog.',
319
+ });
320
+
321
+ // Provider model -- routes through your "my-gateway" AI Gateway,
322
+ // authed with the OPENAI_API_KEY secret above
323
+ const res = await ai.provider('openai').fetch('chat/completions', {
324
+ method: 'POST',
325
+ body: JSON.stringify({ model: 'gpt-4o-mini', messages: [] }),
326
+ });
327
+
328
+ return c.json({ summary, provider: await res.json() });
329
+ });
330
+ ```
331
+
332
+ `ai.gateway` is required for `ai.provider().fetch()` -- without it, that call returns a `501`. No AI traffic or provider secret passes through Void's shared proxy: requests go directly to _your own_ Cloudflare AI Gateway, authenticated with provider secrets from _your_ Worker's environment. (The request and that secret are of course still sent onward to the AI Gateway and the upstream provider you call.)
333
+
334
+ Notes:
335
+
336
+ - **No cross-tenant usage metering.** On the Void platform, AI calls are metered and billed through a shared proxy. Self-hosted, there is no metering -- you get your own [Cloudflare AI Gateway analytics](https://developers.cloudflare.com/ai-gateway/) instead.
337
+ - **The runtime reads the `env.AI` binding by name** -- a custom-named root AI binding is not supported.
338
+ - **`void dev` has no local Workers AI emulation.** In development, AI still routes through Void, so `void dev` AI requires `void auth login` even when you deploy self-hosted.
339
+
340
+ ### ISR (self-host)
341
+
342
+ [Revalidation (ISR)](../guide/edge/revalidation.md) works self-hosted, but the cache KV is **not** auto-injected (a KV binding needs a real namespace id). Create a namespace and bind it as `ISR_CACHE`:
343
+
344
+ ```bash
345
+ wrangler kv namespace create ISR_CACHE
346
+ ```
347
+
348
+ ```jsonc
349
+ // wrangler.jsonc
350
+ {
351
+ "kv_namespaces": [
352
+ {
353
+ "binding": "ISR_CACHE",
354
+ "id": "<your-namespace-id>",
355
+ },
356
+ ],
357
+ }
358
+ ```
359
+
360
+ Configure revalidation exactly as on the platform -- globally or per-path in `void.json`:
361
+
362
+ ```jsonc
363
+ // void.json
364
+ {
365
+ "routing": {
366
+ "revalidate": { "/blog/*": 3600, "*": 60 },
367
+ },
368
+ }
369
+ ```
370
+
371
+ ...or per page with an exported `revalidate` literal in a `.server.ts` companion (Pages mode):
372
+
373
+ ```ts
374
+ // pages/blog/[slug].server.ts
375
+ export const revalidate = 3600; // seconds
376
+ ```
377
+
378
+ On-demand purges work through `revalidate()`:
379
+
380
+ ```ts
381
+ import { revalidate } from 'void/isr';
382
+
383
+ await revalidate({ paths: ['/blog/hello'] });
384
+ // or purge every ISR page:
385
+ await revalidate({ all: true });
386
+ ```
387
+
388
+ Self-hosted, `revalidate()` purges this worker's own KV entries (global, authoritative) plus the current colo's edge cache.
389
+
390
+ Limits (the single-worker cache ladder can't do everything the platform's dispatch layer does):
391
+
392
+ - **No fleet-wide / cross-colo edge purge.** `revalidate()` clears KV globally and the _local_ colo's edge cache; other colos keep serving their edge copy until it expires (bounded by the response's `s-maxage`), then re-render on the next edge miss.
393
+ - **The pages-protocol JSON variant is not served from a cold colo's KV.** A colo that hasn't rendered the HTML yet re-renders the JSON live; the JSON edge cache is warmed only as a side effect of the HTML render path.
394
+ - **The warm cache is dropped on every redeploy.** Each `vite build` bakes a fresh deployment id into the cache keys, so cached HTML never outlives the hashed assets it references -- the first request after a deploy is a cold render.
@@ -89,6 +89,14 @@ src/
89
89
  - **Drizzle-first database**: Schema is defined in `db/schema.ts` (Drizzle table definitions). `void/db` exports a Drizzle D1 instance. `@schema` is an auto-configured alias for `db/schema.ts`. Migrations live in `db/migrations/`. CLI commands: `void db push` (prototype), `void db generate` (create migration files), `void db migrate` (apply locally). `drizzle-orm` and `drizzle-kit` ship with void.
90
90
  - **Journal coherence invariant**: `db/migrations/*.sql` and `db/migrations/meta/_journal.json` must stay in sync — every `.sql` file has a matching journal entry (by `tag`) and vice versa. `assertJournalCoherence` in `src/migrations/validate.ts` is the gate, called from `deploy`, `db status`, `db migrate`, and `db reset`. The two CLI commands that mutate migration state — `void gen migration` and `void db rename-migrations` — are responsible for keeping the journal coherent themselves: the first appends a new entry when writing a file (failing loud if the journal is missing — run `drizzle-kit generate` first), the second rewrites `tag` values alongside the file renames and also renames the matching `meta/<prefix>_snapshot.json` (with byte-level rollback on failure). `gen migration` additionally copies the previous `meta/<idx:04d>_snapshot.json` forward to the new idx so drizzle-kit's drop/rollback tooling keeps working on mixed hand-written + drizzle projects; projects without prior snapshots are skipped silently. Drift will fail loudly before touching any remote DB. Motivated by [void-sdk/void#6](https://github.com/void-sdk/void/issues/6), where an orphan scaffolded `.sql` file half-applied against D1 and wedged the deploy.
91
91
 
92
+ ### Self-Host AI + ISR
93
+
94
+ Two features need extra wiring on a self-hosted `vite build && wrangler deploy` (no Void platform proxy/dispatch in front of the worker). Both are inert on managed `void deploy`.
95
+
96
+ - **`ai.gateway` config + `__VOID_AI_GATEWAY` var** — `void.json`'s `ai.gateway` (validated in `config.ts` as `ai?: { gateway?: string }`; non-empty string, `additionalProperties: false`) names a Cloudflare AI Gateway in the user's own account. `mergeBindings` (`index.ts`): (1) injects `{ ai: { binding: 'AI' } }`, but only when `shouldBindWorkersAi()` is true — the app infers `needsAI`, `command === 'build'`, and neither the resolved root wrangler config nor the result already declares an `ai` binding (so it never clobbers a user's own `ai` binding); (2) emits `vars.__VOID_AI_GATEWAY = config.ai.gateway` whenever `config.ai?.gateway` is set. At runtime, `runtime/ai.ts` `resolveBackend()` selects the direct backend (Priority 3) when `env.AI` is present and no `__VOID_PROXY`/`__VOID_TOKEN` exists. `ai.run/stream/image` hit Workers AI directly; `ai.provider().fetch()` routes through the user's own gateway via `directAi.gateway(__VOID_AI_GATEWAY).getUrl(provider)` with the provider key from the user's own secret (`resolveProviderKey` → `env[PROVIDER_KEY_MAP[provider]]`), returning HTTP 501 if `__VOID_AI_GATEWAY` is unset. No cross-tenant metering on this path. The runtime reads `env.AI` **by name** — a custom-named root AI binding is not honored.
97
+ - **Internal-binding leak check permits exactly `__VOID_AI_GATEWAY`** — the integration guard (`test/integration/kitchen-sink-build.test.ts`) that asserts no `__VOID_*` var reaches `dist/ssr/wrangler.json` explicitly excludes `__VOID_AI_GATEWAY`, because it is a legitimate, non-credential runtime var (unlike the credential keys in `INTERNAL_ENV_KEYS`, which are stripped).
98
+ - **`ISR_CACHE` self-host requirement** — self-host ISR (`runtime/isr-cache.ts`, `runtime/isr.ts`) reads `env.ISR_CACHE` (a `KVNamespace`) + `env.__VOID_ISR_DEPLOYMENT_ID`. Unlike D1/KV/R2, `ISR_CACHE` is **not** auto-injected (a KV binding needs a real namespace id), so the user must create the namespace and declare it as `ISR_CACHE` in their root wrangler config. `routing.revalidate` (void.json) and per-page `export const revalidate` are merged by `collectRevalidate` (`isr-config.ts`) and baked as `__VOID_ISR_REVALIDATE`. On-demand `revalidate()` on self-host purges the worker's own KV (global) plus the local-colo `caches.default` edge variants only.
99
+
92
100
  ### Package Exports
93
101
 
94
102
  | Subpath | What |
@@ -203,3 +211,9 @@ Agent instructions use versioned markers (`<!--injected-by-void-v0.0.1-->` / `<!
203
211
  | `test/unit/plugin-node-target.test.ts` | Node target plugin — binding guard, plugin creation (5 tests) |
204
212
  | `test/integration/node-target.test.ts` | Node app playground — dev server + production build (5 tests) |
205
213
  | `test/integration/queue-playground.test.ts` | Queue send → consumer → KV round-trip (1 test) |
214
+
215
+ ## Pitfalls / Things That Broke
216
+
217
+ - **`__VOID_ISR_DEPLOYMENT_ID` must be a compile-time literal.** `router/compile.ts` stringifies `crypto.randomUUID()` once at build time (`export const __VOID_ISR_DEPLOYMENT_ID = "..."`, exactly like `__VOID_BUILD_KEY`) so every isolate of a deployment shares one id. A bare runtime `crypto.randomUUID()` per-isolate in the emitted source would mint a fresh id per isolate, so the edge-cache key (`handleIsr`'s `deploymentId` option) and the KV key (`env.__VOID_ISR_DEPLOYMENT_ID`, also used by the `revalidate()` purge path) would disagree — breaking cache reads and on-demand purge.
218
+ - **Self-host ISR requires a user-declared `ISR_CACHE` KV.** There is no `id: 'local'` injection for it: a placeholder id is fine for miniflare-backed dev but breaks a real `wrangler deploy`, which rejects an unknown namespace id. The user creates the namespace and binds it as `ISR_CACHE`; `readIsrCache`/`writeIsrCache` fail open to a live render when it is absent.
219
+ - **Workers AI binding injection is `command === 'build'`-gated.** `shouldBindWorkersAi()` only injects `{ ai: { binding: 'AI' } }` on build, because dev/miniflare has no local Workers AI emulation — `void dev` AI still routes through the Void proxy (P2 `__VOID_TOKEN` path) and needs `void auth login`.
@@ -609,6 +609,8 @@ void github join
609
609
 
610
610
  Join the GitHub App installations your organization already has. If a teammate installed the Void GitHub App on a shared GitHub organization, run `void github join` to gain access to those installations without re-installing. Void opens your browser to authorize (a localhost + PKCE handshake, the same mechanics as `void github link`), confirms with GitHub which installations you can manage, and records your membership. Afterwards `void github installations` lists them and `void github connect` can connect your own projects to their repositories.
611
611
 
612
+ In an interactive terminal you rarely need to run this yourself — `void github connect` runs the same join automatically when no active installations are linked to your account. Running `void github join` yourself matters mainly for non-interactive use (without a TTY, `void github connect` never opens a browser), or to link installations ahead of time.
613
+
612
614
  Requires an authenticated CLI (`void auth login` first) and organization-installation sharing enabled on your Void instance; when it is not enabled the command fails closed with a clear message. You can only join installations your GitHub authorization actually returns — you cannot name or join one you cannot access on GitHub.
613
615
 
614
616
  ### `void github connect`
@@ -619,6 +621,8 @@ void github connect [project] [options]
619
621
 
620
622
  Connect a GitHub repository to a Void project for automatic deploys. On every push to the configured branch, Void builds and deploys your project automatically.
621
623
 
624
+ Interactively (TTY), if your account has no active installations linked, `void github connect` first runs the same browser authorize as `void github join` automatically — if a teammate already installed the Void GitHub App on your organization, you join it on the spot and the connect continues. Only when no shared installation is found does it ask you to run `void github install`. The same recovery runs when `--installation <id>` names an installation your account is not linked to yet. Without a TTY, connect never opens a browser: it fails closed and tells you to run `void github install`, or `void github join` if your organization already installed the App.
625
+
622
626
  **Options**
623
627
 
624
628
  | Flag | Description |
@@ -636,7 +640,7 @@ Interactively (TTY), after resolving the repository and branch, `void github con
636
640
 
637
641
  **Non-interactive use (CI)**
638
642
 
639
- When stdin is not a TTY, `void github connect` never prompts — it fails closed and names any flag it needs. `--branch` is always required. `--project` must be resolvable (positional / `--project` / `VOID_PROJECT` / linked `.void/project.json`). `--installation` is required only when your account has more than one installation; otherwise the sole installation is used. `--repo` is required when the installation grants access to all repos or to more than one selected repo; when it grants exactly one repo that repo is used automatically. Use `void github installations` to discover the `installation_id`.
643
+ When stdin is not a TTY, `void github connect` never prompts — it fails closed and names any flag it needs. `--branch` is always required. `--project` must be resolvable (positional / `--project` / `VOID_PROJECT` / linked `.void/project.json`). `--installation` is required only when your account has more than one installation; otherwise the sole installation is used. `--repo` is required when the installation grants access to all repos or to more than one selected repo; when it grants exactly one repo that repo is used automatically. `--repo` is also required when the installation does not expose a repository list to your account (shared org installations hide it). Use `void github installations` to discover the `installation_id`.
640
644
 
641
645
  ```
642
646
  void github connect my-app \
@@ -649,7 +653,7 @@ void github connect my-app \
649
653
 
650
654
  **Connecting as an organization member**
651
655
 
652
- If you are a member of a GitHub organization but not the person who installed the App, `void github connect` first confirms that you personally have access to the specific repository. When it detects this, it opens your browser once to authorize access to that repo on GitHub (a localhost + PKCE handshake), then completes the connection automatically — no extra flags. Run `void github join` first so the installation appears in `void github installations`. You can only connect repositories you can access on GitHub; one you cannot see is refused with a clear message.
656
+ If you are a member of a GitHub organization but not the person who installed the App, `void github connect` first confirms that you personally have access to the specific repository. Interactively (TTY), when it detects this it opens your browser once to authorize access to that repo on GitHub (a localhost + PKCE handshake), then completes the connection automatically — no extra flags. Without a TTY, this per-repo authorization never opens a browser: connect fails closed with an error explaining that the installation requires per-repo authorization, which needs an interactive browser sign-in, and telling you to run `void github connect` locally to authorize, then retry. Interactively, connect joins the shared installation automatically when your account has no active installations linked, so running `void github join` first is optional — useful mainly to see the installation in `void github installations` beforehand. You can only connect repositories you can access on GitHub; one you cannot see is refused with a clear message.
653
657
 
654
658
  ### `void github update`
655
659
 
@@ -696,6 +700,29 @@ void github status my-app
696
700
 
697
701
  **Project resolution** follows the same order as deploy: positional / `--project`, `VOID_PROJECT`, linked project (`.void/project.json`).
698
702
 
703
+ ### `void github disconnect`
704
+
705
+ ```
706
+ void github disconnect [project]
707
+ ```
708
+
709
+ Disconnect a project from its GitHub repository, stopping automatic deploys. Any in-flight builds for the project are cancelled (their deploy tokens are revoked) before the connection is removed. If the project has no connection, it reports that and exits successfully. To point a project at a different repository, disconnect first, then run `void github connect`.
710
+
711
+ You are asked to confirm before anything is removed. Pass `--yes` to skip the prompt; `--yes` is **required** in a non-interactive shell (CI), where there is no prompt to answer.
712
+
713
+ **Options**
714
+
715
+ | Flag | Description |
716
+ | ------------------ | ----------------------------------------------------------------- |
717
+ | `--project <name>` | Project name (alias for the positional argument) |
718
+ | `--yes` | Skip the confirmation prompt (required in non-interactive shells) |
719
+
720
+ ```
721
+ void github disconnect my-app --yes
722
+ ```
723
+
724
+ **Project resolution** follows the same order as deploy: positional / `--project`, `VOID_PROJECT`, linked project (`.void/project.json`).
725
+
699
726
  ## Build
700
727
 
701
728
  Inspect Deploy-on-GitHub builds.
@@ -1,32 +0,0 @@
1
- import { AsyncLocalStorage } from "node:async_hooks";
2
- //#region src/runtime/env-raw.ts
3
- /**
4
- * Raw runtime env resolution — framework-internal only.
5
- *
6
- * This module is NOT exported in package.json, so it cannot be imported
7
- * via "void/..." subpaths. Only framework modules using relative imports
8
- * (isr.ts, ai.ts, env.ts) can access it.
9
- */
10
- /** Prefixes for platform-internal bindings that must be hidden from user code. */
11
- const INTERNAL_BINDING_PREFIXES = ["__VOID_", "__PROJECT_"];
12
- const envContext = new AsyncLocalStorage();
13
- let cloudflareEnv;
14
- if (typeof navigator !== "undefined" && navigator.userAgent === "Cloudflare-Workers") import("cloudflare:workers").then((mod) => {
15
- cloudflareEnv = asEnv(mod.env) ?? void 0;
16
- }).catch(() => {});
17
- function asEnv(value) {
18
- if (typeof value !== "object" || value === null) return null;
19
- if ("then" in value && typeof value.then === "function") return null;
20
- return value;
21
- }
22
- function getNuxtEnv() {
23
- return asEnv(globalThis.__env__);
24
- }
25
- /** Resolve raw env from AsyncLocalStorage, Nuxt globals, or CF module env. */
26
- function getRawRuntimeEnv() {
27
- return envContext.getStore() ?? getNuxtEnv() ?? cloudflareEnv ?? (() => {
28
- throw new Error("env: Cloudflare env is unavailable. Use void runtime bindings inside Nuxt/SvelteKit/Analog server request handlers or Worker handlers.");
29
- })();
30
- }
31
- //#endregion
32
- export { getRawRuntimeEnv as i, asEnv as n, envContext as r, INTERNAL_BINDING_PREFIXES as t };