Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

[TinyUI](https://github.com/tiny-ui/tinyui) 的构建工具:`tinyui build` 把 `tinyui.config.json` 描述的包(`src/pages/**/*.tsx`)编成每页一个 ESM 模块字节码(含运行时模块与 `manifest.json`),`tinyui bundle` 把一次 build 签名成可上传 / 可静态托管的热下发目录,`tinyui keys generate` 生成签名密钥对,`tinyui schema` 从组件 schema 生成 TS 类型与 Kotlin 侧代码。

`tinyui publish` 先对照目标宿主版本的快照核对包用到的宿主能力与组件,再把 `bundle` 的产物传到热下发服务并切换 channel 指针;`tinyui apps` / `packages` / `tokens` / `releases` / `hosts` 是这套服务的管理面(建 app 与签发 app token、登记包的验签公钥、签发与吊销发布 token、列 release 与回滚 / 晋级 / 改灰度、宿主 CI 上传快照)。实例地址取 `--url`、`$TINYUI_UPDATES_URL`、`https://updates.tinyui.app` 三者之一;凭据只从环境变量读,不接受命令行参数:`$TINYUI_TOKEN` 是发布 token,`$TINYUI_ADMIN_TOKEN` 放实例的管理 token 或某个 app 的 app token。`--app` 可用 `$TINYUI_APP` 代替,`--pkg` 缺省取当前目录 `tinyui.config.json` 的 `name`,`--channel` 必须每次显式写。
`tinyui publish` 先对照目标宿主版本的快照核对包用到的宿主能力与组件,再把 `bundle` 的产物传到热下发服务并切换 channel 指针;`tinyui apps` / `packages` / `tokens` / `releases` / `hosts` 是这套服务的管理面(建 app 与签发 app token、登记包的验签公钥、签发与吊销发布 token、列 release 与回滚 / 晋级 / 改灰度、宿主 CI 上传快照)。实例地址取 `--url`、`$TINYUI_UPDATES_URL`、`https://updates.tinyui.app` 三者之一;凭据只从环境变量读,不接受命令行参数:`$TINYUI_TOKEN` 是发布 token,`$TINYUI_ADMIN_TOKEN` 放实例的管理 token 或某个 app 的 app token。`--app` 可用 `$TINYUI_APP` 代替,`--pkg` 缺省取当前目录 `tinyui.config.json` 的 `name`,`--channel` 必须每次显式写。联网命令遵循 `HTTPS_PROXY` / `HTTP_PROXY` / `NO_PROXY`(大小写均可),与 curl、npm 一致;没设代理变量时不加载代理实现。

字节码编译用 quickjs-kmp 的宿主工具 `qjsc-kmp`:依赖里的 npm 包 `qjsc-kmp` 带 macOS 与 Linux 的预编译版,装完即用;其他平台用 `--qjsc` 或 `TINYUI_QJSC` 指向自己编的那份。`qjsc-kmp` 的版本钉在与 tinyui 所用 quickjs-kmp 相同的版本上,编出的字节码才认 App 里的引擎。用法见仓库 `docs/build-chain.md` 与 `docs/updates.md`;与 `tinyui-core` 同版本号。
5 changes: 3 additions & 2 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tinyui-cli",
"version": "0.9.0",
"version": "0.9.1",
"description": "TinyUI build tool: TSX -> h() (ES2025) -> ESM module bytecode.",
"license": "MIT",
"type": "module",
Expand Down Expand Up @@ -34,7 +34,8 @@
"esbuild": "0.28.2",
"magic-string": "1.4.1",
"oxc-parser": "0.150.0",
"qjsc-kmp": "0.1.2"
"qjsc-kmp": "0.1.2",
"undici": "7.30.0"
},
"devDependencies": {
"tinyui-core": "workspace:*",
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ADMIN_TOKEN_ENV, DEFAULT_URL, resolveUrl, requireToken, TOKEN_ENV, Upda
import { loadConfig, requireName } from "./config.ts";
import { i18nTypes, loadI18n } from "./i18n.ts";
import { generateKeyPair } from "./keys.ts";
import { useEnvProxy } from "./proxy.ts";
import { publish } from "./publish.ts";
import { pull } from "./pull.ts";
import { parseHostSnapshot } from "./snapshot.ts";
Expand Down Expand Up @@ -390,6 +391,7 @@ async function main(argv: string[]): Promise<number> {
return argv.includes("--help") || argv.includes("-h") ? 0 : 2;
}
const { values, positionals } = parseArgs({ args: argv, allowPositionals: true, options: command.options });
await useEnvProxy();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Offline commands fail

When a proxy variable contains a malformed URL and the user invokes an offline command, useEnvProxy() constructs EnvHttpProxyAgent before command.run, so the malformed URL throws and commands such as build, bundle, and schema do not run.

Bypass proxy initialization for commands that do not make network requests.

Prompt for AI agents
In `packages/cli/src/bin.ts` at line 394:

**Offline commands fail**

When a proxy variable contains a malformed URL and the user invokes an offline command, `useEnvProxy()` constructs `EnvHttpProxyAgent` before `command.run`, so the malformed URL throws and commands such as `build`, `bundle`, and `schema` do not run.

Bypass proxy initialization for commands that do not make network requests.

return command.run(values, positionals);
}

Expand Down
21 changes: 21 additions & 0 deletions packages/cli/src/proxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const PROXY_ENV = ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"] as const;

/**
* Node's built-in fetch ignores the proxy environment variables that curl, npm and pnpm honour (unless the process was
* started with NODE_USE_ENV_PROXY), so behind a proxy every command that talks to the updates server fails with
* "fetch failed". When one is set, route the global fetch through undici's EnvHttpProxyAgent, which also reads NO_PROXY.
* undici is imported only then: environments without a proxy, such as reproducible builds, never load it.
*/
export async function useEnvProxy(env: NodeJS.ProcessEnv = process.env): Promise<boolean> {
if (!PROXY_ENV.some((name) => env[name])) return false;
const { EnvHttpProxyAgent, setGlobalDispatcher } = await import("undici");
const httpProxy = env.HTTP_PROXY ?? env.http_proxy;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Configured proxy is ignored

When an uppercase proxy or NO_PROXY variable is empty while its lowercase counterpart is configured, ?? selects the empty uppercase value instead of the configured lowercase value, so EnvHttpProxyAgent receives an empty proxy URL or noProxy list and requests use the wrong routing behavior.

Treat empty uppercase values as unset when falling back to lowercase environment variables.

Prompt for AI agents
In `packages/cli/src/proxy.ts` at line 12:

**Configured proxy is ignored**

When an uppercase proxy or `NO_PROXY` variable is empty while its lowercase counterpart is configured, `??` selects the empty uppercase value instead of the configured lowercase value, so `EnvHttpProxyAgent` receives an empty proxy URL or `noProxy` list and requests use the wrong routing behavior.

Treat empty uppercase values as unset when falling back to lowercase environment variables.

const httpsProxy = env.HTTPS_PROXY ?? env.https_proxy;
const noProxy = env.NO_PROXY ?? env.no_proxy;
setGlobalDispatcher(new EnvHttpProxyAgent({
...(httpProxy !== undefined && { httpProxy }),
...(httpsProxy !== undefined && { httpsProxy }),
...(noProxy !== undefined && { noProxy }),
Comment on lines +15 to +18

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Loopback requests leak tokens

When a proxy is configured and an authenticated command targets an allowed loopback HTTP URL that NO_PROXY does not exclude, EnvHttpProxyAgent sends the request through the proxy, so local Wrangler commands fail to reach the server and the proxy can read bearer tokens. Add loopback hosts to the default NO_PROXY exclusions.

Ensure localhost and loopback addresses are excluded from proxying even when NO_PROXY is unset or does not list them.

Prompt for AI agents
In `packages/cli/src/proxy.ts` at lines 15-18:

**Loopback requests leak tokens**

When a proxy is configured and an authenticated command targets an allowed loopback HTTP URL that `NO_PROXY` does not exclude, `EnvHttpProxyAgent` sends the request through the proxy, so local Wrangler commands fail to reach the server and the proxy can read bearer tokens. Add loopback hosts to the default `NO_PROXY` exclusions.

Ensure localhost and loopback addresses are excluded from proxying even when `NO_PROXY` is unset or does not list them.

}));
return true;
}
49 changes: 49 additions & 0 deletions packages/cli/test/proxy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import assert from "node:assert/strict";
import { createServer, type Server } from "node:http";
import type { AddressInfo } from "node:net";
import { after, before, describe, it } from "node:test";
import { Agent, setGlobalDispatcher } from "undici";
import { useEnvProxy } from "../src/proxy.ts";

describe("useEnvProxy", () => {
let proxy: Server;
let origin: Server;
let proxyUrl: string;
const seen: string[] = [];

before(async () => {
// undici tunnels through the proxy with CONNECT even for http targets; answer it, then serve the tunnelled
// request from an in-process "origin" so no real network is touched
origin = createServer((req, res) => {
seen.push(`GET ${req.headers.host}${req.url}`);
res.end("via proxy");
});
proxy = createServer();
proxy.on("connect", (req, socket) => {
seen.push(`CONNECT ${req.url}`);
socket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
origin.emit("connection", socket);
});
await new Promise<void>((resolve) => proxy.listen(0, "127.0.0.1", resolve));
proxyUrl = `http://127.0.0.1:${(proxy.address() as AddressInfo).port}`;
});

after(async () => {
setGlobalDispatcher(new Agent());
// the proxy agent keeps its connection alive; close() alone would wait for it forever
proxy.closeAllConnections();
origin.closeAllConnections();
await new Promise((resolve) => proxy.close(resolve));
});

it("leaves fetch alone without proxy variables", async () => {
assert.equal(await useEnvProxy({}), false);
});

it("routes the global fetch through HTTP_PROXY", async () => {
assert.equal(await useEnvProxy({ HTTP_PROXY: proxyUrl }), true);
const response = await fetch("http://updates.example.invalid/demo/ping");
assert.equal(await response.text(), "via proxy");
assert.deepEqual(seen, ["CONNECT updates.example.invalid:80", "GET updates.example.invalid/demo/ping"]);
});
});
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tinyui-core",
"version": "0.9.0",
"version": "0.9.1",
"description": "TinyUI runtime: signals, ownership, h/For/Show, patch emission.",
"license": "MIT",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/version.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
/** The tinyui-core version; a package records it as the runtime it needs (docs/updates.md §1.1). */
export const VERSION = "0.9.0";
export const VERSION = "0.9.1";
2 changes: 1 addition & 1 deletion packages/native/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tinyui-native",
"version": "0.9.0",
"version": "0.9.1",
"description": "TinyUI host capability API for business code: http, storage, toast, navigation, i18n.",
"license": "MIT",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion packages/native/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const VERSION = "0.9.0";
export const VERSION = "0.9.1";
export * as navigation from "./navigation.ts";
export * as store from "./store.ts";
export * as events from "./events.ts";
Expand Down
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading