diff --git a/open-sse/config/credentialLoader.ts b/open-sse/config/credentialLoader.ts index cc740a48ae..2350abce21 100644 --- a/open-sse/config/credentialLoader.ts +++ b/open-sse/config/credentialLoader.ts @@ -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>(providers: T): T { - // Return cached result if within TTL if (cachedProviders && Date.now() - lastLoadTime < CONFIG_TTL_MS) { return cachedProviders as T; } diff --git a/open-sse/services/autoCombo/persistence.ts b/open-sse/services/autoCombo/persistence.ts index 7dae3521a9..e445ed5766 100644 --- a/open-sse/services/autoCombo/persistence.ts +++ b/open-sse/services/autoCombo/persistence.ts @@ -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; diff --git a/package-lock.json b/package-lock.json index 5da6f07344..85fda8cc74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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, diff --git a/src/app/api/skills/marketplace/route.ts b/src/app/api/skills/marketplace/route.ts index 4c623e3a45..e0ae1fa146 100644 --- a/src/app/api/skills/marketplace/route.ts +++ b/src/app/api/skills/marketplace/route.ts @@ -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).skillsmpApiKey; diff --git a/src/lib/db/encryption.ts b/src/lib/db/encryption.ts index c26cd10624..bff219c1ef 100644 --- a/src/lib/db/encryption.ts +++ b/src/lib/db/encryption.ts @@ -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); diff --git a/tests/unit/db/encryption-error-handling.test.mjs b/tests/unit/db/encryption-error-handling.test.mjs new file mode 100644 index 0000000000..e40e531ed7 --- /dev/null +++ b/tests/unit/db/encryption-error-handling.test.mjs @@ -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"); +});