From 84ff5335bf00487726ff0bc420f799e315a92eee Mon Sep 17 00:00:00 2001 From: Ben Miner Date: Fri, 17 Apr 2026 17:16:47 -0500 Subject: [PATCH] fix(schemas): pre-process spec to patch broken Error $refs The upstream OpenAPI spec has three $ref entries pointing to #/components/schemas/Error, which is not defined. openapi-zod-client crashes with MissingPointerError before producing any output. Rewrite those refs to ErrorResponse (matching what every other 401 uses) before invoking the generator, and drop the now-dead Error->ApiError rename in post-processing. Also sync src/schemas/buyer.ts with the current spec. --- package-lock.json | 16 +++++- scripts/generate-schemas.ts | 20 +++++-- src/schemas/buyer.ts | 110 +++++++++++++++--------------------- 3 files changed, 76 insertions(+), 70 deletions(-) diff --git a/package-lock.json b/package-lock.json index 108579f..20f4de1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -103,6 +103,7 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -167,6 +168,7 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -2460,6 +2462,7 @@ "integrity": "sha512-hRnu+5qggKDSyWHlnmThnUqg62l29Aj/6vcYgUaSFL9oc7DVjeWEQN3PRgdSc6F8d9QRMWkf36CLMch1Do/+RQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -2584,6 +2587,7 @@ "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", @@ -2777,6 +2781,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2943,6 +2948,7 @@ "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", @@ -3187,6 +3193,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -3998,6 +4005,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -4279,6 +4287,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -5309,6 +5318,7 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -6703,7 +6713,8 @@ "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/openapi-zod-client": { "version": "1.18.3", @@ -8251,6 +8262,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -8365,6 +8377,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8695,6 +8708,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/scripts/generate-schemas.ts b/scripts/generate-schemas.ts index 76c0f73..73f4d4f 100644 --- a/scripts/generate-schemas.ts +++ b/scripts/generate-schemas.ts @@ -14,6 +14,20 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const BUYER_SPEC_URL = 'https://api.agentic.scope3.com/api/v2/buyer/openapi.yaml'; const BUYER_SPEC_PATH = join(__dirname, '../.context/attachments/buyer-api-v2.yaml'); +function preProcessSpec(specPath: string) { + let content = readFileSync(specPath, 'utf-8'); + + const brokenRef = "$ref: '#/components/schemas/Error'"; + const fixedRef = "$ref: '#/components/schemas/ErrorResponse'"; + + if (content.includes(brokenRef)) { + const count = content.split(brokenRef).length - 1; + console.log(`Pre-processing: replacing ${count} broken Error $ref(s) with ErrorResponse`); + content = content.replaceAll(brokenRef, fixedRef); + writeFileSync(specPath, content); + } +} + function postProcessSchemas(filePath: string) { let content = readFileSync(filePath, 'utf-8'); @@ -57,10 +71,6 @@ function postProcessSchemas(filePath: string) { content = content.replace(/\\&/g, '&'); - // Rename 'Error' to 'ApiError' to avoid shadowing the global - content = content.replace(/\bconst Error\b/g, 'const ApiError'); - content = content.replace(/^\s+Error,$/m, ' ApiError,'); - // Normalize schema names to PascalCase const nameRenames: Array<[RegExp, string]> = []; const namePattern = /^const ([a-z]\w*_\w+|[a-z]\w+) = /gm; @@ -127,6 +137,8 @@ async function main() { console.log(`Downloaded spec to ${specPath}`); } + preProcessSpec(specPath); + const outputFile = join(schemasDir, 'buyer.ts'); console.log(`Generating schemas to ${outputFile}...`); diff --git a/src/schemas/buyer.ts b/src/schemas/buyer.ts index 8283b09..333059d 100644 --- a/src/schemas/buyer.ts +++ b/src/schemas/buyer.ts @@ -3,7 +3,13 @@ import { z } from 'zod'; -const ApiError = z.object({ error: z.string(), message: z.string().optional() }); +const ApiError = z.object({ + code: z.string(), + message: z.string(), + field: z.string().optional(), + details: z.object({}).partial().passthrough().optional(), +}); +const ErrorResponse = z.object({ data: z.literal(null).nullable(), error: ApiError }); const LinkedAccountInput = z .object({ partnerId: z.string().min(1), @@ -1218,7 +1224,11 @@ const ReportingMetrics = z.object({ ctr: z.number().nullable(), completionRate: z.number().nullable(), }); -const PackageReporting = z.object({ packageId: z.string(), metrics: ReportingMetrics }); +const PackageReporting = z.object({ + packageId: z.string(), + productId: z.string().nullable(), + metrics: ReportingMetrics, +}); const MediaBuyReporting = z.object({ mediaBuyId: z.string(), name: z.string(), @@ -1311,64 +1321,41 @@ const SyncCatalogsBody = z validation_mode: z.enum(['strict', 'lenient']).optional().default('strict'), }) .passthrough(); -const CustomerAccountSummary = z.object({ accountIdentifier: z.string(), Status: z.string() }); -const OAuthInfo = z.object({ - authorizationUrl: z.string().url(), - agentId: z.string(), - agentName: z.string(), -}); -const Agent = z.object({ - agentId: z.string(), - type: z.enum(['SALES', 'SIGNAL', 'CREATIVE', 'OUTCOME']), +const BuyerStorefrontSource = z.object({ + sourceId: z.string(), name: z.string(), - description: z.string().nullish(), - endpointUrl: z.string(), protocol: z.enum(['MCP', 'A2A']), - authenticationType: z.enum(['API_KEY', 'NO_AUTH', 'JWT', 'OAUTH']), - requiresOperatorAuth: z.boolean(), - billingOptions: z - .object({ default: z.string().nullable(), supported: z.array(z.string()) }) - .nullish(), - Status: z.enum(['PENDING', 'ACTIVE', 'DISABLED', 'COMING_SOON']), - relationship: z.enum(['SELF', 'MARKETPLACE']), - customerAccounts: z.array(CustomerAccountSummary).optional(), - requiresAccount: z.boolean(), - authConfigured: z.boolean(), - capabilities: z - .object({ - version: z.enum(['v2', 'v3']), - protocols: z.array(z.string()), - extensions: z.array(z.string()), - features: z - .object({ - inlineCreativeManagement: z.boolean(), - propertyListFiltering: z.boolean(), - contentStandards: z.boolean(), - conversionTracking: z.boolean(), - audienceManagement: z.boolean(), - }) - .partial(), - sandbox: z.boolean(), - publisherDomains: z.array(z.string()).optional(), - channels: z.array(z.string()).optional(), - lastUpdated: z.string().optional(), - accounts: z.object({ - requireOperatorAuth: z.boolean(), - defaultBilling: z.string().nullable(), - supportedBillings: z.array(z.string()), - }), - }) - .nullish(), - createdAt: z.string().datetime({ offset: true }), - oauth: OAuthInfo.nullish(), + requiresCredentials: z.boolean(), + connected: z.boolean(), + customerAccounts: z.array(z.object({ accountIdentifier: z.string(), Status: z.string() })), +}); +const BuyerStorefront = z.object({ + id: z.number().int().gte(-9007199254740991).lte(9007199254740991), + platformId: z.string(), + name: z.string(), + publisherDomain: z.string().nullable(), + sources: z.array(BuyerStorefrontSource), }); -const AgentList = z.object({ - items: z.array(Agent), +const BuyerStorefrontList = z.object({ + items: z.array(BuyerStorefront), total: z.number().int().gte(0).lte(9007199254740991), hasMore: z.boolean(), nextOffset: z.number().int().gte(0).lte(9007199254740991).nullable(), }); -const RegisterSalesAgentAccountBody = z +const OAuthInfo = z.object({ + authorizationUrl: z.string().url(), + agentId: z.string(), + agentName: z.string(), +}); +const AgentAccount = z.object({ + id: z.string(), + accountIdentifier: z.string(), + Status: z.string(), + registeredBy: z.string(), + createdAt: z.string().datetime({ offset: true }), + oauth: OAuthInfo.optional(), +}); +const RegisterSourceCredentialsBody = z .object({ accountIdentifier: z.string().min(1).max(255), auth: z @@ -1396,14 +1383,6 @@ const RegisterSalesAgentAccountBody = z marketplaceAccount: z.boolean().optional(), }) .passthrough(); -const AgentAccount = z.object({ - id: z.string(), - accountIdentifier: z.string(), - Status: z.string(), - registeredBy: z.string(), - createdAt: z.string().datetime({ offset: true }), - oauth: OAuthInfo.optional(), -}); const SyndicateBody = z .object({ resourceType: z.enum(['AUDIENCE', 'EVENT_SOURCE', 'CATALOG']), @@ -1802,6 +1781,7 @@ const McpAskCapabilityRequest = z export const schemas: Record = { ApiError, + ErrorResponse, LinkedAccountInput, OptimizationApplyMode, CreateAdvertiserBody, @@ -1886,12 +1866,12 @@ export const schemas: Record = { AvailableAccountOutput, AvailableAccountListResponse, SyncCatalogsBody, - CustomerAccountSummary, + BuyerStorefrontSource, + BuyerStorefront, + BuyerStorefrontList, OAuthInfo, - Agent, - AgentList, - RegisterSalesAgentAccountBody, AgentAccount, + RegisterSourceCredentialsBody, SyndicateBody, SyndicationStatusOutput, SyndicateResponse,