feat: add Microsoft Designer as image provider (#6672) (#7609)

* feat(sse): add Microsoft Designer as image provider (#6672)

Adds `microsoft-designer-web` — an unofficial, reverse-engineered
Bearer-token web-session image provider, modeled on the existing
`chatgpt-web`/`copilot-m365-web` "-web" provider category.

- Registers the provider in WEB_COOKIE_PROVIDERS (src/shared/constants/
  providers/web-cookie.ts) and IMAGE_PROVIDERS (open-sse/config/
  imageRegistry.ts, new "designer-web" format).
- New handler open-sse/handlers/imageGeneration/providers/designerWeb.ts
  implements the submit-then-poll DallE.ashx flow (Bearer access_token +
  ClientId/SessionId/UserId headers -> form POST -> poll for
  image_urls_thumbnail), wired into handleImageGeneration()'s dispatch.
- The upstream ClientId header is a fixed, publicly-shared value (not a
  secret) — routed through resolvePublicCred() per Hard Rule #11, never
  as a string literal.
- Registers the token-based credential requirement in
  webSessionCredentials.ts so the provider-connect UI asks for the
  right field; connection validation falls back to the existing generic
  web-cookie session-ping validator (no dedicated validator needed).
- Extracted the KIE image-model catalog into a co-located
  open-sse/config/providers/registry/kie/models.ts module (mirrors the
  existing lmarena/directModels.ts pattern) to keep imageRegistry.ts
  under the file-size cap while adding the new provider entry.
- Regenerated docs/reference/PROVIDER_REFERENCE.md (250 -> 251
  providers) and updated the plain-text counts in README.md, AGENTS.md,
  CLAUDE.md.

Tests (tests/unit/microsoft-designer-web-6672.test.ts, 16 cases):
registry-entry shape assertions, the resolvePublicCred() shape
assertion (Hard Rule #11), and the pure header/form-body/response-
parsing helpers plus the handler's submit/poll/error/timeout paths
against a mocked fetch — no live Designer session required.

Reverse-engineered from the g4f MicrosoftDesigner.py provider reference
(researched during #6672 triage); the exact upstream response shape has
not been validated against a live Designer session, so the poll-loop
implementation follows the documented g4f contract as closely as
possible without a live capture.

* fix(providers): satisfy web-cookie executor contract + document designer-web env vars (#6672)
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-17 12:09:12 -03:00
committed by GitHub
parent b0b40a3283
commit 5bacb719d3
15 changed files with 646 additions and 5 deletions

View File

@@ -1425,6 +1425,12 @@ APP_LOG_TO_FILE=true
# NANOBANANA_POLL_TIMEOUT_MS=120000 # Max wait for job completion (default: 120s)
# NANOBANANA_POLL_INTERVAL_MS=2500 # Poll frequency (default: 2.5s)
# ── Microsoft Designer Web (Image Generation) ──
# Polling config for the microsoft-designer-web submit-then-poll image job.
# Used by: open-sse/handlers/imageGeneration/providers/designerWeb.ts
# DESIGNER_WEB_POLL_TIMEOUT_MS=60000 # Max wait for job completion (default: 60s)
# DESIGNER_WEB_POLL_INTERVAL_MS=2000 # Poll frequency (default: 2s)
# ── AWS Bedrock (Kiro / Audio) ──
# Region used to construct AWS Bedrock endpoints. Used by:
# src/lib/providers/validation.ts and open-sse/handlers/audioSpeech.ts.

View File

@@ -0,0 +1 @@
- feat(sse): add Microsoft Designer as an unofficial web-session image provider, reverse-engineered submit-then-poll DallE.ashx flow (#6672)

View File

@@ -801,6 +801,8 @@ Automatic model pricing data synchronization from external sources.
| `MODEL_CATALOG_INCLUDE_NAMES` | `true` | `src/shared/constants/featureFlagDefinitions.ts` | Include display-friendly `name` fields in `/v1/models` responses. Disable for clients that expect IDs only. |
| `NANOBANANA_POLL_TIMEOUT_MS` | `120000` | `open-sse/handlers/imageGeneration.ts` | Max wait for NanoBanana image generation jobs. |
| `NANOBANANA_POLL_INTERVAL_MS` | `2500` | `open-sse/handlers/imageGeneration.ts` | NanoBanana job polling frequency. |
| `DESIGNER_WEB_POLL_TIMEOUT_MS` | `60000` | `open-sse/handlers/imageGeneration/providers/designerWeb.ts` | Max wait for microsoft-designer-web image generation jobs. |
| `DESIGNER_WEB_POLL_INTERVAL_MS` | `2000` | `open-sse/handlers/imageGeneration/providers/designerWeb.ts` | microsoft-designer-web job polling frequency. |
| `AWS_REGION` | _(unset)_ | `src/lib/providers/validation.ts`, `open-sse/handlers/audioSpeech.ts` | Region used to construct AWS Bedrock endpoints (Kiro, audio). |
| `AWS_DEFAULT_REGION` | _(unset)_ | `src/lib/providers/validation.ts`, `open-sse/handlers/audioSpeech.ts` | Fallback when `AWS_REGION` is not set. |
| `CLOUDFLARE_ACCOUNT_ID` | _(unset)_ | `open-sse/executors/cloudflare-ai.ts` | Account ID for Cloudflare Workers AI. |

View File

@@ -10,7 +10,7 @@ lastUpdated: 2026-07-17
> Regenerate with: `npm run gen:provider-reference`
> **Last generated:** 2026-07-17
Total providers: **256**. See category breakdown below.
Total providers: **258**. See category breakdown below.
## Categories
@@ -58,7 +58,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
| `zed` | `zd` | Zed IDE | OAuth | [link](https://zed.dev) | Zed stores LLM provider credentials (OpenAI, Anthropic, Google, Mistral, xAI) in the OS keychain. Use the Import button below to discover and import them automatically. |
| `zed-hosted` | — | Zed Hosted Models | OAuth | [link](https://zed.dev) | Sign in with your Zed account (native-app sign-in). OmniRoute generates a one-time RSA keypair and opens zed.dev to authorize it — on a remote/headless install, copy the resulting 127.0.0.1 callback URL from your browser's address bar and paste it back here. Distinct from the 'Zed IDE' credential-import entry above: this proxies chat completions through Zed's own hosted model aggregator (cloud.zed.dev), fronting Anthropic/OpenAI/Google/xAI models under your Zed plan. |
## Web Cookie Providers (26)
## Web Cookie Providers (27)
| ID | Alias | Name | Tags | Website | Notes |
|----|-------|------|------|---------|-------|
@@ -77,6 +77,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
| `inner-ai` | `in-ai` | Inner.ai (Subscription) | Web cookie | [link](https://app.innerai.com) | Paste your token cookie and email separated by a space: open DevTools → Application → Cookies → .innerai.com, copy the token value, then append a space and your Inner.ai login email. Example: eyJhbG... user@example.com |
| `kimi-web` | `kimi-web` | Kimi Web (Moonshot AI) | Web cookie | [link](https://www.kimi.com) | Paste your Cookie header from www.kimi.com (must contain kimi-auth=...). Find it via DevTools → Network → request → Cookie. |
| `lmarena` | `lma` | Arena (Free) | Web cookie | [link](https://arena.ai) | Paste the full Cookie header from arena.ai (DevTools → Network → request → Cookie). Include arena-auth-prod-v1.0/.1… and cf_clearance/__cf_bm when present. OmniRoute uses Chrome TLS impersonation; if Arena still 403s, set providerSpecificData.recaptchaV3Token from a live browser session. |
| `microsoft-designer-web` | `msdesigner` | Microsoft Designer (Image Generation) | Web cookie | [link](https://designer.microsoft.com) | Sign in at designer.microsoft.com, then open DevTools → Network, generate an image, and find the request to DallE.ashx?action=GetDallEImagesCogSci. Copy the value of its Authorization: Bearer header (the access_token — no 'Bearer ' prefix). The token is short-lived; this is an unofficial, reverse-engineered integration. |
| `muse-spark-web` | `ms-web` | Muse Spark Web (Meta AI) | Web cookie | [link](https://www.meta.ai) | Paste your ecto_1_sess value or full cookie header from meta.ai |
| `notion-web` | `nw` | Notion AI Web (Unofficial/Experimental) | Web cookie | [link](https://www.notion.so) | Paste your token_v2 cookie value from notion.so (DevTools → Application → Cookies). Optionally append `; space_id=...` and/or `; notion_browser_id=...` if your workspace requires them. |
| `perplexity-web` | `pplx-web` | Perplexity Web (Pro/Max) | Web cookie | [link](https://www.perplexity.ai) | Paste your __Secure-next-auth.session-token cookie value from perplexity.ai |
@@ -89,7 +90,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
| `zai-web` | `zw` | Z.ai Web (Free) | Web cookie | [link](https://chat.z.ai) | Paste the full Cookie header from chat.z.ai (must include the token=<JWT> cookie) |
| `zenmux-free` | `zmf` | ZenMux Free (Web) | Web cookie | [link](https://zenmux.ai) | Login at zenmux.ai, then export all cookies using EditThisCookie or Cookie-Editor and paste the full Cookie header string here. Refresh every ~30 days. |
## API Key Providers (paid / paid-with-free-credits) (170)
## API Key Providers (paid / paid-with-free-credits) (171)
| ID | Alias | Name | Tags | Website | Notes |
|----|-------|------|------|---------|-------|
@@ -145,6 +146,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
| `fireworks` | `fireworks` | Fireworks AI | API key | [link](https://fireworks.ai) | $1 free starter credits on signup for API testing |
| `freeaiapikey` | `faik` | FreeAIAPIKey | API key | [link](https://freeaiapikey.com) | — |
| `freemodel-dev` | `fmd` | FreeModel.dev | API key | [link](https://freemodel.dev) | $300 free credits on signup — no credit card required. Access GPT-5.4 and GPT-5.5 (OpenAI's latest flagship models) through an OpenAI-compatible API. |
| `freepik` | `fpk` | Freepik (Mystic) | API key, image | [link](https://freepik.com) | Get API key at freepik.com/developers (Mystic image endpoint) |
| `freetheai` | `fta` | FreeTheAi | API key, aggregator | [link](https://freetheai.xyz) | Join the FreeTheAi Discord to get your free API key. |
| `friendliai` | `friendli` | FriendliAI | API key | [link](https://friendli.ai) | Free tier for serverless inference — no credit card required |
| `galadriel` | `galadriel` | Galadriel | API key | [link](https://galadriel.com) | ⚠️ **DEPRECATED.** api.galadriel.ai no longer resolves (sweep 2026-06-19); the inference API appears discontinued. |

View File

@@ -170,6 +170,17 @@ export const IMAGE_PROVIDERS: Record<string, ImageProviderConfig> = {
supportedSizes: ["1024x1024", "1024x1536", "1536x1024"],
},
"microsoft-designer-web": {
id: "microsoft-designer-web",
alias: "msdesigner",
baseUrl: "https://designerapp.officeapps.live.com/designerapp/DallE.ashx?action=GetDallEImagesCogSci",
authType: "apikey",
authHeader: "bearer",
format: "designer-web",
models: [{ id: "dall-e-3", name: "DALL-E 3 (Microsoft Designer Web)" }],
supportedSizes: ["1024x1024", "1792x1024", "1024x1792"],
},
xai: {
id: "xai",
baseUrl: "https://api.x.ai/v1/images/generations",

View File

@@ -0,0 +1,43 @@
/**
* KIE image-provider model catalog — extracted from imageRegistry.ts
* (god-file decomposition, mirrors the lmarena/directModels.ts pattern).
* Pure data literal; imported by imageRegistry.ts. No behavior change.
*/
export const KIE_IMAGE_MODELS = [
{ id: "gpt4o-image", name: "KIE 4o Image" },
{ id: "seedream/4.5-text-to-image", name: "Seedream 4.5", isMarket: true },
{ id: "seedream/4.5-edit", name: "Seedream 4.5 Edit", isMarket: true },
{ id: "seedream/5.0-lite-text-to-image", name: "Seedream 5.0 Lite", isMarket: true },
{ id: "seedream/5.0-lite-image-to-image", name: "Seedream 5.0 Lite I2I", isMarket: true },
{ id: "z-image/4.0-text-to-image", name: "Z-Image v4.0", isMarket: true },
{ id: "z-image/4.5-text-to-image", name: "Z-Image v4.5", isMarket: true },
{ id: "google-imagen/imagen4-fast", name: "Imagen 4 Fast", isMarket: true },
{ id: "google-imagen/imagen4-ultra", name: "Imagen 4 Ultra", isMarket: true },
{ id: "google-imagen/imagen4", name: "Imagen 4", isMarket: true },
{ id: "google-imagen/nano-banana-2", name: "Nano Banana 2", isMarket: true },
{ id: "google-imagen/nano-banana", name: "Nano Banana", isMarket: true },
{ id: "google-imagen/nano-banana-pro", name: "Nano Banana Pro", isMarket: true },
{ id: "google-imagen/nano-banana-edit", name: "Nano Banana Edit", isMarket: true },
{ id: "flux/2-pro-image-to-image", name: "Flux 2 Pro I2I", isMarket: true },
{ id: "flux/2-pro-text-to-image", name: "Flux 2 Pro T2I", isMarket: true },
{ id: "flux/2-image-to-image", name: "Flux 2 I2I", isMarket: true },
{ id: "flux/2-text-to-image", name: "Flux 2 T2I", isMarket: true },
{ id: "flux/kontext", name: "Flux Kontext", isMarket: true },
{ id: "grok-imagine/text-to-image", name: "Grok Imagine T2I", isMarket: true },
{ id: "grok-imagine/image-to-image", name: "Grok Imagine I2I", isMarket: true },
{ id: "gpt/gpt-image-1.5-text-to-image", name: "GPT Image 1.5 T2I", isMarket: true },
{ id: "gpt/gpt-image-1.5-image-to-image", name: "GPT Image 1.5 I2I", isMarket: true },
{ id: "gpt/gpt-image-2-text-to-image", name: "GPT Image 2 T2I", isMarket: true },
{ id: "gpt/gpt-image-2-image-to-image", name: "GPT Image 2 I2I", isMarket: true },
{ id: "ideogram/v3-text-to-image", name: "Ideogram v3", isMarket: true },
{ id: "ideogram/v3-edit", name: "Ideogram v3 Edit", isMarket: true },
{ id: "ideogram/v3-remix", name: "Ideogram v3 Remix", isMarket: true },
{ id: "ideogram/v3-reframe", name: "Ideogram v3 Reframe", isMarket: true },
{ id: "qwen/text-to-image", name: "Qwen T2I", isMarket: true },
{ id: "qwen/image-to-image", name: "Qwen I2I", isMarket: true },
{ id: "qwen/image-edit", name: "Qwen Edit", isMarket: true },
{ id: "qwen2/image-edit", name: "Qwen2 Edit", isMarket: true },
{ id: "qwen2/text-to-image", name: "Qwen2 T2I", isMarket: true },
{ id: "wan/2.7-image", name: "Wan 2.7 Image", isMarket: true },
{ id: "wan/2.7-image-pro", name: "Wan 2.7 Image Pro", isMarket: true },
];

View File

@@ -36,6 +36,7 @@ import { AdaptaWebExecutor } from "./adapta-web.ts";
import { ClaudeWebWithAutoRefresh } from "./claude-web-with-auto-refresh.ts";
import { CopilotWebExecutor } from "./copilot-web.ts";
import { CopilotM365WebExecutor } from "./copilot-m365-web.ts";
import { MicrosoftDesignerWebExecutor } from "./microsoft-designer-web.ts";
import { VeoAIFreeWebExecutor } from "./veoaifree-web.ts";
import { DuckDuckGoWebExecutor } from "./duckduckgo-web.ts";
import { FeloWebExecutor } from "./felo-web.ts";
@@ -125,6 +126,8 @@ const executors = {
"copilot-web": new CopilotWebExecutor(),
"copilot-m365-web": new CopilotM365WebExecutor(),
copilot: new CopilotWebExecutor(), // Alias
"microsoft-designer-web": new MicrosoftDesignerWebExecutor(),
msdesigner: new MicrosoftDesignerWebExecutor(), // Alias
"veoaifree-web": new VeoAIFreeWebExecutor(),
"veo-free": new VeoAIFreeWebExecutor(), // Alias
"duckduckgo-web": new DuckDuckGoWebExecutor(),
@@ -239,6 +242,7 @@ export { DevinCliExecutor } from "./devin-cli.ts";
export { AuggieExecutor } from "./auggie.ts";
export { CopilotWebExecutor } from "./copilot-web.ts";
export { CopilotM365WebExecutor } from "./copilot-m365-web.ts";
export { MicrosoftDesignerWebExecutor } from "./microsoft-designer-web.ts";
export { VeoAIFreeWebExecutor } from "./veoaifree-web.ts";
export { DuckDuckGoWebExecutor } from "./duckduckgo-web.ts";
export { FeloWebExecutor } from "./felo-web.ts";

View File

@@ -0,0 +1,51 @@
// MicrosoftDesignerWebExecutor — chat-completions guard for the
// microsoft-designer-web web-cookie provider (#6672).
//
// Microsoft Designer (designerapp.officeapps.live.com/DallE.ashx) is an
// image-generation-only upstream: it has no chat/completions surface at all.
// The real request/response flow lives entirely in the image-generation
// handler (open-sse/handlers/imageGeneration/providers/designerWeb.ts),
// dispatched from open-sse/handlers/imageGeneration.ts by
// providerConfig.format === "designer-web" — NOT through getExecutor().
//
// microsoft-designer-web is still listed in WEB_COOKIE_PROVIDERS (it uses
// the same unofficial, DevTools-sourced bearer-token credential UX and
// subscription-risk notice as the other web-cookie providers — see
// tests/unit/microsoft-designer-web-6672.test.ts). Without a registered
// executor here, getExecutor("microsoft-designer-web") silently falls
// through to DefaultExecutor's `PROVIDERS[provider] || PROVIDERS.openai`
// fallback (open-sse/executors/index.ts:176 comment, #6699) — which would
// send the user's real Designer bearer token to api.openai.com, mislabeled
// as an OpenAI request, if anything ever mis-routes a chat/completions call
// to this provider.
//
// This executor closes that gap cheaply: it never calls the network. Any
// chat/completions attempt against microsoft-designer-web is rejected
// immediately with a clean, sanitized 400 telling the caller to use
// /v1/images/generations instead — satisfying the executor wrapper
// contract (tests/unit/executor-web-cookie-sweep.test.ts) without ever
// forwarding credentials anywhere.
import { BaseExecutor, type ExecuteInput } from "./base.ts";
import { makeExecutorErrorResult } from "../utils/error.ts";
const DESIGNER_WEB_BASE_URL =
"https://designerapp.officeapps.live.com/designerapp/DallE.ashx?action=GetDallEImagesCogSci";
export class MicrosoftDesignerWebExecutor extends BaseExecutor {
constructor() {
super("microsoft-designer-web", { id: "microsoft-designer-web", baseUrl: DESIGNER_WEB_BASE_URL });
}
async execute(_input: ExecuteInput) {
return makeExecutorErrorResult(
400,
"microsoft-designer-web is an image-generation-only provider and does not support " +
"chat completions. Use POST /v1/images/generations with model " +
'"microsoft-designer-web/dall-e-3" instead.',
_input.body,
DESIGNER_WEB_BASE_URL
);
}
}
export default MicrosoftDesignerWebExecutor;

View File

@@ -64,6 +64,7 @@ import {
CHATGPT_WEB_IMAGE_ID_RE,
} from "./imageGeneration/providers/chatgptWeb.ts";
import { handleNvidiaNimImageGeneration } from "./imageGeneration/providers/nvidiaNim.ts";
import { handleDesignerWebImageGeneration } from "./imageGeneration/providers/designerWeb.ts";
import { handleMinimaxImageGeneration } from "./imageGeneration/providers/minimax.ts";
@@ -462,6 +463,17 @@ export async function handleImageGeneration({
});
}
if (providerConfig.format === "designer-web") {
return handleDesignerWebImageGeneration({
model,
provider,
providerConfig,
body,
credentials,
log,
});
}
if (providerConfig.format === "nanobanana") {
return handleNanoBananaImageGeneration({
model,

View File

@@ -0,0 +1,265 @@
// Microsoft Designer (unofficial, reverse-engineered web API) image handler.
// Family: designer-web | Provider: microsoft-designer-web
// Reference: g4f/Provider/needs_auth/MicrosoftDesigner.py (fetched + verified
// during triage of #6672) — Bearer access_token auth against
// designerapp.officeapps.live.com/designerapp/DallE.ashx, submit-then-poll
// for image_urls_thumbnail[].ImageUrl.
//
// The upstream ClientId header is a fixed, publicly-shared value the
// designer.microsoft.com frontend sends on every session (not a secret) —
// routed through resolvePublicCred() per Hard Rule #11 / docs/security/PUBLIC_CREDS.md.
import { randomUUID, randomBytes } from "node:crypto";
import { resolvePublicCred } from "../../../utils/publicCreds.ts";
import { sanitizeErrorMessage } from "../../../utils/error.ts";
import { saveImageErrorResult, saveImageSuccessResult } from "../../imageGeneration.ts";
const DESIGNER_WEB_POLL_TIMEOUT_MS_DEFAULT = 60000;
const DESIGNER_WEB_POLL_INTERVAL_MS_DEFAULT = 2000;
const DESIGNER_WEB_BATCH_SIZE = "4";
/** Maps an OpenAI-style "WxH" size string to the closest Designer aspect ratio bucket. */
export function mapDesignerWebImageSize(size: unknown): "1_1" | "16_9" | "9_16" {
if (typeof size !== "string" || !size.includes("x")) return "1_1";
const [wRaw, hRaw] = size.split("x");
const w = Number(wRaw);
const h = Number(hRaw);
if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) return "1_1";
if (w > h * 1.2) return "16_9";
if (h > w * 1.2) return "9_16";
return "1_1";
}
/** Builds the fixed + per-request headers Microsoft Designer expects on every call. */
export function buildDesignerWebHeaders({
accessToken,
sessionId = randomUUID(),
userId = randomBytes(16).toString("hex"),
}: {
accessToken: string;
sessionId?: string;
userId?: string;
}): Record<string, string> {
return {
Authorization: `Bearer ${accessToken}`,
ClientId: resolvePublicCred("microsoft_designer_client_id"),
SessionId: sessionId,
UserId: userId,
"Content-Type": "application/x-www-form-urlencoded",
};
}
/** Builds the DallE.ashx form body from an OpenAI-shaped image-generation request. */
export function buildDesignerWebFormBody(prompt: string, size: unknown): URLSearchParams {
const params = new URLSearchParams();
params.set("dalle-caption", prompt);
params.set("dalle-image-size", mapDesignerWebImageSize(size));
params.set("dalle-batch-size", DESIGNER_WEB_BATCH_SIZE);
params.set("dalle-seed", String(Math.floor(Math.random() * 1_000_000_000)));
return params;
}
interface DesignerWebParsedResponse {
status: "ready" | "pending" | "empty";
imageUrls: string[];
pollIntervalMs: number | null;
}
/** Parses a DallE.ashx JSON body into a ready/pending/empty verdict. */
export function parseDesignerWebResponse(json: unknown): DesignerWebParsedResponse {
const body = (json ?? {}) as Record<string, unknown>;
const thumbs = Array.isArray(body.image_urls_thumbnail) ? body.image_urls_thumbnail : [];
const imageUrls = thumbs
.map((t) => (t && typeof t === "object" ? (t as Record<string, unknown>).ImageUrl : null))
.filter((u): u is string => typeof u === "string" && u.length > 0);
if (imageUrls.length > 0) {
return { status: "ready", imageUrls, pollIntervalMs: null };
}
const pollingMeta = (body.polling_response as Record<string, unknown> | undefined)
?.polling_meta_data as Record<string, unknown> | undefined;
const pollIntervalMs = Number.isFinite(pollingMeta?.poll_interval)
? Number(pollingMeta?.poll_interval)
: null;
if (pollIntervalMs !== null) {
return { status: "pending", imageUrls: [], pollIntervalMs };
}
return { status: "empty", imageUrls: [], pollIntervalMs: null };
}
function normalizePositiveNumber(value: unknown, fallback: number): number {
const n = Number(value);
return Number.isFinite(n) && n > 0 ? n : fallback;
}
interface DesignerWebRequestConfig {
prompt: string;
accessToken: string;
headers: Record<string, string>;
formBody: URLSearchParams;
timeoutMs: number;
pollIntervalMs: number;
}
/** Validates the request and resolves auth + poll timing. Returns an error status/message on failure. */
function resolveDesignerWebRequest(
body: { prompt?: unknown; size?: unknown; timeout_ms?: unknown; poll_interval_ms?: unknown },
credentials: { apiKey?: string; accessToken?: string }
): { ok: true; config: DesignerWebRequestConfig } | { ok: false; status: number; error: string } {
const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
if (!prompt) {
return {
ok: false,
status: 400,
error: "Prompt is required for Microsoft Designer image generation",
};
}
const accessToken = credentials?.apiKey || credentials?.accessToken;
if (!accessToken) {
return { ok: false, status: 401, error: "Microsoft Designer credentials missing access_token" };
}
const timeoutMs = normalizePositiveNumber(
body.timeout_ms,
normalizePositiveNumber(
process.env.DESIGNER_WEB_POLL_TIMEOUT_MS,
DESIGNER_WEB_POLL_TIMEOUT_MS_DEFAULT
)
);
const pollIntervalMs = normalizePositiveNumber(
body.poll_interval_ms,
normalizePositiveNumber(
process.env.DESIGNER_WEB_POLL_INTERVAL_MS,
DESIGNER_WEB_POLL_INTERVAL_MS_DEFAULT
)
);
return {
ok: true,
config: {
prompt,
accessToken,
headers: buildDesignerWebHeaders({ accessToken }),
formBody: buildDesignerWebFormBody(prompt, body.size),
timeoutMs,
pollIntervalMs,
},
};
}
type DesignerWebStepResult =
| { done: false; waitMs: number }
| { done: true; success: true; imageUrls: string[] }
| { done: true; success: false; status: number; error: string };
/** Runs one submit/poll fetch cycle and classifies the outcome. */
async function stepDesignerWebPoll(
baseUrl: string,
headers: Record<string, string>,
formBody: URLSearchParams,
pollIntervalMs: number,
fetchImpl: typeof fetch
): Promise<DesignerWebStepResult> {
const resp = await fetchImpl(baseUrl, { method: "POST", headers, body: formBody });
if (!resp.ok) {
return { done: true, success: false, status: resp.status, error: sanitizeErrorMessage(await resp.text()) };
}
const parsed = parseDesignerWebResponse(await resp.json());
if (parsed.status === "ready") {
return { done: true, success: true, imageUrls: parsed.imageUrls };
}
if (parsed.status === "empty") {
return {
done: true,
success: false,
status: 502,
error: "Microsoft Designer response did not contain image data or polling metadata",
};
}
return { done: false, waitMs: Math.min(parsed.pollIntervalMs ?? pollIntervalMs, pollIntervalMs) };
}
/** Drives the submit-then-poll loop to completion, timeout, or a terminal error. */
async function runDesignerWebPollLoop(
baseUrl: string,
config: DesignerWebRequestConfig,
fetchImpl: typeof fetch,
log?: { info?: (...args: unknown[]) => void }
): Promise<DesignerWebStepResult | { done: true; success: false; status: 504; error: string }> {
const deadline = Date.now() + config.timeoutMs;
let attempt = 0;
while (Date.now() < deadline) {
attempt += 1;
const step = await stepDesignerWebPoll(
baseUrl,
config.headers,
config.formBody,
config.pollIntervalMs,
fetchImpl
);
if (step.done) return step;
log?.info?.("IMAGE", `designer-web pending, poll #${attempt} in ${step.waitMs}ms`);
await new Promise((resolve) => setTimeout(resolve, step.waitMs));
}
return {
done: true,
success: false,
status: 504,
error: "Microsoft Designer image generation timed out waiting for a result",
};
}
export async function handleDesignerWebImageGeneration({
model,
provider,
providerConfig,
body,
credentials,
log,
fetchImpl = fetch,
}: {
model: string;
provider: string;
providerConfig: { baseUrl: string };
body: { prompt?: unknown; size?: unknown; timeout_ms?: unknown; poll_interval_ms?: unknown };
credentials: { apiKey?: string; accessToken?: string };
log?: { info?: (...args: unknown[]) => void; error?: (...args: unknown[]) => void };
fetchImpl?: typeof fetch;
}) {
const startTime = Date.now();
const resolved = resolveDesignerWebRequest(body, credentials);
if (!resolved.ok) {
return saveImageErrorResult({ provider, model, status: resolved.status, startTime, error: resolved.error });
}
try {
const outcome = await runDesignerWebPollLoop(providerConfig.baseUrl, resolved.config, fetchImpl, log);
if (outcome.success) {
return saveImageSuccessResult({
provider,
model,
startTime,
images: outcome.imageUrls.map((url) => ({ url })),
});
}
if (log?.error) {
log.error("IMAGE", `${provider} designer-web error ${outcome.status}: ${outcome.error}`);
}
return saveImageErrorResult({ provider, model, status: outcome.status, startTime, error: outcome.error });
} catch (err) {
const errorText = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
if (log?.error) {
log.error("IMAGE", `${provider} designer-web exception: ${errorText}`);
}
return saveImageErrorResult({ provider, model, status: 500, startTime, error: errorText });
}
}

View File

@@ -184,6 +184,14 @@ const EMBEDDED_DEFAULTS = {
],
// Trae Cloud IDE — public oauth client id
trae_id: [10, 3, 95, 6, 10, 22, 66, 3, 11, 90, 72, 31, 91, 2],
// Microsoft Designer web app — public ClientId header sent by the
// designer.microsoft.com frontend to designerapp.officeapps.live.com
// (not a secret — every browser session sends the same fixed value;
// reverse-engineered from the g4f MicrosoftDesigner provider reference).
microsoft_designer_client_id: [
13, 88, 13, 91, 68, 89, 65, 21, 72, 26, 21, 76, 0, 65, 93, 2, 26, 23, 28, 87, 14, 87, 8, 95, 12,
17, 70, 6, 24, 66, 17, 1, 10, 95, 81, 28,
],
// Microsoft Edge Read Aloud (EdgeTTS) — public "trusted client token" used to
// derive the Sec-MS-GEC anti-abuse header. Hardcoded in every known Edge
// browser build and every open-source edge-tts reimplementation (e.g.

View File

@@ -129,6 +129,19 @@ export const WEB_COOKIE_PROVIDERS = {
subscriptionRisk: true,
riskNoticeVariant: "webCookie",
},
"microsoft-designer-web": {
id: "microsoft-designer-web",
alias: "msdesigner",
name: "Microsoft Designer (Image Generation)",
icon: "auto_awesome",
color: "#0078D4",
textIcon: "MSD",
website: "https://designer.microsoft.com",
authHint:
"Sign in at designer.microsoft.com, then open DevTools → Network, generate an image, and find the request to DallE.ashx?action=GetDallEImagesCogSci. Copy the value of its Authorization: Bearer header (the access_token — no 'Bearer ' prefix). The token is short-lived; this is an unofficial, reverse-engineered integration.",
subscriptionRisk: true,
riskNoticeVariant: "webCookie",
},
"t3-web": {
id: "t3-web",
alias: "t3chat",

View File

@@ -109,6 +109,13 @@ export const WEB_SESSION_CREDENTIAL_REQUIREMENTS = {
acceptsFullCookieHeader: false,
storageKeys: ["token", "access_token", "accessToken"],
},
"microsoft-designer-web": {
kind: "token",
credentialName: "access_token",
placeholder: "access_token=... (Authorization: Bearer header from the DallE.ashx request)",
acceptsFullCookieHeader: false,
storageKeys: ["token", "access_token", "accessToken"],
},
"copilot-m365-web": {
kind: "token",
credentialName: "access_token + chathubPath",

View File

@@ -17,7 +17,7 @@
* The duckduckgo-web executor was the first known case. To prevent any
* future executor from regressing on the same contract, this sweep test
* imports every executor in `WEB_COOKIE_PROVIDERS` + `NOAUTH_PROVIDERS`
* (20 web-cookie + 2 noauth = 22 total), calls `execute()` with a minimal
* (26 web-cookie + 2 noauth = 28 total), calls `execute()` with a minimal
* but valid input, and asserts the wrapper shape. Tests use the
* pre-aborted signal path or empty-creds path so no real upstream call
* is needed.
@@ -116,7 +116,7 @@ function assertExecutorWrapperShape(
}
describe("web-cookie + noauth executor wrapper contract sweep", () => {
describe("WEB_COOKIE_PROVIDERS (20)", () => {
describe("WEB_COOKIE_PROVIDERS (26)", () => {
for (const providerId of WEB_COOKIE_IDS) {
it(`${providerId} executor returns wrapper shape`, async () => {
const executor = getExecutor(providerId);

View File

@@ -0,0 +1,216 @@
import { test } from "node:test";
import assert from "node:assert";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { resolvePublicCred } from "../../open-sse/utils/publicCreds.ts";
import {
buildDesignerWebHeaders,
buildDesignerWebFormBody,
mapDesignerWebImageSize,
parseDesignerWebResponse,
handleDesignerWebImageGeneration,
} from "../../open-sse/handlers/imageGeneration/providers/designerWeb.ts";
import { WEB_COOKIE_PROVIDERS } from "../../src/shared/constants/providers/web-cookie.ts";
import { IMAGE_PROVIDERS } from "../../open-sse/config/imageRegistry.ts";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
// --- Registry entries -------------------------------------------------
test("microsoft-designer-web is registered in WEB_COOKIE_PROVIDERS with a webCookie risk notice", () => {
const entry = (WEB_COOKIE_PROVIDERS as Record<string, unknown>)["microsoft-designer-web"];
assert.ok(entry, "microsoft-designer-web must exist in WEB_COOKIE_PROVIDERS");
assert.equal(entry.id, "microsoft-designer-web");
assert.equal(entry.subscriptionRisk, true);
assert.equal(entry.riskNoticeVariant, "webCookie");
assert.match(entry.website, /designer\.microsoft\.com/);
});
test("microsoft-designer-web is registered in IMAGE_PROVIDERS with the designer-web format", () => {
const entry = (IMAGE_PROVIDERS as Record<string, unknown>)["microsoft-designer-web"];
assert.ok(entry, "microsoft-designer-web must exist in IMAGE_PROVIDERS");
assert.equal(entry.format, "designer-web");
assert.match(entry.baseUrl, /designerapp\.officeapps\.live\.com/);
assert.ok(Array.isArray(entry.models) && entry.models.length > 0);
});
// --- Public credential (Hard Rule #11) ---------------------------------
test("microsoft_designer_client_id embedded default decodes to the public Designer ClientId", () => {
assert.equal(resolvePublicCred("microsoft_designer_client_id"), "b5c2664a-7e9b-4a7a-8c9a-cd2c52dcf621");
});
test("designerWeb.ts never embeds the raw ClientId literal (Hard Rule #11)", () => {
const src = fs.readFileSync(
path.join(repoRoot, "open-sse/handlers/imageGeneration/providers/designerWeb.ts"),
"utf8"
) as string;
assert.ok(
!src.includes("b5c2664a-7e9b-4a7a-8c9a-cd2c52dcf621"),
"designerWeb.ts must resolve the ClientId via resolvePublicCred(), not a string literal"
);
assert.ok(
src.includes('resolvePublicCred("microsoft_designer_client_id")'),
"designerWeb.ts must call resolvePublicCred for the Designer ClientId"
);
});
// --- Pure helpers --------------------------------------------------------
test("mapDesignerWebImageSize buckets sizes into square/landscape/portrait", () => {
assert.equal(mapDesignerWebImageSize("1024x1024"), "1_1");
assert.equal(mapDesignerWebImageSize("1792x1024"), "16_9");
assert.equal(mapDesignerWebImageSize("1024x1792"), "9_16");
assert.equal(mapDesignerWebImageSize(undefined), "1_1");
assert.equal(mapDesignerWebImageSize("garbage"), "1_1");
});
test("buildDesignerWebHeaders sets Bearer auth + the public ClientId + per-request SessionId/UserId", () => {
const headers = buildDesignerWebHeaders({ accessToken: "tok-123" });
assert.equal(headers.Authorization, "Bearer tok-123");
assert.equal(headers.ClientId, "b5c2664a-7e9b-4a7a-8c9a-cd2c52dcf621");
assert.ok(headers.SessionId && headers.SessionId.length > 0);
assert.ok(headers.UserId && headers.UserId.length > 0);
assert.equal(headers["Content-Type"], "application/x-www-form-urlencoded");
});
test("buildDesignerWebFormBody encodes prompt, mapped size, fixed batch size, and a seed", () => {
const form = buildDesignerWebFormBody("a cat astronaut", "1792x1024");
assert.equal(form.get("dalle-caption"), "a cat astronaut");
assert.equal(form.get("dalle-image-size"), "16_9");
assert.equal(form.get("dalle-batch-size"), "4");
assert.ok(Number(form.get("dalle-seed")) >= 0);
});
test("parseDesignerWebResponse: ready state extracts thumbnail image URLs", () => {
const parsed = parseDesignerWebResponse({
image_urls_thumbnail: [{ ImageUrl: "https://example.com/a.png" }, { ImageUrl: "https://example.com/b.png" }],
});
assert.equal(parsed.status, "ready");
assert.deepEqual(parsed.imageUrls, ["https://example.com/a.png", "https://example.com/b.png"]);
});
test("parseDesignerWebResponse: pending state surfaces the polling interval", () => {
const parsed = parseDesignerWebResponse({
polling_response: { polling_meta_data: { poll_interval: 1500 } },
});
assert.equal(parsed.status, "pending");
assert.equal(parsed.pollIntervalMs, 1500);
assert.deepEqual(parsed.imageUrls, []);
});
test("parseDesignerWebResponse: unrecognized shape is 'empty'", () => {
const parsed = parseDesignerWebResponse({ unexpected: true });
assert.equal(parsed.status, "empty");
});
// --- Handler (mocked fetch — no live Designer session required) ---------
function jsonResponse(status: number, body: unknown) {
return {
ok: status >= 200 && status < 300,
status,
json: async () => body,
text: async () => JSON.stringify(body),
} as Response;
}
test("handleDesignerWebImageGeneration returns 400 when prompt is missing", async () => {
const result = await handleDesignerWebImageGeneration({
model: "dall-e-3",
provider: "microsoft-designer-web",
providerConfig: { baseUrl: "https://designerapp.officeapps.live.com/designerapp/DallE.ashx" },
body: {},
credentials: { apiKey: "tok" },
fetchImpl: async () => jsonResponse(200, {}),
});
assert.equal(result.success, false);
assert.equal(result.status, 400);
});
test("handleDesignerWebImageGeneration returns 401 when access_token is missing", async () => {
const result = await handleDesignerWebImageGeneration({
model: "dall-e-3",
provider: "microsoft-designer-web",
providerConfig: { baseUrl: "https://designerapp.officeapps.live.com/designerapp/DallE.ashx" },
body: { prompt: "a cat" },
credentials: {},
fetchImpl: async () => jsonResponse(200, {}),
});
assert.equal(result.success, false);
assert.equal(result.status, 401);
});
test("handleDesignerWebImageGeneration succeeds immediately when the first response is already ready", async () => {
let calls = 0;
const result = await handleDesignerWebImageGeneration({
model: "dall-e-3",
provider: "microsoft-designer-web",
providerConfig: { baseUrl: "https://designerapp.officeapps.live.com/designerapp/DallE.ashx" },
body: { prompt: "a cat astronaut", size: "1024x1024" },
credentials: { apiKey: "tok-abc" },
fetchImpl: async () => {
calls += 1;
return jsonResponse(200, {
image_urls_thumbnail: [{ ImageUrl: "https://example.com/ready.png" }],
});
},
});
assert.equal(calls, 1);
assert.equal(result.success, true);
assert.equal(result.data.data[0].url, "https://example.com/ready.png");
});
test("handleDesignerWebImageGeneration polls until ready, bounded by poll_interval_ms/timeout_ms", async () => {
let calls = 0;
const result = await handleDesignerWebImageGeneration({
model: "dall-e-3",
provider: "microsoft-designer-web",
providerConfig: { baseUrl: "https://designerapp.officeapps.live.com/designerapp/DallE.ashx" },
body: { prompt: "a cat astronaut", timeout_ms: 5000, poll_interval_ms: 1 },
credentials: { apiKey: "tok-abc" },
fetchImpl: async () => {
calls += 1;
if (calls < 3) {
return jsonResponse(200, {
polling_response: { polling_meta_data: { poll_interval: 1 } },
});
}
return jsonResponse(200, {
image_urls_thumbnail: [{ ImageUrl: "https://example.com/final.png" }],
});
},
});
assert.equal(calls, 3);
assert.equal(result.success, true);
assert.equal(result.data.data[0].url, "https://example.com/final.png");
});
test("handleDesignerWebImageGeneration surfaces a sanitized error on a non-OK upstream response", async () => {
const result = await handleDesignerWebImageGeneration({
model: "dall-e-3",
provider: "microsoft-designer-web",
providerConfig: { baseUrl: "https://designerapp.officeapps.live.com/designerapp/DallE.ashx" },
body: { prompt: "a cat astronaut" },
credentials: { apiKey: "expired-token" },
fetchImpl: async () => jsonResponse(401, { error: "invalid_token" }),
});
assert.equal(result.success, false);
assert.equal(result.status, 401);
assert.ok(!String(result.error).includes(" at "), "error must not leak a stack trace");
});
test("handleDesignerWebImageGeneration times out cleanly when the upstream never becomes ready", async () => {
const result = await handleDesignerWebImageGeneration({
model: "dall-e-3",
provider: "microsoft-designer-web",
providerConfig: { baseUrl: "https://designerapp.officeapps.live.com/designerapp/DallE.ashx" },
body: { prompt: "a cat astronaut", timeout_ms: 5, poll_interval_ms: 1 },
credentials: { apiKey: "tok-abc" },
fetchImpl: async () =>
jsonResponse(200, { polling_response: { polling_meta_data: { poll_interval: 1 } } }),
});
assert.equal(result.success, false);
assert.equal(result.status, 504);
});