fix(notify): restrict recognized kdco specs to safe forms
This commit is contained in:
@@ -83,6 +83,19 @@ describe("ensureBundledNotifyOwnership", () => {
|
||||
expect(readConfig(userConfigPath).plugin).toEqual(["oh-my-openagent", canonicalEntry])
|
||||
})
|
||||
|
||||
test("rewrites recognized npm-prefixed external notify with version to bundled owner", () => {
|
||||
// given
|
||||
const userConfigPath = join(userConfigDir, "opencode.json")
|
||||
writeFileSync(userConfigPath, JSON.stringify({ plugin: ["npm:kdco/notify@1.2.3", "oh-my-openagent"] }, null, 2) + "\n")
|
||||
|
||||
// when
|
||||
const result = ensureBundledNotifyOwnership({ projectDirectory: projectDir, packageRoot })
|
||||
|
||||
// then
|
||||
expect(result.changedUserConfig).toBe(true)
|
||||
expect(readConfig(userConfigPath).plugin).toEqual(["oh-my-openagent", canonicalEntry])
|
||||
})
|
||||
|
||||
test("rewrites recognized tuple notify in user config when tuple options are empty", () => {
|
||||
// given
|
||||
const userConfigPath = join(userConfigDir, "opencode.json")
|
||||
@@ -150,6 +163,19 @@ describe("ensureBundledNotifyOwnership", () => {
|
||||
expect(readConfig(userConfigPath).plugin).toEqual(["alias@npm:@custom/opencode-notify@1.2.3", "oh-my-openagent"])
|
||||
})
|
||||
|
||||
test("fails loudly for npm alias string spec targeting recognized kdco/notify package", () => {
|
||||
// given
|
||||
const userConfigPath = join(userConfigDir, "opencode.json")
|
||||
writeFileSync(userConfigPath, JSON.stringify({ plugin: ["alias@npm:kdco/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(["alias@npm:kdco/notify@1.2.3", "oh-my-openagent"])
|
||||
})
|
||||
|
||||
test("fails loudly for npm alias tuple spec targeting custom opencode-notify package", () => {
|
||||
// given
|
||||
const userConfigPath = join(userConfigDir, "opencode.json")
|
||||
@@ -166,6 +192,93 @@ describe("ensureBundledNotifyOwnership", () => {
|
||||
expect(readConfig(userConfigPath).plugin).toEqual([["alias@npm:@custom/opencode-notify@1.2.3", {}], "oh-my-openagent"])
|
||||
})
|
||||
|
||||
test("fails loudly for npm alias tuple spec targeting recognized kdco/notify package", () => {
|
||||
// given
|
||||
const userConfigPath = join(userConfigDir, "opencode.json")
|
||||
writeFileSync(
|
||||
userConfigPath,
|
||||
JSON.stringify({ plugin: [["alias@npm:kdco/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([["alias@npm:kdco/notify@1.2.3", {}], "oh-my-openagent"])
|
||||
})
|
||||
|
||||
test("fails loudly for non-exact recognized-like spec with nested npm alias in string form", () => {
|
||||
// given
|
||||
const userConfigPath = join(userConfigDir, "opencode.json")
|
||||
writeFileSync(userConfigPath, JSON.stringify({ plugin: ["kdco/notify@npm:kdco/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(["kdco/notify@npm:kdco/notify@1.2.3", "oh-my-openagent"])
|
||||
})
|
||||
|
||||
test("fails loudly for non-exact recognized-like spec with nested npm alias in tuple form", () => {
|
||||
// given
|
||||
const userConfigPath = join(userConfigDir, "opencode.json")
|
||||
writeFileSync(
|
||||
userConfigPath,
|
||||
JSON.stringify({ plugin: [["kdco/notify@npm:kdco/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([["kdco/notify@npm:kdco/notify@1.2.3", {}], "oh-my-openagent"])
|
||||
})
|
||||
|
||||
test("fails loudly for recognized notify package with file source suffix in string form", () => {
|
||||
// given
|
||||
const userConfigPath = join(userConfigDir, "opencode.json")
|
||||
writeFileSync(userConfigPath, JSON.stringify({ plugin: ["kdco/notify@file:../local", "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(["kdco/notify@file:../local", "oh-my-openagent"])
|
||||
})
|
||||
|
||||
test("fails loudly for recognized notify package with file source suffix in tuple form", () => {
|
||||
// given
|
||||
const userConfigPath = join(userConfigDir, "opencode.json")
|
||||
writeFileSync(
|
||||
userConfigPath,
|
||||
JSON.stringify({ plugin: [["kdco/notify@file:../local", {}], "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([["kdco/notify@file:../local", {}], "oh-my-openagent"])
|
||||
})
|
||||
|
||||
test("fails loudly for npm-prefixed recognized notify package with workspace source suffix", () => {
|
||||
// given
|
||||
const userConfigPath = join(userConfigDir, "opencode.json")
|
||||
writeFileSync(userConfigPath, JSON.stringify({ plugin: ["npm:kdco/notify@workspace:*", "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:kdco/notify@workspace:*", "oh-my-openagent"])
|
||||
})
|
||||
|
||||
test("migrates stale bundled dist/opencode-notify file URL to canonical bundled entry", () => {
|
||||
// given
|
||||
const userConfigPath = join(userConfigDir, "opencode.json")
|
||||
|
||||
@@ -63,7 +63,30 @@ function isPathLikePluginEntry(entry: string): boolean {
|
||||
|
||||
function isRecognizedExternalNotifyId(entry: string): boolean {
|
||||
const normalized = entry.trim().toLowerCase()
|
||||
return KNOWN_EXTERNAL_NOTIFY_IDS.some((base) => normalized === base || normalized.startsWith(`${base}@`))
|
||||
|
||||
return KNOWN_EXTERNAL_NOTIFY_IDS.some((base) => {
|
||||
if (normalized === base) return true
|
||||
if (!normalized.startsWith(`${base}@`)) return false
|
||||
|
||||
const versionSuffix = normalized.slice(base.length + 1)
|
||||
if (versionSuffix.length === 0) return false
|
||||
if (versionSuffix.includes("@")) return false
|
||||
if (versionSuffix.includes("://")) return false
|
||||
if (versionSuffix.includes(":")) return false
|
||||
if (versionSuffix.includes("/")) return false
|
||||
if (versionSuffix.includes("\\")) return false
|
||||
|
||||
return /^[a-z0-9.*+!~^<>=| -]+$/i.test(versionSuffix)
|
||||
})
|
||||
}
|
||||
|
||||
function isRecognizedNotifyIdWithUnsupportedSuffix(entry: string): boolean {
|
||||
const normalized = entry.trim().toLowerCase()
|
||||
|
||||
return KNOWN_EXTERNAL_NOTIFY_IDS.some((base) => {
|
||||
if (!normalized.startsWith(`${base}@`)) return false
|
||||
return !isRecognizedExternalNotifyId(normalized)
|
||||
})
|
||||
}
|
||||
|
||||
function stripNpmPrefix(entry: string): string {
|
||||
@@ -97,6 +120,18 @@ function extractPackageIdentifierFromSpec(entry: string): string | null {
|
||||
return stripVersionSuffix(packageSpec)
|
||||
}
|
||||
|
||||
function isAliasedRecognizedNotifyTarget(entry: string): boolean {
|
||||
const normalized = stripNpmPrefix(entry.trim().toLowerCase())
|
||||
const aliasSeparatorIndex = normalized.indexOf("@npm:")
|
||||
if (aliasSeparatorIndex <= 0) return false
|
||||
|
||||
const aliasedTargetSpec = normalized.slice(aliasSeparatorIndex + "@npm:".length)
|
||||
if (aliasedTargetSpec.length === 0) return false
|
||||
|
||||
const aliasedPackageIdentifier = stripVersionSuffix(stripNpmPrefix(aliasedTargetSpec))
|
||||
return aliasedPackageIdentifier === "kdco/notify"
|
||||
}
|
||||
|
||||
function isCustomPackageNotifyCandidate(entry: string): boolean {
|
||||
const packageIdentifier = extractPackageIdentifierFromSpec(entry)
|
||||
if (!packageIdentifier) return false
|
||||
@@ -173,6 +208,24 @@ function classifyPluginEntry(entry: OpenCodePluginEntry, index: number, canonica
|
||||
}
|
||||
}
|
||||
|
||||
if (isRecognizedNotifyIdWithUnsupportedSuffix(entry)) {
|
||||
return {
|
||||
kind: "unsafe-external",
|
||||
entry,
|
||||
index,
|
||||
reason: "recognized notify package uses unsupported source/custom suffix",
|
||||
}
|
||||
}
|
||||
|
||||
if (isAliasedRecognizedNotifyTarget(entry)) {
|
||||
return {
|
||||
kind: "unsafe-external",
|
||||
entry,
|
||||
index,
|
||||
reason: "aliased kdco/notify entries are treated as custom notify owners",
|
||||
}
|
||||
}
|
||||
|
||||
if (isCustomPackageNotifyCandidate(entry)) {
|
||||
return {
|
||||
kind: "unsafe-external",
|
||||
@@ -221,6 +274,24 @@ function classifyPluginEntry(entry: OpenCodePluginEntry, index: number, canonica
|
||||
}
|
||||
}
|
||||
|
||||
if (isRecognizedNotifyIdWithUnsupportedSuffix(tupleKey)) {
|
||||
return {
|
||||
kind: "unsafe-external",
|
||||
entry,
|
||||
index,
|
||||
reason: "recognized notify package uses unsupported source/custom suffix",
|
||||
}
|
||||
}
|
||||
|
||||
if (isAliasedRecognizedNotifyTarget(tupleKey)) {
|
||||
return {
|
||||
kind: "unsafe-external",
|
||||
entry,
|
||||
index,
|
||||
reason: "aliased kdco/notify entries are treated as custom notify owners",
|
||||
}
|
||||
}
|
||||
|
||||
if (isCustomPackageNotifyCandidate(tupleKey)) {
|
||||
return {
|
||||
kind: "unsafe-external",
|
||||
|
||||
Reference in New Issue
Block a user