fix(ai): address Alibaba image audit findings
This commit is contained in:
parent
07ce99dc9e
commit
0baabcdc4b
6 changed files with 149 additions and 23 deletions
|
|
@ -2,6 +2,7 @@ import { Config } from "effect"
|
|||
import { Auth } from "../src/route"
|
||||
import type { ModelFactory } from "../src/route/auth-options"
|
||||
import * as OpenAIChat from "../src/protocols/openai-chat"
|
||||
import * as Alibaba from "../src/providers/alibaba"
|
||||
import * as AmazonBedrock from "../src/providers/amazon-bedrock"
|
||||
import * as Anthropic from "../src/providers/anthropic"
|
||||
import * as AnthropicCompatible from "../src/providers/anthropic-compatible"
|
||||
|
|
@ -250,3 +251,9 @@ Cloudflare.CloudflareWorkersAI.configure({ accountId: "account", apiKey: "cf-key
|
|||
GitHubCopilot.configure({ baseURL: "https://copilot.test", apiKey: "copilot-key" }).model("gpt-4.1")
|
||||
// @ts-expect-error GitHub Copilot model selectors only accept model ids.
|
||||
GitHubCopilot.configure({ baseURL: "https://copilot.test", apiKey: "copilot-key" }).model("gpt-4.1", {})
|
||||
|
||||
Alibaba.configure({ image: { providerOptions: { qwen: { promptExtend: true } } } }).image("qwen-image-2.0")
|
||||
// @ts-expect-error Alibaba Qwen options are closed and reject unimplemented parameters.
|
||||
Alibaba.configure({ image: { providerOptions: { qwen: { style: "photorealistic" } } } })
|
||||
// @ts-expect-error Alibaba Wan options are closed and reject unimplemented parameters.
|
||||
Alibaba.configure({ image: { providerOptions: { wan: { parameters: { watermark: true } } } } })
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"metadata": {
|
||||
"tags": ["prefix:alibaba-images", "provider:alibaba", "protocol:alibaba-images"],
|
||||
"name": "alibaba-images/generates-with-qwen-image-2-0",
|
||||
"recordedAt": "2026-07-19T16:05:19.642Z"
|
||||
"recordedAt": "2026-07-19T16:15:36.863Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"output\":{\"choices\":[{\"finish_reason\":\"stop\",\"message\":{\"content\":[{\"image\":\"https://dashscope-463f.oss-accelerate.aliyuncs.com/7d/7b/20260720/991d10e3/fe425a64-dc6f-4c67-a4fa-a06810984ba9.png?Expires=1785082919&OSSAccessKeyId=fixture&Signature=fixture\"}],\"role\":\"assistant\"}}]},\"usage\":{\"height\":512,\"image_count\":1,\"width\":512},\"request_id\":\"484490f4-9881-90eb-8d52-d270d2395920\"}"
|
||||
"body": "{\"output\":{\"choices\":[{\"finish_reason\":\"stop\",\"message\":{\"content\":[{\"image\":\"https://dashscope-463f.oss-accelerate.aliyuncs.com/7d/04/20260720/991d10e3/93325a03-aa95-4dd3-a395-8d7246b5e72c.png?Expires=1785083536&OSSAccessKeyId=%5BREDACTED%5D&Signature=%5BREDACTED%5D\"}],\"role\":\"assistant\"}}]},\"usage\":{\"height\":512,\"image_count\":1,\"width\":512},\"request_id\":\"9f94d544-393b-9d6f-b005-4d0ff21f6fdf\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"metadata": {
|
||||
"tags": ["prefix:alibaba-images", "provider:alibaba", "protocol:alibaba-images"],
|
||||
"name": "alibaba-images/generates-with-wan-2-7",
|
||||
"recordedAt": "2026-07-19T16:05:23.617Z"
|
||||
"recordedAt": "2026-07-19T16:15:40.689Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"output\":{\"choices\":[{\"finish_reason\":\"stop\",\"message\":{\"content\":[{\"image\":\"https://dashscope-463f.oss-accelerate.aliyuncs.com/1d/ae/20260720/10739b93/1784477122_595381b5.png?Expires=1784563522&OSSAccessKeyId=fixture&Signature=fixture\",\"type\":\"image\"}],\"role\":\"assistant\"}}],\"finished\":true},\"usage\":{\"image_count\":1,\"input_tokens\":31,\"output_tokens\":2,\"size\":\"1024*1024\",\"total_tokens\":33},\"request_id\":\"ec5cacb5-c7b0-9e46-8459-49003a3816c3\"}"
|
||||
"body": "{\"output\":{\"choices\":[{\"finish_reason\":\"stop\",\"message\":{\"content\":[{\"image\":\"https://dashscope-463f.oss-accelerate.aliyuncs.com/1d/0b/20260720/10739b93/1784477739_553aee1e.png?Expires=1784564140&OSSAccessKeyId=%5BREDACTED%5D&Signature=%5BREDACTED%5D\",\"type\":\"image\"}],\"role\":\"assistant\"}}],\"finished\":true},\"usage\":{\"image_count\":1,\"input_tokens\":31,\"output_tokens\":2,\"size\":\"1024*1024\",\"total_tokens\":33},\"request_id\":\"3eb93323-2d8f-9aa5-b010-82dd8fab80ea\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Effect, Option, Schema } from "effect"
|
||||
import { Image } from "../../src"
|
||||
import { Alibaba } from "../../src/providers"
|
||||
import { recordedTests } from "../recorded-test"
|
||||
|
|
@ -8,11 +8,77 @@ const alibaba = Alibaba.configure({
|
|||
apiKey: process.env.DASHSCOPE_API_KEY ?? "fixture",
|
||||
})
|
||||
|
||||
const SIGNED_QUERY_CREDENTIALS = new Set(
|
||||
[
|
||||
"AccessKeyId",
|
||||
"AWSAccessKeyId",
|
||||
"GoogleAccessId",
|
||||
"OSSAccessKeyId",
|
||||
"Signature",
|
||||
"SecurityToken",
|
||||
"X-Amz-Credential",
|
||||
"X-Amz-Security-Token",
|
||||
"X-Amz-Signature",
|
||||
"X-Goog-Credential",
|
||||
"X-Goog-Signature",
|
||||
"X-OSS-Security-Token",
|
||||
].map((name) => name.replace(/[^a-z0-9]/gi, "").toLowerCase()),
|
||||
)
|
||||
|
||||
const redactSignedUrl = (value: string) => {
|
||||
if (!URL.canParse(value)) return value
|
||||
const url = new URL(value)
|
||||
if (url.protocol !== "http:" && url.protocol !== "https:") return value
|
||||
url.searchParams.forEach((_, name) => {
|
||||
if (SIGNED_QUERY_CREDENTIALS.has(name.replace(/[^a-z0-9]/gi, "").toLowerCase()))
|
||||
url.searchParams.set(name, "[REDACTED]")
|
||||
})
|
||||
return url.toString()
|
||||
}
|
||||
|
||||
const redactSignedUrls = (body: string) =>
|
||||
Option.match(Schema.decodeUnknownOption(Schema.UnknownFromJsonString)(body), {
|
||||
onNone: () => body,
|
||||
onSome: (value) =>
|
||||
JSON.stringify(
|
||||
(function redact(input: unknown): unknown {
|
||||
if (typeof input === "string") return redactSignedUrl(input)
|
||||
if (Array.isArray(input)) return input.map(redact)
|
||||
if (input === null || typeof input !== "object") return input
|
||||
return Object.fromEntries(Object.entries(input).map(([key, child]) => [key, redact(child)]))
|
||||
})(value),
|
||||
),
|
||||
})
|
||||
|
||||
const recorded = recordedTests({
|
||||
prefix: "alibaba-images",
|
||||
provider: "alibaba",
|
||||
protocol: "alibaba-images",
|
||||
requires: ["DASHSCOPE_API_KEY"],
|
||||
options: { redact: { body: redactSignedUrls } },
|
||||
})
|
||||
|
||||
test("recorder redacts signed URL credentials in nested JSON strings repeatably", () => {
|
||||
const body = JSON.stringify({
|
||||
output: {
|
||||
image:
|
||||
"https://example.oss.aliyuncs.com/image.png?Expires=1893456000&OSSAccessKeyId=access&Signature=signature&x-oss-security-token=token",
|
||||
},
|
||||
mirrors: ["https://storage.googleapis.com/image.png?X-Goog-Credential=credential&X-Goog-Signature=signature"],
|
||||
})
|
||||
const once = redactSignedUrls(body)
|
||||
const twice = redactSignedUrls(once)
|
||||
const parsed = JSON.parse(once)
|
||||
const oss = new URL(parsed.output.image)
|
||||
const gcs = new URL(parsed.mirrors[0])
|
||||
|
||||
expect(oss.searchParams.get("Expires")).toBe("1893456000")
|
||||
expect(oss.searchParams.get("OSSAccessKeyId")).toBe("[REDACTED]")
|
||||
expect(oss.searchParams.get("Signature")).toBe("[REDACTED]")
|
||||
expect(oss.searchParams.get("x-oss-security-token")).toBe("[REDACTED]")
|
||||
expect(gcs.searchParams.get("X-Goog-Credential")).toBe("[REDACTED]")
|
||||
expect(gcs.searchParams.get("X-Goog-Signature")).toBe("[REDACTED]")
|
||||
expect(twice).toBe(once)
|
||||
})
|
||||
|
||||
describe("Alibaba Images recorded", () => {
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ describe("Alibaba Images", () => {
|
|||
})
|
||||
|
||||
expect(result.image?.data).toContain("dashscope-result-intl")
|
||||
expect(result.image?.expiresAt).toBe("2030-01-01T00:00:00.000Z")
|
||||
expect(result.image?.providerMetadata).toEqual({
|
||||
alibaba: {
|
||||
modelId: "qwen-image-2.0-pro",
|
||||
|
|
@ -193,4 +194,54 @@ describe("Alibaba Images", () => {
|
|||
),
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("rejects parameters overlays owned by the protocol", () =>
|
||||
Image.generate({
|
||||
model: Alibaba.configure({ apiKey: "test" }).image("qwen-image-2.0"),
|
||||
prompt: "A robot",
|
||||
http: { body: { parameters: { watermark: true } } },
|
||||
}).pipe(
|
||||
Effect.flip,
|
||||
Effect.tap((error) =>
|
||||
Effect.sync(() => {
|
||||
expect(error.reason._tag).toBe("InvalidRequest")
|
||||
expect(error.message).toContain("http.body cannot overlay protocol-owned field(s): parameters")
|
||||
}),
|
||||
),
|
||||
Effect.provide(
|
||||
ImageClient.layer.pipe(
|
||||
Layer.provide(
|
||||
dynamicResponse(() => Effect.die("reserved body validation must happen before the request is sent")),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("ignores finite expiration values outside the Date range", () =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* Image.generate({
|
||||
model: Alibaba.configure({ apiKey: "test" }).image("qwen-image-2.0"),
|
||||
prompt: "A robot",
|
||||
})
|
||||
|
||||
expect(result.image?.expiresAt).toBeUndefined()
|
||||
expect(result.image?.providerMetadata?.alibaba?.expiresAt).toBeUndefined()
|
||||
}).pipe(
|
||||
Effect.provide(
|
||||
ImageClient.layer.pipe(
|
||||
Layer.provide(
|
||||
dynamicResponse((input) => {
|
||||
const payload = response("qwen")
|
||||
payload.output.choices[0].message.content[0].image =
|
||||
"https://dashscope-result-intl.oss-cn-singapore.aliyuncs.com/result.png?Expires=9007199254740991"
|
||||
return Effect.succeed(
|
||||
input.respond(JSON.stringify(payload), { headers: { "content-type": "application/json" } }),
|
||||
)
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue