Merge pull request #10 from Donaldttt/feat/fixed-justconn-endpoint
feat: fix NocoBase endpoint to justconn.top
This commit is contained in:
+7
-9
@@ -2,11 +2,12 @@
|
||||
|
||||
这是一份供终端型 AI Agent 执行的通用安装协议,不依赖任何特定 Agent 或 MCP 客户端。用户只需要提供本链接、确认本机变更,并在隐藏提示中输入自己的 NocoBase Authorize Key。
|
||||
|
||||
如果你是安装 Agent,请直接执行流程,不要让用户逐条尝试命令。只有在确认系统工具变更、取得 NocoBase URL、要求用户创建 Key,或让用户在隐藏提示中粘贴 Key 时才中断用户。
|
||||
如果你是安装 Agent,请直接执行流程,不要让用户逐条尝试命令。只有在确认系统工具变更、要求用户创建 Key,或让用户在隐藏提示中粘贴 Key 时才中断用户。
|
||||
|
||||
## 完成标准
|
||||
|
||||
- 下载本文件所在的同一固定 Release,不使用默认分支;
|
||||
- 只连接固定的 NocoBase API `https://justconn.top/api`;
|
||||
- MCP 在用户本机以 stdio 运行,默认只读;
|
||||
- 认证属于当前用户自己的个人 Authorize Key 和业务角色;
|
||||
- 当前 MCP 客户端已注册 `nocobase-crm`;
|
||||
@@ -21,16 +22,13 @@
|
||||
- 不静默安装 Git、Node.js、包管理器或其他系统工具。
|
||||
- 修改 MCP 客户端配置前展示目标文件与新增项,创建备份,并结构化合并;不得覆盖其他 server。
|
||||
- 默认不启用写入、原始数据、后台服务或网络监听。
|
||||
- 不询问或接受其他 NocoBase URL,也不允许通过参数、配置或凭证文件覆盖固定 API 地址。
|
||||
|
||||
## 安装流程
|
||||
|
||||
### 1. 确认输入与 Release 权限
|
||||
### 1. 确认本机位置与 Release 权限
|
||||
|
||||
取得:
|
||||
|
||||
- NocoBase 应用 URL,例如 `https://nocobase.example`;
|
||||
- 当前 Agent/MCP 客户端及其配置路径;
|
||||
- 普通用户应用数据目录。
|
||||
确定当前 Agent/MCP 客户端及其配置路径,以及普通用户应用数据目录。NocoBase API 已固定为 `https://justconn.top/api`,不需要向用户询问地址。
|
||||
|
||||
从本文件 URL 解析固定 Release Tag。读取同一 Release 的资产列表,只接受:
|
||||
|
||||
@@ -79,10 +77,10 @@ npm install --prefix <install-prefix> --omit=dev <verified-release.tgz>
|
||||
在用户可直接控制的本机终端运行:
|
||||
|
||||
```bash
|
||||
<installed-command> setup --api-base-url <nocobase-url> --json
|
||||
<installed-command> setup --json
|
||||
```
|
||||
|
||||
安装程序会先验证安全 TTY,再显示隐藏 Key 提示;没有安全 TTY 时会在写文件前停止。Key 通过 `roles:check` 验证成功后才保存,并立即执行 `crm_health`。Key 不会出现在返回 JSON、命令参数、MCP 配置或日志中。
|
||||
安装程序会先验证安全 TTY,再显示固定 API 地址和隐藏 Key 提示;没有安全 TTY 时会在写文件前停止。Key 通过 `https://justconn.top/api/roles:check` 验证成功后才保存,并立即执行 `crm_health`。凭证文件只保存 Key,不保存或接受 API 地址。Key 不会出现在返回 JSON、命令参数、MCP 配置或日志中。
|
||||
|
||||
POSIX 凭证目录使用 `0700`,凭证文件使用 `0600`。Windows 使用当前用户应用数据目录。不要复制其他用户的凭证文件;每个操作系统用户都必须单独设置自己的 Key。
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
正式 Release 是一个已打包的 npm tarball,内部包含 MCP SDK 和运行代码。用户电脑只需要:
|
||||
|
||||
- Node.js 20.11 或更高版本(npm 随 Node.js 一起提供);
|
||||
- 能访问目标 NocoBase;
|
||||
- 能访问固定的 NocoBase 环境 `https://justconn.top`;
|
||||
- 用户自己的个人 Authorize Key。
|
||||
|
||||
用户不再需要 Git、Python、uv、Python 包或 `nb CLI`。安装也不会全局安装本项目,而是放到用户自己的应用数据目录。
|
||||
@@ -40,7 +40,7 @@ node dist/index.js --help
|
||||
开发时初始化个人配置:
|
||||
|
||||
```bash
|
||||
node dist/index.js setup --api-base-url https://nocobase.example --json
|
||||
node dist/index.js setup --json
|
||||
```
|
||||
|
||||
Authorize Key 只能输入到隐藏提示。不要把 Key 放进参数、环境变量、聊天、`config.json`、MCP 客户端配置或日志。
|
||||
@@ -48,7 +48,7 @@ Authorize Key 只能输入到隐藏提示。不要把 Key 放进参数、环境
|
||||
可用命令:
|
||||
|
||||
```text
|
||||
nocobase-crm-mcp setup --api-base-url <url> [--config <path>] [--json]
|
||||
nocobase-crm-mcp setup [--config <path>] [--json]
|
||||
nocobase-crm-mcp [serve] [--config <path>] [--enable-writes]
|
||||
nocobase-crm-mcp doctor [--config <path>] [--json]
|
||||
nocobase-crm-mcp client-config [--config <path>] [--enable-writes] [--json]
|
||||
@@ -71,6 +71,6 @@ nocobase-crm-mcp client-config [--config <path>] [--enable-writes] [--json]
|
||||
AI 客户端 → 本机 TypeScript MCP → HTTPS NocoBase API → 当前用户 Key 对应的 ACL
|
||||
```
|
||||
|
||||
MCP 不缓存共享身份。每个请求都会从当前操作系统用户的私有凭证文件读取 Key,因此撤销或替换 Key 后无需重新生成 MCP 客户端配置。远程地址必须使用 HTTPS(回环地址除外),跨域重定向会在转发 Authorization 前被拒绝,错误内容会脱敏。
|
||||
MCP 不缓存共享身份。每个请求都会从当前操作系统用户的私有凭证文件读取 Key,因此撤销或替换 Key 后无需重新生成 MCP 客户端配置。API 地址固定为 `https://justconn.top/api`,不接受安装参数或凭证文件覆盖;跨域重定向会在转发 Authorization 前被拒绝,错误内容会脱敏。
|
||||
|
||||
内部结构见 [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md),运维与权限验收见 [`docs/LOCAL_DELIVERY.md`](docs/LOCAL_DELIVERY.md)。
|
||||
|
||||
@@ -9,7 +9,7 @@ MCP client → TypeScript tool boundary → CRM domain service → NocoBaseClien
|
||||
## 模块边界
|
||||
|
||||
- `src/index.ts` 是单一可执行入口;`src/cli.ts` 管理 setup、doctor、客户端配置和 stdio 生命周期。
|
||||
- `src/credentials.ts` 负责 URL 安全校验、隐藏 TTY 输入和用户级私有凭证文件。Key 不进入 argv、环境变量、CRM 配置或 MCP 配置。
|
||||
- `src/credentials.ts` 固定 `https://justconn.top/api`、负责隐藏 TTY 输入和用户级私有凭证文件。凭证文件只保存 Key;Key 不进入 argv、环境变量、CRM 配置或 MCP 配置。
|
||||
- `src/nocobase-client.ts` 是唯一远程适配器,直接实现 NocoBase resource action 协议、data-modeling 读取、项目附件关系和 ACL 检查。
|
||||
- `src/crm-service.ts` 负责 CRM 查询、解析、分页、聚合和可读输出;`src/crm-helpers.ts` 与 `src/filters.ts` 保存纯函数。
|
||||
- `src/project-service.ts` 只允许固定项目字段的 create/update;`src/tools.ts` 是稳定 MCP 契约和错误/原始数据策略边界。
|
||||
@@ -18,7 +18,7 @@ MCP client → TypeScript tool boundary → CRM domain service → NocoBaseClien
|
||||
## 身份与网络安全
|
||||
|
||||
- 每个请求重新读取当前 OS 用户的凭证文件,NocoBase ACL 是最终权限边界。
|
||||
- 非回环 HTTP URL 被拒绝;远程 Key 只能发往 HTTPS。
|
||||
- 生产运行时只连接固定 HTTPS API `https://justconn.top/api`,不接受参数、配置或凭证文件覆盖。
|
||||
- Authorization 只允许在同源重定向中保留,跨源重定向在发送凭证前失败。
|
||||
- Token 会从 HTTP 错误中替换为 `<redacted>`。
|
||||
- stdio 是唯一默认传输,不创建后台服务或监听端口。
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
每位用户在自己的电脑运行 MCP,并使用绑定本人业务身份的个人 Authorize Key:
|
||||
|
||||
```text
|
||||
本机 AI 客户端 → 本机 TypeScript MCP → 远程 NocoBase API → 本人 ACL
|
||||
本机 AI 客户端 → 本机 TypeScript MCP → https://justconn.top/api → 本人 ACL
|
||||
```
|
||||
|
||||
项目不保存共享业务账号,也不需要多人共用的远程 MCP 服务。
|
||||
@@ -53,4 +53,4 @@ Agent 下载新的固定 Release tarball并验证 SHA-256,再对同一隔离
|
||||
- `nocobase_request_timeout`:检查 NocoBase 可用性或缩小查询。
|
||||
- `nocobase_request_failed` / `NO_PERMISSIONS`:检查该用户 ACL 和字段/行级权限。
|
||||
- 凭证文件权限错误:POSIX 下修复为 `0600`,父目录为 `0700`,不要放宽为共享可读。
|
||||
- 跨源重定向错误:修正 NocoBase API URL或代理配置;不要关闭同源保护。
|
||||
- 跨源重定向错误:检查 `https://justconn.top/api` 的反向代理配置;不要关闭同源保护。
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"schemaVersion": 2,
|
||||
"id": "com.nocobase.crm-mcp",
|
||||
"name": "NocoBase CRM MCP",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.1",
|
||||
"distribution": {
|
||||
"repository": "https://github.com/Donaldttt/nocobase-mcp",
|
||||
"releaseRef": "must-match-v<version>",
|
||||
@@ -19,6 +19,10 @@
|
||||
"nocobaseCli": false,
|
||||
"python": false
|
||||
},
|
||||
"nocobase": {
|
||||
"apiBaseUrl": "https://justconn.top/api",
|
||||
"userConfigurableEndpoint": false
|
||||
},
|
||||
"installation": {
|
||||
"requiredTools": ["node", "npm"],
|
||||
"sourceCheckout": false,
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@donaldttt/nocobase-crm-mcp",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@donaldttt/nocobase-crm-mcp",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.1",
|
||||
"license": "UNLICENSED",
|
||||
"bin": {
|
||||
"nocobase-crm-mcp": "dist/index.js"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@donaldttt/nocobase-crm-mcp",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.1",
|
||||
"description": "Local NocoBase CRM MCP server using each user's personal Authorize Key",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
|
||||
+13
-12
@@ -6,7 +6,7 @@ import { loadConfig } from "./config.js";
|
||||
import {
|
||||
credentialsPath,
|
||||
loadCredentials,
|
||||
normalizeApiBaseUrl,
|
||||
NOCOBASE_API_BASE_URL,
|
||||
readHidden,
|
||||
saveCredentials,
|
||||
} from "./credentials.js";
|
||||
@@ -18,7 +18,6 @@ import { VERSION } from "./version.js";
|
||||
|
||||
interface ParsedArgs {
|
||||
command: "serve" | "setup" | "doctor" | "client-config" | "help" | "version";
|
||||
apiBaseUrl?: string;
|
||||
config?: string;
|
||||
enableWrites: boolean;
|
||||
json: boolean;
|
||||
@@ -80,27 +79,29 @@ export function createRuntime(configPath: string): {
|
||||
}
|
||||
|
||||
async function setup(args: ParsedArgs): Promise<void> {
|
||||
if (!args.apiBaseUrl) throw new Error("setup requires --api-base-url <https://nocobase.example>");
|
||||
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
||||
throw new Error("Setup requires a user-controlled local terminal; no files were changed");
|
||||
}
|
||||
const apiBaseUrl = normalizeApiBaseUrl(args.apiBaseUrl);
|
||||
process.stderr.write(
|
||||
"Create a revocable personal Authorize Key in NocoBase for your own business account.\n" +
|
||||
`This MCP connects only to ${NOCOBASE_API_BASE_URL}.\n` +
|
||||
"Create a revocable personal Authorize Key in NocoBase for your own business account.\n" +
|
||||
"Do not paste the Key into Agent chat. Enter it only in the hidden prompt below.\n",
|
||||
);
|
||||
const authorizeKey = (await readHidden("Personal NocoBase Authorize Key: ")).trim();
|
||||
if (!authorizeKey) throw new Error("Authorize Key must not be blank; no files were changed");
|
||||
const verificationClient = new NocoBaseClient(() => ({ apiBaseUrl, authorizeKey }));
|
||||
const verificationClient = new NocoBaseClient(() => ({
|
||||
apiBaseUrl: NOCOBASE_API_BASE_URL,
|
||||
authorizeKey,
|
||||
}));
|
||||
await verificationClient.verifyCredentials();
|
||||
const configPath = ensureConfig(args.config);
|
||||
saveCredentials({ apiBaseUrl, authorizeKey });
|
||||
saveCredentials({ authorizeKey });
|
||||
const runtime = createRuntime(configPath);
|
||||
const health = await runtime.crm.crmHealth();
|
||||
printResult(
|
||||
{
|
||||
ok: true,
|
||||
apiBaseUrl,
|
||||
apiBaseUrl: NOCOBASE_API_BASE_URL,
|
||||
credentialsPath: credentialsPath(),
|
||||
keyExposure: "private-tty-and-0600-local-file-only",
|
||||
configPath,
|
||||
@@ -165,11 +166,10 @@ function parseArgs(argv: string[]): ParsedArgs {
|
||||
else if (flag === "--version" || flag === "-v") parsed.command = "version";
|
||||
else if (flag === "--enable-writes") parsed.enableWrites = true;
|
||||
else if (flag === "--json") parsed.json = true;
|
||||
else if (flag === "--api-base-url" || flag === "--config") {
|
||||
else if (flag === "--config") {
|
||||
const value = argv[index + 1];
|
||||
if (!value || value.startsWith("-")) throw new Error(`${flag} requires a value`);
|
||||
if (flag === "--api-base-url") parsed.apiBaseUrl = value;
|
||||
else parsed.config = value;
|
||||
parsed.config = value;
|
||||
index += 1;
|
||||
} else throw new Error(`Unknown option: ${flag}`);
|
||||
index += 1;
|
||||
@@ -185,11 +185,12 @@ function helpText(): string {
|
||||
return `NocoBase CRM MCP ${VERSION}
|
||||
|
||||
Usage:
|
||||
nocobase-crm-mcp setup --api-base-url <url> [--config <path>] [--json]
|
||||
nocobase-crm-mcp setup [--config <path>] [--json]
|
||||
nocobase-crm-mcp [serve] [--config <path>] [--enable-writes]
|
||||
nocobase-crm-mcp doctor [--config <path>] [--json]
|
||||
nocobase-crm-mcp client-config [--config <path>] [--enable-writes] [--json]
|
||||
|
||||
The NocoBase API endpoint is fixed to ${NOCOBASE_API_BASE_URL}.
|
||||
The setup command accepts the personal Authorize Key only through a hidden local TTY prompt.
|
||||
The default stdio server is read-only.
|
||||
`;
|
||||
|
||||
+10
-24
@@ -8,21 +8,7 @@ export interface Credentials {
|
||||
authorizeKey: string;
|
||||
}
|
||||
|
||||
export function normalizeApiBaseUrl(value: string): string {
|
||||
const raw = value.trim();
|
||||
if (!raw) throw new Error("NocoBase URL is required");
|
||||
const url = new URL(raw);
|
||||
if (!(["http:", "https:"] as string[]).includes(url.protocol))
|
||||
throw new Error("NocoBase URL must use HTTP or HTTPS");
|
||||
if (url.username || url.password || url.search || url.hash) {
|
||||
throw new Error("NocoBase URL must not contain credentials, a query string, or a fragment");
|
||||
}
|
||||
const local = url.hostname === "localhost" || url.hostname === "::1" || url.hostname.startsWith("127.");
|
||||
if (url.protocol !== "https:" && !local)
|
||||
throw new Error("A remote Authorize Key may only be sent over HTTPS");
|
||||
url.pathname = `${url.pathname.replace(/\/+$/, "")}${url.pathname.replace(/\/+$/, "").endsWith("/api") ? "" : "/api"}`;
|
||||
return url.toString().replace(/\/$/, "");
|
||||
}
|
||||
export const NOCOBASE_API_BASE_URL = "https://justconn.top/api";
|
||||
|
||||
export function credentialsPath(): string {
|
||||
const override = process.env.NOCOBASE_MCP_CREDENTIALS?.trim();
|
||||
@@ -38,12 +24,12 @@ export function credentialsPath(): string {
|
||||
return join(base, "nocobase-crm-mcp", "credentials.json");
|
||||
}
|
||||
|
||||
export function saveCredentials(credentials: Credentials, path = credentialsPath()): void {
|
||||
const normalized: Credentials = {
|
||||
apiBaseUrl: normalizeApiBaseUrl(credentials.apiBaseUrl),
|
||||
authorizeKey: credentials.authorizeKey.trim(),
|
||||
};
|
||||
if (!normalized.authorizeKey) throw new Error("Authorize Key must not be blank");
|
||||
export function saveCredentials(
|
||||
credentials: Pick<Credentials, "authorizeKey">,
|
||||
path = credentialsPath(),
|
||||
): void {
|
||||
const authorizeKey = credentials.authorizeKey.trim();
|
||||
if (!authorizeKey) throw new Error("Authorize Key must not be blank");
|
||||
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
||||
if (process.platform !== "win32") {
|
||||
try {
|
||||
@@ -56,7 +42,7 @@ export function saveCredentials(credentials: Credentials, path = credentialsPath
|
||||
}
|
||||
const descriptor = openSync(path, "w", 0o600);
|
||||
try {
|
||||
writeFileSync(descriptor, `${JSON.stringify(normalized, null, 2)}\n`, "utf8");
|
||||
writeFileSync(descriptor, `${JSON.stringify({ authorizeKey }, null, 2)}\n`, "utf8");
|
||||
} finally {
|
||||
closeSync(descriptor);
|
||||
}
|
||||
@@ -69,12 +55,12 @@ export function loadCredentials(path = credentialsPath()): Credentials {
|
||||
throw new Error(`Credential file must be private (0600); current mode is ${mode.toString(8)}`);
|
||||
}
|
||||
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
||||
if (!isRecord(parsed) || typeof parsed.apiBaseUrl !== "string" || typeof parsed.authorizeKey !== "string") {
|
||||
if (!isRecord(parsed) || typeof parsed.authorizeKey !== "string") {
|
||||
throw new Error("Credential file is invalid; run setup again");
|
||||
}
|
||||
const authorizeKey = parsed.authorizeKey.trim();
|
||||
if (!authorizeKey) throw new Error("Credential file has no Authorize Key; run setup again");
|
||||
return { apiBaseUrl: normalizeApiBaseUrl(parsed.apiBaseUrl), authorizeKey };
|
||||
return { apiBaseUrl: NOCOBASE_API_BASE_URL, authorizeKey };
|
||||
}
|
||||
|
||||
export async function readHidden(prompt: string): Promise<string> {
|
||||
|
||||
+1
-1
@@ -423,7 +423,7 @@ async function safeTool(operation: () => Promise<ToolPayload>): Promise<ToolPayl
|
||||
error: {
|
||||
code: error.statusCode === 408 ? "nocobase_request_timeout" : "nocobase_request_failed",
|
||||
message: error.message,
|
||||
hint: "Check the NocoBase URL, ACL, collection mapping, and query parameters.",
|
||||
hint: "Check justconn.top availability, ACL, collection mapping, and query parameters.",
|
||||
retryable: error.retryable,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ import { VERSION } from "../src/version.js";
|
||||
test("package, manifest, and runtime versions agree with no runtime npm dependencies", () => {
|
||||
const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
||||
const manifest = JSON.parse(readFileSync(new URL("../install-manifest.json", import.meta.url), "utf8"));
|
||||
assert.equal(VERSION, "0.3.0");
|
||||
assert.equal(VERSION, "0.3.1");
|
||||
assert.equal(packageJson.version, VERSION);
|
||||
assert.equal(manifest.version, VERSION);
|
||||
assert.deepEqual(packageJson.dependencies, undefined);
|
||||
@@ -31,10 +31,7 @@ test("setup refuses a non-TTY before creating credential files", async () => {
|
||||
const previous = process.env.NOCOBASE_MCP_CREDENTIALS;
|
||||
process.env.NOCOBASE_MCP_CREDENTIALS = path;
|
||||
try {
|
||||
await assert.rejects(
|
||||
runCli(["setup", "--api-base-url", "https://nb.example", "--json"]),
|
||||
/local terminal/,
|
||||
);
|
||||
await assert.rejects(runCli(["setup", "--json"]), /local terminal/);
|
||||
assert.equal(existsSync(path), false);
|
||||
assert.equal(existsSync(join(directory, "config.json")), false);
|
||||
} finally {
|
||||
@@ -42,3 +39,10 @@ test("setup refuses a non-TTY before creating credential files", async () => {
|
||||
else process.env.NOCOBASE_MCP_CREDENTIALS = previous;
|
||||
}
|
||||
});
|
||||
|
||||
test("setup rejects API endpoint overrides", async () => {
|
||||
await assert.rejects(
|
||||
runCli(["setup", "--api-base-url", "https://other.example"]),
|
||||
/Unknown option: --api-base-url/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -4,24 +4,20 @@ import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
import { DEFAULT_CONFIG, loadConfig } from "../src/config.js";
|
||||
import { loadCredentials, normalizeApiBaseUrl, saveCredentials } from "../src/credentials.js";
|
||||
import { loadCredentials, NOCOBASE_API_BASE_URL, saveCredentials } from "../src/credentials.js";
|
||||
|
||||
test("normalizes application URLs to the NocoBase API base", () => {
|
||||
assert.equal(normalizeApiBaseUrl("https://nb.example/app/"), "https://nb.example/app/api");
|
||||
assert.equal(normalizeApiBaseUrl("http://127.0.0.1:13000/api"), "http://127.0.0.1:13000/api");
|
||||
assert.throws(() => normalizeApiBaseUrl("http://nb.example"), /HTTPS/i);
|
||||
assert.throws(() => normalizeApiBaseUrl("https://user:pass@nb.example"), /credentials/i);
|
||||
});
|
||||
|
||||
test("stores and reads credentials from a private file", () => {
|
||||
test("stores only the Key and always loads the fixed NocoBase endpoint", () => {
|
||||
const directory = mkdtempSync(join(tmpdir(), "nocobase-mcp-credentials-"));
|
||||
const path = join(directory, "private", "credentials.json");
|
||||
saveCredentials({ apiBaseUrl: "https://nb.example", authorizeKey: "secret" }, path);
|
||||
saveCredentials({ authorizeKey: "secret" }, path);
|
||||
assert.deepEqual(loadCredentials(path), {
|
||||
apiBaseUrl: "https://nb.example/api",
|
||||
apiBaseUrl: NOCOBASE_API_BASE_URL,
|
||||
authorizeKey: "secret",
|
||||
});
|
||||
assert.match(readFileSync(path, "utf8"), /secret/);
|
||||
assert.deepEqual(JSON.parse(readFileSync(path, "utf8")), { authorizeKey: "secret" });
|
||||
|
||||
writeFileSync(path, JSON.stringify({ apiBaseUrl: "https://legacy.example/api", authorizeKey: "secret" }));
|
||||
assert.equal(loadCredentials(path).apiBaseUrl, NOCOBASE_API_BASE_URL);
|
||||
if (process.platform !== "win32") {
|
||||
chmodSync(path, 0o644);
|
||||
assert.throws(() => loadCredentials(path), /0600/);
|
||||
|
||||
Reference in New Issue
Block a user