fix(notify): reject custom package notify owners

This commit is contained in:
Kenny
2026-04-19 14:48:04 +08:00
parent 6fffacd4fc
commit 9f2ba4b384
2 changed files with 73 additions and 0 deletions
@@ -111,6 +111,32 @@ describe("ensureBundledNotifyOwnership", () => {
expect(readConfig(userConfigPath).plugin).toEqual(["team-notify-center", "oh-my-openagent", canonicalEntry])
})
test("fails loudly for custom package-based notify plugin id", () => {
// given
const userConfigPath = join(userConfigDir, "opencode.json")
writeFileSync(userConfigPath, JSON.stringify({ plugin: ["@custom/opencode-notify", "oh-my-openagent"] }, null, 2) + "\n")
// when
const run = () => ensureBundledNotifyOwnership({ projectDirectory: projectDir, packageRoot })
// then
expect(run).toThrow("Unsafe external notify plugin ownership detected")
expect(readConfig(userConfigPath).plugin).toEqual(["@custom/opencode-notify", "oh-my-openagent"])
})
test("fails loudly for custom tuple-based notify package id", () => {
// given
const userConfigPath = join(userConfigDir, "opencode.json")
writeFileSync(userConfigPath, JSON.stringify({ plugin: [["npm:@custom/opencode-notify@1.2.3", {}], "oh-my-openagent"] }, null, 2) + "\n")
// when
const run = () => ensureBundledNotifyOwnership({ projectDirectory: projectDir, packageRoot })
// then
expect(run).toThrow("Unsafe external notify plugin ownership detected")
expect(readConfig(userConfigPath).plugin).toEqual([["npm:@custom/opencode-notify@1.2.3", {}], "oh-my-openagent"])
})
test("migrates stale bundled dist/opencode-notify file URL to canonical bundled entry", () => {
// given
const userConfigPath = join(userConfigDir, "opencode.json")
+47
View File
@@ -66,6 +66,35 @@ function isRecognizedExternalNotifyId(entry: string): boolean {
return KNOWN_EXTERNAL_NOTIFY_IDS.some((base) => normalized === base || normalized.startsWith(`${base}@`))
}
function stripNpmPrefix(entry: string): string {
return entry.startsWith("npm:") ? entry.slice("npm:".length) : entry
}
function stripVersionSuffix(entry: string): string {
const trimmed = entry.trim()
const versionSeparatorIndex = trimmed.lastIndexOf("@")
if (versionSeparatorIndex <= 0) return trimmed
const slashIndex = trimmed.indexOf("/")
if (trimmed.startsWith("@") && versionSeparatorIndex <= slashIndex) {
return trimmed
}
return trimmed.slice(0, versionSeparatorIndex)
}
function isCustomPackageNotifyCandidate(entry: string): boolean {
const normalized = stripNpmPrefix(entry.trim().toLowerCase())
if (normalized.length === 0) return false
if (normalized.includes("://")) return false
const packageIdentifier = stripVersionSuffix(normalized)
if (packageIdentifier === "opencode-notify") return true
if (/^@[a-z0-9._-]+\/opencode-notify$/i.test(packageIdentifier)) return true
if (/^[a-z0-9._-]+\/opencode-notify$/i.test(packageIdentifier)) return true
return false
}
function normalizePathForComparison(pathValue: string): string {
return resolve(pathValue).replace(/\\/g, "/").replace(/\/+$/, "")
}
@@ -133,6 +162,15 @@ function classifyPluginEntry(entry: OpenCodePluginEntry, index: number, canonica
}
}
if (isCustomPackageNotifyCandidate(entry)) {
return {
kind: "unsafe-external",
entry,
index,
reason: "notify package entry is not an exact recognized kdco/notify identifier",
}
}
return { kind: "other", entry, index }
}
@@ -172,6 +210,15 @@ function classifyPluginEntry(entry: OpenCodePluginEntry, index: number, canonica
}
}
if (isCustomPackageNotifyCandidate(tupleKey)) {
return {
kind: "unsafe-external",
entry,
index,
reason: "notify package tuple entry is not an exact recognized kdco/notify identifier",
}
}
return { kind: "other", entry, index }
}