codexpanel 0.1.8 → 0.1.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # codexpanel
2
2
 
3
- [中文版本](README.zh.md)
3
+ [涓枃鐗堟湰](README.zh.md)
4
4
 
5
5
  codexpanel is a mobile control plane for managing local Codex work from a phone. It provides a Next.js web console, relay service, desktop agent, PostgreSQL-backed durable storage, shared protocol package, and reusable UI package in one monorepo.
6
6
 
@@ -59,7 +59,7 @@ Web/admin login uses a session cookie. Configure at least one of these before ex
59
59
  ```env
60
60
  CODEXPANEL_ADMIN_USERNAME=admin
61
61
  CODEXPANEL_ADMIN_USER_ID=admin
62
- CODEXPANEL_ADMIN_PASSWORD=ChangeRoot9361A
62
+ CODEXPANEL_ADMIN_PASSWORD=Admin778899
63
63
  CODEXPANEL_SESSION_SECRET=long_random_secret
64
64
  CODEXPANEL_LOGIN_MAX_ATTEMPTS=8
65
65
  CODEXPANEL_LOGIN_IP_MAX_ATTEMPTS=24
@@ -79,7 +79,7 @@ CODEXPANEL_REGISTRATION_ENABLED=false
79
79
 
80
80
  Desktop setup uses browser login binding or a one-time terminal token. `CODEXPANEL_ADMIN_*` only bootstraps the first admin; additional users must be created through registration or admin operations.
81
81
 
82
- `CODEXPANEL_REGISTRATION_ENABLED=true` opens self-service web registration. If the variable is omitted, development defaults to open and production defaults to closed; production onboarding must opt in explicitly. New users are stored in the existing relay users table with PBKDF2 password hashes and receive `CODEXPANEL_REGISTRATION_DEFAULT_ROLE` (`operator` or `viewer`, default `operator`).
82
+ `CODEXPANEL_REGISTRATION_ENABLED=true` opens self-service web registration. If the variable is omitted, development defaults to open and production defaults to closed; production onboarding must opt in explicitly. New users are stored in the existing relay users table with PBKDF2 password hashes. Permissions are assigned through IAM permission groups, not account roles.
83
83
 
84
84
  Desktop setup start requests are also rate-limited by source IP and capped by pending flow count. Tune `CODEXPANEL_DESKTOP_SETUP_*` only if installer traffic legitimately exceeds the defaults.
85
85
 
@@ -87,7 +87,7 @@ Desktop setup start requests are also rate-limited by source IP and capped by pe
87
87
 
88
88
  Use `/admin/` with an admin session for first-batch testing:
89
89
 
90
- - Enable/disable users, change roles, and reset passwords. These actions invalidate existing local-user sessions.
90
+ - Enable/disable users, assign permission groups, and reset passwords. These actions invalidate existing local-user sessions.
91
91
  - Recharge, gift, or deduct balance through the wallet adjustment action. Every change requires an idempotency key and is recorded in the user wallet ledger; do not edit `balance` directly.
92
92
  - Grant, renew, or revoke monthly cards. Device quota is derived server-side from the user entitlement.
93
93
  - Bind devices to users or disable devices from the admin device table. Ordinary users cannot operate another user's device, and disabled devices are blocked for non-admin operation.
package/README.zh.md CHANGED
@@ -59,7 +59,7 @@ Web/admin 登录使用 session cookie。公网暴露 relay 前至少配置以下
59
59
  ```env
60
60
  CODEXPANEL_ADMIN_USERNAME=admin
61
61
  CODEXPANEL_ADMIN_USER_ID=admin
62
- CODEXPANEL_ADMIN_PASSWORD=ChangeRoot9361A
62
+ CODEXPANEL_ADMIN_PASSWORD=Admin778899
63
63
  CODEXPANEL_SESSION_SECRET=long_random_secret
64
64
  CODEXPANEL_LOGIN_MAX_ATTEMPTS=8
65
65
  CODEXPANEL_LOGIN_IP_MAX_ATTEMPTS=24
@@ -79,7 +79,7 @@ CODEXPANEL_REGISTRATION_ENABLED=false
79
79
 
80
80
  电脑端 setup 支持浏览器登录绑定和终端一次性 token 登录。`CODEXPANEL_ADMIN_*` 只用于启动首个管理员;其他用户必须通过注册或后台运营创建。
81
81
 
82
- `CODEXPANEL_REGISTRATION_ENABLED=true` 会打开自助注册。未配置该变量时,开发环境默认开放、生产环境默认关闭;生产环境如需开放注册必须显式设置为 `true`。新用户写入 relay 的 users 表,密码使用 PBKDF2 hash,角色来自 `CODEXPANEL_REGISTRATION_DEFAULT_ROLE`,默认 `operator`。
82
+ `CODEXPANEL_REGISTRATION_ENABLED=true` 会打开自助注册。未配置该变量时,开发环境默认开放、生产环境默认关闭;生产环境如需开放注册必须显式设置为 `true`。新用户写入 relay 的 users 表,密码使用 PBKDF2 hash;后台权限只通过 IAM 权限组和 permission scopes 分配,不再使用账号角色。
83
83
 
84
84
  电脑端 setup 启动请求会按来源 IP 限流,并受 pending flow 总量限制。只有安装流量确实超过默认值时,才需要调整 `CODEXPANEL_DESKTOP_SETUP_*`。
85
85
 
@@ -11,7 +11,7 @@ const crypto = require("crypto");
11
11
  const readline = require("readline");
12
12
  const { spawn, spawnSync } = require("child_process");
13
13
 
14
- const VERSION = "0.1.8";
14
+ const VERSION = "0.1.10";
15
15
  const PROD_URL = "https://codexpanel.com";
16
16
  const TEST_URL = "https://jd.6a.gs";
17
17
  const LOCAL_HOST = "127.0.0.1";
@@ -34,6 +34,7 @@ Options:
34
34
  --token-login Ask for a one-time terminal token from the setup page
35
35
  --no-browser Do not open a browser; print the login URL instead
36
36
  --no-autostart Do not enable Windows login autostart
37
+ --install-only Install/update the local agent and panel without binding
37
38
  --dry-run Print resolved installer details without installing
38
39
  --print-command Print the equivalent PowerShell bootstrap command after login approval
39
40
  -h, --help Show help
@@ -59,6 +60,7 @@ function parseArgs(argv) {
59
60
  autoStart: true,
60
61
  dryRun: false,
61
62
  printCommand: false,
63
+ installOnly: false,
62
64
  };
63
65
 
64
66
  const readValue = (flag) => {
@@ -80,6 +82,7 @@ function parseArgs(argv) {
80
82
  else if (arg === "--token-login") options.tokenLogin = true;
81
83
  else if (arg === "--no-browser") options.browser = false;
82
84
  else if (arg === "--no-autostart") options.autoStart = false;
85
+ else if (arg === "--install-only") options.installOnly = true;
83
86
  else if (arg === "--legacy-tunnel") throw new Error("--legacy-tunnel has been retired. Device traffic uses the server WSS gateway.");
84
87
  else if (arg === "--no-tunnel") throw new Error("--no-tunnel has been retired. Device traffic uses the server WSS gateway.");
85
88
  else if (arg === "--dry-run") options.dryRun = true;
@@ -364,6 +367,8 @@ async function preloadResources(relayUrl) {
364
367
  let manifest = {};
365
368
  try { manifest = JSON.parse(manifestText); }
366
369
  catch { throw new Error("Server returned an invalid agent manifest."); }
370
+ const cacheKey = crypto.createHash("sha256").update(manifestText).digest("hex").slice(0, 16);
371
+ const preloadDir = path.join(localAgentRoot(), "preload", cacheKey);
367
372
  const resources = [
368
373
  manifest.hostRuntime,
369
374
  manifest.hostRuntimeSchema,
@@ -376,9 +381,16 @@ async function preloadResources(relayUrl) {
376
381
  const text = await downloadText(resourceUrl, 60000);
377
382
  const hash = crypto.createHash("sha256").update(Buffer.from(text, "utf8")).digest("hex");
378
383
  if (item.sha256 && hash !== item.sha256) throw new Error(`Checksum mismatch for ${item.fileName || item.path}`);
384
+ if (!item.dynamicAfterApproval && item.fileName) {
385
+ const target = path.join(preloadDir, ...String(item.fileName).split(/[\\/]+/));
386
+ fs.mkdirSync(path.dirname(target), { recursive: true });
387
+ fs.writeFileSync(target, text, "utf8");
388
+ }
379
389
  }
380
390
  console.log("CodexPanel: 资源已下载并校验完成 / Resources downloaded and verified.");
381
- return manifest;
391
+ fs.mkdirSync(preloadDir, { recursive: true });
392
+ fs.writeFileSync(path.join(preloadDir, "manifest.json"), JSON.stringify(manifest, null, 2));
393
+ return { manifest, preloadDir };
382
394
  }
383
395
 
384
396
  function powershellCommand(url) {
@@ -427,14 +439,14 @@ async function tokenLogin(relayUrl, flowId, flowSecret) {
427
439
  }
428
440
  }
429
441
 
430
- async function runBootstrap(relayUrl, approval, options) {
442
+ async function runBootstrap(relayUrl, profile, options, preloadDir = "") {
431
443
  const url = new URL("/agent/bootstrap.ps1", relayUrl);
432
- url.searchParams.set("setupToken", approval.setupToken);
433
444
  url.searchParams.set("manualDeviceId", "1");
434
445
  url.searchParams.set("manualDeviceName", "1");
435
- url.searchParams.set("deviceId", approval.deviceId || stableDeviceId());
436
- if (approval.deviceName) url.searchParams.set("deviceName", approval.deviceName);
446
+ url.searchParams.set("deviceId", profile.deviceId || stableDeviceId());
447
+ if (profile.deviceName) url.searchParams.set("deviceName", profile.deviceName);
437
448
  if (options.workspace) url.searchParams.set("workspace", options.workspace);
449
+ if (preloadDir) url.searchParams.set("preloadDir", preloadDir);
438
450
  url.searchParams.set("mode", "host-runtime");
439
451
  url.searchParams.set("autoStart", options.autoStart ? "1" : "0");
440
452
 
@@ -453,9 +465,48 @@ async function runBootstrap(relayUrl, approval, options) {
453
465
  if (result.status !== 0) throw new Error(`PowerShell installer exited with code ${result.status}`);
454
466
  }
455
467
 
468
+ async function bindLocalAgent(approval) {
469
+ const root = localAgentRoot();
470
+ const script = path.join(root, "bind-agent.ps1");
471
+ if (!fs.existsSync(script)) throw new Error(`Local bind script not found: ${script}`);
472
+ const args = [
473
+ "-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", script,
474
+ "-SetupToken", approval.setupToken || "",
475
+ "-DeviceToken", approval.deviceToken || "",
476
+ "-UserId", approval.userId || "",
477
+ "-DeviceId", approval.deviceId || "",
478
+ ];
479
+ if (approval.deviceName) args.push("-DeviceName", approval.deviceName);
480
+ const result = spawnSync(powershellPath(), args, { stdio: "inherit", windowsHide: false });
481
+ if (result.error) throw result.error;
482
+ if (result.status !== 0) throw new Error(`PowerShell bind script exited with code ${result.status}`);
483
+ }
484
+
485
+ function localProfileFromConfig(options, resolved) {
486
+ try {
487
+ const runtime = readLocalRuntimeConfig();
488
+ const config = runtime.config || {};
489
+ return {
490
+ ...computerProfile(options, resolved),
491
+ deviceId: config.deviceId || config.agentId || stableDeviceId(),
492
+ deviceName: options.deviceName || config.deviceName || `${os.hostname()} / ${defaultUsername()} Codex Desktop Agent`,
493
+ workspace: options.workspace || config.workspace || process.env.USERPROFILE || process.cwd(),
494
+ };
495
+ } catch {
496
+ return computerProfile(options, resolved);
497
+ }
498
+ }
499
+
456
500
  async function install(options) {
501
+ if (options.command === "login" && !process.env.CODEXPANEL_SERVER) {
502
+ try {
503
+ const existing = readLocalRuntimeConfig().config;
504
+ if (existing.relayUrl && options.server === "production") options.server = existing.relayUrl;
505
+ } catch {}
506
+ }
457
507
  const resolved = await resolveServer(options.server);
458
- const profile = computerProfile(options, resolved);
508
+ const loginOnly = options.command === "login";
509
+ const profile = loginOnly ? localProfileFromConfig(options, resolved) : computerProfile(options, resolved);
459
510
  if (options.dryRun) {
460
511
  console.log(JSON.stringify({
461
512
  ok: true,
@@ -480,7 +531,16 @@ async function install(options) {
480
531
  console.log(`Local relay port / 本地 relay 端口: ${resolved.localPort} (dynamic, not fixed)`);
481
532
  }
482
533
 
483
- const manifest = await preloadResources(resolved.relayUrl);
534
+ let manifest = {};
535
+ if (!loginOnly) {
536
+ const preload = await preloadResources(resolved.relayUrl);
537
+ manifest = preload.manifest;
538
+ await runBootstrap(resolved.relayUrl, profile, options, preload.preloadDir);
539
+ }
540
+ if (options.installOnly) {
541
+ console.log("\nCodexPanel: local agent installed. Run `npx -y codexpanel login` to bind this device.");
542
+ return;
543
+ }
484
544
  const start = await requestJson("POST", resolved.relayUrl, "/api/desktop/setup/start", {
485
545
  ...profile,
486
546
  agentVersion: manifest.agentVersion || "",
@@ -514,7 +574,7 @@ async function install(options) {
514
574
  console.log("\nCodexPanel: 登录绑定成功 / Sign-in and binding approved.");
515
575
  console.log(`User / 用户: ${approval.user?.name || approval.userId || "unknown"}`);
516
576
  console.log(`Device / 设备: ${approval.deviceName || approval.deviceId}`);
517
- await runBootstrap(resolved.relayUrl, approval, options);
577
+ await bindLocalAgent(approval);
518
578
  if (approval.panelUrl) {
519
579
  console.log(`CodexPanel local status panel / 本地状态面板: ${approval.panelUrl}`);
520
580
  try { openBrowser(approval.panelUrl); } catch {}
@@ -263,7 +263,7 @@ relay 会校验:
263
263
  - `deviceToken`:给安装后的 agent 使用的长期设备 token 明文,只在这个 flow 内短暂存在。
264
264
  - `deviceToken.hash`:后续会写入设备记录,服务端长期保存 hash,不保存明文。
265
265
  - `approvedByUserId`
266
- - `approvedByRole`
266
+ - `approvedByPermissionGroupId`
267
267
  - `userId`
268
268
 
269
269
  浏览器显示:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexpanel",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "CodexPanel mobile control plane monorepo.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,