fix(auth): 为登录链路增加有界超时
ci / verify (pull_request) Successful in 10m14s

为 PostgreSQL 连接、就绪检查和本地登录设置分层超时,数据库不可用时返回稳定 503 错误码并记录无凭据的连接池统计。

前端登录在 10 秒后取消请求,Nginx 登录精确路由限制上游为 15 秒,同时更新 OpenAPI 和回归测试。

补充开发工具链安全 override,使高危依赖审计保持为零。
This commit is contained in:
2026-07-21 10:57:51 +08:00
parent 4f45f38b8e
commit bc1ff2b62f
11 changed files with 366 additions and 42 deletions
+6
View File
@@ -5054,6 +5054,12 @@
"schema": {
"$ref": "#/definitions/httpapi.ErrorEnvelope"
}
},
"503": {
"description": "Service Unavailable",
"schema": {
"$ref": "#/definitions/httpapi.ErrorEnvelope"
}
}
}
}
+4
View File
@@ -6219,6 +6219,10 @@ paths:
description: Internal Server Error
schema:
$ref: '#/definitions/httpapi.ErrorEnvelope'
"503":
description: Service Unavailable
schema:
$ref: '#/definitions/httpapi.ErrorEnvelope'
summary: 本地登录
tags:
- auth
@@ -0,0 +1,111 @@
package httpapi
import (
"bytes"
"context"
"encoding/json"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"net/url"
"os"
"strings"
"testing"
"time"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
func TestReadyReturnsPostgresUnavailableWithinTwoSeconds(t *testing.T) {
db := newExhaustedPostgresStore(t)
server := &Server{store: db, logger: slog.New(slog.NewJSONHandler(io.Discard, nil))}
requestContext, cancel := context.WithTimeout(context.Background(), 4*time.Second)
defer cancel()
request := httptest.NewRequest(http.MethodGet, "/readyz", nil).WithContext(requestContext)
recorder := httptest.NewRecorder()
startedAt := time.Now()
server.ready(recorder, request)
elapsed := time.Since(startedAt)
assertUnavailableResponse(t, recorder, "POSTGRES_UNAVAILABLE", "postgres unavailable")
if elapsed > 3*time.Second {
t.Fatalf("readiness timeout took %s, want no more than 3s", elapsed)
}
}
func TestLoginReturnsAuthStoreUnavailableWithinFiveSeconds(t *testing.T) {
db := newExhaustedPostgresStore(t)
var logs bytes.Buffer
server := &Server{store: db, logger: slog.New(slog.NewJSONHandler(&logs, nil))}
requestContext, cancel := context.WithTimeout(context.Background(), 7*time.Second)
defer cancel()
request := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", strings.NewReader(`{"account":"timeout-test-account","password":"timeout-test-password"}`)).WithContext(requestContext)
recorder := httptest.NewRecorder()
startedAt := time.Now()
server.login(recorder, request)
elapsed := time.Since(startedAt)
assertUnavailableResponse(t, recorder, "AUTH_STORE_UNAVAILABLE", "authentication service temporarily unavailable")
if elapsed > 6*time.Second {
t.Fatalf("login timeout took %s, want no more than 6s", elapsed)
}
logOutput := logs.String()
for _, field := range []string{"postgres_pool_max_connections", "postgres_pool_acquired_connections", "postgres_pool_idle_connections", "postgres_pool_empty_acquire_count", "postgres_pool_canceled_acquire_count"} {
if !strings.Contains(logOutput, field) {
t.Fatalf("login failure log did not include %q: %s", field, logOutput)
}
}
if strings.Contains(logOutput, "timeout-test-account") || strings.Contains(logOutput, "timeout-test-password") {
t.Fatalf("login failure log exposed credentials: %s", logOutput)
}
}
func newExhaustedPostgresStore(t *testing.T) *store.Store {
t.Helper()
databaseURL := strings.TrimSpace(os.Getenv("AI_GATEWAY_TEST_DATABASE_URL"))
if databaseURL == "" {
t.Skip("set AI_GATEWAY_TEST_DATABASE_URL to run PostgreSQL availability timeout tests")
}
parsed, err := url.Parse(databaseURL)
if err != nil {
t.Fatalf("parse test database URL: %v", err)
}
query := parsed.Query()
query.Set("pool_max_conns", "1")
query.Set("pool_min_conns", "0")
parsed.RawQuery = query.Encode()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
db, err := store.Connect(ctx, parsed.String())
if err != nil {
t.Fatalf("connect timeout test store: %v", err)
}
t.Cleanup(db.Close)
connection, err := db.Pool().Acquire(ctx)
if err != nil {
t.Fatalf("exhaust timeout test pool: %v", err)
}
t.Cleanup(connection.Release)
return db
}
func assertUnavailableResponse(t *testing.T, recorder *httptest.ResponseRecorder, expectedCode, expectedMessage string) {
t.Helper()
if recorder.Code != http.StatusServiceUnavailable {
t.Fatalf("status = %d, want 503; body=%s", recorder.Code, recorder.Body.String())
}
var envelope ErrorEnvelope
if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
t.Fatalf("decode unavailable response: %v", err)
}
if envelope.Error.Code != expectedCode {
t.Fatalf("error code = %q, want %q; body=%s", envelope.Error.Code, expectedCode, recorder.Body.String())
}
if envelope.Error.Message != expectedMessage {
t.Fatalf("error message = %q, want %q; body=%s", envelope.Error.Message, expectedMessage, recorder.Body.String())
}
}
+38 -3
View File
@@ -17,6 +17,14 @@ import (
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
const (
postgresReadinessTimeout = 2 * time.Second
localLoginStoreTimeout = 5 * time.Second
errorCodePostgresDown = "POSTGRES_UNAVAILABLE"
errorCodeAuthStoreDown = "AUTH_STORE_UNAVAILABLE"
authStoreUnavailableError = "authentication service temporarily unavailable"
)
// health godoc
// @Summary 健康检查
// @Description 返回服务进程、运行环境和身份模式,供负载均衡或人工排障使用。
@@ -42,8 +50,11 @@ func (s *Server) health(w http.ResponseWriter, r *http.Request) {
// @Failure 503 {object} ErrorEnvelope
// @Router /readyz [get]
func (s *Server) ready(w http.ResponseWriter, r *http.Request) {
if err := s.store.Ping(r.Context()); err != nil {
writeError(w, http.StatusServiceUnavailable, "postgres unavailable")
ctx, cancel := context.WithTimeout(r.Context(), postgresReadinessTimeout)
defer cancel()
if err := s.store.Ping(ctx); err != nil {
s.logPostgresUnavailable("postgres readiness check failed")
writeError(w, http.StatusServiceUnavailable, "postgres unavailable", errorCodePostgresDown)
return
}
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
@@ -121,6 +132,7 @@ func (s *Server) register(w http.ResponseWriter, r *http.Request) {
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Failure 503 {object} ErrorEnvelope
// @Router /api/v1/auth/login [post]
func (s *Server) login(w http.ResponseWriter, r *http.Request) {
var input store.LocalLoginInput
@@ -128,12 +140,19 @@ func (s *Server) login(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
user, err := s.store.AuthenticateLocalUser(r.Context(), input)
ctx, cancel := context.WithTimeout(r.Context(), localLoginStoreTimeout)
defer cancel()
user, err := s.store.AuthenticateLocalUser(ctx, input)
if err != nil {
if errors.Is(err, store.ErrInvalidCredentials) {
writeError(w, http.StatusUnauthorized, "invalid account or password")
return
}
if store.IsPostgresUnavailable(err) {
s.logPostgresUnavailable("login authentication store unavailable")
writeError(w, http.StatusServiceUnavailable, authStoreUnavailableError, errorCodeAuthStoreDown)
return
}
s.logger.Error("login local user failed", "error", err)
writeError(w, http.StatusInternalServerError, "login failed")
return
@@ -145,6 +164,22 @@ func (s *Server) login(w http.ResponseWriter, r *http.Request) {
s.writeAuthResponse(w, http.StatusOK, user)
}
func (s *Server) logPostgresUnavailable(message string) {
if s.logger == nil || s.store == nil || s.store.Pool() == nil {
return
}
statistics := s.store.Pool().Stat()
s.logger.Error(message,
"error_category", "postgres_unavailable",
"postgres_pool_max_connections", statistics.MaxConns(),
"postgres_pool_total_connections", statistics.TotalConns(),
"postgres_pool_acquired_connections", statistics.AcquiredConns(),
"postgres_pool_idle_connections", statistics.IdleConns(),
"postgres_pool_empty_acquire_count", statistics.EmptyAcquireCount(),
"postgres_pool_canceled_acquire_count", statistics.CanceledAcquireCount(),
)
}
func (s *Server) localIdentityEnabled() bool {
mode := strings.ToLower(strings.TrimSpace(s.cfg.IdentityMode))
return mode == "" || mode == "standalone" || mode == "hybrid"
+43 -1
View File
@@ -7,6 +7,7 @@ import (
"encoding/base64"
"encoding/json"
"errors"
"net"
"strings"
"time"
"unicode"
@@ -22,6 +23,11 @@ type Store struct {
pool *pgxpool.Pool
}
const (
postgresApplicationName = "easyai-ai-gateway"
postgresConnectTimeout = 5 * time.Second
)
func defaultAPIKeyScopes() []string {
return []string{"chat", "embedding", "rerank", "image", "video", "music", "audio", "voice_clone"}
}
@@ -60,7 +66,11 @@ var (
)
func Connect(ctx context.Context, databaseURL string) (*Store, error) {
pool, err := pgxpool.New(ctx, databaseURL)
config, err := postgresPoolConfig(databaseURL)
if err != nil {
return nil, err
}
pool, err := pgxpool.NewWithConfig(ctx, config)
if err != nil {
return nil, err
}
@@ -71,6 +81,38 @@ func Connect(ctx context.Context, databaseURL string) (*Store, error) {
return &Store{pool: pool}, nil
}
func postgresPoolConfig(databaseURL string) (*pgxpool.Config, error) {
config, err := pgxpool.ParseConfig(databaseURL)
if err != nil {
return nil, err
}
config.ConnConfig.ConnectTimeout = postgresConnectTimeout
config.ConnConfig.RuntimeParams["application_name"] = postgresApplicationName
return config, nil
}
func IsPostgresUnavailable(err error) bool {
if err == nil {
return false
}
if errors.Is(err, context.DeadlineExceeded) {
return true
}
var connectError *pgconn.ConnectError
if errors.As(err, &connectError) {
return true
}
var networkError net.Error
if errors.As(err, &networkError) {
return true
}
var postgresError *pgconn.PgError
if errors.As(err, &postgresError) {
return strings.HasPrefix(postgresError.Code, "08") || postgresError.Code == "53300" || strings.HasPrefix(postgresError.Code, "57P0")
}
return pgconn.SafeToRetry(err)
}
func (s *Store) Close() {
s.pool.Close()
}
@@ -0,0 +1,49 @@
package store
import (
"context"
"testing"
"time"
"github.com/jackc/pgx/v5/pgconn"
)
func TestPostgresPoolConfigSetsDiagnosticAndConnectTimeout(t *testing.T) {
config, err := postgresPoolConfig("postgresql://gateway:password@localhost:5432/gateway?sslmode=disable")
if err != nil {
t.Fatalf("parse PostgreSQL pool config: %v", err)
}
if config.ConnConfig.ConnectTimeout != 5*time.Second {
t.Fatalf("connect timeout = %s, want 5s", config.ConnConfig.ConnectTimeout)
}
if applicationName := config.ConnConfig.RuntimeParams["application_name"]; applicationName != "easyai-ai-gateway" {
t.Fatalf("application_name = %q, want easyai-ai-gateway", applicationName)
}
}
func TestPostgresPoolConfigRejectsMalformedURL(t *testing.T) {
if _, err := postgresPoolConfig("://malformed"); err == nil {
t.Fatal("expected malformed PostgreSQL URL to fail")
}
}
func TestIsPostgresUnavailableClassifiesConnectivityFailures(t *testing.T) {
for _, testCase := range []struct {
name string
err error
}{
{name: "deadline", err: context.DeadlineExceeded},
{name: "connection exception", err: &pgconn.PgError{Code: "08006"}},
{name: "too many connections", err: &pgconn.PgError{Code: "53300"}},
{name: "cannot connect now", err: &pgconn.PgError{Code: "57P03"}},
} {
t.Run(testCase.name, func(t *testing.T) {
if !IsPostgresUnavailable(testCase.err) {
t.Fatalf("error %v was not classified as PostgreSQL unavailable", testCase.err)
}
})
}
if IsPostgresUnavailable(&pgconn.PgError{Code: "42601"}) {
t.Fatal("SQL syntax error was incorrectly classified as PostgreSQL unavailable")
}
}
+27
View File
@@ -9,12 +9,39 @@ import {
getAPITask,
getCurrentUser,
getOpsManagementSkillMetadata,
loginLocalAccount,
OIDC_BROWSER_SESSION_CREDENTIAL,
startIdentityPairing,
retireIdentityPairingSecurityEventConflict,
validateIdentityRevision,
} from './api';
describe('local login transport', () => {
afterEach(() => {
vi.useRealTimers();
vi.unstubAllGlobals();
});
it('aborts after ten seconds and returns a stable login timeout message', async () => {
vi.useFakeTimers();
const fetchMock = vi.fn((_url: string, init?: RequestInit) => new Promise<Response>((_resolve, reject) => {
init?.signal?.addEventListener('abort', () => reject(new DOMException('aborted', 'AbortError')));
}));
vi.stubGlobal('fetch', fetchMock);
const login = loginLocalAccount({ account: 'timeout-test-account', password: 'timeout-test-password' });
const rejection = expect(login).rejects.toMatchObject({
message: '登录请求超时,请稍后重试',
});
await vi.advanceTimersByTimeAsync(10_000);
await rejection;
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit];
expect(init.signal).toBeInstanceOf(AbortSignal);
expect(init.signal?.aborted).toBe(true);
});
});
describe('Gateway provisioning errors', () => {
const cases = [
['GATEWAY_USER_NOT_PROVISIONED', '该账号尚未开通 EasyAI Gateway'],
+27 -14
View File
@@ -119,6 +119,7 @@ export async function loginLocalAccount(input: { account: string; password: stri
auth: false,
body: input,
method: 'POST',
timeoutMs: 10_000,
});
}
@@ -1180,7 +1181,7 @@ export async function deleteFileStorageChannel(token: string, channelId: string)
async function request<T>(
path: string,
options: { token?: string; auth?: boolean; method?: string; body?: unknown; headers?: Record<string, string> } = {},
options: { token?: string; auth?: boolean; method?: string; body?: unknown; headers?: Record<string, string>; timeoutMs?: number } = {},
): Promise<T> {
const headers: Record<string, string> = { ...(options.headers ?? {}) };
if (options.auth !== false && options.token && options.token !== OIDC_BROWSER_SESSION_CREDENTIAL) {
@@ -1189,20 +1190,32 @@ async function request<T>(
if (options.body !== undefined) {
headers['Content-Type'] = 'application/json';
}
const response = await fetch(`${API_BASE}${path}`, {
method: options.method ?? 'GET',
headers,
body: options.body === undefined ? undefined : JSON.stringify(options.body),
credentials: 'include',
});
if (!response.ok) {
const body = await response.text();
throw new GatewayApiError(parseErrorDetails(body, response.status, `Request failed: ${response.status}`));
const controller = options.timeoutMs ? new AbortController() : undefined;
const timeout = controller ? globalThis.setTimeout(() => controller.abort(), options.timeoutMs) : undefined;
try {
const response = await fetch(`${API_BASE}${path}`, {
method: options.method ?? 'GET',
headers,
body: options.body === undefined ? undefined : JSON.stringify(options.body),
credentials: 'include',
signal: controller?.signal,
});
if (!response.ok) {
const body = await response.text();
throw new GatewayApiError(parseErrorDetails(body, response.status, `Request failed: ${response.status}`));
}
if (response.status === 204) {
return undefined as T;
}
return response.json() as Promise<T>;
} catch (error) {
if (controller?.signal.aborted) {
throw new GatewayApiError('登录请求超时,请稍后重试');
}
throw error;
} finally {
if (timeout !== undefined) globalThis.clearTimeout(timeout);
}
if (response.status === 204) {
return undefined as T;
}
return response.json() as Promise<T>;
}
function authorizationHeader(token: string): Record<string, string> {
+14
View File
@@ -33,6 +33,20 @@ server {
return 404;
}
location = /gateway-api/api/v1/auth/login {
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";
proxy_connect_timeout 3s;
proxy_read_timeout 15s;
proxy_send_timeout 15s;
proxy_redirect off;
proxy_pass http://api:8088/api/v1/auth/login;
}
location /gateway-api/ {
proxy_http_version 1.1;
proxy_set_header Host $host;
+43 -23
View File
@@ -6,10 +6,13 @@ settings:
overrides:
'@babel/core@<=7.29.0': 7.29.6
axios@>=1.0.0 <1.18.0: 1.18.0
brace-expansion@>=2.0.0 <2.1.2: 2.1.2
dompurify@<=3.4.10: 3.4.11
esbuild@>=0.27.3 <0.28.1: 0.28.1
form-data@>=4.0.0 <4.0.6: 4.0.6
js-yaml@<3.15.0: 4.1.1
js-yaml@<3.15.0: 4.3.0
js-yaml@>=4.0.0 <4.3.0: 4.3.0
mermaid@>=11.0.0-alpha.1 <=11.14.0: 11.15.0
minimatch@>=9.0.0 <9.0.7: 9.0.7
'@nx/js>picomatch': 4.0.5
@@ -2443,6 +2446,10 @@ packages:
resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==}
engines: {node: '>= 10.0.0'}
agent-base@6.0.2:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'}
ajv@8.20.0:
resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==}
@@ -2491,8 +2498,8 @@ packages:
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
axios@1.16.0:
resolution: {integrity: sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==}
axios@1.18.0:
resolution: {integrity: sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==}
babel-plugin-const-enum@1.2.0:
resolution: {integrity: sha512-o1m/6iyyFnp9MRsK1dHF3bneqyf3AlM2q3A/YbgQr2pCat6B6XJVDv2TXqzfY2RYUi4mak6WAksSBPlyYGx9dg==}
@@ -2553,8 +2560,8 @@ packages:
bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
brace-expansion@2.1.0:
resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==}
brace-expansion@2.1.2:
resolution: {integrity: sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==}
brace-expansion@5.0.7:
resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==}
@@ -3124,10 +3131,6 @@ packages:
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
engines: {node: '>= 0.4'}
hasown@2.0.3:
resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==}
engines: {node: '>= 0.4'}
hasown@2.0.4:
resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==}
engines: {node: '>= 0.4'}
@@ -3184,6 +3187,10 @@ packages:
html-void-elements@3.0.0:
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
https-proxy-agent@5.0.1:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'}
iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
@@ -3281,8 +3288,8 @@ packages:
js-tokens@9.0.1:
resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
js-yaml@4.1.1:
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
js-yaml@4.3.0:
resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==}
hasBin: true
jsesc@3.1.0:
@@ -5820,6 +5827,7 @@ snapshots:
- '@swc-node/register'
- '@swc/core'
- debug
- supports-color
'@phenomnomnominal/tsquery@5.0.1(typescript@5.9.3)':
dependencies:
@@ -7152,7 +7160,7 @@ snapshots:
'@yarnpkg/parsers@3.0.2':
dependencies:
js-yaml: 4.1.1
js-yaml: 4.3.0
tslib: 2.8.1
'@zkochan/js-yaml@0.0.7':
@@ -7161,6 +7169,12 @@ snapshots:
address@1.2.2: {}
agent-base@6.0.2:
dependencies:
debug: 4.4.3
transitivePeerDependencies:
- supports-color
ajv@8.20.0:
dependencies:
fast-deep-equal: 3.1.3
@@ -7258,13 +7272,15 @@ snapshots:
asynckit@0.4.0: {}
axios@1.16.0:
axios@1.18.0:
dependencies:
follow-redirects: 1.16.0
form-data: 4.0.6
https-proxy-agent: 5.0.1
proxy-from-env: 2.1.0
transitivePeerDependencies:
- debug
- supports-color
babel-plugin-const-enum@1.2.0(@babel/core@7.29.6):
dependencies:
@@ -7336,7 +7352,7 @@ snapshots:
inherits: 2.0.4
readable-stream: 3.6.2
brace-expansion@2.1.0:
brace-expansion@2.1.2:
dependencies:
balanced-match: 1.0.2
@@ -7863,7 +7879,7 @@ snapshots:
front-matter@4.0.2:
dependencies:
js-yaml: 4.1.1
js-yaml: 4.3.0
fs-constants@1.0.0: {}
@@ -7912,10 +7928,6 @@ snapshots:
dependencies:
has-symbols: 1.1.0
hasown@2.0.3:
dependencies:
function-bind: 1.1.2
hasown@2.0.4:
dependencies:
function-bind: 1.1.2
@@ -8054,6 +8066,13 @@ snapshots:
html-void-elements@3.0.0: {}
https-proxy-agent@5.0.1:
dependencies:
agent-base: 6.0.2
debug: 4.4.3
transitivePeerDependencies:
- supports-color
iconv-lite@0.6.3:
dependencies:
safer-buffer: 2.1.2
@@ -8088,7 +8107,7 @@ snapshots:
is-core-module@2.16.2:
dependencies:
hasown: 2.0.3
hasown: 2.0.4
is-decimal@2.0.1: {}
@@ -8129,7 +8148,7 @@ snapshots:
js-tokens@9.0.1: {}
js-yaml@4.1.1:
js-yaml@4.3.0:
dependencies:
argparse: 2.0.1
@@ -8677,7 +8696,7 @@ snapshots:
minimatch@5.1.9:
dependencies:
brace-expansion: 2.1.0
brace-expansion: 2.1.2
minimatch@9.0.7:
dependencies:
@@ -8712,7 +8731,7 @@ snapshots:
'@yarnpkg/lockfile': 1.1.0
'@yarnpkg/parsers': 3.0.2
'@zkochan/js-yaml': 0.0.7
axios: 1.16.0
axios: 1.18.0
chalk: 4.1.2
cli-cursor: 3.1.0
cli-spinners: 2.6.1
@@ -8756,6 +8775,7 @@ snapshots:
'@nx/nx-win32-x64-msvc': 21.6.11
transitivePeerDependencies:
- debug
- supports-color
once@1.4.0:
dependencies:
+4 -1
View File
@@ -8,10 +8,13 @@ allowBuilds:
overrides:
'@babel/core@<=7.29.0': 7.29.6
'axios@>=1.0.0 <1.18.0': 1.18.0
'brace-expansion@>=2.0.0 <2.1.2': 2.1.2
'dompurify@<=3.4.10': 3.4.11
'esbuild@>=0.27.3 <0.28.1': 0.28.1
'form-data@>=4.0.0 <4.0.6': 4.0.6
'js-yaml@<3.15.0': 4.1.1
'js-yaml@<3.15.0': 4.3.0
'js-yaml@>=4.0.0 <4.3.0': 4.3.0
'mermaid@>=11.0.0-alpha.1 <=11.14.0': 11.15.0
'minimatch@>=9.0.0 <9.0.7': 9.0.7
'@nx/js>picomatch': 4.0.5