Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/globals/src/settings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export interface Settings {
};
logger: {
/** console | file | dailyRotateFile */
transport: ('console' | 'file' | 'dailyRotateFile')[];
transport?: ('console' | 'file' | 'dailyRotateFile')[];
basePath: string;
level?: 'error' | 'warn' | 'info' | 'debug';
maxFiles?: string | number; // e.g. "14d"
Expand Down
92 changes: 92 additions & 0 deletions packages/tego/src/__tests__/env
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
################# TACHYBASE APPLICATION #################
APP_ENV=development
APP_PORT=3000
APP_KEY=test-key
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Hard-coded secret committed

APP_KEY contains a real-looking key. Even in test fixtures, commit history persists and may leak into production workflows. Replace with an obvious placeholder (e.g. CHANGE_ME) or load from CI secrets.

🤖 Prompt for AI Agents
In packages/tego/src/__tests__/env at line 4, the APP_KEY is hard-coded with a
real-looking secret key, which risks leaking sensitive information through
commit history. Replace the current value with a clear placeholder like
CHANGE_ME or configure the test environment to load this key securely from CI
secrets instead of hard-coding it.


# experimental support
EXTENSION_UI_BASE_PATH=/adapters/

API_BASE_PATH=/api/
API_BASE_URL=

# console | file | dailyRotateFile
LOGGER_TRANSPORT=
LOGGER_BASE_PATH=storage/logs
# error | warn | info | debug
LOGGER_LEVEL=
# If LOGGER_TRANSPORT is dailyRotateFile and using days, add 'd' as the suffix.
LOGGER_MAX_FILES=
# add 'k', 'm', 'g' as the suffix.
LOGGER_MAX_SIZE=
# json | splitter, split by '|' character
LOGGER_FORMAT=

################# DATABASE #################

DB_DIALECT=sqlite
DB_STORAGE=storage/db/tachybase.sqlite
DB_TABLE_PREFIX=
# DB_HOST=localhost
# DB_PORT=5432
# DB_DATABASE=postgres
# DB_USER=tachybase
# DB_PASSWORD=tachybase
# DB_LOGGING=on
# DB_UNDERSCORED=false

#== SSL CONFIG ==#
# DB_DIALECT_OPTIONS_SSL_CA=
# DB_DIALECT_OPTIONS_SSL_KEY=
# DB_DIALECT_OPTIONS_SSL_CERT=
# DB_DIALECT_OPTIONS_SSL_REJECT_UNAUTHORIZED=true

################# CACHE #################
CACHE_DEFAULT_STORE=memory
# max number of items in memory cache
CACHE_MEMORY_MAX=2000
# CACHE_REDIS_URL=

################# STORAGE (Initialization only) #################

INIT_APP_LANG=en-US
[email protected]
INIT_ROOT_PASSWORD=!Admin123.
INIT_ROOT_NICKNAME=Super Admin
INIT_ROOT_USERNAME=tachybase

Comment on lines +51 to +56
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Root account credentials exposed

INIT_ROOT_PASSWORD (and related user details) are in plain text. This is high-risk if the file ever ships outside test scope. Use dummy values or remove the variable entirely from VCS.

🤖 Prompt for AI Agents
In packages/tego/src/__tests__/env around lines 51 to 56, the root account
credentials including INIT_ROOT_PASSWORD are exposed in plain text, which is a
security risk. Replace the real password and related user details with dummy
placeholder values or remove these variables entirely from version control to
prevent accidental exposure outside the test environment.

################# ENCRYPTION FIELD #################

ENCRYPTION_FIELD_KEY=
##### PRESETS #####

# 单写名称:添加指定插件且默认启用 名称前加!:移除指定插件 名称前加|:添加指定插件但默认禁用
# PRESETS_CORE_PLUGINS=api-doc,api-keys,!messages
# PRESETS_LOCAL_PLUGINS=gantt,!iframe-block,|audit-logs
PRESETS_BULTIN_PLUGINS=acl,app-info,auth,backup,cloud-component,collection,cron,data-source,error-handler,event-source,file,workflow,message,pdf,ui-schema,user,web,worker-thread,env-secrets
PRESETS_EXTERNAL_PLUGINS=action-bulk-edit,action-bulk-update,action-custom-request,action-duplicate,action-export,action-import,action-print,block-calendar,block-charts,block-gantt,block-kanban,block-presentation,field-china-region,field-formula,field-sequence,field-encryption,log-viewer,otp,instrumentation,full-text-search,password-policy,auth-pages,manual-notification,auth-main-app,!adapter-bullmq,!adapter-red-node,!adapter-remix,!api-keys,!audit-logs,!auth-cas,!auth-dingtalk,!auth-lark,!auth-oidc,!auth-saml,!auth-sms,!auth-wechat,!auth-wecom,!block-comments,!block-map,!block-step-form,!data-source-common,!demos-game-runesweeper,!devtools,!field-markdown-vditor,!field-snapshot,!hera,!i18n-editor,!multi-app,!multi-app-share-collection,!online-user,!simple-cms,!sub-accounts,!theme-editor,!workflow-approval,!ai-chat,!department,!workflow-analysis,!api-logs,!ocr-convert,!text-copy,!user-manual-feishu,!evaluator-mathjs


# 主应用工作线程默认数量
WORKER_COUNT=1
# WORKER_TIMEOUT=1800
# 主应用工作线程最大数量
WORKER_COUNT_MAX=8
# WORKER_ERROR_RETRY=3
# 子应用工作线程默认数量
WORKER_COUNT_SUB=0
# 子应用工作线程最大数量
WORKER_COUNT_MAX_SUB=1

# export config, max length of export data to use main thread and page size in worker thread
# EXPORT_LENGTH_MAX=2000
# EXPORT_WORKER_PAGESIZE=1000

# 开发环境测试locale 强制使用 cache
#FORCE_LOCALE_CACHE=1

# 禁止子应用装载的插件,多个用逗号分隔
# FORBID_SUB_APP_PLUGINS=multi-app,manual-notification,multi-app-share-collection


# 工作线程最大内存,单位为MB
# WORKER_MAX_MEMORY=4096
4 changes: 1 addition & 3 deletions packages/tego/src/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,8 @@ describe('convertEnvToSettings', () => {
const result = convertEnvToSettings(input as any);

expect(result.logger.transport).toEqual(['console', 'dailyRotateFile']);
expect(result.logger.max_files).toBeUndefined(); // 未提供
expect(result.logger.maxFiles).toBe('7d');
expect(result.database.storage).toBe('storage/db/tachybase.sqlite');
expect(result.cache.default_store).toBe('memory');
expect(result.env.INIT_APP_LANG).toBe('zh-CN');
expect(result.cache.defaultStore).toBe('memory');
});
});
184 changes: 131 additions & 53 deletions packages/tego/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import { access, mkdir, readFile, rm, unlink, writeFile } from 'node:fs/promises
import os from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { pipeline } from 'node:stream/promises';
import TachybaseGlobal from '@tachybase/globals';
import TachybaseGlobal, { Settings } from '@tachybase/globals';

import { config } from 'dotenv';
import { cloneDeep } from 'lodash';
import npmRegistryFetch from 'npm-registry-fetch';
import * as tar from 'tar';

import defaultSettings from '../presets/settings';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix TS build: avoid importing files outside rootDir (pipeline error)

The CI error shows TypeScript rejects importing ../presets/settings because it’s outside packages/tego/src. Two low-risk options:

  1. Re-export defaultSettings via an in-src shim and import from there (recommended).
  2. Adjust tsconfig rootDir/includes (higher blast radius).

Apply this diff here, plus add the small shim file below.

-import defaultSettings from '../presets/settings';
+import defaultSettings from './presets/settings';

Add new file at packages/tego/src/presets/settings.ts:

// Re-export to satisfy tsc rootDir constraints
export { default } from '../../presets/settings';

This resolves the CI error without changing build config.

🧰 Tools
🪛 GitHub Actions: Full Build

[error] 19-19: File '/home/runner/work/tego/tego/packages/tego/presets/settings.js' is not under 'rootDir' '/home/runner/work/tego/tego/packages/tego/src'. 'rootDir' is expected to contain all source files.

🤖 Prompt for AI Agents
In packages/tego/src/utils.ts around line 19, the file imports
../presets/settings which violates TypeScript rootDir constraints; change the
import to point to a new in-src shim (./presets/settings) and add a new file
packages/tego/src/presets/settings.ts that re-exports the original default
export from ../../presets/settings so tsc sees the module inside the package
root. Ensure the utils.ts import is updated to import defaultSettings from
'./presets/settings' and create the shim file that simply re-exports the default
export from the upstream presets path.

import { DEFAULT_WEB_PACKAGE_NAME, LAST_UPDATE_FILE_SUFFIX } from './constants';

export function parseEnvironment() {
Expand Down Expand Up @@ -209,62 +211,138 @@ export class TegoIndexManager {
}

export function convertEnvToSettings(flatEnv: Record<string, string | undefined>) {
const settings: any = {
env: {},
logger: {},
database: {},
cache: {},
encryptionField: {},
presets: {},
worker: {},
export: {},
misc: {},
};

// LOGGER_
for (const key in flatEnv) {
const value = flatEnv[key];
if (value === undefined) continue;

if (key.startsWith('LOGGER_')) {
const subKey = key.replace('LOGGER_', '').toLowerCase();
if (subKey === 'transport') {
settings.logger.transport = value.split(',').map((x) => x.trim());
} else if (subKey === 'maxfiles') {
const settings: Settings = cloneDeep(defaultSettings);

for (const [key, value] of Object.entries(flatEnv)) {
if (!value) continue;

switch (key) {
Comment on lines +207 to +210
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Trim values before processing; skip only when empty after trim

Prevents accidental ignoring when values contain whitespace.

-  for (const [key, value] of Object.entries(flatEnv)) {
-    if (!value) continue;
+  for (const [key, value] of Object.entries(flatEnv)) {
+    if (value == null) continue;
+    const v = String(value).trim();
+    if (!v) continue;

Follow-up: use v instead of value below.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (const [key, value] of Object.entries(flatEnv)) {
if (!value) continue;
switch (key) {
for (const [key, value] of Object.entries(flatEnv)) {
if (value == null) continue;
const v = String(value).trim();
if (!v) continue;
switch (key) {
🤖 Prompt for AI Agents
In packages/tego/src/utils.ts around lines 216 to 219, the loop currently skips
entries based on truthiness of value which will miss non-empty strings
containing only whitespace; convert the entry to a trimmed string first (e.g.
const v = String(value).trim()) and change the guard to if (!v) continue; then
use v (not value) in the subsequent switch/processing so all whitespace-only
values are treated as empty.

/** ================= LOGGER ================= */
case 'LOGGER_TRANSPORT':
settings.logger.transport = value.split(',') as any;
break;
Comment on lines +212 to +214
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Harden LOGGER_TRANSPORT parsing (trim, dedupe, validate)

Trim tokens, dedupe, and ignore invalid entries to avoid type casting to any.

-      case 'LOGGER_TRANSPORT':
-        settings.logger.transport = value.split(',') as any;
+      case 'LOGGER_TRANSPORT': {
+        const allowed = new Set(['console', 'file', 'dailyRotateFile']);
+        const transports = Array.from(
+          new Set(
+            v
+              .split(',')
+              .map((t) => t.trim())
+              .filter((t) => allowed.has(t)),
+          ),
+        );
+        if (transports.length) {
+          settings.logger.transport = transports as any;
+        }
+        break;
+      }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/tego/src/utils.ts around lines 221 to 223, the LOGGER_TRANSPORT
branch currently casts the split result to any; change it to robustly parse the
comma-separated string by splitting, trimming each token, filtering out empty
strings, deduplicating entries, and validating each token against the allowed
transport names (e.g., the enum or union type used for logger transports);
finally assign the resulting typed array (not any) to settings.logger.transport
so invalid entries are dropped and types are preserved.

case 'LOGGER_BASE_PATH':
settings.logger.basePath = value;
break;
case 'LOGGER_LEVEL':
settings.logger.level = value as any;
break;
case 'LOGGER_MAX_FILES':
settings.logger.maxFiles = value;
} else if (subKey === 'maxsize') {
break;
case 'LOGGER_MAX_SIZE':
settings.logger.maxSize = value;
} else if (subKey === 'format') {
settings.logger.format = value;
} else {
settings.logger[subKey] = value;
}
continue;
}

// DB_
if (key.startsWith('DB_')) {
const subKey = key.replace('DB_', '').toLowerCase();
if (subKey.startsWith('dialect_options_ssl_')) {
// e.g. DB_DIALECT_OPTIONS_SSL_CA
const sslKey = subKey.replace('dialect_options_ssl_', '');
break;
case 'LOGGER_FORMAT':
settings.logger.format = value as any;
break;

/** ================= DATABASE ================= */
case 'DB_DIALECT':
settings.database.dialect = value as any;
break;
case 'DB_STORAGE':
settings.database.storage = value;
break;
case 'DB_HOST':
settings.database.host = value;
break;
case 'DB_PORT':
settings.database.port = +value;
break;
case 'DB_DATABASE':
settings.database.database = value;
break;
case 'DB_USER':
settings.database.user = value;
break;
case 'DB_PASSWORD':
settings.database.password = value;
break;
case 'DB_LOGGING':
settings.database.logging = value === 'on';
break;
Comment on lines +254 to +255
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

DB_LOGGING: accept common truthy values ('on', 'true', '1', 'yes')

More forgiving parsing helps compatibility.

-      case 'DB_LOGGING':
-        settings.database.logging = value === 'on';
+      case 'DB_LOGGING': {
+        const truthy = new Set(['on', 'true', '1', 'yes']);
+        settings.database.logging = truthy.has(v.toLowerCase());
+        break;
+      }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/tego/src/utils.ts around lines 263-264, the code currently sets
settings.database.logging only when value === 'on'; change it to accept common
truthy strings (case-insensitive) — e.g. 'on', 'true', '1', 'yes' — by
normalizing the value (toString(), trim(), toLowerCase()) and checking
membership in that set, then set settings.database.logging to true for matches
and false otherwise (handling undefined/null safely).

case 'DB_TABLE_PREFIX':
settings.database.tablePrefix = value;
break;
case 'DB_UNDERSCORED':
settings.database.underscored = value === 'true';
break;
Comment on lines +260 to +261
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

DB_UNDERSCORED: normalize common boolean values

Follow same boolean normalization as DB_LOGGING.

-      case 'DB_UNDERSCORED':
-        settings.database.underscored = value === 'true';
+      case 'DB_UNDERSCORED': {
+        const truthy = new Set(['on', 'true', '1', 'yes']);
+        settings.database.underscored = truthy.has(v.toLowerCase());
+        break;
+      }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/tego/src/utils.ts around lines 269-270, the assignment
settings.database.underscored = value === 'true' only accepts the literal 'true'
string; replace it with the same boolean normalization used for DB_LOGGING
(i.e., normalize common boolean representations from the env value such as
'true'/'false', '1'/'0', 'yes'/'no' in a case-insensitive way) so underscored is
reliably set to true or false for those inputs; implement by reusing or
duplicating the DB_LOGGING normalization helper logic so value is converted to a
boolean before assignment.


case 'DB_DIALECT_OPTIONS_SSL_CA':
settings.database.ssl = settings.database.ssl || {};
settings.database.ssl[sslKey] = value;
} else {
settings.database[subKey] = value;
}
continue;
}

// CACHE_
if (key.startsWith('CACHE_')) {
const subKey = key.replace('CACHE_', '').toLowerCase();
settings.cache[subKey] = value;
continue;
settings.database.ssl.ca = value;
break;
case 'DB_DIALECT_OPTIONS_SSL_KEY':
settings.database.ssl = settings.database.ssl || {};
settings.database.ssl.key = value;
break;
case 'DB_DIALECT_OPTIONS_SSL_CERT':
settings.database.ssl = settings.database.ssl || {};
settings.database.ssl.cert = value;
break;
case 'DB_DIALECT_OPTIONS_SSL_REJECT_UNAUTHORIZED':
settings.database.ssl = settings.database.ssl || {};
settings.database.ssl.rejectUnauthorized = value === 'true';
break;

/** ================= CACHE ================= */
case 'CACHE_DEFAULT_STORE':
settings.cache.defaultStore = value as any;
break;
case 'CACHE_MEMORY_MAX':
settings.cache.memoryMax = +value;
break;
case 'CACHE_REDIS_URL':
settings.cache.redisUrl = value;
break;

/** ================= ENCRYPTION ================= */
case 'ENCRYPTION_FIELD_KEY':
settings.encryptionField.key = value;
break;

/** ================= PRESETS ================= */
case 'PRESETS_BULTIN_PLUGINS':
settings.presets.builtinPlugins = value.split(',');
break;
case 'PRESETS_EXTERNAL_PLUGINS':
settings.presets.externalPlugins = value.split(',').map((name) => {
if (name.startsWith('!')) {
return { name: name.slice(1), enabledByDefault: false };
}
if (name.startsWith('|')) {
return { name: name.slice(1), enabledByDefault: false };
}
return { name, enabledByDefault: true };
});
break;

/** ================= WORKER ================= */
case 'WORKER_COUNT':
settings.worker.count = +value;
break;
case 'WORKER_COUNT_MAX':
settings.worker.countMax = +value;
break;

/** ================= EXPORT ================= */
case 'EXPORT_LENGTH_MAX':
settings.export.lengthMax = +value;
break;
case 'EXPORT_WORKER_PAGESIZE':
settings.export.workerPageSize = +value;
break;

/** ================= MISC ================= */
case 'FORBID_SUB_APP_PLUGINS':
settings.misc.forbidSubAppPlugins = value.split(',');
break;

default:
// 不处理未知 key
break;
}

// 其它 => 默认归到 settings.env
settings.env[key] = value;
}

return settings;
Expand Down
Loading