Compare commits

...
Author SHA1 Message Date
shaw 0f9d460dc6 fix(deps): resolve frontend audit failures (vue, source-map-js, xlsx exceptions)
- Bump vue to ^3.5.43 so @vue/server-renderer is >= 3.5.42
  (GHSA-g2v6-rqmx-r4w6).
- Override source-map-js to >= 1.2.2 (GHSA-68fv-2mgg-jv7q).
- Renew the expired xlsx exceptions until 2027-01-06: xlsx has no patched
  release on npm and is only used for XLSX.write export in the admin usage
  view; both advisories require parsing a crafted file and no read path exists.

Lockfile regenerated with pnpm 9 to match CI.
2026-10-07 09:44:01 +08:00
shaw 6e315616ed fix(setup): stop shipping guessable admin credentials on fresh installs (fixes #7850)
Fresh Docker installs created the admin as admin@sub2api.local and accepted
any ADMIN_PASSWORD (e.g. "123456"), making them easy brute-force targets.

- Generate a random admin-<12 hex>@sub2api.local login email when
  ADMIN_EMAIL is empty; drop the fixed default from .env.example and the
  compose files, and have docker-deploy.sh write a random ADMIN_EMAIL.
- Validate user-supplied ADMIN_PASSWORD (8-72 bytes) and ADMIN_EMAIL on the
  auto-setup path, only when an admin is actually about to be created, so
  existing deployments are never blocked at startup.
- Align validateEmail with the login endpoint's email binding so setup can no
  longer create an admin that can never log in (e.g. "a@b", "Name <a@b.com>").
- Cap validatePassword at bcrypt's 72-byte limit; the CLI reuses it and now
  defaults to a random admin email instead of admin@example.com.
- Update READMEs/deploy docs to stop suggesting guessable credentials.
2026-10-07 09:27:36 +08:00
Wesley Liddick d97ccc952f Merge pull request #7888 from honestTai/fix/easypay-callback-forgery
fix(payment): 修复易支付回调签名复用伪造入账漏洞 (fixes #7881)
2026-10-07 08:33:53 +08:00
Wesley Liddick 33d9e72f90 Merge pull request #7851 from ZeroClover/fix/remote-model-catalog-discovery
fix: enable API-key discovery for remote Codex catalogs
2026-10-07 08:27:17 +08:00
honest.tai d1aac6b989 fix(payment): close EasyPay forged-callback sign reuse (fixes #7881)
An order-creation signature (visible to the payer in popup submit.php
URLs) could be replayed as a payment-success notification, with no
merchant key required. Two compounding defects:

- CanonicalizeReturnURL kept client-supplied query parameters, so a
  return_url ending in '&trade_status=TRADE_SUCCESS' entered the signed
  order string verbatim;
- the sign base string concatenates values unescaped, so a callback that
  decodes return_url only up to the smuggled pair re-sorts to a
  byte-identical sign base and passes verification.

Fixes:

- CanonicalizeReturnURL now drops client query parameters entirely; the
  server builds its own signed set and the client query has no
  legitimate use;
- VerifyNotification rejects any parameter outside the genuine async
  notify set (pid/trade_no/out_trade_no/type/name/money/trade_status/
  param/sign/sign_type), failing closed against the whole smuggling
  class regardless of the injection point.

Regression tests cover the PoC payload, full order-URL replay, genuine
callback acceptance, unknown-parameter rejection (including empty
values), and the return_url smuggling strip. Genuinely paid orders that
an exotic upstream variant might reject are still recovered through the
existing upstream QueryOrder reconcile path.
2026-10-06 23:14:56 +08:00
Zero Clover bbba01dae9 fix: enable API-key discovery for remote Codex catalogs 2026-10-04 05:12:40 +08:00
github-actions[bot] b8dece9000 chore: sync VERSION to 0.2.13 [skip ci] 2026-10-02 12:16:57 +00:00
25 changed files with 775 additions and 162 deletions
+6 -6
View File
@@ -3,16 +3,16 @@ exceptions:
- package: xlsx
advisory: "GHSA-4r6h-8v6p-xvw6"
severity: high
reason: "Admin export only; switched to dynamic import to reduce exposure (CVE-2023-30533)"
mitigation: "Load only on export; restrict export permissions and data scope"
expires_on: "2026-10-06"
reason: "Write-only: admin UsageView lazily imports xlsx solely for XLSX.write export; the advisory requires parsing a crafted file and no XLSX.read path exists (CVE-2023-30533)"
mitigation: "Never parse untrusted spreadsheets with xlsx; no patched version on npm, migrate to SheetJS CDN build or another writer before expiry"
expires_on: "2027-01-06"
owner: "security@your-domain"
- package: xlsx
advisory: "GHSA-5pgg-2g8v-p4x9"
severity: high
reason: "Admin export only; switched to dynamic import to reduce exposure (CVE-2024-22363)"
mitigation: "Load only on export; restrict export permissions and data scope"
expires_on: "2026-10-06"
reason: "Write-only: admin UsageView lazily imports xlsx solely for XLSX.write export; the advisory requires parsing a crafted file and no XLSX.read path exists (CVE-2024-22363)"
mitigation: "Never parse untrusted spreadsheets with xlsx; no patched version on npm, migrate to SheetJS CDN build or another writer before expiry"
expires_on: "2027-01-06"
owner: "security@your-domain"
- package: lodash
advisory: "GHSA-r5fr-rjxr-66jc"
+6 -4
View File
@@ -354,8 +354,10 @@ JWT_SECRET=your_jwt_secret_here
TOTP_ENCRYPTION_KEY=your_totp_key_here
# Optional: Admin account
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=your_admin_password
# Leave empty to auto-generate a random email (login username) and password, shown in logs on first startup.
# Avoid guessable values such as admin@example.com: they are brute-force targets.
ADMIN_EMAIL=
ADMIN_PASSWORD=
# Optional: Custom port
SERVER_PORT=8080
@@ -404,9 +406,9 @@ docker compose -f docker-compose.local.yml logs -f sub2api
Open `http://YOUR_SERVER_IP:8080` in your browser.
If admin password was auto-generated, find it in logs:
If the admin email (login username) or password was auto-generated, find them in logs:
```bash
docker compose -f docker-compose.local.yml logs sub2api | grep "admin password"
docker compose -f docker-compose.local.yml logs sub2api | grep "Generated admin"
```
#### Upgrade
+6 -4
View File
@@ -380,8 +380,10 @@ JWT_SECRET=your_jwt_secret_here
TOTP_ENCRYPTION_KEY=your_totp_key_here
# 可选:管理员账号
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=your_admin_password
# 留空则首次启动时自动生成随机邮箱(登录用户名)和密码,并输出到日志。
# 请勿使用 admin@example.com 这类可被猜中的值,它们是暴力破解的目标。
ADMIN_EMAIL=
ADMIN_PASSWORD=
# 可选:自定义端口
SERVER_PORT=8080
@@ -442,9 +444,9 @@ docker compose -f docker-compose.local.yml logs -f sub2api
在浏览器中打开 `http://你的服务器IP:8080`
如果管理员密码是自动生成的,在日志中查找:
如果管理员邮箱(登录用户名)或密码是自动生成的,在日志中查找:
```bash
docker compose -f docker-compose.local.yml logs sub2api | grep "admin password"
docker compose -f docker-compose.local.yml logs sub2api | grep "Generated admin"
```
#### 升级
+6 -4
View File
@@ -355,8 +355,10 @@ JWT_SECRET=your_jwt_secret_here
TOTP_ENCRYPTION_KEY=your_totp_key_here
# オプション: 管理者アカウント
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=your_admin_password
# 空欄の場合、初回起動時にランダムなメールアドレス(ログインユーザー名)とパスワードが自動生成され、ログに出力されます。
# admin@example.com のような推測されやすい値は総当たり攻撃の標的になるため避けてください。
ADMIN_EMAIL=
ADMIN_PASSWORD=
# オプション: カスタムポート
SERVER_PORT=8080
@@ -405,9 +407,9 @@ docker compose -f docker-compose.local.yml logs -f sub2api
ブラウザで `http://YOUR_SERVER_IP:8080` を開いてください。
管理者パスワードが自動生成された場合は、ログで確認できます:
管理者メールアドレス(ログインユーザー名)またはパスワードが自動生成された場合は、ログで確認できます:
```bash
docker compose -f docker-compose.local.yml logs sub2api | grep "admin password"
docker compose -f docker-compose.local.yml logs sub2api | grep "Generated admin"
```
#### アップグレード
+1 -1
View File
@@ -1 +1 @@
0.2.12
0.2.13
@@ -264,10 +264,11 @@ func loginTestUser(t *testing.T) string {
t.Helper()
// 先尝试用管理员账户登录
adminEmail := getEnv("ADMIN_EMAIL", "admin@sub2api.local")
// 管理员邮箱不再有固定默认值(未设置时安装阶段随机生成),需同时提供邮箱与密码。
adminEmail := getEnv("ADMIN_EMAIL", "")
adminPassword := getEnv("ADMIN_PASSWORD", "")
if adminPassword == "" {
if adminEmail == "" || adminPassword == "" {
// 尝试用测试用户
adminEmail = testUserEmail
adminPassword = testUserPassword
@@ -376,6 +376,9 @@ func (e *EasyPay) VerifyNotification(_ context.Context, rawBody string, _ map[st
// url.ParseQuery already decodes values — no additional decode needed.
params := make(map[string]string)
for k := range values {
if !easyPayNotifyAllowedParams[k] {
return nil, fmt.Errorf("unexpected notify param: %s", k)
}
params[k] = values.Get(k)
}
sign := params["sign"]
@@ -601,3 +604,26 @@ func easyPaySign(params map[string]string, pkey string) string {
func easyPayVerifySign(params map[string]string, pkey string, sign string) bool {
return hmac.Equal([]byte(easyPaySign(params, pkey)), []byte(sign))
}
// easyPayNotifyAllowedParams is the exact parameter set a genuine EasyPay
// (彩虹易支付-compatible) async notification carries. Order-creation-only
// fields (notify_url, return_url, cid, device, clientip, ...) must never
// appear in a callback: because the sign base string concatenates values
// unescaped, a signed order URL whose return_url embeds e.g.
// "trade_status=TRADE_SUCCESS" could otherwise be replayed as a forged
// payment-success notification (issue #7881). Rejecting unknown keys closes
// the whole smuggling class; genuinely paid orders rejected by an exotic
// upstream variant are still recovered by the upstream QueryOrder reconcile
// path.
var easyPayNotifyAllowedParams = map[string]bool{
"pid": true,
"trade_no": true,
"out_trade_no": true,
"type": true,
"name": true,
"money": true,
"trade_status": true,
"param": true,
"sign": true,
"sign_type": true,
}
@@ -0,0 +1,172 @@
package provider
// Regression tests for the EasyPay forged-callback vulnerability
// (Wei-Shaw/sub2api issue #7875).
//
// Attack recap: the order-creation signature (visible to the payer in the
// submit.php popup URL) signs return_url among other fields, and the sign
// base string concatenates values unescaped. A client-supplied return_url
// ending in "&trade_status=TRADE_SUCCESS" therefore produced a signature
// that was byte-identical to a payment-success notification's signature
// once the callback decoded return_url partially and promoted the smuggled
// pair to a top-level parameter. Merchant-key rotation could not help
// because the attacker never needed the key.
//
// Fixes covered here:
// 1. service.CanonicalizeReturnURL drops client-supplied query parameters
// (see payment_resume_service_test.go).
// 2. VerifyNotification rejects any parameter outside the genuine async
// notify set, so smuggled order-creation fields (return_url et al.)
// fail closed even if a signed value smuggles a fake pair.
import (
"context"
"net/url"
"testing"
"github.com/Wei-Shaw/sub2api/internal/payment"
)
// easyPayPoCProvider returns a provider with a fixed test credential set.
func easyPayPoCProvider() *EasyPay {
return &EasyPay{config: map[string]string{
"pid": "1000",
"pkey": "MERCHANT_SECRET_KEY",
"apiBase": "https://pay.example.com",
"notifyUrl": "https://site.example.com/api/v1/payment/webhook/easypay",
"returnUrl": "https://site.example.com/payment/result",
}}
}
// TestEasyPayNotifyRejectsForgedSignReuseCallback is the exact PoC payload
// that was accepted before the fix: the order's own submit.php signature
// replayed with trade_status smuggled out of the return_url value.
func TestEasyPayNotifyRejectsForgedSignReuseCallback(t *testing.T) {
t.Parallel()
e := easyPayPoCProvider()
// Order creation (popup mode): return_url carries the smuggled pair as
// its last sorted inner query key, exactly as buildPaymentReturnURL
// would have produced before CanonicalizeReturnURL stripped user query.
returnURL := "https://site.example.com/payment/result?order_id=99&out_trade_no=ORDER123&status=success&trade_status=TRADE_SUCCESS"
createParams := map[string]string{
"pid": "1000",
"type": "alipay",
"out_trade_no": "ORDER123",
"notify_url": e.config["notifyUrl"],
"return_url": returnURL,
"name": "balance recharge",
"money": "650.00",
}
sign := easyPaySign(createParams, e.config["pkey"])
// Forged callback: return_url encoded only up to status=success, then a
// raw &trade_status=TRADE_SUCCESS promotes it to a top-level parameter.
prefix := "https://site.example.com/payment/result?order_id=99&out_trade_no=ORDER123&status=success"
cb := url.Values{}
cb.Set("pid", "1000")
cb.Set("type", "alipay")
cb.Set("out_trade_no", "ORDER123")
cb.Set("notify_url", e.config["notifyUrl"])
cb.Set("name", "balance recharge")
cb.Set("money", "650.00")
cb.Set("return_url", prefix)
rawCallback := cb.Encode() + "&trade_status=TRADE_SUCCESS" + "&sign=" + sign + "&sign_type=MD5"
if _, err := e.VerifyNotification(context.Background(), rawCallback, nil); err == nil {
t.Fatal("forged sign-reuse callback must be rejected")
}
}
// TestEasyPayNotifyRejectsOrderURLReplay covers the milder variant where the
// attacker replays the complete signed pay URL unchanged: return_url itself
// is not a legitimate notify parameter and must also be rejected.
func TestEasyPayNotifyRejectsOrderURLReplay(t *testing.T) {
t.Parallel()
e := easyPayPoCProvider()
createParams := map[string]string{
"pid": "1000",
"type": "alipay",
"out_trade_no": "ORDER123",
"notify_url": e.config["notifyUrl"],
"return_url": "https://site.example.com/payment/result",
"name": "balance recharge",
"money": "650.00",
}
createParams["sign"] = easyPaySign(createParams, e.config["pkey"])
createParams["sign_type"] = signTypeMD5
q := url.Values{}
for k, v := range createParams {
q.Set(k, v)
}
if _, err := e.VerifyNotification(context.Background(), q.Encode(), nil); err == nil {
t.Fatal("replayed order URL must be rejected")
}
}
// TestEasyPayNotifyAcceptsGenuineCallback locks in the legitimate notify
// contract: the canonical parameter set with a valid signature succeeds.
func TestEasyPayNotifyAcceptsGenuineCallback(t *testing.T) {
t.Parallel()
e := easyPayPoCProvider()
params := map[string]string{
"pid": "1000",
"trade_no": "2026100622001400000001",
"out_trade_no": "ORDER123",
"type": "alipay",
"name": "balance recharge",
"money": "650.00",
"trade_status": tradeStatusSuccess,
}
sign := easyPaySign(params, e.config["pkey"])
params["sign"] = sign
params["sign_type"] = signTypeMD5
q := url.Values{}
for k, v := range params {
q.Set(k, v)
}
n, err := e.VerifyNotification(context.Background(), q.Encode(), nil)
if err != nil {
t.Fatalf("genuine callback rejected: %v", err)
}
if n.Status != payment.ProviderStatusSuccess {
t.Fatalf("status = %v, want success", n.Status)
}
if n.OrderID != "ORDER123" || n.TradeNo != "2026100622001400000001" || n.Amount != 650.00 {
t.Fatalf("unexpected notification: %+v", n)
}
}
// TestEasyPayNotifyRejectsUnknownParam ensures any parameter outside the
// canonical notify set fails closed, including empty-valued ones that the
// signer itself would skip.
func TestEasyPayNotifyRejectsUnknownParam(t *testing.T) {
t.Parallel()
e := easyPayPoCProvider()
params := map[string]string{
"pid": "1000",
"trade_no": "T1",
"out_trade_no": "ORDER123",
"type": "alipay",
"name": "balance recharge",
"money": "650.00",
"trade_status": tradeStatusSuccess,
}
sign := easyPaySign(params, e.config["pkey"])
q := url.Values{}
for k, v := range params {
q.Set(k, v)
}
q.Set("sign", sign)
q.Set("sign_type", signTypeMD5)
q.Set("device", "") // empty value: invisible to the signer, still rejected
if _, err := e.VerifyNotification(context.Background(), q.Encode(), nil); err == nil {
t.Fatal("callback with unknown param must be rejected")
}
}
@@ -245,6 +245,13 @@ func CanonicalizeReturnURL(raw string, srcHost string, srcURL string) (string, e
return "", infraerrors.BadRequest("INVALID_RETURN_URL", "return_url must use http or https")
}
parsed.Fragment = ""
// Client-supplied query parameters are dropped: buildPaymentReturnURL adds
// its own signed set, and user-controlled pairs such as
// "trade_status=TRADE_SUCCESS" inside the signed return_url value can be
// smuggled into EasyPay-style sign base strings (values are concatenated
// unescaped), letting an order-creation signature be replayed as a forged
// payment-success callback. See issue #7881.
parsed.RawQuery = ""
if parsed.Path == "" {
parsed.Path = "/"
}
@@ -10,6 +10,7 @@ import (
"encoding/json"
"net/url"
"strconv"
"strings"
"testing"
"time"
@@ -81,12 +82,30 @@ func TestNormalizePaymentSource(t *testing.T) {
func TestCanonicalizeReturnURL(t *testing.T) {
t.Parallel()
// Client-supplied query and fragment are both dropped: user-controlled
// pairs inside the signed return_url value could be smuggled into
// EasyPay-style sign base strings (issue #7881).
got, err := CanonicalizeReturnURL("https://example.com/payment/result?b=2#a", "example.com", "")
if err != nil {
t.Fatalf("CanonicalizeReturnURL returned error: %v", err)
}
if got != "https://example.com/payment/result?b=2" {
t.Fatalf("CanonicalizeReturnURL = %q, want %q", got, "https://example.com/payment/result?b=2")
if got != "https://example.com/payment/result" {
t.Fatalf("CanonicalizeReturnURL = %q, want %q", got, "https://example.com/payment/result")
}
}
func TestCanonicalizeReturnURLStripsSmuggledTradeStatus(t *testing.T) {
t.Parallel()
got, err := CanonicalizeReturnURL(
"https://example.com/payment/result?trade_status=TRADE_SUCCESS",
"example.com", "",
)
if err != nil {
t.Fatalf("CanonicalizeReturnURL returned error: %v", err)
}
if strings.Contains(got, "trade_status") {
t.Fatalf("CanonicalizeReturnURL kept smuggled trade_status: %q", got)
}
}
@@ -117,8 +136,10 @@ func TestCanonicalizeReturnURLAllowsConfiguredFrontendHost(t *testing.T) {
if err != nil {
t.Fatalf("CanonicalizeReturnURL returned error: %v", err)
}
if got != "https://app.example.com/payment/result?from=checkout" {
t.Fatalf("CanonicalizeReturnURL = %q, want %q", got, "https://app.example.com/payment/result?from=checkout")
// The host allowlist still applies, but client query parameters are
// always dropped (issue #7881: signed-value parameter smuggling).
if got != "https://app.example.com/payment/result" {
t.Fatalf("CanonicalizeReturnURL = %q, want %q", got, "https://app.example.com/payment/result")
}
}
+10 -15
View File
@@ -4,7 +4,6 @@ package setup
import (
"bufio"
"fmt"
"net/mail"
"os"
"regexp"
"strconv"
@@ -29,11 +28,6 @@ func cliValidateUsername(name string) bool {
return validName.MatchString(name) && len(name) <= 63
}
func cliValidateEmail(email string) bool {
_, err := mail.ParseAddress(email)
return err == nil && len(email) <= 254
}
func cliValidatePort(port int) bool {
return port > 0 && port <= 65535
}
@@ -163,9 +157,14 @@ func RunCLI() error {
fmt.Println()
fmt.Println("── Admin Account ──")
// 默认值使用随机邮箱,避免直接回车得到可被猜中的固定管理员用户名(issue #7850)。
defaultAdminEmail, err := generateAdminEmail()
if err != nil {
return err
}
for {
cfg.Admin.Email = promptString(reader, "Admin Email", "admin@example.com")
if cliValidateEmail(cfg.Admin.Email) {
cfg.Admin.Email = promptString(reader, "Admin Email (login username)", defaultAdminEmail)
if validateEmail(cfg.Admin.Email) {
break
}
fmt.Println(" Invalid email format.")
@@ -173,13 +172,9 @@ func RunCLI() error {
for {
cfg.Admin.Password = promptPassword("Admin Password")
// SECURITY: Match Web API requirement of 8 characters minimum
if len(cfg.Admin.Password) < 8 {
fmt.Println(" Password must be at least 8 characters")
continue
}
if len(cfg.Admin.Password) > 128 {
fmt.Println(" Password must be at most 128 characters")
// SECURITY: Match Web API password requirements
if err := validatePassword(cfg.Admin.Password); err != nil {
fmt.Printf(" Invalid password: %v\n", err)
continue
}
confirm := promptPassword("Confirm Password")
+16 -5
View File
@@ -13,6 +13,7 @@ import (
"github.com/Wei-Shaw/sub2api/internal/pkg/sysutil"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
)
// installMutex prevents concurrent installation attempts (TOCTOU protection)
@@ -83,19 +84,29 @@ func validateUsername(name string) bool {
return validName.MatchString(name) && len(name) <= 63
}
// validateEmail checks if email format is valid
// validateEmail checks if email format is valid. The address must also pass the
// login endpoint's `binding:"email"` rule, otherwise the admin could be created
// with an email (e.g. "a@b" or "Name <a@b.com>") that can never log in.
func validateEmail(email string) bool {
_, err := mail.ParseAddress(email)
return err == nil && len(email) <= 254
if _, err := mail.ParseAddress(email); err != nil || len(email) > 254 {
return false
}
loginReq := struct {
Email string `binding:"required,email"`
}{Email: email}
return binding.Validator.ValidateStruct(&loginReq) == nil
}
// maxPasswordBytes is bcrypt's input limit; longer passwords fail to hash.
const maxPasswordBytes = 72
// validatePassword checks password strength
func validatePassword(password string) error {
if len(password) < 8 {
return fmt.Errorf("password must be at least 8 characters")
}
if len(password) > 128 {
return fmt.Errorf("password must be at most 128 characters")
if len(password) > maxPasswordBytes {
return fmt.Errorf("password must be at most %d bytes", maxPasswordBytes)
}
return nil
}
+54 -7
View File
@@ -417,6 +417,10 @@ func createAdminUser(cfg *SetupConfig) (bool, string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return bootstrapAdminUser(ctx, db, cfg)
}
func bootstrapAdminUser(ctx context.Context, db *sql.DB, cfg *SetupConfig) (bool, string, error) {
var totalUsers int64
if err := db.QueryRowContext(ctx, "SELECT COUNT(1) FROM users").Scan(&totalUsers); err != nil {
return false, "", err
@@ -430,12 +434,14 @@ func createAdminUser(cfg *SetupConfig) (bool, string, error) {
return false, decision.reason, nil
}
if strings.TrimSpace(cfg.Admin.Password) == "" {
password, genErr := generateSecret(16)
if genErr != nil {
return false, "", fmt.Errorf("failed to generate admin password: %w", genErr)
}
cfg.Admin.Password = password
emailGenerated, passwordGenerated, err := prepareAdminCredentials(&cfg.Admin)
if err != nil {
return false, "", err
}
if emailGenerated {
fmt.Printf("Generated admin email (login username): %s\n", cfg.Admin.Email)
}
if passwordGenerated {
fmt.Printf("Generated admin password (one-time): %s\n", cfg.Admin.Password)
fmt.Println("IMPORTANT: Save this password! It will not be shown again.")
}
@@ -473,6 +479,47 @@ func createAdminUser(cfg *SetupConfig) (bool, string, error) {
return true, decision.reason, nil
}
// prepareAdminCredentials fills in missing admin credentials with random values
// and rejects invalid user-supplied emails and weak passwords. It only runs
// when an admin is actually about to be created, so existing deployments are
// never affected. A random email keeps fresh installs off the well-known
// default username that brute-force scanners target (issue #7850).
func prepareAdminCredentials(admin *AdminConfig) (emailGenerated, passwordGenerated bool, err error) {
admin.Email = strings.TrimSpace(admin.Email)
if admin.Email == "" {
email, genErr := generateAdminEmail()
if genErr != nil {
return false, false, genErr
}
admin.Email = email
emailGenerated = true
} else if !validateEmail(admin.Email) {
return false, false, fmt.Errorf("invalid admin email: %q is not a valid login email", admin.Email)
}
if strings.TrimSpace(admin.Password) == "" {
password, genErr := generateSecret(16)
if genErr != nil {
return false, false, fmt.Errorf("failed to generate admin password: %w", genErr)
}
admin.Password = password
passwordGenerated = true
} else if validateErr := validatePassword(admin.Password); validateErr != nil {
return false, false, fmt.Errorf("invalid admin password: %w", validateErr)
}
return emailGenerated, passwordGenerated, nil
}
// generateAdminEmail returns a random, non-guessable admin login email.
func generateAdminEmail() (string, error) {
suffix, err := generateSecret(6)
if err != nil {
return "", fmt.Errorf("failed to generate admin email: %w", err)
}
return fmt.Sprintf("admin-%s@sub2api.local", suffix), nil
}
func writeConfigFile(cfg *SetupConfig) error {
// Ensure timezone has a default value
tz := cfg.Timezone
@@ -596,7 +643,7 @@ func AutoSetupFromEnv() error {
EnableTLS: getEnvOrDefault("REDIS_ENABLE_TLS", "false") == "true",
},
Admin: AdminConfig{
Email: getEnvOrDefault("ADMIN_EMAIL", "admin@sub2api.local"),
Email: getEnvOrDefault("ADMIN_EMAIL", ""),
Password: getEnvOrDefault("ADMIN_PASSWORD", ""),
},
Server: ServerConfig{
+236
View File
@@ -1,15 +1,19 @@
package setup
import (
"context"
"database/sql"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
"time"
"github.com/DATA-DOG/go-sqlmock"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin/binding"
"github.com/lib/pq"
"gopkg.in/yaml.v3"
)
@@ -398,3 +402,235 @@ func TestDatabaseConnectionDoesNotFallbackForTargetAuthenticationError(t *testin
t.Fatalf("opened databases = %v, want only configured target %q", opened, cfg.DBName)
}
}
func TestPrepareAdminCredentialsGeneratesMissingValues(t *testing.T) {
t.Parallel()
admin := AdminConfig{Email: " ", Password: ""}
emailGenerated, passwordGenerated, err := prepareAdminCredentials(&admin)
if err != nil {
t.Fatalf("prepareAdminCredentials() error = %v", err)
}
if !emailGenerated || !passwordGenerated {
t.Fatalf("generated flags = (%v, %v), want (true, true)", emailGenerated, passwordGenerated)
}
if !regexp.MustCompile(`^admin-[0-9a-f]{12}@sub2api\.local$`).MatchString(admin.Email) {
t.Fatalf("generated email = %q, want admin-<12 hex>@sub2api.local", admin.Email)
}
// 生成的邮箱必须能通过登录接口的 binding:"required,email" 校验。
loginReq := struct {
Email string `binding:"required,email"`
}{Email: admin.Email}
if err := binding.Validator.ValidateStruct(&loginReq); err != nil {
t.Fatalf("generated email %q rejected by login validator: %v", admin.Email, err)
}
if len(admin.Password) != 32 {
t.Fatalf("generated password length = %d, want 32", len(admin.Password))
}
other := AdminConfig{}
if _, _, err := prepareAdminCredentials(&other); err != nil {
t.Fatalf("prepareAdminCredentials() second call error = %v", err)
}
if other.Email == admin.Email {
t.Fatalf("generated emails should be random, got %q twice", admin.Email)
}
}
func TestPrepareAdminCredentialsKeepsProvidedValues(t *testing.T) {
t.Parallel()
admin := AdminConfig{Email: "owner@example.com", Password: "a-strong-password"}
emailGenerated, passwordGenerated, err := prepareAdminCredentials(&admin)
if err != nil {
t.Fatalf("prepareAdminCredentials() error = %v", err)
}
if emailGenerated || passwordGenerated {
t.Fatalf("generated flags = (%v, %v), want (false, false)", emailGenerated, passwordGenerated)
}
if admin.Email != "owner@example.com" || admin.Password != "a-strong-password" {
t.Fatalf("provided credentials were modified: %+v", admin)
}
}
func TestPrepareAdminCredentialsRejectsWeakPassword(t *testing.T) {
t.Parallel()
tests := []struct {
name string
password string
}{
{name: "too short", password: "123456"},
{name: "seven chars", password: "1234567"},
{name: "exceeds bcrypt limit", password: strings.Repeat("a", 73)},
{name: "former 128 limit", password: strings.Repeat("a", 128)},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
admin := AdminConfig{Email: "owner@example.com", Password: tt.password}
_, _, err := prepareAdminCredentials(&admin)
if err == nil || !strings.Contains(err.Error(), "invalid admin password") {
t.Fatalf("prepareAdminCredentials(%q) error = %v, want invalid admin password error", tt.password, err)
}
})
}
}
func TestPrepareAdminCredentialsRejectsUnloginableEmail(t *testing.T) {
t.Parallel()
for _, email := range []string{"admin", "a@b", "Owner <owner@example.com>", "<owner@example.com>"} {
t.Run(email, func(t *testing.T) {
t.Parallel()
admin := AdminConfig{Email: email, Password: "a-strong-password"}
_, _, err := prepareAdminCredentials(&admin)
if err == nil || !strings.Contains(err.Error(), "invalid admin email") {
t.Fatalf("prepareAdminCredentials(%q) error = %v, want invalid admin email error", email, err)
}
})
}
}
func TestPrepareAdminCredentialsTrimsProvidedEmail(t *testing.T) {
t.Parallel()
admin := AdminConfig{Email: " owner@example.com\n", Password: "a-strong-password"}
if _, _, err := prepareAdminCredentials(&admin); err != nil {
t.Fatalf("prepareAdminCredentials() error = %v", err)
}
if admin.Email != "owner@example.com" {
t.Fatalf("email = %q, want trimmed owner@example.com", admin.Email)
}
}
func TestPrepareAdminCredentialsAcceptsBcryptMaxLengthPassword(t *testing.T) {
t.Parallel()
admin := AdminConfig{Email: "owner@example.com", Password: strings.Repeat("a", 72)}
if _, _, err := prepareAdminCredentials(&admin); err != nil {
t.Fatalf("prepareAdminCredentials() error = %v", err)
}
// 校验上限必须与 bcrypt 实际可哈希的上限一致,否则通过校验后仍会在建号时失败。
user := service.User{}
if err := user.SetPassword(admin.Password); err != nil {
t.Fatalf("SetPassword() with max-length password error = %v", err)
}
}
func expectAdminBootstrapCounts(mock sqlmock.Sqlmock, totalUsers, adminUsers int64) {
mock.ExpectQuery(regexp.QuoteMeta("SELECT COUNT(1) FROM users")).
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(totalUsers))
mock.ExpectQuery(regexp.QuoteMeta("SELECT COUNT(1) FROM users WHERE role = $1")).
WithArgs(service.RoleAdmin).
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(adminUsers))
}
func TestBootstrapAdminUserSkipsValidationWhenNotCreating(t *testing.T) {
t.Parallel()
tests := []struct {
name string
totalUsers int64
adminUsers int64
reason string
}{
{name: "admin exists", totalUsers: 3, adminUsers: 1, reason: adminBootstrapReasonAdminExists},
{name: "users exist without admin", totalUsers: 3, adminUsers: 0, reason: adminBootstrapReasonUsersExistWithoutAdmin},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("sqlmock.New() error = %v", err)
}
defer func() { _ = db.Close() }()
expectAdminBootstrapCounts(mock, tt.totalUsers, tt.adminUsers)
// 已有部署里遗留的弱密码/非法邮箱不能阻断启动。
cfg := &SetupConfig{Admin: AdminConfig{Email: "admin", Password: "123456"}}
created, reason, err := bootstrapAdminUser(context.Background(), db, cfg)
if err != nil || created || reason != tt.reason {
t.Fatalf("bootstrapAdminUser() = (%v, %q, %v), want (false, %q, nil)", created, reason, err, tt.reason)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatalf("unexpected database interaction: %v", err)
}
})
}
}
func TestBootstrapAdminUserRejectsWeakPasswordWithoutInsert(t *testing.T) {
t.Parallel()
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("sqlmock.New() error = %v", err)
}
defer func() { _ = db.Close() }()
expectAdminBootstrapCounts(mock, 0, 0)
cfg := &SetupConfig{Admin: AdminConfig{Password: "123456"}}
created, _, err := bootstrapAdminUser(context.Background(), db, cfg)
if err == nil || created || !strings.Contains(err.Error(), "invalid admin password") {
t.Fatalf("bootstrapAdminUser() = (%v, %v), want invalid admin password error", created, err)
}
// 未设置 INSERT 期望:若发生插入,sqlmock 会返回非预期调用错误,上面的错误断言即失败。
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatalf("unexpected database interaction: %v", err)
}
}
func TestBootstrapAdminUserCreatesAdminWithGeneratedCredentials(t *testing.T) {
t.Parallel()
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("sqlmock.New() error = %v", err)
}
defer func() { _ = db.Close() }()
expectAdminBootstrapCounts(mock, 0, 0)
mock.ExpectExec(regexp.QuoteMeta("INSERT INTO users")).
WithArgs(
sqlmock.AnyArg(), sqlmock.AnyArg(), service.RoleAdmin, sqlmock.AnyArg(),
sqlmock.AnyArg(), service.StatusActive, sqlmock.AnyArg(), sqlmock.AnyArg(),
).
WillReturnResult(sqlmock.NewResult(1, 1))
cfg := &SetupConfig{}
created, reason, err := bootstrapAdminUser(context.Background(), db, cfg)
if err != nil || !created || reason != adminBootstrapReasonEmptyDatabase {
t.Fatalf("bootstrapAdminUser() = (%v, %q, %v), want (true, %q, nil)", created, reason, err, adminBootstrapReasonEmptyDatabase)
}
if !regexp.MustCompile(`^admin-[0-9a-f]{12}@sub2api\.local$`).MatchString(cfg.Admin.Email) {
t.Fatalf("admin email = %q, want generated admin-<12 hex>@sub2api.local", cfg.Admin.Email)
}
if len(cfg.Admin.Password) != 32 {
t.Fatalf("admin password length = %d, want generated 32", len(cfg.Admin.Password))
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatalf("database expectations not met: %v", err)
}
}
func TestPrepareAdminCredentialsAcceptsMinimumLengthPassword(t *testing.T) {
t.Parallel()
admin := AdminConfig{Password: "12345678"}
emailGenerated, passwordGenerated, err := prepareAdminCredentials(&admin)
if err != nil {
t.Fatalf("prepareAdminCredentials() error = %v", err)
}
if !emailGenerated || passwordGenerated {
t.Fatalf("generated flags = (%v, %v), want (true, false)", emailGenerated, passwordGenerated)
}
if admin.Password != "12345678" {
t.Fatalf("password = %q, want unchanged", admin.Password)
}
}
+5 -3
View File
@@ -206,10 +206,12 @@ REDIS_ENABLE_TLS=false
# -----------------------------------------------------------------------------
# Admin Account
# -----------------------------------------------------------------------------
# Email for the admin account
ADMIN_EMAIL=admin@sub2api.local
# Email for the admin account (used as the login username)
# Leave empty to auto-generate a random one (will be shown in logs on first run).
# Avoid well-known values such as admin@sub2api.local: they are brute-force targets.
ADMIN_EMAIL=
# Password for admin account
# Password for admin account (8-72 bytes)
# Leave empty to auto-generate (will be shown in logs on first run)
ADMIN_PASSWORD=
+1 -1
View File
@@ -41,7 +41,7 @@ nano .env
./apple-container.sh status
```
Open `http://localhost:8080`. If `ADMIN_PASSWORD` is empty, retrieve the generated password with:
Open `http://localhost:8080`. If `ADMIN_EMAIL` / `ADMIN_PASSWORD` are empty, retrieve the generated admin email (login username) and password with:
```bash
./apple-container.sh logs app
+8 -8
View File
@@ -79,8 +79,8 @@ docker compose -f docker-compose.local.yml up -d
# View logs
docker compose -f docker-compose.local.yml logs -f sub2api
# If admin password was auto-generated, find it in logs:
docker compose -f docker-compose.local.yml logs sub2api | grep "admin password"
# If admin email/password were auto-generated, find them in logs:
docker compose -f docker-compose.local.yml logs sub2api | grep "Generated admin"
# Access Web UI
# http://localhost:8080
@@ -112,7 +112,7 @@ mkdir -p data postgres_data redis_data
# Start all services using local directory version
docker compose -f docker-compose.local.yml up -d
# View logs (check for auto-generated admin password)
# View logs (check for auto-generated admin email and password)
docker compose -f docker-compose.local.yml logs -f sub2api
# Access Web UI
@@ -136,14 +136,14 @@ When using Docker Compose with `AUTO_SETUP=true`:
- Connects to PostgreSQL and Redis
- Applies database migrations (SQL files in `backend/migrations/*.sql`) and records them in `schema_migrations`
- Generates JWT secret (if not provided)
- Creates admin account (password auto-generated if not provided)
- Creates admin account (email and password auto-generated if not provided; a provided password must be 8-72 bytes)
- Writes config.yaml
2. No manual Setup Wizard needed - just configure `.env` and start
3. If `ADMIN_PASSWORD` is not set, check logs for the generated password:
3. If `ADMIN_EMAIL` / `ADMIN_PASSWORD` are not set, check logs for the generated admin email (login username) and password:
```bash
docker compose logs sub2api | grep "admin password"
docker compose logs sub2api | grep "Generated admin"
```
### Startup and Database Recovery
@@ -257,8 +257,8 @@ docker compose down -v
| `JWT_SECRET` | **Recommended** | *(auto-generated)* | JWT secret (fixed for persistent sessions) |
| `TOTP_ENCRYPTION_KEY` | **Recommended** | *(auto-generated)* | TOTP encryption key (fixed for persistent 2FA) |
| `SERVER_PORT` | No | `8080` | Server port |
| `ADMIN_EMAIL` | No | `admin@sub2api.local` | Admin email |
| `ADMIN_PASSWORD` | No | *(auto-generated)* | Admin password |
| `ADMIN_EMAIL` | No | *(auto-generated)* | Admin email (login username) |
| `ADMIN_PASSWORD` | No | *(auto-generated)* | Admin password (8-72 bytes) |
| `TZ` | No | `Asia/Shanghai` | Timezone |
| `UPDATE_GITHUB_TOKEN` | No | *(empty)* | Token for `api.github.com` release checks only; asset downloads remain anonymous. |
| `GEMINI_OAUTH_CLIENT_ID` | No | *(builtin)* | Google OAuth client ID (Gemini OAuth). Leave empty to use the built-in Gemini CLI client. |
+1 -1
View File
@@ -43,7 +43,7 @@ services:
- REDIS_USERNAME=${REDIS_USERNAME:-}
- REDIS_PASSWORD=${REDIS_PASSWORD:-}
- REDIS_DB=${REDIS_DB:-0}
- ADMIN_EMAIL=${ADMIN_EMAIL:-admin@sub2api.local}
- ADMIN_EMAIL=${ADMIN_EMAIL:-}
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-}
- JWT_SECRET=${JWT_SECRET:-}
- SETUP_MIGRATION_TIMEOUT_SECONDS=${SETUP_MIGRATION_TIMEOUT_SECONDS:-0}
+1 -1
View File
@@ -88,7 +88,7 @@ services:
# =======================================================================
# Admin Account (auto-created on first run)
# =======================================================================
- ADMIN_EMAIL=${ADMIN_EMAIL:-admin@sub2api.local}
- ADMIN_EMAIL=${ADMIN_EMAIL:-}
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-}
# =======================================================================
+1 -1
View File
@@ -74,7 +74,7 @@ services:
# =======================================================================
# Admin Account (auto-created on first run)
# =======================================================================
- ADMIN_EMAIL=${ADMIN_EMAIL:-admin@sub2api.local}
- ADMIN_EMAIL=${ADMIN_EMAIL:-}
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-}
# =======================================================================
+1 -1
View File
@@ -85,7 +85,7 @@ services:
# =======================================================================
# Admin Account (auto-created on first run)
# =======================================================================
- ADMIN_EMAIL=${ADMIN_EMAIL:-admin@sub2api.local}
- ADMIN_EMAIL=${ADMIN_EMAIL:-}
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-}
# =======================================================================
+13
View File
@@ -5,6 +5,7 @@
# This script prepares deployment files for Sub2API:
# - Downloads docker-compose.local.yml and .env.example
# - Generates secure secrets (JWT_SECRET, TOTP_ENCRYPTION_KEY, POSTGRES_PASSWORD)
# - Generates a random admin login email (ADMIN_EMAIL)
# - Creates necessary data directories
#
# After running this script, you can start services with:
@@ -45,6 +46,14 @@ generate_secret() {
openssl rand -hex 32
}
# Generate a random admin login email so new installs never use a guessable default
generate_admin_email() {
local suffix
suffix=$(openssl rand -hex 6) || return 1
[ -n "$suffix" ] || return 1
echo "admin-${suffix}@sub2api.local"
}
# Check if command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
@@ -104,6 +113,7 @@ main() {
JWT_SECRET=$(generate_secret)
TOTP_ENCRYPTION_KEY=$(generate_secret)
POSTGRES_PASSWORD=$(generate_secret)
ADMIN_EMAIL=$(generate_admin_email)
# Create .env from .env.example
cp .env.example .env
@@ -114,11 +124,13 @@ main() {
sed -i "s/^JWT_SECRET=.*/JWT_SECRET=${JWT_SECRET}/" .env
sed -i "s/^TOTP_ENCRYPTION_KEY=.*/TOTP_ENCRYPTION_KEY=${TOTP_ENCRYPTION_KEY}/" .env
sed -i "s/^POSTGRES_PASSWORD=.*/POSTGRES_PASSWORD=${POSTGRES_PASSWORD}/" .env
sed -i "s/^ADMIN_EMAIL=.*/ADMIN_EMAIL=${ADMIN_EMAIL}/" .env
else
# BSD sed (macOS)
sed -i '' "s/^JWT_SECRET=.*/JWT_SECRET=${JWT_SECRET}/" .env
sed -i '' "s/^TOTP_ENCRYPTION_KEY=.*/TOTP_ENCRYPTION_KEY=${TOTP_ENCRYPTION_KEY}/" .env
sed -i '' "s/^POSTGRES_PASSWORD=.*/POSTGRES_PASSWORD=${POSTGRES_PASSWORD}/" .env
sed -i '' "s/^ADMIN_EMAIL=.*/ADMIN_EMAIL=${ADMIN_EMAIL}/" .env
fi
# Create data directories
@@ -139,6 +151,7 @@ main() {
echo " POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}"
echo " JWT_SECRET: ${JWT_SECRET}"
echo " TOTP_ENCRYPTION_KEY: ${TOTP_ENCRYPTION_KEY}"
echo " ADMIN_EMAIL: ${ADMIN_EMAIL} (admin login username)"
echo ""
print_warning "These credentials have been saved to .env file."
print_warning "Please keep them secure and do not share publicly!"
+3 -2
View File
@@ -29,7 +29,7 @@
"marked": "^17.0.1",
"pinia": "^2.1.7",
"qrcode": "^1.5.4",
"vue": "^3.4.0",
"vue": "^3.5.43",
"vue-chartjs": "^5.3.0",
"vue-draggable-plus": "^0.6.1",
"vue-i18n": "^9.14.5",
@@ -65,7 +65,8 @@
"js-cookie": "3.0.7",
"form-data@<4.0.6": ">=4.0.6",
"postcss@<8.5.18": ">=8.5.18",
"dompurify@<3.4.14": ">=3.4.14"
"dompurify@<3.4.14": ">=3.4.14",
"source-map-js@<1.2.2": ">=1.2.2"
}
}
}
+163 -87
View File
@@ -9,6 +9,7 @@ overrides:
form-data@<4.0.6: '>=4.0.6'
postcss@<8.5.18: '>=8.5.18'
dompurify@<3.4.14: '>=3.4.14'
source-map-js@<1.2.2: '>=1.2.2'
importers:
@@ -25,10 +26,10 @@ importers:
version: 9.0.1
'@tanstack/vue-virtual':
specifier: ^3.13.23
version: 3.13.23(vue@3.5.26(typescript@5.6.3))
version: 3.13.23(vue@3.5.43(typescript@5.6.3))
'@vueuse/core':
specifier: ^10.7.0
version: 10.11.1(vue@3.5.26(typescript@5.6.3))
version: 10.11.1(vue@3.5.43(typescript@5.6.3))
axios:
specifier: ^1.20.0
version: 1.20.0
@@ -49,25 +50,25 @@ importers:
version: 17.0.1
pinia:
specifier: ^2.1.7
version: 2.3.1(typescript@5.6.3)(vue@3.5.26(typescript@5.6.3))
version: 2.3.1(typescript@5.6.3)(vue@3.5.43(typescript@5.6.3))
qrcode:
specifier: ^1.5.4
version: 1.5.4
vue:
specifier: ^3.4.0
version: 3.5.26(typescript@5.6.3)
specifier: ^3.5.43
version: 3.5.43(typescript@5.6.3)
vue-chartjs:
specifier: ^5.3.0
version: 5.3.3(chart.js@4.5.1)(vue@3.5.26(typescript@5.6.3))
version: 5.3.3(chart.js@4.5.1)(vue@3.5.43(typescript@5.6.3))
vue-draggable-plus:
specifier: ^0.6.1
version: 0.6.1(@types/sortablejs@1.15.9)
vue-i18n:
specifier: ^9.14.5
version: 9.14.5(vue@3.5.26(typescript@5.6.3))
version: 9.14.5(vue@3.5.43(typescript@5.6.3))
vue-router:
specifier: ^4.2.5
version: 4.6.4(vue@3.5.26(typescript@5.6.3))
version: 4.6.4(vue@3.5.43(typescript@5.6.3))
xlsx:
specifier: ^0.18.5
version: 0.18.5
@@ -98,7 +99,7 @@ importers:
version: 7.18.0(eslint@8.57.1)(typescript@5.6.3)
'@vitejs/plugin-vue':
specifier: ^5.2.3
version: 5.2.4(vite@5.4.21(@types/node@20.19.27))(vue@3.5.26(typescript@5.6.3))
version: 5.2.4(vite@5.4.21(@types/node@20.19.27))(vue@3.5.43(typescript@5.6.3))
'@vitest/coverage-v8':
specifier: ^2.1.9
version: 2.1.9(vitest@2.1.9(@types/node@20.19.27)(jsdom@24.1.3))
@@ -216,15 +217,28 @@ packages:
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
engines: {node: '>=6.9.0'}
'@babel/helper-string-parser@7.29.7':
resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-identifier@7.28.5':
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-identifier@7.29.7':
resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
engines: {node: '>=6.9.0'}
'@babel/parser@7.28.5':
resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==}
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/parser@7.29.9':
resolution: {integrity: sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==}
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/runtime@7.28.4':
resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
engines: {node: '>=6.9.0'}
@@ -245,6 +259,10 @@ packages:
resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
engines: {node: '>=6.9.0'}
'@babel/types@7.29.8':
resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==}
engines: {node: '>=6.9.0'}
'@base-ui/react@1.3.0':
resolution: {integrity: sha512-FwpKqZbPz14AITp1CVgf4AjhKPe1OeeVKSBMdgD10zbFlj3QSWelmtCMLi2+/PFZZcIm3l87G7rwtCZJwHyXWA==}
engines: {node: '>=14.0.0'}
@@ -1692,14 +1710,20 @@ packages:
'@vue/compiler-core@3.5.26':
resolution: {integrity: sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==}
'@vue/compiler-core@3.5.43':
resolution: {integrity: sha512-zdiLhnbe1QQqgDT8xZMpNmyqZ3qlI+/Q/FHQco57Kwl/b05HhCzN6eVGN9QU9rbga4CrS0H5SYY8VGHZCt/1Hg==}
'@vue/compiler-dom@3.5.26':
resolution: {integrity: sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==}
'@vue/compiler-sfc@3.5.26':
resolution: {integrity: sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==}
'@vue/compiler-dom@3.5.43':
resolution: {integrity: sha512-PEZoAk3NQmsn/ejMzSOCyTYqwGqczrWm70PuhBKjjv1+TCoQAaO/zOqNwjV+honlNstT5ILxtc+8r8UUfj+iEQ==}
'@vue/compiler-ssr@3.5.26':
resolution: {integrity: sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==}
'@vue/compiler-sfc@3.5.43':
resolution: {integrity: sha512-FCbrG3XNCRl+js3huuKx4IVHBLTvMkJhVepjbxSPu1gn4yWLaYtGNQdjJGZaMytXB6qb76qQDDmDSLy/vkmleQ==}
'@vue/compiler-ssr@3.5.43':
resolution: {integrity: sha512-GF62orf7KiJX9RqrHNGrYBudsQGD0OhJ5nDs90O8UiDuS40+YMYomiXu6w6EuvtXuRDc3MSNis3EaaSKAVSWpg==}
'@vue/compiler-vue2@2.7.16':
resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==}
@@ -1715,23 +1739,24 @@ packages:
typescript:
optional: true
'@vue/reactivity@3.5.26':
resolution: {integrity: sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==}
'@vue/reactivity@3.5.43':
resolution: {integrity: sha512-G/c9GyOZNI2jVaaS6OX1EF1SSFSv7H0ERqNTl4+DTFMlZmB5eVAB53aLQNam/7NL2NPtaDD7RdVrzf8uJzMuOA==}
'@vue/runtime-core@3.5.26':
resolution: {integrity: sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q==}
'@vue/runtime-core@3.5.43':
resolution: {integrity: sha512-hU6U6VnVhBGQDpvlnnDlIB8ZGJBiOcgk2lh/0InltHiz3D8oSkluvuvY+do1G2H3+udeKFsmaBlgVYP7gXQzEw==}
'@vue/runtime-dom@3.5.26':
resolution: {integrity: sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ==}
'@vue/runtime-dom@3.5.43':
resolution: {integrity: sha512-Bb2Jc0YjjJdMt1SJmb9b2L/IWd3I8lIT9x9eS/xvvP9CiVgna0ffua74xKRmt4/uSJ+0r4iN8ex1jrqkhQGWQw==}
'@vue/server-renderer@3.5.26':
resolution: {integrity: sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==}
peerDependencies:
vue: 3.5.26
'@vue/server-renderer@3.5.43':
resolution: {integrity: sha512-l2Ygjv9NehV94PSBxNWsAHC0j/eIIKbn92mBuWXAPGLnn6HfJ6MH5ubsd+Nk0YoZ5FRuxWI1P2VSoh+dbPQhCQ==}
'@vue/shared@3.5.26':
resolution: {integrity: sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==}
'@vue/shared@3.5.43':
resolution: {integrity: sha512-uksS7YGMR5NZyr4JNq0Rp+QyLns0ueaz20KwzIPW9R0LH1Vnt4E+XUM29PNseEbf1www2gOuhuDi5AKOIXag9Q==}
'@vue/test-utils@2.4.6':
resolution: {integrity: sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==}
@@ -2342,6 +2367,10 @@ packages:
resolution: {integrity: sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==}
engines: {node: '>=0.12'}
entities@7.0.1:
resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
engines: {node: '>=0.12'}
error-ex@1.3.4:
resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
@@ -3304,6 +3333,11 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
nanoid@3.3.20:
resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
@@ -3532,6 +3566,10 @@ packages:
resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==}
engines: {node: ^10 || ^12 || >=14}
postcss@8.5.29:
resolution: {integrity: sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==}
engines: {node: ^10 || ^12 || >=14}
prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
@@ -3928,8 +3966,8 @@ packages:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'}
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
source-map-js@1.2.2:
resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
engines: {node: '>=0.10.0'}
source-map@0.5.7:
@@ -4390,8 +4428,8 @@ packages:
peerDependencies:
typescript: '>=5.0.0'
vue@3.5.26:
resolution: {integrity: sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==}
vue@3.5.43:
resolution: {integrity: sha512-o5qZoksdnjIKvW1srZ3ab7pcDNYAerBjRe54D0LBLfRdCYFrSgBHVXokMas35czQc0//lmx4/tuY4ZNQ+Rf2Ng==}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
@@ -4620,12 +4658,20 @@ snapshots:
'@babel/helper-string-parser@7.27.1': {}
'@babel/helper-string-parser@7.29.7': {}
'@babel/helper-validator-identifier@7.28.5': {}
'@babel/helper-validator-identifier@7.29.7': {}
'@babel/parser@7.28.5':
dependencies:
'@babel/types': 7.28.5
'@babel/parser@7.29.9':
dependencies:
'@babel/types': 7.29.8
'@babel/runtime@7.28.4': {}
'@babel/runtime@7.29.2': {}
@@ -4653,6 +4699,11 @@ snapshots:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5
'@babel/types@7.29.8':
dependencies:
'@babel/helper-string-parser': 7.29.7
'@babel/helper-validator-identifier': 7.29.7
'@base-ui/react@1.3.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
'@babel/runtime': 7.29.2
@@ -4988,7 +5039,7 @@ snapshots:
'@intlify/message-compiler@9.14.5':
dependencies:
'@intlify/shared': 9.14.5
source-map-js: 1.2.1
source-map-js: 1.2.2
'@intlify/shared@9.14.5': {}
@@ -5044,7 +5095,7 @@ snapshots:
'@lobehub/icons@4.0.2(@lobehub/ui@4.9.2)(@types/react@19.2.7)(antd@6.1.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
'@lobehub/ui': 4.9.2(@lobehub/fluent-emoji@4.1.0(@types/react@19.2.7)(antd@6.1.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@lobehub/icons@4.0.2)(@types/mdast@4.0.4)(@types/react@19.2.7)(antd@6.1.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(micromark-util-types@2.0.2)(micromark@4.0.2)(motion@12.23.26(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vue@3.5.26(typescript@5.6.3))
'@lobehub/ui': 4.9.2(@lobehub/fluent-emoji@4.1.0(@types/react@19.2.7)(antd@6.1.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@lobehub/icons@4.0.2)(@types/mdast@4.0.4)(@types/react@19.2.7)(antd@6.1.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(micromark-util-types@2.0.2)(micromark@4.0.2)(motion@12.23.26(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vue@3.5.43(typescript@5.6.3))
antd: 6.1.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
antd-style: 4.1.0(@types/react@19.2.7)(antd@6.1.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
lucide-react: 0.469.0(react@19.2.3)
@@ -5055,7 +5106,7 @@ snapshots:
- '@types/react'
- supports-color
'@lobehub/ui@4.9.2(@lobehub/fluent-emoji@4.1.0(@types/react@19.2.7)(antd@6.1.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@lobehub/icons@4.0.2)(@types/mdast@4.0.4)(@types/react@19.2.7)(antd@6.1.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(micromark-util-types@2.0.2)(micromark@4.0.2)(motion@12.23.26(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vue@3.5.26(typescript@5.6.3))':
'@lobehub/ui@4.9.2(@lobehub/fluent-emoji@4.1.0(@types/react@19.2.7)(antd@6.1.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@lobehub/icons@4.0.2)(@types/mdast@4.0.4)(@types/react@19.2.7)(antd@6.1.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(micromark-util-types@2.0.2)(micromark@4.0.2)(motion@12.23.26(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vue@3.5.43(typescript@5.6.3))':
dependencies:
'@ant-design/cssinjs': 2.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@base-ui/react': 1.3.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -5120,7 +5171,7 @@ snapshots:
remark-github: 12.0.0
remark-math: 6.0.0
shiki: 3.23.0
shiki-stream: 0.1.4(react@19.2.3)(vue@3.5.26(typescript@5.6.3))
shiki-stream: 0.1.4(react@19.2.3)(vue@3.5.43(typescript@5.6.3))
swr: 2.4.1(react@19.2.3)
ts-md5: 2.0.1
unified: 11.0.5
@@ -5872,10 +5923,10 @@ snapshots:
'@tanstack/virtual-core@3.13.23': {}
'@tanstack/vue-virtual@3.13.23(vue@3.5.26(typescript@5.6.3))':
'@tanstack/vue-virtual@3.13.23(vue@3.5.43(typescript@5.6.3))':
dependencies:
'@tanstack/virtual-core': 3.13.23
vue: 3.5.26(typescript@5.6.3)
vue: 3.5.43(typescript@5.6.3)
'@types/d3-array@3.2.2': {}
@@ -6147,10 +6198,10 @@ snapshots:
'@use-gesture/core': 10.3.1
react: 19.2.3
'@vitejs/plugin-vue@5.2.4(vite@5.4.21(@types/node@20.19.27))(vue@3.5.26(typescript@5.6.3))':
'@vitejs/plugin-vue@5.2.4(vite@5.4.21(@types/node@20.19.27))(vue@3.5.43(typescript@5.6.3))':
dependencies:
vite: 5.4.21(@types/node@20.19.27)
vue: 3.5.26(typescript@5.6.3)
vue: 3.5.43(typescript@5.6.3)
'@vitest/coverage-v8@2.1.9(vitest@2.1.9(@types/node@20.19.27)(jsdom@24.1.3))':
dependencies:
@@ -6228,29 +6279,42 @@ snapshots:
'@vue/shared': 3.5.26
entities: 7.0.0
estree-walker: 2.0.2
source-map-js: 1.2.1
source-map-js: 1.2.2
'@vue/compiler-core@3.5.43':
dependencies:
'@babel/parser': 7.29.9
'@vue/shared': 3.5.43
entities: 7.0.1
estree-walker: 2.0.2
source-map-js: 1.2.2
'@vue/compiler-dom@3.5.26':
dependencies:
'@vue/compiler-core': 3.5.26
'@vue/shared': 3.5.26
'@vue/compiler-sfc@3.5.26':
'@vue/compiler-dom@3.5.43':
dependencies:
'@babel/parser': 7.28.5
'@vue/compiler-core': 3.5.26
'@vue/compiler-dom': 3.5.26
'@vue/compiler-ssr': 3.5.26
'@vue/shared': 3.5.26
'@vue/compiler-core': 3.5.43
'@vue/shared': 3.5.43
'@vue/compiler-sfc@3.5.43':
dependencies:
'@babel/parser': 7.29.9
'@vue/compiler-core': 3.5.43
'@vue/compiler-dom': 3.5.43
'@vue/compiler-ssr': 3.5.43
'@vue/shared': 3.5.43
estree-walker: 2.0.2
magic-string: 0.30.21
postcss: 8.5.23
source-map-js: 1.2.1
postcss: 8.5.29
source-map-js: 1.2.2
'@vue/compiler-ssr@3.5.26':
'@vue/compiler-ssr@3.5.43':
dependencies:
'@vue/compiler-dom': 3.5.26
'@vue/shared': 3.5.26
'@vue/compiler-dom': 3.5.43
'@vue/shared': 3.5.43
'@vue/compiler-vue2@2.7.16':
dependencies:
@@ -6272,50 +6336,52 @@ snapshots:
optionalDependencies:
typescript: 5.6.3
'@vue/reactivity@3.5.26':
'@vue/reactivity@3.5.43':
dependencies:
'@vue/shared': 3.5.26
'@vue/shared': 3.5.43
'@vue/runtime-core@3.5.26':
'@vue/runtime-core@3.5.43':
dependencies:
'@vue/reactivity': 3.5.26
'@vue/shared': 3.5.26
'@vue/reactivity': 3.5.43
'@vue/shared': 3.5.43
'@vue/runtime-dom@3.5.26':
'@vue/runtime-dom@3.5.43':
dependencies:
'@vue/reactivity': 3.5.26
'@vue/runtime-core': 3.5.26
'@vue/shared': 3.5.26
'@vue/reactivity': 3.5.43
'@vue/runtime-core': 3.5.43
'@vue/shared': 3.5.43
csstype: 3.2.3
'@vue/server-renderer@3.5.26(vue@3.5.26(typescript@5.6.3))':
'@vue/server-renderer@3.5.43':
dependencies:
'@vue/compiler-ssr': 3.5.26
'@vue/shared': 3.5.26
vue: 3.5.26(typescript@5.6.3)
'@vue/compiler-ssr': 3.5.43
'@vue/runtime-dom': 3.5.43
'@vue/shared': 3.5.43
'@vue/shared@3.5.26': {}
'@vue/shared@3.5.43': {}
'@vue/test-utils@2.4.6':
dependencies:
js-beautify: 1.15.4
vue-component-type-helpers: 2.2.12
'@vueuse/core@10.11.1(vue@3.5.26(typescript@5.6.3))':
'@vueuse/core@10.11.1(vue@3.5.43(typescript@5.6.3))':
dependencies:
'@types/web-bluetooth': 0.0.20
'@vueuse/metadata': 10.11.1
'@vueuse/shared': 10.11.1(vue@3.5.26(typescript@5.6.3))
vue-demi: 0.14.10(vue@3.5.26(typescript@5.6.3))
'@vueuse/shared': 10.11.1(vue@3.5.43(typescript@5.6.3))
vue-demi: 0.14.10(vue@3.5.43(typescript@5.6.3))
transitivePeerDependencies:
- '@vue/composition-api'
- vue
'@vueuse/metadata@10.11.1': {}
'@vueuse/shared@10.11.1(vue@3.5.26(typescript@5.6.3))':
'@vueuse/shared@10.11.1(vue@3.5.43(typescript@5.6.3))':
dependencies:
vue-demi: 0.14.10(vue@3.5.26(typescript@5.6.3))
vue-demi: 0.14.10(vue@3.5.43(typescript@5.6.3))
transitivePeerDependencies:
- '@vue/composition-api'
- vue
@@ -6969,6 +7035,8 @@ snapshots:
entities@7.0.0: {}
entities@7.0.1: {}
error-ex@1.3.4:
dependencies:
is-arrayish: 0.2.1
@@ -7812,7 +7880,7 @@ snapshots:
dependencies:
'@babel/parser': 7.28.5
'@babel/types': 7.28.5
source-map-js: 1.2.1
source-map-js: 1.2.2
make-dir@4.0.0:
dependencies:
@@ -8400,6 +8468,8 @@ snapshots:
nanoid@3.3.18: {}
nanoid@3.3.20: {}
natural-compare@1.4.0: {}
node-releases@2.0.27: {}
@@ -8532,11 +8602,11 @@ snapshots:
pify@2.3.0: {}
pinia@2.3.1(typescript@5.6.3)(vue@3.5.26(typescript@5.6.3)):
pinia@2.3.1(typescript@5.6.3)(vue@3.5.43(typescript@5.6.3)):
dependencies:
'@vue/devtools-api': 6.6.4
vue: 3.5.26(typescript@5.6.3)
vue-demi: 0.14.10(vue@3.5.26(typescript@5.6.3))
vue: 3.5.43(typescript@5.6.3)
vue-demi: 0.14.10(vue@3.5.43(typescript@5.6.3))
optionalDependencies:
typescript: 5.6.3
transitivePeerDependencies:
@@ -8598,7 +8668,13 @@ snapshots:
dependencies:
nanoid: 3.3.18
picocolors: 1.1.1
source-map-js: 1.2.1
source-map-js: 1.2.2
postcss@8.5.29:
dependencies:
nanoid: 3.3.20
picocolors: 1.1.1
source-map-js: 1.2.2
prelude-ls@1.2.1: {}
@@ -9080,12 +9156,12 @@ snapshots:
shebang-regex@3.0.0: {}
shiki-stream@0.1.4(react@19.2.3)(vue@3.5.26(typescript@5.6.3)):
shiki-stream@0.1.4(react@19.2.3)(vue@3.5.43(typescript@5.6.3)):
dependencies:
'@shikijs/core': 3.23.0
optionalDependencies:
react: 19.2.3
vue: 3.5.26(typescript@5.6.3)
vue: 3.5.43(typescript@5.6.3)
shiki@3.23.0:
dependencies:
@@ -9104,7 +9180,7 @@ snapshots:
slash@3.0.0: {}
source-map-js@1.2.1: {}
source-map-js@1.2.2: {}
source-map@0.5.7: {}
@@ -9504,16 +9580,16 @@ snapshots:
vscode-uri@3.1.0: {}
vue-chartjs@5.3.3(chart.js@4.5.1)(vue@3.5.26(typescript@5.6.3)):
vue-chartjs@5.3.3(chart.js@4.5.1)(vue@3.5.43(typescript@5.6.3)):
dependencies:
chart.js: 4.5.1
vue: 3.5.26(typescript@5.6.3)
vue: 3.5.43(typescript@5.6.3)
vue-component-type-helpers@2.2.12: {}
vue-demi@0.14.10(vue@3.5.26(typescript@5.6.3)):
vue-demi@0.14.10(vue@3.5.43(typescript@5.6.3)):
dependencies:
vue: 3.5.26(typescript@5.6.3)
vue: 3.5.43(typescript@5.6.3)
vue-draggable-plus@0.6.1(@types/sortablejs@1.15.9):
dependencies:
@@ -9532,17 +9608,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
vue-i18n@9.14.5(vue@3.5.26(typescript@5.6.3)):
vue-i18n@9.14.5(vue@3.5.43(typescript@5.6.3)):
dependencies:
'@intlify/core-base': 9.14.5
'@intlify/shared': 9.14.5
'@vue/devtools-api': 6.6.4
vue: 3.5.26(typescript@5.6.3)
vue: 3.5.43(typescript@5.6.3)
vue-router@4.6.4(vue@3.5.26(typescript@5.6.3)):
vue-router@4.6.4(vue@3.5.43(typescript@5.6.3)):
dependencies:
'@vue/devtools-api': 6.6.4
vue: 3.5.26(typescript@5.6.3)
vue: 3.5.43(typescript@5.6.3)
vue-tsc@2.2.12(typescript@5.6.3):
dependencies:
@@ -9550,13 +9626,13 @@ snapshots:
'@vue/language-core': 2.2.12(typescript@5.6.3)
typescript: 5.6.3
vue@3.5.26(typescript@5.6.3):
vue@3.5.43(typescript@5.6.3):
dependencies:
'@vue/compiler-dom': 3.5.26
'@vue/compiler-sfc': 3.5.26
'@vue/runtime-dom': 3.5.26
'@vue/server-renderer': 3.5.26(vue@3.5.26(typescript@5.6.3))
'@vue/shared': 3.5.26
'@vue/compiler-dom': 3.5.43
'@vue/compiler-sfc': 3.5.43
'@vue/runtime-dom': 3.5.43
'@vue/server-renderer': 3.5.43
'@vue/shared': 3.5.43
optionalDependencies:
typescript: 5.6.3
+4 -5
View File
@@ -1045,7 +1045,7 @@ ${codexModelCatalogMode.value === 'remote' ? `model_catalog_url = "${escapeTomlB
${generateCodexProviderAuthConfig(apiKey)}
[features]
goals = true`
${codexModelCatalogMode.value === 'remote' ? 'api_key_model_discovery = true\n' : ''}goals = true`
return buildOpenAICodexFileConfigs(configDir, configContent, apiKey)
}
@@ -1288,8 +1288,7 @@ requires_openai_auth = false
# Grok/Sub2API path is HTTP/SSE; disable WS (Codex may otherwise try WebSocket first)
supports_websockets = false
# Optional:
# [features]
${codexModelCatalogMode.value === 'remote' ? '[features]\napi_key_model_discovery = true\n\n# Optional:' : '# Optional:\n# [features]'}
# goals = true`
return [
@@ -1355,7 +1354,7 @@ base_url = "${baseUrl}"
${codexModelCatalogMode.value === 'remote' ? `model_catalog_url = "${escapeTomlBasicString(buildCodexModelCatalogUrl(baseUrl))}"\n` : ''}env_key = "SUB2API_API_KEY"
wire_api = "responses"
requires_openai_auth = false
supports_websockets = false`
supports_websockets = false${codexModelCatalogMode.value === 'remote' ? '\n\n[features]\napi_key_model_discovery = true' : ''}`
return [
{ path: isWindows ? 'PowerShell' : 'Terminal', content: envContent },
@@ -1393,7 +1392,7 @@ supports_websockets = true
${generateCodexProviderAuthConfig(apiKey)}
[features]
responses_websockets_v2 = true
${codexModelCatalogMode.value === 'remote' ? 'api_key_model_discovery = true\n' : ''}responses_websockets_v2 = true
goals = true`
return buildOpenAICodexFileConfigs(configDir, configContent, apiKey)