fix: resolve skills, memory, and encryption system issues (#1456)

Integrated into release/v3.7.0
This commit is contained in:
Paijo
2026-04-21 14:48:32 +07:00
committed by GitHub
parent 1d3d99bb9e
commit 20f04574e5
6 changed files with 80 additions and 20 deletions

View File

@@ -16,7 +16,6 @@
import { readFileSync, existsSync } from "fs";
import { join } from "path";
import { resolveDataDir } from "../../src/lib/dataPaths";
// Fields that can be overridden per provider
const CREDENTIAL_FIELDS = [
@@ -41,27 +40,23 @@ function credGlobals(): CredGlobals {
return globalThis as CredGlobals;
}
/**
* Resolves the path to provider-credentials.json using the application's
* data directory. Delegates to resolveDataDir() which handles DATA_DIR env,
* platform-specific defaults, and fallback logic.
*
* previous: Priority: DATA_DIR env → ./data (project root)
*/
function resolveCredentialsPath(): string {
let resolveDataDir: (options?: { isCloud?: boolean }) => string;
try {
resolveDataDir = require("@/lib/dataPaths").resolveDataDir;
} catch (err) {
const fallbackDataDir = process.env.DATA_DIR || join(process.cwd(), "data");
console.warn(
`[CREDENTIALS] Could not load dataPaths module, using fallback: ${fallbackDataDir}`
);
return join(fallbackDataDir, "provider-credentials.json");
}
return join(resolveDataDir(), "provider-credentials.json");
}
/**
* Load and merge external credentials into the PROVIDERS object.
* Uses TTL-based caching (60s) so credential file changes are picked up
* without requiring a server restart.
*
* @param {object} providers - The PROVIDERS object from constants.js
* @returns {object} The same PROVIDERS object (mutated in place)
*/
export function loadProviderCredentials<T extends Record<string, unknown>>(providers: T): T {
// Return cached result if within TTL
if (cachedProviders && Date.now() - lastLoadTime < CONFIG_TTL_MS) {
return cachedProviders as T;
}

View File

@@ -7,7 +7,7 @@
import fs from "fs";
import path from "path";
import { resolveDataDir } from "../../../src/lib/dataPaths";
import { resolveDataDir } from "@/lib/dataPaths";
export interface AdaptationState {
comboId: string;

1
package-lock.json generated
View File

@@ -11187,6 +11187,7 @@
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,

View File

@@ -1,6 +1,12 @@
import { NextResponse } from "next/server";
import { getSettings } from "@/lib/db/settings";
import { isAuthenticated } from "@/shared/utils/apiAuth";
import { getSkillsProviderSetting } from "@/lib/skills/providerSettings";
const POPULAR_BY_PROVIDER = {
skillsmp: ["web-search", "file-reader", "sql-assistant", "devops-helper", "docs-assistant"],
skillssh: ["git", "terminal", "postgres", "kubernetes", "playwright"],
} as const;
export async function GET(request: Request) {
if (!(await isAuthenticated(request))) {
@@ -8,7 +14,21 @@ export async function GET(request: Request) {
}
try {
const { searchParams } = new URL(request.url);
const q = searchParams.get("q") || "";
const q = searchParams.get("q")?.trim() || "";
const provider = await getSkillsProviderSetting();
// Return popular skills when query is empty
if (!q) {
const popularList = POPULAR_BY_PROVIDER[provider];
const skills = popularList.map((name) => ({
name,
description: `Popular skill: ${name}`,
installCount: 0,
}));
return NextResponse.json({ skills });
}
// Search SkillsMP for non-empty queries
const settings = await getSettings();
const apiKey = (settings as Record<string, unknown>).skillsmpApiKey;

View File

@@ -129,7 +129,17 @@ export function decrypt(ciphertext: string | null | undefined): string | null |
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encryptedHex, "hex", "utf8");
decrypted += decipher.final("utf8");
try {
decrypted += decipher.final("utf8");
} catch (finalErr: unknown) {
const finalMessage = finalErr instanceof Error ? finalErr.message : String(finalErr);
console.error(
`[Encryption] Decryption final() failed: ${finalMessage}. ` +
`Ciphertext prefix: ${ciphertext.slice(0, 30)}... ` +
`Auth tag validation likely failed.`
);
return ciphertext;
}
return decrypted;
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);

View File

@@ -0,0 +1,34 @@
import { test } from "node:test";
import assert from "node:assert";
import { decrypt } from "../../../src/lib/db/encryption.ts";
test("decrypt() with invalid auth tag should not crash and return ciphertext", () => {
const invalidCiphertext = "enc:v1:0000:0000:0000";
const result = decrypt(invalidCiphertext);
assert.strictEqual(result, invalidCiphertext, "Should return ciphertext unchanged on error");
assert.strictEqual(typeof result, "string", "Result should be a string");
});
test("decrypt() with malformed ciphertext should not crash", () => {
const malformed = "enc:v1:invalid";
const result = decrypt(malformed);
assert.strictEqual(result, malformed, "Should return malformed ciphertext unchanged");
});
test("decrypt() with null should return null", () => {
const result = decrypt(null);
assert.strictEqual(result, null, "Should return null for null input");
});
test("decrypt() with undefined should return undefined", () => {
const result = decrypt(undefined);
assert.strictEqual(result, undefined, "Should return undefined for undefined input");
});
test("decrypt() with non-encrypted string should return as-is", () => {
const plaintext = "this-is-not-encrypted";
const result = decrypt(plaintext);
assert.strictEqual(result, plaintext, "Should return plaintext unchanged");
});