vite-plugin-io1 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bitmot
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # vite-plugin-io1
2
+
3
+ Publish your Vite dev server to the public internet over a [bitmot io1](https://bitmot.com) relay: a
4
+ stable, optionally email-gated HTTPS URL that hot-reloads as you edit. The plugin manages the `bitmot`
5
+ CLI for you.
6
+
7
+ While `vite dev` (or `vite preview`) runs, the plugin starts a `bitmot io1 relay` pointed at the
8
+ server's real bound port and tears it down when the server stops. The public URL appears in the
9
+ startup output.
10
+
11
+ ## Why
12
+
13
+ - **Real HTTPS, real cert** so secure-context APIs (service workers, camera, WebCrypto) just work.
14
+ - **Working HMR through the tunnel** -- the shared URL is your live dev server, not a static copy.
15
+ - **Email-gated sharing** via `allowedEmails` makes a preview shared-but-not-public.
16
+ - **No `server.allowedHosts` fuss** -- the relay presents the upstream's own Host, so Vite's host
17
+ check never trips.
18
+
19
+ ## Install
20
+
21
+ ```sh
22
+ npm i -D vite-plugin-io1
23
+ ```
24
+
25
+ You do **not** need to install the `bitmot` CLI first: the plugin downloads the current build for
26
+ your platform on demand (content-addressed and checksum-verified) into `~/.cache/bitmot`. If you
27
+ already have `bitmot` on your PATH, that copy is used instead.
28
+
29
+ One-time per machine, authorize the CLI so the relay can claim hosts on your account:
30
+
31
+ ```sh
32
+ bitmot io1 authorize
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ ```ts
38
+ // vite.config.ts
39
+ import { defineConfig } from 'vite'
40
+ import { io1 } from 'vite-plugin-io1'
41
+
42
+ export default defineConfig({
43
+ plugins: [
44
+ io1({
45
+ host: process.env.IO1_DEV_HOST, // e.g. "🔥.io1.io" or "demo" (-> demo.io1.io)
46
+ }),
47
+ ],
48
+ })
49
+ ```
50
+
51
+ The plugin stays dormant unless a host is configured (via `host` or `$IO1_DEV_HOST`): publishing
52
+ every dev server by default would be a bit dicey, and a hardcoded shared host collides between
53
+ teammates. Point `IO1_DEV_HOST` at a per-developer host in your shell profile, or set `host`
54
+ explicitly for a shared demo host.
55
+
56
+ ## Options
57
+
58
+ | Option | Type | Default | Description |
59
+ | --- | --- | --- | --- |
60
+ | `host` | `string \| string[]` | `$IO1_DEV_HOST` | Host(s) to claim. A bare name is completed to `.io1.io`. |
61
+ | `allowedEmails` | `string[]` | none | Restrict visitors to these verified emails. |
62
+ | `account` | `string` | inferred | Account claim, when your identity has several memberships. |
63
+ | `gateway` | `string` | production | Gateway WebSocket URL override (for a local/dev gateway). |
64
+ | `bitmotPath` | `string` | resolved | Explicit CLI path; skips PATH lookup and download. |
65
+ | `autoInstall` | `boolean` | `true` | Download the CLI when not found. `false` makes a miss an error. |
66
+ | `baseUrl` | `string` | `https://bitmot.com` | Download origin for the CLI. |
67
+ | `extraArgs` | `string[]` | none | Extra `bitmot io1 relay` flags, inserted before `--to`. |
68
+ | `enabled` | `boolean` | on, unless CI or no host | Master switch. |
69
+
70
+ ## How the CLI stays current
71
+
72
+ The `bitmot` CLI carries a version (`bitmot version`), and the gateway advertises the latest one on
73
+ every relay handshake; run by hand, the CLI nudges you when you are out of date. That nudge is
74
+ deliberately silent when its output is piped or `CI` is set -- that is, whenever this plugin runs it.
75
+ So the plugin keeps the CLI current a different way: auto-download resolves the **currently deployed**
76
+ bytes by content hash, so a plugin-managed CLI is always up to date and the nudge is moot. A CLI you
77
+ installed yourself on your PATH is respected as-is and never replaced.
78
+
79
+ ## Caveats
80
+
81
+ - **macOS and Linux** -- published binaries cover macOS (arm64/x86_64) and Linux (x86_64/aarch64).
82
+ Elsewhere (e.g. native Windows), install `bitmot` yourself (the plugin will use it) or set
83
+ `bitmotPath`.
84
+ - **Middleware mode** (e.g. Nuxt) exposes no `httpServer` for the plugin to hook; not yet supported.
85
+ - The relay is a child process tied to the dev server. A hard crash of the Vite process (SIGKILL) can
86
+ orphan it; a normal stop or Ctrl-C cleans it up.
87
+
88
+ ## Development
89
+
90
+ ```sh
91
+ npm install
92
+ npm test # vitest: arg-building, announcement parsing, download+verify, spawn/kill lifecycle
93
+ npm run build # tsc -> dist
94
+ ```
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Pure argument-building and output-parsing for the bitmot io1 relay. Kept free of I/O so the wire
3
+ * contract with the CLI -- which flags it takes, and how it announces a live host -- is unit-tested
4
+ * in isolation and lives in one place.
5
+ */
6
+ export interface RelayInvocation {
7
+ /** Hosts to claim, one `--host` each (the CLI appends across repeats). At least one. */
8
+ readonly hosts: readonly string[];
9
+ /** Allow-list emails; the CLI takes a single comma-separated `--allowed-emails`, not repeats. */
10
+ readonly allowedEmails?: readonly string[];
11
+ /** Account claim, when the identity has more than one membership. */
12
+ readonly account?: string;
13
+ /** Gateway WebSocket URL; omitted to use the CLI's baked-in production default. */
14
+ readonly gateway?: string;
15
+ /** Host header presented to the gateway; omitted to derive it from the gateway URL. */
16
+ readonly gatewayHost?: string;
17
+ /** Escape hatch for flags this plugin does not model (e.g. `--credential`). Inserted verbatim. */
18
+ readonly extraArgs?: readonly string[];
19
+ /** The local dev server to proxy to, e.g. `http://127.0.0.1:5173`. */
20
+ readonly to: string;
21
+ }
22
+ /**
23
+ * The full `bitmot io1 relay ...` argv (without the leading program name). `--allowed-emails` is
24
+ * comma-joined into a single flag because the CLI parses it as one comma-separated value; emitting
25
+ * repeats would make the native CLI keep only the last. `--to` goes last so the line reads as a claim
26
+ * of hosts proxied to a target.
27
+ */
28
+ export declare function buildRelayArgs(invocation: RelayInvocation): string[];
29
+ /**
30
+ * The public URL from a relay's per-host announcement line, or null for any other line. The relay
31
+ * prints one `<arrow> <url> live` line per claimed host once its link is up (uncolored when its
32
+ * stderr is piped, as it is under this plugin). Tolerant of the arrow glyph and surrounding spaces so
33
+ * both the native and JVM CLIs -- and any cached older build -- parse the same; the trailing `live`
34
+ * badge is what distinguishes it from the pending-requests and update-nudge lines, which also carry
35
+ * URLs but must not be mistaken for a live host.
36
+ */
37
+ export declare function parseLiveUrl(line: string): string | null;
38
+ //# sourceMappingURL=announce.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"announce.d.ts","sourceRoot":"","sources":["../src/announce.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,eAAe;IAC9B,wFAAwF;IACxF,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;IACjC,iGAAiG;IACjG,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC1C,qEAAqE;IACrE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,mFAAmF;IACnF,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,uFAAuF;IACvF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,kGAAkG;IAClG,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACtC,sEAAsE;IACtE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,eAAe,GAAG,MAAM,EAAE,CAWpE;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGxD"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Pure argument-building and output-parsing for the bitmot io1 relay. Kept free of I/O so the wire
3
+ * contract with the CLI -- which flags it takes, and how it announces a live host -- is unit-tested
4
+ * in isolation and lives in one place.
5
+ */
6
+ /**
7
+ * The full `bitmot io1 relay ...` argv (without the leading program name). `--allowed-emails` is
8
+ * comma-joined into a single flag because the CLI parses it as one comma-separated value; emitting
9
+ * repeats would make the native CLI keep only the last. `--to` goes last so the line reads as a claim
10
+ * of hosts proxied to a target.
11
+ */
12
+ export function buildRelayArgs(invocation) {
13
+ const args = ['io1', 'relay'];
14
+ for (const host of invocation.hosts)
15
+ args.push('--host', host);
16
+ const emails = invocation.allowedEmails ?? [];
17
+ if (emails.length > 0)
18
+ args.push('--allowed-emails', emails.join(','));
19
+ if (invocation.account)
20
+ args.push('--account', invocation.account);
21
+ if (invocation.gateway)
22
+ args.push('--gateway', invocation.gateway);
23
+ if (invocation.gatewayHost)
24
+ args.push('--gateway-host', invocation.gatewayHost);
25
+ if (invocation.extraArgs)
26
+ args.push(...invocation.extraArgs);
27
+ args.push('--to', invocation.to);
28
+ return args;
29
+ }
30
+ /**
31
+ * The public URL from a relay's per-host announcement line, or null for any other line. The relay
32
+ * prints one `<arrow> <url> live` line per claimed host once its link is up (uncolored when its
33
+ * stderr is piped, as it is under this plugin). Tolerant of the arrow glyph and surrounding spaces so
34
+ * both the native and JVM CLIs -- and any cached older build -- parse the same; the trailing `live`
35
+ * badge is what distinguishes it from the pending-requests and update-nudge lines, which also carry
36
+ * URLs but must not be mistaken for a live host.
37
+ */
38
+ export function parseLiveUrl(line) {
39
+ const match = /(https?:\/\/\S+)\s+live\s*$/u.exec(line);
40
+ return match ? match[1] : null;
41
+ }
42
+ //# sourceMappingURL=announce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"announce.js","sourceRoot":"","sources":["../src/announce.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAmBH;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,UAA2B;IACxD,MAAM,IAAI,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;IAC7B,KAAK,MAAM,IAAI,IAAI,UAAU,CAAC,KAAK;QAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IAC9D,MAAM,MAAM,GAAG,UAAU,CAAC,aAAa,IAAI,EAAE,CAAA;IAC7C,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;IACtE,IAAI,UAAU,CAAC,OAAO;QAAE,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC,OAAO,CAAC,CAAA;IAClE,IAAI,UAAU,CAAC,OAAO;QAAE,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC,OAAO,CAAC,CAAA;IAClE,IAAI,UAAU,CAAC,WAAW;QAAE,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,UAAU,CAAC,WAAW,CAAC,CAAA;IAC/E,IAAI,UAAU,CAAC,SAAS;QAAE,IAAI,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,SAAS,CAAC,CAAA;IAC5D,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC,EAAE,CAAC,CAAA;IAChC,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,MAAM,KAAK,GAAG,8BAA8B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACvD,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,IAAI,CAAA;AACjC,CAAC"}
@@ -0,0 +1,50 @@
1
+ export interface ResolveOptions {
2
+ /** Explicit path to a bitmot binary; when set it is used as-is (no PATH search, no download). */
3
+ readonly bitmotPath?: string;
4
+ /** Download the CLI when not otherwise found. Default true; false makes a miss a hard error. */
5
+ readonly autoInstall?: boolean;
6
+ /** Download origin; mirrors the installer's BITMOT_BASE_URL. Default https://bitmot.com. */
7
+ readonly baseUrl?: string;
8
+ /** Where to cache downloaded binaries. Default ~/.cache/bitmot (or $XDG_CACHE_HOME/bitmot). */
9
+ readonly cacheDir?: string;
10
+ /** Sink for human-facing progress ("downloading ..."). */
11
+ readonly log?: (message: string) => void;
12
+ }
13
+ export interface ResolvedBinary {
14
+ /** Path to invoke. */
15
+ readonly path: string;
16
+ /** How it was found, for logging. */
17
+ readonly source: 'explicit' | 'path' | 'download' | 'cache';
18
+ /** `bitmot version`'s reported semver (e.g. "0.2.1"), or null if it could not be read. */
19
+ readonly version: string | null;
20
+ }
21
+ /**
22
+ * The published artifact name for a platform/arch, or null when unsupported. Mirrors the server's
23
+ * download whitelist (io/io1/gateway/download/DownloadRoutesImpl.kt) and the installer's `uname`
24
+ * mapping: macOS and Linux, on x86_64 or 64-bit ARM. Node's arch is translated to the server's
25
+ * spelling -- 'x64' -> 'x86_64' on both, but 64-bit ARM is 'arm64' on macOS and 'aarch64' on Linux.
26
+ */
27
+ export declare function artifactFor(platform: NodeJS.Platform, arch: string): string | null;
28
+ /** `bitmot version` -> "0.2.1", or null if the binary is missing or its output is unexpected. */
29
+ export declare function readVersion(binary: string): string | null;
30
+ /**
31
+ * Ensures the current published binary is present in the cache and returns its path. The stable
32
+ * download URL 302-redirects to the content-addressed URL of whatever bytes are deployed now, so the
33
+ * followed response's checksum header names the current version: if a file for that hash is already
34
+ * cached the body is dropped undownloaded, otherwise it is streamed, verified against the advertised
35
+ * checksum, and atomically installed. This is what makes a plugin-managed CLI track production
36
+ * automatically -- so the CLI's own "you're out of date" nudge (suppressed anyway when its stderr is
37
+ * piped, as it is here) does not matter for the auto-managed case.
38
+ */
39
+ export declare function ensureDownloaded(baseUrl: string, artifact: string, cacheDir: string, log?: (message: string) => void): Promise<{
40
+ path: string;
41
+ downloaded: boolean;
42
+ }>;
43
+ /**
44
+ * Resolves a bitmot binary to invoke: an explicit path wins; else a bitmot already on PATH is used
45
+ * (respecting a user- or installer-managed copy); else, when autoInstall is on, the current published
46
+ * binary is downloaded and cached. Throws with an actionable message when nothing is available and
47
+ * downloading is off or the platform has no published artifact.
48
+ */
49
+ export declare function resolveBitmot(options?: ResolveOptions): Promise<ResolvedBinary>;
50
+ //# sourceMappingURL=binary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"binary.d.ts","sourceRoot":"","sources":["../src/binary.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,cAAc;IAC7B,iGAAiG;IACjG,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,gGAAgG;IAChG,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAA;IAC9B,4FAA4F;IAC5F,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,+FAA+F;IAC/F,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,0DAA0D;IAC1D,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CACzC;AAED,MAAM,WAAW,cAAc;IAC7B,sBAAsB;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,qCAAqC;IACrC,QAAQ,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,GAAG,UAAU,GAAG,OAAO,CAAA;IAC3D,0FAA0F;IAC1F,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CAChC;AAID;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAYlF;AAgBD,iGAAiG;AACjG,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAKzD;AASD;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAC9B,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,CAAC,CA6ChD;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,cAAc,CAAC,CAwBzF"}
package/dist/binary.js ADDED
@@ -0,0 +1,137 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { createHash, randomBytes } from 'node:crypto';
3
+ import { constants as FS, createWriteStream } from 'node:fs';
4
+ import { access, chmod, mkdir, rename, rm } from 'node:fs/promises';
5
+ import { homedir } from 'node:os';
6
+ import { join } from 'node:path';
7
+ import { Readable, Transform } from 'node:stream';
8
+ import { pipeline } from 'node:stream/promises';
9
+ const DEFAULT_BASE_URL = 'https://bitmot.com';
10
+ /**
11
+ * The published artifact name for a platform/arch, or null when unsupported. Mirrors the server's
12
+ * download whitelist (io/io1/gateway/download/DownloadRoutesImpl.kt) and the installer's `uname`
13
+ * mapping: macOS and Linux, on x86_64 or 64-bit ARM. Node's arch is translated to the server's
14
+ * spelling -- 'x64' -> 'x86_64' on both, but 64-bit ARM is 'arm64' on macOS and 'aarch64' on Linux.
15
+ */
16
+ export function artifactFor(platform, arch) {
17
+ if (platform === 'darwin') {
18
+ if (arch === 'arm64')
19
+ return 'bitmot-macos-arm64';
20
+ if (arch === 'x64')
21
+ return 'bitmot-macos-x86_64';
22
+ return null;
23
+ }
24
+ if (platform === 'linux') {
25
+ if (arch === 'x64')
26
+ return 'bitmot-linux-x86_64';
27
+ if (arch === 'arm64')
28
+ return 'bitmot-linux-aarch64';
29
+ return null;
30
+ }
31
+ return null;
32
+ }
33
+ function defaultCacheDir() {
34
+ const xdg = process.env.XDG_CACHE_HOME;
35
+ return join(xdg && xdg.length > 0 ? xdg : join(homedir(), '.cache'), 'bitmot');
36
+ }
37
+ async function exists(path) {
38
+ try {
39
+ await access(path, FS.F_OK);
40
+ return true;
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ }
46
+ /** `bitmot version` -> "0.2.1", or null if the binary is missing or its output is unexpected. */
47
+ export function readVersion(binary) {
48
+ const result = spawnSync(binary, ['version'], { encoding: 'utf8' });
49
+ if (result.status !== 0 || typeof result.stdout !== 'string')
50
+ return null;
51
+ const match = /\bbitmot\s+(\d+\.\d+\.\d+)\b/.exec(result.stdout);
52
+ return match ? match[1] : null;
53
+ }
54
+ function onPath() {
55
+ // A bare-name spawn resolves via PATH; a clean `version` exit both locates the binary and proves it
56
+ // runs on this machine (arch, permissions), which a filesystem check alone would not.
57
+ const result = spawnSync('bitmot', ['version'], { stdio: 'ignore' });
58
+ return result.status === 0 && result.error === undefined;
59
+ }
60
+ /**
61
+ * Ensures the current published binary is present in the cache and returns its path. The stable
62
+ * download URL 302-redirects to the content-addressed URL of whatever bytes are deployed now, so the
63
+ * followed response's checksum header names the current version: if a file for that hash is already
64
+ * cached the body is dropped undownloaded, otherwise it is streamed, verified against the advertised
65
+ * checksum, and atomically installed. This is what makes a plugin-managed CLI track production
66
+ * automatically -- so the CLI's own "you're out of date" nudge (suppressed anyway when its stderr is
67
+ * piped, as it is here) does not matter for the auto-managed case.
68
+ */
69
+ export async function ensureDownloaded(baseUrl, artifact, cacheDir, log) {
70
+ const response = await fetch(`${baseUrl}/download/${artifact}`);
71
+ if (!response.ok || !response.body) {
72
+ await response.body?.cancel();
73
+ throw new Error(`download failed: ${baseUrl}/download/${artifact} returned ${response.status}`);
74
+ }
75
+ const advertised = response.headers.get('x-checksum-sha256')?.toLowerCase();
76
+ if (!advertised || !/^[0-9a-f]{64}$/.test(advertised)) {
77
+ await response.body.cancel();
78
+ throw new Error('download failed: server did not advertise a sha256 checksum');
79
+ }
80
+ const dest = join(cacheDir, `bitmot-${advertised.slice(0, 8)}`);
81
+ if (await exists(dest)) {
82
+ await response.body.cancel(); // already have these exact bytes; do not re-download the body
83
+ return { path: dest, downloaded: false };
84
+ }
85
+ await mkdir(cacheDir, { recursive: true });
86
+ const temp = `${dest}.${randomBytes(6).toString('hex')}.tmp`;
87
+ const hash = createHash('sha256');
88
+ const tee = new Transform({
89
+ transform(chunk, _encoding, callback) {
90
+ hash.update(chunk);
91
+ callback(null, chunk);
92
+ },
93
+ });
94
+ log?.(`downloading ${artifact} (${advertised.slice(0, 8)})`);
95
+ try {
96
+ await pipeline(Readable.fromWeb(response.body), tee, createWriteStream(temp, { mode: 0o755 }));
97
+ const actual = hash.digest('hex');
98
+ if (actual !== advertised) {
99
+ throw new Error(`checksum mismatch: downloaded ${actual}, server advertised ${advertised}`);
100
+ }
101
+ await chmod(temp, 0o755);
102
+ await rename(temp, dest);
103
+ }
104
+ catch (error) {
105
+ await rm(temp, { force: true });
106
+ throw error;
107
+ }
108
+ return { path: dest, downloaded: true };
109
+ }
110
+ /**
111
+ * Resolves a bitmot binary to invoke: an explicit path wins; else a bitmot already on PATH is used
112
+ * (respecting a user- or installer-managed copy); else, when autoInstall is on, the current published
113
+ * binary is downloaded and cached. Throws with an actionable message when nothing is available and
114
+ * downloading is off or the platform has no published artifact.
115
+ */
116
+ export async function resolveBitmot(options = {}) {
117
+ if (options.bitmotPath) {
118
+ return { path: options.bitmotPath, source: 'explicit', version: readVersion(options.bitmotPath) };
119
+ }
120
+ if (onPath()) {
121
+ return { path: 'bitmot', source: 'path', version: readVersion('bitmot') };
122
+ }
123
+ if (options.autoInstall === false) {
124
+ throw new Error('bitmot CLI not found on PATH and autoInstall is disabled. ' +
125
+ 'Install it with: curl -fsSL https://bitmot.com/install | sh');
126
+ }
127
+ const artifact = artifactFor(process.platform, process.arch);
128
+ if (!artifact) {
129
+ throw new Error(`bitmot CLI not found and no published binary for ${process.platform}/${process.arch}. ` +
130
+ 'Install it manually: curl -fsSL https://bitmot.com/install | sh');
131
+ }
132
+ const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
133
+ const cacheDir = options.cacheDir ?? defaultCacheDir();
134
+ const { path, downloaded } = await ensureDownloaded(baseUrl, artifact, cacheDir, options.log);
135
+ return { path, source: downloaded ? 'download' : 'cache', version: readVersion(path) };
136
+ }
137
+ //# sourceMappingURL=binary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"binary.js","sourceRoot":"","sources":["../src/binary.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import type { Plugin } from 'vite';
2
+ export type { RelayInvocation } from './announce.js';
3
+ export type { ResolveOptions, ResolvedBinary } from './binary.js';
4
+ export interface Io1PluginOptions {
5
+ /**
6
+ * Host(s) to claim, e.g. "🔥.io1.io" or "demo" (completed to demo.io1.io). Falls back to
7
+ * $IO1_DEV_HOST. With none set the plugin stays dormant: publishing every dev server by default
8
+ * would be a footgun, and a hardcoded shared host collides between teammates.
9
+ */
10
+ host?: string | string[];
11
+ /** Allow-list emails; when set, visitors must verify one of them to reach the host. */
12
+ allowedEmails?: string[];
13
+ /** Account claim, when your identity has more than one membership. */
14
+ account?: string;
15
+ /** Gateway WebSocket URL override; defaults to the CLI's production gateway. */
16
+ gateway?: string;
17
+ /** Explicit path to a bitmot binary; skips PATH lookup and auto-download. */
18
+ bitmotPath?: string;
19
+ /** Auto-download the CLI when not found. Default true. */
20
+ autoInstall?: boolean;
21
+ /** Download origin for the CLI; mirrors the installer's BITMOT_BASE_URL. */
22
+ baseUrl?: string;
23
+ /** Extra `bitmot io1 relay` flags this plugin does not model, inserted verbatim before `--to`. */
24
+ extraArgs?: string[];
25
+ /** Master switch. Default: on, unless running under CI or no host is configured. */
26
+ enabled?: boolean;
27
+ }
28
+ /**
29
+ * Vite plugin: while the dev (or preview) server runs, publish it to the internet over a
30
+ * `bitmot io1 relay` child process, proxying to the server's real bound port. The relay's lifetime is
31
+ * tied to the server -- spawned once it is listening, killed when it closes -- so a shared URL never
32
+ * outlives the session. The CLI is located on PATH or auto-downloaded (content-addressed, so it tracks
33
+ * the deployed build). Reconnects are the relay's own concern; a non-zero exit is a definitive refusal
34
+ * (bad auth, host held by another account) and is surfaced, not retried.
35
+ */
36
+ export declare function io1(options?: Io1PluginOptions): Plugin;
37
+ export default io1;
38
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAgC,MAAM,MAAM,CAAA;AAIhE,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AACpD,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAEjE,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IACxB,uFAAuF;IACvF,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,sEAAsE;IACtE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,0DAA0D;IAC1D,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,4EAA4E;IAC5E,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,kGAAkG;IAClG,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;IACpB,oFAAoF;IACpF,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAUD;;;;;;;GAOG;AACH,wBAAgB,GAAG,CAAC,OAAO,GAAE,gBAAqB,GAAG,MAAM,CA0F1D;AAyBD,eAAe,GAAG,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,127 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { buildRelayArgs, parseLiveUrl } from './announce.js';
3
+ import { resolveBitmot } from './binary.js';
4
+ function normalizeHosts(host) {
5
+ const raw = host ?? process.env.IO1_DEV_HOST;
6
+ if (!raw)
7
+ return [];
8
+ return (Array.isArray(raw) ? raw : [raw]).filter((entry) => entry.length > 0);
9
+ }
10
+ /**
11
+ * Vite plugin: while the dev (or preview) server runs, publish it to the internet over a
12
+ * `bitmot io1 relay` child process, proxying to the server's real bound port. The relay's lifetime is
13
+ * tied to the server -- spawned once it is listening, killed when it closes -- so a shared URL never
14
+ * outlives the session. The CLI is located on PATH or auto-downloaded (content-addressed, so it tracks
15
+ * the deployed build). Reconnects are the relay's own concern; a non-zero exit is a definitive refusal
16
+ * (bad auth, host held by another account) and is surfaced, not retried.
17
+ */
18
+ export function io1(options = {}) {
19
+ const hosts = normalizeHosts(options.host);
20
+ const enabled = options.enabled ?? (!process.env.CI && hosts.length > 0);
21
+ let child;
22
+ let stopping = false;
23
+ const supervise = (server) => {
24
+ if (!enabled)
25
+ return;
26
+ const logger = server.config.logger;
27
+ if (hosts.length === 0) {
28
+ logger.warn('[io1] no host configured (set `host` or $IO1_DEV_HOST); relay not started');
29
+ return;
30
+ }
31
+ const httpServer = server.httpServer;
32
+ if (!httpServer) {
33
+ logger.warn('[io1] server has no httpServer (middleware mode?); relay not started');
34
+ return;
35
+ }
36
+ const onExit = () => {
37
+ stopping = true;
38
+ child?.kill('SIGTERM');
39
+ child = undefined;
40
+ process.removeListener('exit', onExit); // avoid piling up a listener per dev-server restart
41
+ };
42
+ httpServer.once('listening', () => {
43
+ void start(server).catch((error) => {
44
+ logger.error(`[io1] ${error.message}`);
45
+ });
46
+ });
47
+ httpServer.once('close', onExit);
48
+ process.once('exit', onExit);
49
+ };
50
+ const start = async (server) => {
51
+ const logger = server.config.logger;
52
+ const address = server.httpServer?.address();
53
+ if (!address || typeof address === 'string') {
54
+ logger.error('[io1] could not determine the dev server port; relay not started');
55
+ return;
56
+ }
57
+ const binary = await resolveBitmot({
58
+ bitmotPath: options.bitmotPath,
59
+ autoInstall: options.autoInstall,
60
+ baseUrl: options.baseUrl,
61
+ log: (message) => logger.info(`[io1] ${message}`),
62
+ });
63
+ if (binary.version)
64
+ logger.info(`[io1] using bitmot ${binary.version} (${binary.source})`);
65
+ const args = buildRelayArgs({
66
+ hosts,
67
+ allowedEmails: options.allowedEmails,
68
+ account: options.account,
69
+ gateway: options.gateway,
70
+ extraArgs: options.extraArgs,
71
+ to: `http://127.0.0.1:${address.port}`,
72
+ });
73
+ if (stopping)
74
+ return; // the server closed while we were resolving the binary
75
+ const relay = spawn(binary.path, args, { stdio: ['ignore', 'ignore', 'pipe'] });
76
+ child = relay;
77
+ relay.on('error', (error) => {
78
+ logger.error(`[io1] failed to start relay: ${error.message}`);
79
+ });
80
+ relay.on('exit', (code, signal) => {
81
+ if (child === relay)
82
+ child = undefined;
83
+ if (stopping || signal)
84
+ return;
85
+ if (code && code !== 0) {
86
+ logger.error(`[io1] relay exited (code ${code}). If this is an auth error, run: bitmot io1 authorize`);
87
+ }
88
+ });
89
+ forwardRelayOutput(relay, server);
90
+ };
91
+ return {
92
+ name: 'vite-plugin-io1',
93
+ configureServer(server) {
94
+ supervise(server);
95
+ },
96
+ configurePreviewServer(server) {
97
+ supervise(server);
98
+ },
99
+ };
100
+ }
101
+ /**
102
+ * Forwards the relay's stderr to Vite's logger line by line: a per-host "live" announcement becomes a
103
+ * banner-style URL line, anything else (the link-established line, a pending-access-requests notice,
104
+ * a diagnostic) is passed through verbatim under an [io1] tag. The relay writes whole lines, but a
105
+ * chunk may split one, so partial lines are buffered until their newline arrives.
106
+ */
107
+ function forwardRelayOutput(relay, server) {
108
+ const logger = server.config.logger;
109
+ let buffer = '';
110
+ relay.stderr?.setEncoding('utf8');
111
+ relay.stderr?.on('data', (chunk) => {
112
+ buffer += chunk;
113
+ for (let newline = buffer.indexOf('\n'); newline >= 0; newline = buffer.indexOf('\n')) {
114
+ const line = buffer.slice(0, newline).replace(/\r$/, '');
115
+ buffer = buffer.slice(newline + 1);
116
+ if (line.trim().length === 0)
117
+ continue;
118
+ const url = parseLiveUrl(line);
119
+ if (url)
120
+ logger.info(` ➜ io1: ${url}`, { clear: false, timestamp: false });
121
+ else
122
+ logger.info(`[io1] ${line}`);
123
+ }
124
+ });
125
+ }
126
+ export default io1;
127
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "vite-plugin-io1",
3
+ "version": "0.1.0",
4
+ "description": "Publish your Vite dev server to the public internet over a bitmot io1 relay, with the CLI auto-managed.",
5
+ "keywords": [
6
+ "vite",
7
+ "vite-plugin",
8
+ "tunnel",
9
+ "bitmot",
10
+ "io1",
11
+ "relay",
12
+ "https",
13
+ "share",
14
+ "dev-server"
15
+ ],
16
+ "type": "module",
17
+ "license": "MIT",
18
+ "author": "Bitmot <shea@bitmot.com>",
19
+ "homepage": "https://io1.io/vite",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://gitlab.com/bitmot/vite-plugin-io1.git"
23
+ },
24
+ "bugs": {
25
+ "url": "https://gitlab.com/bitmot/vite-plugin-io1/-/issues"
26
+ },
27
+ "engines": {
28
+ "node": ">=20"
29
+ },
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "import": "./dist/index.js"
34
+ }
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "src",
39
+ "README.md"
40
+ ],
41
+ "scripts": {
42
+ "build": "tsc -p tsconfig.json",
43
+ "typecheck": "tsc -p tsconfig.json --noEmit",
44
+ "test": "vitest run",
45
+ "prepublishOnly": "npm run build"
46
+ },
47
+ "peerDependencies": {
48
+ "vite": ">=5"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "^22.10.0",
52
+ "typescript": "^5.7.0",
53
+ "vite": "^6.0.0",
54
+ "vitest": "^2.1.0"
55
+ }
56
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Pure argument-building and output-parsing for the bitmot io1 relay. Kept free of I/O so the wire
3
+ * contract with the CLI -- which flags it takes, and how it announces a live host -- is unit-tested
4
+ * in isolation and lives in one place.
5
+ */
6
+
7
+ export interface RelayInvocation {
8
+ /** Hosts to claim, one `--host` each (the CLI appends across repeats). At least one. */
9
+ readonly hosts: readonly string[]
10
+ /** Allow-list emails; the CLI takes a single comma-separated `--allowed-emails`, not repeats. */
11
+ readonly allowedEmails?: readonly string[]
12
+ /** Account claim, when the identity has more than one membership. */
13
+ readonly account?: string
14
+ /** Gateway WebSocket URL; omitted to use the CLI's baked-in production default. */
15
+ readonly gateway?: string
16
+ /** Host header presented to the gateway; omitted to derive it from the gateway URL. */
17
+ readonly gatewayHost?: string
18
+ /** Escape hatch for flags this plugin does not model (e.g. `--credential`). Inserted verbatim. */
19
+ readonly extraArgs?: readonly string[]
20
+ /** The local dev server to proxy to, e.g. `http://127.0.0.1:5173`. */
21
+ readonly to: string
22
+ }
23
+
24
+ /**
25
+ * The full `bitmot io1 relay ...` argv (without the leading program name). `--allowed-emails` is
26
+ * comma-joined into a single flag because the CLI parses it as one comma-separated value; emitting
27
+ * repeats would make the native CLI keep only the last. `--to` goes last so the line reads as a claim
28
+ * of hosts proxied to a target.
29
+ */
30
+ export function buildRelayArgs(invocation: RelayInvocation): string[] {
31
+ const args = ['io1', 'relay']
32
+ for (const host of invocation.hosts) args.push('--host', host)
33
+ const emails = invocation.allowedEmails ?? []
34
+ if (emails.length > 0) args.push('--allowed-emails', emails.join(','))
35
+ if (invocation.account) args.push('--account', invocation.account)
36
+ if (invocation.gateway) args.push('--gateway', invocation.gateway)
37
+ if (invocation.gatewayHost) args.push('--gateway-host', invocation.gatewayHost)
38
+ if (invocation.extraArgs) args.push(...invocation.extraArgs)
39
+ args.push('--to', invocation.to)
40
+ return args
41
+ }
42
+
43
+ /**
44
+ * The public URL from a relay's per-host announcement line, or null for any other line. The relay
45
+ * prints one `<arrow> <url> live` line per claimed host once its link is up (uncolored when its
46
+ * stderr is piped, as it is under this plugin). Tolerant of the arrow glyph and surrounding spaces so
47
+ * both the native and JVM CLIs -- and any cached older build -- parse the same; the trailing `live`
48
+ * badge is what distinguishes it from the pending-requests and update-nudge lines, which also carry
49
+ * URLs but must not be mistaken for a live host.
50
+ */
51
+ export function parseLiveUrl(line: string): string | null {
52
+ const match = /(https?:\/\/\S+)\s+live\s*$/u.exec(line)
53
+ return match ? match[1]! : null
54
+ }
package/src/binary.ts ADDED
@@ -0,0 +1,175 @@
1
+ import { spawnSync } from 'node:child_process'
2
+ import { createHash, randomBytes } from 'node:crypto'
3
+ import { constants as FS, createWriteStream } from 'node:fs'
4
+ import { access, chmod, mkdir, rename, rm } from 'node:fs/promises'
5
+ import { homedir } from 'node:os'
6
+ import { join } from 'node:path'
7
+ import { Readable, Transform } from 'node:stream'
8
+ import { pipeline } from 'node:stream/promises'
9
+ import type { ReadableStream as WebReadableStream } from 'node:stream/web'
10
+
11
+ export interface ResolveOptions {
12
+ /** Explicit path to a bitmot binary; when set it is used as-is (no PATH search, no download). */
13
+ readonly bitmotPath?: string
14
+ /** Download the CLI when not otherwise found. Default true; false makes a miss a hard error. */
15
+ readonly autoInstall?: boolean
16
+ /** Download origin; mirrors the installer's BITMOT_BASE_URL. Default https://bitmot.com. */
17
+ readonly baseUrl?: string
18
+ /** Where to cache downloaded binaries. Default ~/.cache/bitmot (or $XDG_CACHE_HOME/bitmot). */
19
+ readonly cacheDir?: string
20
+ /** Sink for human-facing progress ("downloading ..."). */
21
+ readonly log?: (message: string) => void
22
+ }
23
+
24
+ export interface ResolvedBinary {
25
+ /** Path to invoke. */
26
+ readonly path: string
27
+ /** How it was found, for logging. */
28
+ readonly source: 'explicit' | 'path' | 'download' | 'cache'
29
+ /** `bitmot version`'s reported semver (e.g. "0.2.1"), or null if it could not be read. */
30
+ readonly version: string | null
31
+ }
32
+
33
+ const DEFAULT_BASE_URL = 'https://bitmot.com'
34
+
35
+ /**
36
+ * The published artifact name for a platform/arch, or null when unsupported. Mirrors the server's
37
+ * download whitelist (io/io1/gateway/download/DownloadRoutesImpl.kt) and the installer's `uname`
38
+ * mapping: macOS and Linux, on x86_64 or 64-bit ARM. Node's arch is translated to the server's
39
+ * spelling -- 'x64' -> 'x86_64' on both, but 64-bit ARM is 'arm64' on macOS and 'aarch64' on Linux.
40
+ */
41
+ export function artifactFor(platform: NodeJS.Platform, arch: string): string | null {
42
+ if (platform === 'darwin') {
43
+ if (arch === 'arm64') return 'bitmot-macos-arm64'
44
+ if (arch === 'x64') return 'bitmot-macos-x86_64'
45
+ return null
46
+ }
47
+ if (platform === 'linux') {
48
+ if (arch === 'x64') return 'bitmot-linux-x86_64'
49
+ if (arch === 'arm64') return 'bitmot-linux-aarch64'
50
+ return null
51
+ }
52
+ return null
53
+ }
54
+
55
+ function defaultCacheDir(): string {
56
+ const xdg = process.env.XDG_CACHE_HOME
57
+ return join(xdg && xdg.length > 0 ? xdg : join(homedir(), '.cache'), 'bitmot')
58
+ }
59
+
60
+ async function exists(path: string): Promise<boolean> {
61
+ try {
62
+ await access(path, FS.F_OK)
63
+ return true
64
+ } catch {
65
+ return false
66
+ }
67
+ }
68
+
69
+ /** `bitmot version` -> "0.2.1", or null if the binary is missing or its output is unexpected. */
70
+ export function readVersion(binary: string): string | null {
71
+ const result = spawnSync(binary, ['version'], { encoding: 'utf8' })
72
+ if (result.status !== 0 || typeof result.stdout !== 'string') return null
73
+ const match = /\bbitmot\s+(\d+\.\d+\.\d+)\b/.exec(result.stdout)
74
+ return match ? match[1]! : null
75
+ }
76
+
77
+ function onPath(): boolean {
78
+ // A bare-name spawn resolves via PATH; a clean `version` exit both locates the binary and proves it
79
+ // runs on this machine (arch, permissions), which a filesystem check alone would not.
80
+ const result = spawnSync('bitmot', ['version'], { stdio: 'ignore' })
81
+ return result.status === 0 && result.error === undefined
82
+ }
83
+
84
+ /**
85
+ * Ensures the current published binary is present in the cache and returns its path. The stable
86
+ * download URL 302-redirects to the content-addressed URL of whatever bytes are deployed now, so the
87
+ * followed response's checksum header names the current version: if a file for that hash is already
88
+ * cached the body is dropped undownloaded, otherwise it is streamed, verified against the advertised
89
+ * checksum, and atomically installed. This is what makes a plugin-managed CLI track production
90
+ * automatically -- so the CLI's own "you're out of date" nudge (suppressed anyway when its stderr is
91
+ * piped, as it is here) does not matter for the auto-managed case.
92
+ */
93
+ export async function ensureDownloaded(
94
+ baseUrl: string,
95
+ artifact: string,
96
+ cacheDir: string,
97
+ log?: (message: string) => void,
98
+ ): Promise<{ path: string; downloaded: boolean }> {
99
+ const response = await fetch(`${baseUrl}/download/${artifact}`)
100
+ if (!response.ok || !response.body) {
101
+ await response.body?.cancel()
102
+ throw new Error(`download failed: ${baseUrl}/download/${artifact} returned ${response.status}`)
103
+ }
104
+ const advertised = response.headers.get('x-checksum-sha256')?.toLowerCase()
105
+ if (!advertised || !/^[0-9a-f]{64}$/.test(advertised)) {
106
+ await response.body.cancel()
107
+ throw new Error('download failed: server did not advertise a sha256 checksum')
108
+ }
109
+
110
+ const dest = join(cacheDir, `bitmot-${advertised.slice(0, 8)}`)
111
+ if (await exists(dest)) {
112
+ await response.body.cancel() // already have these exact bytes; do not re-download the body
113
+ return { path: dest, downloaded: false }
114
+ }
115
+
116
+ await mkdir(cacheDir, { recursive: true })
117
+ const temp = `${dest}.${randomBytes(6).toString('hex')}.tmp`
118
+ const hash = createHash('sha256')
119
+ const tee = new Transform({
120
+ transform(chunk, _encoding, callback) {
121
+ hash.update(chunk)
122
+ callback(null, chunk)
123
+ },
124
+ })
125
+ log?.(`downloading ${artifact} (${advertised.slice(0, 8)})`)
126
+ try {
127
+ await pipeline(
128
+ Readable.fromWeb(response.body as unknown as WebReadableStream<Uint8Array>),
129
+ tee,
130
+ createWriteStream(temp, { mode: 0o755 }),
131
+ )
132
+ const actual = hash.digest('hex')
133
+ if (actual !== advertised) {
134
+ throw new Error(`checksum mismatch: downloaded ${actual}, server advertised ${advertised}`)
135
+ }
136
+ await chmod(temp, 0o755)
137
+ await rename(temp, dest)
138
+ } catch (error) {
139
+ await rm(temp, { force: true })
140
+ throw error
141
+ }
142
+ return { path: dest, downloaded: true }
143
+ }
144
+
145
+ /**
146
+ * Resolves a bitmot binary to invoke: an explicit path wins; else a bitmot already on PATH is used
147
+ * (respecting a user- or installer-managed copy); else, when autoInstall is on, the current published
148
+ * binary is downloaded and cached. Throws with an actionable message when nothing is available and
149
+ * downloading is off or the platform has no published artifact.
150
+ */
151
+ export async function resolveBitmot(options: ResolveOptions = {}): Promise<ResolvedBinary> {
152
+ if (options.bitmotPath) {
153
+ return { path: options.bitmotPath, source: 'explicit', version: readVersion(options.bitmotPath) }
154
+ }
155
+ if (onPath()) {
156
+ return { path: 'bitmot', source: 'path', version: readVersion('bitmot') }
157
+ }
158
+ if (options.autoInstall === false) {
159
+ throw new Error(
160
+ 'bitmot CLI not found on PATH and autoInstall is disabled. ' +
161
+ 'Install it with: curl -fsSL https://bitmot.com/install | sh',
162
+ )
163
+ }
164
+ const artifact = artifactFor(process.platform, process.arch)
165
+ if (!artifact) {
166
+ throw new Error(
167
+ `bitmot CLI not found and no published binary for ${process.platform}/${process.arch}. ` +
168
+ 'Install it manually: curl -fsSL https://bitmot.com/install | sh',
169
+ )
170
+ }
171
+ const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL
172
+ const cacheDir = options.cacheDir ?? defaultCacheDir()
173
+ const { path, downloaded } = await ensureDownloaded(baseUrl, artifact, cacheDir, options.log)
174
+ return { path, source: downloaded ? 'download' : 'cache', version: readVersion(path) }
175
+ }
package/src/index.ts ADDED
@@ -0,0 +1,165 @@
1
+ import { spawn, type ChildProcess } from 'node:child_process'
2
+ import type { Plugin, PreviewServer, ViteDevServer } from 'vite'
3
+ import { buildRelayArgs, parseLiveUrl } from './announce.js'
4
+ import { resolveBitmot } from './binary.js'
5
+
6
+ export type { RelayInvocation } from './announce.js'
7
+ export type { ResolveOptions, ResolvedBinary } from './binary.js'
8
+
9
+ export interface Io1PluginOptions {
10
+ /**
11
+ * Host(s) to claim, e.g. "🔥.io1.io" or "demo" (completed to demo.io1.io). Falls back to
12
+ * $IO1_DEV_HOST. With none set the plugin stays dormant: publishing every dev server by default
13
+ * would be a footgun, and a hardcoded shared host collides between teammates.
14
+ */
15
+ host?: string | string[]
16
+ /** Allow-list emails; when set, visitors must verify one of them to reach the host. */
17
+ allowedEmails?: string[]
18
+ /** Account claim, when your identity has more than one membership. */
19
+ account?: string
20
+ /** Gateway WebSocket URL override; defaults to the CLI's production gateway. */
21
+ gateway?: string
22
+ /** Explicit path to a bitmot binary; skips PATH lookup and auto-download. */
23
+ bitmotPath?: string
24
+ /** Auto-download the CLI when not found. Default true. */
25
+ autoInstall?: boolean
26
+ /** Download origin for the CLI; mirrors the installer's BITMOT_BASE_URL. */
27
+ baseUrl?: string
28
+ /** Extra `bitmot io1 relay` flags this plugin does not model, inserted verbatim before `--to`. */
29
+ extraArgs?: string[]
30
+ /** Master switch. Default: on, unless running under CI or no host is configured. */
31
+ enabled?: boolean
32
+ }
33
+
34
+ function normalizeHosts(host: Io1PluginOptions['host']): string[] {
35
+ const raw = host ?? process.env.IO1_DEV_HOST
36
+ if (!raw) return []
37
+ return (Array.isArray(raw) ? raw : [raw]).filter((entry) => entry.length > 0)
38
+ }
39
+
40
+ type ServeServer = ViteDevServer | PreviewServer
41
+
42
+ /**
43
+ * Vite plugin: while the dev (or preview) server runs, publish it to the internet over a
44
+ * `bitmot io1 relay` child process, proxying to the server's real bound port. The relay's lifetime is
45
+ * tied to the server -- spawned once it is listening, killed when it closes -- so a shared URL never
46
+ * outlives the session. The CLI is located on PATH or auto-downloaded (content-addressed, so it tracks
47
+ * the deployed build). Reconnects are the relay's own concern; a non-zero exit is a definitive refusal
48
+ * (bad auth, host held by another account) and is surfaced, not retried.
49
+ */
50
+ export function io1(options: Io1PluginOptions = {}): Plugin {
51
+ const hosts = normalizeHosts(options.host)
52
+ const enabled = options.enabled ?? (!process.env.CI && hosts.length > 0)
53
+
54
+ let child: ChildProcess | undefined
55
+ let stopping = false
56
+
57
+ const supervise = (server: ServeServer): void => {
58
+ if (!enabled) return
59
+ const logger = server.config.logger
60
+ if (hosts.length === 0) {
61
+ logger.warn('[io1] no host configured (set `host` or $IO1_DEV_HOST); relay not started')
62
+ return
63
+ }
64
+ const httpServer = server.httpServer
65
+ if (!httpServer) {
66
+ logger.warn('[io1] server has no httpServer (middleware mode?); relay not started')
67
+ return
68
+ }
69
+
70
+ const onExit = (): void => {
71
+ stopping = true
72
+ child?.kill('SIGTERM')
73
+ child = undefined
74
+ process.removeListener('exit', onExit) // avoid piling up a listener per dev-server restart
75
+ }
76
+
77
+ httpServer.once('listening', () => {
78
+ void start(server).catch((error: unknown) => {
79
+ logger.error(`[io1] ${(error as Error).message}`)
80
+ })
81
+ })
82
+ httpServer.once('close', onExit)
83
+ process.once('exit', onExit)
84
+ }
85
+
86
+ const start = async (server: ServeServer): Promise<void> => {
87
+ const logger = server.config.logger
88
+ const address = server.httpServer?.address()
89
+ if (!address || typeof address === 'string') {
90
+ logger.error('[io1] could not determine the dev server port; relay not started')
91
+ return
92
+ }
93
+
94
+ const binary = await resolveBitmot({
95
+ bitmotPath: options.bitmotPath,
96
+ autoInstall: options.autoInstall,
97
+ baseUrl: options.baseUrl,
98
+ log: (message) => logger.info(`[io1] ${message}`),
99
+ })
100
+ if (binary.version) logger.info(`[io1] using bitmot ${binary.version} (${binary.source})`)
101
+
102
+ const args = buildRelayArgs({
103
+ hosts,
104
+ allowedEmails: options.allowedEmails,
105
+ account: options.account,
106
+ gateway: options.gateway,
107
+ extraArgs: options.extraArgs,
108
+ to: `http://127.0.0.1:${address.port}`,
109
+ })
110
+
111
+ if (stopping) return // the server closed while we were resolving the binary
112
+ const relay = spawn(binary.path, args, { stdio: ['ignore', 'ignore', 'pipe'] })
113
+ child = relay
114
+
115
+ relay.on('error', (error) => {
116
+ logger.error(`[io1] failed to start relay: ${error.message}`)
117
+ })
118
+ relay.on('exit', (code, signal) => {
119
+ if (child === relay) child = undefined
120
+ if (stopping || signal) return
121
+ if (code && code !== 0) {
122
+ logger.error(
123
+ `[io1] relay exited (code ${code}). If this is an auth error, run: bitmot io1 authorize`,
124
+ )
125
+ }
126
+ })
127
+
128
+ forwardRelayOutput(relay, server)
129
+ }
130
+
131
+ return {
132
+ name: 'vite-plugin-io1',
133
+ configureServer(server) {
134
+ supervise(server)
135
+ },
136
+ configurePreviewServer(server) {
137
+ supervise(server)
138
+ },
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Forwards the relay's stderr to Vite's logger line by line: a per-host "live" announcement becomes a
144
+ * banner-style URL line, anything else (the link-established line, a pending-access-requests notice,
145
+ * a diagnostic) is passed through verbatim under an [io1] tag. The relay writes whole lines, but a
146
+ * chunk may split one, so partial lines are buffered until their newline arrives.
147
+ */
148
+ function forwardRelayOutput(relay: ChildProcess, server: ServeServer): void {
149
+ const logger = server.config.logger
150
+ let buffer = ''
151
+ relay.stderr?.setEncoding('utf8')
152
+ relay.stderr?.on('data', (chunk: string) => {
153
+ buffer += chunk
154
+ for (let newline = buffer.indexOf('\n'); newline >= 0; newline = buffer.indexOf('\n')) {
155
+ const line = buffer.slice(0, newline).replace(/\r$/, '')
156
+ buffer = buffer.slice(newline + 1)
157
+ if (line.trim().length === 0) continue
158
+ const url = parseLiveUrl(line)
159
+ if (url) logger.info(` ➜ io1: ${url}`, { clear: false, timestamp: false })
160
+ else logger.info(`[io1] ${line}`)
161
+ }
162
+ })
163
+ }
164
+
165
+ export default io1